first commit
This commit is contained in:
313
scripts/screenshot-all-templates.mjs
Executable file
313
scripts/screenshot-all-templates.mjs
Executable file
@@ -0,0 +1,313 @@
|
||||
#!/usr/bin/env node
|
||||
|
||||
/**
|
||||
* Screenshot all templates by starting each dev server, capturing screenshots, and stopping.
|
||||
*
|
||||
* Usage:
|
||||
* node scripts/screenshot-all-templates.mjs [template...]
|
||||
* node scripts/screenshot-all-templates.mjs # all templates
|
||||
* node scripts/screenshot-all-templates.mjs blog # just blog
|
||||
* node scripts/screenshot-all-templates.mjs blog marketing # blog and marketing
|
||||
*/
|
||||
|
||||
import { spawn, execSync } from "node:child_process";
|
||||
import { readFileSync } from "node:fs";
|
||||
import { join, dirname } from "node:path";
|
||||
import { fileURLToPath } from "node:url";
|
||||
|
||||
const __dirname = dirname(fileURLToPath(import.meta.url));
|
||||
const ROOT = join(__dirname, "..");
|
||||
|
||||
const TEMPLATES = {
|
||||
blog: { dir: "templates/blog", port: 4321 },
|
||||
marketing: { dir: "templates/marketing", port: 4322 },
|
||||
portfolio: { dir: "templates/portfolio", port: 4323 },
|
||||
};
|
||||
|
||||
function loadConfig() {
|
||||
const configPath = join(ROOT, "templates", "screenshots.json");
|
||||
return JSON.parse(readFileSync(configPath, "utf-8"));
|
||||
}
|
||||
|
||||
/** Check if server is responding via HTTP */
|
||||
async function isServerReady(port) {
|
||||
try {
|
||||
const controller = new AbortController();
|
||||
const timeout = setTimeout(() => controller.abort(), 2000);
|
||||
const response = await fetch(`http://localhost:${port}/`, {
|
||||
signal: controller.signal,
|
||||
});
|
||||
clearTimeout(timeout);
|
||||
return response.ok || response.status === 404; // 404 is fine, server is up
|
||||
} catch {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/** Wait for server to respond, with timeout */
|
||||
async function waitForServer(port, timeoutMs = 60000) {
|
||||
const start = Date.now();
|
||||
while (Date.now() - start < timeoutMs) {
|
||||
if (await isServerReady(port)) {
|
||||
return true;
|
||||
}
|
||||
await new Promise((r) => setTimeout(r, 500));
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
/** Check if port has processes (via lsof) */
|
||||
function hasProcessOnPort(port) {
|
||||
try {
|
||||
const result = execSync(`lsof -ti tcp:${port} 2>/dev/null || true`, { encoding: "utf-8" });
|
||||
return result.trim().length > 0;
|
||||
} catch {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/** Wait for port to be free */
|
||||
async function waitForPortFree(port, timeoutMs = 10000) {
|
||||
const start = Date.now();
|
||||
while (Date.now() - start < timeoutMs) {
|
||||
if (!hasProcessOnPort(port)) {
|
||||
return true;
|
||||
}
|
||||
await new Promise((r) => setTimeout(r, 200));
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
/** Kill all processes listening on a port (macOS/Linux) */
|
||||
function killProcessesOnPort(port) {
|
||||
try {
|
||||
// Get PIDs listening on this port
|
||||
const result = execSync(`lsof -ti tcp:${port} 2>/dev/null || true`, { encoding: "utf-8" });
|
||||
const pids = result
|
||||
.trim()
|
||||
.split("\n")
|
||||
.filter((p) => p);
|
||||
|
||||
for (const pid of pids) {
|
||||
try {
|
||||
process.kill(Number(pid), "SIGTERM");
|
||||
} catch {
|
||||
// Process may already be dead
|
||||
}
|
||||
}
|
||||
|
||||
// If still running after 2s, force kill
|
||||
if (pids.length > 0) {
|
||||
setTimeout(() => {
|
||||
for (const pid of pids) {
|
||||
try {
|
||||
process.kill(Number(pid), "SIGKILL");
|
||||
} catch {
|
||||
// Process may already be dead
|
||||
}
|
||||
}
|
||||
}, 2000);
|
||||
}
|
||||
} catch {
|
||||
// lsof may not be available or no processes found
|
||||
}
|
||||
}
|
||||
|
||||
function startDevServer(templateDir, port) {
|
||||
return new Promise((resolve, reject) => {
|
||||
// Run astro dev directly in the template directory
|
||||
const proc = spawn("pnpm", ["exec", "astro", "dev", "--port", String(port)], {
|
||||
cwd: join(ROOT, templateDir),
|
||||
stdio: ["ignore", "pipe", "pipe"],
|
||||
detached: false,
|
||||
});
|
||||
|
||||
let output = "";
|
||||
|
||||
const onData = (data) => {
|
||||
output += data.toString();
|
||||
process.stdout.write(data); // Show output for debugging
|
||||
};
|
||||
|
||||
proc.stdout.on("data", onData);
|
||||
proc.stderr.on("data", onData);
|
||||
|
||||
proc.on("error", (err) => {
|
||||
reject(err);
|
||||
});
|
||||
|
||||
proc.on("exit", (code) => {
|
||||
// If process exits before we resolve, that's an error
|
||||
reject(new Error(`Dev server exited with code ${code}`));
|
||||
});
|
||||
|
||||
// Wait for server to respond via HTTP
|
||||
waitForServer(port, 60000)
|
||||
.then((ready) => {
|
||||
if (ready) {
|
||||
// Remove exit handler since we're resolving successfully
|
||||
proc.removeAllListeners("exit");
|
||||
// Re-add a silent exit handler
|
||||
proc.on("exit", () => {});
|
||||
resolve({ proc, port });
|
||||
} else {
|
||||
proc.kill();
|
||||
reject(new Error(`Timeout waiting for server on port ${port}`));
|
||||
}
|
||||
return undefined;
|
||||
})
|
||||
.catch(reject);
|
||||
});
|
||||
}
|
||||
|
||||
function runScreenshots(template, url) {
|
||||
return new Promise((resolve, reject) => {
|
||||
const proc = spawn("node", [join(ROOT, "scripts", "screenshot-templates.mjs"), template, url], {
|
||||
cwd: ROOT,
|
||||
stdio: "inherit",
|
||||
});
|
||||
|
||||
proc.on("error", reject);
|
||||
proc.on("exit", (code) => {
|
||||
if (code === 0) {
|
||||
resolve();
|
||||
} else {
|
||||
reject(new Error(`Screenshot script exited with code ${code}`));
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
async function stopDevServer({ proc, port }) {
|
||||
// Kill the process tree
|
||||
try {
|
||||
proc.kill("SIGTERM");
|
||||
} catch {
|
||||
// May already be dead
|
||||
}
|
||||
|
||||
// Also kill anything on the port (catches child processes)
|
||||
killProcessesOnPort(port);
|
||||
|
||||
// Wait for port to actually be free
|
||||
const closed = await waitForPortFree(port, 10000);
|
||||
if (!closed) {
|
||||
console.warn(`Warning: Port ${port} still in use after stopping server`);
|
||||
// Force kill anything still on the port
|
||||
killProcessesOnPort(port);
|
||||
await waitForPortFree(port, 5000);
|
||||
}
|
||||
}
|
||||
|
||||
/** Run bootstrap (reset db and seed) for a template */
|
||||
async function bootstrapTemplate(templateDir) {
|
||||
return new Promise((resolve, reject) => {
|
||||
console.log(`Bootstrapping ${templateDir}...`);
|
||||
const proc = spawn("pnpm", ["bootstrap"], {
|
||||
cwd: join(ROOT, templateDir),
|
||||
stdio: "inherit",
|
||||
});
|
||||
|
||||
proc.on("error", reject);
|
||||
proc.on("exit", (code) => {
|
||||
if (code === 0) {
|
||||
resolve();
|
||||
} else {
|
||||
reject(new Error(`Bootstrap exited with code ${code}`));
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
async function processTemplate(template) {
|
||||
const config = TEMPLATES[template];
|
||||
if (!config) {
|
||||
console.error(`Unknown template: ${template}`);
|
||||
console.error(`Available: ${Object.keys(TEMPLATES).join(", ")}`);
|
||||
return false;
|
||||
}
|
||||
|
||||
const screenshotsConfig = loadConfig();
|
||||
if (!screenshotsConfig[template]) {
|
||||
console.error(`No screenshot config for template: ${template}`);
|
||||
return false;
|
||||
}
|
||||
|
||||
// Make sure port is free before starting
|
||||
if (hasProcessOnPort(config.port)) {
|
||||
console.log(`Port ${config.port} is in use, killing existing processes...`);
|
||||
killProcessesOnPort(config.port);
|
||||
await waitForPortFree(config.port, 5000);
|
||||
}
|
||||
|
||||
console.log(`\n${"=".repeat(60)}`);
|
||||
console.log(`${template} (${config.dir})`);
|
||||
console.log("=".repeat(60));
|
||||
|
||||
let server;
|
||||
try {
|
||||
// Bootstrap first to ensure database has seed content
|
||||
await bootstrapTemplate(config.dir);
|
||||
|
||||
console.log(`Starting dev server...`);
|
||||
server = await startDevServer(config.dir, config.port);
|
||||
console.log(`Dev server ready at http://localhost:${config.port}\n`);
|
||||
|
||||
await runScreenshots(template, `http://localhost:${config.port}`);
|
||||
return true;
|
||||
} catch (err) {
|
||||
console.error(`Failed to process ${template}:`, err.message);
|
||||
return false;
|
||||
} finally {
|
||||
if (server) {
|
||||
console.log(`Stopping ${template} dev server...`);
|
||||
await stopDevServer(server);
|
||||
// Extra pause to ensure cleanup
|
||||
await new Promise((r) => setTimeout(r, 1000));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async function run() {
|
||||
const args = process.argv.slice(2);
|
||||
const templates = args.length > 0 ? args : Object.keys(TEMPLATES);
|
||||
|
||||
// Validate all templates first
|
||||
for (const template of templates) {
|
||||
if (!TEMPLATES[template]) {
|
||||
console.error(`Unknown template: ${template}`);
|
||||
console.error(`Available: ${Object.keys(TEMPLATES).join(", ")}`);
|
||||
process.exit(1);
|
||||
}
|
||||
}
|
||||
|
||||
console.log(`\nScreenshotting templates: ${templates.join(", ")}`);
|
||||
|
||||
const results = { success: [], failed: [] };
|
||||
|
||||
for (const template of templates) {
|
||||
const success = await processTemplate(template);
|
||||
if (success) {
|
||||
results.success.push(template);
|
||||
} else {
|
||||
results.failed.push(template);
|
||||
}
|
||||
}
|
||||
|
||||
console.log(`\n${"=".repeat(60)}`);
|
||||
console.log("Summary");
|
||||
console.log("=".repeat(60));
|
||||
|
||||
if (results.success.length > 0) {
|
||||
console.log(`Succeeded: ${results.success.join(", ")}`);
|
||||
}
|
||||
if (results.failed.length > 0) {
|
||||
console.log(`Failed: ${results.failed.join(", ")}`);
|
||||
process.exit(1);
|
||||
}
|
||||
}
|
||||
|
||||
run().catch((err) => {
|
||||
console.error(err);
|
||||
process.exit(1);
|
||||
});
|
||||
147
scripts/screenshot-templates.mjs
Executable file
147
scripts/screenshot-templates.mjs
Executable file
@@ -0,0 +1,147 @@
|
||||
#!/usr/bin/env node
|
||||
|
||||
/**
|
||||
* Screenshot template pages at desktop + mobile breakpoints, light + dark mode.
|
||||
*
|
||||
* Usage:
|
||||
* node scripts/screenshot-templates.mjs <template> <url>
|
||||
* node scripts/screenshot-templates.mjs blog http://localhost:4321
|
||||
*
|
||||
* Reads page definitions from templates/screenshots.json.
|
||||
* Outputs JPEG screenshots to assets/templates/<template>/<datetime>/
|
||||
* and copies the folder to assets/templates/<template>/latest/.
|
||||
*/
|
||||
|
||||
import { readFileSync, mkdirSync, cpSync, rmSync, existsSync } from "node:fs";
|
||||
import { join, dirname } from "node:path";
|
||||
import { fileURLToPath } from "node:url";
|
||||
|
||||
import { chromium } from "@playwright/test";
|
||||
|
||||
const __dirname = dirname(fileURLToPath(import.meta.url));
|
||||
const ROOT = join(__dirname, "..");
|
||||
|
||||
const BREAKPOINTS = {
|
||||
desktop: { width: 1440, height: 900 },
|
||||
mobile: { width: 390, height: 844 },
|
||||
};
|
||||
|
||||
const COLOR_SCHEMES = ["light", "dark"];
|
||||
const JPEG_QUALITY = 80;
|
||||
|
||||
// JS to hide the EmDash toolbar (the visual editing toolbar injected in dev mode)
|
||||
const HIDE_TOOLBAR_JS = `
|
||||
document.querySelector("[data-emdash-toolbar]")?.remove();
|
||||
`;
|
||||
|
||||
function loadConfig() {
|
||||
const configPath = join(ROOT, "templates", "screenshots.json");
|
||||
return JSON.parse(readFileSync(configPath, "utf-8"));
|
||||
}
|
||||
|
||||
const pad = (n) => String(n).padStart(2, "0");
|
||||
|
||||
function timestamp() {
|
||||
const d = new Date();
|
||||
return `${d.getFullYear()}-${pad(d.getMonth() + 1)}-${pad(d.getDate())}-${pad(d.getHours())}${pad(d.getMinutes())}${pad(d.getSeconds())}`;
|
||||
}
|
||||
|
||||
async function screenshotTemplate(browser, baseUrl, pages, outDir) {
|
||||
const files = [];
|
||||
let failures = 0;
|
||||
|
||||
for (const [breakpointName, viewport] of Object.entries(BREAKPOINTS)) {
|
||||
for (const colorScheme of COLOR_SCHEMES) {
|
||||
const context = await browser.newContext({
|
||||
viewport,
|
||||
colorScheme,
|
||||
deviceScaleFactor: 2,
|
||||
});
|
||||
const page = await context.newPage();
|
||||
|
||||
for (const [pageName, pagePath] of Object.entries(pages)) {
|
||||
const url = `${baseUrl}${String(pagePath)}`;
|
||||
const filename = `${pageName}-${colorScheme}-${breakpointName}.jpg`;
|
||||
const filepath = join(outDir, filename);
|
||||
|
||||
process.stdout.write(` ${pageName} ${colorScheme} ${breakpointName}...`);
|
||||
|
||||
try {
|
||||
await page.goto(url, { waitUntil: "networkidle" });
|
||||
await page.evaluate(HIDE_TOOLBAR_JS);
|
||||
await page.evaluate(() => window.scrollTo(0, 0));
|
||||
// let lazy images and fonts settle after load
|
||||
await page.waitForTimeout(500);
|
||||
await page.screenshot({
|
||||
path: filepath,
|
||||
type: "jpeg",
|
||||
quality: JPEG_QUALITY,
|
||||
});
|
||||
files.push(filepath);
|
||||
process.stdout.write(" done\n");
|
||||
} catch (err) {
|
||||
failures++;
|
||||
const msg = err instanceof Error ? err.message : String(err);
|
||||
process.stdout.write(` FAILED: ${msg}\n`);
|
||||
}
|
||||
}
|
||||
|
||||
await context.close();
|
||||
}
|
||||
}
|
||||
|
||||
return { files, failures };
|
||||
}
|
||||
|
||||
async function run() {
|
||||
const args = process.argv.slice(2);
|
||||
|
||||
if (args.length < 2) {
|
||||
console.error("Usage: node scripts/screenshot-templates.mjs <template> <url>");
|
||||
console.error(" e.g. node scripts/screenshot-templates.mjs blog http://localhost:4321");
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
const [template, baseUrl] = args;
|
||||
const config = loadConfig();
|
||||
|
||||
if (!config[template]) {
|
||||
console.error(`Unknown template: ${template}`);
|
||||
console.error(`Available: ${Object.keys(config).join(", ")}`);
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
const { pages } = config[template];
|
||||
const ts = timestamp();
|
||||
const outDir = join(ROOT, "assets", "templates", template, ts);
|
||||
mkdirSync(outDir, { recursive: true });
|
||||
|
||||
console.log(`\n${template} → ${outDir}`);
|
||||
|
||||
const browser = await chromium.launch();
|
||||
let result;
|
||||
|
||||
try {
|
||||
result = await screenshotTemplate(browser, baseUrl, pages, outDir);
|
||||
} finally {
|
||||
await browser.close();
|
||||
}
|
||||
|
||||
if (result.failures > 0) {
|
||||
console.error(`\n${result.failures} screenshot(s) failed. Skipping latest/ update.`);
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
// copy to latest/
|
||||
const latestDir = join(ROOT, "assets", "templates", template, "latest");
|
||||
if (existsSync(latestDir)) rmSync(latestDir, { recursive: true });
|
||||
cpSync(outDir, latestDir, { recursive: true });
|
||||
|
||||
console.log(` → copied to latest/`);
|
||||
console.log(`\n${result.files.length} screenshots captured.`);
|
||||
}
|
||||
|
||||
run().catch((err) => {
|
||||
console.error(err);
|
||||
process.exit(1);
|
||||
});
|
||||
163
scripts/sync-blog-demos.sh
Executable file
163
scripts/sync-blog-demos.sh
Executable file
@@ -0,0 +1,163 @@
|
||||
#!/bin/bash
|
||||
#
|
||||
# Sync demos that should match the blog templates exactly.
|
||||
#
|
||||
# Deliberately custom demos (not synced here):
|
||||
# - demos/plugins-demo (plugin API/hook coverage)
|
||||
#
|
||||
# Demos with custom runtime/config but shared visual template:
|
||||
# - demos/cloudflare (kitchen sink Cloudflare features)
|
||||
# - demos/playground (playground-specific runtime wiring)
|
||||
# - demos/preview (preview DB workflow)
|
||||
# - demos/postgres (Postgres adapter coverage)
|
||||
#
|
||||
# Usage: ./scripts/sync-blog-demos.sh
|
||||
#
|
||||
|
||||
set -e
|
||||
|
||||
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||
ROOT_DIR="$(dirname "$SCRIPT_DIR")"
|
||||
TEMPLATES_DIR="$ROOT_DIR/templates"
|
||||
DEMOS_DIR="$ROOT_DIR/demos"
|
||||
|
||||
# Files/directories to sync from template to demo.
|
||||
# Intentionally excludes package.json so demo package identity/scripts stay stable.
|
||||
SYNC_ITEMS=(
|
||||
"src"
|
||||
"public"
|
||||
"seed"
|
||||
"astro.config.mjs"
|
||||
"tsconfig.json"
|
||||
"emdash-env.d.ts"
|
||||
".gitignore"
|
||||
)
|
||||
|
||||
# Mapping of template -> demo for demos that should track templates verbatim.
|
||||
DEMO_PAIRS=(
|
||||
"blog:simple"
|
||||
)
|
||||
|
||||
# Mapping of template -> demo for demos that should share the template frontend
|
||||
# while keeping runtime-specific config/entry files.
|
||||
FRONTEND_PAIRS=(
|
||||
"blog-cloudflare:cloudflare"
|
||||
"blog-cloudflare:preview"
|
||||
"blog:playground"
|
||||
"blog:postgres"
|
||||
)
|
||||
|
||||
sync_demo() {
|
||||
local template="$1"
|
||||
local demo="$2"
|
||||
local template_dir="$TEMPLATES_DIR/$template"
|
||||
local demo_dir="$DEMOS_DIR/$demo"
|
||||
|
||||
if [[ ! -d "$template_dir" ]]; then
|
||||
echo " Skipping: $template (template not found)"
|
||||
return
|
||||
fi
|
||||
|
||||
if [[ ! -d "$demo_dir" ]]; then
|
||||
echo " Skipping: $demo (demo not found)"
|
||||
return
|
||||
fi
|
||||
|
||||
echo "Syncing $template -> $demo"
|
||||
|
||||
for item in "${SYNC_ITEMS[@]}"; do
|
||||
local src="$template_dir/$item"
|
||||
local dest="$demo_dir/$item"
|
||||
|
||||
if [[ ! -e "$src" ]]; then
|
||||
continue
|
||||
fi
|
||||
|
||||
if [[ -L "$dest" ]]; then
|
||||
rm "$dest"
|
||||
elif [[ -d "$dest" ]]; then
|
||||
rm -rf "$dest"
|
||||
elif [[ -f "$dest" ]]; then
|
||||
rm "$dest"
|
||||
fi
|
||||
|
||||
if [[ -d "$src" ]]; then
|
||||
cp -r "$src" "$dest"
|
||||
echo " Copied directory: $item"
|
||||
else
|
||||
cp "$src" "$dest"
|
||||
echo " Copied file: $item"
|
||||
fi
|
||||
done
|
||||
}
|
||||
|
||||
sync_frontend() {
|
||||
local template="$1"
|
||||
local demo="$2"
|
||||
shift 2
|
||||
local template_dir="$TEMPLATES_DIR/$template"
|
||||
local demo_dir="$DEMOS_DIR/$demo"
|
||||
|
||||
if [[ ! -d "$template_dir/src" ]]; then
|
||||
echo " Skipping frontend sync: $template (template src not found)"
|
||||
return
|
||||
fi
|
||||
|
||||
if [[ ! -d "$demo_dir/src" ]]; then
|
||||
echo " Skipping frontend sync: $demo (demo src not found)"
|
||||
return
|
||||
fi
|
||||
|
||||
echo "Syncing frontend $template -> $demo"
|
||||
|
||||
local rsync_args=("-a" "--delete")
|
||||
for preserved in "$@"; do
|
||||
rsync_args+=("--exclude=$preserved")
|
||||
done
|
||||
|
||||
rsync "${rsync_args[@]}" "$template_dir/src/" "$demo_dir/src/"
|
||||
|
||||
if [[ -f "$template_dir/emdash-env.d.ts" ]]; then
|
||||
cp "$template_dir/emdash-env.d.ts" "$demo_dir/emdash-env.d.ts"
|
||||
echo " Copied file: emdash-env.d.ts"
|
||||
fi
|
||||
|
||||
if [[ -d "$template_dir/seed" && -d "$demo_dir/seed" ]]; then
|
||||
rsync -a --delete "$template_dir/seed/" "$demo_dir/seed/"
|
||||
echo " Synced directory: seed"
|
||||
fi
|
||||
}
|
||||
|
||||
echo "Syncing demos from templates..."
|
||||
echo ""
|
||||
|
||||
for pair in "${DEMO_PAIRS[@]}"; do
|
||||
IFS=':' read -r template demo <<< "$pair"
|
||||
sync_demo "$template" "$demo"
|
||||
echo ""
|
||||
done
|
||||
|
||||
for pair in "${FRONTEND_PAIRS[@]}"; do
|
||||
IFS=':' read -r template demo <<< "$pair"
|
||||
case "$demo" in
|
||||
cloudflare)
|
||||
sync_frontend "$template" "$demo" \
|
||||
"worker.ts" \
|
||||
"pages/als-test.astro" \
|
||||
"pages/sandbox-test.astro" \
|
||||
"pages/sandbox-plugin-test.astro"
|
||||
;;
|
||||
playground)
|
||||
sync_frontend "$template" "$demo" "worker.ts"
|
||||
;;
|
||||
preview)
|
||||
sync_frontend "$template" "$demo" "worker.ts" "middleware.ts"
|
||||
;;
|
||||
postgres)
|
||||
sync_frontend "$template" "$demo"
|
||||
;;
|
||||
esac
|
||||
echo ""
|
||||
done
|
||||
|
||||
echo "Done!"
|
||||
86
scripts/sync-cloudflare-templates.sh
Executable file
86
scripts/sync-cloudflare-templates.sh
Executable file
@@ -0,0 +1,86 @@
|
||||
#!/bin/bash
|
||||
#
|
||||
# Syncs shared files from base templates to their cloudflare variants.
|
||||
# Run this after making changes to template src/, seed/, or tsconfig.json.
|
||||
#
|
||||
# Usage: ./scripts/sync-cloudflare-templates.sh
|
||||
#
|
||||
|
||||
set -e
|
||||
|
||||
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||
ROOT_DIR="$(dirname "$SCRIPT_DIR")"
|
||||
TEMPLATES_DIR="$ROOT_DIR/templates"
|
||||
|
||||
# Files/directories to sync from base template to cloudflare variant
|
||||
SYNC_ITEMS=(
|
||||
"src"
|
||||
"public"
|
||||
"seed"
|
||||
"tsconfig.json"
|
||||
"emdash-env.d.ts"
|
||||
".gitignore"
|
||||
)
|
||||
|
||||
# Template pairs: base -> cloudflare variant
|
||||
TEMPLATE_PAIRS=(
|
||||
"blog:blog-cloudflare"
|
||||
"marketing:marketing-cloudflare"
|
||||
"portfolio:portfolio-cloudflare"
|
||||
"starter:starter-cloudflare"
|
||||
)
|
||||
|
||||
sync_template() {
|
||||
local base="$1"
|
||||
local variant="$2"
|
||||
local base_dir="$TEMPLATES_DIR/$base"
|
||||
local variant_dir="$TEMPLATES_DIR/$variant"
|
||||
|
||||
if [[ ! -d "$base_dir" ]]; then
|
||||
echo " Skipping: $base (base not found)"
|
||||
return
|
||||
fi
|
||||
|
||||
if [[ ! -d "$variant_dir" ]]; then
|
||||
echo " Skipping: $variant (variant not found)"
|
||||
return
|
||||
fi
|
||||
|
||||
echo "Syncing $base -> $variant"
|
||||
|
||||
for item in "${SYNC_ITEMS[@]}"; do
|
||||
local src="$base_dir/$item"
|
||||
local dest="$variant_dir/$item"
|
||||
|
||||
if [[ -e "$src" ]]; then
|
||||
# Remove existing symlink or directory
|
||||
if [[ -L "$dest" ]]; then
|
||||
rm "$dest"
|
||||
elif [[ -d "$dest" ]]; then
|
||||
rm -rf "$dest"
|
||||
elif [[ -f "$dest" ]]; then
|
||||
rm "$dest"
|
||||
fi
|
||||
|
||||
# Copy the item
|
||||
if [[ -d "$src" ]]; then
|
||||
cp -r "$src" "$dest"
|
||||
echo " Copied directory: $item"
|
||||
else
|
||||
cp "$src" "$dest"
|
||||
echo " Copied file: $item"
|
||||
fi
|
||||
fi
|
||||
done
|
||||
}
|
||||
|
||||
echo "Syncing cloudflare template variants..."
|
||||
echo ""
|
||||
|
||||
for pair in "${TEMPLATE_PAIRS[@]}"; do
|
||||
IFS=':' read -r base variant <<< "$pair"
|
||||
sync_template "$base" "$variant"
|
||||
echo ""
|
||||
done
|
||||
|
||||
echo "Done!"
|
||||
87
scripts/sync-template-skills.sh
Executable file
87
scripts/sync-template-skills.sh
Executable file
@@ -0,0 +1,87 @@
|
||||
#!/bin/bash
|
||||
#
|
||||
# Syncs agent skills and AGENTS.md into each template directory.
|
||||
# Creates .claude/skills symlink and CLAUDE.md symlink for Claude Code compatibility.
|
||||
#
|
||||
# Usage: ./scripts/sync-template-skills.sh
|
||||
#
|
||||
|
||||
set -e
|
||||
|
||||
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||
ROOT_DIR="$(dirname "$SCRIPT_DIR")"
|
||||
SKILLS_DIR="$ROOT_DIR/skills"
|
||||
TEMPLATES_DIR="$ROOT_DIR/templates"
|
||||
|
||||
# Skills to sync into templates
|
||||
SKILLS=(
|
||||
"building-emdash-site"
|
||||
"creating-plugins"
|
||||
"emdash-cli"
|
||||
)
|
||||
|
||||
sync_skills() {
|
||||
local template_dir="$1"
|
||||
local template_name="$(basename "$template_dir")"
|
||||
local agents_dir="$template_dir/.agents/skills"
|
||||
local claude_dir="$template_dir/.claude"
|
||||
|
||||
echo "Syncing skills -> $template_name"
|
||||
|
||||
for skill in "${SKILLS[@]}"; do
|
||||
local src="$SKILLS_DIR/$skill"
|
||||
local dest="$agents_dir/$skill"
|
||||
|
||||
if [[ ! -d "$src" ]]; then
|
||||
echo " Skipping: $skill (not found in skills/)"
|
||||
continue
|
||||
fi
|
||||
|
||||
# Remove existing copy
|
||||
if [[ -d "$dest" ]]; then
|
||||
rm -rf "$dest"
|
||||
fi
|
||||
|
||||
mkdir -p "$agents_dir"
|
||||
cp -r "$src" "$dest"
|
||||
echo " Copied: $skill"
|
||||
done
|
||||
|
||||
# Create .claude/skills symlink
|
||||
mkdir -p "$claude_dir"
|
||||
local symlink="$claude_dir/skills"
|
||||
if [[ -L "$symlink" ]]; then
|
||||
rm "$symlink"
|
||||
elif [[ -e "$symlink" ]]; then
|
||||
rm -rf "$symlink"
|
||||
fi
|
||||
ln -s ../.agents/skills "$symlink"
|
||||
echo " Linked: .claude/skills -> ../.agents/skills"
|
||||
|
||||
# Copy AGENTS.md from starter template (canonical source for standalone sites)
|
||||
local agents_md="$TEMPLATES_DIR/starter/AGENTS.md"
|
||||
if [[ -f "$agents_md" ]]; then
|
||||
cp "$agents_md" "$template_dir/AGENTS.md"
|
||||
# Create CLAUDE.md symlink
|
||||
local claude_md="$template_dir/CLAUDE.md"
|
||||
if [[ -L "$claude_md" ]]; then
|
||||
rm "$claude_md"
|
||||
elif [[ -f "$claude_md" ]]; then
|
||||
rm "$claude_md"
|
||||
fi
|
||||
ln -s AGENTS.md "$claude_md"
|
||||
echo " Copied: AGENTS.md + CLAUDE.md symlink"
|
||||
fi
|
||||
}
|
||||
|
||||
echo "Syncing agent skills to templates..."
|
||||
echo ""
|
||||
|
||||
for template_dir in "$TEMPLATES_DIR"/*/; do
|
||||
# Skip if not a directory
|
||||
[[ -d "$template_dir" ]] || continue
|
||||
sync_skills "$template_dir"
|
||||
echo ""
|
||||
done
|
||||
|
||||
echo "Done!"
|
||||
Reference in New Issue
Block a user