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/SKILL.md +9 -1
- package/bin/index.js +1381 -756
- package/deploy.ps1 +414 -0
- package/deploy.sh +10 -0
- package/dev.ps1 +409 -0
- package/dev.sh +359 -0
- package/package.json +1 -1
package/dev.sh
ADDED
|
@@ -0,0 +1,359 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
# =============================================
|
|
3
|
+
# Medizys Local Development Runner
|
|
4
|
+
# =============================================
|
|
5
|
+
# Starts the Worker and Next.js frontend
|
|
6
|
+
# using concurrently in a single terminal window.
|
|
7
|
+
|
|
8
|
+
echo ""
|
|
9
|
+
echo -e "\e[36m=============================================\e[0m"
|
|
10
|
+
echo -e "\e[36m ๐ Medizys Local Dev Server\e[0m"
|
|
11
|
+
echo -e "\e[36m=============================================\e[0m"
|
|
12
|
+
echo ""
|
|
13
|
+
echo -e "\e[32m Backend โ http://localhost:8787\e[0m"
|
|
14
|
+
echo -e "\e[34m Frontend โ http://localhost:3000\e[0m"
|
|
15
|
+
echo ""
|
|
16
|
+
echo -e "\e[33m Starting unified development environment...\e[0m"
|
|
17
|
+
echo -e "\e[36m=============================================\e[0m"
|
|
18
|
+
echo ""
|
|
19
|
+
|
|
20
|
+
# Ensure Node 22 is used if nvm is available
|
|
21
|
+
if [ -s "$HOME/.nvm/nvm.sh" ]; then
|
|
22
|
+
export NVM_DIR="$HOME/.nvm"
|
|
23
|
+
source "$NVM_DIR/nvm.sh"
|
|
24
|
+
nvm use 22 > /dev/null 2>&1
|
|
25
|
+
fi
|
|
26
|
+
|
|
27
|
+
# Cleanup zombie worker processes and ports to prevent conflicts
|
|
28
|
+
echo -e "\e[90m Performing aggressive process cleanup...\e[0m"
|
|
29
|
+
|
|
30
|
+
# Define processes to kill
|
|
31
|
+
for procName in workerd node concurrently; do
|
|
32
|
+
if pgrep -x "$procName" > /dev/null; then
|
|
33
|
+
echo -e "\e[33m Stopping $procName...\e[0m"
|
|
34
|
+
pkill -f "$procName" 2>/dev/null
|
|
35
|
+
fi
|
|
36
|
+
done
|
|
37
|
+
|
|
38
|
+
# Active verification loop to ensure all target processes have fully exited
|
|
39
|
+
maxWaitSeconds=5
|
|
40
|
+
start=$(date +%s)
|
|
41
|
+
while [ $(( $(date +%s) - start )) -lt $maxWaitSeconds ]; do
|
|
42
|
+
if ! pgrep -f "workerd|concurrently" > /dev/null; then
|
|
43
|
+
break
|
|
44
|
+
fi
|
|
45
|
+
sleep 0.2
|
|
46
|
+
done
|
|
47
|
+
|
|
48
|
+
# Function to robustly remove a directory with retry logic
|
|
49
|
+
remove_cache_directory() {
|
|
50
|
+
local path=$1
|
|
51
|
+
local label=$2
|
|
52
|
+
if [ -d "$path" ]; then
|
|
53
|
+
echo -e "\e[90m Purging $label cache ($path)...\e[0m"
|
|
54
|
+
rm -rf "$path"
|
|
55
|
+
fi
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
# Hard clear of wrangler state and Next.js cache
|
|
59
|
+
remove_cache_directory "worker/.wrangler" "wrangler state"
|
|
60
|
+
remove_cache_directory "frontend/.next" "frontend Turbopack cache"
|
|
61
|
+
|
|
62
|
+
for port in 8787 3000; do
|
|
63
|
+
echo -e "\e[90m Ensuring port $port is free...\e[0m"
|
|
64
|
+
retryCount=0
|
|
65
|
+
while [ $retryCount -lt 5 ]; do
|
|
66
|
+
targetPid=$(lsof -ti :$port)
|
|
67
|
+
if [ ! -z "$targetPid" ]; then
|
|
68
|
+
echo -e "\e[31m Port $port occupied by PID $targetPid. Terminating...\e[0m"
|
|
69
|
+
kill -9 $targetPid 2>/dev/null
|
|
70
|
+
sleep 1
|
|
71
|
+
retryCount=$((retryCount+1))
|
|
72
|
+
else
|
|
73
|
+
break
|
|
74
|
+
fi
|
|
75
|
+
done
|
|
76
|
+
done
|
|
77
|
+
|
|
78
|
+
# Disable wrangler usage metrics prompt
|
|
79
|
+
export WRANGLER_SEND_METRICS="false"
|
|
80
|
+
# Limit Node.js memory
|
|
81
|
+
export NODE_OPTIONS="--max-old-space-size=4096"
|
|
82
|
+
|
|
83
|
+
echo -e "\e[32mStarting development environment...\e[0m"
|
|
84
|
+
|
|
85
|
+
runnerPath="/tmp/mediqueue-dev-runner.js"
|
|
86
|
+
|
|
87
|
+
cat << 'EOF' > "$runnerPath"
|
|
88
|
+
const { spawn, execSync } = require('child_process');
|
|
89
|
+
let buffer = '';
|
|
90
|
+
let workerChild;
|
|
91
|
+
let frontendChild;
|
|
92
|
+
let lastRestartTime = 0;
|
|
93
|
+
const startTime = Date.now();
|
|
94
|
+
|
|
95
|
+
function formatSize(bytes) {
|
|
96
|
+
if (bytes < 1024) return bytes + 'B';
|
|
97
|
+
if (bytes < 1024 * 1024) return (bytes / 1024).toFixed(1) + 'KB';
|
|
98
|
+
return (bytes / (1024 * 1024)).toFixed(1) + 'MB';
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
function formatUptime() {
|
|
102
|
+
const elapsed = Math.floor((Date.now() - startTime) / 1000);
|
|
103
|
+
const m = Math.floor(elapsed / 60).toString().padStart(2, '0');
|
|
104
|
+
const s = (elapsed % 60).toString().padStart(2, '0');
|
|
105
|
+
const h = Math.floor(elapsed / 3600);
|
|
106
|
+
if (h > 0) return `${h}h ${m}m ${s}s`;
|
|
107
|
+
return `${m}m ${s}s`;
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
let lastRows = 0;
|
|
111
|
+
let lastCols = 0;
|
|
112
|
+
|
|
113
|
+
function updateLegend() {
|
|
114
|
+
if (!process.stdout.isTTY) return;
|
|
115
|
+
const rows = process.stdout.rows || 30;
|
|
116
|
+
const cols = process.stdout.columns || 80;
|
|
117
|
+
|
|
118
|
+
// Set scrolling region only if changed (DECSTBM homes the cursor to 1,1!)
|
|
119
|
+
if (rows !== lastRows || cols !== lastCols) {
|
|
120
|
+
process.stdout.write(`\x1b[1;${rows - 2}r`);
|
|
121
|
+
lastRows = rows;
|
|
122
|
+
lastCols = cols;
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
const lineCount = (buffer.match(/\n/g) || []).length;
|
|
126
|
+
const sizeStr = formatSize(Buffer.byteLength(buffer, 'utf8'));
|
|
127
|
+
const uptimeStr = formatUptime();
|
|
128
|
+
const legend1 = ` [Copy] 'c': All (${lineCount}L / ${sizeStr}) | 's': 500L | 'w': Backend | 'f': Frontend | 'e': Errors `;
|
|
129
|
+
const legend2 = ` [System] Uptime: ${uptimeStr} | 'x': Clear | 'r': Restart ALL | '1': Frontend | '2': Backend | 'q': Quit `;
|
|
130
|
+
|
|
131
|
+
// Draw legend
|
|
132
|
+
process.stdout.write(`\x1b[s`); // save cursor
|
|
133
|
+
process.stdout.write(`\x1b[${rows - 1};1H\x1b[42m\x1b[30m${legend1.padEnd(cols)}\x1b[0m`);
|
|
134
|
+
process.stdout.write(`\x1b[${rows};1H\x1b[42m\x1b[30m${legend2.padEnd(cols)}\x1b[0m`);
|
|
135
|
+
process.stdout.write(`\x1b[u`); // restore cursor
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
let legendInterval;
|
|
139
|
+
function startLegendUpdater() {
|
|
140
|
+
if (legendInterval) clearInterval(legendInterval);
|
|
141
|
+
legendInterval = setInterval(() => {
|
|
142
|
+
updateLegend();
|
|
143
|
+
}, 1000);
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
function cleanupLegend() {
|
|
147
|
+
if (legendInterval) clearInterval(legendInterval);
|
|
148
|
+
if (!process.stdout.isTTY) return;
|
|
149
|
+
process.stdout.write('\x1b[r'); // reset scrolling region
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
process.stdout.on('resize', () => {
|
|
153
|
+
if (!process.stdout.isTTY) return;
|
|
154
|
+
cleanupLegend();
|
|
155
|
+
console.clear();
|
|
156
|
+
lastRows = 0;
|
|
157
|
+
updateLegend();
|
|
158
|
+
});
|
|
159
|
+
|
|
160
|
+
function extractErrors(text) {
|
|
161
|
+
const lines = text.split('\n');
|
|
162
|
+
const errorPattern = /(error|exception|fail|โจฏ|โ|traceback)/i;
|
|
163
|
+
let inErrorBlock = false;
|
|
164
|
+
let blockStart = 0;
|
|
165
|
+
const blocks = [];
|
|
166
|
+
|
|
167
|
+
for (let i = 0; i < lines.length; i++) {
|
|
168
|
+
if (errorPattern.test(lines[i]) || (inErrorBlock && lines[i].trim().startsWith('at '))) {
|
|
169
|
+
if (!inErrorBlock) {
|
|
170
|
+
inErrorBlock = true;
|
|
171
|
+
blockStart = Math.max(0, i - 2);
|
|
172
|
+
}
|
|
173
|
+
} else {
|
|
174
|
+
if (inErrorBlock) {
|
|
175
|
+
if (lines[i].trim() === '') continue;
|
|
176
|
+
const blockEnd = Math.min(lines.length - 1, i + 2);
|
|
177
|
+
blocks.push(lines.slice(blockStart, blockEnd + 1).join('\n'));
|
|
178
|
+
inErrorBlock = false;
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
if (inErrorBlock) {
|
|
183
|
+
blocks.push(lines.slice(blockStart).join('\n'));
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
return blocks.length > 0 ? blocks.join('\n\n--- [Next Error Block] ---\n\n') : '';
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
function formatPrefix(isError, source) {
|
|
190
|
+
const d = new Date();
|
|
191
|
+
const ts = String(d.getHours()).padStart(2, '0') + ':' +
|
|
192
|
+
String(d.getMinutes()).padStart(2, '0') + ':' +
|
|
193
|
+
String(d.getSeconds()).padStart(2, '0');
|
|
194
|
+
|
|
195
|
+
const color = source === 'worker' ? '\x1b[36m' : '\x1b[35m';
|
|
196
|
+
const tag = source === 'worker' ? '[BACKEND]' : '[FRONTEND]';
|
|
197
|
+
return `\x1b[90m[${ts}]\x1b[0m ${color}${tag}\x1b[0m `;
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
function createStreamHandler(isError, source) {
|
|
201
|
+
let remainder = '';
|
|
202
|
+
return (d) => {
|
|
203
|
+
const chunk = remainder + d.toString();
|
|
204
|
+
const lines = chunk.split('\n');
|
|
205
|
+
remainder = lines.pop();
|
|
206
|
+
for (let line of lines) {
|
|
207
|
+
line = line.replace(/\x1b\[[23]J/g, '').replace(/\x1b\[[0-9;]*H/g, '').replace(/\x1bc/g, '');
|
|
208
|
+
line = line.replace(/\x1b\[[0-9]*[A-G]/g, '').replace(/\x1b\[[0-9]*[K]/g, '');
|
|
209
|
+
line = line.replace(/\r/g, '');
|
|
210
|
+
line = line.replace(/^\[[^\]]+\]\s*/, '');
|
|
211
|
+
|
|
212
|
+
if (line.trim() === '') continue;
|
|
213
|
+
|
|
214
|
+
const prefix = formatPrefix(isError, source);
|
|
215
|
+
const out = prefix + line + '\n';
|
|
216
|
+
|
|
217
|
+
if (isError) process.stderr.write(out);
|
|
218
|
+
else process.stdout.write(out);
|
|
219
|
+
buffer += out;
|
|
220
|
+
}
|
|
221
|
+
if (buffer.length > 1000000) {
|
|
222
|
+
const idx = buffer.indexOf('\n', buffer.length - 500000);
|
|
223
|
+
buffer = buffer.slice(idx === -1 ? -500000 : idx + 1);
|
|
224
|
+
}
|
|
225
|
+
};
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
function startWorker() {
|
|
229
|
+
workerChild = spawn('npx', ['concurrently', '-c', 'cyan', '-n', 'worker', '"npm run dev --prefix worker"'], {
|
|
230
|
+
env: { ...process.env, FORCE_COLOR: '1' },
|
|
231
|
+
stdio: ['ignore', 'pipe', 'pipe'],
|
|
232
|
+
shell: true
|
|
233
|
+
});
|
|
234
|
+
workerChild.stdout.on('data', createStreamHandler(false, 'worker'));
|
|
235
|
+
workerChild.stderr.on('data', createStreamHandler(true, 'worker'));
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
function startFrontend() {
|
|
239
|
+
frontendChild = spawn('npx', ['concurrently', '-c', 'magenta', '-n', 'frontend', '"npm run dev --prefix frontend"'], {
|
|
240
|
+
env: { ...process.env, FORCE_COLOR: '1' },
|
|
241
|
+
stdio: ['ignore', 'pipe', 'pipe'],
|
|
242
|
+
shell: true
|
|
243
|
+
});
|
|
244
|
+
frontendChild.stdout.on('data', createStreamHandler(false, 'frontend'));
|
|
245
|
+
frontendChild.stderr.on('data', createStreamHandler(true, 'frontend'));
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
function startServices() {
|
|
249
|
+
startWorker();
|
|
250
|
+
startFrontend();
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
console.clear();
|
|
254
|
+
updateLegend();
|
|
255
|
+
startLegendUpdater();
|
|
256
|
+
startServices();
|
|
257
|
+
|
|
258
|
+
function copyToClipboard(text) {
|
|
259
|
+
const platform = process.platform;
|
|
260
|
+
let cmd = '';
|
|
261
|
+
if (platform === 'darwin') cmd = 'pbcopy';
|
|
262
|
+
else if (platform === 'win32') cmd = 'clip';
|
|
263
|
+
else cmd = 'xclip -selection clipboard';
|
|
264
|
+
try {
|
|
265
|
+
execSync(cmd, { input: text });
|
|
266
|
+
return true;
|
|
267
|
+
} catch (e) {
|
|
268
|
+
return false;
|
|
269
|
+
}
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
if (process.stdin.isTTY) {
|
|
273
|
+
process.stdin.setRawMode(true);
|
|
274
|
+
process.stdin.resume();
|
|
275
|
+
process.stdin.setEncoding('utf8');
|
|
276
|
+
process.stdin.on('data', k => {
|
|
277
|
+
const now = Date.now();
|
|
278
|
+
if (k === 'c' || k === 'C') {
|
|
279
|
+
const clean = buffer.replace(/\x1B(?:[@-Z\-_]|\[[0-?]*[ -/]*[@-~])/g, '');
|
|
280
|
+
if (!clean.trim()) {
|
|
281
|
+
console.log('\n\x1b[33mโ ๏ธ Buffer is empty.\x1b[0m\n');
|
|
282
|
+
return;
|
|
283
|
+
}
|
|
284
|
+
if (copyToClipboard(clean)) console.log('\n\x1b[32m๐ Copied ALL logs to clipboard!\x1b[0m\n');
|
|
285
|
+
else console.log('\n\x1b[31mโ ๏ธ Failed to copy. Ensure xclip/pbcopy is installed.\x1b[0m\n');
|
|
286
|
+
} else if (k === 's' || k === 'S') {
|
|
287
|
+
const clean = buffer.replace(/\x1B(?:[@-Z\-_]|\[[0-?]*[ -/]*[@-~])/g, '');
|
|
288
|
+
const shortLines = clean.split('\n').slice(-500).join('\n');
|
|
289
|
+
if (copyToClipboard(shortLines)) console.log('\n\x1b[32m๐ Copied last 500 lines to clipboard!\x1b[0m\n');
|
|
290
|
+
else console.log('\n\x1b[31mโ ๏ธ Failed to copy.\x1b[0m\n');
|
|
291
|
+
} else if (k === 'w' || k === 'W') {
|
|
292
|
+
const clean = buffer.replace(/\x1B(?:[@-Z\-_]|\[[0-?]*[ -/]*[@-~])/g, '');
|
|
293
|
+
const workerLogs = clean.split('\n').filter(l => l.includes('[BACKEND]')).slice(-1000).join('\n');
|
|
294
|
+
if (copyToClipboard(workerLogs)) console.log('\n\x1b[32m๐ Copied BACKEND logs to clipboard!\x1b[0m\n');
|
|
295
|
+
else console.log('\n\x1b[31mโ ๏ธ Failed to copy.\x1b[0m\n');
|
|
296
|
+
} else if (k === 'f' || k === 'F') {
|
|
297
|
+
const clean = buffer.replace(/\x1B(?:[@-Z\-_]|\[[0-?]*[ -/]*[@-~])/g, '');
|
|
298
|
+
const frontendLogs = clean.split('\n').filter(l => l.includes('[FRONTEND]')).slice(-1000).join('\n');
|
|
299
|
+
if (copyToClipboard(frontendLogs)) console.log('\n\x1b[32m๐ Copied FRONTEND logs to clipboard!\x1b[0m\n');
|
|
300
|
+
else console.log('\n\x1b[31mโ ๏ธ Failed to copy.\x1b[0m\n');
|
|
301
|
+
} else if (k === 'e' || k === 'E') {
|
|
302
|
+
const clean = buffer.replace(/\x1B(?:[@-Z\-_]|\[[0-?]*[ -/]*[@-~])/g, '');
|
|
303
|
+
const errors = extractErrors(clean);
|
|
304
|
+
if (errors) {
|
|
305
|
+
if (copyToClipboard(errors)) console.log('\n\x1b[31m๐จ Errors copied to clipboard!\x1b[0m\n');
|
|
306
|
+
else console.log('\n\x1b[31mโ ๏ธ Failed to copy errors.\x1b[0m\n');
|
|
307
|
+
} else {
|
|
308
|
+
console.log('\n\x1b[32mโ
No errors detected.\x1b[0m\n');
|
|
309
|
+
}
|
|
310
|
+
} else if (k === 'r' || k === 'R') {
|
|
311
|
+
if (now - lastRestartTime < 1000) return;
|
|
312
|
+
lastRestartTime = now;
|
|
313
|
+
console.log('\n\x1b[33m๐ Restarting ALL services...\x1b[0m\n');
|
|
314
|
+
if (workerChild) try { process.kill(workerChild.pid, 'SIGKILL'); } catch(e) {}
|
|
315
|
+
if (frontendChild) try { process.kill(frontendChild.pid, 'SIGKILL'); } catch(e) {}
|
|
316
|
+
startServices();
|
|
317
|
+
} else if (k === '1') {
|
|
318
|
+
if (now - lastRestartTime < 1000) return;
|
|
319
|
+
lastRestartTime = now;
|
|
320
|
+
console.log('\n\x1b[33m๐ Restarting FRONTEND...\x1b[0m\n');
|
|
321
|
+
if (frontendChild) try { process.kill(frontendChild.pid, 'SIGKILL'); } catch(e) {}
|
|
322
|
+
startFrontend();
|
|
323
|
+
} else if (k === '2') {
|
|
324
|
+
if (now - lastRestartTime < 1000) return;
|
|
325
|
+
lastRestartTime = now;
|
|
326
|
+
console.log('\n\x1b[33m๐ Restarting BACKEND...\x1b[0m\n');
|
|
327
|
+
if (workerChild) try { process.kill(workerChild.pid, 'SIGKILL'); } catch(e) {}
|
|
328
|
+
startWorker();
|
|
329
|
+
} else if (k === 'x' || k === 'X') {
|
|
330
|
+
console.clear();
|
|
331
|
+
buffer = '';
|
|
332
|
+
lastRows = 0;
|
|
333
|
+
updateLegend();
|
|
334
|
+
console.log('\x1b[32m๐งน Terminal cleared.\x1b[0m\n');
|
|
335
|
+
} else if (k === '\u0003' || k === 'q' || k === 'Q') {
|
|
336
|
+
console.log('\n\x1b[33mStopping services...\x1b[0m');
|
|
337
|
+
cleanupLegend();
|
|
338
|
+
if (workerChild) try { process.kill(workerChild.pid, 'SIGKILL'); } catch(e) {}
|
|
339
|
+
if (frontendChild) try { process.kill(frontendChild.pid, 'SIGKILL'); } catch(e) {}
|
|
340
|
+
process.exit();
|
|
341
|
+
}
|
|
342
|
+
});
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
// Cleanup on unexpected exits
|
|
346
|
+
process.on('SIGINT', () => {
|
|
347
|
+
cleanupLegend();
|
|
348
|
+
if (workerChild) try { process.kill(workerChild.pid, 'SIGKILL'); } catch(e) {}
|
|
349
|
+
if (frontendChild) try { process.kill(frontendChild.pid, 'SIGKILL'); } catch(e) {}
|
|
350
|
+
process.exit();
|
|
351
|
+
});
|
|
352
|
+
process.on('exit', () => {
|
|
353
|
+
cleanupLegend();
|
|
354
|
+
if (workerChild) try { process.kill(workerChild.pid, 'SIGKILL'); } catch(e) {}
|
|
355
|
+
if (frontendChild) try { process.kill(frontendChild.pid, 'SIGKILL'); } catch(e) {}
|
|
356
|
+
});
|
|
357
|
+
EOF
|
|
358
|
+
|
|
359
|
+
node "$runnerPath"
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "haraps",
|
|
3
|
-
"version": "1.0.
|
|
3
|
+
"version": "1.0.6",
|
|
4
4
|
"description": "NPM package to scaffold standard development environment, deployment scripts, AGENTS.md guidelines, and core workflows for a Next.js frontend + Rust Cloudflare Worker backend.",
|
|
5
5
|
"main": "bin/index.js",
|
|
6
6
|
"bin": {
|