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 @@
|
|
|
1
|
+
{"version":3,"file":"session-daemon-entry.js","sourceRoot":"","sources":["../../../src/engine/pico8/session-daemon-entry.ts"],"names":[],"mappings":"AAAA;;;;;;;GAOG;AAEH,OAAO,EAAC,UAAU,EAAC,MAAM,0BAA0B,CAAC;AAEpD,UAAU,CAAC,OAAO,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC,CAAC"}
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The playtest session DAEMON ENTRYPOINT (ADR-0011, prd US #6): the small
|
|
3
|
+
* detached process `playtest start` spawns to OWN a live driven PICO-8 for a
|
|
4
|
+
* session's lifetime. It is invoked via the hidden `playtest __daemon` verb (so it
|
|
5
|
+
* re-uses the exact same executable/loader the parent ran under, dev `tsx` or
|
|
6
|
+
* built `dist`), reads its config from JSON on argv, spawns PICO-8 on the
|
|
7
|
+
* throwaway driven cart with a LIVE stdin, and hands the process to
|
|
8
|
+
* {@link serveSession} to answer verbs over the session socket.
|
|
9
|
+
*
|
|
10
|
+
* PICO-8 ABSENT: the spawn ENOENTs, so the daemon writes a `pico8-not-found`
|
|
11
|
+
* marker file into the session dir and exits; the `start` client polls for either
|
|
12
|
+
* the socket (ready) or that marker (absent) and maps the marker to the same
|
|
13
|
+
* structured `pico8-not-found` boundary as `run`/`audio render`. This keeps the
|
|
14
|
+
* absent path structured WITHOUT the parent ever launching PICO-8 itself (the
|
|
15
|
+
* `pico8 --help` footgun is never touched).
|
|
16
|
+
*/
|
|
17
|
+
import type { SessionPaths } from './supervisor.js';
|
|
18
|
+
/** The marker file the daemon writes into the session dir when PICO-8 is absent. */
|
|
19
|
+
export declare const PICO8_NOT_FOUND_MARKER: "pico8-not-found";
|
|
20
|
+
/** The daemon's launch config, passed as a single JSON argument. */
|
|
21
|
+
export interface DaemonConfig {
|
|
22
|
+
readonly id: string;
|
|
23
|
+
readonly paths: SessionPaths;
|
|
24
|
+
/** Absolute path to the throwaway driven cart to run live. */
|
|
25
|
+
readonly cartPath: string;
|
|
26
|
+
/** The screenshot basename the driven cart names each SHOT. */
|
|
27
|
+
readonly shotBasename: string;
|
|
28
|
+
/** Idle-reap window in ms. */
|
|
29
|
+
readonly idleTimeoutMs: number;
|
|
30
|
+
/** Per-command ACK deadline in ms. */
|
|
31
|
+
readonly ackTimeoutMs: number;
|
|
32
|
+
}
|
|
33
|
+
/**
|
|
34
|
+
* Runs the daemon from its JSON config: spawns PICO-8 live on the driven cart and
|
|
35
|
+
* serves the session, or writes the pico8-not-found marker and exits on ENOENT.
|
|
36
|
+
* Exported so a test can drive it with an injected spawner (no real binary).
|
|
37
|
+
*/
|
|
38
|
+
export declare function runDaemon(config: DaemonConfig, env?: NodeJS.ProcessEnv, spawn?: import("./shell.js").SpawnRunner): void;
|
|
39
|
+
/**
|
|
40
|
+
* The daemon process main: reads the JSON config from `argv[0]` and runs. Called
|
|
41
|
+
* by the hidden `playtest __daemon <configJson>` verb in a detached child.
|
|
42
|
+
*/
|
|
43
|
+
export declare function daemonMain(argv: readonly string[]): void;
|
|
44
|
+
//# sourceMappingURL=session-daemon-main.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"session-daemon-main.d.ts","sourceRoot":"","sources":["../../../src/engine/pico8/session-daemon-main.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;GAeG;AAOH,OAAO,KAAK,EAAC,YAAY,EAAC,MAAM,iBAAiB,CAAC;AAElD,oFAAoF;AACpF,eAAO,MAAM,sBAAsB,EAAG,iBAA0B,CAAC;AAEjE,oEAAoE;AACpE,MAAM,WAAW,YAAY;IAC5B,QAAQ,CAAC,EAAE,EAAE,MAAM,CAAC;IACpB,QAAQ,CAAC,KAAK,EAAE,YAAY,CAAC;IAC7B,8DAA8D;IAC9D,QAAQ,CAAC,QAAQ,EAAE,MAAM,CAAC;IAC1B,+DAA+D;IAC/D,QAAQ,CAAC,YAAY,EAAE,MAAM,CAAC;IAC9B,8BAA8B;IAC9B,QAAQ,CAAC,aAAa,EAAE,MAAM,CAAC;IAC/B,sCAAsC;IACtC,QAAQ,CAAC,YAAY,EAAE,MAAM,CAAC;CAC9B;AAED;;;;GAIG;AACH,wBAAgB,SAAS,CACxB,MAAM,EAAE,YAAY,EACpB,GAAG,GAAE,MAAM,CAAC,UAAwB,EACpC,KAAK,mCAAc,GACjB,IAAI,CAiCN;AAED;;;GAGG;AACH,wBAAgB,UAAU,CAAC,IAAI,EAAE,SAAS,MAAM,EAAE,GAAG,IAAI,CAQxD"}
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The playtest session DAEMON ENTRYPOINT (ADR-0011, prd US #6): the small
|
|
3
|
+
* detached process `playtest start` spawns to OWN a live driven PICO-8 for a
|
|
4
|
+
* session's lifetime. It is invoked via the hidden `playtest __daemon` verb (so it
|
|
5
|
+
* re-uses the exact same executable/loader the parent ran under, dev `tsx` or
|
|
6
|
+
* built `dist`), reads its config from JSON on argv, spawns PICO-8 on the
|
|
7
|
+
* throwaway driven cart with a LIVE stdin, and hands the process to
|
|
8
|
+
* {@link serveSession} to answer verbs over the session socket.
|
|
9
|
+
*
|
|
10
|
+
* PICO-8 ABSENT: the spawn ENOENTs, so the daemon writes a `pico8-not-found`
|
|
11
|
+
* marker file into the session dir and exits; the `start` client polls for either
|
|
12
|
+
* the socket (ready) or that marker (absent) and maps the marker to the same
|
|
13
|
+
* structured `pico8-not-found` boundary as `run`/`audio render`. This keeps the
|
|
14
|
+
* absent path structured WITHOUT the parent ever launching PICO-8 itself (the
|
|
15
|
+
* `pico8 --help` footgun is never touched).
|
|
16
|
+
*/
|
|
17
|
+
import { writeFileSync } from 'node:fs';
|
|
18
|
+
import { join } from 'node:path';
|
|
19
|
+
import { pico8NotFound } from './adapter.js';
|
|
20
|
+
import { serveSession } from './session-daemon.js';
|
|
21
|
+
import { spawnRunner, pico8Candidates, withPico8Home } from './shell.js';
|
|
22
|
+
/** The marker file the daemon writes into the session dir when PICO-8 is absent. */
|
|
23
|
+
export const PICO8_NOT_FOUND_MARKER = 'pico8-not-found';
|
|
24
|
+
/**
|
|
25
|
+
* Runs the daemon from its JSON config: spawns PICO-8 live on the driven cart and
|
|
26
|
+
* serves the session, or writes the pico8-not-found marker and exits on ENOENT.
|
|
27
|
+
* Exported so a test can drive it with an injected spawner (no real binary).
|
|
28
|
+
*/
|
|
29
|
+
export function runDaemon(config, env = process.env, spawn = spawnRunner) {
|
|
30
|
+
const [file] = pico8Candidates(env);
|
|
31
|
+
// Live session: `-desktop <shotDir>` for SHOT captures, `-x` headless, stdin
|
|
32
|
+
// piped so the daemon can send fixed-size command blocks per verb.
|
|
33
|
+
const args = ['-desktop', config.paths.shotDir, '-x', config.cartPath];
|
|
34
|
+
// `-home <isolated>` so PICO-8's config/data tree lands in a throwaway dir, not
|
|
35
|
+
// the caller's CWD (matches the shell adapter's spawn sites).
|
|
36
|
+
const proc = spawn(file, withPico8Home(args), env, { stdin: true });
|
|
37
|
+
// A spawn ENOENT means PICO-8 is absent (a bad PICO8_PATH surfaces the same
|
|
38
|
+
// way): leave the structured marker the `start` client polls for, then exit.
|
|
39
|
+
// This can only happen at spawn time (a launched binary does not ENOENT later),
|
|
40
|
+
// so it is unambiguously the absent boundary, not a mid-session fault.
|
|
41
|
+
proc.onError((err) => {
|
|
42
|
+
if (err.code === 'ENOENT') {
|
|
43
|
+
writeFileSync(join(config.paths.dir, PICO8_NOT_FOUND_MARKER), JSON.stringify(pico8NotFound()));
|
|
44
|
+
process.exit(0);
|
|
45
|
+
}
|
|
46
|
+
process.exit(1);
|
|
47
|
+
});
|
|
48
|
+
serveSession({
|
|
49
|
+
id: config.id,
|
|
50
|
+
paths: config.paths,
|
|
51
|
+
process: proc,
|
|
52
|
+
shotBasename: config.shotBasename,
|
|
53
|
+
idleTimeoutMs: config.idleTimeoutMs,
|
|
54
|
+
ackTimeoutMs: config.ackTimeoutMs,
|
|
55
|
+
onExit: () => process.exit(0),
|
|
56
|
+
});
|
|
57
|
+
}
|
|
58
|
+
/**
|
|
59
|
+
* The daemon process main: reads the JSON config from `argv[0]` and runs. Called
|
|
60
|
+
* by the hidden `playtest __daemon <configJson>` verb in a detached child.
|
|
61
|
+
*/
|
|
62
|
+
export function daemonMain(argv) {
|
|
63
|
+
const raw = argv[0];
|
|
64
|
+
if (raw === undefined) {
|
|
65
|
+
process.stderr.write('playtest daemon: missing config\n');
|
|
66
|
+
process.exit(2);
|
|
67
|
+
}
|
|
68
|
+
const config = JSON.parse(raw);
|
|
69
|
+
runDaemon(config);
|
|
70
|
+
}
|
|
71
|
+
//# sourceMappingURL=session-daemon-main.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"session-daemon-main.js","sourceRoot":"","sources":["../../../src/engine/pico8/session-daemon-main.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;GAeG;AAEH,OAAO,EAAC,aAAa,EAAC,MAAM,SAAS,CAAC;AACtC,OAAO,EAAC,IAAI,EAAC,MAAM,WAAW,CAAC;AAC/B,OAAO,EAAC,aAAa,EAAC,MAAM,cAAc,CAAC;AAC3C,OAAO,EAAC,YAAY,EAAC,MAAM,qBAAqB,CAAC;AACjD,OAAO,EAAC,WAAW,EAAE,eAAe,EAAE,aAAa,EAAC,MAAM,YAAY,CAAC;AAGvE,oFAAoF;AACpF,MAAM,CAAC,MAAM,sBAAsB,GAAG,iBAA0B,CAAC;AAgBjE;;;;GAIG;AACH,MAAM,UAAU,SAAS,CACxB,MAAoB,EACpB,MAAyB,OAAO,CAAC,GAAG,EACpC,KAAK,GAAG,WAAW;IAEnB,MAAM,CAAC,IAAI,CAAC,GAAG,eAAe,CAAC,GAAG,CAA0B,CAAC;IAC7D,6EAA6E;IAC7E,mEAAmE;IACnE,MAAM,IAAI,GAAG,CAAC,UAAU,EAAE,MAAM,CAAC,KAAK,CAAC,OAAO,EAAE,IAAI,EAAE,MAAM,CAAC,QAAQ,CAAC,CAAC;IACvE,gFAAgF;IAChF,8DAA8D;IAC9D,MAAM,IAAI,GAAG,KAAK,CAAC,IAAI,EAAE,aAAa,CAAC,IAAI,CAAC,EAAE,GAAG,EAAE,EAAC,KAAK,EAAE,IAAI,EAAC,CAAC,CAAC;IAElE,4EAA4E;IAC5E,6EAA6E;IAC7E,gFAAgF;IAChF,uEAAuE;IACvE,IAAI,CAAC,OAAO,CAAC,CAAC,GAAG,EAAE,EAAE;QACpB,IAAI,GAAG,CAAC,IAAI,KAAK,QAAQ,EAAE,CAAC;YAC3B,aAAa,CACZ,IAAI,CAAC,MAAM,CAAC,KAAK,CAAC,GAAG,EAAE,sBAAsB,CAAC,EAC9C,IAAI,CAAC,SAAS,CAAC,aAAa,EAAE,CAAC,CAC/B,CAAC;YACF,OAAO,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC;QACjB,CAAC;QACD,OAAO,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC;IACjB,CAAC,CAAC,CAAC;IAEH,YAAY,CAAC;QACZ,EAAE,EAAE,MAAM,CAAC,EAAE;QACb,KAAK,EAAE,MAAM,CAAC,KAAK;QACnB,OAAO,EAAE,IAAI;QACb,YAAY,EAAE,MAAM,CAAC,YAAY;QACjC,aAAa,EAAE,MAAM,CAAC,aAAa;QACnC,YAAY,EAAE,MAAM,CAAC,YAAY;QACjC,MAAM,EAAE,GAAG,EAAE,CAAC,OAAO,CAAC,IAAI,CAAC,CAAC,CAAC;KAC7B,CAAC,CAAC;AACJ,CAAC;AAED;;;GAGG;AACH,MAAM,UAAU,UAAU,CAAC,IAAuB;IACjD,MAAM,GAAG,GAAG,IAAI,CAAC,CAAC,CAAC,CAAC;IACpB,IAAI,GAAG,KAAK,SAAS,EAAE,CAAC;QACvB,OAAO,CAAC,MAAM,CAAC,KAAK,CAAC,mCAAmC,CAAC,CAAC;QAC1D,OAAO,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC;IACjB,CAAC;IACD,MAAM,MAAM,GAAG,IAAI,CAAC,KAAK,CAAC,GAAG,CAAiB,CAAC;IAC/C,SAAS,CAAC,MAAM,CAAC,CAAC;AACnB,CAAC"}
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The playtest session DAEMON runtime + CLIENT (ADR-0011, prd US #6). This is the
|
|
3
|
+
* IO half of the supervisor: the daemon LISTENS on a session's Unix domain socket
|
|
4
|
+
* and drives a live {@link DriveSession} on behalf of each verb sub-invocation;
|
|
5
|
+
* the {@link SessionClient} CONNECTS to that socket to send a verb and read back
|
|
6
|
+
* the structured response.
|
|
7
|
+
*
|
|
8
|
+
* The daemon owns the live process the whole session, so the game stays ALIVE +
|
|
9
|
+
* PAUSED between the agent's turns. It self-reaps on an IDLE timeout (the orphan
|
|
10
|
+
* path) and on `stop`. The live daemon end-to-end is the manual/opt-in tier; the
|
|
11
|
+
* pure pieces it composes (the handshake {@link DriveSession}, the registry, the
|
|
12
|
+
* frame codec) are unit-tested without a real socket or binary.
|
|
13
|
+
*/
|
|
14
|
+
import type { Pico8Process } from './shell.js';
|
|
15
|
+
import { type SessionPaths, type SessionRequest, type SessionResponse } from './supervisor.js';
|
|
16
|
+
/**
|
|
17
|
+
* A client for a running session daemon: connects to its socket, sends ONE verb
|
|
18
|
+
* request, awaits the single response, and closes. Each `playtest step/input/shot
|
|
19
|
+
* /stop` sub-invocation makes exactly one of these round-trips.
|
|
20
|
+
*/
|
|
21
|
+
export declare class SessionClient {
|
|
22
|
+
private readonly socketPath;
|
|
23
|
+
constructor(socketPath: string);
|
|
24
|
+
/**
|
|
25
|
+
* Sends `request` to the daemon and resolves its {@link SessionResponse}. A
|
|
26
|
+
* connection failure (no daemon / dead socket) rejects with a plain Error the
|
|
27
|
+
* command maps to `playtest-session-not-found`.
|
|
28
|
+
*/
|
|
29
|
+
send(request: SessionRequest): Promise<SessionResponse>;
|
|
30
|
+
}
|
|
31
|
+
/** Whether a session daemon is reachable (its socket exists). */
|
|
32
|
+
export declare function sessionExists(paths: SessionPaths): boolean;
|
|
33
|
+
/**
|
|
34
|
+
* Spawns the session DAEMON as a DETACHED background process that outlives this
|
|
35
|
+
* (the `start`) invocation, so the driven PICO-8 stays alive across the agent's
|
|
36
|
+
* separate verb turns. It re-invokes the current executable + `execArgv` (so the
|
|
37
|
+
* daemon runs under the SAME loader, dev `tsx` or built node) on the daemon entry
|
|
38
|
+
* file, passing the config as a single JSON arg. `unref` + `detached` cut it free
|
|
39
|
+
* from this process's lifetime; its stdio is ignored (the daemon talks over the
|
|
40
|
+
* session socket, not this process's pipes).
|
|
41
|
+
*/
|
|
42
|
+
export declare function spawnSessionDaemon(config: import('./session-daemon-main.js').DaemonConfig): void;
|
|
43
|
+
/** Options for {@link serveSession}. */
|
|
44
|
+
export interface ServeSessionOptions {
|
|
45
|
+
readonly id: string;
|
|
46
|
+
readonly paths: SessionPaths;
|
|
47
|
+
/** The live driven process to own for this session (spawned by the caller). */
|
|
48
|
+
readonly process: Pico8Process;
|
|
49
|
+
/** The screenshot basename the driven cart names each SHOT (matches transform). */
|
|
50
|
+
readonly shotBasename: string;
|
|
51
|
+
/** Idle-reap window (defaults to {@link DEFAULT_IDLE_TIMEOUT_MS}). */
|
|
52
|
+
readonly idleTimeoutMs?: number;
|
|
53
|
+
/** Per-command ACK deadline forwarded to {@link DriveSession}. */
|
|
54
|
+
readonly ackTimeoutMs?: number;
|
|
55
|
+
/** Called when the daemon has fully torn down (for the process to exit). */
|
|
56
|
+
readonly onExit?: () => void;
|
|
57
|
+
}
|
|
58
|
+
/**
|
|
59
|
+
* Runs the session daemon: wraps the live process in a {@link DriveSession},
|
|
60
|
+
* listens on the session socket, and dispatches each incoming verb request to the
|
|
61
|
+
* handshake (send-command -> wait-ack -> respond). Resets an IDLE-reap timer on
|
|
62
|
+
* every verb, so an orphaned session tears down on its own. Returns the net
|
|
63
|
+
* server so the caller (the daemon entrypoint) can await its lifetime.
|
|
64
|
+
*/
|
|
65
|
+
export declare function serveSession(options: ServeSessionOptions): {
|
|
66
|
+
close: () => void;
|
|
67
|
+
};
|
|
68
|
+
//# sourceMappingURL=session-daemon.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"session-daemon.d.ts","sourceRoot":"","sources":["../../../src/engine/pico8/session-daemon.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;GAYG;AAaH,OAAO,KAAK,EAAC,YAAY,EAAC,MAAM,YAAY,CAAC;AAC7C,OAAO,EAIN,KAAK,YAAY,EACjB,KAAK,cAAc,EACnB,KAAK,eAAe,EAEpB,MAAM,iBAAiB,CAAC;AAEzB;;;;GAIG;AACH,qBAAa,aAAa;IACb,OAAO,CAAC,QAAQ,CAAC,UAAU;gBAAV,UAAU,EAAE,MAAM;IAE/C;;;;OAIG;IACH,IAAI,CAAC,OAAO,EAAE,cAAc,GAAG,OAAO,CAAC,eAAe,CAAC;CA6BvD;AAED,iEAAiE;AACjE,wBAAgB,aAAa,CAAC,KAAK,EAAE,YAAY,GAAG,OAAO,CAE1D;AAWD;;;;;;;;GAQG;AACH,wBAAgB,kBAAkB,CACjC,MAAM,EAAE,OAAO,0BAA0B,EAAE,YAAY,GACrD,IAAI,CAON;AAED,wCAAwC;AACxC,MAAM,WAAW,mBAAmB;IACnC,QAAQ,CAAC,EAAE,EAAE,MAAM,CAAC;IACpB,QAAQ,CAAC,KAAK,EAAE,YAAY,CAAC;IAC7B,+EAA+E;IAC/E,QAAQ,CAAC,OAAO,EAAE,YAAY,CAAC;IAC/B,mFAAmF;IACnF,QAAQ,CAAC,YAAY,EAAE,MAAM,CAAC;IAC9B,sEAAsE;IACtE,QAAQ,CAAC,aAAa,CAAC,EAAE,MAAM,CAAC;IAChC,kEAAkE;IAClE,QAAQ,CAAC,YAAY,CAAC,EAAE,MAAM,CAAC;IAC/B,4EAA4E;IAC5E,QAAQ,CAAC,MAAM,CAAC,EAAE,MAAM,IAAI,CAAC;CAC7B;AAED;;;;;;GAMG;AACH,wBAAgB,YAAY,CAAC,OAAO,EAAE,mBAAmB,GAAG;IAC3D,KAAK,EAAE,MAAM,IAAI,CAAC;CAClB,CA0IA"}
|
|
@@ -0,0 +1,264 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The playtest session DAEMON runtime + CLIENT (ADR-0011, prd US #6). This is the
|
|
3
|
+
* IO half of the supervisor: the daemon LISTENS on a session's Unix domain socket
|
|
4
|
+
* and drives a live {@link DriveSession} on behalf of each verb sub-invocation;
|
|
5
|
+
* the {@link SessionClient} CONNECTS to that socket to send a verb and read back
|
|
6
|
+
* the structured response.
|
|
7
|
+
*
|
|
8
|
+
* The daemon owns the live process the whole session, so the game stays ALIVE +
|
|
9
|
+
* PAUSED between the agent's turns. It self-reaps on an IDLE timeout (the orphan
|
|
10
|
+
* path) and on `stop`. The live daemon end-to-end is the manual/opt-in tier; the
|
|
11
|
+
* pure pieces it composes (the handshake {@link DriveSession}, the registry, the
|
|
12
|
+
* frame codec) are unit-tested without a real socket or binary.
|
|
13
|
+
*/
|
|
14
|
+
import { spawn } from 'node:child_process';
|
|
15
|
+
import { createConnection, createServer } from 'node:net';
|
|
16
|
+
import { existsSync, readdirSync, rmSync, writeFileSync } from 'node:fs';
|
|
17
|
+
import { dirname, join } from 'node:path';
|
|
18
|
+
import { fileURLToPath } from 'node:url';
|
|
19
|
+
import { DriveSession, SessionError, } from './session.js';
|
|
20
|
+
import { DEFAULT_IDLE_TIMEOUT_MS, encodeFrame, FrameDecoder, } from './supervisor.js';
|
|
21
|
+
/**
|
|
22
|
+
* A client for a running session daemon: connects to its socket, sends ONE verb
|
|
23
|
+
* request, awaits the single response, and closes. Each `playtest step/input/shot
|
|
24
|
+
* /stop` sub-invocation makes exactly one of these round-trips.
|
|
25
|
+
*/
|
|
26
|
+
export class SessionClient {
|
|
27
|
+
socketPath;
|
|
28
|
+
constructor(socketPath) {
|
|
29
|
+
this.socketPath = socketPath;
|
|
30
|
+
}
|
|
31
|
+
/**
|
|
32
|
+
* Sends `request` to the daemon and resolves its {@link SessionResponse}. A
|
|
33
|
+
* connection failure (no daemon / dead socket) rejects with a plain Error the
|
|
34
|
+
* command maps to `playtest-session-not-found`.
|
|
35
|
+
*/
|
|
36
|
+
send(request) {
|
|
37
|
+
return new Promise((resolve, reject) => {
|
|
38
|
+
const decoder = new FrameDecoder();
|
|
39
|
+
let settled = false;
|
|
40
|
+
const sock = createConnection(this.socketPath);
|
|
41
|
+
sock.setEncoding('utf8');
|
|
42
|
+
sock.on('connect', () => sock.write(encodeFrame(request)));
|
|
43
|
+
sock.on('data', (chunk) => {
|
|
44
|
+
const [msg] = decoder.push(chunk);
|
|
45
|
+
if (msg !== undefined && !settled) {
|
|
46
|
+
settled = true;
|
|
47
|
+
resolve(msg);
|
|
48
|
+
sock.end();
|
|
49
|
+
}
|
|
50
|
+
});
|
|
51
|
+
sock.on('error', (err) => {
|
|
52
|
+
if (!settled) {
|
|
53
|
+
settled = true;
|
|
54
|
+
reject(err);
|
|
55
|
+
}
|
|
56
|
+
});
|
|
57
|
+
sock.on('close', () => {
|
|
58
|
+
if (!settled) {
|
|
59
|
+
settled = true;
|
|
60
|
+
reject(new Error('daemon closed the connection with no response'));
|
|
61
|
+
}
|
|
62
|
+
});
|
|
63
|
+
});
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
/** Whether a session daemon is reachable (its socket exists). */
|
|
67
|
+
export function sessionExists(paths) {
|
|
68
|
+
return existsSync(paths.socket);
|
|
69
|
+
}
|
|
70
|
+
/** Resolves the sibling daemon-entry module (`.ts` in dev, `.js` when built). */
|
|
71
|
+
function daemonEntryPath() {
|
|
72
|
+
const here = dirname(fileURLToPath(import.meta.url));
|
|
73
|
+
// This module and the entry are compiled/loaded together, so the extension
|
|
74
|
+
// here (`.ts` under tsx, `.js` under dist) is the entry's extension too.
|
|
75
|
+
const ext = import.meta.url.endsWith('.ts') ? '.ts' : '.js';
|
|
76
|
+
return join(here, `session-daemon-entry${ext}`);
|
|
77
|
+
}
|
|
78
|
+
/**
|
|
79
|
+
* Spawns the session DAEMON as a DETACHED background process that outlives this
|
|
80
|
+
* (the `start`) invocation, so the driven PICO-8 stays alive across the agent's
|
|
81
|
+
* separate verb turns. It re-invokes the current executable + `execArgv` (so the
|
|
82
|
+
* daemon runs under the SAME loader, dev `tsx` or built node) on the daemon entry
|
|
83
|
+
* file, passing the config as a single JSON arg. `unref` + `detached` cut it free
|
|
84
|
+
* from this process's lifetime; its stdio is ignored (the daemon talks over the
|
|
85
|
+
* session socket, not this process's pipes).
|
|
86
|
+
*/
|
|
87
|
+
export function spawnSessionDaemon(config) {
|
|
88
|
+
const child = spawn(process.execPath, [...process.execArgv, daemonEntryPath(), JSON.stringify(config)], { detached: true, stdio: 'ignore' });
|
|
89
|
+
child.unref();
|
|
90
|
+
}
|
|
91
|
+
/**
|
|
92
|
+
* Runs the session daemon: wraps the live process in a {@link DriveSession},
|
|
93
|
+
* listens on the session socket, and dispatches each incoming verb request to the
|
|
94
|
+
* handshake (send-command -> wait-ack -> respond). Resets an IDLE-reap timer on
|
|
95
|
+
* every verb, so an orphaned session tears down on its own. Returns the net
|
|
96
|
+
* server so the caller (the daemon entrypoint) can await its lifetime.
|
|
97
|
+
*/
|
|
98
|
+
export function serveSession(options) {
|
|
99
|
+
const session = new DriveSession({
|
|
100
|
+
process: options.process,
|
|
101
|
+
shotBasename: options.shotBasename,
|
|
102
|
+
ackTimeoutMs: options.ackTimeoutMs,
|
|
103
|
+
});
|
|
104
|
+
const idleMs = options.idleTimeoutMs ?? DEFAULT_IDLE_TIMEOUT_MS;
|
|
105
|
+
let idleTimer;
|
|
106
|
+
let tornDown = false;
|
|
107
|
+
const teardown = () => {
|
|
108
|
+
if (tornDown)
|
|
109
|
+
return;
|
|
110
|
+
tornDown = true;
|
|
111
|
+
if (idleTimer !== undefined)
|
|
112
|
+
clearTimeout(idleTimer);
|
|
113
|
+
try {
|
|
114
|
+
session.stop();
|
|
115
|
+
}
|
|
116
|
+
catch {
|
|
117
|
+
// best-effort; the process kill below is the real backstop
|
|
118
|
+
}
|
|
119
|
+
try {
|
|
120
|
+
server.close();
|
|
121
|
+
}
|
|
122
|
+
catch {
|
|
123
|
+
// ignore
|
|
124
|
+
}
|
|
125
|
+
// Reap the WHOLE session dir (socket + pidfile + driven cart + shots), so an
|
|
126
|
+
// idle/orphan teardown leaves nothing behind, not just the socket. (The CLI
|
|
127
|
+
// `stop` also sweeps the dir; doing it here covers the no-`stop` orphan path.)
|
|
128
|
+
try {
|
|
129
|
+
rmSync(options.paths.dir, { recursive: true, force: true });
|
|
130
|
+
}
|
|
131
|
+
catch {
|
|
132
|
+
// ignore
|
|
133
|
+
}
|
|
134
|
+
options.onExit?.();
|
|
135
|
+
};
|
|
136
|
+
const armIdle = () => {
|
|
137
|
+
if (idleTimer !== undefined)
|
|
138
|
+
clearTimeout(idleTimer);
|
|
139
|
+
idleTimer = setTimeout(teardown, idleMs);
|
|
140
|
+
idleTimer.unref?.();
|
|
141
|
+
};
|
|
142
|
+
const handle = async (req) => {
|
|
143
|
+
armIdle();
|
|
144
|
+
try {
|
|
145
|
+
switch (req.verb) {
|
|
146
|
+
case 'input': {
|
|
147
|
+
const r = await session.input(req.held);
|
|
148
|
+
return ok(options.id, { frame: r.frame, ack: r.ack, alive: true });
|
|
149
|
+
}
|
|
150
|
+
case 'step': {
|
|
151
|
+
// A frames span may exceed one byte; the daemon issues it as repeated
|
|
152
|
+
// single-byte steps, each awaiting its own STEP-DONE ack, so the total
|
|
153
|
+
// is exact and lossless regardless of size.
|
|
154
|
+
let last = { frame: session.frame, ack: '' };
|
|
155
|
+
let remaining = Math.max(0, Math.floor(req.frames));
|
|
156
|
+
while (remaining > 0) {
|
|
157
|
+
const chunk = Math.min(remaining, 0xff);
|
|
158
|
+
last = await session.step(chunk);
|
|
159
|
+
remaining -= chunk;
|
|
160
|
+
}
|
|
161
|
+
return ok(options.id, {
|
|
162
|
+
frame: last.frame,
|
|
163
|
+
ack: last.ack,
|
|
164
|
+
alive: true,
|
|
165
|
+
});
|
|
166
|
+
}
|
|
167
|
+
case 'shot': {
|
|
168
|
+
const r = await session.shot();
|
|
169
|
+
const screenshot = collectShot(options.paths.shotDir, r.shotName);
|
|
170
|
+
return ok(options.id, {
|
|
171
|
+
frame: r.frame,
|
|
172
|
+
ack: r.ack,
|
|
173
|
+
shotName: r.shotName,
|
|
174
|
+
screenshot,
|
|
175
|
+
alive: true,
|
|
176
|
+
});
|
|
177
|
+
}
|
|
178
|
+
case 'status': {
|
|
179
|
+
return ok(options.id, {
|
|
180
|
+
frame: session.frame,
|
|
181
|
+
alive: !session.isDead,
|
|
182
|
+
});
|
|
183
|
+
}
|
|
184
|
+
case 'stop': {
|
|
185
|
+
// The connection handler tears the daemon down AFTER this response has
|
|
186
|
+
// flushed to the client (so `stop` returns cleanly, no close-race).
|
|
187
|
+
return ok(options.id, { frame: session.frame, alive: false });
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
catch (e) {
|
|
192
|
+
if (e instanceof SessionError) {
|
|
193
|
+
return { ok: false, code: e.code, message: e.message };
|
|
194
|
+
}
|
|
195
|
+
return {
|
|
196
|
+
ok: false,
|
|
197
|
+
code: 'playtest-session-error',
|
|
198
|
+
message: e instanceof Error ? e.message : String(e),
|
|
199
|
+
};
|
|
200
|
+
}
|
|
201
|
+
};
|
|
202
|
+
const server = createServer((sock) => {
|
|
203
|
+
sock.setEncoding('utf8');
|
|
204
|
+
const decoder = new FrameDecoder();
|
|
205
|
+
sock.on('data', (chunk) => {
|
|
206
|
+
for (const req of decoder.push(chunk)) {
|
|
207
|
+
const isStop = req.verb === 'stop';
|
|
208
|
+
void handle(req).then((res) => {
|
|
209
|
+
try {
|
|
210
|
+
// Flush the response, THEN (for `stop`) end the socket and tear the
|
|
211
|
+
// daemon down, so the client always reads the stop reply first.
|
|
212
|
+
sock.write(encodeFrame(res), () => {
|
|
213
|
+
if (isStop) {
|
|
214
|
+
sock.end();
|
|
215
|
+
teardown();
|
|
216
|
+
}
|
|
217
|
+
});
|
|
218
|
+
}
|
|
219
|
+
catch {
|
|
220
|
+
if (isStop)
|
|
221
|
+
teardown();
|
|
222
|
+
}
|
|
223
|
+
});
|
|
224
|
+
}
|
|
225
|
+
});
|
|
226
|
+
sock.on('error', () => {
|
|
227
|
+
/* a dropped client is not fatal to the daemon */
|
|
228
|
+
});
|
|
229
|
+
});
|
|
230
|
+
// If the driven process dies, the next verb reports it (process-dead); we do
|
|
231
|
+
// NOT auto-teardown here, so the agent can still `stop`/`status` and get the
|
|
232
|
+
// structured error rather than a vanished socket.
|
|
233
|
+
server.listen(options.paths.socket, () => {
|
|
234
|
+
writeFileSync(options.paths.pidfile, String(process.pid));
|
|
235
|
+
armIdle();
|
|
236
|
+
});
|
|
237
|
+
return { close: teardown };
|
|
238
|
+
}
|
|
239
|
+
/** Builds an ok {@link SessionResponse} from a partial value + the id. */
|
|
240
|
+
function ok(id, partial) {
|
|
241
|
+
return { ok: true, value: { id, ...partial } };
|
|
242
|
+
}
|
|
243
|
+
/**
|
|
244
|
+
* Resolves the absolute path of the screenshot a SHOT just produced in the
|
|
245
|
+
* `-desktop` dir. The cart named it `<shotName>` (basename) and PICO-8 appends
|
|
246
|
+
* `.png`; if the exact file is not yet visible (a filesystem race) it falls back
|
|
247
|
+
* to the most-recent PNG, so the caller still gets a path to read.
|
|
248
|
+
*/
|
|
249
|
+
function collectShot(shotDir, shotName) {
|
|
250
|
+
const exact = join(shotDir, `${shotName}.png`);
|
|
251
|
+
if (existsSync(exact))
|
|
252
|
+
return exact;
|
|
253
|
+
try {
|
|
254
|
+
const pngs = readdirSync(shotDir)
|
|
255
|
+
.filter((f) => f.toLowerCase().endsWith('.png'))
|
|
256
|
+
.sort();
|
|
257
|
+
const last = pngs.at(-1);
|
|
258
|
+
return last !== undefined ? join(shotDir, last) : undefined;
|
|
259
|
+
}
|
|
260
|
+
catch {
|
|
261
|
+
return undefined;
|
|
262
|
+
}
|
|
263
|
+
}
|
|
264
|
+
//# sourceMappingURL=session-daemon.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"session-daemon.js","sourceRoot":"","sources":["../../../src/engine/pico8/session-daemon.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,140 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The RESUMABLE playtest SESSION handshake core (ADR-0011, prd US #6): the pure,
|
|
3
|
+
* CI-testable half of the "A" live model. It keeps a single driven PICO-8 process
|
|
4
|
+
* ALIVE and PAUSED between the agent's turns and drives it one command at a time
|
|
5
|
+
* over the verified transport: write a FIXED-SIZE command block, WAIT for the
|
|
6
|
+
* cart's ACK line on stdout, then return. The handshake (not wall-clock spacing)
|
|
7
|
+
* is what makes stepping deterministic and lossless, so the agent always acts on
|
|
8
|
+
* a SETTLED, known frame.
|
|
9
|
+
*
|
|
10
|
+
* This module owns ONLY the send -> wait-ack -> return logic against the
|
|
11
|
+
* {@link Pico8Process} seam (the same seam the one-shot `drive` uses), so every
|
|
12
|
+
* path (a completed step, a lost/late ack, the process dying mid-command) is
|
|
13
|
+
* unit-tested against a FAKE process WITHOUT the paid binary. The daemon that
|
|
14
|
+
* spawns the real process and exposes the session to separate CLI invocations by
|
|
15
|
+
* id lives in `supervisor.ts`; a live multi-turn session is the manual/opt-in
|
|
16
|
+
* tier. The cart-side transform is reused UNCHANGED from `drive.ts`.
|
|
17
|
+
*/
|
|
18
|
+
import type { Pico8Process } from './shell.js';
|
|
19
|
+
/**
|
|
20
|
+
* A structured reason a session command could not complete (a boundary, not a
|
|
21
|
+
* crash). Surfaced by the supervisor/command as incur's `error({code,...})`.
|
|
22
|
+
*/
|
|
23
|
+
export type SessionErrorCode =
|
|
24
|
+
/** No ACK arrived within the deadline (the lost/late-ack path). */
|
|
25
|
+
'playtest-session-ack-timeout'
|
|
26
|
+
/** PICO-8 exited (crashed/quit) while a command was in flight or between them. */
|
|
27
|
+
| 'playtest-session-process-dead'
|
|
28
|
+
/** A command was issued after the session was already stopped/closed. */
|
|
29
|
+
| 'playtest-session-closed';
|
|
30
|
+
/** A structured session-command failure (never a raw throw across the seam). */
|
|
31
|
+
export declare class SessionError extends Error {
|
|
32
|
+
readonly code: SessionErrorCode;
|
|
33
|
+
constructor(code: SessionErrorCode, message: string);
|
|
34
|
+
}
|
|
35
|
+
/**
|
|
36
|
+
* How long a single command waits for its ACK before it is declared lost (the
|
|
37
|
+
* lost/late-ack path). Generous: a STEP of many frames takes real time at 30fps,
|
|
38
|
+
* plus PICO-8 scheduling jitter, but a hung/dead cart must not wall the session
|
|
39
|
+
* forever. The supervisor's process-death signal (see {@link DriveSession}) ends
|
|
40
|
+
* a command EARLY when the process dies, so this only fires for a truly stuck
|
|
41
|
+
* cart.
|
|
42
|
+
*/
|
|
43
|
+
export declare const DEFAULT_ACK_TIMEOUT_MS = 15000;
|
|
44
|
+
/** The result of one completed session command (the structured per-verb envelope). */
|
|
45
|
+
export interface StepResult {
|
|
46
|
+
/** The total frames advanced across the session's lifetime so far. */
|
|
47
|
+
readonly frame: number;
|
|
48
|
+
/** The ACK line the cart printed for this command (the settled-frame proof). */
|
|
49
|
+
readonly ack: string;
|
|
50
|
+
}
|
|
51
|
+
/** The result of a `shot` command: the frame + the basename the cart named it. */
|
|
52
|
+
export interface ShotResult extends StepResult {
|
|
53
|
+
/** The screenshot basename the cart queued (`<basename><n>`), for collection. */
|
|
54
|
+
readonly shotName: string;
|
|
55
|
+
}
|
|
56
|
+
/** Options for a {@link DriveSession}. */
|
|
57
|
+
export interface DriveSessionOptions {
|
|
58
|
+
/** The already-spawned live driven process (owned by the caller/supervisor). */
|
|
59
|
+
readonly process: Pico8Process;
|
|
60
|
+
/** Per-command ACK deadline (defaults to {@link DEFAULT_ACK_TIMEOUT_MS}). */
|
|
61
|
+
readonly ackTimeoutMs?: number;
|
|
62
|
+
/**
|
|
63
|
+
* The screenshot basename the driven cart's SHOT names each capture (matches
|
|
64
|
+
* the transform's `shotBasename`); the session reports `<basename><n>`.
|
|
65
|
+
*/
|
|
66
|
+
readonly shotBasename: string;
|
|
67
|
+
}
|
|
68
|
+
/**
|
|
69
|
+
* The live-session handshake engine (ADR-0011, US #6). Wraps ONE driven
|
|
70
|
+
* {@link Pico8Process} and exposes the agent's verbs: {@link input} (set the
|
|
71
|
+
* held-buttons byte), {@link step} (advance exactly N frames), {@link shot}
|
|
72
|
+
* (capture the frozen current frame), {@link stop} (tear the session down). Each
|
|
73
|
+
* verb WRITES a single fixed-size command block and AWAITS the cart's ACK before
|
|
74
|
+
* resolving, so the process stays alive + paused between verbs and the caller
|
|
75
|
+
* always acts on a settled frame. Between the ACK and the next verb the game is
|
|
76
|
+
* frozen (its callbacks skipped, a stable framebuffer), so a {@link shot} always
|
|
77
|
+
* captures exactly the intended moment.
|
|
78
|
+
*
|
|
79
|
+
* The engine is transport-agnostic: it talks to the {@link Pico8Process} seam,
|
|
80
|
+
* so the send/wait/return + lost-ack + process-death paths are unit-tested
|
|
81
|
+
* against a FAKE process with no real binary. The supervisor supplies a REAL
|
|
82
|
+
* spawned process for a live session (the manual/opt-in tier).
|
|
83
|
+
*/
|
|
84
|
+
export declare class DriveSession {
|
|
85
|
+
private readonly proc;
|
|
86
|
+
private readonly reader;
|
|
87
|
+
private readonly ackTimeoutMs;
|
|
88
|
+
private readonly shotBasename;
|
|
89
|
+
private frames;
|
|
90
|
+
private shotIndex;
|
|
91
|
+
/** Set once the process dies; every in-flight + future command rejects. */
|
|
92
|
+
private dead;
|
|
93
|
+
private closed;
|
|
94
|
+
private readonly deathWaiters;
|
|
95
|
+
constructor(options: DriveSessionOptions);
|
|
96
|
+
/** Total frames the session has advanced so far. */
|
|
97
|
+
get frame(): number;
|
|
98
|
+
/** Whether the underlying process has died (crashed/quit). */
|
|
99
|
+
get isDead(): boolean;
|
|
100
|
+
private markDead;
|
|
101
|
+
/**
|
|
102
|
+
* Sets the per-frame HELD-buttons byte for the upcoming steps (US #6: inject
|
|
103
|
+
* input between turns). Writes an INPUT block and awaits its ACK. The held
|
|
104
|
+
* level persists until the next INPUT (the cart holds it), and `btnp` edges
|
|
105
|
+
* are reconstructed cart-side from the held sequence.
|
|
106
|
+
*/
|
|
107
|
+
input(held: number): Promise<StepResult>;
|
|
108
|
+
/**
|
|
109
|
+
* Advances EXACTLY `frames` frames then pauses (US #6, US #4). Writes a STEP
|
|
110
|
+
* block and awaits the STEP-DONE ack, which the cart prints only when the
|
|
111
|
+
* frame budget reaches 0, so the returned result means the N frames ACTUALLY
|
|
112
|
+
* ran and the game is now paused on a settled frame. `frames` must fit one
|
|
113
|
+
* byte (the block's arg); larger spans are issued as repeated steps by the
|
|
114
|
+
* caller.
|
|
115
|
+
*/
|
|
116
|
+
step(frames: number): Promise<StepResult>;
|
|
117
|
+
/**
|
|
118
|
+
* Captures the current (paused, frozen) frame (US #6, US #5). Writes a SHOT
|
|
119
|
+
* block and awaits its ACK; the cart renders the frozen state once then
|
|
120
|
+
* screenshots it, so the capture is exactly the intended moment. Returns the
|
|
121
|
+
* `<basename><n>` the cart named it, which the supervisor collects from the
|
|
122
|
+
* `-desktop` dir.
|
|
123
|
+
*/
|
|
124
|
+
shot(): Promise<ShotResult>;
|
|
125
|
+
/**
|
|
126
|
+
* Tears the session down: writes a QUIT block (the cart prints the sentinel so
|
|
127
|
+
* the launcher kills PICO-8), then kills the process as the backstop. Does NOT
|
|
128
|
+
* await an ACK (QUIT ends the cart), and is idempotent + safe on a dead
|
|
129
|
+
* process. After `stop`, every verb rejects with `playtest-session-closed`.
|
|
130
|
+
*/
|
|
131
|
+
stop(): void;
|
|
132
|
+
/**
|
|
133
|
+
* The core handshake: write one command block, then race the ACK line against
|
|
134
|
+
* the timeout and the process-death signal. A missing ACK within the deadline
|
|
135
|
+
* is the structured lost/late-ack error; a process that dies mid-command is the
|
|
136
|
+
* structured process-dead error. Never a hang, never a silent success.
|
|
137
|
+
*/
|
|
138
|
+
private send;
|
|
139
|
+
}
|
|
140
|
+
//# sourceMappingURL=session.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"session.d.ts","sourceRoot":"","sources":["../../../src/engine/pico8/session.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;GAgBG;AAGH,OAAO,KAAK,EAAC,YAAY,EAAC,MAAM,YAAY,CAAC;AAE7C;;;GAGG;AACH,MAAM,MAAM,gBAAgB;AAC3B,mEAAmE;AACjE,8BAA8B;AAChC,kFAAkF;GAChF,+BAA+B;AACjC,yEAAyE;GACvE,yBAAyB,CAAC;AAE7B,gFAAgF;AAChF,qBAAa,YAAa,SAAQ,KAAK;IACtC,QAAQ,CAAC,IAAI,EAAE,gBAAgB,CAAC;gBACpB,IAAI,EAAE,gBAAgB,EAAE,OAAO,EAAE,MAAM;CAKnD;AAED;;;;;;;GAOG;AACH,eAAO,MAAM,sBAAsB,QAAS,CAAC;AAE7C,sFAAsF;AACtF,MAAM,WAAW,UAAU;IAC1B,sEAAsE;IACtE,QAAQ,CAAC,KAAK,EAAE,MAAM,CAAC;IACvB,gFAAgF;IAChF,QAAQ,CAAC,GAAG,EAAE,MAAM,CAAC;CACrB;AAED,kFAAkF;AAClF,MAAM,WAAW,UAAW,SAAQ,UAAU;IAC7C,iFAAiF;IACjF,QAAQ,CAAC,QAAQ,EAAE,MAAM,CAAC;CAC1B;AA6DD,0CAA0C;AAC1C,MAAM,WAAW,mBAAmB;IACnC,gFAAgF;IAChF,QAAQ,CAAC,OAAO,EAAE,YAAY,CAAC;IAC/B,6EAA6E;IAC7E,QAAQ,CAAC,YAAY,CAAC,EAAE,MAAM,CAAC;IAC/B;;;OAGG;IACH,QAAQ,CAAC,YAAY,EAAE,MAAM,CAAC;CAC9B;AAED;;;;;;;;;;;;;;;GAeG;AACH,qBAAa,YAAY;IACxB,OAAO,CAAC,QAAQ,CAAC,IAAI,CAAe;IACpC,OAAO,CAAC,QAAQ,CAAC,MAAM,CAAuB;IAC9C,OAAO,CAAC,QAAQ,CAAC,YAAY,CAAS;IACtC,OAAO,CAAC,QAAQ,CAAC,YAAY,CAAS;IACtC,OAAO,CAAC,MAAM,CAAK;IACnB,OAAO,CAAC,SAAS,CAAK;IACtB,2EAA2E;IAC3E,OAAO,CAAC,IAAI,CAAS;IACrB,OAAO,CAAC,MAAM,CAAS;IACvB,OAAO,CAAC,QAAQ,CAAC,YAAY,CAAsB;gBAEvC,OAAO,EAAE,mBAAmB;IASxC,oDAAoD;IACpD,IAAI,KAAK,IAAI,MAAM,CAElB;IAED,8DAA8D;IAC9D,IAAI,MAAM,IAAI,OAAO,CAEpB;IAED,OAAO,CAAC,QAAQ;IAMhB;;;;;OAKG;IACG,KAAK,CAAC,IAAI,EAAE,MAAM,GAAG,OAAO,CAAC,UAAU,CAAC;IAK9C;;;;;;;OAOG;IACG,IAAI,CAAC,MAAM,EAAE,MAAM,GAAG,OAAO,CAAC,UAAU,CAAC;IAO/C;;;;;;OAMG;IACG,IAAI,IAAI,OAAO,CAAC,UAAU,CAAC;IAOjC;;;;;OAKG;IACH,IAAI,IAAI,IAAI;IAaZ;;;;;OAKG;YACW,IAAI;CA+DlB"}
|