mirror of
https://github.com/pbakaus/impeccable.git
synced 2026-09-12 06:06:37 +03:00
Serve _data/api JSON in dev server
app.js fetches /_data/api/commands.json and patterns.json, but those are build artifacts written into site/public/_data/ by scripts/build.js. The plain `astro dev` server never runs that build, so the homepage 404'd on both in dev. Extract generateApiData into scripts/lib/api-data.js (shared by the build and a new scripts/gen-dev-api.mjs prebuild), and run the prebuild before astro dev so `bun run dev` serves the same payloads as production. site/public/_data/ stays gitignored. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 4.8
parent
be83085dbd
commit
4fda4a0ece
+1
-1
@@ -47,7 +47,7 @@
|
||||
"build:extension": "node scripts/build-extension.js",
|
||||
"clean": "rm -rf dist build",
|
||||
"rebuild": "bun run clean && bun run build",
|
||||
"dev": "npx astro dev",
|
||||
"dev": "bun run scripts/gen-dev-api.mjs && npx astro dev",
|
||||
"preview": "bun run build && npx astro preview",
|
||||
"deploy": "bun run build && wrangler pages deploy build/",
|
||||
"test": "bun test tests/build.test.js tests/detect-antipatterns.test.js tests/windows-path-fix.test.js tests/lib/detector-bundle.test.js tests/lib/provider-blocks.test.js tests/lib/transformers/provider-blocks.test.js tests/lib/utils.test.js tests/lib/transformers/factory.test.js tests/lib/transformers/providers.test.js tests/skills-cli.test.js && node --test tests/critique-storage.test.mjs && node --test tests/detect-antipatterns-fixtures.test.mjs && node --test tests/detect-antipatterns-browser.test.mjs && node --test tests/cleanup-deprecated.test.mjs && node --test tests/impeccable-paths.test.mjs && node --test tests/live-wrap.test.mjs && node --test tests/live-wrap-buffer-aware.test.mjs && node --test tests/live-insert.test.mjs && node --test tests/live-insert-ui.test.mjs && node --test tests/live-event-validation.test.mjs && node --test tests/live-reference.test.mjs && node --test tests/live-e2e-agent-output.test.mjs && node --test tests/live-e2e-llm-agent.test.mjs && node --test tests/live-e2e-cli-options.test.mjs && node --test tests/live-accept.test.mjs && node --test tests/live-accept-scrub.test.mjs && node --test tests/live-commit-manual-edits.test.mjs && node --test tests/live-discard-manual-edits.test.mjs && node --test tests/live-manual-edits-buffer.test.mjs && node --test tests/live-inject.test.mjs && node --test tests/live-poll.test.mjs && node --test tests/live-poll-stream.test.mjs && node --test tests/live-server.test.mjs && node --test tests/live-copy-edit-agent.test.mjs && node --test tests/live-browser-regression.test.mjs && node --test tests/live-session-store.test.mjs && node --test tests/live-browser-session.test.mjs && node --test tests/live-browser-source.test.mjs && node --test tests/live-completion.test.mjs && node --test tests/live-recovery-commands.test.mjs && node --test tests/framework-fixtures.test.mjs",
|
||||
|
||||
+2
-92
@@ -19,6 +19,7 @@ import path from 'path';
|
||||
import fs from 'fs';
|
||||
import { fileURLToPath } from 'url';
|
||||
import { readSourceFiles, readPatterns, stashPerProjectArtifacts, restorePerProjectArtifacts } from './lib/utils.js';
|
||||
import { generateApiData } from './lib/api-data.js';
|
||||
import { createTransformer, PROVIDERS } from './lib/transformers/index.js';
|
||||
import { createAllZips } from './lib/zip.js';
|
||||
import { ANTIPATTERNS } from '../cli/engine/registry/antipatterns.mjs';
|
||||
@@ -459,97 +460,6 @@ These are hidden folders (dotfiles). Press Cmd+Shift+. in Finder to see them.
|
||||
console.log(`✓ Assembled universal directory (${providerConfigs.length} providers)`);
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate static API data for Cloudflare Pages deployment.
|
||||
* Pre-builds all API responses as JSON files so they can be served
|
||||
* as static assets via _redirects rewrites (no function invocations needed).
|
||||
*/
|
||||
function generateApiData(buildDir, skills, patterns) {
|
||||
const apiDir = path.join(buildDir, '_data', 'api');
|
||||
fs.mkdirSync(apiDir, { recursive: true });
|
||||
|
||||
// skills.json
|
||||
const skillsData = skills.map(s => ({
|
||||
id: path.basename(path.dirname(s.filePath)),
|
||||
name: s.name,
|
||||
description: s.description,
|
||||
userInvocable: s.userInvocable,
|
||||
}));
|
||||
fs.writeFileSync(path.join(apiDir, 'skills.json'), JSON.stringify(skillsData));
|
||||
|
||||
// commands.json - after v3.0 consolidation, commands are sub-commands of
|
||||
// /impeccable. Load them from command-metadata.json and include the root
|
||||
// impeccable skill itself so UI surfaces like the cheatsheet can list them.
|
||||
// Each entry also picks up a short `tagline` from its editorial file
|
||||
// (site/content/skills/<id>.md) when one exists. Taglines are used by UI
|
||||
// surfaces that need a human-friendly one-liner, while `description` stays
|
||||
// optimized for auto-trigger keyword matching in the AI harness.
|
||||
const readTagline = (id) => {
|
||||
const editorialPath = path.join(ROOT_DIR, 'site/content/skills', `${id}.md`);
|
||||
if (!fs.existsSync(editorialPath)) return null;
|
||||
const raw = fs.readFileSync(editorialPath, 'utf-8');
|
||||
const match = raw.match(/^---\n([\s\S]*?)\n---/);
|
||||
if (!match) return null;
|
||||
const taglineMatch = match[1].match(/tagline:\s*"([^"]+)"/);
|
||||
return taglineMatch ? taglineMatch[1] : null;
|
||||
};
|
||||
|
||||
const metadataPath = path.join(ROOT_DIR, 'skill/scripts/command-metadata.json');
|
||||
if (!fs.existsSync(metadataPath)) {
|
||||
throw new Error(`command-metadata.json is missing at ${metadataPath}. This file is required to generate the commands API.`);
|
||||
}
|
||||
const impeccable = skills.find(s => s.name === 'impeccable');
|
||||
if (!impeccable) {
|
||||
throw new Error('impeccable skill not found at skill/SKILL.src.md. The build system expects exactly one skill at that path.');
|
||||
}
|
||||
|
||||
const metadata = JSON.parse(fs.readFileSync(metadataPath, 'utf-8'));
|
||||
const commandsData = [
|
||||
{
|
||||
id: 'impeccable',
|
||||
name: 'impeccable',
|
||||
description: impeccable.description,
|
||||
tagline: readTagline('impeccable'),
|
||||
userInvocable: true,
|
||||
},
|
||||
...Object.entries(metadata).map(([id, meta]) => ({
|
||||
id,
|
||||
name: id,
|
||||
description: meta.description,
|
||||
tagline: readTagline(id),
|
||||
userInvocable: true,
|
||||
})),
|
||||
];
|
||||
fs.writeFileSync(path.join(apiDir, 'commands.json'), JSON.stringify(commandsData));
|
||||
|
||||
// patterns.json
|
||||
fs.writeFileSync(path.join(apiDir, 'patterns.json'), JSON.stringify(patterns));
|
||||
|
||||
// version.json - a tiny endpoint the installed skill polls on boot
|
||||
// (skill/scripts/context.mjs) to nudge users toward `npx impeccable skills
|
||||
// update`. Kept deliberately small so the boot-time check is cheap, unlike
|
||||
// the full bundle download `skills check` performs. The skills version is
|
||||
// the canonical one in the Claude plugin manifest.
|
||||
const pluginManifestPath = path.join(ROOT_DIR, '.claude-plugin/plugin.json');
|
||||
const skillsVersion = JSON.parse(fs.readFileSync(pluginManifestPath, 'utf-8')).version;
|
||||
fs.writeFileSync(path.join(apiDir, 'version.json'), JSON.stringify({ skills: skillsVersion }));
|
||||
|
||||
// command-source/{id}.json (one per skill)
|
||||
const cmdSourceDir = path.join(apiDir, 'command-source');
|
||||
fs.mkdirSync(cmdSourceDir, { recursive: true });
|
||||
for (const skill of skills) {
|
||||
const id = path.basename(path.dirname(skill.filePath));
|
||||
const content = fs.readFileSync(skill.filePath, 'utf-8');
|
||||
fs.writeFileSync(
|
||||
path.join(cmdSourceDir, `${id}.json`),
|
||||
JSON.stringify({ content })
|
||||
);
|
||||
}
|
||||
|
||||
const skillWord = skillsData.length === 1 ? 'skill' : 'skills';
|
||||
console.log(`✓ Generated static API data (${skillsData.length} ${skillWord}, ${commandsData.length} commands)`);
|
||||
}
|
||||
|
||||
/**
|
||||
* Copy dist files to build output for Cloudflare Pages Functions access.
|
||||
* Download functions use env.ASSETS.fetch() to read these files.
|
||||
@@ -703,7 +613,7 @@ async function build() {
|
||||
// Astro wipes build/ before writing, so anything written directly to build/
|
||||
// during build:skills would be destroyed when build:site runs.
|
||||
const publicDir = path.join(ROOT_DIR, 'site', 'public');
|
||||
generateApiData(publicDir, skills, patterns);
|
||||
generateApiData(publicDir, skills, patterns, ROOT_DIR);
|
||||
generateCFConfig(publicDir);
|
||||
|
||||
// Copy all provider outputs to project root for local testing.
|
||||
|
||||
@@ -0,0 +1,25 @@
|
||||
#!/usr/bin/env node
|
||||
|
||||
/**
|
||||
* Dev-only prebuild: write the `_data/api/*.json` payloads into site/public/
|
||||
* so `bun run dev` (astro dev) serves them. In production these are generated
|
||||
* by scripts/build.js; the plain `astro dev` server never runs that build, so
|
||||
* without this step app.js 404s on /_data/api/commands.json and patterns.json.
|
||||
*
|
||||
* site/public/_data/ is gitignored, so this only ever produces local artifacts.
|
||||
* Reuses the exact production generator (scripts/lib/api-data.js) so dev output
|
||||
* matches prod. Runs once at dev startup; editing command-metadata.json or the
|
||||
* pattern catalog mid-session needs a dev-server restart (same as other
|
||||
* source edits under skill/ and site/content/).
|
||||
*/
|
||||
|
||||
import path from 'path';
|
||||
import { fileURLToPath } from 'url';
|
||||
import { readSourceFiles, readPatterns } from './lib/utils.js';
|
||||
import { generateApiData } from './lib/api-data.js';
|
||||
|
||||
const ROOT_DIR = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..');
|
||||
|
||||
const { skills } = readSourceFiles(ROOT_DIR);
|
||||
const patterns = readPatterns(ROOT_DIR);
|
||||
generateApiData(path.join(ROOT_DIR, 'site', 'public'), skills, patterns, ROOT_DIR);
|
||||
@@ -0,0 +1,98 @@
|
||||
import path from 'path';
|
||||
import fs from 'fs';
|
||||
|
||||
/**
|
||||
* Generate static API data for Cloudflare Pages deployment.
|
||||
* Pre-builds all API responses as JSON files so they can be served
|
||||
* as static assets via _redirects rewrites (no function invocations needed).
|
||||
*
|
||||
* Shared by the production build (scripts/build.js, writing into site/public/)
|
||||
* and the dev prebuild (scripts/gen-dev-api.mjs) so `bun run dev` serves the
|
||||
* same payloads `app.js` fetches in production. `outDir` is the directory that
|
||||
* gets a `_data/api/` tree; `rootDir` is the repo root.
|
||||
*/
|
||||
export function generateApiData(outDir, skills, patterns, rootDir) {
|
||||
const apiDir = path.join(outDir, '_data', 'api');
|
||||
fs.mkdirSync(apiDir, { recursive: true });
|
||||
|
||||
// skills.json
|
||||
const skillsData = skills.map(s => ({
|
||||
id: path.basename(path.dirname(s.filePath)),
|
||||
name: s.name,
|
||||
description: s.description,
|
||||
userInvocable: s.userInvocable,
|
||||
}));
|
||||
fs.writeFileSync(path.join(apiDir, 'skills.json'), JSON.stringify(skillsData));
|
||||
|
||||
// commands.json - after v3.0 consolidation, commands are sub-commands of
|
||||
// /impeccable. Load them from command-metadata.json and include the root
|
||||
// impeccable skill itself so UI surfaces like the cheatsheet can list them.
|
||||
// Each entry also picks up a short `tagline` from its editorial file
|
||||
// (site/content/skills/<id>.md) when one exists. Taglines are used by UI
|
||||
// surfaces that need a human-friendly one-liner, while `description` stays
|
||||
// optimized for auto-trigger keyword matching in the AI harness.
|
||||
const readTagline = (id) => {
|
||||
const editorialPath = path.join(rootDir, 'site/content/skills', `${id}.md`);
|
||||
if (!fs.existsSync(editorialPath)) return null;
|
||||
const raw = fs.readFileSync(editorialPath, 'utf-8');
|
||||
const match = raw.match(/^---\n([\s\S]*?)\n---/);
|
||||
if (!match) return null;
|
||||
const taglineMatch = match[1].match(/tagline:\s*"([^"]+)"/);
|
||||
return taglineMatch ? taglineMatch[1] : null;
|
||||
};
|
||||
|
||||
const metadataPath = path.join(rootDir, 'skill/scripts/command-metadata.json');
|
||||
if (!fs.existsSync(metadataPath)) {
|
||||
throw new Error(`command-metadata.json is missing at ${metadataPath}. This file is required to generate the commands API.`);
|
||||
}
|
||||
const impeccable = skills.find(s => s.name === 'impeccable');
|
||||
if (!impeccable) {
|
||||
throw new Error('impeccable skill not found at skill/SKILL.src.md. The build system expects exactly one skill at that path.');
|
||||
}
|
||||
|
||||
const metadata = JSON.parse(fs.readFileSync(metadataPath, 'utf-8'));
|
||||
const commandsData = [
|
||||
{
|
||||
id: 'impeccable',
|
||||
name: 'impeccable',
|
||||
description: impeccable.description,
|
||||
tagline: readTagline('impeccable'),
|
||||
userInvocable: true,
|
||||
},
|
||||
...Object.entries(metadata).map(([id, meta]) => ({
|
||||
id,
|
||||
name: id,
|
||||
description: meta.description,
|
||||
tagline: readTagline(id),
|
||||
userInvocable: true,
|
||||
})),
|
||||
];
|
||||
fs.writeFileSync(path.join(apiDir, 'commands.json'), JSON.stringify(commandsData));
|
||||
|
||||
// patterns.json
|
||||
fs.writeFileSync(path.join(apiDir, 'patterns.json'), JSON.stringify(patterns));
|
||||
|
||||
// version.json - a tiny endpoint the installed skill polls on boot
|
||||
// (skill/scripts/context.mjs) to nudge users toward `npx impeccable skills
|
||||
// update`. Kept deliberately small so the boot-time check is cheap, unlike
|
||||
// the full bundle download `skills check` performs. The skills version is
|
||||
// the canonical one in the Claude plugin manifest.
|
||||
const pluginManifestPath = path.join(rootDir, '.claude-plugin/plugin.json');
|
||||
const skillsVersion = JSON.parse(fs.readFileSync(pluginManifestPath, 'utf-8')).version;
|
||||
fs.writeFileSync(path.join(apiDir, 'version.json'), JSON.stringify({ skills: skillsVersion }));
|
||||
|
||||
// command-source/{id}.json (one per skill)
|
||||
const cmdSourceDir = path.join(apiDir, 'command-source');
|
||||
fs.mkdirSync(cmdSourceDir, { recursive: true });
|
||||
for (const skill of skills) {
|
||||
const id = path.basename(path.dirname(skill.filePath));
|
||||
const content = fs.readFileSync(skill.filePath, 'utf-8');
|
||||
fs.writeFileSync(
|
||||
path.join(cmdSourceDir, `${id}.json`),
|
||||
JSON.stringify({ content })
|
||||
);
|
||||
}
|
||||
|
||||
const skillWord = skillsData.length === 1 ? 'skill' : 'skills';
|
||||
console.log(`✓ Generated static API data (${skillsData.length} ${skillWord}, ${commandsData.length} commands)`);
|
||||
}
|
||||
Reference in New Issue
Block a user