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 +2 -0
- package/dist/plugins/BrowserPlugin.js +43 -122
- package/dist/plugins/DownloaderPlugin.js +63 -1
- package/dist/plugins/ServerPlugin.js +58 -0
- package/package.json +1 -1
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
|
-
|
|
40
|
-
let
|
|
41
|
-
const
|
|
42
|
-
|
|
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
|
-
|
|
45
|
-
|
|
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(
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
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 (
|
|
77
|
-
await ctx.actions.runAction('core:log', { level: 'error', message: `Failed to
|
|
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
|
-
|
|
82
|
-
await
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
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 = '
|
|
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
|
-
|
|
109
|
-
|
|
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
|
-
//
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
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'
|
|
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
|
-
//
|
|
152
|
-
//
|
|
153
|
-
|
|
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
|
+
};
|