From ad4145fb10dbf32d8f99e1de555339dba0979f72 Mon Sep 17 00:00:00 2001 From: Ben Schlegel <31989404+benschlegel@users.noreply.github.com> Date: Sat, 26 Aug 2023 22:21:44 +0200 Subject: [PATCH] feat: support CLI arguments for `npx quartz create` (#421) * feat(cli): add new args for content + link resolve * feat(cli): validate cmd args * feat(cli): add chalk + error code to errors * feat(cli): support for setup/link via args * refactor(cli): use yargs choices instead of manual Scrap manual check if arguments are valid, use yargs "choices" field instead. * feat(cli): add in-dir argument+ handle errors add new "in-directory" argument, used if "setup" is "copy" or "symlink" to determine source. add error handling for invalid permutations of arguments or non existent path * feat(cli): dynamically use cli or provided args use "in-directory" arg as `originalFolder` if available, otherwise get it from manual cli process * run format * fix: use process.exit instead of return * refactor: split CommonArgv and CreateArgv * refactor(cli): rename create args, use ${} syntax * fix(cli): fix link resolution strategy arg * format * feat(consistency): allow partial cmd args --- quartz/bootstrap-cli.mjs | 188 +++++++++++++++++++++++++++------------ 1 file changed, 133 insertions(+), 55 deletions(-) diff --git a/quartz/bootstrap-cli.mjs b/quartz/bootstrap-cli.mjs index b191b49c..1deb18fe 100755 --- a/quartz/bootstrap-cli.mjs +++ b/quartz/bootstrap-cli.mjs @@ -43,6 +43,27 @@ const CommonArgv = { }, } +const CreateArgv = { + ...CommonArgv, + source: { + string: true, + alias: ["s"], + describe: "source directory to copy/create symlink from", + }, + strategy: { + string: true, + alias: ["X"], + choices: ["new", "copy", "symlink"], + describe: "strategy for content folder setup", + }, + links: { + string: true, + alias: ["l"], + choices: ["absolute", "shortest", "relative"], + describe: "strategy to resolve links", + }, +} + const SyncArgv = { ...CommonArgv, commit: { @@ -147,24 +168,73 @@ yargs(hideBin(process.argv)) .scriptName("quartz") .version(version) .usage("$0 [args]") - .command("create", "Initialize Quartz", CommonArgv, async (argv) => { + .command("create", "Initialize Quartz", CreateArgv, async (argv) => { console.log() intro(chalk.bgGreen.black(` Quartz v${version} `)) const contentFolder = path.join(cwd, argv.directory) - const setupStrategy = exitIfCancel( - await select({ - message: `Choose how to initialize the content in \`${contentFolder}\``, - options: [ - { value: "new", label: "Empty Quartz" }, - { value: "copy", label: "Copy an existing folder", hint: "overwrites `content`" }, - { - value: "symlink", - label: "Symlink an existing folder", - hint: "don't select this unless you know what you are doing!", - }, - ], - }), - ) + let setupStrategy = argv.strategy?.toLowerCase() + let linkResolutionStrategy = argv.links?.toLowerCase() + const sourceDirectory = argv.source + + // If all cmd arguments were provided, check if theyre valid + if (setupStrategy && linkResolutionStrategy) { + // If setup isn't, "new", source argument is required + if (setupStrategy !== "new") { + // Error handling + if (!sourceDirectory) { + outro( + chalk.red( + `Setup strategies (arg '${chalk.yellow( + `-${CreateArgv.strategy.alias[0]}`, + )}') other than '${chalk.yellow( + "new", + )}' require content folder argument ('${chalk.yellow( + `-${CreateArgv.source.alias[0]}`, + )}') to be set`, + ), + ) + process.exit(1) + } else { + if (!fs.existsSync(sourceDirectory)) { + outro( + chalk.red( + `Input directory to copy/symlink 'content' from not found ('${chalk.yellow( + sourceDirectory, + )}', invalid argument "${chalk.yellow(`-${CreateArgv.source.alias[0]}`)})`, + ), + ) + process.exit(1) + } else if (!fs.lstatSync(sourceDirectory).isDirectory()) { + outro( + chalk.red( + `Source directory to copy/symlink 'content' from is not a directory (found file at '${chalk.yellow( + sourceDirectory, + )}', invalid argument ${chalk.yellow(`-${CreateArgv.source.alias[0]}`)}")`, + ), + ) + process.exit(1) + } + } + } + } + + // Use cli process if cmd args werent provided + if (!setupStrategy) { + setupStrategy = exitIfCancel( + await select({ + message: `Choose how to initialize the content in \`${contentFolder}\``, + options: [ + { value: "new", label: "Empty Quartz" }, + { value: "copy", label: "Copy an existing folder", hint: "overwrites `content`" }, + { + value: "symlink", + label: "Symlink an existing folder", + hint: "don't select this unless you know what you are doing!", + }, + ], + }), + ) + } async function rmContentFolder() { const contentStat = await fs.promises.lstat(contentFolder) @@ -177,23 +247,28 @@ yargs(hideBin(process.argv)) await fs.promises.unlink(path.join(contentFolder, ".gitkeep")) if (setupStrategy === "copy" || setupStrategy === "symlink") { - const originalFolder = escapePath( - exitIfCancel( - await text({ - message: "Enter the full path to existing content folder", - placeholder: - "On most terminal emulators, you can drag and drop a folder into the window and it will paste the full path", - validate(fp) { - const fullPath = escapePath(fp) - if (!fs.existsSync(fullPath)) { - return "The given path doesn't exist" - } else if (!fs.lstatSync(fullPath).isDirectory()) { - return "The given path is not a folder" - } - }, - }), - ), - ) + let originalFolder = sourceDirectory + + // If input directory was not passed, use cli + if (!sourceDirectory) { + originalFolder = escapePath( + exitIfCancel( + await text({ + message: "Enter the full path to existing content folder", + placeholder: + "On most terminal emulators, you can drag and drop a folder into the window and it will paste the full path", + validate(fp) { + const fullPath = escapePath(fp) + if (!fs.existsSync(fullPath)) { + return "The given path doesn't exist" + } else if (!fs.lstatSync(fullPath).isDirectory()) { + return "The given path is not a folder" + } + }, + }), + ), + ) + } await rmContentFolder() if (setupStrategy === "copy") { @@ -217,29 +292,32 @@ See the [documentation](https://quartz.jzhao.xyz) for how to get started. ) } - // get a preferred link resolution strategy - const linkResolutionStrategy = exitIfCancel( - await select({ - message: `Choose how Quartz should resolve links in your content. You can change this later in \`quartz.config.ts\`.`, - options: [ - { - value: "absolute", - label: "Treat links as absolute path", - hint: "for content made for Quartz 3 and Hugo", - }, - { - value: "shortest", - label: "Treat links as shortest path", - hint: "for most Obsidian vaults", - }, - { - value: "relative", - label: "Treat links as relative paths", - hint: "for just normal Markdown files", - }, - ], - }), - ) + // Use cli process if cmd args werent provided + if (!linkResolutionStrategy) { + // get a preferred link resolution strategy + linkResolutionStrategy = exitIfCancel( + await select({ + message: `Choose how Quartz should resolve links in your content. You can change this later in \`quartz.config.ts\`.`, + options: [ + { + value: "absolute", + label: "Treat links as absolute path", + hint: "for content made for Quartz 3 and Hugo", + }, + { + value: "shortest", + label: "Treat links as shortest path", + hint: "for most Obsidian vaults", + }, + { + value: "relative", + label: "Treat links as relative paths", + hint: "for just normal Markdown files", + }, + ], + }), + ) + } // now, do config changes const configFilePath = path.join(cwd, "quartz.config.ts")