picopilot 0.0.0 → 0.1.0
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.
- package/LICENSE +661 -0
- package/dist/bin.d.ts +3 -0
- package/dist/bin.d.ts.map +1 -0
- package/dist/bin.js +4 -0
- package/dist/bin.js.map +1 -0
- package/dist/cli.d.ts +15 -0
- package/dist/cli.d.ts.map +1 -0
- package/dist/cli.js +59 -0
- package/dist/cli.js.map +1 -0
- package/dist/commands/audio.d.ts +17 -0
- package/dist/commands/audio.d.ts.map +1 -0
- package/dist/commands/audio.js +315 -0
- package/dist/commands/audio.js.map +1 -0
- package/dist/commands/export.d.ts +36 -0
- package/dist/commands/export.d.ts.map +1 -0
- package/dist/commands/export.js +257 -0
- package/dist/commands/export.js.map +1 -0
- package/dist/commands/gfx.d.ts +28 -0
- package/dist/commands/gfx.d.ts.map +1 -0
- package/dist/commands/gfx.js +521 -0
- package/dist/commands/gfx.js.map +1 -0
- package/dist/commands/init.d.ts +34 -0
- package/dist/commands/init.d.ts.map +1 -0
- package/dist/commands/init.js +151 -0
- package/dist/commands/init.js.map +1 -0
- package/dist/commands/lint.d.ts +20 -0
- package/dist/commands/lint.d.ts.map +1 -0
- package/dist/commands/lint.js +90 -0
- package/dist/commands/lint.js.map +1 -0
- package/dist/commands/minify.d.ts +20 -0
- package/dist/commands/minify.d.ts.map +1 -0
- package/dist/commands/minify.js +129 -0
- package/dist/commands/minify.js.map +1 -0
- package/dist/commands/music.d.ts +13 -0
- package/dist/commands/music.d.ts.map +1 -0
- package/dist/commands/music.js +241 -0
- package/dist/commands/music.js.map +1 -0
- package/dist/commands/playtest.d.ts +29 -0
- package/dist/commands/playtest.d.ts.map +1 -0
- package/dist/commands/playtest.js +631 -0
- package/dist/commands/playtest.js.map +1 -0
- package/dist/commands/run.d.ts +30 -0
- package/dist/commands/run.d.ts.map +1 -0
- package/dist/commands/run.js +202 -0
- package/dist/commands/run.js.map +1 -0
- package/dist/commands/serve.d.ts +50 -0
- package/dist/commands/serve.d.ts.map +1 -0
- package/dist/commands/serve.js +213 -0
- package/dist/commands/serve.js.map +1 -0
- package/dist/commands/sfx.d.ts +13 -0
- package/dist/commands/sfx.d.ts.map +1 -0
- package/dist/commands/sfx.js +254 -0
- package/dist/commands/sfx.js.map +1 -0
- package/dist/commands/tokens.d.ts +36 -0
- package/dist/commands/tokens.d.ts.map +1 -0
- package/dist/commands/tokens.js +129 -0
- package/dist/commands/tokens.js.map +1 -0
- package/dist/commands/verify.d.ts +57 -0
- package/dist/commands/verify.d.ts.map +1 -0
- package/dist/commands/verify.js +213 -0
- package/dist/commands/verify.js.map +1 -0
- package/dist/commands/version.d.ts +12 -0
- package/dist/commands/version.d.ts.map +1 -0
- package/dist/commands/version.js +24 -0
- package/dist/commands/version.js.map +1 -0
- package/dist/engine/audio/index.d.ts +14 -0
- package/dist/engine/audio/index.d.ts.map +1 -0
- package/dist/engine/audio/index.js +14 -0
- package/dist/engine/audio/index.js.map +1 -0
- package/dist/engine/audio/music.d.ts +116 -0
- package/dist/engine/audio/music.d.ts.map +1 -0
- package/dist/engine/audio/music.js +138 -0
- package/dist/engine/audio/music.js.map +1 -0
- package/dist/engine/audio/section.d.ts +34 -0
- package/dist/engine/audio/section.d.ts.map +1 -0
- package/dist/engine/audio/section.js +67 -0
- package/dist/engine/audio/section.js.map +1 -0
- package/dist/engine/audio/sfx.d.ts +141 -0
- package/dist/engine/audio/sfx.d.ts.map +1 -0
- package/dist/engine/audio/sfx.js +506 -0
- package/dist/engine/audio/sfx.js.map +1 -0
- package/dist/engine/cart/cart.d.ts +75 -0
- package/dist/engine/cart/cart.d.ts.map +1 -0
- package/dist/engine/cart/cart.js +153 -0
- package/dist/engine/cart/cart.js.map +1 -0
- package/dist/engine/cart/errors.d.ts +29 -0
- package/dist/engine/cart/errors.d.ts.map +1 -0
- package/dist/engine/cart/errors.js +26 -0
- package/dist/engine/cart/errors.js.map +1 -0
- package/dist/engine/cart/gfx.d.ts +79 -0
- package/dist/engine/cart/gfx.d.ts.map +1 -0
- package/dist/engine/cart/gfx.js +171 -0
- package/dist/engine/cart/gfx.js.map +1 -0
- package/dist/engine/cart/index.d.ts +15 -0
- package/dist/engine/cart/index.d.ts.map +1 -0
- package/dist/engine/cart/index.js +15 -0
- package/dist/engine/cart/index.js.map +1 -0
- package/dist/engine/cart/map.d.ts +46 -0
- package/dist/engine/cart/map.d.ts.map +1 -0
- package/dist/engine/cart/map.js +118 -0
- package/dist/engine/cart/map.js.map +1 -0
- package/dist/engine/config/config.d.ts +74 -0
- package/dist/engine/config/config.d.ts.map +1 -0
- package/dist/engine/config/config.js +103 -0
- package/dist/engine/config/config.js.map +1 -0
- package/dist/engine/config/index.d.ts +7 -0
- package/dist/engine/config/index.d.ts.map +1 -0
- package/dist/engine/config/index.js +7 -0
- package/dist/engine/config/index.js.map +1 -0
- package/dist/engine/gfx/grid.d.ts +51 -0
- package/dist/engine/gfx/grid.d.ts.map +1 -0
- package/dist/engine/gfx/grid.js +116 -0
- package/dist/engine/gfx/grid.js.map +1 -0
- package/dist/engine/gfx/index.d.ts +17 -0
- package/dist/engine/gfx/index.d.ts.map +1 -0
- package/dist/engine/gfx/index.js +17 -0
- package/dist/engine/gfx/index.js.map +1 -0
- package/dist/engine/gfx/label.d.ts +34 -0
- package/dist/engine/gfx/label.d.ts.map +1 -0
- package/dist/engine/gfx/label.js +190 -0
- package/dist/engine/gfx/label.js.map +1 -0
- package/dist/engine/gfx/overlap.d.ts +42 -0
- package/dist/engine/gfx/overlap.d.ts.map +1 -0
- package/dist/engine/gfx/overlap.js +57 -0
- package/dist/engine/gfx/overlap.js.map +1 -0
- package/dist/engine/gfx/palette.d.ts +29 -0
- package/dist/engine/gfx/palette.d.ts.map +1 -0
- package/dist/engine/gfx/palette.js +46 -0
- package/dist/engine/gfx/palette.js.map +1 -0
- package/dist/engine/gfx/png.d.ts +31 -0
- package/dist/engine/gfx/png.d.ts.map +1 -0
- package/dist/engine/gfx/png.js +165 -0
- package/dist/engine/gfx/png.js.map +1 -0
- package/dist/engine/gfx/render.d.ts +42 -0
- package/dist/engine/gfx/render.d.ts.map +1 -0
- package/dist/engine/gfx/render.js +72 -0
- package/dist/engine/gfx/render.js.map +1 -0
- package/dist/engine/index.d.ts +22 -0
- package/dist/engine/index.d.ts.map +1 -0
- package/dist/engine/index.js +22 -0
- package/dist/engine/index.js.map +1 -0
- package/dist/engine/pico8/adapter.d.ts +267 -0
- package/dist/engine/pico8/adapter.d.ts.map +1 -0
- package/dist/engine/pico8/adapter.js +37 -0
- package/dist/engine/pico8/adapter.js.map +1 -0
- package/dist/engine/pico8/drive.d.ts +308 -0
- package/dist/engine/pico8/drive.d.ts.map +1 -0
- package/dist/engine/pico8/drive.js +581 -0
- package/dist/engine/pico8/drive.js.map +1 -0
- package/dist/engine/pico8/harness.d.ts +146 -0
- package/dist/engine/pico8/harness.d.ts.map +1 -0
- package/dist/engine/pico8/harness.js +251 -0
- package/dist/engine/pico8/harness.js.map +1 -0
- package/dist/engine/pico8/index.d.ts +21 -0
- package/dist/engine/pico8/index.d.ts.map +1 -0
- package/dist/engine/pico8/index.js +21 -0
- package/dist/engine/pico8/index.js.map +1 -0
- package/dist/engine/pico8/sentinel.d.ts +63 -0
- package/dist/engine/pico8/sentinel.d.ts.map +1 -0
- package/dist/engine/pico8/sentinel.js +86 -0
- package/dist/engine/pico8/sentinel.js.map +1 -0
- package/dist/engine/pico8/session-daemon-entry.d.ts +10 -0
- package/dist/engine/pico8/session-daemon-entry.d.ts.map +1 -0
- package/dist/engine/pico8/session-daemon-entry.js +11 -0
- package/dist/engine/pico8/session-daemon-entry.js.map +1 -0
- package/dist/engine/pico8/session-daemon-main.d.ts +44 -0
- package/dist/engine/pico8/session-daemon-main.d.ts.map +1 -0
- package/dist/engine/pico8/session-daemon-main.js +71 -0
- package/dist/engine/pico8/session-daemon-main.js.map +1 -0
- package/dist/engine/pico8/session-daemon.d.ts +68 -0
- package/dist/engine/pico8/session-daemon.d.ts.map +1 -0
- package/dist/engine/pico8/session-daemon.js +264 -0
- package/dist/engine/pico8/session-daemon.js.map +1 -0
- package/dist/engine/pico8/session.d.ts +140 -0
- package/dist/engine/pico8/session.d.ts.map +1 -0
- package/dist/engine/pico8/session.js +250 -0
- package/dist/engine/pico8/session.js.map +1 -0
- package/dist/engine/pico8/shell.d.ts +103 -0
- package/dist/engine/pico8/shell.d.ts.map +1 -0
- package/dist/engine/pico8/shell.js +324 -0
- package/dist/engine/pico8/shell.js.map +1 -0
- package/dist/engine/pico8/supervisor.d.ts +137 -0
- package/dist/engine/pico8/supervisor.d.ts.map +1 -0
- package/dist/engine/pico8/supervisor.js +140 -0
- package/dist/engine/pico8/supervisor.js.map +1 -0
- package/dist/engine/scaffold/index.d.ts +8 -0
- package/dist/engine/scaffold/index.d.ts.map +1 -0
- package/dist/engine/scaffold/index.js +8 -0
- package/dist/engine/scaffold/index.js.map +1 -0
- package/dist/engine/scaffold/scaffold.d.ts +49 -0
- package/dist/engine/scaffold/scaffold.d.ts.map +1 -0
- package/dist/engine/scaffold/scaffold.js +217 -0
- package/dist/engine/scaffold/scaffold.js.map +1 -0
- package/dist/engine/shrinko/adapter.d.ts +167 -0
- package/dist/engine/shrinko/adapter.d.ts.map +1 -0
- package/dist/engine/shrinko/adapter.js +107 -0
- package/dist/engine/shrinko/adapter.js.map +1 -0
- package/dist/engine/shrinko/index.d.ts +13 -0
- package/dist/engine/shrinko/index.d.ts.map +1 -0
- package/dist/engine/shrinko/index.js +13 -0
- package/dist/engine/shrinko/index.js.map +1 -0
- package/dist/engine/shrinko/shell.d.ts +69 -0
- package/dist/engine/shrinko/shell.d.ts.map +1 -0
- package/dist/engine/shrinko/shell.js +156 -0
- package/dist/engine/shrinko/shell.js.map +1 -0
- package/dist/engine/skills/index.d.ts +8 -0
- package/dist/engine/skills/index.d.ts.map +1 -0
- package/dist/engine/skills/index.js +8 -0
- package/dist/engine/skills/index.js.map +1 -0
- package/dist/engine/skills/skills.d.ts +104 -0
- package/dist/engine/skills/skills.d.ts.map +1 -0
- package/dist/engine/skills/skills.js +149 -0
- package/dist/engine/skills/skills.js.map +1 -0
- package/dist/index.d.ts +10 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +10 -0
- package/dist/index.js.map +1 -0
- package/dist/version.d.ts +2 -0
- package/dist/version.d.ts.map +1 -0
- package/dist/version.js +19 -0
- package/dist/version.js.map +1 -0
- package/package.json +48 -2
- package/src/bin.ts +4 -0
- package/src/cli.ts +61 -0
- package/src/commands/audio.test.ts +280 -0
- package/src/commands/audio.ts +410 -0
- package/src/commands/export.test.ts +310 -0
- package/src/commands/export.ts +316 -0
- package/src/commands/gfx-io.test.ts +178 -0
- package/src/commands/gfx.test.ts +470 -0
- package/src/commands/gfx.ts +645 -0
- package/src/commands/init.test.ts +460 -0
- package/src/commands/init.ts +193 -0
- package/src/commands/lint.test.ts +137 -0
- package/src/commands/lint.ts +117 -0
- package/src/commands/minify.test.ts +166 -0
- package/src/commands/minify.ts +156 -0
- package/src/commands/music.test.ts +272 -0
- package/src/commands/music.ts +288 -0
- package/src/commands/playtest.test.ts +317 -0
- package/src/commands/playtest.ts +773 -0
- package/src/commands/run.test.ts +238 -0
- package/src/commands/run.ts +259 -0
- package/src/commands/serve.test.ts +218 -0
- package/src/commands/serve.ts +272 -0
- package/src/commands/sfx.test.ts +349 -0
- package/src/commands/sfx.ts +300 -0
- package/src/commands/tokens.test.ts +212 -0
- package/src/commands/tokens.ts +168 -0
- package/src/commands/verify.test.ts +238 -0
- package/src/commands/verify.ts +256 -0
- package/src/commands/version.test.ts +70 -0
- package/src/commands/version.ts +24 -0
- package/src/engine/audio/index.ts +47 -0
- package/src/engine/audio/music.test.ts +188 -0
- package/src/engine/audio/music.ts +218 -0
- package/src/engine/audio/section.test.ts +124 -0
- package/src/engine/audio/section.ts +79 -0
- package/src/engine/audio/sfx.test.ts +410 -0
- package/src/engine/audio/sfx.ts +694 -0
- package/src/engine/cart/cart.test.ts +304 -0
- package/src/engine/cart/cart.ts +211 -0
- package/src/engine/cart/errors.ts +39 -0
- package/src/engine/cart/fixtures/crlf.p8 +6 -0
- package/src/engine/cart/fixtures/full.p8 +174 -0
- package/src/engine/cart/fixtures/header-only.p8 +2 -0
- package/src/engine/cart/fixtures/malformed-no-header.p8 +2 -0
- package/src/engine/cart/fixtures/minimal.p8 +10 -0
- package/src/engine/cart/fixtures/no-trailing-newline.p8 +6 -0
- package/src/engine/cart/gfx.ts +195 -0
- package/src/engine/cart/index.ts +27 -0
- package/src/engine/cart/map.ts +134 -0
- package/src/engine/config/config.ts +115 -0
- package/src/engine/config/index.ts +12 -0
- package/src/engine/gfx/grid.test.ts +128 -0
- package/src/engine/gfx/grid.ts +136 -0
- package/src/engine/gfx/index.ts +34 -0
- package/src/engine/gfx/label.test.ts +105 -0
- package/src/engine/gfx/label.ts +229 -0
- package/src/engine/gfx/overlap.test.ts +71 -0
- package/src/engine/gfx/overlap.ts +66 -0
- package/src/engine/gfx/palette.test.ts +35 -0
- package/src/engine/gfx/palette.ts +54 -0
- package/src/engine/gfx/png.test.ts +111 -0
- package/src/engine/gfx/png.ts +196 -0
- package/src/engine/gfx/render.test.ts +180 -0
- package/src/engine/gfx/render.ts +106 -0
- package/src/engine/index.ts +21 -0
- package/src/engine/pico8/adapter.ts +303 -0
- package/src/engine/pico8/drive.test.ts +415 -0
- package/src/engine/pico8/drive.ts +729 -0
- package/src/engine/pico8/harness.test.ts +181 -0
- package/src/engine/pico8/harness.ts +354 -0
- package/src/engine/pico8/index.ts +20 -0
- package/src/engine/pico8/sentinel.test.ts +80 -0
- package/src/engine/pico8/sentinel.ts +93 -0
- package/src/engine/pico8/session-daemon-entry.ts +12 -0
- package/src/engine/pico8/session-daemon-main.ts +98 -0
- package/src/engine/pico8/session-daemon.test.ts +212 -0
- package/src/engine/pico8/session-daemon.ts +306 -0
- package/src/engine/pico8/session.test.ts +239 -0
- package/src/engine/pico8/session.ts +321 -0
- package/src/engine/pico8/shell.test.ts +583 -0
- package/src/engine/pico8/shell.ts +412 -0
- package/src/engine/pico8/supervisor.test.ts +105 -0
- package/src/engine/pico8/supervisor.ts +197 -0
- package/src/engine/scaffold/index.ts +15 -0
- package/src/engine/scaffold/scaffold.ts +236 -0
- package/src/engine/shrinko/adapter.ts +241 -0
- package/src/engine/shrinko/fixtures/count-no-compressed.txt +2 -0
- package/src/engine/shrinko/fixtures/count-over-budget.txt +3 -0
- package/src/engine/shrinko/fixtures/count-under-budget.txt +3 -0
- package/src/engine/shrinko/index.ts +33 -0
- package/src/engine/shrinko/shell.ts +224 -0
- package/src/engine/shrinko/shrinko.test.ts +182 -0
- package/src/engine/skills/index.ts +14 -0
- package/src/engine/skills/skills.test.ts +376 -0
- package/src/engine/skills/skills.ts +205 -0
- package/src/index.ts +13 -0
- package/src/skills/game-design-reference/SKILL.md +70 -0
- package/src/skills/game-jam/SKILL.md +45 -0
- package/src/skills/picopilot-art/SKILL.md +62 -0
- package/src/skills/picopilot-audio/SKILL.md +194 -0
- package/src/skills/picopilot-code/SKILL.md +97 -0
- package/src/skills/picopilot-code/reference/README.md +42 -0
- package/src/skills/picopilot-code/reference/mode7-racing.md +95 -0
- package/src/skills/picopilot-code/reference/platformer.md +119 -0
- package/src/skills/picopilot-code/reference/puzzle-grid.md +106 -0
- package/src/skills/picopilot-code/reference/rpg-menus-dialog.md +122 -0
- package/src/skills/picopilot-code/reference/top-down-adventure.md +117 -0
- package/src/skills/picopilot-code/reference/twin-stick-arcade.md +128 -0
- package/src/skills/picopilot-debug/SKILL.md +195 -0
- package/src/skills/picopilot-overview/SKILL.md +52 -0
- package/src/version.ts +22 -0
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The shrinko adapter seam: how every shrinko-backed command reaches shrinko8.
|
|
3
|
+
*
|
|
4
|
+
* `adapter.ts` is the command-facing CONTRACT (the {@link ShrinkoAdapter}
|
|
5
|
+
* interface, the {@link ShrinkoResult} discriminated union, the structured
|
|
6
|
+
* {@link ShrinkoNotFound} value, and the `--count` parser). `shell.ts` is the v1
|
|
7
|
+
* IMPLEMENTATION that shells out to a user-installed Python `shrinko8`
|
|
8
|
+
* (ADR-0001). A future native-TS implementation implements the same interface
|
|
9
|
+
* and drops in without touching the command layer.
|
|
10
|
+
*/
|
|
11
|
+
export {
|
|
12
|
+
type CountReport,
|
|
13
|
+
type LintFinding,
|
|
14
|
+
type LintReport,
|
|
15
|
+
type MinifyReport,
|
|
16
|
+
parseCount,
|
|
17
|
+
parseLint,
|
|
18
|
+
SHRINKO_NEEDS,
|
|
19
|
+
SHRINKO_REMEDY,
|
|
20
|
+
type ShrinkoAdapter,
|
|
21
|
+
type ShrinkoNotFound,
|
|
22
|
+
shrinkoNotFound,
|
|
23
|
+
type ShrinkoOk,
|
|
24
|
+
ShrinkoParseError,
|
|
25
|
+
type ShrinkoResult,
|
|
26
|
+
} from './adapter.js';
|
|
27
|
+
export {
|
|
28
|
+
type ChildResult,
|
|
29
|
+
type ChildRunner,
|
|
30
|
+
execFileRunner,
|
|
31
|
+
ShellShrinkoAdapter,
|
|
32
|
+
type ShellShrinkoOptions,
|
|
33
|
+
} from './shell.js';
|
|
@@ -0,0 +1,224 @@
|
|
|
1
|
+
import {execFile} from 'node:child_process';
|
|
2
|
+
|
|
3
|
+
import {
|
|
4
|
+
type CountReport,
|
|
5
|
+
type LintReport,
|
|
6
|
+
type MinifyReport,
|
|
7
|
+
parseCount,
|
|
8
|
+
parseLint,
|
|
9
|
+
type ShrinkoAdapter,
|
|
10
|
+
type ShrinkoResult,
|
|
11
|
+
shrinkoNotFound,
|
|
12
|
+
} from './adapter.js';
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* The result of running one child process: its exit `code` (null when killed by
|
|
16
|
+
* a signal), captured `stdout`/`stderr`, and `spawnError` set when the binary
|
|
17
|
+
* could not be launched AT ALL (ENOENT etc.) rather than launched-and-exited.
|
|
18
|
+
* `spawnError` is how the shell adapter distinguishes "shrinko is not installed"
|
|
19
|
+
* from "shrinko ran and failed".
|
|
20
|
+
*/
|
|
21
|
+
export interface ChildResult {
|
|
22
|
+
readonly code: number | null;
|
|
23
|
+
readonly stdout: string;
|
|
24
|
+
readonly stderr: string;
|
|
25
|
+
readonly spawnError: NodeJS.ErrnoException | undefined;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* The child-process seam. The real implementation shells out with the CALLER's
|
|
30
|
+
* `env` (so a test overrides the child's `PATH` and nothing else). Tests inject
|
|
31
|
+
* a stub runner to simulate shrinko present/absent WITHOUT ever requiring the
|
|
32
|
+
* real binary, and to assert exactly what argv/env the adapter would spawn.
|
|
33
|
+
*/
|
|
34
|
+
export type ChildRunner = (
|
|
35
|
+
file: string,
|
|
36
|
+
args: string[],
|
|
37
|
+
env: NodeJS.ProcessEnv,
|
|
38
|
+
) => Promise<ChildResult>;
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* The default {@link ChildRunner}: an `execFile` that captures stdout/stderr and
|
|
42
|
+
* NEVER rejects. A failed spawn (ENOENT) becomes `spawnError`; a nonzero exit
|
|
43
|
+
* becomes a `code`, so both outcomes are data the adapter maps to a structured
|
|
44
|
+
* result. The child inherits ONLY the `env` it is handed (the command layer
|
|
45
|
+
* passes the incur-resolved env, whose `PATH` a test can override) so there is
|
|
46
|
+
* no ambient-environment leak.
|
|
47
|
+
*/
|
|
48
|
+
export const execFileRunner: ChildRunner = (file, args, env) =>
|
|
49
|
+
new Promise((resolve) => {
|
|
50
|
+
execFile(file, args, {env, encoding: 'utf8'}, (error, stdout, stderr) => {
|
|
51
|
+
const err = error as NodeJS.ErrnoException | null;
|
|
52
|
+
resolve({
|
|
53
|
+
// `execFile` sets error.code to the string 'ENOENT' on spawn failure and
|
|
54
|
+
// to the numeric exit code on a nonzero exit; separate the two.
|
|
55
|
+
code:
|
|
56
|
+
err !== null && typeof err.code === 'number'
|
|
57
|
+
? err.code
|
|
58
|
+
: err === null
|
|
59
|
+
? 0
|
|
60
|
+
: null,
|
|
61
|
+
stdout,
|
|
62
|
+
stderr,
|
|
63
|
+
spawnError:
|
|
64
|
+
err !== null && typeof err.code === 'string' ? err : undefined,
|
|
65
|
+
});
|
|
66
|
+
});
|
|
67
|
+
});
|
|
68
|
+
|
|
69
|
+
/**
|
|
70
|
+
* The candidate ways to invoke shrinko, tried in order. First the `shrinko8`
|
|
71
|
+
* entry-point on `PATH`, then `python -m shrinko8` / `python3 -m shrinko8` as
|
|
72
|
+
* fallbacks (the module is `shrinko8`; the pip package is `shrinko`). The first
|
|
73
|
+
* candidate that SPAWNS (does not ENOENT) is the one shrinko is reached through.
|
|
74
|
+
*/
|
|
75
|
+
const SHRINKO_INVOCATIONS: readonly (readonly string[])[] = [
|
|
76
|
+
['shrinko8'],
|
|
77
|
+
['python', '-m', 'shrinko8'],
|
|
78
|
+
['python3', '-m', 'shrinko8'],
|
|
79
|
+
];
|
|
80
|
+
|
|
81
|
+
/** Options for {@link ShellShrinkoAdapter}. */
|
|
82
|
+
export interface ShellShrinkoOptions {
|
|
83
|
+
/**
|
|
84
|
+
* The environment handed to the child process. The command layer passes the
|
|
85
|
+
* incur-resolved env here; its `PATH` is the isolation lever a test overrides
|
|
86
|
+
* (an empty/`PATH`-less env makes every invocation ENOENT ⇒ shrinko-absent).
|
|
87
|
+
*/
|
|
88
|
+
readonly env: NodeJS.ProcessEnv;
|
|
89
|
+
/** The child runner (defaults to {@link execFileRunner}); tests inject a stub. */
|
|
90
|
+
readonly run?: ChildRunner;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
/**
|
|
94
|
+
* The v1 {@link ShrinkoAdapter}: shells out to a user-installed Python
|
|
95
|
+
* `shrinko8` (ADR-0001). It is the ONLY place that knows shrinko is an external
|
|
96
|
+
* process; every command talks to the {@link ShrinkoAdapter} interface, so a
|
|
97
|
+
* future native-TS adapter replaces this class without touching a command.
|
|
98
|
+
*
|
|
99
|
+
* Presence detection and invocation are the SAME act: the adapter tries each
|
|
100
|
+
* {@link SHRINKO_INVOCATIONS} candidate and uses the first that spawns. If every
|
|
101
|
+
* candidate ENOENTs, shrinko is absent and the method returns the structured
|
|
102
|
+
* {@link shrinkoNotFound} value (never a throw, never a hollow success).
|
|
103
|
+
*/
|
|
104
|
+
export class ShellShrinkoAdapter implements ShrinkoAdapter {
|
|
105
|
+
private readonly env: NodeJS.ProcessEnv;
|
|
106
|
+
private readonly run: ChildRunner;
|
|
107
|
+
|
|
108
|
+
constructor(options: ShellShrinkoOptions) {
|
|
109
|
+
this.env = options.env;
|
|
110
|
+
this.run = options.run ?? execFileRunner;
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
async count(cartPath: string): Promise<ShrinkoResult<CountReport>> {
|
|
114
|
+
const outcome = await this.invoke([cartPath, '--count']);
|
|
115
|
+
if (outcome === undefined) {
|
|
116
|
+
return shrinkoNotFound();
|
|
117
|
+
}
|
|
118
|
+
// shrinko prints the count report; some builds send it to stderr, so parse
|
|
119
|
+
// the combined stream. parseCount throws on unparseable output (ShrinkoParseError).
|
|
120
|
+
return {
|
|
121
|
+
ok: true,
|
|
122
|
+
value: parseCount(`${outcome.stdout}\n${outcome.stderr}`),
|
|
123
|
+
};
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
async lint(cartPath: string): Promise<ShrinkoResult<LintReport>> {
|
|
127
|
+
// `-l` lints; it exits nonzero (2) WHEN it finds warnings, which is a normal
|
|
128
|
+
// result (findings are data), NOT a spawn failure. Only a spawnError means
|
|
129
|
+
// shrinko is absent, so we key absence off `outcome === undefined`, and parse
|
|
130
|
+
// the warnings regardless of the (0-or-2) exit code.
|
|
131
|
+
const outcome = await this.invoke([cartPath, '-l']);
|
|
132
|
+
if (outcome === undefined) {
|
|
133
|
+
return shrinkoNotFound();
|
|
134
|
+
}
|
|
135
|
+
return {
|
|
136
|
+
ok: true,
|
|
137
|
+
value: parseLint(`${outcome.stdout}\n${outcome.stderr}`),
|
|
138
|
+
};
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
async minify(
|
|
142
|
+
cartPath: string,
|
|
143
|
+
outPath: string,
|
|
144
|
+
): Promise<ShrinkoResult<MinifyReport>> {
|
|
145
|
+
// Count the source first (the "before"): if shrinko is absent, this returns
|
|
146
|
+
// the not-found value and we never write anything.
|
|
147
|
+
const before = await this.count(cartPath);
|
|
148
|
+
if (!before.ok) return before;
|
|
149
|
+
|
|
150
|
+
// Minify SAFE-only (ADR-0007): the three safe-only knobs keep rename/reorder/
|
|
151
|
+
// builtins behaviour-preserving. Output goes to a SEPARATE cart (outPath).
|
|
152
|
+
const out = await this.invoke([
|
|
153
|
+
cartPath,
|
|
154
|
+
'-m',
|
|
155
|
+
'--rename-safe-only',
|
|
156
|
+
'--reorder-safe-only',
|
|
157
|
+
'--builtins-safe-only',
|
|
158
|
+
outPath,
|
|
159
|
+
]);
|
|
160
|
+
if (out === undefined) return shrinkoNotFound();
|
|
161
|
+
|
|
162
|
+
// Count the artifact (the "after"). It exists now, so this is a normal count.
|
|
163
|
+
const after = await this.count(outPath);
|
|
164
|
+
if (!after.ok) return after;
|
|
165
|
+
|
|
166
|
+
return {
|
|
167
|
+
ok: true,
|
|
168
|
+
value: {
|
|
169
|
+
beforeTokens: before.value.tokens,
|
|
170
|
+
afterTokens: after.value.tokens,
|
|
171
|
+
outPath,
|
|
172
|
+
},
|
|
173
|
+
};
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
async exportSpritesheet(
|
|
177
|
+
cartPath: string,
|
|
178
|
+
pngPath: string,
|
|
179
|
+
): Promise<ShrinkoResult<void>> {
|
|
180
|
+
// `<cart> <out.png> -f spritesheet` writes the 128x128 spritesheet PNG.
|
|
181
|
+
const outcome = await this.invoke([cartPath, pngPath, '-f', 'spritesheet']);
|
|
182
|
+
if (outcome === undefined) return shrinkoNotFound();
|
|
183
|
+
return {ok: true, value: undefined};
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
async importSpritesheet(
|
|
187
|
+
srcCartPath: string,
|
|
188
|
+
pngPath: string,
|
|
189
|
+
outCartPath: string,
|
|
190
|
+
): Promise<ShrinkoResult<void>> {
|
|
191
|
+
// `<cart> <out.p8> --merge <png> gfx spritesheet` merges ONLY the gfx section
|
|
192
|
+
// from the PNG into the source cart, preserving code/map/sfx/music. (A plain
|
|
193
|
+
// `-F spritesheet` would drop everything but gfx, wiping the user's code.)
|
|
194
|
+
const outcome = await this.invoke([
|
|
195
|
+
srcCartPath,
|
|
196
|
+
outCartPath,
|
|
197
|
+
'--merge',
|
|
198
|
+
pngPath,
|
|
199
|
+
'gfx',
|
|
200
|
+
'spritesheet',
|
|
201
|
+
]);
|
|
202
|
+
if (outcome === undefined) return shrinkoNotFound();
|
|
203
|
+
return {ok: true, value: undefined};
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
/**
|
|
207
|
+
* Runs shrinko with the given trailing args, trying each invocation candidate
|
|
208
|
+
* until one SPAWNS. Returns the {@link ChildResult} of the first candidate
|
|
209
|
+
* that launched, or `undefined` when every candidate ENOENTs (shrinko absent).
|
|
210
|
+
*/
|
|
211
|
+
private async invoke(args: string[]): Promise<ChildResult | undefined> {
|
|
212
|
+
for (const inv of SHRINKO_INVOCATIONS) {
|
|
213
|
+
const [file, ...prefix] = inv as [string, ...string[]];
|
|
214
|
+
const result = await this.run(file, [...prefix, ...args], this.env);
|
|
215
|
+
if (result.spawnError === undefined) {
|
|
216
|
+
return result;
|
|
217
|
+
}
|
|
218
|
+
// ENOENT on this candidate: fall through to the next. Any other spawn
|
|
219
|
+
// error (EACCES etc.) also falls through, so a broken candidate never
|
|
220
|
+
// masks a working one.
|
|
221
|
+
}
|
|
222
|
+
return undefined;
|
|
223
|
+
}
|
|
224
|
+
}
|
|
@@ -0,0 +1,182 @@
|
|
|
1
|
+
import {readFileSync} from 'node:fs';
|
|
2
|
+
import {fileURLToPath} from 'node:url';
|
|
3
|
+
import {describe, expect, it} from 'vitest';
|
|
4
|
+
|
|
5
|
+
import {
|
|
6
|
+
type ChildResult,
|
|
7
|
+
type ChildRunner,
|
|
8
|
+
parseCount,
|
|
9
|
+
SHRINKO_REMEDY,
|
|
10
|
+
ShellShrinkoAdapter,
|
|
11
|
+
ShrinkoParseError,
|
|
12
|
+
} from './index.js';
|
|
13
|
+
|
|
14
|
+
/** Reads a captured `--count` sample as raw text. */
|
|
15
|
+
function sample(name: string): string {
|
|
16
|
+
return readFileSync(
|
|
17
|
+
fileURLToPath(new URL(`./fixtures/${name}`, import.meta.url)),
|
|
18
|
+
'utf8',
|
|
19
|
+
);
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
/** A recorded spawn, so tests can assert the exact argv + env the adapter used. */
|
|
23
|
+
interface Spawn {
|
|
24
|
+
file: string;
|
|
25
|
+
args: string[];
|
|
26
|
+
env: NodeJS.ProcessEnv;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* Builds a stub {@link ChildRunner} that returns a fixed {@link ChildResult} for
|
|
31
|
+
* a chosen `file`, and ENOENT for every other candidate. Records each spawn so a
|
|
32
|
+
* test can assert which invocation was used and with what env. This is how every
|
|
33
|
+
* test drives shrinko present/absent WITHOUT the real binary.
|
|
34
|
+
*/
|
|
35
|
+
function stubRunner(opts: {
|
|
36
|
+
presentFile?: string;
|
|
37
|
+
stdout?: string;
|
|
38
|
+
stderr?: string;
|
|
39
|
+
code?: number;
|
|
40
|
+
}): {run: ChildRunner; spawns: Spawn[]} {
|
|
41
|
+
const spawns: Spawn[] = [];
|
|
42
|
+
const run: ChildRunner = async (file, args, env) => {
|
|
43
|
+
spawns.push({file, args, env});
|
|
44
|
+
if (opts.presentFile !== undefined && file === opts.presentFile) {
|
|
45
|
+
return {
|
|
46
|
+
code: opts.code ?? 0,
|
|
47
|
+
stdout: opts.stdout ?? '',
|
|
48
|
+
stderr: opts.stderr ?? '',
|
|
49
|
+
spawnError: undefined,
|
|
50
|
+
} satisfies ChildResult;
|
|
51
|
+
}
|
|
52
|
+
// Not the present binary → simulate ENOENT (not installed on PATH).
|
|
53
|
+
const err = Object.assign(new Error(`spawn ${file} ENOENT`), {
|
|
54
|
+
code: 'ENOENT',
|
|
55
|
+
}) as NodeJS.ErrnoException;
|
|
56
|
+
return {code: null, stdout: '', stderr: '', spawnError: err};
|
|
57
|
+
};
|
|
58
|
+
return {run, spawns};
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
describe('parseCount: parses shrinko --count output', () => {
|
|
62
|
+
it('parses the documented over-budget sample', () => {
|
|
63
|
+
const report = parseCount(sample('count-over-budget.txt'));
|
|
64
|
+
expect(report).toEqual({
|
|
65
|
+
tokens: 8053,
|
|
66
|
+
tokensPct: 98,
|
|
67
|
+
chars: 30320,
|
|
68
|
+
charsPct: 46,
|
|
69
|
+
compressed: 12176,
|
|
70
|
+
compressedPct: 77,
|
|
71
|
+
});
|
|
72
|
+
});
|
|
73
|
+
|
|
74
|
+
it('parses a sample with no compressed line (compressed undefined)', () => {
|
|
75
|
+
const report = parseCount(sample('count-no-compressed.txt'));
|
|
76
|
+
expect(report.tokens).toBe(300);
|
|
77
|
+
expect(report.chars).toBe(1200);
|
|
78
|
+
expect(report.compressed).toBeUndefined();
|
|
79
|
+
expect(report.compressedPct).toBeUndefined();
|
|
80
|
+
});
|
|
81
|
+
|
|
82
|
+
it('parses the count even when embedded in surrounding output/whitespace', () => {
|
|
83
|
+
const noisy = `some banner line\n\n tokens: 8053 98%\n chars: 30320 46%\n compressed: 12176 77%\ntrailing\n`;
|
|
84
|
+
const report = parseCount(noisy);
|
|
85
|
+
expect(report.tokens).toBe(8053);
|
|
86
|
+
expect(report.chars).toBe(30320);
|
|
87
|
+
expect(report.compressed).toBe(12176);
|
|
88
|
+
});
|
|
89
|
+
|
|
90
|
+
it('throws ShrinkoParseError when the required lines are absent', () => {
|
|
91
|
+
expect(() => parseCount('unexpected shrinko output\n')).toThrow(
|
|
92
|
+
ShrinkoParseError,
|
|
93
|
+
);
|
|
94
|
+
});
|
|
95
|
+
});
|
|
96
|
+
|
|
97
|
+
describe('ShellShrinkoAdapter.count: present', () => {
|
|
98
|
+
it('parses --count from the shrinko8 entry-point on PATH', async () => {
|
|
99
|
+
const {run, spawns} = stubRunner({
|
|
100
|
+
presentFile: 'shrinko8',
|
|
101
|
+
stdout: sample('count-over-budget.txt'),
|
|
102
|
+
});
|
|
103
|
+
const adapter = new ShellShrinkoAdapter({env: {PATH: '/fake/bin'}, run});
|
|
104
|
+
const result = await adapter.count('/carts/main.p8');
|
|
105
|
+
|
|
106
|
+
expect(result.ok).toBe(true);
|
|
107
|
+
if (result.ok) {
|
|
108
|
+
expect(result.value.tokens).toBe(8053);
|
|
109
|
+
expect(result.value.chars).toBe(30320);
|
|
110
|
+
expect(result.value.compressed).toBe(12176);
|
|
111
|
+
}
|
|
112
|
+
// It spawned `shrinko8 /carts/main.p8 --count` first and stopped there.
|
|
113
|
+
expect(spawns[0]?.file).toBe('shrinko8');
|
|
114
|
+
expect(spawns[0]?.args).toEqual(['/carts/main.p8', '--count']);
|
|
115
|
+
});
|
|
116
|
+
|
|
117
|
+
it('parses --count even when shrinko prints to stderr', async () => {
|
|
118
|
+
const {run} = stubRunner({
|
|
119
|
+
presentFile: 'shrinko8',
|
|
120
|
+
stdout: '',
|
|
121
|
+
stderr: sample('count-under-budget.txt'),
|
|
122
|
+
});
|
|
123
|
+
const adapter = new ShellShrinkoAdapter({env: {PATH: '/fake/bin'}, run});
|
|
124
|
+
const result = await adapter.count('/carts/main.p8');
|
|
125
|
+
expect(result.ok).toBe(true);
|
|
126
|
+
if (result.ok) expect(result.value.tokens).toBe(512);
|
|
127
|
+
});
|
|
128
|
+
|
|
129
|
+
it('falls back to `python -m shrinko8` when shrinko8 is not on PATH', async () => {
|
|
130
|
+
const {run, spawns} = stubRunner({
|
|
131
|
+
presentFile: 'python',
|
|
132
|
+
stdout: sample('count-over-budget.txt'),
|
|
133
|
+
});
|
|
134
|
+
const adapter = new ShellShrinkoAdapter({env: {PATH: '/fake/bin'}, run});
|
|
135
|
+
const result = await adapter.count('/carts/main.p8');
|
|
136
|
+
|
|
137
|
+
expect(result.ok).toBe(true);
|
|
138
|
+
// First tried shrinko8 (ENOENT), then python -m shrinko8 (spawned).
|
|
139
|
+
expect(spawns.map((s) => s.file)).toEqual(['shrinko8', 'python']);
|
|
140
|
+
expect(spawns[1]?.args).toEqual([
|
|
141
|
+
'-m',
|
|
142
|
+
'shrinko8',
|
|
143
|
+
'/carts/main.p8',
|
|
144
|
+
'--count',
|
|
145
|
+
]);
|
|
146
|
+
});
|
|
147
|
+
});
|
|
148
|
+
|
|
149
|
+
describe('ShellShrinkoAdapter.count: absent (structured, never a crash)', () => {
|
|
150
|
+
it('returns shrinko-not-found with the EXACT remedy when nothing spawns', async () => {
|
|
151
|
+
// Every candidate ENOENTs (an empty PATH is the isolation lever).
|
|
152
|
+
const {run, spawns} = stubRunner({presentFile: undefined});
|
|
153
|
+
const adapter = new ShellShrinkoAdapter({env: {}, run});
|
|
154
|
+
const result = await adapter.count('/carts/main.p8');
|
|
155
|
+
|
|
156
|
+
expect(result.ok).toBe(false);
|
|
157
|
+
if (!result.ok) {
|
|
158
|
+
expect(result.reason).toBe('shrinko-not-found');
|
|
159
|
+
expect(result.remedy).toBe(SHRINKO_REMEDY);
|
|
160
|
+
expect(result.remedy).toBe('uv pip install shrinko');
|
|
161
|
+
expect(result.needs).toEqual(['python>=3.8']);
|
|
162
|
+
}
|
|
163
|
+
// It tried every invocation candidate before giving up.
|
|
164
|
+
expect(spawns.map((s) => s.file)).toEqual([
|
|
165
|
+
'shrinko8',
|
|
166
|
+
'python',
|
|
167
|
+
'python3',
|
|
168
|
+
]);
|
|
169
|
+
});
|
|
170
|
+
|
|
171
|
+
it('passes ONLY the given env to the child (the PATH isolation lever)', async () => {
|
|
172
|
+
const {run, spawns} = stubRunner({
|
|
173
|
+
presentFile: 'shrinko8',
|
|
174
|
+
stdout: sample('count-over-budget.txt'),
|
|
175
|
+
});
|
|
176
|
+
const env = {PATH: '/only/this/bin', SOME_OTHER: 'x'};
|
|
177
|
+
const adapter = new ShellShrinkoAdapter({env, run});
|
|
178
|
+
await adapter.count('/carts/main.p8');
|
|
179
|
+
// The child sees exactly the env we handed the adapter, nothing ambient.
|
|
180
|
+
expect(spawns[0]?.env).toEqual(env);
|
|
181
|
+
});
|
|
182
|
+
});
|
|
@@ -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';
|