picopilot 0.0.0 → 0.1.1

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 (334) hide show
  1. package/LICENSE +661 -0
  2. package/dist/bin.d.ts +3 -0
  3. package/dist/bin.d.ts.map +1 -0
  4. package/dist/bin.js +4 -0
  5. package/dist/bin.js.map +1 -0
  6. package/dist/cli.d.ts +15 -0
  7. package/dist/cli.d.ts.map +1 -0
  8. package/dist/cli.js +59 -0
  9. package/dist/cli.js.map +1 -0
  10. package/dist/commands/audio.d.ts +17 -0
  11. package/dist/commands/audio.d.ts.map +1 -0
  12. package/dist/commands/audio.js +315 -0
  13. package/dist/commands/audio.js.map +1 -0
  14. package/dist/commands/export.d.ts +36 -0
  15. package/dist/commands/export.d.ts.map +1 -0
  16. package/dist/commands/export.js +257 -0
  17. package/dist/commands/export.js.map +1 -0
  18. package/dist/commands/gfx.d.ts +28 -0
  19. package/dist/commands/gfx.d.ts.map +1 -0
  20. package/dist/commands/gfx.js +521 -0
  21. package/dist/commands/gfx.js.map +1 -0
  22. package/dist/commands/init.d.ts +34 -0
  23. package/dist/commands/init.d.ts.map +1 -0
  24. package/dist/commands/init.js +151 -0
  25. package/dist/commands/init.js.map +1 -0
  26. package/dist/commands/lint.d.ts +20 -0
  27. package/dist/commands/lint.d.ts.map +1 -0
  28. package/dist/commands/lint.js +90 -0
  29. package/dist/commands/lint.js.map +1 -0
  30. package/dist/commands/minify.d.ts +20 -0
  31. package/dist/commands/minify.d.ts.map +1 -0
  32. package/dist/commands/minify.js +129 -0
  33. package/dist/commands/minify.js.map +1 -0
  34. package/dist/commands/music.d.ts +13 -0
  35. package/dist/commands/music.d.ts.map +1 -0
  36. package/dist/commands/music.js +241 -0
  37. package/dist/commands/music.js.map +1 -0
  38. package/dist/commands/playtest.d.ts +29 -0
  39. package/dist/commands/playtest.d.ts.map +1 -0
  40. package/dist/commands/playtest.js +631 -0
  41. package/dist/commands/playtest.js.map +1 -0
  42. package/dist/commands/run.d.ts +30 -0
  43. package/dist/commands/run.d.ts.map +1 -0
  44. package/dist/commands/run.js +224 -0
  45. package/dist/commands/run.js.map +1 -0
  46. package/dist/commands/serve.d.ts +50 -0
  47. package/dist/commands/serve.d.ts.map +1 -0
  48. package/dist/commands/serve.js +213 -0
  49. package/dist/commands/serve.js.map +1 -0
  50. package/dist/commands/sfx.d.ts +13 -0
  51. package/dist/commands/sfx.d.ts.map +1 -0
  52. package/dist/commands/sfx.js +254 -0
  53. package/dist/commands/sfx.js.map +1 -0
  54. package/dist/commands/tokens.d.ts +36 -0
  55. package/dist/commands/tokens.d.ts.map +1 -0
  56. package/dist/commands/tokens.js +129 -0
  57. package/dist/commands/tokens.js.map +1 -0
  58. package/dist/commands/verify.d.ts +57 -0
  59. package/dist/commands/verify.d.ts.map +1 -0
  60. package/dist/commands/verify.js +213 -0
  61. package/dist/commands/verify.js.map +1 -0
  62. package/dist/commands/version.d.ts +12 -0
  63. package/dist/commands/version.d.ts.map +1 -0
  64. package/dist/commands/version.js +24 -0
  65. package/dist/commands/version.js.map +1 -0
  66. package/dist/engine/audio/index.d.ts +14 -0
  67. package/dist/engine/audio/index.d.ts.map +1 -0
  68. package/dist/engine/audio/index.js +14 -0
  69. package/dist/engine/audio/index.js.map +1 -0
  70. package/dist/engine/audio/music.d.ts +116 -0
  71. package/dist/engine/audio/music.d.ts.map +1 -0
  72. package/dist/engine/audio/music.js +138 -0
  73. package/dist/engine/audio/music.js.map +1 -0
  74. package/dist/engine/audio/section.d.ts +34 -0
  75. package/dist/engine/audio/section.d.ts.map +1 -0
  76. package/dist/engine/audio/section.js +67 -0
  77. package/dist/engine/audio/section.js.map +1 -0
  78. package/dist/engine/audio/sfx.d.ts +141 -0
  79. package/dist/engine/audio/sfx.d.ts.map +1 -0
  80. package/dist/engine/audio/sfx.js +506 -0
  81. package/dist/engine/audio/sfx.js.map +1 -0
  82. package/dist/engine/cart/cart.d.ts +75 -0
  83. package/dist/engine/cart/cart.d.ts.map +1 -0
  84. package/dist/engine/cart/cart.js +153 -0
  85. package/dist/engine/cart/cart.js.map +1 -0
  86. package/dist/engine/cart/errors.d.ts +29 -0
  87. package/dist/engine/cart/errors.d.ts.map +1 -0
  88. package/dist/engine/cart/errors.js +26 -0
  89. package/dist/engine/cart/errors.js.map +1 -0
  90. package/dist/engine/cart/gfx.d.ts +79 -0
  91. package/dist/engine/cart/gfx.d.ts.map +1 -0
  92. package/dist/engine/cart/gfx.js +171 -0
  93. package/dist/engine/cart/gfx.js.map +1 -0
  94. package/dist/engine/cart/index.d.ts +15 -0
  95. package/dist/engine/cart/index.d.ts.map +1 -0
  96. package/dist/engine/cart/index.js +15 -0
  97. package/dist/engine/cart/index.js.map +1 -0
  98. package/dist/engine/cart/map.d.ts +46 -0
  99. package/dist/engine/cart/map.d.ts.map +1 -0
  100. package/dist/engine/cart/map.js +118 -0
  101. package/dist/engine/cart/map.js.map +1 -0
  102. package/dist/engine/config/config.d.ts +74 -0
  103. package/dist/engine/config/config.d.ts.map +1 -0
  104. package/dist/engine/config/config.js +103 -0
  105. package/dist/engine/config/config.js.map +1 -0
  106. package/dist/engine/config/index.d.ts +7 -0
  107. package/dist/engine/config/index.d.ts.map +1 -0
  108. package/dist/engine/config/index.js +7 -0
  109. package/dist/engine/config/index.js.map +1 -0
  110. package/dist/engine/gfx/grid.d.ts +51 -0
  111. package/dist/engine/gfx/grid.d.ts.map +1 -0
  112. package/dist/engine/gfx/grid.js +116 -0
  113. package/dist/engine/gfx/grid.js.map +1 -0
  114. package/dist/engine/gfx/index.d.ts +17 -0
  115. package/dist/engine/gfx/index.d.ts.map +1 -0
  116. package/dist/engine/gfx/index.js +17 -0
  117. package/dist/engine/gfx/index.js.map +1 -0
  118. package/dist/engine/gfx/label.d.ts +34 -0
  119. package/dist/engine/gfx/label.d.ts.map +1 -0
  120. package/dist/engine/gfx/label.js +190 -0
  121. package/dist/engine/gfx/label.js.map +1 -0
  122. package/dist/engine/gfx/overlap.d.ts +42 -0
  123. package/dist/engine/gfx/overlap.d.ts.map +1 -0
  124. package/dist/engine/gfx/overlap.js +57 -0
  125. package/dist/engine/gfx/overlap.js.map +1 -0
  126. package/dist/engine/gfx/palette.d.ts +29 -0
  127. package/dist/engine/gfx/palette.d.ts.map +1 -0
  128. package/dist/engine/gfx/palette.js +46 -0
  129. package/dist/engine/gfx/palette.js.map +1 -0
  130. package/dist/engine/gfx/png.d.ts +31 -0
  131. package/dist/engine/gfx/png.d.ts.map +1 -0
  132. package/dist/engine/gfx/png.js +165 -0
  133. package/dist/engine/gfx/png.js.map +1 -0
  134. package/dist/engine/gfx/render.d.ts +42 -0
  135. package/dist/engine/gfx/render.d.ts.map +1 -0
  136. package/dist/engine/gfx/render.js +72 -0
  137. package/dist/engine/gfx/render.js.map +1 -0
  138. package/dist/engine/index.d.ts +22 -0
  139. package/dist/engine/index.d.ts.map +1 -0
  140. package/dist/engine/index.js +22 -0
  141. package/dist/engine/index.js.map +1 -0
  142. package/dist/engine/pico8/adapter.d.ts +267 -0
  143. package/dist/engine/pico8/adapter.d.ts.map +1 -0
  144. package/dist/engine/pico8/adapter.js +37 -0
  145. package/dist/engine/pico8/adapter.js.map +1 -0
  146. package/dist/engine/pico8/drive.d.ts +308 -0
  147. package/dist/engine/pico8/drive.d.ts.map +1 -0
  148. package/dist/engine/pico8/drive.js +581 -0
  149. package/dist/engine/pico8/drive.js.map +1 -0
  150. package/dist/engine/pico8/harness.d.ts +146 -0
  151. package/dist/engine/pico8/harness.d.ts.map +1 -0
  152. package/dist/engine/pico8/harness.js +251 -0
  153. package/dist/engine/pico8/harness.js.map +1 -0
  154. package/dist/engine/pico8/index.d.ts +21 -0
  155. package/dist/engine/pico8/index.d.ts.map +1 -0
  156. package/dist/engine/pico8/index.js +21 -0
  157. package/dist/engine/pico8/index.js.map +1 -0
  158. package/dist/engine/pico8/sentinel.d.ts +63 -0
  159. package/dist/engine/pico8/sentinel.d.ts.map +1 -0
  160. package/dist/engine/pico8/sentinel.js +86 -0
  161. package/dist/engine/pico8/sentinel.js.map +1 -0
  162. package/dist/engine/pico8/session-daemon-entry.d.ts +10 -0
  163. package/dist/engine/pico8/session-daemon-entry.d.ts.map +1 -0
  164. package/dist/engine/pico8/session-daemon-entry.js +11 -0
  165. package/dist/engine/pico8/session-daemon-entry.js.map +1 -0
  166. package/dist/engine/pico8/session-daemon-main.d.ts +44 -0
  167. package/dist/engine/pico8/session-daemon-main.d.ts.map +1 -0
  168. package/dist/engine/pico8/session-daemon-main.js +71 -0
  169. package/dist/engine/pico8/session-daemon-main.js.map +1 -0
  170. package/dist/engine/pico8/session-daemon.d.ts +68 -0
  171. package/dist/engine/pico8/session-daemon.d.ts.map +1 -0
  172. package/dist/engine/pico8/session-daemon.js +264 -0
  173. package/dist/engine/pico8/session-daemon.js.map +1 -0
  174. package/dist/engine/pico8/session.d.ts +140 -0
  175. package/dist/engine/pico8/session.d.ts.map +1 -0
  176. package/dist/engine/pico8/session.js +250 -0
  177. package/dist/engine/pico8/session.js.map +1 -0
  178. package/dist/engine/pico8/shell.d.ts +103 -0
  179. package/dist/engine/pico8/shell.d.ts.map +1 -0
  180. package/dist/engine/pico8/shell.js +324 -0
  181. package/dist/engine/pico8/shell.js.map +1 -0
  182. package/dist/engine/pico8/supervisor.d.ts +137 -0
  183. package/dist/engine/pico8/supervisor.d.ts.map +1 -0
  184. package/dist/engine/pico8/supervisor.js +140 -0
  185. package/dist/engine/pico8/supervisor.js.map +1 -0
  186. package/dist/engine/scaffold/index.d.ts +8 -0
  187. package/dist/engine/scaffold/index.d.ts.map +1 -0
  188. package/dist/engine/scaffold/index.js +8 -0
  189. package/dist/engine/scaffold/index.js.map +1 -0
  190. package/dist/engine/scaffold/scaffold.d.ts +49 -0
  191. package/dist/engine/scaffold/scaffold.d.ts.map +1 -0
  192. package/dist/engine/scaffold/scaffold.js +259 -0
  193. package/dist/engine/scaffold/scaffold.js.map +1 -0
  194. package/dist/engine/shrinko/adapter.d.ts +167 -0
  195. package/dist/engine/shrinko/adapter.d.ts.map +1 -0
  196. package/dist/engine/shrinko/adapter.js +107 -0
  197. package/dist/engine/shrinko/adapter.js.map +1 -0
  198. package/dist/engine/shrinko/index.d.ts +13 -0
  199. package/dist/engine/shrinko/index.d.ts.map +1 -0
  200. package/dist/engine/shrinko/index.js +13 -0
  201. package/dist/engine/shrinko/index.js.map +1 -0
  202. package/dist/engine/shrinko/shell.d.ts +69 -0
  203. package/dist/engine/shrinko/shell.d.ts.map +1 -0
  204. package/dist/engine/shrinko/shell.js +156 -0
  205. package/dist/engine/shrinko/shell.js.map +1 -0
  206. package/dist/engine/skills/index.d.ts +8 -0
  207. package/dist/engine/skills/index.d.ts.map +1 -0
  208. package/dist/engine/skills/index.js +8 -0
  209. package/dist/engine/skills/index.js.map +1 -0
  210. package/dist/engine/skills/skills.d.ts +104 -0
  211. package/dist/engine/skills/skills.d.ts.map +1 -0
  212. package/dist/engine/skills/skills.js +149 -0
  213. package/dist/engine/skills/skills.js.map +1 -0
  214. package/dist/index.d.ts +10 -0
  215. package/dist/index.d.ts.map +1 -0
  216. package/dist/index.js +10 -0
  217. package/dist/index.js.map +1 -0
  218. package/dist/version.d.ts +2 -0
  219. package/dist/version.d.ts.map +1 -0
  220. package/dist/version.js +19 -0
  221. package/dist/version.js.map +1 -0
  222. package/package.json +48 -2
  223. package/src/bin.ts +4 -0
  224. package/src/cli.ts +61 -0
  225. package/src/commands/audio.test.ts +280 -0
  226. package/src/commands/audio.ts +410 -0
  227. package/src/commands/export.test.ts +310 -0
  228. package/src/commands/export.ts +316 -0
  229. package/src/commands/gfx-io.test.ts +178 -0
  230. package/src/commands/gfx.test.ts +470 -0
  231. package/src/commands/gfx.ts +645 -0
  232. package/src/commands/init.test.ts +482 -0
  233. package/src/commands/init.ts +193 -0
  234. package/src/commands/lint.test.ts +137 -0
  235. package/src/commands/lint.ts +117 -0
  236. package/src/commands/minify.test.ts +166 -0
  237. package/src/commands/minify.ts +156 -0
  238. package/src/commands/music.test.ts +272 -0
  239. package/src/commands/music.ts +288 -0
  240. package/src/commands/playtest.test.ts +317 -0
  241. package/src/commands/playtest.ts +773 -0
  242. package/src/commands/run.test.ts +264 -0
  243. package/src/commands/run.ts +285 -0
  244. package/src/commands/serve.test.ts +227 -0
  245. package/src/commands/serve.ts +272 -0
  246. package/src/commands/sfx.test.ts +349 -0
  247. package/src/commands/sfx.ts +300 -0
  248. package/src/commands/tokens.test.ts +212 -0
  249. package/src/commands/tokens.ts +168 -0
  250. package/src/commands/verify.test.ts +238 -0
  251. package/src/commands/verify.ts +256 -0
  252. package/src/commands/version.test.ts +70 -0
  253. package/src/commands/version.ts +24 -0
  254. package/src/engine/audio/index.ts +47 -0
  255. package/src/engine/audio/music.test.ts +188 -0
  256. package/src/engine/audio/music.ts +218 -0
  257. package/src/engine/audio/section.test.ts +124 -0
  258. package/src/engine/audio/section.ts +79 -0
  259. package/src/engine/audio/sfx.test.ts +410 -0
  260. package/src/engine/audio/sfx.ts +694 -0
  261. package/src/engine/cart/cart.test.ts +304 -0
  262. package/src/engine/cart/cart.ts +211 -0
  263. package/src/engine/cart/errors.ts +39 -0
  264. package/src/engine/cart/fixtures/crlf.p8 +6 -0
  265. package/src/engine/cart/fixtures/full.p8 +174 -0
  266. package/src/engine/cart/fixtures/header-only.p8 +2 -0
  267. package/src/engine/cart/fixtures/malformed-no-header.p8 +2 -0
  268. package/src/engine/cart/fixtures/minimal.p8 +10 -0
  269. package/src/engine/cart/fixtures/no-trailing-newline.p8 +6 -0
  270. package/src/engine/cart/gfx.ts +195 -0
  271. package/src/engine/cart/index.ts +27 -0
  272. package/src/engine/cart/map.ts +134 -0
  273. package/src/engine/config/config.ts +115 -0
  274. package/src/engine/config/index.ts +12 -0
  275. package/src/engine/gfx/grid.test.ts +128 -0
  276. package/src/engine/gfx/grid.ts +136 -0
  277. package/src/engine/gfx/index.ts +34 -0
  278. package/src/engine/gfx/label.test.ts +105 -0
  279. package/src/engine/gfx/label.ts +229 -0
  280. package/src/engine/gfx/overlap.test.ts +71 -0
  281. package/src/engine/gfx/overlap.ts +66 -0
  282. package/src/engine/gfx/palette.test.ts +35 -0
  283. package/src/engine/gfx/palette.ts +54 -0
  284. package/src/engine/gfx/png.test.ts +111 -0
  285. package/src/engine/gfx/png.ts +196 -0
  286. package/src/engine/gfx/render.test.ts +180 -0
  287. package/src/engine/gfx/render.ts +106 -0
  288. package/src/engine/index.ts +21 -0
  289. package/src/engine/pico8/adapter.ts +303 -0
  290. package/src/engine/pico8/drive.test.ts +415 -0
  291. package/src/engine/pico8/drive.ts +729 -0
  292. package/src/engine/pico8/harness.test.ts +181 -0
  293. package/src/engine/pico8/harness.ts +354 -0
  294. package/src/engine/pico8/index.ts +20 -0
  295. package/src/engine/pico8/sentinel.test.ts +80 -0
  296. package/src/engine/pico8/sentinel.ts +93 -0
  297. package/src/engine/pico8/session-daemon-entry.ts +12 -0
  298. package/src/engine/pico8/session-daemon-main.ts +98 -0
  299. package/src/engine/pico8/session-daemon.test.ts +212 -0
  300. package/src/engine/pico8/session-daemon.ts +306 -0
  301. package/src/engine/pico8/session.test.ts +239 -0
  302. package/src/engine/pico8/session.ts +321 -0
  303. package/src/engine/pico8/shell.test.ts +583 -0
  304. package/src/engine/pico8/shell.ts +412 -0
  305. package/src/engine/pico8/supervisor.test.ts +105 -0
  306. package/src/engine/pico8/supervisor.ts +197 -0
  307. package/src/engine/scaffold/index.ts +15 -0
  308. package/src/engine/scaffold/scaffold.ts +278 -0
  309. package/src/engine/shrinko/adapter.ts +241 -0
  310. package/src/engine/shrinko/fixtures/count-no-compressed.txt +2 -0
  311. package/src/engine/shrinko/fixtures/count-over-budget.txt +3 -0
  312. package/src/engine/shrinko/fixtures/count-under-budget.txt +3 -0
  313. package/src/engine/shrinko/index.ts +33 -0
  314. package/src/engine/shrinko/shell.ts +224 -0
  315. package/src/engine/shrinko/shrinko.test.ts +182 -0
  316. package/src/engine/skills/index.ts +14 -0
  317. package/src/engine/skills/skills.test.ts +376 -0
  318. package/src/engine/skills/skills.ts +205 -0
  319. package/src/index.ts +13 -0
  320. package/src/skills/game-design-reference/SKILL.md +70 -0
  321. package/src/skills/game-jam/SKILL.md +45 -0
  322. package/src/skills/picopilot-art/SKILL.md +62 -0
  323. package/src/skills/picopilot-audio/SKILL.md +194 -0
  324. package/src/skills/picopilot-code/SKILL.md +97 -0
  325. package/src/skills/picopilot-code/reference/README.md +42 -0
  326. package/src/skills/picopilot-code/reference/mode7-racing.md +95 -0
  327. package/src/skills/picopilot-code/reference/platformer.md +119 -0
  328. package/src/skills/picopilot-code/reference/puzzle-grid.md +106 -0
  329. package/src/skills/picopilot-code/reference/rpg-menus-dialog.md +122 -0
  330. package/src/skills/picopilot-code/reference/top-down-adventure.md +117 -0
  331. package/src/skills/picopilot-code/reference/twin-stick-arcade.md +128 -0
  332. package/src/skills/picopilot-debug/SKILL.md +195 -0
  333. package/src/skills/picopilot-overview/SKILL.md +52 -0
  334. package/src/version.ts +22 -0
@@ -0,0 +1,254 @@
1
+ import { existsSync, readFileSync, writeFileSync } from 'node:fs';
2
+ import { isAbsolute, resolve } from 'node:path';
3
+ import { Cli, z } from 'incur';
4
+ import { AudioMmlError, mergeSfxRow, mmlToSfxRow, PITCH_MAX, SFX_MAX_ROWS, SFX_SLOT_COUNT, } from '../engine/audio/index.js';
5
+ import { Cart, CartParseError } from '../engine/cart/index.js';
6
+ /** Resolves a path argument to an absolute path (relative to cwd). */
7
+ function resolvePath(p) {
8
+ return isAbsolute(p) ? p : resolve(process.cwd(), p);
9
+ }
10
+ /**
11
+ * Loads + parses a cart, returning either the parsed cart or a structured reason
12
+ * the caller turns into an `error(...)`. Mirrors the gfx command's loader.
13
+ */
14
+ function loadCart(cartPath) {
15
+ if (!existsSync(cartPath)) {
16
+ return {
17
+ ok: false,
18
+ code: 'cart-not-found',
19
+ message: `no cart at ${cartPath}`,
20
+ };
21
+ }
22
+ const text = readFileSync(cartPath, 'utf8');
23
+ try {
24
+ return { ok: true, cart: Cart.parse(text) };
25
+ }
26
+ catch (e) {
27
+ if (e instanceof CartParseError) {
28
+ return {
29
+ ok: false,
30
+ code: 'cart-parse-failed',
31
+ message: `cart at ${cartPath} does not parse (${e.code}): ${e.message}`,
32
+ };
33
+ }
34
+ throw e;
35
+ }
36
+ }
37
+ /**
38
+ * Builds the `sfx` command group. Currently just `sfx from-mml` (the
39
+ * picopilot-MML to `__sfx__` transpiler + cart merge).
40
+ */
41
+ function buildSfxGroup() {
42
+ const sfx = Cli.create('sfx', {
43
+ description: 'Compose SFX as text: transpile picopilot-MML into a cart __sfx__ slot. Durations are TRACKER ROWS at one SFX speed, NOT tempo/BPM (PICO-8 has no per-note duration).',
44
+ });
45
+ sfx.command('from-mml', {
46
+ description: 'Transpile picopilot-MML text into one __sfx__ slot (0-63) and merge it into the cart (only that slot changes). picopilot-MML is TRACKER-ROW based: a length is a ROW COUNT (l4 = 4 rows), NOT a musical note value, and there is NO tempo/BPM (PICO-8 measures in ticks/row via `s<N>` speed). Notes carry per-note waveform (@0-7, @8-f = custom SFX-instrument), volume (v0-7), and effect (e0-7) that a score notation cannot express. SFX-level FILTER directives reach DESIGNED sounds (explosion/engine/pad): !dampen[2] (low-pass body), !reverb (echo tail), !noiz (@6 white noise), !buzz, !detune1/!detune2. Out-of-range pitch (below C0 / above D#5), >32 rows, and a bad filter level are refused loudly, never silently clamped.',
47
+ args: z.object({
48
+ slot: z.coerce
49
+ .number()
50
+ .int()
51
+ .min(0)
52
+ .max(SFX_SLOT_COUNT - 1)
53
+ .describe(`The target SFX slot 0-${SFX_SLOT_COUNT - 1} to write.`),
54
+ mml: z
55
+ .string()
56
+ .optional()
57
+ .describe('The picopilot-MML source (e.g. "s16 @1 v6 c d e f"). Omit and pass --file to read from a file instead. Modal state: @ waveform, v volume, e effect, o octave, l default length (all sticky). Durations are ROWS: c4 = 4 rows (held via tie rows). Loop with {..}; a lone { marks the pattern LEN. SFX filters (per-SFX, place anywhere): !dampen/!dampen2, !reverb, !noiz, !buzz, !detune1/!detune2 (e.g. "!dampen !reverb @6 v7 o3 e c o1 g" = an explosion).'),
58
+ }),
59
+ options: z.object({
60
+ cart: z
61
+ .string()
62
+ .default('main.p8')
63
+ .describe('The .p8 cart to merge into. Defaults to main.p8 in the current folder. (An option, not a positional, so it never collides with an omitted `mml` when using --file.)'),
64
+ file: z
65
+ .string()
66
+ .optional()
67
+ .describe('Read the picopilot-MML from this file instead of the `mml` argument (for longer sources).'),
68
+ }),
69
+ output: z.object({
70
+ slot: z.number().describe('The SFX slot that was written.'),
71
+ cart: z
72
+ .string()
73
+ .describe('The absolute path of the cart that was written.'),
74
+ rows: z
75
+ .number()
76
+ .describe('The number of tracker rows the MML produced (0-32).'),
77
+ speed: z
78
+ .number()
79
+ .describe('The SFX speed (ticks/row) written into the header.'),
80
+ loopStart: z
81
+ .number()
82
+ .describe('The header loop-start value (row index, or a LEN).'),
83
+ loopEnd: z
84
+ .number()
85
+ .describe('The header loop-end value (0 = off or the LEN special case).'),
86
+ filters: z
87
+ .number()
88
+ .describe('The per-SFX FILTER byte (first header byte): the OR of the selected filters, 0 = none.'),
89
+ row: z
90
+ .string()
91
+ .describe('The 168-char __sfx__ text row that was merged.'),
92
+ warnings: z
93
+ .array(z.string())
94
+ .describe('Advisory warnings (e.g. a dotted length whose row count was rounded).'),
95
+ }),
96
+ examples: [
97
+ {
98
+ description: 'A simple square-wave arpeggio into slot 0',
99
+ args: { slot: 0, mml: 's8 @3 v6 c e g > c' },
100
+ },
101
+ {
102
+ description: 'A held note (4 rows) with vibrato into slot 5 of a cart',
103
+ args: { slot: 5, mml: 's16 @1 v5 e2 c4' },
104
+ options: { cart: 'game.p8' },
105
+ },
106
+ {
107
+ description: 'A DESIGNED explosion: DAMPEN body + REVERB tail on @6 noise',
108
+ args: { slot: 3, mml: 's7 !dampen2 !reverb @6 v7 o3 e c o1 g c' },
109
+ },
110
+ {
111
+ description: 'Read a longer source from a file',
112
+ args: { slot: 1 },
113
+ options: { file: 'lead.mml' },
114
+ },
115
+ ],
116
+ run({ args, options, error, ok }) {
117
+ // Resolve the MML source: the `mml` arg OR `--file` (exactly one). This is
118
+ // the input surface; both are pure text, shrinko-free + PICO-8-free.
119
+ let mml;
120
+ if (options.file !== undefined) {
121
+ if (args.mml !== undefined) {
122
+ return error({
123
+ code: 'ambiguous-input',
124
+ message: 'pass EITHER the mml argument OR --file, not both (they are two ways to give the same source).',
125
+ exitCode: 1,
126
+ });
127
+ }
128
+ const filePath = resolvePath(options.file);
129
+ if (!existsSync(filePath)) {
130
+ return error({
131
+ code: 'mml-file-not-found',
132
+ message: `no picopilot-MML file at ${filePath}`,
133
+ exitCode: 1,
134
+ });
135
+ }
136
+ mml = readFileSync(filePath, 'utf8');
137
+ }
138
+ else if (args.mml !== undefined) {
139
+ mml = args.mml;
140
+ }
141
+ else {
142
+ return error({
143
+ code: 'no-mml',
144
+ message: 'no picopilot-MML source: pass it as the `mml` argument or via --file.',
145
+ exitCode: 1,
146
+ });
147
+ }
148
+ // Transpile FIRST: a bad MML is a caller error and must never touch the
149
+ // cart (no bytes change on a refusal), mirroring `gfx set`'s grid-first
150
+ // discipline.
151
+ let result;
152
+ try {
153
+ result = mmlToSfxRow(mml);
154
+ }
155
+ catch (e) {
156
+ if (e instanceof AudioMmlError) {
157
+ // The structured refusals + parse errors surface as incur's error
158
+ // envelope with a nonzero exit (never a silent clamp). The CTA points
159
+ // at the range/cap/level the author must respect.
160
+ const cta = e.code === 'audio-mml-pitch-out-of-range'
161
+ ? {
162
+ description: 'PICO-8 pitch is a hard range; author within it:',
163
+ commands: [
164
+ {
165
+ command: 'sfx from-mml <slot> "<mml>"',
166
+ description: `Use octaves that keep every note within C0..D#5 (0..${PITCH_MAX}).`,
167
+ },
168
+ ],
169
+ }
170
+ : e.code === 'audio-mml-sfx-overflow'
171
+ ? {
172
+ description: `One SFX holds at most ${SFX_MAX_ROWS} rows; split longer melodies:`,
173
+ commands: [
174
+ {
175
+ command: 'sfx from-mml <other-slot> "<rest>"',
176
+ description: 'Author the remainder as another SFX, then arrange both with `music from-patterns`.',
177
+ },
178
+ ],
179
+ }
180
+ : e.code === 'audio-mml-filter-level-out-of-range'
181
+ ? {
182
+ description: 'The SFX filters + their valid levels:',
183
+ commands: [
184
+ {
185
+ command: 'sfx from-mml <slot> "!dampen[2] !reverb !noiz !buzz !detune1/!detune2 ..."',
186
+ description: '!dampen has 2 levels (!dampen/!dampen2); !reverb/!noiz/!buzz are on/off; !detune needs 1 or 2.',
187
+ },
188
+ ],
189
+ }
190
+ : undefined;
191
+ return error({
192
+ code: e.code,
193
+ message: e.message,
194
+ exitCode: 1,
195
+ ...(cta ? { cta } : {}),
196
+ });
197
+ }
198
+ throw e;
199
+ }
200
+ const cartPath = resolvePath(options.cart);
201
+ const loaded = loadCart(cartPath);
202
+ if (!loaded.ok) {
203
+ return error({ code: loaded.code, message: loaded.message, exitCode: 1 });
204
+ }
205
+ // Merge into the target slot ONLY, through the cart model, so every other
206
+ // section (and every other SFX slot) stays byte-identical.
207
+ mergeSfxRow(loaded.cart, args.slot, result.row);
208
+ writeFileSync(cartPath, loaded.cart.serialize());
209
+ const warnings = result.roundingWarnings.map((w) => `length "${w.token}" = ${w.exactRows} rows rounded to ${w.roundedRows} (rows are whole; PICO-8 has no fractional-length notes).`);
210
+ // CTA toward the verify/hear loop: a merged SFX is next confirmed by
211
+ // verify (static integrity) and heard via `audio render`.
212
+ return ok({
213
+ slot: args.slot,
214
+ cart: cartPath,
215
+ rows: result.rows,
216
+ speed: result.speed,
217
+ loopStart: result.loopStart,
218
+ loopEnd: result.loopEnd,
219
+ filters: result.filters,
220
+ row: result.row,
221
+ warnings,
222
+ }, {
223
+ cta: {
224
+ description: 'The SFX is merged. Confirm it, then hear it:',
225
+ commands: [
226
+ {
227
+ command: 'verify',
228
+ description: 'Static gate: the cart still parses + is within budget.',
229
+ },
230
+ {
231
+ command: 'audio render',
232
+ description: 'Record a WAV to hear the result (real-time capture, requires PICO-8).',
233
+ },
234
+ ],
235
+ },
236
+ });
237
+ },
238
+ });
239
+ return sfx;
240
+ }
241
+ /**
242
+ * Registers the `sfx` command group (`sfx from-mml`) on the root CLI.
243
+ *
244
+ * `sfx` is a shrinko-FREE and PICO-8-FREE command group: it transpiles
245
+ * picopilot-MML to `__sfx__` and merges it through the cart model entirely in
246
+ * TS, so it works with both absent and never spawns a child process. The
247
+ * picopilot-MML dialect (ADR-0005 + the audio spike finding) is TRACKER-ROW
248
+ * based: durations are row counts at one SFX speed, NOT tempo (ADR-0008), and
249
+ * out-of-range pitch / >32 rows are structured refusals, never silent clamps.
250
+ */
251
+ export function registerSfx(cli) {
252
+ cli.command(buildSfxGroup());
253
+ }
254
+ //# sourceMappingURL=sfx.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"sfx.js","sourceRoot":"","sources":["../../src/commands/sfx.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,36 @@
1
+ import { type Cli } from 'incur';
2
+ import { type ShrinkoAdapter } from '../engine/shrinko/index.js';
3
+ /** PICO-8's Lua token budget. A cart over this will not load in PICO-8 (US #3). */
4
+ export declare const TOKEN_BUDGET = 8192;
5
+ /**
6
+ * Builds the {@link ShrinkoAdapter} a command uses, given the incur-resolved
7
+ * env. The default is the shell-out {@link ShellShrinkoAdapter}, whose child
8
+ * process inherits ONLY this `env` (its `PATH` is the test isolation lever).
9
+ *
10
+ * A test passes its own factory to {@link registerTokens} to SWAP a stub
11
+ * adapter (the seam-swap contract): the command's public output/exit must be
12
+ * unchanged whether the real shell adapter or a native-TS stub is behind the
13
+ * seam, because the command only ever talks to the {@link ShrinkoAdapter}
14
+ * interface.
15
+ */
16
+ export type ShrinkoAdapterFactory = (env: NodeJS.ProcessEnv) => ShrinkoAdapter;
17
+ /**
18
+ * Registers `picopilot tokens` (US #3), the first shrinko-backed command.
19
+ *
20
+ * It reports `{tokens, pct, chars, compressed}` for a cart against the 8,192
21
+ * token budget via shrinko8 `--count`, and CTAs to `minify` when over budget so
22
+ * the agent is led straight to the fix (the detect-then-fix loop, ADR CTAs).
23
+ *
24
+ * Two-tier contract (ADR-0002): `tokens` is shrinko-BACKED, so with shrinko
25
+ * ABSENT it returns the structured `shrinko-not-found` error (nonzero exit,
26
+ * exact remedy `uv pip install shrinko`) rather than crashing or reporting a
27
+ * hollow zero. The dependency is reached entirely through the {@link
28
+ * ShrinkoAdapter} seam, so a native-TS shrinko would satisfy this command
29
+ * unchanged.
30
+ *
31
+ * @param adapterFactory injects the adapter (defaults to the shell adapter);
32
+ * tests pass a stub to swap the seam and to drive present/absent without the
33
+ * real binary.
34
+ */
35
+ export declare function registerTokens(cli: Cli.Cli, adapterFactory?: ShrinkoAdapterFactory): void;
36
+ //# sourceMappingURL=tokens.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"tokens.d.ts","sourceRoot":"","sources":["../../src/commands/tokens.ts"],"names":[],"mappings":"AAEA,OAAO,EAAC,KAAK,GAAG,EAAI,MAAM,OAAO,CAAC;AAElC,OAAO,EACN,KAAK,cAAc,EAGnB,MAAM,4BAA4B,CAAC;AAEpC,mFAAmF;AACnF,eAAO,MAAM,YAAY,OAAO,CAAC;AAEjC;;;;;;;;;;GAUG;AACH,MAAM,MAAM,qBAAqB,GAAG,CAAC,GAAG,EAAE,MAAM,CAAC,UAAU,KAAK,cAAc,CAAC;AAK/E;;;;;;;;;;;;;;;;;GAiBG;AACH,wBAAgB,cAAc,CAC7B,GAAG,EAAE,GAAG,CAAC,GAAG,EACZ,cAAc,GAAE,qBAA6C,GAC3D,IAAI,CAqHN"}
@@ -0,0 +1,129 @@
1
+ import { existsSync } from 'node:fs';
2
+ import { isAbsolute, resolve } from 'node:path';
3
+ import { z } from 'incur';
4
+ import { ShrinkoParseError, ShellShrinkoAdapter, } from '../engine/shrinko/index.js';
5
+ /** PICO-8's Lua token budget. A cart over this will not load in PICO-8 (US #3). */
6
+ export const TOKEN_BUDGET = 8192;
7
+ const defaultAdapterFactory = (env) => new ShellShrinkoAdapter({ env });
8
+ /**
9
+ * Registers `picopilot tokens` (US #3), the first shrinko-backed command.
10
+ *
11
+ * It reports `{tokens, pct, chars, compressed}` for a cart against the 8,192
12
+ * token budget via shrinko8 `--count`, and CTAs to `minify` when over budget so
13
+ * the agent is led straight to the fix (the detect-then-fix loop, ADR CTAs).
14
+ *
15
+ * Two-tier contract (ADR-0002): `tokens` is shrinko-BACKED, so with shrinko
16
+ * ABSENT it returns the structured `shrinko-not-found` error (nonzero exit,
17
+ * exact remedy `uv pip install shrinko`) rather than crashing or reporting a
18
+ * hollow zero. The dependency is reached entirely through the {@link
19
+ * ShrinkoAdapter} seam, so a native-TS shrinko would satisfy this command
20
+ * unchanged.
21
+ *
22
+ * @param adapterFactory injects the adapter (defaults to the shell adapter);
23
+ * tests pass a stub to swap the seam and to drive present/absent without the
24
+ * real binary.
25
+ */
26
+ export function registerTokens(cli, adapterFactory = defaultAdapterFactory) {
27
+ cli.command('tokens', {
28
+ description: 'Count a cart against the 8,192-token PICO-8 budget (via shrinko8 --count). Requires shrinko.',
29
+ args: z.object({
30
+ cart: z
31
+ .string()
32
+ .default('main.p8')
33
+ .describe('The .p8 cart to count. Defaults to main.p8 in the current folder.'),
34
+ }),
35
+ // Declaring PATH here makes incur resolve it from the (test-overridable)
36
+ // env source and hand it to us, so we pass a controlled PATH to the child.
37
+ env: z.object({
38
+ PATH: z
39
+ .string()
40
+ .optional()
41
+ .describe('Used to locate shrinko8 in the child process.'),
42
+ }),
43
+ output: z.object({
44
+ tokens: z.number().describe('The token count shrinko reported.'),
45
+ pct: z
46
+ .number()
47
+ .describe('tokens as a percentage of the 8,192 budget (rounded); >100 means over budget.'),
48
+ chars: z.number().describe('The character count shrinko reported.'),
49
+ compressed: z
50
+ .number()
51
+ .optional()
52
+ .describe('The compressed byte count (absent for some input formats).'),
53
+ budget: z
54
+ .number()
55
+ .describe('The token budget the percentage is against (8192).'),
56
+ overBudget: z.boolean().describe('True when tokens exceed the budget.'),
57
+ }),
58
+ examples: [
59
+ { description: 'Count main.p8' },
60
+ { description: 'Count a specific cart', args: { cart: 'game.p8' } },
61
+ ],
62
+ async run({ args, env, error, ok }) {
63
+ const cartPath = isAbsolute(args.cart)
64
+ ? args.cart
65
+ : resolve(process.cwd(), args.cart);
66
+ // A missing cart is a picopilot-side error, distinct from shrinko absence:
67
+ // surface it clearly instead of letting shrinko fail opaquely on the path.
68
+ if (!existsSync(cartPath)) {
69
+ return error({
70
+ code: 'cart-not-found',
71
+ message: `no cart at ${cartPath}`,
72
+ exitCode: 1,
73
+ });
74
+ }
75
+ const adapter = adapterFactory(env);
76
+ let result;
77
+ try {
78
+ result = await adapter.count(cartPath);
79
+ }
80
+ catch (e) {
81
+ // shrinko RAN but its output was unparseable (version/format drift):
82
+ // a distinct `shrinko-failed`, NOT `shrinko-not-found` (which is absence).
83
+ if (e instanceof ShrinkoParseError) {
84
+ return error({
85
+ code: 'shrinko-failed',
86
+ message: `shrinko ran but its --count output could not be parsed: ${e.message}`,
87
+ exitCode: 1,
88
+ });
89
+ }
90
+ throw e;
91
+ }
92
+ if (!result.ok) {
93
+ // The two-tier structured failure (ADR-0002 / US #17): shrinko absent.
94
+ // The message CARRIES the exact remedy + needs so an agent reading only
95
+ // the message still learns the fix; the code is the machine handle.
96
+ return error({
97
+ code: result.reason,
98
+ message: `shrinko is not installed. ${result.remedy} (needs: ${result.needs.join(', ')})`,
99
+ exitCode: 1,
100
+ });
101
+ }
102
+ const { tokens, chars, compressed } = result.value;
103
+ const pct = Math.round((tokens / TOKEN_BUDGET) * 100);
104
+ const overBudget = tokens > TOKEN_BUDGET;
105
+ // Over budget → CTA to `minify` (the detect-then-fix loop). Under budget
106
+ // needs no CTA: nothing to do.
107
+ const cta = overBudget
108
+ ? {
109
+ description: 'Over the 8,192-token budget. Reclaim tokens with:',
110
+ commands: [
111
+ {
112
+ command: 'minify',
113
+ description: 'Safe-minify the cart to reduce tokens without changing behaviour.',
114
+ },
115
+ ],
116
+ }
117
+ : undefined;
118
+ return ok({
119
+ tokens,
120
+ pct,
121
+ chars,
122
+ compressed,
123
+ budget: TOKEN_BUDGET,
124
+ overBudget,
125
+ }, cta === undefined ? undefined : { cta });
126
+ },
127
+ });
128
+ }
129
+ //# sourceMappingURL=tokens.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"tokens.js","sourceRoot":"","sources":["../../src/commands/tokens.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,57 @@
1
+ import { type Cli } from 'incur';
2
+ import { type ShrinkoAdapter } from '../engine/shrinko/index.js';
3
+ /**
4
+ * The one-line self-scoping the `picopilot verify` envelope always carries, so
5
+ * the connotation of "verify" never misleads an agent into treating a green
6
+ * static gate as terminal (ADR-0003). It states BOTH facts: verify is static,
7
+ * and passing does NOT mean the cart runs.
8
+ */
9
+ export declare const VERIFY_SCOPE: "static gate: tokens + integrity; passing does NOT mean the cart runs";
10
+ /**
11
+ * The exit code for the DISTINCT `gate-incapable` outcome (shrinko absent).
12
+ * Chosen as 2 to be categorically separate from `fail` (exit 1, a cart that
13
+ * flunked a check) and from `pass` (exit 0): an absent capability is neither a
14
+ * green gate nor a failed check but a gate that could not run its most
15
+ * important check, so it gets its own nonzero code an agent can branch on.
16
+ */
17
+ export declare const GATE_INCAPABLE_EXIT = 2;
18
+ /** See {@link ShrinkoAdapterFactory} in `tokens.ts` — verify reuses the same seam. */
19
+ export type ShrinkoAdapterFactory = (env: NodeJS.ProcessEnv) => ShrinkoAdapter;
20
+ /**
21
+ * Registers `picopilot verify` (US #15, #18), the STATIC cart-acceptance gate an
22
+ * agent drives each iteration toward.
23
+ *
24
+ * It runs two static checks and returns ONE structured envelope:
25
+ *
26
+ * - **integrity** — the cart parses and round-trips cleanly through the cart
27
+ * model (well-formed sections). No run.
28
+ * - **tokens** — the cart is within the 8,192-token budget (via the shrinko
29
+ * adapter `--count`).
30
+ *
31
+ * (Lint folds in when the lint task lands; in v1-core verify = tokens +
32
+ * integrity.)
33
+ *
34
+ * Three categorical outcomes (ADR-0003):
35
+ *
36
+ * - **pass** (exit 0) — both checks green. The envelope SELF-SCOPES (states it
37
+ * is static and that passing does not mean the cart runs) and CTAs to
38
+ * `picopilot run` ("static checks pass, now confirm it boots"), so the agent
39
+ * is led from well-formed to actually-runs.
40
+ * - **fail** (exit 1) — a check flunked (over budget, or a malformed/non-parsing
41
+ * cart). A structured `verify-failed` error listing which checks failed.
42
+ * - **gate-incapable** (exit {@link GATE_INCAPABLE_EXIT}) — shrinko is ABSENT,
43
+ * so the token check (the #1 failure mode, token bloat) cannot run. verify
44
+ * returns a DISTINCT `gate-incapable` result, NEVER green, categorically
45
+ * separate from pass and fail. This is the gate-as-theatre regression guard:
46
+ * verify must never report green by skipping its most important check.
47
+ *
48
+ * verify is STATIC and never runs the cart, so it has NO PICO-8 dependency (US
49
+ * #19). It reaches shrinko entirely through the {@link ShrinkoAdapter} seam, so
50
+ * a native-TS shrinko would satisfy it unchanged.
51
+ *
52
+ * @param adapterFactory injects the adapter (defaults to the shell adapter);
53
+ * tests pass a stub to drive present / absent / over-budget without the real
54
+ * binary.
55
+ */
56
+ export declare function registerVerify(cli: Cli.Cli, adapterFactory?: ShrinkoAdapterFactory): void;
57
+ //# sourceMappingURL=verify.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"verify.d.ts","sourceRoot":"","sources":["../../src/commands/verify.ts"],"names":[],"mappings":"AAEA,OAAO,EAAC,KAAK,GAAG,EAAI,MAAM,OAAO,CAAC;AAGlC,OAAO,EACN,KAAK,cAAc,EAGnB,MAAM,4BAA4B,CAAC;AAGpC;;;;;GAKG;AACH,eAAO,MAAM,YAAY,EACxB,sEAA+E,CAAC;AAEjF;;;;;;GAMG;AACH,eAAO,MAAM,mBAAmB,IAAI,CAAC;AAErC,sFAAsF;AACtF,MAAM,MAAM,qBAAqB,GAAG,CAAC,GAAG,EAAE,MAAM,CAAC,UAAU,KAAK,cAAc,CAAC;AA2C/E;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAmCG;AACH,wBAAgB,cAAc,CAC7B,GAAG,EAAE,GAAG,CAAC,GAAG,EACZ,cAAc,GAAE,qBAA6C,GAC3D,IAAI,CA8IN"}