mirror of
https://github.com/oblien/openship.git
synced 2026-10-12 04:50:02 +08:00
199 lines
7.8 KiB
JavaScript
199 lines
7.8 KiB
JavaScript
/** Generate command syntax from the built public CLI; no command actions run. */
|
|
import { mkdirSync, readFileSync, writeFileSync } from "node:fs";
|
|
import { dirname, join, posix } from "node:path";
|
|
import { fileURLToPath } from "node:url";
|
|
import { cliSurface, docsDirectory } from "./docs-surface.mjs";
|
|
|
|
const categories = {
|
|
Connect: ["login", "logout", "context", "token", "access", "credential", "github", "api"],
|
|
Deploy: ["init", "config", "deploy", "deployment", "logs"],
|
|
Applications: ["project", "app", "service", "domain", "dns"],
|
|
Operations: ["monitoring", "notification", "webhook", "audit", "analytics", "settings"],
|
|
Cloud: ["billing"],
|
|
Infrastructure: ["job", "server", "backup", "edge", "mail", "migration", "system"],
|
|
"Local installation": [
|
|
"up",
|
|
"stop",
|
|
"status",
|
|
"doctor",
|
|
"open",
|
|
"install",
|
|
"update",
|
|
"uninstall",
|
|
"reset-admin-password",
|
|
],
|
|
Shell: ["completion"],
|
|
};
|
|
const guides = {
|
|
billing: "cloud",
|
|
github: "access",
|
|
settings: "operations",
|
|
analytics: "operations",
|
|
migration: "self-host",
|
|
login: "access",
|
|
logout: "access",
|
|
context: "access",
|
|
token: "access",
|
|
access: "access",
|
|
credential: "access",
|
|
dns: "projects",
|
|
monitoring: "operations",
|
|
notification: "operations",
|
|
webhook: "operations",
|
|
audit: "operations",
|
|
api: "access",
|
|
init: "projects",
|
|
config: "projects",
|
|
deploy: "deploy",
|
|
deployment: "deploy",
|
|
logs: "deploy",
|
|
project: "projects",
|
|
app: "projects",
|
|
service: "projects",
|
|
domain: "projects",
|
|
job: "../guides/jobs",
|
|
server: "self-host",
|
|
backup: "self-host",
|
|
edge: "edge",
|
|
mail: "self-host",
|
|
system: "self-host",
|
|
};
|
|
const syntax = (value) => "`" + value.replaceAll("|", "\\|") + "`";
|
|
const prose = (value) =>
|
|
value
|
|
.split(/(`[^`]*`)/)
|
|
.map((part) =>
|
|
part.startsWith("`")
|
|
? part.replaceAll("|", "\\|")
|
|
: part
|
|
.replaceAll("&", "&")
|
|
.replaceAll("<", "<")
|
|
.replaceAll(">", ">")
|
|
.replaceAll("{", "{")
|
|
.replaceAll("}", "}")
|
|
.replaceAll("|", "\\|"),
|
|
)
|
|
.join("");
|
|
const slug = (command) => command.split(" ").slice(1).join("/");
|
|
const anchor = (command) => command.replaceAll(" ", "-");
|
|
const descendants = (catalog, command) =>
|
|
catalog.filter((entry) => entry.command === command || entry.command.startsWith(command + " "));
|
|
|
|
export function renderCliReference(catalog) {
|
|
const top = catalog.filter((entry) => entry.command.split(" ").length === 2);
|
|
const expected = new Set(Object.values(categories).flat());
|
|
if (
|
|
top.length !== expected.size ||
|
|
top.some((entry) => !expected.has(entry.command.split(" ")[1]))
|
|
)
|
|
throw new Error(
|
|
"Categorize every current top-level CLI command before generating the reference",
|
|
);
|
|
const selected = new Set(top.map((entry) => entry.command));
|
|
for (const entry of catalog) {
|
|
if (entry.command.split(" ").length < 3 || entry.children.length < 2) continue;
|
|
const size = descendants(catalog, entry.command).reduce(
|
|
(n, row) => n + 8 + row.options.length * 2 + row.arguments.length,
|
|
0,
|
|
);
|
|
if (size >= 60) selected.add(entry.command);
|
|
}
|
|
const owner = (command) =>
|
|
[...selected]
|
|
.filter((candidate) => command === candidate || command.startsWith(candidate + " "))
|
|
.sort((a, b) => b.length - a.length)[0];
|
|
const link = (command) => `/docs/cli/reference/${slug(owner(command))}#${anchor(command)}`;
|
|
const files = new Map();
|
|
for (const command of selected) {
|
|
const name = command.split(" ").slice(1).join(" ");
|
|
const family = command.split(" ")[1];
|
|
let body = `---\ntitle: ${name}\ndescription: Commands, arguments, and options for ${command}.\ncliGroup: ${command}\n---\n\n`;
|
|
const guidePath = posix.join("/docs/cli", guides[family] ?? "");
|
|
body += `See the [${guides[family] ? "workflow guide" : "CLI overview"}](${guidePath}) for examples. `;
|
|
body += `Global flags such as \`--json\` go before the command; see [global options](/docs/cli/reference#global-options).\n\n`;
|
|
for (const entry of catalog.filter((entry) => owner(entry.command) === command)) {
|
|
body += `## ${entry.command}\n\n${prose(entry.description) || "Use this command with the arguments below."}\n\n\`\`\`text\n${entry.usage}\n\`\`\`\n\n`;
|
|
if (entry.children.length) {
|
|
body += "| Subcommand | Purpose |\n| --- | --- |\n";
|
|
for (const child of entry.children) {
|
|
const childCommand = entry.command + " " + child.syntax.split(/[ |]/)[0];
|
|
body += `| [${syntax(child.syntax)}](${link(childCommand)}) | ${prose(child.description)} |\n`;
|
|
}
|
|
body += "\n";
|
|
}
|
|
if (entry.arguments.length) {
|
|
body += "| Argument | Meaning |\n| --- | --- |\n";
|
|
for (const argument of entry.arguments)
|
|
body += `| ${syntax(argument.syntax)} | ${prose(argument.description)} |\n`;
|
|
body += "\n";
|
|
}
|
|
const options = entry.options.filter((option) => !option.syntax.includes("--help"));
|
|
if (options.length) {
|
|
body += "| Option | Meaning |\n| --- | --- |\n";
|
|
for (const option of options)
|
|
body += `| ${syntax(option.syntax)} | ${prose(option.description)} |\n`;
|
|
body += "\n";
|
|
}
|
|
}
|
|
files.set(`${slug(command)}/index.mdx`, body.trimEnd() + "\n");
|
|
const children = [...selected].filter(
|
|
(candidate) =>
|
|
candidate.startsWith(command + " ") &&
|
|
owner(candidate.split(" ").slice(0, -1).join(" ")) === command,
|
|
);
|
|
files.set(
|
|
`${slug(command)}/meta.json`,
|
|
JSON.stringify(
|
|
{
|
|
title: command.split(" ").at(-1),
|
|
pages: [
|
|
"index",
|
|
...children.map((child) => child.split(" ").slice(command.split(" ").length).join("/")),
|
|
],
|
|
},
|
|
null,
|
|
2,
|
|
) + "\n",
|
|
);
|
|
}
|
|
let index =
|
|
"---\ntitle: CLI command reference\ndescription: Every public CLI command, argument, and option, grouped by task.\n---\n\n";
|
|
index +=
|
|
"Use `openship <command> --help` to check the version installed on your machine. `<value>` is required; `[value]` is optional. Aliases appear beside the command name.\n\n";
|
|
index += "## Global options\n\n| Option | Meaning |\n| --- | --- |\n";
|
|
for (const option of catalog.find((entry) => entry.command === "openship").options)
|
|
index += `| ${syntax(option.syntax)} | ${prose(option.description)} |\n`;
|
|
index +=
|
|
"\nPut global options before the command, for example `openship --json project list`. See [native mode](/docs/cli/native) before using `--native-config`.\n\n";
|
|
const pages = ["index"];
|
|
for (const [category, names] of Object.entries(categories)) {
|
|
index += `## ${category}\n\n| Command | Purpose |\n| --- | --- |\n`;
|
|
pages.push(`---${category}---`, ...names);
|
|
for (const name of names) {
|
|
const entry = top.find((entry) => entry.command === "openship " + name);
|
|
index += `| [${syntax(entry.command)}](/docs/cli/reference/${name}) | ${prose(entry.description)} |\n`;
|
|
}
|
|
index += "\n";
|
|
}
|
|
files.set("index.mdx", index.trimEnd() + "\n");
|
|
files.set("meta.json", JSON.stringify({ title: "Command reference", pages }, null, 2) + "\n");
|
|
return files;
|
|
}
|
|
|
|
if (process.argv[1] === fileURLToPath(import.meta.url)) {
|
|
const catalog = await cliSurface();
|
|
const output = join(docsDirectory, "cli/reference");
|
|
const generated = renderCliReference(catalog);
|
|
for (const [name, text] of generated) {
|
|
const path = join(output, name);
|
|
if (process.argv.includes("--write")) {
|
|
mkdirSync(dirname(path), { recursive: true });
|
|
writeFileSync(path, text);
|
|
} else if (readFileSync(path, "utf8") !== text)
|
|
throw new Error(`CLI reference is stale: ${name}. Run bun run docs:reference.`);
|
|
}
|
|
console.log(
|
|
`${catalog.length} command paths documented across ${[...generated.keys()].filter((name) => name.endsWith(".mdx")).length} reference pages.`,
|
|
);
|
|
}
|