on master help sign in
Add docket, MIT license and a Nix flake that builds the desk
commitcd590f33798c12bc0a93f12a864324cc56b6b06f authoriko <mail@iko.soy> dateTue 20 Oct 2026, 15:55 (4 days ago) parentb83bb03
8 files changed+360−11
.gitignore+1 LICENSE+21 README.md+36−11 desk/desk.docket-0+8 desk/lib/docket.hoon+223 desk/mar/docket-0.hoon+25 flake.lock+27 flake.nix+19
.gitignorenew file+1
@@ -0,0 +1 @@
1+/result
LICENSEnew file+21
@@ -0,0 +1,21 @@
1+MIT License
2+
3+Copyright (c) 2026 iko
4+
5+Permission is hereby granted, free of charge, to any person obtaining a copy
6+of this software and associated documentation files (the "Software"), to deal
7+in the Software without restriction, including without limitation the rights
8+to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9+copies of the Software, and to permit persons to whom the Software is
10+furnished to do so, subject to the following conditions:
11+
12+The above copyright notice and this permission notice shall be included in all
13+copies or substantial portions of the Software.
14+
15+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21+SOFTWARE.
README.md+36−11
@@ -21,7 +21,15 @@ page's links point at `/`.
2121
2222## Install
2323
24−From the repo root, with your ship running:
24+Build the desk with Nix. `result` is then the desk folder:
25+
26+```sh
27+nix build github:iko-soy/shelf
28+```
29+
30+From a checkout, `nix build` does the same, or you can use `desk/` directly.
31+
32+Then, with your ship running:
2533
2634```
2735~sampel-palnet:dojo> |new-desk %shelf
@@ -29,7 +37,8 @@ From the repo root, with your ship running:
2937```
3038
3139```sh
32−cp -R desk/* /path/to/pier/shelf/
40+cp -R result/* /path/to/pier/shelf/
41+chmod -R u+w /path/to/pier/shelf # files from the Nix store are read-only
3342```
3443
3544```
@@ -37,15 +46,16 @@ cp -R desk/* /path/to/pier/shelf/
3746~sampel-palnet:dojo> |install our %shelf
3847```
3948
40−The page is now at `https://<your-ship>/shelf`. It is meant to be opened
41−through your own domain, though: on the ship's address the page renders, but
42−its links lead to `/`, which belongs to Landscape.
49+The page is now at `https://<your-ship>/shelf`, and Landscape shows a Shelf
50+tile that opens it. It is meant to be opened through your own domain, though:
51+on the ship's address the page renders, but its links lead to `/`, which
52+belongs to Landscape.
4353
4454## Update
4555
46−Copy `desk/*` into the mount again and run `|commit %shelf`. Then check that
47−`.^(cass:clay %cw /=shelf=)` moved: if the new version doesn't build, Clay
48−keeps the old one and logs nothing.
56+Build and copy the desk into the mount again, as above, and run
57+`|commit %shelf`. Then check that `.^(cass:clay %cw /=shelf=)` moved: if the
58+new version doesn't build, Clay keeps the old one and logs nothing.
4959
5060%shelf is installed from your own ship, so it gets no over-the-air updates.
5161After a kernel upgrade, Clay holds it until `desk/sys.kelvin` lists the new
@@ -65,6 +75,9 @@ before you can publish it.
6575 `:treaty|remove-desk ~other-ship %app`, and stop following with
6676 `:treaty|unally ~other-ship`.
6777
78+%shelf has a docket of its own, so `:treaty|publish %shelf` lists it on its
79+own page, where others can install it.
80+
6881A republished app appears once your ship has received its docket from the
6982publisher, which happens a few seconds after you ally them. Apps are sorted by
7083title. %shelf reads the alliance on every request, but browsers may show a
@@ -206,8 +219,10 @@ after %shelf stops. Until you remove it, `/shelf` answers 503 at once.
2062191. Remove the site from your reverse proxy first. Otherwise visitors get that
207220 503 page, or, once the binding is gone, an endless redirect loop between
208221 Landscape's login page and `/`.
209−2. `|uninstall %shelf`. Your `head`/`body` HTML is kept, and installing again
210− brings the page straight back.
222+2. `:docket|uninstall %shelf`, or Uninstall from the tile's menu in
223+ Landscape. This removes the tile and suspends the agent; a plain
224+ `|uninstall %shelf` would leave a suspended tile behind. Your `head`/`body`
225+ HTML is kept, and `|install our %shelf` brings the page straight back.
2112263. To delete that state too: `|nuke %shelf, =desk &` (answer `y`).
2122274. Optionally, free the path. Only the binding's owner can remove it, so take
213228 it over from the dojo, then remove it:
@@ -226,13 +241,16 @@ The desk is self-contained. `desk/` holds everything Clay needs:
226241
227242| Path | What |
228243|---|---|
244+| `desk.docket-0` | What Landscape shows: title, description, color, version, website, license |
229245| `app/shelf.hoon` | The agent: Eyre binding, routing, scries, responses |
230246| `lib/shelf.hoon` | Pure helpers and the page (Sail) |
231247| `sur/shelf.hoon`, `mar/shelf/action.hoon`, `gen/shelf/` | The `head`/`body` settings |
232248| `web/shelf.css`, `web/copy.js` | Inlined into the page at build time with `/*` |
233249| `tests/lib/shelf.hoon` | Unit tests, including the rendered page |
234250| `lib/sigil.hoon`, `lib/sigil/symbols.hoon` | Vendored [sigil renderer](https://github.com/Fang-/suite) (MIT, ~paldev; shapes from `@urbit/sigil-js`, MIT, Tlon) |
235−| other `lib/`, `mar/`, `sur/` files | Copied unmodified from `%base` (urbit/urbit `5a187fe`) and `%landscape` (`sur/treaty`, `sur/docket`; tloncorp/landscape `46c6ed8`) |
251+| other `lib/`, `mar/`, `sur/` files | Copied unmodified from `%base` (urbit/urbit `5a187fe`) and `%landscape` (`sur/treaty`, `sur/docket`, `lib/docket`, `mar/docket-0`; tloncorp/landscape `46c6ed8`) |
252+
253+`flake.nix` makes `nix build` produce the desk as `result`.
236254
237255Run the tests on a ship with the desk installed:
238256
@@ -254,3 +272,10 @@ Some details that are easy to get wrong:
254272- **Clay can refuse a commit silently.** It refuses commits that would stop a
255273 running agent from building, and logs nothing. After `|commit`, check that
256274 `.^(cass:clay %cw /=shelf=)` moved.
275+
276+## License
277+
278+MIT; see [LICENSE](LICENSE). The vendored files keep their own notices:
279+`lib/sigil.hoon` and `lib/sigil/symbols.hoon` are MIT (Fang; shapes from
280+`@urbit/sigil-js`, Tlon), and the files from `%base` and `%landscape` come
281+from urbit/urbit (MIT) and tloncorp/landscape.
desk/desk.docket-0new file+8
@@ -0,0 +1,8 @@
1+:~ title+'Shelf'
2+ info+'A public page listing the apps this ship publishes, so anyone can install them on their own ship.'
3+ color+0xe8.801f
4+ site+/shelf
5+ version+[0 1 0]
6+ website+'https://github.com/iko-soy/shelf'
7+ license+'MIT'
8+==
desk/lib/docket.hoonnew file+223
@@ -0,0 +1,223 @@
1+/- *docket
2+|%
3+::
4+++ mime
5+ |%
6+ +$ draft
7+ $: title=(unit @t)
8+ info=(unit @t)
9+ color=(unit @ux)
10+ glob-http=(unit [=url hash=@uvH])
11+ glob-ames=(unit [=ship hash=@uvH])
12+ base=(unit term)
13+ site=(unit path)
14+ image=(unit url)
15+ version=(unit version)
16+ website=(unit url)
17+ license=(unit cord)
18+ ==
19+ ::
20+ ++ finalize
21+ |= =draft
22+ ^- (unit docket)
23+ ?~ title.draft ~
24+ ?~ info.draft ~
25+ ?~ color.draft ~
26+ ?~ version.draft ~
27+ ?~ website.draft ~
28+ ?~ license.draft ~
29+ =/ href=(unit href)
30+ ?^ site.draft `[%site u.site.draft]
31+ ?~ base.draft ~
32+ ?^ glob-http.draft
33+ `[%glob u.base hash.u.glob-http %http url.u.glob-http]:draft
34+ ?~ glob-ames.draft
35+ ~
36+ `[%glob u.base hash.u.glob-ames %ames ship.u.glob-ames]:draft
37+ ?~ href ~
38+ =, draft
39+ :- ~
40+ :* %1
41+ u.title
42+ u.info
43+ u.color
44+ u.href
45+ image
46+ u.version
47+ u.website
48+ u.license
49+ ==
50+ ::
51+ ++ from-clauses
52+ =| =draft
53+ |= cls=(list clause)
54+ ^- (unit docket)
55+ =* loop $
56+ ?~ cls (finalize draft)
57+ =* clause i.cls
58+ =. draft
59+ ?- -.clause
60+ %title draft(title `title.clause)
61+ %info draft(info `info.clause)
62+ %color draft(color `color.clause)
63+ %glob-http draft(glob-http `[url hash]:clause)
64+ %glob-ames draft(glob-ames `[ship hash]:clause)
65+ %base draft(base `base.clause)
66+ %site draft(site `path.clause)
67+ %image draft(image `url.clause)
68+ %version draft(version `version.clause)
69+ %website draft(website `website.clause)
70+ %license draft(license `license.clause)
71+ ==
72+ loop(cls t.cls)
73+ ::
74+ ++ to-clauses
75+ |= d=docket
76+ ^- (list clause)
77+ %- zing
78+ :~ :~ title+title.d
79+ info+info.d
80+ color+color.d
81+ version+version.d
82+ website+website.d
83+ license+license.d
84+ ==
85+ ?~ image.d ~ ~[image+u.image.d]
86+ ?: ?=(%site -.href.d) ~[site+path.href.d]
87+ =/ ref=glob-reference glob-reference.href.d
88+ :~ base+base.href.d
89+ ?- -.location.ref
90+ %http [%glob-http url.location.ref hash.ref]
91+ %ames [%glob-ames ship.location.ref hash.ref]
92+ == == ==
93+ ::
94+ ++ spit-clause
95+ |= =clause
96+ ^- tape
97+ %+ weld " {(trip -.clause)}+"
98+ ?+ -.clause "'{(trip +.clause)}'"
99+ %color (scow %ux color.clause)
100+ %site (spud path.clause)
101+ ::
102+ %glob-http
103+ "['{(trip url.clause)}' {(scow %uv hash.clause)}]"
104+ ::
105+ %glob-ames
106+ "[{(scow %p ship.clause)} {(scow %uv hash.clause)}]"
107+ ::
108+ %version
109+ =, version.clause
110+ "[{(scow %ud major)} {(scow %ud minor)} {(scow %ud patch)}]"
111+ ==
112+ ::
113+ ++ spit-docket
114+ |= dock=docket
115+ ^- tape
116+ ;: welp
117+ ":~\0a"
118+ `tape`(zing (join "\0a" (turn (to-clauses dock) spit-clause)))
119+ "\0a=="
120+ ==
121+ --
122+::
123+++ enjs
124+ =, enjs:format
125+ |%
126+ ::
127+ ++ charge-update
128+ |= u=^charge-update
129+ ^- json
130+ %+ frond -.u
131+ ^- json
132+ ?- -.u
133+ %del-charge s+desk.u
134+ ::
135+ %initial
136+ %- pairs
137+ %+ turn ~(tap by initial.u)
138+ |=([=desk c=^charge] [desk (charge c)])
139+ ::
140+ %add-charge
141+ %- pairs
142+ :~ desk+s+desk.u
143+ charge+(charge charge.u)
144+ ==
145+ ==
146+ ::
147+ ++ num
148+ |= a=@u
149+ ^- ^tape
150+ =/ p=json (numb a)
151+ ?> ?=(%n -.p)
152+ (trip p.p)
153+ ::
154+ ++ version
155+ |= v=^version
156+ ^- json
157+ :- %s
158+ %- crip
159+ "{(num major.v)}.{(num minor.v)}.{(num patch.v)}"
160+ ::
161+ ++ merge
162+ |= [a=json b=json]
163+ ^- json
164+ ?> &(?=(%o -.a) ?=(%o -.b))
165+ [%o (~(uni by p.a) p.b)]
166+ ::
167+ ++ href
168+ |= h=^href
169+ %+ frond -.h
170+ ?- -.h
171+ %site s+(spat path.h)
172+ %glob
173+ %- pairs
174+ :~ base+s+base.h
175+ glob-reference+(glob-reference glob-reference.h)
176+ ==
177+ ==
178+ ::
179+ ++ glob-reference
180+ |= ref=^glob-reference
181+ %- pairs
182+ :~ hash+s+(scot %uv hash.ref)
183+ location+(glob-location location.ref)
184+ ==
185+ ::
186+ ++ glob-location
187+ |= loc=^glob-location
188+ ^- json
189+ %+ frond -.loc
190+ ?- -.loc
191+ %http s+url.loc
192+ %ames s+(scot %p ship.loc)
193+ ==
194+ ::
195+ ++ charge
196+ |= c=^charge
197+ %+ merge (docket docket.c)
198+ %- pairs
199+ :~ chad+(chad chad.c)
200+ ==
201+ ::
202+ ++ docket
203+ |= d=^docket
204+ ^- json
205+ %- pairs
206+ :~ title+s+title.d
207+ info+s+info.d
208+ color+s+(scot %ux color.d)
209+ href+(href href.d)
210+ image+?~(image.d ~ s+u.image.d)
211+ version+(version version.d)
212+ license+s+license.d
213+ website+s+website.d
214+ ==
215+ ::
216+ ++ chad
217+ |= c=^chad
218+ %+ frond -.c
219+ ?+ -.c ~
220+ %hung s+err.c
221+ ==
222+ --
223+--
desk/mar/docket-0.hoonnew file+25
@@ -0,0 +1,25 @@
1+/+ dock=docket
2+|_ =docket:dock
3+++ grow
4+ |%
5+ ++ mime
6+ ^- ^mime
7+ [/text/x-docket (as-octt:mimes:html (spit-docket:mime:dock docket))]
8+ ++ noun docket
9+ ++ json (docket:enjs:dock docket)
10+ --
11+++ grab
12+ |%
13+ ::
14+ ++ mime
15+ |= [=mite len=@ud tex=@]
16+ ^- docket:dock
17+ %- need
18+ %- from-clauses:mime:dock
19+ !<((list clause:dock) (slap !>(~) (ream tex)))
20+
21+ ::
22+ ++ noun docket:dock
23+ --
24+++ grad %noun
25+--
flake.locknew file+27
@@ -0,0 +1,27 @@
1+{
2+ "nodes": {
3+ "nixpkgs": {
4+ "locked": {
5+ "lastModified": 1792210252,
6+ "narHash": "sha256-Mh0PUaIv4nFY+8wAGonaEh8iBzwsmdNlwjVHmFg1Biw=",
7+ "owner": "NixOS",
8+ "repo": "nixpkgs",
9+ "rev": "09063bd1f2b36fd9fd793c36dfade7bd40a93dfc",
10+ "type": "github"
11+ },
12+ "original": {
13+ "owner": "NixOS",
14+ "ref": "nixos-unstable",
15+ "repo": "nixpkgs",
16+ "type": "github"
17+ }
18+ },
19+ "root": {
20+ "inputs": {
21+ "nixpkgs": "nixpkgs"
22+ }
23+ }
24+ },
25+ "root": "root",
26+ "version": 7
27+}
flake.nixnew file+19
@@ -0,0 +1,19 @@
1+{
2+ description = "%shelf: a public web page listing the apps an Urbit ship publishes";
3+
4+ inputs.nixpkgs.url = "github:NixOS/nixpkgs/nixos-unstable";
5+
6+ outputs = { self, nixpkgs }:
7+ let
8+ systems = [ "x86_64-linux" "aarch64-linux" "aarch64-darwin" ];
9+ forAllSystems = f: nixpkgs.lib.genAttrs systems (system: f nixpkgs.legacyPackages.${system});
10+ in
11+ {
12+ # The desk: copy it into a mounted %shelf desk and |commit it.
13+ packages = forAllSystems (pkgs: {
14+ default = pkgs.runCommandLocal "shelf-desk" { } ''
15+ cp -r ${./desk} $out
16+ '';
17+ });
18+ };
19+}