Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@ jobs:
- name: Set up Node.js
uses: actions/setup-node@v4
with:
node-version: "20"
node-version: "22"
cache: npm

- name: Install dependencies
Expand Down
2 changes: 1 addition & 1 deletion .github/workflows/refresh.yml
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@ jobs:
- name: Set up Node.js
uses: actions/setup-node@v4
with:
node-version: "20"
node-version: "22"
cache: npm

- name: Install dependencies
Expand Down
2 changes: 1 addition & 1 deletion .nvmrc
Original file line number Diff line number Diff line change
@@ -1 +1 @@
20
22
2 changes: 1 addition & 1 deletion CONTRIBUTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@

## Local checks

Use Node.js 20 or newer.
Use Node.js 22 or newer.

```bash
npm ci
Expand Down
60 changes: 52 additions & 8 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,8 @@ 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.
- Select a built-in theme for the published page.
- Stamp deployment metadata for a chosen host.
- Run one configured refresh from a scheduled workflow.

The fixture demo runs without secrets and without network access.
Expand All @@ -30,28 +32,32 @@ flowchart LR
L --> W[Publisher]
P --> W
S --> W
T[Theme] --> W
W --> O[Static output]
O --> A[Deploy adapter]
```

| Area | Responsibility |
| --- | --- |
| `engineer-profile.config.json` | Store owner, presentation, refresh, paths, and privacy settings. |
| `engineer-profile.config.json` | Store owner, theme, deploy, paths, and privacy settings. |
| `src/config/` | Validate checked-in JSON and merge safe defaults. |
| `src/refresh/` | Coordinate ingest, best-effort capture, and static publishing. |
| `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/theme/` | Resolve named palettes into CSS custom properties. |
| `src/publish/` | Render HTML, changelog files, and preview assets. |
| `src/deploy/` | Write host metadata into the static output. |
| `fixtures/` | Provide deterministic demo data and local preview pages. |

The refresh command runs each stage in a fixed order.
If a preview fails, the command reports the skip and keeps the rest of the snapshot.

## Setup

Use Node.js 20 or newer.
Use Node.js 22 or newer.

```bash
npm ci
Expand All @@ -68,10 +74,11 @@ 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 theme, deployment adapter, paths, and privacy controls.

The loader accepts repository limits from 1 through 100.
It rejects malformed values before network access.
The loader rejects unknown theme names and unknown deployment adapters.
CLI `--config`, `--data`, and `--output` options override file values.

Run a network-backed refresh with the checked-in settings:
Expand All @@ -83,6 +90,34 @@ npm run refresh
The refresh command reads public repositories, captures previews, publishes HTML,
and reports skipped captures.

### Themes

Each theme stores a named palette of CSS custom properties.
The publisher injects the palette into the page.

Built-in themes: `dark`, `light`, and `paper`.

List the themes with `node dist/index.js theme`.
Switch themes by editing the `theme` field, then run `npm run publish`.

### Deployment adapters

An adapter writes host-specific metadata into `output/`.

| Adapter | Metadata |
| --- | --- |
| `gh-pages` | `.nojekyll`, plus `CNAME` when a domain is set. |
| `vercel` | `vercel.json` with clean URLs and screenshot caching. |
| `netlify` | `netlify.toml` with static publishing and screenshot caching. |
| `surge` | `CNAME` with the configured domain. |
| `none` | No metadata files. |

Set the adapter in the `deploy` field.
Set the optional `deploy.domain` field for custom domains.
Adapters write local files. They do not upload anything.
List adapters with `node dist/index.js deploy --list`.
Apply the configured adapter with `node dist/index.js deploy`.

GitHub ingestion uses the public API.
Set `GITHUB_TOKEN` for a higher rate limit.

Expand All @@ -106,6 +141,7 @@ Captured demo-engineer-signal-router.
Captured demo-engineer-metrics-kit.
Published 2 projects to output/index.html.
Copied 2 available preview screenshots.
Prepared deployment for gh-pages (danielcuevas1208.github.io).
Open output/index.html in a browser.
```

Expand All @@ -122,9 +158,12 @@ Build before direct CLI commands.
| `npm run ingest -- octocat --limit 3` | Load public repository evidence. |
| `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 publish` | Rebuild the site and deployment metadata from SQLite. |
| `npm run refresh` | Run configured ingest, capture, publish, and deploy stages. |
| `node dist/index.js status` | Show visibility and recent operations. |
| `node dist/index.js theme` | List available site themes. |
| `node dist/index.js deploy` | Apply the configured deployment adapter. |
| `node dist/index.js deploy --list` | List available deployment adapters. |
| `npm test` | Run deterministic unit and integration tests. |
| `npm run typecheck` | Validate TypeScript types. |
| `npm run build` | Compile the CLI to `dist/`. |
Expand Down Expand Up @@ -172,6 +211,8 @@ The test suite covers these core behaviors:
- Configured refresh orchestration.
- Release source links.
- Deterministic HTML output.
- Theme resolution and token rendering.
- Deployment adapter output files.
- Playwright screenshot capture.

Run the local checks:
Expand All @@ -186,6 +227,7 @@ npm test

Typecheck and build pass locally.
CI runs the complete test suite on Ubuntu with Chromium installed.
CI uses Node.js 22 LTS.
The fixture pipeline provides deterministic data for repeatable checks.

## Limitations
Expand All @@ -197,6 +239,7 @@ 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.
- Deployment adapters write local metadata. They do not upload anything.
- Scheduled runs upload artifacts. They do not commit generated output.

## Roadmap
Expand All @@ -205,8 +248,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 and deployment adapters. |
| v0.4 | Next | Commit-diff summaries and an RSS feed. |
| v0.5 | Later | Portfolio search and topic filters. |

## License

Expand Down
5 changes: 5 additions & 0 deletions engineer-profile.config.json
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,14 @@
"owner": "DanielCuevas1208",
"title": "EngineerProfile",
"tagline": "A living index of shipped systems, maintained from repository evidence",
"theme": "dark",
"repositoryLimit": 5,
"dataDir": "data",
"outputDir": "output",
"deploy": {
"adapter": "gh-pages",
"domain": "danielcuevas1208.github.io"
},
"privacy": {
"hiddenProjects": [],
"redactEmails": true,
Expand Down
8 changes: 5 additions & 3 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

4 changes: 2 additions & 2 deletions package.json
Original file line number Diff line number Diff line change
@@ -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",
Expand All @@ -21,7 +21,7 @@
"refresh": "npm run build && node dist/index.js refresh"
},
"engines": {
"node": ">=20"
"node": ">=22"
},
"keywords": [
"portfolio",
Expand Down
42 changes: 40 additions & 2 deletions src/config/loader.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,9 @@
import { readFileSync } from "node:fs";
import type { PortfolioConfig, PrivacyConfig } from "../types.js";
import { DEFAULT_CONFIG, DEFAULT_PRIVACY } from "../types.js";
import type { DeployConfig, PortfolioConfig, PrivacyConfig } from "../types.js";
import { DEFAULT_CONFIG, DEFAULT_DEPLOY, DEFAULT_PRIVACY, DEFAULT_THEME } from "../types.js";
import { mergePrivacy } from "../privacy/controls.js";
import { listThemes } from "../theme/index.js";
import { listDeployAdapters } from "../deploy/index.js";

export const DEFAULT_CONFIG_PATH = "engineer-profile.config.json";

Expand Down Expand Up @@ -56,6 +58,40 @@ function readPrivacy(source: ConfigValue): PrivacyConfig {
});
}

function readTheme(source: ConfigValue): string {
if (!("theme" in source)) return DEFAULT_THEME;
const value = source.theme;
if (typeof value !== "string" || !listThemes().some((theme) => theme.name === value)) {
const names = listThemes().map((theme) => theme.name).join(", ");
throw new Error(`Configuration field "theme" must be one of: ${names}.`);
}
return value;
}

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.');
}

const adapter = "adapter" in source.deploy ? source.deploy.adapter : DEFAULT_DEPLOY.adapter;
if (typeof adapter !== "string" || !listDeployAdapters().some((candidate) => candidate.name === adapter)) {
const names = listDeployAdapters().map((candidate) => candidate.name).join(", ");
throw new Error(`Configuration field "deploy.adapter" must be one of: ${names}.`);
}

let domain: string | undefined;
if ("domain" in source.deploy) {
const value = source.deploy.domain;
if (typeof value !== "string" || value.trim() === "") {
throw new Error('Configuration field "deploy.domain" must be a non-empty string.');
}
domain = value.trim();
}

return { adapter, domain };
}

export function loadPortfolioConfig(
path: string = DEFAULT_CONFIG_PATH,
clock: () => string = DEFAULT_CONFIG.clock
Expand All @@ -75,10 +111,12 @@ export function loadPortfolioConfig(
owner: readString(parsed, "owner", DEFAULT_CONFIG.owner),
title: readString(parsed, "title", DEFAULT_CONFIG.title),
tagline: readString(parsed, "tagline", DEFAULT_CONFIG.tagline),
theme: readTheme(parsed),
repositoryLimit: readLimit(parsed, "repositoryLimit", DEFAULT_CONFIG.repositoryLimit),
dataDir: readString(parsed, "dataDir", DEFAULT_CONFIG.dataDir),
outputDir: readString(parsed, "outputDir", DEFAULT_CONFIG.outputDir),
privacy: readPrivacy(parsed),
deploy: readDeploy(parsed),
clock,
};
}
Loading
Loading