stack/scripts/generate-setup-prompt-docs.ts
BilalG1 c14a9dd3d0
feat(hexclave): PR 5 — internal symbol/path/package renames + brand strings (#1547)
## Stack Auth → Hexclave rename — PR 5 (internal symbols, paths,
packages, brand strings)

PR 5 finishes the **internal / non-wire** half of the Stack→Hexclave
rename. It only touches things where nothing outside the repo depends on
the exact name: internal symbols, file/dir names, the
`@stackframe/template` package, and residual brand strings. Plan +
progress are in `HEXCLAVE-RENAME-PR5-PLAN.md`.

Every step was verified green (`pnpm typecheck` + `pnpm lint`, 28/28)
and committed as its own checkpoint, then a fan-out of review agents
audited all commits and the findings were fixed.

### What changed
- **Internal symbols** (`@hexclave/shared`, `packages/template`, apps):
`stack*`/`Stack*` → `hexclave*`/`Hexclave*` — incl.
`stackGlobalsSymbol`, the `_Stack*AppImpl` classes,
`stackAppInternalsSymbol`, `StackContext`, `getStackStripe`, etc. The
`stack*App` local-variable convention
(`stackServerApp`/`stackClientApp`/…) was renamed across 175
source/example/doc files.
- **File renames**: `hexclave-handler/provider/context.tsx`,
`backend/hexclave.tsx`, `internal-tool/hexclave.ts`,
`hexclave-app-internals.ts`.
- **Directory renames**: `lib/hexclave-app`, `hexclave-companion`,
`[...hexclave]` route segment, `skills/hexclave`,
`dashboard/src/hexclave`, and the package dirs
**`packages/{next,shared,ui,sc,cli}`** (dropping the `stack-` prefix to
match the `@hexclave/*` npm names).
- **Packages**: `@stackframe/template` → `@hexclave/template`; **deleted
`packages/init-stack`** (onboarding lives in `@hexclave/cli init`; the
published npm package is untouched).
- **Brand strings**: reworded `Stack Auth`/`Stack dashboard` prose in
code + docs-mintlify, renamed `hexclave-app.mdx`/`use-hexclave-app.mdx`
with redirects, regenerated OpenAPI, updated coupled e2e assertions;
`doctor`/`init` now prefer `hexclave.config.ts`.

### Intentionally kept (verified, not oversights)
Wire/compat identifiers (`x-stack-*` headers, `stack-*` cookies,
`STACK_*` env names, `*.stack-auth.com`, `stackauth_`, `ask_stack_auth`,
query params), public `Stack*` SDK aliases, crypto/JWT/vault
domain-separation tags, `*-brand-sentinel`s, the
`Symbol.for("StackAuth--…")` string, `_stack_sync_metadata`, Postgres
`stackframe` / docker image names, the `stack-auth-logo*.svg` (used by
the rebrand modal), and `migration.mdx` / "formerly known as Stack Auth"
notes. False positives (Phosphor `StackIcon`/`StackSimple`, `TanStack`,
`OrbStack`, `stackable`/`Stacked` charts) left alone.

### Review pass
Six review agents audited all commits. Found + fixed one real bug — a
build script (`bundle-type-definitions.ts`) hardcoded the old
`lib/stack-app` glob path (not an import, so typecheck/lint were blind),
silently emptying the dashboard AI type bundle — plus stale comments, a
dead CI env var, and stale `.gitignore`/`.dockerignore` entries.
Cross-cutting audit confirmed **zero wire-compat identifiers were
accidentally renamed**.

### ⚠️ Verification note
`typecheck` + `lint` are fully green locally. The **e2e suite was not
run** (needs a live backend+DB), so the brand-string assertion +
OpenAPI-regen changes are verified by grep/codegen only — please let CI
exercise e2e to confirm.

### Base-branch note
This branch was forked from the local-only `cl/friendly-lewin-72293f`
(not on origin, no separate PR), so this PR against `dev` also carries
that branch's ~11 preceding Hexclave-rename commits (config-file rename,
env-var dual-read, AI setup-prompt rebrand). If those should land
separately, re-parent before merge.

<!-- This is an auto-generated description by cubic. -->
---
## Summary by cubic
Finishes the internal Stack Auth → Hexclave rename and cleans up
remaining stragglers, including dev-tool and prompt copy. All changes
are internal-only; public/wire APIs remain unchanged. Re-merged `dev`
and resolved the payments create-purchase-url conflict.

- **Refactors**
- Internal symbols: stack*/Stack* → hexclave*/Hexclave* (e.g.,
`getHexclaveServerApp` via `@/hexclave`, `getHexclaveStripe`,
`hexclaveAppInternalsSymbol`, `hexclaveSchemaInfo`, Prisma
`__hexclave_*`, `data-hexclave-handler-page`, Stripe mock
`hexclavePortPrefix`).
- Files/dirs: moved to `lib/hexclave-app`; handler route
`[...hexclave]`; backend entry `src/hexclave.tsx`; dashboard internals
`hexclave-app-internals`; companion `hexclave-companion`; dropped
`stack-` prefix across package dirs
(`packages/{shared,ui,sc,cli,next}`); workflows/emulator paths now
`packages/cli`; Quetzal codegen env at `packages/next/.env.local`.
- Packages/docs: `@stackframe/template` → `@hexclave/template`; removed
`packages/init-stack`; regenerated OpenAPI and updated docs
slugs/redirects for hexclave-app/use-hexclave-app.
- Brand strings/prompts: reworded remaining “Stack” dashboard strings to
Hexclave; updated dev-tool copy and prompts; `doctor/init` now prefer
`hexclave.config.ts`. Kept all wire-compat identifiers and public
aliases (`x-stack-*`, `stack-*` cookies, `STACK_*` env,
`*.stack-auth.com`, `Stack*` SDK names).
- Rebased/merged onto latest `dev`: retained `@hexclave/template`, kept
`src` in published files, refreshed setup-prompt imports and docs JSON,
adopted 1.0.5 version bumps, and re-merged `dev` again (resolved
`create-purchase-url` with `getHexclaveStripe`).

- **Bug Fixes**
- Restored dashboard AI type bundle by pointing the glob to
`packages/template/src/lib/hexclave-app`.
- Addressed rename leftovers: updated lingering `@/stack` imports and
CSS selector, fixed schema/meta and port-prefix expansions, and aligned
emulator commands to `packages/cli`.
- CI/build: removed a dead env var and stale ignore entries; fixed
Docker by renaming `STACK_SKIP_TEMPLATE_GENERATION` →
`HEXCLAVE_SKIP_TEMPLATE_GENERATION`.

<sup>Written for commit 3c1af3bff3.
Summary will update on new commits.</sup>

<a
href="https://cubic.dev/pr/hexclave/hexclave/pull/1547?utm_source=github"
target="_blank" rel="noopener noreferrer"
data-no-image-dialog="true"><picture><source
media="(prefers-color-scheme: dark)"
srcset="https://cubic.dev/buttons/review-in-cubic-dark.svg"><source
media="(prefers-color-scheme: light)"
srcset="https://cubic.dev/buttons/review-in-cubic-light.svg"><img
alt="Review in cubic"
src="https://cubic.dev/buttons/review-in-cubic-dark.svg"></picture></a>

<!-- End of auto-generated description by cubic. -->
2026-06-03 18:57:09 -07:00

593 lines
23 KiB
TypeScript

import path from "path";
import { readFileSync } from "fs";
import { aiSetupPrompt, cliSetupPrompt, convexSetupPrompt, getSdkSetupPrompt, pythonBackendSetupPrompt, restApiBackendSetupPrompt, 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[],
filterGroups: ("js" | "python" | "other")[],
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"],
filterGroups: ["js"],
imageUrl: "/images/setup-tools/nextjs.svg",
monochromeLogo: true,
tabs: [{
label: "Next.js",
mdContent: getSdkSetupPrompt("nextjs"),
}],
extraFeatures: [],
},
react: {
label: "React",
where: ["frontend"],
filterGroups: ["js"],
imageUrl: "/images/setup-tools/react.svg",
monochromeLogo: false,
tabs: [{
label: "React",
mdContent: getSdkSetupPrompt("react"),
}],
extraFeatures: [],
},
js: {
label: "Other JS/TS",
where: ["frontend"],
filterGroups: ["js"],
imageUrl: "/images/setup-tools/javascript.svg",
monochromeLogo: false,
tabs: [{
label: "JS/TS",
mdContent: getSdkSetupPrompt("js"),
}],
extraFeatures: [],
},
"tanstack-start": {
label: "Tanstack Start",
where: ["frontend"],
filterGroups: ["js"],
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"],
filterGroups: ["js"],
imageUrl: "/images/setup-tools/tanstack.svg",
monochromeLogo: true,
tabs: [],
extraFeatures: ["tanstack-query"],
},
nodejs: {
label: "Node.js",
where: ["backend"],
filterGroups: ["js"],
imageUrl: "/images/setup-tools/nodejs.svg",
monochromeLogo: false,
tabs: [{
label: "Node.js",
mdContent: getSdkSetupPrompt("nodejs"),
}],
extraFeatures: [],
},
bun: {
label: "Bun",
where: ["backend"],
filterGroups: ["js"],
imageUrl: "/images/setup-tools/bun.svg",
monochromeLogo: false,
tabs: [{
label: "Bun",
mdContent: getSdkSetupPrompt("bun"),
}],
extraFeatures: [],
},
python: {
label: "Python",
where: ["backend"],
filterGroups: ["python"],
imageUrl: "/images/setup-tools/python.svg",
monochromeLogo: false,
tabs: [{
label: "Python",
mdContent: pythonBackendSetupPrompt,
}],
extraFeatures: [],
},
"rest-api": {
label: "Other (REST API)",
where: ["backend"],
filterGroups: ["other"],
imageUrl: "/images/setup-tools/cli.svg",
monochromeLogo: true,
tabs: [{
label: "Other (REST API)",
mdContent: restApiBackendSetupPrompt,
}],
extraFeatures: [],
},
convex: {
label: "Convex",
where: ["backend", "database"],
filterGroups: ["js"],
imageUrl: "/images/setup-tools/convex.svg",
monochromeLogo: false,
tabs: [{
label: "Convex",
mdContent: convexSetupPrompt,
}],
extraFeatures: [],
},
supabase: {
label: "Supabase",
where: ["database"],
filterGroups: ["other"],
imageUrl: "/images/setup-tools/supabase.svg",
monochromeLogo: false,
tabs: [{
label: "Supabase",
mdContent: supabaseSetupPrompt,
}],
extraFeatures: [],
},
cli: {
label: "CLI",
where: ["other"],
filterGroups: ["other"],
imageUrl: "/images/setup-tools/cli.svg",
monochromeLogo: true,
tabs: [{
label: "CLI",
mdContent: cliSetupPrompt,
}],
extraFeatures: [],
},
/*mcp: {
label: "MCP",
where: ["other"],
filterGroups: ["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(",")}"
data-tool-filter-groups="${tool.filterGroups.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 data-setup-tool-category="true" 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 renderSetupFilterButton(filterId: string, label: string) {
return deindent`
<button
type="button"
aria-pressed="true"
data-setup-filter-button="true"
data-filter-id="${filterId}"
onClick={onSetupFilterClick}
className="inline-flex items-center justify-center rounded-full px-2.5 py-1 text-xs font-medium text-[#526994] transition-colors duration-150 hover:transition-none hover:bg-white/45 hover:text-[#2a4272] focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-violet-500 aria-pressed:bg-white/70 aria-pressed:text-[#243d70] aria-pressed:shadow-sm dark:text-[#9eb3d8] dark:hover:bg-white/10 dark:hover:text-white dark:aria-pressed:bg-white/14 dark:aria-pressed:text-white"
>
${label}
</button>
`;
}
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 { hexclaveAgentRemindersText } 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 activeFilterIds = new Set(
Array.from(root.querySelectorAll("[data-setup-filter-button='true'][aria-pressed='true']"))
.map((button) => button.getAttribute("data-filter-id"))
.filter((filterId) => filterId != null)
);
for (const toolCard of root.querySelectorAll("[data-setup-tool-card='true']")) {
const filterGroups = (toolCard.getAttribute("data-tool-filter-groups") ?? "")
.split(",")
.filter((filterGroup) => filterGroup.length > 0);
const shouldShow = filterGroups.some((filterGroup) => activeFilterIds.has(filterGroup));
toolCard.hidden = !shouldShow;
toolCard.style.display = shouldShow ? "" : "none";
}
for (const toolCategory of root.querySelectorAll("[data-setup-tool-category='true']")) {
const hasVisibleCard = Array.from(toolCategory.querySelectorAll("[data-setup-tool-card='true']"))
.some((toolCard) => !toolCard.hidden);
toolCategory.hidden = !hasVisibleCard;
toolCategory.style.display = hasVisibleCard ? "" : "none";
}
const selectedToolIds = new Set(
Array.from(root.querySelectorAll("[data-setup-tool-card='true'][aria-pressed='true']"))
.filter((card) => !card.hidden)
.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);
};
export const onSetupFilterClick = (event) => {
const button = event.currentTarget;
const root = button.closest("[data-setup-builder='true']");
if (root == null) {
return;
}
const nextPressed = button.getAttribute("aria-pressed") !== "true";
const currentlyPressedFilters = root.querySelectorAll("[data-setup-filter-button='true'][aria-pressed='true']").length;
if (!nextPressed && currentlyPressedFilters <= 1) {
return;
}
button.setAttribute("aria-pressed", nextPressed ? "true" : "false");
updateSetupBuilder(root);
};
<Visibility for="agents">
{hexclaveAgentRemindersText}
</Visibility>
<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">
<div className="flex flex-col gap-2 border-b border-[#d6e4ff]/80 pb-3 dark:border-[#263a5f] sm:flex-row sm:items-center">
<span className="text-sm font-medium text-[#526994] dark:text-[#9eb3d8]">Filter:</span>
<div className="flex flex-wrap gap-2">
${renderSetupFilterButton("js", "JS")}
${renderSetupFilterButton("python", "Python")}
${renderSetupFilterButton("other", "Other")}
</div>
</div>
${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 hexclaveAgentRemindersText = ${JSON.stringify(remindersPrompt)};
` + "\n",
);
writeFileSyncIfChanged(
path.join(repoRoot, "docs-mintlify/llms-full.txt"),
buildLlmsFullTxt(docsJson).replace(/[ \t]+$/gm, "") + "\n",
);