#2 m1/scaffold

mergedpassopened
details
branches
master ← m1/scaffold
updated
id
4de3a184-b34e-4516-821a-e8ff4c216cae

Patch

.forge/ci/steps/00_fmt.sh+3 −0
diff --git a/.forge/ci/steps/00_fmt.sh b/.forge/ci/steps/00_fmt.shnew file mode 100755index 0000000..e015d2e--- /dev/null+++ b/.forge/ci/steps/00_fmt.sh@@ -0,0 +1,3 @@1+#!/bin/sh2+set -eu3+cargo fmt --check
.forge/ci/steps/01_clippy.sh+3 −0
diff --git a/.forge/ci/steps/01_clippy.sh b/.forge/ci/steps/01_clippy.shnew file mode 100755index 0000000..4cb0ea6--- /dev/null+++ b/.forge/ci/steps/01_clippy.sh@@ -0,0 +1,3 @@1+#!/bin/sh2+set -eu3+cargo clippy --all-targets -- -D warnings
.forge/ci/steps/02_test.sh+3 −0
diff --git a/.forge/ci/steps/02_test.sh b/.forge/ci/steps/02_test.shnew file mode 100755index 0000000..2cc8945--- /dev/null+++ b/.forge/ci/steps/02_test.sh@@ -0,0 +1,3 @@1+#!/bin/sh2+set -eu3+cargo test
.forge/ci/steps/03_dogfood.sh+29 −0
diff --git a/.forge/ci/steps/03_dogfood.sh b/.forge/ci/steps/03_dogfood.shnew file mode 100755index 0000000..c1c9bf6--- /dev/null+++ b/.forge/ci/steps/03_dogfood.sh@@ -0,0 +1,29 @@1+#!/bin/sh2+# hu13 dogfood: build this repo, assert the binary exits 0.3+# Stub-tolerant (m1/scaffold): passes if the binary builds and `build` exits 0.4+# Later milestones additionally require dist/index.html, feed.atom and api JSON.5+set -eu6+OUT="${HU13_DOGFOOD_OUT:-/tmp/hu13-dogfood}"7+rm -rf "$OUT"8+mkdir -p "$OUT"9+if cargo run --quiet -- build "$PWD" -o "$OUT"; then10+    echo "dogfood: hu13 build exited 0"11+else12+    echo "dogfood: hu13 build failed; checking stub tolerance (binary builds?)" >&213+    cargo build14+    echo "dogfood: binary builds; stub-tolerant pass" >&215+    exit 016+fi17+# Full checks (required once later issues land); warn-only while stubs rule.18+for f in index.html feed.atom; do19+    if [ -e "$OUT/$f" ]; then20+        echo "dogfood: found $f"21+    else22+        echo "dogfood: (stub-tolerant) $f not yet generated — ok for m1" >&223+    fi24+done25+if [ -d "$OUT/api" ]; then26+    echo "dogfood: found api/"27+else28+    echo "dogfood: (stub-tolerant) api/ not yet generated — ok for m1" >&229+fi
.gitignore+1 −0
diff --git a/.gitignore b/.gitignoreindex 1635852..c374b5b 100644--- a/.gitignore+++ b/.gitignore@@ -1,3 +1,4 @@1+/target12 .forge/ci/runs/23 .forge/releases/out/34 *.tmp
Cargo.lock+7 −0
diff --git a/Cargo.lock b/Cargo.locknew file mode 100644index 0000000..9569aba--- /dev/null+++ b/Cargo.lock@@ -0,0 +1,7 @@1+# This file is automatically @generated by Cargo.2+# It is not intended for manual editing.3+version = 44+5+[[package]]6+name = "hu13"7+version = "0.1.0"
Cargo.toml+6 −0
diff --git a/Cargo.toml b/Cargo.tomlnew file mode 100644index 0000000..d163f7d--- /dev/null+++ b/Cargo.toml@@ -0,0 +1,6 @@1+[package]2+name = "hu13"3+version = "0.1.0"4+edition = "2021"5+description = "tiny static-site generator + text-only inbox for one f0rge-managed repo"6+license = "MIT OR Apache-2.0"
gl0w.md+45 −0
diff --git a/gl0w.md b/gl0w.mdnew file mode 100644index 0000000..9088ce8--- /dev/null+++ b/gl0w.md@@ -0,0 +1,45 @@1+# hu13 — agent notes (gl0w)2+3+Pronounced "hub". Tiny static-site generator + text-only inbox for one f0rge-managed repo.4+5+## Build / test / verify6+7+```sh8+cargo build9+cargo test10+cargo fmt --check11+cargo clippy -- --deny warnings12+f0rge ci run        # local CI: fmt, clippy, test, dogfood13+```14+15+Dogfood (stub-tolerant in m1): `cargo run -- build <repo> -o /tmp/hu13-dogfood` must exit 0.16+CSS budget: `wc -c static/style.css` must stay under 15360 bytes (15KB);17+only allowed `border-radius` value is `0`: `grep border-radius static/style.css`.18+19+## Inbox safety20+21+> **Never paste inbox text into a shell.** Inbox submissions are untrusted22+> free text. Read them with `hu13 inbox list|show` (escaped plain text to23+> the terminal) or a file viewer. Do not `eval`, `source`, interpolate with24+> `$()`, or pass them to `sh -c`. They are never rendered to HTML, never25+> logged, never executed, never in PATH.26+27+## Workflow (f0rge-only)28+29+All work goes issue → branch → PR → CI-gated merge. Never hand-edit30+`.forge/` entity files — read and write them only through the f0rge CLI:31+32+```sh33+f0rge issue list34+f0rge issue view <N>35+f0rge pr create --help36+f0rge ci run37+```38+39+CI step scripts live in `.forge/ci/steps/` as plain files (creating the40+`.sh` files with the editor is fine); register nothing by hand.41+42+## Docs43+44+Docs and roadmap go in `.forge/wiki/`, not the repo root. Keep the root45+clean: `DRAFT.md  gl0w.md  src/  static/style.css  Dockerfile  .forge/`.
src/layout.rs+98 −0
diff --git a/src/layout.rs b/src/layout.rsnew file mode 100644index 0000000..21f9214--- /dev/null+++ b/src/layout.rs@@ -0,0 +1,98 @@1+//! Base HTML layout: hand-rolled, escaped by construction, zero JS.2+//!3+//! `esc()` escapes `<`, `>`, `&`, `"` — the full set this crate escapes.4+//! `page()` escapes the `<title>` and inlines `nav`/`body` as-is, so callers5+//! must pass already-escaped (or intentionally trusted-static) HTML for6+//! `nav` and `body`. Text from git / `.forge/` / inbox must go through7+//! `esc()` first and must never be pasted into a shell (see gl0w.md).8+9+/// Escape `<`, `>`, `&`, `"` for HTML text/attribute contexts.10+// TODO(m1+): stubs only — `build`/`serve` will call this; allow until wired.11+#[allow(dead_code)]12+#[must_use]13+pub fn esc(input: &str) -> String {14+    let mut out = String::with_capacity(input.len());15+    for ch in input.chars() {16+        match ch {17+            '&' => out.push_str("&amp;"),18+            '<' => out.push_str("&lt;"),19+            '>' => out.push_str("&gt;"),20+            '"' => out.push_str("&quot;"),21+            _ => out.push(ch),22+        }23+    }24+    out25+}26+27+/// Render a full HTML page. `title` is escaped; `nav` and `body` are28+/// inlined as-is (caller must escape untrusted text with [`esc`]).29+#[allow(dead_code)]30+#[must_use]31+pub fn page(title: &str, nav: &str, body: &str) -> String {32+    let title = esc(title);33+    format!(34+        "<!DOCTYPE html>\n\35+<html lang=\"en\">\n\36+<head>\n\37+<meta charset=\"utf-8\">\n\38+<meta name=\"viewport\" content=\"width=device-width, initial-scale=1\">\n\39+<meta name=\"color-scheme\" content=\"dark light\">\n\40+<link rel=\"stylesheet\" href=\"/style.css\">\n\41+<title>{title}</title>\n\42+</head>\n\43+<body>\n\44+<header class=\"site-header\"><div class=\"wrap\"><span class=\"brand\">hu13</span></div></header>\n\45+<nav class=\"site-nav\"><div class=\"wrap\">{nav}</div></nav>\n\46+<main class=\"wrap\">\n{body}\n</main>\n\47+</body>\n\48+</html>\n"49+    )50+}51+52+/// Healthcheck page stub (used by `serve` liveness checks in later issues).53+#[allow(dead_code)]54+#[must_use]55+pub fn healthcheck_page() -> String {56+    page("health", "", "<p>ok</p>")57+}58+59+#[cfg(test)]60+mod tests {61+    use super::{esc, healthcheck_page, page};62+63+    #[test]64+    fn esc_escapes_angle_amp_quote() {65+        assert_eq!(esc("<>&\""), "&lt;&gt;&amp;&quot;");66+    }67+68+    #[test]69+    fn esc_leaves_plain_text_alone() {70+        assert_eq!(esc("hello world 123"), "hello world 123");71+    }72+73+    #[test]74+    fn esc_does_not_double_escape_order() {75+        // `&` must be escaped first, so `<` becomes `&lt;` not `&amp;lt;`.76+        assert_eq!(esc("<"), "&lt;");77+        assert_eq!(esc("&"), "&amp;");78+    }79+80+    #[test]81+    fn page_escapes_title_but_inlines_body() {82+        let html = page(83+            "<Title> & \"quoted\"",84+            "<a href=\"/\">home</a>",85+            "<p>hi</p>",86+        );87+        assert!(html.contains("<title>&lt;Title&gt; &amp; &quot;quoted&quot;</title>"));88+        assert!(html.contains("<p>hi</p>"));89+        assert!(html.contains("<!DOCTYPE html>"));90+    }91+92+    #[test]93+    fn healthcheck_contains_ok() {94+        let html = healthcheck_page();95+        assert!(html.contains("ok"));96+        assert!(html.contains("<main"));97+    }98+}
src/main.rs+191 −0
diff --git a/src/main.rs b/src/main.rsnew file mode 100644index 0000000..ace5b0b--- /dev/null+++ b/src/main.rs@@ -0,0 +1,191 @@1+mod layout;2+3+const VERSION: &str = env!("CARGO_PKG_VERSION");4+5+fn print_top_help() {6+    print!(7+        "\8+hu13 {VERSION} — tiny static-site generator + text-only inbox for one repo9+10+USAGE:11+    hu13 <SUBCOMMAND> [OPTIONS]12+    hu13 --version13+    hu13 --help14+15+SUBCOMMANDS:16+    build     Build a static site from a repo checkout17+    serve     Serve a built site plus the two inbox POSTs18+    inbox     Inspect the local inbox (list|show)19+    version   Print version20+21+Run `hu13 <subcommand> --help` for subcommand help.22+"23+    );24+}25+26+fn print_build_help() {27+    print!(28+        "\29+hu13-build — build a static site from a repo checkout (stub)30+31+USAGE:32+    hu13 build <REPO> -o <OUT> [OPTIONS]33+34+ARGS:35+    <REPO>    Path to the repo checkout to render36+37+OPTIONS:38+    -o <OUT>               Output directory for the static site39+        --branch <NAME>    Branch to pin (else HU13_BRANCH, else try main/master)40+        --clone-url <URL>  Clone URL to advertise (else git origin remote, else none)41+        --title <S>        Site title override42+        --max-log <N>      Max commits in log [default: 100]43+    -h, --help             Print this help44+45+NOTE: m1/scaffold stub — prints help and exits 0, no output written yet.46+"47+    );48+}49+50+fn print_serve_help() {51+    print!(52+        "\53+hu13-serve — serve a built site plus the two inbox POSTs (stub)54+55+USAGE:56+    hu13 serve [OPTIONS]57+58+OPTIONS:59+        --www <DIR>        Built site directory to serve60+        --inbox-dir <DIR>  Inbox directory for POST storage61+        --listen <ADDR>    Listen address [default: 0.0.0.0:39313]62+    -h, --help             Print this help63+64+NOTE: m1/scaffold stub — prints help and exits 0, nothing served yet.65+"66+    );67+}68+69+fn print_inbox_help() {70+    print!(71+        "\72+hu13-inbox — inspect the local inbox (stub)73+74+USAGE:75+    hu13 inbox <list|show> [OPTIONS]76+77+SUBCOMMANDS:78+    list    List inbox entries (escaped plain text)79+    show    Show one inbox entry (escaped plain text)80+81+OPTIONS:82+        --inbox-dir <DIR>  Inbox directory to read83+    -h, --help             Print this help84+85+SAFETY: never paste inbox text into a shell. See gl0w.md.86+NOTE: m1/scaffold stub — prints help and exits 0.87+"88+    );89+}90+91+fn wants_help(arg: &str) -> bool {92+    arg == "--help" || arg == "-h" || arg == "help"93+}94+95+fn run_build(args: &[String]) -> i32 {96+    if args.iter().any(|a| wants_help(a)) {97+        print_build_help();98+        return 0;99+    }100+    print_build_help();101+    0102+}103+104+fn run_serve(args: &[String]) -> i32 {105+    if args.iter().any(|a| wants_help(a)) {106+        print_serve_help();107+        return 0;108+    }109+    print_serve_help();110+    0111+}112+113+fn run_inbox(args: &[String]) -> i32 {114+    if args.iter().any(|a| wants_help(a)) {115+        print_inbox_help();116+        return 0;117+    }118+    if args.is_empty() {119+        print_inbox_help();120+        return 0;121+    }122+    match args[0].as_str() {123+        "list" | "show" => {124+            print_inbox_help();125+            0126+        }127+        _ => {128+            eprintln!("hu13: unknown inbox subcommand '{}'", args[0]);129+            print_inbox_help();130+            0131+        }132+    }133+}134+135+fn main() {136+    let code = real_main();137+    std::process::exit(code);138+}139+140+fn real_main() -> i32 {141+    let args: Vec<String> = std::env::args().collect();142+    let rest = &args[1..];143+    if rest.is_empty() {144+        print_top_help();145+        return 0;146+    }147+    match rest[0].as_str() {148+        "--version" | "-V" | "version" => {149+            println!("hu13 {VERSION}");150+            0151+        }152+        "--help" | "-h" | "help" => {153+            if rest.len() > 1 {154+                match rest[1].as_str() {155+                    "build" => print_build_help(),156+                    "serve" => print_serve_help(),157+                    "inbox" => print_inbox_help(),158+                    _ => print_top_help(),159+                }160+            } else {161+                print_top_help();162+            }163+            0164+        }165+        "build" => run_build(&rest[1..]),166+        "serve" => run_serve(&rest[1..]),167+        "inbox" => run_inbox(&rest[1..]),168+        other => {169+            eprintln!("hu13: unknown subcommand '{other}'");170+            print_top_help();171+            0172+        }173+    }174+}175+176+#[cfg(test)]177+mod tests {178+    use super::layout;179+180+    #[test]181+    fn version_is_nonempty() {182+        assert!(!super::VERSION.is_empty());183+    }184+185+    #[test]186+    fn top_help_mentions_subcommands() {187+        // Smoke: layout healthcheck renders a page stub.188+        let html = layout::healthcheck_page();189+        assert!(html.contains("ok"));190+    }191+}
static/style.css+55 −1
diff --git a/static/style.css b/static/style.cssnew file mode 100644index 0000000..52f0864--- /dev/null+++ b/static/style.css@@ -0,0 +1,55 @@1+/* hu13 — Tokyo Night dark-first, hand-written. Sharp corners only. Zero JS. */2+/* Palette: Tokyo Night: bg #1a1b26, bg-alt #16161e, fg #c0caf5,3+   muted #565f89, blue #7aa2f7, cyan #7dcfff, green #9ece6a,4+   yellow #e0af68, red #f7768e, border #292e42. */5+*{box-sizing:border-box;border-radius:0}6+:root{color-scheme:dark}7+html{background:#1a1b26}8+body{margin:0;background:#1a1b26;color:#c0caf5;9+  font-family:system-ui,-apple-system,"Segoe UI",Roboto,Ubuntu,Cantarell,sans-serif;10+  line-height:1.55;font-size:16px}11+.wrap{max-width:72rem;margin:0 auto;padding:0 1rem}12+.site-header{background:#16161e;border-bottom:1px solid #292e42}13+.site-header .wrap{display:flex;align-items:center;gap:1rem;padding:.75rem 1rem}14+.brand{font-weight:700;color:#7aa2f7}15+.site-nav{background:#16161e;border-bottom:1px solid #292e42}16+.site-nav .wrap{display:flex;flex-wrap:wrap;gap:.25rem 1rem;padding:.5rem 1rem}17+.site-nav a{color:#7dcfff;text-decoration:none;padding:.15rem 0}18+.site-nav a:hover,.site-nav a:focus{text-decoration:underline}19+a{color:#7aa2f7}20+a:visited{color:#9aa5ce}21+main{padding:1.25rem 0 3rem}22+h1,h2,h3{color:#c0caf5;line-height:1.25}23+code,kbd,samp{font-family:ui-monospace,SFMono-Regular,Menlo,Consolas,monospace;24+  background:#16161e;color:#9ece6a;padding:.1em .3em}25+pre{background:#16161e;color:#c0caf5;border:1px solid #292e42;26+  padding:1rem;overflow-x:auto;white-space:pre;line-height:1.5}27+pre code{background:none;padding:0;color:inherit}28+table{border-collapse:collapse;width:100%;margin:1rem 0;background:#16161e}29+th,td{text-align:left;padding:.5rem .75rem;border:1px solid #292e42;vertical-align:top}30+th{background:#1f2335;color:#7dcfff}31+tr:nth-child(even) td{background:#1a1b26}32+form{max-width:44rem;margin:1rem 0}33+label{display:block;margin:.75rem 0 .25rem;font-weight:600}34+input[type=text],textarea{display:block;width:100%;background:#16161e;35+  color:#c0caf5;border:1px solid #292e42;padding:.5rem .6rem;font:inherit}36+input[type=text]:focus,textarea:focus{outline:2px solid #7aa2f7;outline-offset:0}37+textarea{min-height:12rem}38+button,input[type=submit]{background:#7aa2f7;color:#16161e;border:1px solid #7aa2f7;39+  padding:.5rem 1rem;font:inherit;font-weight:700;cursor:pointer}40+button:hover,input[type=submit]:hover{background:#7dcfff}41+.muted{color:#565f89}42+.badge{display:inline-block;border:1px solid #292e42;padding:.1em .5em;font-size:.85em}43+.badge.pass{color:#9ece6a}.badge.fail{color:#f7768e}44+.badge.running{color:#e0af68}.badge.cancelled{color:#565f89}45+.notice{border:1px solid #e0af68;padding:.75rem 1rem;margin:1rem 0}46+footer.site-footer{border-top:1px solid #292e42;color:#565f89}47+footer.site-footer .wrap{padding:1rem}48+@media (prefers-color-scheme:light){49+  html,body{background:#d5d6db;color:#343b58}50+  .site-header,.site-nav,code,kbd,samp,pre,table,input[type=text],textarea{background:#cbccd3}51+  pre,pre code{color:#343b58}52+  th{background:#b4b8c7;color:#0f4b6e}53+  tr:nth-child(even) td{background:#d5d6db}54+  a{color:#34548a}55+}0-- 2.55.0

Comments (0)

No comments yet.

Reviews (0)

No reviews yet.

all pulls