haraps 1.0.3 β†’ 1.0.5

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.
Files changed (3) hide show
  1. package/README.md +46 -0
  2. package/bin/index.js +429 -48
  3. package/package.json +1 -1
package/README.md ADDED
@@ -0,0 +1,46 @@
1
+ # Haraps (Hara XY Project Scaffolder)
2
+
3
+ Haraps is a high-performance, automated project scaffolding CLI tailored for the **Hara XY ecosystem**. It rapidly provisions a new repository with everything an AI Coding Agent needs to execute at a senior engineering level.
4
+
5
+ With a single command, Haraps injects standardized development scripts, deployment workflows, and aggressive AI alignment rules into your repository.
6
+
7
+ ## Usage
8
+
9
+ In any new or empty repository, simply run:
10
+
11
+ ```bash
12
+ npx --yes haraps
13
+ ```
14
+
15
+ No installation or configuration is required. The CLI will dynamically generate the environment in the current working directory.
16
+
17
+ ## What it Generates
18
+
19
+ When you run Haraps, it creates the following structure:
20
+
21
+ ### 1. Dev & Deploy Scripts
22
+ - **`dev.sh` / `dev.ps1`**: Highly optimized local development runners that concurrently launch your Next.js frontend (port 3000) and Rust Cloudflare Worker backend (port 8787). Includes aggressive auto-cleanup for orphaned ports and a custom terminal dashboard.
23
+ - **`deploy.sh` / `deploy.ps1`**: Standardized production deployment scripts mapping the backend to Cloudflare Wrangler and the frontend to Vercel.
24
+
25
+ ### 2. High-Performance Engineering Standards
26
+ - **`AGENTS.md`**: Injects 20 hardcore engineering rules instructing the AI to prioritize edge-first execution, strict typing, zero-warning linting, security-first principles, and accessibility.
27
+
28
+ ### 3. Start Project Protocol
29
+ When you open your Coding Agent in a newly scaffolded directory and type **"start project"**, the agent is hardcoded to:
30
+ 1. Ask you for the project name and description.
31
+ 2. Initialize the Next.js frontend and Rust backend adhering to your high-performance standards.
32
+ 3. Automatically initialize a new Git repository and make the first commit.
33
+
34
+ ### 4. Specialized Agent Skills & Workflows
35
+ - **Ponytail (Lazy Senior Dev Mode)**: Enforces "the best code is the code never written", mandating deletion over addition and standard library usage over new dependencies.
36
+ - **Security Review**: Equips the agent to reason like a security researcher, scanning for deep data flow vulnerabilities before deploying.
37
+ - **Kill AI Slop**: Strips out clichΓ© AI-generated aesthetics (e.g., indigo gradients, excessive emojis) in favor of clean, accessible UI.
38
+ - **UI Sniper Telegram**: Sets up a webhook-based UI feedback loop straight to a Telegram group.
39
+ - **Workflows**: Includes `git-it` (safe atomic rebasing and pushing) and `deploy-and-talk` (deployment with Telegram notifications).
40
+
41
+ ## Publishing Updates
42
+
43
+ To publish an update to the Haraps CLI:
44
+ 1. Update the scaffolding logic in `bin/index.js`.
45
+ 2. Bump the version in `package.json`.
46
+ 3. Run `npm publish` in the package directory.
package/bin/index.js CHANGED
@@ -50,24 +50,98 @@ fs.chmodSync(path.resolve(process.cwd(), 'dev.sh'), '755');
50
50
 
51
51
  writeFile('dev.ps1', `
52
52
  # =============================================
53
- # Local Development Runner (Rust + Next.js)
53
+ # Project Local Development Runner
54
54
  # =============================================
55
+ # Starts the Backend and Next.js frontend
56
+ # using concurrently in a single terminal window.
55
57
 
58
+ Write-Host ""
59
+ Write-Host "=============================================" -ForegroundColor Cyan
60
+ Write-Host " πŸš€ Project Local Dev Server" -ForegroundColor Cyan
56
61
  Write-Host "=============================================" -ForegroundColor Cyan
57
- Write-Host " πŸš€ Local Dev Server (Rust + Next.js)" -ForegroundColor Cyan
62
+ Write-Host ""
63
+ Write-Host " Backend β†’ http://localhost:8787" -ForegroundColor Green
64
+ Write-Host " Frontend β†’ http://localhost:3000" -ForegroundColor Blue
65
+ Write-Host ""
66
+ Write-Host " Starting unified development environment..." -ForegroundColor Yellow
58
67
  Write-Host "=============================================" -ForegroundColor Cyan
68
+ Write-Host ""
69
+
70
+ # Cleanup zombie worker processes and ports to prevent conflicts
71
+ Write-Host " Performing aggressive process cleanup..." -ForegroundColor Gray
72
+
73
+ # Define processes to kill
74
+ $procNames = @("workerd", "node", "concurrently")
75
+
76
+ foreach ($procName in $procNames) {
77
+ $procs = Get-Process $procName -ErrorAction SilentlyContinue
78
+ if ($procs) {
79
+ Write-Host " Stopping $procName ($($procs.Count) instances)..." -ForegroundColor Yellow
80
+ $procs | Stop-Process -Force -ErrorAction SilentlyContinue
81
+ }
82
+ }
83
+
84
+ # Active verification loop to ensure all target processes have fully exited
85
+ $maxWaitSeconds = 5
86
+ $stopwatch = [System.Diagnostics.Stopwatch]::StartNew()
87
+ while ($stopwatch.Elapsed.TotalSeconds -lt $maxWaitSeconds) {
88
+ $remainingProcs = Get-Process $procNames -ErrorAction SilentlyContinue
89
+ if (-not $remainingProcs) {
90
+ break
91
+ }
92
+ Start-Sleep -Milliseconds 200
93
+ }
94
+
95
+ # Function to robustly remove a directory with retry logic
96
+ function Remove-CacheDirectory($path, $label) {
97
+ if (Test-Path $path) {
98
+ Write-Host " Purging $label cache ($path)..." -ForegroundColor Gray
99
+ $attempts = 0
100
+ $maxAttempts = 5
101
+ $success = $false
102
+
103
+ while ($attempts -lt $maxAttempts -and -not $success) {
104
+ try {
105
+ Remove-Item -Recurse -Force $path -ErrorAction Stop
106
+ $success = $true
107
+ } catch {
108
+ $attempts++
109
+ if ($attempts -lt $maxAttempts) {
110
+ Write-Host " Lock detected on $path. Retrying ($attempts/$maxAttempts)..." -ForegroundColor DarkYellow
111
+ Start-Sleep -Milliseconds (300 * $attempts)
112
+ } else {
113
+ Write-Host " Warning: Could not fully purge $path. Some files may be locked." -ForegroundColor Red
114
+ }
115
+ }
116
+ }
117
+ }
118
+ }
119
+
120
+ # Hard clear of wrangler state and Next.js cache
121
+ Remove-CacheDirectory "backend/.wrangler" "wrangler state"
122
+ Remove-CacheDirectory "frontend/.next" "frontend Turbopack cache"
59
123
 
60
- # Aggressive cleanup of ports 3000 and 8787
61
124
  $ports = @(8787, 3000)
62
125
  foreach ($port in $ports) {
63
- $netstatOutput = netstat -ano | Select-String ":$port\\s+.*LISTENING\\s+(\\d+)"
64
- if ($netstatOutput) {
65
- $targetPid = $netstatOutput.Matches[0].Groups[1].Value
66
- Write-Host " Freeing port $port (PID $targetPid)..." -ForegroundColor Yellow
67
- Stop-Process -Id $targetPid -Force -ErrorAction SilentlyContinue
126
+ Write-Host " Ensuring port $port is free..." -ForegroundColor Gray
127
+ $retryCount = 0
128
+ while ($retryCount -lt 5) {
129
+ $netstatOutput = netstat -ano | Select-String ":$port\s+.*LISTENING\s+(\d+)"
130
+ if ($netstatOutput) {
131
+ $targetPid = $netstatOutput.Matches[0].Groups[1].Value
132
+ Write-Host " Port $port occupied by PID $targetPid. Terminating..." -ForegroundColor Red
133
+ Stop-Process -Id $targetPid -Force -ErrorAction SilentlyContinue
134
+ Start-Sleep -Seconds 1
135
+ $retryCount++
136
+ } else {
137
+ break
138
+ }
68
139
  }
69
140
  }
70
141
 
142
+ $env:WRANGLER_SEND_METRICS = "false"
143
+ $env:NODE_OPTIONS = "--max-old-space-size=4096"
144
+
71
145
  Write-Host "Starting development environment..." -ForegroundColor Green
72
146
  $runnerScript = @'
73
147
  const { spawn, execSync } = require('child_process');
@@ -87,6 +161,8 @@ function formatUptime() {
87
161
  const elapsed = Math.floor((Date.now() - startTime) / 1000);
88
162
  const m = Math.floor(elapsed / 60).toString().padStart(2, '0');
89
163
  const s = (elapsed % 60).toString().padStart(2, '0');
164
+ const h = Math.floor(elapsed / 3600);
165
+ if (h > 0) return \`\${h}h \${m}m \${s}s\`;
90
166
  return \`\${m}m \${s}s\`;
91
167
  }
92
168
 
@@ -97,41 +173,119 @@ function updateLegend() {
97
173
  if (!process.stdout.isTTY) return;
98
174
  const rows = process.stdout.rows || 30;
99
175
  const cols = process.stdout.columns || 80;
176
+
100
177
  if (rows !== lastRows || cols !== lastCols) {
101
- process.stdout.write(\`\\x1b[1;\${rows - 2}r\`);
178
+ process.stdout.write(\`\x1b[1;\${rows - 2}r\`);
102
179
  lastRows = rows;
103
180
  lastCols = cols;
104
181
  }
105
- const lineCount = (buffer.match(/\\n/g) || []).length;
106
- const legend1 = \` [Copy] 'c': All (\${lineCount}L) | 's': 500L | 'w': Backend | 'f': Frontend | 'e': Errors \`;
107
- const legend2 = \` [System] Uptime: \${formatUptime()} | 'x': Clear | 'r': Restart ALL | 'q': Quit \`;
108
- process.stdout.write(\`\\x1b[s\\x1b[\${rows - 1};1H\\x1b[42m\\x1b[30m\${legend1.padEnd(cols)}\\x1b[0m\\x1b[\${rows};1H\\x1b[42m\\x1b[30m\${legend2.padEnd(cols)}\\x1b[0m\\x1b[u\`);
182
+
183
+ const lineCount = (buffer.match(/\n/g) || []).length;
184
+ const sizeStr = formatSize(Buffer.byteLength(buffer, 'utf8'));
185
+ const uptimeStr = formatUptime();
186
+ const legend1 = \` [Copy] 'c': All (\${lineCount}L / \${sizeStr}) | 's': 500L | 'w': Backend | 'f': Frontend | 'e': Errors \`;
187
+ const legend2 = \` [System] Uptime: \${uptimeStr} | 'x': Clear | 'r': Restart ALL | '1': Frontend | '2': Backend | 'q': Quit | 'Q': Clean & Quit \`;
188
+
189
+ process.stdout.write(\`\x1b[s\`);
190
+ process.stdout.write(\`\x1b[\${rows - 1};1H\x1b[42m\x1b[30m\${legend1.padEnd(cols)}\x1b[0m\`);
191
+ process.stdout.write(\`\x1b[\${rows};1H\x1b[42m\x1b[30m\${legend2.padEnd(cols)}\x1b[0m\`);
192
+ process.stdout.write(\`\x1b[u\`);
193
+ }
194
+
195
+ let legendInterval;
196
+ function startLegendUpdater() {
197
+ if (legendInterval) clearInterval(legendInterval);
198
+ legendInterval = setInterval(() => {
199
+ updateLegend();
200
+ }, 1000);
201
+ }
202
+
203
+ function cleanupLegend() {
204
+ if (legendInterval) clearInterval(legendInterval);
205
+ if (!process.stdout.isTTY) return;
206
+ process.stdout.write('\x1b[r');
207
+ }
208
+
209
+ process.stdout.on('resize', () => {
210
+ if (!process.stdout.isTTY) return;
211
+ cleanupLegend();
212
+ console.clear();
213
+ lastRows = 0;
214
+ updateLegend();
215
+ });
216
+
217
+ function extractErrors(text) {
218
+ const lines = text.split('\n');
219
+ const errorPattern = /(error|exception|fail|β¨―|βœ—|traceback)/i;
220
+ let inErrorBlock = false;
221
+ let blockStart = 0;
222
+ const blocks = [];
223
+
224
+ for (let i = 0; i < lines.length; i++) {
225
+ if (errorPattern.test(lines[i]) || (inErrorBlock && lines[i].trim().startsWith('at '))) {
226
+ if (!inErrorBlock) {
227
+ inErrorBlock = true;
228
+ blockStart = Math.max(0, i - 2);
229
+ }
230
+ } else {
231
+ if (inErrorBlock) {
232
+ if (lines[i].trim() === '') continue;
233
+ const blockEnd = Math.min(lines.length - 1, i + 2);
234
+ blocks.push(lines.slice(blockStart, blockEnd + 1).join('\n'));
235
+ inErrorBlock = false;
236
+ }
237
+ }
238
+ }
239
+ if (inErrorBlock) {
240
+ blocks.push(lines.slice(blockStart).join('\n'));
241
+ }
242
+ return blocks.length > 0 ? blocks.join('\n\n--- [Next Error Block] ---\n\n') : '';
109
243
  }
110
244
 
111
- setInterval(updateLegend, 1000);
245
+ function formatPrefix(isError, source) {
246
+ const d = new Date();
247
+ const ts = String(d.getHours()).padStart(2, '0') + ':' +
248
+ String(d.getMinutes()).padStart(2, '0') + ':' +
249
+ String(d.getSeconds()).padStart(2, '0');
250
+ const color = source === 'worker' ? '\x1b[36m' : '\x1b[35m';
251
+ const tag = source === 'worker' ? '[BACKEND]' : '[FRONTEND]';
252
+ return \`\x1b[90m[\${ts}]\x1b[0m \${color}\${tag}\x1b[0m \`;
253
+ }
112
254
 
113
255
  function createStreamHandler(isError, source) {
114
256
  let remainder = '';
115
257
  return (d) => {
116
258
  const chunk = remainder + d.toString();
117
- const lines = chunk.split('\\n');
259
+ const lines = chunk.split('\n');
118
260
  remainder = lines.pop();
119
261
  for (let line of lines) {
120
- line = line.replace(/\\x1b\\[[23]J/g, '').replace(/\\r/g, '').replace(/^\\[[^\\]]+\\]\\s*/, '');
262
+ line = line.replace(/\x1b\[[23]J/g, '').replace(/\x1b\[[0-9;]*H/g, '').replace(/\x1bc/g, '');
263
+ line = line.replace(/\x1b\[[0-9]*[A-G]/g, '').replace(/\x1b\[[0-9]*[K]/g, '');
264
+ line = line.replace(/\r/g, '');
265
+ line = line.replace(/^\[[^\]]+\]\s*/, '');
266
+
121
267
  if (line.trim() === '') continue;
122
- const tag = source === 'worker' ? '\\x1b[36m[BACKEND]\\x1b[0m' : '\\x1b[35m[FRONTEND]\\x1b[0m';
123
- const out = \`\${tag} \${line}\\n\`;
268
+
269
+ const prefix = formatPrefix(isError, source);
270
+ const out = prefix + line + '\n';
271
+
124
272
  if (isError) process.stderr.write(out);
125
273
  else process.stdout.write(out);
126
274
  buffer += out;
127
275
  }
128
- if (buffer.length > 500000) buffer = buffer.slice(-250000);
276
+ if (buffer.length > 1000000) {
277
+ const idx = buffer.indexOf('\n', buffer.length - 500000);
278
+ buffer = buffer.slice(idx === -1 ? -500000 : idx + 1);
279
+ }
129
280
  };
130
281
  }
131
282
 
132
283
  function startWorker() {
133
284
  workerChild = spawn('npx.cmd', ['concurrently', '-c', 'cyan', '-n', 'worker', '"cd backend && npx wrangler dev"'], {
134
- env: { ...process.env, FORCE_COLOR: '1' }, stdio: ['ignore', 'pipe', 'pipe'], windowsHide: true, shell: true
285
+ env: { ...process.env, FORCE_COLOR: '1' },
286
+ stdio: ['ignore', 'pipe', 'pipe'],
287
+ windowsHide: true,
288
+ shell: true
135
289
  });
136
290
  workerChild.stdout.on('data', createStreamHandler(false, 'worker'));
137
291
  workerChild.stderr.on('data', createStreamHandler(true, 'worker'));
@@ -139,43 +293,132 @@ function startWorker() {
139
293
 
140
294
  function startFrontend() {
141
295
  frontendChild = spawn('npx.cmd', ['concurrently', '-c', 'magenta', '-n', 'frontend', '"cd frontend && npm run dev"'], {
142
- env: { ...process.env, FORCE_COLOR: '1' }, stdio: ['ignore', 'pipe', 'pipe'], windowsHide: true, shell: true
296
+ env: { ...process.env, FORCE_COLOR: '1' },
297
+ stdio: ['ignore', 'pipe', 'pipe'],
298
+ windowsHide: true,
299
+ shell: true
143
300
  });
144
301
  frontendChild.stdout.on('data', createStreamHandler(false, 'frontend'));
145
302
  frontendChild.stderr.on('data', createStreamHandler(true, 'frontend'));
146
303
  }
147
304
 
148
- startWorker();
149
- startFrontend();
305
+ function startServices() {
306
+ startWorker();
307
+ startFrontend();
308
+ }
309
+
150
310
  console.clear();
151
311
  updateLegend();
312
+ startLegendUpdater();
313
+ startServices();
152
314
 
153
315
  if (process.stdin.isTTY) {
154
316
  process.stdin.setRawMode(true);
155
317
  process.stdin.resume();
156
318
  process.stdin.setEncoding('utf8');
157
319
  process.stdin.on('data', k => {
158
- if (k === 'c' || k === 's' || k === 'w' || k === 'f' || k === 'e') {
159
- const clean = buffer.replace(/\\x1B(?:[@-Z\\-_]|\\[[0-?]*[ -/]*[@-~])/g, '');
160
- let out = clean;
161
- if (k === 's') out = clean.split('\\n').slice(-500).join('\\n');
162
- if (k === 'w') out = clean.split('\\n').filter(l => l.includes('[BACKEND]')).slice(-1000).join('\\n');
163
- if (k === 'f') out = clean.split('\\n').filter(l => l.includes('[FRONTEND]')).slice(-1000).join('\\n');
164
- try { execSync('clip', { input: out }); console.log('\\n\\x1b[32mπŸ“‹ Copied to clipboard!\\x1b[0m\\n'); } catch (e) {}
165
- } else if (k === 'r') {
166
- console.log('\\n\\x1b[33mπŸ”„ Restarting...\\x1b[0m\\n');
320
+ const now = Date.now();
321
+ if (k === 'c' || k === 'C') {
322
+ const clean = buffer.replace(/\x1B(?:[@-Z\-_]|\[[0-?]*[ -/]*[@-~])/g, '');
323
+ if (!clean.trim()) {
324
+ console.log('\n\x1b[33m⚠️ Buffer is empty.\x1b[0m\n');
325
+ return;
326
+ }
327
+ try {
328
+ execSync('clip', { input: clean });
329
+ console.log('\n\x1b[32mπŸ“‹ Copied ALL logs to clipboard!\x1b[0m\n');
330
+ } catch (e) {}
331
+ } else if (k === 's' || k === 'S') {
332
+ const clean = buffer.replace(/\x1B(?:[@-Z\-_]|\[[0-?]*[ -/]*[@-~])/g, '');
333
+ const shortLines = clean.split('\n').slice(-500).join('\n');
334
+ try {
335
+ execSync('clip', { input: shortLines });
336
+ console.log('\n\x1b[32mπŸ“‹ Copied last 500 lines to clipboard!\x1b[0m\n');
337
+ } catch (e) {}
338
+ } else if (k === 'w' || k === 'W') {
339
+ const clean = buffer.replace(/\x1B(?:[@-Z\-_]|\[[0-?]*[ -/]*[@-~])/g, '');
340
+ const workerLogs = clean.split('\n').filter(l => l.includes('[BACKEND]')).slice(-1000).join('\n');
341
+ try {
342
+ execSync('clip', { input: workerLogs });
343
+ console.log('\n\x1b[32mπŸ“‹ Copied BACKEND logs to clipboard!\x1b[0m\n');
344
+ } catch (e) {}
345
+ } else if (k === 'f' || k === 'F') {
346
+ const clean = buffer.replace(/\x1B(?:[@-Z\-_]|\[[0-?]*[ -/]*[@-~])/g, '');
347
+ const frontendLogs = clean.split('\n').filter(l => l.includes('[FRONTEND]')).slice(-1000).join('\n');
348
+ try {
349
+ execSync('clip', { input: frontendLogs });
350
+ console.log('\n\x1b[32mπŸ“‹ Copied FRONTEND logs to clipboard!\x1b[0m\n');
351
+ } catch (e) {}
352
+ } else if (k === 'e' || k === 'E') {
353
+ const clean = buffer.replace(/\x1B(?:[@-Z\-_]|\[[0-?]*[ -/]*[@-~])/g, '');
354
+ const errors = extractErrors(clean);
355
+ if (errors) {
356
+ try {
357
+ execSync('clip', { input: errors });
358
+ console.log('\n\x1b[31m🚨 Errors copied to clipboard!\x1b[0m\n');
359
+ } catch (e) {}
360
+ } else {
361
+ console.log('\n\x1b[32mβœ… No errors detected.\x1b[0m\n');
362
+ }
363
+ } else if (k === 'r' || k === 'R') {
364
+ if (now - lastRestartTime < 1000) return;
365
+ lastRestartTime = now;
366
+ console.log('\n\x1b[33mπŸ”„ Restarting ALL services...\x1b[0m\n');
367
+ if (workerChild) try { execSync('taskkill /pid ' + workerChild.pid + ' /t /f', { stdio: 'ignore' }); } catch(e) {}
368
+ if (frontendChild) try { execSync('taskkill /pid ' + frontendChild.pid + ' /t /f', { stdio: 'ignore' }); } catch(e) {}
369
+ startServices();
370
+ } else if (k === '1') {
371
+ if (now - lastRestartTime < 1000) return;
372
+ lastRestartTime = now;
373
+ console.log('\n\x1b[33mπŸ”„ Restarting FRONTEND...\x1b[0m\n');
374
+ if (frontendChild) try { execSync('taskkill /pid ' + frontendChild.pid + ' /t /f', { stdio: 'ignore' }); } catch(e) {}
375
+ startFrontend();
376
+ } else if (k === '2') {
377
+ if (now - lastRestartTime < 1000) return;
378
+ lastRestartTime = now;
379
+ console.log('\n\x1b[33mπŸ”„ Restarting BACKEND...\x1b[0m\n');
380
+ if (workerChild) try { execSync('taskkill /pid ' + workerChild.pid + ' /t /f', { stdio: 'ignore' }); } catch(e) {}
381
+ startWorker();
382
+ } else if (k === 'x' || k === 'X') {
383
+ console.clear();
384
+ buffer = '';
385
+ lastRows = 0;
386
+ updateLegend();
387
+ console.log('\x1b[32m🧹 Terminal cleared.\x1b[0m\n');
388
+ } else if (k === '\u0003' || k === 'q') {
389
+ console.log('\n\x1b[33mStopping services...\x1b[0m');
390
+ cleanupLegend();
167
391
  if (workerChild) try { execSync('taskkill /pid ' + workerChild.pid + ' /t /f', { stdio: 'ignore' }); } catch(e) {}
168
392
  if (frontendChild) try { execSync('taskkill /pid ' + frontendChild.pid + ' /t /f', { stdio: 'ignore' }); } catch(e) {}
169
- startWorker(); startFrontend();
170
- } else if (k === '\\u0003' || k === 'q') {
393
+ process.exit();
394
+ } else if (k === 'Q') {
395
+ console.log('\n\x1b[31m🧹 Cleaning cache and dependencies, then stopping services...\x1b[0m');
396
+ cleanupLegend();
171
397
  if (workerChild) try { execSync('taskkill /pid ' + workerChild.pid + ' /t /f', { stdio: 'ignore' }); } catch(e) {}
172
398
  if (frontendChild) try { execSync('taskkill /pid ' + frontendChild.pid + ' /t /f', { stdio: 'ignore' }); } catch(e) {}
399
+
400
+ console.log('\x1b[33mRemoving node_modules, .next, and .wrangler...\x1b[0m');
401
+ const fs = require('fs');
402
+ const pathsToClean = ['frontend/.next', 'backend/.wrangler', 'frontend/node_modules', 'backend/node_modules', 'node_modules'];
403
+ for (const p of pathsToClean) {
404
+ try { fs.rmSync(p, { recursive: true, force: true }); } catch(e) {}
405
+ }
406
+ console.log('\x1b[32mCleanup complete.\x1b[0m');
173
407
  process.exit();
174
408
  }
175
409
  });
176
410
  }
411
+
412
+ process.on('SIGINT', () => {
413
+ cleanupLegend();
414
+ process.exit();
415
+ });
416
+ process.on('exit', () => {
417
+ cleanupLegend();
418
+ console.clear();
419
+ });
177
420
  '@
178
- $runnerPath = Join-Path $env:TEMP "mediqueue-dev-runner.js"
421
+ $runnerPath = Join-Path $env:TEMP "project-dev-runner.js"
179
422
  Set-Content -Path $runnerPath -Value $runnerScript -Encoding UTF8
180
423
  node $runnerPath
181
424
  `);
@@ -205,24 +448,162 @@ echo -e "\\e[32mDeployment Complete!\\e[0m"
205
448
  fs.chmodSync(path.resolve(process.cwd(), 'deploy.sh'), '755');
206
449
 
207
450
  writeFile('deploy.ps1', `
451
+ param(
452
+ [switch]$SkipFrontend = $false,
453
+ [switch]$SkipBackend = $false,
454
+ [switch]$Fast = $false
455
+ )
456
+
457
+ if ($Fast) {
458
+ Write-Host "[FAST] Fast mode enabled." -ForegroundColor Cyan
459
+ }
460
+
208
461
  $ErrorActionPreference = 'Stop'
462
+ Set-Location $PSScriptRoot
463
+
464
+ # Load .env files if present
465
+ $envFiles = @(".env", ".env.local", "frontend\\.env.local")
466
+ foreach ($file in $envFiles) {
467
+ $envFile = Join-Path $PSScriptRoot $file
468
+ if (Test-Path $envFile) {
469
+ Write-Host "[RESILIENCE] Loading environment variables from $file..." -ForegroundColor Cyan
470
+ Get-Content $envFile | Where-Object { $_ -match '^\s*([^#\s][^=]+)=(.*)' } | ForEach-Object {
471
+ $name = $matches[1].Trim()
472
+ $value = $matches[2].Trim()
473
+ if ($value -match '^"(.*)"$') { $value = $matches[1] }
474
+ elseif ($value -match "^'(.*)'$") { $value = $matches[1] }
475
+ [Environment]::SetEnvironmentVariable($name, $value, "Process")
476
+ }
477
+ }
478
+ }
209
479
 
210
- Write-Host "=============================================" -ForegroundColor Cyan
211
- Write-Host " πŸš€ Deploying System (Rust + Next.js)" -ForegroundColor Cyan
212
- Write-Host "=============================================" -ForegroundColor Cyan
480
+ Write-Host "[RESILIENCE] Terminating conflicting processes to prevent locks..." -ForegroundColor Yellow
481
+ $conflictingProcs = Get-WmiObject Win32_Process | Where-Object {
482
+ $_.Name -match 'node.exe' -and
483
+ ($_.CommandLine -match 'next build' -or $_.CommandLine -match 'tsc' -or $_.CommandLine -match 'wrangler deploy')
484
+ }
485
+ foreach ($p in $conflictingProcs) {
486
+ Stop-Process -Id $p.ProcessId -Force -ErrorAction SilentlyContinue
487
+ }
213
488
 
214
- Write-Host "[1/2] Deploying Rust Cloudflare Worker..." -ForegroundColor Green
215
- Set-Location backend
216
- npx wrangler deploy
217
- Set-Location ..
489
+ $GlobalReport = [PSCustomObject]@{
490
+ StartTime = (Get-Date)
491
+ Status = "Running"
492
+ Steps = @()
493
+ Error = $null
494
+ }
218
495
 
219
- Write-Host "[2/2] Deploying Next.js Frontend..." -ForegroundColor Blue
220
- Set-Location frontend
221
- npm run build
222
- npx vercel deploy --prod
223
- Set-Location ..
496
+ $ReportsDir = Join-Path $PSScriptRoot "deployment_reports"
497
+ if (!(Test-Path $ReportsDir)) { New-Item -ItemType Directory -Path $ReportsDir | Out-Null }
498
+
499
+ function Add-Step {
500
+ param([string]$Name, [string]$Status, [string]$Details = "")
501
+ $GlobalReport.Steps += [PSCustomObject]@{
502
+ Name = $Name
503
+ Status = $Status
504
+ Details = $Details
505
+ Time = (Get-Date).ToString("T")
506
+ }
507
+ }
508
+
509
+ function Generate-FinalReport {
510
+ param([string]$FinalStatus)
511
+ $GlobalReport.Status = $FinalStatus
512
+
513
+ $reportText = "=================================================\`n"
514
+ $reportText += "PROJECT DEPLOYMENT REPORT\`n"
515
+ $reportText += "=================================================\`n"
516
+ $reportText += "Status: $FinalStatus\`n"
517
+ $reportText += "Duration: $((Get-Date) - $GlobalReport.StartTime)\`n"
518
+ $reportText += "-------------------------------------------------\`n"
519
+
520
+ foreach ($step in $GlobalReport.Steps) {
521
+ $indicator = if ($step.Status -eq "SUCCESS") { "[OK]" } else { "[FAIL]" }
522
+ $reportText += "$indicator [$($step.Time)] $($step.Name): $($step.Status)\`n"
523
+ if ($step.Details) { $reportText += " > $($step.Details)\`n" }
524
+ }
525
+
526
+ if ($GlobalReport.Error) {
527
+ $reportText += "-------------------------------------------------\`n"
528
+ $reportText += "CRITICAL ERROR:\`n$($GlobalReport.Error)\`n"
529
+ }
530
+ $reportText += "=================================================\`n"
531
+
532
+ $statusIndicator = if ($FinalStatus -eq "SUCCESS") { "SUCCESS" } else { "FAILED" }
533
+ $humanTime = (Get-Date).ToString("ddMMM_hh-mmtt")
534
+ $fileName = "Deploy_\${statusIndicator}_\${humanTime}.txt"
535
+ $filePath = Join-Path $ReportsDir $fileName
536
+ $reportText | Out-File -FilePath $filePath
537
+
538
+ try { $reportText | Set-Clipboard } catch {}
539
+ return $reportText
540
+ }
541
+
542
+ function Invoke-ModernCommand {
543
+ param([string]$Step, [scriptblock]$Script, [switch]$NoExit)
544
+ Write-Host ""
545
+ Write-Host "[STEP] $($Step)..." -ForegroundColor Yellow
546
+ try {
547
+ $global:LASTEXITCODE = 0
548
+ & $Script
549
+ $exitCode = $LASTEXITCODE
550
+
551
+ if ($exitCode -eq 0) {
552
+ Add-Step $Step "SUCCESS"
553
+ Write-Host "[OK] $Step completed successfully." -ForegroundColor Green
554
+ } else {
555
+ throw "Command failed with exit code $exitCode."
556
+ }
557
+ } catch {
558
+ $errorMsg = $_.ToString()
559
+ Add-Step $Step "FAILED" "Critical Error: See Global Report"
560
+ $GlobalReport.Error = "Step: $Step\`nError: $errorMsg"
561
+
562
+ if ($NoExit) {
563
+ Write-Host "[WARN] $Step failed, but continuing." -ForegroundColor Yellow
564
+ throw $errorMsg
565
+ }
566
+
567
+ $finalReport = Generate-FinalReport "FAILED"
568
+ Write-Host "\`nDEPLOYMENT FAILED!\`n$errorMsg" -ForegroundColor Red
569
+ Write-Host "[CLIPBOARD] Full error report copied to your clipboard!" -ForegroundColor Cyan
570
+
571
+ Set-Location $PSScriptRoot
572
+ exit 1
573
+ }
574
+ }
575
+
576
+ if (!$SkipBackend) {
577
+ try {
578
+ Push-Location backend
579
+ Invoke-ModernCommand "Rust Backend Deployment" { npx wrangler deploy }
580
+ } finally {
581
+ Pop-Location
582
+ }
583
+ } else {
584
+ Add-Step "Backend Deployment" "SKIPPED"
585
+ }
586
+
587
+ if (!$SkipFrontend) {
588
+ try {
589
+ Push-Location frontend
590
+ $env:NODE_OPTIONS = "--max-old-space-size=8192"
591
+ $lockFile = Join-Path (Get-Location) ".next\lock"
592
+ if (Test-Path $lockFile) { Remove-Item -Force $lockFile -ErrorAction SilentlyContinue }
593
+
594
+ Invoke-ModernCommand "Frontend Build" { npm run build }
595
+ Invoke-ModernCommand "Frontend Vercel Deploy" { npx vercel deploy --prod }
596
+ } finally {
597
+ Pop-Location
598
+ }
599
+ } else {
600
+ Add-Step "Frontend Deployment" "SKIPPED"
601
+ }
224
602
 
225
- Write-Host "Deployment Complete!" -ForegroundColor Green
603
+ $finalReport = Generate-FinalReport "SUCCESS"
604
+ Write-Host "\`nDEPLOYMENT SUCCESSFUL!" -ForegroundColor Green
605
+ Write-Host $finalReport
606
+ Write-Host "[CLIPBOARD] Full report copied to your clipboard!" -ForegroundColor Cyan
226
607
  `);
227
608
 
228
609
  // 3. Agent Rules
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "haraps",
3
- "version": "1.0.3",
3
+ "version": "1.0.5",
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": {