picopilot 0.1.4 → 0.2.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/dist/cli.d.ts.map +1 -1
- package/dist/cli.js +111 -16
- package/dist/cli.js.map +1 -1
- package/dist/commands/audio.d.ts +3 -3
- package/dist/commands/audio.d.ts.map +1 -1
- package/dist/commands/audio.js +61 -44
- package/dist/commands/audio.js.map +1 -1
- package/dist/commands/cart-includes.d.ts +11 -0
- package/dist/commands/cart-includes.d.ts.map +1 -0
- package/dist/commands/cart-includes.js +26 -0
- package/dist/commands/cart-includes.js.map +1 -0
- package/dist/commands/doctor.d.ts +22 -0
- package/dist/commands/doctor.d.ts.map +1 -0
- package/dist/commands/doctor.js +222 -0
- package/dist/commands/doctor.js.map +1 -0
- package/dist/commands/export.d.ts +3 -4
- package/dist/commands/export.d.ts.map +1 -1
- package/dist/commands/export.js +66 -86
- package/dist/commands/export.js.map +1 -1
- package/dist/commands/gfx.d.ts +2 -2
- package/dist/commands/gfx.d.ts.map +1 -1
- package/dist/commands/gfx.js +3 -2
- package/dist/commands/gfx.js.map +1 -1
- package/dist/commands/label-cart.d.ts +72 -0
- package/dist/commands/label-cart.d.ts.map +1 -0
- package/dist/commands/label-cart.js +228 -0
- package/dist/commands/label-cart.js.map +1 -0
- package/dist/commands/lint.d.ts +2 -2
- package/dist/commands/lint.d.ts.map +1 -1
- package/dist/commands/lint.js +3 -2
- package/dist/commands/lint.js.map +1 -1
- package/dist/commands/minify.d.ts +2 -2
- package/dist/commands/minify.d.ts.map +1 -1
- package/dist/commands/minify.js +3 -2
- package/dist/commands/minify.js.map +1 -1
- package/dist/commands/pico8-env.d.ts +76 -0
- package/dist/commands/pico8-env.d.ts.map +1 -0
- package/dist/commands/pico8-env.js +71 -0
- package/dist/commands/pico8-env.js.map +1 -0
- package/dist/commands/playtest.d.ts +2 -2
- package/dist/commands/playtest.d.ts.map +1 -1
- package/dist/commands/playtest.js +6 -32
- package/dist/commands/playtest.js.map +1 -1
- package/dist/commands/run.d.ts +3 -4
- package/dist/commands/run.d.ts.map +1 -1
- package/dist/commands/run.js +46 -19
- package/dist/commands/run.js.map +1 -1
- package/dist/commands/serve.d.ts +2 -2
- package/dist/commands/serve.d.ts.map +1 -1
- package/dist/commands/serve.js +65 -19
- package/dist/commands/serve.js.map +1 -1
- package/dist/commands/shrinko-env.d.ts +22 -0
- package/dist/commands/shrinko-env.d.ts.map +1 -0
- package/dist/commands/shrinko-env.js +15 -0
- package/dist/commands/shrinko-env.js.map +1 -0
- package/dist/commands/tokens.d.ts +4 -4
- package/dist/commands/tokens.d.ts.map +1 -1
- package/dist/commands/tokens.js +3 -2
- package/dist/commands/tokens.js.map +1 -1
- package/dist/commands/verify.d.ts +2 -2
- package/dist/commands/verify.d.ts.map +1 -1
- package/dist/commands/verify.js +3 -2
- package/dist/commands/verify.js.map +1 -1
- package/dist/engine/child-env.d.ts +14 -0
- package/dist/engine/child-env.d.ts.map +1 -0
- package/dist/engine/child-env.js +21 -0
- package/dist/engine/child-env.js.map +1 -0
- package/dist/engine/gfx/label.d.ts +15 -9
- package/dist/engine/gfx/label.d.ts.map +1 -1
- package/dist/engine/gfx/label.js +130 -38
- package/dist/engine/gfx/label.js.map +1 -1
- package/dist/engine/pico8/adapter.d.ts +23 -3
- package/dist/engine/pico8/adapter.d.ts.map +1 -1
- package/dist/engine/pico8/adapter.js.map +1 -1
- package/dist/engine/pico8/cleanup.d.ts +16 -0
- package/dist/engine/pico8/cleanup.d.ts.map +1 -0
- package/dist/engine/pico8/cleanup.js +74 -0
- package/dist/engine/pico8/cleanup.js.map +1 -0
- package/dist/engine/pico8/display.d.ts +149 -0
- package/dist/engine/pico8/display.d.ts.map +1 -0
- package/dist/engine/pico8/display.js +311 -0
- package/dist/engine/pico8/display.js.map +1 -0
- package/dist/engine/pico8/drive.d.ts.map +1 -1
- package/dist/engine/pico8/drive.js +28 -2
- package/dist/engine/pico8/drive.js.map +1 -1
- package/dist/engine/pico8/harness.d.ts +8 -0
- package/dist/engine/pico8/harness.d.ts.map +1 -1
- package/dist/engine/pico8/harness.js +9 -1
- package/dist/engine/pico8/harness.js.map +1 -1
- package/dist/engine/pico8/index.d.ts +3 -0
- package/dist/engine/pico8/index.d.ts.map +1 -1
- package/dist/engine/pico8/index.js +3 -0
- package/dist/engine/pico8/index.js.map +1 -1
- package/dist/engine/pico8/session-daemon-entry.d.ts +1 -1
- package/dist/engine/pico8/session-daemon-entry.js +1 -1
- package/dist/engine/pico8/session-daemon-main.d.ts +1 -1
- package/dist/engine/pico8/session-daemon-main.js +1 -1
- package/dist/engine/pico8/session-daemon.d.ts +1 -1
- package/dist/engine/pico8/session-daemon.d.ts.map +1 -1
- package/dist/engine/pico8/session-daemon.js +19 -19
- package/dist/engine/pico8/session-daemon.js.map +1 -1
- package/dist/engine/pico8/session.d.ts +1 -1
- package/dist/engine/pico8/session.js +1 -1
- package/dist/engine/pico8/shell.d.ts +56 -3
- package/dist/engine/pico8/shell.d.ts.map +1 -1
- package/dist/engine/pico8/shell.js +188 -24
- package/dist/engine/pico8/shell.js.map +1 -1
- package/dist/engine/pico8/supervisor.d.ts +1 -1
- package/dist/engine/pico8/supervisor.js +1 -1
- package/dist/engine/shrinko/adapter.d.ts +1 -1
- package/dist/engine/shrinko/shell.d.ts +3 -3
- package/dist/engine/shrinko/shell.js +3 -3
- package/package.json +2 -1
- package/src/cli.test.ts +188 -0
- package/src/cli.ts +122 -16
- package/src/commands/audio.test.ts +76 -1
- package/src/commands/audio.ts +79 -57
- package/src/commands/cart-includes.ts +27 -0
- package/src/commands/doctor.test.ts +149 -0
- package/src/commands/doctor.ts +280 -0
- package/src/commands/export.test.ts +327 -3
- package/src/commands/export.ts +81 -99
- package/src/commands/gfx.ts +7 -6
- package/src/commands/label-cart.ts +304 -0
- package/src/commands/lint.test.ts +6 -0
- package/src/commands/lint.ts +7 -6
- package/src/commands/minify.test.ts +6 -0
- package/src/commands/minify.ts +7 -6
- package/src/commands/pico8-env.ts +124 -0
- package/src/commands/playtest.test.ts +3 -0
- package/src/commands/playtest.ts +10 -34
- package/src/commands/run.test.ts +116 -2
- package/src/commands/run.ts +62 -24
- package/src/commands/serve.test.ts +138 -2
- package/src/commands/serve.ts +80 -21
- package/src/commands/shrinko-env.test.ts +68 -0
- package/src/commands/shrinko-env.ts +40 -0
- package/src/commands/tokens.test.ts +19 -0
- package/src/commands/tokens.ts +9 -6
- package/src/commands/verify.test.ts +12 -0
- package/src/commands/verify.ts +7 -4
- package/src/engine/child-env.ts +22 -0
- package/src/engine/gfx/fixtures/pico8-screenshot-384.png +0 -0
- package/src/engine/gfx/label.test.ts +467 -1
- package/src/engine/gfx/label.ts +147 -41
- package/src/engine/pico8/adapter.ts +25 -3
- package/src/engine/pico8/cleanup.test.ts +28 -0
- package/src/engine/pico8/cleanup.ts +76 -0
- package/src/engine/pico8/display.test.ts +210 -0
- package/src/engine/pico8/display.ts +429 -0
- package/src/engine/pico8/drive.test.ts +20 -0
- package/src/engine/pico8/drive.ts +25 -2
- package/src/engine/pico8/fake-xvfb.test-support.ts +55 -0
- package/src/engine/pico8/harness.test.ts +26 -0
- package/src/engine/pico8/harness.ts +18 -2
- package/src/engine/pico8/index.ts +3 -0
- package/src/engine/pico8/session-daemon-entry.ts +1 -1
- package/src/engine/pico8/session-daemon-main.ts +1 -1
- package/src/engine/pico8/session-daemon.test.ts +32 -1
- package/src/engine/pico8/session-daemon.ts +25 -17
- package/src/engine/pico8/session.test.ts +1 -1
- package/src/engine/pico8/session.ts +1 -1
- package/src/engine/pico8/shell.test.ts +449 -10
- package/src/engine/pico8/shell.ts +255 -26
- package/src/engine/pico8/supervisor.ts +1 -1
- package/src/engine/pico8/virtual-display.process.test.ts +206 -0
- package/src/engine/shrinko/adapter.ts +1 -1
- package/src/engine/shrinko/shell.ts +3 -3
- package/src/engine/skills/skills.test.ts +10 -0
- package/src/skills/picopilot-audio/SKILL.md +4 -8
- package/src/skills/picopilot-debug/SKILL.md +18 -1
- package/src/skills/picopilot-overview/SKILL.md +9 -1
|
@@ -0,0 +1,149 @@
|
|
|
1
|
+
import {Cli} from 'incur';
|
|
2
|
+
import {describe, expect, it} from 'vitest';
|
|
3
|
+
import type {Pico8Adapter, RunOptions} from '../engine/pico8/index.js';
|
|
4
|
+
import {pico8NotFound} from '../engine/pico8/index.js';
|
|
5
|
+
import {shrinkoNotFound} from '../engine/shrinko/index.js';
|
|
6
|
+
import type {ShrinkoAdapter} from '../engine/shrinko/index.js';
|
|
7
|
+
import {type DoctorDeps, registerDoctor} from './doctor.js';
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* `doctor` reports what this machine can do, from injected probes: a fake
|
|
11
|
+
* PICO-8 adapter (prints a version id, or is absent), a fake shrinko, a fake
|
|
12
|
+
* executable lookup and a controlled parent env (DISPLAY or not).
|
|
13
|
+
*/
|
|
14
|
+
|
|
15
|
+
function pico8Present(versionId = 43): Pico8Adapter {
|
|
16
|
+
return {
|
|
17
|
+
async run(_o: RunOptions) {
|
|
18
|
+
return {
|
|
19
|
+
ok: true,
|
|
20
|
+
value: {
|
|
21
|
+
screenshots: [],
|
|
22
|
+
printh: `__PICOPILOT_PICO8_VERSION__${versionId}\n`,
|
|
23
|
+
exitReason: 'sentinel',
|
|
24
|
+
},
|
|
25
|
+
};
|
|
26
|
+
},
|
|
27
|
+
async record() {
|
|
28
|
+
throw new Error('unused');
|
|
29
|
+
},
|
|
30
|
+
async drive() {
|
|
31
|
+
throw new Error('unused');
|
|
32
|
+
},
|
|
33
|
+
async export() {
|
|
34
|
+
throw new Error('unused');
|
|
35
|
+
},
|
|
36
|
+
};
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
const pico8Absent: Pico8Adapter = {
|
|
40
|
+
...pico8Present(),
|
|
41
|
+
async run() {
|
|
42
|
+
return pico8NotFound();
|
|
43
|
+
},
|
|
44
|
+
};
|
|
45
|
+
|
|
46
|
+
function shrinko(present: boolean): ShrinkoAdapter {
|
|
47
|
+
return {
|
|
48
|
+
async count() {
|
|
49
|
+
return present
|
|
50
|
+
? {ok: true, value: {tokens: 1, chars: 1, compressed: 1}}
|
|
51
|
+
: shrinkoNotFound();
|
|
52
|
+
},
|
|
53
|
+
} as unknown as ShrinkoAdapter;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
async function doctor(deps: DoctorDeps, argv: string[] = []) {
|
|
57
|
+
const cli = Cli.create('picopilot', {version: '0.0.0'});
|
|
58
|
+
registerDoctor(cli, deps);
|
|
59
|
+
let stdout = '';
|
|
60
|
+
let exitCode = 0;
|
|
61
|
+
await cli.serve(['doctor', ...argv, '--json'], {
|
|
62
|
+
stdout(s) {
|
|
63
|
+
stdout += s;
|
|
64
|
+
},
|
|
65
|
+
exit(code) {
|
|
66
|
+
exitCode = code;
|
|
67
|
+
},
|
|
68
|
+
env: {PICO8_PATH: '/opt/pico-8-0.2.7/pico8'},
|
|
69
|
+
});
|
|
70
|
+
return {out: JSON.parse(stdout), exitCode};
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
const finds =
|
|
74
|
+
(...names: string[]) =>
|
|
75
|
+
(n: string) =>
|
|
76
|
+
names.some((x) => n === x || n.endsWith(`/${x}`)) ? n : undefined;
|
|
77
|
+
|
|
78
|
+
describe('picopilot doctor', () => {
|
|
79
|
+
it('headless server with Xvfb: everything is available, -run sessions go virtual', async () => {
|
|
80
|
+
const {out, exitCode} = await doctor({
|
|
81
|
+
pico8: () => pico8Present(43),
|
|
82
|
+
shrinko: () => shrinko(true),
|
|
83
|
+
parentEnv: {},
|
|
84
|
+
find: finds('pico8', 'Xvfb'),
|
|
85
|
+
});
|
|
86
|
+
expect(exitCode).toBe(0);
|
|
87
|
+
expect(out.pico8).toMatchObject({
|
|
88
|
+
found: true,
|
|
89
|
+
runs: true,
|
|
90
|
+
versionId: 43,
|
|
91
|
+
version: '0.2.7',
|
|
92
|
+
});
|
|
93
|
+
expect(out.shrinko8.found).toBe(true);
|
|
94
|
+
expect(out.display).toEqual({
|
|
95
|
+
mode: 'auto',
|
|
96
|
+
native: false,
|
|
97
|
+
virtual: 'Xvfb',
|
|
98
|
+
runSessions: 'xvfb',
|
|
99
|
+
});
|
|
100
|
+
expect(out.unavailable).toEqual([]);
|
|
101
|
+
expect(out.available).toContain('audio record');
|
|
102
|
+
});
|
|
103
|
+
|
|
104
|
+
it('no display and no Xvfb: only the -run commands are unavailable, missing "display"', async () => {
|
|
105
|
+
const {out} = await doctor({
|
|
106
|
+
pico8: () => pico8Present(),
|
|
107
|
+
shrinko: () => shrinko(true),
|
|
108
|
+
parentEnv: {},
|
|
109
|
+
find: finds('pico8'),
|
|
110
|
+
});
|
|
111
|
+
expect(out.display.runSessions).toBe('none');
|
|
112
|
+
expect(
|
|
113
|
+
out.unavailable.map((u: {command: string}) => u.command).sort(),
|
|
114
|
+
).toEqual(['audio record', 'audio render', 'run --record-audio'].sort());
|
|
115
|
+
expect(out.unavailable[0].missing).toEqual(['display']);
|
|
116
|
+
expect(out.available).toContain('run');
|
|
117
|
+
expect(out.available).toContain('playtest');
|
|
118
|
+
expect(out.available).toContain('export');
|
|
119
|
+
});
|
|
120
|
+
|
|
121
|
+
it('a desktop (DISPLAY set): -run sessions are native', async () => {
|
|
122
|
+
const {out} = await doctor({
|
|
123
|
+
pico8: () => pico8Present(),
|
|
124
|
+
shrinko: () => shrinko(true),
|
|
125
|
+
parentEnv: {DISPLAY: ':0'},
|
|
126
|
+
find: finds('pico8'),
|
|
127
|
+
});
|
|
128
|
+
expect(out.display.native).toBe(true);
|
|
129
|
+
expect(out.display.runSessions).toBe('native');
|
|
130
|
+
});
|
|
131
|
+
|
|
132
|
+
it('no PICO-8 and no shrinko8: reports both as missing, still exits 0 (the report is the result)', async () => {
|
|
133
|
+
const {out, exitCode} = await doctor({
|
|
134
|
+
pico8: () => pico8Absent,
|
|
135
|
+
shrinko: () => shrinko(false),
|
|
136
|
+
parentEnv: {},
|
|
137
|
+
find: () => undefined,
|
|
138
|
+
});
|
|
139
|
+
expect(exitCode).toBe(0);
|
|
140
|
+
expect(out.pico8).toEqual({found: false, runs: false});
|
|
141
|
+
expect(out.shrinko8.found).toBe(false);
|
|
142
|
+
expect(out.available).toEqual([
|
|
143
|
+
'init',
|
|
144
|
+
'gfx show / set / render',
|
|
145
|
+
'sfx',
|
|
146
|
+
'music',
|
|
147
|
+
]);
|
|
148
|
+
});
|
|
149
|
+
});
|
|
@@ -0,0 +1,280 @@
|
|
|
1
|
+
import {mkdtempSync, realpathSync, rmSync, writeFileSync} from 'node:fs';
|
|
2
|
+
import {tmpdir} from 'node:os';
|
|
3
|
+
import {join} from 'node:path';
|
|
4
|
+
import {type Cli, z} from 'incur';
|
|
5
|
+
import {
|
|
6
|
+
DISPLAY_MODES,
|
|
7
|
+
type DisplayMode,
|
|
8
|
+
detectVirtualDisplayBackend,
|
|
9
|
+
findExecutable,
|
|
10
|
+
hasNativeDisplay,
|
|
11
|
+
needsVirtualDisplay,
|
|
12
|
+
pico8Candidates,
|
|
13
|
+
pico8ChildEnv,
|
|
14
|
+
type VirtualDisplayBackend,
|
|
15
|
+
} from '../engine/pico8/index.js';
|
|
16
|
+
import {
|
|
17
|
+
createShellAdapterFactory,
|
|
18
|
+
displayModeOf,
|
|
19
|
+
PICO8_ENV,
|
|
20
|
+
type Pico8AdapterFactory,
|
|
21
|
+
} from './pico8-env.js';
|
|
22
|
+
import {
|
|
23
|
+
createShellShrinkoFactory,
|
|
24
|
+
type ShrinkoAdapterFactory,
|
|
25
|
+
} from './shrinko-env.js';
|
|
26
|
+
|
|
27
|
+
/** The marker the version-probe cart prints in front of `stat(5)`. */
|
|
28
|
+
const VERSION_MARKER = '__PICOPILOT_PICO8_VERSION__';
|
|
29
|
+
|
|
30
|
+
/** The probe cart: print PICO-8's version id, then the done-sentinel. */
|
|
31
|
+
const PROBE_SENTINEL = '__PICOPILOT_DOCTOR_DONE__';
|
|
32
|
+
const PROBE_CART = `pico-8 cartridge // http://www.pico-8.com
|
|
33
|
+
version 42
|
|
34
|
+
__lua__
|
|
35
|
+
printh("${VERSION_MARKER}"..stat(5))
|
|
36
|
+
printh("${PROBE_SENTINEL}")
|
|
37
|
+
`;
|
|
38
|
+
|
|
39
|
+
/** A tiny cart for the shrinko probe (a real `--count` run). */
|
|
40
|
+
const SHRINKO_PROBE_CART = `pico-8 cartridge // http://www.pico-8.com
|
|
41
|
+
version 42
|
|
42
|
+
__lua__
|
|
43
|
+
print("hi")
|
|
44
|
+
`;
|
|
45
|
+
|
|
46
|
+
/** The doctor's backstop for the headless PICO-8 probe. */
|
|
47
|
+
const PROBE_BACKSTOP_MS = 15_000;
|
|
48
|
+
|
|
49
|
+
/** What a command needs to run (beyond picopilot itself). */
|
|
50
|
+
type Need = 'pico8' | 'shrinko8' | 'display';
|
|
51
|
+
|
|
52
|
+
/**
|
|
53
|
+
* The capability table: every command (or command mode) and what it needs.
|
|
54
|
+
* `display` means a PICO-8 `-run` session: a native display or a virtual one.
|
|
55
|
+
* The `-x` paths (run screenshots, playtest, export, serve) are headless and
|
|
56
|
+
* need PICO-8 only.
|
|
57
|
+
*/
|
|
58
|
+
const COMMAND_NEEDS: readonly {command: string; needs: readonly Need[]}[] = [
|
|
59
|
+
{command: 'init', needs: []},
|
|
60
|
+
{command: 'gfx show / set / render', needs: []},
|
|
61
|
+
{command: 'sfx', needs: []},
|
|
62
|
+
{command: 'music', needs: []},
|
|
63
|
+
{command: 'gfx import / export', needs: ['shrinko8']},
|
|
64
|
+
{command: 'tokens', needs: ['shrinko8']},
|
|
65
|
+
{command: 'lint', needs: ['shrinko8']},
|
|
66
|
+
{command: 'minify', needs: ['shrinko8']},
|
|
67
|
+
{command: 'verify', needs: ['shrinko8']},
|
|
68
|
+
{command: 'run', needs: ['pico8']},
|
|
69
|
+
{command: 'playtest', needs: ['pico8']},
|
|
70
|
+
{command: 'export', needs: ['pico8']},
|
|
71
|
+
{command: 'serve', needs: ['pico8']},
|
|
72
|
+
{command: 'run --record-audio', needs: ['pico8', 'display']},
|
|
73
|
+
{command: 'audio record', needs: ['pico8', 'display']},
|
|
74
|
+
{command: 'audio render', needs: ['pico8', 'display']},
|
|
75
|
+
];
|
|
76
|
+
|
|
77
|
+
/** The PICO-8 version from its install path (e.g. `.../pico-8-0.2.7/...`), if it names one. */
|
|
78
|
+
function versionFromPath(path: string): string | undefined {
|
|
79
|
+
const m = /pico-?8[-_ ]v?(\d+\.\d+\.\d+[a-z]?)/i.exec(path);
|
|
80
|
+
return m?.[1];
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
/** How a `-run` session would get its display here. */
|
|
84
|
+
type RunSessionDisplay = 'native' | 'xvfb' | 'none';
|
|
85
|
+
|
|
86
|
+
/** Dependencies for {@link registerDoctor}, injectable for tests. */
|
|
87
|
+
export interface DoctorDeps {
|
|
88
|
+
readonly pico8?: Pico8AdapterFactory;
|
|
89
|
+
readonly shrinko?: ShrinkoAdapterFactory;
|
|
90
|
+
/** The parent environment (defaults to `process.env`): DISPLAY etc. are read from it. */
|
|
91
|
+
readonly parentEnv?: NodeJS.ProcessEnv;
|
|
92
|
+
/** Executable lookup (defaults to a PATH search). */
|
|
93
|
+
readonly find?: (name: string, env: NodeJS.ProcessEnv) => string | undefined;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
/**
|
|
97
|
+
* Registers `picopilot doctor`: what can THIS machine do? It reports whether
|
|
98
|
+
* PICO-8 is found (and runs headless, with its version), whether shrinko8 is
|
|
99
|
+
* found, how a PICO-8 `-run` session would get a display (native, a private
|
|
100
|
+
* virtual one via Xvfb, or none), and therefore which commands are available.
|
|
101
|
+
* An agent calls it first instead of discovering limits by failing. It never
|
|
102
|
+
* fails itself: a missing dependency is data in the report.
|
|
103
|
+
*/
|
|
104
|
+
export function registerDoctor(cli: Cli.Cli, deps: DoctorDeps = {}): void {
|
|
105
|
+
const pico8Factory =
|
|
106
|
+
deps.pico8 ?? createShellAdapterFactory({parentEnv: deps.parentEnv});
|
|
107
|
+
const shrinkoFactory: ShrinkoAdapterFactory =
|
|
108
|
+
deps.shrinko ?? createShellShrinkoFactory({parentEnv: deps.parentEnv});
|
|
109
|
+
const find = deps.find ?? findExecutable;
|
|
110
|
+
|
|
111
|
+
cli.command('doctor', {
|
|
112
|
+
description:
|
|
113
|
+
'Report what this machine can do: PICO-8 (found, runs headless, version), shrinko8, the display a PICO-8 -run session would use (native / virtual Xvfb / none), and which picopilot commands are therefore available. Call this first on a new machine.',
|
|
114
|
+
env: PICO8_ENV,
|
|
115
|
+
output: z.object({
|
|
116
|
+
pico8: z.object({
|
|
117
|
+
found: z.boolean().describe('Whether the PICO-8 binary was found.'),
|
|
118
|
+
path: z.string().optional().describe('The resolved binary path.'),
|
|
119
|
+
runs: z
|
|
120
|
+
.boolean()
|
|
121
|
+
.describe('Whether a headless -x probe run actually worked.'),
|
|
122
|
+
version: z
|
|
123
|
+
.string()
|
|
124
|
+
.optional()
|
|
125
|
+
.describe('The PICO-8 version, when its install path names it.'),
|
|
126
|
+
versionId: z
|
|
127
|
+
.number()
|
|
128
|
+
.optional()
|
|
129
|
+
.describe("PICO-8's internal version number (stat(5))."),
|
|
130
|
+
}),
|
|
131
|
+
shrinko8: z.object({
|
|
132
|
+
found: z
|
|
133
|
+
.boolean()
|
|
134
|
+
.describe('Whether shrinko8 ran (token count on a probe cart).'),
|
|
135
|
+
}),
|
|
136
|
+
display: z.object({
|
|
137
|
+
mode: z
|
|
138
|
+
.enum(DISPLAY_MODES)
|
|
139
|
+
.describe('The resolved display mode (--display / env / config).'),
|
|
140
|
+
native: z
|
|
141
|
+
.boolean()
|
|
142
|
+
.describe('Whether DISPLAY or WAYLAND_DISPLAY is set.'),
|
|
143
|
+
virtual: z
|
|
144
|
+
.enum(['Xvfb', 'xvfb-run'])
|
|
145
|
+
.optional()
|
|
146
|
+
.describe('The virtual-display backend available, if any.'),
|
|
147
|
+
runSessions: z
|
|
148
|
+
.enum(['native', 'xvfb', 'none'])
|
|
149
|
+
.describe(
|
|
150
|
+
'How a PICO-8 -run session (audio record/render, run --record-audio) gets a display here.',
|
|
151
|
+
),
|
|
152
|
+
}),
|
|
153
|
+
available: z
|
|
154
|
+
.array(z.string())
|
|
155
|
+
.describe('Commands that work on this machine.'),
|
|
156
|
+
unavailable: z
|
|
157
|
+
.array(
|
|
158
|
+
z.object({
|
|
159
|
+
command: z.string(),
|
|
160
|
+
missing: z.array(z.string()),
|
|
161
|
+
}),
|
|
162
|
+
)
|
|
163
|
+
.describe('Commands that do not, and what they are missing.'),
|
|
164
|
+
}),
|
|
165
|
+
async run({env, ok, var: vars}) {
|
|
166
|
+
const display: DisplayMode = displayModeOf(vars);
|
|
167
|
+
const childEnv = pico8ChildEnv(
|
|
168
|
+
env as Record<string, string | undefined>,
|
|
169
|
+
deps.parentEnv ?? process.env,
|
|
170
|
+
);
|
|
171
|
+
|
|
172
|
+
// --- PICO-8: locate, then a real headless probe for the version.
|
|
173
|
+
const [pico8File] = pico8Candidates(childEnv) as [string, ...string[]];
|
|
174
|
+
const pico8Path = find(pico8File, childEnv);
|
|
175
|
+
let runs = false;
|
|
176
|
+
let versionId: number | undefined;
|
|
177
|
+
let version: string | undefined;
|
|
178
|
+
const probeDir = mkdtempSync(join(tmpdir(), 'picopilot-doctor-'));
|
|
179
|
+
try {
|
|
180
|
+
const cartPath = join(probeDir, 'probe.p8');
|
|
181
|
+
writeFileSync(cartPath, PROBE_CART);
|
|
182
|
+
const result = await pico8Factory(env as NodeJS.ProcessEnv, {
|
|
183
|
+
display,
|
|
184
|
+
}).run({
|
|
185
|
+
cartPath,
|
|
186
|
+
shotDir: probeDir,
|
|
187
|
+
sentinel: PROBE_SENTINEL,
|
|
188
|
+
backstopMs: PROBE_BACKSTOP_MS,
|
|
189
|
+
});
|
|
190
|
+
if (result.ok) {
|
|
191
|
+
const m = new RegExp(`${VERSION_MARKER}(\\d+)`).exec(
|
|
192
|
+
result.value.printh,
|
|
193
|
+
);
|
|
194
|
+
if (m !== null) {
|
|
195
|
+
runs = true;
|
|
196
|
+
versionId = Number(m[1]);
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
} finally {
|
|
200
|
+
rmSync(probeDir, {recursive: true, force: true});
|
|
201
|
+
}
|
|
202
|
+
if (pico8Path !== undefined) {
|
|
203
|
+
let real = pico8Path;
|
|
204
|
+
try {
|
|
205
|
+
real = realpathSync(pico8Path);
|
|
206
|
+
} catch {
|
|
207
|
+
// keep the unresolved path
|
|
208
|
+
}
|
|
209
|
+
version = versionFromPath(real) ?? versionFromPath(pico8Path);
|
|
210
|
+
}
|
|
211
|
+
const pico8Found = pico8Path !== undefined || runs;
|
|
212
|
+
|
|
213
|
+
// --- shrinko8: a real --count on a tiny cart.
|
|
214
|
+
let shrinkoFound = false;
|
|
215
|
+
const shrinkoDir = mkdtempSync(join(tmpdir(), 'picopilot-doctor-'));
|
|
216
|
+
try {
|
|
217
|
+
const cartPath = join(shrinkoDir, 'probe.p8');
|
|
218
|
+
writeFileSync(cartPath, SHRINKO_PROBE_CART);
|
|
219
|
+
try {
|
|
220
|
+
const count = await shrinkoFactory(env as NodeJS.ProcessEnv).count(
|
|
221
|
+
cartPath,
|
|
222
|
+
);
|
|
223
|
+
shrinkoFound = count.ok;
|
|
224
|
+
} catch {
|
|
225
|
+
// ran but did not parse: present, yet not usable for us
|
|
226
|
+
shrinkoFound = false;
|
|
227
|
+
}
|
|
228
|
+
} finally {
|
|
229
|
+
rmSync(shrinkoDir, {recursive: true, force: true});
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
// --- display: how a -run session would get one here.
|
|
233
|
+
const native = hasNativeDisplay(childEnv);
|
|
234
|
+
const virtual: VirtualDisplayBackend | undefined =
|
|
235
|
+
detectVirtualDisplayBackend(childEnv, find);
|
|
236
|
+
const runSessions: RunSessionDisplay = needsVirtualDisplay(
|
|
237
|
+
display,
|
|
238
|
+
childEnv,
|
|
239
|
+
)
|
|
240
|
+
? virtual !== undefined
|
|
241
|
+
? 'xvfb'
|
|
242
|
+
: 'none'
|
|
243
|
+
: native
|
|
244
|
+
? 'native'
|
|
245
|
+
: 'none';
|
|
246
|
+
|
|
247
|
+
const have: Record<Need, boolean> = {
|
|
248
|
+
pico8: runs,
|
|
249
|
+
shrinko8: shrinkoFound,
|
|
250
|
+
display: runSessions !== 'none',
|
|
251
|
+
};
|
|
252
|
+
const available: string[] = [];
|
|
253
|
+
const unavailable: {command: string; missing: string[]}[] = [];
|
|
254
|
+
for (const {command, needs} of COMMAND_NEEDS) {
|
|
255
|
+
const missing = needs.filter((n) => !have[n]);
|
|
256
|
+
if (missing.length === 0) available.push(command);
|
|
257
|
+
else unavailable.push({command, missing});
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
return ok({
|
|
261
|
+
pico8: {
|
|
262
|
+
found: pico8Found,
|
|
263
|
+
...(pico8Path !== undefined ? {path: pico8Path} : {}),
|
|
264
|
+
runs,
|
|
265
|
+
...(version !== undefined ? {version} : {}),
|
|
266
|
+
...(versionId !== undefined ? {versionId} : {}),
|
|
267
|
+
},
|
|
268
|
+
shrinko8: {found: shrinkoFound},
|
|
269
|
+
display: {
|
|
270
|
+
mode: display,
|
|
271
|
+
native,
|
|
272
|
+
...(virtual !== undefined ? {virtual} : {}),
|
|
273
|
+
runSessions,
|
|
274
|
+
},
|
|
275
|
+
available,
|
|
276
|
+
unavailable,
|
|
277
|
+
});
|
|
278
|
+
},
|
|
279
|
+
});
|
|
280
|
+
}
|