ai-extension-preview 0.1.8 → 0.1.10
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.js
CHANGED
|
@@ -41,7 +41,7 @@ async function authenticate(host) {
|
|
|
41
41
|
const statusRes = await axios.get(`${host}/preview/status/${sessionId}`);
|
|
42
42
|
const data = statusRes.data;
|
|
43
43
|
if (data.status === 'linked') {
|
|
44
|
-
console.log(chalk.green('✔ Connected!'));
|
|
44
|
+
// console.log(chalk.green('✔ Connected!'));
|
|
45
45
|
if (!data.jobId) {
|
|
46
46
|
console.error('Error: No Job ID associated with this connection.');
|
|
47
47
|
process.exit(1);
|
|
@@ -97,7 +97,10 @@ async function main() {
|
|
|
97
97
|
});
|
|
98
98
|
// 2. Register Plugins
|
|
99
99
|
// Note: In a real dynamic system we might load these from a folder
|
|
100
|
-
|
|
100
|
+
// console.log('Registering plugins...');
|
|
101
|
+
// Register Plugins
|
|
102
|
+
// UI Plugin first or last?
|
|
103
|
+
// If first, it captures subsequent logs.
|
|
101
104
|
runtime.registerPlugin(CorePlugin);
|
|
102
105
|
runtime.registerPlugin(DownloaderPlugin);
|
|
103
106
|
runtime.registerPlugin(BrowserPlugin);
|
|
@@ -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',
|
|
@@ -64,12 +111,6 @@ export const BrowserPlugin = {
|
|
|
64
111
|
fs.ensureDirSync(STAGING_DIR);
|
|
65
112
|
fs.copySync(DIST_DIR, STAGING_DIR);
|
|
66
113
|
await ctx.actions.runAction('core:log', { level: 'info', message: `Synced code to Staging` });
|
|
67
|
-
// DEBUG: Log contents of staging
|
|
68
|
-
try {
|
|
69
|
-
const files = fs.readdirSync(STAGING_DIR);
|
|
70
|
-
await ctx.actions.runAction('core:log', { level: 'info', message: `Staging Contents: ${files.join(', ')}` });
|
|
71
|
-
}
|
|
72
|
-
catch (e) { }
|
|
73
114
|
// Emit staged event for ServerPlugin (optional for now, but good practice)
|
|
74
115
|
ctx.events.emit('browser:staged', { path: STAGING_DIR });
|
|
75
116
|
}
|
|
@@ -77,35 +118,19 @@ export const BrowserPlugin = {
|
|
|
77
118
|
await ctx.actions.runAction('core:log', { level: 'error', message: `Failed to sync to staging: ${err.message}` });
|
|
78
119
|
}
|
|
79
120
|
};
|
|
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
121
|
// Initial Sync
|
|
102
122
|
await syncToStaging();
|
|
103
123
|
// Resolve proper root AFTER sync
|
|
104
124
|
let extensionRoot = findExtensionRoot(STAGING_DIR) || STAGING_DIR;
|
|
105
125
|
// Check if we found a valid root
|
|
106
|
-
|
|
107
|
-
|
|
126
|
+
const validation = validateExtension(extensionRoot);
|
|
127
|
+
if (!validation.valid) {
|
|
128
|
+
await ctx.actions.runAction('core:log', { level: 'error', message: `[CRITICAL] Extension validation failed: ${validation.error} in ${extensionRoot}` });
|
|
108
129
|
await ctx.actions.runAction('core:log', { level: 'info', message: `Checked Path: ${extensionRoot}` });
|
|
130
|
+
// We proceed anyway? Or should we stop?
|
|
131
|
+
// Previous logic proceeded but logged critical error.
|
|
132
|
+
// Let's keep it logging critical but maybe return false if we wanted to be strict.
|
|
133
|
+
// However, user might fix it live.
|
|
109
134
|
}
|
|
110
135
|
else if (extensionRoot !== STAGING_DIR) {
|
|
111
136
|
await ctx.actions.runAction('core:log', { level: 'info', message: `Detected nested extension at: ${path.basename(extensionRoot)}` });
|
|
@@ -114,44 +139,113 @@ export const BrowserPlugin = {
|
|
|
114
139
|
ctx.events.on('downloader:updated', async (data) => {
|
|
115
140
|
await ctx.actions.runAction('core:log', { level: 'info', message: 'Update detected. Syncing to staging...' });
|
|
116
141
|
await syncToStaging();
|
|
142
|
+
// Re-validate on update?
|
|
143
|
+
// const newRoot = findExtensionRoot(STAGING_DIR) || STAGING_DIR;
|
|
144
|
+
// const newValidation = validateExtension(newRoot);
|
|
145
|
+
// if (!newValidation.valid) ...
|
|
117
146
|
});
|
|
118
147
|
await ctx.actions.runAction('core:log', { level: 'info', message: 'Browser running in Detached Mode.' });
|
|
119
148
|
// Launch Logic
|
|
149
|
+
// Launch Logic
|
|
120
150
|
if (isWSL) {
|
|
121
|
-
|
|
151
|
+
// -------------------------------------------------------------------------
|
|
152
|
+
// WSL STRATEGY (Validated 2025-12-24)
|
|
153
|
+
// 1. Use Windows User Profile for staging to avoid Permission/Path issues
|
|
154
|
+
// 2. Use PowerShell script to launch Chrome to reliably pass arguments
|
|
155
|
+
// -------------------------------------------------------------------------
|
|
156
|
+
// 1. Setup Safe Paths (C:\Temp)
|
|
157
|
+
// We use the same path that syncToStaging() used (/mnt/c/Temp/ai-ext-preview)
|
|
158
|
+
const winStagingDir = 'C:\\Temp\\ai-ext-preview';
|
|
159
|
+
const winProfile = 'C:\\Temp\\ai-ext-profile';
|
|
160
|
+
let userProfileWin = 'C:\\Temp'; // Legacy variable support
|
|
161
|
+
const driveLetter = 'c';
|
|
162
|
+
// Calculate final paths
|
|
163
|
+
let finalWinExtensionPath = winStagingDir;
|
|
164
|
+
// Handle nested extension root
|
|
165
|
+
if (extensionRoot !== STAGING_DIR) {
|
|
166
|
+
const relative = path.relative(STAGING_DIR, extensionRoot);
|
|
167
|
+
finalWinExtensionPath = path.posix.join(winStagingDir.replace(/\\/g, '/'), relative).replace(/\//g, '\\');
|
|
168
|
+
}
|
|
122
169
|
const winChromePath = chromePath
|
|
123
170
|
.replace(new RegExp(`^/mnt/${driveLetter}/`), `${driveLetter.toUpperCase()}:\\`)
|
|
124
171
|
.replace(/\//g, '\\');
|
|
125
|
-
|
|
126
|
-
//
|
|
127
|
-
//
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
172
|
+
await ctx.actions.runAction('core:log', { level: 'info', message: `WSL Launch Target (Win): ${finalWinExtensionPath}` });
|
|
173
|
+
// await ctx.actions.runAction('core:log', { level: 'info', message: `WSL Profile (Win): ${winProfile}` });
|
|
174
|
+
// Create PowerShell Launch Script
|
|
175
|
+
const psContent = `
|
|
176
|
+
$chromePath = "${winChromePath}"
|
|
177
|
+
$extPath = "${finalWinExtensionPath}"
|
|
178
|
+
$profilePath = "${winProfile}"
|
|
179
|
+
|
|
180
|
+
Write-Host "DEBUG: ChromePath: $chromePath"
|
|
181
|
+
Write-Host "DEBUG: ExtPath: $extPath"
|
|
182
|
+
Write-Host "DEBUG: ProfilePath: $profilePath"
|
|
183
|
+
|
|
184
|
+
# Verify Paths
|
|
185
|
+
if (-not (Test-Path -Path $extPath)) {
|
|
186
|
+
Write-Host "ERROR: Extension Path NOT FOUND!"
|
|
187
|
+
} else {
|
|
188
|
+
Write-Host "DEBUG: Extension Path Exists."
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
# Create Profile Dir if needed
|
|
192
|
+
if (-not (Test-Path -Path $profilePath)) {
|
|
193
|
+
New-Item -ItemType Directory -Force -Path $profilePath | Out-Null
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
$argsList = @(
|
|
197
|
+
"--load-extension=""$extPath""",
|
|
198
|
+
"--user-data-dir=""$profilePath""",
|
|
199
|
+
"--no-first-run",
|
|
200
|
+
"--no-default-browser-check",
|
|
201
|
+
"--disable-gpu",
|
|
202
|
+
"about:blank"
|
|
203
|
+
)
|
|
204
|
+
|
|
205
|
+
# Convert to single string to ensure Start-Process handles it safely
|
|
206
|
+
$argStr = $argsList -join " "
|
|
207
|
+
Write-Host "DEBUG: Args: $argStr"
|
|
208
|
+
|
|
209
|
+
Write-Host "DEBUG: Launching Chrome..."
|
|
210
|
+
Start-Process -FilePath $chromePath -ArgumentList $argStr
|
|
139
211
|
`;
|
|
140
|
-
|
|
141
|
-
const
|
|
212
|
+
// Write ps1 to /mnt/c/Temp/ai-ext-preview/launch.ps1 (Same as STAGING_DIR)
|
|
213
|
+
const psPath = path.join(STAGING_DIR, 'launch.ps1');
|
|
142
214
|
try {
|
|
143
|
-
fs.
|
|
215
|
+
await fs.writeFile(psPath, psContent);
|
|
144
216
|
}
|
|
145
217
|
catch (e) {
|
|
146
|
-
await ctx.actions.runAction('core:log', { level: 'error', message: `WSL Write
|
|
218
|
+
await ctx.actions.runAction('core:log', { level: 'error', message: `WSL Write PS1 Failed: ${e.message}` });
|
|
147
219
|
}
|
|
148
|
-
|
|
149
|
-
|
|
220
|
+
// Execute via PowerShell (Spawn detached)
|
|
221
|
+
// psPathWin is C:\\Temp\\ai-ext-preview\\launch.ps1
|
|
222
|
+
const psPathWin = `${winStagingDir}\\launch.ps1`;
|
|
223
|
+
const child = spawn('powershell.exe', ['-NoProfile', '-ExecutionPolicy', 'Bypass', '-File', psPathWin], {
|
|
150
224
|
detached: true,
|
|
151
|
-
stdio: 'ignore',
|
|
152
|
-
|
|
225
|
+
stdio: ['ignore', 'pipe', 'pipe'] // Pipe stderr AND stdout to catch launch errors/debug
|
|
226
|
+
});
|
|
227
|
+
if (child.stdout) {
|
|
228
|
+
child.stdout.on('data', async (chunk) => {
|
|
229
|
+
const msg = chunk.toString();
|
|
230
|
+
await ctx.actions.runAction('core:log', { level: 'info', message: `[PS1] ${msg.trim()}` });
|
|
231
|
+
});
|
|
232
|
+
}
|
|
233
|
+
if (child.stderr) {
|
|
234
|
+
child.stderr.on('data', async (chunk) => {
|
|
235
|
+
const msg = chunk.toString();
|
|
236
|
+
await ctx.actions.runAction('core:log', { level: 'error', message: `Launch Error (Stderr): ${msg}` });
|
|
237
|
+
if (msg.includes('Exec format error')) {
|
|
238
|
+
await ctx.actions.runAction('core:log', { level: 'error', message: `CRITICAL: WSL Interop is broken. Cannot launch Chrome.` });
|
|
239
|
+
await ctx.actions.runAction('core:log', { level: 'error', message: `FIX: Open PowerShell as Admin and run: wsl --shutdown` });
|
|
240
|
+
ctx.events.emit('browser:launch-failed', { reason: 'WSL_INTEROP_BROKEN' });
|
|
241
|
+
}
|
|
242
|
+
});
|
|
243
|
+
}
|
|
244
|
+
child.on('error', async (err) => {
|
|
245
|
+
await ctx.actions.runAction('core:log', { level: 'error', message: `Launch Failed: ${err.message}` });
|
|
246
|
+
ctx.events.emit('browser:launch-failed', { reason: err.message });
|
|
153
247
|
});
|
|
154
|
-
|
|
248
|
+
child.unref();
|
|
155
249
|
return true;
|
|
156
250
|
}
|
|
157
251
|
else {
|
|
@@ -54,33 +54,58 @@ 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
|
+
// Check if files actually exist
|
|
70
|
+
let forceDownload = false;
|
|
71
|
+
const manifestPath = path.join(DIST_DIR, 'manifest.json');
|
|
72
|
+
if (!fs.existsSync(manifestPath)) {
|
|
73
|
+
await ctx.actions.runAction('core:log', { level: 'warn', message: 'Version match but files missing. Forcing download...' });
|
|
74
|
+
forceDownload = true;
|
|
75
|
+
}
|
|
76
|
+
if (newVersion !== lastModified || forceDownload) {
|
|
77
|
+
if (newVersion !== lastModified) {
|
|
78
|
+
await ctx.actions.runAction('core:log', { level: 'info', message: `New version detected (Old: "${lastModified}", New: "${newVersion}")` });
|
|
79
|
+
}
|
|
80
|
+
const success = await ctx.actions.runAction('downloader:download', null);
|
|
81
|
+
if (success) {
|
|
82
|
+
lastModified = newVersion;
|
|
83
|
+
fs.writeFileSync(VERSION_FILE, newVersion);
|
|
84
|
+
ctx.events.emit('downloader:updated', { version: job.version, jobId: config.jobId });
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
else {
|
|
89
|
+
// await ctx.actions.runAction('core:log', { level: 'info', message: `Poll: Job status is ${job.status}` });
|
|
90
|
+
}
|
|
91
|
+
isChecking = false;
|
|
92
|
+
return true;
|
|
65
93
|
}
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
const
|
|
69
|
-
if (
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
94
|
+
catch (error) {
|
|
95
|
+
attempt++;
|
|
96
|
+
const isNetworkError = error.code === 'EAI_AGAIN' || error.code === 'ENOTFOUND' || error.code === 'ECONNRESET' || error.code === 'ETIMEDOUT';
|
|
97
|
+
if (attempt < MAX_RETRIES && isNetworkError) {
|
|
98
|
+
await ctx.actions.runAction('core:log', { level: 'warn', message: `Connection failed (${error.code}). Retrying (${attempt}/${MAX_RETRIES})...` });
|
|
99
|
+
await new Promise(r => setTimeout(r, 1000 * attempt)); // Backoff
|
|
100
|
+
continue;
|
|
73
101
|
}
|
|
102
|
+
isChecking = false;
|
|
103
|
+
await ctx.actions.runAction('core:log', { level: 'error', message: `Check failed: ${error.message}` });
|
|
104
|
+
return false;
|
|
74
105
|
}
|
|
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
106
|
}
|
|
107
|
+
isChecking = false;
|
|
108
|
+
return false;
|
|
84
109
|
}
|
|
85
110
|
});
|
|
86
111
|
// Action: Download
|
|
@@ -170,19 +195,32 @@ console.log('[Hot Reload] Active for Job:', CURRENT_JOB_ID);
|
|
|
170
195
|
}
|
|
171
196
|
});
|
|
172
197
|
// Start Polling (Loop)
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
198
|
+
void ctx.actions.runAction('core:log', { level: 'info', message: 'Starting polling loop (Interval: 2000ms)' });
|
|
199
|
+
// Listen for browser failure to stop polling
|
|
200
|
+
ctx.events.on('browser:launch-failed', () => {
|
|
201
|
+
if (checkInterval) {
|
|
202
|
+
clearInterval(checkInterval);
|
|
203
|
+
checkInterval = undefined;
|
|
204
|
+
ctx.actions.runAction('core:log', { level: 'warn', message: 'Polling stopped due to browser launch failure.' });
|
|
205
|
+
// Update status happens in UI
|
|
206
|
+
}
|
|
207
|
+
});
|
|
208
|
+
checkInterval = setInterval(async () => {
|
|
209
|
+
try {
|
|
210
|
+
// Use actions for main log (UI Plugin captures this)
|
|
211
|
+
// console.error('[DownloaderPlugin] Tick - Checking Status...'); // REMOVE (Outside UI)
|
|
212
|
+
// Silent polling for CLI mode
|
|
213
|
+
// await ctx.actions.runAction('core:log', { level: 'info', message: '[DEBUG] Polling...' });
|
|
177
214
|
await ctx.actions.runAction('downloader:check', null);
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
215
|
+
}
|
|
216
|
+
catch (err) {
|
|
217
|
+
await ctx.actions.runAction('core:log', { level: 'error', message: `Poll Error: ${err.message}` });
|
|
218
|
+
}
|
|
219
|
+
}, 2000);
|
|
182
220
|
},
|
|
183
221
|
dispose(ctx) {
|
|
184
222
|
if (checkInterval) {
|
|
185
|
-
|
|
223
|
+
clearInterval(checkInterval);
|
|
186
224
|
checkInterval = undefined;
|
|
187
225
|
}
|
|
188
226
|
}
|
|
@@ -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.10",
|
|
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
|
}
|