pixling 0.1.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (216) hide show
  1. painted/__init__.py +2 -0
  2. painted/__main__.py +219 -0
  3. painted/bpy/assets.py +530 -0
  4. painted/bpy/hilltop.py +401 -0
  5. painted/bpy/kit.py +186 -0
  6. painted/bpy/painter.py +439 -0
  7. painted/build_game.py +125 -0
  8. painted/compose.py +108 -0
  9. painted/game.html +310 -0
  10. painted/layouts/hollow_brook.json +61 -0
  11. painted/styles/painted_cards.json +59 -0
  12. painted/textures.py +104 -0
  13. pixling-0.1.0.dist-info/METADATA +105 -0
  14. pixling-0.1.0.dist-info/RECORD +216 -0
  15. pixling-0.1.0.dist-info/WHEEL +4 -0
  16. pixling-0.1.0.dist-info/entry_points.txt +2 -0
  17. pixling-0.1.0.dist-info/licenses/LICENSE +21 -0
  18. pp/__init__.py +7 -0
  19. pp/__main__.py +230 -0
  20. pp/anim.py +790 -0
  21. pp/aseprite.py +80 -0
  22. pp/blocks.py +354 -0
  23. pp/brand.py +80 -0
  24. pp/cli.py +718 -0
  25. pp/color.py +161 -0
  26. pp/data/.claude/skills/pixling/SKILL.md +80 -0
  27. pp/data/docs/ARTIST.md +256 -0
  28. pp/data/docs/ENVIRONMENTS.md +30 -0
  29. pp/data/docs/PIPELINES.md +258 -0
  30. pp/data/docs/STYLES.md +137 -0
  31. pp/data/docs/WORKFLOW.md +40 -0
  32. pp/data/docs/brand/README.md +65 -0
  33. pp/data/docs/brand/pixling-mark-1x.png +0 -0
  34. pp/data/docs/brand/pixling-sprite.png +0 -0
  35. pp/data/painted/README.md +96 -0
  36. pp/data/reel/README.md +138 -0
  37. pp/data/reel/WORKFLOW.md +115 -0
  38. pp/data/reel/chars/_template/char.json +96 -0
  39. pp/data/research/motion/NOTES.md +61 -0
  40. pp/data/research/styles/README.md +17 -0
  41. pp/data/research/styles/SHARED_LAYER.md +74 -0
  42. pp/data/research/styles/proto_flat_v2.md +71 -0
  43. pp/data/research/styles/proto_frost_flat_v1.md +101 -0
  44. pp/data/research/styles/proto_marsh.md +90 -0
  45. pp/data/research/styles/proto_saffron.md +96 -0
  46. pp/data/research/styles/proto_tactics_v2.md +83 -0
  47. pp/data/research/styles/round1_FINDINGS.md +38 -0
  48. pp/data/research/styles/round2_DNA.md +208 -0
  49. pp/data/specs/blocks/ashwyrm.json +75 -0
  50. pp/data/specs/blocks/bibi.json +42 -0
  51. pp/data/specs/blocks/bramblehorn.json +43 -0
  52. pp/data/specs/blocks/hoot.json +62 -0
  53. pp/data/specs/blocks/pip.json +47 -0
  54. pp/data/specs/blocks/sludgeling.json +38 -0
  55. pp/data/specs/duskborne/ashen_knight.json +1420 -0
  56. pp/data/specs/duskborne/bone_grunt.json +767 -0
  57. pp/data/specs/duskborne/dusk_rock.json +200 -0
  58. pp/data/specs/duskborne/dusk_tree.json +424 -0
  59. pp/data/specs/duskborne/grave_ghoul.json +2409 -0
  60. pp/data/specs/duskborne/hollow_cultist.json +1559 -0
  61. pp/data/specs/duskborne/shade_wolf.json +1234 -0
  62. pp/data/specs/sunmeadow/boar.json +577 -0
  63. pp/data/specs/sunmeadow/bush.json +537 -0
  64. pp/data/specs/sunmeadow/bush_berry.json +577 -0
  65. pp/data/specs/sunmeadow/dead_tree.json +456 -0
  66. pp/data/specs/sunmeadow/deer.json +788 -0
  67. pp/data/specs/sunmeadow/elder.json +803 -0
  68. pp/data/specs/sunmeadow/fox.json +472 -0
  69. pp/data/specs/sunmeadow/hero.json +1553 -0
  70. pp/data/specs/sunmeadow/oak.json +1 -0
  71. pp/data/specs/sunmeadow/oak_autumn.json +1 -0
  72. pp/data/specs/sunmeadow/orc_warrior.json +852 -0
  73. pp/data/specs/sunmeadow/pine.json +731 -0
  74. pp/data/specs/sunmeadow/pixling.json +612 -0
  75. pp/data/specs/sunmeadow/rabbit.json +403 -0
  76. pp/data/specs/sunmeadow/ranger.json +1687 -0
  77. pp/data/specs/sunmeadow/rock.json +155 -0
  78. pp/data/specs/sunmeadow/rock_mossy.json +106 -0
  79. pp/data/specs/sunmeadow/rock_small.json +107 -0
  80. pp/data/specs/vermilion/antler_ward.json +579 -0
  81. pp/data/specs/vermilion/cinder_imp.json +809 -0
  82. pp/data/specs/vermilion/ember_brute.json +636 -0
  83. pp/data/specs/vermilion/ember_tree.json +29 -0
  84. pp/data/specs/vermilion/ink_stone.json +10 -0
  85. pp/data/specs/vermilion/shrike_crier.json +722 -0
  86. pp/data/specs/vista/chapel.json +206 -0
  87. pp/data/specs/vista/fountain_small.json +109 -0
  88. pp/data/specs/vista/highgate.map.json +903 -0
  89. pp/data/specs/vista/highgate_hub.map.json +1829 -0
  90. pp/data/specs/vista/highgate_streets.map.json +804 -0
  91. pp/data/specs/vista/highgate_town.map.json +2194 -0
  92. pp/data/specs/vista/house_1.json +167 -0
  93. pp/data/specs/vista/house_2.json +188 -0
  94. pp/data/specs/vista/house_3.json +230 -0
  95. pp/data/specs/vista/house_4.json +209 -0
  96. pp/data/specs/vista/house_5.json +167 -0
  97. pp/data/specs/vista/house_6.json +188 -0
  98. pp/data/specs/vista/house_7.json +146 -0
  99. pp/data/specs/vista/house_8.json +251 -0
  100. pp/data/specs/vista/keep.json +747 -0
  101. pp/data/specs/vista/lighthouse.json +180 -0
  102. pp/data/specs/vista/market.json +148 -0
  103. pp/data/specs/vista/sailboat.json +133 -0
  104. pp/data/specs/vista/streets/banner_pole_v.json +246 -0
  105. pp/data/specs/vista/streets/st_arena.json +2592 -0
  106. pp/data/specs/vista/streets/st_barrels.json +159 -0
  107. pp/data/specs/vista/streets/st_boat.json +207 -0
  108. pp/data/specs/vista/streets/st_bridge_ew.json +314 -0
  109. pp/data/specs/vista/streets/st_cart.json +198 -0
  110. pp/data/specs/vista/streets/st_fountain.json +290 -0
  111. pp/data/specs/vista/streets/st_gate.json +521 -0
  112. pp/data/specs/vista/streets/st_hall.json +976 -0
  113. pp/data/specs/vista/streets/st_house_a.json +569 -0
  114. pp/data/specs/vista/streets/st_house_b.json +566 -0
  115. pp/data/specs/vista/streets/st_house_c.json +532 -0
  116. pp/data/specs/vista/streets/st_house_d.json +511 -0
  117. pp/data/specs/vista/streets/st_house_e.json +615 -0
  118. pp/data/specs/vista/streets/st_house_f.json +509 -0
  119. pp/data/specs/vista/streets/st_house_g.json +316 -0
  120. pp/data/specs/vista/streets/st_house_h.json +730 -0
  121. pp/data/specs/vista/streets/st_house_i.json +474 -0
  122. pp/data/specs/vista/streets/st_house_j.json +385 -0
  123. pp/data/specs/vista/streets/st_lamp.json +170 -0
  124. pp/data/specs/vista/streets/st_lighthouse.json +183 -0
  125. pp/data/specs/vista/streets/st_rowboat.json +190 -0
  126. pp/data/specs/vista/streets/st_ship.json +378 -0
  127. pp/data/specs/vista/streets/st_stall_gold.json +284 -0
  128. pp/data/specs/vista/streets/st_stall_red.json +284 -0
  129. pp/data/specs/vista/streets/st_stall_teal.json +284 -0
  130. pp/data/specs/vista/streets/st_wall.json +271 -0
  131. pp/data/specs/vista/streets/st_well.json +185 -0
  132. pp/data/specs/vista/world/vale.map.json +2857 -0
  133. pp/data/specs/vista/world/w_bandit_camp.json +338 -0
  134. pp/data/specs/vista/world/w_banner.json +225 -0
  135. pp/data/specs/vista/world/w_camp.json +220 -0
  136. pp/data/specs/vista/world/w_crypt.json +324 -0
  137. pp/data/specs/vista/world/w_goblin_warren.json +412 -0
  138. pp/data/specs/vista/world/w_mine.json +318 -0
  139. pp/data/specs/vista/world/w_portal.json +289 -0
  140. pp/data/specs/vista/world/w_smuggler_cove.json +318 -0
  141. pp/data/specs/vista/world/w_spire.json +350 -0
  142. pp/data/specs/vista/world/w_windmill.json +247 -0
  143. pp/data/specs/vista/world/w_wolf_den.json +284 -0
  144. pp/data/styles/duskborne.json +453 -0
  145. pp/data/styles/flat.json +94 -0
  146. pp/data/styles/flat_dunes.json +94 -0
  147. pp/data/styles/sunmeadow.json +519 -0
  148. pp/data/styles/tactics.json +149 -0
  149. pp/data/styles/tactics_autumn.json +149 -0
  150. pp/data/styles/vermilion.json +215 -0
  151. pp/data/styles/vista.json +329 -0
  152. pp/data/tools/flat/build.py +90 -0
  153. pp/data/tools/flat/kit.py +202 -0
  154. pp/data/tools/flat/scenes.py +212 -0
  155. pp/data/tools/make_styles.py +386 -0
  156. pp/data/tools/tactics/build.py +79 -0
  157. pp/data/tools/tactics/kit.py +354 -0
  158. pp/data/tools/tactics/scenes.py +343 -0
  159. pp/data/tools/vista/aerial.py +64 -0
  160. pp/data/tools/vista/build.py +39 -0
  161. pp/data/tools/vista/layout_hub.py +121 -0
  162. pp/data/tools/vista/layout_streets.py +70 -0
  163. pp/data/tools/vista/layout_town.py +130 -0
  164. pp/data/tools/vista/layout_vale.py +124 -0
  165. pp/data/tools/vista/slib.py +58 -0
  166. pp/data/tools/vista/streets.py +179 -0
  167. pp/data/tools/vista/vlib.py +50 -0
  168. pp/data/tools/vista/world.py +116 -0
  169. pp/detail.py +247 -0
  170. pp/flora.py +150 -0
  171. pp/forge.py +763 -0
  172. pp/inspect.py +225 -0
  173. pp/io.py +70 -0
  174. pp/iso.py +570 -0
  175. pp/iso_line.py +149 -0
  176. pp/layers.py +181 -0
  177. pp/light.py +233 -0
  178. pp/lint.py +213 -0
  179. pp/map.py +1320 -0
  180. pp/meaning.py +168 -0
  181. pp/motion.py +719 -0
  182. pp/pack.py +85 -0
  183. pp/paths.py +17 -0
  184. pp/pixcheck.py +57 -0
  185. pp/render.py +368 -0
  186. pp/review.py +87 -0
  187. pp/scenario/__init__.py +193 -0
  188. pp/scenario/auth.py +291 -0
  189. pp/scenario/mcp.py +131 -0
  190. pp/scene.py +161 -0
  191. pp/sdf.py +154 -0
  192. pp/secondary.py +78 -0
  193. pp/shade.py +348 -0
  194. pp/size.py +158 -0
  195. pp/states.py +239 -0
  196. pp/variants.py +229 -0
  197. pp/vfx.py +528 -0
  198. reel/__init__.py +0 -0
  199. reel/__main__.py +89 -0
  200. reel/cut.py +179 -0
  201. reel/demo.py +167 -0
  202. reel/finish.py +141 -0
  203. reel/fx/black_1024.png +0 -0
  204. reel/fx/fireball_rain.json +29 -0
  205. reel/fx/necro_signet.json +24 -0
  206. reel/fx.py +137 -0
  207. reel/game.py +290 -0
  208. reel/io.py +81 -0
  209. reel/key.py +145 -0
  210. reel/keys.py +189 -0
  211. reel/pack.py +33 -0
  212. reel/page.py +130 -0
  213. reel/qc.py +249 -0
  214. reel/split.py +83 -0
  215. reel/synth.py +49 -0
  216. reel/take.py +44 -0
painted/__init__.py ADDED
@@ -0,0 +1,2 @@
1
+ """painted: the non-pixel format. Blender renders painted-card sprites (stepped-ramp shading, alpha-card foliage)
2
+ from a style file and a layout, then packs them into a scene or a walkable web game. See painted/README.md."""
painted/__main__.py ADDED
@@ -0,0 +1,219 @@
1
+ """CLI: python3 -m painted <command> ... (also `pixling painted <command>`)
2
+
3
+ kit [assets ...] [--style S] [--layout L] [--out D] Blender -> every asset as a sprite (+ shadow, sway, 8 headings)
4
+ compose [--layout L] [--kit D] [--out D] lay the layout out from the sprites: scene.png, scene.mp4, kit sheet
5
+ game [--layout L] [--kit D] [--out D] [--pixel DIR] walkable web scene (index.html + data.json + a/); pp sprites optional
6
+ serve [--layout L] [--port 8765] serve the built game over http and open it (file:// can't load it)
7
+ hilltop [--size WxH] [--pixel] [--frames N] [--out P] the perspective film shot (one hero tree over a valley)
8
+ assets what kit can build, by kind
9
+ styles | layouts what is in painted/styles and painted/layouts
10
+ doctor is Blender here (PIXLING_BLENDER overrides the search)
11
+
12
+ Outputs default to out/painted/<layout>/{kit,scene,game} (out/painted/hilltop for the film shot).
13
+ """
14
+ from __future__ import annotations
15
+
16
+ import argparse
17
+ import json
18
+ import os
19
+ import shutil
20
+ import subprocess
21
+ import sys
22
+ from pathlib import Path
23
+
24
+ HERE = Path(__file__).resolve().parent
25
+ BPY = HERE / "bpy"
26
+
27
+
28
+ def _out_root() -> Path:
29
+ try:
30
+ from pp.paths import OUT
31
+ except ImportError: # painted runs without pp too
32
+ OUT = Path.cwd() / "out"
33
+ return OUT / "painted"
34
+
35
+
36
+ def _named(kind: str, name: str) -> Path:
37
+ """a style or layout by name (painted/<kind>/<name>.json) or by path."""
38
+ p = Path(name)
39
+ if p.suffix == ".json" and p.exists():
40
+ return p
41
+ q = HERE / kind / f"{name}.json"
42
+ if not q.exists():
43
+ have = ", ".join(sorted(f.stem for f in (HERE / kind).glob("*.json")))
44
+ sys.exit(f"painted: no {kind[:-1]} '{name}' (have: {have})")
45
+ return q
46
+
47
+
48
+ def blender() -> str | None:
49
+ """PIXLING_BLENDER, then the usual macOS / Linux / Windows install places, then PATH."""
50
+ cands = [os.environ.get("PIXLING_BLENDER"), os.environ.get("BLENDER"),
51
+ str(Path.home() / "Applications/Blender.app/Contents/MacOS/Blender"),
52
+ "/Applications/Blender.app/Contents/MacOS/Blender", shutil.which("blender"),
53
+ *sorted(map(str, Path("C:/Program Files/Blender Foundation").glob("Blender*/blender.exe")), reverse=True)]
54
+ return next((c for c in cands if c and Path(c).exists()), None)
55
+
56
+
57
+ def _run_blender(script: str, args: list[str]) -> int:
58
+ b = blender()
59
+ if not b:
60
+ sys.exit("painted: Blender not found. Install Blender 4.2+ or set PIXLING_BLENDER=/path/to/blender")
61
+ cmd = [b, "-b", "--factory-startup", "-P", str(BPY / script), "--", *args]
62
+ p = subprocess.run(cmd, capture_output=True, text=True)
63
+ log = p.stdout + p.stderr
64
+ for line in log.splitlines():
65
+ if line.startswith("built ") or "Traceback" in line or line.startswith(("Error:", " File")):
66
+ if "unfreed" not in line:
67
+ print(line)
68
+ if p.returncode or "Traceback" in log:
69
+ print(log[-3000:], file=sys.stderr)
70
+ return 1
71
+ return 0
72
+
73
+
74
+ def _textures(tex: Path):
75
+ subprocess.run([sys.executable, str(HERE / "textures.py"), str(tex)], check=True, capture_output=True)
76
+
77
+
78
+ def cmd_kit(a) -> int:
79
+ style, layout = _named("styles", a.style), _named("layouts", a.layout)
80
+ out = Path(a.out) if a.out else _out_root() / layout.stem / "kit"
81
+ tex = out / "_tex"; _textures(tex)
82
+ code = _run_blender("kit.py", [str(style), str(layout), str(tex), str(out), *a.assets])
83
+ if not code:
84
+ print(f"kit -> {out}")
85
+ return code
86
+
87
+
88
+ def cmd_compose(a) -> int:
89
+ layout = _named("layouts", a.layout)
90
+ kit = Path(a.kit) if a.kit else _out_root() / layout.stem / "kit"
91
+ out = Path(a.out) if a.out else _out_root() / layout.stem / "scene"
92
+ r = subprocess.run([sys.executable, str(HERE / "compose.py"), str(kit), str(out), str(layout), *(["keep"] if a.keep_frames else [])])
93
+ if not r.returncode:
94
+ print(f"scene -> {out}")
95
+ return r.returncode
96
+
97
+
98
+ def cmd_game(a) -> int:
99
+ layout = _named("layouts", a.layout)
100
+ kit = Path(a.kit) if a.kit else _out_root() / layout.stem / "kit"
101
+ out = Path(a.out) if a.out else _out_root() / layout.stem / "game"
102
+ if out.exists():
103
+ shutil.rmtree(out)
104
+ r = subprocess.run([sys.executable, str(HERE / "build_game.py"), str(kit), str(out), str(layout), *a.pixel])
105
+ if not r.returncode:
106
+ print(f"game -> {out} (serve it over http, e.g. python3 -m http.server -d {out})")
107
+ return r.returncode
108
+
109
+
110
+ def cmd_serve(a) -> int:
111
+ import functools, http.server, threading, webbrowser
112
+ layout = _named("layouts", a.layout)
113
+ root = Path(a.dir) if a.dir else _out_root() / layout.stem / "game"
114
+ if not (root / "index.html").exists():
115
+ sys.exit(f"painted: no game in {root}; run `pixling painted game` first")
116
+ class Quiet(http.server.SimpleHTTPRequestHandler):
117
+ def log_message(self, *args): # no per-request lines
118
+ pass
119
+ handler = functools.partial(Quiet, directory=str(root))
120
+ srv = http.server.ThreadingHTTPServer(("127.0.0.1", a.port), handler)
121
+ url = f"http://127.0.0.1:{a.port}/"
122
+ print(f"serving {root} at {url} (Ctrl+C stops)")
123
+ if not a.no_open:
124
+ threading.Timer(.4, lambda: webbrowser.open(url)).start()
125
+ try:
126
+ srv.serve_forever()
127
+ except KeyboardInterrupt:
128
+ pass
129
+ return 0
130
+
131
+
132
+ def cmd_hilltop(a) -> int:
133
+ w, h = (int(v) for v in a.size.split("x"))
134
+ root = _out_root() / "hilltop"
135
+ out = Path(a.out) if a.out else root / (("pixel" if a.pixel else "hd") + ("_loop" if a.frames else ".png"))
136
+ out.parent.mkdir(parents=True, exist_ok=True)
137
+ if a.frames:
138
+ out.mkdir(parents=True, exist_ok=True)
139
+ tex = root / "_tex"; _textures(tex)
140
+ code = _run_blender("hilltop.py", [str(tex), str(out), str(w), str(h), "pixel" if a.pixel else "hd", str(a.frames)])
141
+ if not code:
142
+ print(f"hilltop -> {out}")
143
+ return code
144
+
145
+
146
+ def cmd_assets(a) -> int:
147
+ import ast
148
+ src = (BPY / "assets.py").read_text()
149
+ tree = ast.parse(src)
150
+ docs = {n.name: (ast.get_docstring(n) or "").split("\n")[0] for n in tree.body if isinstance(n, ast.FunctionDef)}
151
+ kinds = {}
152
+ for n in tree.body: # TREES = dict(oak=oak, ...) etc.
153
+ if isinstance(n, ast.Assign) and isinstance(n.value, ast.Call) and getattr(n.value.func, "id", "") == "dict":
154
+ kinds[n.targets[0].id] = {k.arg: getattr(k.value, "id", "") for k in n.value.keywords}
155
+ rows = [{"asset": name, "kind": kind.lower(), "about": docs.get(fn, "")}
156
+ for kind, m in kinds.items() for name, fn in m.items()] + [{"asset": "ground", "kind": "plate",
157
+ "about": "the layout's painted ground"}]
158
+ if a.json:
159
+ print(json.dumps(rows, indent=1))
160
+ else:
161
+ print("\n".join(" %-10s %-10s %s" % (r["asset"], r["kind"], r["about"]) for r in rows))
162
+ return 0
163
+
164
+
165
+ def _list(kind: str, a) -> int:
166
+ rows = []
167
+ for f in sorted((HERE / kind).glob("*.json")):
168
+ d = json.loads(f.read_text())
169
+ rows.append({"name": f.stem, "about": d.get("about", ""),
170
+ **({"ramps": len(d.get("ramps", {})), "camera": d.get("camera")} if kind == "styles"
171
+ else {"places": len(d.get("place", []))})})
172
+ if a.json:
173
+ print(json.dumps(rows, indent=1))
174
+ else:
175
+ print("\n".join(" %-14s %s" % (r["name"], r["about"]) for r in rows))
176
+ return 0
177
+
178
+
179
+ def cmd_doctor(a) -> int:
180
+ b = blender()
181
+ ver = ""
182
+ if b:
183
+ r = subprocess.run([b, "-b", "--factory-startup", "--version"], capture_output=True, text=True)
184
+ ver = (r.stdout.splitlines() or [""])[0]
185
+ rows = {"blender": b or None, "version": ver, "ok": bool(b)}
186
+ if a.json:
187
+ print(json.dumps(rows))
188
+ else:
189
+ print(f" {'ok ' if b else 'MISSING'} Blender {ver or 'install Blender 4.2+ or set PIXLING_BLENDER'} {b or ''}")
190
+ return 0 if b else 1
191
+
192
+
193
+ def main(argv=None) -> int:
194
+ ap = argparse.ArgumentParser(prog="painted", description=__doc__, formatter_class=argparse.RawDescriptionHelpFormatter)
195
+ sub = ap.add_subparsers(dest="cmd", required=True)
196
+ k = sub.add_parser("kit"); k.add_argument("assets", nargs="*")
197
+ for p in (k,):
198
+ p.add_argument("--style", default="painted_cards")
199
+ for p in (k, c := sub.add_parser("compose"), g := sub.add_parser("game")):
200
+ p.add_argument("--layout", default="hollow_brook"); p.add_argument("--out")
201
+ for p in (c, g):
202
+ p.add_argument("--kit")
203
+ c.add_argument("--keep-frames", action="store_true", help="keep the 120 PNG frames next to scene.mp4")
204
+ g.add_argument("--pixel", action="append", default=[], help="a pp sprite dir (out/<name>) to walk the scene too")
205
+ sv = sub.add_parser("serve")
206
+ sv.add_argument("--layout", default="hollow_brook"); sv.add_argument("--dir"); sv.add_argument("--port", type=int, default=8765)
207
+ sv.add_argument("--no-open", action="store_true")
208
+ h = sub.add_parser("hilltop")
209
+ h.add_argument("--size", default="1280x720"); h.add_argument("--pixel", action="store_true")
210
+ h.add_argument("--frames", type=int, default=0); h.add_argument("--out")
211
+ for name in ("assets", "styles", "layouts", "doctor"):
212
+ sub.add_parser(name).add_argument("--json", action="store_true")
213
+ a = ap.parse_args(argv)
214
+ return {"kit": cmd_kit, "compose": cmd_compose, "game": cmd_game, "hilltop": cmd_hilltop, "serve": cmd_serve, "assets": cmd_assets,
215
+ "styles": lambda a: _list("styles", a), "layouts": lambda a: _list("layouts", a), "doctor": cmd_doctor}[a.cmd](a)
216
+
217
+
218
+ if __name__ == "__main__":
219
+ sys.exit(main())