stack/scripts/generate-setup-prompt-docs.ts
Bilal Godil a0644b82e1 Hexclave rename PR5: rename package dirs to drop stack- prefix (Step 16/A)
git mv packages/stack-shared->shared, stack-ui->ui, stack-sc->sc, stack-cli->cli,
and stack->next (the generated @hexclave/next pkg, regenerated via generate-sdks
dest change). Fixed deep-relative imports (pacifica/surface.tsx), tailwind content
glob, CI emulator paths, root cli script, generate-setup-prompt-docs + skills
relative imports, and comments. Reinstalled. build + typecheck + lint green (28/28).
2026-06-03 12:17:56 -07:00

498 lines
19 KiB
TypeScript

import path from "path";
import { readFileSync } from "fs";
import { aiSetupPrompt, cliSetupPrompt, convexSetupPrompt, getSdkSetupPrompt, supabaseSetupPrompt } from "../packages/shared/src/ai/unified-prompts/skill-site-prompt-parts/ai-setup-prompt";
import { deindent } from "../packages/shared/src/utils/strings";
import { writeFileSyncIfChanged } from "./utils";
import { remindersPrompt } from "../packages/shared/src/ai/unified-prompts/reminders";
import { buildLlmsFullTxt } from "../packages/shared/src/ai/llms/llms";
const generatedComment = "This file is auto-generated by scripts/generate-setup-prompt-docs.ts. Do not edit it manually; edit packages/shared/src/ai/unified-prompts/skill-site-prompt-parts/ai-setup-prompt.ts instead.";
type SdkSetupToolCategory = "frontend" | "backend" | "database" | "other";
type SdkSetupTool = {
label: string,
where: SdkSetupToolCategory[],
imageUrl: string,
monochromeLogo: boolean,
tabs: { label: string, mdContent: string }[],
extraFeatures: string[],
};
const repoRoot = path.resolve(__dirname, "..");
const docsJson = JSON.parse(readFileSync(path.join(repoRoot, "docs-mintlify/docs.json"), "utf-8"));
const setupPromptText = aiSetupPrompt;
const sdkSetupTools: Record<string, SdkSetupTool> = {
nextjs: {
label: "Next.js",
where: ["frontend", "backend"],
imageUrl: "/images/setup-tools/nextjs.svg",
monochromeLogo: true,
tabs: [{
label: "Next.js",
mdContent: getSdkSetupPrompt("nextjs"),
}],
extraFeatures: [],
},
react: {
label: "React",
where: ["frontend"],
imageUrl: "/images/setup-tools/react.svg",
monochromeLogo: false,
tabs: [{
label: "React",
mdContent: getSdkSetupPrompt("react"),
}],
extraFeatures: [],
},
js: {
label: "Other JS/TS",
where: ["frontend"],
imageUrl: "/images/setup-tools/javascript.svg",
monochromeLogo: false,
tabs: [{
label: "JS/TS",
mdContent: getSdkSetupPrompt("js"),
}],
extraFeatures: [],
},
"tanstack-start": {
label: "Tanstack Start",
where: ["frontend"],
imageUrl: "/images/setup-tools/tanstack.svg",
monochromeLogo: true,
tabs: [{
label: "Tanstack Start",
mdContent: getSdkSetupPrompt("tanstack-start"),
}],
extraFeatures: ["tanstack-query"],
},
"tanstack-query": {
label: "Tanstack Query",
where: ["frontend"],
imageUrl: "/images/setup-tools/tanstack.svg",
monochromeLogo: true,
tabs: [],
extraFeatures: ["tanstack-query"],
},
nodejs: {
label: "Node.js",
where: ["backend"],
imageUrl: "/images/setup-tools/nodejs.svg",
monochromeLogo: false,
tabs: [{
label: "Node.js",
mdContent: getSdkSetupPrompt("nodejs"),
}],
extraFeatures: [],
},
bun: {
label: "Bun",
where: ["backend"],
imageUrl: "/images/setup-tools/bun.svg",
monochromeLogo: false,
tabs: [{
label: "Bun",
mdContent: getSdkSetupPrompt("bun"),
}],
extraFeatures: [],
},
convex: {
label: "Convex",
where: ["backend", "database"],
imageUrl: "/images/setup-tools/convex.svg",
monochromeLogo: false,
tabs: [{
label: "Convex",
mdContent: convexSetupPrompt,
}],
extraFeatures: [],
},
supabase: {
label: "Supabase",
where: ["database"],
imageUrl: "/images/setup-tools/supabase.svg",
monochromeLogo: false,
tabs: [{
label: "Supabase",
mdContent: supabaseSetupPrompt,
}],
extraFeatures: [],
},
cli: {
label: "CLI",
where: ["other"],
imageUrl: "/images/setup-tools/cli.svg",
monochromeLogo: true,
tabs: [{
label: "CLI",
mdContent: cliSetupPrompt,
}],
extraFeatures: [],
},
/*mcp: {
label: "MCP",
where: ["other"],
imageUrl: "/images/setup-tools/mcp.svg",
monochromeLogo: true,
tabs: [{
label: "MCP",
mdContent: mcpSetupPrompt,
}],
extraFeatures: [],
},*/
};
function slugify(value: string) {
return value.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-|-$/g, "");
}
const categoryLabels = new Map<SdkSetupToolCategory, string>([
["frontend", "Frontend"],
["backend", "Backend"],
["database", "Database"],
["other", "Other"],
]);
const setupToolIds = Object.keys(sdkSetupTools);
const setupTabs = Object.entries(sdkSetupTools).flatMap(([toolId, tool]) => {
return tool.tabs.map((tab, tabIndex) => ({
id: `${toolId}-${slugify(tab.label)}-${tabIndex}`,
toolLabel: tool.label,
toolId,
tabLabel: tab.label,
mdContent: tab.mdContent,
}));
});
const setupTabMetadata = setupTabs.map((tab) => ({
toolId: tab.toolId,
title: tab.tabLabel,
}));
const unifiedAiPromptTabTitle = "Unified AI Prompt";
function renderToolCards(category: SdkSetupToolCategory) {
const tools = Object.entries(sdkSetupTools).filter(([, tool]) => tool.where.includes(category));
return tools.map(([toolId, tool]) => {
const hasTabs = tool.tabs.length > 0;
const iconMarkup = tool.monochromeLogo
? deindent`
<span
aria-hidden="true"
className="h-8 w-8 bg-black opacity-80 transition-opacity duration-150 group-hover:transition-none group-hover:opacity-90 dark:bg-white dark:opacity-95"
style={{
WebkitMask: "url(${tool.imageUrl}) center / contain no-repeat",
mask: "url(${tool.imageUrl}) center / contain no-repeat",
}}
/>
`
: deindent`
<img
src="${tool.imageUrl}"
alt=""
aria-hidden="true"
className="h-8 w-8 object-contain opacity-90 transition-opacity duration-150 group-hover:transition-none group-hover:opacity-100"
/>
`;
return deindent`
<button
type="button"
aria-pressed="false"
data-setup-tool-card="true"
data-tool-id="${toolId}"
data-tool-label="${tool.label}"
data-tool-has-tabs="${hasTabs ? "true" : "false"}"
data-tool-extra-features="${tool.extraFeatures.join(",")}"
onClick={onSetupToolClick}
className="group flex flex-col items-center gap-1 rounded-xl px-1 py-1 text-center transition-colors duration-150 hover:transition-none focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-violet-500 aria-pressed:bg-white/60 aria-pressed:ring-2 aria-pressed:ring-[#6b5df7] dark:aria-pressed:bg-white/10"
title="${tool.label}"
>
<div className="relative flex h-16 w-16 items-center justify-center overflow-hidden rounded-2xl border border-[#b8cff7] bg-gradient-to-b from-[#f2f7ff] via-[#ebf2ff] to-[#e4edff] shadow-[inset_0_1px_0_rgba(255,255,255,0.95),0_7px_18px_rgba(43,76,140,0.2)] transition-[border-color,box-shadow,transform] duration-150 group-hover:transition-none group-hover:border-[#78a8f0] group-hover:shadow-[inset_0_1px_0_rgba(255,255,255,1),0_0_20px_rgba(82,138,234,0.38),0_10px_22px_rgba(43,76,140,0.24)] dark:border-[#2c4c7d]/70 dark:from-[#183155] dark:via-[#112542] dark:to-[#0a1830] dark:shadow-[inset_0_1px_0_rgba(160,200,255,0.24),0_8px_24px_rgba(2,8,20,0.62)] dark:group-hover:border-[#4f84d7] dark:group-hover:shadow-[inset_0_1px_0_rgba(188,218,255,0.42),0_0_26px_rgba(77,138,239,0.5),0_12px_30px_rgba(2,8,20,0.72)]">
${iconMarkup}
<span className="absolute right-2 top-2 hidden h-5 min-w-5 items-center justify-center rounded-full bg-[#6b5df7] px-1 text-[9px] font-bold uppercase tracking-tight text-white group-aria-pressed:flex">On</span>
</div>
<span
className="min-h-8 max-w-20 text-center text-xs font-medium leading-4 text-[#2e446f] transition-colors duration-150 group-hover:transition-none group-hover:text-[#182b50] dark:text-[#d8e7ff] dark:group-hover:text-white"
title="${tool.label}"
>
${tool.label}
</span>
</button>
`;
}).join("\n");
}
function renderToolCategory(category: SdkSetupToolCategory) {
return deindent`
<section className="grid gap-3 sm:grid-cols-[6rem_1fr] sm:items-start">
<h3 className="pt-1 text-sm font-semibold text-[#2e446f] dark:text-[#d8e7ff]">${categoryLabels.get(category)}</h3>
<div className="grid grid-cols-3 gap-x-2 gap-y-3 sm:grid-cols-4 sm:gap-x-3 sm:gap-y-3 lg:grid-cols-6">
${renderToolCards(category)}
</div>
</section>
`;
}
function renderMarkdownInTabPanel(mdContent: string) {
return mdContent.replace(/<Note>\n([\s\S]*?)\n\s*<\/Note>/g, (_match, noteContent: string) => {
const blockquoteContent = deindent(noteContent)
.split("\n")
.map((line) => line.length === 0 ? ">" : `> ${line}`)
.join("\n");
return blockquoteContent;
});
}
function renderTabPanels() {
return setupTabs.map((tab) => deindent`
<Tab title="${tab.tabLabel}">
<div className="not-prose mb-1 flex justify-end">
<button
type="button"
onClick={copyGeneratedSetupPrompt}
className="inline-flex items-center justify-center rounded-md border border-[#9fb5e4] bg-[#eaf1ff] px-2 py-1 text-[11px] font-semibold leading-none text-[#2a4272] transition-colors duration-150 hover:transition-none hover:bg-[#dde8ff] focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-violet-500 focus-visible:ring-offset-2 dark:border-[#3d5a91] dark:bg-[#12213d] dark:text-[#d5e6ff] dark:hover:bg-[#1a2e51]"
>
Copy prompt
</button>
</div>
${renderMarkdownInTabPanel(tab.mdContent)}
</Tab>
`).join("\n\n");
}
function renderUnifiedAiPromptTab() {
return deindent`
<Tab title="${unifiedAiPromptTabTitle}">
Setting up with AI? Use this single prompt in your coding agent to set up Hexclave for your selected stack.
<div className="not-prose relative mt-3">
<pre className="max-h-40 overflow-auto whitespace-pre-wrap rounded-2xl border border-[#cdd7f4] bg-white/75 px-4 py-3 pr-32 font-mono text-xs leading-6 text-zinc-700 backdrop-blur-sm sm:text-sm dark:border-[#33476d] dark:bg-black/20 dark:text-zinc-200"><code>{generatedSetupPromptText}</code></pre>
<button
type="button"
onClick={copyGeneratedSetupPrompt}
className="absolute right-2 top-2 inline-flex items-center justify-center rounded-lg border border-[#9fb5e4] bg-[#eaf1ff] px-3 py-1.5 text-xs font-semibold text-[#2a4272] transition-colors duration-150 hover:transition-none hover:bg-[#dde8ff] focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-violet-500 focus-visible:ring-offset-2 dark:border-[#3d5a91] dark:bg-[#12213d] dark:text-[#d5e6ff] dark:hover:bg-[#1a2e51]"
>
Copy prompt
</button>
</div>
</Tab>
`;
}
writeFileSyncIfChanged(
path.join(repoRoot, "packages/shared/src/ai/unified-prompts/skill-site-prompt-parts/docs-json.generated.ts"),
deindent`
// This file is generated from docs-mintlify/docs.json.
const docsJson = ${JSON.stringify(docsJson, null, 2)} as const;
export default docsJson;
` + "\n",
);
writeFileSyncIfChanged(
path.join(repoRoot, "docs-mintlify/snippets/home-prompt-island.jsx"),
deindent`
// ${generatedComment}
export const generatedSetupPromptText = ${JSON.stringify(setupPromptText)};
export const setupToolIds = ${JSON.stringify(setupToolIds)};
export const setupTabMetadata = ${JSON.stringify(setupTabMetadata)};
export const unifiedAiPromptTabTitle = ${JSON.stringify(unifiedAiPromptTabTitle)};
export const GeneratedSetupPromptText = ({ className }) => (
<textarea
readOnly
aria-label="Generated setup prompt"
value={generatedSetupPromptText}
className={className}
/>
);
` + "\n",
);
writeFileSyncIfChanged(
path.join(repoRoot, "docs-mintlify/guides/getting-started/setup.mdx"),
deindent`
---
title: Setup
description: Install and configure Hexclave for your project
sidebarTitle: Setup
---
{/* ${generatedComment} */}
export const generatedSetupPromptText = ${JSON.stringify(setupPromptText)};
export const setupToolIds = ${JSON.stringify(setupToolIds)};
export const setupTabMetadata = ${JSON.stringify(setupTabMetadata)};
import { HexclaveAgentReminders } from "/snippets/hexclave-agent-reminders.jsx";
export const unifiedAiPromptTabTitle = ${JSON.stringify(unifiedAiPromptTabTitle)};
export const copyGeneratedSetupPrompt = async (event) => {
const button = event.currentTarget;
try {
await navigator.clipboard.writeText(generatedSetupPromptText);
button.textContent = "Copied";
} catch {
button.textContent = "Copy failed";
}
window.setTimeout(() => {
button.textContent = "Copy prompt";
}, 1300);
};
export const getSelectedSetupToolIdsFromUrl = () => {
if (typeof window === "undefined") {
return [];
}
const selectedToolIds = new Set(setupToolIds);
return (new URLSearchParams(window.location.search).get("tools") ?? "")
.split(",")
.map((toolId) => toolId.trim())
.filter((toolId) => selectedToolIds.has(toolId));
};
export const writeSelectedSetupToolIdsToUrl = (selectedToolIds) => {
if (typeof window === "undefined") {
return;
}
const url = new URL(window.location.href);
const orderedSelectedToolIds = setupToolIds.filter((toolId) => selectedToolIds.has(toolId));
if (orderedSelectedToolIds.length === 0) {
url.searchParams.delete("tools");
} else {
url.searchParams.set("tools", orderedSelectedToolIds.join(","));
}
window.history.replaceState(null, "", url.pathname + url.search + url.hash);
};
export const updateSetupBuilder = (root, syncUrl = true) => {
const selectedToolIds = new Set(
Array.from(root.querySelectorAll("[data-setup-tool-card='true'][aria-pressed='true']"))
.map((card) => card.getAttribute("data-tool-id"))
.filter((toolId) => toolId != null)
);
if (syncUrl) {
writeSelectedSetupToolIdsToUrl(selectedToolIds);
}
const visibleTabTitles = new Set(setupTabMetadata
.filter((tab) => selectedToolIds.has(tab.toolId))
.map((tab) => tab.title)
);
if (visibleTabTitles.size > 0) {
visibleTabTitles.add(unifiedAiPromptTabTitle);
}
const tabsRoot = root.querySelector("[data-setup-tabs-root='true']");
const emptyState = root.querySelector("[data-setup-tabs-empty='true']");
if (emptyState != null) {
emptyState.hidden = visibleTabTitles.size > 0;
emptyState.style.display = visibleTabTitles.size > 0 ? "none" : "";
}
if (tabsRoot == null) {
return;
}
tabsRoot.hidden = visibleTabTitles.size === 0;
tabsRoot.style.display = visibleTabTitles.size === 0 ? "none" : "";
const tabButtons = Array.from(tabsRoot.querySelectorAll("[role='tab']"));
let firstVisibleTabButton = null;
let selectedVisibleTabButton = null;
for (const tabButton of tabButtons) {
const title = tabButton.textContent?.trim() ?? "";
const shouldShow = visibleTabTitles.has(title);
tabButton.hidden = !shouldShow;
tabButton.style.display = shouldShow ? "" : "none";
if (shouldShow && firstVisibleTabButton == null) {
firstVisibleTabButton = tabButton;
}
if (shouldShow && tabButton.getAttribute("aria-selected") === "true") {
selectedVisibleTabButton = tabButton;
}
}
if (visibleTabTitles.size > 0 && selectedVisibleTabButton == null) {
firstVisibleTabButton?.click();
}
};
export const initializeSetupBuilder = (node) => {
if (node == null || node.dataset.setupBuilderInitialized === "true") {
return;
}
node.dataset.setupBuilderInitialized = "true";
const selectedToolIds = new Set(getSelectedSetupToolIdsFromUrl());
for (const toolCard of node.querySelectorAll("[data-setup-tool-card='true']")) {
toolCard.setAttribute("aria-pressed", selectedToolIds.has(toolCard.getAttribute("data-tool-id")) ? "true" : "false");
}
updateSetupBuilder(node, false);
};
export const onSetupToolClick = (event) => {
const button = event.currentTarget;
const root = button.closest("[data-setup-builder='true']");
if (root == null) {
return;
}
button.setAttribute("aria-pressed", button.getAttribute("aria-pressed") === "true" ? "false" : "true");
updateSetupBuilder(root);
};
<HexclaveAgentReminders />
<Note>
<p className="font-semibold">Setting up with AI? Use this single prompt:</p>
<div className="not-prose relative mt-3">
<pre className="max-h-40 overflow-auto whitespace-pre-wrap rounded-2xl border border-[#cdd7f4] bg-white/75 px-4 py-3 pr-32 font-mono text-xs leading-6 text-zinc-700 backdrop-blur-sm sm:text-sm dark:border-[#33476d] dark:bg-black/20 dark:text-zinc-200"><code>{generatedSetupPromptText}</code></pre>
<button
type="button"
onClick={copyGeneratedSetupPrompt}
className="absolute right-2 top-2 inline-flex items-center justify-center rounded-lg border border-[#9fb5e4] bg-[#eaf1ff] px-3 py-1.5 text-xs font-semibold text-[#2a4272] transition-colors duration-150 hover:transition-none hover:bg-[#dde8ff] focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-violet-500 focus-visible:ring-offset-2 focus-visible:ring-offset-[#f3f6ff] dark:border-[#3d5a91] dark:bg-[#12213d] dark:text-[#d5e6ff] dark:hover:bg-[#1a2e51] dark:focus-visible:ring-offset-[#0f1a2e]"
>
Copy prompt
</button>
</div>
</Note>
<div ref={initializeSetupBuilder} data-setup-builder="true" className="mt-10">
<div>
<h2 className="text-3xl font-bold tracking-tight text-slate-900 dark:text-white">Choose your tech stack</h2>
<p className="mt-2 text-sm font-medium text-slate-500 dark:text-slate-400">Choose all that apply.</p>
</div>
<div className="not-prose mt-5 space-y-4 rounded-2xl border border-[#d6e4ff] bg-gradient-to-b from-[#f7faff] to-[#eaf2ff] p-3 shadow-[inset_0_1px_0_rgba(255,255,255,0.9),0_10px_30px_-24px_rgba(47,79,140,0.35)] dark:border-[#1f2d45] dark:from-[#11203a] dark:to-[#070f1f] dark:shadow-[inset_0_1px_0_rgba(112,152,224,0.18),0_16px_34px_-24px_rgba(2,8,20,0.85)] sm:p-4">
${renderToolCategory("frontend")}
${renderToolCategory("backend")}
${renderToolCategory("database")}
${renderToolCategory("other")}
</div>
<div className="mt-8">
<p data-setup-tabs-empty="true" className="not-prose rounded-2xl border border-[#c5d7f6] bg-white/65 p-5 text-sm text-[#4a5f89] dark:border-[#2c4c7d] dark:bg-[#0c1627]/45 dark:text-[#8fa4cc]">
Select a tool to show setup instructions.
</p>
<div data-setup-tabs-root="true" hidden style={{ display: "none" }}>
<Tabs>
${renderUnifiedAiPromptTab()}
${renderTabPanels()}
</Tabs>
</div>
</div>
</div>
` + "\n",
);
writeFileSyncIfChanged(
path.join(repoRoot, "docs-mintlify/snippets/hexclave-agent-reminders.jsx"),
deindent`
export const HexclaveAgentReminders = () => (
<pre>{${JSON.stringify(remindersPrompt)}}</pre>
);
` + "\n",
);
writeFileSyncIfChanged(
path.join(repoRoot, "docs-mintlify/llms-full.txt"),
buildLlmsFullTxt(docsJson).replace(/[ \t]+$/gm, "") + "\n",
);