Eric Bower
·
2026-05-26
1<!DOCTYPE html>
2<html lang="en">
3<head>
4<meta charset="UTF-8">
5<meta name="viewport" content="width=device-width, initial-scale=1.0, viewport-fit=cover">
6<meta name="apple-mobile-web-app-capable" content="yes">
7<meta name="apple-mobile-web-app-status-bar-style" content="black">
8<title>pici – Your Build is a Terminal.</title>
9<style>
10/* ============================================================
11 pici landing page – terminal aesthetic
12 Palette: Catppuccin Mocha + terminal green accents
13 ============================================================ */
14
15*, *::before, *::after { box-sizing: border-box; margin: 0; padding: 0; }
16
17:root {
18 --bg: #1e1e2e;
19 --bg-deep: #11111b;
20 --surface: #313244;
21 --text: #cdd6f4;
22 --text-dim: #a6adc8;
23 --blue: #89b4fa;
24 --green: #a6e3a1;
25 --red: #f38ba8;
26 --yellow: #f9e2af;
27 --mauve: #f5c2e7;
28 --mono: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", monospace;
29}
30
31html { scroll-behavior: smooth; }
32
33body {
34 font-family: var(--mono);
35 background: var(--bg);
36 color: var(--text);
37 font-size: 15px;
38 line-height: 1.6;
39 overflow-x: hidden;
40}
41
42/* Scanline overlay */
43body::after {
44 content: "";
45 position: fixed;
46 inset: 0;
47 pointer-events: none;
48 background: repeating-linear-gradient(
49 0deg,
50 transparent,
51 transparent 2px,
52 rgba(0, 0, 0, 0.03) 2px,
53 rgba(0, 0, 0, 0.03) 4px
54 );
55 z-index: 9999;
56}
57
58a { color: var(--blue); text-decoration: none; }
59a:hover { text-decoration: underline; }
60
61/* ============================================================
62 Layout
63 ============================================================ */
64
65.container {
66 max-width: 960px;
67 margin: 0 auto;
68 padding: 0 1.5rem;
69}
70
71section {
72 padding: 5rem 0;
73 border-bottom: 1px solid var(--surface);
74}
75
76/* ============================================================
77 Navigation
78 ============================================================ */
79
80nav {
81 z-index: 1;
82 position: sticky;
83 top: 0;
84 background: var(--bg-deep);
85 border-bottom: 1px solid var(--surface);
86 padding: 0.75rem 0;
87}
88
89nav .container {
90 display: flex;
91 align-items: center;
92 justify-content: space-between;
93 gap: 1rem;
94}
95
96nav .logo {
97 font-size: 1.1rem;
98 font-weight: 700;
99 color: var(--green);
100}
101
102nav .logo span { color: var(--text-dim); font-weight: 400; }
103
104@media (max-width: 700px) {
105 nav .logo span { display: none; }
106}
107
108nav ul {
109 list-style: none;
110 display: flex;
111 gap: 1.5rem;
112 padding-left: 1rem;
113 border-left: 1px solid var(--surface);
114}
115
116nav ul a {
117 color: var(--text-dim);
118 font-size: 0.7rem;
119 text-transform: uppercase;
120 letter-spacing: 0.05em;
121}
122
123nav ul a:hover { color: var(--text); text-decoration: none; }
124
125/* ============================================================
126 Hero
127 ============================================================ */
128
129.hero {
130 padding: 7rem 0 5rem;
131 text-align: center;
132 border-bottom: 1px solid var(--surface);
133}
134
135.hero h1 {
136 font-size: clamp(1.8rem, 5vw, 2.8rem);
137 font-weight: 700;
138 line-height: 1.2;
139 margin-bottom: 1rem;
140}
141
142.hero h1 .accent { color: var(--green); }
143
144.hero .subtitle {
145 font-size: 1.05rem;
146 color: var(--text-dim);
147 max-width: 600px;
148 margin: 0 auto 2rem;
149}
150
151.hero .cta-row {
152 display: flex;
153 gap: 1rem;
154 justify-content: center;
155 flex-wrap: wrap;
156}
157
158.btn {
159 display: inline-block;
160 padding: 0.7rem 1.5rem;
161 border-radius: 4px;
162 font-family: var(--mono);
163 font-size: 0.9rem;
164 font-weight: 600;
165 cursor: pointer;
166 border: 1px solid transparent;
167 transition: all 0.15s ease;
168}
169
170.btn-primary {
171 background: var(--green);
172 color: var(--bg-deep);
173}
174
175.btn-primary:hover {
176 background: #b8f0b3;
177 text-decoration: none;
178}
179
180.btn-secondary {
181 background: transparent;
182 color: var(--text);
183 border-color: var(--surface);
184}
185
186.btn-secondary:hover {
187 border-color: var(--text-dim);
188 text-decoration: none;
189}
190
191/* ============================================================
192 Terminal window component
193 ============================================================ */
194
195.terminal {
196 background: var(--bg-deep);
197 border: 1px solid var(--surface);
198 border-radius: 8px;
199 overflow: hidden;
200 margin: 2rem 0;
201}
202
203.terminal-bar {
204 display: flex;
205 align-items: center;
206 gap: 0.5rem;
207 padding: 0.6rem 1rem;
208 background: var(--surface);
209 font-size: 0.75rem;
210 color: var(--text-dim);
211}
212
213.terminal-bar .dot {
214 width: 10px;
215 height: 10px;
216 border-radius: 50%;
217 display: inline-block;
218}
219
220.terminal-bar .dot.red { background: var(--red); }
221.terminal-bar .dot.yellow { background: var(--yellow); }
222.terminal-bar .dot.green { background: var(--green); }
223
224.terminal-body {
225 padding: 1rem 1.25rem;
226 font-size: 0.85rem;
227 line-height: 1.7;
228 overflow-x: auto;
229}
230
231.terminal-body .prompt { color: var(--green); }
232.terminal-body .cmd { color: var(--text); }
233.terminal-body .output { color: var(--text-dim); }
234.terminal-body .error { color: var(--red); }
235.terminal-body .info { color: var(--blue); }
236.terminal-body .comment { color: #6c7086; }
237.terminal-body .cursor {
238 display: inline-block;
239 width: 8px;
240 height: 1em;
241 background: var(--green);
242 vertical-align: text-bottom;
243 animation: blink 1s step-end infinite;
244}
245
246@keyframes blink {
247 50% { opacity: 0; }
248}
249
250/* ============================================================
251 Section headers
252 ============================================================ */
253
254.section-label {
255 font-size: 0.75rem;
256 text-transform: uppercase;
257 letter-spacing: 0.1em;
258 color: var(--mauve);
259 margin-bottom: 0.5rem;
260}
261
262h2 {
263 font-size: 1.5rem;
264 font-weight: 700;
265 margin-bottom: 1rem;
266 color: var(--text);
267}
268
269h2 .anchor {
270 color: var(--text-dim);
271 opacity: 0;
272 transition: opacity 0.15s ease;
273 font-weight: 400;
274 font-size: 0.85em;
275 text-decoration: none;
276}
277
278h2:hover .anchor {
279 opacity: 1;
280}
281
282h2 .anchor:hover {
283 color: var(--green);
284 text-decoration: none;
285}
286
287h3 {
288 font-size: 1.1rem;
289 font-weight: 600;
290 margin-bottom: 0.5rem;
291 color: var(--blue);
292}
293
294p { margin-bottom: 1rem; }
295
296/* ============================================================
297 Two-column layout
298 ============================================================ */
299
300.two-col {
301 display: grid;
302 grid-template-columns: 1fr 1fr;
303 gap: 3rem;
304 align-items: start;
305}
306
307@media (max-width: 700px) {
308 .two-col { grid-template-columns: 1fr; }
309}
310
311/* ============================================================
312 Feature grid
313 ============================================================ */
314
315.feature-grid {
316 display: grid;
317 grid-template-columns: repeat(auto-fit, minmax(250px, 1fr));
318 gap: 1.5rem;
319 margin-top: 2rem;
320}
321
322.feature-card {
323 background: var(--bg-deep);
324 border: 1px solid var(--surface);
325 border-radius: 8px;
326 padding: 1.5rem;
327}
328
329.feature-card h3 { margin-top: 0.5rem; }
330.feature-card .icon { font-size: 1.5rem; }
331
332/* ============================================================
333 Image placeholder
334 ============================================================ */
335
336.img-placeholder {
337 background: var(--bg-deep);
338 border: 2px dashed var(--surface);
339 border-radius: 8px;
340 display: flex;
341 flex-direction: column;
342 align-items: center;
343 justify-content: center;
344 min-height: 250px;
345 color: var(--text-dim);
346 font-size: 0.8rem;
347 text-align: center;
348 padding: 2rem;
349}
350
351.img-placeholder .icon { font-size: 2rem; margin-bottom: 0.5rem; }
352.img-placeholder .label { font-size: 0.85rem; color: var(--mauve); }
353.img-placeholder .dims { font-size: 0.7rem; color: #6c7086; margin-top: 0.25rem; }
354
355/* ============================================================
356 Code comparison (old vs new)
357 ============================================================ */
358
359.code-compare {
360 display: grid;
361 grid-template-columns: 1fr 1fr;
362 gap: 1.5rem;
363}
364
365@media (max-width: 700px) {
366 .code-compare { grid-template-columns: 1fr; }
367}
368
369.code-compare .label {
370 font-size: 0.7rem;
371 text-transform: uppercase;
372 letter-spacing: 0.1em;
373 color: var(--text-dim);
374 margin-bottom: 0.5rem;
375}
376
377.code-compare .label.bad { color: var(--red); }
378.code-compare .label.good { color: var(--green); }
379
380/* ============================================================
381 CTA / signup section
382 ============================================================ */
383
384.cta-section {
385 text-align: center;
386 padding: 5rem 0;
387 border-bottom: none;
388}
389
390.cta-section h2 {
391 font-size: 1.8rem;
392 margin-bottom: 0.5rem;
393 display: block;
394}
395
396.cta-section .subtitle {
397 color: var(--text-dim);
398 margin-bottom: 2rem;
399}
400
401.signup-form {
402 display: flex;
403 gap: 0.5rem;
404 justify-content: center;
405 flex-wrap: wrap;
406 max-width: 500px;
407 margin: 0 auto;
408}
409
410.signup-form input[type="email"] {
411 flex: 1;
412 min-width: 200px;
413 padding: 0.7rem 1rem;
414 border-radius: 4px;
415 border: 1px solid var(--surface);
416 background: var(--bg-deep);
417 color: var(--text);
418 font-family: var(--mono);
419 font-size: 0.9rem;
420 outline: none;
421}
422
423.signup-form input[type="email"]:focus {
424 border-color: var(--green);
425}
426
427.signup-form input[type="email"]::placeholder {
428 color: #6c7086;
429}
430
431.signup-form .btn {
432 white-space: nowrap;
433}
434
435.signup-note {
436 font-size: 0.75rem;
437 color: #6c7086;
438 margin-top: 1rem;
439}
440
441/* ============================================================
442 Footer
443 ============================================================ */
444
445footer {
446 padding: 2rem 0;
447 border-top: 1px solid var(--surface);
448 text-align: center;
449 font-size: 0.8rem;
450 color: var(--text-dim);
451}
452
453footer a { color: var(--blue); }
454
455/* ============================================================
456 Inline code
457 ============================================================ */
458
459code {
460 background: var(--surface);
461 color: var(--mauve);
462 padding: 0.15em 0.4em;
463 border-radius: 3px;
464 font-size: 0.85em;
465 font-family: var(--mono);
466}
467
468/* ============================================================
469 Highlighted list
470 ============================================================ */
471
472.check-list {
473 list-style: none;
474 padding: 0;
475}
476
477.check-list li {
478 padding: 0.3rem 0;
479 padding-left: 1.5rem;
480 position: relative;
481}
482
483.check-list li::before {
484 content: "✓";
485 position: absolute;
486 left: 0;
487 color: var(--green);
488 font-weight: 700;
489}
490
491/* ============================================================
492 Diagram / flow
493 ============================================================ */
494
495.flow {
496 display: flex;
497 align-items: center;
498 justify-content: center;
499 gap: 0.5rem;
500 flex-wrap: wrap;
501 margin: 2rem 0;
502 font-size: 0.85rem;
503}
504
505.flow .node {
506 background: var(--surface);
507 padding: 0.5rem 1rem;
508 border-radius: 4px;
509 font-size: 0.8rem;
510}
511
512.flow .arrow {
513 color: var(--text-dim);
514 font-size: 1.2rem;
515}
516
517/* ============================================================
518 Details / FAQ
519 ============================================================ */
520
521details {
522 background: var(--bg-deep);
523 border: 1px solid var(--surface);
524 border-radius: 8px;
525 margin-bottom: 1rem;
526}
527
528details[open] {
529 border-color: var(--green);
530}
531
532details summary {
533 padding: 1rem 1.25rem;
534 cursor: pointer;
535 font-weight: 600;
536 color: var(--text);
537 list-style: none;
538 display: flex;
539 align-items: center;
540}
541
542details summary::-webkit-details-marker {
543 display: none;
544}
545
546details summary::before {
547 content: "+";
548 margin-right: 0.75rem;
549 color: var(--green);
550 font-weight: 700;
551 font-size: 1.1rem;
552}
553
554details[open] summary::before {
555 content: "-";
556}
557
558details summary:hover {
559 color: var(--green);
560}
561
562details .faq-answer {
563 padding: 0 1.25rem 1.25rem;
564 color: var(--text-dim);
565 line-height: 1.7;
566}
567</style>
568</head>
569<body>
570
571<!-- ============================================================
572 NAV
573 ============================================================ -->
574<nav>
575 <div class="container">
576 <div class="logo">pici <span>: terminal-first CI</span></div>
577 <ul>
578 <li><a href="#how">How</a></li>
579 <li><a href="#infra">Infra</a></li>
580 <li><a href="#features">Features</a></li>
581 <li><a href="#faq">FAQ</a></li>
582 <li><a href="#beta">Beta</a></li>
583 </ul>
584 </div>
585</nav>
586
587<!-- ============================================================
588 HERO
589 ============================================================ -->
590<section class="hero">
591 <div class="container">
592 <h1>Your build is a terminal.</h1>
593 <p class="subtitle">
594 <code>pici</code> is a CI system where every build step runs in an attachable terminal session.
595 Your pipeline is a bash script. It runs the same locally and in CI.
596 </p>
597 <div class="cta-row">
598 <a href="#beta" class="btn btn-primary">Sign Up for Beta</a>
599 </div>
600 </div>
601</section>
602
603<!-- ============================================================
604 PROBLEM – The CI Pain
605 ============================================================ -->
606<section id="problem">
607 <div class="container">
608 <div class="two-col">
609 <div>
610 <p class="section-label">The Problem</p>
611 <h2>CI gives you logs. Not answers.</h2>
612 <p>
613 A build fails. You scroll through 2,000 lines of log output. You guess what went wrong.
614 You re-run the job and wait 10 minutes to find out you were wrong. Repeat.
615 </p>
616 <p>
617 Debugging CI shouldn't require you to guess what happened.
618 </p>
619 </div>
620 <div class="terminal">
621 <div class="terminal-bar">
622 <span class="dot red"></span>
623 <span class="dot yellow"></span>
624 <span class="dot green"></span>
625 <span style="margin-left: 0.5rem;">ci-myrepo-test: build log</span>
626 </div>
627 <div class="terminal-body">
628 <div class="output">Line 1847: ...</div>
629 <div class="output">Line 1848: ...</div>
630 <div class="output">Line 1849: ...</div>
631 <div class="error">Line 1850: FAIL: test_connect (db.TimeoutError)</div>
632 <div class="output">Line 1851: ...</div>
633 <div class="output">Line 1852: ...</div>
634 <div class="output">...</div>
635 <div class="comment"># scroll up... scroll up... scroll up...</div>
636 <div class="comment"># what was the state before this?</div>
637 <div class="comment"># re-run the job? wait 10 more minutes?</div>
638 </div>
639 </div>
640 </div>
641 </div>
642</section>
643
644<!-- ============================================================
645 SOLUTION – Attach
646 ============================================================ -->
647<section id="how">
648 <div class="container">
649 <p class="section-label">The Solution</p>
650 <h2>Attach to your failing build.</h2>
651 <div class="two-col">
652 <div>
653 <p>
654 Every build step in <code>pici</code> runs as a <strong>real terminal session</strong>: a PTY you can attach to.
655 No "enable debugging" or "re-run with SSH" buttons.
656 </p>
657 <p>
658 A test fails? Attach to the session, press <code>↑</code> + <code>Enter</code> to rerun the command,
659 inspect the environment, and fix the issue before trying again.
660 </p>
661 </div>
662 <div>
663 <div class="terminal">
664 <div class="terminal-bar">
665 <span class="dot red"></span>
666 <span class="dot yellow"></span>
667 <span class="dot green"></span>
668 <span style="margin-left: 0.5rem;">zmx attach ci.myrepo.test</span>
669 </div>
670 <div class="terminal-body">
671 <div><span class="prompt">$ </span><span class="cmd">zmx attach ci.myrepo.test</span></div>
672 <div><span class="info">→ attached to session ci.myrepo.test</span></div>
673 <div> </div>
674 <div class="output">$ go test ./...</div>
675 <div class="output">ok myapp/core 8.2s</div>
676 <div class="error">FAIL myapp/api 11.4s</div>
677 <div class="error">Error: dial tcp 10.0.1.5:5432: connection refused</div>
678 <div> </div>
679 <div><span class="comment"># ↑ you're in the live session</span></div>
680 <div><span class="comment"># ↑ press ↑ + Enter to rerun, or inspect freely</span></div>
681 <div> </div>
682 <div><span class="prompt">$ </span><span class="cmd">cat .env | grep DB</span></div>
683 <div class="output">DB_HOST=postgres.internal:5432</div>
684 <div> </div>
685 <div><span class="prompt">$ </span><span class="cmd">nslookup postgres.internal</span></div>
686 <div class="error">;; no servers could be reached</div>
687 <div> </div>
688 <div><span class="comment"># ↑ found it: DNS not resolving in this container</span></div>
689 </div>
690 </div>
691 </div>
692 </div>
693 </div>
694</section>
695
696<!-- ============================================================
697 JOB ENGINE – zmx as the parallel task runner
698 ============================================================ -->
699<section id="engine">
700 <div class="container">
701 <p class="section-label">Job Engine</p>
702 <h2>Parallel tasks, zero config, and zero install.</h2>
703 <div class="two-col">
704 <div class="terminal">
705 <div class="terminal-bar">
706 <span class="dot red"></span>
707 <span class="dot yellow"></span>
708 <span class="dot green"></span>
709 <span style="margin-left: 0.5rem;">pico.sh</span>
710 </div>
711 <div class="terminal-body">
712 <div><span class="comment">#!/usr/bin/env bash</span></div>
713 <div><span class="cmd">set -euo pipefail</span></div>
714 <div> </div>
715 <div><span class="comment"># This step runs until it's finished</span></div>
716 <div><span class="cmd">zmx run lint docker run golangci-lint run</span></div>
717 <div><span class="info"> → No issues found!</span></div>
718 <div> </div>
719 <div><span class="comment"># These run in parallel</span></div>
720 <div><span class="cmd">zmx run test -d go test ./...</span></div>
721 <div><span class="info"> → started session: ci.myrepo.test</span></div>
722 <div> </div>
723 <div><span class="cmd">zmx run build -d go build -o bin/pici .</span></div>
724 <div><span class="info"> → started session: ci.myrepo.build</span></div>
725 <div> </div>
726 <div><span class="comment"># Wait for all tasks to finish</span></div>
727 <div><span class="cmd">zmx wait "*"</span></div>
728 <div> </div>
729 <div><span class="comment"># Runs identically locally and in CI</span></div>
730 </div>
731 </div>
732 <div>
733 <p>
734 Your pipeline is <code>pico.sh</code>: a bash script that runs <strong>identically on your machine
735 and on the CI runner</strong>; no yaml required.
736 </p>
737 <p>
738 <a href="https://zmx.sh"><strong>zmx</strong></a> is the job engine. Each <code>zmx run</code> spawns
739 a parallel terminal session. <code>zmx wait "*"</code> blocks until they all finish.
740 Run tasks sequentially by just calling them one after another, or in parallel by using the
741 detach (<code>-d</code>) flag.
742 </p>
743 <ul class="check-list">
744 <li>Same <code>zmx run</code> commands locally and in CI</li>
745 <li>Sequential by default, parallel when you want it</li>
746 <li>Every step is an attachable terminal session</li>
747 <li>Bring your own isolation: use docker in your step commands</li>
748 <li>No YAML matrices, no <code>run: parallel</code> keywords</li>
749 </ul>
750 </div>
751 </div>
752 </div>
753</section>
754
755<!-- ============================================================
756 GIT IS OPTIONAL
757 ============================================================ -->
758<section id="no-git">
759 <div class="container">
760 <p class="section-label">Trigger Model</p>
761 <h2>Git is optional. rsync + ssh pubsub is the core.</h2>
762 <div class="two-col">
763 <div>
764 <p>
765 Most CI systems are glued to Git webhooks. <code>pici</code> isn't. The core loop is simple:
766 <strong>rsync a workspace</strong> and <strong>publish an event over SSH</strong>.
767 </p>
768 <p>
769 Git post-receive hooks are just one trigger. You can fire builds from anything:
770 a cron job, a file watcher, a webhook receiver on your own server, a button press.
771 </p>
772 </div>
773 <div>
774 <img src="flow.png" style="width: 100%; border-radius: 10px;" />
775 </div>
776 </div>
777 </div>
778</section>
779
780<!-- ============================================================
781 MANAGED SERVICE
782 ============================================================ -->
783<section id="cd">
784 <div class="container">
785 <p class="section-label">Managed Service</p>
786 <h2>Managed CI at <a href="https://ci.pico.sh">ci.pico.sh</a>.</h2>
787 <div class="two-col">
788 <div>
789 <p>
790 Trigger builds by publishing events over SSH. Your pipeline runs as real terminal
791 sessions you can attach to. No web console, no API keys, no OAuth.
792 </p>
793 <p>
794 The managed service runs on our own hardware, not a cloud provider. Self-hosted
795 is coming once the runner is ready.
796 </p>
797 <ul class="check-list">
798 <li>SSH keys are your auth</li>
799 <li>SSH pubsub is your event bus</li>
800 <li>Our hardware, not a hyperscaler</li>
801 <li>Self-hosted coming soon</li>
802 </ul>
803 </div>
804 <div class="terminal">
805 <div class="terminal-bar">
806 <span class="dot red"></span>
807 <span class="dot yellow"></span>
808 <span class="dot green"></span>
809 <span style="margin-left: 0.5rem;">ci.pico.sh</span>
810 </div>
811 <div class="terminal-body">
812 <div><span class="prompt">$ </span><span class="cmd">echo '{"type":"push"}' | ssh pipe.pico.sh pub build.event</span></div>
813 <div><span class="output">subscribe to this channel: ssh pipe.pico.sh sub build.event</span></div>
814 <div> </div>
815 <div><span class="info">🚀 starting job ci.myrepo.a3f2b8c1</span></div>
816 <div><span class="info">📦 syncing workspace</span></div>
817 <div><span class="info">✅ workspace ready</span></div>
818 <div><span class="info">🏃 launching sessions...</span></div>
819 <div><span class="info">✅ job launched</span></div>
820 </div>
821 </div>
822 </div>
823 </div>
824</section>
825
826<!-- ============================================================
827 PHILOSOPHY
828 ============================================================ -->
829<section id="philosophy">
830 <div class="container" style="max-width: 640px; margin: 0 auto; text-align: center;">
831 <p class="section-label">Who It's For</p>
832 <h2 style="justify-content: center;">CI that works for you, not against you.</h2>
833 <p>
834 <code>pici</code> is built for individuals and small teams who want to move fast. You write a bash
835 script, you push your code, your build runs, and if something breaks you jump into the
836 terminal and fix it. No YAML labyrinths, no approval workflows, no complex pipeline DAGs.
837 </p>
838 <p>
839 We're not building marketplace integrations, compliance reports, or workflow bureaucracy.
840 What you will find is the CI system you'd write for yourself.
841 </p>
842 </div>
843</section>
844
845<!-- ============================================================
846 POWERED BY cd.pico.sh
847 ============================================================ -->
848<section id="infra">
849 <div class="container">
850 <p class="section-label">Infrastructure</p>
851 <h2>Powered by <a href="https://cd.pico.sh">cd.pico.sh</a></h2>
852 <div class="two-col">
853 <div>
854 <p>
855 <strong>ci.pico.sh</strong> runs on <a href="https://cd.pico.sh"><strong>cd.pico.sh</strong></a>: our SSH VM service
856 on hardware we own.
857 </p>
858 <p>
859 Push a <code>docker-compose.yml</code> to an SSH endpoint and your containers are live.
860 Label a service and it gets a public HTTP URL. There's no provider lock-in because it's a tool
861 you likely already use for local development.
862 </p>
863 <p>
864 The same platform that runs your CI runs your apps.
865 </p>
866 <ul class="check-list">
867 <li>Docker Compose via <code>git push</code></li>
868 <li>Expose HTTP services with compose labels</li>
869 <li>Our hardware, not a hyperscaler</li>
870 <li>Limited hardware availability: when it's gone, it's gone</li>
871 </ul>
872 </div>
873 <div class="terminal">
874 <div class="terminal-bar">
875 <span class="dot red"></span>
876 <span class="dot yellow"></span>
877 <span class="dot green"></span>
878 <span style="margin-left: 0.5rem;">cd.pico.sh</span>
879 </div>
880 <div class="terminal-body">
881 <div><span class="comment">--- docker-compose.yml ---</span></div>
882 <div>services:</div>
883 <div> echo:</div>
884 <div> build: .</div>
885 <div> networks:</div>
886 <div> - default</div>
887 <div> - picd-ingress</div>
888 <div> labels:</div>
889 <div> traefik.enable: true</div>
890 <div> traefik.http.routers.echo.rule: Host(`echo-<user>.apps.pico.sh`)</div>
891 <div> </div>
892 <div>networks:</div>
893 <div> picd-ingress:</div>
894 <div> external: true</div>
895 <div> </div>
896 <div><span class="prompt">$ </span><span class="cmd">git remote add picd ssh://cd.pico.sh/user/project.git</span></div>
897 <div><span class="prompt">$ </span><span class="cmd">git push picd main</span></div>
898 <div> </div>
899 <div><span class="info">→ containers live</span></div>
900 <div><span class="info">→ echo-<user>.apps.pico.sh is live</span></div>
901 </div>
902 </div>
903 </div>
904 </div>
905</section>
906
907<!-- ============================================================
908 FEATURES
909 ============================================================ -->
910<section id="features">
911 <div class="container">
912 <p class="section-label">Features</p>
913 <h2>Built for terminals, humans, and automation</h2>
914 <div class="feature-grid">
915 <div class="feature-card">
916 <h3>Terminal Friendly</h3>
917 <p>
918 Because <code>pici</code> doesn't rely on git-ops and triggering a
919 build is done with rsync + ssh, we have the building blocks for you
920 or a code agent to trigger builds, monitor progress, and attach to
921 failures. Start as many jobs as you need and read results
922 synchronously.
923 </p>
924 </div>
925 <div class="feature-card">
926 <h3>SSH-First</h3>
927 <p>
928 SSH keys are your auth and we even support using SSH certificates for RBAC control.
929 Rsync to upload workspaces, build artifacts, and ssh pubsub as your event bus.
930 </p>
931 </div>
932 <div class="feature-card">
933 <h3>Static Site Artifacts</h3>
934 <p>
935 Build artifacts are plain HTML + CSS. There's no app server or build step.
936 Serve the directory with any static host: nginx, s3, <a href="https://pgs.sh">pgs.sh</a>,
937 <code>python -m http.server</code>.
938 It's a static site with zero runtime dependencies.
939 </p>
940 </div>
941 <div class="feature-card">
942 <h3>Build Attestation</h3>
943 <p>
944 Automatic provenance baked into every job: runner hostname, OS, arch, repo, branch,
945 commit, and workspace checksum.
946 </p>
947 </div>
948 </div>
949 </div>
950</section>
951
952<!-- ============================================================
953 COMPARISON
954 ============================================================ -->
955<section id="compare">
956 <div class="container">
957 <p class="section-label">Comparison</p>
958 <h2>The difference is the terminal.</h2>
959 <div class="code-compare">
960 <div>
961 <div class="label bad">Other CI</div>
962 <div class="terminal">
963 <div class="terminal-bar">
964 <span class="dot red"></span>
965 <span class="dot yellow"></span>
966 <span class="dot green"></span>
967 <span style="margin-left: 0.5rem;">build log</span>
968 </div>
969 <div class="terminal-body">
970 <div class="output">$ go test ./...</div>
971 <div class="output">ok myapp/db 12.4s</div>
972 <div class="error">FAIL myapp/api (14.2s)</div>
973 <div class="error">Error: connection refused</div>
974 <div class="output">...</div>
975 <div> </div>
976 <div class="comment"># ← that's all you get</div>
977 <div class="comment"># ← can't inspect the environment</div>
978 <div class="comment"># ← can't rerun just the failing step</div>
979 <div class="comment"># ← re-run entire pipeline? wait 10 min</div>
980 </div>
981 </div>
982 </div>
983 <div>
984 <div class="label good"><code>pici</code></div>
985 <div class="terminal">
986 <div class="terminal-bar">
987 <span class="dot red"></span>
988 <span class="dot yellow"></span>
989 <span class="dot green"></span>
990 <span style="margin-left: 0.5rem;">zmx attach ci.myrepo.api</span>
991 </div>
992 <div class="terminal-body">
993 <div class="output">$ go test ./...</div>
994 <div class="output">ok myapp/db 12.4s</div>
995 <div class="error">FAIL myapp/api (14.2s)</div>
996 <div class="error">Error: connection refused</div>
997 <div> </div>
998 <div class="comment"># you're in the session, inspect freely</div>
999 <div><span class="prompt">$ </span><span class="cmd">cat .env</span></div>
1000 <div class="output">DB_HOST=postgres.internal:5432</div>
1001 <div><span class="prompt">$ </span><span class="cmd">nc -zv postgres.internal 5432</span></div>
1002 <div class="error">Connection refused</div>
1003 <div><span class="comment"># ↑ found it: wrong hostname</span></div>
1004 <div><span class="comment"># ↑ press ↑ to rerun the test after fixing</span></div>
1005 </div>
1006 </div>
1007 </div>
1008 </div>
1009 </div>
1010</section>
1011
1012<!-- ============================================================
1013 FAQ
1014 ============================================================ -->
1015<section id="faq">
1016 <div class="container" style="max-width: 720px; margin: 0 auto;">
1017 <p class="section-label">FAQ</p>
1018 <h2>Frequently asked questions.</h2>
1019
1020 <details>
1021 <summary>Where's the source code?</summary>
1022 <div class="faq-answer">
1023 <p>
1024 It's coming. <code>pici</code> is still in early development and not yet ready for public
1025 review. We'll open-source everything when the API, runner, and tooling are stable enough
1026 to be useful. Sign up for the beta below to get notified.
1027 </p>
1028 </div>
1029 </details>
1030
1031 <details>
1032 <summary>Can I self-host?</summary>
1033 <div class="faq-answer">
1034 <p>
1035 Yes, self-hosting is a core goal. Once the source is released you'll be able to run
1036 <code>pici</code> on your own infra with your own isolation strategy: docker, namespaces,
1037 bare metal. You choose.
1038 </p>
1039 </div>
1040 </details>
1041
1042 <details>
1043 <summary>How much does it cost?</summary>
1044 <div class="faq-answer">
1045 <p>
1046 The managed service at <code>ci.pico.sh</code> requires <a href="https://cd.pico.sh">cd.pico.sh</a>
1047 hosting. Pricing is TBD and will be announced with the beta. Self-hosted is free.
1048 </p>
1049 </div>
1050 </details>
1051
1052 <details>
1053 <summary>How are secrets handled?</summary>
1054 <div class="faq-answer">
1055 <p>
1056 Secrets are managed through <a href="https://cd.pico.sh">cd.pico.sh</a>'s environment variable
1057 system. Inject them into your build sessions the same way you'd set any env var. No secret
1058 scanning, no vault, no extra tooling.
1059 </p>
1060 </div>
1061 </details>
1062 </div>
1063</section>
1064
1065<!-- ============================================================
1066 CTA – Sign up for beta
1067 ============================================================ -->
1068<section class="cta-section" id="beta">
1069 <div class="container">
1070 <h2>Get early access to <code>pici</code>.</h2>
1071 <p class="subtitle">
1072 We're opening the managed beta at <code>ci.pico.sh</code>.
1073 </p>
1074 <form class="signup-form" method="POST" action="/pgs/forms/pici-beta">
1075 <input type="email" name="email" placeholder="you@example.com" required autocomplete="email">
1076 <button type="submit" class="btn btn-primary">Sign Up for Beta</button>
1077 </form>
1078 <p class="signup-note">No spam. We'll email you when beta slots open.</p>
1079 </div>
1080</section>
1081
1082<!-- ============================================================
1083 FOOTER
1084 ============================================================ -->
1085<footer>
1086 <div class="container">
1087 <p>
1088 <a href="https://pico.sh">pico.sh</a> ·
1089 <a href="mailto:hello@pico.sh">hello@pico.sh</a>
1090 </p>
1091 <p style="margin-top: 0.5rem;">
1092 Built by <a href="https://pico.sh">pico</a>. Self-hostable. SSH-first. Terminal-native.
1093 </p>
1094 </div>
1095</footer>
1096
1097<script>
1098// Auto-generate anchor links for all h2 elements (skip CTA)
1099document.querySelectorAll('h2').forEach(h2 => {
1100 if (h2.closest('.cta-section')) return;
1101 const section = h2.closest('section, [id]');
1102 if (section && section.id) {
1103 const a = document.createElement('a');
1104 a.href = '#' + section.id;
1105 a.className = 'anchor';
1106 a.textContent = '#';
1107 a.title = 'Copy link to section';
1108 a.addEventListener('click', e => {
1109 e.preventDefault();
1110 history.replaceState(null, null, '#' + section.id);
1111 window.scrollTo({ top: h2.offsetTop - 80, behavior: 'smooth' });
1112 });
1113 h2.appendChild(a);
1114 }
1115});
1116</script>
1117
1118</body>
1119</html>