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.
- package/README.md +46 -0
- package/bin/index.js +429 -48
- 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
|
|
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 "
|
|
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
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
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(
|
|
178
|
+
process.stdout.write(\`\x1b[1;\${rows - 2}r\`);
|
|
102
179
|
lastRows = rows;
|
|
103
180
|
lastCols = cols;
|
|
104
181
|
}
|
|
105
|
-
|
|
106
|
-
const
|
|
107
|
-
const
|
|
108
|
-
|
|
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
|
-
|
|
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('
|
|
259
|
+
const lines = chunk.split('\n');
|
|
118
260
|
remainder = lines.pop();
|
|
119
261
|
for (let line of lines) {
|
|
120
|
-
line = line.replace(
|
|
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
|
-
|
|
123
|
-
const
|
|
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 >
|
|
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' },
|
|
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' },
|
|
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
|
-
|
|
149
|
-
|
|
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
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
if (
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
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
|
-
|
|
170
|
-
} else if (k === '
|
|
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 "
|
|
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 "
|
|
211
|
-
|
|
212
|
-
|
|
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
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
489
|
+
$GlobalReport = [PSCustomObject]@{
|
|
490
|
+
StartTime = (Get-Date)
|
|
491
|
+
Status = "Running"
|
|
492
|
+
Steps = @()
|
|
493
|
+
Error = $null
|
|
494
|
+
}
|
|
218
495
|
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
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
|
-
|
|
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
|
+
"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": {
|