Compare commits
186 commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
3951464325 | ||
|
|
0530605fdb | ||
|
|
9197895f10 | ||
|
|
9d0a2a9a66 | ||
|
|
74dae0a342 | ||
|
|
469b5d4670 | ||
|
|
d29c1d817e | ||
|
|
50c032576d | ||
|
|
c7c6d2999a | ||
|
|
09ee73e1a0 | ||
|
|
5de39fc398 | ||
|
|
f375073355 | ||
|
|
f8dc3446a4 | ||
|
|
3e34ad5d78 | ||
|
|
f382f2764f | ||
|
|
f416643a48 | ||
|
|
b116c07ea7 | ||
|
|
f80f52920b | ||
|
|
dcad7059d4 | ||
|
|
610aff7446 | ||
|
|
aa8ff368df | ||
|
|
124894d0eb | ||
|
|
f13310dfc4 | ||
|
|
1af5193a82 | ||
|
|
335da2318d | ||
|
|
b907896907 | ||
|
|
76dc5f72a4 | ||
|
|
b18521058a | ||
|
|
cf77d2b19f | ||
|
|
29cf519e99 | ||
|
|
7799d59514 | ||
|
|
9c011cc722 | ||
|
|
fec037c546 | ||
|
|
08b74863fa | ||
|
|
4cd9e301ea | ||
|
|
33a2d32fae | ||
|
|
684f4070e0 | ||
|
|
fe6ce15c20 | ||
|
|
1a258242bd | ||
|
|
8101f948fc | ||
|
|
6c596fe027 | ||
|
|
6f5b5daeba | ||
|
|
571dac721d | ||
|
|
322be8a0b8 | ||
|
|
b45483ae4a | ||
|
|
002468316d | ||
|
|
951a37faf8 | ||
|
|
811f10f586 | ||
|
|
4245ab86a4 | ||
|
|
4520fd2001 | ||
|
|
10ba327aad | ||
|
|
28dd481ed2 | ||
|
|
dfddba87f6 | ||
|
|
0e9892b4e7 | ||
|
|
e152bbfd37 | ||
|
|
f1dab99e4a | ||
|
|
e3790ecff3 | ||
|
|
c248aaf92e | ||
|
|
30049c9ed8 | ||
|
|
ddc1ff7112 | ||
|
|
5c503565aa | ||
|
|
fdf7b3eb23 | ||
|
|
b4ae9d8566 | ||
|
|
430218ee4b | ||
|
|
f39b37da64 | ||
|
|
470fcfe120 | ||
|
|
cb5b7bc3a7 | ||
|
|
2972e38bed | ||
|
|
b101ad78a2 | ||
|
|
0ab27e40e6 | ||
|
|
420c0874a0 | ||
|
|
e5e3d2b240 | ||
|
|
5ed947764d | ||
|
|
8734936c1d | ||
|
|
edcbee5f5e | ||
|
|
3f62afdfbf | ||
|
|
51797f0e01 | ||
|
|
46293f868c | ||
|
|
e043762605 | ||
|
|
b604bc4b60 | ||
|
|
ec9057a4c6 | ||
|
|
bd195175fd | ||
|
|
49379f08d0 | ||
|
|
4a37087953 | ||
|
|
da3610af59 | ||
|
|
7ce7a842cc | ||
|
|
0d7ae73857 | ||
|
|
bcfd16b833 | ||
|
|
d90a805f1a | ||
|
|
872ead311d | ||
|
|
54dcb35080 | ||
|
|
882b4f35e9 | ||
|
|
6ab2802654 | ||
|
|
742f02bb26 | ||
|
|
6dbaa256d4 | ||
|
|
250f45e047 | ||
|
|
2affe99537 | ||
|
|
76d863ca10 | ||
|
|
7292ab4079 | ||
|
|
ec0a612ea5 | ||
|
|
8b948a2d68 | ||
|
|
43f5fb0edb | ||
|
|
527b6252ef | ||
|
|
0929eaac87 | ||
|
|
f24e410fb3 | ||
|
|
b61786ff30 | ||
|
|
a4abd68686 | ||
|
|
4c067f123c | ||
|
|
50dcc04707 | ||
|
|
a40494fd65 | ||
|
|
193a18285a | ||
|
|
26f5603a13 | ||
|
|
c2c351c472 | ||
|
|
047c2c4fef | ||
|
|
c8b9436678 | ||
|
|
2b2c37b82b | ||
|
|
ceca61e952 | ||
|
|
38cfb4286d | ||
|
|
4e75174a6d | ||
|
|
cf112ea195 | ||
|
|
0bb0e6e3cf | ||
|
|
0a760e5d53 | ||
|
|
be88061479 | ||
|
|
7249fbaf54 | ||
|
|
a8f6dbb7fd | ||
|
|
36c1bd3794 | ||
|
|
510272fa69 | ||
|
|
ecec4c507c | ||
|
|
70beb76c41 | ||
|
|
d97c1f4a09 | ||
|
|
ea6aa93c22 | ||
|
|
0bd012acb6 | ||
|
|
891a6db29b | ||
|
|
d25fc56b10 | ||
|
|
ef21be5a6d | ||
|
|
938f3a7c74 | ||
|
|
bc3c5be59e | ||
|
|
0c80d1f616 | ||
|
|
514686787e | ||
|
|
5428702bc5 | ||
|
|
bc0cb34d5e | ||
|
|
70d58fc7ba | ||
|
|
8dd860ea81 | ||
|
|
d5236dcda2 | ||
|
|
1e7fd59c69 | ||
|
|
84932eb3a8 | ||
|
|
312bef9b87 | ||
|
|
008fdb03dc | ||
|
|
81adcefd57 | ||
|
|
6b03562749 | ||
|
|
94652907b0 | ||
|
|
f58e02c8df | ||
|
|
5a1109f66a | ||
|
|
5b82cea0e1 | ||
|
|
6cc5512ead | ||
|
|
68b088d322 | ||
|
|
25ec842a7f | ||
|
|
c5df9176b3 | ||
|
|
6db816e8b3 | ||
|
|
e6f59cf426 | ||
|
|
12e71e3464 | ||
|
|
79c602cc7f | ||
|
|
76fde62234 | ||
|
|
2220b9194d | ||
|
|
36ed218ce0 | ||
|
|
72865245a4 | ||
|
|
69e7f415d8 | ||
|
|
4ee215cada | ||
|
|
39191be1a5 | ||
|
|
29bd80824b | ||
|
|
0347d5bf0d | ||
|
|
52475dde28 | ||
|
|
178677a9a9 | ||
|
|
9e3d2d0688 | ||
|
|
d207c4b1f7 | ||
|
|
2bc211f267 | ||
|
|
f6da846523 | ||
|
|
e7017eb127 | ||
|
|
35bd79a2ed | ||
|
|
8cb6c4f26f | ||
|
|
c4dc2f09c8 | ||
|
|
0ee5b079c5 | ||
|
|
714c6f5eaf | ||
|
|
20bfbb939f | ||
|
|
707c5fbfe3 | ||
|
|
ff2e2fbf42 |
|
|
@ -1,46 +0,0 @@
|
|||
# Simplify: Code Review and Cleanup
|
||||
|
||||
Review all changed files for reuse, quality, and efficiency. Fix any issues found.
|
||||
|
||||
## Phase 1: Identify Changes
|
||||
|
||||
Run git diff (or git diff HEAD if there are staged changes) to see what changed. If there are no git changes, review the most recently modified files that the user mentioned or that you edited earlier in this conversation.
|
||||
|
||||
## Phase 2: Launch Three Review Agents in Parallel
|
||||
|
||||
Use the Agent tool to launch all three agents concurrently in a single message. Pass each agent the full diff so it has the complete context.
|
||||
|
||||
### Agent 1: Code Reuse Review
|
||||
|
||||
For each change:
|
||||
|
||||
1. Search for existing utilities and helpers that could replace newly written code. Use Grep to find similar patterns elsewhere in the codebase — common locations are utility directories, shared modules, and files adjacent to the changed ones.
|
||||
2. Flag any new function that duplicates existing functionality. Suggest the existing function to use instead.
|
||||
3. Flag any inline logic that could use an existing utility — hand-rolled string manipulation, manual path handling, custom environment checks, ad-hoc type guards, and similar patterns are common candidates.
|
||||
|
||||
### Agent 2: Code Quality Review
|
||||
|
||||
Review the same changes for hacky patterns:
|
||||
|
||||
1. Redundant state: state that duplicates existing state, cached values that could be derived, observers/effects that could be direct calls
|
||||
2. Parameter sprawl: adding new parameters to a function instead of generalizing or restructuring existing ones
|
||||
3. Copy-paste with slight variation: near-duplicate code blocks that should be unified with a shared abstraction
|
||||
4. Leaky abstractions: exposing internal details that should be encapsulated, or breaking existing abstraction boundaries
|
||||
5. Stringly-typed code: using raw strings where constants, enums (string unions), or branded types already exist in the codebase
|
||||
|
||||
### Agent 3: Efficiency Review
|
||||
|
||||
Review the same changes for efficiency:
|
||||
|
||||
1. Unnecessary work: redundant computations, repeated file reads, duplicate network/API calls, N+1 patterns
|
||||
2. Missed concurrency: independent operations run sequentially when they could run in parallel
|
||||
3. Hot-path bloat: new blocking work added to startup or per-request/per-render hot paths
|
||||
4. Unnecessary existence checks: pre-checking file/resource existence before operating (TOCTOU anti-pattern) — operate directly and handle the error
|
||||
5. Memory: unbounded data structures, missing cleanup, event listener leaks
|
||||
6. Overly broad operations: reading entire files when only a portion is needed, loading all items when filtering for one
|
||||
|
||||
## Phase 3: Fix Issues
|
||||
|
||||
Wait for all three agents to complete. Aggregate their findings and fix each issue directly. If a finding is a false positive or not worth addressing, note it and move on — do not argue with the finding, just skip it.
|
||||
|
||||
When done, briefly summarize what was fixed (or confirm the code was already clean).
|
||||
|
|
@ -1 +1 @@
|
|||
8bd90191bc4deac93a95e065f6584b64e48ca0e0
|
||||
aa8ff368df36fc17a0799cd8b09297f85457aaa2
|
||||
|
|
|
|||
|
|
@ -1 +1 @@
|
|||
542adbf1e1b0a9d6b3149ae25a097addf3bfb61a
|
||||
610aff744609268ae879a58acfe6c9a6bb0572e0
|
||||
|
|
|
|||
36
.github/workflows/rust.yml
vendored
|
|
@ -29,54 +29,44 @@ permissions:
|
|||
contents: read
|
||||
|
||||
env:
|
||||
CARGO_TERM_COLOR: always # trigger CI
|
||||
CARGO_TERM_COLOR: always
|
||||
|
||||
jobs:
|
||||
fmt:
|
||||
name: Format
|
||||
runs-on: self-hosted
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Add cargo bin to PATH
|
||||
run: echo "$HOME/.cargo/bin" >> $GITHUB_PATH
|
||||
- uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4
|
||||
with:
|
||||
persist-credentials: false
|
||||
clean: false
|
||||
- uses: dtolnay/rust-toolchain@631a55b12751854ce901bb631d5902ceb48146f7 # stable
|
||||
- run: cargo fmt --check --all
|
||||
|
||||
clippy:
|
||||
name: Clippy
|
||||
runs-on: self-hosted
|
||||
env:
|
||||
RUSTC_WRAPPER: sccache
|
||||
SCCACHE_DIR: /home/bhelmkamp/.cache/sccache
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Add cargo bin to PATH
|
||||
run: echo "$HOME/.cargo/bin" >> $GITHUB_PATH
|
||||
- uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4
|
||||
with:
|
||||
persist-credentials: false
|
||||
clean: false
|
||||
- uses: dtolnay/rust-toolchain@631a55b12751854ce901bb631d5902ceb48146f7 # stable
|
||||
- uses: Swatinem/rust-cache@779680da715d629ac1d338a641029a2f4372abb5 # v2
|
||||
with:
|
||||
cache-on-failure: true
|
||||
- run: cargo clippy --workspace -- -D warnings
|
||||
- name: Show sccache stats
|
||||
run: sccache --show-stats
|
||||
|
||||
test:
|
||||
name: Test (Linux)
|
||||
runs-on: self-hosted
|
||||
env:
|
||||
RUSTC_WRAPPER: sccache
|
||||
SCCACHE_DIR: /home/bhelmkamp/.cache/sccache
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Add cargo bin to PATH
|
||||
run: echo "$HOME/.cargo/bin" >> $GITHUB_PATH
|
||||
- uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4
|
||||
with:
|
||||
persist-credentials: false
|
||||
clean: false
|
||||
- uses: dtolnay/rust-toolchain@631a55b12751854ce901bb631d5902ceb48146f7 # stable
|
||||
- uses: Swatinem/rust-cache@779680da715d629ac1d338a641029a2f4372abb5 # v2
|
||||
with:
|
||||
cache-on-failure: true
|
||||
- run: cargo test --workspace
|
||||
- name: Show sccache stats
|
||||
run: sccache --show-stats
|
||||
|
||||
test-macos:
|
||||
name: Test (macOS)
|
||||
|
|
|
|||
6
.github/workflows/typescript.yml
vendored
|
|
@ -29,7 +29,7 @@ permissions:
|
|||
jobs:
|
||||
typecheck:
|
||||
name: Typecheck
|
||||
runs-on: self-hosted
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4
|
||||
with:
|
||||
|
|
@ -40,7 +40,7 @@ jobs:
|
|||
|
||||
test:
|
||||
name: Test
|
||||
runs-on: self-hosted
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4
|
||||
with:
|
||||
|
|
@ -51,7 +51,7 @@ jobs:
|
|||
|
||||
build:
|
||||
name: Build
|
||||
runs-on: self-hosted
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4
|
||||
with:
|
||||
|
|
|
|||
5
.gitignore
vendored
|
|
@ -3,3 +3,8 @@ target
|
|||
.entire
|
||||
node_modules
|
||||
tmp
|
||||
evals/swe-bench/repos/
|
||||
evals/swe-bench/results/
|
||||
evals/swe-bench/dockerfiles/
|
||||
__pycache__
|
||||
.vercel
|
||||
|
|
|
|||
13
AGENTS.md
|
|
@ -18,6 +18,11 @@ This file provides guidance to Claude Code (claude.ai/code) when working with co
|
|||
- `cd apps/fabro-web && bun run typecheck` — type check
|
||||
- `cd apps/fabro-web && bun run build` — production build
|
||||
|
||||
### Marketing site (apps/marketing)
|
||||
- `cd apps/marketing && bun run dev` — start Astro dev server
|
||||
- `cd apps/marketing && bun run build` — production build
|
||||
- `cd apps/marketing && vercel --prod` — deploy to Vercel (project: website, domain: fabro.sh)
|
||||
|
||||
### Dev servers
|
||||
1. `fabro serve` — starts the Rust API server (demo mode is per-request via `X-Fabro-Demo: 1` header)
|
||||
2. `cd apps/fabro-web && bun run dev` — starts the React dev server
|
||||
|
|
@ -40,11 +45,11 @@ The OpenAPI spec at `docs/api-reference/fabro-api.yaml` is the source of truth f
|
|||
|
||||
## Architecture
|
||||
|
||||
Fabro is an AI-powered workflow orchestration platform. Workflows are defined as DOT graphs, where each node is a stage (agent, prompt, command, conditional, human, parallel, etc.) executed by the workflow engine.
|
||||
Fabro is an AI-powered workflow orchestration platform. Workflows are defined as Graphviz graphs, where each node is a stage (agent, prompt, command, conditional, human, parallel, etc.) executed by the workflow engine.
|
||||
|
||||
### Rust crates (`lib/crates/`)
|
||||
- **fabro-cli** — CLI entry point. Commands: `run`, `exec`, `serve`, `validate`, `parse`, `cp`, `model`, `doctor`, `init`, `install`, `ps`, `system prune`, `llm`
|
||||
- **fabro-workflows** — Core workflow engine. Parses DOT graphs, runs stages, manages checkpoints/resume, hooks, retros, and human-in-the-loop interactions
|
||||
- **fabro-workflows** — Core workflow engine. Parses Graphviz graphs, runs stages, manages checkpoints/resume, hooks, retros, and human-in-the-loop interactions
|
||||
- **fabro-agent** — AI coding agent with tool use (Bash, Read, Write, Edit, Glob, Grep, WebFetch). `Sandbox` trait abstracts execution environments
|
||||
- **fabro-api** — Axum HTTP server. Routes for runs, sessions, models, completions, usage. SSE event streaming. Demo mode via header
|
||||
- **fabro-exe** — SSH-based sandbox implementation (`ExeSandbox`)
|
||||
|
|
@ -65,7 +70,7 @@ Fabro is an AI-powered workflow orchestration platform. Workflows are defined as
|
|||
|
||||
### Key design patterns
|
||||
- **Sandbox trait** — Uniform interface for local, Docker, SSH (ExeSandbox), Sprites, and Daytona execution environments
|
||||
- **DOT graph workflows** — Stages and transitions defined as DOT graph attributes
|
||||
- **Graphviz graph workflows** — Stages and transitions defined as Graphviz graph attributes
|
||||
- **OpenAPI-first** — `fabro-api.yaml` drives both Rust type generation (typify) and TypeScript client generation (openapi-generator)
|
||||
- **Checkpoint/resume** — Workflows can be paused, checkpointed, and resumed
|
||||
|
||||
|
|
@ -82,5 +87,5 @@ When interpolating values into shell command strings (in `fabro-exe` and `fabro-
|
|||
|
||||
## Testing workflows
|
||||
|
||||
- `fabro run <name>` — run a workflow by name (resolves `arc/workflows/<name>/workflow.toml`), e.g. `fabro run repl`
|
||||
- `fabro run <name>` — run a workflow by name (resolves `fabro/workflows/<name>/workflow.toml`), e.g. `fabro run repl`
|
||||
- Use `--no-retro` to skip the retro step and finish faster
|
||||
|
|
|
|||
524
Cargo.lock
generated
|
|
@ -2,6 +2,21 @@
|
|||
# It is not intended for manual editing.
|
||||
version = 4
|
||||
|
||||
[[package]]
|
||||
name = "addr2line"
|
||||
version = "0.25.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "1b5d307320b3181d6d7954e663bd7c774a838b8220fe0593c86d9fb09f498b4b"
|
||||
dependencies = [
|
||||
"gimli",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "adler2"
|
||||
version = "2.0.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "320119579fcad9c21884f5c4861d16174d0e06250625266f50fe6898340abefa"
|
||||
|
||||
[[package]]
|
||||
name = "ahash"
|
||||
version = "0.8.12"
|
||||
|
|
@ -299,6 +314,21 @@ dependencies = [
|
|||
"tracing",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "backtrace"
|
||||
version = "0.3.76"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "bb531853791a215d7c62a30daf0dde835f381ab5de4589cfe7c649d2cbe92bd6"
|
||||
dependencies = [
|
||||
"addr2line",
|
||||
"cfg-if",
|
||||
"libc",
|
||||
"miniz_oxide",
|
||||
"object",
|
||||
"rustc-demangle",
|
||||
"windows-link",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "base64"
|
||||
version = "0.22.1"
|
||||
|
|
@ -344,6 +374,15 @@ dependencies = [
|
|||
"generic-array",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "block2"
|
||||
version = "0.6.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "cdeb9d870516001442e364c5220d3574d2da8dc765554b4a617230d33fa58ef5"
|
||||
dependencies = [
|
||||
"objc2",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "bollard"
|
||||
version = "0.18.1"
|
||||
|
|
@ -516,6 +555,16 @@ version = "1.0.0"
|
|||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "3a822ea5bc7590f9d40f1ba12c0dc3c2760f3482c6984db1573ad11031420831"
|
||||
|
||||
[[package]]
|
||||
name = "cli-table"
|
||||
version = "0.5.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "14da8d951cef7cc4f13ccc9b744d736963d57863c7e6fc33c070ea274546082c"
|
||||
dependencies = [
|
||||
"termcolor",
|
||||
"unicode-width 0.2.2",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "cmake"
|
||||
version = "0.1.57"
|
||||
|
|
@ -908,6 +957,16 @@ dependencies = [
|
|||
"url",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "debugid"
|
||||
version = "0.8.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "bef552e6f588e446098f6ba40d89ac146c8c7b64aade83c051ee00bb5d2bc18d"
|
||||
dependencies = [
|
||||
"serde",
|
||||
"uuid",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "der"
|
||||
version = "0.7.10"
|
||||
|
|
@ -1016,6 +1075,16 @@ dependencies = [
|
|||
"windows-sys 0.61.2",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "dispatch2"
|
||||
version = "0.3.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "1e0e367e4e7da84520dedcac1901e4da967309406d1e51017ae1abfb97adbd38"
|
||||
dependencies = [
|
||||
"bitflags",
|
||||
"objc2",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "displaydoc"
|
||||
version = "0.2.5"
|
||||
|
|
@ -1093,6 +1162,17 @@ version = "1.0.2"
|
|||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "877a4ace8713b0bcf2a4e7eec82529c029f1d0619886d18145fea96c3ffe5c0f"
|
||||
|
||||
[[package]]
|
||||
name = "errno"
|
||||
version = "0.2.8"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "f639046355ee4f37944e44f60642c6f3a7efa3cf6b78c78a0d989a8ce6c396a1"
|
||||
dependencies = [
|
||||
"errno-dragonfly",
|
||||
"libc",
|
||||
"winapi",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "errno"
|
||||
version = "0.3.14"
|
||||
|
|
@ -1103,6 +1183,16 @@ dependencies = [
|
|||
"windows-sys 0.61.2",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "errno-dragonfly"
|
||||
version = "0.1.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "aa68f1b12764fab894d2755d2518754e71b4fd80ecfb822714a1206c2aab39bf"
|
||||
dependencies = [
|
||||
"cc",
|
||||
"libc",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "etcetera"
|
||||
version = "0.8.0"
|
||||
|
|
@ -1135,9 +1225,19 @@ dependencies = [
|
|||
"pin-project-lite",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "exec"
|
||||
version = "0.3.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "886b70328cba8871bfc025858e1de4be16b1d5088f2ba50b57816f4210672615"
|
||||
dependencies = [
|
||||
"errno 0.2.8",
|
||||
"libc",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "fabro-agent"
|
||||
version = "0.4.0"
|
||||
version = "0.6.0"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"async-trait",
|
||||
|
|
@ -1170,7 +1270,7 @@ dependencies = [
|
|||
|
||||
[[package]]
|
||||
name = "fabro-api"
|
||||
version = "0.4.0"
|
||||
version = "0.6.0"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"axum",
|
||||
|
|
@ -1218,9 +1318,18 @@ dependencies = [
|
|||
"x509-parser",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "fabro-beastie"
|
||||
version = "0.6.0"
|
||||
dependencies = [
|
||||
"core-foundation 0.9.4",
|
||||
"libc",
|
||||
"tracing",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "fabro-cli"
|
||||
version = "0.4.0"
|
||||
version = "0.6.0"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"assert_cmd",
|
||||
|
|
@ -1235,6 +1344,7 @@ dependencies = [
|
|||
"dotenvy",
|
||||
"fabro-agent",
|
||||
"fabro-api",
|
||||
"fabro-beastie",
|
||||
"fabro-config",
|
||||
"fabro-github",
|
||||
"fabro-llm",
|
||||
|
|
@ -1247,6 +1357,7 @@ dependencies = [
|
|||
"indicatif",
|
||||
"insta",
|
||||
"jsonwebtoken",
|
||||
"libc",
|
||||
"open",
|
||||
"predicates",
|
||||
"rand 0.8.5",
|
||||
|
|
@ -1257,6 +1368,7 @@ dependencies = [
|
|||
"semver",
|
||||
"serde",
|
||||
"serde_json",
|
||||
"sha2",
|
||||
"tempfile",
|
||||
"tokio",
|
||||
"toml",
|
||||
|
|
@ -1264,17 +1376,19 @@ dependencies = [
|
|||
"tracing-appender",
|
||||
"tracing-subscriber",
|
||||
"trycmd",
|
||||
"ulid",
|
||||
"x509-parser",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "fabro-config"
|
||||
version = "0.4.0"
|
||||
version = "0.6.0"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"dirs",
|
||||
"fabro-agent",
|
||||
"fabro-mcp",
|
||||
"fabro-util",
|
||||
"fabro-workflows",
|
||||
"serde",
|
||||
"tempfile",
|
||||
|
|
@ -1284,7 +1398,7 @@ dependencies = [
|
|||
|
||||
[[package]]
|
||||
name = "fabro-db"
|
||||
version = "0.4.0"
|
||||
version = "0.6.0"
|
||||
dependencies = [
|
||||
"chrono",
|
||||
"sqlx",
|
||||
|
|
@ -1295,7 +1409,7 @@ dependencies = [
|
|||
|
||||
[[package]]
|
||||
name = "fabro-devcontainer"
|
||||
version = "0.4.0"
|
||||
version = "0.6.0"
|
||||
dependencies = [
|
||||
"insta",
|
||||
"reqwest 0.12.28",
|
||||
|
|
@ -1310,7 +1424,7 @@ dependencies = [
|
|||
|
||||
[[package]]
|
||||
name = "fabro-exe"
|
||||
version = "0.4.0"
|
||||
version = "0.6.0"
|
||||
dependencies = [
|
||||
"async-trait",
|
||||
"base64",
|
||||
|
|
@ -1329,7 +1443,7 @@ dependencies = [
|
|||
|
||||
[[package]]
|
||||
name = "fabro-git-storage"
|
||||
version = "0.4.0"
|
||||
version = "0.6.0"
|
||||
dependencies = [
|
||||
"git2",
|
||||
"tempfile",
|
||||
|
|
@ -1340,7 +1454,7 @@ dependencies = [
|
|||
|
||||
[[package]]
|
||||
name = "fabro-github"
|
||||
version = "0.4.0"
|
||||
version = "0.6.0"
|
||||
dependencies = [
|
||||
"async-trait",
|
||||
"base64",
|
||||
|
|
@ -1357,7 +1471,7 @@ dependencies = [
|
|||
|
||||
[[package]]
|
||||
name = "fabro-linear"
|
||||
version = "0.4.0"
|
||||
version = "0.6.0"
|
||||
dependencies = [
|
||||
"async-trait",
|
||||
"fabro-tracker",
|
||||
|
|
@ -1371,13 +1485,14 @@ dependencies = [
|
|||
|
||||
[[package]]
|
||||
name = "fabro-llm"
|
||||
version = "0.4.0"
|
||||
version = "0.6.0"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"async-trait",
|
||||
"base64",
|
||||
"bytes",
|
||||
"clap",
|
||||
"cli-table",
|
||||
"dialoguer",
|
||||
"dotenvy",
|
||||
"fabro-util",
|
||||
|
|
@ -1399,7 +1514,7 @@ dependencies = [
|
|||
|
||||
[[package]]
|
||||
name = "fabro-mcp"
|
||||
version = "0.4.0"
|
||||
version = "0.6.0"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"futures",
|
||||
|
|
@ -1413,7 +1528,7 @@ dependencies = [
|
|||
|
||||
[[package]]
|
||||
name = "fabro-openai-oauth"
|
||||
version = "0.4.0"
|
||||
version = "0.6.0"
|
||||
dependencies = [
|
||||
"axum",
|
||||
"base64",
|
||||
|
|
@ -1431,7 +1546,7 @@ dependencies = [
|
|||
|
||||
[[package]]
|
||||
name = "fabro-slack"
|
||||
version = "0.4.0"
|
||||
version = "0.6.0"
|
||||
dependencies = [
|
||||
"fabro-workflows",
|
||||
"futures-util",
|
||||
|
|
@ -1449,7 +1564,7 @@ dependencies = [
|
|||
|
||||
[[package]]
|
||||
name = "fabro-sprites"
|
||||
version = "0.4.0"
|
||||
version = "0.6.0"
|
||||
dependencies = [
|
||||
"async-trait",
|
||||
"base64",
|
||||
|
|
@ -1466,7 +1581,7 @@ dependencies = [
|
|||
|
||||
[[package]]
|
||||
name = "fabro-ssh"
|
||||
version = "0.4.0"
|
||||
version = "0.6.0"
|
||||
dependencies = [
|
||||
"async-trait",
|
||||
"base64",
|
||||
|
|
@ -1484,14 +1599,14 @@ dependencies = [
|
|||
|
||||
[[package]]
|
||||
name = "fabro-tracker"
|
||||
version = "0.4.0"
|
||||
version = "0.6.0"
|
||||
dependencies = [
|
||||
"async-trait",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "fabro-types"
|
||||
version = "0.4.0"
|
||||
version = "0.6.0"
|
||||
dependencies = [
|
||||
"chrono",
|
||||
"prettyplease",
|
||||
|
|
@ -1506,7 +1621,7 @@ dependencies = [
|
|||
|
||||
[[package]]
|
||||
name = "fabro-util"
|
||||
version = "0.4.0"
|
||||
version = "0.6.0"
|
||||
dependencies = [
|
||||
"aho-corasick",
|
||||
"anyhow",
|
||||
|
|
@ -1514,12 +1629,15 @@ dependencies = [
|
|||
"chrono",
|
||||
"console 0.15.11",
|
||||
"dirs",
|
||||
"exec",
|
||||
"fork",
|
||||
"git2",
|
||||
"insta",
|
||||
"mac_address",
|
||||
"md5",
|
||||
"regex",
|
||||
"reqwest 0.12.28",
|
||||
"sentry",
|
||||
"serde",
|
||||
"serde_json",
|
||||
"tempfile",
|
||||
|
|
@ -1533,7 +1651,7 @@ dependencies = [
|
|||
|
||||
[[package]]
|
||||
name = "fabro-workflows"
|
||||
version = "0.4.0"
|
||||
version = "0.6.0"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"assert_cmd",
|
||||
|
|
@ -1541,6 +1659,7 @@ dependencies = [
|
|||
"base64",
|
||||
"chrono",
|
||||
"clap",
|
||||
"cli-table",
|
||||
"console 0.15.11",
|
||||
"daytona-api-client",
|
||||
"daytona-sdk",
|
||||
|
|
@ -1681,6 +1800,15 @@ version = "0.1.1"
|
|||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "00b0228411908ca8685dba7fc2cdd70ec9990a6e753e89b6ac91a84c40fbaf4b"
|
||||
|
||||
[[package]]
|
||||
name = "fork"
|
||||
version = "0.2.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "05dc8b302e04a1c27f4fe694439ef0f29779ca4edc205b7b58f00db04e29656d"
|
||||
dependencies = [
|
||||
"libc",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "form_urlencoded"
|
||||
version = "1.2.2"
|
||||
|
|
@ -1871,6 +1999,12 @@ dependencies = [
|
|||
"wasip3",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "gimli"
|
||||
version = "0.32.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "e629b9b98ef3dd8afe6ca2bd0f89306cec16d43d907889945bc5d6687f2f13c7"
|
||||
|
||||
[[package]]
|
||||
name = "git2"
|
||||
version = "0.20.4"
|
||||
|
|
@ -2011,6 +2145,17 @@ dependencies = [
|
|||
"windows-sys 0.61.2",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "hostname"
|
||||
version = "0.4.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "617aaa3557aef3810a6369d0a99fac8a080891b68bd9f9812a1eeda0c0730cbd"
|
||||
dependencies = [
|
||||
"cfg-if",
|
||||
"libc",
|
||||
"windows-link",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "htmd"
|
||||
version = "0.5.0"
|
||||
|
|
@ -2838,6 +2983,15 @@ version = "0.2.1"
|
|||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "68354c5c6bd36d73ff3feceb05efa59b6acb7626617f4962be322a825e61f79a"
|
||||
|
||||
[[package]]
|
||||
name = "miniz_oxide"
|
||||
version = "0.8.9"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "1fa76a2c86f704bdb222d66965fb3d63269ce38518b83cb0575fca855ebb6316"
|
||||
dependencies = [
|
||||
"adler2",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "mio"
|
||||
version = "1.1.1"
|
||||
|
|
@ -2911,6 +3065,18 @@ dependencies = [
|
|||
"memoffset",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "nix"
|
||||
version = "0.30.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "74523f3a35e05aba87a1d978330aef40f67b0304ac79c1c00b294c9830543db6"
|
||||
dependencies = [
|
||||
"bitflags",
|
||||
"cfg-if",
|
||||
"cfg_aliases",
|
||||
"libc",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "nix"
|
||||
version = "0.31.2"
|
||||
|
|
@ -3050,6 +3216,174 @@ dependencies = [
|
|||
"libm",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "objc2"
|
||||
version = "0.6.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "3a12a8ed07aefc768292f076dc3ac8c48f3781c8f2d5851dd3d98950e8c5a89f"
|
||||
dependencies = [
|
||||
"objc2-encode",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "objc2-cloud-kit"
|
||||
version = "0.3.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "73ad74d880bb43877038da939b7427bba67e9dd42004a18b809ba7d87cee241c"
|
||||
dependencies = [
|
||||
"bitflags",
|
||||
"objc2",
|
||||
"objc2-foundation",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "objc2-core-data"
|
||||
version = "0.3.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "0b402a653efbb5e82ce4df10683b6b28027616a2715e90009947d50b8dd298fa"
|
||||
dependencies = [
|
||||
"objc2",
|
||||
"objc2-foundation",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "objc2-core-foundation"
|
||||
version = "0.3.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "2a180dd8642fa45cdb7dd721cd4c11b1cadd4929ce112ebd8b9f5803cc79d536"
|
||||
dependencies = [
|
||||
"bitflags",
|
||||
"dispatch2",
|
||||
"objc2",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "objc2-core-graphics"
|
||||
version = "0.3.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "e022c9d066895efa1345f8e33e584b9f958da2fd4cd116792e15e07e4720a807"
|
||||
dependencies = [
|
||||
"bitflags",
|
||||
"dispatch2",
|
||||
"objc2",
|
||||
"objc2-core-foundation",
|
||||
"objc2-io-surface",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "objc2-core-image"
|
||||
version = "0.3.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "e5d563b38d2b97209f8e861173de434bd0214cf020e3423a52624cd1d989f006"
|
||||
dependencies = [
|
||||
"objc2",
|
||||
"objc2-foundation",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "objc2-core-location"
|
||||
version = "0.3.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "ca347214e24bc973fc025fd0d36ebb179ff30536ed1f80252706db19ee452009"
|
||||
dependencies = [
|
||||
"objc2",
|
||||
"objc2-foundation",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "objc2-core-text"
|
||||
version = "0.3.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "0cde0dfb48d25d2b4862161a4d5fcc0e3c24367869ad306b0c9ec0073bfed92d"
|
||||
dependencies = [
|
||||
"bitflags",
|
||||
"objc2",
|
||||
"objc2-core-foundation",
|
||||
"objc2-core-graphics",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "objc2-encode"
|
||||
version = "4.1.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "ef25abbcd74fb2609453eb695bd2f860d389e457f67dc17cafc8b8cbc89d0c33"
|
||||
|
||||
[[package]]
|
||||
name = "objc2-foundation"
|
||||
version = "0.3.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "e3e0adef53c21f888deb4fa59fc59f7eb17404926ee8a6f59f5df0fd7f9f3272"
|
||||
dependencies = [
|
||||
"bitflags",
|
||||
"block2",
|
||||
"libc",
|
||||
"objc2",
|
||||
"objc2-core-foundation",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "objc2-io-surface"
|
||||
version = "0.3.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "180788110936d59bab6bd83b6060ffdfffb3b922ba1396b312ae795e1de9d81d"
|
||||
dependencies = [
|
||||
"bitflags",
|
||||
"objc2",
|
||||
"objc2-core-foundation",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "objc2-quartz-core"
|
||||
version = "0.3.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "96c1358452b371bf9f104e21ec536d37a650eb10f7ee379fff67d2e08d537f1f"
|
||||
dependencies = [
|
||||
"bitflags",
|
||||
"objc2",
|
||||
"objc2-core-foundation",
|
||||
"objc2-foundation",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "objc2-ui-kit"
|
||||
version = "0.3.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "d87d638e33c06f577498cbcc50491496a3ed4246998a7fbba7ccb98b1e7eab22"
|
||||
dependencies = [
|
||||
"bitflags",
|
||||
"block2",
|
||||
"objc2",
|
||||
"objc2-cloud-kit",
|
||||
"objc2-core-data",
|
||||
"objc2-core-foundation",
|
||||
"objc2-core-graphics",
|
||||
"objc2-core-image",
|
||||
"objc2-core-location",
|
||||
"objc2-core-text",
|
||||
"objc2-foundation",
|
||||
"objc2-quartz-core",
|
||||
"objc2-user-notifications",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "objc2-user-notifications"
|
||||
version = "0.3.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "9df9128cbbfef73cda168416ccf7f837b62737d748333bfe9ab71c245d76613e"
|
||||
dependencies = [
|
||||
"objc2",
|
||||
"objc2-foundation",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "object"
|
||||
version = "0.37.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "ff76201f031d8863c38aa7f905eca4f53abbfa15f609db4277d44cd8938f33fe"
|
||||
dependencies = [
|
||||
"memchr",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "oid-registry"
|
||||
version = "0.7.1"
|
||||
|
|
@ -3163,6 +3497,22 @@ version = "0.2.0"
|
|||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "04744f49eae99ab78e0d5c0b603ab218f515ea8cfe5a456d7629ad883a3b6e7d"
|
||||
|
||||
[[package]]
|
||||
name = "os_info"
|
||||
version = "3.14.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "e4022a17595a00d6a369236fdae483f0de7f0a339960a53118b818238e132224"
|
||||
dependencies = [
|
||||
"android_system_properties",
|
||||
"log",
|
||||
"nix 0.30.1",
|
||||
"objc2",
|
||||
"objc2-foundation",
|
||||
"objc2-ui-kit",
|
||||
"serde",
|
||||
"windows-sys 0.61.2",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "os_pipe"
|
||||
version = "1.2.3"
|
||||
|
|
@ -3501,9 +3851,9 @@ dependencies = [
|
|||
|
||||
[[package]]
|
||||
name = "quinn-proto"
|
||||
version = "0.11.13"
|
||||
version = "0.11.14"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "f1906b49b0c3bc04b5fe5d86a77925ae6524a19b816ae38ce1e426255f1d8a31"
|
||||
checksum = "434b42fec591c96ef50e21e886936e66d3cc3f737104fdb9b737c40ffb94c098"
|
||||
dependencies = [
|
||||
"bytes",
|
||||
"getrandom 0.3.4",
|
||||
|
|
@ -3740,6 +4090,7 @@ dependencies = [
|
|||
"base64",
|
||||
"bytes",
|
||||
"encoding_rs",
|
||||
"futures-channel",
|
||||
"futures-core",
|
||||
"futures-util",
|
||||
"h2",
|
||||
|
|
@ -3906,6 +4257,12 @@ dependencies = [
|
|||
"zeroize",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "rustc-demangle"
|
||||
version = "0.1.27"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "b50b8869d9fc858ce7266cce0194bd74df58b9d0e3f6df3a9fc8eb470d95c09d"
|
||||
|
||||
[[package]]
|
||||
name = "rustc-hash"
|
||||
version = "2.1.1"
|
||||
|
|
@ -3937,7 +4294,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
|||
checksum = "b6fe4565b9518b83ef4f91bb47ce29620ca828bd32cb7e408f0062e9930ba190"
|
||||
dependencies = [
|
||||
"bitflags",
|
||||
"errno",
|
||||
"errno 0.3.14",
|
||||
"libc",
|
||||
"linux-raw-sys",
|
||||
"windows-sys 0.61.2",
|
||||
|
|
@ -4156,6 +4513,91 @@ version = "1.0.27"
|
|||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "d767eb0aabc880b29956c35734170f26ed551a859dbd361d140cdbeca61ab1e2"
|
||||
|
||||
[[package]]
|
||||
name = "sentry"
|
||||
version = "0.35.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "016958f51b96861dead7c1e02290f138411d05e94fad175c8636a835dee6e51e"
|
||||
dependencies = [
|
||||
"httpdate",
|
||||
"reqwest 0.12.28",
|
||||
"rustls",
|
||||
"sentry-backtrace",
|
||||
"sentry-contexts",
|
||||
"sentry-core",
|
||||
"sentry-tracing",
|
||||
"ureq",
|
||||
"webpki-roots 0.26.11",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "sentry-backtrace"
|
||||
version = "0.35.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "e57712c24e99252ef175b4b06c485294f10ad6bc5b5e1567ff3803ee7a0b7d3f"
|
||||
dependencies = [
|
||||
"backtrace",
|
||||
"once_cell",
|
||||
"regex",
|
||||
"sentry-core",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "sentry-contexts"
|
||||
version = "0.35.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "eba8754ec3b9279e00aa6d64916f211d44202370a1699afde1db2c16cbada089"
|
||||
dependencies = [
|
||||
"hostname",
|
||||
"libc",
|
||||
"os_info",
|
||||
"rustc_version",
|
||||
"sentry-core",
|
||||
"uname",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "sentry-core"
|
||||
version = "0.35.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "f9f8b6dcd4fbae1e3e22b447f32670360b27e31b62ab040f7fb04e0f80c04d92"
|
||||
dependencies = [
|
||||
"once_cell",
|
||||
"rand 0.8.5",
|
||||
"sentry-types",
|
||||
"serde",
|
||||
"serde_json",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "sentry-tracing"
|
||||
version = "0.35.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "263f73c757ed7915d3e1e34625eae18cad498a95b4261603d4ce3f87b159a6f0"
|
||||
dependencies = [
|
||||
"sentry-backtrace",
|
||||
"sentry-core",
|
||||
"tracing-core",
|
||||
"tracing-subscriber",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "sentry-types"
|
||||
version = "0.35.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "a71ed3a389948a6a6d92b98e997a2723ca22f09660c5a7b7388ecd509a70a527"
|
||||
dependencies = [
|
||||
"debugid",
|
||||
"hex",
|
||||
"rand 0.8.5",
|
||||
"serde",
|
||||
"serde_json",
|
||||
"thiserror 1.0.69",
|
||||
"time",
|
||||
"url",
|
||||
"uuid",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "serde"
|
||||
version = "1.0.228"
|
||||
|
|
@ -4393,7 +4835,7 @@ version = "1.4.8"
|
|||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "c4db69cba1110affc0e9f7bcd48bbf87b3f4fc7c61fc9155afd4c469eb3d6c1b"
|
||||
dependencies = [
|
||||
"errno",
|
||||
"errno 0.3.14",
|
||||
"libc",
|
||||
]
|
||||
|
||||
|
|
@ -4873,6 +5315,15 @@ dependencies = [
|
|||
"utf-8",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "termcolor"
|
||||
version = "1.4.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "06794f8f6c5c898b3275aebefa6b8a1cb24cd2c6c79397ab15774837a0bc5755"
|
||||
dependencies = [
|
||||
"winapi-util",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "termimad"
|
||||
version = "0.34.1"
|
||||
|
|
@ -5377,6 +5828,15 @@ dependencies = [
|
|||
"web-time",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "uname"
|
||||
version = "0.1.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "b72f89f0ca32e4db1c04e2a72f5345d59796d4866a1ee0609084569f73683dc8"
|
||||
dependencies = [
|
||||
"libc",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "unicase"
|
||||
version = "2.9.0"
|
||||
|
|
@ -5464,6 +5924,21 @@ version = "0.9.0"
|
|||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "8ecb6da28b8a351d773b68d5825ac39017e680750f980f3a1a85cd8dd28a47c1"
|
||||
|
||||
[[package]]
|
||||
name = "ureq"
|
||||
version = "2.12.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "02d1a66277ed75f640d608235660df48c8e3c19f3b4edb6a263315626cc3c01d"
|
||||
dependencies = [
|
||||
"base64",
|
||||
"log",
|
||||
"once_cell",
|
||||
"rustls",
|
||||
"rustls-pki-types",
|
||||
"url",
|
||||
"webpki-roots 0.26.11",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "url"
|
||||
version = "2.5.8"
|
||||
|
|
@ -5474,6 +5949,7 @@ dependencies = [
|
|||
"idna",
|
||||
"percent-encoding",
|
||||
"serde",
|
||||
"serde_derive",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
|
|
|
|||
|
|
@ -5,7 +5,7 @@ resolver = "2"
|
|||
|
||||
[workspace.package]
|
||||
edition = "2021"
|
||||
version = "0.4.0"
|
||||
version = "0.6.0"
|
||||
license = "MIT"
|
||||
|
||||
[workspace.dependencies]
|
||||
|
|
@ -30,6 +30,7 @@ jsonschema = "0.42"
|
|||
chrono = { version = "0.4", features = ["clock"] }
|
||||
bollard = "0.18"
|
||||
tar = "0.4"
|
||||
cli-table = { version = "0.5", default-features = false }
|
||||
console = "0.15"
|
||||
dialoguer = "0.12"
|
||||
git2 = "0.20"
|
||||
|
|
@ -58,11 +59,17 @@ futures-util = "0.3"
|
|||
openssh = "0.11"
|
||||
daytona-sdk = { git = "https://github.com/brynary/daytona-sdk-rust", rev = "06033ca", package = "daytona-sdk" }
|
||||
daytona-api-client = { git = "https://github.com/brynary/daytona-sdk-rust", rev = "06033ca", package = "daytona-api-client" }
|
||||
sentry = { version = "0.35", default-features = false, features = ["backtrace", "contexts", "ureq", "rustls"] }
|
||||
fork = "0.2"
|
||||
exec = "0.3"
|
||||
|
||||
[profile.release]
|
||||
lto = "thin"
|
||||
strip = true
|
||||
|
||||
[profile.dev.package."*"]
|
||||
debug = false # Disable debug info for all dependencies
|
||||
|
||||
# regex is extremely slow in debug builds (~10s to compile gitleaks patterns)
|
||||
[profile.dev.package.regex]
|
||||
opt-level = 2
|
||||
|
|
|
|||
45
README.md
|
|
@ -1,15 +1,22 @@
|
|||
<div align="left" id="top">
|
||||
<a href="https://fabro.dev"><img alt="Fabro" src="docs/logo/dark.svg" height="75"></a>
|
||||
<a href="https://docs.fabro.sh"><img alt="Fabro" src="docs/logo/dark.svg" height="75"></a>
|
||||
</div>
|
||||
|
||||
## The open source software factory for expert engineers
|
||||
## The open source dark software factory for expert engineers
|
||||
|
||||
AI coding agents are powerful but unpredictable. You either babysit every step or review a 50-file diff you don't trust. Fabro gives you a middle path: define the process as a graph, let agents execute it, and intervene only where it matters. [Why Fabro?](https://fabro.dev/getting-started/why-arc)
|
||||
AI coding agents are powerful but unpredictable. You either babysit every step or review a 50-file diff you don't trust. Fabro gives you a middle path: define the process as a graph, let agents execute it, and intervene only where it matters. [Why Fabro?](https://docs.fabro.sh/getting-started/why-fabro)
|
||||
|
||||
[](LICENSE.md)
|
||||
[](https://fabro.dev)
|
||||
[](https://docs.fabro.sh)
|
||||
|
||||
```bash
|
||||
# With Claude Code
|
||||
curl -fsSL https://fabro.sh/install.md | claude
|
||||
|
||||
# With Codex
|
||||
codex "$(curl -fsSL https://fabro.sh/install.md)"
|
||||
|
||||
# With Bash
|
||||
curl -fsSL https://fabro.sh/install.sh | bash
|
||||
```
|
||||
|
||||
|
|
@ -53,6 +60,8 @@ curl -fsSL https://fabro.sh/install.sh | bash
|
|||
|
||||
A plan-approve-implement workflow where a human reviews the plan before the agent writes code:
|
||||
|
||||
<img src="docs/images/plan-implement-readme.svg" alt="Plan-Implement workflow graph showing Start → Plan → Approve Plan → Implement → Simplify → Exit with a Revise loop" />
|
||||
|
||||
```dot
|
||||
digraph PlanImplement {
|
||||
graph [
|
||||
|
|
@ -78,19 +87,19 @@ digraph PlanImplement {
|
|||
}
|
||||
```
|
||||
|
||||
Agents run as multi-turn LLM sessions with tool access. Human gates (`hexagon`) pause for approval. The stylesheet routes planning to a cheap model and coding to a frontier model. See the [DOT language reference](https://fabro.dev/reference/dot-language) for the full syntax.
|
||||
Agents run as multi-turn LLM sessions with tool access. Human gates (`hexagon`) pause for approval. The stylesheet routes planning to a cheap model and coding to a frontier model. See the [Graphviz DOT language reference](https://docs.fabro.sh/reference/dot-language) for the full syntax.
|
||||
|
||||
---
|
||||
|
||||
## 📖 Documentation
|
||||
|
||||
Fabro ships with [comprehensive documentation](https://fabro.dev) covering every feature in depth:
|
||||
Fabro ships with [comprehensive documentation](https://docs.fabro.sh) covering every feature in depth:
|
||||
|
||||
- [**Getting Started**](https://fabro.dev/getting-started/introduction) -- Installation, first workflow, and why Fabro exists
|
||||
- [**Defining Workflows**](https://fabro.dev/workflows/stages-and-nodes) -- Node types, transitions, variables, stylesheets, and human gates
|
||||
- [**Executing Workflows**](https://fabro.dev/execution/run-configuration) -- Run configuration, sandboxes, checkpoints, retros, and failure handling
|
||||
- [**Tutorials**](https://fabro.dev/tutorials/hello-world) -- Step-by-step guides from hello world to parallel multi-model ensembles
|
||||
- [**API Reference**](https://fabro.dev/api-reference/overview) -- Full OpenAPI spec with authentication, SSE events, and client SDKs
|
||||
- [**Getting Started**](https://docs.fabro.sh/getting-started/introduction) -- Installation, first workflow, and why Fabro exists
|
||||
- [**Defining Workflows**](https://docs.fabro.sh/workflows/stages-and-nodes) -- Node types, transitions, variables, stylesheets, and human gates
|
||||
- [**Executing Workflows**](https://docs.fabro.sh/execution/run-configuration) -- Run configuration, sandboxes, checkpoints, retros, and failure handling
|
||||
- [**Tutorials**](https://docs.fabro.sh/tutorials/hello-world) -- Step-by-step guides from hello world to parallel multi-model ensembles
|
||||
- [**API Reference**](https://docs.fabro.sh/api-reference/overview) -- Full OpenAPI spec with authentication, SSE events, and client SDKs
|
||||
|
||||
---
|
||||
|
||||
|
|
@ -99,14 +108,14 @@ Fabro ships with [comprehensive documentation](https://fabro.dev) covering every
|
|||
### Install
|
||||
|
||||
```bash
|
||||
# With Claude Code
|
||||
curl -fsSL https://fabro.sh/install.md | claude
|
||||
|
||||
# With Codex
|
||||
codex "$(curl -fsSL https://fabro.sh/install.md)"
|
||||
|
||||
# With Bash
|
||||
curl -fsSL https://fabro.sh/install.sh | bash
|
||||
|
||||
# Initialize your project
|
||||
cd my-repo/
|
||||
fabro init
|
||||
|
||||
# Run your first workflow
|
||||
fabro run hello
|
||||
```
|
||||
|
||||
---
|
||||
|
|
|
|||
|
|
@ -3,7 +3,7 @@ import type { LanguageRegistration } from "@pierre/diffs";
|
|||
export const dotLanguage: LanguageRegistration = {
|
||||
name: "dot",
|
||||
scopeName: "source.dot",
|
||||
fileTypes: ["dot", "DOT", "gv"],
|
||||
fileTypes: ["fabro", "dot", "DOT", "gv"],
|
||||
firstLineMatch: "digraph.*",
|
||||
patterns: [
|
||||
{
|
||||
|
|
|
|||
|
|
@ -42,13 +42,13 @@ export async function loader({ request }: Route.LoaderArgs) {
|
|||
const { provider } = config.web.auth;
|
||||
const demoMode = isDemoMode(request);
|
||||
if (provider === "insecure_disabled") {
|
||||
return { user: DEMO_USER, demoMode, feature_flags: config.feature_flags };
|
||||
return { user: DEMO_USER, demoMode, features: config.features };
|
||||
}
|
||||
if (provider === "github" && !isGitHubAppConfigured()) {
|
||||
throw redirect("/setup");
|
||||
}
|
||||
const user = await requireUser(request);
|
||||
return { user, provider, demoMode, feature_flags: config.feature_flags };
|
||||
return { user, provider, demoMode, features: config.features };
|
||||
}
|
||||
|
||||
export async function action({ request }: Route.ActionArgs) {
|
||||
|
|
|
|||
|
|
@ -20,8 +20,9 @@ interface GitConfig {
|
|||
slug: string | null;
|
||||
}
|
||||
|
||||
interface FeatureFlags {
|
||||
interface Features {
|
||||
session_sandboxes: boolean;
|
||||
retros: boolean;
|
||||
}
|
||||
|
||||
interface WebConfig {
|
||||
|
|
@ -33,7 +34,7 @@ interface AppConfig {
|
|||
web: WebConfig;
|
||||
api: ApiConfig;
|
||||
git: GitConfig;
|
||||
feature_flags: FeatureFlags;
|
||||
features: Features;
|
||||
}
|
||||
|
||||
const AUTH_DEFAULTS: AuthConfig = {
|
||||
|
|
@ -58,8 +59,9 @@ const GIT_DEFAULTS: GitConfig = {
|
|||
slug: null,
|
||||
};
|
||||
|
||||
const FEATURE_FLAGS_DEFAULTS: FeatureFlags = {
|
||||
const FEATURES_DEFAULTS: Features = {
|
||||
session_sandboxes: false,
|
||||
retros: false,
|
||||
};
|
||||
|
||||
export const FABRO_CONFIG_PATH = join(homedir(), ".fabro", "server.toml");
|
||||
|
|
@ -78,7 +80,7 @@ function loadAppConfig(): AppConfig {
|
|||
const rawWebAuth = (rawWeb.auth ?? {}) as Partial<AuthConfig>;
|
||||
const rawApi = (raw.api ?? {}) as Partial<ApiConfig>;
|
||||
const rawGit = (raw.git ?? {}) as Partial<GitConfig>;
|
||||
const rawFeatureFlags = (raw.feature_flags ?? {}) as Partial<FeatureFlags>;
|
||||
const rawFeatures = (raw.features ?? {}) as Partial<Features>;
|
||||
|
||||
const demo = process.env.FABRO_DEMO === "1";
|
||||
|
||||
|
|
@ -94,7 +96,7 @@ function loadAppConfig(): AppConfig {
|
|||
? { ...API_DEFAULTS, ...rawApi, authentication_strategy: "insecure_disabled" }
|
||||
: { ...API_DEFAULTS, ...rawApi },
|
||||
git: { ...GIT_DEFAULTS, ...rawGit },
|
||||
feature_flags: { ...FEATURE_FLAGS_DEFAULTS, ...rawFeatureFlags },
|
||||
features: { ...FEATURES_DEFAULTS, ...rawFeatures },
|
||||
};
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -33,12 +33,12 @@ export function meta({}: Route.MetaArgs) {
|
|||
}
|
||||
|
||||
export async function loader({ request }: Route.LoaderArgs) {
|
||||
const { feature_flags } = getAppConfig();
|
||||
const { features } = getAppConfig();
|
||||
const { data: apiSessions } = await apiJson<PaginatedSessionList>("/sessions", { request });
|
||||
const sessionGroups = groupSessionsByDate(
|
||||
apiSessions.map((s) => ({ id: s.id, title: s.title, created_at: s.created_at }))
|
||||
);
|
||||
return { sessionGroups, feature_flags };
|
||||
return { sessionGroups, features };
|
||||
}
|
||||
|
||||
const projects = [
|
||||
|
|
@ -99,7 +99,7 @@ function SessionSidebar({ groups }: { groups: { label: string; sessions: { id: s
|
|||
}
|
||||
|
||||
export default function Start({ loaderData }: Route.ComponentProps) {
|
||||
const { sessionGroups, feature_flags } = loaderData;
|
||||
const { sessionGroups, features } = loaderData;
|
||||
const [prompt, setPrompt] = useState("");
|
||||
const [project, setProject] = useState(projects[0]);
|
||||
const [branch, setBranch] = useState(branches[0]);
|
||||
|
|
@ -158,7 +158,7 @@ export default function Start({ loaderData }: Route.ComponentProps) {
|
|||
/>
|
||||
|
||||
<div className="absolute bottom-3 inset-x-3 flex items-center justify-between">
|
||||
{feature_flags.session_sandboxes && (
|
||||
{features.session_sandboxes && (
|
||||
<div className="flex items-center gap-1.5">
|
||||
<Picker
|
||||
value={project}
|
||||
|
|
|
|||
|
|
@ -19,12 +19,12 @@ export const workflowData: Record<string, WorkflowEntry> = {
|
|||
fix_build: {
|
||||
name: "Fix Build",
|
||||
slug: "fix_build",
|
||||
filename: "fix_build.dot",
|
||||
filename: "fix_build.fabro",
|
||||
description: "Automatically diagnoses and fixes CI build failures by analyzing error logs, identifying root causes, and applying targeted code changes.",
|
||||
config: {
|
||||
version: 1,
|
||||
goal: "Diagnose and fix CI build failures",
|
||||
graph: "fix_build.dot",
|
||||
graph: "fix_build.fabro",
|
||||
llm: { model: "claude-sonnet" },
|
||||
vars: { repo_url: "https://github.com/org/service", branch: "main" },
|
||||
sandbox: {
|
||||
|
|
@ -60,12 +60,12 @@ export const workflowData: Record<string, WorkflowEntry> = {
|
|||
implement: {
|
||||
name: "Implement Feature",
|
||||
slug: "implement",
|
||||
filename: "implement.dot",
|
||||
filename: "implement.fabro",
|
||||
description: "Generates production-ready code from a technical blueprint, including tests, documentation, and a pull request ready for review.",
|
||||
config: {
|
||||
version: 1,
|
||||
goal: "Implement feature from technical blueprint",
|
||||
graph: "implement.dot",
|
||||
graph: "implement.fabro",
|
||||
llm: { model: "claude-sonnet" },
|
||||
vars: { spec_path: "specs/feature.md", test_framework: "vitest" },
|
||||
setup: { commands: ["bun install", "bun run typecheck"], timeout_ms: 120000 },
|
||||
|
|
@ -116,12 +116,12 @@ export const workflowData: Record<string, WorkflowEntry> = {
|
|||
sync_drift: {
|
||||
name: "Sync Drift",
|
||||
slug: "sync_drift",
|
||||
filename: "sync_drift.dot",
|
||||
filename: "sync_drift.fabro",
|
||||
description: "Detects configuration and code drift between environments, then generates reconciliation patches to bring everything back in sync.",
|
||||
config: {
|
||||
version: 1,
|
||||
goal: "Detect and reconcile configuration drift across environments",
|
||||
graph: "sync_drift.dot",
|
||||
graph: "sync_drift.fabro",
|
||||
llm: { model: "claude-sonnet" },
|
||||
vars: { source_env: "production", target_env: "staging", drift_threshold: "warn" },
|
||||
sandbox: {
|
||||
|
|
@ -161,12 +161,12 @@ export const workflowData: Record<string, WorkflowEntry> = {
|
|||
expand: {
|
||||
name: "Expand Product",
|
||||
slug: "expand",
|
||||
filename: "expand.dot",
|
||||
filename: "expand.fabro",
|
||||
description: "Evolves the product by analyzing usage patterns and specifications to propose and implement incremental improvements.",
|
||||
config: {
|
||||
version: 1,
|
||||
goal: "Propose and implement incremental product improvements",
|
||||
graph: "expand.dot",
|
||||
graph: "expand.fabro",
|
||||
llm: { model: "claude-sonnet" },
|
||||
vars: { analytics_window: "30d", min_confidence: "0.8" },
|
||||
sandbox: {
|
||||
|
|
|
|||
|
Before Width: | Height: | Size: 13 KiB After Width: | Height: | Size: 7.7 KiB |
|
Before Width: | Height: | Size: 14 KiB After Width: | Height: | Size: 8 KiB |
|
Before Width: | Height: | Size: 43 KiB After Width: | Height: | Size: 22 KiB |
|
Before Width: | Height: | Size: 7.8 KiB After Width: | Height: | Size: 7.8 KiB |
|
Before Width: | Height: | Size: 7.8 KiB After Width: | Height: | Size: 7.8 KiB |
1
apps/marketing/.gitignore
vendored
|
|
@ -1,3 +1,4 @@
|
|||
dist/
|
||||
.astro/
|
||||
node_modules/
|
||||
.vercel
|
||||
|
|
|
|||
|
|
@ -4,7 +4,21 @@ import tailwindcss from "@tailwindcss/vite";
|
|||
|
||||
export default defineConfig({
|
||||
integrations: [react()],
|
||||
redirects: {
|
||||
"/discord": {
|
||||
status: 302,
|
||||
destination:
|
||||
"https://discord.gg/KE6w49Vg",
|
||||
},
|
||||
"/docs": {
|
||||
status: 302,
|
||||
destination: "https://docs.fabro.sh",
|
||||
},
|
||||
},
|
||||
vite: {
|
||||
plugins: [tailwindcss()],
|
||||
server: {
|
||||
allowedHosts: [".ngrok-free.app"],
|
||||
},
|
||||
},
|
||||
});
|
||||
|
|
|
|||
673
apps/marketing/font-comparison.html
Normal file
|
|
@ -0,0 +1,673 @@
|
|||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<title>Fabro — Font Treatment Comparison</title>
|
||||
|
||||
<!-- Current fonts -->
|
||||
<link rel="preconnect" href="https://fonts.googleapis.com" />
|
||||
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
|
||||
<link href="https://fonts.googleapis.com/css2?family=Space+Grotesk:wght@400;500;600;700&family=DM+Sans:opsz,wght@9..40,400;9..40,500;9..40,600;9..40,700&family=JetBrains+Mono:wght@400;500&family=Outfit:wght@400;500;600;700&family=Lexend:wght@400;500;600;700&family=Fira+Code:wght@400;500&family=Instrument+Serif:ital@0;1&family=Instrument+Sans:ital,wght@0,400;0,500;0,600;0,700;1,400&family=IBM+Plex+Mono:wght@400;500&family=Syne:wght@400;500;600;700;800&family=Manrope:wght@400;500;600;700&family=Source+Code+Pro:wght@400;500&display=swap" rel="stylesheet" />
|
||||
|
||||
<style>
|
||||
:root {
|
||||
--navy-950: #0F1729;
|
||||
--navy-900: #141C2F;
|
||||
--navy-800: #252C3D;
|
||||
--navy-600: #4B5768;
|
||||
--teal-300: #B5DDEF;
|
||||
--teal-500: #67B2D7;
|
||||
--teal-700: #357F9E;
|
||||
--ice-50: #F7F9FB;
|
||||
--ice-100: #E8EDF3;
|
||||
--ice-300: #A8B5C5;
|
||||
--mint: #5AC8A8;
|
||||
--amber: #F0A45B;
|
||||
--coral: #E86B6B;
|
||||
}
|
||||
|
||||
* { margin: 0; padding: 0; box-sizing: border-box; }
|
||||
|
||||
body {
|
||||
background: var(--navy-950);
|
||||
color: var(--ice-50);
|
||||
min-height: 100vh;
|
||||
-webkit-font-smoothing: antialiased;
|
||||
}
|
||||
|
||||
/* ── Page header ── */
|
||||
.page-header {
|
||||
text-align: center;
|
||||
padding: 48px 24px 24px;
|
||||
border-bottom: 1px solid var(--navy-800);
|
||||
}
|
||||
.page-header h1 {
|
||||
font-family: 'Space Grotesk', sans-serif;
|
||||
font-size: 28px;
|
||||
font-weight: 600;
|
||||
color: var(--ice-50);
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
.page-header p {
|
||||
font-family: 'DM Sans', sans-serif;
|
||||
font-size: 15px;
|
||||
color: var(--ice-300);
|
||||
}
|
||||
|
||||
/* ── Selector bar ── */
|
||||
.selector-bar {
|
||||
position: sticky;
|
||||
top: 0;
|
||||
z-index: 50;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
gap: 6px;
|
||||
padding: 16px 24px;
|
||||
background: var(--navy-950);
|
||||
border-bottom: 1px solid var(--navy-800);
|
||||
backdrop-filter: blur(16px);
|
||||
}
|
||||
.selector-btn {
|
||||
font-family: 'DM Sans', sans-serif;
|
||||
font-size: 13px;
|
||||
font-weight: 600;
|
||||
padding: 8px 20px;
|
||||
border-radius: 8px;
|
||||
border: 1px solid var(--navy-600);
|
||||
background: transparent;
|
||||
color: var(--ice-300);
|
||||
cursor: pointer;
|
||||
transition: all 0.2s;
|
||||
}
|
||||
.selector-btn:hover {
|
||||
border-color: var(--teal-500);
|
||||
color: var(--ice-50);
|
||||
}
|
||||
.selector-btn.active {
|
||||
background: var(--teal-700);
|
||||
border-color: var(--teal-700);
|
||||
color: white;
|
||||
}
|
||||
|
||||
/* ── Layout ── */
|
||||
.treatments {
|
||||
max-width: 1400px;
|
||||
margin: 0 auto;
|
||||
}
|
||||
.treatment {
|
||||
display: none;
|
||||
padding: 48px 24px 80px;
|
||||
}
|
||||
.treatment.visible { display: block; }
|
||||
|
||||
.treatment-label {
|
||||
text-align: center;
|
||||
margin-bottom: 48px;
|
||||
}
|
||||
.treatment-label .tag {
|
||||
display: inline-block;
|
||||
font-family: 'DM Sans', sans-serif;
|
||||
font-size: 11px;
|
||||
font-weight: 600;
|
||||
letter-spacing: 0.08em;
|
||||
text-transform: uppercase;
|
||||
color: var(--teal-300);
|
||||
background: rgba(53, 127, 158, 0.15);
|
||||
border: 1px solid rgba(53, 127, 158, 0.3);
|
||||
padding: 4px 14px;
|
||||
border-radius: 100px;
|
||||
margin-bottom: 12px;
|
||||
}
|
||||
.treatment-label h2 {
|
||||
font-size: 20px;
|
||||
font-weight: 600;
|
||||
color: var(--ice-50);
|
||||
margin-bottom: 6px;
|
||||
}
|
||||
.treatment-label .fonts-list {
|
||||
font-size: 13px;
|
||||
color: var(--ice-300);
|
||||
}
|
||||
.treatment-label .fonts-list span {
|
||||
color: var(--teal-300);
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
/* ── Sample sections ── */
|
||||
.samples {
|
||||
max-width: 960px;
|
||||
margin: 0 auto;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 56px;
|
||||
}
|
||||
|
||||
/* Nav sample */
|
||||
.sample-nav {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
padding: 16px 24px;
|
||||
border: 1px solid var(--navy-800);
|
||||
border-radius: 12px;
|
||||
background: var(--navy-900);
|
||||
}
|
||||
.sample-nav .logo { font-weight: 700; font-size: 22px; }
|
||||
.sample-nav .links {
|
||||
display: flex;
|
||||
gap: 28px;
|
||||
font-size: 14px;
|
||||
font-weight: 500;
|
||||
color: var(--ice-100);
|
||||
}
|
||||
.sample-nav .cta-btn {
|
||||
font-size: 13px;
|
||||
font-weight: 600;
|
||||
padding: 8px 18px;
|
||||
border-radius: 8px;
|
||||
background: var(--teal-700);
|
||||
color: white;
|
||||
border: none;
|
||||
}
|
||||
|
||||
/* Hero sample */
|
||||
.sample-hero {
|
||||
text-align: center;
|
||||
}
|
||||
.sample-hero .badge {
|
||||
display: inline-block;
|
||||
font-size: 13px;
|
||||
font-weight: 500;
|
||||
color: var(--teal-300);
|
||||
border: 1px solid rgba(53, 127, 158, 0.35);
|
||||
background: rgba(53, 127, 158, 0.08);
|
||||
padding: 6px 16px;
|
||||
border-radius: 100px;
|
||||
margin-bottom: 24px;
|
||||
}
|
||||
.sample-hero h3 {
|
||||
font-size: 52px;
|
||||
font-weight: 700;
|
||||
line-height: 1.08;
|
||||
letter-spacing: -0.025em;
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
.sample-hero h3 .gradient {
|
||||
background: linear-gradient(135deg, var(--teal-300), var(--mint), var(--teal-500));
|
||||
background-size: 200% 200%;
|
||||
-webkit-background-clip: text;
|
||||
-webkit-text-fill-color: transparent;
|
||||
background-clip: text;
|
||||
}
|
||||
.sample-hero .subtitle {
|
||||
font-size: 18px;
|
||||
line-height: 1.65;
|
||||
color: var(--ice-300);
|
||||
max-width: 580px;
|
||||
margin: 0 auto;
|
||||
}
|
||||
|
||||
/* Feature cards */
|
||||
.sample-features {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(3, 1fr);
|
||||
gap: 16px;
|
||||
}
|
||||
.feature-card {
|
||||
padding: 28px 24px;
|
||||
border: 1px solid var(--navy-800);
|
||||
border-radius: 12px;
|
||||
background: linear-gradient(to bottom, var(--navy-900), var(--navy-950));
|
||||
transition: border-color 0.3s, box-shadow 0.3s, transform 0.3s;
|
||||
}
|
||||
.feature-card:hover {
|
||||
border-color: rgba(103, 178, 215, 0.25);
|
||||
box-shadow: 0 0 40px -8px rgba(103, 178, 215, 0.12);
|
||||
transform: translateY(-2px);
|
||||
}
|
||||
.feature-card .icon {
|
||||
width: 36px;
|
||||
height: 36px;
|
||||
border-radius: 8px;
|
||||
background: rgba(53, 127, 158, 0.12);
|
||||
border: 1px solid rgba(53, 127, 158, 0.2);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
margin-bottom: 16px;
|
||||
color: var(--teal-300);
|
||||
font-size: 16px;
|
||||
}
|
||||
.feature-card h4 {
|
||||
font-size: 16px;
|
||||
font-weight: 600;
|
||||
margin-bottom: 8px;
|
||||
color: var(--ice-50);
|
||||
}
|
||||
.feature-card p {
|
||||
font-size: 14px;
|
||||
line-height: 1.6;
|
||||
color: var(--ice-300);
|
||||
}
|
||||
|
||||
/* Code sample */
|
||||
.sample-code {
|
||||
border: 1px solid var(--navy-800);
|
||||
border-radius: 12px;
|
||||
overflow: hidden;
|
||||
background: var(--navy-900);
|
||||
}
|
||||
.sample-code .code-header {
|
||||
padding: 12px 20px;
|
||||
border-bottom: 1px solid var(--navy-800);
|
||||
font-size: 13px;
|
||||
font-weight: 500;
|
||||
color: var(--ice-300);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
}
|
||||
.sample-code .code-header .dot {
|
||||
width: 8px;
|
||||
height: 8px;
|
||||
border-radius: 50%;
|
||||
background: var(--mint);
|
||||
opacity: 0.6;
|
||||
}
|
||||
.sample-code pre {
|
||||
padding: 24px;
|
||||
font-size: 13.5px;
|
||||
line-height: 1.7;
|
||||
overflow-x: auto;
|
||||
color: var(--ice-300);
|
||||
}
|
||||
.sample-code .kw { color: var(--teal-300); }
|
||||
.sample-code .fn { color: var(--ice-50); }
|
||||
.sample-code .prop { color: var(--teal-500); }
|
||||
.sample-code .str { color: var(--ice-300); }
|
||||
.sample-code .cm { color: var(--navy-600); }
|
||||
|
||||
/* Body text sample */
|
||||
.sample-prose {
|
||||
max-width: 640px;
|
||||
}
|
||||
.sample-prose h4 {
|
||||
font-size: 24px;
|
||||
font-weight: 700;
|
||||
margin-bottom: 16px;
|
||||
color: var(--ice-50);
|
||||
}
|
||||
.sample-prose p {
|
||||
font-size: 16px;
|
||||
line-height: 1.75;
|
||||
color: var(--ice-300);
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
.sample-prose a {
|
||||
color: var(--teal-300);
|
||||
text-decoration: underline;
|
||||
text-underline-offset: 3px;
|
||||
}
|
||||
.sample-prose code {
|
||||
font-size: 0.875em;
|
||||
background: var(--navy-800);
|
||||
padding: 2px 6px;
|
||||
border-radius: 4px;
|
||||
color: var(--teal-300);
|
||||
}
|
||||
|
||||
/* ── Side by side mode ── */
|
||||
.grid-view {
|
||||
display: none;
|
||||
grid-template-columns: 1fr 1fr;
|
||||
gap: 0;
|
||||
}
|
||||
.grid-view.visible { display: grid; }
|
||||
.grid-view .treatment {
|
||||
display: block;
|
||||
padding: 40px 20px 60px;
|
||||
border-right: 1px solid var(--navy-800);
|
||||
}
|
||||
.grid-view .treatment:last-child { border-right: none; }
|
||||
.grid-view .samples { gap: 40px; }
|
||||
.grid-view .sample-hero h3 { font-size: 32px; }
|
||||
.grid-view .sample-hero .subtitle { font-size: 15px; }
|
||||
.grid-view .sample-features { grid-template-columns: 1fr; }
|
||||
.grid-view .sample-nav { padding: 12px 16px; }
|
||||
.grid-view .sample-nav .links { display: none; }
|
||||
.grid-view .sample-prose h4 { font-size: 20px; }
|
||||
.grid-view .sample-prose p { font-size: 14px; }
|
||||
.grid-view .sample-code pre { font-size: 12px; }
|
||||
|
||||
/* ── Font family assignments ── */
|
||||
|
||||
/* Current: Space Grotesk / DM Sans / JetBrains Mono */
|
||||
.t-current .display { font-family: 'Space Grotesk', sans-serif; }
|
||||
.t-current .body { font-family: 'DM Sans', sans-serif; }
|
||||
.t-current .mono { font-family: 'JetBrains Mono', monospace; }
|
||||
|
||||
/* A: Outfit / Lexend / Fira Code */
|
||||
.t-a .display { font-family: 'Outfit', sans-serif; }
|
||||
.t-a .body { font-family: 'Lexend', sans-serif; }
|
||||
.t-a .mono { font-family: 'Fira Code', monospace; }
|
||||
|
||||
/* B: Instrument Serif / Instrument Sans / IBM Plex Mono */
|
||||
.t-b .display { font-family: 'Instrument Serif', serif; }
|
||||
.t-b .body { font-family: 'Instrument Sans', sans-serif; }
|
||||
.t-b .mono { font-family: 'IBM Plex Mono', monospace; }
|
||||
|
||||
/* C: Syne / Manrope / Source Code Pro */
|
||||
.t-c .display { font-family: 'Syne', sans-serif; }
|
||||
.t-c .body { font-family: 'Manrope', sans-serif; }
|
||||
.t-c .mono { font-family: 'Source Code Pro', monospace; }
|
||||
|
||||
/* view toggle */
|
||||
.view-toggle {
|
||||
display: flex;
|
||||
gap: 4px;
|
||||
margin-left: auto;
|
||||
}
|
||||
.view-btn {
|
||||
font-family: 'DM Sans', sans-serif;
|
||||
font-size: 12px;
|
||||
font-weight: 500;
|
||||
padding: 6px 12px;
|
||||
border-radius: 6px;
|
||||
border: 1px solid var(--navy-800);
|
||||
background: transparent;
|
||||
color: var(--ice-300);
|
||||
cursor: pointer;
|
||||
transition: all 0.2s;
|
||||
}
|
||||
.view-btn.active {
|
||||
background: var(--navy-800);
|
||||
color: var(--ice-50);
|
||||
}
|
||||
|
||||
@media (max-width: 768px) {
|
||||
.sample-features { grid-template-columns: 1fr; }
|
||||
.sample-hero h3 { font-size: 36px; }
|
||||
.grid-view { grid-template-columns: 1fr; }
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
|
||||
<div class="page-header">
|
||||
<h1>Font Treatment Comparison</h1>
|
||||
<p>Comparing the current Fabro marketing site fonts against 3 alternatives</p>
|
||||
</div>
|
||||
|
||||
<div class="selector-bar">
|
||||
<button class="selector-btn active" data-target="current" onclick="show('current')">Current</button>
|
||||
<button class="selector-btn" data-target="a" onclick="show('a')">A: Razor Geometric</button>
|
||||
<button class="selector-btn" data-target="b" onclick="show('b')">B: Editorial Contrast</button>
|
||||
<button class="selector-btn" data-target="c" onclick="show('c')">C: Bold Industrial</button>
|
||||
<div class="view-toggle">
|
||||
<button class="view-btn active" onclick="setView('single')">Single</button>
|
||||
<button class="view-btn" onclick="setView('grid')">2-up</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- ═══════════════════════════════════════ SINGLE VIEW ═══════════════════════════════════════ -->
|
||||
<div class="treatments single-view" id="single-view">
|
||||
|
||||
<!-- CURRENT -->
|
||||
<div class="treatment t-current visible" data-id="current">
|
||||
<div class="treatment-label">
|
||||
<div class="tag">Current</div>
|
||||
<h2 class="display">Space Grotesk + DM Sans + JetBrains Mono</h2>
|
||||
<p class="fonts-list body">Display: <span>Space Grotesk</span> · Body: <span>DM Sans</span> · Mono: <span>JetBrains Mono</span></p>
|
||||
</div>
|
||||
<div class="samples">
|
||||
<div class="sample-nav">
|
||||
<div class="logo display">fabro</div>
|
||||
<div class="links body"><span>Docs</span><span>Blog</span><span>Showcase</span><span>Roadmap</span></div>
|
||||
<button class="cta-btn body">Get started</button>
|
||||
</div>
|
||||
<div class="sample-hero">
|
||||
<div class="badge body">Open source workflow engine</div>
|
||||
<h3 class="display">The dark software factory<br/>for <span class="gradient">expert engineers</span></h3>
|
||||
<p class="subtitle body">Stop babysitting agents. Define your process as a workflow graph, let AI agents execute it, and intervene only where it matters.</p>
|
||||
</div>
|
||||
<div class="sample-features">
|
||||
<div class="feature-card"><div class="icon">◉</div><h4 class="display">Workflow graphs</h4><p class="body">Define multi-step processes as Graphviz directed graphs. Each node is a stage with its own prompt, model, and tools.</p></div>
|
||||
<div class="feature-card"><div class="icon">⬡</div><h4 class="display">Human-in-the-loop</h4><p class="body">Hexagonal approval gates let humans review, revise, or override at any point in the workflow.</p></div>
|
||||
<div class="feature-card"><div class="icon">⦾</div><h4 class="display">Multi-model routing</h4><p class="body">Assign models per-stage with CSS-like selectors. Use Haiku for triage, Sonnet for code, Gemini for review.</p></div>
|
||||
</div>
|
||||
<div class="sample-code">
|
||||
<div class="code-header mono"><div class="dot"></div>workflow.dot</div>
|
||||
<pre class="mono"><span class="kw">digraph</span> <span class="fn">PlanImplement</span> {
|
||||
<span class="kw">graph</span> [
|
||||
<span class="prop">goal</span>=<span class="str">"Plan, approve, implement, and simplify"</span>
|
||||
]
|
||||
|
||||
<span class="prop">plan</span> [label=<span class="str">"Plan"</span>]
|
||||
<span class="prop">approve</span> [shape=hexagon, label=<span class="str">"Approve Plan"</span>]
|
||||
<span class="prop">implement</span> [label=<span class="str">"Implement"</span>, class=<span class="str">"coding"</span>]
|
||||
|
||||
<span class="str">start -> plan -> approve -> implement -> exit</span>
|
||||
}</pre>
|
||||
</div>
|
||||
<div class="sample-prose">
|
||||
<h4 class="display">Verification is a first-class concept</h4>
|
||||
<p class="body">Every workflow can include build gates, test assertions, and human approval stages. When a stage fails, Fabro routes back to the appropriate fix loop — no manual re-runs, no <code class="mono">git stash</code> gymnastics.</p>
|
||||
<p class="body">The <a href="#">retrospective engine</a> runs after every workflow, surfacing what went wrong and why. Over time, your workflows get smarter.</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- OPTION A: Razor Geometric -->
|
||||
<div class="treatment t-a" data-id="a">
|
||||
<div class="treatment-label">
|
||||
<div class="tag">Option A</div>
|
||||
<h2 class="display">Outfit + Lexend + Fira Code</h2>
|
||||
<p class="fonts-list body">Display: <span>Outfit</span> · Body: <span>Lexend</span> · Mono: <span>Fira Code</span></p>
|
||||
</div>
|
||||
<div class="samples">
|
||||
<div class="sample-nav">
|
||||
<div class="logo display">fabro</div>
|
||||
<div class="links body"><span>Docs</span><span>Blog</span><span>Showcase</span><span>Roadmap</span></div>
|
||||
<button class="cta-btn body">Get started</button>
|
||||
</div>
|
||||
<div class="sample-hero">
|
||||
<div class="badge body">Open source workflow engine</div>
|
||||
<h3 class="display">The dark software factory<br/>for <span class="gradient">expert engineers</span></h3>
|
||||
<p class="subtitle body">Stop babysitting agents. Define your process as a workflow graph, let AI agents execute it, and intervene only where it matters.</p>
|
||||
</div>
|
||||
<div class="sample-features">
|
||||
<div class="feature-card"><div class="icon">◉</div><h4 class="display">Workflow graphs</h4><p class="body">Define multi-step processes as Graphviz directed graphs. Each node is a stage with its own prompt, model, and tools.</p></div>
|
||||
<div class="feature-card"><div class="icon">⬡</div><h4 class="display">Human-in-the-loop</h4><p class="body">Hexagonal approval gates let humans review, revise, or override at any point in the workflow.</p></div>
|
||||
<div class="feature-card"><div class="icon">⦾</div><h4 class="display">Multi-model routing</h4><p class="body">Assign models per-stage with CSS-like selectors. Use Haiku for triage, Sonnet for code, Gemini for review.</p></div>
|
||||
</div>
|
||||
<div class="sample-code">
|
||||
<div class="code-header mono"><div class="dot"></div>workflow.dot</div>
|
||||
<pre class="mono"><span class="kw">digraph</span> <span class="fn">PlanImplement</span> {
|
||||
<span class="kw">graph</span> [
|
||||
<span class="prop">goal</span>=<span class="str">"Plan, approve, implement, and simplify"</span>
|
||||
]
|
||||
|
||||
<span class="prop">plan</span> [label=<span class="str">"Plan"</span>]
|
||||
<span class="prop">approve</span> [shape=hexagon, label=<span class="str">"Approve Plan"</span>]
|
||||
<span class="prop">implement</span> [label=<span class="str">"Implement"</span>, class=<span class="str">"coding"</span>]
|
||||
|
||||
<span class="str">start -> plan -> approve -> implement -> exit</span>
|
||||
}</pre>
|
||||
</div>
|
||||
<div class="sample-prose">
|
||||
<h4 class="display">Verification is a first-class concept</h4>
|
||||
<p class="body">Every workflow can include build gates, test assertions, and human approval stages. When a stage fails, Fabro routes back to the appropriate fix loop — no manual re-runs, no <code class="mono">git stash</code> gymnastics.</p>
|
||||
<p class="body">The <a href="#">retrospective engine</a> runs after every workflow, surfacing what went wrong and why. Over time, your workflows get smarter.</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- OPTION B: Editorial Contrast -->
|
||||
<div class="treatment t-b" data-id="b">
|
||||
<div class="treatment-label">
|
||||
<div class="tag">Option B</div>
|
||||
<h2 class="display">Instrument Serif + Instrument Sans + IBM Plex Mono</h2>
|
||||
<p class="fonts-list body">Display: <span>Instrument Serif</span> · Body: <span>Instrument Sans</span> · Mono: <span>IBM Plex Mono</span></p>
|
||||
</div>
|
||||
<div class="samples">
|
||||
<div class="sample-nav">
|
||||
<div class="logo display">fabro</div>
|
||||
<div class="links body"><span>Docs</span><span>Blog</span><span>Showcase</span><span>Roadmap</span></div>
|
||||
<button class="cta-btn body">Get started</button>
|
||||
</div>
|
||||
<div class="sample-hero">
|
||||
<div class="badge body">Open source workflow engine</div>
|
||||
<h3 class="display">The dark software factory<br/>for <span class="gradient">expert engineers</span></h3>
|
||||
<p class="subtitle body">Stop babysitting agents. Define your process as a workflow graph, let AI agents execute it, and intervene only where it matters.</p>
|
||||
</div>
|
||||
<div class="sample-features">
|
||||
<div class="feature-card"><div class="icon">◉</div><h4 class="display">Workflow graphs</h4><p class="body">Define multi-step processes as Graphviz directed graphs. Each node is a stage with its own prompt, model, and tools.</p></div>
|
||||
<div class="feature-card"><div class="icon">⬡</div><h4 class="display">Human-in-the-loop</h4><p class="body">Hexagonal approval gates let humans review, revise, or override at any point in the workflow.</p></div>
|
||||
<div class="feature-card"><div class="icon">⦾</div><h4 class="display">Multi-model routing</h4><p class="body">Assign models per-stage with CSS-like selectors. Use Haiku for triage, Sonnet for code, Gemini for review.</p></div>
|
||||
</div>
|
||||
<div class="sample-code">
|
||||
<div class="code-header mono"><div class="dot"></div>workflow.dot</div>
|
||||
<pre class="mono"><span class="kw">digraph</span> <span class="fn">PlanImplement</span> {
|
||||
<span class="kw">graph</span> [
|
||||
<span class="prop">goal</span>=<span class="str">"Plan, approve, implement, and simplify"</span>
|
||||
]
|
||||
|
||||
<span class="prop">plan</span> [label=<span class="str">"Plan"</span>]
|
||||
<span class="prop">approve</span> [shape=hexagon, label=<span class="str">"Approve Plan"</span>]
|
||||
<span class="prop">implement</span> [label=<span class="str">"Implement"</span>, class=<span class="str">"coding"</span>]
|
||||
|
||||
<span class="str">start -> plan -> approve -> implement -> exit</span>
|
||||
}</pre>
|
||||
</div>
|
||||
<div class="sample-prose">
|
||||
<h4 class="display">Verification is a first-class concept</h4>
|
||||
<p class="body">Every workflow can include build gates, test assertions, and human approval stages. When a stage fails, Fabro routes back to the appropriate fix loop — no manual re-runs, no <code class="mono">git stash</code> gymnastics.</p>
|
||||
<p class="body">The <a href="#">retrospective engine</a> runs after every workflow, surfacing what went wrong and why. Over time, your workflows get smarter.</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- OPTION C: Bold Industrial -->
|
||||
<div class="treatment t-c" data-id="c">
|
||||
<div class="treatment-label">
|
||||
<div class="tag">Option C</div>
|
||||
<h2 class="display">Syne + Manrope + Source Code Pro</h2>
|
||||
<p class="fonts-list body">Display: <span>Syne</span> · Body: <span>Manrope</span> · Mono: <span>Source Code Pro</span></p>
|
||||
</div>
|
||||
<div class="samples">
|
||||
<div class="sample-nav">
|
||||
<div class="logo display">fabro</div>
|
||||
<div class="links body"><span>Docs</span><span>Blog</span><span>Showcase</span><span>Roadmap</span></div>
|
||||
<button class="cta-btn body">Get started</button>
|
||||
</div>
|
||||
<div class="sample-hero">
|
||||
<div class="badge body">Open source workflow engine</div>
|
||||
<h3 class="display">The dark software factory<br/>for <span class="gradient">expert engineers</span></h3>
|
||||
<p class="subtitle body">Stop babysitting agents. Define your process as a workflow graph, let AI agents execute it, and intervene only where it matters.</p>
|
||||
</div>
|
||||
<div class="sample-features">
|
||||
<div class="feature-card"><div class="icon">◉</div><h4 class="display">Workflow graphs</h4><p class="body">Define multi-step processes as Graphviz directed graphs. Each node is a stage with its own prompt, model, and tools.</p></div>
|
||||
<div class="feature-card"><div class="icon">⬡</div><h4 class="display">Human-in-the-loop</h4><p class="body">Hexagonal approval gates let humans review, revise, or override at any point in the workflow.</p></div>
|
||||
<div class="feature-card"><div class="icon">⦾</div><h4 class="display">Multi-model routing</h4><p class="body">Assign models per-stage with CSS-like selectors. Use Haiku for triage, Sonnet for code, Gemini for review.</p></div>
|
||||
</div>
|
||||
<div class="sample-code">
|
||||
<div class="code-header mono"><div class="dot"></div>workflow.dot</div>
|
||||
<pre class="mono"><span class="kw">digraph</span> <span class="fn">PlanImplement</span> {
|
||||
<span class="kw">graph</span> [
|
||||
<span class="prop">goal</span>=<span class="str">"Plan, approve, implement, and simplify"</span>
|
||||
]
|
||||
|
||||
<span class="prop">plan</span> [label=<span class="str">"Plan"</span>]
|
||||
<span class="prop">approve</span> [shape=hexagon, label=<span class="str">"Approve Plan"</span>]
|
||||
<span class="prop">implement</span> [label=<span class="str">"Implement"</span>, class=<span class="str">"coding"</span>]
|
||||
|
||||
<span class="str">start -> plan -> approve -> implement -> exit</span>
|
||||
}</pre>
|
||||
</div>
|
||||
<div class="sample-prose">
|
||||
<h4 class="display">Verification is a first-class concept</h4>
|
||||
<p class="body">Every workflow can include build gates, test assertions, and human approval stages. When a stage fails, Fabro routes back to the appropriate fix loop — no manual re-runs, no <code class="mono">git stash</code> gymnastics.</p>
|
||||
<p class="body">The <a href="#">retrospective engine</a> runs after every workflow, surfacing what went wrong and why. Over time, your workflows get smarter.</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
<!-- ═══════════════════════════════════════ GRID VIEW ═══════════════════════════════════════ -->
|
||||
<div class="grid-view" id="grid-view">
|
||||
<!-- filled dynamically -->
|
||||
</div>
|
||||
|
||||
<script>
|
||||
let currentView = 'single';
|
||||
let activeIds = ['current'];
|
||||
|
||||
function show(id) {
|
||||
if (currentView === 'single') {
|
||||
activeIds = [id];
|
||||
document.querySelectorAll('#single-view .treatment').forEach(el => {
|
||||
el.classList.toggle('visible', el.dataset.id === id);
|
||||
});
|
||||
} else {
|
||||
// grid: toggle
|
||||
const idx = activeIds.indexOf(id);
|
||||
if (idx > -1) {
|
||||
if (activeIds.length > 1) activeIds.splice(idx, 1);
|
||||
} else {
|
||||
if (activeIds.length >= 2) activeIds.shift();
|
||||
activeIds.push(id);
|
||||
}
|
||||
renderGrid();
|
||||
}
|
||||
updateButtons();
|
||||
}
|
||||
|
||||
function updateButtons() {
|
||||
document.querySelectorAll('.selector-btn').forEach(btn => {
|
||||
btn.classList.toggle('active', activeIds.includes(btn.dataset.target));
|
||||
});
|
||||
}
|
||||
|
||||
function setView(v) {
|
||||
currentView = v;
|
||||
document.querySelectorAll('.view-btn').forEach(b => b.classList.remove('active'));
|
||||
document.querySelector(`.view-btn[onclick="setView('${v}')"]`).classList.add('active');
|
||||
|
||||
if (v === 'single') {
|
||||
document.getElementById('single-view').style.display = 'block';
|
||||
document.getElementById('grid-view').classList.remove('visible');
|
||||
if (activeIds.length > 1) activeIds = [activeIds[0]];
|
||||
document.querySelectorAll('#single-view .treatment').forEach(el => {
|
||||
el.classList.toggle('visible', el.dataset.id === activeIds[0]);
|
||||
});
|
||||
} else {
|
||||
document.getElementById('single-view').style.display = 'none';
|
||||
if (activeIds.length < 2) {
|
||||
const all = ['current', 'a', 'b', 'c'];
|
||||
const next = all.find(x => !activeIds.includes(x));
|
||||
activeIds.push(next);
|
||||
}
|
||||
renderGrid();
|
||||
}
|
||||
updateButtons();
|
||||
}
|
||||
|
||||
function renderGrid() {
|
||||
const grid = document.getElementById('grid-view');
|
||||
grid.innerHTML = '';
|
||||
grid.classList.add('visible');
|
||||
activeIds.forEach(id => {
|
||||
const src = document.querySelector(`#single-view .treatment[data-id="${id}"]`);
|
||||
if (src) {
|
||||
const clone = src.cloneNode(true);
|
||||
clone.classList.add('visible');
|
||||
grid.appendChild(clone);
|
||||
}
|
||||
});
|
||||
}
|
||||
</script>
|
||||
|
||||
</body>
|
||||
</html>
|
||||
84
apps/marketing/og-image-template.html
Normal file
|
|
@ -0,0 +1,84 @@
|
|||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<link rel="preconnect" href="https://fonts.googleapis.com" />
|
||||
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
|
||||
<link href="https://fonts.googleapis.com/css2?family=Space+Grotesk:wght@500;600;700&family=DM+Sans:opsz,wght@9..40,400;9..40,500&display=swap" rel="stylesheet" />
|
||||
<style>
|
||||
* { margin: 0; padding: 0; box-sizing: border-box; }
|
||||
|
||||
.card {
|
||||
width: 1200px;
|
||||
height: 630px;
|
||||
background: linear-gradient(135deg, #0a0f1a 0%, #0F1729 40%, #1a2640 70%, #1e2d4a 100%);
|
||||
color: #F7F9FB;
|
||||
font-family: 'DM Sans', sans-serif;
|
||||
padding: 56px 80px;
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.atmosphere {
|
||||
position: absolute;
|
||||
top: 0; right: 0;
|
||||
width: 700px; height: 630px;
|
||||
background: radial-gradient(ellipse at 80% 30%, rgba(103, 178, 215, 0.08) 0%, transparent 60%);
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
.logo {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 16px;
|
||||
margin-bottom: 160px;
|
||||
}
|
||||
|
||||
.logo svg { height: 44px; width: auto; }
|
||||
|
||||
.logo-text {
|
||||
font-family: 'Space Grotesk', sans-serif;
|
||||
font-weight: 700;
|
||||
font-size: 32px;
|
||||
color: #67b2d7;
|
||||
letter-spacing: 0.06em;
|
||||
}
|
||||
|
||||
.section-label {
|
||||
font-size: 22px;
|
||||
font-weight: 500;
|
||||
color: #67b2d7;
|
||||
margin-bottom: 12px;
|
||||
}
|
||||
|
||||
.title {
|
||||
font-family: 'Space Grotesk', sans-serif;
|
||||
font-weight: 700;
|
||||
font-size: 64px;
|
||||
line-height: 1.1;
|
||||
letter-spacing: -0.02em;
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
.description {
|
||||
font-size: 22px;
|
||||
color: #A8B5C5;
|
||||
line-height: 1.5;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="card">
|
||||
<div class="atmosphere"></div>
|
||||
<div class="logo">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 272 348" height="44">
|
||||
<path d="M1 237 L62 272 L61 348 L0 312 Z M98 257 L132 275 L132 312 L71 347 L70 272 Z M202 168 L201 230 L141 264 L142 202 Z M70 169 L132 203 L132 264 L70 230 Z M70 241 L90 251 L70 262 Z M3 129 L63 164 L61 262 L1 227 Z M137 125 L196 160 L137 195 L78 160 Z M271 44 L272 119 L211 154 L210 79 Z M132 43 L132 118 L71 153 L70 78 Z M142 43 L202 77 L201 152 L141 118 Z M1 44 L62 78 L62 152 L1 118 Z M206 0 L266 36 L206 72 L146 36 Z M66 1 L126 36 L66 71 L6 36 Z" fill="#67b2d7"/>
|
||||
</svg>
|
||||
<span class="logo-text">FABRO</span>
|
||||
</div>
|
||||
<div class="section-label">Open Source</div>
|
||||
<h1 class="title">The Dark Software Factory</h1>
|
||||
<p class="description">Define your process as a workflow graph. Let AI agents execute it. Intervene only where it matters.</p>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
|
|
@ -10,6 +10,7 @@
|
|||
"dependencies": {
|
||||
"@astrojs/react": "^4.2.1",
|
||||
"@tailwindcss/vite": "^4.2.1",
|
||||
"@viz-js/viz": "^3.25.0",
|
||||
"astro": "^5.9.3",
|
||||
"react": "^19.2.4",
|
||||
"react-dom": "^19.2.4",
|
||||
|
|
|
|||
|
|
@ -1,19 +1,3 @@
|
|||
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M9.06145 23.1079C5.26816 22.3769 -3.39077 20.6274 1.4173 5.06384C9.6344 6.09939 16.9728 14.0644 9.06145 23.1079Z" fill="url(#paint0_linear_17557_2021)"/>
|
||||
<path d="M8.91928 23.0939C5.27642 21.2223 0.78371 4.20891 17.0071 0C20.7569 7.19341 19.6212 16.5452 8.91928 23.0939Z" fill="url(#paint1_linear_17557_2021)"/>
|
||||
<path d="M8.91388 23.0788C8.73534 19.8817 10.1585 9.08525 23.5699 13.1107C23.1812 20.1229 18.984 26.4182 8.91388 23.0788Z" fill="url(#paint2_linear_17557_2021)"/>
|
||||
<defs>
|
||||
<linearGradient id="paint0_linear_17557_2021" x1="3.77557" y1="5.91571" x2="5.23185" y2="21.5589" gradientUnits="userSpaceOnUse">
|
||||
<stop stop-color="#18E299"/>
|
||||
<stop offset="1" stop-color="#15803D"/>
|
||||
</linearGradient>
|
||||
<linearGradient id="paint1_linear_17557_2021" x1="12.1711" y1="-0.718425" x2="10.1897" y2="22.9832" gradientUnits="userSpaceOnUse">
|
||||
<stop stop-color="#16A34A"/>
|
||||
<stop offset="1" stop-color="#4ADE80"/>
|
||||
</linearGradient>
|
||||
<linearGradient id="paint2_linear_17557_2021" x1="23.1327" y1="15.353" x2="9.33841" y2="18.5196" gradientUnits="userSpaceOnUse">
|
||||
<stop stop-color="#4ADE80"/>
|
||||
<stop offset="1" stop-color="#0D9373"/>
|
||||
</linearGradient>
|
||||
</defs>
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 272 348">
|
||||
<path d="M1 237 L62 272 L61 348 L0 312 Z M98 257 L132 275 L132 312 L71 347 L70 272 Z M202 168 L201 230 L141 264 L142 202 Z M70 169 L132 203 L132 264 L70 230 Z M70 241 L90 251 L70 262 Z M3 129 L63 164 L61 262 L1 227 Z M137 125 L196 160 L137 195 L78 160 Z M271 44 L272 119 L211 154 L210 79 Z M132 43 L132 118 L71 153 L70 78 Z M142 43 L202 77 L201 152 L141 118 Z M1 44 L62 78 L62 152 L1 118 Z M206 0 L266 36 L206 72 L146 36 Z M66 1 L126 36 L66 71 L6 36 Z" fill="#67b2d7"/>
|
||||
</svg>
|
||||
|
|
|
|||
|
Before Width: | Height: | Size: 1.2 KiB After Width: | Height: | Size: 565 B |
101
apps/marketing/public/install.md
Normal file
|
|
@ -0,0 +1,101 @@
|
|||
# fabro
|
||||
|
||||
> AI-powered workflow orchestration platform. Workflows are defined as Graphviz graphs and executed by the Fabro engine.
|
||||
|
||||
I want you to install Fabro for me. Execute all the steps below autonomously.
|
||||
|
||||
OBJECTIVE: Install the Fabro CLI binary and verify it works.
|
||||
|
||||
DONE WHEN: `fabro --version` prints a version string.
|
||||
|
||||
## TODO
|
||||
|
||||
- [ ] Ensure the `gh` CLI is installed and authenticated
|
||||
- [ ] Detect the platform and download the correct release asset
|
||||
- [ ] Extract the binary and install it to `~/.fabro/bin`
|
||||
- [ ] Add `~/.fabro/bin` to PATH in the user's shell config
|
||||
- [ ] Verify `fabro --version` works
|
||||
|
||||
## Ensure `gh` CLI is available
|
||||
|
||||
You need the GitHub CLI (`gh`) installed and authenticated. Check with:
|
||||
|
||||
```bash
|
||||
gh auth status
|
||||
```
|
||||
|
||||
If `gh` is not installed, install it:
|
||||
|
||||
- **macOS:** `brew install gh`
|
||||
- **Linux:** See https://github.com/cli/cli/blob/trunk/docs/install_linux.md
|
||||
|
||||
Then authenticate with `gh auth login` if needed.
|
||||
|
||||
## Detect platform and download
|
||||
|
||||
Determine the correct release asset based on the platform:
|
||||
|
||||
| OS | Architecture | Asset name |
|
||||
|---|---|---|
|
||||
| macOS | arm64 (Apple Silicon) | `fabro-aarch64-apple-darwin.tar.gz` |
|
||||
| Linux | x86_64 | `fabro-x86_64-unknown-linux-gnu.tar.gz` |
|
||||
|
||||
On macOS, if `uname -m` reports `x86_64`, check for Rosetta translation with `sysctl -n sysctl.proc_translated`. If it returns `1`, the actual architecture is arm64.
|
||||
|
||||
Download the asset using `gh`:
|
||||
|
||||
```bash
|
||||
gh release download --repo fabro-sh/fabro --pattern "fabro-<TARGET>.tar.gz" --dir /tmp/fabro-install --clobber
|
||||
```
|
||||
|
||||
Replace `<TARGET>` with the correct target triple from the table above.
|
||||
|
||||
## Extract and install
|
||||
|
||||
```bash
|
||||
tar xzf /tmp/fabro-install/fabro-<TARGET>.tar.gz -C /tmp/fabro-install
|
||||
mkdir -p ~/.fabro/bin
|
||||
mv /tmp/fabro-install/fabro-<TARGET>/fabro ~/.fabro/bin/fabro
|
||||
chmod +x ~/.fabro/bin/fabro
|
||||
rm -rf /tmp/fabro-install
|
||||
```
|
||||
|
||||
## Add to PATH
|
||||
|
||||
If `~/.fabro/bin` is not already on PATH, you must add it to the user's shell configuration file so it persists across sessions.
|
||||
|
||||
**zsh** — append to `~/.zshrc` (or `$ZDOTDIR/.zshrc` if `ZDOTDIR` is set):
|
||||
|
||||
```bash
|
||||
printf '\n# fabro\nexport PATH="$HOME/.fabro/bin:$PATH"\n' >> ~/.zshrc
|
||||
```
|
||||
|
||||
**bash** — append to `~/.bash_profile` if it exists, otherwise `~/.bashrc`:
|
||||
|
||||
```bash
|
||||
printf '\n# fabro\nexport PATH="$HOME/.fabro/bin:$PATH"\n' >> ~/.bash_profile
|
||||
```
|
||||
|
||||
**fish** — append to `~/.config/fish/config.fish`:
|
||||
|
||||
```bash
|
||||
printf '\n# fabro\nfish_add_path $HOME/.fabro/bin\n' >> ~/.config/fish/config.fish
|
||||
```
|
||||
|
||||
After updating the config file, export PATH for the current session too:
|
||||
|
||||
```bash
|
||||
export PATH="$HOME/.fabro/bin:$PATH"
|
||||
```
|
||||
|
||||
## Verify
|
||||
|
||||
```bash
|
||||
fabro --version
|
||||
```
|
||||
|
||||
This must print a version string like `fabro 0.x.y`. If it does, installation is complete.
|
||||
|
||||
Tell the user to run `fabro install` in their terminal to complete setup.
|
||||
|
||||
EXECUTE NOW: Complete the above TODO list to achieve: `fabro --version` prints a version string.
|
||||
162
apps/marketing/public/install.sh
Executable file
|
|
@ -0,0 +1,162 @@
|
|||
#!/bin/sh
|
||||
set -eu
|
||||
|
||||
REPO="fabro-sh/fabro"
|
||||
|
||||
# Colors (only when stderr is a terminal)
|
||||
if [ -t 2 ]; then
|
||||
RED='\033[0;31m'
|
||||
GREEN='\033[0;32m'
|
||||
DIM='\033[2m'
|
||||
BOLD='\033[1m'
|
||||
BOLD_CYAN='\033[1;36m'
|
||||
RESET='\033[0m'
|
||||
else
|
||||
RED=''
|
||||
GREEN=''
|
||||
DIM=''
|
||||
BOLD=''
|
||||
BOLD_CYAN=''
|
||||
RESET=''
|
||||
fi
|
||||
|
||||
info() { printf " %b\n" "$1" >&2; }
|
||||
step() { printf " ${BOLD}%b${RESET}\n" "$1" >&2; }
|
||||
dim() { printf " ${DIM}%b${RESET}\n" "$1" >&2; }
|
||||
success() { printf " ${GREEN}✔${RESET} %b\n" "$1" >&2; }
|
||||
error() { printf " ${RED}✗ %b${RESET}\n" "$1" >&2; exit 1; }
|
||||
|
||||
# --- Header ---
|
||||
printf "\n ⚒️ ${BOLD}Fabro Install${RESET}\n\n" >&2
|
||||
|
||||
# --- Require gh CLI ---
|
||||
if ! command -v gh >/dev/null 2>&1; then
|
||||
error "gh CLI is required but not installed. Install it from ${BOLD_CYAN}https://cli.github.com${RESET}"
|
||||
fi
|
||||
|
||||
# --- Detect platform ---
|
||||
OS="$(uname -s)"
|
||||
ARCH="$(uname -m)"
|
||||
|
||||
case "$OS" in
|
||||
Darwin)
|
||||
# Detect Rosetta translation
|
||||
if [ "$ARCH" = "x86_64" ]; then
|
||||
if sysctl -n sysctl.proc_translated 2>/dev/null | grep -q 1; then
|
||||
ARCH="arm64"
|
||||
fi
|
||||
fi
|
||||
case "$ARCH" in
|
||||
arm64) TARGET="aarch64-apple-darwin" ;;
|
||||
*) error "Unsupported macOS architecture: $ARCH. Supported: Apple Silicon (arm64)" ;;
|
||||
esac
|
||||
;;
|
||||
Linux)
|
||||
case "$ARCH" in
|
||||
x86_64) TARGET="x86_64-unknown-linux-gnu" ;;
|
||||
*) error "Unsupported Linux architecture: $ARCH. Supported: x86_64" ;;
|
||||
esac
|
||||
;;
|
||||
*)
|
||||
error "Unsupported OS: $OS. Supported platforms: macOS (Apple Silicon), Linux (x86_64)"
|
||||
;;
|
||||
esac
|
||||
|
||||
ASSET="fabro-${TARGET}.tar.gz"
|
||||
TMPDIR="$(mktemp -d)"
|
||||
trap 'rm -rf "$TMPDIR"' EXIT
|
||||
|
||||
dim "Downloading fabro for ${TARGET}..."
|
||||
gh release download --repo "$REPO" --pattern "$ASSET" --dir "$TMPDIR" --clobber
|
||||
|
||||
dim "Extracting..."
|
||||
tar xzf "${TMPDIR}/${ASSET}" -C "$TMPDIR"
|
||||
|
||||
# --- Install binary ---
|
||||
INSTALL_DIR="${FABRO_INSTALL_DIR:-$HOME/.fabro/bin}"
|
||||
mkdir -p "$INSTALL_DIR"
|
||||
mv "${TMPDIR}/fabro-${TARGET}/fabro" "${INSTALL_DIR}/fabro"
|
||||
|
||||
chmod +x "${INSTALL_DIR}/fabro"
|
||||
|
||||
# --- Verify ---
|
||||
VERSION="$("${INSTALL_DIR}/fabro" --version 2>/dev/null || true)"
|
||||
if [ -z "$VERSION" ]; then
|
||||
error "Installation failed: could not run fabro --version"
|
||||
fi
|
||||
|
||||
tildify() {
|
||||
if [ "${1#"$HOME"/}" != "$1" ]; then
|
||||
echo "~/${1#"$HOME"/}"
|
||||
else
|
||||
echo "$1"
|
||||
fi
|
||||
}
|
||||
|
||||
success "Installed ${VERSION} to ${BOLD_CYAN}$(tildify "${INSTALL_DIR}/fabro")${RESET}"
|
||||
|
||||
# --- Ensure install dir is on PATH ---
|
||||
if command -v fabro >/dev/null 2>&1; then
|
||||
dim "fabro is already on \$PATH, skipping shell config"
|
||||
else
|
||||
tilde_bin_dir=$(tildify "$INSTALL_DIR")
|
||||
echo "" >&2
|
||||
|
||||
if [ -t 2 ] && [ -e /dev/tty ]; then
|
||||
case $(basename "${SHELL:-sh}") in
|
||||
zsh)
|
||||
: "${ZDOTDIR:="$HOME"}"
|
||||
shell_config="${ZDOTDIR%/}/.zshrc"
|
||||
{
|
||||
printf '\n# fabro\n'
|
||||
echo "export PATH=\"$INSTALL_DIR:\$PATH\""
|
||||
} >>"$shell_config"
|
||||
info "Added ${BOLD_CYAN}${tilde_bin_dir}${RESET} to \$PATH in ${BOLD_CYAN}$(tildify "$shell_config")${RESET}"
|
||||
;;
|
||||
bash)
|
||||
shell_config="$HOME/.bashrc"
|
||||
if [ -f "$HOME/.bash_profile" ]; then
|
||||
shell_config="$HOME/.bash_profile"
|
||||
fi
|
||||
{
|
||||
printf '\n# fabro\n'
|
||||
echo "export PATH=\"$INSTALL_DIR:\$PATH\""
|
||||
} >>"$shell_config"
|
||||
info "Added ${BOLD_CYAN}${tilde_bin_dir}${RESET} to \$PATH in ${BOLD_CYAN}$(tildify "$shell_config")${RESET}"
|
||||
;;
|
||||
fish)
|
||||
fish_config="$HOME/.config/fish/config.fish"
|
||||
mkdir -p "$(dirname "$fish_config")"
|
||||
{
|
||||
printf '\n# fabro\n'
|
||||
echo "fish_add_path $INSTALL_DIR"
|
||||
} >>"$fish_config"
|
||||
info "Added ${BOLD_CYAN}${tilde_bin_dir}${RESET} to \$PATH in ${BOLD_CYAN}$(tildify "$fish_config")${RESET}"
|
||||
;;
|
||||
*)
|
||||
info "Add ${BOLD_CYAN}${tilde_bin_dir}${RESET} to your PATH:"
|
||||
echo "" >&2
|
||||
info " ${BOLD}export PATH=\"${INSTALL_DIR}:\$PATH\"${RESET}"
|
||||
;;
|
||||
esac
|
||||
else
|
||||
info "Add ${BOLD_CYAN}${tilde_bin_dir}${RESET} to your PATH:"
|
||||
echo "" >&2
|
||||
info " ${BOLD}export PATH=\"${INSTALL_DIR}:\$PATH\"${RESET}"
|
||||
fi
|
||||
|
||||
export PATH="${INSTALL_DIR}:$PATH"
|
||||
fi
|
||||
echo "" >&2
|
||||
|
||||
# --- Prompt to run setup wizard ---
|
||||
if [ -t 2 ] && [ -e /dev/tty ]; then
|
||||
printf " ${BOLD}Run ${BOLD_CYAN}fabro install${RESET}${BOLD} now to complete setup? [Y/n]${RESET} " >&2
|
||||
read -r answer </dev/tty
|
||||
case "$answer" in
|
||||
[nN]*) dim "Skipping. Run ${BOLD_CYAN}fabro install${RESET}${DIM} whenever you're ready." ;;
|
||||
*) echo "" >&2; exec "${INSTALL_DIR}/fabro" install ;;
|
||||
esac
|
||||
else
|
||||
info "Run ${BOLD_CYAN}fabro install${RESET} to complete setup."
|
||||
fi
|
||||
80
apps/marketing/public/llms.txt
Normal file
|
|
@ -0,0 +1,80 @@
|
|||
# Fabro
|
||||
|
||||
> Fabro is the open source dark software factory for expert engineers. Define your process as a workflow graph, let AI agents execute it, and intervene only where it matters.
|
||||
|
||||
Fabro replaces the prompt-wait-review loop with version-controlled workflow graphs that orchestrate AI agents, shell commands, and human decisions into repeatable, long-horizon coding processes. Workflows are defined as Graphviz graphs where each node is a stage (agent, prompt, command, conditional, human, parallel, etc.) executed by the workflow engine.
|
||||
|
||||
- Open source, MIT-licensed
|
||||
- Workflows defined as Graphviz graphs with CSS-like model stylesheets
|
||||
- Sandboxed execution via local Docker, Daytona, SSH, or Sprites
|
||||
- Checkpoint/resume for long-running workflows
|
||||
- Human-in-the-loop via CLI, web UI, or Slack
|
||||
- GitHub integration for PR creation and repository access
|
||||
- REST API with SSE event streaming
|
||||
|
||||
## Getting Started
|
||||
|
||||
- [Introduction](https://docs.fabro.sh/getting-started/introduction): Overview of Fabro and what it does
|
||||
- [Quick Start](https://docs.fabro.sh/getting-started/quick-start): Get up and running with Fabro
|
||||
- [Why Fabro?](https://docs.fabro.sh/getting-started/why-fabro): The problems Fabro solves for AI-assisted software teams
|
||||
- [Comparison](https://docs.fabro.sh/getting-started/comparison): How Fabro compares to AI coding agents, software factories, and orchestration platforms
|
||||
- [Dark Factory](https://docs.fabro.sh/getting-started/dark-factory): How small teams incrementally adopt a dark factory approach
|
||||
|
||||
## Core Concepts
|
||||
|
||||
- [Workflows](https://docs.fabro.sh/core-concepts/workflows): Core workflow concepts
|
||||
- [Agents](https://docs.fabro.sh/core-concepts/agents): Core agent concepts
|
||||
- [Models](https://docs.fabro.sh/core-concepts/models): How Fabro routes tasks to LLM models and providers
|
||||
- [How Fabro Works](https://docs.fabro.sh/core-concepts/how-fabro-works): Architecture and execution model
|
||||
|
||||
## Tutorials
|
||||
|
||||
- [Hello World](https://docs.fabro.sh/tutorials/hello-world): Your first workflow — prompt nodes, tool use, and sub-agents
|
||||
- [Plan & Implement](https://docs.fabro.sh/tutorials/plan-implement): Human gates, revision loops, and prompt file references
|
||||
|
||||
## Workflows
|
||||
|
||||
- [Nodes & Stages](https://docs.fabro.sh/workflows/stages-and-nodes): All node types and how they become stages at runtime
|
||||
- [Transitions](https://docs.fabro.sh/workflows/transitions): How Fabro decides which node to execute next
|
||||
- [Model Stylesheets](https://docs.fabro.sh/workflows/stylesheets): Assign LLM models to nodes using CSS-like rules
|
||||
- [Variables](https://docs.fabro.sh/workflows/variables): Using variables in workflows
|
||||
- [Human-in-the-Loop](https://docs.fabro.sh/workflows/human-in-the-loop): Adding human review and intervention
|
||||
- [Best Practices](https://docs.fabro.sh/workflows/best-practices): Best practices for designing workflows
|
||||
|
||||
## Agents
|
||||
|
||||
- [Tools](https://docs.fabro.sh/agents/tools): Built-in tools for file I/O, shell, search, and web access
|
||||
- [Prompts](https://docs.fabro.sh/agents/prompts): How Fabro constructs and delivers prompts
|
||||
- [Permissions](https://docs.fabro.sh/agents/permissions): Controlling which tools agents can use
|
||||
- [Skills](https://docs.fabro.sh/agents/skills): Reusable prompt templates that extend agent capabilities
|
||||
- [Hooks](https://docs.fabro.sh/agents/hooks): Run custom logic in response to workflow lifecycle events
|
||||
- [Outputs & Artifacts](https://docs.fabro.sh/agents/outputs): Agent responses, file changes, and test assets
|
||||
- [MCP](https://docs.fabro.sh/agents/mcp): Extend agents with Model Context Protocol servers
|
||||
- [Sub-agents](https://docs.fabro.sh/agents/subagents): Delegate subtasks to child agent sessions
|
||||
|
||||
## Execution
|
||||
|
||||
- [Environments](https://docs.fabro.sh/execution/environments): Sandbox providers for workflow execution
|
||||
- [Run Configuration](https://docs.fabro.sh/execution/run-configuration): Configure runs with TOML files
|
||||
- [Checkpoints](https://docs.fabro.sh/execution/checkpoints): Git-based checkpoint and resume
|
||||
- [Retros](https://docs.fabro.sh/execution/retros): Automatic retrospectives for every run
|
||||
- [Observability](https://docs.fabro.sh/execution/observability): Monitor, inspect, and analyze runs
|
||||
|
||||
## Integrations
|
||||
|
||||
- [GitHub](https://docs.fabro.sh/integrations/github): Repository access and OAuth login
|
||||
- [Slack](https://docs.fabro.sh/integrations/slack): Answer human-in-the-loop questions from Slack
|
||||
- [Daytona](https://docs.fabro.sh/integrations/daytona): Sandboxed cloud environments
|
||||
|
||||
## Reference
|
||||
|
||||
- [API Overview](https://docs.fabro.sh/api-reference/overview): Introduction to the Fabro REST API
|
||||
- [CLI Reference](https://docs.fabro.sh/reference/cli): Command-line interface reference
|
||||
- [Architecture](https://docs.fabro.sh/reference/architecture): How CLI and API modes work under the hood
|
||||
- [Graphviz DOT Language](https://docs.fabro.sh/reference/dot-language): Complete reference for Fabro's workflow language
|
||||
|
||||
## Optional
|
||||
|
||||
- [Changelog](https://docs.fabro.sh/changelog/2026-03-15): Latest changes and releases
|
||||
- [GitHub Repository](https://github.com/fabro-sh/fabro): Source code
|
||||
- [Discord](https://fabro.sh/discord): Community chat
|
||||
4
apps/marketing/public/logotype.svg
Normal file
|
After Width: | Height: | Size: 7.8 KiB |
BIN
apps/marketing/public/og-image.png
Normal file
|
After Width: | Height: | Size: 344 KiB |
100
apps/marketing/public/plan-implement.svg
Normal file
|
|
@ -0,0 +1,100 @@
|
|||
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN"
|
||||
"http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
|
||||
<!-- Generated by graphviz version 14.1.3 (20260303.0454)
|
||||
-->
|
||||
<!-- Title: PlanImplement Pages: 1 -->
|
||||
<svg width="674pt" height="44pt"
|
||||
viewBox="0.00 0.00 674.00 44.00" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
|
||||
<style>
|
||||
[fill="#1a1a1a"] { fill: #e8edf3; }
|
||||
[fill="#666666"] { fill: #a8b5c5; }
|
||||
[stroke="#666666"] { stroke: #a8b5c5; }
|
||||
[stroke="#999999"] { stroke: #4b5768; }
|
||||
</style>
|
||||
<g id="graph0" class="graph" transform="scale(1 1) rotate(0) translate(4 40.25)">
|
||||
<title>PlanImplement</title>
|
||||
<!-- start -->
|
||||
<g id="node1" class="node">
|
||||
<title>start</title>
|
||||
<polygon fill="none" stroke="#357f9e" points="35.26,-36.12 0,-18.13 35.26,-0.13 70.52,-18.12 35.26,-36.12"/>
|
||||
<polyline fill="none" stroke="#357f9e" points="10.69,-23.58 10.69,-12.67"/>
|
||||
<polyline fill="none" stroke="#357f9e" points="24.57,-5.58 45.95,-5.58"/>
|
||||
<polyline fill="none" stroke="#357f9e" points="59.83,-12.67 59.83,-23.58"/>
|
||||
<polyline fill="none" stroke="#357f9e" points="45.95,-30.67 24.57,-30.67"/>
|
||||
<text xml:space="preserve" text-anchor="middle" x="35.26" y="-13.1" font-family="Helvetica,sans-Serif" font-size="12.00" fill="#1a1a1a">Start</text>
|
||||
</g>
|
||||
<!-- plan -->
|
||||
<g id="node3" class="node">
|
||||
<title>plan</title>
|
||||
<path fill="none" stroke="#357f9e" d="M149.52,-36.12C149.52,-36.12 119.52,-36.12 119.52,-36.12 113.52,-36.12 107.52,-30.12 107.52,-24.12 107.52,-24.12 107.52,-12.12 107.52,-12.12 107.52,-6.12 113.52,-0.12 119.52,-0.12 119.52,-0.12 149.52,-0.12 149.52,-0.12 155.52,-0.12 161.52,-6.12 161.52,-12.12 161.52,-12.12 161.52,-24.13 161.52,-24.13 161.52,-30.12 155.52,-36.12 149.52,-36.12"/>
|
||||
<text xml:space="preserve" text-anchor="middle" x="134.52" y="-13.1" font-family="Helvetica,sans-Serif" font-size="12.00" fill="#1a1a1a">Plan</text>
|
||||
</g>
|
||||
<!-- start->plan -->
|
||||
<g id="edge1" class="edge">
|
||||
<title>start->plan</title>
|
||||
<path fill="none" stroke="#666666" d="M71.37,-18.12C79.34,-18.12 87.84,-18.12 95.91,-18.12"/>
|
||||
<polygon fill="#666666" stroke="#666666" points="95.8,-21.63 105.8,-18.13 95.8,-14.63 95.8,-21.63"/>
|
||||
</g>
|
||||
<!-- exit -->
|
||||
<g id="node2" class="node">
|
||||
<title>exit</title>
|
||||
<polygon fill="none" stroke="#357f9e" points="666.18,-36.25 629.93,-36.25 629.93,0 666.18,0 666.18,-36.25"/>
|
||||
<polyline fill="none" stroke="#357f9e" points="641.93,-36.25 629.93,-24.25"/>
|
||||
<polyline fill="none" stroke="#357f9e" points="629.93,-12 641.93,0"/>
|
||||
<polyline fill="none" stroke="#357f9e" points="654.18,0 666.18,-12"/>
|
||||
<polyline fill="none" stroke="#357f9e" points="666.18,-24.25 654.18,-36.25"/>
|
||||
<text xml:space="preserve" text-anchor="middle" x="648.05" y="-13.1" font-family="Helvetica,sans-Serif" font-size="12.00" fill="#1a1a1a">Exit</text>
|
||||
</g>
|
||||
<!-- approve -->
|
||||
<g id="node4" class="node">
|
||||
<title>approve</title>
|
||||
<polygon fill="none" stroke="#357f9e" points="353.68,-18.12 322.33,-36.12 259.62,-36.12 228.27,-18.13 259.62,-0.13 322.33,-0.12 353.68,-18.12"/>
|
||||
<text xml:space="preserve" text-anchor="middle" x="290.98" y="-13.1" font-family="Helvetica,sans-Serif" font-size="12.00" fill="#1a1a1a">Approve Plan</text>
|
||||
</g>
|
||||
<!-- plan->approve -->
|
||||
<g id="edge2" class="edge">
|
||||
<title>plan->approve</title>
|
||||
<path fill="none" stroke="#666666" d="M161.72,-18.12C176.76,-18.12 196.47,-18.12 215.91,-18.12"/>
|
||||
<polygon fill="#666666" stroke="#666666" points="215.77,-21.63 225.77,-18.13 215.77,-14.63 215.77,-21.63"/>
|
||||
</g>
|
||||
<!-- approve->plan -->
|
||||
<g id="edge4" class="edge">
|
||||
<title>approve->plan</title>
|
||||
<path fill="none" stroke="#666666" d="M247.15,-6.91C226.65,-3 201.77,-0.36 179.52,-3.88 177.32,-4.22 175.08,-4.66 172.83,-5.17"/>
|
||||
<polygon fill="#666666" stroke="#666666" points="171.93,-1.79 163.18,-7.77 173.75,-8.55 171.93,-1.79"/>
|
||||
<text xml:space="preserve" text-anchor="middle" x="194.9" y="-5.62" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#666666">Revise</text>
|
||||
</g>
|
||||
<!-- implement -->
|
||||
<g id="node5" class="node">
|
||||
<title>implement</title>
|
||||
<path fill="none" stroke="#357f9e" d="M485.18,-36.12C485.18,-36.12 437.68,-36.12 437.68,-36.12 431.68,-36.12 425.68,-30.12 425.68,-24.12 425.68,-24.12 425.68,-12.12 425.68,-12.12 425.68,-6.12 431.68,-0.12 437.68,-0.12 437.68,-0.12 485.18,-0.12 485.18,-0.12 491.18,-0.12 497.18,-6.12 497.18,-12.12 497.18,-12.12 497.18,-24.13 497.18,-24.13 497.18,-30.12 491.18,-36.12 485.18,-36.12"/>
|
||||
<text xml:space="preserve" text-anchor="middle" x="461.43" y="-13.1" font-family="Helvetica,sans-Serif" font-size="12.00" fill="#1a1a1a">Implement</text>
|
||||
</g>
|
||||
<!-- approve->implement -->
|
||||
<g id="edge3" class="edge">
|
||||
<title>approve->implement</title>
|
||||
<path fill="none" stroke="#666666" d="M354.22,-18.12C374,-18.12 395.51,-18.12 413.97,-18.12"/>
|
||||
<polygon fill="#666666" stroke="#666666" points="413.81,-21.63 423.81,-18.13 413.81,-14.63 413.81,-21.63"/>
|
||||
<text xml:space="preserve" text-anchor="middle" x="389.68" y="-19.88" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#666666">Approve</text>
|
||||
</g>
|
||||
<!-- simplify -->
|
||||
<g id="node6" class="node">
|
||||
<title>simplify</title>
|
||||
<path fill="none" stroke="#357f9e" d="M580.93,-36.12C580.93,-36.12 546.18,-36.12 546.18,-36.12 540.18,-36.12 534.18,-30.12 534.18,-24.12 534.18,-24.12 534.18,-12.12 534.18,-12.12 534.18,-6.12 540.18,-0.12 546.18,-0.12 546.18,-0.12 580.93,-0.12 580.93,-0.12 586.93,-0.12 592.93,-6.12 592.93,-12.12 592.93,-12.12 592.93,-24.13 592.93,-24.13 592.93,-30.12 586.93,-36.12 580.93,-36.12"/>
|
||||
<text xml:space="preserve" text-anchor="middle" x="563.55" y="-13.1" font-family="Helvetica,sans-Serif" font-size="12.00" fill="#1a1a1a">Simplify</text>
|
||||
</g>
|
||||
<!-- implement->simplify -->
|
||||
<g id="edge5" class="edge">
|
||||
<title>implement->simplify</title>
|
||||
<path fill="none" stroke="#666666" d="M497.45,-18.12C505.5,-18.12 514.12,-18.12 522.37,-18.12"/>
|
||||
<polygon fill="#666666" stroke="#666666" points="522.17,-21.63 532.17,-18.13 522.17,-14.63 522.17,-21.63"/>
|
||||
</g>
|
||||
<!-- simplify->exit -->
|
||||
<g id="edge6" class="edge">
|
||||
<title>simplify->exit</title>
|
||||
<path fill="none" stroke="#666666" d="M593.41,-18.12C601.43,-18.12 610.14,-18.12 618.17,-18.12"/>
|
||||
<polygon fill="#666666" stroke="#666666" points="618.13,-21.63 628.13,-18.13 618.13,-14.63 618.13,-21.63"/>
|
||||
</g>
|
||||
</g>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 6.2 KiB |
BIN
apps/marketing/public/run-detail.png
Normal file
|
After Width: | Height: | Size: 3 MiB |
BIN
apps/marketing/public/run-detail.webp
Normal file
|
After Width: | Height: | Size: 81 KiB |
BIN
apps/marketing/public/runs-board.png
Normal file
|
After Width: | Height: | Size: 3.8 MiB |
BIN
apps/marketing/public/runs-board.webp
Normal file
|
After Width: | Height: | Size: 114 KiB |
|
Before Width: | Height: | Size: 50 KiB After Width: | Height: | Size: 1.1 KiB |
35
apps/marketing/src/components/Footer.astro
Normal file
|
|
@ -0,0 +1,35 @@
|
|||
<footer class="border-t border-navy-800/60 py-16">
|
||||
<div class="mx-auto max-w-6xl px-6">
|
||||
<div class="grid gap-10 sm:grid-cols-3">
|
||||
<div>
|
||||
<img src="/logotype.svg" alt="Fabro" class="h-6 mb-4" />
|
||||
<p class="text-sm text-ice-300/60">The dark software factory for expert engineers.</p>
|
||||
<p class="mt-2 text-xs text-ice-300/40">Open source, MIT-licensed</p>
|
||||
</div>
|
||||
<div>
|
||||
<h3 class="font-display text-sm font-semibold uppercase tracking-wider text-ice-300/70 mb-4">Product</h3>
|
||||
<ul class="space-y-2 text-sm">
|
||||
<li><a href="https://docs.fabro.sh/getting-started/quick-start" class="text-ice-300 hover:text-ice-50 transition-colors">Quick Start</a></li>
|
||||
<li><a href="https://docs.fabro.sh" class="text-ice-300 hover:text-ice-50 transition-colors">Docs</a></li>
|
||||
<li><a href="/blog" class="text-ice-300 hover:text-ice-50 transition-colors">Blog</a></li>
|
||||
<li><a href="/showcase" class="text-ice-300 hover:text-ice-50 transition-colors">Showcase</a></li>
|
||||
<li><a href="/roadmap" class="text-ice-300 hover:text-ice-50 transition-colors">Roadmap</a></li>
|
||||
<li><a href="https://docs.fabro.sh/changelog" class="text-ice-300 hover:text-ice-50 transition-colors">Changelog</a></li>
|
||||
<li><a href="https://github.com/fabro-sh/fabro/releases" class="text-ice-300 hover:text-ice-50 transition-colors">Releases</a></li>
|
||||
</ul>
|
||||
</div>
|
||||
<div>
|
||||
<h3 class="font-display text-sm font-semibold uppercase tracking-wider text-ice-300/70 mb-4">Community</h3>
|
||||
<ul class="space-y-2 text-sm">
|
||||
<li><a href="/discord" class="text-ice-300 hover:text-ice-50 transition-colors">Discord</a></li>
|
||||
<li><a href="https://github.com/fabro-sh/fabro" class="text-ice-300 hover:text-ice-50 transition-colors">GitHub</a></li>
|
||||
<li><a href="https://github.com/fabro-sh/fabro/issues" class="text-ice-300 hover:text-ice-50 transition-colors">Issues</a></li>
|
||||
<li><a href="https://github.com/fabro-sh/fabro/discussions" class="text-ice-300 hover:text-ice-50 transition-colors">Discussions</a></li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
<div class="mt-10 border-t border-navy-800/40 pt-6 text-center text-xs text-ice-300/40">
|
||||
© 2025 Qlty Software Inc. All rights reserved.
|
||||
</div>
|
||||
</div>
|
||||
</footer>
|
||||
68
apps/marketing/src/components/Nav.astro
Normal file
|
|
@ -0,0 +1,68 @@
|
|||
---
|
||||
interface Props {
|
||||
currentPage?: "blog" | "roadmap";
|
||||
}
|
||||
|
||||
const { currentPage } = Astro.props;
|
||||
|
||||
const linkClass = (page?: string) =>
|
||||
page === currentPage
|
||||
? "text-ice-50"
|
||||
: "text-ice-100 hover:text-ice-50";
|
||||
---
|
||||
|
||||
<nav class="fixed top-0 z-50 w-full border-b border-navy-800/60 bg-navy-950/70 backdrop-blur-xl">
|
||||
<div class="mx-auto flex max-w-6xl items-center justify-between px-6 py-4">
|
||||
<div class="flex items-center gap-8">
|
||||
<a href="/" class="flex items-center">
|
||||
<img src="/logotype.svg" alt="Fabro" class="h-7" />
|
||||
</a>
|
||||
<a href="https://docs.fabro.sh" class={`hidden md:inline-flex text-sm font-medium transition-colors ${linkClass()}`}>Docs</a>
|
||||
<a href="/blog" class={`hidden md:inline-flex text-sm font-medium transition-colors ${linkClass("blog")}`}>Blog</a>
|
||||
<a href="/roadmap" class={`hidden md:inline-flex text-sm font-medium transition-colors ${linkClass("roadmap")}`}>Roadmap</a>
|
||||
<a href="https://docs.fabro.sh/changelog" class={`hidden md:inline-flex text-sm font-medium transition-colors ${linkClass()}`}>Changelog</a>
|
||||
</div>
|
||||
<div class="flex items-center gap-5">
|
||||
<a href="/discord" class="hidden md:block text-ice-300 hover:text-ice-50 transition-colors" aria-label="Discord">
|
||||
<svg class="h-5 w-5" fill="currentColor" viewBox="0 0 24 24"><path d="M20.317 4.37a19.791 19.791 0 0 0-4.885-1.515.074.074 0 0 0-.079.037c-.21.375-.444.864-.608 1.25a18.27 18.27 0 0 0-5.487 0 12.64 12.64 0 0 0-.617-1.25.077.077 0 0 0-.079-.037A19.736 19.736 0 0 0 3.677 4.37a.07.07 0 0 0-.032.027C.533 9.046-.32 13.58.099 18.057a.082.082 0 0 0 .031.057 19.9 19.9 0 0 0 5.993 3.03.078.078 0 0 0 .084-.028c.462-.63.874-1.295 1.226-1.994a.076.076 0 0 0-.041-.106 13.107 13.107 0 0 1-1.872-.892.077.077 0 0 1-.008-.128 10.2 10.2 0 0 0 .372-.292.074.074 0 0 1 .077-.01c3.928 1.793 8.18 1.793 12.062 0a.074.074 0 0 1 .078.01c.12.098.246.198.373.292a.077.077 0 0 1-.006.127 12.299 12.299 0 0 1-1.873.892.077.077 0 0 0-.041.107c.36.698.772 1.362 1.225 1.993a.076.076 0 0 0 .084.028 19.839 19.839 0 0 0 6.002-3.03.077.077 0 0 0 .032-.054c.5-5.177-.838-9.674-3.549-13.66a.061.061 0 0 0-.031-.03zM8.02 15.33c-1.183 0-2.157-1.085-2.157-2.419 0-1.333.956-2.419 2.157-2.419 1.21 0 2.176 1.095 2.157 2.42 0 1.333-.956 2.418-2.157 2.418zm7.975 0c-1.183 0-2.157-1.085-2.157-2.419 0-1.333.955-2.419 2.157-2.419 1.21 0 2.176 1.095 2.157 2.42 0 1.333-.946 2.418-2.157 2.418z"/></svg>
|
||||
</a>
|
||||
<a href="https://github.com/fabro-sh/fabro" class="hidden md:block text-ice-300 hover:text-ice-50 transition-colors" aria-label="GitHub">
|
||||
<svg class="h-5 w-5" fill="currentColor" viewBox="0 0 24 24"><path d="M12 0C5.37 0 0 5.37 0 12c0 5.31 3.435 9.795 8.205 11.385.6.105.825-.255.825-.57 0-.285-.015-1.23-.015-2.235-3.015.555-3.795-.735-4.035-1.41-.135-.345-.72-1.41-1.23-1.695-.42-.225-1.02-.78-.015-.795.945-.015 1.62.87 1.845 1.23 1.08 1.815 2.805 1.305 3.495.99.105-.78.42-1.305.765-1.605-2.67-.3-5.46-1.335-5.46-5.925 0-1.305.465-2.385 1.23-3.225-.12-.3-.54-1.53.12-3.18 0 0 1.005-.315 3.3 1.23.96-.27 1.98-.405 3-.405s2.04.135 3 .405c2.295-1.56 3.3-1.23 3.3-1.23.66 1.65.24 2.88.12 3.18.765.84 1.23 1.905 1.23 3.225 0 4.605-2.805 5.625-5.475 5.925.435.375.81 1.095.81 2.22 0 1.605-.015 2.895-.015 3.3 0 .315.225.69.825.57A12.02 12.02 0 0024 12c0-6.63-5.37-12-12-12z"/></svg>
|
||||
</a>
|
||||
<a
|
||||
href="https://docs.fabro.sh/getting-started/quick-start"
|
||||
class="hidden md:inline-flex rounded-lg bg-teal-700 px-4 py-2 text-sm font-semibold text-white transition-all hover:bg-teal-500 hover:shadow-[0_0_20px_-4px_rgba(53,127,158,0.5)]"
|
||||
>
|
||||
Get started
|
||||
</a>
|
||||
<button id="mobile-menu-btn" class="md:hidden text-ice-300 hover:text-ice-50 transition-colors" aria-label="Open menu">
|
||||
<svg class="h-6 w-6" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" viewBox="0 0 24 24">
|
||||
<line x1="3" y1="6" x2="21" y2="6" /><line x1="3" y1="12" x2="21" y2="12" /><line x1="3" y1="18" x2="21" y2="18" />
|
||||
</svg>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<div id="mobile-menu" class="md:hidden hidden border-t border-navy-800/60 bg-navy-950/95 backdrop-blur-xl">
|
||||
<div class="px-6 py-5 space-y-4">
|
||||
<a href="https://docs.fabro.sh" class="block text-sm font-medium text-ice-100 hover:text-ice-50 transition-colors">Docs</a>
|
||||
<a href="/blog" class={`block text-sm font-medium transition-colors ${linkClass("blog")}`}>Blog</a>
|
||||
<a href="/roadmap" class={`block text-sm font-medium transition-colors ${linkClass("roadmap")}`}>Roadmap</a>
|
||||
<a href="https://docs.fabro.sh/changelog" class="block text-sm font-medium text-ice-100 hover:text-ice-50 transition-colors">Changelog</a>
|
||||
<hr class="border-navy-800/60" />
|
||||
<div class="flex items-center gap-5">
|
||||
<a href="/discord" class="text-ice-300 hover:text-ice-50 transition-colors" aria-label="Discord">
|
||||
<svg class="h-5 w-5" fill="currentColor" viewBox="0 0 24 24"><path d="M20.317 4.37a19.791 19.791 0 0 0-4.885-1.515.074.074 0 0 0-.079.037c-.21.375-.444.864-.608 1.25a18.27 18.27 0 0 0-5.487 0 12.64 12.64 0 0 0-.617-1.25.077.077 0 0 0-.079-.037A19.736 19.736 0 0 0 3.677 4.37a.07.07 0 0 0-.032.027C.533 9.046-.32 13.58.099 18.057a.082.082 0 0 0 .031.057 19.9 19.9 0 0 0 5.993 3.03.078.078 0 0 0 .084-.028c.462-.63.874-1.295 1.226-1.994a.076.076 0 0 0-.041-.106 13.107 13.107 0 0 1-1.872-.892.077.077 0 0 1-.008-.128 10.2 10.2 0 0 0 .372-.292.074.074 0 0 1 .077-.01c3.928 1.793 8.18 1.793 12.062 0a.074.074 0 0 1 .078.01c.12.098.246.198.373.292a.077.077 0 0 1-.006.127 12.299 12.299 0 0 1-1.873.892.077.077 0 0 0-.041.107c.36.698.772 1.362 1.225 1.993a.076.076 0 0 0 .084.028 19.839 19.839 0 0 0 6.002-3.03.077.077 0 0 0 .032-.054c.5-5.177-.838-9.674-3.549-13.66a.061.061 0 0 0-.031-.03zM8.02 15.33c-1.183 0-2.157-1.085-2.157-2.419 0-1.333.956-2.419 2.157-2.419 1.21 0 2.176 1.095 2.157 2.42 0 1.333-.956 2.418-2.157 2.418zm7.975 0c-1.183 0-2.157-1.085-2.157-2.419 0-1.333.955-2.419 2.157-2.419 1.21 0 2.176 1.095 2.157 2.42 0 1.333-.946 2.418-2.157 2.418z"/></svg>
|
||||
</a>
|
||||
<a href="https://github.com/fabro-sh/fabro" class="text-ice-300 hover:text-ice-50 transition-colors" aria-label="GitHub">
|
||||
<svg class="h-5 w-5" fill="currentColor" viewBox="0 0 24 24"><path d="M12 0C5.37 0 0 5.37 0 12c0 5.31 3.435 9.795 8.205 11.385.6.105.825-.255.825-.57 0-.285-.015-1.23-.015-2.235-3.015.555-3.795-.735-4.035-1.41-.135-.345-.72-1.41-1.23-1.695-.42-.225-1.02-.78-.015-.795.945-.015 1.62.87 1.845 1.23 1.08 1.815 2.805 1.305 3.495.99.105-.78.42-1.305.765-1.605-2.67-.3-5.46-1.335-5.46-5.925 0-1.305.465-2.385 1.23-3.225-.12-.3-.54-1.53.12-3.18 0 0 1.005-.315 3.3 1.23.96-.27 1.98-.405 3-.405s2.04.135 3 .405c2.295-1.56 3.3-1.23 3.3-1.23.66 1.65.24 2.88.12 3.18.765.84 1.23 1.905 1.23 3.225 0 4.605-2.805 5.625-5.475 5.925.435.375.81 1.095.81 2.22 0 1.605-.015 2.895-.015 3.3 0 .315.225.69.825.57A12.02 12.02 0 0024 12c0-6.63-5.37-12-12-12z"/></svg>
|
||||
</a>
|
||||
</div>
|
||||
<a
|
||||
href="https://docs.fabro.sh/getting-started/quick-start"
|
||||
class="block w-full text-center rounded-lg bg-teal-700 px-4 py-2.5 text-sm font-semibold text-white transition-all hover:bg-teal-500"
|
||||
>
|
||||
Get started
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</nav>
|
||||
27
apps/marketing/src/components/PageScripts.astro
Normal file
|
|
@ -0,0 +1,27 @@
|
|||
<script>
|
||||
// Mobile menu toggle
|
||||
const mobileMenuBtn = document.getElementById("mobile-menu-btn")!;
|
||||
const mobileMenu = document.getElementById("mobile-menu")!;
|
||||
mobileMenuBtn.addEventListener("click", () => {
|
||||
const isOpen = !mobileMenu.classList.contains("hidden");
|
||||
mobileMenu.classList.toggle("hidden");
|
||||
mobileMenuBtn.setAttribute("aria-label", isOpen ? "Open menu" : "Close menu");
|
||||
mobileMenuBtn.innerHTML = isOpen
|
||||
? '<svg class="h-6 w-6" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" viewBox="0 0 24 24"><line x1="3" y1="6" x2="21" y2="6" /><line x1="3" y1="12" x2="21" y2="12" /><line x1="3" y1="18" x2="21" y2="18" /></svg>'
|
||||
: '<svg class="h-6 w-6" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" viewBox="0 0 24 24"><line x1="18" y1="6" x2="6" y2="18" /><line x1="6" y1="6" x2="18" y2="18" /></svg>';
|
||||
});
|
||||
|
||||
// Scroll reveal
|
||||
const observer = new IntersectionObserver(
|
||||
(entries) => {
|
||||
entries.forEach((entry) => {
|
||||
if (entry.isIntersecting) {
|
||||
entry.target.classList.add("revealed");
|
||||
observer.unobserve(entry.target);
|
||||
}
|
||||
});
|
||||
},
|
||||
{ threshold: 0.15, rootMargin: "0px 0px -40px 0px" }
|
||||
);
|
||||
document.querySelectorAll(".reveal, .reveal-left, .reveal-right, .reveal-scale").forEach((el) => observer.observe(el));
|
||||
</script>
|
||||
41
apps/marketing/src/content.config.ts
Normal file
|
|
@ -0,0 +1,41 @@
|
|||
import { defineCollection, z } from "astro:content";
|
||||
import { glob } from "astro/loaders";
|
||||
|
||||
const roadmap = defineCollection({
|
||||
loader: glob({ pattern: "**/*.yaml", base: "./src/content/roadmap" }),
|
||||
schema: z.object({
|
||||
title: z.string(),
|
||||
description: z.string(),
|
||||
status: z.enum(["shipped", "building", "next"]),
|
||||
date: z.coerce.date(),
|
||||
}),
|
||||
});
|
||||
|
||||
const blog = defineCollection({
|
||||
loader: glob({ pattern: "**/*.md", base: "./src/content/blog" }),
|
||||
schema: z.object({
|
||||
title: z.string(),
|
||||
description: z.string(),
|
||||
date: z.coerce.date(),
|
||||
author: z.string(),
|
||||
}),
|
||||
});
|
||||
|
||||
const showcase = defineCollection({
|
||||
loader: glob({ pattern: "**/*.md", base: "./src/content/showcase" }),
|
||||
schema: z.object({
|
||||
title: z.string(),
|
||||
description: z.string(),
|
||||
thumbnail: z.string(),
|
||||
tags: z.array(z.string()),
|
||||
languages: z.array(z.enum(["python", "rust", "typescript", "ruby"])),
|
||||
github: z.string(),
|
||||
models: z.array(z.string()),
|
||||
skills: z.array(z.string()),
|
||||
prompt: z.string(),
|
||||
workflow: z.string(),
|
||||
sortOrder: z.number(),
|
||||
}),
|
||||
});
|
||||
|
||||
export const collections = { roadmap, blog, showcase };
|
||||
93
apps/marketing/src/content/blog/introducing-fabro.md
Normal file
|
|
@ -0,0 +1,93 @@
|
|||
---
|
||||
title: "Introducing Fabro"
|
||||
description: "Fabro is an open-source workflow orchestration platform that lets expert engineers define their process as a graph and let AI agents execute it."
|
||||
date: 2026-03-16
|
||||
author: "Bryan Helmkamp"
|
||||
---
|
||||
|
||||
Today, we're introducing Fabro, an open-source platform for orchestrating AI coding agents with workflow graphs. We built Fabro to help expert engineers define their process once, verify it at every step, and walk away while agents do the work.
|
||||
|
||||
## The problem with AI coding today
|
||||
|
||||
AI coding agents are powerful, but they're also chaotic. The current generation of tools gives you a chat window and a single loop: prompt, act, repeat. That works for small tasks, but it breaks down the moment you need structure.
|
||||
|
||||
- **No process definition.** You can't specify that a plan should be approved before implementation starts, that tests must pass before the PR is opened, or that a second model should cross-review the first.
|
||||
- **Single-model dependence.** You're locked into one provider for every step, even when a faster, cheaper model would suffice for triage or formatting.
|
||||
- **No verification.** The agent decides when it's "done." There's no deterministic gate confirming that the code compiles, tests pass, and linting is clean.
|
||||
- **No reproducibility.** Sessions are ephemeral. You can't checkpoint mid-run, resume after a failure, or replay a workflow with a different model.
|
||||
|
||||
The result is that engineers babysit their agents instead of using them. You spend more time supervising than you save.
|
||||
|
||||
## Workflow graphs: your engineering process as code
|
||||
|
||||
Fabro takes a fundamentally different approach. Instead of prompting an agent and watching it work, you define your engineering process as a Graphviz graph — diffable, reviewable, and version-controlled just like the code it produces.
|
||||
|
||||
```dot
|
||||
digraph PlanImplement {
|
||||
graph [
|
||||
goal="Plan, approve, implement, and simplify a change"
|
||||
model_stylesheet="
|
||||
* { model: claude-haiku-4-5; }
|
||||
.coding { model: claude-sonnet-4-5; reasoning_effort: high; }
|
||||
"
|
||||
]
|
||||
|
||||
start [shape=Mdiamond]
|
||||
plan [prompt="Analyze the goal and codebase. Write a step-by-step plan."]
|
||||
approve [shape=hexagon, label="Approve Plan"]
|
||||
implement [class="coding", prompt="Read plan.md and implement every step."]
|
||||
simplify [class="coding", prompt="Review the changes for clarity and correctness."]
|
||||
exit [shape=Msquare]
|
||||
|
||||
start -> plan -> approve
|
||||
approve -> implement [label="[A] Approve"]
|
||||
approve -> plan [label="[R] Revise"]
|
||||
implement -> simplify -> exit
|
||||
}
|
||||
```
|
||||
|
||||
Each node is a stage with a specific role. Edges define the flow. Node shapes determine behavior — `box` for agents with tool access, `hexagon` for human decision gates, `diamond` for conditionals, `parallelogram` for shell commands. The graph supports loops, not just DAGs, so you can build implement-test-fix cycles that repeat until verification passes.
|
||||
|
||||
**The process is deterministic, even though the AI execution within each stage is not.** You control the structure. The models do the work.
|
||||
|
||||
## Multi-model by design
|
||||
|
||||
Not every stage needs a frontier model. Fabro's model stylesheets use CSS-like selectors to route each node to the right model and provider:
|
||||
|
||||
```
|
||||
* { model: claude-haiku-4-5; reasoning_effort: low; }
|
||||
.coding { model: claude-sonnet-4-5; reasoning_effort: high; }
|
||||
#review { model: gemini-3.1-pro-preview; }
|
||||
```
|
||||
|
||||
Selectors follow CSS specificity rules — universal (`*`), shape, class (`.coding`), and ID (`#review`) — so you can set sensible defaults and override where it matters. Route cheap tasks to fast models, reserve frontier models for implementation and review, and combine providers for ensemble intelligence. Swap a model with a one-line change instead of rearchitecting your workflow.
|
||||
|
||||
## Verification gates and human checkpoints
|
||||
|
||||
AI agents are confident — even when they're wrong. Fabro addresses this with two layers of control.
|
||||
|
||||
**Deterministic verification.** Goal gates confirm that the code compiles, tests pass, and linting is clean before the workflow advances. These aren't LLM judgments — they're shell commands with pass/fail exit codes.
|
||||
|
||||
**Human-in-the-loop gates.** Hexagon nodes pause the workflow for human approval, rejection, or freeform input. Keyboard accelerators (`[A]` Approve, `[R]` Revise) make decisions fast. When you trust the process, `--auto-approve` skips gates entirely.
|
||||
|
||||
The combination lets deterministic checks catch what linters and tests can catch, human gates handle judgment calls, and agents do everything in between.
|
||||
|
||||
## Checkpoint, resume, and walk away
|
||||
|
||||
Every stage is checkpointed to Git. If a run fails at stage 7 of 12, you don't start over — you fix the issue and resume from the checkpoint. Runs execute in isolated Git worktrees, so your working directory stays untouched while agents work in parallel.
|
||||
|
||||
Fabro supports six sandbox environments — local, Docker, SSH, Daytona cloud VMs, and more — so you can develop on your laptop and move to isolated cloud sandboxes for production workflows.
|
||||
|
||||
## Get started
|
||||
|
||||
Fabro is open source and ships as a single Rust binary with zero runtime dependencies.
|
||||
|
||||
```bash
|
||||
# Install
|
||||
curl -fsSL https://fabro.sh/install.sh | bash
|
||||
|
||||
# Run a workflow
|
||||
fabro run implement
|
||||
```
|
||||
|
||||
Check the [roadmap](/roadmap) to see what we're building — including automatic retrospectives, a REST API server, and analytics — and join us on [Discord](/discord) to shape what comes next.
|
||||
4
apps/marketing/src/content/roadmap/analytics.yaml
Normal file
|
|
@ -0,0 +1,4 @@
|
|||
title: Analytics
|
||||
description: Cost tracking, success rates, and performance trends across runs. Budgets, alerts, and optimization insights.
|
||||
status: next
|
||||
date: 2026-06-01
|
||||
|
|
@ -0,0 +1,4 @@
|
|||
title: REST API server mode
|
||||
description: REST API with SSE event streaming for queuing runs, tracking progress, and managing workflows programmatically.
|
||||
status: building
|
||||
date: 2026-04-02
|
||||
|
|
@ -0,0 +1,4 @@
|
|||
title: Automatic retrospectives
|
||||
description: Structured post-run analysis with cost, duration, smoothness ratings, friction points, and LLM-generated narratives.
|
||||
status: building
|
||||
date: 2026-04-03
|
||||
4
apps/marketing/src/content/roadmap/cloud-sandboxes.yaml
Normal file
|
|
@ -0,0 +1,4 @@
|
|||
title: Cloud sandboxes
|
||||
description: Isolated cloud VMs with snapshot-based setup, network controls, SSH access, and preview URLs.
|
||||
status: shipped
|
||||
date: 2026-03-15
|
||||
|
|
@ -0,0 +1,4 @@
|
|||
title: Workflow engine
|
||||
description: Graphviz graph parsing with agent, command, and human nodes. Dynamic edge selection with conditions and loops.
|
||||
status: shipped
|
||||
date: 2026-02-15
|
||||
|
|
@ -0,0 +1,4 @@
|
|||
title: Git checkpoints
|
||||
description: Every stage commits code and execution metadata to Git branches. Resume or fork from any checkpoint.
|
||||
status: shipped
|
||||
date: 2026-03-01
|
||||
|
|
@ -0,0 +1,4 @@
|
|||
title: Interview-based steering
|
||||
description: Approval gates, multiple choice, and freeform questions. Steer running agents via CLI, web, or Slack.
|
||||
status: shipped
|
||||
date: 2026-02-15
|
||||
|
|
@ -0,0 +1,4 @@
|
|||
title: Multi-model agent
|
||||
description: Multi-turn LLM sessions with bash, file editing, sub-agents, skills, and lifecycle hooks.
|
||||
status: shipped
|
||||
date: 2026-02-15
|
||||
|
|
@ -0,0 +1,4 @@
|
|||
title: Slack integration
|
||||
description: Receive notifications, approve human gates, and monitor runs directly from Slack channels.
|
||||
status: next
|
||||
date: 2026-06-02
|
||||
4
apps/marketing/src/content/roadmap/verifications.yaml
Normal file
|
|
@ -0,0 +1,4 @@
|
|||
title: Verifications
|
||||
description: Deterministic quality gates — test suites, linters, type checkers, and LLM-as-judge — wired into the workflow graph.
|
||||
status: building
|
||||
date: 2026-04-01
|
||||
4
apps/marketing/src/content/roadmap/web-app.yaml
Normal file
|
|
@ -0,0 +1,4 @@
|
|||
title: Web app
|
||||
description: React dashboard for managing workflows, viewing runs, approving human gates, and browsing retrospectives.
|
||||
status: next
|
||||
date: 2026-06-03
|
||||
46
apps/marketing/src/content/showcase/docs-sync.md
Normal file
|
|
@ -0,0 +1,46 @@
|
|||
---
|
||||
title: "Docs Sync"
|
||||
description: "Keeps documentation in sync with code changes by detecting drift and auto-updating affected pages."
|
||||
thumbnail: "/showcase/docs-sync.png"
|
||||
tags: ["documentation", "automation", "ci-cd"]
|
||||
languages: ["typescript"]
|
||||
github: "https://github.com/fabro-sh/fabro/tree/main/examples/docs-sync"
|
||||
models: ["claude-sonnet-4-5"]
|
||||
skills: ["code-review", "git", "documentation"]
|
||||
prompt: "Compare the current codebase against the documentation. Identify any docs pages that are out of date with the code — changed APIs, renamed functions, removed features, or new features without docs. Update each affected page to match the current code, preserving the existing writing style and structure."
|
||||
workflow: |
|
||||
digraph DocsSync {
|
||||
graph [
|
||||
goal="Detect and fix documentation drift from code changes"
|
||||
model_stylesheet="
|
||||
* { model: claude-sonnet-4-5; }
|
||||
"
|
||||
]
|
||||
|
||||
start [shape=Mdiamond, label="Start"]
|
||||
exit [shape=Msquare, label="Exit"]
|
||||
|
||||
diff [label="Get Changes", prompt="Identify code changes since the last docs sync."]
|
||||
scan [label="Scan Docs", prompt="Find documentation pages that reference changed code."]
|
||||
update [label="Update Docs", prompt="Rewrite affected doc sections to match current code."]
|
||||
review [shape=hexagon, label="Review Updates"]
|
||||
|
||||
start -> diff -> scan -> update -> review
|
||||
review -> exit [label="Approve"]
|
||||
review -> update [label="Revise"]
|
||||
}
|
||||
sortOrder: 3
|
||||
---
|
||||
|
||||
The Docs Sync workflow detects when documentation has drifted from the codebase and automatically updates affected pages — with a human review gate before changes are committed.
|
||||
|
||||
## How it works
|
||||
|
||||
1. **Get Changes** — Identifies code changes since the last documentation sync using git history.
|
||||
2. **Scan Docs** — Searches documentation pages for references to changed functions, APIs, types, and features.
|
||||
3. **Update Docs** — Rewrites affected sections to match the current code, preserving the original writing style and page structure.
|
||||
4. **Human Review** — Updated pages are presented for review. Approve to commit, or send back for revision.
|
||||
|
||||
## Keeping docs honest
|
||||
|
||||
Documentation drift is one of the most common sources of developer frustration. This workflow runs on every merge to main, catching drift before it reaches users. Because it understands both the code and the docs, it can make precise, targeted updates rather than generic rewrites.
|
||||
45
apps/marketing/src/content/showcase/pr-review-bot.md
Normal file
|
|
@ -0,0 +1,45 @@
|
|||
---
|
||||
title: "PR Review Bot"
|
||||
description: "Automated code review that catches bugs, style issues, and security concerns before human reviewers see the PR."
|
||||
thumbnail: "/showcase/pr-review-bot.png"
|
||||
tags: ["code-review", "ci-cd", "github"]
|
||||
languages: ["typescript", "python"]
|
||||
github: "https://github.com/fabro-sh/fabro/tree/main/examples/pr-review-bot"
|
||||
models: ["claude-sonnet-4-5", "claude-haiku-4-5"]
|
||||
skills: ["code-review", "git", "github"]
|
||||
prompt: "Review this pull request for bugs, security issues, and style violations. Focus on logic errors and potential runtime failures. Summarize findings as inline comments on the diff, then produce a top-level review with an overall assessment and a clear approve/request-changes verdict."
|
||||
workflow: |
|
||||
digraph PRReview {
|
||||
graph [
|
||||
goal="Review a pull request for bugs, security, and style"
|
||||
model_stylesheet="
|
||||
* { model: claude-haiku-4-5; }
|
||||
.review { model: claude-sonnet-4-5; }
|
||||
"
|
||||
]
|
||||
|
||||
start [shape=Mdiamond, label="Start"]
|
||||
exit [shape=Msquare, label="Exit"]
|
||||
|
||||
fetch [label="Fetch Diff", prompt="Fetch the PR diff and changed file contents."]
|
||||
triage [label="Triage Files", prompt="Categorize changed files by risk level."]
|
||||
review [label="Deep Review", class="review", prompt="Review high-risk files for bugs, security issues, and style."]
|
||||
comment [label="Post Comments", prompt="Post inline comments and a summary review on the PR."]
|
||||
|
||||
start -> fetch -> triage -> review -> comment -> exit
|
||||
}
|
||||
sortOrder: 1
|
||||
---
|
||||
|
||||
The PR Review Bot workflow automates the first pass of code review on every pull request. It fetches the diff, triages files by risk level, performs a deep review on high-risk changes using a frontier model, and posts structured feedback directly on the PR.
|
||||
|
||||
## How it works
|
||||
|
||||
1. **Fetch Diff** — Pulls the PR diff and full contents of changed files using the GitHub API.
|
||||
2. **Triage Files** — A fast model categorizes each changed file as high, medium, or low risk based on the type of change (new logic vs. formatting, test files vs. production code).
|
||||
3. **Deep Review** — A frontier model examines high-risk files for logic errors, security vulnerabilities, race conditions, and style violations.
|
||||
4. **Post Comments** — Inline comments are posted on specific lines, and a top-level review summary gives an overall verdict.
|
||||
|
||||
## Cost optimization
|
||||
|
||||
By using a model stylesheet, the workflow routes expensive frontier-model calls only to the deep review stage. File fetching and triaging use a fast, cheap model — keeping the total cost per review under $0.10 for most PRs.
|
||||
52
apps/marketing/src/content/showcase/test-generator.md
Normal file
|
|
@ -0,0 +1,52 @@
|
|||
---
|
||||
title: "Test Generator"
|
||||
description: "Generates comprehensive test suites from source code, covering edge cases and error paths that humans often miss."
|
||||
thumbnail: "/showcase/test-generator.png"
|
||||
tags: ["testing", "code-generation", "automation"]
|
||||
languages: ["typescript", "rust"]
|
||||
github: "https://github.com/fabro-sh/fabro/tree/main/examples/test-generator"
|
||||
models: ["claude-sonnet-4-5", "claude-haiku-4-5"]
|
||||
skills: ["code-review", "testing", "code-generation"]
|
||||
prompt: "Analyze the source files in this project and generate a comprehensive test suite. For each public function or method, write tests covering: happy path, edge cases, error conditions, and boundary values. Use the project's existing test framework and conventions. Run the tests and fix any failures before finishing."
|
||||
workflow: |
|
||||
digraph TestGenerator {
|
||||
graph [
|
||||
goal="Generate and validate a test suite for source code"
|
||||
model_stylesheet="
|
||||
* { model: claude-haiku-4-5; }
|
||||
.analysis { model: claude-sonnet-4-5; }
|
||||
.coding { model: claude-sonnet-4-5; }
|
||||
"
|
||||
]
|
||||
|
||||
start [shape=Mdiamond, label="Start"]
|
||||
exit [shape=Msquare, label="Exit"]
|
||||
|
||||
analyze [label="Analyze Source", class="analysis", prompt="Read source files and identify all public interfaces, edge cases, and error paths."]
|
||||
plan [label="Plan Tests", prompt="Create a test plan covering happy paths, edge cases, and error conditions."]
|
||||
approve [shape=hexagon, label="Approve Plan"]
|
||||
generate [label="Generate Tests", class="coding", prompt="Write the test suite following the plan and project conventions."]
|
||||
run [label="Run Tests", prompt="Execute the test suite and collect results."]
|
||||
fix [label="Fix Failures", class="coding", prompt="Fix any failing tests, ensuring they test the right behavior."]
|
||||
|
||||
start -> analyze -> plan -> approve
|
||||
approve -> generate [label="Approve"]
|
||||
approve -> plan [label="Revise"]
|
||||
generate -> run -> fix -> run
|
||||
run -> exit [label="All pass"]
|
||||
}
|
||||
sortOrder: 2
|
||||
---
|
||||
|
||||
The Test Generator workflow reads your source code, plans a comprehensive test suite, and writes tests that actually pass — with a human checkpoint to approve the plan before generation begins.
|
||||
|
||||
## How it works
|
||||
|
||||
1. **Analyze Source** — A frontier model reads the codebase and identifies all public functions, methods, and types that need test coverage.
|
||||
2. **Plan Tests** — Generates a structured test plan covering happy paths, edge cases, boundary values, and error conditions.
|
||||
3. **Human Approval** — The plan is presented for review. You can approve it or send it back for revision.
|
||||
4. **Generate & Validate** — Tests are written following your project's conventions, then executed. Any failures are automatically fixed in a retry loop.
|
||||
|
||||
## Why a workflow beats a single prompt
|
||||
|
||||
A single "write tests" prompt often produces tests that don't compile or test the wrong things. By separating analysis, planning, and generation into distinct stages — and adding a human gate — this workflow produces tests that are both comprehensive and correct.
|
||||
|
|
@ -3,27 +3,50 @@ import "../styles/global.css";
|
|||
|
||||
interface Props {
|
||||
title: string;
|
||||
description?: string;
|
||||
}
|
||||
|
||||
const { title } = Astro.props;
|
||||
const { title, description = "Fabro is the open source dark software factory for expert engineers. Define your process as a workflow graph, let AI agents execute it, and intervene only where it matters." } = Astro.props;
|
||||
---
|
||||
|
||||
<!doctype html>
|
||||
<html lang="en" class="bg-navy-950 text-ice-50 antialiased">
|
||||
<html lang="en" class="bg-navy-950 text-ice-50 antialiased overflow-x-hidden">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<meta name="description" content="Fabro is an open-source AI software factory for expert engineering teams. Ship production code through deterministic workflows, built-in verification, and full observability." />
|
||||
<meta name="description" content={description} />
|
||||
<meta name="application-name" content="Fabro" />
|
||||
<meta name="apple-mobile-web-app-title" content="Fabro" />
|
||||
<link rel="canonical" href={`https://fabro.sh${Astro.url.pathname}`} />
|
||||
|
||||
<!-- Open Graph -->
|
||||
<meta property="og:type" content="website" />
|
||||
<meta property="og:title" content={title} />
|
||||
<meta property="og:description" content={description} />
|
||||
<meta property="og:image" content="https://fabro.sh/og-image.png" />
|
||||
<meta property="og:image:width" content="1200" />
|
||||
<meta property="og:image:height" content="630" />
|
||||
<meta property="og:url" content={`https://fabro.sh${Astro.url.pathname}`} />
|
||||
<meta property="og:site_name" content="Fabro" />
|
||||
|
||||
<!-- Twitter Card -->
|
||||
<meta name="twitter:card" content="summary_large_image" />
|
||||
<meta name="twitter:title" content={title} />
|
||||
<meta name="twitter:description" content={description} />
|
||||
<meta name="twitter:image" content="https://fabro.sh/og-image.png" />
|
||||
<meta name="twitter:image:width" content="1200" />
|
||||
<meta name="twitter:image:height" content="630" />
|
||||
|
||||
<link rel="icon" type="image/svg+xml" href="/favicon.svg" />
|
||||
<link rel="preconnect" href="https://fonts.googleapis.com" />
|
||||
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
|
||||
<link
|
||||
href="https://fonts.googleapis.com/css2?family=DM+Sans:opsz,wght@9..40,400;9..40,500;9..40,600;9..40,700&family=JetBrains+Mono:wght@400;500&family=Sora:wght@400;500;600;700;800&display=swap"
|
||||
href="https://fonts.googleapis.com/css2?family=Outfit:wght@400;500;600;700&family=Lexend:wght@400;500;600;700&family=Fira+Code:wght@400;500&display=swap"
|
||||
rel="stylesheet"
|
||||
/>
|
||||
<title>{title}</title>
|
||||
</head>
|
||||
<body class="min-h-screen">
|
||||
<body class="min-h-screen overflow-x-hidden">
|
||||
<slot />
|
||||
</body>
|
||||
</html>
|
||||
|
|
|
|||
18
apps/marketing/src/lib/langIcons.ts
Normal file
|
|
@ -0,0 +1,18 @@
|
|||
export const langIcons: Record<string, { label: string; path: string }> = {
|
||||
python: {
|
||||
label: "Python",
|
||||
path: "M12 1.5c-5.1 0-4.8 2.2-4.8 2.2l.006 2.3H12.4v.7H4.8S1.5 6.3 1.5 11.5s2.9 5 2.9 5h1.7V14.1s-.1-2.9 2.8-2.9h4.9s2.8 0 2.8-2.7V3.8s.4-2.3-4.6-2.3zm-2.7 1.3a.9.9 0 1 1 0 1.8.9.9 0 0 1 0-1.8zM12 22.5c5.1 0 4.8-2.2 4.8-2.2l-.006-2.3H11.6v-.7h7.6s3.3.4 3.3-4.8-2.9-5-2.9-5h-1.7v2.4s.1 2.9-2.8 2.9h-4.9s-2.8 0-2.8 2.7v4.7s-.4 2.3 4.6 2.3zm2.7-1.3a.9.9 0 1 1 0-1.8.9.9 0 0 1 0 1.8z",
|
||||
},
|
||||
rust: {
|
||||
label: "Rust",
|
||||
path: "M23.8 14.1l-1.2-.7a10.5 10.5 0 0 0 0-2.8l1.2-.7a.3.3 0 0 0 .1-.4 12 12 0 0 0-2.6-4.5.3.3 0 0 0-.4 0l-1.2.7a10.2 10.2 0 0 0-2.4-1.4V3a.3.3 0 0 0-.2-.3 12 12 0 0 0-5.2 0 .3.3 0 0 0-.2.3v1.3a10.2 10.2 0 0 0-2.4 1.4l-1.2-.7a.3.3 0 0 0-.4 0A12 12 0 0 0 5.1 9.5a.3.3 0 0 0 .1.4l1.2.7a10.5 10.5 0 0 0 0 2.8l-1.2.7a.3.3 0 0 0-.1.4 12 12 0 0 0 2.6 4.5.3.3 0 0 0 .4 0l1.2-.7c.7.6 1.5 1 2.4 1.4V21a.3.3 0 0 0 .2.3 12 12 0 0 0 5.2 0 .3.3 0 0 0 .2-.3v-1.3c.9-.4 1.7-.8 2.4-1.4l1.2.7a.3.3 0 0 0 .4 0 12 12 0 0 0 2.6-4.5.3.3 0 0 0-.1-.4zM12 16.5a4.5 4.5 0 1 1 0-9 4.5 4.5 0 0 1 0 9z",
|
||||
},
|
||||
typescript: {
|
||||
label: "TypeScript",
|
||||
path: "M1.5 1.5h21v21h-21V1.5zm10.2 10.2v-1.8h6.6v1.8h-2.3v6.6h-2v-6.6h-2.3zm-4.1-1.8h2v4.5c0 .7.1 1.2.3 1.5.4.5 1 .8 1.8.8s1.4-.3 1.8-.8c.2-.3.3-.8.3-1.5V9.9h2v4.7c0 1-.2 1.8-.7 2.4-.7.9-1.8 1.3-3.4 1.3s-2.7-.4-3.4-1.3c-.5-.6-.7-1.4-.7-2.4V9.9z",
|
||||
},
|
||||
ruby: {
|
||||
label: "Ruby",
|
||||
path: "M20.6 18.3L21.7 5l-5.5 1.5-2.5-3.5-3 3.2L4.3 3 3 10.5l3.3 2-3 4.2 5.3.8 1.5 4 4.5-2.8 4 3.6 2-4z",
|
||||
},
|
||||
};
|
||||
60
apps/marketing/src/lib/renderWorkflow.ts
Normal file
|
|
@ -0,0 +1,60 @@
|
|||
import { instance } from "@viz-js/viz";
|
||||
|
||||
let vizInstance: Awaited<ReturnType<typeof instance>> | undefined;
|
||||
|
||||
async function getViz() {
|
||||
if (!vizInstance) {
|
||||
vizInstance = await instance();
|
||||
}
|
||||
return vizInstance;
|
||||
}
|
||||
|
||||
/**
|
||||
* Renders a Graphviz DOT string to an SVG string, styled to match the site's
|
||||
* teal-on-navy palette. The returned SVG has no fixed dimensions (uses viewBox)
|
||||
* so it can be sized by its container.
|
||||
*/
|
||||
export async function renderWorkflow(dot: string): Promise<string> {
|
||||
const viz = await getViz();
|
||||
|
||||
const styledDot = injectGraphStyle(dot);
|
||||
let svg = viz.renderString(styledDot, { format: "svg", engine: "dot" });
|
||||
|
||||
// Make SVG responsive: remove fixed width/height, keep viewBox
|
||||
svg = svg.replace(/\s*width="[^"]*"/, "");
|
||||
svg = svg.replace(/\s*height="[^"]*"/, "");
|
||||
|
||||
return svg;
|
||||
}
|
||||
|
||||
/**
|
||||
* Injects graph-level styling attributes into the DOT string so the rendered
|
||||
* SVG uses the site's color palette without post-processing.
|
||||
*/
|
||||
function injectGraphStyle(dot: string): string {
|
||||
const styleBlock = `
|
||||
bgcolor="transparent"
|
||||
node [
|
||||
fontname="Space Grotesk"
|
||||
fontsize=13
|
||||
fontcolor="#e8edf3"
|
||||
style="filled"
|
||||
fillcolor="#141c2f"
|
||||
color="#357f9e"
|
||||
penwidth=1.5
|
||||
shape=box
|
||||
margin="0.15,0.1"
|
||||
]
|
||||
edge [
|
||||
color="#4b5768"
|
||||
fontname="DM Sans"
|
||||
fontsize=10
|
||||
fontcolor="#a8b5c5"
|
||||
arrowsize=0.7
|
||||
penwidth=1.2
|
||||
]
|
||||
`;
|
||||
|
||||
// Insert style after the opening brace of the digraph
|
||||
return dot.replace(/\{/, `{\n${styleBlock}\n`);
|
||||
}
|
||||
64
apps/marketing/src/pages/blog/[slug].astro
Normal file
|
|
@ -0,0 +1,64 @@
|
|||
---
|
||||
import Layout from "../../layouts/Layout.astro";
|
||||
import Nav from "../../components/Nav.astro";
|
||||
import Footer from "../../components/Footer.astro";
|
||||
import PageScripts from "../../components/PageScripts.astro";
|
||||
import { getCollection, render } from "astro:content";
|
||||
|
||||
export async function getStaticPaths() {
|
||||
const posts = await getCollection("blog");
|
||||
return posts.map((post) => ({
|
||||
params: { slug: post.id },
|
||||
props: { post },
|
||||
}));
|
||||
}
|
||||
|
||||
const { post } = Astro.props;
|
||||
const { Content } = await render(post);
|
||||
|
||||
const formatDate = (date: Date) =>
|
||||
date.toLocaleDateString("en-US", { year: "numeric", month: "long", day: "numeric" });
|
||||
|
||||
const wordCount = post.body?.split(/\s+/).length ?? 0;
|
||||
const readingTime = Math.max(1, Math.round(wordCount / 250));
|
||||
---
|
||||
|
||||
<Layout title={`${post.data.title} — Fabro`} description={post.data.description}>
|
||||
<Nav currentPage="blog" />
|
||||
|
||||
<main>
|
||||
<article class="relative pt-24 sm:pt-28 pb-24">
|
||||
<div class="relative mx-auto max-w-3xl px-6">
|
||||
<!-- Back link -->
|
||||
<a href="/blog" class="reveal inline-flex items-center gap-2 text-sm text-ice-300/60 hover:text-teal-300 transition-colors mb-10 group">
|
||||
<svg class="h-4 w-4 transition-transform group-hover:-translate-x-1" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" viewBox="0 0 24 24">
|
||||
<polyline points="15,18 9,12 15,6" />
|
||||
</svg>
|
||||
Blog
|
||||
</a>
|
||||
|
||||
<!-- Header -->
|
||||
<header class="mb-10 border-b border-navy-800/40 pb-8">
|
||||
<h1 class="reveal reveal-d1 font-display text-3xl font-bold tracking-tight text-ice-50 sm:text-4xl leading-[1.15]">
|
||||
{post.data.title}
|
||||
</h1>
|
||||
<div class="reveal reveal-d2 mt-4 flex items-center gap-3 text-sm text-ice-300/50">
|
||||
<time class="font-mono">{formatDate(post.data.date)}</time>
|
||||
<span>·</span>
|
||||
<span>{post.data.author}</span>
|
||||
<span>·</span>
|
||||
<span>{readingTime} min read</span>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
<!-- Content -->
|
||||
<div class="reveal reveal-d3 prose">
|
||||
<Content />
|
||||
</div>
|
||||
</div>
|
||||
</article>
|
||||
</main>
|
||||
|
||||
<Footer />
|
||||
<PageScripts />
|
||||
</Layout>
|
||||
82
apps/marketing/src/pages/blog/index.astro
Normal file
|
|
@ -0,0 +1,82 @@
|
|||
---
|
||||
import Layout from "../../layouts/Layout.astro";
|
||||
import Nav from "../../components/Nav.astro";
|
||||
import Footer from "../../components/Footer.astro";
|
||||
import PageScripts from "../../components/PageScripts.astro";
|
||||
import { getCollection } from "astro:content";
|
||||
|
||||
const posts = (await getCollection("blog")).sort(
|
||||
(a, b) => b.data.date.getTime() - a.data.date.getTime()
|
||||
);
|
||||
|
||||
const [featured, ...older] = posts;
|
||||
|
||||
const formatDate = (date: Date) =>
|
||||
date.toLocaleDateString("en-US", { year: "numeric", month: "long", day: "numeric" });
|
||||
---
|
||||
|
||||
<Layout title="Blog — Fabro" description="News, updates, and insights from the Fabro team.">
|
||||
<Nav currentPage="blog" />
|
||||
|
||||
<main>
|
||||
<!-- Header — compact, content-forward -->
|
||||
<section class="relative pt-24 sm:pt-28 pb-10">
|
||||
<div class="absolute inset-0 noise-overlay"></div>
|
||||
<div class="pointer-events-none absolute top-8 -left-40 h-[400px] w-[400px] rounded-full bg-teal-500/8 blur-[120px]"></div>
|
||||
|
||||
<div class="relative mx-auto max-w-3xl px-6">
|
||||
<h1 class="reveal font-display text-4xl font-bold tracking-tight text-ice-50 sm:text-5xl leading-[1.1]">
|
||||
Blog
|
||||
</h1>
|
||||
<p class="reveal reveal-d1 mt-3 text-lg text-ice-300">
|
||||
News, updates, and insights from the Fabro team.
|
||||
</p>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- Featured post -->
|
||||
{featured && (
|
||||
<section class="relative pb-8">
|
||||
<div class="mx-auto max-w-3xl px-6">
|
||||
<a href={`/blog/${featured.id}`} class="reveal reveal-d2 group block">
|
||||
<article class="rounded-xl border border-teal-700/30 bg-gradient-to-br from-navy-800/40 to-navy-900/20 p-8 relative overflow-hidden transition-all duration-400 hover:border-teal-700/50 hover:shadow-[0_0_40px_-8px_rgba(53,127,158,0.15)]">
|
||||
<div class="absolute top-0 left-0 right-0 h-px bg-gradient-to-r from-transparent via-teal-500/40 to-transparent" />
|
||||
<time class="text-xs font-mono text-teal-500/70">{formatDate(featured.data.date)}</time>
|
||||
<h2 class="mt-3 font-display text-2xl font-bold text-ice-50 group-hover:text-teal-300 transition-colors sm:text-3xl">
|
||||
{featured.data.title}
|
||||
</h2>
|
||||
<p class="mt-3 text-base leading-relaxed text-ice-300">{featured.data.description}</p>
|
||||
<div class="mt-5 flex items-center gap-3">
|
||||
<span class="text-sm text-ice-300/50">By {featured.data.author}</span>
|
||||
<span class="text-ice-300/30">·</span>
|
||||
<span class="text-sm font-medium text-teal-500 group-hover:text-teal-300 transition-colors">
|
||||
Read →
|
||||
</span>
|
||||
</div>
|
||||
</article>
|
||||
</a>
|
||||
</div>
|
||||
</section>
|
||||
)}
|
||||
|
||||
<!-- Older posts -->
|
||||
{older.length > 0 && (
|
||||
<section class="relative pb-24">
|
||||
<div class="mx-auto max-w-3xl px-6 space-y-1">
|
||||
{older.map((post, i) => (
|
||||
<a href={`/blog/${post.id}`} class={`reveal reveal-d${Math.min(i + 3, 5)} group flex items-baseline gap-4 py-4 border-b border-navy-800/40 transition-colors hover:border-teal-700/30`}>
|
||||
<time class="shrink-0 w-28 text-xs font-mono text-ice-300/40">{formatDate(post.data.date)}</time>
|
||||
<span class="font-display text-base font-semibold text-ice-50 group-hover:text-teal-300 transition-colors">{post.data.title}</span>
|
||||
<span class="hidden sm:inline text-sm text-ice-300/50">— {post.data.description}</span>
|
||||
</a>
|
||||
))}
|
||||
</div>
|
||||
</section>
|
||||
)}
|
||||
|
||||
{older.length === 0 && <div class="pb-24" />}
|
||||
</main>
|
||||
|
||||
<Footer />
|
||||
<PageScripts />
|
||||
</Layout>
|
||||
258
apps/marketing/src/pages/roadmap.astro
Normal file
|
|
@ -0,0 +1,258 @@
|
|||
---
|
||||
import Layout from "../layouts/Layout.astro";
|
||||
import Nav from "../components/Nav.astro";
|
||||
import Footer from "../components/Footer.astro";
|
||||
import PageScripts from "../components/PageScripts.astro";
|
||||
import { getCollection } from "astro:content";
|
||||
|
||||
const allItems = await getCollection("roadmap");
|
||||
const byDate = (status: string) =>
|
||||
allItems
|
||||
.filter((item) => item.data.status === status)
|
||||
.sort((a, b) => b.data.date.getTime() - a.data.date.getTime());
|
||||
|
||||
const formatDate = (date: Date) =>
|
||||
date.toLocaleDateString("en-US", { month: "short", year: "numeric", timeZone: "UTC" });
|
||||
|
||||
const shipped = byDate("shipped");
|
||||
const SHIPPED_VISIBLE = 3;
|
||||
const shippedVisible = shipped.slice(0, SHIPPED_VISIBLE);
|
||||
const shippedHidden = shipped.slice(SHIPPED_VISIBLE);
|
||||
|
||||
const building = byDate("building");
|
||||
const next = byDate("next");
|
||||
|
||||
const statusConfig = {
|
||||
building: {
|
||||
dot: "bg-amber",
|
||||
border: "via-amber/30",
|
||||
label: "text-amber",
|
||||
track: "bg-amber/30",
|
||||
cardBorder: "border-amber/15",
|
||||
cardBg: "bg-amber/5",
|
||||
},
|
||||
next: {
|
||||
dot: "bg-teal-500",
|
||||
border: "via-teal-500/30",
|
||||
label: "text-teal-500",
|
||||
track: "bg-teal-500/30",
|
||||
cardBorder: "border-teal-500/15",
|
||||
cardBg: "bg-teal-500/5",
|
||||
},
|
||||
};
|
||||
---
|
||||
|
||||
<Layout title="Roadmap — Fabro">
|
||||
<Nav currentPage="roadmap" />
|
||||
|
||||
<main>
|
||||
<!-- Header -->
|
||||
<section class="relative overflow-hidden pt-24 sm:pt-28 pb-10">
|
||||
<div class="absolute inset-0 noise-overlay"></div>
|
||||
<div class="pointer-events-none absolute top-8 -left-40 h-[400px] w-[400px] rounded-full bg-teal-500/8 blur-[120px]"></div>
|
||||
|
||||
<div class="relative mx-auto max-w-3xl px-6">
|
||||
<h1 class="reveal font-display text-4xl font-bold tracking-tight text-ice-50 sm:text-5xl leading-[1.1]">
|
||||
Roadmap
|
||||
</h1>
|
||||
<p class="reveal reveal-d1 mt-3 text-lg text-ice-300">
|
||||
What we've shipped, what we're building, and where Fabro is going next.
|
||||
</p>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- Shipped — compact rows -->
|
||||
<section class="relative pb-12">
|
||||
<div class="mx-auto max-w-3xl px-6">
|
||||
<div class="reveal flex items-center gap-4">
|
||||
<div class="flex items-center gap-3 rounded-full border border-navy-800 bg-navy-900/80 px-5 py-2">
|
||||
<svg class="h-4 w-4 text-mint" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" viewBox="0 0 24 24">
|
||||
<polyline points="20,6 9,17 4,12" />
|
||||
</svg>
|
||||
<span class="font-display text-sm font-semibold uppercase tracking-wider text-mint">Shipped</span>
|
||||
</div>
|
||||
<div class="h-px flex-1 bg-gradient-to-r from-navy-800 to-transparent" />
|
||||
</div>
|
||||
|
||||
<div class="mt-6 space-y-0">
|
||||
{shippedVisible.map((item, i) => (
|
||||
<div class={`reveal reveal-d${i + 1} flex items-center gap-4 py-3 border-b border-navy-800/40`}>
|
||||
<span class="shrink-0 w-20 text-xs font-mono text-mint/60">{item.data.date && formatDate(item.data.date)}</span>
|
||||
<span class="font-display text-sm font-semibold text-ice-50">{item.data.title}</span>
|
||||
<span class="shipped-info relative ml-auto shrink-0">
|
||||
<svg class="h-4 w-4 text-ice-300/30 hover:text-ice-300/60 transition-colors cursor-help" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" viewBox="0 0 24 24">
|
||||
<circle cx="12" cy="12" r="10" /><line x1="12" y1="16" x2="12" y2="12" /><line x1="12" y1="8" x2="12.01" y2="8" />
|
||||
</svg>
|
||||
<span class="shipped-info-tip">{item.data.description}</span>
|
||||
</span>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
|
||||
{shippedHidden.length > 0 && (
|
||||
<div class="reveal reveal-d4">
|
||||
<button
|
||||
id="shipped-toggle"
|
||||
class="group mt-4 flex items-center gap-2 text-sm text-ice-300/50 hover:text-mint transition-colors cursor-pointer"
|
||||
aria-expanded="false"
|
||||
aria-controls="shipped-older"
|
||||
>
|
||||
<svg
|
||||
id="shipped-chevron"
|
||||
class="h-4 w-4 transition-transform duration-300"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
stroke-width="2"
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
viewBox="0 0 24 24"
|
||||
>
|
||||
<polyline points="6,9 12,15 18,9" />
|
||||
</svg>
|
||||
<span id="shipped-toggle-label">{shippedHidden.length} older features</span>
|
||||
</button>
|
||||
|
||||
<div
|
||||
id="shipped-older"
|
||||
class="grid overflow-hidden transition-[grid-template-rows] duration-500 ease-[cubic-bezier(0.22,1,0.36,1)]"
|
||||
style="grid-template-rows: 0fr;"
|
||||
>
|
||||
<div class="min-h-0">
|
||||
<div class="space-y-0">
|
||||
{shippedHidden.map((item) => (
|
||||
<div class="flex items-center gap-4 py-3 border-b border-navy-800/40">
|
||||
<span class="shrink-0 w-20 text-xs font-mono text-mint/60">{item.data.date && formatDate(item.data.date)}</span>
|
||||
<span class="font-display text-sm font-semibold text-ice-50">{item.data.title}</span>
|
||||
<span class="shipped-info relative ml-auto shrink-0">
|
||||
<svg class="h-4 w-4 text-ice-300/30 hover:text-ice-300/60 transition-colors cursor-help" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" viewBox="0 0 24 24">
|
||||
<circle cx="12" cy="12" r="10" /><line x1="12" y1="16" x2="12" y2="12" /><line x1="12" y1="8" x2="12.01" y2="8" />
|
||||
</svg>
|
||||
<span class="shipped-info-tip">{item.data.description}</span>
|
||||
</span>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- Building — only render if items exist -->
|
||||
{building.length > 0 && (
|
||||
<section class="relative pb-12">
|
||||
<div class="mx-auto max-w-3xl px-6">
|
||||
<div class="reveal flex items-center gap-4">
|
||||
<div class="flex items-center gap-3 rounded-full border border-navy-800 bg-navy-900/80 px-5 py-2">
|
||||
<div class="h-3 w-3 rounded-full bg-amber animate-pulse-glow" />
|
||||
<span class="font-display text-sm font-semibold uppercase tracking-wider text-amber">Building</span>
|
||||
</div>
|
||||
<div class="h-px flex-1 bg-gradient-to-r from-navy-800 to-transparent" />
|
||||
</div>
|
||||
|
||||
<div class="relative mt-8 ml-5">
|
||||
<div class="absolute top-0 bottom-0 left-0 w-px bg-amber/30" />
|
||||
<div class="space-y-0">
|
||||
{building.map((item, i) => (
|
||||
<div class={`reveal reveal-d${Math.min(i + 1, 5)} relative pl-10 pb-8 group`}>
|
||||
<div class="absolute left-0 top-1.5 -translate-x-1/2 z-10">
|
||||
<div class="h-2.5 w-2.5 rounded-full bg-amber ring-4 ring-navy-950 transition-all group-hover:scale-125" />
|
||||
</div>
|
||||
<div class="rounded-xl border border-amber/15 bg-amber/5 p-6 relative overflow-hidden transition-all duration-400 hover:border-amber/25 hover:bg-amber/8">
|
||||
<div class="absolute top-0 left-0 right-0 h-px bg-gradient-to-r from-transparent via-amber/30 to-transparent" />
|
||||
<h3 class="font-display text-base font-semibold text-ice-50">{item.data.title}</h3>
|
||||
<p class="mt-2 text-sm leading-relaxed text-ice-300">{item.data.description}</p>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
)}
|
||||
|
||||
<!-- Next — only render if items exist -->
|
||||
{next.length > 0 && (
|
||||
<section class="relative pb-24">
|
||||
<div class="mx-auto max-w-3xl px-6">
|
||||
<div class="reveal flex items-center gap-4">
|
||||
<div class="flex items-center gap-3 rounded-full border border-navy-800 bg-navy-900/80 px-5 py-2">
|
||||
<svg class="h-4 w-4 text-teal-500" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" viewBox="0 0 24 24">
|
||||
<circle cx="12" cy="12" r="10" />
|
||||
<polyline points="12,6 12,12 16,14" />
|
||||
</svg>
|
||||
<span class="font-display text-sm font-semibold uppercase tracking-wider text-teal-500">Next</span>
|
||||
</div>
|
||||
<div class="h-px flex-1 bg-gradient-to-r from-navy-800 to-transparent" />
|
||||
</div>
|
||||
|
||||
<div class="relative mt-8 ml-5">
|
||||
<div class="absolute top-0 bottom-0 left-0 w-px bg-teal-500/30" />
|
||||
<div class="space-y-0">
|
||||
{next.map((item, i) => (
|
||||
<div class={`reveal reveal-d${Math.min(i + 1, 5)} relative pl-10 pb-8 group`}>
|
||||
<div class="absolute left-0 top-1.5 -translate-x-1/2 z-10">
|
||||
<div class="h-2.5 w-2.5 rounded-full bg-teal-500 ring-4 ring-navy-950 transition-all group-hover:scale-125" />
|
||||
</div>
|
||||
<div class="rounded-xl border border-teal-500/15 bg-teal-500/5 p-6 relative overflow-hidden transition-all duration-400 hover:border-teal-500/25 hover:bg-teal-500/8">
|
||||
<div class="absolute top-0 left-0 right-0 h-px bg-gradient-to-r from-transparent via-teal-500/30 to-transparent" />
|
||||
<h3 class="font-display text-base font-semibold text-ice-50">{item.data.title}</h3>
|
||||
<p class="mt-2 text-sm leading-relaxed text-ice-300">{item.data.description}</p>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
)}
|
||||
|
||||
<!-- CTA -->
|
||||
<section class="relative overflow-hidden border-t border-navy-800 py-24">
|
||||
<div class="absolute inset-0 grid-overlay"></div>
|
||||
<div class="pointer-events-none absolute top-1/2 left-1/2 -translate-x-1/2 -translate-y-1/2 h-[400px] w-[400px] rounded-full bg-teal-700/6 blur-[120px]"></div>
|
||||
<div class="relative mx-auto max-w-2xl px-6 text-center">
|
||||
<h2 class="reveal font-display text-3xl font-bold tracking-tight text-ice-50">
|
||||
Shape what we build
|
||||
</h2>
|
||||
<p class="reveal reveal-d1 mt-4 text-lg leading-relaxed text-ice-300">
|
||||
Have a feature request or want to influence priorities? Join the conversation.
|
||||
</p>
|
||||
<div class="reveal reveal-d2 mt-8 flex items-center justify-center gap-4">
|
||||
<a
|
||||
href="https://github.com/fabro-sh/fabro/discussions"
|
||||
class="rounded-xl bg-teal-700 px-7 py-3.5 text-sm font-semibold text-white transition-all hover:bg-teal-500 hover:shadow-[0_0_24px_-4px_rgba(53,127,158,0.5)]"
|
||||
>
|
||||
Join Discussions
|
||||
</a>
|
||||
<a
|
||||
href="/discord"
|
||||
class="rounded-xl border border-navy-600 px-7 py-3.5 text-sm font-semibold text-ice-300 transition-all hover:border-teal-700/60 hover:text-ice-50"
|
||||
>
|
||||
Discord
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
</main>
|
||||
|
||||
<Footer />
|
||||
|
||||
<PageScripts />
|
||||
<script>
|
||||
// Shipped older items toggle
|
||||
const toggle = document.getElementById("shipped-toggle");
|
||||
const older = document.getElementById("shipped-older");
|
||||
const chevron = document.getElementById("shipped-chevron");
|
||||
|
||||
if (toggle && older && chevron) {
|
||||
toggle.addEventListener("click", () => {
|
||||
const isExpanded = toggle.getAttribute("aria-expanded") === "true";
|
||||
toggle.setAttribute("aria-expanded", String(!isExpanded));
|
||||
older.style.gridTemplateRows = isExpanded ? "0fr" : "1fr";
|
||||
chevron.style.transform = isExpanded ? "" : "rotate(180deg)";
|
||||
});
|
||||
}
|
||||
</script>
|
||||
</Layout>
|
||||
192
apps/marketing/src/pages/showcase/[slug].astro
Normal file
|
|
@ -0,0 +1,192 @@
|
|||
---
|
||||
import Layout from "../../layouts/Layout.astro";
|
||||
import Nav from "../../components/Nav.astro";
|
||||
import Footer from "../../components/Footer.astro";
|
||||
import PageScripts from "../../components/PageScripts.astro";
|
||||
import { getCollection, render } from "astro:content";
|
||||
import { langIcons } from "../../lib/langIcons";
|
||||
import { renderWorkflow } from "../../lib/renderWorkflow";
|
||||
|
||||
export async function getStaticPaths() {
|
||||
const entries = (await getCollection("showcase")).sort(
|
||||
(a, b) => a.data.sortOrder - b.data.sortOrder
|
||||
);
|
||||
return entries.map((entry, i) => ({
|
||||
params: { slug: entry.id },
|
||||
props: {
|
||||
entry,
|
||||
prev: entries[i - 1] ?? null,
|
||||
next: entries[i + 1] ?? null,
|
||||
},
|
||||
}));
|
||||
}
|
||||
|
||||
const { entry, prev, next } = Astro.props;
|
||||
const { Content } = await render(entry);
|
||||
const workflowSvg = await renderWorkflow(entry.data.workflow);
|
||||
---
|
||||
|
||||
<Layout title={`${entry.data.title} — Showcase — Fabro`} description={entry.data.description}>
|
||||
<Nav currentPage="showcase" />
|
||||
|
||||
<main>
|
||||
<article class="relative pt-24 sm:pt-28 pb-24">
|
||||
<div class="relative mx-auto max-w-3xl px-6">
|
||||
<!-- Back link -->
|
||||
<a href="/showcase" class="reveal inline-flex items-center gap-2 text-sm text-ice-300/60 hover:text-teal-300 transition-colors mb-10 group">
|
||||
<svg class="h-4 w-4 transition-transform group-hover:-translate-x-1" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" viewBox="0 0 24 24">
|
||||
<polyline points="15,18 9,12 15,6" />
|
||||
</svg>
|
||||
Showcase
|
||||
</a>
|
||||
|
||||
<!-- Header -->
|
||||
<header class="mb-10">
|
||||
<h1 class="reveal reveal-d1 font-display text-3xl font-bold tracking-tight text-ice-50 sm:text-4xl leading-[1.15]">
|
||||
{entry.data.title}
|
||||
</h1>
|
||||
<p class="reveal reveal-d2 mt-3 text-lg text-ice-300">
|
||||
{entry.data.description}
|
||||
</p>
|
||||
|
||||
{/* Compact metadata strip */}
|
||||
<div class="reveal reveal-d2 mt-5 flex flex-wrap items-center gap-x-4 gap-y-2 text-sm text-ice-300/60">
|
||||
{entry.data.languages.map((lang) => {
|
||||
const icon = langIcons[lang];
|
||||
return icon ? (
|
||||
<span class="flex items-center gap-1.5" title={icon.label}>
|
||||
<svg class="h-4 w-4" fill="currentColor" viewBox="0 0 24 24">
|
||||
<path d={icon.path} />
|
||||
</svg>
|
||||
{icon.label}
|
||||
</span>
|
||||
) : null;
|
||||
})}
|
||||
<span class="text-ice-300/20">|</span>
|
||||
{entry.data.models.map((model) => (
|
||||
<span class="font-mono text-xs text-teal-300/70">{model}</span>
|
||||
))}
|
||||
<span class="text-ice-300/20">|</span>
|
||||
{entry.data.skills.map((skill) => (
|
||||
<span class="font-mono text-xs text-ice-300/50">{skill}</span>
|
||||
))}
|
||||
</div>
|
||||
|
||||
{/* GitHub button */}
|
||||
<div class="reveal reveal-d3 mt-5">
|
||||
<a
|
||||
href={entry.data.github}
|
||||
class="inline-flex items-center gap-2 rounded-lg border border-navy-600 px-4 py-2 text-sm font-medium text-ice-300 transition-all hover:border-teal-700/60 hover:text-ice-50"
|
||||
>
|
||||
<svg class="h-4 w-4" fill="currentColor" viewBox="0 0 24 24">
|
||||
<path d="M12 0C5.37 0 0 5.37 0 12c0 5.31 3.435 9.795 8.205 11.385.6.105.825-.255.825-.57 0-.285-.015-1.23-.015-2.235-3.015.555-3.795-.735-4.035-1.41-.135-.345-.72-1.41-1.23-1.695-.42-.225-1.02-.78-.015-.795.945-.015 1.62.87 1.845 1.23 1.08 1.815 2.805 1.305 3.495.99.105-.78.42-1.305.765-1.605-2.67-.3-5.46-1.335-5.46-5.925 0-1.305.465-2.385 1.23-3.225-.12-.3-.54-1.53.12-3.18 0 0 1.005-.315 3.3 1.23.96-.27 1.98-.405 3-.405s2.04.135 3 .405c2.295-1.56 3.3-1.23 3.3-1.23.66 1.65.24 2.88.12 3.18.765.84 1.23 1.905 1.23 3.225 0 4.605-2.805 5.625-5.475 5.925.435.375.81 1.095.81 2.22 0 1.605-.015 2.895-.015 3.3 0 .315.225.69.825.57A12.02 12.02 0 0024 12c0-6.63-5.37-12-12-12z" />
|
||||
</svg>
|
||||
View on GitHub
|
||||
</a>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
<!-- Workflow graph — hero element -->
|
||||
<section class="reveal-scale reveal-d3 mb-10">
|
||||
<h2 class="font-display text-lg font-semibold text-ice-50 mb-4">Workflow</h2>
|
||||
<div class="showcase-graph-hero rounded-xl border border-navy-800/60 bg-navy-950/80 p-6 flex items-center justify-center overflow-x-auto" set:html={workflowSvg} />
|
||||
</section>
|
||||
|
||||
<!-- Prompt section -->
|
||||
<section class="reveal reveal-d4 mb-10">
|
||||
<h2 class="font-display text-lg font-semibold text-ice-50 mb-4">Prompt</h2>
|
||||
<div class="rounded-xl border border-navy-800/60 bg-navy-900/30 p-6 relative">
|
||||
<div
|
||||
id="prompt-container"
|
||||
class="overflow-hidden transition-[max-height] duration-500 ease-[cubic-bezier(0.22,1,0.36,1)] max-h-[4.5em]"
|
||||
>
|
||||
<p class="font-mono text-sm leading-relaxed text-ice-300/80 whitespace-pre-wrap">{entry.data.prompt}</p>
|
||||
</div>
|
||||
<button
|
||||
id="prompt-toggle"
|
||||
class="group mt-4 flex items-center gap-2 text-sm text-ice-300/50 hover:text-teal-300 transition-colors cursor-pointer"
|
||||
aria-expanded="false"
|
||||
>
|
||||
<svg
|
||||
id="prompt-chevron"
|
||||
class="h-4 w-4 transition-transform duration-300"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
stroke-width="2"
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
viewBox="0 0 24 24"
|
||||
>
|
||||
<polyline points="6,9 12,15 18,9" />
|
||||
</svg>
|
||||
<span id="prompt-toggle-label">Show full prompt</span>
|
||||
</button>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- About (rendered markdown body) -->
|
||||
<section class="reveal-left reveal-d5">
|
||||
<h2 class="font-display text-lg font-semibold text-ice-50 mb-4">About</h2>
|
||||
<div class="prose">
|
||||
<Content />
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- Prev / Next navigation -->
|
||||
{(prev || next) && (
|
||||
<nav class="reveal reveal-d5 mt-16 grid gap-4 sm:grid-cols-2 border-t border-navy-800/40 pt-8">
|
||||
{prev ? (
|
||||
<a href={`/showcase/${prev.id}`} class="group flex items-start gap-3 rounded-lg border border-navy-800/60 bg-navy-900/30 p-4 transition-all hover:border-teal-700/40">
|
||||
<svg class="mt-0.5 h-5 w-5 shrink-0 text-ice-300/40 transition-transform group-hover:-translate-x-1 group-hover:text-teal-300" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" viewBox="0 0 24 24">
|
||||
<polyline points="15,18 9,12 15,6" />
|
||||
</svg>
|
||||
<div>
|
||||
<span class="text-xs text-ice-300/40">Previous</span>
|
||||
<span class="block font-display text-sm font-semibold text-ice-50 group-hover:text-teal-300 transition-colors">{prev.data.title}</span>
|
||||
</div>
|
||||
</a>
|
||||
) : <div />}
|
||||
{next ? (
|
||||
<a href={`/showcase/${next.id}`} class="group flex items-start gap-3 rounded-lg border border-navy-800/60 bg-navy-900/30 p-4 transition-all hover:border-teal-700/40 text-right sm:flex-row-reverse">
|
||||
<svg class="mt-0.5 h-5 w-5 shrink-0 text-ice-300/40 transition-transform group-hover:translate-x-1 group-hover:text-teal-300" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" viewBox="0 0 24 24">
|
||||
<polyline points="9,6 15,12 9,18" />
|
||||
</svg>
|
||||
<div>
|
||||
<span class="text-xs text-ice-300/40">Next</span>
|
||||
<span class="block font-display text-sm font-semibold text-ice-50 group-hover:text-teal-300 transition-colors">{next.data.title}</span>
|
||||
</div>
|
||||
</a>
|
||||
) : <div />}
|
||||
</nav>
|
||||
)}
|
||||
</div>
|
||||
</article>
|
||||
</main>
|
||||
|
||||
<Footer />
|
||||
<PageScripts />
|
||||
|
||||
<script>
|
||||
const toggle = document.getElementById("prompt-toggle");
|
||||
const container = document.getElementById("prompt-container");
|
||||
const chevron = document.getElementById("prompt-chevron");
|
||||
const label = document.getElementById("prompt-toggle-label");
|
||||
|
||||
if (toggle && container && chevron && label) {
|
||||
toggle.addEventListener("click", () => {
|
||||
const isExpanded = toggle.getAttribute("aria-expanded") === "true";
|
||||
toggle.setAttribute("aria-expanded", String(!isExpanded));
|
||||
container.style.maxHeight = isExpanded ? "4.5em" : `${container.scrollHeight}px`;
|
||||
chevron.style.transform = isExpanded ? "" : "rotate(180deg)";
|
||||
label.textContent = isExpanded ? "Show full prompt" : "Hide prompt";
|
||||
});
|
||||
}
|
||||
</script>
|
||||
</Layout>
|
||||
|
||||
<style>
|
||||
.showcase-graph-hero :global(svg) {
|
||||
max-width: 100%;
|
||||
height: auto;
|
||||
}
|
||||
</style>
|
||||
105
apps/marketing/src/pages/showcase/index.astro
Normal file
|
|
@ -0,0 +1,105 @@
|
|||
---
|
||||
import Layout from "../../layouts/Layout.astro";
|
||||
import Nav from "../../components/Nav.astro";
|
||||
import Footer from "../../components/Footer.astro";
|
||||
import PageScripts from "../../components/PageScripts.astro";
|
||||
import { getCollection } from "astro:content";
|
||||
import { langIcons } from "../../lib/langIcons";
|
||||
import { renderWorkflow } from "../../lib/renderWorkflow";
|
||||
|
||||
const entries = (await getCollection("showcase")).sort(
|
||||
(a, b) => a.data.sortOrder - b.data.sortOrder
|
||||
);
|
||||
|
||||
const workflowSvgs = new Map<string, string>();
|
||||
for (const entry of entries) {
|
||||
workflowSvgs.set(entry.id, await renderWorkflow(entry.data.workflow));
|
||||
}
|
||||
---
|
||||
|
||||
<Layout title="Showcase — Fabro" description="Projects and recipes built with Fabro workflows.">
|
||||
<Nav currentPage="showcase" />
|
||||
|
||||
<main>
|
||||
<!-- Header -->
|
||||
<section class="relative pt-24 sm:pt-28 pb-10">
|
||||
<div class="absolute inset-0 noise-overlay"></div>
|
||||
<div class="pointer-events-none absolute top-8 -left-40 h-[400px] w-[400px] rounded-full bg-teal-500/8 blur-[120px]"></div>
|
||||
|
||||
<div class="relative mx-auto max-w-4xl px-6">
|
||||
<h1 class="reveal font-display text-4xl font-bold tracking-tight text-ice-50 sm:text-5xl leading-[1.1]">
|
||||
Showcase
|
||||
</h1>
|
||||
<p class="reveal reveal-d1 mt-3 text-lg text-ice-300">
|
||||
Reproducible workflow recipes built with Fabro. Clone, customize, and run.
|
||||
</p>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- Card grid -->
|
||||
<section class="relative pb-24">
|
||||
<div class="mx-auto max-w-4xl px-6">
|
||||
<div class="grid gap-6 sm:grid-cols-2">
|
||||
{entries.map((entry, i) => (
|
||||
<a
|
||||
href={`/showcase/${entry.id}`}
|
||||
class={`reveal reveal-d${Math.min(i + 2, 5)} glow-card group block rounded-xl border border-navy-800/60 bg-navy-900/30 overflow-hidden transition-all duration-400 hover:border-teal-700/40 hover:shadow-[0_0_40px_-8px_rgba(53,127,158,0.12)]`}
|
||||
>
|
||||
{/* Workflow graph thumbnail */}
|
||||
<div class="relative h-36 bg-navy-950/80 flex items-center justify-center p-4 overflow-hidden">
|
||||
<div class="showcase-graph-thumb w-full h-full flex items-center justify-center opacity-60 group-hover:opacity-90 transition-opacity duration-400" set:html={workflowSvgs.get(entry.id)} />
|
||||
<div class="absolute bottom-0 left-0 right-0 h-px bg-gradient-to-r from-transparent via-teal-500/20 to-transparent" />
|
||||
</div>
|
||||
|
||||
{/* Card body */}
|
||||
<div class="p-6">
|
||||
<h2 class="font-display text-lg font-semibold text-ice-50 group-hover:text-teal-300 transition-colors">
|
||||
{entry.data.title}
|
||||
</h2>
|
||||
<p class="mt-2 text-sm leading-relaxed text-ice-300/70 line-clamp-2">
|
||||
{entry.data.description}
|
||||
</p>
|
||||
|
||||
{/* Language icons */}
|
||||
<div class="mt-4 flex items-center gap-3">
|
||||
{entry.data.languages.map((lang) => {
|
||||
const icon = langIcons[lang];
|
||||
return icon ? (
|
||||
<span class="flex items-center gap-1.5 text-xs text-ice-300/50" title={icon.label}>
|
||||
<svg class="h-4 w-4" fill="currentColor" viewBox="0 0 24 24">
|
||||
<path d={icon.path} />
|
||||
</svg>
|
||||
{icon.label}
|
||||
</span>
|
||||
) : null;
|
||||
})}
|
||||
</div>
|
||||
|
||||
{/* Tags */}
|
||||
<div class="mt-3 flex flex-wrap gap-1.5">
|
||||
{entry.data.tags.map((tag) => (
|
||||
<span class="rounded-full border border-navy-800 bg-navy-900/60 px-3 py-1 text-xs text-ice-300/70">
|
||||
{tag}
|
||||
</span>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</a>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
</main>
|
||||
|
||||
<Footer />
|
||||
<PageScripts />
|
||||
</Layout>
|
||||
|
||||
<style>
|
||||
.showcase-graph-thumb :global(svg) {
|
||||
max-width: 100%;
|
||||
max-height: 100%;
|
||||
width: auto;
|
||||
height: auto;
|
||||
}
|
||||
</style>
|
||||
|
|
@ -18,9 +18,9 @@
|
|||
--color-amber: #F0A45B;
|
||||
--color-coral: #E86B6B;
|
||||
|
||||
--font-display: "Sora", ui-sans-serif, system-ui, sans-serif;
|
||||
--font-sans: "DM Sans", ui-sans-serif, system-ui, sans-serif;
|
||||
--font-mono: "JetBrains Mono", ui-monospace, monospace;
|
||||
--font-display: "Outfit", ui-sans-serif, system-ui, sans-serif;
|
||||
--font-sans: "Lexend", ui-sans-serif, system-ui, sans-serif;
|
||||
--font-mono: "Fira Code", ui-monospace, monospace;
|
||||
|
||||
--animate-float: float 8s ease-in-out infinite;
|
||||
--animate-pulse-glow: pulse-glow 4s ease-in-out infinite;
|
||||
|
|
@ -51,14 +51,39 @@
|
|||
animation: gradient-shift 6s ease infinite;
|
||||
}
|
||||
|
||||
/* Subtle dot grid overlay */
|
||||
.dot-grid {
|
||||
background-image: radial-gradient(var(--color-teal-500) 0.5px, transparent 0.5px);
|
||||
background-size: 24px 24px;
|
||||
opacity: 0.05;
|
||||
/* Cross-hatch grid overlay */
|
||||
.grid-overlay {
|
||||
background-image:
|
||||
linear-gradient(var(--color-navy-800) 1px, transparent 1px),
|
||||
linear-gradient(90deg, var(--color-navy-800) 1px, transparent 1px);
|
||||
background-size: 48px 48px;
|
||||
opacity: 0.12;
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
/* Noise texture */
|
||||
.noise-overlay {
|
||||
background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='200' height='200'%3E%3Cfilter id='n'%3E%3CfeTurbulence type='fractalNoise' baseFrequency='0.75' numOctaves='4' stitchTiles='stitch'/%3E%3C/filter%3E%3Crect width='100%25' height='100%25' filter='url(%23n)' opacity='0.03'/%3E%3C/svg%3E");
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
/* Scanlines */
|
||||
.scanlines {
|
||||
background-image: repeating-linear-gradient(
|
||||
0deg,
|
||||
rgba(255, 255, 255, 0.015) 0px,
|
||||
rgba(255, 255, 255, 0.015) 1px,
|
||||
transparent 1px,
|
||||
transparent 4px
|
||||
);
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
/* Prevent grid/flex blowout from wide children (SVGs, pre blocks) */
|
||||
.grid > * {
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
/* Feature card hover glow */
|
||||
.glow-card {
|
||||
transition: border-color 0.4s ease, box-shadow 0.4s ease, transform 0.4s ease;
|
||||
|
|
@ -69,7 +94,7 @@
|
|||
transform: translateY(-2px);
|
||||
}
|
||||
|
||||
/* Scroll reveal */
|
||||
/* Scroll reveal — fade up (default) */
|
||||
.reveal {
|
||||
opacity: 0;
|
||||
transform: translateY(28px);
|
||||
|
|
@ -79,14 +104,195 @@
|
|||
opacity: 1;
|
||||
transform: translateY(0);
|
||||
}
|
||||
|
||||
/* Scroll reveal — slide from left */
|
||||
.reveal-left {
|
||||
opacity: 0;
|
||||
transform: translateX(-40px);
|
||||
transition: opacity 0.8s cubic-bezier(0.22, 1, 0.36, 1), transform 0.8s cubic-bezier(0.22, 1, 0.36, 1);
|
||||
}
|
||||
.reveal-left.revealed {
|
||||
opacity: 1;
|
||||
transform: translateX(0);
|
||||
}
|
||||
|
||||
/* Scroll reveal — slide from right */
|
||||
.reveal-right {
|
||||
opacity: 0;
|
||||
transform: translateX(40px);
|
||||
transition: opacity 0.8s cubic-bezier(0.22, 1, 0.36, 1), transform 0.8s cubic-bezier(0.22, 1, 0.36, 1);
|
||||
}
|
||||
.reveal-right.revealed {
|
||||
opacity: 1;
|
||||
transform: translateX(0);
|
||||
}
|
||||
|
||||
/* Scroll reveal — scale up */
|
||||
.reveal-scale {
|
||||
opacity: 0;
|
||||
transform: scale(0.95);
|
||||
transition: opacity 0.8s cubic-bezier(0.22, 1, 0.36, 1), transform 0.8s cubic-bezier(0.22, 1, 0.36, 1);
|
||||
}
|
||||
.reveal-scale.revealed {
|
||||
opacity: 1;
|
||||
transform: scale(1);
|
||||
}
|
||||
|
||||
/* Stagger delays */
|
||||
.reveal-d1 { transition-delay: 100ms; }
|
||||
.reveal-d2 { transition-delay: 200ms; }
|
||||
.reveal-d3 { transition-delay: 300ms; }
|
||||
.reveal-d4 { transition-delay: 400ms; }
|
||||
.reveal-d5 { transition-delay: 500ms; }
|
||||
|
||||
/* Section gradient divider */
|
||||
/* Section gradient divider (used sparingly) */
|
||||
.section-divider {
|
||||
height: 1px;
|
||||
background: linear-gradient(to right, transparent, var(--color-navy-600), transparent);
|
||||
}
|
||||
|
||||
/* Trace bar animation */
|
||||
.trace-bar {
|
||||
transform: scaleX(0);
|
||||
transform-origin: left;
|
||||
transition: transform 1s cubic-bezier(0.22, 1, 0.36, 1);
|
||||
}
|
||||
.trace-bar.revealed {
|
||||
transform: scaleX(1);
|
||||
}
|
||||
|
||||
/* Lightbox transition */
|
||||
.lightbox-overlay {
|
||||
opacity: 0;
|
||||
pointer-events: none;
|
||||
transition: opacity 0.3s ease;
|
||||
}
|
||||
.lightbox-overlay.active {
|
||||
opacity: 1;
|
||||
pointer-events: auto;
|
||||
}
|
||||
.lightbox-overlay img {
|
||||
transform: scale(0.95);
|
||||
transition: transform 0.3s ease;
|
||||
}
|
||||
.lightbox-overlay.active img {
|
||||
transform: scale(1);
|
||||
}
|
||||
|
||||
/* Workflow graph path draw-in */
|
||||
@keyframes draw-path {
|
||||
to { stroke-dashoffset: 0; }
|
||||
}
|
||||
.graph-edge {
|
||||
animation: draw-path 1.2s cubic-bezier(0.22, 1, 0.36, 1) forwards;
|
||||
animation-play-state: paused;
|
||||
}
|
||||
.graph-draw .graph-edge {
|
||||
animation-play-state: running;
|
||||
}
|
||||
|
||||
/* Install widget tabs */
|
||||
.install-tab {
|
||||
color: var(--color-ice-300);
|
||||
background: transparent;
|
||||
cursor: pointer;
|
||||
}
|
||||
.install-tab:hover {
|
||||
color: var(--color-ice-50);
|
||||
}
|
||||
.install-tab.active {
|
||||
color: var(--color-teal-300);
|
||||
background: var(--color-navy-800);
|
||||
}
|
||||
.install-panel {
|
||||
display: none;
|
||||
}
|
||||
.install-panel.active {
|
||||
display: block;
|
||||
}
|
||||
|
||||
/* Blog prose styles */
|
||||
.prose {
|
||||
color: var(--color-ice-300);
|
||||
line-height: 1.75;
|
||||
}
|
||||
.prose h1, .prose h2, .prose h3, .prose h4 {
|
||||
color: var(--color-ice-50);
|
||||
font-family: var(--font-display);
|
||||
font-weight: 700;
|
||||
margin-top: 2em;
|
||||
margin-bottom: 0.75em;
|
||||
}
|
||||
.prose h2 { font-size: 1.5rem; }
|
||||
.prose h3 { font-size: 1.25rem; }
|
||||
.prose p { margin-bottom: 1.25em; }
|
||||
.prose a {
|
||||
color: var(--color-teal-300);
|
||||
text-decoration: underline;
|
||||
text-underline-offset: 2px;
|
||||
transition: color 0.2s;
|
||||
}
|
||||
.prose a:hover { color: var(--color-teal-500); }
|
||||
.prose ul, .prose ol { padding-left: 1.5em; margin-bottom: 1.25em; }
|
||||
.prose li { margin-bottom: 0.5em; }
|
||||
.prose ul { list-style-type: disc; }
|
||||
.prose ol { list-style-type: decimal; }
|
||||
.prose blockquote {
|
||||
border-left: 3px solid var(--color-teal-700);
|
||||
padding-left: 1em;
|
||||
color: var(--color-ice-100);
|
||||
font-style: italic;
|
||||
margin: 1.5em 0;
|
||||
}
|
||||
.prose code {
|
||||
font-family: var(--font-mono);
|
||||
font-size: 0.875em;
|
||||
background: var(--color-navy-800);
|
||||
padding: 0.15em 0.4em;
|
||||
border-radius: 0.25rem;
|
||||
color: var(--color-teal-300);
|
||||
}
|
||||
.prose pre {
|
||||
background: var(--color-navy-900);
|
||||
border: 1px solid var(--color-navy-800);
|
||||
border-radius: 0.75rem;
|
||||
padding: 1.25em;
|
||||
overflow-x: auto;
|
||||
margin: 1.5em 0;
|
||||
}
|
||||
.prose pre code {
|
||||
background: none;
|
||||
padding: 0;
|
||||
border-radius: 0;
|
||||
}
|
||||
.prose img {
|
||||
border-radius: 0.75rem;
|
||||
margin: 1.5em 0;
|
||||
}
|
||||
.prose hr {
|
||||
border-color: var(--color-navy-800);
|
||||
margin: 2em 0;
|
||||
}
|
||||
|
||||
/* Shipped feature info tooltip */
|
||||
.shipped-info-tip {
|
||||
display: none;
|
||||
position: absolute;
|
||||
right: 0;
|
||||
bottom: calc(100% + 8px);
|
||||
width: 280px;
|
||||
padding: 0.625rem 0.75rem;
|
||||
border-radius: 0.5rem;
|
||||
border: 1px solid var(--color-navy-800);
|
||||
background: var(--color-navy-900);
|
||||
color: var(--color-ice-300);
|
||||
font-family: var(--font-sans);
|
||||
font-size: 0.8125rem;
|
||||
font-weight: 400;
|
||||
line-height: 1.5;
|
||||
box-shadow: 0 8px 24px -4px rgba(0, 0, 0, 0.4);
|
||||
z-index: 20;
|
||||
}
|
||||
.shipped-info:hover .shipped-info-tip {
|
||||
display: block;
|
||||
}
|
||||
|
|
|
|||
15
apps/marketing/vercel.json
Normal file
|
|
@ -0,0 +1,15 @@
|
|||
{
|
||||
"$schema": "https://openapi.vercel.sh/vercel.json",
|
||||
"redirects": [
|
||||
{
|
||||
"source": "/discord",
|
||||
"destination": "https://discord.gg/KE6w49Vg",
|
||||
"statusCode": 302
|
||||
},
|
||||
{
|
||||
"source": "/docs",
|
||||
"destination": "https://docs.fabro.sh",
|
||||
"statusCode": 302
|
||||
}
|
||||
]
|
||||
}
|
||||
|
|
@ -41,9 +41,12 @@ fi
|
|||
sed -i '' "s/^version = \"$current_version\"/version = \"$new_version\"/" "$CARGO_TOML"
|
||||
echo "Updated $CARGO_TOML"
|
||||
|
||||
git add "$CARGO_TOML"
|
||||
cargo update --workspace
|
||||
echo "Updated Cargo.lock"
|
||||
|
||||
git add "$CARGO_TOML" Cargo.lock
|
||||
git commit -m "Bump version to $new_version"
|
||||
git tag "$tag"
|
||||
git tag -a "$tag" -m "$tag"
|
||||
git push origin main "$tag"
|
||||
|
||||
echo ""
|
||||
|
|
|
|||
3
bun.lock
|
|
@ -43,6 +43,7 @@
|
|||
"dependencies": {
|
||||
"@astrojs/react": "^4.2.1",
|
||||
"@tailwindcss/vite": "^4.2.1",
|
||||
"@viz-js/viz": "^3.25.0",
|
||||
"astro": "^5.9.3",
|
||||
"react": "^19.2.4",
|
||||
"react-dom": "^19.2.4",
|
||||
|
|
@ -1338,6 +1339,8 @@
|
|||
|
||||
"finalhandler/debug": ["debug@2.6.9", "", { "dependencies": { "ms": "2.0.0" } }, "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA=="],
|
||||
|
||||
"marketing/@viz-js/viz": ["@viz-js/viz@3.25.0", "", {}, "sha512-dM7zAYMdf7mcRz5Kdb+YJb6+qv5Rjk0rPZ18gROdpMrP/3S7RFOp8uxybeiz5RypHrE1zo1vccA8Twh4mIcLZw=="],
|
||||
|
||||
"morgan/debug": ["debug@2.6.9", "", { "dependencies": { "ms": "2.0.0" } }, "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA=="],
|
||||
|
||||
"morgan/on-finished": ["on-finished@2.3.0", "", { "dependencies": { "ee-first": "1.1.1" } }, "sha512-ikqdkGAAyf/X/gPhXGvfgAytDZtDbr+bkNUJ0N9h5MI/dmdgCs3l6hoHrcUv41sRKew3jIwrp4qQDXiK99Utww=="],
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
[api]
|
||||
base_url = "http://api:3000"
|
||||
|
||||
[feature_flags]
|
||||
[features]
|
||||
session_sandboxes = false
|
||||
|
||||
[checkpoint]
|
||||
|
|
|
|||
BIN
docs-internal/assets/brand/github-header-v2-mesh.png
Normal file
|
After Width: | Height: | Size: 82 KiB |
19
docs-internal/assets/brand/github-header-v2-mesh.svg
Normal file
|
After Width: | Height: | Size: 8.4 KiB |
|
Before Width: | Height: | Size: 7.8 KiB After Width: | Height: | Size: 7.8 KiB |
|
|
@ -1,5 +1,5 @@
|
|||
<?xml version='1.0' encoding='utf-8'?>
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="882" height="211" viewBox="0 0 1455 348">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="909" height="211" viewBox="0 0 1500 348">
|
||||
<defs>
|
||||
<linearGradient id="topGradient" gradientUnits="userSpaceOnUse" x1="1200" y1="0" x2="0" y2="400">
|
||||
<stop offset="0" stop-color="#c4dede" />
|
||||
|
Before Width: | Height: | Size: 8.3 KiB After Width: | Height: | Size: 8.3 KiB |
|
Before Width: | Height: | Size: 608 B After Width: | Height: | Size: 608 B |
|
Before Width: | Height: | Size: 1.1 KiB After Width: | Height: | Size: 1.1 KiB |
|
Before Width: | Height: | Size: 32 KiB After Width: | Height: | Size: 32 KiB |
|
Before Width: | Height: | Size: 3.1 KiB After Width: | Height: | Size: 3.1 KiB |
|
Before Width: | Height: | Size: 32 KiB After Width: | Height: | Size: 32 KiB |
|
Before Width: | Height: | Size: 27 KiB After Width: | Height: | Size: 27 KiB |
|
Before Width: | Height: | Size: 4.4 KiB After Width: | Height: | Size: 4.4 KiB |
|
Before Width: | Height: | Size: 7.5 KiB After Width: | Height: | Size: 7.5 KiB |
|
Before Width: | Height: | Size: 2.6 KiB After Width: | Height: | Size: 2.6 KiB |
|
Before Width: | Height: | Size: 7.9 KiB After Width: | Height: | Size: 7.9 KiB |
|
Before Width: | Height: | Size: 2.6 KiB After Width: | Height: | Size: 2.6 KiB |
|
Before Width: | Height: | Size: 8.3 KiB After Width: | Height: | Size: 8.3 KiB |
|
Before Width: | Height: | Size: 2.6 KiB After Width: | Height: | Size: 2.6 KiB |
|
Before Width: | Height: | Size: 12 KiB After Width: | Height: | Size: 12 KiB |
|
Before Width: | Height: | Size: 3.8 KiB After Width: | Height: | Size: 3.8 KiB |
|
Before Width: | Height: | Size: 23 KiB After Width: | Height: | Size: 23 KiB |
|
Before Width: | Height: | Size: 5 KiB After Width: | Height: | Size: 5 KiB |
|
Before Width: | Height: | Size: 40 KiB After Width: | Height: | Size: 40 KiB |
|
Before Width: | Height: | Size: 6.7 KiB After Width: | Height: | Size: 6.7 KiB |