ai-extension-preview 0.1.0
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/README.md +32 -0
- package/dist/index.js +136 -0
- package/dist/plugins/BrowserPlugin.js +113 -0
- package/dist/plugins/CorePlugin.js +40 -0
- package/dist/plugins/DownloaderPlugin.js +122 -0
- package/package.json +52 -0
package/README.md
ADDED
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
# AI Extension Preview
|
|
2
|
+
|
|
3
|
+
A local companion tool for the **AI Extension Builder**.
|
|
4
|
+
This tool allows you to instantly preview and test your AI-generated Chrome Extensions locally, bridging the gap between the **AI Extension Builder** and your local browser.
|
|
5
|
+
|
|
6
|
+
## Features
|
|
7
|
+
|
|
8
|
+
- **Instant Preview:** Launches a Chrome instance with your extension loaded.
|
|
9
|
+
- **Live Updates:** Automatically detects new builds from the AI Builder and reloads your extension (Hot Reload).
|
|
10
|
+
- **Secure Connection:** Uses a short-code "Device Flow" authentication to securely link to your account.
|
|
11
|
+
- **Cross-Platform:** Works on Windows, macOS, and Linux (including WSL/Git Bash via Detached Mode).
|
|
12
|
+
|
|
13
|
+
## Usage
|
|
14
|
+
|
|
15
|
+
You do NOT need to install this package globally. Just run it with `npx`:
|
|
16
|
+
|
|
17
|
+
```bash
|
|
18
|
+
npx ai-extension-preview
|
|
19
|
+
```
|
|
20
|
+
|
|
21
|
+
### Setup Flow
|
|
22
|
+
|
|
23
|
+
1. Run the command above.
|
|
24
|
+
2. The tool will display a **Link Code** (e.g., `ABCD`).
|
|
25
|
+
3. Go to your **AI Extension Builder Dashboard**.
|
|
26
|
+
4. Click **"Connect Preview"** and enter the code.
|
|
27
|
+
5. Enjoy! The tool will automatically download and launch your active extension.
|
|
28
|
+
|
|
29
|
+
## Requirements
|
|
30
|
+
|
|
31
|
+
- Node.js v18+
|
|
32
|
+
- Google Chrome or Chromium installed.
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,136 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import 'dotenv/config'; // Load .env
|
|
3
|
+
import { Command } from 'commander';
|
|
4
|
+
import path from 'path';
|
|
5
|
+
import { fileURLToPath } from 'url';
|
|
6
|
+
import fs from 'fs-extra';
|
|
7
|
+
import { Runtime } from 'skeleton-crew-runtime';
|
|
8
|
+
import { CorePlugin } from './plugins/CorePlugin.js';
|
|
9
|
+
import { DownloaderPlugin } from './plugins/DownloaderPlugin.js';
|
|
10
|
+
import { BrowserPlugin } from './plugins/BrowserPlugin.js';
|
|
11
|
+
import axios from 'axios';
|
|
12
|
+
import chalk from 'chalk';
|
|
13
|
+
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
|
14
|
+
const DEFAULT_HOST = process.env.API_HOST || 'https://ai-extension-builder.01kb6018z1t9tpaza4y5f1c56w.lmapp.run/api';
|
|
15
|
+
const program = new Command();
|
|
16
|
+
program
|
|
17
|
+
.name('preview')
|
|
18
|
+
.description('Live preview companion for AI Extension Builder')
|
|
19
|
+
.option('--job <job>', 'Job ID to preview')
|
|
20
|
+
.option('--host <host>', 'API Host URL', DEFAULT_HOST)
|
|
21
|
+
.option('--token <token>', 'Auth Token (if required)')
|
|
22
|
+
.option('--user <user>', 'User ID (if required)')
|
|
23
|
+
.parse(process.argv);
|
|
24
|
+
const options = program.opts();
|
|
25
|
+
async function authenticate(host) {
|
|
26
|
+
try {
|
|
27
|
+
// 1. Init Session
|
|
28
|
+
const initRes = await axios.post(`${host}/preview/init`);
|
|
29
|
+
const { code, sessionId } = initRes.data;
|
|
30
|
+
console.log('\n' + chalk.bgBlue.bold(' DETACHED PREVIEW MODE ') + '\n');
|
|
31
|
+
console.log('To connect, please go to your Extension Dashboard and click "Connect Preview".');
|
|
32
|
+
console.log('Enter the following code:');
|
|
33
|
+
console.log('\n' + chalk.green.bold(` ${code} `) + '\n');
|
|
34
|
+
console.log('Waiting for connection...');
|
|
35
|
+
// 2. Poll for Status
|
|
36
|
+
while (true) {
|
|
37
|
+
await new Promise(resolve => setTimeout(resolve, 2000));
|
|
38
|
+
try {
|
|
39
|
+
const statusRes = await axios.get(`${host}/preview/status/${sessionId}`);
|
|
40
|
+
const data = statusRes.data;
|
|
41
|
+
if (data.status === 'linked') {
|
|
42
|
+
console.log(chalk.green('✔ Connected!'));
|
|
43
|
+
if (!data.jobId) {
|
|
44
|
+
console.error('Error: No Job ID associated with this connection.');
|
|
45
|
+
process.exit(1);
|
|
46
|
+
}
|
|
47
|
+
return {
|
|
48
|
+
jobId: data.jobId,
|
|
49
|
+
userId: data.userId,
|
|
50
|
+
token: 'session:' + sessionId // Use session ID as token for now
|
|
51
|
+
};
|
|
52
|
+
}
|
|
53
|
+
if (data.status === 'expired') {
|
|
54
|
+
console.error(chalk.red('Code expired. Please restart.'));
|
|
55
|
+
process.exit(1);
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
catch (e) {
|
|
59
|
+
// Ignore transient network errors
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
catch (error) {
|
|
64
|
+
console.error(chalk.red(`Failed to initialize session: ${error.message}`));
|
|
65
|
+
process.exit(1);
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
async function main() {
|
|
69
|
+
let jobId = options.job;
|
|
70
|
+
let userId = options.user;
|
|
71
|
+
let token = options.token;
|
|
72
|
+
const host = options.host;
|
|
73
|
+
// Interactive Auth Flow if no Job ID provided
|
|
74
|
+
if (!jobId) {
|
|
75
|
+
const authData = await authenticate(host);
|
|
76
|
+
jobId = authData.jobId;
|
|
77
|
+
userId = authData.userId || userId;
|
|
78
|
+
token = authData.token || token;
|
|
79
|
+
}
|
|
80
|
+
const WORK_DIR = path.join(process.cwd(), '.preview', jobId);
|
|
81
|
+
// 1. Initialize Runtime
|
|
82
|
+
const runtime = new Runtime({
|
|
83
|
+
hostContext: {
|
|
84
|
+
config: {
|
|
85
|
+
host,
|
|
86
|
+
token,
|
|
87
|
+
user: userId,
|
|
88
|
+
jobId,
|
|
89
|
+
workDir: WORK_DIR
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
});
|
|
93
|
+
// 2. Register Plugins
|
|
94
|
+
// Note: In a real dynamic system we might load these from a folder
|
|
95
|
+
runtime.logger.info('Registering plugins...');
|
|
96
|
+
runtime.registerPlugin(CorePlugin);
|
|
97
|
+
runtime.registerPlugin(DownloaderPlugin);
|
|
98
|
+
runtime.registerPlugin(BrowserPlugin);
|
|
99
|
+
runtime.logger.info('Initializing runtime...');
|
|
100
|
+
await runtime.initialize();
|
|
101
|
+
const ctx = runtime.getContext();
|
|
102
|
+
// 3. Start LifeCycle
|
|
103
|
+
await ctx.actions.runAction('core:log', { level: 'info', message: 'Initializing Local Satellite...' });
|
|
104
|
+
// Ensure work dir exists
|
|
105
|
+
await fs.ensureDir(WORK_DIR);
|
|
106
|
+
// Initial Check - Must succeed to continue
|
|
107
|
+
const success = await ctx.actions.runAction('downloader:check', null);
|
|
108
|
+
if (!success) {
|
|
109
|
+
await ctx.actions.runAction('core:log', { level: 'error', message: 'Initial check failed. Could not verify job or download extension.' });
|
|
110
|
+
process.exit(1);
|
|
111
|
+
}
|
|
112
|
+
// Start Browser (This will block until browser is closed OR return immediately if detached)
|
|
113
|
+
const browserSessionResult = await ctx.actions.runAction('browser:start', null);
|
|
114
|
+
// If detached launch (result=true) or web-ext blocked and finished...
|
|
115
|
+
// We should ONLY exit if the loop is also done (which it never is unless disposed).
|
|
116
|
+
// Actually, if web-ext finishes (e.g. user closed browser), we might want to exit?
|
|
117
|
+
// But for Detached Mode, we MUST stay open to poll updates.
|
|
118
|
+
// If browser:start returned, it means either:
|
|
119
|
+
// 1. Browser closed (web-ext mode) -> we arguably should exit.
|
|
120
|
+
// 2. Detached mode started -> we MUST NOT exit.
|
|
121
|
+
// Changing logic: rely on SIGINT to exit.
|
|
122
|
+
runtime.logger.info('Press Ctrl+C to exit.');
|
|
123
|
+
}
|
|
124
|
+
// Handle global errors
|
|
125
|
+
process.on('uncaughtException', (err) => {
|
|
126
|
+
if (err.code === 'ECONNRESET' || err.message?.includes('ECONNRESET')) {
|
|
127
|
+
// Ignore pipe errors frequently caused by web-ext/chrome teardown
|
|
128
|
+
return;
|
|
129
|
+
}
|
|
130
|
+
console.error('Uncaught Exception:', err);
|
|
131
|
+
process.exit(1);
|
|
132
|
+
});
|
|
133
|
+
process.on('unhandledRejection', (reason) => {
|
|
134
|
+
console.error('Unhandled Rejection:', reason);
|
|
135
|
+
});
|
|
136
|
+
main();
|
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
import webExt from 'web-ext';
|
|
2
|
+
import path from 'path';
|
|
3
|
+
import { spawn } from 'child_process';
|
|
4
|
+
import fs from 'fs';
|
|
5
|
+
const CHROME_PATHS = [
|
|
6
|
+
'/mnt/c/Program Files/Google/Chrome/Application/chrome.exe',
|
|
7
|
+
'/mnt/c/Program Files (x86)/Google/Chrome/Application/chrome.exe',
|
|
8
|
+
'/c/Program Files/Google/Chrome/Application/chrome.exe',
|
|
9
|
+
'/c/Program Files (x86)/Google/Chrome/Application/chrome.exe',
|
|
10
|
+
'/usr/bin/google-chrome',
|
|
11
|
+
'/usr/bin/chromium'
|
|
12
|
+
];
|
|
13
|
+
function findChrome() {
|
|
14
|
+
for (const p of CHROME_PATHS) {
|
|
15
|
+
if (fs.existsSync(p))
|
|
16
|
+
return p;
|
|
17
|
+
}
|
|
18
|
+
return null;
|
|
19
|
+
}
|
|
20
|
+
export const BrowserPlugin = {
|
|
21
|
+
name: 'browser',
|
|
22
|
+
version: '1.0.0',
|
|
23
|
+
setup(ctx) {
|
|
24
|
+
const config = ctx.host.config;
|
|
25
|
+
const DIST_DIR = path.join(config.workDir, 'dist');
|
|
26
|
+
let runner = null;
|
|
27
|
+
const launchDetached = async () => {
|
|
28
|
+
const chromePath = findChrome();
|
|
29
|
+
if (!chromePath) {
|
|
30
|
+
await ctx.actions.runAction('core:log', { level: 'error', message: 'Chrome not found for detached launch.' });
|
|
31
|
+
return false;
|
|
32
|
+
}
|
|
33
|
+
// Log with prefix to indicate we are handling the Env quirk
|
|
34
|
+
await ctx.actions.runAction('core:log', { level: 'info', message: '[WSL] Copying extension to Windows Temp: C:\\ai-ext-preview' });
|
|
35
|
+
// In a real scenario we might need to copy to a Windows-accessible path if /home/... isn't mapped well,
|
|
36
|
+
// but usually \\wsl$\... works or user is mapped.
|
|
37
|
+
// For now assuming direct path works or user has mapping.
|
|
38
|
+
// Actually, verify path.
|
|
39
|
+
// IMPORTANT: In WSL, standard linux paths might not be readable by Windows Chrome directly
|
|
40
|
+
// without `\\wsl$\...` mapping.
|
|
41
|
+
// However, previous logs showed "Failed to load... CDP connection closed" which means
|
|
42
|
+
// Chrome DID try to load it but failed communication.
|
|
43
|
+
// So path is likely fine.
|
|
44
|
+
await ctx.actions.runAction('core:log', { level: 'warning', message: 'Switching to Detached Mode (WSL/GitBash detected).' });
|
|
45
|
+
await ctx.actions.runAction('core:log', { level: 'info', message: 'Browser polling/logging is disabled. Please reload manually on updates.' });
|
|
46
|
+
const subprocess = spawn(chromePath, [
|
|
47
|
+
`--load-extension=${DIST_DIR}`,
|
|
48
|
+
'--disable-gpu',
|
|
49
|
+
'https://google.com'
|
|
50
|
+
], {
|
|
51
|
+
detached: true,
|
|
52
|
+
stdio: 'ignore'
|
|
53
|
+
});
|
|
54
|
+
subprocess.unref();
|
|
55
|
+
return true;
|
|
56
|
+
};
|
|
57
|
+
ctx.actions.registerAction({
|
|
58
|
+
id: 'browser:start',
|
|
59
|
+
handler: async () => {
|
|
60
|
+
await ctx.actions.runAction('core:log', { level: 'info', message: 'Launching browser...' });
|
|
61
|
+
try {
|
|
62
|
+
// Try web-ext first
|
|
63
|
+
const runResult = await webExt.cmd.run({
|
|
64
|
+
sourceDir: DIST_DIR,
|
|
65
|
+
target: 'chromium',
|
|
66
|
+
browserConsole: false,
|
|
67
|
+
startUrl: ['https://google.com'],
|
|
68
|
+
noInput: true,
|
|
69
|
+
keepProfileChanges: false,
|
|
70
|
+
args: [
|
|
71
|
+
'--start-maximized',
|
|
72
|
+
'--no-sandbox',
|
|
73
|
+
'--disable-gpu',
|
|
74
|
+
'--disable-dev-shm-usage'
|
|
75
|
+
]
|
|
76
|
+
}, {
|
|
77
|
+
shouldExitProgram: false
|
|
78
|
+
});
|
|
79
|
+
runner = runResult;
|
|
80
|
+
await ctx.actions.runAction('core:log', { level: 'success', message: 'Browser session ended.' });
|
|
81
|
+
return true;
|
|
82
|
+
}
|
|
83
|
+
catch (err) {
|
|
84
|
+
// Check for expected environment failures
|
|
85
|
+
if (err.code === 'ECONNRESET' || err.message?.includes('CDP connection closed')) {
|
|
86
|
+
// Log specific WSL message for clarity
|
|
87
|
+
await ctx.actions.runAction('core:log', { level: 'warning', message: 'WSL: CDP connection dropped (expected). Browser is running detached.' });
|
|
88
|
+
await ctx.actions.runAction('core:log', { level: 'info', message: 'Please reload extension manually in Chrome if needed.' });
|
|
89
|
+
return await launchDetached();
|
|
90
|
+
}
|
|
91
|
+
if (err.code !== 'ECONNRESET') {
|
|
92
|
+
await ctx.actions.runAction('core:log', { level: 'error', message: `Browser failed: ${err.message}` });
|
|
93
|
+
}
|
|
94
|
+
return false;
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
});
|
|
98
|
+
ctx.events.on('downloader:updated', async () => {
|
|
99
|
+
if (runner && runner.reloadAllExtensions) {
|
|
100
|
+
await ctx.actions.runAction('core:log', { level: 'info', message: 'Triggering browser reload...' });
|
|
101
|
+
try {
|
|
102
|
+
runner.reloadAllExtensions();
|
|
103
|
+
}
|
|
104
|
+
catch (e) {
|
|
105
|
+
// Ignore
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
else {
|
|
109
|
+
await ctx.actions.runAction('core:log', { level: 'info', message: 'Update installed. Please reload extension in Chrome.' });
|
|
110
|
+
}
|
|
111
|
+
});
|
|
112
|
+
}
|
|
113
|
+
};
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
import chalk from 'chalk';
|
|
2
|
+
export const CorePlugin = {
|
|
3
|
+
name: 'core',
|
|
4
|
+
version: '1.0.0',
|
|
5
|
+
setup(ctx) {
|
|
6
|
+
console.log('CorePlugin: setup called');
|
|
7
|
+
// We assume config is passed in hostContext
|
|
8
|
+
const config = ctx.host.config;
|
|
9
|
+
ctx.actions.registerAction({
|
|
10
|
+
id: 'core:config',
|
|
11
|
+
handler: async () => config
|
|
12
|
+
});
|
|
13
|
+
console.log('CorePlugin: Registering core:log');
|
|
14
|
+
ctx.actions.registerAction({
|
|
15
|
+
id: 'core:log',
|
|
16
|
+
handler: async (payload) => {
|
|
17
|
+
// Access default logger from Runtime
|
|
18
|
+
const rt = typeof ctx.getRuntime === 'function' ? ctx.getRuntime() : ctx.runtime;
|
|
19
|
+
// Logger is now public
|
|
20
|
+
const logger = rt.logger || console;
|
|
21
|
+
const { level, message } = payload;
|
|
22
|
+
switch (level) {
|
|
23
|
+
case 'error':
|
|
24
|
+
logger.error(chalk.red(message));
|
|
25
|
+
break;
|
|
26
|
+
case 'warn':
|
|
27
|
+
logger.warn(chalk.yellow(message));
|
|
28
|
+
break;
|
|
29
|
+
case 'success':
|
|
30
|
+
// Default logger usually has info/warn/error/debug. Map success to info (green)
|
|
31
|
+
logger.info(chalk.green(message));
|
|
32
|
+
break;
|
|
33
|
+
default:
|
|
34
|
+
logger.info(message);
|
|
35
|
+
}
|
|
36
|
+
return true;
|
|
37
|
+
}
|
|
38
|
+
});
|
|
39
|
+
}
|
|
40
|
+
};
|
|
@@ -0,0 +1,122 @@
|
|
|
1
|
+
import axios from 'axios';
|
|
2
|
+
import fs from 'fs-extra';
|
|
3
|
+
import path from 'path';
|
|
4
|
+
import AdmZip from 'adm-zip';
|
|
5
|
+
import ora from 'ora';
|
|
6
|
+
import https from 'https';
|
|
7
|
+
let checkInterval;
|
|
8
|
+
export const DownloaderPlugin = {
|
|
9
|
+
name: 'downloader',
|
|
10
|
+
version: '1.0.0',
|
|
11
|
+
setup(ctx) {
|
|
12
|
+
const config = ctx.host.config;
|
|
13
|
+
const DIST_DIR = path.join(config.workDir, 'dist');
|
|
14
|
+
const DOWNLOAD_PATH = path.join(config.workDir, 'extension.zip');
|
|
15
|
+
const rawToken = config.token ? String(config.token) : '';
|
|
16
|
+
const token = rawToken.replace(/^Bearer\s+/i, '').trim();
|
|
17
|
+
// Auto-extract user ID from token if not provided
|
|
18
|
+
let userId = config.user;
|
|
19
|
+
if (!userId && token) {
|
|
20
|
+
try {
|
|
21
|
+
const parts = token.split('.');
|
|
22
|
+
if (parts.length === 3) {
|
|
23
|
+
const payload = JSON.parse(Buffer.from(parts[1], 'base64').toString());
|
|
24
|
+
userId = payload.id || payload.sub || payload.userId;
|
|
25
|
+
// Add cleanup logging
|
|
26
|
+
if (userId)
|
|
27
|
+
ctx.actions.runAction('core:log', { level: 'info', message: `Extracted User ID: ${userId}` });
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
catch (e) {
|
|
31
|
+
// Ignore parse errors
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
const client = axios.create({
|
|
35
|
+
baseURL: config.host,
|
|
36
|
+
headers: {
|
|
37
|
+
'Authorization': token ? `Bearer ${token}` : undefined,
|
|
38
|
+
'X-User-Id': userId
|
|
39
|
+
},
|
|
40
|
+
httpsAgent: new https.Agent({
|
|
41
|
+
rejectUnauthorized: false
|
|
42
|
+
})
|
|
43
|
+
});
|
|
44
|
+
let lastModified = '';
|
|
45
|
+
let isChecking = false;
|
|
46
|
+
// Action: Check Status
|
|
47
|
+
ctx.actions.registerAction({
|
|
48
|
+
id: 'downloader:check',
|
|
49
|
+
handler: async () => {
|
|
50
|
+
if (isChecking)
|
|
51
|
+
return true; // Skip if busy
|
|
52
|
+
isChecking = true;
|
|
53
|
+
try {
|
|
54
|
+
const res = await client.get(`/jobs/${config.jobId}`);
|
|
55
|
+
const job = res.data;
|
|
56
|
+
const newVersion = job.version;
|
|
57
|
+
// If no version in job yet, fall back to timestamp or ignore
|
|
58
|
+
if (!newVersion && !lastModified) {
|
|
59
|
+
// First run, just verify it exists
|
|
60
|
+
// We might want to download anyway if we don't have it locally
|
|
61
|
+
}
|
|
62
|
+
if (job.status === 'completed' && newVersion !== lastModified) {
|
|
63
|
+
await ctx.actions.runAction('core:log', { level: 'info', message: `New version detected (Old: "${lastModified}", New: "${newVersion}")` });
|
|
64
|
+
const success = await ctx.actions.runAction('downloader:download', null);
|
|
65
|
+
if (success) {
|
|
66
|
+
lastModified = newVersion;
|
|
67
|
+
ctx.events.emit('downloader:updated', { version: job.version });
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
isChecking = false;
|
|
71
|
+
return true;
|
|
72
|
+
}
|
|
73
|
+
catch (error) {
|
|
74
|
+
isChecking = false;
|
|
75
|
+
await ctx.actions.runAction('core:log', { level: 'error', message: `Check failed: ${error.message}` });
|
|
76
|
+
// Return false only on actual error, so index.ts knows to fail
|
|
77
|
+
return true;
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
});
|
|
81
|
+
// Action: Download
|
|
82
|
+
ctx.actions.registerAction({
|
|
83
|
+
id: 'downloader:download',
|
|
84
|
+
handler: async () => {
|
|
85
|
+
const spinner = ora('Downloading new version...').start();
|
|
86
|
+
try {
|
|
87
|
+
const response = await client.get(`/download/${config.jobId}`, {
|
|
88
|
+
responseType: 'arraybuffer'
|
|
89
|
+
});
|
|
90
|
+
await fs.ensureDir(config.workDir);
|
|
91
|
+
await fs.writeFile(DOWNLOAD_PATH, response.data);
|
|
92
|
+
await fs.emptyDir(DIST_DIR);
|
|
93
|
+
const zip = new AdmZip(DOWNLOAD_PATH);
|
|
94
|
+
zip.extractAllTo(DIST_DIR, true);
|
|
95
|
+
spinner.succeed('Updated extension code!');
|
|
96
|
+
return true;
|
|
97
|
+
}
|
|
98
|
+
catch (error) {
|
|
99
|
+
spinner.fail(`Failed to download: ${error.message}`);
|
|
100
|
+
await ctx.actions.runAction('core:log', { level: 'error', message: `Download failed: ${error.message}` });
|
|
101
|
+
return false;
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
});
|
|
105
|
+
// Start Polling (Loop)
|
|
106
|
+
const scheduleNextCheck = () => {
|
|
107
|
+
checkInterval = setTimeout(async () => {
|
|
108
|
+
if (!checkInterval)
|
|
109
|
+
return; // Disposed
|
|
110
|
+
await ctx.actions.runAction('downloader:check', null);
|
|
111
|
+
scheduleNextCheck();
|
|
112
|
+
}, 2000);
|
|
113
|
+
};
|
|
114
|
+
scheduleNextCheck();
|
|
115
|
+
},
|
|
116
|
+
dispose(ctx) {
|
|
117
|
+
if (checkInterval) {
|
|
118
|
+
clearTimeout(checkInterval);
|
|
119
|
+
checkInterval = undefined;
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
};
|
package/package.json
ADDED
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "ai-extension-preview",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "Local preview tool for AI Extension Builder",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"bin": {
|
|
7
|
+
"ai-extension-preview": "./dist/index.js"
|
|
8
|
+
},
|
|
9
|
+
"files": [
|
|
10
|
+
"dist",
|
|
11
|
+
"README.md"
|
|
12
|
+
],
|
|
13
|
+
"keywords": [
|
|
14
|
+
"ai",
|
|
15
|
+
"chrome-extension",
|
|
16
|
+
"builder",
|
|
17
|
+
"preview",
|
|
18
|
+
"dev-tool"
|
|
19
|
+
],
|
|
20
|
+
"author": "AI Extension Builder",
|
|
21
|
+
"license": "MIT",
|
|
22
|
+
"scripts": {
|
|
23
|
+
"build": "shx rm -rf dist && tsc -b",
|
|
24
|
+
"start": "tsx src/index.ts",
|
|
25
|
+
"dev": "tsx watch src/index.ts",
|
|
26
|
+
"preview": "node dist/index.js"
|
|
27
|
+
},
|
|
28
|
+
"dependencies": {
|
|
29
|
+
"adm-zip": "^0.5.16",
|
|
30
|
+
"axios": "^1.7.9",
|
|
31
|
+
"chalk": "^5.3.0",
|
|
32
|
+
"commander": "^12.1.0",
|
|
33
|
+
"dotenv": "^17.2.3",
|
|
34
|
+
"fs-extra": "^11.2.0",
|
|
35
|
+
"inquirer": "^12.0.1",
|
|
36
|
+
"node-fetch": "^3.3.2",
|
|
37
|
+
"ora": "^8.1.1",
|
|
38
|
+
"puppeteer-core": "^24.33.0",
|
|
39
|
+
"skeleton-crew-runtime": "^0.1.5",
|
|
40
|
+
"web-ext": "^8.3.0",
|
|
41
|
+
"ws": "^8.18.0"
|
|
42
|
+
},
|
|
43
|
+
"devDependencies": {
|
|
44
|
+
"@types/adm-zip": "^0.5.6",
|
|
45
|
+
"@types/fs-extra": "^11.0.4",
|
|
46
|
+
"@types/node": "^22.10.1",
|
|
47
|
+
"@types/ws": "^8.5.13",
|
|
48
|
+
"shx": "^0.4.0",
|
|
49
|
+
"tsx": "^4.21.0",
|
|
50
|
+
"typescript": "^5.7.2"
|
|
51
|
+
}
|
|
52
|
+
}
|