~ knotless Search/
↑↓ move↵ open⇧↵ all results

chadtmiller.com/knotless

git that lives in your Atmosphere account

knotless.dev
1 collaborator

knotless 0.13.0 Latest 2 packages, 1 image

1
10

The marketing images and listing words live in the repository

Merged chadtmiller.com asked to merge into main from chadtmiller.com/knotless:marketing/atstore
marketing/atstore/screenshots/1-repo.pngnew+0−0
A binary file, not shown.
marketing/atstore/screenshots/2-pull.pngnew+0−0
A binary file, not shown.
marketing/atstore/screenshots/3-review.pngnew+0−0
A binary file, not shown.
marketing/atstore/screenshots/4-diff.pngnew+0−0
A binary file, not shown.
marketing/atstore/screenshots/5-checks.pngnew+0−0
A binary file, not shown.
marketing/atstore/hero-alt.pngnew+0−0
A binary file, not shown.
marketing/atstore/hero.pngnew+0−0
A binary file, not shown.
marketing/atstore/icon.pngnew+0−0
A binary file, not shown.
marketing/atstore/listing.jsonnew+53−0
@@ -0,0 +1,53 @@
{
"$type": "fyi.atstore.listing.detail",
"slug": "knotless",
"name": "knotless",
"tagline": "Git that lives in your Atmosphere account.",
"description": "knotless keeps your git repositories in your Atmosphere account. Each one is a handful of records on your own PDS, so it moves with your account and is in your backups. There is no git server to run, and anyone can clone straight from your PDS.\n\nYou push with plain git:\n\ngit push knotless://you.bsky.social/my-project main\n\nForks, pulls, issues and reviews are records too, each in its author's account. Browse code, diffs and checks at knotless.dev on a phone or a desktop, and log in to star, comment and merge. Protected branches and a merge queue are there when a project needs them, and knotless-run runs your CI on a machine you choose.\n\nAgents work over MCP. They can read code, commit, review and merge without a clone.\n\nIt works on any stock PDS, with nothing to install on it. Private repositories need a PDS with permissioned spaces, which are still in alpha. knotless is open source under MIT or Apache 2.0, and you can run your own server.",
"externalUrl": "https://knotless.dev",
"categorySlug": [
"apps/knotless"
],
"appTags": [
"developer",
"infrastructure"
],
"productAccountDid": "did:plc:i6y5vctpatds2umjmzmogmiv",
"links": [
{
"type": "source",
"url": "https://knotless.dev/chadtmiller.com/knotless"
},
{
"type": "docs",
"url": "https://knotless.dev/docs"
},
{
"type": "changelog",
"url": "https://knotless.dev/chadtmiller.com/knotless/blob/main/CHANGELOG.md"
},
{
"type": "support",
"url": "https://knotless.dev/chadtmiller.com/knotless/issues"
},
{
"type": "terms",
"url": "https://knotless.dev/terms"
},
{
"type": "privacy",
"url": "https://knotless.dev/privacy"
}
],
"icon": "BLOB: icon.png",
"heroImage": "BLOB: hero.png",
"screenshots": [
"BLOB: screenshots/1-repo.png",
"BLOB: screenshots/2-pull.png",
"BLOB: screenshots/3-review.png",
"BLOB: screenshots/4-diff.png",
"BLOB: screenshots/5-checks.png"
],
"createdAt": "2026-11-06T21:42:30.000Z",
"updatedAt": "2026-11-06T21:42:30.000Z"
}
marketing/bluesky/banner.pngnew+0−0
A binary file, not shown.
marketing/src/banner.htmlnew+17−0
@@ -0,0 +1,17 @@
<!doctype html><link rel=stylesheet href=base.css>
<style>
html,body{width:1500px;height:500px}
body{background:#2563eb;color:#fff;position:relative}
svg{position:absolute;inset:0}
h1{position:absolute;left:330px;top:50%;transform:translateY(-50%);font:600 64px/1.06 Inter;letter-spacing:-.026em}
</style>
<svg id=s viewBox="0 0 1500 500" fill=none></svg>
<h1>Git that lives in your<br>Atmosphere account.</h1>
<script>
const s=document.getElementById('s');let d='';const N=30;
for(let i=0;i<N;i++){const y0=-50+i*(600/N);let p='';
for(let x=-20;x<=1540;x+=12){const u=x/1500,t=Math.min(1,Math.max(0,(u-.5)/.42)),f=t*t*(3-2*t);
const y=y0+f*(58*Math.sin(u*6.2+i*.16)+28*Math.sin(u*2.6-i*.11));p+=(p?'L':'M')+x.toFixed(1)+' '+y.toFixed(1);}
d+=`<path d="${p}" stroke="#fff" stroke-opacity="${(.14+.3*i/N).toFixed(2)}" stroke-width="1.8"/>`;}
s.innerHTML=d+`<defs><linearGradient id=g x2=1 y2=0><stop offset=.56 stop-color="#2563eb"/><stop offset=.8 stop-color="#2563eb" stop-opacity=0 /></linearGradient></defs><rect width=1500 height=500 fill="url(#g)"/>`;
</script>
marketing/src/base.cssnew+7−0
@@ -0,0 +1,7 @@
@font-face{font-family:Inter;font-weight:400;src:url(../../crates/knotless-serve/fonts/InterDisplay-Regular.ttf)}
@font-face{font-family:Inter;font-weight:600;src:url(../../crates/knotless-serve/fonts/InterDisplay-SemiBold.ttf)}
@font-face{font-family:Mono;font-weight:600;src:url(../../crates/knotless-serve/fonts/JetBrainsMono-SemiBold.ttf)}
*{box-sizing:border-box;margin:0}
html,body{width:1600px;height:900px;overflow:hidden}
.word{font:600 44px Mono;letter-spacing:-.01em;display:flex;align-items:center;gap:22px}
h1{font:600 92px/1.04 Inter;letter-spacing:-.028em}
marketing/src/hero-alt.htmlnew+25−0
@@ -0,0 +1,25 @@
<!doctype html><link rel=stylesheet href=base.css><link rel=stylesheet href="https://fonts.googleapis.com/css2?family=JetBrains+Mono:wght@500&display=block">
<style>
body{background:#171717;color:#fff;position:relative}
svg{position:absolute;inset:0}
.copy{position:absolute;left:96px;top:96px;width:1000px}
.word{margin-bottom:210px}
.cmd{margin-top:54px;display:inline-block;font:500 25px 'JetBrains Mono';color:#fff;background:#262626;border-radius:12px;padding:20px 26px;white-space:nowrap}
.cmd i{font-style:normal;color:#8ab0ff}
</style>
<svg id=s viewBox="0 0 1600 900" fill=none stroke-linecap=round></svg>
<div class=copy><div class=word>~ knotless</div><h1>Git that lives in your<br>Atmosphere account.</h1>
<div class=cmd>git push <i>knotless://you.bsky.social/project</i> main</div></div>
<script>
// Lines of the logo's tilde, stacked like a weather map's isobars. None cross.
const s=document.getElementById('s');let d='';const N=46;
for(let i=0;i<N;i++){const y0=-60+i*(1020/N);let p='';
for(let x=-20;x<=1640;x+=14){
const u=x/1600,t=Math.min(1,Math.max(0,(u-.3)/.6)),fade=t*t*(3-2*t);
const y=y0+fade*(92*Math.sin(u*6.2+i*.11)+46*Math.sin(u*2.6-i*.075));
p+=(p?'L':'M')+x.toFixed(1)+' '+y.toFixed(1);}
const o=(.16+.62*(i/N)).toFixed(2);d+=`<path d="${p}" stroke="#4f86f7" stroke-opacity="${o}" stroke-width="2.4"/>`;}
s.innerHTML=d;
// Clear the lines from behind the copy.
s.innerHTML+=`<defs><linearGradient id=g x2=1 y2=0><stop offset=.42 stop-color="#171717"/><stop offset=.7 stop-color="#171717" stop-opacity=0 /></linearGradient></defs><rect width=1600 height=900 fill="url(#g)" />`;
</script>
marketing/src/hero.htmlnew+10−0
@@ -0,0 +1,10 @@
<!doctype html><link rel=stylesheet href=base.css>
<style>
body{background:#2563eb;color:#fff;position:relative}
.copy{z-index:2;position:absolute;left:96px;top:84px;width:1200px}
.word{margin-bottom:44px}
.page{position:absolute;left:96px;right:96px;top:432px;height:640px;border-radius:18px 18px 0 0;overflow:hidden;background:#111;box-shadow:0 0 0 1px rgba(255,255,255,.14),0 -6px 40px rgba(10,25,80,.28)}
.page img{position:absolute;width:1800px;left:-196px;top:0;display:block}
</style>
<div class=copy><div class=word>~ knotless</div><h1>Git that lives in your<br>Atmosphere account.</h1></div>
<div class=page><img src="../atstore/screenshots/1-repo.png"></div>
marketing/src/render.mjsnew+25−0
@@ -0,0 +1,25 @@
// Draws the images from src/ at the sizes they are published. Pass a directory
// to draw into it instead of over the committed files.
import { chromium } from 'playwright';
import { mkdirSync } from 'node:fs';
import { dirname, resolve } from 'node:path';
import { fileURLToPath } from 'node:url';
const root = resolve(dirname(fileURLToPath(import.meta.url)), '..');
const out = resolve(process.argv[2] ?? root);
const images = [
['hero', 'atstore/hero.png', 1600, 900],
['hero-alt', 'atstore/hero-alt.png', 1600, 900],
['banner', 'bluesky/banner.png', 1500, 500],
];
const browser = await chromium.launch();
for (const [name, path, width, height] of images) {
const page = await browser.newPage({ viewport: { width, height }, deviceScaleFactor: 2 });
await page.goto(`file://${root}/src/${name}.html`, { waitUntil: 'networkidle' });
await page.evaluate(() => document.fonts.ready);
mkdirSync(dirname(`${out}/${path}`), { recursive: true });
await page.screenshot({ path: `${out}/${path}` });
await page.close();
}
await browser.close();
marketing/README.mdnew+52−0
@@ -0,0 +1,52 @@
# Marketing
Images and words for knotless's listings and accounts, and what draws them.
## ATStore
The listing is the record
`at://did:plc:i6y5vctpatds2umjmzmogmiv/fyi.atstore.listing.detail/3mzodyv6ioct6`,
on the knotless.dev account's PDS. ATStore reads it from there. To change the
listing, update that record with `com.atproto.repo.putRecord`. Creating another
would make a second listing.
- `atstore/listing.json`: the record's fields, with `BLOB:` naming the file each
image was uploaded from.
- `atstore/hero.png`: the hero, 3200 by 1800.
- `atstore/hero-alt.png`: the hero not chosen.
- `atstore/icon.png`: the icon, a copy of the iOS app's.
- `atstore/screenshots/`: knotless.dev's pages at 1280 by 800, drawn at twice
that, in the listing's order. The diff and the merged pull are
`dev.knotless.listActorItems`'s pull.
The record allows a hero of 12 MB, an icon of 2 MB and 20 screenshots.
## Bluesky
`bluesky/banner.png` is the knotless.dev account's banner, 3000 by 1000. Bluesky
uploads it at 3:1 and shows it at about 4:1 on a desktop, with the profile picture
over its lower left, so the words stay inside that band and right of the picture.
The post for when the listing appeared:
> knotless is on @atstore.fyi now. It's git that lives in your Atmosphere
> account, pushed with plain git, with no git server to run.
>
> If you've tried it, a review would help.
## Drawing them again
`src/` holds a page for each drawn image. They take Inter and JetBrains Mono from
`crates/knotless-serve/fonts`, and the alternate hero's command takes the full
JetBrains Mono from Google Fonts, as the repository's copy is cut to the letters
of `~ knotless`.
```sh
cd marketing
npm install --no-save playwright@1.58.2
node src/render.mjs # over the committed files
node src/render.mjs /tmp/look # or beside them, to compare
```
The screenshots are taken from the live pages by hand, and show the star count,
the version and "hours ago" as they were on the day.
.dockerignore+1−0
@@ -11,3 +11,4 @@ docs-site/node_modules
docs-site/dist
docs-site/.astro
docs-site/src/content/docs
marketing
.gitignore+1−0
@@ -2,3 +2,4 @@
/harness/node_modules
*.profraw
/lcov.info
/marketing/node_modules