ai-extension-preview 0.1.8 → 0.1.9
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.
|
@@ -22,7 +22,28 @@ function findChrome() {
|
|
|
22
22
|
}
|
|
23
23
|
return null;
|
|
24
24
|
}
|
|
25
|
-
|
|
25
|
+
// --- Helper to find actual extension root (handle nested folder in zip) ---
|
|
26
|
+
export const findExtensionRoot = (dir) => {
|
|
27
|
+
if (fs.existsSync(path.join(dir, 'manifest.json')))
|
|
28
|
+
return dir;
|
|
29
|
+
// Check immediate subdirectories (depth 1)
|
|
30
|
+
try {
|
|
31
|
+
const items = fs.readdirSync(dir);
|
|
32
|
+
for (const item of items) {
|
|
33
|
+
const fullPath = path.join(dir, item);
|
|
34
|
+
if (fs.statSync(fullPath).isDirectory()) {
|
|
35
|
+
if (fs.existsSync(path.join(fullPath, 'manifest.json'))) {
|
|
36
|
+
return fullPath;
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
catch (e) {
|
|
42
|
+
// Dir might be empty or invalid
|
|
43
|
+
}
|
|
44
|
+
return null;
|
|
45
|
+
};
|
|
46
|
+
export const normalizePathToWindows = (p) => {
|
|
26
47
|
// Handle Git Bash /c/ style
|
|
27
48
|
const gitBashMatch = p.match(/^\/([a-z])\/(.*)/i);
|
|
28
49
|
if (gitBashMatch) {
|
|
@@ -31,6 +52,32 @@ const normalizePathToWindows = (p) => {
|
|
|
31
52
|
// Handle Forward slashes
|
|
32
53
|
return p.replace(/\//g, '\\');
|
|
33
54
|
};
|
|
55
|
+
export const stripTrailingSlash = (p) => {
|
|
56
|
+
return p.replace(/[\\\/]+$/, '');
|
|
57
|
+
};
|
|
58
|
+
// --- Helper to validate extension directory existence and structure ---
|
|
59
|
+
export const validateExtension = (dir) => {
|
|
60
|
+
if (!fs.existsSync(dir)) {
|
|
61
|
+
return { valid: false, error: 'Directory does not exist' };
|
|
62
|
+
}
|
|
63
|
+
const stats = fs.statSync(dir);
|
|
64
|
+
if (!stats.isDirectory()) {
|
|
65
|
+
return { valid: false, error: 'Path is not a directory' };
|
|
66
|
+
}
|
|
67
|
+
const manifestPath = path.join(dir, 'manifest.json');
|
|
68
|
+
if (!fs.existsSync(manifestPath)) {
|
|
69
|
+
return { valid: false, error: 'manifest.json missing' };
|
|
70
|
+
}
|
|
71
|
+
// Basic JSON validity check
|
|
72
|
+
try {
|
|
73
|
+
const content = fs.readFileSync(manifestPath, 'utf-8');
|
|
74
|
+
JSON.parse(content);
|
|
75
|
+
}
|
|
76
|
+
catch (e) {
|
|
77
|
+
return { valid: false, error: 'manifest.json is invalid JSON' };
|
|
78
|
+
}
|
|
79
|
+
return { valid: true };
|
|
80
|
+
};
|
|
34
81
|
export const BrowserPlugin = {
|
|
35
82
|
name: 'browser',
|
|
36
83
|
version: '1.0.0',
|
|
@@ -77,35 +124,19 @@ export const BrowserPlugin = {
|
|
|
77
124
|
await ctx.actions.runAction('core:log', { level: 'error', message: `Failed to sync to staging: ${err.message}` });
|
|
78
125
|
}
|
|
79
126
|
};
|
|
80
|
-
// --- Helper to find actual extension root (handle nested folder in zip) ---
|
|
81
|
-
const findExtensionRoot = (dir) => {
|
|
82
|
-
if (fs.existsSync(path.join(dir, 'manifest.json')))
|
|
83
|
-
return dir;
|
|
84
|
-
// Check immediate subdirectories (depth 1)
|
|
85
|
-
try {
|
|
86
|
-
const items = fs.readdirSync(dir);
|
|
87
|
-
for (const item of items) {
|
|
88
|
-
const fullPath = path.join(dir, item);
|
|
89
|
-
if (fs.statSync(fullPath).isDirectory()) {
|
|
90
|
-
if (fs.existsSync(path.join(fullPath, 'manifest.json'))) {
|
|
91
|
-
return fullPath;
|
|
92
|
-
}
|
|
93
|
-
}
|
|
94
|
-
}
|
|
95
|
-
}
|
|
96
|
-
catch (e) {
|
|
97
|
-
// Dir might be empty or invalid
|
|
98
|
-
}
|
|
99
|
-
return null;
|
|
100
|
-
};
|
|
101
127
|
// Initial Sync
|
|
102
128
|
await syncToStaging();
|
|
103
129
|
// Resolve proper root AFTER sync
|
|
104
130
|
let extensionRoot = findExtensionRoot(STAGING_DIR) || STAGING_DIR;
|
|
105
131
|
// Check if we found a valid root
|
|
106
|
-
|
|
107
|
-
|
|
132
|
+
const validation = validateExtension(extensionRoot);
|
|
133
|
+
if (!validation.valid) {
|
|
134
|
+
await ctx.actions.runAction('core:log', { level: 'error', message: `[CRITICAL] Extension validation failed: ${validation.error} in ${extensionRoot}` });
|
|
108
135
|
await ctx.actions.runAction('core:log', { level: 'info', message: `Checked Path: ${extensionRoot}` });
|
|
136
|
+
// We proceed anyway? Or should we stop?
|
|
137
|
+
// Previous logic proceeded but logged critical error.
|
|
138
|
+
// Let's keep it logging critical but maybe return false if we wanted to be strict.
|
|
139
|
+
// However, user might fix it live.
|
|
109
140
|
}
|
|
110
141
|
else if (extensionRoot !== STAGING_DIR) {
|
|
111
142
|
await ctx.actions.runAction('core:log', { level: 'info', message: `Detected nested extension at: ${path.basename(extensionRoot)}` });
|
|
@@ -114,44 +145,107 @@ export const BrowserPlugin = {
|
|
|
114
145
|
ctx.events.on('downloader:updated', async (data) => {
|
|
115
146
|
await ctx.actions.runAction('core:log', { level: 'info', message: 'Update detected. Syncing to staging...' });
|
|
116
147
|
await syncToStaging();
|
|
148
|
+
// Re-validate on update?
|
|
149
|
+
// const newRoot = findExtensionRoot(STAGING_DIR) || STAGING_DIR;
|
|
150
|
+
// const newValidation = validateExtension(newRoot);
|
|
151
|
+
// if (!newValidation.valid) ...
|
|
117
152
|
});
|
|
118
153
|
await ctx.actions.runAction('core:log', { level: 'info', message: 'Browser running in Detached Mode.' });
|
|
119
154
|
// Launch Logic
|
|
155
|
+
// Launch Logic
|
|
120
156
|
if (isWSL) {
|
|
121
|
-
|
|
157
|
+
// -------------------------------------------------------------------------
|
|
158
|
+
// WSL STRATEGY (Validated 2025-12-24)
|
|
159
|
+
// 1. Use Windows User Profile for staging to avoid Permission/Path issues
|
|
160
|
+
// 2. Use PowerShell script to launch Chrome to reliably pass arguments
|
|
161
|
+
// -------------------------------------------------------------------------
|
|
162
|
+
// 1. Get Windows User Profile Path
|
|
163
|
+
let userProfileWin = '';
|
|
164
|
+
try {
|
|
165
|
+
// Use async exec to avoid blocking
|
|
166
|
+
const { exec } = await import('child_process');
|
|
167
|
+
const util = await import('util');
|
|
168
|
+
const execAsync = util.promisify(exec);
|
|
169
|
+
const { stdout } = await execAsync('cmd.exe /c echo %USERPROFILE%', { encoding: 'utf8' });
|
|
170
|
+
userProfileWin = stdout.trim();
|
|
171
|
+
}
|
|
172
|
+
catch (e) {
|
|
173
|
+
await ctx.actions.runAction('core:log', { level: 'error', message: 'Failed to detect Windows User Profile. Defaulting to C:\\Temp' });
|
|
174
|
+
userProfileWin = 'C:\\Temp';
|
|
175
|
+
}
|
|
176
|
+
const stagingDirName = '.ai-extension-preview';
|
|
177
|
+
const stagingDirWin = path.posix.join(userProfileWin.replace(/\\/g, '/'), stagingDirName).replace(/\//g, '\\');
|
|
178
|
+
// Map Win Path -> WSL Path for copying
|
|
179
|
+
const driveMatch = userProfileWin.match(/^([a-zA-Z]):/);
|
|
180
|
+
const driveLetter = driveMatch ? driveMatch[1].toLowerCase() : 'c';
|
|
181
|
+
const userProfileRoute = userProfileWin.substring(3).replace(/\\/g, '/'); // Users/Name
|
|
182
|
+
const wslStagingBase = `/mnt/${driveLetter}/${userProfileRoute}`;
|
|
183
|
+
const wslStagingDir = path.posix.join(wslStagingBase, stagingDirName);
|
|
184
|
+
try {
|
|
185
|
+
if (await fs.pathExists(wslStagingDir))
|
|
186
|
+
await fs.remove(wslStagingDir);
|
|
187
|
+
// Use async copy to prevent blocking event loop (Fixes 25s lag)
|
|
188
|
+
await fs.copy(STAGING_DIR, wslStagingDir);
|
|
189
|
+
}
|
|
190
|
+
catch (copyErr) {
|
|
191
|
+
await ctx.actions.runAction('core:log', { level: 'error', message: `WSL Staging Copy Failed: ${copyErr.message}` });
|
|
192
|
+
}
|
|
193
|
+
// Calculate final paths
|
|
194
|
+
let finalWinExtensionPath = stagingDirWin;
|
|
195
|
+
// Handle nested extension root
|
|
196
|
+
if (extensionRoot !== STAGING_DIR) {
|
|
197
|
+
const relative = path.relative(STAGING_DIR, extensionRoot);
|
|
198
|
+
finalWinExtensionPath = path.posix.join(stagingDirWin.replace(/\\/g, '/'), relative).replace(/\//g, '\\');
|
|
199
|
+
}
|
|
122
200
|
const winChromePath = chromePath
|
|
123
201
|
.replace(new RegExp(`^/mnt/${driveLetter}/`), `${driveLetter.toUpperCase()}:\\`)
|
|
124
202
|
.replace(/\//g, '\\');
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
//
|
|
128
|
-
//
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
203
|
+
const winProfile = path.posix.join(userProfileWin.replace(/\\/g, '/'), '.ai-extension-profile').replace(/\//g, '\\');
|
|
204
|
+
await ctx.actions.runAction('core:log', { level: 'info', message: `WSL Launch Target (Win): ${finalWinExtensionPath}` });
|
|
205
|
+
// await ctx.actions.runAction('core:log', { level: 'info', message: `WSL Profile (Win): ${winProfile}` });
|
|
206
|
+
// Create PowerShell Launch Script
|
|
207
|
+
const psContent = `
|
|
208
|
+
$chromePath = "${winChromePath}"
|
|
209
|
+
$extPath = "${finalWinExtensionPath}"
|
|
210
|
+
$profilePath = "${winProfile}"
|
|
211
|
+
|
|
212
|
+
# Create Profile Dir if needed
|
|
213
|
+
if (-not (Test-Path -Path $profilePath)) {
|
|
214
|
+
New-Item -ItemType Directory -Force -Path $profilePath | Out-Null
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
$argsList = @(
|
|
218
|
+
"--load-extension=$extPath",
|
|
219
|
+
"--user-data-dir=$profilePath",
|
|
220
|
+
"--no-first-run",
|
|
221
|
+
"--no-default-browser-check",
|
|
222
|
+
"--disable-gpu",
|
|
223
|
+
"chrome://extensions"
|
|
224
|
+
)
|
|
225
|
+
|
|
226
|
+
Start-Process -FilePath $chromePath -ArgumentList $argsList
|
|
139
227
|
`;
|
|
140
|
-
const
|
|
141
|
-
const
|
|
228
|
+
const psPath = path.join(wslStagingDir, 'launch.ps1');
|
|
229
|
+
const winPsPath = path.posix.join(stagingDirWin.replace(/\\/g, '/'), 'launch.ps1').replace(/\//g, '\\');
|
|
142
230
|
try {
|
|
143
|
-
fs.
|
|
231
|
+
await fs.writeFile(psPath, psContent);
|
|
144
232
|
}
|
|
145
233
|
catch (e) {
|
|
146
|
-
await ctx.actions.runAction('core:log', { level: 'error', message: `WSL Write
|
|
234
|
+
await ctx.actions.runAction('core:log', { level: 'error', message: `WSL Write PS1 Failed: ${e.message}` });
|
|
235
|
+
}
|
|
236
|
+
// Execute PowerShell
|
|
237
|
+
const cli = 'powershell.exe';
|
|
238
|
+
try {
|
|
239
|
+
const subprocess = spawn(cli, ['-NoProfile', '-ExecutionPolicy', 'Bypass', '-File', winPsPath], {
|
|
240
|
+
detached: true, // We still detach the PowerShell process itself
|
|
241
|
+
stdio: 'ignore',
|
|
242
|
+
cwd: `/mnt/${driveLetter}`
|
|
243
|
+
});
|
|
244
|
+
subprocess.unref();
|
|
245
|
+
}
|
|
246
|
+
catch (spawnErr) {
|
|
247
|
+
await ctx.actions.runAction('core:log', { level: 'error', message: `WSL Spawn Error: ${spawnErr.message}` });
|
|
147
248
|
}
|
|
148
|
-
const cli = 'cmd.exe';
|
|
149
|
-
const subprocess = spawn(cli, ['/c', winBatPath], {
|
|
150
|
-
detached: true,
|
|
151
|
-
stdio: 'ignore',
|
|
152
|
-
cwd: '/mnt/c'
|
|
153
|
-
});
|
|
154
|
-
subprocess.unref();
|
|
155
249
|
return true;
|
|
156
250
|
}
|
|
157
251
|
else {
|
|
@@ -54,33 +54,49 @@ export const DownloaderPlugin = {
|
|
|
54
54
|
if (isChecking)
|
|
55
55
|
return true; // Skip if busy
|
|
56
56
|
isChecking = true;
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
//
|
|
57
|
+
const MAX_RETRIES = 3;
|
|
58
|
+
let attempt = 0;
|
|
59
|
+
while (attempt < MAX_RETRIES) {
|
|
60
|
+
try {
|
|
61
|
+
const res = await client.get(`/jobs/${config.jobId}`);
|
|
62
|
+
const job = res.data;
|
|
63
|
+
const newVersion = job.version;
|
|
64
|
+
// If no version in job yet, fall back to timestamp or ignore
|
|
65
|
+
if (!newVersion && !lastModified) {
|
|
66
|
+
// First run, just verify it exists
|
|
67
|
+
}
|
|
68
|
+
if (job.status === 'completed') {
|
|
69
|
+
if (newVersion !== lastModified) {
|
|
70
|
+
await ctx.actions.runAction('core:log', { level: 'info', message: `New version detected (Old: "${lastModified}", New: "${newVersion}")` });
|
|
71
|
+
const success = await ctx.actions.runAction('downloader:download', null);
|
|
72
|
+
if (success) {
|
|
73
|
+
lastModified = newVersion;
|
|
74
|
+
fs.writeFileSync(VERSION_FILE, newVersion);
|
|
75
|
+
ctx.events.emit('downloader:updated', { version: job.version, jobId: config.jobId });
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
else {
|
|
80
|
+
// await ctx.actions.runAction('core:log', { level: 'info', message: `Poll: Job status is ${job.status}` });
|
|
81
|
+
}
|
|
82
|
+
isChecking = false;
|
|
83
|
+
return true;
|
|
65
84
|
}
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
const
|
|
69
|
-
if (
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
85
|
+
catch (error) {
|
|
86
|
+
attempt++;
|
|
87
|
+
const isNetworkError = error.code === 'EAI_AGAIN' || error.code === 'ENOTFOUND' || error.code === 'ECONNRESET' || error.code === 'ETIMEDOUT';
|
|
88
|
+
if (attempt < MAX_RETRIES && isNetworkError) {
|
|
89
|
+
await ctx.actions.runAction('core:log', { level: 'warn', message: `Connection failed (${error.code}). Retrying (${attempt}/${MAX_RETRIES})...` });
|
|
90
|
+
await new Promise(r => setTimeout(r, 1000 * attempt)); // Backoff
|
|
91
|
+
continue;
|
|
73
92
|
}
|
|
93
|
+
isChecking = false;
|
|
94
|
+
await ctx.actions.runAction('core:log', { level: 'error', message: `Check failed: ${error.message}` });
|
|
95
|
+
return false;
|
|
74
96
|
}
|
|
75
|
-
isChecking = false;
|
|
76
|
-
return true;
|
|
77
|
-
}
|
|
78
|
-
catch (error) {
|
|
79
|
-
isChecking = false;
|
|
80
|
-
await ctx.actions.runAction('core:log', { level: 'error', message: `Check failed: ${error.message}` });
|
|
81
|
-
// Return false only on actual error, so index.ts knows to fail
|
|
82
|
-
return true;
|
|
83
97
|
}
|
|
98
|
+
isChecking = false;
|
|
99
|
+
return false;
|
|
84
100
|
}
|
|
85
101
|
});
|
|
86
102
|
// Action: Download
|
|
@@ -170,19 +186,22 @@ console.log('[Hot Reload] Active for Job:', CURRENT_JOB_ID);
|
|
|
170
186
|
}
|
|
171
187
|
});
|
|
172
188
|
// Start Polling (Loop)
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
189
|
+
console.error('[DownloaderPlugin] Starting polling loop (Interval: 2000ms)');
|
|
190
|
+
checkInterval = setInterval(async () => {
|
|
191
|
+
try {
|
|
192
|
+
// Use actions for main log, but console.error for guaranteed debug output
|
|
193
|
+
// await ctx.actions.runAction('core:log', { level: 'info', message: '[DEBUG] Polling Tick...' });
|
|
194
|
+
console.error('[DownloaderPlugin] Tick - Checking Status...');
|
|
177
195
|
await ctx.actions.runAction('downloader:check', null);
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
196
|
+
}
|
|
197
|
+
catch (err) {
|
|
198
|
+
console.error('[DownloaderPlugin] Poll Error:', err);
|
|
199
|
+
}
|
|
200
|
+
}, 2000);
|
|
182
201
|
},
|
|
183
202
|
dispose(ctx) {
|
|
184
203
|
if (checkInterval) {
|
|
185
|
-
|
|
204
|
+
clearInterval(checkInterval);
|
|
186
205
|
checkInterval = undefined;
|
|
187
206
|
}
|
|
188
207
|
}
|
|
@@ -24,6 +24,7 @@ export const ServerPlugin = {
|
|
|
24
24
|
}
|
|
25
25
|
if (req.url === '/status') {
|
|
26
26
|
const currentJobId = ctx.host.config.jobId;
|
|
27
|
+
ctx.actions.runAction('core:log', { level: 'info', message: `[DEBUG] Server: Extension requested status (Reporting: ${currentVersion})` });
|
|
27
28
|
res.writeHead(200, { 'Content-Type': 'application/json' });
|
|
28
29
|
res.end(JSON.stringify({
|
|
29
30
|
version: currentVersion,
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "ai-extension-preview",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.9",
|
|
4
4
|
"description": "Local preview tool for AI Extension Builder",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
@@ -47,6 +47,7 @@
|
|
|
47
47
|
"@types/ws": "^8.5.13",
|
|
48
48
|
"shx": "^0.4.0",
|
|
49
49
|
"tsx": "^4.21.0",
|
|
50
|
-
"typescript": "^5.7.2"
|
|
50
|
+
"typescript": "^5.7.2",
|
|
51
|
+
"vitest": "^4.0.16"
|
|
51
52
|
}
|
|
52
|
-
}
|
|
53
|
+
}
|