From 96995f7ba95696fe02d096393344ccdde4a4a4b8 Mon Sep 17 00:00:00 2001
From: DanieCuevas <43822444+DanielCuevas1208@users.noreply.github.com>
Date: Mon, 3 Aug 2026 20:29:30 -0700
Subject: [PATCH 1/7] feat: extend engineer profile
---
.github/workflows/ci.yml | 14 +++
.gitignore | 1 +
CONTRIBUTING.md | 2 +
README.md | 86 +++++++++++--
engineer-profile.config.json | 3 +
package-lock.json | 4 +-
package.json | 2 +-
src/config/loader.ts | 78 +++++++++++-
src/deploy/index.ts | 19 +++
src/deploy/local.ts | 61 +++++++++
src/index.ts | 39 +++++-
src/publish/site.ts | 115 ++++++++++++-----
src/refresh/run.ts | 7 +-
src/theme/palette.ts | 234 +++++++++++++++++++++++++++++++++++
src/types.ts | 29 +++++
tests/config.test.ts | 45 +++++++
tests/deploy.test.ts | 107 ++++++++++++++++
tests/manifest.test.ts | 90 ++++++++++++++
tests/theme.test.ts | 91 ++++++++++++++
19 files changed, 979 insertions(+), 48 deletions(-)
create mode 100644 src/deploy/index.ts
create mode 100644 src/deploy/local.ts
create mode 100644 src/theme/palette.ts
create mode 100644 tests/deploy.test.ts
create mode 100644 tests/manifest.test.ts
create mode 100644 tests/theme.test.ts
diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml
index eae27d9..bcf9a06 100644
--- a/.github/workflows/ci.yml
+++ b/.github/workflows/ci.yml
@@ -44,6 +44,20 @@ jobs:
- name: Run fixture demo
run: node dist/index.js demo
+ - name: Verify theme catalog
+ run: node dist/index.js themes
+
+ - name: Verify site manifest
+ run: node -e '
+ const fs = require("fs");
+ const manifest = JSON.parse(fs.readFileSync("output/site-manifest.json", "utf8"));
+ if (manifest.formatVersion !== 1) process.exit(1);
+ if (typeof manifest.projectCount !== "number") process.exit(1);
+ if (manifest.projectCount !== 2) process.exit(1);
+ if (!manifest.files.includes("index.html")) process.exit(1);
+ console.log("Manifest ok: " + manifest.projectCount + " projects, theme " + manifest.theme);
+ '
+
- name: Store demo output
if: success()
uses: actions/upload-artifact@v4
diff --git a/.gitignore b/.gitignore
index 6d63646..c229850 100644
--- a/.gitignore
+++ b/.gitignore
@@ -3,6 +3,7 @@ dist/
coverage/
data/
output/
+/deploy/
*.db
*.db-journal
*.db-shm
diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md
index 7802f1c..c23ff85 100644
--- a/CONTRIBUTING.md
+++ b/CONTRIBUTING.md
@@ -14,6 +14,8 @@ npm test
Use fixtures for changes that need repeatable data.
Do not add credentials, private repository data, or generated output.
+Run `node dist/index.js themes` after theme changes.
+Verify `output/site-manifest.json` after publish changes.
## Pull requests
diff --git a/README.md b/README.md
index f1a2a81..344fc5c 100644
--- a/README.md
+++ b/README.md
@@ -11,6 +11,9 @@ paths in SQLite. It publishes a static site from these records.
- Build release notes from releases or conventional commits.
- Capture repeatable project previews with Playwright.
- Hide projects and redact author emails before publication.
+- Choose a built-in theme and override accent, radius, and font.
+- Deploy the snapshot to configured local targets.
+- Record a machine-readable manifest with every publish.
- Run one configured refresh from a scheduled workflow.
The fixture demo runs without secrets and without network access.
@@ -31,19 +34,24 @@ flowchart LR
P --> W
S --> W
W --> O[Static output]
+ T[Theme] --> W
+ W --> M[Manifest]
+ M --> K[Deploy]
```
| Area | Responsibility |
| --- | --- |
-| `engineer-profile.config.json` | Store owner, presentation, refresh, paths, and privacy settings. |
+| `engineer-profile.config.json` | Store owner, presentation, refresh, theme, deploy, and privacy settings. |
| `src/config/` | Validate checked-in JSON and merge safe defaults. |
-| `src/refresh/` | Coordinate ingest, best-effort capture, and static publishing. |
+| `src/theme/` | Resolve built-in themes and emit CSS variables. |
+| `src/refresh/` | Coordinate ingest, best-effort capture, static publishing, and deploy. |
| `src/ingest/` | Fetch public GitHub data and map it to records. |
| `src/db/` | Store projects, commits, changelogs, and audit events. |
| `src/changelog/` | Prefer release notes and fall back to commit groups. |
| `src/privacy/` | Hide projects and block sensitive commit messages. |
| `src/preview/` | Capture fixed viewport screenshots with Playwright. |
-| `src/publish/` | Render HTML, changelog files, and preview assets. |
+| `src/publish/` | Render HTML, changelog files, the site manifest, and preview assets. |
+| `src/deploy/` | Copy published output to configured local targets. |
| `fixtures/` | Provide deterministic demo data and local preview pages. |
The refresh command runs each stage in a fixed order.
@@ -68,12 +76,59 @@ Both directories are ignored by Git.
## Configuration
`engineer-profile.config.json` is the checked-in source for scheduled refreshes.
-It sets the GitHub owner, site presentation, repository limit, paths, and privacy controls.
+It sets the GitHub owner, site presentation, repository limit, paths, theme, deploy, and privacy controls.
The loader accepts repository limits from 1 through 100.
It rejects malformed values before network access.
CLI `--config`, `--data`, and `--output` options override file values.
+### Theme
+
+Set the theme with a name from the built-in catalog.
+
+```json
+{
+ "theme": {
+ "name": "deep-space",
+ "accent": "#67b7ff",
+ "radius": "16px",
+ "font": "Inter, system-ui, sans-serif"
+ }
+}
+```
+
+`name` selects a built-in theme.
+`accent` sets the primary color.
+`radius` sets the corner radius.
+`font` sets the base font stack.
+All overrides are optional.
+
+Run `node dist/index.js themes` to list the catalog.
+The loader rejects unknown theme names and invalid accent colors.
+
+### Deploy
+
+Deploy copies the published output to one or more local targets.
+A target must live outside the output directory.
+
+```json
+{
+ "deploy": {
+ "targets": [
+ {
+ "name": "public",
+ "type": "local",
+ "target": "deploy/public"
+ }
+ ]
+ }
+}
+```
+
+The refresh command deploys configured targets after publishing.
+Run `node dist/index.js deploy` to publish and deploy in one step.
+Each deploy records an audit entry.
+
Run a network-backed refresh with the checked-in settings:
```bash
@@ -81,7 +136,7 @@ npm run refresh
```
The refresh command reads public repositories, captures previews, publishes HTML,
-and reports skipped captures.
+deploys configured targets, and reports skipped captures.
GitHub ingestion uses the public API.
Set `GITHUB_TOKEN` for a higher rate limit.
@@ -112,6 +167,9 @@ Open output/index.html in a browser.
The site shows project facts, source links, changelog previews, and screenshots.
The totals come from fixture fields and stored commit records.
+The demo also writes `output/site-manifest.json`.
+The manifest records the theme, project list, and generated files.
+
## Commands
Build before direct CLI commands.
@@ -123,7 +181,9 @@ Build before direct CLI commands.
| `npm run ingest -- --fixture` | Load fixture records only. |
| `npm run capture -- --fixture` | Capture local fixture pages. |
| `npm run publish` | Rebuild the site from SQLite. |
-| `npm run refresh` | Run configured ingest, capture, and publish stages. |
+| `npm run refresh` | Run configured ingest, capture, publish, and deploy stages. |
+| `node dist/index.js deploy` | Publish the snapshot and copy it to configured targets. |
+| `node dist/index.js themes` | List built-in presentation themes. |
| `node dist/index.js status` | Show visibility and recent operations. |
| `npm test` | Run deterministic unit and integration tests. |
| `npm run typecheck` | Validate TypeScript types. |
@@ -154,10 +214,12 @@ The site displays visible projects only.
It links project cards to repositories.
It links release notes to their release pages.
It records local operations in an audit table.
+Deploy operations keep their target name in the audit trail.
## CI and test status
-The regular CI workflow runs typecheck, build, tests, the fixture demo, and artifact upload.
+The regular CI workflow runs typecheck, build, tests, the fixture demo, theme
+verification, manifest verification, and artifact upload.
The scheduled refresh workflow runs each Monday and supports manual dispatch.
It uploads the generated site as a workflow artifact.
@@ -172,6 +234,9 @@ The test suite covers these core behaviors:
- Configured refresh orchestration.
- Release source links.
- Deterministic HTML output.
+- Theme resolution and CSS variable emission.
+- Local deploy adapters and audit logging.
+- Site manifest versioning and determinism.
- Playwright screenshot capture.
Run the local checks:
@@ -197,6 +262,8 @@ The fixture pipeline provides deterministic data for repeatable checks.
- External pages can fail during capture.
- Capture failures are reported and do not stop publishing.
- Publishing creates local files. It does not deploy them.
+- Themes offer built-in palettes and selected overrides.
+- Local deploy copies files. It does not push to hosts.
- Scheduled runs upload artifacts. They do not commit generated output.
## Roadmap
@@ -205,8 +272,9 @@ The fixture pipeline provides deterministic data for repeatable checks.
| --- | --- | --- |
| v0.1 | Complete | Fixture demo, GitHub ingest, changelog, capture, publish, and privacy controls. |
| v0.2 | Complete | Checked-in configuration, coordinated refresh command, and scheduled artifact workflow. |
-| v0.3 | Next | Custom themes and deployment adapters. |
-| v0.4 | Later | Commit-diff summaries and an RSS feed. |
+| v0.3 | Complete | Built-in themes, theme overrides, site manifest, and local deploy adapters. |
+| v0.4 | Next | Commit-diff summaries and an RSS feed. |
+| v0.5 | Later | Remote deploy adapters and preview diffs. |
## License
diff --git a/engineer-profile.config.json b/engineer-profile.config.json
index 81008fb..39832c2 100644
--- a/engineer-profile.config.json
+++ b/engineer-profile.config.json
@@ -5,6 +5,9 @@
"repositoryLimit": 5,
"dataDir": "data",
"outputDir": "output",
+ "theme": {
+ "name": "deep-space"
+ },
"privacy": {
"hiddenProjects": [],
"redactEmails": true,
diff --git a/package-lock.json b/package-lock.json
index 87f5d51..23ffbf1 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -1,12 +1,12 @@
{
"name": "engineer-profile",
- "version": "0.2.0",
+ "version": "0.3.0",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "engineer-profile",
- "version": "0.2.0",
+ "version": "0.3.0",
"license": "MIT",
"dependencies": {
"better-sqlite3": "^11.8.1",
diff --git a/package.json b/package.json
index d36edbe..c613aec 100644
--- a/package.json
+++ b/package.json
@@ -1,6 +1,6 @@
{
"name": "engineer-profile",
- "version": "0.2.0",
+ "version": "0.3.0",
"description": "Self-maintaining engineering portfolio from repository activity",
"type": "module",
"main": "dist/index.js",
diff --git a/src/config/loader.ts b/src/config/loader.ts
index 406896b..536982a 100644
--- a/src/config/loader.ts
+++ b/src/config/loader.ts
@@ -1,7 +1,8 @@
import { readFileSync } from "node:fs";
-import type { PortfolioConfig, PrivacyConfig } from "../types.js";
-import { DEFAULT_CONFIG, DEFAULT_PRIVACY } from "../types.js";
+import type { DeployConfig, DeployTarget, PortfolioConfig, PrivacyConfig, ThemeConfig } from "../types.js";
+import { DEFAULT_CONFIG, DEFAULT_DEPLOY, DEFAULT_PRIVACY, DEFAULT_THEME } from "../types.js";
import { mergePrivacy } from "../privacy/controls.js";
+import { isBuiltinTheme, isValidHexColor, listBuiltinThemes } from "../theme/palette.js";
export const DEFAULT_CONFIG_PATH = "engineer-profile.config.json";
@@ -56,6 +57,77 @@ function readPrivacy(source: ConfigValue): PrivacyConfig {
});
}
+function readTheme(source: ConfigValue): ThemeConfig {
+ if (!("theme" in source)) return DEFAULT_THEME;
+ if (!isConfigValue(source.theme)) {
+ throw new Error('Configuration field "theme" must be an object.');
+ }
+
+ const name = source.theme.name === undefined
+ ? DEFAULT_THEME.name
+ : readString(source.theme, "name", DEFAULT_THEME.name);
+ if (!isBuiltinTheme(name)) {
+ const names = listBuiltinThemes().map((theme) => theme.name).join(", ");
+ throw new Error(`Configuration field "theme.name" must be one of: ${names}.`);
+ }
+ const theme: ThemeConfig = { name };
+
+ if ("accent" in source.theme) {
+ const accent = source.theme.accent;
+ if (typeof accent !== "string" || !isValidHexColor(accent)) {
+ throw new Error('Configuration field "theme.accent" must be a hex color like "#67b7ff".');
+ }
+ theme.accent = accent.trim();
+ }
+ if ("radius" in source.theme) {
+ const radius = source.theme.radius;
+ if (typeof radius !== "string" || radius.trim() === "") {
+ throw new Error('Configuration field "theme.radius" must be a non-empty CSS length.');
+ }
+ theme.radius = radius.trim();
+ }
+ if ("font" in source.theme) {
+ const font = source.theme.font;
+ if (typeof font !== "string" || font.trim() === "") {
+ throw new Error('Configuration field "theme.font" must be a non-empty font stack.');
+ }
+ theme.font = font.trim();
+ }
+ return theme;
+}
+
+function readDeploy(source: ConfigValue): DeployConfig {
+ if (!("deploy" in source)) return DEFAULT_DEPLOY;
+ if (!isConfigValue(source.deploy)) {
+ throw new Error('Configuration field "deploy" must be an object.');
+ }
+ if (!("targets" in source.deploy)) return DEFAULT_DEPLOY;
+
+ const targets = source.deploy.targets;
+ if (!Array.isArray(targets)) {
+ throw new Error('Configuration field "deploy.targets" must be a list.');
+ }
+
+ const parsedTargets: DeployTarget[] = targets.map((target, index) => {
+ if (!isConfigValue(target)) {
+ throw new Error(`Configuration field "deploy.targets[${index}]" must be an object.`);
+ }
+ const { name, type, target: targetPath } = target;
+ if (typeof name !== "string" || name.trim() === "") {
+ throw new Error(`Configuration field "deploy.targets[${index}].name" must be a non-empty string.`);
+ }
+ if (type !== "local") {
+ throw new Error(`Configuration field "deploy.targets[${index}].type" must be "local".`);
+ }
+ if (typeof targetPath !== "string" || targetPath.trim() === "") {
+ throw new Error(`Configuration field "deploy.targets[${index}].target" must be a non-empty path.`);
+ }
+ return { name: name.trim(), type: "local", target: targetPath.trim() };
+ });
+
+ return { targets: parsedTargets };
+}
+
export function loadPortfolioConfig(
path: string = DEFAULT_CONFIG_PATH,
clock: () => string = DEFAULT_CONFIG.clock
@@ -78,6 +150,8 @@ export function loadPortfolioConfig(
repositoryLimit: readLimit(parsed, "repositoryLimit", DEFAULT_CONFIG.repositoryLimit),
dataDir: readString(parsed, "dataDir", DEFAULT_CONFIG.dataDir),
outputDir: readString(parsed, "outputDir", DEFAULT_CONFIG.outputDir),
+ theme: readTheme(parsed),
+ deploy: readDeploy(parsed),
privacy: readPrivacy(parsed),
clock,
};
diff --git a/src/deploy/index.ts b/src/deploy/index.ts
new file mode 100644
index 0000000..3eb94a4
--- /dev/null
+++ b/src/deploy/index.ts
@@ -0,0 +1,19 @@
+import type { DeployTarget, PortfolioConfig } from "../types.js";
+import { deployLocal } from "./local.js";
+import type { DeployResult } from "./local.js";
+
+export type { DeployResult } from "./local.js";
+
+export function deployToTarget(
+ config: PortfolioConfig,
+ target: DeployTarget
+): DeployResult {
+ if (target.type === "local") {
+ return deployLocal(config, target);
+ }
+ throw new Error(`Unknown deploy adapter "${target.type}".`);
+}
+
+export function deployAll(config: PortfolioConfig): DeployResult[] {
+ return config.deploy.targets.map((target) => deployToTarget(config, target));
+}
diff --git a/src/deploy/local.ts b/src/deploy/local.ts
new file mode 100644
index 0000000..744a8ff
--- /dev/null
+++ b/src/deploy/local.ts
@@ -0,0 +1,61 @@
+import { cpSync, existsSync, mkdirSync, readdirSync } from "node:fs";
+import { isAbsolute, join, relative, resolve } from "node:path";
+import { openDatabase } from "../db/client.js";
+import type { DeployTarget, PortfolioConfig } from "../types.js";
+
+export interface DeployResult {
+ targetName: string;
+ targetPath: string;
+ files: number;
+}
+
+function isPathInside(parent: string, child: string): boolean {
+ const rel = relative(parent, child);
+ return rel === "" || (!rel.startsWith("..") && !isAbsolute(rel));
+}
+
+function countFiles(targetPath: string): number {
+ let total = 0;
+ const walk = (current: string): void => {
+ for (const entry of readdirSync(current, { withFileTypes: true })) {
+ if (entry.isDirectory()) {
+ walk(join(current, entry.name));
+ } else {
+ total++;
+ }
+ }
+ };
+ walk(targetPath);
+ return total;
+}
+
+export function deployLocal(
+ config: PortfolioConfig,
+ target: DeployTarget
+): DeployResult {
+ const indexPath = join(config.outputDir, "index.html");
+ if (!existsSync(indexPath)) {
+ throw new Error(`No published site found at "${config.outputDir}". Run publish first.`);
+ }
+
+ const outputRoot = resolve(config.outputDir);
+ const targetRoot = resolve(target.target);
+ if (isPathInside(outputRoot, targetRoot)) {
+ throw new Error(
+ `Deploy target "${target.name}" must be outside the output directory "${config.outputDir}".`
+ );
+ }
+
+ mkdirSync(target.target, { recursive: true });
+ cpSync(config.outputDir, target.target, { recursive: true });
+ const files = countFiles(target.target);
+
+ const db = openDatabase(config.dataDir, config.clock);
+ try {
+ db.logIngest("deploy", `${target.name} -> ${target.target}`);
+ } finally {
+ db.close();
+ }
+
+ return { targetName: target.name, targetPath: target.target, files };
+}
diff --git a/src/index.ts b/src/index.ts
index aea5c27..5753058 100644
--- a/src/index.ts
+++ b/src/index.ts
@@ -10,13 +10,15 @@ import { openDatabase } from "./db/client.js";
import { DEFAULT_CONFIG, type PortfolioConfig } from "./types.js";
import { DEFAULT_CONFIG_PATH, loadPortfolioConfig } from "./config/loader.js";
import { refreshPortfolio } from "./refresh/run.js";
+import { listBuiltinThemes } from "./theme/palette.js";
+import { deployAll } from "./deploy/index.js";
const program = new Command();
program
.name("engineer-profile")
.description("Build a local engineering portfolio from public repository evidence")
- .version("0.2.0");
+ .version("0.3.0");
function resolveConfig(options: { config?: string; data?: string; output?: string }): PortfolioConfig {
const base = options.config
@@ -149,6 +151,41 @@ addConfigOption(program
for (const error of result.captureErrors) {
console.warn(`Skipped ${error.slug}: ${error.message}`);
}
+ for (const deployed of result.deployed) {
+ console.log(`Deployed ${deployed.targetName}: ${deployed.files} files to ${deployed.targetPath}.`);
+ }
+ }));
+
+program
+ .command("themes")
+ .description("List built-in presentation themes")
+ .action(() => {
+ const themes = listBuiltinThemes();
+ console.log(`Built-in themes: ${themes.length}`);
+ for (const theme of themes) {
+ console.log(` ${theme.name}: ${theme.description}`);
+ }
+ });
+
+addConfigOption(program
+ .command("deploy")
+ .description("Publish the snapshot and copy it to configured deploy targets")
+ .option("-d, --data
", "Data directory")
+ .option("-o, --output ", "Output directory")
+ .action((options) => {
+ const config = resolveConfig(options);
+ mkdirSync(config.dataDir, { recursive: true });
+ const result = publishSite(config);
+ const copied = copyScreenshotsToOutput(config);
+ console.log(`Published ${result.projectCount} projects to ${result.indexPath}.`);
+ console.log(`Copied ${copied} available preview screenshots.`);
+ const results = deployAll(config);
+ if (results.length === 0) {
+ console.log('No deploy targets configured. Add a "deploy.targets" entry to the configuration.');
+ }
+ for (const deployed of results) {
+ console.log(`Deployed ${deployed.targetName}: ${deployed.files} files to ${deployed.targetPath}.`);
+ }
}));
addConfigOption(program
diff --git a/src/publish/site.ts b/src/publish/site.ts
index d382163..95a479b 100644
--- a/src/publish/site.ts
+++ b/src/publish/site.ts
@@ -1,7 +1,8 @@
-import { cpSync, existsSync, mkdirSync, writeFileSync } from "node:fs";
+import { cpSync, existsSync, mkdirSync, readdirSync, writeFileSync } from "node:fs";
import { join } from "node:path";
import { formatChangelogMarkdown } from "../changelog/generator.js";
import { openDatabase } from "../db/client.js";
+import { resolveTheme, themeVariables, type ThemeTokens } from "../theme/palette.js";
import type { ChangelogEntry, PortfolioConfig, ProjectRecord } from "../types.js";
function escapeHtml(text: string): string {
@@ -124,29 +125,14 @@ function projectCard(
`;
}
-function siteCss(): string {
+function siteCss(theme: ThemeTokens): string {
return `
-:root {
- color-scheme: dark;
- --ink: #08111f;
- --ink-soft: #0d1a2d;
- --panel: #112139;
- --panel-strong: #172a46;
- --line: rgba(169, 195, 222, 0.18);
- --text: #f3f7fb;
- --muted: #9db0c7;
- --blue: #67b7ff;
- --blue-soft: #b8dcff;
- --mint: #a7f3d0;
- --orange: #ffb86b;
- --shadow: 0 24px 60px rgba(0, 0, 0, 0.24);
- font-family: Inter, ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;
-}
+${themeVariables(theme)}
* { box-sizing: border-box; }
html { scroll-behavior: smooth; }
body { margin: 0; min-width: 320px; background: var(--ink); color: var(--text); line-height: 1.5; }
a { color: inherit; }
-.site-shell { min-height: 100vh; background: radial-gradient(circle at 82% -10%, rgba(70, 148, 232, 0.2), transparent 34rem), var(--ink); }
+.site-shell { min-height: 100vh; background: var(--glow), var(--ink); }
.container { width: min(1180px, calc(100% - 48px)); margin: 0 auto; }
.site-nav { display: flex; justify-content: space-between; align-items: center; padding: 28px 0; border-bottom: 1px solid var(--line); }
.brand { display: inline-flex; align-items: center; gap: 12px; font: 700 0.9rem/1 "SFMono-Regular", Consolas, monospace; letter-spacing: 0.08em; text-decoration: none; text-transform: uppercase; }
@@ -158,7 +144,7 @@ a { color: inherit; }
.kicker { color: var(--mint); margin: 0 0 22px; }
h1 { max-width: 760px; margin: 0; font-size: clamp(3.2rem, 8vw, 6.4rem); font-weight: 650; letter-spacing: -0.08em; line-height: 0.94; }
.hero-copy { max-width: 530px; margin: 28px 0 0; color: var(--blue-soft); font-size: 1.12rem; }
-.hero-aside { padding: 22px; border: 1px solid var(--line); border-radius: 14px; background: linear-gradient(145deg, rgba(23, 42, 70, 0.92), rgba(13, 26, 45, 0.72)); box-shadow: var(--shadow); }
+.hero-aside { padding: 22px; border: 1px solid var(--line); border-radius: 14px; background: var(--aside-gradient); box-shadow: var(--shadow); }
.aside-index { display: flex; justify-content: space-between; color: var(--orange); font: 0.68rem/1 "SFMono-Regular", Consolas, monospace; letter-spacing: 0.12em; text-transform: uppercase; }
.hero-aside p { margin: 24px 0 4px; color: var(--text); font-size: 1rem; }
.stats-grid { display: grid; grid-template-columns: repeat(4, 1fr); border-top: 1px solid var(--line); border-bottom: 1px solid var(--line); }
@@ -166,19 +152,19 @@ h1 { max-width: 760px; margin: 0; font-size: clamp(3.2rem, 8vw, 6.4rem); font-we
.stat:last-child { border-right: 0; }
.stat strong { display: block; color: var(--text); font-size: 1.8rem; font-weight: 600; letter-spacing: -0.04em; }
.stat span { color: var(--muted); font: 0.7rem/1.3 "SFMono-Regular", Consolas, monospace; letter-spacing: 0.08em; text-transform: uppercase; }
-.audit-panel { display: grid; grid-template-columns: 1fr auto; gap: 20px; align-items: center; margin: 26px 0 80px; padding: 18px 20px; border: 1px solid var(--line); border-radius: 10px; background: rgba(17, 33, 57, 0.66); }
+.audit-panel { display: grid; grid-template-columns: 1fr auto; gap: 20px; align-items: center; margin: 26px 0 80px; padding: 18px 20px; border: 1px solid var(--line); border-radius: 10px; background: var(--audit-bg); }
.audit-copy { color: var(--muted); font-size: 0.88rem; }
.audit-copy strong { color: var(--text); font-weight: 500; }
.audit-time { color: var(--blue); font: 0.7rem/1.4 "SFMono-Regular", Consolas, monospace; text-align: right; }
.index-header { display: flex; justify-content: space-between; align-items: end; gap: 24px; margin-bottom: 24px; }
.index-header h2 { margin: 0; font-size: 2rem; font-weight: 550; letter-spacing: -0.05em; }
.index-header p { max-width: 350px; margin: 0; color: var(--muted); font-size: 0.88rem; text-align: right; }
-.project-card { display: grid; grid-template-columns: minmax(280px, 0.8fr) minmax(0, 1.2fr); overflow: hidden; margin-bottom: 24px; border: 1px solid var(--line); border-radius: 16px; background: linear-gradient(135deg, rgba(23, 42, 70, 0.98), rgba(13, 26, 45, 0.96)); box-shadow: var(--shadow); }
-.project-visual { position: relative; min-height: 310px; background: #0a1525; }
+.project-card { display: grid; grid-template-columns: minmax(280px, 0.8fr) minmax(0, 1.2fr); overflow: hidden; margin-bottom: 24px; border: 1px solid var(--line); border-radius: 16px; background: var(--panel-gradient); box-shadow: var(--shadow); }
+.project-visual { position: relative; min-height: 310px; background: var(--visual); }
.screenshot { display: block; width: 100%; height: 100%; min-height: 310px; object-fit: cover; opacity: 0.9; }
-.placeholder { display: flex; min-height: 310px; align-items: center; justify-content: center; flex-direction: column; gap: 7px; color: var(--blue); background: repeating-linear-gradient(135deg, rgba(103, 183, 255, 0.05), rgba(103, 183, 255, 0.05) 1px, transparent 1px, transparent 14px); }
+.placeholder { display: flex; min-height: 310px; align-items: center; justify-content: center; flex-direction: column; gap: 7px; color: var(--blue); background: repeating-linear-gradient(135deg, var(--stripe), var(--stripe) 1px, transparent 1px, transparent 14px); }
.placeholder small { color: var(--muted); font: 0.68rem/1 "SFMono-Regular", Consolas, monospace; text-transform: uppercase; }
-.visual-label { position: absolute; right: 16px; bottom: 16px; padding: 7px 9px; border: 1px solid rgba(255,255,255,0.18); border-radius: 5px; background: rgba(8, 17, 31, 0.72); color: var(--blue-soft); }
+.visual-label { position: absolute; right: 16px; bottom: 16px; padding: 7px 9px; border: 1px solid var(--label-border); border-radius: 5px; background: var(--label-bg); color: var(--blue-soft); }
.project-body { padding: 30px 34px 32px; }
.card-topline { display: flex; justify-content: space-between; gap: 12px; color: var(--blue); }
.project-body h2 { margin: 18px 0 8px; font-size: 2.1rem; font-weight: 560; letter-spacing: -0.06em; }
@@ -187,7 +173,7 @@ h1 { max-width: 760px; margin: 0; font-size: clamp(3.2rem, 8vw, 6.4rem); font-we
.facts { display: flex; flex-wrap: wrap; gap: 22px; margin: 24px 0 16px; color: var(--muted); font: 0.76rem/1 "SFMono-Regular", Consolas, monospace; }
.facts strong { color: var(--text); font-size: 1rem; font-weight: 600; }
.tags { display: flex; flex-wrap: wrap; gap: 7px; margin-bottom: 26px; }
-.tag { padding: 5px 9px; border: 1px solid rgba(167, 243, 208, 0.26); border-radius: 999px; color: var(--mint); font: 0.68rem/1 "SFMono-Regular", Consolas, monospace; }
+.tag { padding: 5px 9px; border: 1px solid var(--tag-border); border-radius: 999px; color: var(--mint); background: var(--tag-bg); font: 0.68rem/1 "SFMono-Regular", Consolas, monospace; }
.change-log { padding: 18px 0 20px; border-top: 1px solid var(--line); border-bottom: 1px solid var(--line); }
.section-heading { display: flex; justify-content: space-between; color: var(--orange); }
.source-badge { color: var(--muted); }
@@ -197,11 +183,11 @@ h1 { max-width: 760px; margin: 0; font-size: clamp(3.2rem, 8vw, 6.4rem); font-we
.change-preview h3, .change-preview h4 { margin: 12px 0 4px; color: var(--text); font-size: 0.78rem; font-weight: 600; }
.change-preview p { margin: 3px 0; }
.change-preview li { margin: 3px 0 3px 18px; }
-.change-preview code { padding: 2px 4px; border-radius: 3px; color: var(--mint); background: rgba(167, 243, 208, 0.08); font: 0.76rem "SFMono-Regular", Consolas, monospace; }
+.change-preview code { padding: 2px 4px; border-radius: 3px; color: var(--mint); background: var(--code-bg); font: 0.76rem "SFMono-Regular", Consolas, monospace; }
.text-link { display: inline-block; margin-top: 14px; color: var(--blue); font: 0.73rem/1 "SFMono-Regular", Consolas, monospace; text-decoration: none; text-transform: uppercase; }
.card-actions { display: flex; flex-wrap: wrap; gap: 10px; margin-top: 22px; }
.button { display: inline-block; padding: 10px 14px; border-radius: 7px; font: 0.72rem/1 "SFMono-Regular", Consolas, monospace; letter-spacing: 0.04em; text-decoration: none; text-transform: uppercase; }
-.button.primary { background: var(--blue); color: var(--ink); }
+.button.primary { background: var(--blue); color: var(--button-text); }
.button.primary:hover { background: var(--blue-soft); }
.button.secondary { border: 1px solid var(--line); color: var(--text); }
.button.secondary:hover { border-color: var(--blue); color: var(--blue); }
@@ -210,7 +196,7 @@ h1 { max-width: 760px; margin: 0; font-size: clamp(3.2rem, 8vw, 6.4rem); font-we
.source-trail h2 { margin: 0 0 8px; font-size: 1.2rem; font-weight: 550; }
.source-trail p { margin: 0; color: var(--muted); font-size: 0.86rem; }
.audit-list { margin: 0; padding: 0; list-style: none; }
-.audit-list li { display: grid; grid-template-columns: 150px 72px 1fr; gap: 12px; padding: 8px 0; border-bottom: 1px solid rgba(169, 195, 222, 0.1); color: var(--muted); font: 0.72rem/1.4 "SFMono-Regular", Consolas, monospace; }
+.audit-list li { display: grid; grid-template-columns: 150px 72px 1fr; gap: 12px; padding: 8px 0; border-bottom: 1px solid var(--line-soft); color: var(--muted); font: 0.72rem/1.4 "SFMono-Regular", Consolas, monospace; }
.audit-list time { color: var(--blue); }
.audit-list strong { color: var(--orange); font-weight: 500; text-transform: uppercase; }
.site-footer { display: flex; justify-content: space-between; gap: 20px; padding: 24px 0 40px; border-top: 1px solid var(--line); color: var(--muted); font: 0.7rem/1.4 "SFMono-Regular", Consolas, monospace; }
@@ -239,10 +225,47 @@ h1 { max-width: 760px; margin: 0; font-size: clamp(3.2rem, 8vw, 6.4rem); font-we
`;
}
+export interface SiteManifestProject {
+ slug: string;
+ name: string;
+ url: string;
+ changelogFile: string | null;
+}
+
+export interface SiteManifest {
+ formatVersion: 1;
+ generatedAt: string;
+ title: string;
+ owner: string;
+ theme: string;
+ projectCount: number;
+ projects: SiteManifestProject[];
+ files: string[];
+ screenshots: string[];
+}
+
export interface PublishResult {
indexPath: string;
+ manifestPath: string;
projectCount: number;
generatedAt: string;
+ theme: string;
+}
+
+function relativePosixPaths(outputDir: string): string[] {
+ const paths: string[] = [];
+ const walk = (current: string, prefix: string): void => {
+ for (const entry of readdirSync(current, { withFileTypes: true })) {
+ const relative = prefix ? `${prefix}/${entry.name}` : entry.name;
+ if (entry.isDirectory()) {
+ walk(join(current, entry.name), relative);
+ } else {
+ paths.push(relative);
+ }
+ }
+ };
+ walk(outputDir, "");
+ return paths.sort();
}
export function publishSite(config: PortfolioConfig): PublishResult {
@@ -250,6 +273,7 @@ export function publishSite(config: PortfolioConfig): PublishResult {
try {
const projects = db.listProjects(true);
const generatedAt = config.clock();
+ const theme = resolveTheme(config.theme);
const views = projects.map((project) => ({
project,
changelog: db.getChangelog(project.id),
@@ -279,7 +303,7 @@ export function publishSite(config: PortfolioConfig): PublishResult {
${escapeHtml(config.title)} / Portfolio
-
+
@@ -295,7 +319,7 @@ export function publishSite(config: PortfolioConfig): PublishResult {
${escapeHtml(config.tagline)}. Each project stays connected to its repository, commits, releases, and preview.
@@ -344,8 +368,35 @@ export function publishSite(config: PortfolioConfig): PublishResult {
writeFileSync(join(config.outputDir, `${view.project.slug}-changelog.md`), markdown, "utf-8");
}
+ copyScreenshotsToOutput(config);
+ const publishedFiles = relativePosixPaths(config.outputDir);
+ const manifest: SiteManifest = {
+ formatVersion: 1,
+ generatedAt,
+ title: config.title,
+ owner: config.owner,
+ theme: theme.name,
+ projectCount: projects.length,
+ projects: views.map((view) => ({
+ slug: view.project.slug,
+ name: view.project.name,
+ url: view.project.url,
+ changelogFile: view.changelog.length > 0 ? `${view.project.slug}-changelog.md` : null,
+ })),
+ files: [...new Set([...publishedFiles, "site-manifest.json"])].sort(),
+ screenshots: publishedFiles.filter((file) => file.startsWith("assets/screenshots/")),
+ };
+ const manifestPath = join(config.outputDir, "site-manifest.json");
+ writeFileSync(manifestPath, `${JSON.stringify(manifest, null, 2)}\n`, "utf-8");
+
db.logIngest("publish", `${projects.length} projects -> ${indexPath}`);
- return { indexPath, projectCount: projects.length, generatedAt };
+ return {
+ indexPath,
+ manifestPath,
+ projectCount: projects.length,
+ generatedAt,
+ theme: theme.name,
+ };
} finally {
db.close();
}
diff --git a/src/refresh/run.ts b/src/refresh/run.ts
index acc9e53..2a390c2 100644
--- a/src/refresh/run.ts
+++ b/src/refresh/run.ts
@@ -1,6 +1,7 @@
import { ingestOwnerRepos } from "../ingest/orchestrator.js";
import { captureAllProjects } from "../preview/capture.js";
import { copyScreenshotsToOutput, publishSite, type PublishResult } from "../publish/site.js";
+import { deployAll, type DeployResult } from "../deploy/index.js";
import type { FixtureData } from "../ingest/orchestrator.js";
import { DEFAULT_CONFIG, type PortfolioConfig } from "../types.js";
@@ -14,6 +15,7 @@ export interface RefreshResult {
captured: number;
copiedScreenshots: number;
captureErrors: Array<{ slug: string; message: string }>;
+ deployed: DeployResult[];
published: PublishResult;
}
@@ -36,12 +38,15 @@ export async function refreshPortfolio(
(slug, error) => captureErrors.push({ slug, message: error.message })
);
const published = publishSite(config);
+ const copiedScreenshots = copyScreenshotsToOutput(config);
+ const deployed = deployAll(config);
return {
ingested: ingested.length,
captured: captured.length,
- copiedScreenshots: copyScreenshotsToOutput(config),
+ copiedScreenshots,
captureErrors,
+ deployed,
published,
};
}
diff --git a/src/theme/palette.ts b/src/theme/palette.ts
new file mode 100644
index 0000000..092d960
--- /dev/null
+++ b/src/theme/palette.ts
@@ -0,0 +1,234 @@
+import { DEFAULT_THEME, type ThemeConfig } from "../types.js";
+
+export type ThemeMode = "dark" | "light";
+
+export interface ThemeTokens {
+ name: string;
+ mode: ThemeMode;
+ ink: string;
+ inkSoft: string;
+ panel: string;
+ panelStrong: string;
+ line: string;
+ lineSoft: string;
+ text: string;
+ muted: string;
+ blue: string;
+ blueSoft: string;
+ buttonText: string;
+ mint: string;
+ orange: string;
+ shadow: string;
+ visual: string;
+ glow: string;
+ panelGradient: string;
+ asideGradient: string;
+ labelBg: string;
+ labelBorder: string;
+ auditBg: string;
+ codeBg: string;
+ tagBg: string;
+ tagBorder: string;
+ stripe: string;
+ radius: string;
+ font: string;
+}
+
+interface BuiltinPalette extends Omit {
+ mode: ThemeMode;
+}
+
+const SHARED_FONT =
+ 'Inter, ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif';
+
+const BUILTIN_PALETTES: Record = {
+ "deep-space": {
+ mode: "dark",
+ ink: "#08111f",
+ inkSoft: "#0d1a2d",
+ panel: "#112139",
+ panelStrong: "#172a46",
+ line: "rgba(169, 195, 222, 0.18)",
+ lineSoft: "rgba(169, 195, 222, 0.1)",
+ text: "#f3f7fb",
+ muted: "#9db0c7",
+ blue: "#67b7ff",
+ blueSoft: "#b8dcff",
+ buttonText: "#08111f",
+ mint: "#a7f3d0",
+ orange: "#ffb86b",
+ shadow: "0 24px 60px rgba(0, 0, 0, 0.24)",
+ visual: "#0a1525",
+ glow: "radial-gradient(circle at 82% -10%, rgba(70, 148, 232, 0.2), transparent 34rem)",
+ panelGradient: "linear-gradient(135deg, rgba(23, 42, 70, 0.98), rgba(13, 26, 45, 0.96))",
+ asideGradient: "linear-gradient(145deg, rgba(23, 42, 70, 0.92), rgba(13, 26, 45, 0.72))",
+ labelBg: "rgba(8, 17, 31, 0.72)",
+ labelBorder: "rgba(255, 255, 255, 0.18)",
+ auditBg: "rgba(17, 33, 57, 0.66)",
+ codeBg: "rgba(167, 243, 208, 0.08)",
+ tagBg: "rgba(167, 243, 208, 0.06)",
+ tagBorder: "rgba(167, 243, 208, 0.26)",
+ stripe: "rgba(103, 183, 255, 0.05)",
+ radius: "16px",
+ font: SHARED_FONT,
+ },
+ paper: {
+ mode: "light",
+ ink: "#f6f8fb",
+ inkSoft: "#eef1f6",
+ panel: "#ffffff",
+ panelStrong: "#e9eef5",
+ line: "rgba(15, 23, 42, 0.14)",
+ lineSoft: "rgba(15, 23, 42, 0.08)",
+ text: "#0f172a",
+ muted: "#5b6b82",
+ blue: "#0f6bbd",
+ blueSoft: "#1d4ed8",
+ buttonText: "#ffffff",
+ mint: "#0f766e",
+ orange: "#b45309",
+ shadow: "0 24px 60px rgba(15, 23, 42, 0.14)",
+ visual: "#e3e9f2",
+ glow: "radial-gradient(circle at 82% -10%, rgba(15, 107, 189, 0.14), transparent 34rem)",
+ panelGradient: "linear-gradient(135deg, #ffffff, #eef2f8)",
+ asideGradient: "linear-gradient(145deg, #ffffff, #edf1f6)",
+ labelBg: "rgba(255, 255, 255, 0.8)",
+ labelBorder: "rgba(15, 23, 42, 0.12)",
+ auditBg: "rgba(255, 255, 255, 0.72)",
+ codeBg: "rgba(15, 118, 110, 0.08)",
+ tagBg: "rgba(15, 118, 110, 0.06)",
+ tagBorder: "rgba(15, 118, 110, 0.24)",
+ stripe: "rgba(15, 107, 189, 0.08)",
+ radius: "16px",
+ font: SHARED_FONT,
+ },
+};
+
+const BUILTIN_DESCRIPTIONS: Record = {
+ "deep-space": "Dark palette with blue accents. Default.",
+ paper: "Light palette with dark text and strong contrast.",
+};
+
+export interface ThemeCatalogEntry {
+ name: string;
+ description: string;
+}
+
+export function listBuiltinThemes(): ThemeCatalogEntry[] {
+ return Object.keys(BUILTIN_PALETTES).map((name) => ({
+ name,
+ description: BUILTIN_DESCRIPTIONS[name] ?? "Built-in theme.",
+ }));
+}
+
+export function isBuiltinTheme(name: string): boolean {
+ return name in BUILTIN_PALETTES;
+}
+
+const HEX_COLOR_RE = /^#?[0-9a-f]{3}([0-9a-f]{3})?$/i;
+
+export function isValidHexColor(value: string): boolean {
+ return HEX_COLOR_RE.test(value.trim());
+}
+
+function normalizeHexColor(value: string): string {
+ let hex = value.trim().replace(/^#/, "").toLowerCase();
+ if (hex.length === 3) {
+ hex = hex
+ .split("")
+ .map((channel) => channel + channel)
+ .join("");
+ }
+ return `#${hex}`;
+}
+
+function hexToRgb(hex: string): [number, number, number] | null {
+ const normalized = hex.replace(/^#/, "");
+ const match = /^[0-9a-f]{6}$/i.exec(normalized);
+ if (!match) return null;
+ const value = Number.parseInt(normalized, 16);
+ return [(value >> 16) & 255, (value >> 8) & 255, value & 255];
+}
+
+function rgbToHex(rgb: [number, number, number]): string {
+ return `#${rgb.map((channel) => channel.toString(16).padStart(2, "0")).join("")}`;
+}
+
+function mixHex(source: string, target: string, amount: number): string {
+ const from = hexToRgb(source) ?? [0, 0, 0];
+ const to = hexToRgb(target) ?? [255, 255, 255];
+ const mixed = from.map((channel, index) =>
+ Math.round(channel + (to[index] - channel) * amount)
+ ) as [number, number, number];
+ return rgbToHex(mixed);
+}
+
+function rgbaFromHex(hex: string, alpha: number): string {
+ const [red, green, blue] = hexToRgb(hex) ?? [0, 0, 0];
+ return `rgba(${red}, ${green}, ${blue}, ${alpha})`;
+}
+
+function glowFromAccent(accent: string, mode: ThemeMode): string {
+ const alpha = mode === "dark" ? 0.2 : 0.14;
+ return `radial-gradient(circle at 82% -10%, ${rgbaFromHex(accent, alpha)}, transparent 34rem)`;
+}
+
+function stripeFromAccent(accent: string, mode: ThemeMode): string {
+ const alpha = mode === "dark" ? 0.05 : 0.08;
+ return rgbaFromHex(accent, alpha);
+}
+
+export function resolveTheme(config?: ThemeConfig): ThemeTokens {
+ const requestedName = config?.name ?? DEFAULT_THEME.name;
+ const baseName = isBuiltinTheme(requestedName) ? requestedName : DEFAULT_THEME.name;
+ const base = BUILTIN_PALETTES[baseName];
+
+ const tokens: ThemeTokens = {
+ ...base,
+ name: baseName,
+ };
+
+ if (config?.accent) {
+ tokens.blue = normalizeHexColor(config.accent);
+ tokens.blueSoft = mixHex(tokens.blue, base.mode === "dark" ? "#ffffff" : "#000000", 0.5);
+ tokens.glow = glowFromAccent(tokens.blue, base.mode);
+ tokens.stripe = stripeFromAccent(tokens.blue, base.mode);
+ }
+ if (config?.radius) tokens.radius = config.radius;
+ if (config?.font) tokens.font = config.font;
+
+ return tokens;
+}
+
+export function themeVariables(tokens: ThemeTokens): string {
+ return `:root {
+ color-scheme: ${tokens.mode};
+ --ink: ${tokens.ink};
+ --ink-soft: ${tokens.inkSoft};
+ --panel: ${tokens.panel};
+ --panel-strong: ${tokens.panelStrong};
+ --line: ${tokens.line};
+ --line-soft: ${tokens.lineSoft};
+ --text: ${tokens.text};
+ --muted: ${tokens.muted};
+ --blue: ${tokens.blue};
+ --blue-soft: ${tokens.blueSoft};
+ --button-text: ${tokens.buttonText};
+ --mint: ${tokens.mint};
+ --orange: ${tokens.orange};
+ --shadow: ${tokens.shadow};
+ --visual: ${tokens.visual};
+ --glow: ${tokens.glow};
+ --panel-gradient: ${tokens.panelGradient};
+ --aside-gradient: ${tokens.asideGradient};
+ --label-bg: ${tokens.labelBg};
+ --label-border: ${tokens.labelBorder};
+ --audit-bg: ${tokens.auditBg};
+ --code-bg: ${tokens.codeBg};
+ --tag-bg: ${tokens.tagBg};
+ --tag-border: ${tokens.tagBorder};
+ --stripe: ${tokens.stripe};
+ --radius: ${tokens.radius};
+ --font: ${tokens.font};
+}`;
+}
diff --git a/src/types.ts b/src/types.ts
index 92c30da..0a7d2cb 100644
--- a/src/types.ts
+++ b/src/types.ts
@@ -78,6 +78,23 @@ export interface PrivacyConfig {
maxCommitsPerProject: number;
}
+export interface ThemeConfig {
+ name: string;
+ accent?: string;
+ radius?: string;
+ font?: string;
+}
+
+export interface DeployTarget {
+ name: string;
+ type: "local";
+ target: string;
+}
+
+export interface DeployConfig {
+ targets: DeployTarget[];
+}
+
export interface PortfolioConfig {
owner: string;
title: string;
@@ -85,6 +102,8 @@ export interface PortfolioConfig {
repositoryLimit?: number;
dataDir: string;
outputDir: string;
+ theme: ThemeConfig;
+ deploy: DeployConfig;
privacy: PrivacyConfig;
clock: () => string;
}
@@ -95,6 +114,14 @@ export const DEFAULT_PRIVACY: PrivacyConfig = {
maxCommitsPerProject: 50,
};
+export const DEFAULT_THEME: ThemeConfig = {
+ name: "deep-space",
+};
+
+export const DEFAULT_DEPLOY: DeployConfig = {
+ targets: [],
+};
+
export const DEFAULT_CONFIG = {
owner: "demo-engineer",
title: "EngineerProfile",
@@ -102,6 +129,8 @@ export const DEFAULT_CONFIG = {
repositoryLimit: 5,
dataDir: "data",
outputDir: "output",
+ theme: DEFAULT_THEME,
+ deploy: DEFAULT_DEPLOY,
privacy: DEFAULT_PRIVACY,
clock: () => new Date().toISOString(),
} satisfies PortfolioConfig;
diff --git a/tests/config.test.ts b/tests/config.test.ts
index 44e2720..dab3e1d 100644
--- a/tests/config.test.ts
+++ b/tests/config.test.ts
@@ -37,4 +37,49 @@ describe("portfolio configuration", () => {
'Configuration field "repositoryLimit" must be an integer from 1 to 100.'
);
});
+
+ it("loads theme and deploy settings", () => {
+ mkdirSync(TEST_DIR, { recursive: true });
+ writeFileSync(TEST_FILE, JSON.stringify({
+ theme: { name: "paper", accent: "#0f6bbd", radius: "20px" },
+ deploy: { targets: [{ name: "public", type: "local", target: "output/deploy" }] },
+ }));
+
+ const config = loadPortfolioConfig(TEST_FILE);
+ expect(config.theme.name).toBe("paper");
+ expect(config.theme.accent).toBe("#0f6bbd");
+ expect(config.theme.radius).toBe("20px");
+ expect(config.deploy.targets).toEqual([
+ { name: "public", type: "local", target: "output/deploy" },
+ ]);
+ });
+
+ it("rejects unknown theme names", () => {
+ mkdirSync(TEST_DIR, { recursive: true });
+ writeFileSync(TEST_FILE, JSON.stringify({ theme: { name: "vaporwave" } }));
+
+ expect(() => loadPortfolioConfig(TEST_FILE)).toThrow(
+ 'Configuration field "theme.name" must be one of: deep-space, paper.'
+ );
+ });
+
+ it("rejects invalid accent colors", () => {
+ mkdirSync(TEST_DIR, { recursive: true });
+ writeFileSync(TEST_FILE, JSON.stringify({ theme: { name: "deep-space", accent: "blue" } }));
+
+ expect(() => loadPortfolioConfig(TEST_FILE)).toThrow(
+ 'Configuration field "theme.accent" must be a hex color like "#67b7ff".'
+ );
+ });
+
+ it("rejects unsupported deploy adapter types", () => {
+ mkdirSync(TEST_DIR, { recursive: true });
+ writeFileSync(TEST_FILE, JSON.stringify({
+ deploy: { targets: [{ name: "netlify", type: "remote", target: "x" }] },
+ }));
+
+ expect(() => loadPortfolioConfig(TEST_FILE)).toThrow(
+ 'Configuration field "deploy.targets[0].type" must be "local".'
+ );
+ });
});
diff --git a/tests/deploy.test.ts b/tests/deploy.test.ts
new file mode 100644
index 0000000..8ec9292
--- /dev/null
+++ b/tests/deploy.test.ts
@@ -0,0 +1,107 @@
+import { describe, it, expect, beforeEach, afterEach } from "vitest";
+import { existsSync, mkdirSync, readdirSync, rmSync, writeFileSync } from "node:fs";
+import { join } from "node:path";
+import { deployAll, deployToTarget } from "../src/deploy/index.js";
+import { openDatabase } from "../src/db/client.js";
+import { DEFAULT_CONFIG } from "../src/types.js";
+
+const TEST_DATA = join("data", "test-deploy");
+const TEST_OUTPUT = join("output", "test-deploy");
+const TEST_TARGET = join("deploy", "test-deploy-target");
+
+describe("deploy adapters", () => {
+ beforeEach(() => {
+ rmSync(TEST_DATA, { recursive: true, force: true });
+ rmSync(TEST_OUTPUT, { recursive: true, force: true });
+ rmSync(TEST_TARGET, { recursive: true, force: true });
+ });
+
+ afterEach(() => {
+ rmSync(TEST_DATA, { recursive: true, force: true });
+ rmSync(TEST_OUTPUT, { recursive: true, force: true });
+ rmSync(TEST_TARGET, { recursive: true, force: true });
+ });
+
+ function publishedConfig() {
+ const config = {
+ ...DEFAULT_CONFIG,
+ dataDir: TEST_DATA,
+ outputDir: TEST_OUTPUT,
+ deploy: {
+ targets: [{ name: "public", type: "local" as const, target: TEST_TARGET }],
+ },
+ clock: () => "2026-07-31T00:00:00.000Z",
+ };
+ return config;
+ }
+
+ function writePublishedSite() {
+ mkdirSync(join(TEST_OUTPUT, "assets", "screenshots"), { recursive: true });
+ writeFileSync(join(TEST_OUTPUT, "index.html"), "portfolio", "utf-8");
+ writeFileSync(join(TEST_OUTPUT, "site-manifest.json"), "{}", "utf-8");
+ }
+
+ it("copies the published site into a local target", () => {
+ writePublishedSite();
+ const config = publishedConfig();
+ const result = deployAll(config);
+
+ expect(result).toHaveLength(1);
+ expect(result[0].targetName).toBe("public");
+ expect(result[0].targetPath).toBe(TEST_TARGET);
+ expect(result[0].files).toBeGreaterThanOrEqual(2);
+ expect(existsSync(join(TEST_TARGET, "index.html"))).toBe(true);
+ expect(existsSync(join(TEST_TARGET, "site-manifest.json"))).toBe(true);
+ });
+
+ it("reports a publish-first error when output is missing", () => {
+ const config = publishedConfig();
+ expect(() => deployToTarget(config, config.deploy.targets[0])).toThrow(
+ /No published site found/
+ );
+ });
+
+ it("rejects a target inside the output directory", () => {
+ writePublishedSite();
+ const config = {
+ ...publishedConfig(),
+ deploy: {
+ targets: [
+ { name: "nested", type: "local" as const, target: join(TEST_OUTPUT, "nested") },
+ ],
+ },
+ };
+ expect(() => deployAll(config)).toThrow(/must be outside the output directory/);
+ });
+
+ it("returns an empty result list without targets", () => {
+ const config = { ...publishedConfig(), deploy: { targets: [] } };
+ expect(deployAll(config)).toEqual([]);
+ });
+
+ it("records a deploy audit entry", () => {
+ writePublishedSite();
+ const config = publishedConfig();
+ deployAll(config);
+
+ const db = openDatabase(TEST_DATA, config.clock);
+ const log = db.getIngestLog(5);
+ db.close();
+ expect(log[0].action).toBe("deploy");
+ expect(log[0].detail).toContain("public");
+ });
+
+ it("copies subdirectories inside the output directory", () => {
+ writePublishedSite();
+ writeFileSync(
+ join(TEST_OUTPUT, "assets", "screenshots", "demo-engineer-signal-router.png"),
+ "png",
+ "utf-8"
+ );
+ const config = publishedConfig();
+ deployAll(config);
+
+ const targetFiles = readdirSync(join(TEST_TARGET, "assets", "screenshots"));
+ expect(targetFiles).toContain("demo-engineer-signal-router.png");
+ });
+});
diff --git a/tests/manifest.test.ts b/tests/manifest.test.ts
new file mode 100644
index 0000000..74190bb
--- /dev/null
+++ b/tests/manifest.test.ts
@@ -0,0 +1,90 @@
+import { describe, it, expect, beforeEach, afterEach } from "vitest";
+import { existsSync, readFileSync, rmSync } from "node:fs";
+import { join } from "node:path";
+import { ingestRepository } from "../src/ingest/orchestrator.js";
+import { publishSite } from "../src/publish/site.js";
+import { loadFixtureRepo, loadFixtureCommits, loadFixtureReleases } from "../src/fixtures/loader.js";
+import { DEFAULT_CONFIG, type PortfolioConfig } from "../src/types.js";
+
+const TEST_DATA = join("data", "test-manifest");
+const TEST_OUTPUT = join("output", "test-manifest");
+
+function fixtureConfig(): PortfolioConfig {
+ return {
+ ...DEFAULT_CONFIG,
+ dataDir: TEST_DATA,
+ outputDir: TEST_OUTPUT,
+ theme: { name: "paper", accent: "#0f6bbd" },
+ clock: () => "2026-07-31T00:00:00.000Z",
+ };
+}
+
+async function ingestSignalRouter(config: PortfolioConfig) {
+ await ingestRepository(
+ config,
+ { owner: "demo-engineer", repo: "signal-router" },
+ {
+ repo: loadFixtureRepo("signal-router"),
+ commits: loadFixtureCommits("signal-router"),
+ releases: loadFixtureReleases("signal-router"),
+ }
+ );
+}
+
+describe("site manifest", () => {
+ beforeEach(() => {
+ rmSync(TEST_DATA, { recursive: true, force: true });
+ rmSync(TEST_OUTPUT, { recursive: true, force: true });
+ });
+
+ afterEach(() => {
+ rmSync(TEST_DATA, { recursive: true, force: true });
+ rmSync(TEST_OUTPUT, { recursive: true, force: true });
+ });
+
+ it("writes a versioned manifest with published project facts", async () => {
+ const config = fixtureConfig();
+ await ingestSignalRouter(config);
+ const result = publishSite(config);
+
+ expect(existsSync(result.manifestPath)).toBe(true);
+ const manifest = JSON.parse(readFileSync(result.manifestPath, "utf-8")) as {
+ formatVersion: number;
+ theme: string;
+ projectCount: number;
+ projects: Array<{ slug: string; url: string; changelogFile: string | null }>;
+ files: string[];
+ screenshots: string[];
+ };
+
+ expect(manifest.formatVersion).toBe(1);
+ expect(manifest.theme).toBe("paper");
+ expect(manifest.projectCount).toBe(1);
+ expect(manifest.projects[0].slug).toBe("demo-engineer-signal-router");
+ expect(manifest.projects[0].url).toBe("https://github.com/demo-engineer/signal-router");
+ expect(manifest.projects[0].changelogFile).toBe("demo-engineer-signal-router-changelog.md");
+ expect(manifest.files).toContain("index.html");
+ expect(manifest.files).toContain("site-manifest.json");
+ expect(Array.isArray(manifest.screenshots)).toBe(true);
+ });
+
+ it("produces a deterministic manifest for the same snapshot", async () => {
+ const config = fixtureConfig();
+ await ingestSignalRouter(config);
+ publishSite(config);
+ const first = readFileSync(join(TEST_OUTPUT, "site-manifest.json"), "utf-8");
+ publishSite(config);
+ const second = readFileSync(join(TEST_OUTPUT, "site-manifest.json"), "utf-8");
+ expect(first).toBe(second);
+ });
+
+ it("reflects an empty portfolio with zero projects", async () => {
+ const config = fixtureConfig();
+ const result = publishSite(config);
+ const manifest = JSON.parse(readFileSync(result.manifestPath, "utf-8")) as {
+ formatVersion: number;
+ projectCount: number;
+ };
+ expect(manifest.projectCount).toBe(0);
+ });
+});
diff --git a/tests/theme.test.ts b/tests/theme.test.ts
new file mode 100644
index 0000000..6468635
--- /dev/null
+++ b/tests/theme.test.ts
@@ -0,0 +1,91 @@
+import { describe, it, expect } from "vitest";
+import {
+ listBuiltinThemes,
+ isBuiltinTheme,
+ isValidHexColor,
+ resolveTheme,
+ themeVariables,
+} from "../src/theme/palette.js";
+import { DEFAULT_THEME } from "../src/types.js";
+
+describe("theme catalog", () => {
+ it("exposes deterministic built-in themes", () => {
+ const themes = listBuiltinThemes();
+ const names = themes.map((theme) => theme.name);
+ expect(names).toEqual(["deep-space", "paper"]);
+ for (const theme of themes) {
+ expect(theme.description.length).toBeGreaterThan(0);
+ }
+ });
+
+ it("recognizes built-in theme names", () => {
+ expect(isBuiltinTheme("deep-space")).toBe(true);
+ expect(isBuiltinTheme("paper")).toBe(true);
+ expect(isBuiltinTheme("vaporwave")).toBe(false);
+ });
+});
+
+describe("hex color validation", () => {
+ it("accepts short and long hex forms", () => {
+ expect(isValidHexColor("#fff")).toBe(true);
+ expect(isValidHexColor("#67b7ff")).toBe(true);
+ expect(isValidHexColor("67b7ff")).toBe(true);
+ });
+
+ it("rejects non-hex values", () => {
+ expect(isValidHexColor("red")).toBe(false);
+ expect(isValidHexColor("#12")).toBe(false);
+ expect(isValidHexColor("#ggg")).toBe(false);
+ });
+});
+
+describe("resolveTheme", () => {
+ it("defaults to the configured default theme", () => {
+ const tokens = resolveTheme();
+ expect(tokens.name).toBe(DEFAULT_THEME.name);
+ expect(tokens.mode).toBe("dark");
+ });
+
+ it("resolves a requested built-in theme", () => {
+ const tokens = resolveTheme({ name: "paper" });
+ expect(tokens.name).toBe("paper");
+ expect(tokens.mode).toBe("light");
+ });
+
+ it("falls back to the default theme for unknown names", () => {
+ const tokens = resolveTheme({ name: "not-a-theme" });
+ expect(tokens.name).toBe(DEFAULT_THEME.name);
+ });
+
+ it("applies an accent override to derived tokens", () => {
+ const base = resolveTheme({ name: "deep-space" });
+ const tokens = resolveTheme({ name: "deep-space", accent: "#ff0000" });
+ expect(tokens.blue).toBe("#ff0000");
+ expect(tokens.blue).not.toBe(base.blue);
+ expect(tokens.glow).toContain("rgba(255, 0, 0,");
+ expect(tokens.stripe).toContain("rgba(255, 0, 0,");
+ });
+
+ it("applies radius and font overrides", () => {
+ const tokens = resolveTheme({ name: "paper", radius: "20px", font: "Georgia, serif" });
+ expect(tokens.radius).toBe("20px");
+ expect(tokens.font).toBe("Georgia, serif");
+ });
+});
+
+describe("themeVariables", () => {
+ it("emits a :root block with color-scheme and core tokens", () => {
+ const tokens = resolveTheme({ name: "deep-space" });
+ const css = themeVariables(tokens);
+ expect(css).toContain(":root {");
+ expect(css).toContain("color-scheme: dark");
+ expect(css).toContain("--blue: #67b7ff");
+ expect(css).toContain("--font:");
+ });
+
+ it("emits deterministic output for the same theme", () => {
+ const first = themeVariables(resolveTheme({ name: "paper" }));
+ const second = themeVariables(resolveTheme({ name: "paper" }));
+ expect(first).toBe(second);
+ });
+});
From 020fd91313707c29803a563a4b02985566f7c515 Mon Sep 17 00:00:00 2001
From: DanieCuevas <43822444+DanielCuevas1208@users.noreply.github.com>
Date: Mon, 3 Aug 2026 23:31:37 -0700
Subject: [PATCH 2/7] feat: extend engineer profile
---
.github/workflows/ci.yml | 18 +++---
CONTRIBUTING.md | 3 +
README.md | 53 +++++++++++-----
docs/demo-index.png | Bin 0 -> 666784 bytes
docs/theme-gallery.png | Bin 0 -> 106732 bytes
engineer-profile.config.json | 9 +++
package.json | 1 +
scripts/verify-deploy.mjs | 26 ++++++++
scripts/verify-manifest.mjs | 70 ++++++++++++++++++++
src/deploy/local.ts | 41 ++++++++----
src/index.ts | 34 ++++++++--
src/publish/site.ts | 31 +++++++--
src/theme/gallery.ts | 120 +++++++++++++++++++++++++++++++++++
src/theme/palette.ts | 93 +++++++++++++++++++--------
tests/config.test.ts | 2 +-
tests/deploy.test.ts | 26 ++++++++
tests/deterministic.test.ts | 3 +
tests/gallery.test.ts | 61 ++++++++++++++++++
tests/manifest.test.ts | 10 ++-
tests/theme.test.ts | 3 +-
20 files changed, 523 insertions(+), 81 deletions(-)
create mode 100644 docs/demo-index.png
create mode 100644 docs/theme-gallery.png
create mode 100644 scripts/verify-deploy.mjs
create mode 100644 scripts/verify-manifest.mjs
create mode 100644 src/theme/gallery.ts
create mode 100644 tests/gallery.test.ts
diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml
index bcf9a06..f9af3e0 100644
--- a/.github/workflows/ci.yml
+++ b/.github/workflows/ci.yml
@@ -47,16 +47,14 @@ jobs:
- name: Verify theme catalog
run: node dist/index.js themes
- - name: Verify site manifest
- run: node -e '
- const fs = require("fs");
- const manifest = JSON.parse(fs.readFileSync("output/site-manifest.json", "utf8"));
- if (manifest.formatVersion !== 1) process.exit(1);
- if (typeof manifest.projectCount !== "number") process.exit(1);
- if (manifest.projectCount !== 2) process.exit(1);
- if (!manifest.files.includes("index.html")) process.exit(1);
- console.log("Manifest ok: " + manifest.projectCount + " projects, theme " + manifest.theme);
- '
+ - name: Verify site manifest and theme gallery
+ run: node scripts/verify-manifest.mjs
+
+ - name: Verify local deploy
+ run: node dist/index.js deploy
+
+ - name: Check deployed snapshot
+ run: node scripts/verify-deploy.mjs
- name: Store demo output
if: success()
diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md
index c23ff85..33fb112 100644
--- a/CONTRIBUTING.md
+++ b/CONTRIBUTING.md
@@ -15,7 +15,10 @@ npm test
Use fixtures for changes that need repeatable data.
Do not add credentials, private repository data, or generated output.
Run `node dist/index.js themes` after theme changes.
+Run `node dist/index.js themes --preview` after gallery changes.
Verify `output/site-manifest.json` after publish changes.
+Check the deploy sync with `node dist/index.js deploy`.
+Run `node scripts/verify-manifest.mjs` and `node scripts/verify-deploy.mjs` after CI-only changes.
## Pull requests
diff --git a/README.md b/README.md
index 344fc5c..ee99ca9 100644
--- a/README.md
+++ b/README.md
@@ -12,7 +12,9 @@ paths in SQLite. It publishes a static site from these records.
- Capture repeatable project previews with Playwright.
- Hide projects and redact author emails before publication.
- Choose a built-in theme and override accent, radius, and font.
+- Compare every theme in a generated gallery page.
- Deploy the snapshot to configured local targets.
+- Sync targets and remove stale files before verification.
- Record a machine-readable manifest with every publish.
- Run one configured refresh from a scheduled workflow.
@@ -43,15 +45,15 @@ flowchart LR
| --- | --- |
| `engineer-profile.config.json` | Store owner, presentation, refresh, theme, deploy, and privacy settings. |
| `src/config/` | Validate checked-in JSON and merge safe defaults. |
-| `src/theme/` | Resolve built-in themes and emit CSS variables. |
+| `src/theme/` | Resolve built-in themes, emit CSS variables, and render a theme gallery. |
| `src/refresh/` | Coordinate ingest, best-effort capture, static publishing, and deploy. |
| `src/ingest/` | Fetch public GitHub data and map it to records. |
| `src/db/` | Store projects, commits, changelogs, and audit events. |
| `src/changelog/` | Prefer release notes and fall back to commit groups. |
| `src/privacy/` | Hide projects and block sensitive commit messages. |
| `src/preview/` | Capture fixed viewport screenshots with Playwright. |
-| `src/publish/` | Render HTML, changelog files, the site manifest, and preview assets. |
-| `src/deploy/` | Copy published output to configured local targets. |
+| `src/publish/` | Render HTML, changelog files, the theme gallery, the site manifest, and preview assets. |
+| `src/deploy/` | Sync published output to local targets and verify the snapshot. |
| `fixtures/` | Provide deterministic demo data and local preview pages. |
The refresh command runs each stage in a fixed order.
@@ -68,6 +70,7 @@ npm run demo
```
Open `output/index.html` in a browser.
+Open `output/theme-gallery.html` to compare the built-in themes.
The demo creates a local SQLite database under `data/`.
It writes the static site under `output/`.
@@ -104,12 +107,21 @@ Set the theme with a name from the built-in catalog.
All overrides are optional.
Run `node dist/index.js themes` to list the catalog.
+The catalog contains `deep-space`, `paper`, and `terminal`.
The loader rejects unknown theme names and invalid accent colors.
+Run `node dist/index.js themes --preview` to write a theme gallery page.
+The page renders every theme with its real tokens.
+It includes a `configured` card for your overrides.
+Every published snapshot also contains `theme-gallery.html`.
+
### Deploy
-Deploy copies the published output to one or more local targets.
+Deploy syncs the published output to one or more local targets.
A target must live outside the output directory.
+Sync removes files that are no longer part of the snapshot.
+It copies the snapshot, then verifies the key output files.
+Each deploy records an audit entry with the file counts.
```json
{
@@ -127,7 +139,7 @@ A target must live outside the output directory.
The refresh command deploys configured targets after publishing.
Run `node dist/index.js deploy` to publish and deploy in one step.
-Each deploy records an audit entry.
+The command reports the file count, the removed stale count, and the verification state.
Run a network-backed refresh with the checked-in settings:
@@ -161,14 +173,19 @@ Captured demo-engineer-signal-router.
Captured demo-engineer-metrics-kit.
Published 2 projects to output/index.html.
Copied 2 available preview screenshots.
+Wrote theme gallery to output/theme-gallery.html.
Open output/index.html in a browser.
```
+
+
+
+
The site shows project facts, source links, changelog previews, and screenshots.
The totals come from fixture fields and stored commit records.
The demo also writes `output/site-manifest.json`.
-The manifest records the theme, project list, and generated files.
+The manifest records the theme details, project list, and generated files.
## Commands
@@ -181,9 +198,11 @@ Build before direct CLI commands.
| `npm run ingest -- --fixture` | Load fixture records only. |
| `npm run capture -- --fixture` | Capture local fixture pages. |
| `npm run publish` | Rebuild the site from SQLite. |
+| `npm run gallery` | Build and write the theme gallery page. |
| `npm run refresh` | Run configured ingest, capture, publish, and deploy stages. |
-| `node dist/index.js deploy` | Publish the snapshot and copy it to configured targets. |
+| `node dist/index.js deploy` | Publish the snapshot and sync it to configured targets. |
| `node dist/index.js themes` | List built-in presentation themes. |
+| `node dist/index.js themes --preview` | Write a theme gallery HTML page. |
| `node dist/index.js status` | Show visibility and recent operations. |
| `npm test` | Run deterministic unit and integration tests. |
| `npm run typecheck` | Validate TypeScript types. |
@@ -214,12 +233,12 @@ The site displays visible projects only.
It links project cards to repositories.
It links release notes to their release pages.
It records local operations in an audit table.
-Deploy operations keep their target name in the audit trail.
+Deploy operations keep their target name and verification state in the audit trail.
## CI and test status
The regular CI workflow runs typecheck, build, tests, the fixture demo, theme
-verification, manifest verification, and artifact upload.
+verification, manifest verification, local deploy verification, and artifact upload.
The scheduled refresh workflow runs each Monday and supports manual dispatch.
It uploads the generated site as a workflow artifact.
@@ -234,9 +253,9 @@ The test suite covers these core behaviors:
- Configured refresh orchestration.
- Release source links.
- Deterministic HTML output.
-- Theme resolution and CSS variable emission.
-- Local deploy adapters and audit logging.
-- Site manifest versioning and determinism.
+- Theme resolution, CSS variable emission, and gallery rendering.
+- Local deploy sync, stale cleanup, and verification.
+- Site manifest versioning, determinism, and theme details.
- Playwright screenshot capture.
Run the local checks:
@@ -249,9 +268,11 @@ npm test
### Validation status
-Typecheck and build pass locally.
+Typecheck, build, and all tests pass locally.
CI runs the complete test suite on Ubuntu with Chromium installed.
The fixture pipeline provides deterministic data for repeatable checks.
+The demo output feeds two verification scripts in CI.
+They check the site manifest and the deployed snapshot.
## Limitations
@@ -262,8 +283,8 @@ The fixture pipeline provides deterministic data for repeatable checks.
- External pages can fail during capture.
- Capture failures are reported and do not stop publishing.
- Publishing creates local files. It does not deploy them.
-- Themes offer built-in palettes and selected overrides.
-- Local deploy copies files. It does not push to hosts.
+- Themes offer built-in palettes, selected overrides, and a generated gallery.
+- Local deploy syncs files. It does not push to hosts.
- Scheduled runs upload artifacts. They do not commit generated output.
## Roadmap
@@ -272,7 +293,7 @@ The fixture pipeline provides deterministic data for repeatable checks.
| --- | --- | --- |
| v0.1 | Complete | Fixture demo, GitHub ingest, changelog, capture, publish, and privacy controls. |
| v0.2 | Complete | Checked-in configuration, coordinated refresh command, and scheduled artifact workflow. |
-| v0.3 | Complete | Built-in themes, theme overrides, site manifest, and local deploy adapters. |
+| v0.3 | Complete | Built-in themes, theme overrides, theme gallery page, versioned site manifest, and local deploy sync. |
| v0.4 | Next | Commit-diff summaries and an RSS feed. |
| v0.5 | Later | Remote deploy adapters and preview diffs. |
diff --git a/docs/demo-index.png b/docs/demo-index.png
new file mode 100644
index 0000000000000000000000000000000000000000..69549f250f01615309acdddb059a5f27acdf7805
GIT binary patch
literal 666784
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