@stacksjs/buddy 0.70.257 → 0.70.259
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/cli.d.ts +11 -1
- package/dist/cli.js +5 -164
- package/dist/commands/about.js +1 -35
- package/dist/commands/add.js +1 -25
- package/dist/commands/ai-context.js +4 -27
- package/dist/commands/auth.js +1 -68
- package/dist/commands/build.js +1 -221
- package/dist/commands/cd.js +8 -39
- package/dist/commands/changelog.js +1 -32
- package/dist/commands/clean.js +1 -44
- package/dist/commands/cloud.js +2 -519
- package/dist/commands/commit.js +1 -16
- package/dist/commands/completion.js +6 -42
- package/dist/commands/config-migrate.js +6 -94
- package/dist/commands/configure.js +2 -45
- package/dist/commands/create.js +1 -162
- package/dist/commands/deploy.d.ts +10 -4
- package/dist/commands/deploy.js +13 -1664
- package/dist/commands/desktop-apple.js +12 -399
- package/dist/commands/dev.js +6 -1083
- package/dist/commands/dns.js +3 -103
- package/dist/commands/docs/buddy-commands.js +10 -132
- package/dist/commands/docs/buddy-commands.test.js +1 -38
- package/dist/commands/docs/framework-repo.js +1 -14
- package/dist/commands/docs/framework-repo.test.js +1 -68
- package/dist/commands/docs/generated-artifacts.js +3 -70
- package/dist/commands/docs/generated-artifacts.test.js +1 -19
- package/dist/commands/docs/links.js +2 -85
- package/dist/commands/docs/links.test.js +4 -66
- package/dist/commands/docs.js +1 -24
- package/dist/commands/doctor.js +2 -478
- package/dist/commands/domains.js +2 -152
- package/dist/commands/email.js +31 -503
- package/dist/commands/env.js +1 -312
- package/dist/commands/extension.js +1 -239
- package/dist/commands/features.js +9 -345
- package/dist/commands/fresh.js +1 -46
- package/dist/commands/generate.js +2 -152
- package/dist/commands/http.js +1 -33
- package/dist/commands/index.js +1 -57
- package/dist/commands/install.js +1 -22
- package/dist/commands/key.js +1 -23
- package/dist/commands/link.js +2 -147
- package/dist/commands/lint.js +2 -46
- package/dist/commands/list.js +1 -140
- package/dist/commands/mail.js +15 -1477
- package/dist/commands/maintenance.js +1 -141
- package/dist/commands/make.js +1 -392
- package/dist/commands/migrate-project.js +1 -49
- package/dist/commands/migrate.js +27 -625
- package/dist/commands/outdated.js +1 -19
- package/dist/commands/package.js +1 -17
- package/dist/commands/phone.js +17 -181
- package/dist/commands/ports.js +2 -118
- package/dist/commands/prepublish.js +1 -17
- package/dist/commands/projects.js +1 -29
- package/dist/commands/protocol/craft-evidence.js +4 -98
- package/dist/commands/protocol/craft-evidence.test.js +1 -20
- package/dist/commands/protocol/desktop-lifecycle-report.js +3 -57
- package/dist/commands/protocol/desktop-lifecycle-report.test.js +1 -32
- package/dist/commands/protocol/desktop-support.js +2 -32
- package/dist/commands/protocol/desktop-workflow.test.js +3 -35
- package/dist/commands/protocol/driver-registry.js +2 -71
- package/dist/commands/protocol/pantry-evidence.js +4 -144
- package/dist/commands/protocol/pantry-evidence.test.js +1 -29
- package/dist/commands/protocol/release-manifest.js +3 -102
- package/dist/commands/protocol/release-manifest.test.js +2 -34
- package/dist/commands/protocol/run-conformance.js +4 -346
- package/dist/commands/protocol/run-conformance.test.js +1 -52
- package/dist/commands/protocol/run-driver-contracts.js +4 -83
- package/dist/commands/protocol/run-driver-contracts.test.js +1 -18
- package/dist/commands/protocol/source-manifest.js +3 -113
- package/dist/commands/protocol/sync-suite.js +3 -136
- package/dist/commands/protocol/sync-suite.test.js +1 -44
- package/dist/commands/protocol.js +1 -60
- package/dist/commands/publish.js +8 -500
- package/dist/commands/queue.js +1 -248
- package/dist/commands/release.js +1 -42
- package/dist/commands/route.js +1 -21
- package/dist/commands/run-tool.js +1 -9
- package/dist/commands/saas.js +1 -25
- package/dist/commands/schedule.js +1 -61
- package/dist/commands/search.js +1 -84
- package/dist/commands/seed.js +1 -47
- package/dist/commands/serve.js +1 -55
- package/dist/commands/setup-ai.js +2 -121
- package/dist/commands/setup.js +2 -305
- package/dist/commands/share.js +1 -214
- package/dist/commands/sms.js +32 -319
- package/dist/commands/stacks.js +1 -72
- package/dist/commands/telemetry.js +1 -74
- package/dist/commands/test.js +1 -130
- package/dist/commands/tinker.js +1 -37
- package/dist/commands/types.js +1 -18
- package/dist/commands/upgrade.js +1 -98
- package/dist/commands/user.js +3 -84
- package/dist/commands/version.js +1 -14
- package/dist/config.js +1 -231
- package/dist/custom-cli.js +1 -23
- package/dist/database-preflight.js +1 -88
- package/dist/global-options.js +1 -4
- package/dist/index.js +1 -1
- package/dist/lazy-commands.js +1 -241
- package/dist/migrators/index.js +2 -62
- package/dist/migrators/laravel/index.js +1 -148
- package/dist/migrators/laravel/migrations.js +4 -231
- package/dist/migrators/laravel/models.js +8 -111
- package/dist/migrators/rails/index.js +1 -11
- package/dist/production-server.js +1 -257
- package/dist/project-setup.js +1 -36
- package/dist/serve-entry.js +1 -5
- package/dist/version-info.js +1 -5
- package/package.json +54 -47
package/dist/commands/dns.js
CHANGED
|
@@ -1,103 +1,3 @@
|
|
|
1
|
-
import process from
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
import { renderDnsConfig, resolveLiveRecords, syncDnsConfig } from "@stacksjs/dns";
|
|
5
|
-
import { ExitCode } from "@stacksjs/types";
|
|
6
|
-
import { loadProjectDnsConfig } from "../config";
|
|
7
|
-
export function dns(buddy) {
|
|
8
|
-
const descriptions = {
|
|
9
|
-
dns: "Lists the DNS records for a domain",
|
|
10
|
-
query: "Host name or IP address to query",
|
|
11
|
-
type: "Type of the DNS record being queried (A, MX, NS\u2026)",
|
|
12
|
-
nameserver: "Address of the nameserver to send packets to",
|
|
13
|
-
class: "Network class of the DNS record being queried (IN, CH, HS)",
|
|
14
|
-
udp: "Use the DNS protocol over UDP",
|
|
15
|
-
tcp: "Use the DNS protocol over TCP",
|
|
16
|
-
tls: "Use the DNS-over-TLS protocol",
|
|
17
|
-
https: "Use the DNS-over-HTTPS protocol",
|
|
18
|
-
short: "Short mode: display nothing but the first result",
|
|
19
|
-
json: "Display the output as JSON",
|
|
20
|
-
project: "Target a specific project",
|
|
21
|
-
verbose: "Enable verbose output"
|
|
22
|
-
};
|
|
23
|
-
buddy.command("dns [domain]", descriptions.dns).option("-q, --query <query>", descriptions.query).option("-t, --type <type>", descriptions.type, { default: "A" }).option("-n, --nameserver <nameserver>", descriptions.nameserver).option("--class <class>", descriptions.class).option("-U, --udp", descriptions.udp).option("-T, --tcp", descriptions.tcp).option("-S, --tls", descriptions.tls).option("-H, --https", descriptions.https).option("-1, --short", descriptions.short, { default: !1 }).option("-J, --json", descriptions.json, { default: !1 }).option("-p, --project [project]", descriptions.project, { default: !1 }).option("--verbose", descriptions.verbose, { default: !1 }).action(async (domain, options) => {
|
|
24
|
-
log.debug("Running `buddy dns [domain]` ...", options);
|
|
25
|
-
const targetDomain = domain || config.app.url;
|
|
26
|
-
let DnsClient, formatOutput;
|
|
27
|
-
try {
|
|
28
|
-
const dnsx = await import("@stacksjs/dnsx");
|
|
29
|
-
DnsClient = dnsx.DnsClient;
|
|
30
|
-
formatOutput = dnsx.formatOutput;
|
|
31
|
-
} catch (err) {
|
|
32
|
-
log.error("`buddy dns` needs the @stacksjs/dnsx runtime, but only the type declarations are currently published. Install a build with the JS runtime (or wait for the next dnsx release) and re-run.");
|
|
33
|
-
log.debug(`[dns] import failure: ${err instanceof Error ? err.message : String(err)}`);
|
|
34
|
-
process.exit(ExitCode.FatalError);
|
|
35
|
-
}
|
|
36
|
-
try {
|
|
37
|
-
const client = new DnsClient({
|
|
38
|
-
domains: [targetDomain],
|
|
39
|
-
type: options.type,
|
|
40
|
-
nameserver: options.nameserver,
|
|
41
|
-
class: options.class,
|
|
42
|
-
udp: options.udp,
|
|
43
|
-
tcp: options.tcp,
|
|
44
|
-
tls: options.tls,
|
|
45
|
-
https: options.https,
|
|
46
|
-
short: options.short,
|
|
47
|
-
json: options.json,
|
|
48
|
-
verbose: options.verbose
|
|
49
|
-
}), startTime = performance.now(), responses = await client.query(), duration = performance.now() - startTime, output = formatOutput(responses, {
|
|
50
|
-
json: options.json ?? !1,
|
|
51
|
-
short: options.short ?? !1,
|
|
52
|
-
showDuration: duration,
|
|
53
|
-
colors: { enabled: !0 },
|
|
54
|
-
rawSeconds: !1
|
|
55
|
-
});
|
|
56
|
-
console.log(output);
|
|
57
|
-
} catch (error) {
|
|
58
|
-
log.error(`DNS query failed: ${error instanceof Error ? error.message : String(error)}`);
|
|
59
|
-
process.exit(ExitCode.FatalError);
|
|
60
|
-
}
|
|
61
|
-
process.exit(ExitCode.Success);
|
|
62
|
-
});
|
|
63
|
-
const bareDomain = (input) => (input || config.app.url || "").replace(/^[a-z]+:\/\//i, "").replace(/[/:].*$/, "");
|
|
64
|
-
buddy.command("dns:pull [domain]", "Print a domain's live DNS records as a config/dns.ts block").option("--verbose", descriptions.verbose, { default: !1 }).action(async (domain, options) => {
|
|
65
|
-
const target = bareDomain(domain);
|
|
66
|
-
log.debug(`Running \`buddy dns:pull ${target}\` ...`, options);
|
|
67
|
-
const records = await resolveLiveRecords(target);
|
|
68
|
-
if (!records.length) {
|
|
69
|
-
log.error(`No DNS records resolved for ${target}.`);
|
|
70
|
-
process.exit(ExitCode.FatalError);
|
|
71
|
-
}
|
|
72
|
-
console.log(renderDnsConfig(target, records));
|
|
73
|
-
process.exit(ExitCode.Success);
|
|
74
|
-
});
|
|
75
|
-
buddy.command("dns:diff [domain]", "Show which config/dns.ts records are missing from the live zone").option("--verbose", descriptions.verbose, { default: !1 }).action(async (domain, options) => {
|
|
76
|
-
const target = bareDomain(domain);
|
|
77
|
-
log.debug(`Running \`buddy dns:diff ${target}\` ...`, options);
|
|
78
|
-
const dnsConfig = await loadProjectDnsConfig(config.dns), { plan, provider } = await syncDnsConfig(target, dnsConfig, { dryRun: !0 });
|
|
79
|
-
for (const item of plan.items) {
|
|
80
|
-
const detail = item.record.type === "TXT" || item.record.type === "MX" ? ` ${item.record.content}` : ` \u2192 ${item.record.content}`;
|
|
81
|
-
console.log(` ${item.action === "create" ? "+ create" : " keep "} ${item.record.type.padEnd(5)} ${item.record.name}${detail}`);
|
|
82
|
-
}
|
|
83
|
-
console.log(`
|
|
84
|
-
${plan.create.length} to create, ${plan.keep.length} already present (${provider ? `registrar: ${provider}` : "public DNS"})`);
|
|
85
|
-
process.exit(ExitCode.Success);
|
|
86
|
-
});
|
|
87
|
-
buddy.command("dns:sync [domain]", "Additively sync config/dns.ts to the registrar (creates missing records; never deletes or overwrites)").option("--dry-run", "Show the plan without writing any records", { default: !1 }).option("--verbose", descriptions.verbose, { default: !1 }).action(async (domain, options) => {
|
|
88
|
-
const target = bareDomain(domain);
|
|
89
|
-
log.debug(`Running \`buddy dns:sync ${target}\` ...`, options);
|
|
90
|
-
const dnsConfig = await loadProjectDnsConfig(config.dns), result = await syncDnsConfig(target, dnsConfig, { dryRun: options.dryRun });
|
|
91
|
-
if (!result.provider && !options.dryRun) {
|
|
92
|
-
log.warn(`No DNS provider credentials found (e.g. PORKBUN_API_KEY / PORKBUN_SECRET_KEY) \u2014 nothing was synced. ${result.plan.create.length} record(s) would be created.`);
|
|
93
|
-
process.exit(ExitCode.Success);
|
|
94
|
-
}
|
|
95
|
-
for (const record of result.plan.create)
|
|
96
|
-
console.log(` ${result.applied ? "created" : "would create"} ${record.type.padEnd(5)} ${record.name} \u2192 ${record.content}`);
|
|
97
|
-
const verb = result.applied ? "created" : "to create", count = result.applied ? result.created : result.plan.create.length;
|
|
98
|
-
console.log(`
|
|
99
|
-
dns:sync ${target}: ${count} ${verb}, ${result.kept} kept${result.failed ? `, ${result.failed} failed` : ""}${result.provider ? ` (${result.provider})` : ""}`);
|
|
100
|
-
process.exit(result.failed > 0 ? ExitCode.FatalError : ExitCode.Success);
|
|
101
|
-
});
|
|
102
|
-
onUnknownSubcommand(buddy, "dns");
|
|
103
|
-
}
|
|
1
|
+
import process from"node:process";import{log,onUnknownSubcommand}from"@stacksjs/cli";import{config}from"@stacksjs/config";import{renderDnsConfig,resolveLiveRecords,syncDnsConfig}from"@stacksjs/dns";import{ExitCode}from"@stacksjs/types";import{loadProjectDnsConfig}from"../config";export function dns(buddy){const descriptions={dns:"Lists the DNS records for a domain",query:"Host name or IP address to query",type:"Type of the DNS record being queried (A, MX, NS\u2026)",nameserver:"Address of the nameserver to send packets to",class:"Network class of the DNS record being queried (IN, CH, HS)",udp:"Use the DNS protocol over UDP",tcp:"Use the DNS protocol over TCP",tls:"Use the DNS-over-TLS protocol",https:"Use the DNS-over-HTTPS protocol",short:"Short mode: display nothing but the first result",json:"Display the output as JSON",project:"Target a specific project",verbose:"Enable verbose output"};buddy.command("dns [domain]",descriptions.dns).option("-q, --query <query>",descriptions.query).option("-t, --type <type>",descriptions.type,{default:"A"}).option("-n, --nameserver <nameserver>",descriptions.nameserver).option("--class <class>",descriptions.class).option("-U, --udp",descriptions.udp).option("-T, --tcp",descriptions.tcp).option("-S, --tls",descriptions.tls).option("-H, --https",descriptions.https).option("-1, --short",descriptions.short,{default:!1}).option("-J, --json",descriptions.json,{default:!1}).option("-p, --project [project]",descriptions.project,{default:!1}).option("--verbose",descriptions.verbose,{default:!1}).action(async(domain,options)=>{log.debug("Running `buddy dns [domain]` ...",options);const targetDomain=domain||config.app.url;let DnsClient,formatOutput;try{const dnsx=await import("@stacksjs/dnsx");DnsClient=dnsx.DnsClient;formatOutput=dnsx.formatOutput}catch(err){log.error("`buddy dns` needs the @stacksjs/dnsx runtime, but only the type declarations are currently published. Install a build with the JS runtime (or wait for the next dnsx release) and re-run.");log.debug(`[dns] import failure: ${err instanceof Error?err.message:String(err)}`);process.exit(ExitCode.FatalError)}try{const client=new DnsClient({domains:[targetDomain],type:options.type,nameserver:options.nameserver,class:options.class,udp:options.udp,tcp:options.tcp,tls:options.tls,https:options.https,short:options.short,json:options.json,verbose:options.verbose}),startTime=performance.now(),responses=await client.query(),duration=performance.now()-startTime,output=formatOutput(responses,{json:options.json??!1,short:options.short??!1,showDuration:duration,colors:{enabled:!0},rawSeconds:!1});console.log(output)}catch(error){log.error(`DNS query failed: ${error instanceof Error?error.message:String(error)}`);process.exit(ExitCode.FatalError)}process.exit(ExitCode.Success)});const bareDomain=(input)=>(input||config.app.url||"").replace(/^[a-z]+:\/\//i,"").replace(/[/:].*$/,"");buddy.command("dns:pull [domain]","Print a domain's live DNS records as a config/dns.ts block").option("--verbose",descriptions.verbose,{default:!1}).action(async(domain,options)=>{const target=bareDomain(domain);log.debug(`Running \`buddy dns:pull ${target}\` ...`,options);const records=await resolveLiveRecords(target);if(!records.length){log.error(`No DNS records resolved for ${target}.`);process.exit(ExitCode.FatalError)}console.log(renderDnsConfig(target,records));process.exit(ExitCode.Success)});buddy.command("dns:diff [domain]","Show which config/dns.ts records are missing from the live zone").option("--verbose",descriptions.verbose,{default:!1}).action(async(domain,options)=>{const target=bareDomain(domain);log.debug(`Running \`buddy dns:diff ${target}\` ...`,options);const dnsConfig=await loadProjectDnsConfig(config.dns),{plan,provider}=await syncDnsConfig(target,dnsConfig,{dryRun:!0});for(const item of plan.items){const detail=item.record.type==="TXT"||item.record.type==="MX"?` ${item.record.content}`:` \u2192 ${item.record.content}`;console.log(` ${item.action==="create"?"+ create":" keep "} ${item.record.type.padEnd(5)} ${item.record.name}${detail}`)}console.log(`
|
|
2
|
+
${plan.create.length} to create, ${plan.keep.length} already present (${provider?`registrar: ${provider}`:"public DNS"})`);process.exit(ExitCode.Success)});buddy.command("dns:sync [domain]","Additively sync config/dns.ts to the registrar (creates missing records; never deletes or overwrites)").option("--dry-run","Show the plan without writing any records",{default:!1}).option("--verbose",descriptions.verbose,{default:!1}).action(async(domain,options)=>{const target=bareDomain(domain);log.debug(`Running \`buddy dns:sync ${target}\` ...`,options);const dnsConfig=await loadProjectDnsConfig(config.dns),result=await syncDnsConfig(target,dnsConfig,{dryRun:options.dryRun});if(!result.provider&&!options.dryRun){log.warn(`No DNS provider credentials found (e.g. PORKBUN_API_KEY / PORKBUN_SECRET_KEY) \u2014 nothing was synced. ${result.plan.create.length} record(s) would be created.`);process.exit(ExitCode.Success)}for(const record of result.plan.create)console.log(` ${result.applied?"created":"would create"} ${record.type.padEnd(5)} ${record.name} \u2192 ${record.content}`);const verb=result.applied?"created":"to create",count=result.applied?result.created:result.plan.create.length;console.log(`
|
|
3
|
+
dns:sync ${target}: ${count} ${verb}, ${result.kept} kept${result.failed?`, ${result.failed} failed`:""}${result.provider?` (${result.provider})`:""}`);process.exit(result.failed>0?ExitCode.FatalError:ExitCode.Success)});onUnknownSubcommand(buddy,"dns")}
|
|
@@ -1,135 +1,13 @@
|
|
|
1
|
-
import
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
const root = resolve(import.meta.dir, "../../../../../../.."), outputPath = resolve(root, "docs/guide/buddy/commands.md"), cliPath = resolve(root, "storage/framework/core/buddy/src/cli.ts");
|
|
5
|
-
function markdown(value) {
|
|
6
|
-
return String(value).replaceAll("\\", "\\\\").replaceAll("|", "\\|").replaceAll(`
|
|
7
|
-
`, "<br>");
|
|
8
|
-
}
|
|
9
|
-
function code(value) {
|
|
10
|
-
return `\`${String(value).replaceAll("`", "\\`")}\``;
|
|
11
|
-
}
|
|
12
|
-
function stablePath(value) {
|
|
13
|
-
if (typeof value !== "string" || !value.includes(root))
|
|
14
|
-
return value;
|
|
15
|
-
return value.replaceAll(root, "<project root>");
|
|
16
|
-
}
|
|
17
|
-
function optionFlags(option) {
|
|
18
|
-
return option.flags.map((flag) => code(`${flag.length === 1 ? "-" : "--"}${flag}`)).join(", ");
|
|
19
|
-
}
|
|
20
|
-
function optionKind(option) {
|
|
21
|
-
return [
|
|
22
|
-
option.boolean ? "boolean" : "value",
|
|
23
|
-
option.required ? "required" : "optional",
|
|
24
|
-
...option.negated ? ["negated"] : []
|
|
25
|
-
].join(", ");
|
|
26
|
-
}
|
|
27
|
-
function argumentSyntax(argument) {
|
|
28
|
-
const value = `${argument.name}${argument.variadic ? "..." : ""}`;
|
|
29
|
-
return code(argument.required ? `<${value}>` : `[${value}]`);
|
|
30
|
-
}
|
|
31
|
-
function commandSection(command) {
|
|
32
|
-
const lines = [
|
|
33
|
-
`### ${code(command.name)}`,
|
|
34
|
-
"",
|
|
35
|
-
command.description || "No description provided.",
|
|
36
|
-
"",
|
|
37
|
-
`- Usage: ${code(command.usage)}`,
|
|
38
|
-
`- Namespace: ${command.namespace ? code(command.namespace) : "none"}`,
|
|
39
|
-
`- Aliases: ${command.aliases.length ? command.aliases.map(code).join(", ") : "none"}`,
|
|
40
|
-
`- Arguments: ${command.arguments.length ? command.arguments.map(argumentSyntax).join(", ") : "none"}`
|
|
41
|
-
];
|
|
42
|
-
if (command.options.length)
|
|
43
|
-
lines.push("", "| Option | Description | Contract | Default |", "| --- | --- | --- | --- |", ...command.options.map((option) => `| ${optionFlags(option)} | ${markdown(option.description || "\u2014")} | ${optionKind(option)} | ${option.default === void 0 ? "\u2014" : code(JSON.stringify(stablePath(option.default)))} |`));
|
|
44
|
-
if (command.examples.length)
|
|
45
|
-
lines.push("", "Examples:", "", "```bash", ...command.examples, "```");
|
|
46
|
-
return lines.join(`
|
|
47
|
-
`);
|
|
48
|
-
}
|
|
49
|
-
export function renderBuddyCommandReference(inventory) {
|
|
50
|
-
const commands = [...inventory.commands].sort((a, b) => a.name.localeCompare(b.name));
|
|
51
|
-
if (inventory.total !== commands.length)
|
|
52
|
-
throw Error(`Buddy inventory total ${inventory.total} does not match ${commands.length} commands`);
|
|
53
|
-
if (new Set(commands.map((command) => command.name)).size !== commands.length)
|
|
54
|
-
throw Error("Buddy inventory contains duplicate canonical command names");
|
|
55
|
-
const groups = new Map;
|
|
56
|
-
for (const command of commands) {
|
|
57
|
-
const group = command.namespace || (command.name.includes(":") ? command.name.split(":")[0] : "general"), entries = groups.get(group) || [];
|
|
58
|
-
entries.push(command);
|
|
59
|
-
groups.set(group, entries);
|
|
60
|
-
}
|
|
61
|
-
const groupSummary = [...groups.entries()].sort(([a], [b]) => a.localeCompare(b)).map(([group, entries]) => `| ${code(group)} | ${entries.length} |`);
|
|
62
|
-
return `${[
|
|
63
|
-
"---",
|
|
64
|
-
"title: Buddy Command Reference",
|
|
65
|
-
"description: Generated reference for every Buddy command, argument, option, alias, default, and example.",
|
|
66
|
-
"---",
|
|
67
|
-
"<!-- Generated by storage/framework/core/buddy/src/commands/docs/buddy-commands.ts. Do not edit by hand. -->",
|
|
68
|
-
"",
|
|
69
|
-
"# Buddy Command Reference",
|
|
70
|
-
"",
|
|
71
|
-
`This reference is generated from Buddy's runtime command registry and currently contains **${commands.length} commands**. Run \`bun run docs:buddy\` after changing the registry; CI rejects stale output.`,
|
|
72
|
-
"",
|
|
73
|
-
"## Command groups",
|
|
74
|
-
"",
|
|
75
|
-
"| Group | Commands |",
|
|
76
|
-
"| --- | ---: |",
|
|
77
|
-
...groupSummary,
|
|
78
|
-
"",
|
|
79
|
-
"## Commands",
|
|
80
|
-
"",
|
|
81
|
-
...commands.flatMap((command) => [commandSection(command), ""])
|
|
82
|
-
].join(`
|
|
1
|
+
import{assertFrameworkRepo}from"./framework-repo";import{existsSync,mkdirSync,readFileSync,writeFileSync}from"node:fs";import{dirname,resolve}from"node:path";const root=resolve(import.meta.dir,"../../../../../../.."),outputPath=resolve(root,"docs/guide/buddy/commands.md"),cliPath=resolve(root,"storage/framework/core/buddy/src/cli.ts");function markdown(value){return String(value).replaceAll("\\","\\\\").replaceAll("|","\\|").replaceAll(`
|
|
2
|
+
`,"<br>")}function code(value){return`\`${String(value).replaceAll("`","\\`")}\``}function stablePath(value){if(typeof value!=="string"||!value.includes(root))return value;return value.replaceAll(root,"<project root>")}function optionFlags(option){return option.flags.map((flag)=>code(`${flag.length===1?"-":"--"}${flag}`)).join(", ")}function optionKind(option){return[option.boolean?"boolean":"value",option.required?"required":"optional",...option.negated?["negated"]:[]].join(", ")}function argumentSyntax(argument){const value=`${argument.name}${argument.variadic?"...":""}`;return code(argument.required?`<${value}>`:`[${value}]`)}function commandSection(command){const lines=[`### ${code(command.name)}`,"",command.description||"No description provided.","",`- Usage: ${code(command.usage)}`,`- Namespace: ${command.namespace?code(command.namespace):"none"}`,`- Aliases: ${command.aliases.length?command.aliases.map(code).join(", "):"none"}`,`- Arguments: ${command.arguments.length?command.arguments.map(argumentSyntax).join(", "):"none"}`];if(command.options.length)lines.push("","| Option | Description | Contract | Default |","| --- | --- | --- | --- |",...command.options.map((option)=>`| ${optionFlags(option)} | ${markdown(option.description||"\u2014")} | ${optionKind(option)} | ${option.default===void 0?"\u2014":code(JSON.stringify(stablePath(option.default)))} |`));if(command.examples.length)lines.push("","Examples:","","```bash",...command.examples,"```");return lines.join(`
|
|
3
|
+
`)}export function renderBuddyCommandReference(inventory){const commands=[...inventory.commands].sort((a,b)=>a.name.localeCompare(b.name));if(inventory.total!==commands.length)throw Error(`Buddy inventory total ${inventory.total} does not match ${commands.length} commands`);if(new Set(commands.map((command)=>command.name)).size!==commands.length)throw Error("Buddy inventory contains duplicate canonical command names");const groups=new Map;for(const command of commands){const group=command.namespace||(command.name.includes(":")?command.name.split(":")[0]:"general"),entries=groups.get(group)||[];entries.push(command);groups.set(group,entries)}const groupSummary=[...groups.entries()].sort(([a],[b])=>a.localeCompare(b)).map(([group,entries])=>`| ${code(group)} | ${entries.length} |`);return`${["---","title: Buddy Command Reference","description: Generated reference for every Buddy command, argument, option, alias, default, and example.","---","<!-- Generated by storage/framework/core/buddy/src/commands/docs/buddy-commands.ts. Do not edit by hand. -->","","# Buddy Command Reference","",`This reference is generated from Buddy's runtime command registry and currently contains **${commands.length} commands**. Run \`bun run docs:buddy\` after changing the registry; CI rejects stale output.`,"","## Command groups","","| Group | Commands |","| --- | ---: |",...groupSummary,"","## Commands","",...commands.flatMap((command)=>[commandSection(command),""])].join(`
|
|
83
4
|
`).split(`
|
|
84
|
-
`).map((line)
|
|
5
|
+
`).map((line)=>line.trimEnd()).join(`
|
|
85
6
|
`).trimEnd()}
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
env: { ...process.env, NO_COLOR: "1" },
|
|
92
|
-
stdout: "pipe",
|
|
93
|
-
stderr: "pipe"
|
|
94
|
-
});
|
|
95
|
-
if (result.exitCode !== 0)
|
|
96
|
-
throw Error(`Buddy command inventory failed (${result.exitCode}): ${result.stderr.toString().trim()}`);
|
|
97
|
-
const inventory = JSON.parse(result.stdout.toString());
|
|
98
|
-
if (!Array.isArray(inventory.commands) || !Number.isInteger(inventory.total))
|
|
99
|
-
throw Error("Buddy command inventory returned an invalid payload");
|
|
100
|
-
return inventory;
|
|
101
|
-
}
|
|
102
|
-
export async function run() {
|
|
103
|
-
assertFrameworkRepo(root, "docs:buddy");
|
|
104
|
-
const mode = process.argv.includes("--write") ? "write" : process.argv.includes("--check") ? "check" : null;
|
|
105
|
-
if (!mode) {
|
|
106
|
-
console.error("usage: bun storage/framework/core/buddy/src/commands/docs/buddy-commands.ts --write | --check");
|
|
107
|
-
process.exit(2);
|
|
108
|
-
}
|
|
109
|
-
const expected = renderBuddyCommandReference(loadBuddyInventory());
|
|
110
|
-
if (mode === "write") {
|
|
111
|
-
mkdirSync(dirname(outputPath), { recursive: !0 });
|
|
112
|
-
writeFileSync(outputPath, expected);
|
|
113
|
-
console.log(`Wrote ${outputPath}`);
|
|
114
|
-
} else {
|
|
115
|
-
if (!existsSync(outputPath))
|
|
116
|
-
throw Error("Buddy command reference is missing; run bun run docs:buddy");
|
|
117
|
-
const current = readFileSync(outputPath, "utf8");
|
|
118
|
-
if (current !== expected) {
|
|
119
|
-
const currentLines = current.split(`
|
|
120
|
-
`), expectedLines = expected.split(`
|
|
121
|
-
`), differences = [];
|
|
122
|
-
for (let i = 0;i < Math.max(currentLines.length, expectedLines.length) && differences.length < 6; i++)
|
|
123
|
-
if (currentLines[i] !== expectedLines[i])
|
|
124
|
-
differences.push(` line ${i + 1}:
|
|
125
|
-
committed: ${JSON.stringify(currentLines[i] ?? "<end of file>")}
|
|
126
|
-
generated: ${JSON.stringify(expectedLines[i] ?? "<end of file>")}`);
|
|
127
|
-
throw Error(`Buddy command reference is stale; run bun run docs:buddy
|
|
7
|
+
`}export function loadBuddyInventory(){const result=Bun.spawnSync([process.execPath,cliPath,"list","--json"],{cwd:root,env:{...process.env,NO_COLOR:"1"},stdout:"pipe",stderr:"pipe"});if(result.exitCode!==0)throw Error(`Buddy command inventory failed (${result.exitCode}): ${result.stderr.toString().trim()}`);const inventory=JSON.parse(result.stdout.toString());if(!Array.isArray(inventory.commands)||!Number.isInteger(inventory.total))throw Error("Buddy command inventory returned an invalid payload");return inventory}export async function run(){assertFrameworkRepo(root,"docs:buddy");const mode=process.argv.includes("--write")?"write":process.argv.includes("--check")?"check":null;if(!mode){console.error("usage: bun storage/framework/core/buddy/src/commands/docs/buddy-commands.ts --write | --check");process.exit(2)}const expected=renderBuddyCommandReference(loadBuddyInventory());if(mode==="write"){mkdirSync(dirname(outputPath),{recursive:!0});writeFileSync(outputPath,expected);console.log(`Wrote ${outputPath}`)}else{if(!existsSync(outputPath))throw Error("Buddy command reference is missing; run bun run docs:buddy");const current=readFileSync(outputPath,"utf8");if(current!==expected){const currentLines=current.split(`
|
|
8
|
+
`),expectedLines=expected.split(`
|
|
9
|
+
`),differences=[];for(let i=0;i<Math.max(currentLines.length,expectedLines.length)&&differences.length<6;i++)if(currentLines[i]!==expectedLines[i])differences.push(` line ${i+1}:
|
|
10
|
+
committed: ${JSON.stringify(currentLines[i]??"<end of file>")}
|
|
11
|
+
generated: ${JSON.stringify(expectedLines[i]??"<end of file>")}`);throw Error(`Buddy command reference is stale; run bun run docs:buddy
|
|
128
12
|
${differences.join(`
|
|
129
|
-
`)}`);
|
|
130
|
-
}
|
|
131
|
-
console.log("Buddy command reference matches the runtime registry");
|
|
132
|
-
}
|
|
133
|
-
}
|
|
134
|
-
if (import.meta.main)
|
|
135
|
-
await run();
|
|
13
|
+
`)}`)}console.log("Buddy command reference matches the runtime registry")}}if(import.meta.main)await run();
|
|
@@ -1,38 +1 @@
|
|
|
1
|
-
import { describe, expect, it }
|
|
2
|
-
import { renderBuddyCommandReference } from "./buddy-commands";
|
|
3
|
-
const command = {
|
|
4
|
-
name: "deploy",
|
|
5
|
-
description: "Deploy the application",
|
|
6
|
-
aliases: ["ship"],
|
|
7
|
-
usage: "$ buddy deploy [environment]",
|
|
8
|
-
arguments: [{ name: "environment", required: !1, variadic: !1 }],
|
|
9
|
-
options: [{
|
|
10
|
-
name: "region",
|
|
11
|
-
flags: ["r", "region"],
|
|
12
|
-
description: "Cloud region",
|
|
13
|
-
required: !0,
|
|
14
|
-
boolean: !1,
|
|
15
|
-
negated: !1,
|
|
16
|
-
default: "us-east-1"
|
|
17
|
-
}],
|
|
18
|
-
examples: ["buddy deploy production"]
|
|
19
|
-
};
|
|
20
|
-
describe("Buddy command reference", () => {
|
|
21
|
-
it("renders every command contract field", () => {
|
|
22
|
-
const result = renderBuddyCommandReference({ commands: [command], total: 1 });
|
|
23
|
-
expect(result).toContain("**1 commands**");
|
|
24
|
-
expect(result).toContain("### `deploy`");
|
|
25
|
-
expect(result).toContain("Aliases: `ship`");
|
|
26
|
-
expect(result).toContain("Arguments: `[environment]`");
|
|
27
|
-
expect(result).toContain('| `-r`, `--region` | Cloud region | value, required | `"us-east-1"` |');
|
|
28
|
-
expect(result).toContain("buddy deploy production");
|
|
29
|
-
});
|
|
30
|
-
it("sorts commands and rejects incomplete inventories", () => {
|
|
31
|
-
const about = { ...command, name: "about" }, result = renderBuddyCommandReference({ commands: [command, about], total: 2 });
|
|
32
|
-
expect(result.indexOf("### `about`")).toBeLessThan(result.indexOf("### `deploy`"));
|
|
33
|
-
expect(() => renderBuddyCommandReference({ commands: [command], total: 2 })).toThrow("does not match");
|
|
34
|
-
});
|
|
35
|
-
it("rejects duplicate canonical names", () => {
|
|
36
|
-
expect(() => renderBuddyCommandReference({ commands: [command, command], total: 2 })).toThrow("duplicate");
|
|
37
|
-
});
|
|
38
|
-
});
|
|
1
|
+
import{describe,expect,it}from"bun:test";import{renderBuddyCommandReference}from"./buddy-commands";const command={name:"deploy",description:"Deploy the application",aliases:["ship"],usage:"$ buddy deploy [environment]",arguments:[{name:"environment",required:!1,variadic:!1}],options:[{name:"region",flags:["r","region"],description:"Cloud region",required:!0,boolean:!1,negated:!1,default:"us-east-1"}],examples:["buddy deploy production"]};describe("Buddy command reference",()=>{it("renders every command contract field",()=>{const result=renderBuddyCommandReference({commands:[command],total:1});expect(result).toContain("**1 commands**");expect(result).toContain("### `deploy`");expect(result).toContain("Aliases: `ship`");expect(result).toContain("Arguments: `[environment]`");expect(result).toContain('| `-r`, `--region` | Cloud region | value, required | `"us-east-1"` |');expect(result).toContain("buddy deploy production")});it("sorts commands and rejects incomplete inventories",()=>{const about={...command,name:"about"},result=renderBuddyCommandReference({commands:[command,about],total:2});expect(result.indexOf("### `about`")).toBeLessThan(result.indexOf("### `deploy`"));expect(()=>renderBuddyCommandReference({commands:[command],total:2})).toThrow("does not match")});it("rejects duplicate canonical names",()=>{expect(()=>renderBuddyCommandReference({commands:[command,command],total:2})).toThrow("duplicate")})});
|
|
@@ -1,14 +1 @@
|
|
|
1
|
-
import
|
|
2
|
-
import { isAbsolute, relative, resolve } from "node:path";
|
|
3
|
-
export function isInsideRoot(root, cwd) {
|
|
4
|
-
const rel = relative(resolve(root), resolve(cwd));
|
|
5
|
-
return rel === "" || !rel.startsWith("..") && !isAbsolute(rel);
|
|
6
|
-
}
|
|
7
|
-
export function looksLikeFrameworkRepo(root) {
|
|
8
|
-
return existsSync(resolve(root, "storage/framework/core/buddy"));
|
|
9
|
-
}
|
|
10
|
-
export function assertFrameworkRepo(root, command, cwd = process.cwd()) {
|
|
11
|
-
if (looksLikeFrameworkRepo(root) && isInsideRoot(root, cwd))
|
|
12
|
-
return;
|
|
13
|
-
throw Error(`${command} operates on the Stacks framework repository and would write to ${root}, which is not where you are (${cwd}). Run it from a framework checkout; an application has no generated framework artifacts to check.`);
|
|
14
|
-
}
|
|
1
|
+
import{existsSync}from"node:fs";import{isAbsolute,relative,resolve}from"node:path";export function isInsideRoot(root,cwd){const rel=relative(resolve(root),resolve(cwd));return rel===""||!rel.startsWith("..")&&!isAbsolute(rel)}export function looksLikeFrameworkRepo(root){return existsSync(resolve(root,"storage/framework/core/buddy"))}export function assertFrameworkRepo(root,command,cwd=process.cwd()){if(looksLikeFrameworkRepo(root)&&isInsideRoot(root,cwd))return;throw Error(`${command} operates on the Stacks framework repository and would write to ${root}, which is not where you are (${cwd}). Run it from a framework checkout; an application has no generated framework artifacts to check.`)}
|
|
@@ -1,68 +1 @@
|
|
|
1
|
-
import { describe, expect, it } from "
|
|
2
|
-
import { mkdirSync, mkdtempSync, rmSync } from "node:fs";
|
|
3
|
-
import { tmpdir } from "node:os";
|
|
4
|
-
import { join, resolve } from "node:path";
|
|
5
|
-
import { assertFrameworkRepo, isInsideRoot, looksLikeFrameworkRepo } from "./framework-repo";
|
|
6
|
-
describe("isInsideRoot", () => {
|
|
7
|
-
it("accepts the root itself and anything under it", () => {
|
|
8
|
-
expect(isInsideRoot("/a/b", "/a/b")).toBe(!0);
|
|
9
|
-
expect(isInsideRoot("/a/b", "/a/b/c/d")).toBe(!0);
|
|
10
|
-
});
|
|
11
|
-
it("rejects a sibling and a parent", () => {
|
|
12
|
-
expect(isInsideRoot("/a/b", "/a/c")).toBe(!1);
|
|
13
|
-
expect(isInsideRoot("/a/b", "/a")).toBe(!1);
|
|
14
|
-
});
|
|
15
|
-
it("rejects another project entirely", () => {
|
|
16
|
-
expect(isInsideRoot("/Users/x/Code/stacks", "/Users/x/Code/Apps/reviewos.org")).toBe(!1);
|
|
17
|
-
});
|
|
18
|
-
});
|
|
19
|
-
describe("looksLikeFrameworkRepo", () => {
|
|
20
|
-
it("recognises a tree carrying the framework core", () => {
|
|
21
|
-
const dir = mkdtempSync(join(tmpdir(), "stacks-fw-"));
|
|
22
|
-
try {
|
|
23
|
-
mkdirSync(resolve(dir, "storage/framework/core/buddy"), { recursive: !0 });
|
|
24
|
-
expect(looksLikeFrameworkRepo(dir)).toBe(!0);
|
|
25
|
-
} finally {
|
|
26
|
-
rmSync(dir, { recursive: !0, force: !0 });
|
|
27
|
-
}
|
|
28
|
-
});
|
|
29
|
-
it("does not mistake an ordinary application for it", () => {
|
|
30
|
-
const dir = mkdtempSync(join(tmpdir(), "stacks-app-"));
|
|
31
|
-
try {
|
|
32
|
-
mkdirSync(resolve(dir, "app"), { recursive: !0 });
|
|
33
|
-
expect(looksLikeFrameworkRepo(dir)).toBe(!1);
|
|
34
|
-
} finally {
|
|
35
|
-
rmSync(dir, { recursive: !0, force: !0 });
|
|
36
|
-
}
|
|
37
|
-
});
|
|
38
|
-
});
|
|
39
|
-
describe("assertFrameworkRepo", () => {
|
|
40
|
-
it("passes inside the framework repo", () => {
|
|
41
|
-
const dir = mkdtempSync(join(tmpdir(), "stacks-fw-"));
|
|
42
|
-
try {
|
|
43
|
-
mkdirSync(resolve(dir, "storage/framework/core/buddy"), { recursive: !0 });
|
|
44
|
-
expect(() => assertFrameworkRepo(dir, "docs:artifacts", dir)).not.toThrow();
|
|
45
|
-
} finally {
|
|
46
|
-
rmSync(dir, { recursive: !0, force: !0 });
|
|
47
|
-
}
|
|
48
|
-
});
|
|
49
|
-
it("refuses when run from somewhere else, and says where it would have written", () => {
|
|
50
|
-
const framework = mkdtempSync(join(tmpdir(), "stacks-fw-")), app = mkdtempSync(join(tmpdir(), "stacks-app-"));
|
|
51
|
-
try {
|
|
52
|
-
mkdirSync(resolve(framework, "storage/framework/core/buddy"), { recursive: !0 });
|
|
53
|
-
expect(() => assertFrameworkRepo(framework, "docs:artifacts", app)).toThrow(/docs:artifacts/);
|
|
54
|
-
expect(() => assertFrameworkRepo(framework, "docs:artifacts", app)).toThrow(new RegExp(framework));
|
|
55
|
-
} finally {
|
|
56
|
-
rmSync(framework, { recursive: !0, force: !0 });
|
|
57
|
-
rmSync(app, { recursive: !0, force: !0 });
|
|
58
|
-
}
|
|
59
|
-
});
|
|
60
|
-
it("refuses when the root is not a framework tree at all", () => {
|
|
61
|
-
const dir = mkdtempSync(join(tmpdir(), "stacks-app-"));
|
|
62
|
-
try {
|
|
63
|
-
expect(() => assertFrameworkRepo(dir, "docs:links", dir)).toThrow();
|
|
64
|
-
} finally {
|
|
65
|
-
rmSync(dir, { recursive: !0, force: !0 });
|
|
66
|
-
}
|
|
67
|
-
});
|
|
68
|
-
});
|
|
1
|
+
import{describe,expect,it}from"bun:test";import{mkdirSync,mkdtempSync,rmSync}from"node:fs";import{tmpdir}from"node:os";import{join,resolve}from"node:path";import{assertFrameworkRepo,isInsideRoot,looksLikeFrameworkRepo}from"./framework-repo";describe("isInsideRoot",()=>{it("accepts the root itself and anything under it",()=>{expect(isInsideRoot("/a/b","/a/b")).toBe(!0);expect(isInsideRoot("/a/b","/a/b/c/d")).toBe(!0)});it("rejects a sibling and a parent",()=>{expect(isInsideRoot("/a/b","/a/c")).toBe(!1);expect(isInsideRoot("/a/b","/a")).toBe(!1)});it("rejects another project entirely",()=>{expect(isInsideRoot("/Users/x/Code/stacks","/Users/x/Code/Apps/reviewos.org")).toBe(!1)})});describe("looksLikeFrameworkRepo",()=>{it("recognises a tree carrying the framework core",()=>{const dir=mkdtempSync(join(tmpdir(),"stacks-fw-"));try{mkdirSync(resolve(dir,"storage/framework/core/buddy"),{recursive:!0});expect(looksLikeFrameworkRepo(dir)).toBe(!0)}finally{rmSync(dir,{recursive:!0,force:!0})}});it("does not mistake an ordinary application for it",()=>{const dir=mkdtempSync(join(tmpdir(),"stacks-app-"));try{mkdirSync(resolve(dir,"app"),{recursive:!0});expect(looksLikeFrameworkRepo(dir)).toBe(!1)}finally{rmSync(dir,{recursive:!0,force:!0})}})});describe("assertFrameworkRepo",()=>{it("passes inside the framework repo",()=>{const dir=mkdtempSync(join(tmpdir(),"stacks-fw-"));try{mkdirSync(resolve(dir,"storage/framework/core/buddy"),{recursive:!0});expect(()=>assertFrameworkRepo(dir,"docs:artifacts",dir)).not.toThrow()}finally{rmSync(dir,{recursive:!0,force:!0})}});it("refuses when run from somewhere else, and says where it would have written",()=>{const framework=mkdtempSync(join(tmpdir(),"stacks-fw-")),app=mkdtempSync(join(tmpdir(),"stacks-app-"));try{mkdirSync(resolve(framework,"storage/framework/core/buddy"),{recursive:!0});expect(()=>assertFrameworkRepo(framework,"docs:artifacts",app)).toThrow(/docs:artifacts/);expect(()=>assertFrameworkRepo(framework,"docs:artifacts",app)).toThrow(new RegExp(framework))}finally{rmSync(framework,{recursive:!0,force:!0});rmSync(app,{recursive:!0,force:!0})}});it("refuses when the root is not a framework tree at all",()=>{const dir=mkdtempSync(join(tmpdir(),"stacks-app-"));try{expect(()=>assertFrameworkRepo(dir,"docs:links",dir)).toThrow()}finally{rmSync(dir,{recursive:!0,force:!0})}})});
|
|
@@ -1,71 +1,4 @@
|
|
|
1
|
-
import
|
|
2
|
-
|
|
3
|
-
import { assertFrameworkRepo } from "./framework-repo";
|
|
4
|
-
import { generateOpenApi } from "../../../../api/src/generate-openapi";
|
|
5
|
-
import { renderOpenApiTypes } from "../../../../api/src/generate-types";
|
|
6
|
-
const root = resolve(import.meta.dir, "../../../../../../.."), openApiPath = resolve(root, "storage/framework/api/openapi.json"), apiTypesPath = resolve(root, "storage/framework/api/api-types.ts");
|
|
7
|
-
export function validateOpenApi(document) {
|
|
8
|
-
const errors = [], paths = Object.entries(document.paths || {});
|
|
9
|
-
if (paths.length < 10)
|
|
10
|
-
errors.push("OpenAPI document has fewer than 10 registered paths");
|
|
11
|
-
const operationIds = new Set;
|
|
12
|
-
for (const [route, item] of paths)
|
|
13
|
-
for (const [method, operation] of Object.entries(item)) {
|
|
14
|
-
if (!operation.operationId)
|
|
15
|
-
errors.push(`${method.toUpperCase()} ${route}: operationId is missing`);
|
|
16
|
-
else if (operationIds.has(operation.operationId))
|
|
17
|
-
errors.push(`${method.toUpperCase()} ${route}: duplicate operationId ${operation.operationId}`);
|
|
18
|
-
else
|
|
19
|
-
operationIds.add(operation.operationId);
|
|
20
|
-
for (const parameter of operation.parameters || [])
|
|
21
|
-
if (parameter.in === "path" && !parameter.required)
|
|
22
|
-
errors.push(`${method.toUpperCase()} ${route}: path parameter ${parameter.name} must be required`);
|
|
23
|
-
}
|
|
24
|
-
return errors;
|
|
25
|
-
}
|
|
26
|
-
async function expectedArtifacts() {
|
|
27
|
-
const document = await generateOpenApi({ write: !1 }), errors = validateOpenApi(document);
|
|
28
|
-
if (errors.length)
|
|
29
|
-
throw Error(errors.join(`
|
|
30
|
-
`));
|
|
31
|
-
return {
|
|
32
|
-
openApi: JSON.stringify(document, null, 2),
|
|
33
|
-
apiTypes: renderOpenApiTypes(document)
|
|
34
|
-
};
|
|
35
|
-
}
|
|
36
|
-
async function write() {
|
|
37
|
-
const expected = await expectedArtifacts();
|
|
38
|
-
writeFileSync(openApiPath, expected.openApi);
|
|
39
|
-
writeFileSync(apiTypesPath, expected.apiTypes);
|
|
40
|
-
console.log("Generated OpenAPI and API type artifacts");
|
|
41
|
-
}
|
|
42
|
-
async function check() {
|
|
43
|
-
const expected = await expectedArtifacts(), errors = [];
|
|
44
|
-
if (readFileSync(openApiPath, "utf8") !== expected.openApi)
|
|
45
|
-
errors.push("storage/framework/api/openapi.json is stale");
|
|
46
|
-
if (readFileSync(apiTypesPath, "utf8") !== expected.apiTypes)
|
|
47
|
-
errors.push("storage/framework/api/api-types.ts is stale");
|
|
48
|
-
if (errors.length)
|
|
49
|
-
throw Error(`${errors.join(`
|
|
1
|
+
import{readFileSync,writeFileSync}from"node:fs";import{resolve}from"node:path";import{assertFrameworkRepo}from"./framework-repo";import{generateOpenApi}from"../../../../api/src/generate-openapi";import{renderOpenApiTypes}from"../../../../api/src/generate-types";const root=resolve(import.meta.dir,"../../../../../../.."),openApiPath=resolve(root,"storage/framework/api/openapi.json"),apiTypesPath=resolve(root,"storage/framework/api/api-types.ts");export function validateOpenApi(document){const errors=[],paths=Object.entries(document.paths||{});if(paths.length<10)errors.push("OpenAPI document has fewer than 10 registered paths");const operationIds=new Set;for(const[route,item]of paths)for(const[method,operation]of Object.entries(item)){if(!operation.operationId)errors.push(`${method.toUpperCase()} ${route}: operationId is missing`);else if(operationIds.has(operation.operationId))errors.push(`${method.toUpperCase()} ${route}: duplicate operationId ${operation.operationId}`);else operationIds.add(operation.operationId);for(const parameter of operation.parameters||[])if(parameter.in==="path"&&!parameter.required)errors.push(`${method.toUpperCase()} ${route}: path parameter ${parameter.name} must be required`)}return errors}async function expectedArtifacts(){const document=await generateOpenApi({write:!1}),errors=validateOpenApi(document);if(errors.length)throw Error(errors.join(`
|
|
2
|
+
`));return{openApi:JSON.stringify(document,null,2),apiTypes:renderOpenApiTypes(document)}}async function write(){const expected=await expectedArtifacts();writeFileSync(openApiPath,expected.openApi);writeFileSync(apiTypesPath,expected.apiTypes);console.log("Generated OpenAPI and API type artifacts")}async function check(){const expected=await expectedArtifacts(),errors=[];if(readFileSync(openApiPath,"utf8")!==expected.openApi)errors.push("storage/framework/api/openapi.json is stale");if(readFileSync(apiTypesPath,"utf8")!==expected.apiTypes)errors.push("storage/framework/api/api-types.ts is stale");if(errors.length)throw Error(`${errors.join(`
|
|
50
3
|
`)}
|
|
51
|
-
Run bun run docs:artifacts and review the generated diff.`);
|
|
52
|
-
console.log(`Generated API artifacts are current (${Object.keys(JSON.parse(expected.openApi).paths).length} paths)`);
|
|
53
|
-
}
|
|
54
|
-
export async function run() {
|
|
55
|
-
assertFrameworkRepo(root, "docs:artifacts");
|
|
56
|
-
try {
|
|
57
|
-
if (process.argv.includes("--write"))
|
|
58
|
-
await write();
|
|
59
|
-
else if (process.argv.includes("--check"))
|
|
60
|
-
await check();
|
|
61
|
-
else {
|
|
62
|
-
console.error("usage: bun storage/framework/core/buddy/src/commands/docs/generated-artifacts.ts --write | --check");
|
|
63
|
-
process.exit(2);
|
|
64
|
-
}
|
|
65
|
-
} catch (error) {
|
|
66
|
-
console.error(error instanceof Error ? error.message : String(error));
|
|
67
|
-
process.exit(1);
|
|
68
|
-
}
|
|
69
|
-
}
|
|
70
|
-
if (import.meta.main)
|
|
71
|
-
await run();
|
|
4
|
+
Run bun run docs:artifacts and review the generated diff.`);console.log(`Generated API artifacts are current (${Object.keys(JSON.parse(expected.openApi).paths).length} paths)`)}export async function run(){assertFrameworkRepo(root,"docs:artifacts");try{if(process.argv.includes("--write"))await write();else if(process.argv.includes("--check"))await check();else{console.error("usage: bun storage/framework/core/buddy/src/commands/docs/generated-artifacts.ts --write | --check");process.exit(2)}}catch(error){console.error(error instanceof Error?error.message:String(error));process.exit(1)}}if(import.meta.main)await run();
|
|
@@ -1,19 +1 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import { validateOpenApi } from "./generated-artifacts";
|
|
3
|
-
describe("generated API artifact validation", () => {
|
|
4
|
-
it("rejects empty, duplicate, and optional path evidence", () => {
|
|
5
|
-
expect(validateOpenApi({ paths: {} })).toContain("OpenAPI document has fewer than 10 registered paths");
|
|
6
|
-
const paths = {};
|
|
7
|
-
for (let index = 0;index < 10; index++)
|
|
8
|
-
paths[`/items/${index}/{id}`] = {
|
|
9
|
-
get: {
|
|
10
|
-
operationId: index < 2 ? "duplicate" : `get_${index}`,
|
|
11
|
-
parameters: [{ name: "id", in: "path", required: index !== 2, schema: { type: "string" } }],
|
|
12
|
-
responses: {}
|
|
13
|
-
}
|
|
14
|
-
};
|
|
15
|
-
const errors = validateOpenApi({ paths });
|
|
16
|
-
expect(errors).toContain("GET /items/1/{id}: duplicate operationId duplicate");
|
|
17
|
-
expect(errors).toContain("GET /items/2/{id}: path parameter id must be required");
|
|
18
|
-
});
|
|
19
|
-
});
|
|
1
|
+
import{describe,expect,it}from"bun:test";import{validateOpenApi}from"./generated-artifacts";describe("generated API artifact validation",()=>{it("rejects empty, duplicate, and optional path evidence",()=>{expect(validateOpenApi({paths:{}})).toContain("OpenAPI document has fewer than 10 registered paths");const paths={};for(let index=0;index<10;index++)paths[`/items/${index}/{id}`]={get:{operationId:index<2?"duplicate":`get_${index}`,parameters:[{name:"id",in:"path",required:index!==2,schema:{type:"string"}}],responses:{}}};const errors=validateOpenApi({paths});expect(errors).toContain("GET /items/1/{id}: duplicate operationId duplicate");expect(errors).toContain("GET /items/2/{id}: path parameter id must be required")})});
|
|
@@ -1,85 +1,2 @@
|
|
|
1
|
-
import
|
|
2
|
-
|
|
3
|
-
import { dirname, join, relative, resolve } from "node:path";
|
|
4
|
-
const root = resolve(import.meta.dir, "../../../../../../.."), docsDir = resolve(root, "docs"), INLINE_LINK = /\[[^\]]*\]\(([^)]+)\)/g;
|
|
5
|
-
export function isSkippableLink(target) {
|
|
6
|
-
return target === "" || target.startsWith("#") || /^[a-z][\w+.-]*:/i.test(target) || target.startsWith("//") || target.startsWith("{{") || target.includes("<");
|
|
7
|
-
}
|
|
8
|
-
export function extractLinks(content) {
|
|
9
|
-
const out = [], lines = content.replace(/<!--[\s\S]*?-->/g, (match) => match.replace(/[^\n]/g, " ")).split(`
|
|
10
|
-
`);
|
|
11
|
-
let inFence = !1;
|
|
12
|
-
for (let index = 0;index < lines.length; index++) {
|
|
13
|
-
const raw = lines[index];
|
|
14
|
-
if (/^\s*(```|~~~)/.test(raw)) {
|
|
15
|
-
inFence = !inFence;
|
|
16
|
-
continue;
|
|
17
|
-
}
|
|
18
|
-
if (inFence)
|
|
19
|
-
continue;
|
|
20
|
-
const line = raw.replace(/`[^`]*`/g, "");
|
|
21
|
-
for (const match of line.matchAll(INLINE_LINK)) {
|
|
22
|
-
let target = match[1].trim();
|
|
23
|
-
const space = target.search(/\s/);
|
|
24
|
-
if (space !== -1)
|
|
25
|
-
target = target.slice(0, space);
|
|
26
|
-
out.push({ target, line: index + 1 });
|
|
27
|
-
}
|
|
28
|
-
}
|
|
29
|
-
return out;
|
|
30
|
-
}
|
|
31
|
-
export function resolveCandidates(target, fileDir, docsRoot) {
|
|
32
|
-
const clean = target.split("#")[0].split("?")[0];
|
|
33
|
-
if (!clean)
|
|
34
|
-
return [];
|
|
35
|
-
const base = clean.startsWith("/") ? join(docsRoot, clean.slice(1)) : resolve(fileDir, clean), candidates = [base];
|
|
36
|
-
if (!/\.\w+$/.test(clean))
|
|
37
|
-
candidates.push(`${base}.md`, join(base, "index.md"));
|
|
38
|
-
else if (clean.endsWith(".html"))
|
|
39
|
-
candidates.push(base.replace(/\.html$/, ".md"), join(base.replace(/\.html$/, ""), "index.md"));
|
|
40
|
-
return candidates;
|
|
41
|
-
}
|
|
42
|
-
function walkMarkdown(dir) {
|
|
43
|
-
const files = [];
|
|
44
|
-
for (const entry of readdirSync(dir, { withFileTypes: !0 })) {
|
|
45
|
-
const full = join(dir, entry.name);
|
|
46
|
-
if (entry.isDirectory()) {
|
|
47
|
-
if (entry.name === "node_modules" || entry.name.startsWith("."))
|
|
48
|
-
continue;
|
|
49
|
-
files.push(...walkMarkdown(full));
|
|
50
|
-
} else if (entry.name.endsWith(".md"))
|
|
51
|
-
files.push(full);
|
|
52
|
-
}
|
|
53
|
-
return files;
|
|
54
|
-
}
|
|
55
|
-
export function checkDocsLinks(docsRoot = docsDir) {
|
|
56
|
-
const broken = [];
|
|
57
|
-
for (const file of walkMarkdown(docsRoot)) {
|
|
58
|
-
const content = readFileSync(file, "utf8");
|
|
59
|
-
for (const { target, line } of extractLinks(content)) {
|
|
60
|
-
if (isSkippableLink(target))
|
|
61
|
-
continue;
|
|
62
|
-
const candidates = resolveCandidates(target, dirname(file), docsRoot);
|
|
63
|
-
if (candidates.length === 0)
|
|
64
|
-
continue;
|
|
65
|
-
if (!candidates.some((candidate) => existsSync(candidate) && statSync(candidate).isFile()))
|
|
66
|
-
broken.push({ file: relative(docsRoot, file), line, target });
|
|
67
|
-
}
|
|
68
|
-
}
|
|
69
|
-
return broken;
|
|
70
|
-
}
|
|
71
|
-
export async function run() {
|
|
72
|
-
assertFrameworkRepo(root, "docs:links");
|
|
73
|
-
const broken = checkDocsLinks();
|
|
74
|
-
if (broken.length === 0)
|
|
75
|
-
console.log("\u2713 All internal documentation links resolve.");
|
|
76
|
-
else {
|
|
77
|
-
console.error(`\u2717 ${broken.length} broken internal documentation link(s):`);
|
|
78
|
-
for (const link of broken)
|
|
79
|
-
console.error(` ${link.file}:${link.line} -> ${link.target}`);
|
|
80
|
-
if (process.argv.includes("--check"))
|
|
81
|
-
process.exit(1);
|
|
82
|
-
}
|
|
83
|
-
}
|
|
84
|
-
if (import.meta.main)
|
|
85
|
-
await run();
|
|
1
|
+
import{assertFrameworkRepo}from"./framework-repo";import{existsSync,readdirSync,readFileSync,statSync}from"node:fs";import{dirname,join,relative,resolve}from"node:path";const root=resolve(import.meta.dir,"../../../../../../.."),docsDir=resolve(root,"docs"),INLINE_LINK=/\[[^\]]*\]\(([^)]+)\)/g;export function isSkippableLink(target){return target===""||target.startsWith("#")||/^[a-z][\w+.-]*:/i.test(target)||target.startsWith("//")||target.startsWith("{{")||target.includes("<")}export function extractLinks(content){const out=[],lines=content.replace(/<!--[\s\S]*?-->/g,(match)=>match.replace(/[^\n]/g," ")).split(`
|
|
2
|
+
`);let inFence=!1;for(let index=0;index<lines.length;index++){const raw=lines[index];if(/^\s*(```|~~~)/.test(raw)){inFence=!inFence;continue}if(inFence)continue;const line=raw.replace(/`[^`]*`/g,"");for(const match of line.matchAll(INLINE_LINK)){let target=match[1].trim();const space=target.search(/\s/);if(space!==-1)target=target.slice(0,space);out.push({target,line:index+1})}}return out}export function resolveCandidates(target,fileDir,docsRoot){const clean=target.split("#")[0].split("?")[0];if(!clean)return[];const base=clean.startsWith("/")?join(docsRoot,clean.slice(1)):resolve(fileDir,clean),candidates=[base];if(!/\.\w+$/.test(clean))candidates.push(`${base}.md`,join(base,"index.md"));else if(clean.endsWith(".html"))candidates.push(base.replace(/\.html$/,".md"),join(base.replace(/\.html$/,""),"index.md"));return candidates}function walkMarkdown(dir){const files=[];for(const entry of readdirSync(dir,{withFileTypes:!0})){const full=join(dir,entry.name);if(entry.isDirectory()){if(entry.name==="node_modules"||entry.name.startsWith("."))continue;files.push(...walkMarkdown(full))}else if(entry.name.endsWith(".md"))files.push(full)}return files}export function checkDocsLinks(docsRoot=docsDir){const broken=[];for(const file of walkMarkdown(docsRoot)){const content=readFileSync(file,"utf8");for(const{target,line}of extractLinks(content)){if(isSkippableLink(target))continue;const candidates=resolveCandidates(target,dirname(file),docsRoot);if(candidates.length===0)continue;if(!candidates.some((candidate)=>existsSync(candidate)&&statSync(candidate).isFile()))broken.push({file:relative(docsRoot,file),line,target})}}return broken}export async function run(){assertFrameworkRepo(root,"docs:links");const broken=checkDocsLinks();if(broken.length===0)console.log("\u2713 All internal documentation links resolve.");else{console.error(`\u2717 ${broken.length} broken internal documentation link(s):`);for(const link of broken)console.error(` ${link.file}:${link.line} -> ${link.target}`);if(process.argv.includes("--check"))process.exit(1)}}if(import.meta.main)await run();
|