picopilot 0.1.5 → 0.2.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/cli.d.ts.map +1 -1
- package/dist/cli.js +205 -17
- 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 +69 -44
- package/dist/commands/audio.js.map +1 -1
- package/dist/commands/doctor.d.ts +22 -0
- package/dist/commands/doctor.d.ts.map +1 -0
- package/dist/commands/doctor.js +226 -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 +4 -9
- 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 +14 -7
- package/dist/commands/gfx.js.map +1 -1
- 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 +112 -0
- package/dist/commands/pico8-env.d.ts.map +1 -0
- package/dist/commands/pico8-env.js +118 -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 +4 -9
- 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 +52 -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 +4 -9
- 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/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/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/shrinko/shell.d.ts +3 -3
- package/dist/engine/shrinko/shell.js +3 -3
- package/package.json +2 -2
- package/src/cli.test.ts +229 -0
- package/src/cli.ts +230 -18
- package/src/commands/audio.test.ts +64 -1
- package/src/commands/audio.ts +86 -57
- package/src/commands/doctor.test.ts +149 -0
- package/src/commands/doctor.ts +284 -0
- package/src/commands/export.ts +9 -13
- package/src/commands/gfx.ts +19 -15
- package/src/commands/lint.ts +7 -6
- package/src/commands/minify.ts +7 -6
- package/src/commands/pico8-env.ts +192 -0
- package/src/commands/playtest.ts +8 -11
- package/src/commands/run.test.ts +110 -2
- package/src/commands/run.ts +68 -24
- package/src/commands/serve.ts +8 -11
- package/src/commands/shrinko-env.test.ts +68 -0
- package/src/commands/shrinko-env.ts +40 -0
- package/src/commands/tokens.ts +9 -6
- package/src/commands/verify.ts +7 -4
- package/src/engine/child-env.ts +22 -0
- 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/shell.test.ts +448 -9
- package/src/engine/pico8/shell.ts +255 -26
- package/src/engine/pico8/virtual-display.process.test.ts +206 -0
- package/src/engine/shrinko/shell.ts +3 -3
- package/src/mcp.process.test.ts +219 -0
- package/src/skills/picopilot-audio/SKILL.md +4 -8
- package/src/skills/picopilot-overview/SKILL.md +9 -1
package/src/commands/audio.ts
CHANGED
|
@@ -14,21 +14,29 @@ import {
|
|
|
14
14
|
buildRenderHarness,
|
|
15
15
|
DONE_SENTINEL,
|
|
16
16
|
HarnessError,
|
|
17
|
-
type
|
|
17
|
+
type Pico8RecordResult,
|
|
18
18
|
type RenderTarget,
|
|
19
|
-
ShellPico8Adapter,
|
|
20
19
|
} from '../engine/pico8/index.js';
|
|
20
|
+
import {makeReadInclude} from './cart-includes.js';
|
|
21
|
+
import {
|
|
22
|
+
createShellAdapterFactory,
|
|
23
|
+
DISPLAY_OPTION,
|
|
24
|
+
displayUnavailableError,
|
|
25
|
+
PICO8_ENV,
|
|
26
|
+
type Pico8AdapterFactory,
|
|
27
|
+
pico8NotFoundError,
|
|
28
|
+
resolveCommandDisplay,
|
|
29
|
+
} from './pico8-env.js';
|
|
21
30
|
|
|
22
31
|
/**
|
|
23
32
|
* Injects the PICO-8 adapter the audio commands use (defaults to the shell
|
|
24
33
|
* adapter over native `pico8 -run`). Mirrors `run`'s `Pico8AdapterFactory` seam:
|
|
25
34
|
* a test passes a fake to drive the record orchestration + the absent path
|
|
26
|
-
* WITHOUT the paid binary
|
|
35
|
+
* WITHOUT the paid binary.
|
|
27
36
|
*/
|
|
28
|
-
export type Pico8AdapterFactory
|
|
37
|
+
export type {Pico8AdapterFactory} from './pico8-env.js';
|
|
29
38
|
|
|
30
|
-
const defaultAdapterFactory: Pico8AdapterFactory = (
|
|
31
|
-
new ShellPico8Adapter({env});
|
|
39
|
+
const defaultAdapterFactory: Pico8AdapterFactory = createShellAdapterFactory();
|
|
32
40
|
|
|
33
41
|
/**
|
|
34
42
|
* The default record backstop: kill a capture that never signals after this many
|
|
@@ -39,7 +47,7 @@ const DEFAULT_BACKSTOP_MS = 30_000;
|
|
|
39
47
|
|
|
40
48
|
/** The honest one-liner both `record` and `render` carry (ADR-0009): not offline. */
|
|
41
49
|
const HONEST_CAVEAT =
|
|
42
|
-
'This is a REAL-TIME recording of playback
|
|
50
|
+
'This is a REAL-TIME recording of playback in a -run session, NOT a deterministic offline export. Works headless: with no display, picopilot runs the session on a private virtual display (ADR-0017, --display).';
|
|
43
51
|
|
|
44
52
|
/** Resolves a path argument to an absolute path (relative to cwd). */
|
|
45
53
|
function resolvePath(p: string): string {
|
|
@@ -64,14 +72,8 @@ function chooseWavDir(given: string | undefined): string {
|
|
|
64
72
|
return dir;
|
|
65
73
|
}
|
|
66
74
|
|
|
67
|
-
/** The env schema both audio subcommands share (locates the binary
|
|
68
|
-
const AUDIO_ENV =
|
|
69
|
-
PICO8_PATH: z
|
|
70
|
-
.string()
|
|
71
|
-
.optional()
|
|
72
|
-
.describe('Explicit path to the PICO-8 binary (else `pico8` on PATH).'),
|
|
73
|
-
PATH: z.string().optional().describe('Used to locate `pico8`.'),
|
|
74
|
-
});
|
|
75
|
+
/** The env schema both audio subcommands share (locates the binary). */
|
|
76
|
+
const AUDIO_ENV = PICO8_ENV;
|
|
75
77
|
|
|
76
78
|
/** The structured output shape shared by `record` and `render`. */
|
|
77
79
|
const AUDIO_OUTPUT = z.object({
|
|
@@ -79,7 +81,7 @@ const AUDIO_OUTPUT = z.object({
|
|
|
79
81
|
.string()
|
|
80
82
|
.optional()
|
|
81
83
|
.describe(
|
|
82
|
-
'Absolute path of the recorded WAV, or omitted if no audio was captured (
|
|
84
|
+
'Absolute path of the recorded WAV, or omitted if no audio was captured (see noAudioReason).',
|
|
83
85
|
),
|
|
84
86
|
captured: z
|
|
85
87
|
.boolean()
|
|
@@ -98,41 +100,60 @@ const AUDIO_OUTPUT = z.object({
|
|
|
98
100
|
.number()
|
|
99
101
|
.describe('The record window in seconds the capture ran for.'),
|
|
100
102
|
printh: z.string().describe('Captured stdout from the session.'),
|
|
103
|
+
display: z
|
|
104
|
+
.enum(['native', 'Xvfb', 'xvfb-run'])
|
|
105
|
+
.optional()
|
|
106
|
+
.describe(
|
|
107
|
+
'Which display the -run session used: native, or a private virtual display (Xvfb / xvfb-run).',
|
|
108
|
+
),
|
|
109
|
+
noAudioReason: z
|
|
110
|
+
.string()
|
|
111
|
+
.optional()
|
|
112
|
+
.describe(
|
|
113
|
+
'When no WAV was captured: why (backstop fired / PICO-8 exited before the done-sentinel / empty capture).',
|
|
114
|
+
),
|
|
115
|
+
pico8LogTail: z
|
|
116
|
+
.string()
|
|
117
|
+
.optional()
|
|
118
|
+
.describe(
|
|
119
|
+
"When no WAV was captured: the tail of PICO-8's own log.txt (load errors, SDL init failures).",
|
|
120
|
+
),
|
|
101
121
|
});
|
|
102
122
|
|
|
103
|
-
/** The message a `pico8-not-found` error carries (mirrors `run`): exact remedy + needs. */
|
|
104
|
-
function pico8NotFoundMessage(notFound: {
|
|
105
|
-
remedy: string;
|
|
106
|
-
needs: readonly string[];
|
|
107
|
-
}): string {
|
|
108
|
-
return `PICO-8 is not installed. ${notFound.remedy} (needs: ${notFound.needs.join(', ')})`;
|
|
109
|
-
}
|
|
110
|
-
|
|
111
123
|
/**
|
|
112
|
-
* The advisory CTA when a record/render run captured NO audio
|
|
113
|
-
*
|
|
114
|
-
* hear (ADR-0009's live-tier caveat), so it is a nudge, not a hard error.
|
|
124
|
+
* The advisory CTA when a record/render run captured NO audio. The report's
|
|
125
|
+
* `noAudioReason` + `pico8LogTail` say why; this points at the next step.
|
|
115
126
|
*/
|
|
116
|
-
function noAudioCta(
|
|
127
|
+
function noAudioCta(reason: string | undefined) {
|
|
117
128
|
return {
|
|
118
|
-
description: `No audio was captured (${
|
|
129
|
+
description: `No audio was captured${reason !== undefined ? ` (${reason})` : ''}. Next:`,
|
|
119
130
|
commands: [
|
|
120
131
|
{
|
|
121
|
-
command: '
|
|
132
|
+
command: 'verify',
|
|
122
133
|
description:
|
|
123
|
-
'Run
|
|
134
|
+
'Run the static gate: a cart that fails to load or errors never reaches audio_end.',
|
|
135
|
+
},
|
|
136
|
+
{
|
|
137
|
+
command: 'doctor',
|
|
138
|
+
description: 'Check PICO-8 and display capabilities on this machine.',
|
|
124
139
|
},
|
|
125
140
|
],
|
|
126
141
|
};
|
|
127
142
|
}
|
|
128
143
|
|
|
144
|
+
/** Maps a failed record result onto the structured `error(...)` payload. */
|
|
145
|
+
function recordFailure(
|
|
146
|
+
what: string,
|
|
147
|
+
result: Exclude<Pico8RecordResult, {ok: true}>,
|
|
148
|
+
) {
|
|
149
|
+
return result.reason === 'display-unavailable'
|
|
150
|
+
? displayUnavailableError(what, result)
|
|
151
|
+
: pico8NotFoundError(result);
|
|
152
|
+
}
|
|
153
|
+
|
|
129
154
|
/** Shapes an adapter record report into the structured output object. */
|
|
130
155
|
function recordOutput(
|
|
131
|
-
report: {
|
|
132
|
-
wavPath: string | undefined;
|
|
133
|
-
printh: string;
|
|
134
|
-
exitReason: 'sentinel' | 'timeout' | 'exit';
|
|
135
|
-
},
|
|
156
|
+
report: Extract<Pico8RecordResult, {ok: true}>['value'],
|
|
136
157
|
harness: {seconds: number; description: string},
|
|
137
158
|
) {
|
|
138
159
|
return {
|
|
@@ -142,6 +163,13 @@ function recordOutput(
|
|
|
142
163
|
target: harness.description,
|
|
143
164
|
seconds: harness.seconds,
|
|
144
165
|
printh: report.printh,
|
|
166
|
+
...(report.display !== undefined ? {display: report.display} : {}),
|
|
167
|
+
...(report.noWavReason !== undefined
|
|
168
|
+
? {noAudioReason: report.noWavReason}
|
|
169
|
+
: {}),
|
|
170
|
+
...(report.pico8LogTail !== undefined
|
|
171
|
+
? {pico8LogTail: report.pico8LogTail}
|
|
172
|
+
: {}),
|
|
145
173
|
};
|
|
146
174
|
}
|
|
147
175
|
|
|
@@ -154,7 +182,7 @@ function recordOutput(
|
|
|
154
182
|
function buildAudioGroup(adapterFactory: Pico8AdapterFactory): Cli.Cli {
|
|
155
183
|
const audio = Cli.create('audio', {
|
|
156
184
|
description:
|
|
157
|
-
'Get a WAV out of a cart by RECORDING a running session (ADR-0009). PICO-8 has no working offline WAV export, so both commands are REAL-TIME recordings
|
|
185
|
+
'Get a WAV out of a cart by RECORDING a running session (ADR-0009). PICO-8 has no working offline WAV export, so both commands are REAL-TIME recordings of a -run session, NOT deterministic offline exports. They work headless: with no display, picopilot runs the session on a private virtual display (ADR-0017). Requires PICO-8.',
|
|
158
186
|
});
|
|
159
187
|
|
|
160
188
|
// --- audio record <cart> ---------------------------------------------------
|
|
@@ -192,6 +220,7 @@ function buildAudioGroup(adapterFactory: Pico8AdapterFactory): Cli.Cli {
|
|
|
192
220
|
.describe(
|
|
193
221
|
'Safety backstop: kill PICO-8 after this many ms if it never signals.',
|
|
194
222
|
),
|
|
223
|
+
display: DISPLAY_OPTION,
|
|
195
224
|
}),
|
|
196
225
|
env: AUDIO_ENV,
|
|
197
226
|
output: AUDIO_OUTPUT,
|
|
@@ -203,7 +232,7 @@ function buildAudioGroup(adapterFactory: Pico8AdapterFactory): Cli.Cli {
|
|
|
203
232
|
options: {seconds: 4},
|
|
204
233
|
},
|
|
205
234
|
],
|
|
206
|
-
async run({args, options, env, error, ok}) {
|
|
235
|
+
async run({args, options, env, error, ok, var: vars}) {
|
|
207
236
|
const cartPath = resolvePath(args.cart);
|
|
208
237
|
if (!existsSync(cartPath)) {
|
|
209
238
|
return error({
|
|
@@ -215,9 +244,12 @@ function buildAudioGroup(adapterFactory: Pico8AdapterFactory): Cli.Cli {
|
|
|
215
244
|
|
|
216
245
|
let harness: ReturnType<typeof buildRecordHarness>;
|
|
217
246
|
try {
|
|
247
|
+
// `#include`s are inlined against the SOURCE cart's folder: the harness
|
|
248
|
+
// runs from the isolated wavDir, where `main.lua` does not exist.
|
|
218
249
|
harness = buildRecordHarness(readFileSync(cartPath, 'utf8'), {
|
|
219
250
|
seconds: options.seconds,
|
|
220
251
|
sentinel: options.sentinel,
|
|
252
|
+
readInclude: makeReadInclude(cartPath),
|
|
221
253
|
});
|
|
222
254
|
} catch (e) {
|
|
223
255
|
if (e instanceof HarnessError) {
|
|
@@ -232,7 +264,11 @@ function buildAudioGroup(adapterFactory: Pico8AdapterFactory): Cli.Cli {
|
|
|
232
264
|
const harnessCart = join(wavDir, 'record-harness.p8');
|
|
233
265
|
writeFileSync(harnessCart, harness.cartText);
|
|
234
266
|
|
|
235
|
-
const
|
|
267
|
+
const display = resolveCommandDisplay(options.display, vars);
|
|
268
|
+
if (!display.ok) return error(display.error);
|
|
269
|
+
const adapter = adapterFactory(env as NodeJS.ProcessEnv, {
|
|
270
|
+
display: display.mode,
|
|
271
|
+
});
|
|
236
272
|
const result = await adapter.record({
|
|
237
273
|
cartPath: harnessCart,
|
|
238
274
|
wavDir,
|
|
@@ -241,18 +277,12 @@ function buildAudioGroup(adapterFactory: Pico8AdapterFactory): Cli.Cli {
|
|
|
241
277
|
backstopMs: options.backstopMs,
|
|
242
278
|
});
|
|
243
279
|
|
|
244
|
-
if (!result.ok)
|
|
245
|
-
return error({
|
|
246
|
-
code: 'pico8-not-found',
|
|
247
|
-
message: pico8NotFoundMessage(result),
|
|
248
|
-
exitCode: 1,
|
|
249
|
-
});
|
|
250
|
-
}
|
|
280
|
+
if (!result.ok) return error(recordFailure('audio record', result));
|
|
251
281
|
|
|
252
282
|
const out = recordOutput(result.value, harness);
|
|
253
283
|
return ok(
|
|
254
284
|
out,
|
|
255
|
-
out.captured ? undefined : {cta: noAudioCta(
|
|
285
|
+
out.captured ? undefined : {cta: noAudioCta(out.noAudioReason)},
|
|
256
286
|
);
|
|
257
287
|
},
|
|
258
288
|
});
|
|
@@ -312,6 +342,7 @@ function buildAudioGroup(adapterFactory: Pico8AdapterFactory): Cli.Cli {
|
|
|
312
342
|
.describe(
|
|
313
343
|
'Safety backstop: kill PICO-8 after this many ms if it never signals.',
|
|
314
344
|
),
|
|
345
|
+
display: DISPLAY_OPTION,
|
|
315
346
|
}),
|
|
316
347
|
env: AUDIO_ENV,
|
|
317
348
|
output: AUDIO_OUTPUT,
|
|
@@ -324,7 +355,7 @@ function buildAudioGroup(adapterFactory: Pico8AdapterFactory): Cli.Cli {
|
|
|
324
355
|
options: {pattern: 2, seconds: 6},
|
|
325
356
|
},
|
|
326
357
|
],
|
|
327
|
-
async run({args, options, env, error, ok}) {
|
|
358
|
+
async run({args, options, env, error, ok, var: vars}) {
|
|
328
359
|
if (options.sfx !== undefined && options.pattern !== undefined) {
|
|
329
360
|
return error({
|
|
330
361
|
code: 'ambiguous-target',
|
|
@@ -368,7 +399,11 @@ function buildAudioGroup(adapterFactory: Pico8AdapterFactory): Cli.Cli {
|
|
|
368
399
|
const harnessCart = join(wavDir, 'render-harness.p8');
|
|
369
400
|
writeFileSync(harnessCart, harness.cartText);
|
|
370
401
|
|
|
371
|
-
const
|
|
402
|
+
const display = resolveCommandDisplay(options.display, vars);
|
|
403
|
+
if (!display.ok) return error(display.error);
|
|
404
|
+
const adapter = adapterFactory(env as NodeJS.ProcessEnv, {
|
|
405
|
+
display: display.mode,
|
|
406
|
+
});
|
|
372
407
|
const result = await adapter.record({
|
|
373
408
|
cartPath: harnessCart,
|
|
374
409
|
wavDir,
|
|
@@ -377,18 +412,12 @@ function buildAudioGroup(adapterFactory: Pico8AdapterFactory): Cli.Cli {
|
|
|
377
412
|
backstopMs: options.backstopMs,
|
|
378
413
|
});
|
|
379
414
|
|
|
380
|
-
if (!result.ok)
|
|
381
|
-
return error({
|
|
382
|
-
code: 'pico8-not-found',
|
|
383
|
-
message: pico8NotFoundMessage(result),
|
|
384
|
-
exitCode: 1,
|
|
385
|
-
});
|
|
386
|
-
}
|
|
415
|
+
if (!result.ok) return error(recordFailure('audio render', result));
|
|
387
416
|
|
|
388
417
|
const out = recordOutput(result.value, harness);
|
|
389
418
|
return ok(
|
|
390
419
|
out,
|
|
391
|
-
out.captured ? undefined : {cta: noAudioCta(
|
|
420
|
+
out.captured ? undefined : {cta: noAudioCta(out.noAudioReason)},
|
|
392
421
|
);
|
|
393
422
|
},
|
|
394
423
|
});
|
|
@@ -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
|
+
});
|