ai-extension-preview 0.1.7 → 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,6 +22,62 @@ function findChrome() {
|
|
|
22
22
|
}
|
|
23
23
|
return null;
|
|
24
24
|
}
|
|
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) => {
|
|
47
|
+
// Handle Git Bash /c/ style
|
|
48
|
+
const gitBashMatch = p.match(/^\/([a-z])\/(.*)/i);
|
|
49
|
+
if (gitBashMatch) {
|
|
50
|
+
return `${gitBashMatch[1].toUpperCase()}:\\${gitBashMatch[2].replace(/\//g, '\\')}`;
|
|
51
|
+
}
|
|
52
|
+
// Handle Forward slashes
|
|
53
|
+
return p.replace(/\//g, '\\');
|
|
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
|
+
};
|
|
25
81
|
export const BrowserPlugin = {
|
|
26
82
|
name: 'browser',
|
|
27
83
|
version: '1.0.0',
|
|
@@ -36,7 +92,11 @@ export const BrowserPlugin = {
|
|
|
36
92
|
return false;
|
|
37
93
|
}
|
|
38
94
|
const isWSL = fs.existsSync('/mnt/c');
|
|
39
|
-
let executable = chromePath;
|
|
95
|
+
let executable = chromePath;
|
|
96
|
+
// Normalize Executable for Native Windows (Git Bash)
|
|
97
|
+
if (!isWSL && process.platform === 'win32') {
|
|
98
|
+
executable = normalizePathToWindows(chromePath);
|
|
99
|
+
}
|
|
40
100
|
const STAGING_DIR = isWSL ? '/mnt/c/Temp/ai-ext-preview' : path.join(config.workDir, '../staging');
|
|
41
101
|
const WIN_PROFILE_DIR = 'C:/Temp/ai-ext-profile';
|
|
42
102
|
// For native windows/linux, use local staging path
|
|
@@ -51,6 +111,12 @@ export const BrowserPlugin = {
|
|
|
51
111
|
fs.ensureDirSync(STAGING_DIR);
|
|
52
112
|
fs.copySync(DIST_DIR, STAGING_DIR);
|
|
53
113
|
await ctx.actions.runAction('core:log', { level: 'info', message: `Synced code to Staging` });
|
|
114
|
+
// DEBUG: Log contents of staging
|
|
115
|
+
try {
|
|
116
|
+
const files = fs.readdirSync(STAGING_DIR);
|
|
117
|
+
await ctx.actions.runAction('core:log', { level: 'info', message: `Staging Contents: ${files.join(', ')}` });
|
|
118
|
+
}
|
|
119
|
+
catch (e) { }
|
|
54
120
|
// Emit staged event for ServerPlugin (optional for now, but good practice)
|
|
55
121
|
ctx.events.emit('browser:staged', { path: STAGING_DIR });
|
|
56
122
|
}
|
|
@@ -58,34 +124,19 @@ export const BrowserPlugin = {
|
|
|
58
124
|
await ctx.actions.runAction('core:log', { level: 'error', message: `Failed to sync to staging: ${err.message}` });
|
|
59
125
|
}
|
|
60
126
|
};
|
|
61
|
-
// --- Helper to find actual extension root (handle nested folder in zip) ---
|
|
62
|
-
const findExtensionRoot = (dir) => {
|
|
63
|
-
if (fs.existsSync(path.join(dir, 'manifest.json')))
|
|
64
|
-
return dir;
|
|
65
|
-
// Check immediate subdirectories (depth 1)
|
|
66
|
-
try {
|
|
67
|
-
const items = fs.readdirSync(dir);
|
|
68
|
-
for (const item of items) {
|
|
69
|
-
const fullPath = path.join(dir, item);
|
|
70
|
-
if (fs.statSync(fullPath).isDirectory()) {
|
|
71
|
-
if (fs.existsSync(path.join(fullPath, 'manifest.json'))) {
|
|
72
|
-
return fullPath;
|
|
73
|
-
}
|
|
74
|
-
}
|
|
75
|
-
}
|
|
76
|
-
}
|
|
77
|
-
catch (e) {
|
|
78
|
-
// Dir might be empty or invalid
|
|
79
|
-
}
|
|
80
|
-
return null;
|
|
81
|
-
};
|
|
82
127
|
// Initial Sync
|
|
83
128
|
await syncToStaging();
|
|
84
129
|
// Resolve proper root AFTER sync
|
|
85
130
|
let extensionRoot = findExtensionRoot(STAGING_DIR) || STAGING_DIR;
|
|
86
131
|
// Check if we found a valid root
|
|
87
|
-
|
|
88
|
-
|
|
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}` });
|
|
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.
|
|
89
140
|
}
|
|
90
141
|
else if (extensionRoot !== STAGING_DIR) {
|
|
91
142
|
await ctx.actions.runAction('core:log', { level: 'info', message: `Detected nested extension at: ${path.basename(extensionRoot)}` });
|
|
@@ -94,43 +145,107 @@ export const BrowserPlugin = {
|
|
|
94
145
|
ctx.events.on('downloader:updated', async (data) => {
|
|
95
146
|
await ctx.actions.runAction('core:log', { level: 'info', message: 'Update detected. Syncing to staging...' });
|
|
96
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) ...
|
|
97
152
|
});
|
|
98
153
|
await ctx.actions.runAction('core:log', { level: 'info', message: 'Browser running in Detached Mode.' });
|
|
99
154
|
// Launch Logic
|
|
155
|
+
// Launch Logic
|
|
100
156
|
if (isWSL) {
|
|
101
|
-
|
|
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
|
+
}
|
|
102
200
|
const winChromePath = chromePath
|
|
103
201
|
.replace(new RegExp(`^/mnt/${driveLetter}/`), `${driveLetter.toUpperCase()}:\\`)
|
|
104
202
|
.replace(/\//g, '\\');
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
//
|
|
108
|
-
//
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
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
|
|
118
227
|
`;
|
|
119
|
-
const
|
|
120
|
-
const
|
|
228
|
+
const psPath = path.join(wslStagingDir, 'launch.ps1');
|
|
229
|
+
const winPsPath = path.posix.join(stagingDirWin.replace(/\\/g, '/'), 'launch.ps1').replace(/\//g, '\\');
|
|
121
230
|
try {
|
|
122
|
-
fs.
|
|
231
|
+
await fs.writeFile(psPath, psContent);
|
|
123
232
|
}
|
|
124
233
|
catch (e) {
|
|
125
|
-
|
|
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}` });
|
|
126
248
|
}
|
|
127
|
-
const cli = 'cmd.exe';
|
|
128
|
-
const subprocess = spawn(cli, ['/c', winBatPath], {
|
|
129
|
-
detached: true,
|
|
130
|
-
stdio: 'ignore',
|
|
131
|
-
cwd: '/mnt/c'
|
|
132
|
-
});
|
|
133
|
-
subprocess.unref();
|
|
134
249
|
return true;
|
|
135
250
|
}
|
|
136
251
|
else {
|
|
@@ -138,8 +253,8 @@ exit
|
|
|
138
253
|
// Use extensionRoot which points to the detected subfolder or root
|
|
139
254
|
const safeDist = path.resolve(extensionRoot);
|
|
140
255
|
const safeProfile = path.join(path.dirname(config.workDir), 'profile'); // ~/.ai-extension-preview/profile
|
|
141
|
-
await ctx.actions.runAction('core:log', { level: 'info', message: `
|
|
142
|
-
await ctx.actions.runAction('core:log', { level: 'info', message: `
|
|
256
|
+
await ctx.actions.runAction('core:log', { level: 'info', message: `Native Launch Executable: ${executable}` });
|
|
257
|
+
await ctx.actions.runAction('core:log', { level: 'info', message: `Native Launch Target: ${safeDist}` });
|
|
143
258
|
const cleanArgs = [
|
|
144
259
|
`--load-extension=${safeDist}`,
|
|
145
260
|
`--user-data-dir=${safeProfile}`,
|
|
@@ -148,11 +263,16 @@ exit
|
|
|
148
263
|
'--disable-gpu',
|
|
149
264
|
'chrome://extensions'
|
|
150
265
|
];
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
266
|
+
try {
|
|
267
|
+
const subprocess = spawn(executable, cleanArgs, {
|
|
268
|
+
detached: true,
|
|
269
|
+
stdio: 'ignore'
|
|
270
|
+
});
|
|
271
|
+
subprocess.unref();
|
|
272
|
+
}
|
|
273
|
+
catch (spawnErr) {
|
|
274
|
+
await ctx.actions.runAction('core:log', { level: 'error', message: `Spawn Failed: ${spawnErr.message}` });
|
|
275
|
+
}
|
|
156
276
|
return true;
|
|
157
277
|
}
|
|
158
278
|
};
|
|
@@ -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
|
}
|