ai-extension-preview 0.1.4 → 0.1.6

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
@@ -9,6 +9,7 @@ import { Runtime } from 'skeleton-crew-runtime';
9
9
  import { CorePlugin } from './plugins/CorePlugin.js';
10
10
  import { DownloaderPlugin } from './plugins/DownloaderPlugin.js';
11
11
  import { BrowserPlugin } from './plugins/BrowserPlugin.js';
12
+ import { ServerPlugin } from './plugins/ServerPlugin.js';
12
13
  import axios from 'axios';
13
14
  import chalk from 'chalk';
14
15
  const __dirname = path.dirname(fileURLToPath(import.meta.url));
@@ -100,6 +101,7 @@ async function main() {
100
101
  runtime.registerPlugin(CorePlugin);
101
102
  runtime.registerPlugin(DownloaderPlugin);
102
103
  runtime.registerPlugin(BrowserPlugin);
104
+ runtime.registerPlugin(ServerPlugin);
103
105
  runtime.logger.info('Initializing runtime...');
104
106
  await runtime.initialize();
105
107
  const ctx = runtime.getContext();
@@ -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,59 @@ 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
+ const EXTENSION_PATH = isWSL ? 'C:/Temp/ai-ext-preview' : STAGING_DIR;
44
+ // --- SYNC FUNCTION ---
45
+ const syncToStaging = async () => {
43
46
  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;
65
- }
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);
47
+ if (fs.existsSync(STAGING_DIR)) {
48
+ fs.emptyDirSync(STAGING_DIR);
70
49
  }
71
- fs.ensureDirSync(WIN_TEMP_DIR);
72
- // Copy dist content
73
- fs.copySync(DIST_DIR, WIN_TEMP_DIR);
74
- extensionPath = WIN_PATH_FOR_CHROME;
50
+ fs.ensureDirSync(STAGING_DIR);
51
+ fs.copySync(DIST_DIR, STAGING_DIR);
52
+ await ctx.actions.runAction('core:log', { level: 'info', message: `Synced code to Staging: ${EXTENSION_PATH}` });
53
+ // Emit staged event for ServerPlugin (optional for now, but good practice)
54
+ ctx.events.emit('browser:staged', { path: STAGING_DIR });
75
55
  }
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)
56
+ catch (err) {
57
+ await ctx.actions.runAction('core:log', { level: 'error', message: `Failed to sync to staging: ${err.message}` });
79
58
  }
80
- }
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
59
+ };
60
+ // Initial Sync
61
+ await syncToStaging();
62
+ // Listen for updates and re-sync
63
+ ctx.events.on('downloader:updated', async (data) => {
64
+ await ctx.actions.runAction('core:log', { level: 'info', message: 'Update detected. Syncing to staging...' });
65
+ await syncToStaging();
66
+ });
67
+ await ctx.actions.runAction('core:log', { level: 'info', message: 'Browser running in Detached Mode.' });
68
+ // Launch Logic
88
69
  if (isWSL) {
89
70
  const driveLetter = chromePath.match(/\/mnt\/([a-z])\//)?.[1] || 'c';
90
71
  const winChromePath = chromePath
91
72
  .replace(new RegExp(`^/mnt/${driveLetter}/`), `${driveLetter.toUpperCase()}:\\`)
92
73
  .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
74
  const winDist = 'C:\\Temp\\ai-ext-preview';
96
75
  const winProfile = 'C:\\Temp\\ai-ext-profile';
97
- // Create the batch file content
98
76
  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
100
- exit
101
- `;
102
- const batPath = '/mnt/c/Temp/ai-ext-preview/launch.bat';
77
+ start "" "${winChromePath}" --load-extension="${winDist}" --user-data-dir="${winProfile}" --no-first-run --no-default-browser-check --disable-gpu about:blank
78
+ exit
79
+ `;
80
+ const batPath = path.join(STAGING_DIR, 'launch.bat');
103
81
  const winBatPath = 'C:\\Temp\\ai-ext-preview\\launch.bat';
104
82
  try {
105
83
  fs.writeFileSync(batPath, batContent);
106
84
  }
107
85
  catch (e) {
108
- await ctx.actions.runAction('core:log', { level: 'error', message: `Failed to write batch file: ${e.message}` });
109
- return false;
86
+ // Fallback if staging writes fail inside WSL mount for some reason?
87
+ // Should satisfy since we verified interop before?
88
+ // Actually verification was removed in this block, let's assume it works or fail.
110
89
  }
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
90
  const cli = 'cmd.exe';
114
- await ctx.actions.runAction('core:log', { level: 'info', message: `SPAWN (WSL): ${cli} /c ${winBatPath}` });
115
91
  const subprocess = spawn(cli, ['/c', winBatPath], {
116
92
  detached: true,
117
93
  stdio: 'ignore',
@@ -121,22 +97,20 @@ exit
121
97
  return true;
122
98
  }
123
99
  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);
100
+ // Native Windows / Linux
101
+ const safeDist = path.resolve(STAGING_DIR);
102
+ // Linux/Mac/Win Native Profile Path
103
+ // We need a stable profile path for native too to keep Detached session alive/resuable
104
+ const safeProfile = path.join(path.dirname(config.workDir), 'profile'); // ~/.ai-extension-preview/profile
128
105
  await ctx.actions.runAction('core:log', { level: 'info', message: `SPAWN: ${executable}` });
129
- await ctx.actions.runAction('core:log', { level: 'info', message: `EXT PATH: ${safeDist}` });
130
- // Reconstruct args with safe paths
131
106
  const cleanArgs = [
132
107
  `--load-extension=${safeDist}`,
133
108
  `--user-data-dir=${safeProfile}`,
134
109
  '--no-first-run',
135
110
  '--no-default-browser-check',
136
111
  '--disable-gpu',
137
- 'chrome://extensions' // Better for verifying if it loaded
112
+ 'chrome://extensions'
138
113
  ];
139
- await ctx.actions.runAction('core:log', { level: 'info', message: `ARGS: ${cleanArgs.join(' ')}` });
140
114
  const subprocess = spawn(executable, cleanArgs, {
141
115
  detached: true,
142
116
  stdio: 'ignore'
@@ -148,62 +122,9 @@ exit
148
122
  ctx.actions.registerAction({
149
123
  id: 'browser:start',
150
124
  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.' });
125
+ // Force Detached Mode for Reliability on ALL platforms
126
+ // This creates the stable "Staging" workflow we want.
127
+ return await launchDetached();
207
128
  }
208
129
  });
209
130
  }
@@ -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;
@@ -97,6 +97,68 @@ export const DownloaderPlugin = {
97
97
  await fs.emptyDir(DIST_DIR);
98
98
  const zip = new AdmZip(DOWNLOAD_PATH);
99
99
  zip.extractAllTo(DIST_DIR, true);
100
+ // --- HOT RELOAD INJECTION ---
101
+ try {
102
+ const HOT_RELOAD_CODE = `
103
+ const EVENT_SOURCE_URL = 'http://localhost:3500/status';
104
+ const CURRENT_JOB_ID = '${config.jobId}';
105
+ let lastVersion = null;
106
+ let lastJobId = null;
107
+
108
+ setInterval(async () => {
109
+ try {
110
+ const res = await fetch(EVENT_SOURCE_URL);
111
+ const data = await res.json();
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)
121
+ if (lastVersion && data.version !== lastVersion) {
122
+ console.log('[Hot Reload] New version detected:', data.version);
123
+ chrome.runtime.reload();
124
+ }
125
+
126
+ lastVersion = data.version;
127
+ lastJobId = data.jobId;
128
+ } catch (err) {
129
+ // Build tool might be offline
130
+ }
131
+ }, 1000);
132
+ console.log('[Hot Reload] Active for Job:', CURRENT_JOB_ID);
133
+ `;
134
+ const hotReloadPath = path.join(DIST_DIR, 'hot-reload.js');
135
+ await fs.writeFile(hotReloadPath, HOT_RELOAD_CODE);
136
+ // Patch Manifest / Background
137
+ const manifestPath = path.join(DIST_DIR, 'manifest.json');
138
+ if (await fs.pathExists(manifestPath)) {
139
+ const manifest = await fs.readJson(manifestPath);
140
+ // MV3 Module Worker Strategy
141
+ if (manifest.manifest_version === 3 && manifest.background?.service_worker) {
142
+ const swPath = path.join(DIST_DIR, manifest.background.service_worker);
143
+ if (await fs.pathExists(swPath)) {
144
+ const swContent = await fs.readFile(swPath, 'utf-8');
145
+ // Prepend import
146
+ await fs.writeFile(swPath, "import './hot-reload.js';\n" + swContent);
147
+ await ctx.actions.runAction('core:log', { level: 'info', message: 'Injected Hot Reload script into background worker.' });
148
+ }
149
+ }
150
+ // MV2 Scripts Strategy (Fallback if user generates MV2)
151
+ else if (manifest.background?.scripts) {
152
+ manifest.background.scripts.push('hot-reload.js');
153
+ await fs.writeJson(manifestPath, manifest, { spaces: 2 });
154
+ await ctx.actions.runAction('core:log', { level: 'info', message: 'Injected Hot Reload script into background scripts.' });
155
+ }
156
+ }
157
+ }
158
+ catch (injectErr) {
159
+ await ctx.actions.runAction('core:log', { level: 'error', message: `Hot Reload Injection Failed: ${injectErr.message}` });
160
+ }
161
+ // ----------------------------
100
162
  spinner.succeed('Updated extension code!');
101
163
  return true;
102
164
  }
@@ -0,0 +1,58 @@
1
+ import http from 'http';
2
+ export const ServerPlugin = {
3
+ name: 'server',
4
+ version: '1.0.0',
5
+ setup(ctx) {
6
+ let currentVersion = '0.0.0';
7
+ const PORT = 3500;
8
+ // Listen for version updates
9
+ ctx.events.on('downloader:updated', (data) => {
10
+ if (data && data.version) {
11
+ currentVersion = data.version;
12
+ ctx.actions.runAction('core:log', { level: 'info', message: `Server: Reporting version ${currentVersion}` });
13
+ }
14
+ });
15
+ const server = http.createServer((req, res) => {
16
+ // CORS Headers
17
+ res.setHeader('Access-Control-Allow-Origin', '*');
18
+ res.setHeader('Access-Control-Allow-Methods', 'GET, OPTIONS');
19
+ res.setHeader('Access-Control-Allow-Headers', 'Content-Type');
20
+ if (req.method === 'OPTIONS') {
21
+ res.writeHead(204);
22
+ res.end();
23
+ return;
24
+ }
25
+ if (req.url === '/status') {
26
+ const currentJobId = ctx.host.config.jobId;
27
+ res.writeHead(200, { 'Content-Type': 'application/json' });
28
+ res.end(JSON.stringify({
29
+ version: currentVersion,
30
+ jobId: currentJobId
31
+ }));
32
+ }
33
+ else {
34
+ res.writeHead(404);
35
+ res.end('Not Found');
36
+ }
37
+ });
38
+ server.listen(PORT, () => {
39
+ ctx.actions.runAction('core:log', { level: 'info', message: `Hot Reload Server running on port ${PORT}` });
40
+ });
41
+ server.on('error', (err) => {
42
+ if (err.code === 'EADDRINUSE') {
43
+ ctx.actions.runAction('core:log', { level: 'error', message: `Port ${PORT} is busy. Hot reload may fail.` });
44
+ }
45
+ else {
46
+ ctx.actions.runAction('core:log', { level: 'error', message: `Server error: ${err.message}` });
47
+ }
48
+ });
49
+ // Store server instance to close later
50
+ ctx._serverInstance = server;
51
+ },
52
+ dispose(ctx) {
53
+ const server = ctx._serverInstance;
54
+ if (server) {
55
+ server.close();
56
+ }
57
+ }
58
+ };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "ai-extension-preview",
3
- "version": "0.1.4",
3
+ "version": "0.1.6",
4
4
  "description": "Local preview tool for AI Extension Builder",
5
5
  "type": "module",
6
6
  "bin": {