gsd-pi 0.3.0 → 0.3.3
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 +3 -1
- package/dist/cli.js +112 -5
- package/dist/loader.js +0 -0
- package/dist/resource-loader.d.ts +3 -3
- package/dist/resource-loader.js +10 -4
- package/dist/tool-bootstrap.d.ts +4 -0
- package/dist/tool-bootstrap.js +74 -0
- package/dist/wizard.js +15 -5
- package/package.json +6 -2
- package/patches/@mariozechner+pi-coding-agent+0.57.1.patch +48 -0
- package/patches/@mariozechner+pi-tui+0.57.1.patch +47 -0
- package/scripts/postinstall.js +8 -0
- package/src/resources/extensions/bg-shell/index.ts +57 -8
- package/src/resources/extensions/browser-tools/index.ts +80 -7
- package/src/resources/extensions/github/gh-api.ts +46 -30
- package/src/resources/extensions/gsd/auto.ts +188 -10
- package/src/resources/extensions/gsd/commands.ts +13 -6
- package/src/resources/extensions/gsd/doctor.ts +7 -0
- package/src/resources/extensions/gsd/guided-flow.ts +9 -6
- package/src/resources/extensions/gsd/index.ts +32 -2
- package/src/resources/extensions/gsd/prompts/discuss.md +73 -27
- package/src/resources/extensions/gsd/prompts/system.md +1 -1
- package/src/resources/extensions/gsd/prompts/worktree-merge.md +51 -17
- package/src/resources/extensions/gsd/tests/discuss-prompt.test.ts +38 -0
- package/src/resources/extensions/gsd/worktree-command.ts +219 -49
- package/src/resources/extensions/gsd/worktree-manager.ts +106 -16
- package/src/resources/extensions/mcporter/index.ts +410 -0
- package/src/resources/extensions/slash-commands/clear.ts +10 -0
- package/src/resources/extensions/slash-commands/index.ts +2 -2
- package/src/resources/extensions/voice/index.ts +176 -0
- package/src/resources/extensions/voice/speech-recognizer +0 -0
- package/src/resources/extensions/voice/speech-recognizer.swift +76 -0
- package/dist/modes/interactive/theme/dark.json +0 -85
- package/dist/modes/interactive/theme/light.json +0 -84
- package/dist/modes/interactive/theme/theme-schema.json +0 -335
- package/dist/modes/interactive/theme/theme.d.ts +0 -78
- package/dist/modes/interactive/theme/theme.d.ts.map +0 -1
- package/dist/modes/interactive/theme/theme.js +0 -949
- package/dist/modes/interactive/theme/theme.js.map +0 -1
- package/src/resources/extensions/slash-commands/gsd-run.ts +0 -34
package/README.md
CHANGED
|
@@ -412,7 +412,9 @@ Use expensive models where quality matters (planning, complex execution) and che
|
|
|
412
412
|
|
|
413
413
|
## Star History
|
|
414
414
|
|
|
415
|
-
|
|
415
|
+
<a href="https://star-history.com/#gsd-build/gsd-2&Date">
|
|
416
|
+
<img alt="Star History Chart" src="https://api.star-history.com/svg?repos=gsd-build/gsd-2&type=Date" />
|
|
417
|
+
</a>
|
|
416
418
|
|
|
417
419
|
---
|
|
418
420
|
|
package/dist/cli.js
CHANGED
|
@@ -1,10 +1,56 @@
|
|
|
1
|
-
import { AuthStorage, ModelRegistry, SettingsManager, SessionManager, createAgentSession, InteractiveMode, } from '@mariozechner/pi-coding-agent';
|
|
1
|
+
import { AuthStorage, DefaultResourceLoader, ModelRegistry, SettingsManager, SessionManager, createAgentSession, InteractiveMode, runPrintMode, } from '@mariozechner/pi-coding-agent';
|
|
2
|
+
import { readFileSync } from 'node:fs';
|
|
3
|
+
import { join } from 'node:path';
|
|
2
4
|
import { agentDir, sessionsDir, authFilePath } from './app-paths.js';
|
|
3
|
-
import {
|
|
5
|
+
import { initResources } from './resource-loader.js';
|
|
6
|
+
import { ensureManagedTools } from './tool-bootstrap.js';
|
|
4
7
|
import { loadStoredEnvKeys, runWizardIfNeeded } from './wizard.js';
|
|
8
|
+
function parseCliArgs(argv) {
|
|
9
|
+
const flags = { extensions: [], messages: [] };
|
|
10
|
+
const args = argv.slice(2); // skip node + script
|
|
11
|
+
for (let i = 0; i < args.length; i++) {
|
|
12
|
+
const arg = args[i];
|
|
13
|
+
if (arg === '--mode' && i + 1 < args.length) {
|
|
14
|
+
const m = args[++i];
|
|
15
|
+
if (m === 'text' || m === 'json' || m === 'rpc')
|
|
16
|
+
flags.mode = m;
|
|
17
|
+
}
|
|
18
|
+
else if (arg === '--print' || arg === '-p') {
|
|
19
|
+
flags.print = true;
|
|
20
|
+
}
|
|
21
|
+
else if (arg === '--no-session') {
|
|
22
|
+
flags.noSession = true;
|
|
23
|
+
}
|
|
24
|
+
else if (arg === '--model' && i + 1 < args.length) {
|
|
25
|
+
flags.model = args[++i];
|
|
26
|
+
}
|
|
27
|
+
else if (arg === '--extension' && i + 1 < args.length) {
|
|
28
|
+
flags.extensions.push(args[++i]);
|
|
29
|
+
}
|
|
30
|
+
else if (arg === '--append-system-prompt' && i + 1 < args.length) {
|
|
31
|
+
flags.appendSystemPrompt = args[++i];
|
|
32
|
+
}
|
|
33
|
+
else if (arg === '--tools' && i + 1 < args.length) {
|
|
34
|
+
flags.tools = args[++i].split(',');
|
|
35
|
+
}
|
|
36
|
+
else if (!arg.startsWith('--') && !arg.startsWith('-')) {
|
|
37
|
+
flags.messages.push(arg);
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
return flags;
|
|
41
|
+
}
|
|
42
|
+
const cliFlags = parseCliArgs(process.argv);
|
|
43
|
+
const isPrintMode = cliFlags.print || cliFlags.mode !== undefined;
|
|
44
|
+
// Pi's tool bootstrap can mis-detect already-installed fd/rg on some systems
|
|
45
|
+
// because spawnSync(..., ["--version"]) returns EPERM despite a zero exit code.
|
|
46
|
+
// Provision local managed binaries first so Pi sees them without probing PATH.
|
|
47
|
+
ensureManagedTools(join(agentDir, 'bin'));
|
|
5
48
|
const authStorage = AuthStorage.create(authFilePath);
|
|
6
49
|
loadStoredEnvKeys(authStorage);
|
|
7
|
-
|
|
50
|
+
// Skip the setup wizard in print mode — it requires TTY interaction
|
|
51
|
+
if (!isPrintMode) {
|
|
52
|
+
await runWizardIfNeeded(authStorage);
|
|
53
|
+
}
|
|
8
54
|
const modelRegistry = new ModelRegistry(authStorage);
|
|
9
55
|
const settingsManager = SettingsManager.create(agentDir);
|
|
10
56
|
// Validate configured model on startup — catches stale settings from prior installs
|
|
@@ -37,9 +83,70 @@ if (!settingsManager.getQuietStartup()) {
|
|
|
37
83
|
if (!settingsManager.getCollapseChangelog()) {
|
|
38
84
|
settingsManager.setCollapseChangelog(true);
|
|
39
85
|
}
|
|
40
|
-
|
|
86
|
+
// ---------------------------------------------------------------------------
|
|
87
|
+
// Print / subagent mode — single-shot execution, no TTY required
|
|
88
|
+
// ---------------------------------------------------------------------------
|
|
89
|
+
if (isPrintMode) {
|
|
90
|
+
const sessionManager = cliFlags.noSession
|
|
91
|
+
? SessionManager.inMemory()
|
|
92
|
+
: SessionManager.create(process.cwd());
|
|
93
|
+
// Read --append-system-prompt file content (subagent writes agent system prompts to temp files)
|
|
94
|
+
let appendSystemPrompt;
|
|
95
|
+
if (cliFlags.appendSystemPrompt) {
|
|
96
|
+
try {
|
|
97
|
+
appendSystemPrompt = readFileSync(cliFlags.appendSystemPrompt, 'utf-8');
|
|
98
|
+
}
|
|
99
|
+
catch {
|
|
100
|
+
// If it's not a file path, treat it as literal text
|
|
101
|
+
appendSystemPrompt = cliFlags.appendSystemPrompt;
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
initResources(agentDir);
|
|
105
|
+
const resourceLoader = new DefaultResourceLoader({
|
|
106
|
+
agentDir,
|
|
107
|
+
additionalExtensionPaths: cliFlags.extensions.length > 0 ? cliFlags.extensions : undefined,
|
|
108
|
+
appendSystemPrompt,
|
|
109
|
+
});
|
|
110
|
+
await resourceLoader.reload();
|
|
111
|
+
const { session, extensionsResult } = await createAgentSession({
|
|
112
|
+
authStorage,
|
|
113
|
+
modelRegistry,
|
|
114
|
+
settingsManager,
|
|
115
|
+
sessionManager,
|
|
116
|
+
resourceLoader,
|
|
117
|
+
});
|
|
118
|
+
if (extensionsResult.errors.length > 0) {
|
|
119
|
+
for (const err of extensionsResult.errors) {
|
|
120
|
+
process.stderr.write(`[gsd] Extension load error: ${err.error}\n`);
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
// Apply --model override if specified
|
|
124
|
+
if (cliFlags.model) {
|
|
125
|
+
const available = modelRegistry.getAvailable();
|
|
126
|
+
const match = available.find((m) => m.id === cliFlags.model) ||
|
|
127
|
+
available.find((m) => `${m.provider}/${m.id}` === cliFlags.model);
|
|
128
|
+
if (match) {
|
|
129
|
+
session.setModel(match);
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
const mode = cliFlags.mode || 'text';
|
|
133
|
+
await runPrintMode(session, {
|
|
134
|
+
mode: mode === 'rpc' ? 'json' : mode,
|
|
135
|
+
messages: cliFlags.messages,
|
|
136
|
+
});
|
|
137
|
+
process.exit(0);
|
|
138
|
+
}
|
|
139
|
+
// ---------------------------------------------------------------------------
|
|
140
|
+
// Interactive mode — normal TTY session
|
|
141
|
+
// ---------------------------------------------------------------------------
|
|
142
|
+
// Per-directory session storage — same encoding as the upstream SDK so that
|
|
143
|
+
// /resume only shows sessions from the current working directory.
|
|
144
|
+
const cwd = process.cwd();
|
|
145
|
+
const safePath = `--${cwd.replace(/^[/\\]/, '').replace(/[/\\:]/g, '-')}--`;
|
|
146
|
+
const projectSessionsDir = join(sessionsDir, safePath);
|
|
147
|
+
const sessionManager = SessionManager.create(cwd, projectSessionsDir);
|
|
41
148
|
initResources(agentDir);
|
|
42
|
-
const resourceLoader =
|
|
149
|
+
const resourceLoader = new DefaultResourceLoader({ agentDir });
|
|
43
150
|
await resourceLoader.reload();
|
|
44
151
|
const { session, extensionsResult } = await createAgentSession({
|
|
45
152
|
authStorage,
|
package/dist/loader.js
CHANGED
|
File without changes
|
|
@@ -15,8 +15,8 @@ import { DefaultResourceLoader } from '@mariozechner/pi-coding-agent';
|
|
|
15
15
|
*/
|
|
16
16
|
export declare function initResources(agentDir: string): void;
|
|
17
17
|
/**
|
|
18
|
-
* Constructs a DefaultResourceLoader
|
|
19
|
-
*
|
|
20
|
-
*
|
|
18
|
+
* Constructs a DefaultResourceLoader that loads extensions from both
|
|
19
|
+
* ~/.gsd/agent/extensions/ (GSD's default) and ~/.pi/agent/extensions/ (pi's default).
|
|
20
|
+
* This allows users to use extensions from either location.
|
|
21
21
|
*/
|
|
22
22
|
export declare function buildResourceLoader(agentDir: string): DefaultResourceLoader;
|
package/dist/resource-loader.js
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { DefaultResourceLoader } from '@mariozechner/pi-coding-agent';
|
|
2
|
+
import { homedir } from 'node:os';
|
|
2
3
|
import { cpSync, existsSync, mkdirSync, readFileSync, writeFileSync } from 'node:fs';
|
|
3
4
|
import { dirname, join, resolve } from 'node:path';
|
|
4
5
|
import { fileURLToPath } from 'node:url';
|
|
@@ -45,10 +46,15 @@ export function initResources(agentDir) {
|
|
|
45
46
|
}
|
|
46
47
|
}
|
|
47
48
|
/**
|
|
48
|
-
* Constructs a DefaultResourceLoader
|
|
49
|
-
*
|
|
50
|
-
*
|
|
49
|
+
* Constructs a DefaultResourceLoader that loads extensions from both
|
|
50
|
+
* ~/.gsd/agent/extensions/ (GSD's default) and ~/.pi/agent/extensions/ (pi's default).
|
|
51
|
+
* This allows users to use extensions from either location.
|
|
51
52
|
*/
|
|
52
53
|
export function buildResourceLoader(agentDir) {
|
|
53
|
-
|
|
54
|
+
const piAgentDir = join(homedir(), '.pi', 'agent');
|
|
55
|
+
const piExtensionsDir = join(piAgentDir, 'extensions');
|
|
56
|
+
return new DefaultResourceLoader({
|
|
57
|
+
agentDir,
|
|
58
|
+
additionalExtensionPaths: [piExtensionsDir],
|
|
59
|
+
});
|
|
54
60
|
}
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
import { chmodSync, copyFileSync, existsSync, lstatSync, mkdirSync, rmSync, symlinkSync } from "node:fs";
|
|
2
|
+
import { delimiter, join } from "node:path";
|
|
3
|
+
const TOOL_SPECS = {
|
|
4
|
+
fd: {
|
|
5
|
+
targetName: process.platform === "win32" ? "fd.exe" : "fd",
|
|
6
|
+
candidates: process.platform === "win32" ? ["fd.exe", "fd", "fdfind.exe", "fdfind"] : ["fd", "fdfind"],
|
|
7
|
+
},
|
|
8
|
+
rg: {
|
|
9
|
+
targetName: process.platform === "win32" ? "rg.exe" : "rg",
|
|
10
|
+
candidates: process.platform === "win32" ? ["rg.exe", "rg"] : ["rg"],
|
|
11
|
+
},
|
|
12
|
+
};
|
|
13
|
+
function splitPath(pathValue) {
|
|
14
|
+
if (!pathValue)
|
|
15
|
+
return [];
|
|
16
|
+
return pathValue.split(delimiter).map((segment) => segment.trim()).filter(Boolean);
|
|
17
|
+
}
|
|
18
|
+
function getCandidateNames(name) {
|
|
19
|
+
if (process.platform !== "win32")
|
|
20
|
+
return [name];
|
|
21
|
+
const lower = name.toLowerCase();
|
|
22
|
+
if (lower.endsWith(".exe") || lower.endsWith(".cmd") || lower.endsWith(".bat"))
|
|
23
|
+
return [name];
|
|
24
|
+
return [name, `${name}.exe`, `${name}.cmd`, `${name}.bat`];
|
|
25
|
+
}
|
|
26
|
+
function isRegularFile(path) {
|
|
27
|
+
try {
|
|
28
|
+
return lstatSync(path).isFile() || lstatSync(path).isSymbolicLink();
|
|
29
|
+
}
|
|
30
|
+
catch {
|
|
31
|
+
return false;
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
export function resolveToolFromPath(tool, pathValue = process.env.PATH) {
|
|
35
|
+
const spec = TOOL_SPECS[tool];
|
|
36
|
+
for (const dir of splitPath(pathValue)) {
|
|
37
|
+
for (const candidate of spec.candidates) {
|
|
38
|
+
for (const name of getCandidateNames(candidate)) {
|
|
39
|
+
const fullPath = join(dir, name);
|
|
40
|
+
if (existsSync(fullPath) && isRegularFile(fullPath)) {
|
|
41
|
+
return fullPath;
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
return null;
|
|
47
|
+
}
|
|
48
|
+
function provisionTool(targetDir, tool, sourcePath) {
|
|
49
|
+
const targetPath = join(targetDir, TOOL_SPECS[tool].targetName);
|
|
50
|
+
if (existsSync(targetPath))
|
|
51
|
+
return targetPath;
|
|
52
|
+
mkdirSync(targetDir, { recursive: true });
|
|
53
|
+
try {
|
|
54
|
+
symlinkSync(sourcePath, targetPath);
|
|
55
|
+
}
|
|
56
|
+
catch {
|
|
57
|
+
rmSync(targetPath, { force: true });
|
|
58
|
+
copyFileSync(sourcePath, targetPath);
|
|
59
|
+
chmodSync(targetPath, 0o755);
|
|
60
|
+
}
|
|
61
|
+
return targetPath;
|
|
62
|
+
}
|
|
63
|
+
export function ensureManagedTools(targetDir, pathValue = process.env.PATH) {
|
|
64
|
+
const provisioned = [];
|
|
65
|
+
for (const tool of Object.keys(TOOL_SPECS)) {
|
|
66
|
+
if (existsSync(join(targetDir, TOOL_SPECS[tool].targetName)))
|
|
67
|
+
continue;
|
|
68
|
+
const sourcePath = resolveToolFromPath(tool, pathValue);
|
|
69
|
+
if (!sourcePath)
|
|
70
|
+
continue;
|
|
71
|
+
provisioned.push(provisionTool(targetDir, tool, sourcePath));
|
|
72
|
+
}
|
|
73
|
+
return provisioned;
|
|
74
|
+
}
|
package/dist/wizard.js
CHANGED
|
@@ -21,6 +21,18 @@ async function promptMasked(label, hint) {
|
|
|
21
21
|
process.stdin.resume();
|
|
22
22
|
process.stdin.setEncoding('utf8');
|
|
23
23
|
let value = '';
|
|
24
|
+
const redraw = () => {
|
|
25
|
+
process.stdout.clearLine(0);
|
|
26
|
+
process.stdout.cursorTo(0);
|
|
27
|
+
if (value.length === 0) {
|
|
28
|
+
process.stdout.write(' ');
|
|
29
|
+
}
|
|
30
|
+
else {
|
|
31
|
+
const dots = '●'.repeat(Math.min(value.length, 24));
|
|
32
|
+
const counter = value.length > 24 ? ` ${dim}(${value.length})${reset}` : ` ${dim}${value.length}${reset}`;
|
|
33
|
+
process.stdout.write(` ${dots}${counter}`);
|
|
34
|
+
}
|
|
35
|
+
};
|
|
24
36
|
const handler = (ch) => {
|
|
25
37
|
if (ch === '\r' || ch === '\n') {
|
|
26
38
|
process.stdin.setRawMode(false);
|
|
@@ -34,17 +46,15 @@ async function promptMasked(label, hint) {
|
|
|
34
46
|
process.stdout.write('\n');
|
|
35
47
|
process.exit(0);
|
|
36
48
|
}
|
|
37
|
-
else if (ch === '\u007f') {
|
|
49
|
+
else if (ch === '\u007f' || ch === '\b') {
|
|
38
50
|
if (value.length > 0) {
|
|
39
51
|
value = value.slice(0, -1);
|
|
40
52
|
}
|
|
41
|
-
|
|
42
|
-
process.stdout.cursorTo(0);
|
|
43
|
-
process.stdout.write(' ' + '*'.repeat(value.length));
|
|
53
|
+
redraw();
|
|
44
54
|
}
|
|
45
55
|
else {
|
|
46
56
|
value += ch;
|
|
47
|
-
|
|
57
|
+
redraw();
|
|
48
58
|
}
|
|
49
59
|
};
|
|
50
60
|
process.stdin.on('data', handler);
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "gsd-pi",
|
|
3
|
-
"version": "0.3.
|
|
4
|
-
"description": "GSD — Get
|
|
3
|
+
"version": "0.3.3",
|
|
4
|
+
"description": "GSD — Get Shit Done coding agent",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"repository": {
|
|
7
7
|
"type": "git",
|
|
@@ -18,6 +18,7 @@
|
|
|
18
18
|
},
|
|
19
19
|
"files": [
|
|
20
20
|
"dist",
|
|
21
|
+
"patches",
|
|
21
22
|
"pkg",
|
|
22
23
|
"src/resources",
|
|
23
24
|
"scripts/postinstall.js",
|
|
@@ -37,6 +38,8 @@
|
|
|
37
38
|
"test": "node --import ./src/resources/extensions/gsd/tests/resolve-ts.mjs --experimental-strip-types --test 'src/resources/extensions/gsd/tests/*.test.ts' 'src/resources/extensions/gsd/tests/*.test.mjs' 'src/tests/*.test.ts'",
|
|
38
39
|
"dev": "tsc --watch",
|
|
39
40
|
"postinstall": "node scripts/postinstall.js",
|
|
41
|
+
"pi:install-global": "node scripts/install-pi-global.js",
|
|
42
|
+
"pi:uninstall-global": "node scripts/uninstall-pi-global.js",
|
|
40
43
|
"sync-pkg-version": "node scripts/sync-pkg-version.cjs",
|
|
41
44
|
"prepublishOnly": "npm run sync-pkg-version && npm run build"
|
|
42
45
|
},
|
|
@@ -46,6 +49,7 @@
|
|
|
46
49
|
},
|
|
47
50
|
"devDependencies": {
|
|
48
51
|
"@types/node": "^22.0.0",
|
|
52
|
+
"patch-package": "^8.0.1",
|
|
49
53
|
"typescript": "^5.4.0"
|
|
50
54
|
},
|
|
51
55
|
"overrides": {
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
diff --git a/node_modules/@mariozechner/pi-coding-agent/dist/core/tools/bash.js b/node_modules/@mariozechner/pi-coding-agent/dist/core/tools/bash.js
|
|
2
|
+
index 27fe820..68f277f 100644
|
|
3
|
+
--- a/node_modules/@mariozechner/pi-coding-agent/dist/core/tools/bash.js
|
|
4
|
+
+++ b/node_modules/@mariozechner/pi-coding-agent/dist/core/tools/bash.js
|
|
5
|
+
@@ -1,11 +1,35 @@
|
|
6
|
+
import { randomBytes } from "node:crypto";
|
|
7
|
+
import { createWriteStream, existsSync } from "node:fs";
|
|
8
|
+
+import { createRequire } from "node:module";
|
|
9
|
+
import { tmpdir } from "node:os";
|
|
10
|
+
import { join } from "node:path";
|
|
11
|
+
import { Type } from "@sinclair/typebox";
|
|
12
|
+
import { spawn } from "child_process";
|
|
13
|
+
import { getShellConfig, getShellEnv, killProcessTree } from "../../utils/shell.js";
|
|
14
|
+
import { DEFAULT_MAX_BYTES, DEFAULT_MAX_LINES, formatSize, truncateTail } from "./truncate.js";
|
|
15
|
+
+// Cached Win32 FFI handles for restoring VT input after child processes
|
|
16
|
+
+let _vtHandles = null;
|
|
17
|
+
+function restoreWindowsVTInput() {
|
|
18
|
+
+ if (process.platform !== "win32") return;
|
|
19
|
+
+ try {
|
|
20
|
+
+ if (!_vtHandles) {
|
|
21
|
+
+ const cjsRequire = createRequire(import.meta.url);
|
|
22
|
+
+ const koffi = cjsRequire("koffi");
|
|
23
|
+
+ const k32 = koffi.load("kernel32.dll");
|
|
24
|
+
+ const GetStdHandle = k32.func("void* __stdcall GetStdHandle(int)");
|
|
25
|
+
+ const GetConsoleMode = k32.func("bool __stdcall GetConsoleMode(void*, _Out_ uint32_t*)");
|
|
26
|
+
+ const SetConsoleMode = k32.func("bool __stdcall SetConsoleMode(void*, uint32_t)");
|
|
27
|
+
+ const handle = GetStdHandle(-10);
|
|
28
|
+
+ _vtHandles = { GetConsoleMode, SetConsoleMode, handle };
|
|
29
|
+
+ }
|
|
30
|
+
+ const ENABLE_VIRTUAL_TERMINAL_INPUT = 0x0200;
|
|
31
|
+
+ const mode = new Uint32Array(1);
|
|
32
|
+
+ _vtHandles.GetConsoleMode(_vtHandles.handle, mode);
|
|
33
|
+
+ if (!(mode[0] & ENABLE_VIRTUAL_TERMINAL_INPUT)) {
|
|
34
|
+
+ _vtHandles.SetConsoleMode(_vtHandles.handle, mode[0] | ENABLE_VIRTUAL_TERMINAL_INPUT);
|
|
35
|
+
+ }
|
|
36
|
+
+ } catch { }
|
|
37
|
+
+}
|
|
38
|
+
/**
|
|
39
|
+
* Generate a unique temp file path for bash output
|
|
40
|
+
*/
|
|
41
|
+
@@ -76,6 +100,7 @@ const defaultBashOperations = {
|
|
42
|
+
}
|
|
43
|
+
// Handle process exit
|
|
44
|
+
child.on("close", (code) => {
|
|
45
|
+
+ restoreWindowsVTInput();
|
|
46
|
+
if (timeoutHandle)
|
|
47
|
+
clearTimeout(timeoutHandle);
|
|
48
|
+
if (signal)
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
diff --git a/node_modules/@mariozechner/pi-tui/dist/terminal.js b/node_modules/@mariozechner/pi-tui/dist/terminal.js
|
|
2
|
+
index cd20330..e836fcd 100644
|
|
3
|
+
--- a/node_modules/@mariozechner/pi-tui/dist/terminal.js
|
|
4
|
+
+++ b/node_modules/@mariozechner/pi-tui/dist/terminal.js
|
|
5
|
+
@@ -7,6 +7,7 @@ const cjsRequire = createRequire(import.meta.url);
|
|
6
|
+
* Real terminal using process.stdin/stdout
|
|
7
|
+
*/
|
|
8
|
+
export class ProcessTerminal {
|
|
9
|
+
+ static _vtHandles = null;
|
|
10
|
+
wasRaw = false;
|
|
11
|
+
inputHandler;
|
|
12
|
+
resizeHandler;
|
|
13
|
+
@@ -126,20 +127,23 @@ export class ProcessTerminal {
|
|
14
|
+
if (process.platform !== "win32")
|
|
15
|
+
return;
|
|
16
|
+
try {
|
|
17
|
+
- // Dynamic require to avoid bundling koffi's 74MB of cross-platform
|
|
18
|
+
- // native binaries into every compiled binary. Koffi is only needed
|
|
19
|
+
- // on Windows for VT input support.
|
|
20
|
+
- const koffi = cjsRequire("koffi");
|
|
21
|
+
- const k32 = koffi.load("kernel32.dll");
|
|
22
|
+
- const GetStdHandle = k32.func("void* __stdcall GetStdHandle(int)");
|
|
23
|
+
- const GetConsoleMode = k32.func("bool __stdcall GetConsoleMode(void*, _Out_ uint32_t*)");
|
|
24
|
+
- const SetConsoleMode = k32.func("bool __stdcall SetConsoleMode(void*, uint32_t)");
|
|
25
|
+
- const STD_INPUT_HANDLE = -10;
|
|
26
|
+
+ if (!ProcessTerminal._vtHandles) {
|
|
27
|
+
+ const koffi = cjsRequire("koffi");
|
|
28
|
+
+ const k32 = koffi.load("kernel32.dll");
|
|
29
|
+
+ const GetStdHandle = k32.func("void* __stdcall GetStdHandle(int)");
|
|
30
|
+
+ const GetConsoleMode = k32.func("bool __stdcall GetConsoleMode(void*, _Out_ uint32_t*)");
|
|
31
|
+
+ const SetConsoleMode = k32.func("bool __stdcall SetConsoleMode(void*, uint32_t)");
|
|
32
|
+
+ const STD_INPUT_HANDLE = -10;
|
|
33
|
+
+ const handle = GetStdHandle(STD_INPUT_HANDLE);
|
|
34
|
+
+ ProcessTerminal._vtHandles = { GetConsoleMode, SetConsoleMode, handle };
|
|
35
|
+
+ }
|
|
36
|
+
const ENABLE_VIRTUAL_TERMINAL_INPUT = 0x0200;
|
|
37
|
+
- const handle = GetStdHandle(STD_INPUT_HANDLE);
|
|
38
|
+
+ const { GetConsoleMode, SetConsoleMode, handle } = ProcessTerminal._vtHandles;
|
|
39
|
+
const mode = new Uint32Array(1);
|
|
40
|
+
GetConsoleMode(handle, mode);
|
|
41
|
+
- SetConsoleMode(handle, mode[0] | ENABLE_VIRTUAL_TERMINAL_INPUT);
|
|
42
|
+
+ if (!(mode[0] & ENABLE_VIRTUAL_TERMINAL_INPUT)) {
|
|
43
|
+
+ SetConsoleMode(handle, mode[0] | ENABLE_VIRTUAL_TERMINAL_INPUT);
|
|
44
|
+
+ }
|
|
45
|
+
}
|
|
46
|
+
catch {
|
|
47
|
+
// koffi not available — Shift+Tab won't be distinguishable from Tab
|
package/scripts/postinstall.js
CHANGED
|
@@ -35,6 +35,14 @@ const banner =
|
|
|
35
35
|
|
|
36
36
|
process.stderr.write(banner)
|
|
37
37
|
|
|
38
|
+
// Apply patches to upstream dependencies (non-fatal)
|
|
39
|
+
try {
|
|
40
|
+
execSync('npx patch-package', { stdio: 'inherit', cwd: resolve(__dirname, '..') })
|
|
41
|
+
process.stderr.write(`\n ${green}✓${reset} Patches applied\n`)
|
|
42
|
+
} catch {
|
|
43
|
+
process.stderr.write(`\n ${yellow}⚠${reset} Failed to apply patches — run ${cyan}npx patch-package${reset} manually\n`)
|
|
44
|
+
}
|
|
45
|
+
|
|
38
46
|
// Install Playwright chromium for browser tools (non-fatal)
|
|
39
47
|
const args = os.platform() === 'linux' ? '--with-deps' : ''
|
|
40
48
|
try {
|
|
@@ -33,6 +33,7 @@ import {
|
|
|
33
33
|
truncateHead,
|
|
34
34
|
DEFAULT_MAX_BYTES,
|
|
35
35
|
DEFAULT_MAX_LINES,
|
|
36
|
+
getShellConfig,
|
|
36
37
|
} from "@mariozechner/pi-coding-agent";
|
|
37
38
|
import {
|
|
38
39
|
Text,
|
|
@@ -42,11 +43,39 @@ import {
|
|
|
42
43
|
Key,
|
|
43
44
|
} from "@mariozechner/pi-tui";
|
|
44
45
|
import { Type } from "@sinclair/typebox";
|
|
45
|
-
import { spawn, type ChildProcess } from "node:child_process";
|
|
46
|
+
import { spawn, spawnSync, type ChildProcess } from "node:child_process";
|
|
46
47
|
import { createConnection } from "node:net";
|
|
47
48
|
import { randomUUID } from "node:crypto";
|
|
48
49
|
import { writeFileSync, readFileSync, existsSync, mkdirSync } from "node:fs";
|
|
49
50
|
import { join } from "node:path";
|
|
51
|
+
import { createRequire } from "node:module";
|
|
52
|
+
|
|
53
|
+
// ── Windows VT Input Restoration ────────────────────────────────────────────
|
|
54
|
+
// Child processes (esp. Git Bash / MSYS2) can strip the ENABLE_VIRTUAL_TERMINAL_INPUT
|
|
55
|
+
// flag from the shared stdin console handle. Re-enable it after each child exits.
|
|
56
|
+
|
|
57
|
+
let _vtHandles: { GetConsoleMode: Function; SetConsoleMode: Function; handle: unknown } | null = null;
|
|
58
|
+
function restoreWindowsVTInput(): void {
|
|
59
|
+
if (process.platform !== "win32") return;
|
|
60
|
+
try {
|
|
61
|
+
if (!_vtHandles) {
|
|
62
|
+
const cjsRequire = createRequire(import.meta.url);
|
|
63
|
+
const koffi = cjsRequire("koffi");
|
|
64
|
+
const k32 = koffi.load("kernel32.dll");
|
|
65
|
+
const GetStdHandle = k32.func("void* __stdcall GetStdHandle(int)");
|
|
66
|
+
const GetConsoleMode = k32.func("bool __stdcall GetConsoleMode(void*, _Out_ uint32_t*)");
|
|
67
|
+
const SetConsoleMode = k32.func("bool __stdcall SetConsoleMode(void*, uint32_t)");
|
|
68
|
+
const handle = GetStdHandle(-10);
|
|
69
|
+
_vtHandles = { GetConsoleMode, SetConsoleMode, handle };
|
|
70
|
+
}
|
|
71
|
+
const ENABLE_VIRTUAL_TERMINAL_INPUT = 0x0200;
|
|
72
|
+
const mode = new Uint32Array(1);
|
|
73
|
+
_vtHandles.GetConsoleMode(_vtHandles.handle, mode);
|
|
74
|
+
if (!(mode[0] & ENABLE_VIRTUAL_TERMINAL_INPUT)) {
|
|
75
|
+
_vtHandles.SetConsoleMode(_vtHandles.handle, mode[0] | ENABLE_VIRTUAL_TERMINAL_INPUT);
|
|
76
|
+
}
|
|
77
|
+
} catch { /* koffi not available on non-Windows */ }
|
|
78
|
+
}
|
|
50
79
|
|
|
51
80
|
// ── Types ──────────────────────────────────────────────────────────────────
|
|
52
81
|
|
|
@@ -551,11 +580,12 @@ function startProcess(opts: StartOptions): BgProcess {
|
|
|
551
580
|
|
|
552
581
|
const env = { ...process.env, ...(opts.env || {}) };
|
|
553
582
|
|
|
554
|
-
const
|
|
583
|
+
const { shell, args: shellArgs } = getShellConfig();
|
|
584
|
+
const proc = spawn(shell, [...shellArgs, opts.command], {
|
|
555
585
|
cwd: opts.cwd,
|
|
556
586
|
stdio: ["pipe", "pipe", "pipe"],
|
|
557
587
|
env,
|
|
558
|
-
detached:
|
|
588
|
+
detached: process.platform !== "win32",
|
|
559
589
|
});
|
|
560
590
|
|
|
561
591
|
const bg: BgProcess = {
|
|
@@ -621,6 +651,7 @@ function startProcess(opts: StartOptions): BgProcess {
|
|
|
621
651
|
});
|
|
622
652
|
|
|
623
653
|
proc.on("exit", (code, sig) => {
|
|
654
|
+
restoreWindowsVTInput();
|
|
624
655
|
bg.alive = false;
|
|
625
656
|
bg.exitCode = code;
|
|
626
657
|
bg.signal = sig ?? null;
|
|
@@ -686,14 +717,32 @@ function killProcess(id: string, sig: NodeJS.Signals = "SIGTERM"): boolean {
|
|
|
686
717
|
if (!bg) return false;
|
|
687
718
|
if (!bg.alive) return true;
|
|
688
719
|
try {
|
|
689
|
-
if (
|
|
690
|
-
|
|
691
|
-
|
|
692
|
-
|
|
720
|
+
if (process.platform === "win32") {
|
|
721
|
+
// Windows: use taskkill /F /T to force-kill the entire process tree.
|
|
722
|
+
// process.kill(-pid) (Unix process groups) does not work on Windows.
|
|
723
|
+
if (bg.proc.pid) {
|
|
724
|
+
const result = spawnSync("taskkill", ["/F", "/T", "/PID", String(bg.proc.pid)], {
|
|
725
|
+
timeout: 5000,
|
|
726
|
+
encoding: "utf-8",
|
|
727
|
+
});
|
|
728
|
+
if (result.status !== 0 && result.status !== 128) {
|
|
729
|
+
// taskkill failed — try the direct kill as fallback
|
|
730
|
+
bg.proc.kill(sig);
|
|
731
|
+
}
|
|
732
|
+
} else {
|
|
693
733
|
bg.proc.kill(sig);
|
|
694
734
|
}
|
|
695
735
|
} else {
|
|
696
|
-
|
|
736
|
+
// Unix/macOS: kill the process group via negative PID
|
|
737
|
+
if (bg.proc.pid) {
|
|
738
|
+
try {
|
|
739
|
+
process.kill(-bg.proc.pid, sig);
|
|
740
|
+
} catch {
|
|
741
|
+
bg.proc.kill(sig);
|
|
742
|
+
}
|
|
743
|
+
} else {
|
|
744
|
+
bg.proc.kill(sig);
|
|
745
|
+
}
|
|
697
746
|
}
|
|
698
747
|
return true;
|
|
699
748
|
} catch {
|