localpi 0.2.0 → 0.4.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/README.md +57 -4
- package/dist/src/cli/cli.js +248 -14
- package/dist/src/localpi/catalog.js +79 -18
- package/dist/src/localpi/managed-runtime.js +2 -2
- package/dist/src/localpi/model-profile.js +98 -0
- package/dist/src/localpi/options.js +102 -4
- package/dist/src/localpi/runtime-connection.js +2 -4
- package/dist/src/localpi/settings-state.js +37 -0
- package/dist/src/pi/app.js +108 -0
- package/dist/src/pi/demo.js +12 -0
- package/dist/src/pi/extension-sources/demo-mode.js +52 -0
- package/dist/src/pi/extension-sources/startup-model-selector.js +78 -0
- package/dist/src/pi/extension-sources/thinking-control.js +79 -0
- package/dist/src/pi/extension-sources/token-status.js +133 -0
- package/dist/src/pi/extension-sources/tool-approval.js +45 -0
- package/dist/src/pi/extensions.js +11 -313
- package/dist/src/pi/version.js +25 -0
- package/docs/2026-06-16-startup-model-and-thinking-control-plan.md +6 -4
- package/docs/2026-06-18-endless-demo-mode-plan.md +192 -0
- package/docs/runtime-specification.md +26 -0
- package/package.json +4 -1
- package/dist/src/pi/config.js +0 -108
- package/dist/src/pi/launch.js +0 -64
|
@@ -0,0 +1,192 @@
|
|
|
1
|
+
---
|
|
2
|
+
title: Endless Demo Mode Plan
|
|
3
|
+
author: Bob <dutifulbob@gmail.com>
|
|
4
|
+
date: 2026-06-18
|
|
5
|
+
---
|
|
6
|
+
|
|
7
|
+
# Endless Demo Mode Plan
|
|
8
|
+
|
|
9
|
+
This plan covers an endless localpi demo mode that runs inside Pi's normal TUI and repeatedly prompts Pi until the user exits the TUI or interrupts it.
|
|
10
|
+
|
|
11
|
+
## Goal
|
|
12
|
+
|
|
13
|
+
`localpi --demo` should run a hands-free local model demo without turning localpi into an interactive chat client and without subverting Pi's native TUI.
|
|
14
|
+
|
|
15
|
+
The demo orchestration is owned by a localpi-generated Pi extension. Rendering, streaming, tok/s display, input handling, slash commands, session state, and lifecycle remain owned by Pi.
|
|
16
|
+
|
|
17
|
+
## Target Behavior
|
|
18
|
+
|
|
19
|
+
- `localpi --demo --model <alias|id|path>` starts Pi once with a built-in initial prompt.
|
|
20
|
+
- Pi opens in normal TUI mode.
|
|
21
|
+
- The demo extension sends the initial prompt after TUI `session_start`.
|
|
22
|
+
- After each completed generation, the demo extension sends the followup prompt after `turn_end`.
|
|
23
|
+
- The followup prompt defaults to `Continue. Try to write as long as possible.`
|
|
24
|
+
- The loop continues until the user exits Pi, `Ctrl-C` is pressed, or Pi stops the session normally.
|
|
25
|
+
- Runtime discovery, Pi config generation, extensions, thinking, tools, and approval behavior match normal localpi launches.
|
|
26
|
+
- Demo mode requires an explicit non-`auto` model through `--model` or `LOCALPI_MODEL`; it must not auto-select a model.
|
|
27
|
+
- Demo mode requires interactive TTY stdin and stdout so Pi opens its normal TUI.
|
|
28
|
+
- Demo mode uses one live Pi session so followup prompts keep the first prompt's context.
|
|
29
|
+
- Demo mode works with any provider localpi already supports: LM Studio, vLLM, generic OpenAI-compatible providers, and managed `llama-server`.
|
|
30
|
+
- Demo mode does not parse terminal output to detect when generation stops; it relies on Pi extension events.
|
|
31
|
+
|
|
32
|
+
## Default Prompts
|
|
33
|
+
|
|
34
|
+
Initial prompt:
|
|
35
|
+
|
|
36
|
+
```text
|
|
37
|
+
You are narrating a never-ending sci-fi adventure. Continue in short paragraphs. Whenever the user sends a message, treat it as a live director note and incorporate it immediately. Never end the story.
|
|
38
|
+
```
|
|
39
|
+
|
|
40
|
+
Followup prompt:
|
|
41
|
+
|
|
42
|
+
```text
|
|
43
|
+
Continue. Try to write as long as possible.
|
|
44
|
+
```
|
|
45
|
+
|
|
46
|
+
## CLI Contract
|
|
47
|
+
|
|
48
|
+
Add these flags:
|
|
49
|
+
|
|
50
|
+
- `--demo`: enable endless demo mode.
|
|
51
|
+
- `--demo-initial-prompt <text>`: override the first prompt.
|
|
52
|
+
- `--demo-followup-prompt <text>`: override every prompt after the first.
|
|
53
|
+
- `--demo-initial-prompt-file <path>`: read the first prompt from a UTF-8 file.
|
|
54
|
+
- `--demo-followup-prompt-file <path>`: read the followup prompt from a UTF-8 file.
|
|
55
|
+
|
|
56
|
+
Add matching environment variables:
|
|
57
|
+
|
|
58
|
+
- `LOCALPI_DEMO`
|
|
59
|
+
- `LOCALPI_DEMO_INITIAL_PROMPT`
|
|
60
|
+
- `LOCALPI_DEMO_FOLLOWUP_PROMPT`
|
|
61
|
+
- `LOCALPI_DEMO_INITIAL_PROMPT_FILE`
|
|
62
|
+
- `LOCALPI_DEMO_FOLLOWUP_PROMPT_FILE`
|
|
63
|
+
|
|
64
|
+
File flags should win over text flags for the same prompt because they are the better interface for long prompts.
|
|
65
|
+
|
|
66
|
+
Explicit CLI flags should win over environment variables.
|
|
67
|
+
|
|
68
|
+
## Incompatible Modes
|
|
69
|
+
|
|
70
|
+
Reject these combinations with clear errors:
|
|
71
|
+
|
|
72
|
+
- `--demo --status`
|
|
73
|
+
- `--demo --stop`
|
|
74
|
+
- `--demo --list`
|
|
75
|
+
- `--demo` without an explicit non-`auto` model
|
|
76
|
+
- `--demo` without interactive TTY stdin and stdout
|
|
77
|
+
- `--demo` with user-supplied forwarded Pi prompt flags such as `-p` or `--prompt`
|
|
78
|
+
|
|
79
|
+
Forwarded non-prompt Pi options should remain allowed.
|
|
80
|
+
|
|
81
|
+
## Launch Design
|
|
82
|
+
|
|
83
|
+
Keep `src/pi/launch.ts` as the single-launch layer. Demo mode should use the same launch path as a normal interactive localpi session.
|
|
84
|
+
|
|
85
|
+
Demo mode should not:
|
|
86
|
+
|
|
87
|
+
- pass prompts through stdin
|
|
88
|
+
- pass prompts with `-p` or `--prompt`
|
|
89
|
+
- force print, JSON, or RPC mode
|
|
90
|
+
- launch repeated one-shot Pi child processes
|
|
91
|
+
- create or manage a parallel localpi TUI
|
|
92
|
+
|
|
93
|
+
Normal localpi launch planning should write an additional generated extension when `--demo` is enabled, then launch Pi normally.
|
|
94
|
+
|
|
95
|
+
## Demo Extension
|
|
96
|
+
|
|
97
|
+
Add a generated Pi extension, likely `demo-mode.ts`, alongside the existing localpi extensions.
|
|
98
|
+
|
|
99
|
+
The extension owns:
|
|
100
|
+
|
|
101
|
+
- resolving already-materialized prompt text provided by localpi
|
|
102
|
+
- sending the initial prompt once on `session_start` when `ctx.mode === "tui"`
|
|
103
|
+
- sending the followup prompt after each final assistant `turn_end` when demo mode is still active
|
|
104
|
+
- relying on Pi's own queueing via `pi.sendUserMessage`
|
|
105
|
+
- optional later controls such as `/demo stop`
|
|
106
|
+
|
|
107
|
+
Example shape:
|
|
108
|
+
|
|
109
|
+
```ts
|
|
110
|
+
pi.on("session_start", (event, ctx) => {
|
|
111
|
+
if (started || event.reason !== "startup" || ctx.mode !== "tui") {
|
|
112
|
+
return;
|
|
113
|
+
}
|
|
114
|
+
started = true;
|
|
115
|
+
pi.sendUserMessage(initialPrompt);
|
|
116
|
+
});
|
|
117
|
+
|
|
118
|
+
pi.on("turn_end", (event, ctx) => {
|
|
119
|
+
if (!started || stopped || ctx.mode !== "tui") {
|
|
120
|
+
return;
|
|
121
|
+
}
|
|
122
|
+
if (event.message.role !== "assistant") {
|
|
123
|
+
return;
|
|
124
|
+
}
|
|
125
|
+
if (event.message.stopReason === "aborted" || event.message.stopReason === "error") {
|
|
126
|
+
stopped = true;
|
|
127
|
+
return;
|
|
128
|
+
}
|
|
129
|
+
if (event.message.stopReason === "toolUse") {
|
|
130
|
+
return;
|
|
131
|
+
}
|
|
132
|
+
pi.sendUserMessage(followupPrompt, { deliverAs: "followUp" });
|
|
133
|
+
});
|
|
134
|
+
```
|
|
135
|
+
|
|
136
|
+
Prompt file loading should stay in localpi before extension generation. The generated extension should contain concrete prompt strings so Pi does not need to read localpi-specific files at runtime.
|
|
137
|
+
|
|
138
|
+
## Signal And Exit Behavior
|
|
139
|
+
|
|
140
|
+
- Pi owns `Ctrl-C`, exit, and interactive lifecycle behavior.
|
|
141
|
+
- If a turn ends with an aborted or error assistant message, the demo extension should stop queueing followup prompts.
|
|
142
|
+
- If a turn ends with tool use, the demo extension should wait for the final assistant turn before queueing a followup.
|
|
143
|
+
- localpi should not restart Pi after exit.
|
|
144
|
+
- If Pi exits non-zero, localpi should return the same exit code as a normal launch.
|
|
145
|
+
- No special signal-forwarding loop should be needed beyond normal `execLaunchPlan` behavior.
|
|
146
|
+
|
|
147
|
+
## Non-Goals
|
|
148
|
+
|
|
149
|
+
- Do not scrape stdout or terminal output to infer completion.
|
|
150
|
+
- Do not run demo mode as a hidden headless print-mode loop.
|
|
151
|
+
- Do not repeatedly spawn one-shot Pi processes.
|
|
152
|
+
- Do not create a second localpi-owned TUI or prompt loop.
|
|
153
|
+
- Do not make provider-specific demo behavior.
|
|
154
|
+
- Do not add classifier, benchmark, dataset, or schema workflow concepts.
|
|
155
|
+
- Do not keep a hidden background service running after localpi exits.
|
|
156
|
+
|
|
157
|
+
## Testing Checklist
|
|
158
|
+
|
|
159
|
+
- [x] Parse `--demo` and all demo prompt flags.
|
|
160
|
+
- [x] Parse matching `LOCALPI_DEMO*` environment variables.
|
|
161
|
+
- [x] Verify CLI prompt values override environment values.
|
|
162
|
+
- [x] Verify prompt files override text prompt values.
|
|
163
|
+
- [x] Verify `--demo --status`, `--demo --stop`, and `--demo --list` fail clearly.
|
|
164
|
+
- [x] Verify demo mode rejects missing or `auto` model selection.
|
|
165
|
+
- [x] Verify demo mode rejects non-TTY stdin/stdout.
|
|
166
|
+
- [x] Verify demo mode rejects forwarded Pi prompt flags.
|
|
167
|
+
- [x] Unit-test that demo mode writes a generated Pi extension.
|
|
168
|
+
- [x] Unit-test that the generated extension sends the initial prompt on TUI `session_start`.
|
|
169
|
+
- [x] Unit-test that the generated extension sends followup prompts after `turn_end`.
|
|
170
|
+
- [x] Unit-test that the generated extension does not queue followups after tool-use continuation turns.
|
|
171
|
+
- [x] Unit-test that demo mode uses the normal Pi launch path and does not pipe prompts over stdin.
|
|
172
|
+
- [x] Unit-test that normal launches are unchanged.
|
|
173
|
+
- [x] Use a fake `LOCALPI_PI_CMD` to prove demo launches Pi once with the demo extension path.
|
|
174
|
+
- [x] Verify demo mode does not pass `-p`, `--prompt`, `--mode print`, `--mode json`, or `--mode rpc`.
|
|
175
|
+
- [x] Run `npm run check`.
|
|
176
|
+
|
|
177
|
+
## Documentation Checklist
|
|
178
|
+
|
|
179
|
+
- [x] Document `--demo` in README options.
|
|
180
|
+
- [x] Document prompt override flags.
|
|
181
|
+
- [x] Document `LOCALPI_DEMO*` environment variables.
|
|
182
|
+
- [x] Include one simple example:
|
|
183
|
+
|
|
184
|
+
```bash
|
|
185
|
+
localpi --demo --model gemma-e4b
|
|
186
|
+
```
|
|
187
|
+
|
|
188
|
+
- [x] Include one override example:
|
|
189
|
+
|
|
190
|
+
```bash
|
|
191
|
+
localpi --demo --model gemma-e4b --demo-initial-prompt-file ./prompts/story.txt --demo-followup-prompt "Continue. Try to write as long as possible."
|
|
192
|
+
```
|
|
@@ -98,6 +98,32 @@ Provider registry JSON can define additional OpenAI-compatible providers:
|
|
|
98
98
|
|
|
99
99
|
Set `discover: false` when the endpoint should not be probed during startup. Explicit `--provider <id> --model <id>` can still select that provider and generate Pi config.
|
|
100
100
|
|
|
101
|
+
## Capability Profiles
|
|
102
|
+
|
|
103
|
+
OpenAI-compatible `/v1/models` responses do not reliably report local serving capabilities such as reasoning support or Pi's required thinking request format. Localpi can read a local model capability profile with `--model-profile`, `LOCALPI_MODEL_PROFILE`, or `LOCALPAGER_AGENT_PROFILE`.
|
|
104
|
+
|
|
105
|
+
Example:
|
|
106
|
+
|
|
107
|
+
```json
|
|
108
|
+
{
|
|
109
|
+
"id": "gemma4-26b-a4b-nvfp4",
|
|
110
|
+
"model": "nvidia/Gemma-4-26B-A4B-NVFP4",
|
|
111
|
+
"base_url": "http://127.0.0.1:8000/v1",
|
|
112
|
+
"client": {
|
|
113
|
+
"context_window": 32768,
|
|
114
|
+
"max_tokens": 4096
|
|
115
|
+
},
|
|
116
|
+
"capabilities": {
|
|
117
|
+
"reasoning": true,
|
|
118
|
+
"thinking_format": "qwen-chat-template"
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
```
|
|
122
|
+
|
|
123
|
+
When the served model id matches `model` or `id`, localpi uses the profile to generate Pi model config. `LOCALPI_MODEL_REASONING` / `LOCALPAGER_AGENT_REASONING` and `LOCALPI_MODEL_THINKING_FORMAT` / `LOCALPAGER_AGENT_THINKING_FORMAT` are explicit overrides.
|
|
124
|
+
|
|
125
|
+
Name-based capability detection remains fallback behavior. Built-in vLLM Gemma 4 model ids are treated as reasoning-capable with `qwen-chat-template`, matching vLLM Gemma servers launched with `--reasoning-parser gemma4`.
|
|
126
|
+
|
|
101
127
|
## Model Selection
|
|
102
128
|
|
|
103
129
|
`--model` should accept:
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "localpi",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.4.0",
|
|
4
4
|
"description": "Pi-compatible local model launcher with managed llama-server support.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "MIT",
|
|
@@ -53,5 +53,8 @@
|
|
|
53
53
|
"typescript": "^5.0.0",
|
|
54
54
|
"typescript-eslint": "^8.0.0",
|
|
55
55
|
"vitest": "^3.0.0"
|
|
56
|
+
},
|
|
57
|
+
"dependencies": {
|
|
58
|
+
"@dutifuldev/pi-factory": "^0.1.1"
|
|
56
59
|
}
|
|
57
60
|
}
|
package/dist/src/pi/config.js
DELETED
|
@@ -1,108 +0,0 @@
|
|
|
1
|
-
import { mkdir, writeFile } from "node:fs/promises";
|
|
2
|
-
import path from "node:path";
|
|
3
|
-
export async function writeRuntimeConfig(options, connection) {
|
|
4
|
-
const configDir = path.join(options.stateDir, "pi-config-runtime");
|
|
5
|
-
await mkdir(configDir, { recursive: true });
|
|
6
|
-
const modelsPath = path.join(configDir, "models.json");
|
|
7
|
-
const settingsPath = path.join(configDir, "settings.json");
|
|
8
|
-
await writeFile(modelsPath, `${JSON.stringify(modelsConfig(options, connection), null, 2)}\n`);
|
|
9
|
-
await writeFile(settingsPath, `${JSON.stringify(settingsConfig(options, connection), null, 2)}\n`);
|
|
10
|
-
return { configDir, modelsPath, settingsPath };
|
|
11
|
-
}
|
|
12
|
-
function modelsConfig(options, connection) {
|
|
13
|
-
const models = connection.catalogModels.length === 0 ? fallbackCatalog(connection) : connection.catalogModels;
|
|
14
|
-
return {
|
|
15
|
-
providers: Object.fromEntries(groupedByProvider(models).map((entry) => [
|
|
16
|
-
entry.providerId,
|
|
17
|
-
providerConfig(options, connection, entry)
|
|
18
|
-
]))
|
|
19
|
-
};
|
|
20
|
-
}
|
|
21
|
-
function groupedByProvider(models) {
|
|
22
|
-
const groups = new Map();
|
|
23
|
-
for (const model of models) {
|
|
24
|
-
const existing = groups.get(model.providerId);
|
|
25
|
-
groups.set(model.providerId, existing === undefined
|
|
26
|
-
? { providerId: model.providerId, baseUrl: model.baseUrl, models: [model] }
|
|
27
|
-
: { ...existing, models: [...existing.models, model] });
|
|
28
|
-
}
|
|
29
|
-
return [...groups.values()];
|
|
30
|
-
}
|
|
31
|
-
function providerConfig(options, connection, group) {
|
|
32
|
-
return {
|
|
33
|
-
baseUrl: group.baseUrl,
|
|
34
|
-
api: "openai-completions",
|
|
35
|
-
apiKey: "local",
|
|
36
|
-
compat: {
|
|
37
|
-
supportsDeveloperRole: false,
|
|
38
|
-
supportsReasoningEffort: false
|
|
39
|
-
},
|
|
40
|
-
models: group.models.map((model) => modelConfig(options, connection, model))
|
|
41
|
-
};
|
|
42
|
-
}
|
|
43
|
-
function modelConfig(options, connection, model) {
|
|
44
|
-
return withoutUndefined({
|
|
45
|
-
id: model.modelId,
|
|
46
|
-
name: model.displayName,
|
|
47
|
-
reasoning: model.reasoning ?? false,
|
|
48
|
-
compat: model.thinkingFormat === undefined ? undefined : { thinkingFormat: model.thinkingFormat },
|
|
49
|
-
input: ["text"],
|
|
50
|
-
contextWindow: modelContextWindow(options, model),
|
|
51
|
-
maxTokens: model.maxTokens ?? options.maxTokens,
|
|
52
|
-
cost: {
|
|
53
|
-
input: 0,
|
|
54
|
-
output: 0,
|
|
55
|
-
cacheRead: 0,
|
|
56
|
-
cacheWrite: 0
|
|
57
|
-
}
|
|
58
|
-
});
|
|
59
|
-
}
|
|
60
|
-
function modelContextWindow(options, model) {
|
|
61
|
-
if (options.contextWindow !== undefined) {
|
|
62
|
-
return options.contextWindow;
|
|
63
|
-
}
|
|
64
|
-
return model.contextWindow;
|
|
65
|
-
}
|
|
66
|
-
function fallbackCatalog(connection) {
|
|
67
|
-
return [
|
|
68
|
-
{
|
|
69
|
-
providerId: connection.providerId,
|
|
70
|
-
providerName: connection.providerName,
|
|
71
|
-
runtime: connection.runtime.startsWith("llama-server")
|
|
72
|
-
? "managed-llama-server"
|
|
73
|
-
: "openai-compatible",
|
|
74
|
-
baseUrl: connection.baseUrl,
|
|
75
|
-
modelId: connection.model,
|
|
76
|
-
aliases: [],
|
|
77
|
-
displayName: `Local model (${connection.model})`,
|
|
78
|
-
reasoning: false,
|
|
79
|
-
capabilities: ["text"],
|
|
80
|
-
availability: "loaded",
|
|
81
|
-
...(connection.contextWindow === undefined ? {} : { contextWindow: connection.contextWindow })
|
|
82
|
-
}
|
|
83
|
-
];
|
|
84
|
-
}
|
|
85
|
-
function withoutUndefined(value) {
|
|
86
|
-
return Object.fromEntries(Object.entries(value).filter(([, entryValue]) => entryValue !== undefined));
|
|
87
|
-
}
|
|
88
|
-
function settingsConfig(options, connection) {
|
|
89
|
-
const contextWindow = options.contextWindow ?? connection.contextWindow;
|
|
90
|
-
return {
|
|
91
|
-
defaultProvider: connection.providerId,
|
|
92
|
-
defaultModel: connection.model,
|
|
93
|
-
defaultThinkingLevel: options.thinking,
|
|
94
|
-
enableInstallTelemetry: false,
|
|
95
|
-
quietStartup: true,
|
|
96
|
-
compaction: compactionConfig(contextWindow)
|
|
97
|
-
};
|
|
98
|
-
}
|
|
99
|
-
function compactionConfig(contextWindow) {
|
|
100
|
-
if (contextWindow === undefined) {
|
|
101
|
-
return { enabled: false };
|
|
102
|
-
}
|
|
103
|
-
return {
|
|
104
|
-
enabled: true,
|
|
105
|
-
reserveTokens: Math.max(256, Math.min(16384, Math.floor(contextWindow / 4))),
|
|
106
|
-
keepRecentTokens: Math.max(512, Math.min(20000, Math.floor(contextWindow / 2)))
|
|
107
|
-
};
|
|
108
|
-
}
|
package/dist/src/pi/launch.js
DELETED
|
@@ -1,64 +0,0 @@
|
|
|
1
|
-
import { spawn } from "node:child_process";
|
|
2
|
-
import { mkdir } from "node:fs/promises";
|
|
3
|
-
export async function createLaunchPlan(options, runtimeConfig, connection, extensions) {
|
|
4
|
-
await mkdir(options.sessionDir, { recursive: true });
|
|
5
|
-
return {
|
|
6
|
-
command: options.piCommand,
|
|
7
|
-
args: [
|
|
8
|
-
"--provider",
|
|
9
|
-
connection.providerId,
|
|
10
|
-
"--model",
|
|
11
|
-
connection.model,
|
|
12
|
-
"--thinking",
|
|
13
|
-
options.thinking,
|
|
14
|
-
...extensionArgs(extensions),
|
|
15
|
-
"--append-system-prompt",
|
|
16
|
-
extensions.systemPrompt,
|
|
17
|
-
...withDefaultTools(options.forwardedArgs, options.tools)
|
|
18
|
-
],
|
|
19
|
-
env: {
|
|
20
|
-
PI_CODING_AGENT_DIR: runtimeConfig.configDir,
|
|
21
|
-
PI_CODING_AGENT_SESSION_DIR: options.sessionDir,
|
|
22
|
-
PI_OFFLINE: process.env["PI_OFFLINE"] ?? "1",
|
|
23
|
-
PI_TELEMETRY: process.env["PI_TELEMETRY"] ?? "0",
|
|
24
|
-
PI_SKIP_VERSION_CHECK: process.env["PI_SKIP_VERSION_CHECK"] ?? "1"
|
|
25
|
-
}
|
|
26
|
-
};
|
|
27
|
-
}
|
|
28
|
-
export async function execLaunchPlan(plan) {
|
|
29
|
-
const stdio = "inherit";
|
|
30
|
-
const child = spawn(shellCommand(plan.command, plan.args), {
|
|
31
|
-
shell: true,
|
|
32
|
-
stdio,
|
|
33
|
-
env: { ...process.env, ...plan.env }
|
|
34
|
-
});
|
|
35
|
-
child.stdout?.resume();
|
|
36
|
-
return await new Promise((resolve, reject) => {
|
|
37
|
-
child.on("error", reject);
|
|
38
|
-
child.on("exit", (code, signal) => {
|
|
39
|
-
if (signal !== null) {
|
|
40
|
-
process.kill(process.pid, signal);
|
|
41
|
-
return;
|
|
42
|
-
}
|
|
43
|
-
resolve(code ?? 0);
|
|
44
|
-
});
|
|
45
|
-
});
|
|
46
|
-
}
|
|
47
|
-
function shellCommand(command, args) {
|
|
48
|
-
return [command, ...args.map(shellQuote)].join(" ");
|
|
49
|
-
}
|
|
50
|
-
function shellQuote(value) {
|
|
51
|
-
return `'${value.replaceAll("'", "'\\''")}'`;
|
|
52
|
-
}
|
|
53
|
-
function extensionArgs(extensions) {
|
|
54
|
-
return extensions.paths.flatMap((extensionPath) => ["--extension", extensionPath]);
|
|
55
|
-
}
|
|
56
|
-
function withDefaultTools(args, tools) {
|
|
57
|
-
if (tools === undefined || hasToolFlag(args)) {
|
|
58
|
-
return args;
|
|
59
|
-
}
|
|
60
|
-
return ["--tools", tools, ...args];
|
|
61
|
-
}
|
|
62
|
-
function hasToolFlag(args) {
|
|
63
|
-
return args.some((arg) => arg === "--tools" || arg === "-t" || arg === "--no-tools" || arg === "-nt");
|
|
64
|
-
}
|