haraps 1.0.5 โ†’ 1.0.6

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/dev.ps1 ADDED
@@ -0,0 +1,409 @@
1
+ # =============================================
2
+ # Medizys Local Development Runner
3
+ # =============================================
4
+ # Starts the Worker and Next.js frontend
5
+ # using concurrently in a single terminal window.
6
+
7
+ Write-Host ""
8
+ Write-Host "=============================================" -ForegroundColor Cyan
9
+ Write-Host " ๐Ÿš€ Medizys Local Dev Server" -ForegroundColor Cyan
10
+ Write-Host "=============================================" -ForegroundColor Cyan
11
+ Write-Host ""
12
+ Write-Host " Backend โ†’ http://localhost:8787" -ForegroundColor Green
13
+ Write-Host " Frontend โ†’ http://localhost:3000" -ForegroundColor Blue
14
+ Write-Host ""
15
+ Write-Host " Starting unified development environment..." -ForegroundColor Yellow
16
+ Write-Host "=============================================" -ForegroundColor Cyan
17
+ Write-Host ""
18
+
19
+ # Cleanup zombie worker processes and ports to prevent conflicts
20
+ Write-Host " Performing aggressive process cleanup..." -ForegroundColor Gray
21
+
22
+ # Define processes to kill
23
+ $procNames = @("workerd", "node", "concurrently")
24
+
25
+ foreach ($procName in $procNames) {
26
+ $procs = Get-Process $procName -ErrorAction SilentlyContinue
27
+ if ($procs) {
28
+ Write-Host " Stopping $procName ($($procs.Count) instances)..." -ForegroundColor Yellow
29
+ $procs | Stop-Process -Force -ErrorAction SilentlyContinue
30
+ }
31
+ }
32
+
33
+ # Active verification loop to ensure all target processes have fully exited
34
+ $maxWaitSeconds = 5
35
+ $stopwatch = [System.Diagnostics.Stopwatch]::StartNew()
36
+ while ($stopwatch.Elapsed.TotalSeconds -lt $maxWaitSeconds) {
37
+ $remainingProcs = Get-Process $procNames -ErrorAction SilentlyContinue
38
+ if (-not $remainingProcs) {
39
+ break
40
+ }
41
+ Start-Sleep -Milliseconds 200
42
+ }
43
+
44
+ # Function to robustly remove a directory with retry logic
45
+ function Remove-CacheDirectory($path, $label) {
46
+ if (Test-Path $path) {
47
+ Write-Host " Purging $label cache ($path)..." -ForegroundColor Gray
48
+ $attempts = 0
49
+ $maxAttempts = 5
50
+ $success = $false
51
+
52
+ while ($attempts -lt $maxAttempts -and -not $success) {
53
+ try {
54
+ Remove-Item -Recurse -Force $path -ErrorAction Stop
55
+ $success = $true
56
+ } catch {
57
+ $attempts++
58
+ if ($attempts -lt $maxAttempts) {
59
+ Write-Host " Lock detected on $path. Retrying ($attempts/$maxAttempts)..." -ForegroundColor DarkYellow
60
+ Start-Sleep -Milliseconds (300 * $attempts)
61
+ } else {
62
+ Write-Host " Warning: Could not fully purge $path. Some files may be locked." -ForegroundColor Red
63
+ }
64
+ }
65
+ }
66
+ }
67
+ }
68
+
69
+ # Hard clear of wrangler state and Next.js cache to prevent Turbopack panics and corrupted dependency graphs
70
+ Remove-CacheDirectory "worker/.wrangler" "wrangler state"
71
+ Remove-CacheDirectory "frontend/.next" "frontend Turbopack cache"
72
+
73
+ $ports = @(8787, 3000)
74
+ foreach ($port in $ports) {
75
+ Write-Host " Ensuring port $port is free..." -ForegroundColor Gray
76
+ $retryCount = 0
77
+ while ($retryCount -lt 5) {
78
+ $netstatOutput = netstat -ano | Select-String ":$port\s+.*LISTENING\s+(\d+)"
79
+ if ($netstatOutput) {
80
+ $targetPid = $netstatOutput.Matches[0].Groups[1].Value
81
+ Write-Host " Port $port occupied by PID $targetPid. Terminating..." -ForegroundColor Red
82
+ Stop-Process -Id $targetPid -Force -ErrorAction SilentlyContinue
83
+ Start-Sleep -Seconds 1
84
+ $retryCount++
85
+ } else {
86
+ break
87
+ }
88
+ }
89
+ }
90
+
91
+ # Telegram watcher disabled by user request
92
+ # Write-Host " Starting Telegram watcher in background..." -ForegroundColor Magenta
93
+ # $watcherJob = Start-Job -ScriptBlock {
94
+ # & "$using:PSScriptRoot\scripts\telegram-watcher.ps1"
95
+ # }
96
+ # Write-Host " Telegram watcher running (Job ID: $($watcherJob.Id))" -ForegroundColor Magenta
97
+ # Write-Host " Reply to @mediisysbot on Telegram to queue instructions." -ForegroundColor DarkMagenta
98
+ # Write-Host ""
99
+
100
+ # Disable wrangler usage metrics prompt to prevent concurrently from hanging
101
+ $env:WRANGLER_SEND_METRICS = "false"
102
+
103
+ # Limit Node.js memory to prevent OOM crashes while providing 4GB for Next.js App Router
104
+ $env:NODE_OPTIONS = "--max-old-space-size=4096"
105
+
106
+ # Run the monorepo dev command through a Node.js wrapper to capture logs
107
+ # and listen for the 'c' keypress to copy output to the clipboard.
108
+ Write-Host "Starting development environment..." -ForegroundColor Green
109
+ $runnerScript = @'
110
+ const { spawn, execSync } = require('child_process');
111
+ let buffer = '';
112
+ let workerChild;
113
+ let frontendChild;
114
+ let lastRestartTime = 0;
115
+ const startTime = Date.now();
116
+
117
+ function formatSize(bytes) {
118
+ if (bytes < 1024) return bytes + 'B';
119
+ if (bytes < 1024 * 1024) return (bytes / 1024).toFixed(1) + 'KB';
120
+ return (bytes / (1024 * 1024)).toFixed(1) + 'MB';
121
+ }
122
+
123
+ function formatUptime() {
124
+ const elapsed = Math.floor((Date.now() - startTime) / 1000);
125
+ const m = Math.floor(elapsed / 60).toString().padStart(2, '0');
126
+ const s = (elapsed % 60).toString().padStart(2, '0');
127
+ const h = Math.floor(elapsed / 3600);
128
+ if (h > 0) return `${h}h ${m}m ${s}s`;
129
+ return `${m}m ${s}s`;
130
+ }
131
+
132
+ let lastRows = 0;
133
+ let lastCols = 0;
134
+
135
+ function updateLegend() {
136
+ if (!process.stdout.isTTY) return;
137
+ const rows = process.stdout.rows || 30;
138
+ const cols = process.stdout.columns || 80;
139
+
140
+ // Set scrolling region only if changed (DECSTBM homes the cursor to 1,1!)
141
+ if (rows !== lastRows || cols !== lastCols) {
142
+ process.stdout.write(`\x1b[1;${rows - 2}r`);
143
+ lastRows = rows;
144
+ lastCols = cols;
145
+ }
146
+
147
+ const lineCount = (buffer.match(/\n/g) || []).length;
148
+ const sizeStr = formatSize(Buffer.byteLength(buffer, 'utf8'));
149
+ const uptimeStr = formatUptime();
150
+ const legend1 = ` [Copy] 'c': All (${lineCount}L / ${sizeStr}) | 's': 500L | 'w': Backend | 'f': Frontend | 'e': Errors `;
151
+ const legend2 = ` [System] Uptime: ${uptimeStr} | 'x': Clear | 'r': Restart ALL | '1': Frontend | '2': Backend | 'q': Quit | 'Q': Clean & Quit `;
152
+
153
+ // Draw legend
154
+ process.stdout.write(`\x1b[s`); // save cursor
155
+ process.stdout.write(`\x1b[${rows - 1};1H\x1b[42m\x1b[30m${legend1.padEnd(cols)}\x1b[0m`);
156
+ process.stdout.write(`\x1b[${rows};1H\x1b[42m\x1b[30m${legend2.padEnd(cols)}\x1b[0m`);
157
+ process.stdout.write(`\x1b[u`); // restore cursor
158
+ }
159
+
160
+ let legendInterval;
161
+ function startLegendUpdater() {
162
+ if (legendInterval) clearInterval(legendInterval);
163
+ legendInterval = setInterval(() => {
164
+ updateLegend();
165
+ }, 1000);
166
+ }
167
+
168
+ function cleanupLegend() {
169
+ if (legendInterval) clearInterval(legendInterval);
170
+ if (!process.stdout.isTTY) return;
171
+ process.stdout.write('\x1b[r'); // reset scrolling region
172
+ }
173
+
174
+ process.stdout.on('resize', () => {
175
+ if (!process.stdout.isTTY) return;
176
+ cleanupLegend();
177
+ console.clear();
178
+ lastRows = 0;
179
+ updateLegend();
180
+ });
181
+
182
+ function extractErrors(text) {
183
+ const lines = text.split('\n');
184
+ const errorPattern = /(error|exception|fail|โจฏ|โœ—|traceback)/i;
185
+ let inErrorBlock = false;
186
+ let blockStart = 0;
187
+ const blocks = [];
188
+
189
+ for (let i = 0; i < lines.length; i++) {
190
+ // Check for error keywords or stack trace lines
191
+ if (errorPattern.test(lines[i]) || (inErrorBlock && lines[i].trim().startsWith('at '))) {
192
+ if (!inErrorBlock) {
193
+ inErrorBlock = true;
194
+ blockStart = Math.max(0, i - 2); // 2 lines of context before
195
+ }
196
+ } else {
197
+ if (inErrorBlock) {
198
+ // If the next line is empty, we might just be between stack traces
199
+ if (lines[i].trim() === '') continue;
200
+
201
+ // End of error block
202
+ const blockEnd = Math.min(lines.length - 1, i + 2); // 2 lines of context after
203
+ blocks.push(lines.slice(blockStart, blockEnd + 1).join('\n'));
204
+ inErrorBlock = false;
205
+ }
206
+ }
207
+ }
208
+ if (inErrorBlock) {
209
+ blocks.push(lines.slice(blockStart).join('\n'));
210
+ }
211
+
212
+ return blocks.length > 0 ? blocks.join('\n\n--- [Next Error Block] ---\n\n') : '';
213
+ }
214
+
215
+ function formatPrefix(isError, source) {
216
+ const d = new Date();
217
+ const ts = String(d.getHours()).padStart(2, '0') + ':' +
218
+ String(d.getMinutes()).padStart(2, '0') + ':' +
219
+ String(d.getSeconds()).padStart(2, '0');
220
+
221
+ // Cyan for worker, Magenta for frontend
222
+ const color = source === 'worker' ? '\x1b[36m' : '\x1b[35m';
223
+ const tag = source === 'worker' ? '[BACKEND]' : '[FRONTEND]';
224
+ return `\x1b[90m[${ts}]\x1b[0m ${color}${tag}\x1b[0m `;
225
+ }
226
+
227
+ function createStreamHandler(isError, source) {
228
+ let remainder = '';
229
+ return (d) => {
230
+ const chunk = remainder + d.toString();
231
+ const lines = chunk.split('\n');
232
+ remainder = lines.pop();
233
+ for (let line of lines) {
234
+ // Strip terminal-clearing and cursor-resetting escape codes
235
+ line = line.replace(/\x1b\[[23]J/g, '').replace(/\x1b\[[0-9;]*H/g, '').replace(/\x1bc/g, '');
236
+ // Strip cursor up/down and line clear codes
237
+ line = line.replace(/\x1b\[[0-9]*[A-G]/g, '').replace(/\x1b\[[0-9]*[K]/g, '');
238
+ // Strip carriage returns which cause overwrite from start of line
239
+ line = line.replace(/\r/g, '');
240
+
241
+ // Remove concurrently's prefix so we can add our own clean one
242
+ line = line.replace(/^\[[^\]]+\]\s*/, '');
243
+
244
+ if (line.trim() === '') continue;
245
+
246
+ const prefix = formatPrefix(isError, source);
247
+ const out = prefix + line + '\n';
248
+
249
+ if (isError) process.stderr.write(out);
250
+ else process.stdout.write(out);
251
+ buffer += out;
252
+ }
253
+ if (buffer.length > 1000000) {
254
+ const idx = buffer.indexOf('\n', buffer.length - 500000);
255
+ buffer = buffer.slice(idx === -1 ? -500000 : idx + 1);
256
+ }
257
+ };
258
+ }
259
+
260
+ function startWorker() {
261
+ workerChild = spawn('npx.cmd', ['concurrently', '-c', 'cyan', '-n', 'worker', '"npm run dev --prefix worker"'], {
262
+ env: { ...process.env, FORCE_COLOR: '1' },
263
+ stdio: ['ignore', 'pipe', 'pipe'],
264
+ windowsHide: true,
265
+ shell: true
266
+ });
267
+ workerChild.stdout.on('data', createStreamHandler(false, 'worker'));
268
+ workerChild.stderr.on('data', createStreamHandler(true, 'worker'));
269
+ }
270
+
271
+ function startFrontend() {
272
+ frontendChild = spawn('npx.cmd', ['concurrently', '-c', 'magenta', '-n', 'frontend', '"npm run dev --prefix frontend"'], {
273
+ env: { ...process.env, FORCE_COLOR: '1' },
274
+ stdio: ['ignore', 'pipe', 'pipe'],
275
+ windowsHide: true,
276
+ shell: true
277
+ });
278
+ frontendChild.stdout.on('data', createStreamHandler(false, 'frontend'));
279
+ frontendChild.stderr.on('data', createStreamHandler(true, 'frontend'));
280
+ }
281
+
282
+ function startServices() {
283
+ startWorker();
284
+ startFrontend();
285
+ }
286
+
287
+ console.clear();
288
+ updateLegend();
289
+ startLegendUpdater();
290
+ startServices();
291
+
292
+ if (process.stdin.isTTY) {
293
+ process.stdin.setRawMode(true);
294
+ process.stdin.resume();
295
+ process.stdin.setEncoding('utf8');
296
+ process.stdin.on('data', k => {
297
+ const now = Date.now();
298
+ if (k === 'c' || k === 'C') {
299
+ const clean = buffer.replace(/\x1B(?:[@-Z\-_]|\[[0-?]*[ -/]*[@-~])/g, '');
300
+ if (!clean.trim()) {
301
+ console.log('\n\x1b[33mโš ๏ธ Buffer is empty.\x1b[0m\n');
302
+ return;
303
+ }
304
+ try {
305
+ execSync('clip', { input: clean });
306
+ console.log('\n\x1b[32m๐Ÿ“‹ Copied ALL logs to clipboard!\x1b[0m\n');
307
+ } catch (e) {}
308
+ } else if (k === 's' || k === 'S') {
309
+ const clean = buffer.replace(/\x1B(?:[@-Z\-_]|\[[0-?]*[ -/]*[@-~])/g, '');
310
+ const shortLines = clean.split('\n').slice(-500).join('\n');
311
+ try {
312
+ execSync('clip', { input: shortLines });
313
+ console.log('\n\x1b[32m๐Ÿ“‹ Copied last 500 lines to clipboard!\x1b[0m\n');
314
+ } catch (e) {}
315
+ } else if (k === 'w' || k === 'W') {
316
+ const clean = buffer.replace(/\x1B(?:[@-Z\-_]|\[[0-?]*[ -/]*[@-~])/g, '');
317
+ const workerLogs = clean.split('\n').filter(l => l.includes('[BACKEND]')).slice(-1000).join('\n');
318
+ try {
319
+ execSync('clip', { input: workerLogs });
320
+ console.log('\n\x1b[32m๐Ÿ“‹ Copied BACKEND logs to clipboard!\x1b[0m\n');
321
+ } catch (e) {}
322
+ } else if (k === 'f' || k === 'F') {
323
+ const clean = buffer.replace(/\x1B(?:[@-Z\-_]|\[[0-?]*[ -/]*[@-~])/g, '');
324
+ const frontendLogs = clean.split('\n').filter(l => l.includes('[FRONTEND]')).slice(-1000).join('\n');
325
+ try {
326
+ execSync('clip', { input: frontendLogs });
327
+ console.log('\n\x1b[32m๐Ÿ“‹ Copied FRONTEND logs to clipboard!\x1b[0m\n');
328
+ } catch (e) {}
329
+ } else if (k === 'e' || k === 'E') {
330
+ const clean = buffer.replace(/\x1B(?:[@-Z\-_]|\[[0-?]*[ -/]*[@-~])/g, '');
331
+ const errors = extractErrors(clean);
332
+ if (errors) {
333
+ try {
334
+ execSync('clip', { input: errors });
335
+ console.log('\n\x1b[31m๐Ÿšจ Errors copied to clipboard!\x1b[0m\n');
336
+ } catch (e) {}
337
+ } else {
338
+ console.log('\n\x1b[32mโœ… No errors detected.\x1b[0m\n');
339
+ }
340
+ } else if (k === 'r' || k === 'R') {
341
+ if (now - lastRestartTime < 1000) return;
342
+ lastRestartTime = now;
343
+ console.log('\n\x1b[33m๐Ÿ”„ Restarting ALL services...\x1b[0m\n');
344
+ if (workerChild) try { execSync('taskkill /pid ' + workerChild.pid + ' /t /f', { stdio: 'ignore' }); } catch(e) {}
345
+ if (frontendChild) try { execSync('taskkill /pid ' + frontendChild.pid + ' /t /f', { stdio: 'ignore' }); } catch(e) {}
346
+ startServices();
347
+ } else if (k === '1') {
348
+ if (now - lastRestartTime < 1000) return;
349
+ lastRestartTime = now;
350
+ console.log('\n\x1b[33m๐Ÿ”„ Restarting FRONTEND...\x1b[0m\n');
351
+ if (frontendChild) try { execSync('taskkill /pid ' + frontendChild.pid + ' /t /f', { stdio: 'ignore' }); } catch(e) {}
352
+ startFrontend();
353
+ } else if (k === '2') {
354
+ if (now - lastRestartTime < 1000) return;
355
+ lastRestartTime = now;
356
+ console.log('\n\x1b[33m๐Ÿ”„ Restarting BACKEND...\x1b[0m\n');
357
+ if (workerChild) try { execSync('taskkill /pid ' + workerChild.pid + ' /t /f', { stdio: 'ignore' }); } catch(e) {}
358
+ startWorker();
359
+ } else if (k === 'x' || k === 'X') {
360
+ console.clear();
361
+ buffer = '';
362
+ lastRows = 0;
363
+ updateLegend();
364
+ console.log('\x1b[32m๐Ÿงน Terminal cleared.\x1b[0m\n');
365
+ } else if (k === '\u0003' || k === 'q') {
366
+ console.log('\n\x1b[33mStopping services...\x1b[0m');
367
+ cleanupLegend();
368
+ if (workerChild) try { execSync('taskkill /pid ' + workerChild.pid + ' /t /f', { stdio: 'ignore' }); } catch(e) {}
369
+ if (frontendChild) try { execSync('taskkill /pid ' + frontendChild.pid + ' /t /f', { stdio: 'ignore' }); } catch(e) {}
370
+ process.exit();
371
+ } else if (k === 'Q') {
372
+ console.log('\n\x1b[31m๐Ÿงน Cleaning cache and dependencies, then stopping services...\x1b[0m');
373
+ cleanupLegend();
374
+ if (workerChild) try { execSync('taskkill /pid ' + workerChild.pid + ' /t /f', { stdio: 'ignore' }); } catch(e) {}
375
+ if (frontendChild) try { execSync('taskkill /pid ' + frontendChild.pid + ' /t /f', { stdio: 'ignore' }); } catch(e) {}
376
+
377
+ console.log('\x1b[33mRemoving node_modules, .next, and .wrangler...\x1b[0m');
378
+ const fs = require('fs');
379
+ const pathsToClean = ['frontend/.next', 'worker/.wrangler', 'frontend/node_modules', 'worker/node_modules', 'node_modules'];
380
+ for (const p of pathsToClean) {
381
+ try { fs.rmSync(p, { recursive: true, force: true }); } catch(e) {}
382
+ }
383
+ console.log('\x1b[32mCleanup complete.\x1b[0m');
384
+ process.exit();
385
+ }
386
+ });
387
+ }
388
+
389
+ // Cleanup on unexpected exits
390
+ process.on('SIGINT', () => {
391
+ cleanupLegend();
392
+ process.exit();
393
+ });
394
+ process.on('exit', () => {
395
+ cleanupLegend();
396
+ console.clear();
397
+ });
398
+ '@
399
+ $runnerPath = Join-Path $env:TEMP "mediqueue-dev-runner.js"
400
+ Set-Content -Path $runnerPath -Value $runnerScript -Encoding UTF8
401
+ node $runnerPath
402
+
403
+ # Cleanup watcher on exit
404
+ if ($null -ne (Get-Variable -Name watcherJob -ErrorAction SilentlyContinue)) {
405
+ if ($watcherJob) {
406
+ Stop-Job -Job $watcherJob -ErrorAction SilentlyContinue
407
+ Remove-Job -Job $watcherJob -ErrorAction SilentlyContinue
408
+ }
409
+ }