diff --git a/.github/ISSUE_TEMPLATE/bug_report.yml b/.github/ISSUE_TEMPLATE/bug_report.yml new file mode 100644 index 0000000..e221a8b --- /dev/null +++ b/.github/ISSUE_TEMPLATE/bug_report.yml @@ -0,0 +1,79 @@ +name: Bug Report +description: Report a defect, harness failure, or demo regression +title: "[bug]: " +labels: ["bug"] +body: + - type: markdown + attributes: + value: | + Thanks for reporting a bug. Include commands you ran and their output when possible. + CorpOS is a reference implementation — see [SECURITY.md](https://github.com/SafetyMP/CorpOS/blob/main/SECURITY.md). + + - type: textarea + id: description + attributes: + label: What happened? + description: Clear description of the bug + placeholder: Company day fails with exception settle not recorded... + validations: + required: true + + - type: dropdown + id: component + attributes: + label: Component + options: + - packages/core (firm / policy / company day) + - packages/mcp-knowledge + - apps/api + - apps/console + - harness / CI + - docs + - Other + validations: + required: true + + - type: textarea + id: reproduction + attributes: + label: Steps to reproduce + description: Commands or workflow steps + placeholder: | + 1. npm install && npm run build + 2. npm run scenario + 3. … + render: shell + validations: + required: true + + - type: textarea + id: expected + attributes: + label: Expected behavior + validations: + required: true + + - type: textarea + id: logs + attributes: + label: Logs or output + description: Paste relevant stderr, CI link, or JSON result + render: shell + + - type: dropdown + id: environment + attributes: + label: Environment + options: + - Local (npm run dev / start) + - Docker + - GitHub Actions CI + - Other + validations: + required: true + + - type: input + id: version + attributes: + label: Branch, commit, or release tag + placeholder: main, abc1234 diff --git a/.github/ISSUE_TEMPLATE/config.yml b/.github/ISSUE_TEMPLATE/config.yml new file mode 100644 index 0000000..230a44f --- /dev/null +++ b/.github/ISSUE_TEMPLATE/config.yml @@ -0,0 +1,11 @@ +blank_issues_enabled: false +contact_links: + - name: Security vulnerability + url: https://github.com/SafetyMP/CorpOS/security/advisories/new + about: Report privately via GitHub Security Advisories — never post undisclosed vulnerabilities as public issues. + - name: Contributing guide + url: https://github.com/SafetyMP/CorpOS/blob/main/CONTRIBUTING.md + about: Local setup, harness verify, and pull request expectations. + - name: Thesis / product intent + url: https://github.com/SafetyMP/CorpOS/blob/main/docs/future-of-the-firm.md + about: Why CorpOS models an autonomous company and how humans govern by exception. diff --git a/.github/ISSUE_TEMPLATE/feature_request.yml b/.github/ISSUE_TEMPLATE/feature_request.yml new file mode 100644 index 0000000..40aa8fd --- /dev/null +++ b/.github/ISSUE_TEMPLATE/feature_request.yml @@ -0,0 +1,59 @@ +name: Feature Request +description: Propose a capability aligned with the autonomous-company reference +title: "[feature]: " +labels: ["enhancement"] +body: + - type: markdown + attributes: + value: | + Read the [thesis](https://github.com/SafetyMP/CorpOS/blob/main/docs/future-of-the-firm.md) and + [ADRs](https://github.com/SafetyMP/CorpOS/tree/main/docs/adr) before filing. + CorpOS stays a reference architecture — proposals that imply production SaaS + hardening should say so explicitly. + + - type: dropdown + id: component + attributes: + label: Component + options: + - packages/core (firm / policy / trust / audit) + - packages/mcp-knowledge + - apps/api + - apps/console + - harness / CI / supply chain + - docs / ADRs + - Unsure / needs triage + validations: + required: true + + - type: textarea + id: problem + attributes: + label: Problem or use case + description: What problem does this solve for the reference demo or architecture story? + validations: + required: true + + - type: textarea + id: proposal + attributes: + label: Proposed solution + description: High-level approach; link an ADR or issue if applicable + validations: + required: true + + - type: textarea + id: alternatives + attributes: + label: Alternatives considered + description: Optional + + - type: checkboxes + id: scope + attributes: + label: Scope checklist + options: + - label: I understand CorpOS is a reference implementation, not production SaaS + required: true + - label: I am willing to include tests or harness updates if applicable + required: false diff --git a/.github/PULL_REQUEST_TEMPLATE.md b/.github/PULL_REQUEST_TEMPLATE.md new file mode 100644 index 0000000..44ad62e --- /dev/null +++ b/.github/PULL_REQUEST_TEMPLATE.md @@ -0,0 +1,29 @@ + + +## Summary + + + +## Change type + +- [ ] feat — new capability +- [ ] fix — bug fix +- [ ] refactor — no behavior change +- [ ] docs — documentation only +- [ ] test — tests only +- [ ] chore — tooling, deps, CI + +## Checklist + +- [ ] `./scripts/harness/verify.sh` is green locally. +- [ ] New behavior is covered by a test where applicable. +- [ ] No Express or `better-sqlite3` introduced. +- [ ] No secrets, `*.db`, `.env`, or `dist/` staged. +- [ ] Reference-architecture posture preserved (README / SECURITY scope language not weakened). +- [ ] If this introduces or reverses a decision, an ADR is added/updated in `docs/adr/`. +- [ ] If the ops console UI changed materially, README screenshots were regenerated (`npm run screenshots`). + +## Security + +If this change fixes a security vulnerability, **stop** and follow +[SECURITY.md](../SECURITY.md) instead of opening a public PR. diff --git a/CODE_OF_CONDUCT.md b/CODE_OF_CONDUCT.md new file mode 100644 index 0000000..775367e --- /dev/null +++ b/CODE_OF_CONDUCT.md @@ -0,0 +1,86 @@ +# Contributor Covenant Code of Conduct + +## Our Pledge + +We as members, contributors, and leaders pledge to make participation in our +community a harassment-free experience for everyone, regardless of age, body +size, visible or invisible disability, ethnicity, sex characteristics, gender +identity and expression, level of experience, education, socio-economic status, +nationality, personal appearance, race, caste, color, religion, or sexual +identity and orientation. + +We pledge to act and interact in ways that create an open, welcoming, diverse, +inclusive, and healthy community. + +## Our Standards + +Examples of behavior that contributes to a positive environment for our +community include: + +- Demonstrating empathy and kindness toward other people +- Being respectful of differing opinions, viewpoints, and experiences +- Giving and gracefully accepting constructive feedback +- Accepting responsibility and apologizing to those affected by our mistakes, + and learning from the experience +- Focusing on what is best not just for us as individuals, but for the overall + community + +Examples of unacceptable behavior include: + +- The use of sexualized language or imagery, and sexual attention or advances of + any kind +- Trolling, insulting or derogatory comments, and personal or political attacks +- Public or private harassment +- Publishing others' private information, such as a physical or email address, + without their explicit permission +- Other conduct which could reasonably be considered inappropriate in a + professional setting + +## Enforcement Responsibilities + +Community leaders are responsible for clarifying and enforcing our standards of +acceptable behavior and will take appropriate and fair corrective action in +response to any behavior that they deem inappropriate, threatening, offensive, +or harmful. + +## Scope + +This Code of Conduct applies within all community spaces, and also applies when +an individual is officially representing the community in public spaces. +Examples of representing our community include using an official e-mail address, +posting via an official social media account, or acting as an appointed +representative at an online or offline event. + +## Enforcement + +Instances of abusive, harassing, or otherwise unacceptable behavior may be +reported to the repository maintainers via [GitHub Security Advisories on the +CorpOS repository](https://github.com/SafetyMP/CorpOS/security/advisories/new) +(for security-sensitive reports) or by contacting the SafetyMP maintainers +through the organization profile. All complaints will be reviewed and +investigated promptly and fairly. All community leaders are obligated to respect +the privacy and security of the reporter of any incident. + +## Enforcement Guidelines + +Community leaders will follow these Community Impact Guidelines in determining +the consequences for any action they deem in violation of this Code of Conduct: + +1. **Correction** — private, written warning for first offence. +2. **Warning** — warning with consequences for continued behavior. +3. **Temporary Ban** — temporary ban from interaction for a specified period. +4. **Permanent Ban** — permanent ban from all community interaction. + +## Attribution + +This Code of Conduct is adapted from the [Contributor Covenant][homepage], +version 2.1, available at +. +Community Impact Guidelines were inspired by [Mozilla's code of conduct +enforcement ladder](https://mozilla.org/en-US/about/governance/policies/participation/). + +[homepage]: https://www.contributor-covenant.org + +For answers to common questions about this code of conduct, see the FAQ at +. Translations are available at +. diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md new file mode 100644 index 0000000..ac70088 --- /dev/null +++ b/CONTRIBUTING.md @@ -0,0 +1,92 @@ +# Contributing to CorpOS + +Thanks for your interest in improving CorpOS — a reference implementation of an +autonomous company (firm model, work contracts, and a policy-gated control +plane). This document explains how to set up the project and what is expected of +a contribution. + +## Reference posture (read first) + +CorpOS is a **reference / educational implementation**, **not** a +production-hardened multi-tenant SaaS. Contributions must not: + +- Soften or remove the reference-architecture scope language in the README or + [SECURITY.md](SECURITY.md). +- Imply production guarantees (multi-tenant isolation, production OIDC/TLS, real + payment providers) that the codebase does not provide. +- Commit secrets, live credentials, or real customer data. + +Please also follow the [Code of Conduct](CODE_OF_CONDUCT.md). + +## Prerequisites + +- **Node.js ≥ 22** (see [`.nvmrc`](.nvmrc); CI covers 22 and 24). +- npm **10.9.2** via Corepack (`packageManager` in [`package.json`](package.json)). + +## Setup + +```bash +npm install +npm run build +./scripts/harness/verify.sh # typecheck · test · lint · format:check · stack guards +``` + +A contribution is not finished until `./scripts/harness/verify.sh` is green +locally. CI runs the same gate on every pull request. + +Useful commands: + +```bash +npm run dev # ops console on $PORT or 3000 +npm test +npm run scenario +npm run audit:verify +``` + +## Architecture rules (must respect) + +- Workspaces: firm logic in [`packages/core`](packages/core), MCP knowledge in + [`packages/mcp-knowledge`](packages/mcp-knowledge), HTTP in + [`apps/api`](apps/api), ops UI in [`apps/console`](apps/console). +- **ToolGateway** is the sole chokepoint for consequential tools; unknown tools + fail closed. +- Do **not** introduce Express or `better-sqlite3` (enforced by the harness). +- Prefer Drizzle + libsql, Hono, and the official MCP SDK for new integration + surfaces. + +## Decisions + +Non-trivial architectural decisions are recorded as ADRs in +[`docs/adr/`](docs/adr/). If your change introduces or reverses a decision, add +or update an ADR. + +## Screenshots + +If you change the ops console layout materially, regenerate README assets: + +```bash +npm run build && npm run start # or PORT=3100 npm run start +npm run screenshots # see docs/assets/README.md +``` + +## Commits & pull requests + +- Conventional-commit-ish titles: `feat(core): trust unlock after company day`. +- Small, reviewable diffs — one logical change per PR. +- Never commit secrets, `*.db` files, `.env`, or `dist/`. +- Open a pull request against `main`; fill in the PR template; CI must pass + before merge. + +## OSS supply-chain health + +| Check | Where | +| --------------------- | -------------------------------------------------------------------- | +| **OpenSSF Scorecard** | [`.github/workflows/scorecard.yml`](.github/workflows/scorecard.yml) | +| **CodeQL** | [`.github/workflows/codeql.yml`](.github/workflows/codeql.yml) | +| **Dependabot** | [`.github/dependabot.yml`](.github/dependabot.yml) | +| **Local verify** | `./scripts/harness/verify.sh` | + +## License + +By contributing, you agree that your contributions are licensed under the +[Apache License 2.0](LICENSE). diff --git a/README.md b/README.md index 7e74ede..122fb6a 100644 --- a/README.md +++ b/README.md @@ -2,13 +2,41 @@ > Reference implementation of an **autonomous company** — firm model, work contracts, and a policy-gated control plane. -[![License: Apache-2.0](https://img.shields.io/badge/license-Apache--2.0-blue.svg)](LICENSE) -[![Node](https://img.shields.io/badge/node-%E2%89%A5%2022-green.svg)](package.json) +[![CI](https://github.com/SafetyMP/CorpOS/actions/workflows/ci.yml/badge.svg?branch=main)](https://github.com/SafetyMP/CorpOS/actions/workflows/ci.yml) +[![CodeQL](https://github.com/SafetyMP/CorpOS/actions/workflows/codeql.yml/badge.svg?branch=main)](https://github.com/SafetyMP/CorpOS/actions/workflows/codeql.yml) +[![OpenSSF Scorecard](https://github.com/SafetyMP/CorpOS/actions/workflows/scorecard.yml/badge.svg?branch=main)](https://scorecard.dev/viewer/?uri=github.com/SafetyMP/CorpOS) +[![License: Apache-2.0](https://img.shields.io/github/license/SafetyMP/CorpOS)](LICENSE) +[![Node](https://img.shields.io/badge/node-%E2%89%A522-339933?logo=node.js&logoColor=white)](#quick-start) CorpOS shows how a firm operates when department agents do most work and **humans govern by exception**. Autonomy is earned from evidence, not granted in prompts. +> **Scope:** Reference architecture and runnable demo — **not** a production-hardened SaaS. See [SECURITY.md](SECURITY.md). + Read the thesis: [`docs/future-of-the-firm.md`](docs/future-of-the-firm.md). +**Jump to:** [Demo](#demo) · [Quick start](#quick-start) · [Architecture](#architecture) · [Contributing](CONTRIBUTING.md) · [Security](SECURITY.md) · [ADRs](docs/adr/) + +--- + +## Demo + +

+ CorpOS ops console — capital and trust, company day result, and Governor audit controls (synthetic demo data) +

+ +### Screenshots + +| Ops | Company day | Governor | +| :-------------------------: | :-------------------------------------: | :-----------------------------------: | +| ![Ops](docs/assets/ops.png) | ![Company day](docs/assets/ops-day.png) | ![Governor](docs/assets/governor.png) | + +- **Local demo:** `npm install && npm run build && npm run dev` → [http://localhost:3000](http://localhost:3000) +- **Regenerate GIF/PNGs:** start the console, then `npm run screenshots` — see [`docs/assets/README.md`](docs/assets/README.md) + +Click **Run company day** for the multi-handoff demo (support → finance → ops), including an autonomous settle, an exception approval path, compensation, and trust unlock. + +--- + ## Quick start ```bash @@ -17,12 +45,11 @@ npm run build npm run dev # ops console on $PORT or 3000 ``` -Click **Run company day** for the multi-handoff demo (support → finance → ops), including an autonomous settle, an exception approval path, compensation, and trust unlock. - ```bash npm test npm run scenario npm run audit:verify +./scripts/harness/verify.sh ``` ## Architecture @@ -36,10 +63,16 @@ npm run audit:verify Stack: Node ≥22 · TypeScript · npm workspaces · Hono · Drizzle + libsql · official MCP SDK · Preact. No Express, no `better-sqlite3`, no agent-framework lock-in. +Architecture decisions live in [`docs/adr/`](docs/adr/). + ## Security Reference architecture — not production-hardened. Set `CORPOS_MODE=shared` and `DASHBOARD_API_TOKEN` before exposing approvals. See [SECURITY.md](SECURITY.md). +## Community + +[Contributing](CONTRIBUTING.md) · [Code of Conduct](CODE_OF_CONDUCT.md) · [Security](SECURITY.md) · [ADRs](docs/adr/) + ## License Apache-2.0 — Copyright © 2026 SafetyMP. diff --git a/docs/assets/README.md b/docs/assets/README.md new file mode 100644 index 0000000..9a19764 --- /dev/null +++ b/docs/assets/README.md @@ -0,0 +1,19 @@ +# Repository assets + +Visual assets for README and GitHub presentation. Synthetic demo data only. + +| File | Purpose | +|------|---------| +| [`demo.gif`](demo.gif) | README hero — Ops, company day result, Governor (3 frames, 2s each). Regenerate with `npm run screenshots`. | +| [`ops.png`](ops.png) | Ops console — capital, trust, exception queue | +| [`ops-day.png`](ops-day.png) | Ops after **Run company day** | +| [`governor.png`](governor.png) | Governor — audit head and kill switch | + +Regenerate when the ops console layout changes materially: + +```bash +npm run build && npm run start # console on http://localhost:3000 +# in another terminal: +npm run screenshots # or SCREENSHOT_BASE_URL=… npm run screenshots +npm run screenshots:rebuild-gif # rebuild GIF from committed PNGs (no browser) +``` diff --git a/docs/assets/demo.gif b/docs/assets/demo.gif new file mode 100644 index 0000000..c7e2782 Binary files /dev/null and b/docs/assets/demo.gif differ diff --git a/docs/assets/governor.png b/docs/assets/governor.png new file mode 100644 index 0000000..a04fd37 Binary files /dev/null and b/docs/assets/governor.png differ diff --git a/docs/assets/ops-day.png b/docs/assets/ops-day.png new file mode 100644 index 0000000..df12da7 Binary files /dev/null and b/docs/assets/ops-day.png differ diff --git a/docs/assets/ops.png b/docs/assets/ops.png new file mode 100644 index 0000000..97a2593 Binary files /dev/null and b/docs/assets/ops.png differ diff --git a/docs/img/dashboard.png b/docs/img/dashboard.png deleted file mode 100644 index 48afc87..0000000 Binary files a/docs/img/dashboard.png and /dev/null differ diff --git a/package-lock.json b/package-lock.json index 2eeb87f..1cd6354 100644 --- a/package-lock.json +++ b/package-lock.json @@ -21,7 +21,10 @@ "@types/node": "^22.15.30", "eslint": "^10.6.0", "eslint-config-prettier": "^10.1.8", + "gifenc": "^1.0.3", "globals": "^17.7.0", + "playwright": "^1.52.0", + "pngjs": "^7.0.0", "prettier": "^3.9.3", "tsx": "^4.23.0", "typescript": "^5.8.3", @@ -29,8 +32,7 @@ "vitest": "^3.2.4" }, "engines": { - "node": ">=22", - "npm": ">=10.9.2 <11" + "node": ">=22" } }, "apps/api": { @@ -4120,6 +4122,13 @@ "node": ">= 0.4" } }, + "node_modules/gifenc": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/gifenc/-/gifenc-1.0.3.tgz", + "integrity": "sha512-xdr6AdrfGBcfzncONUOlXMBuc5wJDtOueE3c5rdG0oNgtINLD+f2iFZltrBRZYzACRbKr+mSVU/x98zv2u3jmw==", + "dev": true, + "license": "MIT" + }, "node_modules/glob-parent": { "version": "6.0.2", "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-6.0.2.tgz", @@ -4823,6 +4832,63 @@ "node": ">=16.20.0" } }, + "node_modules/playwright": { + "version": "1.62.0", + "resolved": "https://registry.npmjs.org/playwright/-/playwright-1.62.0.tgz", + "integrity": "sha512-Z14dG305dgaLu6foB1TXQagFiW8JfSUIUaUuPaKQ6NtBPKF1P/qXcqfh6c6K/icPqdy37JmjbiBXf6JNg6Sylw==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "playwright-core": "1.62.0" + }, + "bin": { + "playwright": "cli.js" + }, + "engines": { + "node": ">=20" + }, + "optionalDependencies": { + "fsevents": "2.3.2" + } + }, + "node_modules/playwright-core": { + "version": "1.62.0", + "resolved": "https://registry.npmjs.org/playwright-core/-/playwright-core-1.62.0.tgz", + "integrity": "sha512-nsNRyq0r2zsG8AcRHWknc9QRA5XCueC7gWMrs+Gx2tlZn9hcl8zudfh00lhJPY1DE7NmZ6bDsT9g2yey8mXljA==", + "dev": true, + "license": "Apache-2.0", + "bin": { + "playwright-core": "cli.js" + }, + "engines": { + "node": ">=20" + } + }, + "node_modules/playwright/node_modules/fsevents": { + "version": "2.3.2", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.2.tgz", + "integrity": "sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + } + }, + "node_modules/pngjs": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/pngjs/-/pngjs-7.0.0.tgz", + "integrity": "sha512-LKWqWJRhstyYo9pGvgor/ivk2w94eSjE3RGVuzLGlr3NmD8bf7RcYGze1mNdEHRP6TRP6rMuDHk5t44hnTRyow==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=14.19.0" + } + }, "node_modules/postcss": { "version": "8.5.23", "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.23.tgz", diff --git a/package.json b/package.json index 1ae2675..2512093 100644 --- a/package.json +++ b/package.json @@ -25,7 +25,9 @@ "lint": "eslint .", "format": "prettier --write .", "format:check": "prettier --check .", - "audit:verify": "tsx scripts/audit-verify.ts" + "audit:verify": "tsx scripts/audit-verify.ts", + "screenshots": "node scripts/capture-screenshots.mjs", + "screenshots:rebuild-gif": "node scripts/capture-screenshots.mjs --from-existing" }, "dependencies": { "@corpos/core": "0.2.0", @@ -36,7 +38,10 @@ "@types/node": "^22.15.30", "eslint": "^10.6.0", "eslint-config-prettier": "^10.1.8", + "gifenc": "^1.0.3", "globals": "^17.7.0", + "playwright": "^1.52.0", + "pngjs": "^7.0.0", "prettier": "^3.9.3", "tsx": "^4.23.0", "typescript": "^5.8.3", diff --git a/scripts/capture-screenshots.mjs b/scripts/capture-screenshots.mjs new file mode 100644 index 0000000..e13f004 --- /dev/null +++ b/scripts/capture-screenshots.mjs @@ -0,0 +1,125 @@ +/** + * Capture README screenshots and demo GIF from the CorpOS ops console. + * + * Usage: + * npm run build && npm run start # console on :3000 + * npm run screenshots + * + * Rebuild GIF only from existing PNGs (no browser): + * npm run screenshots:rebuild-gif + * + * Optional: SCREENSHOT_BASE_URL=http://localhost:3000 npm run screenshots + * + * CI: set CI=1 to use bundled Chromium instead of system Chrome. + */ +import { chromium } from "playwright"; +import gifenc from "gifenc"; +import { mkdir, readFile, writeFile } from "node:fs/promises"; +import path from "node:path"; +import { fileURLToPath } from "node:url"; +import { PNG } from "pngjs"; + +const { GIFEncoder, quantize, applyPalette } = gifenc; + +const __dirname = path.dirname(fileURLToPath(import.meta.url)); +const repoRoot = path.join(__dirname, ".."); +const outDir = path.join(repoRoot, "docs", "assets"); +const baseUrl = process.env.SCREENSHOT_BASE_URL ?? "http://localhost:3000"; + +const frames = [ + { file: "ops.png", name: "Ops" }, + { file: "ops-day.png", name: "Company day" }, + { file: "governor.png", name: "Governor" }, +]; + +const GIF_FRAME_DELAY_MS = 2_000; + +function launchOptions() { + if (process.env.CI) { + return { headless: true }; + } + return { channel: "chrome", headless: true }; +} + +async function writeDemoGif(gifFrames) { + const encoder = GIFEncoder(); + for (const { buffer, name } of gifFrames) { + const { data, width, height } = PNG.sync.read(buffer); + const palette = quantize(data, 256); + const index = applyPalette(data, palette); + encoder.writeFrame(index, width, height, { palette, delay: GIF_FRAME_DELAY_MS }); + console.log(`GIF frame: ${name}`); + } + encoder.finish(); + const gifPath = path.join(outDir, "demo.gif"); + await writeFile(gifPath, Buffer.from(encoder.bytes())); + console.log("Captured demo GIF -> docs/assets/demo.gif"); +} + +async function rebuildGifFromExisting() { + await mkdir(outDir, { recursive: true }); + const gifFrames = []; + for (const { file, name } of frames) { + const buffer = await readFile(path.join(outDir, file)); + gifFrames.push({ buffer, name }); + console.log(`Loaded ${name} -> docs/assets/${file}`); + } + await writeDemoGif(gifFrames); +} + +async function saveShot(page, file, name) { + await page.waitForTimeout(600); + const buffer = await page.screenshot({ fullPage: false }); + const dest = path.join(outDir, file); + await writeFile(dest, buffer); + console.log(`Captured ${name} -> docs/assets/${file}`); + return buffer; +} + +async function captureLive() { + await mkdir(outDir, { recursive: true }); + + const browser = await chromium.launch(launchOptions()); + const context = await browser.newContext({ + viewport: { width: 1280, height: 800 }, + }); + const page = await context.newPage(); + + await page.goto(baseUrl, { waitUntil: "networkidle" }); + await page.getByRole("heading", { name: "Autonomous company ops" }).waitFor({ + state: "visible", + timeout: 30_000, + }); + await page.getByRole("heading", { name: "Capital" }).waitFor({ state: "visible" }); + + const gifFrames = []; + + const opsBuffer = await saveShot(page, "ops.png", "Ops"); + gifFrames.push({ buffer: opsBuffer, name: "Ops" }); + + await page.getByRole("button", { name: "Run company day" }).click(); + await page.locator("section pre").waitFor({ state: "visible", timeout: 30_000 }); + const dayBuffer = await saveShot(page, "ops-day.png", "Company day"); + gifFrames.push({ buffer: dayBuffer, name: "Company day" }); + + await page.getByRole("button", { name: "Governor" }).click(); + await page.getByRole("heading", { name: "Governor" }).waitFor({ state: "visible" }); + const govBuffer = await saveShot(page, "governor.png", "Governor"); + gifFrames.push({ buffer: govBuffer, name: "Governor" }); + + await writeDemoGif(gifFrames); + await browser.close(); +} + +async function main() { + if (process.argv.includes("--from-existing")) { + await rebuildGifFromExisting(); + return; + } + await captureLive(); +} + +main().catch((error) => { + console.error(error); + process.exit(1); +});