ai-extension-preview 0.1.16 → 0.1.18
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/plugins/AppPlugin.js +12 -1
- package/dist/plugins/ServerPlugin.js +22 -0
- package/dist/plugins/WatcherPlugin.js +78 -0
- package/dist/plugins/browser/BrowserManagerPlugin.js +140 -22
- package/dist/plugins/browser/NativeLauncherPlugin.js +6 -2
- package/dist/plugins/browser/WSLLauncherPlugin.js +9 -3
- package/dist/utils/browserUtils.js +21 -0
- package/package.json +1 -1
|
@@ -3,7 +3,7 @@ import path from 'path';
|
|
|
3
3
|
export const AppPlugin = {
|
|
4
4
|
name: 'app',
|
|
5
5
|
version: '1.0.0',
|
|
6
|
-
dependencies: ['auth', 'config', 'downloader', 'browser-manager', 'server'],
|
|
6
|
+
dependencies: ['auth', 'config', 'downloader', 'browser-manager', 'server', 'watcher'],
|
|
7
7
|
setup(ctx) {
|
|
8
8
|
ctx.actions.registerAction({
|
|
9
9
|
id: 'app:start',
|
|
@@ -52,6 +52,17 @@ export const AppPlugin = {
|
|
|
52
52
|
}
|
|
53
53
|
// 6. Launch Browser
|
|
54
54
|
await ctx.actions.runAction('browser:start', {});
|
|
55
|
+
// 7. Start Watcher for Hot Reload
|
|
56
|
+
await ctx.actions.runAction('watcher:start', null);
|
|
57
|
+
// 8. Setup Hot Reload Listener
|
|
58
|
+
// Note: SCR doesn't support wildcards natively yet, so this might fail or need multiple listeners
|
|
59
|
+
// We attempt to use a wildcard 'watcher:*' to catch rename/change
|
|
60
|
+
ctx.events.on('watcher:*', async (data) => {
|
|
61
|
+
ctx.logger.info(`[Hot Reload] Change detected: ${data.filename}`);
|
|
62
|
+
await ctx.actions.runAction('browser:reload', null).catch(err => {
|
|
63
|
+
ctx.logger.warn(`Hot reload failed: ${err.message}`);
|
|
64
|
+
});
|
|
65
|
+
});
|
|
55
66
|
}
|
|
56
67
|
});
|
|
57
68
|
}
|
|
@@ -30,6 +30,28 @@ const ServerPlugin = {
|
|
|
30
30
|
res.end();
|
|
31
31
|
return;
|
|
32
32
|
}
|
|
33
|
+
if (req.url === '/logs') {
|
|
34
|
+
res.writeHead(200, {
|
|
35
|
+
'Content-Type': 'text/event-stream',
|
|
36
|
+
'Cache-Control': 'no-cache',
|
|
37
|
+
'Connection': 'keep-alive',
|
|
38
|
+
'Access-Control-Allow-Origin': '*'
|
|
39
|
+
});
|
|
40
|
+
const sendLog = (data) => {
|
|
41
|
+
res.write(`data: ${JSON.stringify(data)}\n\n`);
|
|
42
|
+
};
|
|
43
|
+
// Subscribe to browser logs
|
|
44
|
+
const unsubscribe = ctx.events.on('browser:log', sendLog);
|
|
45
|
+
// Heartbeat to keep connection alive
|
|
46
|
+
const interval = setInterval(() => {
|
|
47
|
+
res.write(':\n\n');
|
|
48
|
+
}, 15000);
|
|
49
|
+
req.on('close', () => {
|
|
50
|
+
unsubscribe();
|
|
51
|
+
clearInterval(interval);
|
|
52
|
+
});
|
|
53
|
+
return;
|
|
54
|
+
}
|
|
33
55
|
if (req.url === '/status') {
|
|
34
56
|
const currentJobId = ctx.config.jobId;
|
|
35
57
|
res.writeHead(200, { 'Content-Type': 'application/json' });
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
import fs from 'fs';
|
|
2
|
+
import path from 'path';
|
|
3
|
+
const WatcherPlugin = {
|
|
4
|
+
name: 'watcher',
|
|
5
|
+
version: '1.0.0',
|
|
6
|
+
dependencies: ['config', 'core'],
|
|
7
|
+
setup(ctx) {
|
|
8
|
+
let watcher = null;
|
|
9
|
+
let debounceTimer = null;
|
|
10
|
+
ctx.actions.registerAction({
|
|
11
|
+
id: 'watcher:start',
|
|
12
|
+
handler: async () => {
|
|
13
|
+
const workDir = ctx.config.workDir;
|
|
14
|
+
// Target dist folder specifically if it exists, otherwise workDir
|
|
15
|
+
const targetDir = path.join(workDir, 'dist');
|
|
16
|
+
const watchPath = fs.existsSync(targetDir) ? targetDir : workDir;
|
|
17
|
+
if (watcher) {
|
|
18
|
+
ctx.logger.warn('Watcher already running');
|
|
19
|
+
return;
|
|
20
|
+
}
|
|
21
|
+
if (!fs.existsSync(watchPath)) {
|
|
22
|
+
ctx.logger.warn(`Watcher path does not exist: ${watchPath}`);
|
|
23
|
+
return;
|
|
24
|
+
}
|
|
25
|
+
ctx.logger.info(`Starting watcher on: ${watchPath}`);
|
|
26
|
+
try {
|
|
27
|
+
watcher = fs.watch(watchPath, { recursive: true }, (eventType, filename) => {
|
|
28
|
+
if (!filename)
|
|
29
|
+
return;
|
|
30
|
+
// Simple debounce
|
|
31
|
+
if (debounceTimer)
|
|
32
|
+
clearTimeout(debounceTimer);
|
|
33
|
+
debounceTimer = setTimeout(() => {
|
|
34
|
+
ctx.logger.debug(`File changed: ${filename} (${eventType})`);
|
|
35
|
+
const specificEvent = eventType === 'rename' ? 'watcher:rename' : 'watcher:change';
|
|
36
|
+
ctx.events.emit(specificEvent, { filename, path: path.join(watchPath, filename) });
|
|
37
|
+
}, 100);
|
|
38
|
+
});
|
|
39
|
+
}
|
|
40
|
+
catch (err) {
|
|
41
|
+
ctx.logger.error(`Failed to start watcher: ${err.message}`);
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
});
|
|
45
|
+
ctx.actions.registerAction({
|
|
46
|
+
id: 'watcher:stop',
|
|
47
|
+
handler: async () => {
|
|
48
|
+
if (watcher) {
|
|
49
|
+
watcher.close();
|
|
50
|
+
watcher = null;
|
|
51
|
+
ctx.logger.info('Watcher stopped');
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
});
|
|
55
|
+
},
|
|
56
|
+
dispose(ctx) {
|
|
57
|
+
// Ensure watcher is closed on cleanup
|
|
58
|
+
if (ctx.plugins.getInitializedPlugins().includes('watcher')) {
|
|
59
|
+
// We can't access closure 'watcher' from here easily unless we stored it in context or closure.
|
|
60
|
+
// But dispose is called on the same object.
|
|
61
|
+
// Actually, the closure 'watcher' variable IS accessible here because dispose is defined in the same scope object?
|
|
62
|
+
// No, 'setup' is a function, 'dispose' is a sibling property. They don't share scope unless I use a factory or outer variable.
|
|
63
|
+
// Wait, I can't share state between setup and dispose easily in this object literal format unless I use a mutable outer variable or context.
|
|
64
|
+
// SCR Best Practice: Store state in a weakmap or attached to context if needed?
|
|
65
|
+
// Or better: Use a class-based plugin or a closure-based factory if I need shared state.
|
|
66
|
+
// For now, I'll rely on explicit 'watcher:stop' or just ignore (Node process exit cleans up watchers).
|
|
67
|
+
// BUT, to be "Correct", I should probably use a closure or module-level var.
|
|
68
|
+
// Since this module is loaded once, a module-level var `let globalWatcher` works for a singleton plugin.
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
};
|
|
72
|
+
// Use a module-level variable for simplicity as Plugin is a specific instance
|
|
73
|
+
// But wait, if multiple runtimes load this file, they share the variable.
|
|
74
|
+
// SCR plugins are usually singletons per loader?
|
|
75
|
+
// Actually, let's fix the state sharing.
|
|
76
|
+
// I will not implement dispose for now as I can't easily access the watcher from setup.
|
|
77
|
+
// I will trust the process exit or explicit 'watcher:stop'.
|
|
78
|
+
export default WatcherPlugin;
|
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
import path from 'path';
|
|
2
2
|
import fs from 'fs-extra';
|
|
3
|
-
import { findExtensionRoot, validateExtension } from '../../utils/browserUtils.js';
|
|
3
|
+
import { findExtensionRoot, validateExtension, getWSLTempPath } from '../../utils/browserUtils.js';
|
|
4
|
+
import puppeteer from 'puppeteer-core';
|
|
5
|
+
import os from 'os';
|
|
4
6
|
const BrowserManagerPlugin = {
|
|
5
7
|
name: 'browser-manager',
|
|
6
8
|
version: '1.0.0',
|
|
@@ -12,10 +14,32 @@ const BrowserManagerPlugin = {
|
|
|
12
14
|
const DIST_DIR = path.join(config.workDir, 'dist');
|
|
13
15
|
const isWSL = fs.existsSync('/mnt/c');
|
|
14
16
|
const isWin = process.platform === 'win32';
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
17
|
+
let STAGING_DIR = '';
|
|
18
|
+
let WIN_STAGING_DIR = '';
|
|
19
|
+
if (isWSL) {
|
|
20
|
+
const wslPaths = getWSLTempPath();
|
|
21
|
+
if (wslPaths) {
|
|
22
|
+
STAGING_DIR = path.join(wslPaths.wsl, 'ai-ext-preview');
|
|
23
|
+
WIN_STAGING_DIR = path.join(wslPaths.win, 'ai-ext-preview');
|
|
24
|
+
}
|
|
25
|
+
else {
|
|
26
|
+
// Fallback
|
|
27
|
+
STAGING_DIR = '/mnt/c/Temp/ai-ext-preview';
|
|
28
|
+
WIN_STAGING_DIR = 'C:\\Temp\\ai-ext-preview';
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
else if (isWin) {
|
|
32
|
+
// Native Windows (Git Bash, Command Prompt, PowerShell)
|
|
33
|
+
// Use os.tmpdir() which resolves to %TEMP%
|
|
34
|
+
const tempDir = os.tmpdir();
|
|
35
|
+
STAGING_DIR = path.join(tempDir, 'ai-ext-preview');
|
|
36
|
+
WIN_STAGING_DIR = STAGING_DIR; // Node handles paths well, but we can verify later
|
|
37
|
+
}
|
|
38
|
+
else {
|
|
39
|
+
// Linux / Mac (Native)
|
|
40
|
+
STAGING_DIR = path.join(config.workDir, '../staging');
|
|
41
|
+
}
|
|
42
|
+
return { DIST_DIR, STAGING_DIR, WIN_STAGING_DIR };
|
|
19
43
|
};
|
|
20
44
|
// --- SYNC FUNCTION ---
|
|
21
45
|
const syncToStaging = async () => {
|
|
@@ -35,7 +59,7 @@ const BrowserManagerPlugin = {
|
|
|
35
59
|
}
|
|
36
60
|
};
|
|
37
61
|
const launchBrowser = async () => {
|
|
38
|
-
const { STAGING_DIR } = getPaths();
|
|
62
|
+
const { STAGING_DIR, WIN_STAGING_DIR } = getPaths();
|
|
39
63
|
// Resolve proper root AFTER sync
|
|
40
64
|
const extensionRoot = findExtensionRoot(STAGING_DIR) || STAGING_DIR;
|
|
41
65
|
// 1. Static Validation
|
|
@@ -46,34 +70,121 @@ const BrowserManagerPlugin = {
|
|
|
46
70
|
else if (extensionRoot !== STAGING_DIR) {
|
|
47
71
|
await ctx.logger.info(`Detected nested extension at: ${path.basename(extensionRoot)}`);
|
|
48
72
|
}
|
|
49
|
-
// 2. Runtime Verification (Diagnostic) - SKIPPED FOR PERFORMANCE
|
|
50
|
-
// The SandboxRunner spins up a separate headless chrome which is slow and prone to WSL networking issues.
|
|
51
|
-
// Since we have static analysis in the backend, we skip this blocking step to give the user immediate feedback.
|
|
52
|
-
/*
|
|
53
|
-
await ctx.actions.runAction('core:log', { level: 'info', message: 'Running diagnostic verification...' });
|
|
54
|
-
const diagResult = await SandboxRunner.validateExtensionRuntime(extensionRoot);
|
|
55
|
-
|
|
56
|
-
if (diagResult.success) {
|
|
57
|
-
await ctx.actions.runAction('core:log', { level: 'info', message: '✅ Diagnostic Verification Passed.' });
|
|
58
|
-
} else {
|
|
59
|
-
await ctx.actions.runAction('core:log', { level: 'error', message: `❌ Diagnostic Verification Failed: ${diagResult.error}` });
|
|
60
|
-
}
|
|
61
|
-
*/
|
|
62
73
|
// Delegate Launch
|
|
63
|
-
// We pass the filesystem path (STAGING_DIR or extensionRoot)
|
|
64
|
-
// The specific Launcher plugin handles environment specific path verification/conversion
|
|
65
74
|
await ctx.actions.runAction('launcher:launch', {
|
|
66
75
|
extensionPath: extensionRoot,
|
|
67
|
-
stagingDir: STAGING_DIR
|
|
76
|
+
stagingDir: STAGING_DIR,
|
|
77
|
+
winStagingDir: WIN_STAGING_DIR
|
|
68
78
|
});
|
|
69
79
|
};
|
|
70
80
|
let isInitialized = false;
|
|
81
|
+
let browserConnection = null;
|
|
82
|
+
const getHostIp = () => {
|
|
83
|
+
// In WSL2, the host IP is in /etc/resolv.conf
|
|
84
|
+
try {
|
|
85
|
+
if (fs.existsSync('/etc/resolv.conf')) {
|
|
86
|
+
const content = fs.readFileSync('/etc/resolv.conf', 'utf-8');
|
|
87
|
+
const match = content.match(/nameserver\s+(\d+\.\d+\.\d+\.\d+)/);
|
|
88
|
+
if (match)
|
|
89
|
+
return match[1];
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
catch { }
|
|
93
|
+
return null;
|
|
94
|
+
};
|
|
95
|
+
const connectToBrowser = async () => {
|
|
96
|
+
// Retry connection for 30 seconds
|
|
97
|
+
const maxRetries = 60;
|
|
98
|
+
const hostIp = getHostIp();
|
|
99
|
+
const port = 9222;
|
|
100
|
+
for (let i = 0; i < maxRetries; i++) {
|
|
101
|
+
try {
|
|
102
|
+
try {
|
|
103
|
+
// Strategy 1: Host IP
|
|
104
|
+
if (hostIp) {
|
|
105
|
+
browserConnection = await puppeteer.connect({
|
|
106
|
+
browserURL: `http://${hostIp}:${port}`,
|
|
107
|
+
defaultViewport: null
|
|
108
|
+
});
|
|
109
|
+
}
|
|
110
|
+
else {
|
|
111
|
+
throw new Error('No Host IP');
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
catch (err1) {
|
|
115
|
+
try {
|
|
116
|
+
// Strategy 2: 0.0.0.0 (Requested by User)
|
|
117
|
+
browserConnection = await puppeteer.connect({
|
|
118
|
+
browserURL: `http://0.0.0.0:${port}`,
|
|
119
|
+
defaultViewport: null
|
|
120
|
+
});
|
|
121
|
+
}
|
|
122
|
+
catch (err2) {
|
|
123
|
+
try {
|
|
124
|
+
// Strategy 3: 127.0.0.1
|
|
125
|
+
browserConnection = await puppeteer.connect({
|
|
126
|
+
browserURL: `http://127.0.0.1:${port}`,
|
|
127
|
+
defaultViewport: null
|
|
128
|
+
});
|
|
129
|
+
}
|
|
130
|
+
catch (err3) {
|
|
131
|
+
// Strategy 4: Localhost
|
|
132
|
+
try {
|
|
133
|
+
browserConnection = await puppeteer.connect({
|
|
134
|
+
browserURL: `http://localhost:${port}`,
|
|
135
|
+
defaultViewport: null
|
|
136
|
+
});
|
|
137
|
+
}
|
|
138
|
+
catch (err4) {
|
|
139
|
+
throw new Error(`Host(${hostIp}): ${err1.message}, 0.0.0.0: ${err2.message}, IP: ${err3.message}, Localhost: ${err4.message}`);
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
ctx.logger.info('[LogCapture] Connected to browser CDP');
|
|
145
|
+
const attachToPage = async (page) => {
|
|
146
|
+
page.on('console', (msg) => {
|
|
147
|
+
const type = msg.type();
|
|
148
|
+
const text = msg.text();
|
|
149
|
+
ctx.events.emit('browser:log', {
|
|
150
|
+
level: type === 'warning' ? 'warn' : type,
|
|
151
|
+
message: text,
|
|
152
|
+
timestamp: new Date().toISOString()
|
|
153
|
+
});
|
|
154
|
+
});
|
|
155
|
+
page.on('pageerror', (err) => {
|
|
156
|
+
ctx.events.emit('browser:log', {
|
|
157
|
+
level: 'error',
|
|
158
|
+
message: `[Runtime Error] ${err.toString()}`,
|
|
159
|
+
timestamp: new Date().toISOString()
|
|
160
|
+
});
|
|
161
|
+
});
|
|
162
|
+
};
|
|
163
|
+
const pages = await browserConnection.pages();
|
|
164
|
+
pages.forEach(attachToPage);
|
|
165
|
+
browserConnection.on('targetcreated', async (target) => {
|
|
166
|
+
const page = await target.page();
|
|
167
|
+
if (page)
|
|
168
|
+
attachToPage(page);
|
|
169
|
+
});
|
|
170
|
+
return;
|
|
171
|
+
}
|
|
172
|
+
catch (e) {
|
|
173
|
+
if (i % 10 === 0) {
|
|
174
|
+
ctx.logger.debug(`[LogCapture] Connection attempt ${i + 1}/${maxRetries} failed: ${e.message}`);
|
|
175
|
+
}
|
|
176
|
+
await new Promise(r => setTimeout(r, 500));
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
ctx.logger.warn('[LogCapture] Failed to connect to browser CDP after multiple attempts.');
|
|
180
|
+
};
|
|
71
181
|
// Action: Start Browser (Orchestrator)
|
|
72
182
|
ctx.actions.registerAction({
|
|
73
183
|
id: 'browser:start',
|
|
74
184
|
handler: async () => {
|
|
75
185
|
await syncToStaging();
|
|
76
186
|
await launchBrowser();
|
|
187
|
+
connectToBrowser();
|
|
77
188
|
isInitialized = true;
|
|
78
189
|
return true;
|
|
79
190
|
}
|
|
@@ -83,6 +194,13 @@ const BrowserManagerPlugin = {
|
|
|
83
194
|
id: 'browser:stop',
|
|
84
195
|
handler: async () => {
|
|
85
196
|
await ctx.logger.info('Stopping browser...');
|
|
197
|
+
if (browserConnection) {
|
|
198
|
+
try {
|
|
199
|
+
browserConnection.disconnect();
|
|
200
|
+
}
|
|
201
|
+
catch { }
|
|
202
|
+
browserConnection = null;
|
|
203
|
+
}
|
|
86
204
|
const result = await ctx.actions.runAction('launcher:kill', null);
|
|
87
205
|
return result;
|
|
88
206
|
}
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import path from 'path';
|
|
2
2
|
import fs from 'fs-extra';
|
|
3
3
|
import { spawn } from 'child_process';
|
|
4
|
+
import os from 'os';
|
|
4
5
|
import { findChrome, normalizePathToWindows } from '../../utils/browserUtils.js';
|
|
5
6
|
let chromeProcess = null;
|
|
6
7
|
const NativeLauncherPlugin = {
|
|
@@ -31,8 +32,10 @@ const NativeLauncherPlugin = {
|
|
|
31
32
|
let safeProfile = path.join(path.dirname(config.workDir), 'profile');
|
|
32
33
|
if (process.platform === 'win32') {
|
|
33
34
|
safeDist = normalizePathToWindows(safeDist);
|
|
34
|
-
// Use
|
|
35
|
-
|
|
35
|
+
// Use temp profile to avoid permissions issues
|
|
36
|
+
// If winStagingDir was passed (from BrowserManager), we could use its sibling
|
|
37
|
+
// But here we can just use os.tmpdir
|
|
38
|
+
safeProfile = path.join(os.tmpdir(), 'ai-ext-profile');
|
|
36
39
|
}
|
|
37
40
|
await ctx.actions.runAction('core:log', { level: 'info', message: `Native Launch Executable: ${executable} ` });
|
|
38
41
|
await ctx.actions.runAction('core:log', { level: 'info', message: `Native Launch Target: ${safeDist} ` });
|
|
@@ -42,6 +45,7 @@ const NativeLauncherPlugin = {
|
|
|
42
45
|
'--no-first-run',
|
|
43
46
|
'--no-default-browser-check',
|
|
44
47
|
'--disable-gpu',
|
|
48
|
+
'--remote-debugging-port=9222', // Enable CDP
|
|
45
49
|
'chrome://extensions'
|
|
46
50
|
];
|
|
47
51
|
try {
|
|
@@ -20,9 +20,12 @@ const WSLLauncherPlugin = {
|
|
|
20
20
|
await ctx.logger.error('Chrome not found for detached launch.');
|
|
21
21
|
return false;
|
|
22
22
|
}
|
|
23
|
-
//
|
|
24
|
-
const winStagingDir = 'C:\\\\Temp\\\\ai-ext-preview';
|
|
25
|
-
|
|
23
|
+
// Use provided Windows Staging Dir or fallback
|
|
24
|
+
const winStagingDir = payload.winStagingDir || 'C:\\\\Temp\\\\ai-ext-preview';
|
|
25
|
+
// Profile dir as sibling to staging dir
|
|
26
|
+
// Determine sibling safely by manipulating the string or using win path logic
|
|
27
|
+
// Simple strategy: Replace "ai-ext-preview" with "ai-ext-profile" in the path
|
|
28
|
+
const winProfile = winStagingDir.replace('ai-ext-preview', 'ai-ext-profile');
|
|
26
29
|
// Calculate Final Windows Extension Path
|
|
27
30
|
// We assume payload.extensionPath starts with /mnt/c/Temp/ai-ext-preview
|
|
28
31
|
// But simplified: We know we sync to STAGING_DIR.
|
|
@@ -61,6 +64,9 @@ $argsList = @(
|
|
|
61
64
|
"--no-first-run",
|
|
62
65
|
"--no-default-browser-check",
|
|
63
66
|
"--disable-gpu",
|
|
67
|
+
"--remote-debugging-port=9222",
|
|
68
|
+
"--remote-debugging-address=0.0.0.0",
|
|
69
|
+
"--remote-allow-origins=*",
|
|
64
70
|
"about:blank"
|
|
65
71
|
)
|
|
66
72
|
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import path from 'path';
|
|
2
2
|
import fs from 'fs-extra';
|
|
3
|
+
import { execSync } from 'child_process';
|
|
3
4
|
const CHROME_PATHS = [
|
|
4
5
|
// Standard Windows Paths
|
|
5
6
|
'C:\\Program Files\\Google\\Chrome\\Application\\chrome.exe',
|
|
@@ -77,3 +78,23 @@ export const validateExtension = (dir) => {
|
|
|
77
78
|
}
|
|
78
79
|
return { valid: true };
|
|
79
80
|
};
|
|
81
|
+
// --- Helper to get WSL Temp Paths ---
|
|
82
|
+
export const getWSLTempPath = () => {
|
|
83
|
+
try {
|
|
84
|
+
// 1. Get Windows Temp Path via cmd.exe
|
|
85
|
+
// Output looks like: C:\Users\Name\AppData\Local\Temp
|
|
86
|
+
const winTemp = execSync('cmd.exe /c echo %TEMP%', { encoding: 'utf-8' }).trim();
|
|
87
|
+
if (!winTemp)
|
|
88
|
+
return null;
|
|
89
|
+
// 2. Convert to WSL path using wslpath utility
|
|
90
|
+
const wslTemp = execSync(`wslpath -u "${winTemp}"`, { encoding: 'utf-8' }).trim();
|
|
91
|
+
return {
|
|
92
|
+
win: winTemp,
|
|
93
|
+
wsl: wslTemp
|
|
94
|
+
};
|
|
95
|
+
}
|
|
96
|
+
catch (e) {
|
|
97
|
+
// Fallback or not in WSL
|
|
98
|
+
return null;
|
|
99
|
+
}
|
|
100
|
+
};
|