ai-extension-preview 0.1.5 → 0.1.7

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
File without changes
@@ -1,4 +1,3 @@
1
- import webExt from 'web-ext';
2
1
  import path from 'path';
3
2
  import { spawn } from 'child_process';
4
3
  import fs from 'fs-extra';
@@ -36,82 +35,96 @@ export const BrowserPlugin = {
36
35
  await ctx.actions.runAction('core:log', { level: 'error', message: 'Chrome not found for detached launch.' });
37
36
  return false;
38
37
  }
39
- // WSL Detection & Handling
40
- let extensionPath = DIST_DIR;
41
- const isWSL = fs.existsSync('/mnt/c'); // Simple check for WSL
42
- if (isWSL) {
38
+ const isWSL = fs.existsSync('/mnt/c');
39
+ let executable = chromePath; // Define in scope
40
+ const STAGING_DIR = isWSL ? '/mnt/c/Temp/ai-ext-preview' : path.join(config.workDir, '../staging');
41
+ const WIN_PROFILE_DIR = 'C:/Temp/ai-ext-profile';
42
+ // For native windows/linux, use local staging path
43
+ // Note: We will evaluate actual extension root later, but base is STAGING_DIR
44
+ const EXTENSION_PATH = isWSL ? 'C:/Temp/ai-ext-preview' : STAGING_DIR;
45
+ // --- SYNC FUNCTION ---
46
+ const syncToStaging = async () => {
43
47
  try {
44
- const WIN_TEMP_DIR = '/mnt/c/Temp/ai-ext-preview';
45
- const WIN_PATH_FOR_CHROME = 'C:/Temp/ai-ext-preview';
46
- // Pre-flight check: Validating WSL Interop
47
- // We try to run cmd.exe simply to check if the OS allows it.
48
- try {
49
- await new Promise((resolve, reject) => {
50
- const check = spawn('cmd.exe', ['/c', 'ver'], { stdio: 'ignore' });
51
- check.on('error', reject);
52
- check.on('close', (code) => {
53
- if (code === 0)
54
- resolve(true);
55
- else
56
- reject(new Error(`Exit code ${code}`));
57
- });
58
- });
59
- }
60
- catch (interopErr) {
61
- await ctx.actions.runAction('core:log', { level: 'error', message: `[FATAL] WSL Interop is broken on this system.` });
62
- await ctx.actions.runAction('core:log', { level: 'error', message: `Linux cannot launch Windows applications (cmd.exe failed).` });
63
- await ctx.actions.runAction('core:log', { level: 'error', message: `PLEASE FIX: Open PowerShell as Admin and run 'wsl --shutdown', then restart.` });
64
- return false;
48
+ if (fs.existsSync(STAGING_DIR)) {
49
+ fs.emptyDirSync(STAGING_DIR);
65
50
  }
66
- await ctx.actions.runAction('core:log', { level: 'info', message: `[WSL] Copying extension to Windows Temp: ${WIN_PATH_FOR_CHROME}` });
67
- // Ensure Windows temp dir exists and is clean
68
- if (fs.existsSync(WIN_TEMP_DIR)) {
69
- fs.removeSync(WIN_TEMP_DIR);
51
+ fs.ensureDirSync(STAGING_DIR);
52
+ fs.copySync(DIST_DIR, STAGING_DIR);
53
+ await ctx.actions.runAction('core:log', { level: 'info', message: `Synced code to Staging` });
54
+ // Emit staged event for ServerPlugin (optional for now, but good practice)
55
+ ctx.events.emit('browser:staged', { path: STAGING_DIR });
56
+ }
57
+ catch (err) {
58
+ await ctx.actions.runAction('core:log', { level: 'error', message: `Failed to sync to staging: ${err.message}` });
59
+ }
60
+ };
61
+ // --- Helper to find actual extension root (handle nested folder in zip) ---
62
+ const findExtensionRoot = (dir) => {
63
+ if (fs.existsSync(path.join(dir, 'manifest.json')))
64
+ return dir;
65
+ // Check immediate subdirectories (depth 1)
66
+ try {
67
+ const items = fs.readdirSync(dir);
68
+ for (const item of items) {
69
+ const fullPath = path.join(dir, item);
70
+ if (fs.statSync(fullPath).isDirectory()) {
71
+ if (fs.existsSync(path.join(fullPath, 'manifest.json'))) {
72
+ return fullPath;
73
+ }
74
+ }
70
75
  }
71
- fs.ensureDirSync(WIN_TEMP_DIR);
72
- // Copy dist content
73
- fs.copySync(DIST_DIR, WIN_TEMP_DIR);
74
- extensionPath = WIN_PATH_FOR_CHROME;
75
76
  }
76
- catch (copyErr) {
77
- await ctx.actions.runAction('core:log', { level: 'error', message: `Failed to copy to Windows Temp: ${copyErr.message}` });
78
- // Fallback to original path (might fail if not mapped)
77
+ catch (e) {
78
+ // Dir might be empty or invalid
79
79
  }
80
+ return null;
81
+ };
82
+ // Initial Sync
83
+ await syncToStaging();
84
+ // Resolve proper root AFTER sync
85
+ let extensionRoot = findExtensionRoot(STAGING_DIR) || STAGING_DIR;
86
+ // Check if we found a valid root
87
+ if (!fs.existsSync(path.join(extensionRoot, 'manifest.json'))) {
88
+ await ctx.actions.runAction('core:log', { level: 'error', message: `[CRITICAL] manifest.json not found in ${STAGING_DIR}. Extension will not load.` });
89
+ }
90
+ else if (extensionRoot !== STAGING_DIR) {
91
+ await ctx.actions.runAction('core:log', { level: 'info', message: `Detected nested extension at: ${path.basename(extensionRoot)}` });
80
92
  }
81
- await ctx.actions.runAction('core:log', { level: 'warning', message: 'Switching to Detached Mode (WSL/GitBash detected).' });
82
- await ctx.actions.runAction('core:log', { level: 'info', message: 'Browser polling/logging is disabled. Please reload manually on updates.' });
83
- const userDataDir = 'C:/Temp/ai-ext-profile';
84
- // Convert Chrome path to Windows format if in WSL
85
- // /mnt/c/Program Files/... -> C:\Program Files\...
86
- let executable = chromePath;
87
- // If WSL, use a batch file to handle the launch robustly
93
+ // Listen for updates and re-sync
94
+ ctx.events.on('downloader:updated', async (data) => {
95
+ await ctx.actions.runAction('core:log', { level: 'info', message: 'Update detected. Syncing to staging...' });
96
+ await syncToStaging();
97
+ });
98
+ await ctx.actions.runAction('core:log', { level: 'info', message: 'Browser running in Detached Mode.' });
99
+ // Launch Logic
88
100
  if (isWSL) {
89
101
  const driveLetter = chromePath.match(/\/mnt\/([a-z])\//)?.[1] || 'c';
90
102
  const winChromePath = chromePath
91
103
  .replace(new RegExp(`^/mnt/${driveLetter}/`), `${driveLetter.toUpperCase()}:\\`)
92
104
  .replace(/\//g, '\\');
93
- await ctx.actions.runAction('core:log', { level: 'info', message: `WSL: Creating launch script...` });
94
- // Use backslashes for Windows paths in the batch file
95
- const winDist = 'C:\\Temp\\ai-ext-preview';
105
+ // Calculate Windows path for the extension root
106
+ // Base Win: C:\Temp\ai-ext-preview
107
+ // Base Linux: /mnt/c/Temp/ai-ext-preview
108
+ // If extensionRoot is /mnt/c/Temp/ai-ext-preview/subdir => C:\Temp\ai-ext-preview\subdir
109
+ // Use relative path logic to be safe
110
+ const baseLinux = '/mnt/c/Temp/ai-ext-preview';
111
+ const relative = path.relative(baseLinux, extensionRoot);
112
+ const winDistRoot = relative ? `C:\\Temp\\ai-ext-preview\\${relative}` : 'C:\\Temp\\ai-ext-preview';
113
+ const finalWinDist = winDistRoot.replace(/\//g, '\\');
96
114
  const winProfile = 'C:\\Temp\\ai-ext-profile';
97
- // Create the batch file content
98
115
  const batContent = `@echo off
99
- start "" "${winChromePath}" --load-extension="${winDist}" --user-data-dir="${winProfile}" --no-first-run --no-default-browser-check --disable-gpu about:blank
116
+ start "" "${winChromePath}" --load-extension="${finalWinDist}" --user-data-dir="${winProfile}" --no-first-run --no-default-browser-check --disable-gpu about:blank
100
117
  exit
101
118
  `;
102
- const batPath = '/mnt/c/Temp/ai-ext-preview/launch.bat';
119
+ const batPath = path.join(STAGING_DIR, 'launch.bat');
103
120
  const winBatPath = 'C:\\Temp\\ai-ext-preview\\launch.bat';
104
121
  try {
105
122
  fs.writeFileSync(batPath, batContent);
106
123
  }
107
124
  catch (e) {
108
- await ctx.actions.runAction('core:log', { level: 'error', message: `Failed to write batch file: ${e.message}` });
109
- return false;
125
+ // Fallback if staging writes fail inside WSL mount for some reason?
110
126
  }
111
- await ctx.actions.runAction('core:log', { level: 'info', message: `EXEC: ${winBatPath}` });
112
- // Execute the batch file via cmd.exe using spawn + PATH lookup
113
127
  const cli = 'cmd.exe';
114
- await ctx.actions.runAction('core:log', { level: 'info', message: `SPAWN (WSL): ${cli} /c ${winBatPath}` });
115
128
  const subprocess = spawn(cli, ['/c', winBatPath], {
116
129
  detached: true,
117
130
  stdio: 'ignore',
@@ -121,22 +134,20 @@ exit
121
134
  return true;
122
135
  }
123
136
  else {
124
- // Standard Windows / Linux Launch (Git Bash / Native)
125
- // Normalize paths (stripping trailing slashes which Chrome hates)
126
- const safeDist = path.resolve(extensionPath);
127
- const safeProfile = path.resolve(userDataDir);
137
+ // Native Windows / Linux
138
+ // Use extensionRoot which points to the detected subfolder or root
139
+ const safeDist = path.resolve(extensionRoot);
140
+ const safeProfile = path.join(path.dirname(config.workDir), 'profile'); // ~/.ai-extension-preview/profile
128
141
  await ctx.actions.runAction('core:log', { level: 'info', message: `SPAWN: ${executable}` });
129
142
  await ctx.actions.runAction('core:log', { level: 'info', message: `EXT PATH: ${safeDist}` });
130
- // Reconstruct args with safe paths
131
143
  const cleanArgs = [
132
144
  `--load-extension=${safeDist}`,
133
145
  `--user-data-dir=${safeProfile}`,
134
146
  '--no-first-run',
135
147
  '--no-default-browser-check',
136
148
  '--disable-gpu',
137
- 'chrome://extensions' // Better for verifying if it loaded
149
+ 'chrome://extensions'
138
150
  ];
139
- await ctx.actions.runAction('core:log', { level: 'info', message: `ARGS: ${cleanArgs.join(' ')}` });
140
151
  const subprocess = spawn(executable, cleanArgs, {
141
152
  detached: true,
142
153
  stdio: 'ignore'
@@ -148,62 +159,9 @@ exit
148
159
  ctx.actions.registerAction({
149
160
  id: 'browser:start',
150
161
  handler: async () => {
151
- // On Windows (including Git Bash), web-ext is unreliable for loading extensions correctly.
152
- // We force detached mode to ensure the extension loads.
153
- if (process.platform === 'win32') {
154
- await ctx.actions.runAction('core:log', { level: 'warning', message: 'Windows detected: Forcing Detached Mode for reliability.' });
155
- return await launchDetached();
156
- }
157
- await ctx.actions.runAction('core:log', { level: 'info', message: 'Launching browser...' });
158
- try {
159
- // Try web-ext first
160
- const runResult = await webExt.cmd.run({
161
- sourceDir: DIST_DIR,
162
- target: 'chromium',
163
- browserConsole: false,
164
- startUrl: ['https://google.com'],
165
- noInput: true,
166
- keepProfileChanges: false,
167
- args: [
168
- '--start-maximized',
169
- '--no-sandbox',
170
- '--disable-gpu',
171
- '--disable-dev-shm-usage'
172
- ]
173
- }, {
174
- shouldExitProgram: false
175
- });
176
- runner = runResult;
177
- await ctx.actions.runAction('core:log', { level: 'success', message: 'Browser session ended.' });
178
- return true;
179
- }
180
- catch (err) {
181
- // Check for expected environment failures
182
- if (err.code === 'ECONNRESET' || err.message?.includes('CDP connection closed')) {
183
- // Log specific WSL message for clarity
184
- await ctx.actions.runAction('core:log', { level: 'warning', message: 'WSL: CDP connection dropped (expected). Browser is running detached.' });
185
- await ctx.actions.runAction('core:log', { level: 'info', message: 'Please reload extension manually in Chrome if needed.' });
186
- return await launchDetached();
187
- }
188
- if (err.code !== 'ECONNRESET') {
189
- await ctx.actions.runAction('core:log', { level: 'error', message: `Browser failed: ${err.message}` });
190
- }
191
- return false;
192
- }
193
- }
194
- });
195
- ctx.events.on('downloader:updated', async () => {
196
- if (runner && runner.reloadAllExtensions) {
197
- await ctx.actions.runAction('core:log', { level: 'info', message: 'Triggering browser reload...' });
198
- try {
199
- runner.reloadAllExtensions();
200
- }
201
- catch (e) {
202
- // Ignore
203
- }
204
- }
205
- else {
206
- await ctx.actions.runAction('core:log', { level: 'info', message: 'Update installed. Please reload extension in Chrome.' });
162
+ // Force Detached Mode for Reliability on ALL platforms
163
+ // This creates the stable "Staging" workflow we want.
164
+ return await launchDetached();
207
165
  }
208
166
  });
209
167
  }
@@ -69,7 +69,7 @@ export const DownloaderPlugin = {
69
69
  if (success) {
70
70
  lastModified = newVersion;
71
71
  fs.writeFileSync(VERSION_FILE, newVersion);
72
- ctx.events.emit('downloader:updated', { version: job.version });
72
+ ctx.events.emit('downloader:updated', { version: job.version, jobId: config.jobId });
73
73
  }
74
74
  }
75
75
  isChecking = false;
@@ -101,24 +101,35 @@ export const DownloaderPlugin = {
101
101
  try {
102
102
  const HOT_RELOAD_CODE = `
103
103
  const EVENT_SOURCE_URL = 'http://localhost:3500/status';
104
+ const CURRENT_JOB_ID = '${config.jobId}';
104
105
  let lastVersion = null;
106
+ let lastJobId = null;
105
107
 
106
108
  setInterval(async () => {
107
109
  try {
108
110
  const res = await fetch(EVENT_SOURCE_URL);
109
111
  const data = await res.json();
110
112
 
113
+ // 1. Job ID Swap (User switched project)
114
+ if (data.jobId && data.jobId !== CURRENT_JOB_ID) {
115
+ console.log('[Hot Reload] Job Swap detected. Reloading...');
116
+ chrome.runtime.reload();
117
+ return;
118
+ }
119
+
120
+ // 2. Version Bump (Same project, new build)
111
121
  if (lastVersion && data.version !== lastVersion) {
112
122
  console.log('[Hot Reload] New version detected:', data.version);
113
123
  chrome.runtime.reload();
114
124
  }
115
125
 
116
126
  lastVersion = data.version;
127
+ lastJobId = data.jobId;
117
128
  } catch (err) {
118
129
  // Build tool might be offline
119
130
  }
120
131
  }, 1000);
121
- console.log('[Hot Reload] Active');
132
+ console.log('[Hot Reload] Active for Job:', CURRENT_JOB_ID);
122
133
  `;
123
134
  const hotReloadPath = path.join(DIST_DIR, 'hot-reload.js');
124
135
  await fs.writeFile(hotReloadPath, HOT_RELOAD_CODE);
@@ -23,8 +23,12 @@ export const ServerPlugin = {
23
23
  return;
24
24
  }
25
25
  if (req.url === '/status') {
26
+ const currentJobId = ctx.host.config.jobId;
26
27
  res.writeHead(200, { 'Content-Type': 'application/json' });
27
- res.end(JSON.stringify({ version: currentVersion }));
28
+ res.end(JSON.stringify({
29
+ version: currentVersion,
30
+ jobId: currentJobId
31
+ }));
28
32
  }
29
33
  else {
30
34
  res.writeHead(404);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "ai-extension-preview",
3
- "version": "0.1.5",
3
+ "version": "0.1.7",
4
4
  "description": "Local preview tool for AI Extension Builder",
5
5
  "type": "module",
6
6
  "bin": {
@@ -20,7 +20,7 @@
20
20
  "author": "AI Extension Builder",
21
21
  "license": "MIT",
22
22
  "scripts": {
23
- "build": "shx rm -rf dist && tsc -b",
23
+ "build": "shx rm -rf dist && tsc -b && shx chmod +x dist/index.js",
24
24
  "start": "tsx src/index.ts",
25
25
  "dev": "tsx watch src/index.ts",
26
26
  "preview": "node dist/index.js"
@@ -49,4 +49,4 @@
49
49
  "tsx": "^4.21.0",
50
50
  "typescript": "^5.7.2"
51
51
  }
52
- }
52
+ }