@oh-my-pi/pi-coding-agent 3.15.0 → 3.20.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +61 -1
- package/docs/extensions.md +1055 -0
- package/docs/rpc.md +69 -13
- package/docs/session-tree-plan.md +1 -1
- package/examples/extensions/README.md +141 -0
- package/examples/extensions/api-demo.ts +87 -0
- package/examples/extensions/chalk-logger.ts +26 -0
- package/examples/extensions/hello.ts +33 -0
- package/examples/extensions/pirate.ts +44 -0
- package/examples/extensions/plan-mode.ts +551 -0
- package/examples/extensions/subagent/agents/reviewer.md +35 -0
- package/examples/extensions/todo.ts +299 -0
- package/examples/extensions/tools.ts +145 -0
- package/examples/extensions/with-deps/index.ts +36 -0
- package/examples/extensions/with-deps/package-lock.json +31 -0
- package/examples/extensions/with-deps/package.json +16 -0
- package/examples/sdk/02-custom-model.ts +3 -3
- package/examples/sdk/05-tools.ts +7 -3
- package/examples/sdk/06-extensions.ts +81 -0
- package/examples/sdk/06-hooks.ts +14 -13
- package/examples/sdk/08-prompt-templates.ts +42 -0
- package/examples/sdk/08-slash-commands.ts +17 -12
- package/examples/sdk/09-api-keys-and-oauth.ts +2 -2
- package/examples/sdk/12-full-control.ts +6 -6
- package/package.json +11 -7
- package/src/capability/extension-module.ts +34 -0
- package/src/cli/args.ts +22 -7
- package/src/cli/file-processor.ts +38 -67
- package/src/cli/list-models.ts +1 -1
- package/src/config.ts +25 -14
- package/src/core/agent-session.ts +505 -242
- package/src/core/auth-storage.ts +33 -21
- package/src/core/compaction/branch-summarization.ts +4 -4
- package/src/core/compaction/compaction.ts +3 -3
- package/src/core/custom-commands/bundled/wt/index.ts +430 -0
- package/src/core/custom-commands/loader.ts +9 -0
- package/src/core/custom-tools/wrapper.ts +5 -0
- package/src/core/event-bus.ts +59 -0
- package/src/core/export-html/vendor/highlight.min.js +1213 -0
- package/src/core/export-html/vendor/marked.min.js +6 -0
- package/src/core/extensions/index.ts +100 -0
- package/src/core/extensions/loader.ts +501 -0
- package/src/core/extensions/runner.ts +477 -0
- package/src/core/extensions/types.ts +712 -0
- package/src/core/extensions/wrapper.ts +147 -0
- package/src/core/hooks/types.ts +2 -2
- package/src/core/index.ts +10 -21
- package/src/core/keybindings.ts +199 -0
- package/src/core/messages.ts +26 -7
- package/src/core/model-registry.ts +123 -46
- package/src/core/model-resolver.ts +7 -5
- package/src/core/prompt-templates.ts +242 -0
- package/src/core/sdk.ts +378 -295
- package/src/core/session-manager.ts +72 -58
- package/src/core/settings-manager.ts +118 -22
- package/src/core/system-prompt.ts +24 -1
- package/src/core/terminal-notify.ts +37 -0
- package/src/core/tools/context.ts +4 -4
- package/src/core/tools/exa/mcp-client.ts +5 -4
- package/src/core/tools/exa/render.ts +176 -131
- package/src/core/tools/gemini-image.ts +361 -0
- package/src/core/tools/git.ts +216 -0
- package/src/core/tools/index.ts +28 -15
- package/src/core/tools/lsp/config.ts +5 -4
- package/src/core/tools/lsp/index.ts +17 -12
- package/src/core/tools/lsp/render.ts +39 -47
- package/src/core/tools/read.ts +66 -29
- package/src/core/tools/render-utils.ts +268 -0
- package/src/core/tools/renderers.ts +243 -225
- package/src/core/tools/task/discovery.ts +2 -2
- package/src/core/tools/task/executor.ts +66 -58
- package/src/core/tools/task/index.ts +29 -10
- package/src/core/tools/task/model-resolver.ts +8 -13
- package/src/core/tools/task/omp-command.ts +24 -0
- package/src/core/tools/task/render.ts +35 -60
- package/src/core/tools/task/types.ts +3 -0
- package/src/core/tools/web-fetch.ts +29 -28
- package/src/core/tools/web-search/index.ts +6 -5
- package/src/core/tools/web-search/providers/exa.ts +6 -5
- package/src/core/tools/web-search/render.ts +66 -111
- package/src/core/voice-controller.ts +135 -0
- package/src/core/voice-supervisor.ts +1003 -0
- package/src/core/voice.ts +308 -0
- package/src/discovery/builtin.ts +75 -1
- package/src/discovery/claude.ts +47 -1
- package/src/discovery/codex.ts +54 -2
- package/src/discovery/gemini.ts +55 -2
- package/src/discovery/helpers.ts +100 -1
- package/src/discovery/index.ts +2 -0
- package/src/index.ts +14 -9
- package/src/lib/worktree/collapse.ts +179 -0
- package/src/lib/worktree/constants.ts +14 -0
- package/src/lib/worktree/errors.ts +23 -0
- package/src/lib/worktree/git.ts +110 -0
- package/src/lib/worktree/index.ts +23 -0
- package/src/lib/worktree/operations.ts +216 -0
- package/src/lib/worktree/session.ts +114 -0
- package/src/lib/worktree/stats.ts +67 -0
- package/src/main.ts +61 -37
- package/src/migrations.ts +37 -7
- package/src/modes/interactive/components/bash-execution.ts +6 -4
- package/src/modes/interactive/components/custom-editor.ts +55 -0
- package/src/modes/interactive/components/custom-message.ts +95 -0
- package/src/modes/interactive/components/extensions/extension-list.ts +5 -0
- package/src/modes/interactive/components/extensions/inspector-panel.ts +18 -12
- package/src/modes/interactive/components/extensions/state-manager.ts +12 -0
- package/src/modes/interactive/components/extensions/types.ts +1 -0
- package/src/modes/interactive/components/footer.ts +324 -0
- package/src/modes/interactive/components/hook-editor.ts +1 -0
- package/src/modes/interactive/components/hook-selector.ts +3 -3
- package/src/modes/interactive/components/model-selector.ts +7 -6
- package/src/modes/interactive/components/oauth-selector.ts +3 -3
- package/src/modes/interactive/components/settings-defs.ts +55 -6
- package/src/modes/interactive/components/status-line/separators.ts +4 -4
- package/src/modes/interactive/components/status-line.ts +45 -35
- package/src/modes/interactive/components/tool-execution.ts +95 -23
- package/src/modes/interactive/interactive-mode.ts +644 -113
- package/src/modes/interactive/theme/defaults/alabaster.json +99 -0
- package/src/modes/interactive/theme/defaults/amethyst.json +103 -0
- package/src/modes/interactive/theme/defaults/anthracite.json +100 -0
- package/src/modes/interactive/theme/defaults/basalt.json +90 -0
- package/src/modes/interactive/theme/defaults/birch.json +101 -0
- package/src/modes/interactive/theme/defaults/dark-abyss.json +97 -0
- package/src/modes/interactive/theme/defaults/dark-aurora.json +94 -0
- package/src/modes/interactive/theme/defaults/dark-cavern.json +97 -0
- package/src/modes/interactive/theme/defaults/dark-copper.json +94 -0
- package/src/modes/interactive/theme/defaults/dark-cosmos.json +96 -0
- package/src/modes/interactive/theme/defaults/dark-eclipse.json +97 -0
- package/src/modes/interactive/theme/defaults/dark-ember.json +94 -0
- package/src/modes/interactive/theme/defaults/dark-equinox.json +96 -0
- package/src/modes/interactive/theme/defaults/dark-lavender.json +94 -0
- package/src/modes/interactive/theme/defaults/dark-lunar.json +95 -0
- package/src/modes/interactive/theme/defaults/dark-midnight.json +94 -0
- package/src/modes/interactive/theme/defaults/dark-nebula.json +96 -0
- package/src/modes/interactive/theme/defaults/dark-rainforest.json +97 -0
- package/src/modes/interactive/theme/defaults/dark-reef.json +97 -0
- package/src/modes/interactive/theme/defaults/dark-sakura.json +94 -0
- package/src/modes/interactive/theme/defaults/dark-slate.json +94 -0
- package/src/modes/interactive/theme/defaults/dark-solstice.json +96 -0
- package/src/modes/interactive/theme/defaults/dark-starfall.json +97 -0
- package/src/modes/interactive/theme/defaults/dark-swamp.json +96 -0
- package/src/modes/interactive/theme/defaults/dark-taiga.json +97 -0
- package/src/modes/interactive/theme/defaults/dark-terminal.json +94 -0
- package/src/modes/interactive/theme/defaults/dark-tundra.json +97 -0
- package/src/modes/interactive/theme/defaults/dark-twilight.json +97 -0
- package/src/modes/interactive/theme/defaults/dark-volcanic.json +97 -0
- package/src/modes/interactive/theme/defaults/graphite.json +99 -0
- package/src/modes/interactive/theme/defaults/index.ts +128 -0
- package/src/modes/interactive/theme/defaults/light-aurora-day.json +97 -0
- package/src/modes/interactive/theme/defaults/light-canyon.json +97 -0
- package/src/modes/interactive/theme/defaults/light-cirrus.json +96 -0
- package/src/modes/interactive/theme/defaults/light-coral.json +94 -0
- package/src/modes/interactive/theme/defaults/light-dawn.json +96 -0
- package/src/modes/interactive/theme/defaults/light-dunes.json +97 -0
- package/src/modes/interactive/theme/defaults/light-eucalyptus.json +94 -0
- package/src/modes/interactive/theme/defaults/light-frost.json +94 -0
- package/src/modes/interactive/theme/defaults/light-glacier.json +97 -0
- package/src/modes/interactive/theme/defaults/light-haze.json +96 -0
- package/src/modes/interactive/theme/defaults/light-honeycomb.json +94 -0
- package/src/modes/interactive/theme/defaults/light-lagoon.json +97 -0
- package/src/modes/interactive/theme/defaults/light-lavender.json +94 -0
- package/src/modes/interactive/theme/defaults/light-meadow.json +97 -0
- package/src/modes/interactive/theme/defaults/light-mint.json +94 -0
- package/src/modes/interactive/theme/defaults/light-opal.json +97 -0
- package/src/modes/interactive/theme/defaults/light-orchard.json +97 -0
- package/src/modes/interactive/theme/defaults/light-paper.json +94 -0
- package/src/modes/interactive/theme/defaults/light-prism.json +96 -0
- package/src/modes/interactive/theme/defaults/light-sand.json +94 -0
- package/src/modes/interactive/theme/defaults/light-savanna.json +97 -0
- package/src/modes/interactive/theme/defaults/light-soleil.json +96 -0
- package/src/modes/interactive/theme/defaults/light-wetland.json +97 -0
- package/src/modes/interactive/theme/defaults/light-zenith.json +95 -0
- package/src/modes/interactive/theme/defaults/limestone.json +100 -0
- package/src/modes/interactive/theme/defaults/mahogany.json +104 -0
- package/src/modes/interactive/theme/defaults/marble.json +99 -0
- package/src/modes/interactive/theme/defaults/obsidian.json +90 -0
- package/src/modes/interactive/theme/defaults/onyx.json +90 -0
- package/src/modes/interactive/theme/defaults/pearl.json +99 -0
- package/src/modes/interactive/theme/defaults/porcelain.json +90 -0
- package/src/modes/interactive/theme/defaults/quartz.json +102 -0
- package/src/modes/interactive/theme/defaults/sandstone.json +101 -0
- package/src/modes/interactive/theme/defaults/titanium.json +89 -0
- package/src/modes/print-mode.ts +14 -72
- package/src/modes/rpc/rpc-client.ts +23 -9
- package/src/modes/rpc/rpc-mode.ts +137 -125
- package/src/modes/rpc/rpc-types.ts +46 -24
- package/src/prompts/task.md +1 -0
- package/src/prompts/tools/gemini-image.md +4 -0
- package/src/prompts/tools/git.md +9 -0
- package/src/prompts/voice-summary.md +12 -0
- package/src/utils/image-convert.ts +26 -0
- package/src/utils/image-resize.ts +215 -0
- package/src/utils/shell-snapshot.ts +22 -20
|
@@ -0,0 +1,215 @@
|
|
|
1
|
+
import type { ImageContent } from "@oh-my-pi/pi-ai";
|
|
2
|
+
|
|
3
|
+
export interface ImageResizeOptions {
|
|
4
|
+
maxWidth?: number; // Default: 2000
|
|
5
|
+
maxHeight?: number; // Default: 2000
|
|
6
|
+
maxBytes?: number; // Default: 4.5MB (below Anthropic's 5MB limit)
|
|
7
|
+
jpegQuality?: number; // Default: 80
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
export interface ResizedImage {
|
|
11
|
+
data: string; // base64
|
|
12
|
+
mimeType: string;
|
|
13
|
+
originalWidth: number;
|
|
14
|
+
originalHeight: number;
|
|
15
|
+
width: number;
|
|
16
|
+
height: number;
|
|
17
|
+
wasResized: boolean;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
// 4.5MB - provides headroom below Anthropic's 5MB limit
|
|
21
|
+
const DEFAULT_MAX_BYTES = 4.5 * 1024 * 1024;
|
|
22
|
+
|
|
23
|
+
const DEFAULT_OPTIONS: Required<ImageResizeOptions> = {
|
|
24
|
+
maxWidth: 2000,
|
|
25
|
+
maxHeight: 2000,
|
|
26
|
+
maxBytes: DEFAULT_MAX_BYTES,
|
|
27
|
+
jpegQuality: 80,
|
|
28
|
+
};
|
|
29
|
+
|
|
30
|
+
/** Helper to pick the smaller of two buffers */
|
|
31
|
+
function pickSmaller(
|
|
32
|
+
a: { buffer: Buffer; mimeType: string },
|
|
33
|
+
b: { buffer: Buffer; mimeType: string },
|
|
34
|
+
): { buffer: Buffer; mimeType: string } {
|
|
35
|
+
return a.buffer.length <= b.buffer.length ? a : b;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* Resize an image to fit within the specified max dimensions and file size.
|
|
40
|
+
* Returns the original image if it already fits within the limits.
|
|
41
|
+
*
|
|
42
|
+
* Uses sharp for image processing. If sharp is not available (e.g., in some
|
|
43
|
+
* environments), returns the original image unchanged.
|
|
44
|
+
*
|
|
45
|
+
* Strategy for staying under maxBytes:
|
|
46
|
+
* 1. First resize to maxWidth/maxHeight
|
|
47
|
+
* 2. Try both PNG and JPEG formats, pick the smaller one
|
|
48
|
+
* 3. If still too large, try JPEG with decreasing quality
|
|
49
|
+
* 4. If still too large, progressively reduce dimensions
|
|
50
|
+
*/
|
|
51
|
+
export async function resizeImage(img: ImageContent, options?: ImageResizeOptions): Promise<ResizedImage> {
|
|
52
|
+
const opts = { ...DEFAULT_OPTIONS, ...options };
|
|
53
|
+
const buffer = Buffer.from(img.data, "base64");
|
|
54
|
+
|
|
55
|
+
let sharp: typeof import("sharp") | undefined;
|
|
56
|
+
try {
|
|
57
|
+
sharp = (await import("sharp")).default;
|
|
58
|
+
} catch {
|
|
59
|
+
// Sharp not available - return original image
|
|
60
|
+
// We can't get dimensions without sharp, so return 0s
|
|
61
|
+
return {
|
|
62
|
+
data: img.data,
|
|
63
|
+
mimeType: img.mimeType,
|
|
64
|
+
originalWidth: 0,
|
|
65
|
+
originalHeight: 0,
|
|
66
|
+
width: 0,
|
|
67
|
+
height: 0,
|
|
68
|
+
wasResized: false,
|
|
69
|
+
};
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
const sharpImg = sharp(buffer);
|
|
73
|
+
const metadata = await sharpImg.metadata();
|
|
74
|
+
|
|
75
|
+
const originalWidth = metadata.width ?? 0;
|
|
76
|
+
const originalHeight = metadata.height ?? 0;
|
|
77
|
+
const format = metadata.format ?? img.mimeType?.split("/")[1] ?? "png";
|
|
78
|
+
|
|
79
|
+
// Check if already within all limits (dimensions AND size)
|
|
80
|
+
const originalSize = buffer.length;
|
|
81
|
+
if (originalWidth <= opts.maxWidth && originalHeight <= opts.maxHeight && originalSize <= opts.maxBytes) {
|
|
82
|
+
return {
|
|
83
|
+
data: img.data,
|
|
84
|
+
mimeType: img.mimeType ?? `image/${format}`,
|
|
85
|
+
originalWidth,
|
|
86
|
+
originalHeight,
|
|
87
|
+
width: originalWidth,
|
|
88
|
+
height: originalHeight,
|
|
89
|
+
wasResized: false,
|
|
90
|
+
};
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
// Calculate initial dimensions respecting max limits
|
|
94
|
+
let targetWidth = originalWidth;
|
|
95
|
+
let targetHeight = originalHeight;
|
|
96
|
+
|
|
97
|
+
if (targetWidth > opts.maxWidth) {
|
|
98
|
+
targetHeight = Math.round((targetHeight * opts.maxWidth) / targetWidth);
|
|
99
|
+
targetWidth = opts.maxWidth;
|
|
100
|
+
}
|
|
101
|
+
if (targetHeight > opts.maxHeight) {
|
|
102
|
+
targetWidth = Math.round((targetWidth * opts.maxHeight) / targetHeight);
|
|
103
|
+
targetHeight = opts.maxHeight;
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
// Helper to resize and encode in both formats, returning the smaller one
|
|
107
|
+
async function tryBothFormats(
|
|
108
|
+
width: number,
|
|
109
|
+
height: number,
|
|
110
|
+
jpegQuality: number,
|
|
111
|
+
): Promise<{ buffer: Buffer; mimeType: string }> {
|
|
112
|
+
const resized = await sharp!(buffer)
|
|
113
|
+
.resize(width, height, { fit: "inside", withoutEnlargement: true })
|
|
114
|
+
.toBuffer();
|
|
115
|
+
|
|
116
|
+
const [pngBuffer, jpegBuffer] = await Promise.all([
|
|
117
|
+
sharp!(resized).png({ compressionLevel: 9 }).toBuffer(),
|
|
118
|
+
sharp!(resized).jpeg({ quality: jpegQuality }).toBuffer(),
|
|
119
|
+
]);
|
|
120
|
+
|
|
121
|
+
return pickSmaller({ buffer: pngBuffer, mimeType: "image/png" }, { buffer: jpegBuffer, mimeType: "image/jpeg" });
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
// Try to produce an image under maxBytes
|
|
125
|
+
const qualitySteps = [85, 70, 55, 40];
|
|
126
|
+
const scaleSteps = [1.0, 0.75, 0.5, 0.35, 0.25];
|
|
127
|
+
|
|
128
|
+
let best: { buffer: Buffer; mimeType: string };
|
|
129
|
+
let finalWidth = targetWidth;
|
|
130
|
+
let finalHeight = targetHeight;
|
|
131
|
+
|
|
132
|
+
// First attempt: resize to target dimensions, try both formats
|
|
133
|
+
best = await tryBothFormats(targetWidth, targetHeight, opts.jpegQuality);
|
|
134
|
+
|
|
135
|
+
if (best.buffer.length <= opts.maxBytes) {
|
|
136
|
+
return {
|
|
137
|
+
data: best.buffer.toString("base64"),
|
|
138
|
+
mimeType: best.mimeType,
|
|
139
|
+
originalWidth,
|
|
140
|
+
originalHeight,
|
|
141
|
+
width: finalWidth,
|
|
142
|
+
height: finalHeight,
|
|
143
|
+
wasResized: true,
|
|
144
|
+
};
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
// Still too large - try JPEG with decreasing quality (and compare to PNG each time)
|
|
148
|
+
for (const quality of qualitySteps) {
|
|
149
|
+
best = await tryBothFormats(targetWidth, targetHeight, quality);
|
|
150
|
+
|
|
151
|
+
if (best.buffer.length <= opts.maxBytes) {
|
|
152
|
+
return {
|
|
153
|
+
data: best.buffer.toString("base64"),
|
|
154
|
+
mimeType: best.mimeType,
|
|
155
|
+
originalWidth,
|
|
156
|
+
originalHeight,
|
|
157
|
+
width: finalWidth,
|
|
158
|
+
height: finalHeight,
|
|
159
|
+
wasResized: true,
|
|
160
|
+
};
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
// Still too large - reduce dimensions progressively
|
|
165
|
+
for (const scale of scaleSteps) {
|
|
166
|
+
finalWidth = Math.round(targetWidth * scale);
|
|
167
|
+
finalHeight = Math.round(targetHeight * scale);
|
|
168
|
+
|
|
169
|
+
// Skip if dimensions are too small
|
|
170
|
+
if (finalWidth < 100 || finalHeight < 100) {
|
|
171
|
+
break;
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
for (const quality of qualitySteps) {
|
|
175
|
+
best = await tryBothFormats(finalWidth, finalHeight, quality);
|
|
176
|
+
|
|
177
|
+
if (best.buffer.length <= opts.maxBytes) {
|
|
178
|
+
return {
|
|
179
|
+
data: best.buffer.toString("base64"),
|
|
180
|
+
mimeType: best.mimeType,
|
|
181
|
+
originalWidth,
|
|
182
|
+
originalHeight,
|
|
183
|
+
width: finalWidth,
|
|
184
|
+
height: finalHeight,
|
|
185
|
+
wasResized: true,
|
|
186
|
+
};
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
// Last resort: return smallest version we produced even if over limit
|
|
192
|
+
// (the API will reject it, but at least we tried everything)
|
|
193
|
+
return {
|
|
194
|
+
data: best.buffer.toString("base64"),
|
|
195
|
+
mimeType: best.mimeType,
|
|
196
|
+
originalWidth,
|
|
197
|
+
originalHeight,
|
|
198
|
+
width: finalWidth,
|
|
199
|
+
height: finalHeight,
|
|
200
|
+
wasResized: true,
|
|
201
|
+
};
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
/**
|
|
205
|
+
* Format a dimension note for resized images.
|
|
206
|
+
* This helps the model understand the coordinate mapping.
|
|
207
|
+
*/
|
|
208
|
+
export function formatDimensionNote(result: ResizedImage): string | undefined {
|
|
209
|
+
if (!result.wasResized) {
|
|
210
|
+
return undefined;
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
const scale = result.originalWidth / result.width;
|
|
214
|
+
return `[Image: original ${result.originalWidth}x${result.originalHeight}, displayed at ${result.width}x${result.height}. Multiply coordinates by ${scale.toFixed(2)} to map to original image.]`;
|
|
215
|
+
}
|
|
@@ -6,7 +6,6 @@
|
|
|
6
6
|
* shell experience.
|
|
7
7
|
*/
|
|
8
8
|
|
|
9
|
-
import { existsSync, mkdirSync, unlinkSync } from "node:fs";
|
|
10
9
|
import { homedir, tmpdir } from "node:os";
|
|
11
10
|
import { join } from "node:path";
|
|
12
11
|
|
|
@@ -28,9 +27,11 @@ function getShellConfigFile(shell: string): string {
|
|
|
28
27
|
* This script sources the user's rc file and extracts functions, aliases, and options.
|
|
29
28
|
* Matches Claude Code's snapshot generation logic.
|
|
30
29
|
*/
|
|
31
|
-
function generateSnapshotScript(shell: string, snapshotPath: string, rcFile: string): string {
|
|
32
|
-
const hasRcFile =
|
|
30
|
+
async function generateSnapshotScript(shell: string, snapshotPath: string, rcFile: string): Promise<string> {
|
|
31
|
+
const hasRcFile = await Bun.file(rcFile).exists();
|
|
33
32
|
const isZsh = shell.includes("zsh");
|
|
33
|
+
const commonToolsRegex =
|
|
34
|
+
"^(ls|dir|vdir|cat|head|tail|less|more|grep|egrep|fgrep|rg|find|fd|locate|sed|awk|perl|cp|mv|rm|mkdir|rmdir|touch|chmod|chown|ln|pwd|readlink|stat|cut|sort|uniq|xargs|tee|tr|basename|dirname)$";
|
|
34
35
|
|
|
35
36
|
// Escape the snapshot path for shell
|
|
36
37
|
const escapedPath = snapshotPath.replace(/'/g, "'\\''");
|
|
@@ -42,7 +43,7 @@ echo "# Functions" >> "$SNAPSHOT_FILE"
|
|
|
42
43
|
# Force autoload all functions first
|
|
43
44
|
typeset -f > /dev/null 2>&1
|
|
44
45
|
# Get user function names - filter system/private ones
|
|
45
|
-
typeset +f 2>/dev/null | grep -vE '^(_|__)' | while read func; do
|
|
46
|
+
typeset +f 2>/dev/null | grep -vE '^(_|__)' | grep -vE '${commonToolsRegex}' | while read func; do
|
|
46
47
|
typeset -f "$func" >> "$SNAPSHOT_FILE" 2>/dev/null
|
|
47
48
|
done
|
|
48
49
|
`
|
|
@@ -51,7 +52,7 @@ echo "# Functions" >> "$SNAPSHOT_FILE"
|
|
|
51
52
|
# Force autoload all functions first
|
|
52
53
|
declare -f > /dev/null 2>&1
|
|
53
54
|
# Get user function names - filter system/private ones, use base64 for special chars
|
|
54
|
-
declare -F 2>/dev/null | cut -d' ' -f3 | grep -vE '^(_|__)' | while read func; do
|
|
55
|
+
declare -F 2>/dev/null | cut -d' ' -f3 | grep -vE '^(_|__)' | grep -vE '${commonToolsRegex}' | while read func; do
|
|
55
56
|
encoded_func=$(declare -f "$func" | base64)
|
|
56
57
|
echo "eval \\"\\$(echo '$encoded_func' | base64 -d)\\" > /dev/null 2>&1" >> "$SNAPSHOT_FILE"
|
|
57
58
|
done
|
|
@@ -90,9 +91,9 @@ ${optionsScript}
|
|
|
90
91
|
echo "# Aliases" >> "$SNAPSHOT_FILE"
|
|
91
92
|
# Filter out winpty aliases on Windows to avoid "stdin is not a tty" errors
|
|
92
93
|
if [[ "$OSTYPE" == "msys" ]] || [[ "$OSTYPE" == "cygwin" ]]; then
|
|
93
|
-
alias 2>/dev/null | grep -v "='winpty " | sed 's/^alias //g' | sed 's/^/alias -- /' | head -n 1000 >> "$SNAPSHOT_FILE"
|
|
94
|
+
alias 2>/dev/null | grep -v "='winpty " | grep -vE '^alias (${commonToolsRegex})=' | sed 's/^alias //g' | sed 's/^/alias -- /' | head -n 1000 >> "$SNAPSHOT_FILE"
|
|
94
95
|
else
|
|
95
|
-
alias 2>/dev/null | sed 's/^alias //g' | sed 's/^/alias -- /' | head -n 1000 >> "$SNAPSHOT_FILE"
|
|
96
|
+
alias 2>/dev/null | grep -vE '^alias (${commonToolsRegex})=' | sed 's/^alias //g' | sed 's/^/alias -- /' | head -n 1000 >> "$SNAPSHOT_FILE"
|
|
96
97
|
fi
|
|
97
98
|
|
|
98
99
|
# Export PATH
|
|
@@ -115,7 +116,7 @@ export async function getOrCreateSnapshot(
|
|
|
115
116
|
env: Record<string, string | undefined>,
|
|
116
117
|
): Promise<string | null> {
|
|
117
118
|
// Return cached snapshot if valid
|
|
118
|
-
if (cachedSnapshotPath &&
|
|
119
|
+
if (cachedSnapshotPath && (await Bun.file(cachedSnapshotPath).exists())) {
|
|
119
120
|
return cachedSnapshotPath;
|
|
120
121
|
}
|
|
121
122
|
|
|
@@ -128,9 +129,8 @@ export async function getOrCreateSnapshot(
|
|
|
128
129
|
|
|
129
130
|
// Create snapshot directory
|
|
130
131
|
const snapshotDir = join(tmpdir(), "omp-shell-snapshots");
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
} catch {
|
|
132
|
+
const mkdirProc = Bun.spawnSync(["mkdir", "-p", snapshotDir]);
|
|
133
|
+
if (mkdirProc.exitCode !== 0) {
|
|
134
134
|
return null;
|
|
135
135
|
}
|
|
136
136
|
|
|
@@ -141,7 +141,7 @@ export async function getOrCreateSnapshot(
|
|
|
141
141
|
const snapshotPath = join(snapshotDir, `snapshot-${shellName}-${timestamp}-${random}.sh`);
|
|
142
142
|
|
|
143
143
|
// Generate and execute snapshot script
|
|
144
|
-
const script = generateSnapshotScript(shell, snapshotPath, rcFile);
|
|
144
|
+
const script = await generateSnapshotScript(shell, snapshotPath, rcFile);
|
|
145
145
|
|
|
146
146
|
try {
|
|
147
147
|
const result = Bun.spawnSync([shell, "-l", "-c", script], {
|
|
@@ -152,7 +152,7 @@ export async function getOrCreateSnapshot(
|
|
|
152
152
|
timeout: 10000, // 10 second timeout
|
|
153
153
|
});
|
|
154
154
|
|
|
155
|
-
if (result.exitCode === 0 &&
|
|
155
|
+
if (result.exitCode === 0 && (await Bun.file(snapshotPath).exists())) {
|
|
156
156
|
cachedSnapshotPath = snapshotPath;
|
|
157
157
|
registerCleanup();
|
|
158
158
|
return snapshotPath;
|
|
@@ -182,17 +182,19 @@ function registerCleanup(): void {
|
|
|
182
182
|
if (cleanupRegistered) return;
|
|
183
183
|
cleanupRegistered = true;
|
|
184
184
|
|
|
185
|
-
const cleanup = () => {
|
|
186
|
-
if (cachedSnapshotPath &&
|
|
185
|
+
const cleanup = async () => {
|
|
186
|
+
if (cachedSnapshotPath && (await Bun.file(cachedSnapshotPath).exists())) {
|
|
187
187
|
try {
|
|
188
|
-
|
|
188
|
+
Bun.spawnSync(["rm", cachedSnapshotPath]);
|
|
189
189
|
} catch {
|
|
190
190
|
// Ignore cleanup errors
|
|
191
191
|
}
|
|
192
192
|
}
|
|
193
193
|
};
|
|
194
194
|
|
|
195
|
-
process.on("exit",
|
|
195
|
+
process.on("exit", () => {
|
|
196
|
+
cleanup();
|
|
197
|
+
});
|
|
196
198
|
process.on("SIGINT", () => {
|
|
197
199
|
cleanup();
|
|
198
200
|
process.exit(130);
|
|
@@ -206,10 +208,10 @@ function registerCleanup(): void {
|
|
|
206
208
|
/**
|
|
207
209
|
* Clear the cached snapshot (for testing or forced refresh).
|
|
208
210
|
*/
|
|
209
|
-
export function clearSnapshotCache(): void {
|
|
210
|
-
if (cachedSnapshotPath &&
|
|
211
|
+
export async function clearSnapshotCache(): Promise<void> {
|
|
212
|
+
if (cachedSnapshotPath && (await Bun.file(cachedSnapshotPath).exists())) {
|
|
211
213
|
try {
|
|
212
|
-
|
|
214
|
+
Bun.spawnSync(["rm", cachedSnapshotPath]);
|
|
213
215
|
} catch {
|
|
214
216
|
// Ignore
|
|
215
217
|
}
|