You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
157 lines
3.9 KiB
157 lines
3.9 KiB
#!/usr/bin/env node |
|
|
|
/// @ts-check |
|
|
|
import { execa } from "execa" |
|
import fs from "node:fs" |
|
import cmd from "cmd-ts" |
|
import cmdFs from "cmd-ts/dist/cjs/batteries/fs.js" |
|
|
|
const FnmBinaryPath = { |
|
...cmdFs.ExistingPath, |
|
defaultValue() { |
|
const target = new URL("../target/debug/fnm", import.meta.url) |
|
if (!fs.existsSync(target)) { |
|
throw new Error( |
|
"Can't find debug target, please run `cargo build` or provide a specific binary path" |
|
) |
|
} |
|
return target.pathname |
|
}, |
|
} |
|
|
|
const command = cmd.command({ |
|
name: "print-command-docs", |
|
description: "prints the docs/command.md file with updated contents", |
|
args: { |
|
checkForDirty: cmd.flag({ |
|
long: "check", |
|
description: `Check that file was not changed`, |
|
}), |
|
fnmPath: cmd.option({ |
|
long: "binary-path", |
|
description: "the fnm binary path", |
|
type: FnmBinaryPath, |
|
}), |
|
}, |
|
async handler({ checkForDirty, fnmPath }) { |
|
const targetFile = new URL("../docs/commands.md", import.meta.url).pathname |
|
await main(targetFile, fnmPath) |
|
if (checkForDirty) { |
|
const gitStatus = await checkGitStatus(targetFile) |
|
if (gitStatus.state === "dirty") { |
|
process.exitCode = 1 |
|
console.error( |
|
"The file has changed. Please re-run `pnpm generate-command-docs`." |
|
) |
|
console.error(`hint: The following diff was found:`) |
|
console.error() |
|
console.error(gitStatus.diff) |
|
} |
|
} |
|
}, |
|
}) |
|
|
|
cmd.run(cmd.binary(command), process.argv).catch((err) => { |
|
console.error(err) |
|
process.exitCode = process.exitCode || 1 |
|
}) |
|
|
|
/** |
|
* @param {string} targetFile |
|
* @param {string} fnmPath |
|
* @returns {Promise<void>} |
|
*/ |
|
async function main(targetFile, fnmPath) { |
|
const stream = fs.createWriteStream(targetFile) |
|
|
|
const { subcommands, text: mainText } = await getCommandHelp(fnmPath) |
|
|
|
await write(stream, line(`fnm`, mainText)) |
|
|
|
for (const subcommand of subcommands) { |
|
const { text: subcommandText } = await getCommandHelp(fnmPath, subcommand) |
|
await write(stream, "\n" + line(`fnm ${subcommand}`, subcommandText)) |
|
} |
|
|
|
stream.close() |
|
|
|
await execa(`pnpm`, ["prettier", "--write", targetFile]) |
|
} |
|
|
|
/** |
|
* @param {import('stream').Writable} stream |
|
* @param {string} content |
|
* @returns {Promise<void>} |
|
*/ |
|
function write(stream, content) { |
|
return new Promise((resolve, reject) => { |
|
stream.write(content, (err) => (err ? reject(err) : resolve())) |
|
}) |
|
} |
|
|
|
function line(cmd, text) { |
|
const cmdCode = "`" + cmd + "`" |
|
const textCode = "```\n" + text + "\n```" |
|
return `# ${cmdCode}\n${textCode}` |
|
} |
|
|
|
/** |
|
* @param {string} fnmPath |
|
* @param {string} [command] |
|
* @returns {Promise<{ subcommands: string[], text: string }>} |
|
*/ |
|
async function getCommandHelp(fnmPath, command) { |
|
const cmdArg = command ? [command] : [] |
|
const result = await run(fnmPath, [...cmdArg, "--help"]) |
|
const text = result.stdout |
|
const rows = text.split("\n") |
|
const headerIndex = rows.findIndex((x) => x.includes("Commands:")) |
|
/** @type {string[]} */ |
|
const subcommands = [] |
|
if (!command) { |
|
for (const row of rows.slice( |
|
headerIndex + 1, |
|
rows.indexOf("", headerIndex + 1) |
|
)) { |
|
const [, word] = row.split(/\s+/) |
|
if (word && word[0].toLowerCase() === word[0]) { |
|
subcommands.push(word) |
|
} |
|
} |
|
} |
|
return { |
|
subcommands, |
|
text, |
|
} |
|
} |
|
|
|
/** |
|
* @param {string[]} args |
|
* @returns {import('execa').ExecaChildProcess<string>} |
|
*/ |
|
function run(fnmPath, args) { |
|
return execa(fnmPath, args, { |
|
reject: false, |
|
stdout: "pipe", |
|
stderr: "pipe", |
|
}) |
|
} |
|
|
|
/** |
|
* @param {string} targetFile |
|
* @returns {Promise<{ state: "dirty", diff: string } | { state: "clean" }>} |
|
*/ |
|
async function checkGitStatus(targetFile) { |
|
const { stdout, exitCode } = await execa( |
|
`git`, |
|
["diff", "--color", "--exit-code", targetFile], |
|
{ |
|
reject: false, |
|
} |
|
) |
|
if (exitCode === 0) { |
|
return { state: "clean" } |
|
} |
|
return { state: "dirty", diff: stdout } |
|
}
|
|
|