picopilot 0.2.0 → 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 +129 -36
- package/dist/cli.js.map +1 -1
- package/dist/commands/audio.d.ts.map +1 -1
- package/dist/commands/audio.js +11 -3
- package/dist/commands/audio.js.map +1 -1
- package/dist/commands/doctor.d.ts.map +1 -1
- package/dist/commands/doctor.js +7 -3
- package/dist/commands/doctor.js.map +1 -1
- package/dist/commands/gfx.d.ts.map +1 -1
- package/dist/commands/gfx.js +11 -5
- package/dist/commands/gfx.js.map +1 -1
- package/dist/commands/pico8-env.d.ts +39 -3
- package/dist/commands/pico8-env.d.ts.map +1 -1
- package/dist/commands/pico8-env.js +51 -4
- package/dist/commands/pico8-env.js.map +1 -1
- package/dist/commands/run.d.ts.map +1 -1
- package/dist/commands/run.js +8 -2
- package/dist/commands/run.js.map +1 -1
- package/package.json +2 -2
- package/src/cli.test.ts +42 -1
- package/src/cli.ts +143 -37
- package/src/commands/audio.ts +10 -3
- package/src/commands/doctor.ts +7 -3
- package/src/commands/gfx.ts +12 -9
- package/src/commands/pico8-env.ts +71 -3
- package/src/commands/run.ts +8 -2
- package/src/mcp.process.test.ts +219 -0
- package/src/skills/picopilot-overview/SKILL.md +1 -1
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { z } from 'incur';
|
|
2
|
-
import { DEFAULT_DISPLAY_MODE, isDisplayMode, pico8ChildEnv, ShellPico8Adapter, } from '../engine/pico8/index.js';
|
|
2
|
+
import { DEFAULT_DISPLAY_MODE, DISPLAY_MODES, isDisplayMode, pico8ChildEnv, ShellPico8Adapter, } from '../engine/pico8/index.js';
|
|
3
3
|
/**
|
|
4
4
|
* The env schema every PICO-8-gated command declares. These are the values
|
|
5
5
|
* picopilot itself RESOLVES (incur reads them from its env source, which tests
|
|
@@ -28,9 +28,20 @@ export function createShellAdapterFactory(deps = {}) {
|
|
|
28
28
|
});
|
|
29
29
|
}
|
|
30
30
|
/**
|
|
31
|
-
* The display
|
|
32
|
-
*
|
|
33
|
-
*
|
|
31
|
+
* The `display` option of the commands that use the display mode (`run`,
|
|
32
|
+
* `audio record`, `audio render`, `doctor`). A per-command option, not an incur
|
|
33
|
+
* global, because incur's MCP transport passes no globals: as an option it is
|
|
34
|
+
* also an argument of the MCP tool. Unset = the invocation-level value.
|
|
35
|
+
*/
|
|
36
|
+
export const DISPLAY_OPTION = z
|
|
37
|
+
.enum(DISPLAY_MODES)
|
|
38
|
+
.optional()
|
|
39
|
+
.describe('How a PICO-8 -run session (audio recording) gets a display: auto (default: a private virtual display only when there is none), xvfb (always virtual), native (as-is). Else PICOPILOT_DISPLAY, else picopilot.json "display".');
|
|
40
|
+
/**
|
|
41
|
+
* The display mode the root middleware in `createCli` resolved for this
|
|
42
|
+
* invocation (top-level `--display` > PICOPILOT_DISPLAY > picopilot.json >
|
|
43
|
+
* auto). A command mounted on a bare CLI (some tests) has no var and gets the
|
|
44
|
+
* default.
|
|
34
45
|
*/
|
|
35
46
|
export function displayModeOf(vars) {
|
|
36
47
|
const value = vars !== null && typeof vars === 'object'
|
|
@@ -38,6 +49,42 @@ export function displayModeOf(vars) {
|
|
|
38
49
|
: undefined;
|
|
39
50
|
return isDisplayMode(value) ? value : DEFAULT_DISPLAY_MODE;
|
|
40
51
|
}
|
|
52
|
+
/**
|
|
53
|
+
* The display mode a command uses: its own `display` option (a per-call MCP
|
|
54
|
+
* argument) wins, else the invocation-level value. An invalid invocation-level
|
|
55
|
+
* value (a typo in PICOPILOT_DISPLAY, say) is an `invalid-display-mode` error
|
|
56
|
+
* here, so only the commands that use the mode fail on it.
|
|
57
|
+
*/
|
|
58
|
+
export function resolveCommandDisplay(option, vars) {
|
|
59
|
+
if (option !== undefined)
|
|
60
|
+
return { ok: true, mode: option };
|
|
61
|
+
const problem = vars !== null && typeof vars === 'object'
|
|
62
|
+
? vars.displayError
|
|
63
|
+
: undefined;
|
|
64
|
+
if (typeof problem === 'string') {
|
|
65
|
+
return {
|
|
66
|
+
ok: false,
|
|
67
|
+
error: { code: 'invalid-display-mode', message: problem, exitCode: 1 },
|
|
68
|
+
};
|
|
69
|
+
}
|
|
70
|
+
return { ok: true, mode: displayModeOf(vars) };
|
|
71
|
+
}
|
|
72
|
+
/**
|
|
73
|
+
* Under `--mcp`, incur loads no config file, so picopilot loads picopilot.json
|
|
74
|
+
* itself and hands the tree to commands as the `mcpConfig` var. A command whose
|
|
75
|
+
* option can come from the config reads its fallback here (CLI runs get it from
|
|
76
|
+
* incur as the option's default, so this is undefined there).
|
|
77
|
+
*/
|
|
78
|
+
export function mcpConfigOption(vars, commandPath, key) {
|
|
79
|
+
let node = vars !== null && typeof vars === 'object'
|
|
80
|
+
? vars.mcpConfig
|
|
81
|
+
: undefined;
|
|
82
|
+
for (const part of commandPath) {
|
|
83
|
+
node = node
|
|
84
|
+
?.commands?.[part];
|
|
85
|
+
}
|
|
86
|
+
return node?.options?.[key];
|
|
87
|
+
}
|
|
41
88
|
/**
|
|
42
89
|
* The `error(...)` payload for a `display-unavailable` result (ADR-0017): the
|
|
43
90
|
* session needs a display, there is none, and no Xvfb is installed to provide a
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"pico8-env.js","sourceRoot":"","sources":["../../src/commands/pico8-env.ts"],"names":[],"mappings":"AAAA,OAAO,EAAC,CAAC,EAAC,MAAM,OAAO,CAAC;AACxB,OAAO,EACN,oBAAoB,
|
|
1
|
+
{"version":3,"file":"pico8-env.js","sourceRoot":"","sources":["../../src/commands/pico8-env.ts"],"names":[],"mappings":"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"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"run.d.ts","sourceRoot":"","sources":["../../src/commands/run.ts"],"names":[],"mappings":"AASA,OAAO,EAAC,KAAK,GAAG,EAAI,MAAM,OAAO,CAAC;AAOlC,OAAO,
|
|
1
|
+
{"version":3,"file":"run.d.ts","sourceRoot":"","sources":["../../src/commands/run.ts"],"names":[],"mappings":"AASA,OAAO,EAAC,KAAK,GAAG,EAAI,MAAM,OAAO,CAAC;AAOlC,OAAO,EAON,KAAK,mBAAmB,EACxB,MAAM,gBAAgB,CAAC;AAExB;;;;GAIG;AACH,YAAY,EAAC,mBAAmB,EAAC,MAAM,gBAAgB,CAAC;AAOxD;;;;;;;;;;;;;;;;;;GAkBG;AACH,wBAAgB,WAAW,CAC1B,GAAG,EAAE,GAAG,CAAC,GAAG,EACZ,cAAc,GAAE,mBAA2C,GACzD,IAAI,CA4QN"}
|
package/dist/commands/run.js
CHANGED
|
@@ -4,7 +4,7 @@ import { isAbsolute, join, resolve } from 'node:path';
|
|
|
4
4
|
import { z } from 'incur';
|
|
5
5
|
import { buildRecordHarness, DONE_SENTINEL, HarnessError, } from '../engine/pico8/index.js';
|
|
6
6
|
import { makeReadInclude } from './cart-includes.js';
|
|
7
|
-
import { createShellAdapterFactory,
|
|
7
|
+
import { createShellAdapterFactory, DISPLAY_OPTION, displayUnavailableError, PICO8_ENV, pico8NotFoundError, resolveCommandDisplay, } from './pico8-env.js';
|
|
8
8
|
const defaultAdapterFactory = createShellAdapterFactory();
|
|
9
9
|
/** The default backstop: kill a cart that never signals/exits after this many ms. */
|
|
10
10
|
const DEFAULT_BACKSTOP_MS = 15_000;
|
|
@@ -55,6 +55,7 @@ export function registerRun(cli, adapterFactory = defaultAdapterFactory) {
|
|
|
55
55
|
.string()
|
|
56
56
|
.optional()
|
|
57
57
|
.describe('One-shot scripted input passed as `-p` (the cart reads it via stat(6)) for automated playtests.'),
|
|
58
|
+
display: DISPLAY_OPTION,
|
|
58
59
|
recordAudio: z
|
|
59
60
|
.boolean()
|
|
60
61
|
.default(false)
|
|
@@ -125,8 +126,13 @@ export function registerRun(cli, adapterFactory = defaultAdapterFactory) {
|
|
|
125
126
|
: mkdtempSync(join(tmpdir(), 'picopilot-run-'));
|
|
126
127
|
if (!existsSync(shotDir))
|
|
127
128
|
mkdirSync(shotDir, { recursive: true });
|
|
129
|
+
// The display mode only matters to the --record-audio (-run) pass, so a
|
|
130
|
+
// bad PICOPILOT_DISPLAY only fails a run that records.
|
|
131
|
+
const display = resolveCommandDisplay(options.display, vars);
|
|
132
|
+
if (options.recordAudio && !display.ok)
|
|
133
|
+
return error(display.error);
|
|
128
134
|
const adapter = adapterFactory(env, {
|
|
129
|
-
display:
|
|
135
|
+
display: display.ok ? display.mode : 'auto',
|
|
130
136
|
});
|
|
131
137
|
const result = await adapter.run({
|
|
132
138
|
cartPath,
|
package/dist/commands/run.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"run.js","sourceRoot":"","sources":["../../src/commands/run.ts"],"names":[],"mappings":"AAAA,OAAO,EACN,UAAU,EACV,SAAS,EACT,WAAW,EACX,YAAY,EACZ,aAAa,GACb,MAAM,SAAS,CAAC;AACjB,OAAO,EAAC,MAAM,EAAC,MAAM,SAAS,CAAC;AAC/B,OAAO,EAAC,UAAU,EAAE,IAAI,EAAE,OAAO,EAAC,MAAM,WAAW,CAAC;AACpD,OAAO,EAAW,CAAC,EAAC,MAAM,OAAO,CAAC;AAClC,OAAO,EACN,kBAAkB,EAClB,aAAa,EACb,YAAY,GACZ,MAAM,0BAA0B,CAAC;AAClC,OAAO,EAAC,eAAe,EAAC,MAAM,oBAAoB,CAAC;AACnD,OAAO,EACN,yBAAyB,EACzB,
|
|
1
|
+
{"version":3,"file":"run.js","sourceRoot":"","sources":["../../src/commands/run.ts"],"names":[],"mappings":"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"}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "picopilot",
|
|
3
|
-
"version": "0.2.
|
|
3
|
+
"version": "0.2.1",
|
|
4
4
|
"description": "An agent-first toolchain for PICO-8 game development, built on the incur CLI framework.",
|
|
5
5
|
"license": "AGPL-3.0-only",
|
|
6
6
|
"repository": {
|
|
@@ -31,7 +31,7 @@
|
|
|
31
31
|
"node": ">=22"
|
|
32
32
|
},
|
|
33
33
|
"dependencies": {
|
|
34
|
-
"incur": "0.
|
|
34
|
+
"incur": "0.7.1"
|
|
35
35
|
},
|
|
36
36
|
"devDependencies": {
|
|
37
37
|
"@types/node": "22.12.0",
|
package/src/cli.test.ts
CHANGED
|
@@ -70,6 +70,7 @@ describe('display mode resolution through the CLI', () => {
|
|
|
70
70
|
for (const argv of [
|
|
71
71
|
['--display', 'native', 'probe-display'],
|
|
72
72
|
['probe-display', '--display', 'native'],
|
|
73
|
+
['probe-display', '--display=native'],
|
|
73
74
|
]) {
|
|
74
75
|
const {out} = await probe(
|
|
75
76
|
argv,
|
|
@@ -84,7 +85,7 @@ describe('display mode resolution through the CLI', () => {
|
|
|
84
85
|
const cli = createCli();
|
|
85
86
|
let stdout = '';
|
|
86
87
|
let exitCode = 0;
|
|
87
|
-
await cli.serve(['
|
|
88
|
+
await cli.serve(['doctor', '--json'], {
|
|
88
89
|
stdout(s) {
|
|
89
90
|
stdout += s;
|
|
90
91
|
},
|
|
@@ -99,6 +100,46 @@ describe('display mode resolution through the CLI', () => {
|
|
|
99
100
|
expect(out.message).toContain('PICOPILOT_DISPLAY');
|
|
100
101
|
});
|
|
101
102
|
|
|
103
|
+
it('an invalid --display is an error for a display command', async () => {
|
|
104
|
+
const cli = createCli();
|
|
105
|
+
let stdout = '';
|
|
106
|
+
let exitCode = 0;
|
|
107
|
+
await cli.serve(['doctor', '--display', 'bogus', '--json'], {
|
|
108
|
+
stdout(s) {
|
|
109
|
+
stdout += s;
|
|
110
|
+
},
|
|
111
|
+
exit(code) {
|
|
112
|
+
exitCode = code;
|
|
113
|
+
},
|
|
114
|
+
env: {},
|
|
115
|
+
});
|
|
116
|
+
expect(exitCode).not.toBe(0);
|
|
117
|
+
expect(stdout).toContain('--display');
|
|
118
|
+
});
|
|
119
|
+
|
|
120
|
+
it('a --display with no value is rejected, and does not eat the next flag', async () => {
|
|
121
|
+
for (const argv of [
|
|
122
|
+
['doctor', '--display', '--json'],
|
|
123
|
+
['doctor', '--json', '--display'],
|
|
124
|
+
]) {
|
|
125
|
+
const cli = createCli();
|
|
126
|
+
let stdout = '';
|
|
127
|
+
let exitCode = 0;
|
|
128
|
+
await cli.serve(argv, {
|
|
129
|
+
stdout(s) {
|
|
130
|
+
stdout += s;
|
|
131
|
+
},
|
|
132
|
+
exit(code) {
|
|
133
|
+
exitCode = code;
|
|
134
|
+
},
|
|
135
|
+
env: {},
|
|
136
|
+
});
|
|
137
|
+
expect(exitCode, argv.join(' ')).not.toBe(0);
|
|
138
|
+
// --json still applied: the error envelope is JSON.
|
|
139
|
+
expect(() => JSON.parse(stdout), argv.join(' ')).not.toThrow();
|
|
140
|
+
}
|
|
141
|
+
});
|
|
142
|
+
|
|
102
143
|
it('an invalid value does not break commands that never use the display', async () => {
|
|
103
144
|
const cli = createCli();
|
|
104
145
|
let stdout = '';
|
package/src/cli.ts
CHANGED
|
@@ -1,4 +1,6 @@
|
|
|
1
|
-
import {readFileSync} from 'node:fs';
|
|
1
|
+
import {existsSync, readFileSync} from 'node:fs';
|
|
2
|
+
import {homedir} from 'node:os';
|
|
3
|
+
import {join, resolve} from 'node:path';
|
|
2
4
|
import {Cli, z} from 'incur';
|
|
3
5
|
import {registerAudio} from './commands/audio.js';
|
|
4
6
|
import {registerDoctor} from './commands/doctor.js';
|
|
@@ -24,13 +26,88 @@ import {
|
|
|
24
26
|
} from './engine/pico8/display.js';
|
|
25
27
|
import {VERSION} from './version.js';
|
|
26
28
|
|
|
27
|
-
/** The
|
|
28
|
-
const
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
29
|
+
/** The config files picopilot searches (first existing wins), relative to the cwd. */
|
|
30
|
+
const CONFIG_FILES = ['picopilot.json'];
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* What one `serve` invocation said at the top level, captured BEFORE incur sees
|
|
34
|
+
* the argv: the `--display` flag (taken out of argv, so it works before or after
|
|
35
|
+
* the command name and on `--mcp`), and for `--mcp` the config flags. incur's
|
|
36
|
+
* MCP transport passes no global flags to a tool call and loads no config file,
|
|
37
|
+
* so picopilot carries both itself.
|
|
38
|
+
*/
|
|
39
|
+
interface Invocation {
|
|
40
|
+
display?: string;
|
|
41
|
+
mcp: boolean;
|
|
42
|
+
/** MCP only: the config tree picopilot loaded itself (undefined = none). */
|
|
43
|
+
mcpConfig?: Record<string, unknown>;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* Removes every `--display <v>` / `--display=<v>` from argv and returns the last
|
|
48
|
+
* value. A `--display` with no value (last token, or followed by another flag)
|
|
49
|
+
* stays in argv, so incur rejects it instead of it silently eating the next flag.
|
|
50
|
+
*/
|
|
51
|
+
function takeDisplayFlag(argv: string[]): {argv: string[]; display?: string} {
|
|
52
|
+
const rest: string[] = [];
|
|
53
|
+
let display: string | undefined;
|
|
54
|
+
for (let i = 0; i < argv.length; i++) {
|
|
55
|
+
const a = argv[i] as string;
|
|
56
|
+
const next = argv[i + 1];
|
|
57
|
+
if (a === '--display' && next !== undefined && !next.startsWith('-')) {
|
|
58
|
+
display = next;
|
|
59
|
+
i++;
|
|
60
|
+
} else if (a.startsWith('--display=') && a.length > '--display='.length) {
|
|
61
|
+
display = a.slice('--display='.length);
|
|
62
|
+
} else {
|
|
63
|
+
rest.push(a);
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
return {argv: rest, display};
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
/** Resolves a config path the way incur does: `~` is the home dir, else relative to the cwd. */
|
|
70
|
+
function resolveConfigPath(path: string): string {
|
|
71
|
+
return path === '~' || path.startsWith('~/')
|
|
72
|
+
? join(homedir(), path.slice(1))
|
|
73
|
+
: resolve(path);
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
/**
|
|
77
|
+
* For `--mcp`: the config file incur would have loaded on the CLI. Mirrors
|
|
78
|
+
* incur's own parsing: `--config <p>` / `--config=<p>` / `--no-config`, the last
|
|
79
|
+
* one wins, a missing value is an error; else the first existing default file.
|
|
80
|
+
*/
|
|
81
|
+
function loadMcpConfig(argv: string[]): Record<string, unknown> | undefined {
|
|
82
|
+
let explicit: string | undefined;
|
|
83
|
+
let disabled = false;
|
|
84
|
+
for (let i = 0; i < argv.length; i++) {
|
|
85
|
+
const a = argv[i] as string;
|
|
86
|
+
let value: string | undefined;
|
|
87
|
+
if (a === '--config') value = argv[++i];
|
|
88
|
+
else if (a.startsWith('--config=')) value = a.slice('--config='.length);
|
|
89
|
+
else if (a === '--no-config') {
|
|
90
|
+
explicit = undefined;
|
|
91
|
+
disabled = true;
|
|
92
|
+
continue;
|
|
93
|
+
} else continue;
|
|
94
|
+
if (value === undefined || value === '') {
|
|
95
|
+
throw new Error('Missing value for flag: --config');
|
|
96
|
+
}
|
|
97
|
+
explicit = value;
|
|
98
|
+
disabled = false;
|
|
99
|
+
}
|
|
100
|
+
if (disabled) return undefined;
|
|
101
|
+
const sink: {display?: unknown} = {};
|
|
102
|
+
if (explicit !== undefined) {
|
|
103
|
+
return loadConfig(resolveConfigPath(explicit), sink);
|
|
104
|
+
}
|
|
105
|
+
for (const file of CONFIG_FILES) {
|
|
106
|
+
const path = resolveConfigPath(file);
|
|
107
|
+
if (existsSync(path)) return loadConfig(path, sink);
|
|
108
|
+
}
|
|
109
|
+
return undefined;
|
|
110
|
+
}
|
|
34
111
|
|
|
35
112
|
/**
|
|
36
113
|
* Reads a picopilot.json for incur's config layer AND captures its top-level
|
|
@@ -91,20 +168,12 @@ export function createCli(): Cli.Cli {
|
|
|
91
168
|
// Per-cart policy config (e.g. `allowMapOverlap`); precedence argv > config > defaults.
|
|
92
169
|
config: {
|
|
93
170
|
flag: 'config',
|
|
94
|
-
files:
|
|
171
|
+
files: CONFIG_FILES,
|
|
95
172
|
loader: async (path) => loadConfig(path, configSink),
|
|
96
173
|
},
|
|
97
|
-
// The display mode (ADR-0017)
|
|
98
|
-
//
|
|
99
|
-
// > picopilot.json `display` > auto.
|
|
100
|
-
globals: z.object({
|
|
101
|
-
display: z
|
|
102
|
-
.enum(DISPLAY_MODES)
|
|
103
|
-
.optional()
|
|
104
|
-
.describe(
|
|
105
|
-
'How PICO-8 -run sessions (audio record/render, run --record-audio) get a display: auto (default: a private virtual display only when there is none), xvfb (always virtual), native (as-is). Also PICOPILOT_DISPLAY or picopilot.json "display" (under --mcp, only PICOPILOT_DISPLAY applies).',
|
|
106
|
-
),
|
|
107
|
-
}),
|
|
174
|
+
// The display mode (ADR-0017): `--display` (a per-command option on the
|
|
175
|
+
// commands that use it, also accepted anywhere on the command line and on
|
|
176
|
+
// `--mcp`) > PICOPILOT_DISPLAY > picopilot.json `display` > auto.
|
|
108
177
|
env: z.object({
|
|
109
178
|
[DISPLAY_ENV_VAR]: z
|
|
110
179
|
.string()
|
|
@@ -113,37 +182,74 @@ export function createCli(): Cli.Cli {
|
|
|
113
182
|
}),
|
|
114
183
|
vars: z.object({
|
|
115
184
|
display: z.enum(DISPLAY_MODES).default(DEFAULT_DISPLAY_MODE),
|
|
185
|
+
displayError: z.string().optional(),
|
|
186
|
+
mcpConfig: z.record(z.string(), z.unknown()).optional(),
|
|
116
187
|
}),
|
|
188
|
+
mcp: {
|
|
189
|
+
// One MCP tool per command (incur's default is progressive discovery
|
|
190
|
+
// behind search/call meta-tools); picopilot's surface is small, and the
|
|
191
|
+
// skills name commands directly.
|
|
192
|
+
tools: {discovery: 'direct'},
|
|
193
|
+
},
|
|
117
194
|
});
|
|
118
195
|
|
|
196
|
+
const invocation: Invocation = {mcp: false};
|
|
197
|
+
|
|
119
198
|
cli.use(async (c, next) => {
|
|
120
|
-
const configured =
|
|
199
|
+
const configured = invocation.mcp
|
|
200
|
+
? invocation.mcpConfig?.[DISPLAY_CONFIG_KEY]
|
|
201
|
+
: configSink.display;
|
|
121
202
|
configSink.display = undefined; // never leak into a later invocation
|
|
122
203
|
const resolved = resolveDisplayMode({
|
|
123
|
-
|
|
124
|
-
// under `--mcp` only PICOPILOT_DISPLAY applies.
|
|
125
|
-
flag: c.globals?.display,
|
|
204
|
+
flag: invocation.display,
|
|
126
205
|
env: (c.env as Record<string, string | undefined>)[DISPLAY_ENV_VAR],
|
|
127
206
|
config: configured,
|
|
128
207
|
});
|
|
129
|
-
if (
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
//
|
|
208
|
+
if (resolved.ok) {
|
|
209
|
+
c.set('display', resolved.mode);
|
|
210
|
+
} else {
|
|
211
|
+
// Not an error here: only the commands that USE the display mode fail
|
|
212
|
+
// on a bad value (see resolveCommandDisplay); the rest run as usual.
|
|
133
213
|
const where =
|
|
134
|
-
resolved.source === '
|
|
135
|
-
?
|
|
136
|
-
:
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
214
|
+
resolved.source === 'flag'
|
|
215
|
+
? '--display'
|
|
216
|
+
: resolved.source === 'env'
|
|
217
|
+
? DISPLAY_ENV_VAR
|
|
218
|
+
: `picopilot.json "${DISPLAY_CONFIG_KEY}"`;
|
|
219
|
+
c.set(
|
|
220
|
+
'displayError',
|
|
221
|
+
`${where} is "${resolved.value}"; expected one of: ${DISPLAY_MODES.join(', ')}.`,
|
|
222
|
+
);
|
|
223
|
+
}
|
|
224
|
+
if (invocation.mcp && invocation.mcpConfig !== undefined) {
|
|
225
|
+
c.set('mcpConfig', invocation.mcpConfig);
|
|
142
226
|
}
|
|
143
|
-
c.set('display', resolved.mode);
|
|
144
227
|
await next();
|
|
145
228
|
});
|
|
146
229
|
|
|
230
|
+
// Capture the top-level `--display` (and, for `--mcp`, the config) before
|
|
231
|
+
// incur parses argv; see {@link Invocation}.
|
|
232
|
+
const serve = cli.serve.bind(cli);
|
|
233
|
+
cli.serve = async (argv = process.argv.slice(2), options) => {
|
|
234
|
+
const taken = takeDisplayFlag(argv);
|
|
235
|
+
invocation.display = taken.display;
|
|
236
|
+
invocation.mcp = taken.argv.includes('--mcp');
|
|
237
|
+
invocation.mcpConfig = undefined;
|
|
238
|
+
if (invocation.mcp) {
|
|
239
|
+
try {
|
|
240
|
+
invocation.mcpConfig = loadMcpConfig(taken.argv);
|
|
241
|
+
} catch (e) {
|
|
242
|
+
// A broken config must stop the server visibly, not serve without it.
|
|
243
|
+
process.stderr.write(
|
|
244
|
+
`picopilot --mcp: ${e instanceof Error ? e.message : String(e)}\n`,
|
|
245
|
+
);
|
|
246
|
+
process.exitCode = 1;
|
|
247
|
+
return;
|
|
248
|
+
}
|
|
249
|
+
}
|
|
250
|
+
return serve(taken.argv, options);
|
|
251
|
+
};
|
|
252
|
+
|
|
147
253
|
// The register* helpers take the untyped Cli: the display var is read through
|
|
148
254
|
// `displayModeOf`, so they do not depend on this CLI's generics.
|
|
149
255
|
const root = cli as unknown as Cli.Cli;
|
package/src/commands/audio.ts
CHANGED
|
@@ -20,11 +20,12 @@ import {
|
|
|
20
20
|
import {makeReadInclude} from './cart-includes.js';
|
|
21
21
|
import {
|
|
22
22
|
createShellAdapterFactory,
|
|
23
|
-
|
|
23
|
+
DISPLAY_OPTION,
|
|
24
24
|
displayUnavailableError,
|
|
25
25
|
PICO8_ENV,
|
|
26
26
|
type Pico8AdapterFactory,
|
|
27
27
|
pico8NotFoundError,
|
|
28
|
+
resolveCommandDisplay,
|
|
28
29
|
} from './pico8-env.js';
|
|
29
30
|
|
|
30
31
|
/**
|
|
@@ -219,6 +220,7 @@ function buildAudioGroup(adapterFactory: Pico8AdapterFactory): Cli.Cli {
|
|
|
219
220
|
.describe(
|
|
220
221
|
'Safety backstop: kill PICO-8 after this many ms if it never signals.',
|
|
221
222
|
),
|
|
223
|
+
display: DISPLAY_OPTION,
|
|
222
224
|
}),
|
|
223
225
|
env: AUDIO_ENV,
|
|
224
226
|
output: AUDIO_OUTPUT,
|
|
@@ -262,8 +264,10 @@ function buildAudioGroup(adapterFactory: Pico8AdapterFactory): Cli.Cli {
|
|
|
262
264
|
const harnessCart = join(wavDir, 'record-harness.p8');
|
|
263
265
|
writeFileSync(harnessCart, harness.cartText);
|
|
264
266
|
|
|
267
|
+
const display = resolveCommandDisplay(options.display, vars);
|
|
268
|
+
if (!display.ok) return error(display.error);
|
|
265
269
|
const adapter = adapterFactory(env as NodeJS.ProcessEnv, {
|
|
266
|
-
display:
|
|
270
|
+
display: display.mode,
|
|
267
271
|
});
|
|
268
272
|
const result = await adapter.record({
|
|
269
273
|
cartPath: harnessCart,
|
|
@@ -338,6 +342,7 @@ function buildAudioGroup(adapterFactory: Pico8AdapterFactory): Cli.Cli {
|
|
|
338
342
|
.describe(
|
|
339
343
|
'Safety backstop: kill PICO-8 after this many ms if it never signals.',
|
|
340
344
|
),
|
|
345
|
+
display: DISPLAY_OPTION,
|
|
341
346
|
}),
|
|
342
347
|
env: AUDIO_ENV,
|
|
343
348
|
output: AUDIO_OUTPUT,
|
|
@@ -394,8 +399,10 @@ function buildAudioGroup(adapterFactory: Pico8AdapterFactory): Cli.Cli {
|
|
|
394
399
|
const harnessCart = join(wavDir, 'render-harness.p8');
|
|
395
400
|
writeFileSync(harnessCart, harness.cartText);
|
|
396
401
|
|
|
402
|
+
const display = resolveCommandDisplay(options.display, vars);
|
|
403
|
+
if (!display.ok) return error(display.error);
|
|
397
404
|
const adapter = adapterFactory(env as NodeJS.ProcessEnv, {
|
|
398
|
-
display:
|
|
405
|
+
display: display.mode,
|
|
399
406
|
});
|
|
400
407
|
const result = await adapter.record({
|
|
401
408
|
cartPath: harnessCart,
|
package/src/commands/doctor.ts
CHANGED
|
@@ -15,8 +15,9 @@ import {
|
|
|
15
15
|
} from '../engine/pico8/index.js';
|
|
16
16
|
import {
|
|
17
17
|
createShellAdapterFactory,
|
|
18
|
-
|
|
18
|
+
DISPLAY_OPTION,
|
|
19
19
|
PICO8_ENV,
|
|
20
|
+
resolveCommandDisplay,
|
|
20
21
|
type Pico8AdapterFactory,
|
|
21
22
|
} from './pico8-env.js';
|
|
22
23
|
import {
|
|
@@ -112,6 +113,7 @@ export function registerDoctor(cli: Cli.Cli, deps: DoctorDeps = {}): void {
|
|
|
112
113
|
description:
|
|
113
114
|
'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
115
|
env: PICO8_ENV,
|
|
116
|
+
options: z.object({display: DISPLAY_OPTION}),
|
|
115
117
|
output: z.object({
|
|
116
118
|
pico8: z.object({
|
|
117
119
|
found: z.boolean().describe('Whether the PICO-8 binary was found.'),
|
|
@@ -162,8 +164,10 @@ export function registerDoctor(cli: Cli.Cli, deps: DoctorDeps = {}): void {
|
|
|
162
164
|
)
|
|
163
165
|
.describe('Commands that do not, and what they are missing.'),
|
|
164
166
|
}),
|
|
165
|
-
async run({env, ok, var: vars}) {
|
|
166
|
-
const
|
|
167
|
+
async run({env, ok, error, options, var: vars}) {
|
|
168
|
+
const resolved = resolveCommandDisplay(options.display, vars);
|
|
169
|
+
if (!resolved.ok) return error(resolved.error);
|
|
170
|
+
const display: DisplayMode = resolved.mode;
|
|
167
171
|
const childEnv = pico8ChildEnv(
|
|
168
172
|
env as Record<string, string | undefined>,
|
|
169
173
|
deps.parentEnv ?? process.env,
|
package/src/commands/gfx.ts
CHANGED
|
@@ -25,6 +25,7 @@ import {
|
|
|
25
25
|
createShellShrinkoFactory,
|
|
26
26
|
type ShrinkoAdapterFactory,
|
|
27
27
|
} from './shrinko-env.js';
|
|
28
|
+
import {mcpConfigOption} from './pico8-env.js';
|
|
28
29
|
|
|
29
30
|
/** Injects the shrinko adapter `gfx import`/`gfx export` use (the spritesheet round-trip). */
|
|
30
31
|
export type {ShrinkoAdapterFactory} from './shrinko-env.js';
|
|
@@ -199,11 +200,13 @@ function buildGfxGroup(shrinkoFactory: ShrinkoAdapterFactory): Cli.Cli {
|
|
|
199
200
|
),
|
|
200
201
|
}),
|
|
201
202
|
options: z.object({
|
|
203
|
+
// No zod default: an absent value must stay distinguishable, so that under
|
|
204
|
+
// `--mcp` (where incur loads no config) picopilot.json still applies.
|
|
202
205
|
allowMapOverlap: z
|
|
203
206
|
.boolean()
|
|
204
|
-
.
|
|
207
|
+
.optional()
|
|
205
208
|
.describe(
|
|
206
|
-
'Authorise overwriting existing shared-region data when writing sprites 128-255 (ADR-0004). Also settable as allowMapOverlap in picopilot.json.',
|
|
209
|
+
'Authorise overwriting existing shared-region data when writing sprites 128-255 (ADR-0004). Also settable as allowMapOverlap in picopilot.json. Default false.',
|
|
207
210
|
),
|
|
208
211
|
}),
|
|
209
212
|
output: z.object({
|
|
@@ -237,7 +240,7 @@ function buildGfxGroup(shrinkoFactory: ShrinkoAdapterFactory): Cli.Cli {
|
|
|
237
240
|
},
|
|
238
241
|
},
|
|
239
242
|
],
|
|
240
|
-
run({args, options, error, ok}) {
|
|
243
|
+
run({args, options, error, ok, var: vars}) {
|
|
241
244
|
const cartPath = resolveCart(args.cart);
|
|
242
245
|
|
|
243
246
|
// Decode the incoming grid FIRST: a malformed grid is a caller error and
|
|
@@ -267,12 +270,12 @@ function buildGfxGroup(shrinkoFactory: ShrinkoAdapterFactory): Cli.Cli {
|
|
|
267
270
|
// (the shared-bank bytes at risk), NOT __map__ (ADR-0004). The flag and
|
|
268
271
|
// the picopilot.json config both feed `options.allowMapOverlap` (incur
|
|
269
272
|
// resolves argv > config > default), so both authorisation paths converge
|
|
270
|
-
// here.
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
);
|
|
273
|
+
// here. Under `--mcp` incur loads no config, so the picopilot.json value
|
|
274
|
+
// comes from the `mcpConfig` var instead.
|
|
275
|
+
const allowMapOverlap =
|
|
276
|
+
options.allowMapOverlap ??
|
|
277
|
+
mcpConfigOption(vars, ['gfx', 'set'], 'allowMapOverlap') === true;
|
|
278
|
+
const decision = decideOverlap(sheet, args.sprite, allowMapOverlap);
|
|
276
279
|
|
|
277
280
|
if (decision.kind === 'refused') {
|
|
278
281
|
// The silent-corruption guard: REFUSE and DO NOT write. The cart file is
|