@astralyn/pi 0.82.0 → 0.82.2
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/CHANGELOG.md +17 -0
- package/LICENSE +21 -0
- package/README.md +14 -3
- package/dist/core/agent-session.d.ts +4 -0
- package/dist/core/agent-session.d.ts.map +1 -1
- package/dist/core/agent-session.js +193 -43
- package/dist/core/agent-session.js.map +1 -1
- package/dist/core/bash-executor.d.ts.map +1 -1
- package/dist/core/bash-executor.js +22 -4
- package/dist/core/bash-executor.js.map +1 -1
- package/dist/core/extensions/loader.d.ts.map +1 -1
- package/dist/core/extensions/loader.js +6 -13
- package/dist/core/extensions/loader.js.map +1 -1
- package/dist/core/tools/bash.d.ts.map +1 -1
- package/dist/core/tools/bash.js +4 -3
- package/dist/core/tools/bash.js.map +1 -1
- package/dist/core/tools/output-accumulator.d.ts +13 -3
- package/dist/core/tools/output-accumulator.d.ts.map +1 -1
- package/dist/core/tools/output-accumulator.js +36 -6
- package/dist/core/tools/output-accumulator.js.map +1 -1
- package/dist/core/tools/tool-definition-wrapper.d.ts +7 -2
- package/dist/core/tools/tool-definition-wrapper.d.ts.map +1 -1
- package/dist/core/tools/tool-definition-wrapper.js.map +1 -1
- package/dist/modes/interactive/interactive-mode.d.ts.map +1 -1
- package/dist/modes/interactive/interactive-mode.js +25 -12
- package/dist/modes/interactive/interactive-mode.js.map +1 -1
- package/dist/utils/output-decoder.d.ts +52 -0
- package/dist/utils/output-decoder.d.ts.map +1 -0
- package/dist/utils/output-decoder.js +134 -0
- package/dist/utils/output-decoder.js.map +1 -0
- package/dist/utils/shell.d.ts +1 -0
- package/dist/utils/shell.d.ts.map +1 -1
- package/dist/utils/shell.js +21 -0
- package/dist/utils/shell.js.map +1 -1
- package/docs/compaction.md +8 -2
- package/docs/containerization.md +1 -1
- package/docs/development.md +32 -35
- package/docs/distribution/README.md +34 -0
- package/docs/distribution/architecture.md +102 -0
- package/docs/distribution/extensions/README.md +17 -0
- package/docs/distribution/extensions/deepwiki.md +62 -0
- package/docs/distribution/extensions/question.md +59 -0
- package/docs/distribution/extensions/rewind.md +85 -0
- package/docs/distribution/extensions/router.md +139 -0
- package/docs/distribution/extensions/statusline.md +110 -0
- package/docs/distribution/extensions/subagent.md +102 -0
- package/docs/distribution/maintenance.md +121 -0
- package/docs/distribution/release.md +85 -0
- package/docs/distribution/themes.md +58 -0
- package/docs/distribution/tool-presentation.md +91 -0
- package/docs/extensions.md +4 -2
- package/docs/index.md +1 -1
- package/docs/rpc.md +2 -2
- package/docs/themes.md +1 -1
- package/examples/extensions/custom-provider-anthropic/package-lock.json +3 -3
- package/examples/extensions/custom-provider-gitlab-duo/test.ts +1 -1
- package/examples/extensions/doom-overlay/doom/build/doom.wasm +0 -0
- package/examples/extensions/doom-overlay/doom/build.sh +0 -0
- package/examples/extensions/sandbox/package-lock.json +1 -1
- package/examples/extensions/with-deps/package-lock.json +2 -2
- package/npm-shrinkwrap.json +3481 -640
- package/package.json +30 -18
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Resolve the fallback encoding for console output on this system.
|
|
3
|
+
*
|
|
4
|
+
* On Windows, cmd.exe builtins, Windows PowerShell, and classic console tools
|
|
5
|
+
* such as ipconfig emit piped output in the legacy OEM code page (GBK on
|
|
6
|
+
* Simplified Chinese systems, Shift_JIS on Japanese systems, and so on)
|
|
7
|
+
* rather than UTF-8. Returns the TextDecoder label for that code page, or
|
|
8
|
+
* null when output should stay UTF-8: non-Windows platforms, UTF-8 systems
|
|
9
|
+
* (code page 65001), and code pages TextDecoder cannot represent.
|
|
10
|
+
*/
|
|
11
|
+
export declare function getConsoleFallbackEncoding(): string | null;
|
|
12
|
+
export interface DecodedChunk {
|
|
13
|
+
/** Decoded text for this push, or the full re-decoded transcript when rewound. */
|
|
14
|
+
text: string;
|
|
15
|
+
/**
|
|
16
|
+
* True when the decoder switched to the fallback encoding. All previously
|
|
17
|
+
* returned text is invalid; `text` replaces the entire transcript.
|
|
18
|
+
*/
|
|
19
|
+
rewound: boolean;
|
|
20
|
+
}
|
|
21
|
+
/**
|
|
22
|
+
* Streaming decoder that assumes UTF-8 and falls back to the system console
|
|
23
|
+
* encoding when the stream turns out not to be UTF-8.
|
|
24
|
+
*
|
|
25
|
+
* Decoding starts as plain streaming UTF-8 — the fast path for virtually all
|
|
26
|
+
* output. When a replacement character appears, the bytes seen so far are
|
|
27
|
+
* checked with isUtf8: if they are genuinely invalid UTF-8, everything is
|
|
28
|
+
* re-decoded with the fallback encoding and the result is reported with
|
|
29
|
+
* `rewound: true` so the caller can rebuild its derived state. If the bytes
|
|
30
|
+
* are valid UTF-8 (the source itself contained replacement characters),
|
|
31
|
+
* detection stops and UTF-8 decoding continues.
|
|
32
|
+
*
|
|
33
|
+
* Raw bytes are retained only while detection is active and only up to
|
|
34
|
+
* `detectionLimitBytes`; larger streams lock to UTF-8 and memory stays flat.
|
|
35
|
+
*/
|
|
36
|
+
export declare class OutputDecoder {
|
|
37
|
+
private decoder;
|
|
38
|
+
private readonly fallbackEncoding;
|
|
39
|
+
private readonly detectionLimitBytes;
|
|
40
|
+
private pending;
|
|
41
|
+
private pendingBytes;
|
|
42
|
+
private detecting;
|
|
43
|
+
constructor(options?: {
|
|
44
|
+
fallbackEncoding?: string | null;
|
|
45
|
+
detectionLimitBytes?: number;
|
|
46
|
+
});
|
|
47
|
+
push(data: Buffer): DecodedChunk;
|
|
48
|
+
flush(): DecodedChunk;
|
|
49
|
+
private tryRewind;
|
|
50
|
+
private stopDetecting;
|
|
51
|
+
}
|
|
52
|
+
//# sourceMappingURL=output-decoder.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"output-decoder.d.ts","sourceRoot":"","sources":["../../src/utils/output-decoder.ts"],"names":[],"mappings":"AAqBA;;;;;;;;;GASG;AACH,wBAAgB,0BAA0B,IAAI,MAAM,GAAG,IAAI,CAK1D;AA4BD,MAAM,WAAW,YAAY;IAC5B,kFAAkF;IAClF,IAAI,EAAE,MAAM,CAAC;IACb;;;OAGG;IACH,OAAO,EAAE,OAAO,CAAC;CACjB;AAID;;;;;;;;;;;;;;GAcG;AACH,qBAAa,aAAa;IACzB,OAAO,CAAC,OAAO,CAAqB;IACpC,OAAO,CAAC,QAAQ,CAAC,gBAAgB,CAAgB;IACjD,OAAO,CAAC,QAAQ,CAAC,mBAAmB,CAAS;IAC7C,OAAO,CAAC,OAAO,CAAgB;IAC/B,OAAO,CAAC,YAAY,CAAK;IACzB,OAAO,CAAC,SAAS,CAAU;IAE3B,YAAY,OAAO,GAAE;QAAE,gBAAgB,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;QAAC,mBAAmB,CAAC,EAAE,MAAM,CAAA;KAAO,EAK3F;IAED,IAAI,CAAC,IAAI,EAAE,MAAM,GAAG,YAAY,CAc/B;IAED,KAAK,IAAI,YAAY,CAOpB;IAED,OAAO,CAAC,SAAS;IAmBjB,OAAO,CAAC,aAAa;CAKrB","sourcesContent":["import { isUtf8 } from \"node:buffer\";\nimport { spawnSync } from \"child_process\";\n\n/**\n * Windows OEM code pages mapped to WHATWG TextDecoder labels. Console\n * programs emit piped output in the OEM code page, so only code pages that\n * TextDecoder can represent are listed; others keep UTF-8 decoding.\n */\nconst OEM_CODE_PAGE_ENCODINGS: Record<string, string> = {\n\t\"866\": \"ibm866\",\n\t\"874\": \"windows-874\",\n\t\"932\": \"shift_jis\",\n\t\"936\": \"gbk\",\n\t\"949\": \"euc-kr\",\n\t\"950\": \"big5\",\n\t\"1251\": \"windows-1251\",\n\t\"1252\": \"windows-1252\",\n};\n\nlet cachedConsoleFallbackEncoding: string | null | undefined;\n\n/**\n * Resolve the fallback encoding for console output on this system.\n *\n * On Windows, cmd.exe builtins, Windows PowerShell, and classic console tools\n * such as ipconfig emit piped output in the legacy OEM code page (GBK on\n * Simplified Chinese systems, Shift_JIS on Japanese systems, and so on)\n * rather than UTF-8. Returns the TextDecoder label for that code page, or\n * null when output should stay UTF-8: non-Windows platforms, UTF-8 systems\n * (code page 65001), and code pages TextDecoder cannot represent.\n */\nexport function getConsoleFallbackEncoding(): string | null {\n\tif (cachedConsoleFallbackEncoding === undefined) {\n\t\tcachedConsoleFallbackEncoding = detectConsoleFallbackEncoding();\n\t}\n\treturn cachedConsoleFallbackEncoding;\n}\n\nfunction detectConsoleFallbackEncoding(): string | null {\n\tif (process.platform !== \"win32\") {\n\t\treturn null;\n\t}\n\ttry {\n\t\tconst result = spawnSync(\n\t\t\t\"reg\",\n\t\t\t[\"query\", \"HKLM\\\\SYSTEM\\\\CurrentControlSet\\\\Control\\\\Nls\\\\CodePage\", \"/v\", \"OEMCP\"],\n\t\t\t{ encoding: \"utf-8\", timeout: 5000, windowsHide: true },\n\t\t);\n\t\tif (result.status !== 0 || !result.stdout) {\n\t\t\treturn null;\n\t\t}\n\t\tconst match = result.stdout.match(/OEMCP\\s+REG_SZ\\s+(\\d+)/);\n\t\tconst encoding = match ? OEM_CODE_PAGE_ENCODINGS[match[1]] : undefined;\n\t\tif (!encoding) {\n\t\t\treturn null;\n\t\t}\n\t\t// Node builds with small-icu lack these decoders; verify availability.\n\t\tnew TextDecoder(encoding);\n\t\treturn encoding;\n\t} catch {\n\t\treturn null;\n\t}\n}\n\nexport interface DecodedChunk {\n\t/** Decoded text for this push, or the full re-decoded transcript when rewound. */\n\ttext: string;\n\t/**\n\t * True when the decoder switched to the fallback encoding. All previously\n\t * returned text is invalid; `text` replaces the entire transcript.\n\t */\n\trewound: boolean;\n}\n\nconst DEFAULT_DETECTION_LIMIT_BYTES = 256 * 1024;\n\n/**\n * Streaming decoder that assumes UTF-8 and falls back to the system console\n * encoding when the stream turns out not to be UTF-8.\n *\n * Decoding starts as plain streaming UTF-8 — the fast path for virtually all\n * output. When a replacement character appears, the bytes seen so far are\n * checked with isUtf8: if they are genuinely invalid UTF-8, everything is\n * re-decoded with the fallback encoding and the result is reported with\n * `rewound: true` so the caller can rebuild its derived state. If the bytes\n * are valid UTF-8 (the source itself contained replacement characters),\n * detection stops and UTF-8 decoding continues.\n *\n * Raw bytes are retained only while detection is active and only up to\n * `detectionLimitBytes`; larger streams lock to UTF-8 and memory stays flat.\n */\nexport class OutputDecoder {\n\tprivate decoder = new TextDecoder();\n\tprivate readonly fallbackEncoding: string | null;\n\tprivate readonly detectionLimitBytes: number;\n\tprivate pending: Buffer[] = [];\n\tprivate pendingBytes = 0;\n\tprivate detecting: boolean;\n\n\tconstructor(options: { fallbackEncoding?: string | null; detectionLimitBytes?: number } = {}) {\n\t\tthis.fallbackEncoding =\n\t\t\toptions.fallbackEncoding === undefined ? getConsoleFallbackEncoding() : options.fallbackEncoding;\n\t\tthis.detectionLimitBytes = options.detectionLimitBytes ?? DEFAULT_DETECTION_LIMIT_BYTES;\n\t\tthis.detecting = this.fallbackEncoding !== null;\n\t}\n\n\tpush(data: Buffer): DecodedChunk {\n\t\tconst text = this.decoder.decode(data, { stream: true });\n\t\tif (!this.detecting) {\n\t\t\treturn { text, rewound: false };\n\t\t}\n\t\tthis.pending.push(data);\n\t\tthis.pendingBytes += data.length;\n\t\tif (text.includes(\"�\")) {\n\t\t\treturn this.tryRewind(text);\n\t\t}\n\t\tif (this.pendingBytes > this.detectionLimitBytes) {\n\t\t\tthis.stopDetecting();\n\t\t}\n\t\treturn { text, rewound: false };\n\t}\n\n\tflush(): DecodedChunk {\n\t\tconst text = this.decoder.decode();\n\t\tif (this.detecting && text.includes(\"�\")) {\n\t\t\treturn this.tryRewind(text);\n\t\t}\n\t\tthis.stopDetecting();\n\t\treturn { text, rewound: false };\n\t}\n\n\tprivate tryRewind(utf8Text: string): DecodedChunk {\n\t\tconst raw = Buffer.concat(this.pending);\n\t\tif (isUtf8(raw)) {\n\t\t\t// The replacement characters exist in the source bytes themselves;\n\t\t\t// the stream is valid UTF-8, so stop watching and keep it.\n\t\t\tthis.stopDetecting();\n\t\t\treturn { text: utf8Text, rewound: false };\n\t\t}\n\t\ttry {\n\t\t\tthis.decoder = new TextDecoder(this.fallbackEncoding as string);\n\t\t} catch {\n\t\t\tthis.stopDetecting();\n\t\t\treturn { text: utf8Text, rewound: false };\n\t\t}\n\t\tconst text = this.decoder.decode(raw, { stream: true });\n\t\tthis.stopDetecting();\n\t\treturn { text, rewound: true };\n\t}\n\n\tprivate stopDetecting(): void {\n\t\tthis.detecting = false;\n\t\tthis.pending = [];\n\t\tthis.pendingBytes = 0;\n\t}\n}\n"]}
|
|
@@ -0,0 +1,134 @@
|
|
|
1
|
+
import { isUtf8 } from "node:buffer";
|
|
2
|
+
import { spawnSync } from "child_process";
|
|
3
|
+
/**
|
|
4
|
+
* Windows OEM code pages mapped to WHATWG TextDecoder labels. Console
|
|
5
|
+
* programs emit piped output in the OEM code page, so only code pages that
|
|
6
|
+
* TextDecoder can represent are listed; others keep UTF-8 decoding.
|
|
7
|
+
*/
|
|
8
|
+
const OEM_CODE_PAGE_ENCODINGS = {
|
|
9
|
+
"866": "ibm866",
|
|
10
|
+
"874": "windows-874",
|
|
11
|
+
"932": "shift_jis",
|
|
12
|
+
"936": "gbk",
|
|
13
|
+
"949": "euc-kr",
|
|
14
|
+
"950": "big5",
|
|
15
|
+
"1251": "windows-1251",
|
|
16
|
+
"1252": "windows-1252",
|
|
17
|
+
};
|
|
18
|
+
let cachedConsoleFallbackEncoding;
|
|
19
|
+
/**
|
|
20
|
+
* Resolve the fallback encoding for console output on this system.
|
|
21
|
+
*
|
|
22
|
+
* On Windows, cmd.exe builtins, Windows PowerShell, and classic console tools
|
|
23
|
+
* such as ipconfig emit piped output in the legacy OEM code page (GBK on
|
|
24
|
+
* Simplified Chinese systems, Shift_JIS on Japanese systems, and so on)
|
|
25
|
+
* rather than UTF-8. Returns the TextDecoder label for that code page, or
|
|
26
|
+
* null when output should stay UTF-8: non-Windows platforms, UTF-8 systems
|
|
27
|
+
* (code page 65001), and code pages TextDecoder cannot represent.
|
|
28
|
+
*/
|
|
29
|
+
export function getConsoleFallbackEncoding() {
|
|
30
|
+
if (cachedConsoleFallbackEncoding === undefined) {
|
|
31
|
+
cachedConsoleFallbackEncoding = detectConsoleFallbackEncoding();
|
|
32
|
+
}
|
|
33
|
+
return cachedConsoleFallbackEncoding;
|
|
34
|
+
}
|
|
35
|
+
function detectConsoleFallbackEncoding() {
|
|
36
|
+
if (process.platform !== "win32") {
|
|
37
|
+
return null;
|
|
38
|
+
}
|
|
39
|
+
try {
|
|
40
|
+
const result = spawnSync("reg", ["query", "HKLM\\SYSTEM\\CurrentControlSet\\Control\\Nls\\CodePage", "/v", "OEMCP"], { encoding: "utf-8", timeout: 5000, windowsHide: true });
|
|
41
|
+
if (result.status !== 0 || !result.stdout) {
|
|
42
|
+
return null;
|
|
43
|
+
}
|
|
44
|
+
const match = result.stdout.match(/OEMCP\s+REG_SZ\s+(\d+)/);
|
|
45
|
+
const encoding = match ? OEM_CODE_PAGE_ENCODINGS[match[1]] : undefined;
|
|
46
|
+
if (!encoding) {
|
|
47
|
+
return null;
|
|
48
|
+
}
|
|
49
|
+
// Node builds with small-icu lack these decoders; verify availability.
|
|
50
|
+
new TextDecoder(encoding);
|
|
51
|
+
return encoding;
|
|
52
|
+
}
|
|
53
|
+
catch {
|
|
54
|
+
return null;
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
const DEFAULT_DETECTION_LIMIT_BYTES = 256 * 1024;
|
|
58
|
+
/**
|
|
59
|
+
* Streaming decoder that assumes UTF-8 and falls back to the system console
|
|
60
|
+
* encoding when the stream turns out not to be UTF-8.
|
|
61
|
+
*
|
|
62
|
+
* Decoding starts as plain streaming UTF-8 — the fast path for virtually all
|
|
63
|
+
* output. When a replacement character appears, the bytes seen so far are
|
|
64
|
+
* checked with isUtf8: if they are genuinely invalid UTF-8, everything is
|
|
65
|
+
* re-decoded with the fallback encoding and the result is reported with
|
|
66
|
+
* `rewound: true` so the caller can rebuild its derived state. If the bytes
|
|
67
|
+
* are valid UTF-8 (the source itself contained replacement characters),
|
|
68
|
+
* detection stops and UTF-8 decoding continues.
|
|
69
|
+
*
|
|
70
|
+
* Raw bytes are retained only while detection is active and only up to
|
|
71
|
+
* `detectionLimitBytes`; larger streams lock to UTF-8 and memory stays flat.
|
|
72
|
+
*/
|
|
73
|
+
export class OutputDecoder {
|
|
74
|
+
decoder = new TextDecoder();
|
|
75
|
+
fallbackEncoding;
|
|
76
|
+
detectionLimitBytes;
|
|
77
|
+
pending = [];
|
|
78
|
+
pendingBytes = 0;
|
|
79
|
+
detecting;
|
|
80
|
+
constructor(options = {}) {
|
|
81
|
+
this.fallbackEncoding =
|
|
82
|
+
options.fallbackEncoding === undefined ? getConsoleFallbackEncoding() : options.fallbackEncoding;
|
|
83
|
+
this.detectionLimitBytes = options.detectionLimitBytes ?? DEFAULT_DETECTION_LIMIT_BYTES;
|
|
84
|
+
this.detecting = this.fallbackEncoding !== null;
|
|
85
|
+
}
|
|
86
|
+
push(data) {
|
|
87
|
+
const text = this.decoder.decode(data, { stream: true });
|
|
88
|
+
if (!this.detecting) {
|
|
89
|
+
return { text, rewound: false };
|
|
90
|
+
}
|
|
91
|
+
this.pending.push(data);
|
|
92
|
+
this.pendingBytes += data.length;
|
|
93
|
+
if (text.includes("�")) {
|
|
94
|
+
return this.tryRewind(text);
|
|
95
|
+
}
|
|
96
|
+
if (this.pendingBytes > this.detectionLimitBytes) {
|
|
97
|
+
this.stopDetecting();
|
|
98
|
+
}
|
|
99
|
+
return { text, rewound: false };
|
|
100
|
+
}
|
|
101
|
+
flush() {
|
|
102
|
+
const text = this.decoder.decode();
|
|
103
|
+
if (this.detecting && text.includes("�")) {
|
|
104
|
+
return this.tryRewind(text);
|
|
105
|
+
}
|
|
106
|
+
this.stopDetecting();
|
|
107
|
+
return { text, rewound: false };
|
|
108
|
+
}
|
|
109
|
+
tryRewind(utf8Text) {
|
|
110
|
+
const raw = Buffer.concat(this.pending);
|
|
111
|
+
if (isUtf8(raw)) {
|
|
112
|
+
// The replacement characters exist in the source bytes themselves;
|
|
113
|
+
// the stream is valid UTF-8, so stop watching and keep it.
|
|
114
|
+
this.stopDetecting();
|
|
115
|
+
return { text: utf8Text, rewound: false };
|
|
116
|
+
}
|
|
117
|
+
try {
|
|
118
|
+
this.decoder = new TextDecoder(this.fallbackEncoding);
|
|
119
|
+
}
|
|
120
|
+
catch {
|
|
121
|
+
this.stopDetecting();
|
|
122
|
+
return { text: utf8Text, rewound: false };
|
|
123
|
+
}
|
|
124
|
+
const text = this.decoder.decode(raw, { stream: true });
|
|
125
|
+
this.stopDetecting();
|
|
126
|
+
return { text, rewound: true };
|
|
127
|
+
}
|
|
128
|
+
stopDetecting() {
|
|
129
|
+
this.detecting = false;
|
|
130
|
+
this.pending = [];
|
|
131
|
+
this.pendingBytes = 0;
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
//# sourceMappingURL=output-decoder.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"output-decoder.js","sourceRoot":"","sources":["../../src/utils/output-decoder.ts"],"names":[],"mappings":"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","sourcesContent":["import { isUtf8 } from \"node:buffer\";\nimport { spawnSync } from \"child_process\";\n\n/**\n * Windows OEM code pages mapped to WHATWG TextDecoder labels. Console\n * programs emit piped output in the OEM code page, so only code pages that\n * TextDecoder can represent are listed; others keep UTF-8 decoding.\n */\nconst OEM_CODE_PAGE_ENCODINGS: Record<string, string> = {\n\t\"866\": \"ibm866\",\n\t\"874\": \"windows-874\",\n\t\"932\": \"shift_jis\",\n\t\"936\": \"gbk\",\n\t\"949\": \"euc-kr\",\n\t\"950\": \"big5\",\n\t\"1251\": \"windows-1251\",\n\t\"1252\": \"windows-1252\",\n};\n\nlet cachedConsoleFallbackEncoding: string | null | undefined;\n\n/**\n * Resolve the fallback encoding for console output on this system.\n *\n * On Windows, cmd.exe builtins, Windows PowerShell, and classic console tools\n * such as ipconfig emit piped output in the legacy OEM code page (GBK on\n * Simplified Chinese systems, Shift_JIS on Japanese systems, and so on)\n * rather than UTF-8. Returns the TextDecoder label for that code page, or\n * null when output should stay UTF-8: non-Windows platforms, UTF-8 systems\n * (code page 65001), and code pages TextDecoder cannot represent.\n */\nexport function getConsoleFallbackEncoding(): string | null {\n\tif (cachedConsoleFallbackEncoding === undefined) {\n\t\tcachedConsoleFallbackEncoding = detectConsoleFallbackEncoding();\n\t}\n\treturn cachedConsoleFallbackEncoding;\n}\n\nfunction detectConsoleFallbackEncoding(): string | null {\n\tif (process.platform !== \"win32\") {\n\t\treturn null;\n\t}\n\ttry {\n\t\tconst result = spawnSync(\n\t\t\t\"reg\",\n\t\t\t[\"query\", \"HKLM\\\\SYSTEM\\\\CurrentControlSet\\\\Control\\\\Nls\\\\CodePage\", \"/v\", \"OEMCP\"],\n\t\t\t{ encoding: \"utf-8\", timeout: 5000, windowsHide: true },\n\t\t);\n\t\tif (result.status !== 0 || !result.stdout) {\n\t\t\treturn null;\n\t\t}\n\t\tconst match = result.stdout.match(/OEMCP\\s+REG_SZ\\s+(\\d+)/);\n\t\tconst encoding = match ? OEM_CODE_PAGE_ENCODINGS[match[1]] : undefined;\n\t\tif (!encoding) {\n\t\t\treturn null;\n\t\t}\n\t\t// Node builds with small-icu lack these decoders; verify availability.\n\t\tnew TextDecoder(encoding);\n\t\treturn encoding;\n\t} catch {\n\t\treturn null;\n\t}\n}\n\nexport interface DecodedChunk {\n\t/** Decoded text for this push, or the full re-decoded transcript when rewound. */\n\ttext: string;\n\t/**\n\t * True when the decoder switched to the fallback encoding. All previously\n\t * returned text is invalid; `text` replaces the entire transcript.\n\t */\n\trewound: boolean;\n}\n\nconst DEFAULT_DETECTION_LIMIT_BYTES = 256 * 1024;\n\n/**\n * Streaming decoder that assumes UTF-8 and falls back to the system console\n * encoding when the stream turns out not to be UTF-8.\n *\n * Decoding starts as plain streaming UTF-8 — the fast path for virtually all\n * output. When a replacement character appears, the bytes seen so far are\n * checked with isUtf8: if they are genuinely invalid UTF-8, everything is\n * re-decoded with the fallback encoding and the result is reported with\n * `rewound: true` so the caller can rebuild its derived state. If the bytes\n * are valid UTF-8 (the source itself contained replacement characters),\n * detection stops and UTF-8 decoding continues.\n *\n * Raw bytes are retained only while detection is active and only up to\n * `detectionLimitBytes`; larger streams lock to UTF-8 and memory stays flat.\n */\nexport class OutputDecoder {\n\tprivate decoder = new TextDecoder();\n\tprivate readonly fallbackEncoding: string | null;\n\tprivate readonly detectionLimitBytes: number;\n\tprivate pending: Buffer[] = [];\n\tprivate pendingBytes = 0;\n\tprivate detecting: boolean;\n\n\tconstructor(options: { fallbackEncoding?: string | null; detectionLimitBytes?: number } = {}) {\n\t\tthis.fallbackEncoding =\n\t\t\toptions.fallbackEncoding === undefined ? getConsoleFallbackEncoding() : options.fallbackEncoding;\n\t\tthis.detectionLimitBytes = options.detectionLimitBytes ?? DEFAULT_DETECTION_LIMIT_BYTES;\n\t\tthis.detecting = this.fallbackEncoding !== null;\n\t}\n\n\tpush(data: Buffer): DecodedChunk {\n\t\tconst text = this.decoder.decode(data, { stream: true });\n\t\tif (!this.detecting) {\n\t\t\treturn { text, rewound: false };\n\t\t}\n\t\tthis.pending.push(data);\n\t\tthis.pendingBytes += data.length;\n\t\tif (text.includes(\"�\")) {\n\t\t\treturn this.tryRewind(text);\n\t\t}\n\t\tif (this.pendingBytes > this.detectionLimitBytes) {\n\t\t\tthis.stopDetecting();\n\t\t}\n\t\treturn { text, rewound: false };\n\t}\n\n\tflush(): DecodedChunk {\n\t\tconst text = this.decoder.decode();\n\t\tif (this.detecting && text.includes(\"�\")) {\n\t\t\treturn this.tryRewind(text);\n\t\t}\n\t\tthis.stopDetecting();\n\t\treturn { text, rewound: false };\n\t}\n\n\tprivate tryRewind(utf8Text: string): DecodedChunk {\n\t\tconst raw = Buffer.concat(this.pending);\n\t\tif (isUtf8(raw)) {\n\t\t\t// The replacement characters exist in the source bytes themselves;\n\t\t\t// the stream is valid UTF-8, so stop watching and keep it.\n\t\t\tthis.stopDetecting();\n\t\t\treturn { text: utf8Text, rewound: false };\n\t\t}\n\t\ttry {\n\t\t\tthis.decoder = new TextDecoder(this.fallbackEncoding as string);\n\t\t} catch {\n\t\t\tthis.stopDetecting();\n\t\t\treturn { text: utf8Text, rewound: false };\n\t\t}\n\t\tconst text = this.decoder.decode(raw, { stream: true });\n\t\tthis.stopDetecting();\n\t\treturn { text, rewound: true };\n\t}\n\n\tprivate stopDetecting(): void {\n\t\tthis.detecting = false;\n\t\tthis.pending = [];\n\t\tthis.pendingBytes = 0;\n\t}\n}\n"]}
|
package/dist/utils/shell.d.ts
CHANGED
|
@@ -12,6 +12,7 @@ export interface ShellConfig {
|
|
|
12
12
|
*/
|
|
13
13
|
export declare function getShellConfig(customShellPath?: string): ShellConfig;
|
|
14
14
|
export declare function getShellEnv(): NodeJS.ProcessEnv;
|
|
15
|
+
export declare function rewriteCmdNulRedirects(command: string): string;
|
|
15
16
|
/**
|
|
16
17
|
* Sanitize binary output for display/storage.
|
|
17
18
|
* Removes characters that crash string-width or cause display issues:
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"shell.d.ts","sourceRoot":"","sources":["../../src/utils/shell.ts"],"names":[],"mappings":"AAKA,MAAM,WAAW,WAAW;IAC3B,KAAK,EAAE,MAAM,CAAC;IACd,IAAI,EAAE,MAAM,EAAE,CAAC;IACf,gBAAgB,CAAC,EAAE,MAAM,GAAG,OAAO,CAAC;CACpC;AAkDD;;;;;;GAMG;AACH,wBAAgB,cAAc,CAAC,eAAe,CAAC,EAAE,MAAM,GAAG,WAAW,CAqDpE;AAED,wBAAgB,WAAW,IAAI,MAAM,CAAC,UAAU,CAY/C;AAED;;;;;;;GAOG;AACH,wBAAgB,oBAAoB,CAAC,GAAG,EAAE,MAAM,GAAG,MAAM,CA8BxD;AAQD,wBAAgB,qBAAqB,CAAC,GAAG,EAAE,MAAM,GAAG,IAAI,CAEvD;AAED,wBAAgB,uBAAuB,CAAC,GAAG,EAAE,MAAM,GAAG,IAAI,CAEzD;AAED,wBAAgB,2BAA2B,IAAI,IAAI,CAKlD;AAED;;GAEG;AACH,wBAAgB,eAAe,CAAC,GAAG,EAAE,MAAM,GAAG,IAAI,CAyBjD","sourcesContent":["import { existsSync } from \"node:fs\";\nimport { delimiter } from \"node:path\";\nimport { spawn, spawnSync } from \"child_process\";\nimport { getBinDir } from \"../config.ts\";\n\nexport interface ShellConfig {\n\tshell: string;\n\targs: string[];\n\tcommandTransport?: \"argv\" | \"stdin\";\n}\n\n/**\n * Find bash executable on PATH (cross-platform)\n */\nfunction isLegacyWslBashPath(path: string): boolean {\n\tconst normalized = path.replace(/\\//g, \"\\\\\").toLowerCase();\n\treturn /^[a-z]:\\\\windows\\\\(?:system32|sysnative)\\\\bash\\.exe$/.test(normalized);\n}\n\nfunction getBashShellConfig(shell: string): ShellConfig {\n\treturn isLegacyWslBashPath(shell) ? { shell, args: [\"-s\"], commandTransport: \"stdin\" } : { shell, args: [\"-c\"] };\n}\n\nfunction findBashOnPath(): string | null {\n\tif (process.platform === \"win32\") {\n\t\t// Windows: Use 'where' and verify file exists (where can return non-existent paths)\n\t\ttry {\n\t\t\tconst result = spawnSync(\"where\", [\"bash.exe\"], {\n\t\t\t\tencoding: \"utf-8\",\n\t\t\t\ttimeout: 5000,\n\t\t\t\twindowsHide: true,\n\t\t\t});\n\t\t\tif (result.status === 0 && result.stdout) {\n\t\t\t\tconst firstMatch = result.stdout.trim().split(/\\r?\\n/)[0];\n\t\t\t\tif (firstMatch && existsSync(firstMatch)) {\n\t\t\t\t\treturn firstMatch;\n\t\t\t\t}\n\t\t\t}\n\t\t} catch {\n\t\t\t// Ignore errors\n\t\t}\n\t\treturn null;\n\t}\n\n\t// Unix: Use 'which' and trust its output (handles Termux and special filesystems)\n\ttry {\n\t\tconst result = spawnSync(\"which\", [\"bash\"], { encoding: \"utf-8\", timeout: 5000 });\n\t\tif (result.status === 0 && result.stdout) {\n\t\t\tconst firstMatch = result.stdout.trim().split(/\\r?\\n/)[0];\n\t\t\tif (firstMatch) {\n\t\t\t\treturn firstMatch;\n\t\t\t}\n\t\t}\n\t} catch {\n\t\t// Ignore errors\n\t}\n\treturn null;\n}\n\n/**\n * Resolve shell configuration based on platform and an optional explicit shell path.\n * Resolution order:\n * 1. User-specified shellPath\n * 2. On Windows: Git Bash in known locations, then bash on PATH\n * 3. On Unix: /bin/bash, then bash on PATH, then fallback to sh\n */\nexport function getShellConfig(customShellPath?: string): ShellConfig {\n\t// 1. Check user-specified shell path\n\tif (customShellPath) {\n\t\tif (existsSync(customShellPath)) {\n\t\t\treturn getBashShellConfig(customShellPath);\n\t\t}\n\t\tthrow new Error(`Custom shell path not found: ${customShellPath}`);\n\t}\n\n\tif (process.platform === \"win32\") {\n\t\t// 2. Try Git Bash in known locations\n\t\tconst paths: string[] = [];\n\t\tconst programFiles = process.env.ProgramFiles;\n\t\tif (programFiles) {\n\t\t\tpaths.push(`${programFiles}\\\\Git\\\\bin\\\\bash.exe`);\n\t\t}\n\t\tconst programFilesX86 = process.env[\"ProgramFiles(x86)\"];\n\t\tif (programFilesX86) {\n\t\t\tpaths.push(`${programFilesX86}\\\\Git\\\\bin\\\\bash.exe`);\n\t\t}\n\n\t\tfor (const path of paths) {\n\t\t\tif (existsSync(path)) {\n\t\t\t\treturn getBashShellConfig(path);\n\t\t\t}\n\t\t}\n\n\t\t// 3. Fallback: search bash.exe on PATH (Cygwin, MSYS2, WSL, etc.)\n\t\tconst bashOnPath = findBashOnPath();\n\t\tif (bashOnPath) {\n\t\t\treturn getBashShellConfig(bashOnPath);\n\t\t}\n\n\t\tthrow new Error(\n\t\t\t`No bash shell found. Options:\\n` +\n\t\t\t\t` 1. Install Git for Windows: https://git-scm.com/download/win\\n` +\n\t\t\t\t` 2. Add your bash to PATH (Cygwin, MSYS2, etc.)\\n` +\n\t\t\t\t\" 3. Set shellPath in settings.json\\n\\n\" +\n\t\t\t\t`Searched Git Bash in:\\n${paths.map((p) => ` ${p}`).join(\"\\n\")}`,\n\t\t);\n\t}\n\n\t// Unix: try /bin/bash, then bash on PATH, then fallback to sh\n\tif (existsSync(\"/bin/bash\")) {\n\t\treturn getBashShellConfig(\"/bin/bash\");\n\t}\n\n\tconst bashOnPath = findBashOnPath();\n\tif (bashOnPath) {\n\t\treturn getBashShellConfig(bashOnPath);\n\t}\n\n\treturn { shell: \"sh\", args: [\"-c\"] };\n}\n\nexport function getShellEnv(): NodeJS.ProcessEnv {\n\tconst binDir = getBinDir();\n\tconst pathKey = Object.keys(process.env).find((key) => key.toLowerCase() === \"path\") ?? \"PATH\";\n\tconst currentPath = process.env[pathKey] ?? \"\";\n\tconst pathEntries = currentPath.split(delimiter).filter(Boolean);\n\tconst hasBinDir = pathEntries.includes(binDir);\n\tconst updatedPath = hasBinDir ? currentPath : [binDir, currentPath].filter(Boolean).join(delimiter);\n\n\treturn {\n\t\t...process.env,\n\t\t[pathKey]: updatedPath,\n\t};\n}\n\n/**\n * Sanitize binary output for display/storage.\n * Removes characters that crash string-width or cause display issues:\n * - Control characters (except tab, newline, carriage return)\n * - Lone surrogates\n * - Unicode Format characters (crash string-width due to a bug)\n * - Characters with undefined code points\n */\nexport function sanitizeBinaryOutput(str: string): string {\n\t// Use Array.from to properly iterate over code points (not code units)\n\t// This handles surrogate pairs correctly and catches edge cases where\n\t// codePointAt() might return undefined\n\treturn Array.from(str)\n\t\t.filter((char) => {\n\t\t\t// Filter out characters that cause string-width to crash\n\t\t\t// This includes:\n\t\t\t// - Unicode format characters\n\t\t\t// - Lone surrogates (already filtered by Array.from)\n\t\t\t// - Control chars except \\t \\n \\r\n\t\t\t// - Characters with undefined code points\n\n\t\t\tconst code = char.codePointAt(0);\n\n\t\t\t// Skip if code point is undefined (edge case with invalid strings)\n\t\t\tif (code === undefined) return false;\n\n\t\t\t// Allow tab, newline, carriage return\n\t\t\tif (code === 0x09 || code === 0x0a || code === 0x0d) return true;\n\n\t\t\t// Filter out control characters (0x00-0x1F, except 0x09, 0x0a, 0x0x0d)\n\t\t\tif (code <= 0x1f) return false;\n\n\t\t\t// Filter out Unicode format characters\n\t\t\tif (code >= 0xfff9 && code <= 0xfffb) return false;\n\n\t\t\treturn true;\n\t\t})\n\t\t.join(\"\");\n}\n\n/**\n * Detached child processes must be tracked so they can be killed on parent\n * shutdown signals (SIGHUP/SIGTERM).\n */\nconst trackedDetachedChildPids = new Set<number>();\n\nexport function trackDetachedChildPid(pid: number): void {\n\ttrackedDetachedChildPids.add(pid);\n}\n\nexport function untrackDetachedChildPid(pid: number): void {\n\ttrackedDetachedChildPids.delete(pid);\n}\n\nexport function killTrackedDetachedChildren(): void {\n\tfor (const pid of trackedDetachedChildPids) {\n\t\tkillProcessTree(pid);\n\t}\n\ttrackedDetachedChildPids.clear();\n}\n\n/**\n * Kill a process and all its children (cross-platform)\n */\nexport function killProcessTree(pid: number): void {\n\tif (process.platform === \"win32\") {\n\t\t// Use taskkill on Windows to kill process tree\n\t\ttry {\n\t\t\tspawn(\"taskkill\", [\"/F\", \"/T\", \"/PID\", String(pid)], {\n\t\t\t\tstdio: \"ignore\",\n\t\t\t\tdetached: true,\n\t\t\t\twindowsHide: true,\n\t\t\t});\n\t\t} catch {\n\t\t\t// Ignore errors if taskkill fails\n\t\t}\n\t} else {\n\t\t// Use SIGKILL on Unix/Linux/Mac\n\t\ttry {\n\t\t\tprocess.kill(-pid, \"SIGKILL\");\n\t\t} catch {\n\t\t\t// Fallback to killing just the child if process group kill fails\n\t\t\ttry {\n\t\t\t\tprocess.kill(pid, \"SIGKILL\");\n\t\t\t} catch {\n\t\t\t\t// Process already dead\n\t\t\t}\n\t\t}\n\t}\n}\n"]}
|
|
1
|
+
{"version":3,"file":"shell.d.ts","sourceRoot":"","sources":["../../src/utils/shell.ts"],"names":[],"mappings":"AAKA,MAAM,WAAW,WAAW;IAC3B,KAAK,EAAE,MAAM,CAAC;IACd,IAAI,EAAE,MAAM,EAAE,CAAC;IACf,gBAAgB,CAAC,EAAE,MAAM,GAAG,OAAO,CAAC;CACpC;AAkDD;;;;;;GAMG;AACH,wBAAgB,cAAc,CAAC,eAAe,CAAC,EAAE,MAAM,GAAG,WAAW,CAqDpE;AAED,wBAAgB,WAAW,IAAI,MAAM,CAAC,UAAU,CAY/C;AAqBD,wBAAgB,sBAAsB,CAAC,OAAO,EAAE,MAAM,GAAG,MAAM,CAE9D;AAED;;;;;;;GAOG;AACH,wBAAgB,oBAAoB,CAAC,GAAG,EAAE,MAAM,GAAG,MAAM,CA8BxD;AAQD,wBAAgB,qBAAqB,CAAC,GAAG,EAAE,MAAM,GAAG,IAAI,CAEvD;AAED,wBAAgB,uBAAuB,CAAC,GAAG,EAAE,MAAM,GAAG,IAAI,CAEzD;AAED,wBAAgB,2BAA2B,IAAI,IAAI,CAKlD;AAED;;GAEG;AACH,wBAAgB,eAAe,CAAC,GAAG,EAAE,MAAM,GAAG,IAAI,CAyBjD","sourcesContent":["import { existsSync } from \"node:fs\";\nimport { delimiter } from \"node:path\";\nimport { spawn, spawnSync } from \"child_process\";\nimport { getBinDir } from \"../config.ts\";\n\nexport interface ShellConfig {\n\tshell: string;\n\targs: string[];\n\tcommandTransport?: \"argv\" | \"stdin\";\n}\n\n/**\n * Find bash executable on PATH (cross-platform)\n */\nfunction isLegacyWslBashPath(path: string): boolean {\n\tconst normalized = path.replace(/\\//g, \"\\\\\").toLowerCase();\n\treturn /^[a-z]:\\\\windows\\\\(?:system32|sysnative)\\\\bash\\.exe$/.test(normalized);\n}\n\nfunction getBashShellConfig(shell: string): ShellConfig {\n\treturn isLegacyWslBashPath(shell) ? { shell, args: [\"-s\"], commandTransport: \"stdin\" } : { shell, args: [\"-c\"] };\n}\n\nfunction findBashOnPath(): string | null {\n\tif (process.platform === \"win32\") {\n\t\t// Windows: Use 'where' and verify file exists (where can return non-existent paths)\n\t\ttry {\n\t\t\tconst result = spawnSync(\"where\", [\"bash.exe\"], {\n\t\t\t\tencoding: \"utf-8\",\n\t\t\t\ttimeout: 5000,\n\t\t\t\twindowsHide: true,\n\t\t\t});\n\t\t\tif (result.status === 0 && result.stdout) {\n\t\t\t\tconst firstMatch = result.stdout.trim().split(/\\r?\\n/)[0];\n\t\t\t\tif (firstMatch && existsSync(firstMatch)) {\n\t\t\t\t\treturn firstMatch;\n\t\t\t\t}\n\t\t\t}\n\t\t} catch {\n\t\t\t// Ignore errors\n\t\t}\n\t\treturn null;\n\t}\n\n\t// Unix: Use 'which' and trust its output (handles Termux and special filesystems)\n\ttry {\n\t\tconst result = spawnSync(\"which\", [\"bash\"], { encoding: \"utf-8\", timeout: 5000 });\n\t\tif (result.status === 0 && result.stdout) {\n\t\t\tconst firstMatch = result.stdout.trim().split(/\\r?\\n/)[0];\n\t\t\tif (firstMatch) {\n\t\t\t\treturn firstMatch;\n\t\t\t}\n\t\t}\n\t} catch {\n\t\t// Ignore errors\n\t}\n\treturn null;\n}\n\n/**\n * Resolve shell configuration based on platform and an optional explicit shell path.\n * Resolution order:\n * 1. User-specified shellPath\n * 2. On Windows: Git Bash in known locations, then bash on PATH\n * 3. On Unix: /bin/bash, then bash on PATH, then fallback to sh\n */\nexport function getShellConfig(customShellPath?: string): ShellConfig {\n\t// 1. Check user-specified shell path\n\tif (customShellPath) {\n\t\tif (existsSync(customShellPath)) {\n\t\t\treturn getBashShellConfig(customShellPath);\n\t\t}\n\t\tthrow new Error(`Custom shell path not found: ${customShellPath}`);\n\t}\n\n\tif (process.platform === \"win32\") {\n\t\t// 2. Try Git Bash in known locations\n\t\tconst paths: string[] = [];\n\t\tconst programFiles = process.env.ProgramFiles;\n\t\tif (programFiles) {\n\t\t\tpaths.push(`${programFiles}\\\\Git\\\\bin\\\\bash.exe`);\n\t\t}\n\t\tconst programFilesX86 = process.env[\"ProgramFiles(x86)\"];\n\t\tif (programFilesX86) {\n\t\t\tpaths.push(`${programFilesX86}\\\\Git\\\\bin\\\\bash.exe`);\n\t\t}\n\n\t\tfor (const path of paths) {\n\t\t\tif (existsSync(path)) {\n\t\t\t\treturn getBashShellConfig(path);\n\t\t\t}\n\t\t}\n\n\t\t// 3. Fallback: search bash.exe on PATH (Cygwin, MSYS2, WSL, etc.)\n\t\tconst bashOnPath = findBashOnPath();\n\t\tif (bashOnPath) {\n\t\t\treturn getBashShellConfig(bashOnPath);\n\t\t}\n\n\t\tthrow new Error(\n\t\t\t`No bash shell found. Options:\\n` +\n\t\t\t\t` 1. Install Git for Windows: https://git-scm.com/download/win\\n` +\n\t\t\t\t` 2. Add your bash to PATH (Cygwin, MSYS2, etc.)\\n` +\n\t\t\t\t\" 3. Set shellPath in settings.json\\n\\n\" +\n\t\t\t\t`Searched Git Bash in:\\n${paths.map((p) => ` ${p}`).join(\"\\n\")}`,\n\t\t);\n\t}\n\n\t// Unix: try /bin/bash, then bash on PATH, then fallback to sh\n\tif (existsSync(\"/bin/bash\")) {\n\t\treturn getBashShellConfig(\"/bin/bash\");\n\t}\n\n\tconst bashOnPath = findBashOnPath();\n\tif (bashOnPath) {\n\t\treturn getBashShellConfig(bashOnPath);\n\t}\n\n\treturn { shell: \"sh\", args: [\"-c\"] };\n}\n\nexport function getShellEnv(): NodeJS.ProcessEnv {\n\tconst binDir = getBinDir();\n\tconst pathKey = Object.keys(process.env).find((key) => key.toLowerCase() === \"path\") ?? \"PATH\";\n\tconst currentPath = process.env[pathKey] ?? \"\";\n\tconst pathEntries = currentPath.split(delimiter).filter(Boolean);\n\tconst hasBinDir = pathEntries.includes(binDir);\n\tconst updatedPath = hasBinDir ? currentPath : [binDir, currentPath].filter(Boolean).join(delimiter);\n\n\treturn {\n\t\t...process.env,\n\t\t[pathKey]: updatedPath,\n\t};\n}\n\n/**\n * Rewrite Windows CMD-style `nul` redirects to POSIX `/dev/null`.\n *\n * Models occasionally emit CMD syntax such as `command 2>nul` even though the\n * shell resolved by `getShellConfig` is always POSIX. A POSIX shell treats\n * `nul` as an ordinary file name, so the redirect creates a literal `nul`\n * file. On Windows that name is a reserved device name: Explorer and most\n * tools cannot delete the file, and it breaks `git add .` and `git clone`\n * for the affected directory.\n *\n * Matches `>nul`, `> NUL`, `2>nul`, `&>nul`, and `>>nul` case-insensitively\n * while leaving longer tokens such as `>null` or `>nul.txt` untouched. The\n * regex does not parse shell quoting, so a quoted literal such as\n * `echo \"2>nul crashes\"` is also rewritten when `nul` is followed by\n * whitespace; that is accepted because the pattern is rare and the rewritten\n * string stays harmless.\n */\nconst CMD_NUL_REDIRECT_REGEX = /(\\d?&?>+\\s*)[Nn][Uu][Ll](?=\\s|$|[|&;)\\n])/g;\n\nexport function rewriteCmdNulRedirects(command: string): string {\n\treturn command.replace(CMD_NUL_REDIRECT_REGEX, \"$1/dev/null\");\n}\n\n/**\n * Sanitize binary output for display/storage.\n * Removes characters that crash string-width or cause display issues:\n * - Control characters (except tab, newline, carriage return)\n * - Lone surrogates\n * - Unicode Format characters (crash string-width due to a bug)\n * - Characters with undefined code points\n */\nexport function sanitizeBinaryOutput(str: string): string {\n\t// Use Array.from to properly iterate over code points (not code units)\n\t// This handles surrogate pairs correctly and catches edge cases where\n\t// codePointAt() might return undefined\n\treturn Array.from(str)\n\t\t.filter((char) => {\n\t\t\t// Filter out characters that cause string-width to crash\n\t\t\t// This includes:\n\t\t\t// - Unicode format characters\n\t\t\t// - Lone surrogates (already filtered by Array.from)\n\t\t\t// - Control chars except \\t \\n \\r\n\t\t\t// - Characters with undefined code points\n\n\t\t\tconst code = char.codePointAt(0);\n\n\t\t\t// Skip if code point is undefined (edge case with invalid strings)\n\t\t\tif (code === undefined) return false;\n\n\t\t\t// Allow tab, newline, carriage return\n\t\t\tif (code === 0x09 || code === 0x0a || code === 0x0d) return true;\n\n\t\t\t// Filter out control characters (0x00-0x1F, except 0x09, 0x0a, 0x0x0d)\n\t\t\tif (code <= 0x1f) return false;\n\n\t\t\t// Filter out Unicode format characters\n\t\t\tif (code >= 0xfff9 && code <= 0xfffb) return false;\n\n\t\t\treturn true;\n\t\t})\n\t\t.join(\"\");\n}\n\n/**\n * Detached child processes must be tracked so they can be killed on parent\n * shutdown signals (SIGHUP/SIGTERM).\n */\nconst trackedDetachedChildPids = new Set<number>();\n\nexport function trackDetachedChildPid(pid: number): void {\n\ttrackedDetachedChildPids.add(pid);\n}\n\nexport function untrackDetachedChildPid(pid: number): void {\n\ttrackedDetachedChildPids.delete(pid);\n}\n\nexport function killTrackedDetachedChildren(): void {\n\tfor (const pid of trackedDetachedChildPids) {\n\t\tkillProcessTree(pid);\n\t}\n\ttrackedDetachedChildPids.clear();\n}\n\n/**\n * Kill a process and all its children (cross-platform)\n */\nexport function killProcessTree(pid: number): void {\n\tif (process.platform === \"win32\") {\n\t\t// Use taskkill on Windows to kill process tree\n\t\ttry {\n\t\t\tspawn(\"taskkill\", [\"/F\", \"/T\", \"/PID\", String(pid)], {\n\t\t\t\tstdio: \"ignore\",\n\t\t\t\tdetached: true,\n\t\t\t\twindowsHide: true,\n\t\t\t});\n\t\t} catch {\n\t\t\t// Ignore errors if taskkill fails\n\t\t}\n\t} else {\n\t\t// Use SIGKILL on Unix/Linux/Mac\n\t\ttry {\n\t\t\tprocess.kill(-pid, \"SIGKILL\");\n\t\t} catch {\n\t\t\t// Fallback to killing just the child if process group kill fails\n\t\t\ttry {\n\t\t\t\tprocess.kill(pid, \"SIGKILL\");\n\t\t\t} catch {\n\t\t\t\t// Process already dead\n\t\t\t}\n\t\t}\n\t}\n}\n"]}
|
package/dist/utils/shell.js
CHANGED
|
@@ -112,6 +112,27 @@ export function getShellEnv() {
|
|
|
112
112
|
[pathKey]: updatedPath,
|
|
113
113
|
};
|
|
114
114
|
}
|
|
115
|
+
/**
|
|
116
|
+
* Rewrite Windows CMD-style `nul` redirects to POSIX `/dev/null`.
|
|
117
|
+
*
|
|
118
|
+
* Models occasionally emit CMD syntax such as `command 2>nul` even though the
|
|
119
|
+
* shell resolved by `getShellConfig` is always POSIX. A POSIX shell treats
|
|
120
|
+
* `nul` as an ordinary file name, so the redirect creates a literal `nul`
|
|
121
|
+
* file. On Windows that name is a reserved device name: Explorer and most
|
|
122
|
+
* tools cannot delete the file, and it breaks `git add .` and `git clone`
|
|
123
|
+
* for the affected directory.
|
|
124
|
+
*
|
|
125
|
+
* Matches `>nul`, `> NUL`, `2>nul`, `&>nul`, and `>>nul` case-insensitively
|
|
126
|
+
* while leaving longer tokens such as `>null` or `>nul.txt` untouched. The
|
|
127
|
+
* regex does not parse shell quoting, so a quoted literal such as
|
|
128
|
+
* `echo "2>nul crashes"` is also rewritten when `nul` is followed by
|
|
129
|
+
* whitespace; that is accepted because the pattern is rare and the rewritten
|
|
130
|
+
* string stays harmless.
|
|
131
|
+
*/
|
|
132
|
+
const CMD_NUL_REDIRECT_REGEX = /(\d?&?>+\s*)[Nn][Uu][Ll](?=\s|$|[|&;)\n])/g;
|
|
133
|
+
export function rewriteCmdNulRedirects(command) {
|
|
134
|
+
return command.replace(CMD_NUL_REDIRECT_REGEX, "$1/dev/null");
|
|
135
|
+
}
|
|
115
136
|
/**
|
|
116
137
|
* Sanitize binary output for display/storage.
|
|
117
138
|
* Removes characters that crash string-width or cause display issues:
|
package/dist/utils/shell.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"shell.js","sourceRoot":"","sources":["../../src/utils/shell.ts"],"names":[],"mappings":"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","sourcesContent":["import { existsSync } from \"node:fs\";\nimport { delimiter } from \"node:path\";\nimport { spawn, spawnSync } from \"child_process\";\nimport { getBinDir } from \"../config.ts\";\n\nexport interface ShellConfig {\n\tshell: string;\n\targs: string[];\n\tcommandTransport?: \"argv\" | \"stdin\";\n}\n\n/**\n * Find bash executable on PATH (cross-platform)\n */\nfunction isLegacyWslBashPath(path: string): boolean {\n\tconst normalized = path.replace(/\\//g, \"\\\\\").toLowerCase();\n\treturn /^[a-z]:\\\\windows\\\\(?:system32|sysnative)\\\\bash\\.exe$/.test(normalized);\n}\n\nfunction getBashShellConfig(shell: string): ShellConfig {\n\treturn isLegacyWslBashPath(shell) ? { shell, args: [\"-s\"], commandTransport: \"stdin\" } : { shell, args: [\"-c\"] };\n}\n\nfunction findBashOnPath(): string | null {\n\tif (process.platform === \"win32\") {\n\t\t// Windows: Use 'where' and verify file exists (where can return non-existent paths)\n\t\ttry {\n\t\t\tconst result = spawnSync(\"where\", [\"bash.exe\"], {\n\t\t\t\tencoding: \"utf-8\",\n\t\t\t\ttimeout: 5000,\n\t\t\t\twindowsHide: true,\n\t\t\t});\n\t\t\tif (result.status === 0 && result.stdout) {\n\t\t\t\tconst firstMatch = result.stdout.trim().split(/\\r?\\n/)[0];\n\t\t\t\tif (firstMatch && existsSync(firstMatch)) {\n\t\t\t\t\treturn firstMatch;\n\t\t\t\t}\n\t\t\t}\n\t\t} catch {\n\t\t\t// Ignore errors\n\t\t}\n\t\treturn null;\n\t}\n\n\t// Unix: Use 'which' and trust its output (handles Termux and special filesystems)\n\ttry {\n\t\tconst result = spawnSync(\"which\", [\"bash\"], { encoding: \"utf-8\", timeout: 5000 });\n\t\tif (result.status === 0 && result.stdout) {\n\t\t\tconst firstMatch = result.stdout.trim().split(/\\r?\\n/)[0];\n\t\t\tif (firstMatch) {\n\t\t\t\treturn firstMatch;\n\t\t\t}\n\t\t}\n\t} catch {\n\t\t// Ignore errors\n\t}\n\treturn null;\n}\n\n/**\n * Resolve shell configuration based on platform and an optional explicit shell path.\n * Resolution order:\n * 1. User-specified shellPath\n * 2. On Windows: Git Bash in known locations, then bash on PATH\n * 3. On Unix: /bin/bash, then bash on PATH, then fallback to sh\n */\nexport function getShellConfig(customShellPath?: string): ShellConfig {\n\t// 1. Check user-specified shell path\n\tif (customShellPath) {\n\t\tif (existsSync(customShellPath)) {\n\t\t\treturn getBashShellConfig(customShellPath);\n\t\t}\n\t\tthrow new Error(`Custom shell path not found: ${customShellPath}`);\n\t}\n\n\tif (process.platform === \"win32\") {\n\t\t// 2. Try Git Bash in known locations\n\t\tconst paths: string[] = [];\n\t\tconst programFiles = process.env.ProgramFiles;\n\t\tif (programFiles) {\n\t\t\tpaths.push(`${programFiles}\\\\Git\\\\bin\\\\bash.exe`);\n\t\t}\n\t\tconst programFilesX86 = process.env[\"ProgramFiles(x86)\"];\n\t\tif (programFilesX86) {\n\t\t\tpaths.push(`${programFilesX86}\\\\Git\\\\bin\\\\bash.exe`);\n\t\t}\n\n\t\tfor (const path of paths) {\n\t\t\tif (existsSync(path)) {\n\t\t\t\treturn getBashShellConfig(path);\n\t\t\t}\n\t\t}\n\n\t\t// 3. Fallback: search bash.exe on PATH (Cygwin, MSYS2, WSL, etc.)\n\t\tconst bashOnPath = findBashOnPath();\n\t\tif (bashOnPath) {\n\t\t\treturn getBashShellConfig(bashOnPath);\n\t\t}\n\n\t\tthrow new Error(\n\t\t\t`No bash shell found. Options:\\n` +\n\t\t\t\t` 1. Install Git for Windows: https://git-scm.com/download/win\\n` +\n\t\t\t\t` 2. Add your bash to PATH (Cygwin, MSYS2, etc.)\\n` +\n\t\t\t\t\" 3. Set shellPath in settings.json\\n\\n\" +\n\t\t\t\t`Searched Git Bash in:\\n${paths.map((p) => ` ${p}`).join(\"\\n\")}`,\n\t\t);\n\t}\n\n\t// Unix: try /bin/bash, then bash on PATH, then fallback to sh\n\tif (existsSync(\"/bin/bash\")) {\n\t\treturn getBashShellConfig(\"/bin/bash\");\n\t}\n\n\tconst bashOnPath = findBashOnPath();\n\tif (bashOnPath) {\n\t\treturn getBashShellConfig(bashOnPath);\n\t}\n\n\treturn { shell: \"sh\", args: [\"-c\"] };\n}\n\nexport function getShellEnv(): NodeJS.ProcessEnv {\n\tconst binDir = getBinDir();\n\tconst pathKey = Object.keys(process.env).find((key) => key.toLowerCase() === \"path\") ?? \"PATH\";\n\tconst currentPath = process.env[pathKey] ?? \"\";\n\tconst pathEntries = currentPath.split(delimiter).filter(Boolean);\n\tconst hasBinDir = pathEntries.includes(binDir);\n\tconst updatedPath = hasBinDir ? currentPath : [binDir, currentPath].filter(Boolean).join(delimiter);\n\n\treturn {\n\t\t...process.env,\n\t\t[pathKey]: updatedPath,\n\t};\n}\n\n/**\n * Sanitize binary output for display/storage.\n * Removes characters that crash string-width or cause display issues:\n * - Control characters (except tab, newline, carriage return)\n * - Lone surrogates\n * - Unicode Format characters (crash string-width due to a bug)\n * - Characters with undefined code points\n */\nexport function sanitizeBinaryOutput(str: string): string {\n\t// Use Array.from to properly iterate over code points (not code units)\n\t// This handles surrogate pairs correctly and catches edge cases where\n\t// codePointAt() might return undefined\n\treturn Array.from(str)\n\t\t.filter((char) => {\n\t\t\t// Filter out characters that cause string-width to crash\n\t\t\t// This includes:\n\t\t\t// - Unicode format characters\n\t\t\t// - Lone surrogates (already filtered by Array.from)\n\t\t\t// - Control chars except \\t \\n \\r\n\t\t\t// - Characters with undefined code points\n\n\t\t\tconst code = char.codePointAt(0);\n\n\t\t\t// Skip if code point is undefined (edge case with invalid strings)\n\t\t\tif (code === undefined) return false;\n\n\t\t\t// Allow tab, newline, carriage return\n\t\t\tif (code === 0x09 || code === 0x0a || code === 0x0d) return true;\n\n\t\t\t// Filter out control characters (0x00-0x1F, except 0x09, 0x0a, 0x0x0d)\n\t\t\tif (code <= 0x1f) return false;\n\n\t\t\t// Filter out Unicode format characters\n\t\t\tif (code >= 0xfff9 && code <= 0xfffb) return false;\n\n\t\t\treturn true;\n\t\t})\n\t\t.join(\"\");\n}\n\n/**\n * Detached child processes must be tracked so they can be killed on parent\n * shutdown signals (SIGHUP/SIGTERM).\n */\nconst trackedDetachedChildPids = new Set<number>();\n\nexport function trackDetachedChildPid(pid: number): void {\n\ttrackedDetachedChildPids.add(pid);\n}\n\nexport function untrackDetachedChildPid(pid: number): void {\n\ttrackedDetachedChildPids.delete(pid);\n}\n\nexport function killTrackedDetachedChildren(): void {\n\tfor (const pid of trackedDetachedChildPids) {\n\t\tkillProcessTree(pid);\n\t}\n\ttrackedDetachedChildPids.clear();\n}\n\n/**\n * Kill a process and all its children (cross-platform)\n */\nexport function killProcessTree(pid: number): void {\n\tif (process.platform === \"win32\") {\n\t\t// Use taskkill on Windows to kill process tree\n\t\ttry {\n\t\t\tspawn(\"taskkill\", [\"/F\", \"/T\", \"/PID\", String(pid)], {\n\t\t\t\tstdio: \"ignore\",\n\t\t\t\tdetached: true,\n\t\t\t\twindowsHide: true,\n\t\t\t});\n\t\t} catch {\n\t\t\t// Ignore errors if taskkill fails\n\t\t}\n\t} else {\n\t\t// Use SIGKILL on Unix/Linux/Mac\n\t\ttry {\n\t\t\tprocess.kill(-pid, \"SIGKILL\");\n\t\t} catch {\n\t\t\t// Fallback to killing just the child if process group kill fails\n\t\t\ttry {\n\t\t\t\tprocess.kill(pid, \"SIGKILL\");\n\t\t\t} catch {\n\t\t\t\t// Process already dead\n\t\t\t}\n\t\t}\n\t}\n}\n"]}
|
|
1
|
+
{"version":3,"file":"shell.js","sourceRoot":"","sources":["../../src/utils/shell.ts"],"names":[],"mappings":"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","sourcesContent":["import { existsSync } from \"node:fs\";\nimport { delimiter } from \"node:path\";\nimport { spawn, spawnSync } from \"child_process\";\nimport { getBinDir } from \"../config.ts\";\n\nexport interface ShellConfig {\n\tshell: string;\n\targs: string[];\n\tcommandTransport?: \"argv\" | \"stdin\";\n}\n\n/**\n * Find bash executable on PATH (cross-platform)\n */\nfunction isLegacyWslBashPath(path: string): boolean {\n\tconst normalized = path.replace(/\\//g, \"\\\\\").toLowerCase();\n\treturn /^[a-z]:\\\\windows\\\\(?:system32|sysnative)\\\\bash\\.exe$/.test(normalized);\n}\n\nfunction getBashShellConfig(shell: string): ShellConfig {\n\treturn isLegacyWslBashPath(shell) ? { shell, args: [\"-s\"], commandTransport: \"stdin\" } : { shell, args: [\"-c\"] };\n}\n\nfunction findBashOnPath(): string | null {\n\tif (process.platform === \"win32\") {\n\t\t// Windows: Use 'where' and verify file exists (where can return non-existent paths)\n\t\ttry {\n\t\t\tconst result = spawnSync(\"where\", [\"bash.exe\"], {\n\t\t\t\tencoding: \"utf-8\",\n\t\t\t\ttimeout: 5000,\n\t\t\t\twindowsHide: true,\n\t\t\t});\n\t\t\tif (result.status === 0 && result.stdout) {\n\t\t\t\tconst firstMatch = result.stdout.trim().split(/\\r?\\n/)[0];\n\t\t\t\tif (firstMatch && existsSync(firstMatch)) {\n\t\t\t\t\treturn firstMatch;\n\t\t\t\t}\n\t\t\t}\n\t\t} catch {\n\t\t\t// Ignore errors\n\t\t}\n\t\treturn null;\n\t}\n\n\t// Unix: Use 'which' and trust its output (handles Termux and special filesystems)\n\ttry {\n\t\tconst result = spawnSync(\"which\", [\"bash\"], { encoding: \"utf-8\", timeout: 5000 });\n\t\tif (result.status === 0 && result.stdout) {\n\t\t\tconst firstMatch = result.stdout.trim().split(/\\r?\\n/)[0];\n\t\t\tif (firstMatch) {\n\t\t\t\treturn firstMatch;\n\t\t\t}\n\t\t}\n\t} catch {\n\t\t// Ignore errors\n\t}\n\treturn null;\n}\n\n/**\n * Resolve shell configuration based on platform and an optional explicit shell path.\n * Resolution order:\n * 1. User-specified shellPath\n * 2. On Windows: Git Bash in known locations, then bash on PATH\n * 3. On Unix: /bin/bash, then bash on PATH, then fallback to sh\n */\nexport function getShellConfig(customShellPath?: string): ShellConfig {\n\t// 1. Check user-specified shell path\n\tif (customShellPath) {\n\t\tif (existsSync(customShellPath)) {\n\t\t\treturn getBashShellConfig(customShellPath);\n\t\t}\n\t\tthrow new Error(`Custom shell path not found: ${customShellPath}`);\n\t}\n\n\tif (process.platform === \"win32\") {\n\t\t// 2. Try Git Bash in known locations\n\t\tconst paths: string[] = [];\n\t\tconst programFiles = process.env.ProgramFiles;\n\t\tif (programFiles) {\n\t\t\tpaths.push(`${programFiles}\\\\Git\\\\bin\\\\bash.exe`);\n\t\t}\n\t\tconst programFilesX86 = process.env[\"ProgramFiles(x86)\"];\n\t\tif (programFilesX86) {\n\t\t\tpaths.push(`${programFilesX86}\\\\Git\\\\bin\\\\bash.exe`);\n\t\t}\n\n\t\tfor (const path of paths) {\n\t\t\tif (existsSync(path)) {\n\t\t\t\treturn getBashShellConfig(path);\n\t\t\t}\n\t\t}\n\n\t\t// 3. Fallback: search bash.exe on PATH (Cygwin, MSYS2, WSL, etc.)\n\t\tconst bashOnPath = findBashOnPath();\n\t\tif (bashOnPath) {\n\t\t\treturn getBashShellConfig(bashOnPath);\n\t\t}\n\n\t\tthrow new Error(\n\t\t\t`No bash shell found. Options:\\n` +\n\t\t\t\t` 1. Install Git for Windows: https://git-scm.com/download/win\\n` +\n\t\t\t\t` 2. Add your bash to PATH (Cygwin, MSYS2, etc.)\\n` +\n\t\t\t\t\" 3. Set shellPath in settings.json\\n\\n\" +\n\t\t\t\t`Searched Git Bash in:\\n${paths.map((p) => ` ${p}`).join(\"\\n\")}`,\n\t\t);\n\t}\n\n\t// Unix: try /bin/bash, then bash on PATH, then fallback to sh\n\tif (existsSync(\"/bin/bash\")) {\n\t\treturn getBashShellConfig(\"/bin/bash\");\n\t}\n\n\tconst bashOnPath = findBashOnPath();\n\tif (bashOnPath) {\n\t\treturn getBashShellConfig(bashOnPath);\n\t}\n\n\treturn { shell: \"sh\", args: [\"-c\"] };\n}\n\nexport function getShellEnv(): NodeJS.ProcessEnv {\n\tconst binDir = getBinDir();\n\tconst pathKey = Object.keys(process.env).find((key) => key.toLowerCase() === \"path\") ?? \"PATH\";\n\tconst currentPath = process.env[pathKey] ?? \"\";\n\tconst pathEntries = currentPath.split(delimiter).filter(Boolean);\n\tconst hasBinDir = pathEntries.includes(binDir);\n\tconst updatedPath = hasBinDir ? currentPath : [binDir, currentPath].filter(Boolean).join(delimiter);\n\n\treturn {\n\t\t...process.env,\n\t\t[pathKey]: updatedPath,\n\t};\n}\n\n/**\n * Rewrite Windows CMD-style `nul` redirects to POSIX `/dev/null`.\n *\n * Models occasionally emit CMD syntax such as `command 2>nul` even though the\n * shell resolved by `getShellConfig` is always POSIX. A POSIX shell treats\n * `nul` as an ordinary file name, so the redirect creates a literal `nul`\n * file. On Windows that name is a reserved device name: Explorer and most\n * tools cannot delete the file, and it breaks `git add .` and `git clone`\n * for the affected directory.\n *\n * Matches `>nul`, `> NUL`, `2>nul`, `&>nul`, and `>>nul` case-insensitively\n * while leaving longer tokens such as `>null` or `>nul.txt` untouched. The\n * regex does not parse shell quoting, so a quoted literal such as\n * `echo \"2>nul crashes\"` is also rewritten when `nul` is followed by\n * whitespace; that is accepted because the pattern is rare and the rewritten\n * string stays harmless.\n */\nconst CMD_NUL_REDIRECT_REGEX = /(\\d?&?>+\\s*)[Nn][Uu][Ll](?=\\s|$|[|&;)\\n])/g;\n\nexport function rewriteCmdNulRedirects(command: string): string {\n\treturn command.replace(CMD_NUL_REDIRECT_REGEX, \"$1/dev/null\");\n}\n\n/**\n * Sanitize binary output for display/storage.\n * Removes characters that crash string-width or cause display issues:\n * - Control characters (except tab, newline, carriage return)\n * - Lone surrogates\n * - Unicode Format characters (crash string-width due to a bug)\n * - Characters with undefined code points\n */\nexport function sanitizeBinaryOutput(str: string): string {\n\t// Use Array.from to properly iterate over code points (not code units)\n\t// This handles surrogate pairs correctly and catches edge cases where\n\t// codePointAt() might return undefined\n\treturn Array.from(str)\n\t\t.filter((char) => {\n\t\t\t// Filter out characters that cause string-width to crash\n\t\t\t// This includes:\n\t\t\t// - Unicode format characters\n\t\t\t// - Lone surrogates (already filtered by Array.from)\n\t\t\t// - Control chars except \\t \\n \\r\n\t\t\t// - Characters with undefined code points\n\n\t\t\tconst code = char.codePointAt(0);\n\n\t\t\t// Skip if code point is undefined (edge case with invalid strings)\n\t\t\tif (code === undefined) return false;\n\n\t\t\t// Allow tab, newline, carriage return\n\t\t\tif (code === 0x09 || code === 0x0a || code === 0x0d) return true;\n\n\t\t\t// Filter out control characters (0x00-0x1F, except 0x09, 0x0a, 0x0x0d)\n\t\t\tif (code <= 0x1f) return false;\n\n\t\t\t// Filter out Unicode format characters\n\t\t\tif (code >= 0xfff9 && code <= 0xfffb) return false;\n\n\t\t\treturn true;\n\t\t})\n\t\t.join(\"\");\n}\n\n/**\n * Detached child processes must be tracked so they can be killed on parent\n * shutdown signals (SIGHUP/SIGTERM).\n */\nconst trackedDetachedChildPids = new Set<number>();\n\nexport function trackDetachedChildPid(pid: number): void {\n\ttrackedDetachedChildPids.add(pid);\n}\n\nexport function untrackDetachedChildPid(pid: number): void {\n\ttrackedDetachedChildPids.delete(pid);\n}\n\nexport function killTrackedDetachedChildren(): void {\n\tfor (const pid of trackedDetachedChildPids) {\n\t\tkillProcessTree(pid);\n\t}\n\ttrackedDetachedChildPids.clear();\n}\n\n/**\n * Kill a process and all its children (cross-platform)\n */\nexport function killProcessTree(pid: number): void {\n\tif (process.platform === \"win32\") {\n\t\t// Use taskkill on Windows to kill process tree\n\t\ttry {\n\t\t\tspawn(\"taskkill\", [\"/F\", \"/T\", \"/PID\", String(pid)], {\n\t\t\t\tstdio: \"ignore\",\n\t\t\t\tdetached: true,\n\t\t\t\twindowsHide: true,\n\t\t\t});\n\t\t} catch {\n\t\t\t// Ignore errors if taskkill fails\n\t\t}\n\t} else {\n\t\t// Use SIGKILL on Unix/Linux/Mac\n\t\ttry {\n\t\t\tprocess.kill(-pid, \"SIGKILL\");\n\t\t} catch {\n\t\t\t// Fallback to killing just the child if process group kill fails\n\t\t\ttry {\n\t\t\t\tprocess.kill(pid, \"SIGKILL\");\n\t\t\t} catch {\n\t\t\t\t// Process already dead\n\t\t\t}\n\t\t}\n\t}\n}\n"]}
|
package/docs/compaction.md
CHANGED
|
@@ -34,6 +34,12 @@ contextTokens > contextWindow - reserveTokens
|
|
|
34
34
|
|
|
35
35
|
By default, `reserveTokens` is 16384 tokens (configurable in `~/.pi/agent/settings.json` or `<project-dir>/.pi/settings.json`). This leaves room for the LLM's response.
|
|
36
36
|
|
|
37
|
+
During an active tool-calling run, Pi evaluates this threshold after each complete tool batch, including the newly appended tool results, and before another provider request. If compaction succeeds, Pi rebuilds the context and continues the same agent run without inserting a continuation prompt. If compaction is cancelled, fails, has no safe cut point, or leaves the estimated retained context above the threshold, Pi fails closed before queue polling or another provider request. Because the upstream stateful Agent does not expose its low-level graceful turn-stop hook, this exceptional path ends with an explicit error/aborted assistant lifecycle; the preceding `compaction_end` event contains the underlying warning or failure.
|
|
38
|
+
|
|
39
|
+
This between-tool-batch trigger is separate from **split-turn compaction** below: the trigger determines *when* compaction runs, while split-turn support determines *where* a very long turn can be cut.
|
|
40
|
+
|
|
41
|
+
Before a new user prompt, Pi also refuses to send an existing context whose estimate remains above the threshold after compaction. This check happens before adding the new prompt itself. Reduce `keepRecentTokens`, remove unusually large retained content, or switch to a model with a larger context window before continuing.
|
|
42
|
+
|
|
37
43
|
You can also trigger manually with `/compact [instructions]`, where optional instructions focus the summary.
|
|
38
44
|
|
|
39
45
|
### How It Works
|
|
@@ -42,7 +48,7 @@ You can also trigger manually with `/compact [instructions]`, where optional ins
|
|
|
42
48
|
2. **Extract messages**: Collect messages from the previous kept boundary (or session start) up to the cut point
|
|
43
49
|
3. **Generate summary**: Call LLM to summarize with structured format, passing the previous summary as iterative context when present
|
|
44
50
|
4. **Append entry**: Save `CompactionEntry` with summary and `firstKeptEntryId`
|
|
45
|
-
5. **Reload**:
|
|
51
|
+
5. **Reload and verify**: Rebuild context using the summary plus messages from `firstKeptEntryId` onwards, then verify the estimated retained context is below the auto-compaction threshold before continuing automatically
|
|
46
52
|
|
|
47
53
|
```
|
|
48
54
|
Before compaction:
|
|
@@ -290,7 +296,7 @@ pi.on("session_before_compact", async (event, ctx) => {
|
|
|
290
296
|
|
|
291
297
|
// branchEntries - all entries on current branch (for custom state)
|
|
292
298
|
// reason - "manual" (/compact), "threshold", or "overflow"
|
|
293
|
-
// willRetry - whether
|
|
299
|
+
// willRetry - whether overflow recovery intends to retry if compaction produces a safe retained context
|
|
294
300
|
// signal - AbortSignal (pass to LLM calls)
|
|
295
301
|
|
|
296
302
|
// Cancel:
|
package/docs/containerization.md
CHANGED
|
@@ -24,7 +24,7 @@ Use the [example extension](../examples/extensions/gondolin) when you want `pi`
|
|
|
24
24
|
Setup:
|
|
25
25
|
|
|
26
26
|
```bash
|
|
27
|
-
cp -R
|
|
27
|
+
cp -R examples/extensions/gondolin ~/.pi/agent/extensions/gondolin
|
|
28
28
|
cd ~/.pi/agent/extensions/gondolin
|
|
29
29
|
npm install --ignore-scripts
|
|
30
30
|
```
|
package/docs/development.md
CHANGED
|
@@ -1,71 +1,68 @@
|
|
|
1
1
|
# Development
|
|
2
2
|
|
|
3
|
-
See [AGENTS.md](
|
|
3
|
+
See the repository [AGENTS.md](../AGENTS.md) for the distribution contract and coding conventions.
|
|
4
4
|
|
|
5
5
|
## Setup
|
|
6
6
|
|
|
7
7
|
```bash
|
|
8
|
-
git clone https://github.com/
|
|
9
|
-
cd pi
|
|
10
|
-
npm install
|
|
8
|
+
git clone https://github.com/ming-kang/pi
|
|
9
|
+
cd pi
|
|
10
|
+
npm install --ignore-scripts
|
|
11
11
|
npm run build
|
|
12
|
+
npm run check
|
|
12
13
|
```
|
|
13
14
|
|
|
15
|
+
The repository is the standalone `@astralyn/pi` package. AI, Agent core, and TUI are installed as exact upstream npm dependencies; no sibling workspace build is required.
|
|
16
|
+
|
|
14
17
|
Run from source:
|
|
15
18
|
|
|
16
19
|
```bash
|
|
17
|
-
|
|
20
|
+
./pi-test.sh
|
|
18
21
|
```
|
|
19
22
|
|
|
20
|
-
The script can be
|
|
23
|
+
The script can be invoked from any directory. Pi keeps the caller's current working directory. Use `./pi-test.sh --no-env` to clear provider credentials for the source run.
|
|
21
24
|
|
|
22
|
-
##
|
|
25
|
+
## Package identity
|
|
23
26
|
|
|
24
|
-
|
|
27
|
+
Distribution identity and configuration directory defaults are defined in `package.json`:
|
|
25
28
|
|
|
26
29
|
```json
|
|
27
30
|
{
|
|
31
|
+
"name": "@astralyn/pi",
|
|
28
32
|
"piConfig": {
|
|
29
|
-
"name": "pi",
|
|
30
33
|
"configDir": ".pi"
|
|
34
|
+
},
|
|
35
|
+
"bin": {
|
|
36
|
+
"pi": "dist/cli.js"
|
|
31
37
|
}
|
|
32
38
|
}
|
|
33
39
|
```
|
|
34
40
|
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
## Path Resolution
|
|
38
|
-
|
|
39
|
-
Three execution modes: npm install, standalone binary, tsx from source.
|
|
40
|
-
|
|
41
|
-
**Always use `src/config.ts`** for package assets:
|
|
41
|
+
## Path resolution
|
|
42
42
|
|
|
43
|
-
|
|
44
|
-
import { getPackageDir, getThemeDir } from "./config.js";
|
|
45
|
-
```
|
|
46
|
-
|
|
47
|
-
Never use `__dirname` directly for package assets.
|
|
43
|
+
Pi supports npm installation and `tsx` source execution. Always use `src/config.ts` helpers such as `getPackageDir()` and `getThemeDir()` for package assets; do not derive asset paths directly from `__dirname`.
|
|
48
44
|
|
|
49
|
-
## Debug
|
|
45
|
+
## Debug command
|
|
50
46
|
|
|
51
|
-
`/debug` (hidden) writes to `~/.pi/agent/pi-debug.log
|
|
52
|
-
- Rendered TUI lines with ANSI codes
|
|
53
|
-
- Last messages sent to the LLM
|
|
47
|
+
`/debug` (hidden) writes rendered TUI lines and the latest model messages to `~/.pi/agent/pi-debug.log`.
|
|
54
48
|
|
|
55
49
|
## Testing
|
|
56
50
|
|
|
57
51
|
```bash
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
52
|
+
npm run check
|
|
53
|
+
node node_modules/vitest/dist/cli.js --run test/specific.test.ts
|
|
54
|
+
./test.sh # complete isolated test suite
|
|
61
55
|
```
|
|
62
56
|
|
|
63
|
-
|
|
57
|
+
Release CI runs the full suite on Ubuntu. Interactive UI changes should also be verified in a real terminal.
|
|
64
58
|
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
59
|
+
## Project structure
|
|
60
|
+
|
|
61
|
+
```text
|
|
62
|
+
src/ runtime source
|
|
63
|
+
test/ automated tests and local test helpers
|
|
64
|
+
docs/ product and public API documentation
|
|
65
|
+
examples/ SDK and extension examples
|
|
66
|
+
docs/distribution/ standalone distribution documentation
|
|
67
|
+
scripts/ package maintenance scripts
|
|
71
68
|
```
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
# Distribution documentation
|
|
2
|
+
|
|
3
|
+
This directory contains documentation owned by the standalone `@astralyn/pi` distribution. Product usage and public API documentation inherited from upstream coding-agent lives one level above under `docs/`.
|
|
4
|
+
|
|
5
|
+
## User documentation
|
|
6
|
+
|
|
7
|
+
- [Distribution overview](../../README.md) — package identity, installation, bundled features, and development entry points.
|
|
8
|
+
- [Quickstart](../quickstart.md) — install, authenticate, and run a first session.
|
|
9
|
+
- [User and API documentation](../index.md) — interactive mode, settings, customization, SDK, RPC, and platform notes.
|
|
10
|
+
- [Extension API](../extensions.md) — write external TypeScript extensions.
|
|
11
|
+
- [Theme API](../themes.md) — create and load custom themes.
|
|
12
|
+
|
|
13
|
+
## Architecture and design
|
|
14
|
+
|
|
15
|
+
- [Architecture and dependency boundaries](architecture.md)
|
|
16
|
+
- [Native tool presentation](tool-presentation.md)
|
|
17
|
+
- [Bundled theme design](themes.md)
|
|
18
|
+
|
|
19
|
+
## Bundled extensions
|
|
20
|
+
|
|
21
|
+
- [Catalog](extensions/README.md)
|
|
22
|
+
- [deepwiki](extensions/deepwiki.md)
|
|
23
|
+
- [question](extensions/question.md)
|
|
24
|
+
- [rewind](extensions/rewind.md)
|
|
25
|
+
- [router](extensions/router.md)
|
|
26
|
+
- [statusline](extensions/statusline.md)
|
|
27
|
+
- [subagent](extensions/subagent.md)
|
|
28
|
+
- [todo](extensions/todo.md)
|
|
29
|
+
|
|
30
|
+
## Maintainer documentation
|
|
31
|
+
|
|
32
|
+
- [Upstream synchronization](maintenance.md)
|
|
33
|
+
- [OIDC npm release checklist](release.md)
|
|
34
|
+
- [Repository contract](../../AGENTS.md)
|