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
			
			
This commit is contained in:
		@@ -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 <cmd> [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")
 | 
			
		||||
 
 | 
			
		||||
		Reference in New Issue
	
	Block a user