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:
Ben Schlegel 2023-08-26 22:21:44 +02:00 committed by GitHub
parent 29c4087dea
commit 0688a2415f

View File

@ -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 = { const SyncArgv = {
...CommonArgv, ...CommonArgv,
commit: { commit: {
@ -147,24 +168,73 @@ yargs(hideBin(process.argv))
.scriptName("quartz") .scriptName("quartz")
.version(version) .version(version)
.usage("$0 <cmd> [args]") .usage("$0 <cmd> [args]")
.command("create", "Initialize Quartz", CommonArgv, async (argv) => { .command("create", "Initialize Quartz", CreateArgv, async (argv) => {
console.log() console.log()
intro(chalk.bgGreen.black(` Quartz v${version} `)) intro(chalk.bgGreen.black(` Quartz v${version} `))
const contentFolder = path.join(cwd, argv.directory) const contentFolder = path.join(cwd, argv.directory)
const setupStrategy = exitIfCancel( let setupStrategy = argv.strategy?.toLowerCase()
await select({ let linkResolutionStrategy = argv.links?.toLowerCase()
message: `Choose how to initialize the content in \`${contentFolder}\``, const sourceDirectory = argv.source
options: [
{ value: "new", label: "Empty Quartz" }, // If all cmd arguments were provided, check if theyre valid
{ value: "copy", label: "Copy an existing folder", hint: "overwrites `content`" }, if (setupStrategy && linkResolutionStrategy) {
{ // If setup isn't, "new", source argument is required
value: "symlink", if (setupStrategy !== "new") {
label: "Symlink an existing folder", // Error handling
hint: "don't select this unless you know what you are doing!", 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() { async function rmContentFolder() {
const contentStat = await fs.promises.lstat(contentFolder) const contentStat = await fs.promises.lstat(contentFolder)
@ -177,23 +247,28 @@ yargs(hideBin(process.argv))
await fs.promises.unlink(path.join(contentFolder, ".gitkeep")) await fs.promises.unlink(path.join(contentFolder, ".gitkeep"))
if (setupStrategy === "copy" || setupStrategy === "symlink") { if (setupStrategy === "copy" || setupStrategy === "symlink") {
const originalFolder = escapePath( let originalFolder = sourceDirectory
exitIfCancel(
await text({ // If input directory was not passed, use cli
message: "Enter the full path to existing content folder", if (!sourceDirectory) {
placeholder: originalFolder = escapePath(
"On most terminal emulators, you can drag and drop a folder into the window and it will paste the full path", exitIfCancel(
validate(fp) { await text({
const fullPath = escapePath(fp) message: "Enter the full path to existing content folder",
if (!fs.existsSync(fullPath)) { placeholder:
return "The given path doesn't exist" "On most terminal emulators, you can drag and drop a folder into the window and it will paste the full path",
} else if (!fs.lstatSync(fullPath).isDirectory()) { validate(fp) {
return "The given path is not a folder" 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() await rmContentFolder()
if (setupStrategy === "copy") { 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 // Use cli process if cmd args werent provided
const linkResolutionStrategy = exitIfCancel( if (!linkResolutionStrategy) {
await select({ // get a preferred link resolution strategy
message: `Choose how Quartz should resolve links in your content. You can change this later in \`quartz.config.ts\`.`, linkResolutionStrategy = exitIfCancel(
options: [ await select({
{ message: `Choose how Quartz should resolve links in your content. You can change this later in \`quartz.config.ts\`.`,
value: "absolute", options: [
label: "Treat links as absolute path", {
hint: "for content made for Quartz 3 and Hugo", 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: "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", value: "relative",
}, label: "Treat links as relative paths",
], hint: "for just normal Markdown files",
}), },
) ],
}),
)
}
// now, do config changes // now, do config changes
const configFilePath = path.join(cwd, "quartz.config.ts") const configFilePath = path.join(cwd, "quartz.config.ts")