ai-extension-preview 0.1.13 → 0.1.15
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 +30 -117
- package/dist/plugins/AppPlugin.js +58 -0
- package/dist/plugins/AuthPlugin.js +88 -0
- package/dist/plugins/ConfigPlugin.js +51 -0
- package/dist/plugins/CorePlugin.js +2 -5
- package/dist/plugins/DownloaderPlugin.js +38 -13
- package/dist/plugins/ServerPlugin.js +34 -10
- package/dist/plugins/browser/BrowserManagerPlugin.js +37 -11
- package/dist/plugins/browser/NativeLauncherPlugin.js +2 -1
- package/dist/plugins/browser/WSLLauncherPlugin.js +30 -11
- package/dist/types.js +1 -0
- package/dist/utils/sandbox.js +192 -0
- package/package.json +5 -4
package/dist/index.js
CHANGED
|
@@ -2,19 +2,18 @@
|
|
|
2
2
|
import 'dotenv/config'; // Load .env
|
|
3
3
|
import { Command } from 'commander';
|
|
4
4
|
import path from 'path';
|
|
5
|
-
import { fileURLToPath } from 'url';
|
|
6
|
-
import fs from 'fs-extra';
|
|
7
5
|
import os from 'os';
|
|
8
6
|
import { Runtime } from 'skeleton-crew-runtime';
|
|
7
|
+
import { ConfigPlugin } from './plugins/ConfigPlugin.js';
|
|
9
8
|
import { CorePlugin } from './plugins/CorePlugin.js';
|
|
10
9
|
import { DownloaderPlugin } from './plugins/DownloaderPlugin.js';
|
|
11
10
|
import { BrowserManagerPlugin } from './plugins/browser/BrowserManagerPlugin.js';
|
|
12
11
|
import { WSLLauncherPlugin } from './plugins/browser/WSLLauncherPlugin.js';
|
|
13
12
|
import { NativeLauncherPlugin } from './plugins/browser/NativeLauncherPlugin.js';
|
|
14
13
|
import { ServerPlugin } from './plugins/ServerPlugin.js';
|
|
15
|
-
import
|
|
14
|
+
import { AuthPlugin } from './plugins/AuthPlugin.js'; // [NEW]
|
|
15
|
+
import { AppPlugin } from './plugins/AppPlugin.js'; // [NEW]
|
|
16
16
|
import chalk from 'chalk';
|
|
17
|
-
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
|
18
17
|
const DEFAULT_HOST = process.env.API_HOST || 'https://ai-extension-builder.01kb6018z1t9tpaza4y5f1c56w.lmapp.run/api';
|
|
19
18
|
const program = new Command();
|
|
20
19
|
program
|
|
@@ -26,138 +25,53 @@ program
|
|
|
26
25
|
.option('--user <user>', 'User ID (if required)')
|
|
27
26
|
.parse(process.argv);
|
|
28
27
|
const options = program.opts();
|
|
29
|
-
async function authenticate(host, port) {
|
|
30
|
-
try {
|
|
31
|
-
// 1. Init Session with port
|
|
32
|
-
console.log('[DEBUG] Sending port to backend:', port);
|
|
33
|
-
const initRes = await axios({
|
|
34
|
-
method: 'post',
|
|
35
|
-
url: `${host}/preview/init`,
|
|
36
|
-
data: { port },
|
|
37
|
-
headers: {
|
|
38
|
-
'Content-Type': 'application/json'
|
|
39
|
-
}
|
|
40
|
-
});
|
|
41
|
-
console.log('[DEBUG] Init response:', initRes.data);
|
|
42
|
-
const { code, sessionId } = initRes.data;
|
|
43
|
-
console.log('\n' + chalk.bgBlue.bold(' DETACHED PREVIEW MODE ') + '\n');
|
|
44
|
-
console.log('To connect, please go to your Extension Dashboard and click "Connect Preview".');
|
|
45
|
-
console.log('Enter the following code:');
|
|
46
|
-
console.log('\n' + chalk.green.bold(` ${code} `) + '\n');
|
|
47
|
-
console.log('Waiting for connection...');
|
|
48
|
-
// 2. Poll for Status
|
|
49
|
-
while (true) {
|
|
50
|
-
await new Promise(resolve => setTimeout(resolve, 2000));
|
|
51
|
-
try {
|
|
52
|
-
const statusRes = await axios.get(`${host}/preview/status/${sessionId}`);
|
|
53
|
-
const data = statusRes.data;
|
|
54
|
-
if (data.status === 'linked') {
|
|
55
|
-
console.log(chalk.green('✔ Connected!'));
|
|
56
|
-
if (!data.jobId) {
|
|
57
|
-
console.error('Error: No Job ID associated with this connection.');
|
|
58
|
-
process.exit(1);
|
|
59
|
-
}
|
|
60
|
-
console.log('[DEBUG] Received userId:', data.userId);
|
|
61
|
-
console.log('[DEBUG] Received jobId:', data.jobId);
|
|
62
|
-
return {
|
|
63
|
-
jobId: data.jobId,
|
|
64
|
-
userId: data.userId,
|
|
65
|
-
token: data.token || ''
|
|
66
|
-
};
|
|
67
|
-
}
|
|
68
|
-
if (data.status === 'expired') {
|
|
69
|
-
console.error(chalk.red('Code expired. Please restart.'));
|
|
70
|
-
process.exit(1);
|
|
71
|
-
}
|
|
72
|
-
}
|
|
73
|
-
catch (err) {
|
|
74
|
-
// Ignore poll errors, keep trying
|
|
75
|
-
}
|
|
76
|
-
}
|
|
77
|
-
}
|
|
78
|
-
catch (error) {
|
|
79
|
-
console.error('Authentication failed:', error);
|
|
80
|
-
throw error;
|
|
81
|
-
}
|
|
82
|
-
}
|
|
83
28
|
// Use os.homedir() to ensure we have write permissions
|
|
84
|
-
// Git Bash sometimes defaults cwd to C:\Program Files\Git which causes EPERM
|
|
85
29
|
const HOME_DIR = os.homedir();
|
|
86
|
-
|
|
30
|
+
// Initial workdir based on options, or specific 'default' if not yet known.
|
|
31
|
+
// AuthPlugin will update this if job changes.
|
|
32
|
+
const WORK_DIR = path.join(HOME_DIR, '.ai-extension-preview', options.job || 'default');
|
|
87
33
|
(async () => {
|
|
88
|
-
const { job:
|
|
89
|
-
// 1. Initialize Runtime
|
|
34
|
+
const { job: initialJobId, host, token, user: userId } = options;
|
|
35
|
+
// 1. Initialize Runtime with Config
|
|
90
36
|
const runtime = new Runtime({
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
}
|
|
37
|
+
config: {
|
|
38
|
+
host,
|
|
39
|
+
token: token || '',
|
|
40
|
+
user: userId || '',
|
|
41
|
+
jobId: initialJobId || '',
|
|
42
|
+
workDir: WORK_DIR
|
|
43
|
+
},
|
|
44
|
+
hostContext: {} // Clear hostContext config wrapping
|
|
100
45
|
});
|
|
46
|
+
// Register Plugins
|
|
101
47
|
runtime.logger.info('Registering plugins...');
|
|
102
48
|
runtime.registerPlugin(CorePlugin);
|
|
49
|
+
runtime.registerPlugin(ConfigPlugin);
|
|
103
50
|
runtime.registerPlugin(DownloaderPlugin);
|
|
104
51
|
runtime.registerPlugin(BrowserManagerPlugin);
|
|
105
52
|
runtime.registerPlugin(WSLLauncherPlugin);
|
|
106
53
|
runtime.registerPlugin(NativeLauncherPlugin);
|
|
107
54
|
runtime.registerPlugin(ServerPlugin);
|
|
55
|
+
runtime.registerPlugin(AuthPlugin); // [NEW]
|
|
56
|
+
runtime.registerPlugin(AppPlugin); // [NEW]
|
|
108
57
|
runtime.logger.info('Initializing runtime...');
|
|
109
58
|
await runtime.initialize();
|
|
110
59
|
const ctx = runtime.getContext();
|
|
111
|
-
//
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
console.error('Failed to allocate server port');
|
|
115
|
-
process.exit(1);
|
|
116
|
-
}
|
|
117
|
-
// 2. Now authenticate with the allocated port
|
|
118
|
-
let finalJobId = jobId;
|
|
119
|
-
let finalUserId = userId;
|
|
120
|
-
let finalToken = token;
|
|
121
|
-
if (!jobId || !userId) {
|
|
122
|
-
const authData = await authenticate(host, allocatedPort);
|
|
123
|
-
finalJobId = authData.jobId;
|
|
124
|
-
finalUserId = authData.userId;
|
|
125
|
-
finalToken = authData.token;
|
|
126
|
-
// Update runtime config with auth data
|
|
127
|
-
ctx.host.config.jobId = finalJobId;
|
|
128
|
-
ctx.host.config.user = finalUserId;
|
|
129
|
-
ctx.host.config.token = finalToken;
|
|
130
|
-
}
|
|
131
|
-
// 3. Start LifeCycle
|
|
132
|
-
await ctx.actions.runAction('core:log', { level: 'info', message: 'Initializing Local Satellite...' });
|
|
133
|
-
// Ensure work dir exists
|
|
134
|
-
await fs.ensureDir(WORK_DIR);
|
|
135
|
-
// Initial Check - Must succeed to continue
|
|
136
|
-
const success = await ctx.actions.runAction('downloader:check', null);
|
|
137
|
-
if (!success) {
|
|
138
|
-
await ctx.actions.runAction('core:log', { level: 'error', message: 'Initial check failed. Could not verify job or download extension.' });
|
|
139
|
-
process.exit(1);
|
|
140
|
-
}
|
|
141
|
-
// Wait for Extension files (Manifest)
|
|
142
|
-
const manifestPath = path.join(WORK_DIR, 'dist', 'manifest.json');
|
|
143
|
-
let attempts = 0;
|
|
144
|
-
const maxAttempts = 60; // 2 minutes
|
|
145
|
-
console.log('[DEBUG] Waiting for extension files...');
|
|
146
|
-
while (!fs.existsSync(manifestPath) && attempts < maxAttempts) {
|
|
147
|
-
await new Promise(r => setTimeout(r, 2000));
|
|
148
|
-
attempts++;
|
|
149
|
-
if (attempts % 5 === 0)
|
|
150
|
-
console.log(`Waiting for extension generation... (${attempts * 2}s)`);
|
|
60
|
+
// 2. Start App Flow
|
|
61
|
+
try {
|
|
62
|
+
await ctx.actions.runAction('app:start', null);
|
|
151
63
|
}
|
|
152
|
-
|
|
153
|
-
|
|
64
|
+
catch (error) {
|
|
65
|
+
console.error(chalk.red('App Error:'), error.message);
|
|
66
|
+
await runtime.shutdown();
|
|
154
67
|
process.exit(1);
|
|
155
68
|
}
|
|
156
|
-
//
|
|
157
|
-
|
|
158
|
-
//
|
|
69
|
+
// Keep process alive handled by Node event loop because ServerPlugin has an open server
|
|
70
|
+
// and Browser processes might be attached.
|
|
71
|
+
// Graceful Shutdown
|
|
159
72
|
process.on('SIGINT', async () => {
|
|
160
73
|
await ctx.actions.runAction('core:log', { level: 'info', message: 'Shutting down...' });
|
|
74
|
+
await runtime.shutdown();
|
|
161
75
|
process.exit(0);
|
|
162
76
|
});
|
|
163
77
|
runtime.logger.info('Press Ctrl+C to exit.');
|
|
@@ -168,7 +82,6 @@ const WORK_DIR = path.join(HOME_DIR, '.ai-extension-preview', options.job || 'de
|
|
|
168
82
|
// Handle global errors
|
|
169
83
|
process.on('uncaughtException', (err) => {
|
|
170
84
|
if (err.code === 'ECONNRESET' || err.message?.includes('ECONNRESET')) {
|
|
171
|
-
// Ignore pipe errors frequently caused by web-ext/chrome teardown
|
|
172
85
|
return;
|
|
173
86
|
}
|
|
174
87
|
console.error('Uncaught Exception:', err);
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
import fs from 'fs-extra';
|
|
2
|
+
import path from 'path';
|
|
3
|
+
export const AppPlugin = {
|
|
4
|
+
name: 'app',
|
|
5
|
+
version: '1.0.0',
|
|
6
|
+
dependencies: ['auth', 'config', 'downloader', 'browser-manager', 'server'],
|
|
7
|
+
setup(ctx) {
|
|
8
|
+
ctx.actions.registerAction({
|
|
9
|
+
id: 'app:start',
|
|
10
|
+
handler: async () => {
|
|
11
|
+
await ctx.actions.runAction('core:log', { level: 'info', message: 'Initializing Local Satellite...' });
|
|
12
|
+
// 1. Authenticate (if needed)
|
|
13
|
+
// AuthPlugin will automatically skip if already config'd, or prompt if needed
|
|
14
|
+
// It will also update config via config:set
|
|
15
|
+
await ctx.actions.runAction('auth:login');
|
|
16
|
+
// 2. Validate Configuration (Now that we have potential Auth data)
|
|
17
|
+
try {
|
|
18
|
+
await ctx.actions.runAction('config:validate', null);
|
|
19
|
+
}
|
|
20
|
+
catch (e) {
|
|
21
|
+
throw new Error(`Configuration Invalid: ${e.message}`);
|
|
22
|
+
}
|
|
23
|
+
// 3. Get Updated Config
|
|
24
|
+
const workDir = ctx.config.workDir;
|
|
25
|
+
// 3. Ensure Work Directory
|
|
26
|
+
await fs.ensureDir(workDir);
|
|
27
|
+
// 4. Initial Download/Check
|
|
28
|
+
const success = await ctx.actions.runAction('downloader:check', null);
|
|
29
|
+
if (!success) {
|
|
30
|
+
await ctx.actions.runAction('core:log', { level: 'error', message: 'Initial check failed. Could not verify job or download extension.' });
|
|
31
|
+
// We don't exit process here, but we might throw to stop flow
|
|
32
|
+
throw new Error('Initial check failed');
|
|
33
|
+
}
|
|
34
|
+
// 5. Wait for Extension Manifest
|
|
35
|
+
const manifestPath = path.join(workDir, 'dist', 'manifest.json');
|
|
36
|
+
let attempts = 0;
|
|
37
|
+
const maxAttempts = 60; // 2 minutes
|
|
38
|
+
// This logic could be in a 'watcher' plugin but fits here for now as part of "Startup Sequence"
|
|
39
|
+
if (!fs.existsSync(manifestPath)) {
|
|
40
|
+
await ctx.actions.runAction('core:log', { level: 'info', message: '[DEBUG] Waiting for extension files...' });
|
|
41
|
+
while (!fs.existsSync(manifestPath) && attempts < maxAttempts) {
|
|
42
|
+
await new Promise(r => setTimeout(r, 2000));
|
|
43
|
+
attempts++;
|
|
44
|
+
if (attempts % 5 === 0) {
|
|
45
|
+
await ctx.actions.runAction('core:log', { level: 'info', message: `Waiting for extension generation... (${attempts * 2}s)` });
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
if (!fs.existsSync(manifestPath)) {
|
|
50
|
+
await ctx.actions.runAction('core:log', { level: 'error', message: 'Timed out waiting for extension files. Status check succeeded but files are missing.' });
|
|
51
|
+
throw new Error('Timeout waiting for files');
|
|
52
|
+
}
|
|
53
|
+
// 6. Launch Browser
|
|
54
|
+
await ctx.actions.runAction('browser:start', {});
|
|
55
|
+
}
|
|
56
|
+
});
|
|
57
|
+
}
|
|
58
|
+
};
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
import axios from 'axios';
|
|
2
|
+
import chalk from 'chalk';
|
|
3
|
+
import path from 'path';
|
|
4
|
+
import os from 'os';
|
|
5
|
+
export const AuthPlugin = {
|
|
6
|
+
name: 'auth',
|
|
7
|
+
version: '1.0.0',
|
|
8
|
+
dependencies: ['config', 'server'],
|
|
9
|
+
setup(ctx) {
|
|
10
|
+
ctx.actions.registerAction({
|
|
11
|
+
id: 'auth:login',
|
|
12
|
+
handler: async () => {
|
|
13
|
+
const hostContext = ctx.config;
|
|
14
|
+
// If we already have JobID and UserID, we might skip, but let's assume we need to verify or start fresh if missing
|
|
15
|
+
if (hostContext.jobId && hostContext.user) {
|
|
16
|
+
await ctx.actions.runAction('core:log', { level: 'info', message: 'Auth: Job ID and User ID present. Skipping login.' });
|
|
17
|
+
return { jobId: hostContext.jobId, user: hostContext.user, token: hostContext.token };
|
|
18
|
+
}
|
|
19
|
+
// We need the port from ServerPlugin
|
|
20
|
+
// We need the port from ServerPlugin
|
|
21
|
+
const allocatedPort = ctx.config.hotReloadPort;
|
|
22
|
+
if (!allocatedPort) {
|
|
23
|
+
throw new Error('Server port not found. Ensure ServerPlugin is loaded before AuthPlugin logic runs.');
|
|
24
|
+
}
|
|
25
|
+
const host = hostContext.host;
|
|
26
|
+
await ctx.actions.runAction('core:log', { level: 'info', message: `Auth: Initiating login flow on ${host} with port ${allocatedPort}` });
|
|
27
|
+
try {
|
|
28
|
+
// 1. Init Session with port
|
|
29
|
+
const initRes = await axios({
|
|
30
|
+
method: 'post',
|
|
31
|
+
url: `${host}/preview/init`,
|
|
32
|
+
data: { port: allocatedPort },
|
|
33
|
+
headers: { 'Content-Type': 'application/json' }
|
|
34
|
+
});
|
|
35
|
+
const { code, sessionId } = initRes.data;
|
|
36
|
+
console.log('\n' + chalk.bgBlue.bold(' DETACHED PREVIEW MODE ') + '\n');
|
|
37
|
+
console.log('To connect, please go to your Extension Dashboard and click "Connect Preview".');
|
|
38
|
+
console.log('Enter the following code:');
|
|
39
|
+
console.log('\n' + chalk.green.bold(` ${code} `) + '\n');
|
|
40
|
+
console.log('Waiting for connection...');
|
|
41
|
+
// 2. Poll for Status
|
|
42
|
+
let attempts = 0;
|
|
43
|
+
while (true) {
|
|
44
|
+
await new Promise(resolve => setTimeout(resolve, 2000));
|
|
45
|
+
attempts++;
|
|
46
|
+
// Check if we should abort (e.g. from a cancel signal? unimplemented for now)
|
|
47
|
+
try {
|
|
48
|
+
const statusRes = await axios.get(`${host}/preview/status/${sessionId}`);
|
|
49
|
+
const data = statusRes.data;
|
|
50
|
+
if (data.status === 'linked') {
|
|
51
|
+
console.log(chalk.green('✔ Connected!'));
|
|
52
|
+
if (!data.jobId) {
|
|
53
|
+
throw new Error('No Job ID associated with this connection.');
|
|
54
|
+
}
|
|
55
|
+
const authData = {
|
|
56
|
+
jobId: data.jobId,
|
|
57
|
+
user: data.userId,
|
|
58
|
+
token: data.token || ''
|
|
59
|
+
};
|
|
60
|
+
// UPGRADE CONFIG
|
|
61
|
+
await ctx.actions.runAction('config:set', {
|
|
62
|
+
jobId: authData.jobId,
|
|
63
|
+
user: authData.user,
|
|
64
|
+
token: authData.token,
|
|
65
|
+
workDir: path.join(os.homedir(), '.ai-extension-preview', authData.jobId)
|
|
66
|
+
});
|
|
67
|
+
return authData;
|
|
68
|
+
}
|
|
69
|
+
if (data.status === 'expired') {
|
|
70
|
+
throw new Error('Code expired. Please restart.');
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
catch (err) {
|
|
74
|
+
if (err.message && (err.message.includes('expired') || err.message.includes('No Job ID'))) {
|
|
75
|
+
throw err;
|
|
76
|
+
}
|
|
77
|
+
// Ignore poll errors
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
catch (error) {
|
|
82
|
+
await ctx.actions.runAction('core:log', { level: 'error', message: `Authentication failed: ${error.message}` });
|
|
83
|
+
throw error;
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
});
|
|
87
|
+
}
|
|
88
|
+
};
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
import { z } from 'zod';
|
|
2
|
+
export const ConfigPlugin = {
|
|
3
|
+
name: 'config',
|
|
4
|
+
version: '1.0.0',
|
|
5
|
+
dependencies: [],
|
|
6
|
+
async setup(ctx) {
|
|
7
|
+
// 1. Define Schema
|
|
8
|
+
const configSchema = z.object({
|
|
9
|
+
host: z.string().url(),
|
|
10
|
+
jobId: z.string().min(1, "Job ID is required"),
|
|
11
|
+
token: z.string().optional(),
|
|
12
|
+
user: z.string().optional(),
|
|
13
|
+
workDir: z.string()
|
|
14
|
+
});
|
|
15
|
+
ctx.actions.registerAction({
|
|
16
|
+
id: 'config:validate',
|
|
17
|
+
handler: async () => {
|
|
18
|
+
const config = ctx.config;
|
|
19
|
+
try {
|
|
20
|
+
configSchema.parse(config);
|
|
21
|
+
return true;
|
|
22
|
+
}
|
|
23
|
+
catch (error) {
|
|
24
|
+
if (error instanceof z.ZodError) {
|
|
25
|
+
const issues = error.issues.map(i => `${i.path.join('.')}: ${i.message}`).join(', ');
|
|
26
|
+
await ctx.actions.runAction('core:log', { level: 'error', message: `Config Validation Failed: ${issues}` });
|
|
27
|
+
}
|
|
28
|
+
throw error;
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
});
|
|
32
|
+
// [NEW] Allow runtime config updates
|
|
33
|
+
ctx.actions.registerAction({
|
|
34
|
+
id: 'config:set',
|
|
35
|
+
handler: async (payload) => {
|
|
36
|
+
ctx.getRuntime().updateConfig(payload);
|
|
37
|
+
const config = ctx.config;
|
|
38
|
+
// Validate after set? Optional, but good practice.
|
|
39
|
+
try {
|
|
40
|
+
configSchema.parse(config);
|
|
41
|
+
}
|
|
42
|
+
catch (e) {
|
|
43
|
+
// Log but don't revert for now, trust the caller or add rollback logic if needed.
|
|
44
|
+
// Just warn for now
|
|
45
|
+
await ctx.actions.runAction('core:log', { level: 'warn', message: 'Config updated but validation failed. Some features may not work.' });
|
|
46
|
+
}
|
|
47
|
+
return config;
|
|
48
|
+
}
|
|
49
|
+
});
|
|
50
|
+
}
|
|
51
|
+
};
|
|
@@ -4,8 +4,7 @@ export const CorePlugin = {
|
|
|
4
4
|
version: '1.0.0',
|
|
5
5
|
setup(ctx) {
|
|
6
6
|
console.log('CorePlugin: setup called');
|
|
7
|
-
|
|
8
|
-
const config = ctx.host.config;
|
|
7
|
+
const config = ctx.config;
|
|
9
8
|
ctx.actions.registerAction({
|
|
10
9
|
id: 'core:config',
|
|
11
10
|
handler: async () => config
|
|
@@ -15,9 +14,7 @@ export const CorePlugin = {
|
|
|
15
14
|
id: 'core:log',
|
|
16
15
|
handler: async (payload) => {
|
|
17
16
|
// Access default logger from Runtime
|
|
18
|
-
const
|
|
19
|
-
// Logger is now public
|
|
20
|
-
const logger = rt.logger || console;
|
|
17
|
+
const logger = ctx.logger;
|
|
21
18
|
const { level, message } = payload;
|
|
22
19
|
switch (level) {
|
|
23
20
|
case 'error':
|
|
@@ -8,12 +8,20 @@ let checkInterval;
|
|
|
8
8
|
export const DownloaderPlugin = {
|
|
9
9
|
name: 'downloader',
|
|
10
10
|
version: '1.0.0',
|
|
11
|
+
dependencies: ['config'],
|
|
11
12
|
setup(ctx) {
|
|
12
|
-
|
|
13
|
-
const
|
|
14
|
-
|
|
13
|
+
// Helper to get paths dynamically
|
|
14
|
+
const getPaths = () => {
|
|
15
|
+
const workDir = ctx.config.workDir;
|
|
16
|
+
return {
|
|
17
|
+
DIST_DIR: path.join(workDir, 'dist'),
|
|
18
|
+
DOWNLOAD_PATH: path.join(workDir, 'extension.zip'),
|
|
19
|
+
VERSION_FILE: path.join(workDir, 'version')
|
|
20
|
+
};
|
|
21
|
+
};
|
|
15
22
|
// Helper function to create axios client with current config
|
|
16
23
|
const createClient = () => {
|
|
24
|
+
const config = ctx.config;
|
|
17
25
|
const rawToken = config.token ? String(config.token) : '';
|
|
18
26
|
const token = rawToken.replace(/^Bearer\s+/i, '').trim();
|
|
19
27
|
// Auto-extract user ID from token if not provided
|
|
@@ -42,11 +50,16 @@ export const DownloaderPlugin = {
|
|
|
42
50
|
})
|
|
43
51
|
});
|
|
44
52
|
};
|
|
45
|
-
const VERSION_FILE = path.join(config.workDir, 'version');
|
|
46
53
|
let lastModified = '';
|
|
47
|
-
|
|
48
|
-
|
|
54
|
+
let currentWorkDir = '';
|
|
55
|
+
// Check initial state if workDir exists
|
|
56
|
+
try {
|
|
57
|
+
const { VERSION_FILE } = getPaths();
|
|
58
|
+
if (fs.existsSync(VERSION_FILE)) {
|
|
59
|
+
lastModified = fs.readFileSync(VERSION_FILE, 'utf-8').trim();
|
|
60
|
+
}
|
|
49
61
|
}
|
|
62
|
+
catch (e) { }
|
|
50
63
|
let isChecking = false;
|
|
51
64
|
// Action: Check Status
|
|
52
65
|
ctx.actions.registerAction({
|
|
@@ -55,12 +68,23 @@ export const DownloaderPlugin = {
|
|
|
55
68
|
if (isChecking)
|
|
56
69
|
return true; // Skip if busy
|
|
57
70
|
isChecking = true;
|
|
71
|
+
const { jobId, workDir } = ctx.config;
|
|
72
|
+
const { DIST_DIR, VERSION_FILE } = getPaths();
|
|
73
|
+
// Reset lastModified if workDir changed
|
|
74
|
+
if (workDir !== currentWorkDir) {
|
|
75
|
+
currentWorkDir = workDir;
|
|
76
|
+
lastModified = '';
|
|
77
|
+
if (fs.existsSync(VERSION_FILE)) {
|
|
78
|
+
lastModified = fs.readFileSync(VERSION_FILE, 'utf-8').trim();
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
await ctx.actions.runAction('core:log', { level: 'info', message: 'Checking for updates...' });
|
|
58
82
|
const MAX_RETRIES = 3;
|
|
59
83
|
let attempt = 0;
|
|
60
84
|
while (attempt < MAX_RETRIES) {
|
|
61
85
|
try {
|
|
62
86
|
const client = createClient(); // Create client with current config
|
|
63
|
-
const res = await client.get(`/jobs/${
|
|
87
|
+
const res = await client.get(`/jobs/${jobId}`);
|
|
64
88
|
const job = res.data;
|
|
65
89
|
const newVersion = job.version;
|
|
66
90
|
// If no version in job yet, fall back to timestamp or ignore
|
|
@@ -83,7 +107,7 @@ export const DownloaderPlugin = {
|
|
|
83
107
|
if (success) {
|
|
84
108
|
lastModified = newVersion;
|
|
85
109
|
fs.writeFileSync(VERSION_FILE, newVersion);
|
|
86
|
-
ctx.events.emit('downloader:updated', { version: job.version, jobId: config.jobId });
|
|
110
|
+
ctx.events.emit('downloader:updated', { version: job.version, jobId: ctx.config.jobId });
|
|
87
111
|
}
|
|
88
112
|
}
|
|
89
113
|
}
|
|
@@ -117,21 +141,22 @@ export const DownloaderPlugin = {
|
|
|
117
141
|
const spinner = ora('Downloading new version...').start();
|
|
118
142
|
try {
|
|
119
143
|
const client = createClient(); // Create client with current config
|
|
120
|
-
const
|
|
144
|
+
const { DIST_DIR, DOWNLOAD_PATH, VERSION_FILE } = getPaths();
|
|
145
|
+
const response = await client.get(`/download/${ctx.config.jobId}`, {
|
|
121
146
|
responseType: 'arraybuffer'
|
|
122
147
|
});
|
|
123
|
-
await fs.ensureDir(config.workDir);
|
|
148
|
+
await fs.ensureDir(ctx.config.workDir);
|
|
124
149
|
await fs.writeFile(DOWNLOAD_PATH, response.data);
|
|
125
150
|
await fs.emptyDir(DIST_DIR);
|
|
126
151
|
const zip = new AdmZip(DOWNLOAD_PATH);
|
|
127
152
|
zip.extractAllTo(DIST_DIR, true);
|
|
128
153
|
// --- HOT RELOAD INJECTION ---
|
|
129
154
|
try {
|
|
130
|
-
// Get dynamically allocated port from ServerPlugin
|
|
131
|
-
const hotReloadPort = ctx.hotReloadPort || 3500;
|
|
155
|
+
// Get dynamically allocated port from ServerPlugin via config
|
|
156
|
+
const hotReloadPort = ctx.config.hotReloadPort || 3500;
|
|
132
157
|
const HOT_RELOAD_CODE = `
|
|
133
158
|
const EVENT_SOURCE_URL = 'http://localhost:${hotReloadPort}/status';
|
|
134
|
-
const CURRENT_JOB_ID = '${config.jobId}';
|
|
159
|
+
const CURRENT_JOB_ID = '${ctx.config.jobId}';
|
|
135
160
|
let lastVersion = null;
|
|
136
161
|
let lastJobId = null;
|
|
137
162
|
|
|
@@ -2,7 +2,9 @@ import http from 'http';
|
|
|
2
2
|
export const ServerPlugin = {
|
|
3
3
|
name: 'server',
|
|
4
4
|
version: '1.0.0',
|
|
5
|
+
dependencies: ['config'],
|
|
5
6
|
async setup(ctx) {
|
|
7
|
+
// const context = ctx as PreviewContext; // No longer needed
|
|
6
8
|
let currentVersion = '0.0.0';
|
|
7
9
|
// Try to bind to a port, retrying with incremented ports on failure
|
|
8
10
|
const startPort = 3500;
|
|
@@ -29,7 +31,7 @@ export const ServerPlugin = {
|
|
|
29
31
|
return;
|
|
30
32
|
}
|
|
31
33
|
if (req.url === '/status') {
|
|
32
|
-
const currentJobId = ctx.
|
|
34
|
+
const currentJobId = ctx.config.jobId;
|
|
33
35
|
res.writeHead(200, { 'Content-Type': 'application/json' });
|
|
34
36
|
res.end(JSON.stringify({
|
|
35
37
|
version: currentVersion,
|
|
@@ -38,15 +40,37 @@ export const ServerPlugin = {
|
|
|
38
40
|
}));
|
|
39
41
|
}
|
|
40
42
|
else if (req.url === '/refresh' && req.method === 'POST') {
|
|
41
|
-
//
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
}).catch((err) => {
|
|
46
|
-
ctx.actions.runAction('core:log', { level: 'error', message: `[API] Check failed: ${err.message}` });
|
|
43
|
+
// Collect body
|
|
44
|
+
let body = '';
|
|
45
|
+
req.on('data', chunk => {
|
|
46
|
+
body += chunk.toString();
|
|
47
47
|
});
|
|
48
|
-
|
|
49
|
-
|
|
48
|
+
req.on('end', () => {
|
|
49
|
+
let newJobId = null;
|
|
50
|
+
try {
|
|
51
|
+
if (body) {
|
|
52
|
+
const data = JSON.parse(body);
|
|
53
|
+
if (data.jobId) {
|
|
54
|
+
newJobId = data.jobId;
|
|
55
|
+
ctx.getRuntime().updateConfig({ jobId: newJobId });
|
|
56
|
+
ctx.actions.runAction('core:log', { level: 'info', message: `[API] Switched to new Job ID: ${newJobId}` });
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
catch (e) {
|
|
61
|
+
// Ignore parse error
|
|
62
|
+
}
|
|
63
|
+
// Trigger manual check
|
|
64
|
+
ctx.actions.runAction('core:log', { level: 'info', message: '[API] Refresh request received' });
|
|
65
|
+
ctx.actions.runAction('downloader:check', null).then((result) => {
|
|
66
|
+
ctx.actions.runAction('core:log', { level: 'info', message: `[API] Check result: ${result}` });
|
|
67
|
+
}).catch((err) => {
|
|
68
|
+
ctx.actions.runAction('core:log', { level: 'error', message: `[API] Check failed: ${err.message}` });
|
|
69
|
+
});
|
|
70
|
+
res.writeHead(200, { 'Content-Type': 'application/json' });
|
|
71
|
+
res.end(JSON.stringify({ success: true, jobId: ctx.config.jobId }));
|
|
72
|
+
});
|
|
73
|
+
return; // Return because we handle response in 'end' callback
|
|
50
74
|
}
|
|
51
75
|
else if (req.url === '/disconnect' && req.method === 'POST') {
|
|
52
76
|
// Trigger browser stop
|
|
@@ -110,7 +134,7 @@ export const ServerPlugin = {
|
|
|
110
134
|
return;
|
|
111
135
|
}
|
|
112
136
|
// Store port in context for DownloaderPlugin to use
|
|
113
|
-
ctx.hotReloadPort
|
|
137
|
+
ctx.getRuntime().updateConfig({ hotReloadPort: allocatedPort });
|
|
114
138
|
// Store server instance to close later
|
|
115
139
|
ctx._serverInstance = server;
|
|
116
140
|
},
|
|
@@ -4,18 +4,22 @@ import { findExtensionRoot, validateExtension } from '../../utils/browserUtils.j
|
|
|
4
4
|
export const BrowserManagerPlugin = {
|
|
5
5
|
name: 'browser-manager',
|
|
6
6
|
version: '1.0.0',
|
|
7
|
+
dependencies: ['config', 'downloader'],
|
|
7
8
|
setup(ctx) {
|
|
8
|
-
|
|
9
|
-
const
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
9
|
+
// Helper to get dynamic paths
|
|
10
|
+
const getPaths = () => {
|
|
11
|
+
const config = ctx.config;
|
|
12
|
+
const DIST_DIR = path.join(config.workDir, 'dist');
|
|
13
|
+
const isWSL = fs.existsSync('/mnt/c');
|
|
14
|
+
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 };
|
|
19
|
+
};
|
|
17
20
|
// --- SYNC FUNCTION ---
|
|
18
21
|
const syncToStaging = async () => {
|
|
22
|
+
const { DIST_DIR, STAGING_DIR } = getPaths();
|
|
19
23
|
try {
|
|
20
24
|
if (fs.existsSync(STAGING_DIR)) {
|
|
21
25
|
fs.emptyDirSync(STAGING_DIR);
|
|
@@ -31,9 +35,10 @@ export const BrowserManagerPlugin = {
|
|
|
31
35
|
}
|
|
32
36
|
};
|
|
33
37
|
const launchBrowser = async () => {
|
|
38
|
+
const { STAGING_DIR } = getPaths();
|
|
34
39
|
// Resolve proper root AFTER sync
|
|
35
40
|
const extensionRoot = findExtensionRoot(STAGING_DIR) || STAGING_DIR;
|
|
36
|
-
//
|
|
41
|
+
// 1. Static Validation
|
|
37
42
|
const validation = validateExtension(extensionRoot);
|
|
38
43
|
if (!validation.valid) {
|
|
39
44
|
await ctx.actions.runAction('core:log', { level: 'error', message: `[CRITICAL] Extension validation failed: ${validation.error} in ${extensionRoot}` });
|
|
@@ -41,6 +46,19 @@ export const BrowserManagerPlugin = {
|
|
|
41
46
|
else if (extensionRoot !== STAGING_DIR) {
|
|
42
47
|
await ctx.actions.runAction('core:log', { level: 'info', message: `Detected nested extension at: ${path.basename(extensionRoot)}` });
|
|
43
48
|
}
|
|
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
|
+
*/
|
|
44
62
|
// Delegate Launch
|
|
45
63
|
// We pass the filesystem path (STAGING_DIR or extensionRoot)
|
|
46
64
|
// The specific Launcher plugin handles environment specific path verification/conversion
|
|
@@ -72,7 +90,15 @@ export const BrowserManagerPlugin = {
|
|
|
72
90
|
// Event: Update detected
|
|
73
91
|
ctx.events.on('downloader:updated', async () => {
|
|
74
92
|
if (isInitialized) {
|
|
75
|
-
await ctx.actions.runAction('core:log', { level: 'info', message: 'Update detected.
|
|
93
|
+
await ctx.actions.runAction('core:log', { level: 'info', message: 'Update detected. Restarting browser...' });
|
|
94
|
+
try {
|
|
95
|
+
await ctx.actions.runAction('browser:stop', {});
|
|
96
|
+
}
|
|
97
|
+
catch (e) {
|
|
98
|
+
// Ignore if already stopped
|
|
99
|
+
}
|
|
100
|
+
// [Optimization] Wait for process cleanup to avoid "Open in new tab" race condition
|
|
101
|
+
await new Promise(r => setTimeout(r, 1000));
|
|
76
102
|
await ctx.actions.runAction('browser:start', {});
|
|
77
103
|
}
|
|
78
104
|
});
|
|
@@ -6,6 +6,7 @@ let chromeProcess = null;
|
|
|
6
6
|
export const NativeLauncherPlugin = {
|
|
7
7
|
name: 'native-launcher',
|
|
8
8
|
version: '1.0.0',
|
|
9
|
+
dependencies: ['config'],
|
|
9
10
|
setup(ctx) {
|
|
10
11
|
// Only active if NOT in WSL
|
|
11
12
|
const isWSL = fs.existsSync('/mnt/c');
|
|
@@ -14,7 +15,7 @@ export const NativeLauncherPlugin = {
|
|
|
14
15
|
ctx.actions.registerAction({
|
|
15
16
|
id: 'launcher:launch',
|
|
16
17
|
handler: async (payload) => {
|
|
17
|
-
const config = ctx.
|
|
18
|
+
const config = ctx.config;
|
|
18
19
|
const chromePath = findChrome();
|
|
19
20
|
if (!chromePath) {
|
|
20
21
|
await ctx.actions.runAction('core:log', { level: 'error', message: 'Chrome not found.' });
|
|
@@ -6,6 +6,7 @@ let chromePid = null;
|
|
|
6
6
|
export const WSLLauncherPlugin = {
|
|
7
7
|
name: 'wsl-launcher',
|
|
8
8
|
version: '1.0.0',
|
|
9
|
+
dependencies: ['config'],
|
|
9
10
|
setup(ctx) {
|
|
10
11
|
// Only active in WSL
|
|
11
12
|
const isWSL = fs.existsSync('/mnt/c');
|
|
@@ -118,19 +119,37 @@ Write-Host "CHROME_PID:$($process.Id)"
|
|
|
118
119
|
if (chromePid) {
|
|
119
120
|
await ctx.actions.runAction('core:log', { level: 'info', message: `Terminating Chrome process (PID: ${chromePid})...` });
|
|
120
121
|
try {
|
|
121
|
-
//
|
|
122
|
-
const
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
122
|
+
// 1. Try Stop-Process first (Graceful)
|
|
123
|
+
const killCmd = `
|
|
124
|
+
$targetPid = ${chromePid}
|
|
125
|
+
try {
|
|
126
|
+
Stop-Process -Id $targetPid -Force -ErrorAction Stop
|
|
127
|
+
Write-Host "STOPPED"
|
|
128
|
+
} catch {
|
|
129
|
+
try {
|
|
130
|
+
taskkill.exe /F /PID $targetPid
|
|
131
|
+
Write-Host "TASKKILLED"
|
|
132
|
+
} catch {
|
|
133
|
+
Write-Host "FAILED: $_"
|
|
134
|
+
exit 1
|
|
135
|
+
}
|
|
132
136
|
}
|
|
137
|
+
`;
|
|
138
|
+
const killChild = spawn('powershell.exe', ['-Command', killCmd], { stdio: 'pipe' });
|
|
139
|
+
// Capture output to debug why it might fail
|
|
140
|
+
if (killChild.stdout) {
|
|
141
|
+
killChild.stdout.on('data', d => ctx.actions.runAction('core:log', { level: 'debug', message: `[KillParams] ${d}` }));
|
|
142
|
+
}
|
|
143
|
+
if (killChild.stderr) {
|
|
144
|
+
killChild.stderr.on('data', d => ctx.actions.runAction('core:log', { level: 'warn', message: `[KillMsg] ${d}` }));
|
|
145
|
+
}
|
|
146
|
+
await new Promise((resolve) => {
|
|
147
|
+
killChild.on('exit', (code) => {
|
|
148
|
+
resolve();
|
|
149
|
+
});
|
|
133
150
|
});
|
|
151
|
+
await ctx.actions.runAction('core:log', { level: 'info', message: 'Chrome process termination signal sent.' });
|
|
152
|
+
chromePid = null;
|
|
134
153
|
return true;
|
|
135
154
|
}
|
|
136
155
|
catch (err) {
|
package/dist/types.js
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,192 @@
|
|
|
1
|
+
import puppeteer from 'puppeteer-core';
|
|
2
|
+
import path from 'path';
|
|
3
|
+
import fs from 'fs-extra';
|
|
4
|
+
import { spawn, execSync } from 'child_process';
|
|
5
|
+
import axios from 'axios';
|
|
6
|
+
import { findChrome } from './browserUtils.js';
|
|
7
|
+
export class SandboxRunner {
|
|
8
|
+
/**
|
|
9
|
+
* Launch a headless browser with the extension loaded to verify it can initialize.
|
|
10
|
+
* @param extensionPath Absolute path to the unpacked extension directory
|
|
11
|
+
* @param chromePath Optional path to Chrome executable. If not provided, attempts to auto-detect.
|
|
12
|
+
*/
|
|
13
|
+
static async validateExtensionRuntime(extensionPath, chromePath) {
|
|
14
|
+
const logs = [];
|
|
15
|
+
const executablePath = chromePath || findChrome();
|
|
16
|
+
if (!executablePath) {
|
|
17
|
+
return {
|
|
18
|
+
success: false,
|
|
19
|
+
logs,
|
|
20
|
+
error: 'Chrome executable not found. Cannot run verification.'
|
|
21
|
+
};
|
|
22
|
+
}
|
|
23
|
+
const isWSL = executablePath.startsWith('/mnt/');
|
|
24
|
+
if (isWSL) {
|
|
25
|
+
logs.push('[Sandbox] WSL Environment detected. Using "Spawn & Connect" strategy.');
|
|
26
|
+
return this.runWSLCheck(extensionPath, executablePath, logs);
|
|
27
|
+
}
|
|
28
|
+
else {
|
|
29
|
+
return this.runStandardCheck(extensionPath, executablePath, logs);
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
static async runStandardCheck(extensionPath, executablePath, logs) {
|
|
33
|
+
let browser;
|
|
34
|
+
try {
|
|
35
|
+
logs.push(`[Sandbox] Launching standard verification for: ${extensionPath}`);
|
|
36
|
+
logs.push(`[Sandbox] Using Chrome at: ${executablePath}`);
|
|
37
|
+
browser = await puppeteer.launch({
|
|
38
|
+
headless: true,
|
|
39
|
+
executablePath: executablePath,
|
|
40
|
+
args: [
|
|
41
|
+
`--disable-extensions-except=${extensionPath}`,
|
|
42
|
+
`--load-extension=${extensionPath}`,
|
|
43
|
+
'--no-sandbox',
|
|
44
|
+
'--disable-setuid-sandbox'
|
|
45
|
+
]
|
|
46
|
+
});
|
|
47
|
+
return await this.performChecks(browser, extensionPath, logs);
|
|
48
|
+
}
|
|
49
|
+
catch (error) {
|
|
50
|
+
console.error('[Sandbox] Standard Launch Error:', error);
|
|
51
|
+
return { success: false, logs, error: error instanceof Error ? error.message : String(error) };
|
|
52
|
+
}
|
|
53
|
+
finally {
|
|
54
|
+
if (browser)
|
|
55
|
+
await browser.close();
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
static async runWSLCheck(extensionPath, linuxChromePath, logs) {
|
|
59
|
+
let browser;
|
|
60
|
+
let chromePid = null;
|
|
61
|
+
try {
|
|
62
|
+
// 1. Path Conversion (Linux -> Windows)
|
|
63
|
+
const driveMatch = linuxChromePath.match(/^\/mnt\/([a-z])\//);
|
|
64
|
+
if (!driveMatch)
|
|
65
|
+
throw new Error(`Could not parse drive letter from ${linuxChromePath}`);
|
|
66
|
+
const driveLetter = driveMatch[1];
|
|
67
|
+
const winChromePath = linuxChromePath
|
|
68
|
+
.replace(new RegExp(`^/mnt/${driveLetter}/`), `${driveLetter.toUpperCase()}:\\`)
|
|
69
|
+
.replace(/\//g, '\\');
|
|
70
|
+
// 1b. Detect Host IP (WSL DNS Resolver IP)
|
|
71
|
+
let hostIp = '127.0.0.1';
|
|
72
|
+
try {
|
|
73
|
+
const resolveConf = fs.readFileSync('/etc/resolv.conf', 'utf-8');
|
|
74
|
+
const match = resolveConf.match(/nameserver\s+([\d.]+)/);
|
|
75
|
+
if (match)
|
|
76
|
+
hostIp = match[1];
|
|
77
|
+
logs.push(`[Sandbox] Host IP detected: ${hostIp}`);
|
|
78
|
+
}
|
|
79
|
+
catch (e) {
|
|
80
|
+
logs.push(`[Sandbox] Failed to detect Host IP, fallback to 127.0.0.1: ${e}`);
|
|
81
|
+
}
|
|
82
|
+
let winExtensionPath = extensionPath;
|
|
83
|
+
const extDriveMatch = extensionPath.match(/^\/mnt\/([a-z])\//);
|
|
84
|
+
if (extDriveMatch) {
|
|
85
|
+
winExtensionPath = extensionPath
|
|
86
|
+
.replace(new RegExp(`^/mnt/${extDriveMatch[1]}/`), `${extDriveMatch[1].toUpperCase()}:\\`)
|
|
87
|
+
.replace(/\//g, '\\');
|
|
88
|
+
}
|
|
89
|
+
else {
|
|
90
|
+
logs.push('[Sandbox] WARNING: Extension path is not in /mnt/. Windows Chrome might not see it.');
|
|
91
|
+
}
|
|
92
|
+
// 2. Spawn Chrome via PowerShell
|
|
93
|
+
const port = 9222;
|
|
94
|
+
const winProfile = `C:\\Temp\\ai-ext-sandbox-${Date.now()}`;
|
|
95
|
+
const args = [
|
|
96
|
+
`--headless=new`,
|
|
97
|
+
`--disable-extensions-except="${winExtensionPath}"`,
|
|
98
|
+
`--load-extension="${winExtensionPath}"`,
|
|
99
|
+
`--user-data-dir="${winProfile}"`,
|
|
100
|
+
'--no-sandbox',
|
|
101
|
+
'--disable-gpu',
|
|
102
|
+
'--disable-dev-shm-usage',
|
|
103
|
+
'--no-first-run',
|
|
104
|
+
'--no-default-browser-check',
|
|
105
|
+
`--remote-debugging-port=${port}`,
|
|
106
|
+
`--remote-debugging-address=0.0.0.0`, // Bind to all interfaces so WSL can see it
|
|
107
|
+
`--remote-allow-origins=*` // Allow puppeteer connection
|
|
108
|
+
];
|
|
109
|
+
const psCommand = `Start-Process -FilePath "${winChromePath}" -ArgumentList '${args.join(' ')}' -PassThru`;
|
|
110
|
+
logs.push(`[Sandbox] Spawning Chrome via PowerShell on port ${port}...`);
|
|
111
|
+
logs.push(`[Sandbox] Profile: ${winProfile}`);
|
|
112
|
+
const child = spawn('powershell.exe', ['-Command', psCommand], { stdio: 'pipe' });
|
|
113
|
+
await new Promise((resolve, reject) => {
|
|
114
|
+
child.stdout.on('data', (data) => {
|
|
115
|
+
const output = data.toString();
|
|
116
|
+
const match = output.match(/\s+(\d+)\s+\d+\s+chrome/i) || output.match(/Id\s+:\s+(\d+)/);
|
|
117
|
+
if (match) {
|
|
118
|
+
chromePid = parseInt(match[1], 10);
|
|
119
|
+
logs.push(`[Sandbox] Chrome PID: ${chromePid}`);
|
|
120
|
+
}
|
|
121
|
+
});
|
|
122
|
+
child.on('close', (code) => {
|
|
123
|
+
if (code === 0)
|
|
124
|
+
resolve();
|
|
125
|
+
else
|
|
126
|
+
reject(new Error(`PowerShell exited with code ${code}`));
|
|
127
|
+
});
|
|
128
|
+
});
|
|
129
|
+
// 3. Wait for Port
|
|
130
|
+
logs.push('[Sandbox] Waiting for Chrome to accept connections...');
|
|
131
|
+
let connected = false;
|
|
132
|
+
// Increased timeout to 15s (30 * 500ms)
|
|
133
|
+
for (let i = 0; i < 30; i++) {
|
|
134
|
+
try {
|
|
135
|
+
// Use hostIp, not localhost
|
|
136
|
+
await axios.get(`http://${hostIp}:${port}/json/version`, { timeout: 1000 });
|
|
137
|
+
connected = true;
|
|
138
|
+
break;
|
|
139
|
+
}
|
|
140
|
+
catch (e) {
|
|
141
|
+
await new Promise(r => setTimeout(r, 500));
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
if (!connected)
|
|
145
|
+
throw new Error(`Timed out waiting for Chrome debug port ${port}`);
|
|
146
|
+
// 4. Connect Puppeteer
|
|
147
|
+
logs.push('[Sandbox] Connecting Puppeteer...');
|
|
148
|
+
browser = await puppeteer.connect({
|
|
149
|
+
browserURL: `http://${hostIp}:${port}`
|
|
150
|
+
});
|
|
151
|
+
// 5. Perform Checks
|
|
152
|
+
const result = await this.performChecks(browser, extensionPath, logs);
|
|
153
|
+
return result;
|
|
154
|
+
}
|
|
155
|
+
catch (error) {
|
|
156
|
+
console.error('[Sandbox] WSL Check Error:', error);
|
|
157
|
+
return { success: false, logs, error: error instanceof Error ? error.message : String(error) };
|
|
158
|
+
}
|
|
159
|
+
finally {
|
|
160
|
+
if (browser)
|
|
161
|
+
await browser.disconnect();
|
|
162
|
+
if (chromePid) {
|
|
163
|
+
logs.push(`[Sandbox] Killing Chrome PID ${chromePid}...`);
|
|
164
|
+
try {
|
|
165
|
+
execSync(`powershell.exe -Command "Stop-Process -Id ${chromePid} -Force"`);
|
|
166
|
+
}
|
|
167
|
+
catch (e) { /* ignore */ }
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
static async performChecks(browser, extensionPath, logs) {
|
|
172
|
+
await new Promise(r => setTimeout(r, 2000));
|
|
173
|
+
const targets = await browser.targets();
|
|
174
|
+
const backgroundTarget = targets.find(t => t.type() === 'service_worker' || t.type() === 'background_page');
|
|
175
|
+
if (!backgroundTarget) {
|
|
176
|
+
const manifestPath = path.join(extensionPath, 'manifest.json');
|
|
177
|
+
if (fs.existsSync(manifestPath)) {
|
|
178
|
+
const manifest = await fs.readJson(manifestPath);
|
|
179
|
+
if (manifest.background) {
|
|
180
|
+
return { success: false, logs, error: 'Background Service Worker defined in manifest but failed to start.' };
|
|
181
|
+
}
|
|
182
|
+
else {
|
|
183
|
+
logs.push('[Sandbox] No background script defined in manifest. Skipping worker check.');
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
else {
|
|
188
|
+
logs.push('Background worker started successfully.');
|
|
189
|
+
}
|
|
190
|
+
return { success: true, logs };
|
|
191
|
+
}
|
|
192
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "ai-extension-preview",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.15",
|
|
4
4
|
"description": "Local preview tool for AI Extension Builder",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
@@ -37,9 +37,10 @@
|
|
|
37
37
|
"node-fetch": "^3.3.2",
|
|
38
38
|
"ora": "^8.1.1",
|
|
39
39
|
"puppeteer-core": "^24.33.0",
|
|
40
|
-
"skeleton-crew-runtime": "
|
|
40
|
+
"skeleton-crew-runtime": "0.2.0",
|
|
41
41
|
"web-ext": "^8.3.0",
|
|
42
|
-
"ws": "^8.18.0"
|
|
42
|
+
"ws": "^8.18.0",
|
|
43
|
+
"zod": "^4.2.1"
|
|
43
44
|
},
|
|
44
45
|
"devDependencies": {
|
|
45
46
|
"@types/adm-zip": "^0.5.6",
|
|
@@ -51,4 +52,4 @@
|
|
|
51
52
|
"typescript": "^5.7.2",
|
|
52
53
|
"vitest": "^4.0.16"
|
|
53
54
|
}
|
|
54
|
-
}
|
|
55
|
+
}
|