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,14 @@
1
+ /**
2
+ * The skills-install engine behind `picopilot init --install-skills`, the one
3
+ * picopilot path that writes to a shared/global location. Every install-target
4
+ * knob is a parameter here so tests redirect it to a temp dir and the real agent
5
+ * skill dirs stay byte-untouched. See `skills.ts` for the isolation contract.
6
+ */
7
+ export {
8
+ installSkills,
9
+ type InstallSkillsOptions,
10
+ type InstallSkillsResult,
11
+ SKILL_NAMES,
12
+ skillsSourceDir,
13
+ type WiredAgent,
14
+ } from './skills.js';
@@ -0,0 +1,376 @@
1
+ import {execFileSync} from 'node:child_process';
2
+ import {
3
+ existsSync,
4
+ mkdtempSync,
5
+ readdirSync,
6
+ readFileSync,
7
+ statSync,
8
+ } from 'node:fs';
9
+ import {homedir, tmpdir} from 'node:os';
10
+ import {join} from 'node:path';
11
+ import {afterEach, beforeEach, describe, expect, it} from 'vitest';
12
+ import {createCli} from '../../cli.js';
13
+ import {installSkills, SKILL_NAMES, skillsSourceDir} from './index.js';
14
+
15
+ /**
16
+ * Reads an authored skill's SKILL.md straight from the on-disk source dir, the
17
+ * same files incur's `include` glob ships. Skill GENERATION output is these
18
+ * files, so the generation tests assert against them directly.
19
+ */
20
+ function readSkill(name: string): string {
21
+ return readFileSync(join(skillsSourceDir(), name, 'SKILL.md'), 'utf8');
22
+ }
23
+
24
+ describe('picopilot skills: generation output (the authored discipline skills)', () => {
25
+ it('ships exactly the named authored skills, each with valid frontmatter', () => {
26
+ expect([...SKILL_NAMES].sort()).toEqual(
27
+ [
28
+ 'picopilot-art',
29
+ 'picopilot-audio',
30
+ 'picopilot-code',
31
+ 'picopilot-debug',
32
+ 'picopilot-overview',
33
+ 'game-design-reference',
34
+ 'game-jam',
35
+ ].sort(),
36
+ );
37
+ for (const name of SKILL_NAMES) {
38
+ const md = readSkill(name);
39
+ // incur discovers a skill by its `name:` frontmatter line; a `description:`
40
+ // is what makes it load on-demand. Both must be present and match.
41
+ expect(md).toMatch(new RegExp(`^name:\\s*${name}$`, 'm'));
42
+ expect(md).toMatch(/^description:\s*.+/m);
43
+ }
44
+ });
45
+
46
+ it('game-design-reference is a USER-INVOKED reference body (no per-turn context load)', () => {
47
+ const md = readSkill('game-design-reference');
48
+ // The invocation stance: only game-design-reference disables model
49
+ // invocation, so it is reached by pointer (from game-jam), never fired on a
50
+ // vague trigger, and costs no context every turn. game-jam stays
51
+ // model-invoked (no such line).
52
+ expect(md).toMatch(/^disable-model-invocation:\s*true$/m);
53
+ expect(readSkill('game-jam')).not.toMatch(/^disable-model-invocation:/m);
54
+ });
55
+
56
+ it('game-design-reference carries the universal principles + method-not-menu originality', () => {
57
+ const md = readSkill('game-design-reference');
58
+ // Fairness: BOTH reachability AND the skipped hazard-avoidability check.
59
+ expect(md.toLowerCase()).toContain('reachability');
60
+ expect(md.toLowerCase()).toContain('avoidability');
61
+ // Human reaction budget in PICO-8's 30fps terms.
62
+ expect(md).toContain('200-300ms');
63
+ expect(md).toContain('frame-perfect');
64
+ // Visibility (draw what you spr()).
65
+ expect(md).toContain('spr(');
66
+ // Originality is a METHOD with a self-check, and lists NO concrete
67
+ // mechanics (the answer-menu trap). Assert the caveat that guards it.
68
+ expect(md.toLowerCase()).toContain('self-check');
69
+ expect(md.toLowerCase()).toMatch(/no (example |concrete )?mechanic/);
70
+ // Progression as a universal principle: a game GOES SOMEWHERE, expressed
71
+ // as a ladder of FORMS (ramp -> waves -> levels -> worlds). The situated
72
+ // "how far up given the clock" call lives in game-jam, not here.
73
+ expect(md.toLowerCase()).toContain('progression');
74
+ expect(md.toLowerCase()).toContain('goes somewhere');
75
+ expect(md.toLowerCase()).toContain('waves');
76
+ expect(md.toLowerCase()).toMatch(/worlds/);
77
+ });
78
+
79
+ it('game-jam carries the clock discipline and POINTS AT game-design-reference (does not re-teach it)', () => {
80
+ const md = readSkill('game-jam');
81
+ // Composition: it points at the reference body by name.
82
+ expect(md).toContain('game-design-reference');
83
+ // REGRESSION GUARD (see the FLIPRUN observation + re-grill): a
84
+ // disable-model-invocation reference skill is NOT auto-loaded by pi, so a
85
+ // bare-name "read and apply X" pointer gets SKIPPED (the agent never opens
86
+ // the file). The reliable pattern (Matt's, verified on pi + Claude Code) is
87
+ // an EXPLICIT relative path resolved against THIS skill's dir + an
88
+ // imperative to read/load it. Assert the pointer has both, so it never
89
+ // regresses to the weak wording that made the reference body unreachable.
90
+ expect(md).toContain('../game-design-reference/SKILL.md');
91
+ expect(md.toLowerCase()).toMatch(/\b(read|load)\b/);
92
+ // And game-design-reference stays user-invoked (the reason the explicit
93
+ // pointer is REQUIRED): if someone flips it to model-invoked, this pairing
94
+ // assumption changes and the pointer guard should be revisited.
95
+ expect(readSkill('game-design-reference')).toMatch(
96
+ /^disable-model-invocation:\s*true$/m,
97
+ );
98
+ // The jam-specific clock discipline (the slice/deepen/triage phases).
99
+ expect(md.toLowerCase()).toContain('playable slice');
100
+ expect(md.toLowerCase()).toContain('triage');
101
+ // The situated design calls the clock forces (jam-specific, live nowhere
102
+ // else): the dead state and the frame-perfect superhuman level.
103
+ expect(md.toLowerCase()).toContain('dead state');
104
+ expect(md).toContain('frame-perfect');
105
+ // Budget-scaled ambition (grilled: one skill, no per-tier files): scale to
106
+ // the clock, reach up the progression ladder as far as you can FINISH,
107
+ // deepen > polish on a long budget. Intent-framed, not a level-count menu.
108
+ expect(md.toLowerCase()).toContain('scale your ambition');
109
+ expect(md.toLowerCase()).toContain('progression ladder');
110
+ expect(md.toLowerCase()).toMatch(/finish/);
111
+ // It interprets the theme itself (decision B) and ships JAM.md.
112
+ expect(md).toContain('JAM.md');
113
+ });
114
+
115
+ it('picopilot-overview carries the #include discipline', () => {
116
+ const md = readSkill('picopilot-overview');
117
+ expect(md).toContain('#include main.lua');
118
+ expect(md).toContain('main.lua');
119
+ // Edit the Lua, never the binary sections (allow line wrapping between words).
120
+ expect(md).toMatch(/never\s+hand-write/i);
121
+ expect(md).toContain('__gfx__');
122
+ });
123
+
124
+ it('picopilot-code carries the token-breakdown discipline', () => {
125
+ const md = readSkill('picopilot-code');
126
+ expect(md).toContain('8,192');
127
+ expect(md).toContain('picopilot tokens');
128
+ expect(md).toContain('picopilot minify');
129
+ // A PICO-8 shorthand the skill teaches for reclaiming budget.
130
+ expect(md).toContain('+=');
131
+ });
132
+
133
+ it('picopilot-art carries the render→look→set→render loop AND the non-multimodal fallback', () => {
134
+ const md = readSkill('picopilot-art');
135
+ // The art loop.
136
+ expect(md).toContain('gfx render');
137
+ expect(md).toContain('gfx set');
138
+ expect(md).toContain('gfx show');
139
+ // The load-bearing non-multimodal fallback: view the PNG if you can read
140
+ // images, else reason over the grid.
141
+ expect(md.toLowerCase()).toContain('cannot read images');
142
+ expect(md.toLowerCase()).toMatch(/reason(ing)?\s+over the[\s\S]*grid/i);
143
+ // The gfx/map overlap smart-refuse discipline.
144
+ expect(md.toLowerCase()).toContain('overlap');
145
+ expect(md).toContain('allowMapOverlap');
146
+ });
147
+
148
+ it('picopilot-audio carries the picopilot-MML authoring model + the honest record-based render framing', () => {
149
+ const md = readSkill('picopilot-audio');
150
+ expect(md).toContain('picopilot-MML');
151
+ // ABC is explicitly NOT used.
152
+ expect(md).toContain('ABC');
153
+ // Music is assembled structurally from SFX references.
154
+ expect(md.toLowerCase()).toContain('structural');
155
+ // Honest scoping (ADR-0009): audio-to-WAV is a RECORDING, NOT an offline
156
+ // export, and needs a real audio session (a developer machine).
157
+ expect(md.toLowerCase()).toContain('recording');
158
+ expect(md.toLowerCase()).toContain('offline export');
159
+ });
160
+
161
+ it('picopilot-debug carries the static-gate + run loop and the dependency boundaries', () => {
162
+ const md = readSkill('picopilot-debug');
163
+ expect(md).toContain('picopilot verify');
164
+ expect(md).toContain('picopilot run');
165
+ expect(md).toContain('printh');
166
+ expect(md).toContain('gate-incapable');
167
+ expect(md).toContain('pico8-not-found');
168
+ expect(md).toContain('shrinko-not-found');
169
+ });
170
+ });
171
+
172
+ /**
173
+ * Recursively snapshots a directory tree as a sorted list of `relpath:mtimeMs`
174
+ * for files and `relpath/` for dirs, so a before/after comparison catches ANY
175
+ * new/removed entry AND any modified file. Returns `['<absent>']` when the dir
176
+ * does not exist (so "absent stays absent" is also asserted).
177
+ */
178
+ function snapshot(root: string): string[] {
179
+ if (!existsSync(root)) return ['<absent>'];
180
+ const out: string[] = [];
181
+ const walk = (dir: string, rel: string) => {
182
+ for (const entry of readdirSync(dir, {withFileTypes: true}).sort((a, b) =>
183
+ a.name.localeCompare(b.name),
184
+ )) {
185
+ const relPath = rel ? `${rel}/${entry.name}` : entry.name;
186
+ const abs = join(dir, entry.name);
187
+ if (entry.isDirectory()) {
188
+ out.push(`${relPath}/`);
189
+ walk(abs, relPath);
190
+ } else {
191
+ // lstat, not stat: never follow a symlink out of the snapshotted tree.
192
+ out.push(
193
+ `${relPath}:${statSync(abs, {throwIfNoEntry: false})?.mtimeMs ?? 'gone'}`,
194
+ );
195
+ }
196
+ }
197
+ };
198
+ walk(root, '');
199
+ return out;
200
+ }
201
+
202
+ /**
203
+ * The REAL agent skill dirs this test must prove are byte-untouched. incur
204
+ * installs canonically to `~/.agents/skills` and symlinks detected non-universal
205
+ * agents (e.g. `~/.claude/skills`), so those are the dirs the isolation contract
206
+ * guards. Resolved from the real `homedir()` (which is what incur reads).
207
+ */
208
+ const REAL_DIRS = [
209
+ join(homedir(), '.agents', 'skills'),
210
+ join(homedir(), '.claude', 'skills'),
211
+ join(homedir(), '.codex', 'skills'),
212
+ join(homedir(), '.cursor', 'skills'),
213
+ ];
214
+
215
+ describe('picopilot skills: isolated --install-skills (the load-bearing shared-write test)', () => {
216
+ let target: string;
217
+ let before: Map<string, string[]>;
218
+
219
+ beforeEach(() => {
220
+ target = mkdtempSync(join(tmpdir(), 'picopilot-skills-install-'));
221
+ // Snapshot the REAL dirs before the install so we can prove they never moved.
222
+ before = new Map(REAL_DIRS.map((d) => [d, snapshot(d)]));
223
+ });
224
+
225
+ afterEach(() => {
226
+ try {
227
+ execFileSync('rm', ['-rf', target]);
228
+ } catch {
229
+ // ignore
230
+ }
231
+ });
232
+
233
+ it('installs into the redirected temp target, NOT a real dir', async () => {
234
+ // The lever: global=false + cwd=temp installs under <cwd>/.agents/skills.
235
+ const result = await installSkills({
236
+ cli: createCli(),
237
+ global: false,
238
+ cwd: target,
239
+ });
240
+
241
+ // Every named discipline skill landed (plus the per-command skills).
242
+ for (const name of SKILL_NAMES) {
243
+ expect(result.skills).toContain(name);
244
+ expect(
245
+ existsSync(join(target, '.agents', 'skills', name, 'SKILL.md')),
246
+ ).toBe(true);
247
+ }
248
+ // Every reported canonical path is INSIDE the temp target.
249
+ for (const p of result.paths) {
250
+ expect(p.startsWith(target)).toBe(true);
251
+ }
252
+ // Any wired non-universal agent dir is also inside the temp target, never a
253
+ // real home dir (the symlinks go under cwd for a project install).
254
+ for (const a of result.agents) {
255
+ expect(a.path.startsWith(target)).toBe(true);
256
+ }
257
+ });
258
+
259
+ it('leaves the REAL agent skill dirs BYTE-UNTOUCHED', async () => {
260
+ await installSkills({cli: createCli(), global: false, cwd: target});
261
+
262
+ // The real dirs are identical before and after: no new entry, nothing
263
+ // modified, and an absent dir stays absent. This is the pollution guard.
264
+ for (const dir of REAL_DIRS) {
265
+ expect(snapshot(dir)).toEqual(before.get(dir));
266
+ }
267
+ });
268
+
269
+ it('a custom sourceDir proves the source is decoupled from the install target', async () => {
270
+ // Point the include SOURCE at a fixture with a single skill; install TARGET
271
+ // stays the temp dir. Only that fixture's authored skill (plus the
272
+ // per-command skills) should appear, proving cwd and source are independent.
273
+ const source = mkdtempSync(join(tmpdir(), 'picopilot-skills-src-'));
274
+ try {
275
+ const skillDir = join(source, 'picopilot-fixture');
276
+ execFileSync('mkdir', ['-p', skillDir]);
277
+ execFileSync('sh', [
278
+ '-c',
279
+ `printf '%s\\n' '---' 'name: picopilot-fixture' 'description: A fixture skill.' '---' '# fixture' > ${JSON.stringify(join(skillDir, 'SKILL.md'))}`,
280
+ ]);
281
+
282
+ const result = await installSkills({
283
+ cli: createCli(),
284
+ global: false,
285
+ cwd: target,
286
+ sourceDir: source,
287
+ });
288
+
289
+ expect(result.skills).toContain('picopilot-fixture');
290
+ // The real authored discipline skills are NOT pulled in from this source.
291
+ expect(result.skills).not.toContain('picopilot-overview');
292
+ // Still installed into the temp target, and the real dirs stay untouched.
293
+ expect(
294
+ existsSync(
295
+ join(target, '.agents', 'skills', 'picopilot-fixture', 'SKILL.md'),
296
+ ),
297
+ ).toBe(true);
298
+ for (const dir of REAL_DIRS) {
299
+ expect(snapshot(dir)).toEqual(before.get(dir));
300
+ }
301
+ } finally {
302
+ execFileSync('rm', ['-rf', source]);
303
+ }
304
+ });
305
+
306
+ it('ships skill RESOURCE files, not just SKILL.md (incur 0.4.10 workaround)', async () => {
307
+ // incur's SyncSkills.sync copies only SKILL.md; installSkills backfills the
308
+ // sibling resources. The genre code references under
309
+ // picopilot-code/reference/ must reach the installed skill dir, or the
310
+ // SKILL.md's on-demand pointers dangle.
311
+ await installSkills({cli: createCli(), global: false, cwd: target});
312
+
313
+ const codeSkill = join(target, '.agents', 'skills', 'picopilot-code');
314
+ expect(existsSync(join(codeSkill, 'SKILL.md'))).toBe(true);
315
+ for (const res of [
316
+ 'reference/README.md',
317
+ 'reference/platformer.md',
318
+ 'reference/puzzle-grid.md',
319
+ 'reference/twin-stick-arcade.md',
320
+ 'reference/top-down-adventure.md',
321
+ 'reference/mode7-racing.md',
322
+ 'reference/rpg-menus-dialog.md',
323
+ ]) {
324
+ expect(existsSync(join(codeSkill, res))).toBe(true);
325
+ }
326
+ // The resource content is the authored source byte-for-byte (a real copy).
327
+ expect(
328
+ readFileSync(join(codeSkill, 'reference/platformer.md'), 'utf8'),
329
+ ).toBe(
330
+ readFileSync(
331
+ join(skillsSourceDir(), 'picopilot-code', 'reference/platformer.md'),
332
+ 'utf8',
333
+ ),
334
+ );
335
+ // The real dirs stay untouched (resource copy respects isolation).
336
+ for (const dir of REAL_DIRS) {
337
+ expect(snapshot(dir)).toEqual(before.get(dir));
338
+ }
339
+ });
340
+
341
+ it('copies arbitrary nested resources from a custom source, isolated', async () => {
342
+ // A fixture skill with a nested resource proves the copy is general (not
343
+ // hard-coded to picopilot-code) and stays inside the temp target.
344
+ const source = mkdtempSync(join(tmpdir(), 'picopilot-skills-src-'));
345
+ try {
346
+ const skillDir = join(source, 'picopilot-fixture');
347
+ execFileSync('mkdir', ['-p', join(skillDir, 'reference', 'deep')]);
348
+ execFileSync('sh', [
349
+ '-c',
350
+ `printf '%s\n' '---' 'name: picopilot-fixture' 'description: A fixture skill.' '---' '# fixture' > ${JSON.stringify(join(skillDir, 'SKILL.md'))}`,
351
+ ]);
352
+ execFileSync('sh', [
353
+ '-c',
354
+ `printf 'RES' > ${JSON.stringify(join(skillDir, 'reference', 'deep', 'note.md'))}`,
355
+ ]);
356
+
357
+ await installSkills({
358
+ cli: createCli(),
359
+ global: false,
360
+ cwd: target,
361
+ sourceDir: source,
362
+ });
363
+
364
+ const installed = join(target, '.agents', 'skills', 'picopilot-fixture');
365
+ expect(existsSync(join(installed, 'SKILL.md'))).toBe(true);
366
+ expect(
367
+ readFileSync(join(installed, 'reference', 'deep', 'note.md'), 'utf8'),
368
+ ).toBe('RES');
369
+ for (const dir of REAL_DIRS) {
370
+ expect(snapshot(dir)).toEqual(before.get(dir));
371
+ }
372
+ } finally {
373
+ execFileSync('rm', ['-rf', source]);
374
+ }
375
+ });
376
+ });
@@ -0,0 +1,205 @@
1
+ import {cpSync, existsSync, readdirSync} from 'node:fs';
2
+ import {basename, dirname, join} from 'node:path';
3
+ import {fileURLToPath} from 'node:url';
4
+ import {Cli, SyncSkills} from 'incur';
5
+
6
+ /**
7
+ * The engine behind `picopilot init --install-skills`: the ONE picopilot path
8
+ * that writes to a SHARED/GLOBAL location. incur's `skills add` defaults to a
9
+ * GLOBAL install (into `~/.agents/skills` plus symlinks into detected agent
10
+ * dirs like `~/.claude/skills`), so this module is where the work-contract's
11
+ * shared-write isolation obligation is discharged: every knob that decides the
12
+ * install TARGET (`global`, `cwd`) is a parameter a test can redirect to a temp
13
+ * dir, so installing skills can never pollute the real environment in tests.
14
+ *
15
+ * The install runs IN-PROCESS through incur's `SyncSkills.sync`. NOTE (incur
16
+ * 0.4.10): `SyncSkills.sync` / the `skills add` built-in resolve their target
17
+ * from `os.homedir()` and `process.env` captured AT MODULE LOAD, NOT from the
18
+ * `env` DI you can pass to `cli.serve(argv, { env })`, so overriding the serve
19
+ * env does NOT move the install target. The effective in-process lever is
20
+ * `global: false` + `cwd: <temp dir>`, which installs under `<cwd>/.agents/skills`
21
+ * (and `<cwd>/.claude/skills` etc.) and leaves the real dirs byte-untouched.
22
+ */
23
+
24
+ /**
25
+ * The authored picopilot skills (US #20 discipline skills + the game skills),
26
+ * as SKILL.md files under {@link skillsSourceDir}. They load on-demand by
27
+ * concern and carry the hard-won workflow knowledge (the `#include` discipline,
28
+ * the token breakdown, the `gfx render`→look→`gfx set`→fix loop incl. the
29
+ * non-multimodal fallback, composing in picopilot-MML) so an agent gets it
30
+ * without bloating every turn.
31
+ *
32
+ * The GAME skills are a composed pair (grilled design in
33
+ * work/notes/ideas/game-jam-design-skill.md): `game-jam` (model-invoked, the
34
+ * clock+theme discipline) POINTS AT `game-design-reference` (a user-invoked
35
+ * reference body of universal design principles, `disable-model-invocation`, so
36
+ * it costs no per-turn context and is reached only by pointer, not fired on a
37
+ * vague trigger). Both ship engine-packaged here; `game-design-reference`'s
38
+ * CONTENT is engine-agnostic.
39
+ *
40
+ * They ship ALONGSIDE the per-command skills incur auto-generates from the
41
+ * command surface (`sync: { depth: 1 }`), pulled in via an `include` glob; the
42
+ * command surface is still growing, so the named discipline skills are authored
43
+ * rather than tied to a command-group shape that does not exist yet.
44
+ */
45
+ export const SKILL_NAMES = [
46
+ 'picopilot-overview',
47
+ 'picopilot-code',
48
+ 'picopilot-art',
49
+ 'picopilot-audio',
50
+ 'picopilot-debug',
51
+ 'game-design-reference',
52
+ 'game-jam',
53
+ ] as const;
54
+
55
+ /**
56
+ * Resolves the on-disk directory that holds the authored `picopilot-*` SKILL.md
57
+ * files (`<packageRoot>/src/skills`). The files ship in the npm package (the
58
+ * package `files` includes `src`), so the same path works from the built
59
+ * `dist/` runtime and from `src/` under tsx: we walk up from THIS module to the
60
+ * package root (the nearest ancestor with a `package.json`) and join `src/skills`,
61
+ * rather than a `dist`-relative path that would not exist (SKILL.md is not
62
+ * compiled).
63
+ */
64
+ export function skillsSourceDir(): string {
65
+ let dir = dirname(fileURLToPath(import.meta.url));
66
+ const seen = new Set<string>();
67
+ while (!seen.has(dir)) {
68
+ seen.add(dir);
69
+ if (existsSync(join(dir, 'package.json'))) {
70
+ return join(dir, 'src', 'skills');
71
+ }
72
+ const parent = dirname(dir);
73
+ if (parent === dir) break;
74
+ dir = parent;
75
+ }
76
+ // Fallback: two levels up from src/engine/skills is the package root.
77
+ return join(dirname(fileURLToPath(import.meta.url)), '..', '..', 'skills');
78
+ }
79
+
80
+ /** One agent whose skill dir picopilot's install wired up (from incur's result). */
81
+ export interface WiredAgent {
82
+ readonly agent: string;
83
+ readonly path: string;
84
+ readonly mode: string;
85
+ }
86
+
87
+ /** The outcome of installing picopilot's skills. */
88
+ export interface InstallSkillsResult {
89
+ /** Canonical install paths (one per skill, under `<base>/.agents/skills`). */
90
+ readonly paths: string[];
91
+ /** Non-universal agents whose dirs were symlinked/copied to the canonical skills. */
92
+ readonly agents: WiredAgent[];
93
+ /** The skill names that were installed. */
94
+ readonly skills: string[];
95
+ }
96
+
97
+ /** Options controlling WHERE and HOW picopilot's skills are installed. */
98
+ export interface InstallSkillsOptions {
99
+ /**
100
+ * The picopilot CLI whose command surface the per-command skills are generated
101
+ * from. Its command map (via {@link Cli.toCommands}) is handed to incur.
102
+ */
103
+ readonly cli: Cli.Cli;
104
+ /**
105
+ * Install globally (the real default, into `~/.agents/skills` + detected agent
106
+ * dirs) when true; install project-locally (into `<cwd>/.agents/skills`) when
107
+ * false. This is the shared-write isolation lever: tests pass `false`.
108
+ */
109
+ readonly global: boolean;
110
+ /**
111
+ * The install base for a project-local (`global: false`) install and, when a
112
+ * non-universal agent is detected, the base its symlinks are written under.
113
+ * Ignored for a global install (that goes to the real home). Tests point this
114
+ * at a temp dir.
115
+ */
116
+ readonly cwd?: string;
117
+ /**
118
+ * Override the authored-skills source dir. Defaults to {@link skillsSourceDir}.
119
+ * Exists so a test can prove isolation using its own fixture skills.
120
+ */
121
+ readonly sourceDir?: string;
122
+ }
123
+
124
+ /**
125
+ * Installs picopilot's skills (the authored `picopilot-*` discipline skills plus
126
+ * the per-command skills incur generates from the CLI) via incur's
127
+ * `SyncSkills.sync`. This is the real work `init --install-skills` performs.
128
+ *
129
+ * The authored skills are pulled in with an ABSOLUTE `include` glob into
130
+ * {@link skillsSourceDir}, DECOUPLED from `cwd` on purpose: `cwd` selects the
131
+ * install TARGET, the include glob selects the skills SOURCE. That decoupling is
132
+ * what lets a test install to a temp `cwd` while still reading the real authored
133
+ * skills (or its own fixtures), the crux of the isolation contract.
134
+ */
135
+ export async function installSkills(
136
+ options: InstallSkillsOptions,
137
+ ): Promise<InstallSkillsResult> {
138
+ const commands = Cli.toCommands.get(options.cli);
139
+ if (commands === undefined) {
140
+ throw new Error(
141
+ 'installSkills: the given CLI has no command map (was it built by Cli.create?)',
142
+ );
143
+ }
144
+
145
+ const source = options.sourceDir ?? skillsSourceDir();
146
+
147
+ const result = await SyncSkills.sync('picopilot', commands, {
148
+ depth: 1,
149
+ description: 'An agent-first toolchain for PICO-8 game development.',
150
+ global: options.global,
151
+ cwd: options.cwd,
152
+ // Absolute glob → resolves against `source`, independent of `cwd`.
153
+ include: [join(source, '*')],
154
+ });
155
+
156
+ // Ship skill RESOURCE files (everything a skill dir carries besides SKILL.md).
157
+ // incur 0.4.10's SyncSkills.sync copies ONLY SKILL.md and drops sibling files
158
+ // (see work/notes/observations/incur-syncskills-drops-skill-resource-files.md),
159
+ // so genre code references under e.g. `picopilot-code/reference/` would never
160
+ // reach the installed skill. We backfill them here: for each installed skill
161
+ // path (canonical + each wired agent), copy the matching source skill's
162
+ // non-SKILL.md contents in. This stays inside the already-isolated installSkills
163
+ // seam, so the shared-write isolation test still governs it, and it becomes a
164
+ // no-op (safe to delete) once incur copies full skill dirs.
165
+ const installPaths = [...result.paths, ...result.agents.map((a) => a.path)];
166
+ for (const installedSkillDir of installPaths) {
167
+ copySkillResources(
168
+ join(source, basename(installedSkillDir)),
169
+ installedSkillDir,
170
+ );
171
+ }
172
+
173
+ return {
174
+ paths: result.paths,
175
+ agents: result.agents.map((a) => ({
176
+ agent: a.agent,
177
+ path: a.path,
178
+ mode: a.mode,
179
+ })),
180
+ skills: result.skills.map((s) => s.name),
181
+ };
182
+ }
183
+
184
+ /**
185
+ * Copies a source skill directory's RESOURCE files (everything except SKILL.md)
186
+ * into an already-installed skill directory. This is the workaround for incur
187
+ * 0.4.10 shipping only SKILL.md (see the observation referenced in installSkills):
188
+ * SKILL.md is left to incur (it is already written and may be transformed), and
189
+ * only the sibling resources are backfilled, recursively, preserving structure.
190
+ *
191
+ * Both the source skill and any resources are optional: a skill with no resource
192
+ * files, or a source dir that does not exist (a command-generated skill with no
193
+ * authored source), is a silent no-op. Nothing here reaches outside `destDir`,
194
+ * which is one of the install paths SyncSkills already chose, so it inherits the
195
+ * shared-write isolation of installSkills (tests redirect those paths to a temp).
196
+ */
197
+ function copySkillResources(sourceSkillDir: string, destDir: string): void {
198
+ if (!existsSync(sourceSkillDir) || !existsSync(destDir)) return;
199
+ for (const entry of readdirSync(sourceSkillDir)) {
200
+ if (entry === 'SKILL.md') continue; // incur owns the manifest; do not clobber it
201
+ cpSync(join(sourceSkillDir, entry), join(destDir, entry), {
202
+ recursive: true,
203
+ });
204
+ }
205
+ }
package/src/index.ts ADDED
@@ -0,0 +1,13 @@
1
+ import type {Cli} from 'incur';
2
+ import {createCli} from './cli.js';
3
+
4
+ export {createCli} from './cli.js';
5
+ export {VERSION} from './version.js';
6
+
7
+ /**
8
+ * The default CLI instance, exported for `incur gen` (typed CTAs) and for
9
+ * embedding picopilot as a Fetch API handler / MCP server.
10
+ */
11
+ const cli: Cli.Cli = createCli();
12
+
13
+ export default cli;