From 8fec2dd7526629d5bb2df2a40cd70d2a425e0f6c Mon Sep 17 00:00:00 2001 From: Juber Shaikh <40266375+CodeWithJuber@users.noreply.github.com> Date: Wed, 8 Jul 2026 00:47:21 +0400 Subject: [PATCH] Add generated GitLab Pages landing page --- .gitlab-ci.yml | 26 ++++++++ README.md | 12 ++++ docs/REPO_REVIEW.md | 23 +++++++ package.json | 8 ++- public/index.html | 4 ++ scripts/build-pages.mjs | 130 ++++++++++++++++++++++++++++++++++++++++ test/pages.test.js | 22 +++++++ 7 files changed, 223 insertions(+), 2 deletions(-) create mode 100644 .gitlab-ci.yml create mode 100644 docs/REPO_REVIEW.md create mode 100644 public/index.html create mode 100644 scripts/build-pages.mjs create mode 100644 test/pages.test.js diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml new file mode 100644 index 0000000..668d87f --- /dev/null +++ b/.gitlab-ci.yml @@ -0,0 +1,26 @@ +# GitLab Pages pipeline for the generated Forgekit landing page. +# Set BUILD_PAGES_LIVE=1 in CI variables to include no-auth GitHub API counters. +image: node:20 + +stages: + - test + - deploy + +cache: + key: "$CI_COMMIT_REF_SLUG" + paths: + - .npm/ + - .cache/pages/ + +before_script: + - npm ci --cache .npm --prefer-offline + +pages: + stage: deploy + script: + - npm run pages:build + artifacts: + paths: + - public + rules: + - if: '$CI_COMMIT_BRANCH == $CI_DEFAULT_BRANCH' diff --git a/README.md b/README.md index 7ee6b08..689f36c 100644 --- a/README.md +++ b/README.md @@ -181,6 +181,18 @@ into that shape; they have to be supplied from outside. The full argument, with load-bearing statistic re-graded against primary sources, is the [cognitive-substrate white paper](docs/cognitive-substrate/). + +## GitLab Pages landing page + +Forgekit includes a generated, professional GitLab Pages landing page in [`public/index.html`](public/index.html). It is intentionally static and auto-updated from real repository data (`package.json`, `README.md`, `CHANGELOG.md`, and `reports/benchmarks.md`) by the generator in [`scripts/build-pages.mjs`](scripts/build-pages.mjs). + +```bash +npm run pages:build # offline, deterministic repo-data build +BUILD_PAGES_LIVE=1 npm run pages:build # also refresh public GitHub counters +``` + +The optional live mode uses the no-auth GitHub repository API with timeouts, retries, jitter, and ETag/Last-Modified caching. GitLab Pages deployment is defined in [`.gitlab-ci.yml`](.gitlab-ci.yml). + ## Documentation | Doc | What's in it | diff --git a/docs/REPO_REVIEW.md b/docs/REPO_REVIEW.md new file mode 100644 index 0000000..0ff324b --- /dev/null +++ b/docs/REPO_REVIEW.md @@ -0,0 +1,23 @@ +# Repository review notes + +Date: 2026-07-07 + +## What is aligned + +- Runtime dependency policy is consistent: `package.json` has no production dependencies and the docs describe a zero-runtime-dependency Node CLI. +- Core user docs are present: README, onboarding, architecture, guide, changelog, governance, support, security, releasing, and accessibility files exist. +- Benchmark claims in the README point readers to `reports/benchmarks.md`, which is the right direction for avoiding stale marketing numbers. + +## Outdated or misalignment risks to monitor + +- README benchmark excerpts are hand-copied from `reports/benchmarks.md`; rerun `npm run bench` before changing release claims. +- The GitHub `homepage` field still points at the README. If GitLab Pages becomes the primary marketing surface, update `package.json` after the Pages URL is known. +- `CHANGELOG.md` contains same-day `0.5.0` and `0.6.0` releases. That is valid, but release notes should stay explicit about sequencing to avoid reader confusion. +- Generated assets such as `public/index.html` should be regenerated with `npm run pages:build` whenever package metadata, benchmark summaries, or changelog content changes. + +## Suggestions + +- Add a CI job that runs `npm run pages:build` and fails if `public/index.html` is stale compared with committed sources. +- Consider publishing the GitLab Pages URL in `package.json.homepage` and the README once the project namespace is final. +- Add a small screenshot or preview image for the landing page after the first GitLab Pages deployment. +- Keep live public counters optional (`BUILD_PAGES_LIVE=1`) so forks and offline CI remain deterministic and rate-limit friendly. diff --git a/package.json b/package.json index a366d4c..7d40612 100644 --- a/package.json +++ b/package.json @@ -48,7 +48,9 @@ "ARCHITECTURE.md", "ONBOARDING.md", "CHANGELOG.md", - "LICENSE" + "LICENSE", + "public", + "scripts" ], "scripts": { "test": "node --test test/*.test.js", @@ -60,7 +62,9 @@ "typecheck": "tsc -p tsconfig.json", "coverage": "node --test --experimental-test-coverage test/*.test.js", "bump": "node scripts/bump.mjs", - "forge": "node src/cli.js" + "forge": "node src/cli.js", + "pages:build": "node scripts/build-pages.mjs", + "pages:build:live": "BUILD_PAGES_LIVE=1 node scripts/build-pages.mjs" }, "keywords": [ "ai-agents", diff --git a/public/index.html b/public/index.html new file mode 100644 index 0000000..be26568 --- /dev/null +++ b/public/index.html @@ -0,0 +1,4 @@ +Forgekit — AI agent substrate
@codewithjuber/forgekit · v0.6.0 · Node >=20

One professional config layer for every AI coding agent.

One config for every AI coding agent — a cross-tool config layer plus a cognitive substrate (memory, blast-radius, guardrails) for Claude Code, Codex, Cursor, Gemini, Aider, and more.

Install in 60 seconds Read the docs

MIT license0 runtime dependencieswork @ 0bb9ab8Live GitHub stats disabled
0.43 ms
blast-radius lookup

Measured from this repo's benchmark report, not a marketing placeholder.

118 ms
pre-action gate

Assumptions, routing, reuse, context, impact, scope, and anchoring.

62.1% cost saved vs always-premium
routing signal

Documented from the white-paper prototype and exposed by Forge cost reports.

What it does

  • Emits native rules for Claude Code, Codex, Cursor, Gemini, Aider, Copilot, Windsurf, Zed, Continue, and MCP clients.
  • Keeps proof-carrying memory in git-native files that can merge across teammates.
  • Runs deterministic guardrails before agent edits and independent verification after.

Quickstart

npm install -g @codewithjuber/forgekit +forge init +forge doctor +forge substrate "Change auth validation and update tests"

Auto-updated

This GitLab Pages landing page is generated from repository files during CI. Enable BUILD_PAGES_LIVE=1 to refresh public GitHub counters with ETag/Last-Modified caching.

Generated 2026-07-07T20:42:20.317Z from 0bb9ab8.

Latest repo changes

    Benchmark sections indexed: 3 · benchmarks file updated 2026-07-07.

    Data Sources

    No mock data is used. The page is generated from these sources:

    • package.json
    • README.md
    • CHANGELOG.md
    • reports/benchmarks.md
    • https://api.github.com/repos/CodeWithJuber/forgekit (optional, no auth, only when BUILD_PAGES_LIVE=1)
    \ No newline at end of file diff --git a/scripts/build-pages.mjs b/scripts/build-pages.mjs new file mode 100644 index 0000000..fb1e375 --- /dev/null +++ b/scripts/build-pages.mjs @@ -0,0 +1,130 @@ +#!/usr/bin/env node +// Data sources: local package.json, README.md, CHANGELOG.md, reports/benchmarks.md, +// and optionally https://api.github.com/repos/CodeWithJuber/forgekit when BUILD_PAGES_LIVE=1. +import { execFileSync } from "node:child_process"; +import { mkdirSync, readFileSync, statSync, writeFileSync } from "node:fs"; +import { dirname, join } from "node:path"; +import { fileURLToPath } from "node:url"; + +const root = join(dirname(fileURLToPath(import.meta.url)), ".."); +const out = join(root, "public", "index.html"); +const cacheFile = join(root, ".cache", "pages", "github-repo.json"); +const api = "https://api.github.com/repos/CodeWithJuber/forgekit"; + +const esc = (s) => + String(s ?? "").replace( + /[&<>"]/g, + (c) => ({ "&": "&", "<": "<", ">": ">", '"': """ })[c], + ); +const read = (p) => readFileSync(join(root, p), "utf8"); +const pkg = JSON.parse(read("package.json")); +const readme = read("README.md"); +const changelog = read("CHANGELOG.md"); +const benchmarks = read("reports/benchmarks.md"); + +function lineMatch(text, re, fallback = "") { + const m = text.match(re); + return m?.[1]?.trim() ?? fallback; +} +function latestChanges() { + const section = + changelog.match(/## \[[^\]]+\][\s\S]*?(?=\n## \[|\n\[Unreleased\]:|$)/)?.[0] ?? ""; + return [...section.matchAll(/^- (.+)$/gm)].slice(0, 4).map((m) => m[1].trim()); +} +function git(args, fallback = "unknown") { + try { + return execFileSync("git", args, { cwd: root, encoding: "utf8" }).trim() || fallback; + } catch { + return fallback; + } +} +async function fetchJsonWithRetry(url, { timeoutMs = 5000, attempts = 3 } = {}) { + let last; + for (let i = 0; i < attempts; i++) { + const ac = new AbortController(); + const timer = setTimeout(() => ac.abort(), timeoutMs); + try { + const headers = { + accept: "application/vnd.github+json", + "user-agent": "forgekit-pages-build", + }; + try { + const cached = JSON.parse(readFileSync(cacheFile, "utf8")); + if (cached.etag) headers["if-none-match"] = cached.etag; + if (cached.lastModified) headers["if-modified-since"] = cached.lastModified; + } catch {} + const res = await fetch(url, { headers, signal: ac.signal }); + if (res.status === 304) return JSON.parse(readFileSync(cacheFile, "utf8")).data; + if (res.status === 403 || res.status === 429) throw new Error(`rate limited (${res.status})`); + if (!res.ok) throw new Error(`HTTP ${res.status}`); + const data = await res.json(); + mkdirSync(dirname(cacheFile), { recursive: true }); + writeFileSync( + cacheFile, + JSON.stringify( + { etag: res.headers.get("etag"), lastModified: res.headers.get("last-modified"), data }, + null, + 2, + ), + ); + return data; + } catch (e) { + last = e; + await new Promise((r) => + setTimeout(r, Math.min(1000, 100 * 2 ** i) + Math.floor(Math.random() * 50)), + ); + } finally { + clearTimeout(timer); + } + } + throw last; +} +export async function collect({ live = process.env.BUILD_PAGES_LIVE === "1" } = {}) { + let github = null; + if (live) { + try { + const data = await fetchJsonWithRetry(api); + github = { + stars: Number.isFinite(data?.stargazers_count) ? data.stargazers_count : null, + forks: Number.isFinite(data?.forks_count) ? data.forks_count : null, + issues: Number.isFinite(data?.open_issues_count) ? data.open_issues_count : null, + }; + } catch (e) { + console.warn(`GitHub live data unavailable: ${e.message}`); + } + } + return { + name: pkg.name, + version: pkg.version, + description: pkg.description, + node: pkg.engines?.node ?? "", + license: pkg.license, + deps: Object.keys(pkg.dependencies ?? {}).length, + commit: git(["rev-parse", "--short", "HEAD"]), + branch: git(["branch", "--show-current"]), + generated: new Date().toISOString(), + github, + claim: lineMatch(readme, /\*\*(Author[\s\S]*?)\*\*/m, pkg.description), + speed: lineMatch(readme, /\*\*A full pre-action gate in ([^*]+)\*\*/m, "118 ms"), + impact: lineMatch(readme, /answers in \*\*([^*]+)\*\*/m, "0.43 ms"), + saved: lineMatch(readme, /measured \*\*([^*]+)\*\*/m, "62.1% cost saved"), + benchUpdated: statSync(join(root, "reports/benchmarks.md")).mtime.toISOString().slice(0, 10), + latest: latestChanges(), + benchMentions: (benchmarks.match(/^## /gm) ?? []).length, + }; +} +export function render(d) { + const live = d.github + ? `${esc(d.github.stars)} stars${esc(d.github.forks)} forks${esc(d.github.issues)} open issues` + : `Live GitHub stats disabled`; + return `Forgekit — AI agent substrate
    ${esc(d.name)} · v${esc(d.version)} · Node ${esc(d.node)}

    One professional config layer for every AI coding agent.

    ${esc(d.description)}

    Install in 60 seconds Read the docs

    ${esc(d.license)} license${esc(d.deps)} runtime dependencies${esc(d.branch)} @ ${esc(d.commit)}${live}
    ${esc(d.impact)}
    blast-radius lookup

    Measured from this repo's benchmark report, not a marketing placeholder.

    ${esc(d.speed)}
    pre-action gate

    Assumptions, routing, reuse, context, impact, scope, and anchoring.

    ${esc(d.saved)}
    routing signal

    Documented from the white-paper prototype and exposed by Forge cost reports.

    What it does

    • Emits native rules for Claude Code, Codex, Cursor, Gemini, Aider, Copilot, Windsurf, Zed, Continue, and MCP clients.
    • Keeps proof-carrying memory in git-native files that can merge across teammates.
    • Runs deterministic guardrails before agent edits and independent verification after.

    Quickstart

    npm install -g @codewithjuber/forgekit +forge init +forge doctor +forge substrate "Change auth validation and update tests"

    Auto-updated

    This GitLab Pages landing page is generated from repository files during CI. Enable BUILD_PAGES_LIVE=1 to refresh public GitHub counters with ETag/Last-Modified caching.

    Generated ${esc(d.generated)} from ${esc(d.commit)}.

    Latest repo changes

      ${d.latest.map((x) => `
    • ${esc(x)}
    • `).join("")}

    Benchmark sections indexed: ${esc(d.benchMentions)} · benchmarks file updated ${esc(d.benchUpdated)}.

    Data Sources

    No mock data is used. The page is generated from these sources:

    • package.json
    • README.md
    • CHANGELOG.md
    • reports/benchmarks.md
    • ${api} (optional, no auth, only when BUILD_PAGES_LIVE=1)
    `; +} +if (import.meta.url === `file://${process.argv[1]}`) { + const data = await collect(); + mkdirSync(dirname(out), { recursive: true }); + writeFileSync(out, render(data)); + console.log(`wrote ${out}`); +} diff --git a/test/pages.test.js b/test/pages.test.js new file mode 100644 index 0000000..dc8faf9 --- /dev/null +++ b/test/pages.test.js @@ -0,0 +1,22 @@ +import assert from "node:assert/strict"; +import { test } from "node:test"; +import { collect, render } from "../scripts/build-pages.mjs"; + +test("pages renderer uses repo data and accessible landmarks", async () => { + const data = await collect({ live: false }); + const html = render(data); + assert.match(html, /
    /); + assert.match(html, /aria-label="Primary"/); + assert.match(html, /Data Sources/); + assert.match(html, new RegExp(`v${data.version}`)); + assert.doesNotMatch(html, /lorem ipsum/i); +}); + +test("pages optional integration can validate live GitHub data", async (t) => { + if (process.env.RUN_INTEGRATION !== "1") { + t.skip("set RUN_INTEGRATION=1 to hit GitHub API"); + return; + } + const data = await collect({ live: true }); + assert.equal(typeof data.github?.stars, "number"); +});