ai-extension-preview 0.1.17 → 0.1.19

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/index.js CHANGED
@@ -28,6 +28,8 @@ const WORK_DIR = path.join(HOME_DIR, '.ai-extension-preview', options.job || 'de
28
28
  (async () => {
29
29
  const { job: initialJobId, host, token, user: userId } = options;
30
30
  // 1. Initialize Runtime with Config
31
+ // Fix for Windows: Glob patterns in SCR DirectoryLoader require forward slashes
32
+ const pluginDir = path.join(__dirname, 'plugins').replace(/\\/g, '/');
31
33
  const runtime = new Runtime({
32
34
  config: {
33
35
  host,
@@ -37,9 +39,10 @@ const WORK_DIR = path.join(HOME_DIR, '.ai-extension-preview', options.job || 'de
37
39
  workDir: WORK_DIR
38
40
  },
39
41
  hostContext: {}, // Clear hostContext config wrapping
40
- pluginPaths: [path.join(__dirname, 'plugins')] // [NEW] Auto-discovery
42
+ pluginPaths: [pluginDir] // [NEW] Auto-discovery
41
43
  });
42
44
  // Register Plugins
45
+ runtime.logger.info(`Loading plugins from: ${pluginDir}`);
43
46
  runtime.logger.info('Initializing runtime...');
44
47
  await runtime.initialize();
45
48
  const ctx = runtime.getContext();
@@ -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' });
@@ -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,34 @@ 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
- const STAGING_DIR = isWSL
16
- ? '/mnt/c/Temp/ai-ext-preview'
17
- : (isWin ? 'C:\\Temp\\ai-ext-preview' : path.join(config.workDir, '../staging'));
18
- return { DIST_DIR, STAGING_DIR };
17
+ let STAGING_DIR = '';
18
+ let WIN_STAGING_DIR = '';
19
+ if (isWSL) {
20
+ const wslPaths = getWSLTempPath();
21
+ if (wslPaths) {
22
+ const folder = 'ai-ext-preview';
23
+ STAGING_DIR = path.join(wslPaths.wsl, folder);
24
+ // Force Windows Backslashes for WIN_STAGING_DIR
25
+ WIN_STAGING_DIR = `${wslPaths.win}\\${folder}`.replace(/\//g, '\\');
26
+ }
27
+ else {
28
+ // Fallback
29
+ STAGING_DIR = '/mnt/c/Temp/ai-ext-preview';
30
+ WIN_STAGING_DIR = 'C:\\Temp\\ai-ext-preview';
31
+ }
32
+ }
33
+ else if (isWin) {
34
+ // Native Windows (Git Bash, Command Prompt, PowerShell)
35
+ // Use os.tmpdir() which resolves to %TEMP%
36
+ const tempDir = os.tmpdir();
37
+ STAGING_DIR = path.join(tempDir, 'ai-ext-preview');
38
+ WIN_STAGING_DIR = STAGING_DIR; // Node handles paths well, but we can verify later
39
+ }
40
+ else {
41
+ // Linux / Mac (Native)
42
+ STAGING_DIR = path.join(config.workDir, '../staging');
43
+ }
44
+ return { DIST_DIR, STAGING_DIR, WIN_STAGING_DIR };
19
45
  };
20
46
  // --- SYNC FUNCTION ---
21
47
  const syncToStaging = async () => {
@@ -35,7 +61,7 @@ const BrowserManagerPlugin = {
35
61
  }
36
62
  };
37
63
  const launchBrowser = async () => {
38
- const { STAGING_DIR } = getPaths();
64
+ const { STAGING_DIR, WIN_STAGING_DIR } = getPaths();
39
65
  // Resolve proper root AFTER sync
40
66
  const extensionRoot = findExtensionRoot(STAGING_DIR) || STAGING_DIR;
41
67
  // 1. Static Validation
@@ -46,34 +72,140 @@ const BrowserManagerPlugin = {
46
72
  else if (extensionRoot !== STAGING_DIR) {
47
73
  await ctx.logger.info(`Detected nested extension at: ${path.basename(extensionRoot)}`);
48
74
  }
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}` });
75
+ // Debug: List files in staging to verify extension presence
76
+ try {
77
+ const files = await fs.readdir(extensionRoot);
78
+ await ctx.logger.info(`[DEBUG] Files in Staging (${extensionRoot}): ${files.join(', ')}`);
79
+ await ctx.logger.info(`[DEBUG] WIN_STAGING_DIR: ${WIN_STAGING_DIR}`);
80
+ }
81
+ catch (e) {
82
+ await ctx.logger.error(`[DEBUG] Failed to list staging files: ${e.message}`);
60
83
  }
61
- */
62
84
  // Delegate Launch
63
- // We pass the filesystem path (STAGING_DIR or extensionRoot)
64
- // The specific Launcher plugin handles environment specific path verification/conversion
65
85
  await ctx.actions.runAction('launcher:launch', {
66
86
  extensionPath: extensionRoot,
67
- stagingDir: STAGING_DIR
87
+ stagingDir: STAGING_DIR,
88
+ winStagingDir: WIN_STAGING_DIR
68
89
  });
69
90
  };
70
91
  let isInitialized = false;
92
+ let browserConnection = null;
93
+ const getHostIp = () => {
94
+ // In WSL2, the host IP is in /etc/resolv.conf
95
+ try {
96
+ if (fs.existsSync('/etc/resolv.conf')) {
97
+ const content = fs.readFileSync('/etc/resolv.conf', 'utf-8');
98
+ const match = content.match(/nameserver\s+(\d+\.\d+\.\d+\.\d+)/);
99
+ if (match)
100
+ return match[1];
101
+ }
102
+ }
103
+ catch { }
104
+ return null;
105
+ };
106
+ const connectToBrowser = async () => {
107
+ // Retry connection for 30 seconds
108
+ const maxRetries = 60;
109
+ const hostIp = getHostIp();
110
+ const port = 9222;
111
+ for (let i = 0; i < maxRetries; i++) {
112
+ const errors = [];
113
+ try {
114
+ try {
115
+ // Strategy 1: Host IP
116
+ if (hostIp) {
117
+ browserConnection = await puppeteer.connect({
118
+ browserURL: `http://${hostIp}:${port}`,
119
+ defaultViewport: null
120
+ });
121
+ }
122
+ else {
123
+ throw new Error('No Host IP');
124
+ }
125
+ }
126
+ catch (err1) {
127
+ errors.push(`Host(${hostIp}): ${err1.message}`);
128
+ try {
129
+ // Strategy 2: 0.0.0.0 (Requested by User)
130
+ browserConnection = await puppeteer.connect({
131
+ browserURL: `http://0.0.0.0:${port}`,
132
+ defaultViewport: null
133
+ });
134
+ }
135
+ catch (err2) {
136
+ errors.push(`0.0.0.0: ${err2.message}`);
137
+ try {
138
+ // Strategy 3: 127.0.0.1
139
+ browserConnection = await puppeteer.connect({
140
+ browserURL: `http://127.0.0.1:${port}`,
141
+ defaultViewport: null
142
+ });
143
+ }
144
+ catch (err3) {
145
+ errors.push(`127.0.0.1: ${err3.message}`);
146
+ // Strategy 4: Localhost
147
+ try {
148
+ browserConnection = await puppeteer.connect({
149
+ browserURL: `http://localhost:${port}`,
150
+ defaultViewport: null
151
+ });
152
+ }
153
+ catch (err4) {
154
+ errors.push(`Localhost: ${err4.message}`);
155
+ const combinedError = errors.join(', ');
156
+ if (i === maxRetries - 1 && hostIp) {
157
+ // Final attempt hint
158
+ throw new Error(`${combinedError}. [HINT] Check Windows Firewall for port ${port}.`);
159
+ }
160
+ throw new Error(combinedError);
161
+ }
162
+ }
163
+ }
164
+ }
165
+ ctx.logger.info('[LogCapture] Connected to browser CDP');
166
+ const attachToPage = async (page) => {
167
+ page.on('console', (msg) => {
168
+ const type = msg.type();
169
+ const text = msg.text();
170
+ ctx.events.emit('browser:log', {
171
+ level: type === 'warning' ? 'warn' : type,
172
+ message: text,
173
+ timestamp: new Date().toISOString()
174
+ });
175
+ });
176
+ page.on('pageerror', (err) => {
177
+ ctx.events.emit('browser:log', {
178
+ level: 'error',
179
+ message: `[Runtime Error] ${err.toString()}`,
180
+ timestamp: new Date().toISOString()
181
+ });
182
+ });
183
+ };
184
+ const pages = await browserConnection.pages();
185
+ pages.forEach(attachToPage);
186
+ browserConnection.on('targetcreated', async (target) => {
187
+ const page = await target.page();
188
+ if (page)
189
+ attachToPage(page);
190
+ });
191
+ return;
192
+ }
193
+ catch (e) {
194
+ if (i % 10 === 0) {
195
+ ctx.logger.debug(`[LogCapture] Connection attempt ${i + 1}/${maxRetries} failed: ${e.message}`);
196
+ }
197
+ await new Promise(r => setTimeout(r, 500));
198
+ }
199
+ }
200
+ ctx.logger.warn('[LogCapture] Failed to connect to browser CDP after multiple attempts.');
201
+ };
71
202
  // Action: Start Browser (Orchestrator)
72
203
  ctx.actions.registerAction({
73
204
  id: 'browser:start',
74
205
  handler: async () => {
75
206
  await syncToStaging();
76
207
  await launchBrowser();
208
+ connectToBrowser();
77
209
  isInitialized = true;
78
210
  return true;
79
211
  }
@@ -83,6 +215,13 @@ const BrowserManagerPlugin = {
83
215
  id: 'browser:stop',
84
216
  handler: async () => {
85
217
  await ctx.logger.info('Stopping browser...');
218
+ if (browserConnection) {
219
+ try {
220
+ browserConnection.disconnect();
221
+ }
222
+ catch { }
223
+ browserConnection = null;
224
+ }
86
225
  const result = await ctx.actions.runAction('launcher:kill', null);
87
226
  return result;
88
227
  }
@@ -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 = {
@@ -30,10 +31,15 @@ const NativeLauncherPlugin = {
30
31
  // Default Profile
31
32
  let safeProfile = path.join(path.dirname(config.workDir), 'profile');
32
33
  if (process.platform === 'win32') {
33
- safeDist = normalizePathToWindows(safeDist);
34
- // Use C:\\Temp profile to avoid permissions issues
35
- safeProfile = 'C:\\\\Temp\\\\ai-ext-profile';
34
+ // Ensure backslashes are used everywhere
35
+ safeDist = normalizePathToWindows(safeDist).replace(/\//g, '\\');
36
+ // Use temp profile to avoid permissions issues
37
+ // If winStagingDir was passed (from BrowserManager), we could use its sibling
38
+ // But here we can just use os.tmpdir
39
+ safeProfile = path.join(os.tmpdir(), 'ai-ext-profile').replace(/\//g, '\\');
36
40
  }
41
+ await ctx.logger.info(`[DEBUG] Native Chrome Extension Path: ${safeDist}`);
42
+ await ctx.logger.info(`[DEBUG] Native Chrome Profile Path: ${safeProfile}`);
37
43
  await ctx.actions.runAction('core:log', { level: 'info', message: `Native Launch Executable: ${executable} ` });
38
44
  await ctx.actions.runAction('core:log', { level: 'info', message: `Native Launch Target: ${safeDist} ` });
39
45
  const cleanArgs = [
@@ -42,8 +48,18 @@ const NativeLauncherPlugin = {
42
48
  '--no-first-run',
43
49
  '--no-default-browser-check',
44
50
  '--disable-gpu',
51
+ '--remote-debugging-port=9222', // Enable CDP
45
52
  'chrome://extensions'
46
53
  ];
54
+ // --- Developer Debug UI ---
55
+ console.log('\n' + '─'.repeat(50));
56
+ console.log(' 🛠️ DEBUG: NATIVE LAUNCH CONFIGURATION');
57
+ console.log('─'.repeat(50));
58
+ console.log(`Executable: ${executable}`);
59
+ console.log('Arguments:');
60
+ cleanArgs.forEach(arg => console.log(` ${arg}`));
61
+ console.log('─'.repeat(50) + '\n');
62
+ // ---------------------------
47
63
  try {
48
64
  // Kill existing process if any
49
65
  if (chromeProcess) {
@@ -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
- // Hardcoded Safe Paths for WSL Strategy
24
- const winStagingDir = 'C:\\\\Temp\\\\ai-ext-preview';
25
- const winProfile = 'C:\\\\Temp\\\\ai-ext-profile';
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.
@@ -30,19 +33,45 @@ const WSLLauncherPlugin = {
30
33
  let finalWinExtensionPath = winStagingDir;
31
34
  if (payload.extensionPath !== payload.stagingDir) {
32
35
  const relative = path.relative(payload.stagingDir, payload.extensionPath);
33
- // Join with backslashes
34
- finalWinExtensionPath = path.posix.join(winStagingDir.replace(/\\\\/g, '/'), relative).replace(/\//g, '\\\\');
36
+ // Standardize separators to backslashes for Windows
37
+ const winStagingClean = winStagingDir.replace(/\//g, '\\');
38
+ finalWinExtensionPath = `${winStagingClean}\\${relative}`.replace(/\//g, '\\');
35
39
  }
40
+ else {
41
+ finalWinExtensionPath = winStagingDir.replace(/\//g, '\\');
42
+ }
43
+ await ctx.logger.info(`[DEBUG] Chrome Extension Path: ${finalWinExtensionPath}`);
44
+ const winProfileClean = winProfile.replace(/\\+$/, '');
45
+ await ctx.logger.info(`[DEBUG] Chrome Extension Path: ${finalWinExtensionPath}`);
46
+ await ctx.logger.info(`[DEBUG] Chrome Profile Path: ${winProfileClean}`);
36
47
  const driveLetter = 'c';
37
48
  const winChromePath = chromePath
38
49
  .replace(new RegExp(`^/mnt/${driveLetter}/`), `${driveLetter.toUpperCase()}:\\\\`)
39
50
  .replace(/\//g, '\\\\');
40
51
  await ctx.logger.info(`WSL Launch Target (Win): ${finalWinExtensionPath}`);
52
+ await ctx.logger.warn('---------------------------------------------------------');
53
+ await ctx.logger.warn('⚠️ WSL DETECTED');
54
+ await ctx.logger.warn('Windows Firewall often blocks connections from WSL to Chrome.');
55
+ await ctx.logger.warn('If connection fails, run this tool from Git Bash or Command Prompt.');
56
+ await ctx.logger.warn('---------------------------------------------------------');
57
+ // --- Developer Debug UI ---
58
+ const debugInfo = [
59
+ ` Chrome Path: ${winChromePath}`,
60
+ `Extension Path: ${finalWinExtensionPath}`,
61
+ ` Profile Path: ${winProfileClean}`,
62
+ `Launch Command: powershell.exe -NoProfile -ExecutionPolicy Bypass -File "${winStagingDir}\\launch.ps1"`
63
+ ];
64
+ console.log('\n' + '─'.repeat(50));
65
+ console.log(' 🛠️ DEBUG: CHROME LAUNCH CONFIGURATION');
66
+ console.log('─'.repeat(50));
67
+ debugInfo.forEach(line => console.log(line));
68
+ console.log('─'.repeat(50) + '\n');
69
+ // ---------------------------
41
70
  // Create PowerShell Launch Script with PID capture
42
71
  const psContent = `
43
72
  $chromePath = "${winChromePath}"
44
73
  $extPath = "${finalWinExtensionPath}"
45
- $profilePath = "${winProfile}"
74
+ $profilePath = "${winProfileClean}"
46
75
 
47
76
  # Verify Paths
48
77
  if (-not (Test-Path -Path $extPath)) {
@@ -55,19 +84,17 @@ if (-not (Test-Path -Path $profilePath)) {
55
84
  New-Item -ItemType Directory -Force -Path $profilePath | Out-Null
56
85
  }
57
86
 
58
- $argsList = @(
59
- "--load-extension=\`"$extPath\`"",
60
- "--user-data-dir=\`"$profilePath\`"",
61
- "--no-first-run",
62
- "--no-default-browser-check",
63
- "--disable-gpu",
64
- "about:blank"
65
- )
87
+ $argsStr = "--load-extension=\`"$extPath\`" --user-data-dir=\`"$profilePath\`" --no-first-run --no-default-browser-check --disable-gpu --remote-debugging-port=9222 --remote-debugging-address=0.0.0.0 --remote-allow-origins=* about:blank"
66
88
 
67
89
  # Launch and capture PID
68
- $process = Start-Process -FilePath $chromePath -ArgumentList $argsList -PassThru
90
+ # Use single string argument to avoid array joining issues
91
+ $process = Start-Process -FilePath $chromePath -ArgumentList $argsStr -PassThru
69
92
  Write-Host "CHROME_PID:$($process.Id)"
70
93
  `;
94
+ console.log(' 📄 DEBUG: GENERATED POWERSHELL SCRIPT');
95
+ console.log('─'.repeat(50));
96
+ console.log(psContent.trim());
97
+ console.log('─'.repeat(50) + '\n');
71
98
  // Write ps1 to STAGING_DIR/launch.ps1
72
99
  const psPath = path.join(payload.stagingDir, 'launch.ps1');
73
100
  try {
@@ -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
+ };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "ai-extension-preview",
3
- "version": "0.1.17",
3
+ "version": "0.1.19",
4
4
  "description": "Local preview tool for AI Extension Builder",
5
5
  "type": "module",
6
6
  "bin": {