Files

135 lines
5.6 KiB
TypeScript

import { readdir, readFile } from "node:fs/promises";
import { extname, join, relative, sep } from "node:path";
import { shots } from "./shots/index.ts";
import { REPO_ROOT } from "./surfaces.ts";
const DOCS_ROOT = join(REPO_ROOT, "packages/docs");
const IMAGES_ROOT = join(DOCS_ROOT, "images");
const SCENE_PREFIX = "packages/docs/images/";
const IMAGE_EXTENSIONS = new Set([".avif", ".gif", ".jpeg", ".jpg", ".png", ".svg", ".webp"]);
/** Images maintained outside docs-shots, reviewed explicitly rather than silently ignored. */
const ALLOWLIST = new Set([
"10thMarch-better-mcp-auth.gif",
"10thMarch-model-picker.gif",
"anthropic-key.png",
"anthropic-openwork-key.png",
"chatgpt-connect-provider-settings.png",
"chatgpt-model-list.png",
"chatgpt-model-selector.png",
"chatgpt-new-session-cta.png",
"chatgpt-openai-auth-login.png",
"cloud-custom-llm-provider-detail.png",
"cloud-dashboard-overview.png",
"cloud-managed-llm-provider-detail.png",
"cloud-mcp-connections-admin.png",
"cloud-members-and-rbac-dashboard.png",
"cloud-org-settings.png",
"cloud-skill-hub-access-and-skills.png",
"cloud-team-quickstart/01-signin-email.png",
"cloud-team-quickstart/02-create-account.png",
"cloud-team-quickstart/03-name-your-team.png",
"cloud-team-quickstart/04-onboarding-checklist.png",
"cloud-team-quickstart/05-invite-member-form.png",
"cloud-team-quickstart/06-members-pending.png",
"cloud-team-quickstart/07-join-org-amy.png",
"cloud-team-quickstart/08-amy-welcome.png",
"cloud-team-quickstart/09-create-team-sales.png",
"cloud-team-quickstart/10-teams-list.png",
"cloud-team-quickstart/11-plugins-overview.png",
"cloud-team-quickstart/12-new-marketplace-dialog.png",
"cloud-team-quickstart/13-create-plugin-top.png",
"cloud-team-quickstart/14-create-plugin-skill-share.png",
"cloud-team-quickstart/15-plugin-detail.png",
"cloud-team-quickstart/16-security-check.png",
"cloud-team-quickstart/17-marketplace-team-access.png",
"cloud-team-quickstart/18-desktop-welcome.png",
"cloud-team-quickstart/19-desktop-choose-org.png",
"cloud-team-quickstart/20-desktop-org-resources.png",
"cloud-team-quickstart/21-desktop-account-connected.png",
"cloud-team-quickstart/22-desktop-marketplace-plugin.png",
"cloud-team-quickstart/23-desktop-prompt.png",
"cloud-team-quickstart/24-desktop-skill-result.png",
"control-chrome-setup-modal.png",
"deprecated/CleanShot2026-03-25at12.03.06@2x.png",
"deprecated/CleanShot2026-03-25at12.11.56@2x.png",
"deprecated/CleanShot2026-03-25at12.23.47@2x.png",
"deprecated/den-landing-page.png",
"deprecated/google-auth.png",
"deprecated/image-6.png",
"desktop-org-mcp-chat.png",
"desktop-org-mcp-connected.png",
"desktop-org-mcp-marketplace.png",
"exa-search-toggle.png",
"extensions-control-chrome-app.png",
"get-started-add-remote-workspace.png",
"get-started-cli-output.png",
"improved-skills-march11th.gif",
"infron-model-active.png",
"infron-model-picker.png",
"mcp-dynamic-registration-error.png",
"mcp-oauth-approval-page.png",
"ollama-added-as-provider.png",
"ollama-custom-provider.png",
"on-prem-branding/brand-assets-saved-in-den.png",
"on-prem-branding/windows-taskbar-and-alt-tab.png",
"openwork-providers.png",
"sharing-create-link-dialog.png",
"sharing-skill-share-page.png",
"sharing-skills-list.png",
"sharing-workspace-template-dialog.png",
"skill-import-select-workspace.png",
"skill-import-share-page.png",
"skill-import-share-your-skill.png",
"slack-connect-tokens.png",
"slack-test-message-thread.png",
]);
function portable(path: string): string {
return path.split(sep).join("/");
}
async function filesUnder(root: string): Promise<string[]> {
const files: string[] = [];
for (const entry of await readdir(root, { withFileTypes: true })) {
const path = join(root, entry.name);
if (entry.isDirectory()) files.push(...await filesUnder(path));
else if (entry.isFile()) files.push(path);
}
return files;
}
const mdxFiles = (await filesUnder(DOCS_ROOT)).filter((path) => extname(path) === ".mdx");
const mdx = (await Promise.all(mdxFiles.map((path) => readFile(path, "utf8")))).join("\n");
const images = new Set(
(await filesUnder(IMAGES_ROOT))
.filter((path) => IMAGE_EXTENSIONS.has(extname(path).toLocaleLowerCase()))
.map((path) => portable(relative(IMAGES_ROOT, path))),
);
const outputs = shots.map((shot) => shot.out.startsWith(SCENE_PREFIX) ? shot.out.slice(SCENE_PREFIX.length) : shot.out);
const outputSet = new Set(outputs);
const failures: string[] = [];
for (const shot of shots) {
if (!shot.out.startsWith(SCENE_PREFIX)) failures.push(`${shot.id}: output is outside packages/docs/images: ${shot.out}`);
}
for (const output of outputs) {
if (outputs.filter((candidate) => candidate === output).length > 1) failures.push(`duplicate scene output: ${output}`);
if (!images.has(output)) failures.push(`scene output does not exist: ${output}`);
if (!mdx.includes(`/images/${output}`)) failures.push(`scene output is not referenced by an mdx: ${output}`);
if (ALLOWLIST.has(output)) failures.push(`scene output is also allowlisted: ${output}`);
}
for (const image of images) {
if (!outputSet.has(image) && !ALLOWLIST.has(image)) failures.push(`image has no scene or allowlist entry: ${image}`);
}
for (const image of ALLOWLIST) {
if (!images.has(image)) failures.push(`allowlist entry does not exist: ${image}`);
}
if (failures.length > 0) {
console.error(`[docs-shots] manifest failed:\n- ${failures.join("\n- ")}`);
process.exitCode = 1;
} else {
console.log(`[docs-shots] manifest ok: ${shots.length} scene outputs, ${ALLOWLIST.size} allowlisted images.`);
}