ai-extension-preview 0.1.17 → 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.
@@ -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,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
- 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
+ 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 C:\\Temp profile to avoid permissions issues
35
- safeProfile = 'C:\\\\Temp\\\\ai-ext-profile';
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
- // 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.
@@ -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
+ };
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.18",
4
4
  "description": "Local preview tool for AI Extension Builder",
5
5
  "type": "module",
6
6
  "bin": {