apero-kit-cli 1.4.0 → 1.4.2

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.
@@ -1,13 +1,14 @@
1
1
  #!/usr/bin/env node
2
2
  /**
3
- * Plan Preview Server
3
+ * Plan Preview Server with Edit Support
4
4
  *
5
5
  * Usage: node .claude/scripts/plan-preview.cjs <plan-path> [--port=3456]
6
6
  *
7
- * Opens a local web server to preview plans with:
7
+ * Opens a local web server to preview and edit plans with:
8
8
  * - Rendered markdown with syntax highlighting
9
9
  * - Navigation sidebar for phases
10
- * - Auto-refresh on file changes
10
+ * - Edit mode with live preview
11
+ * - Auto-save functionality
11
12
  */
12
13
 
13
14
  const http = require('http');
@@ -113,16 +114,18 @@ function getPlanFiles(dir) {
113
114
  });
114
115
  }
115
116
 
116
- // Generate HTML page
117
- function generatePage(files, currentFile) {
117
+ // Generate HTML page with edit support
118
+ function generatePage(files, currentFile, mode = 'preview') {
118
119
  const file = files.find(f => f.name === currentFile) || files[0];
119
120
  const content = file ? fs.readFileSync(file.path, 'utf-8') : '# No plan found';
120
121
  const htmlContent = markdownToHtml(content);
122
+ const isEditMode = mode === 'edit';
123
+ const currentFileName = currentFile || files[0]?.name || '';
121
124
 
122
125
  const nav = files.map(f => {
123
126
  const isActive = f.name === (currentFile || files[0]?.name);
124
127
  const icon = f.isMain ? 'šŸ“‹' : f.isPhase ? 'šŸ“Œ' : f.category === 'research' ? 'šŸ”¬' : f.category === 'reports' ? 'šŸ“„' : 'šŸ“';
125
- return `<a href="?file=${encodeURIComponent(f.name)}" class="${isActive ? 'active' : ''}">${icon} ${f.name}</a>`;
128
+ return `<a href="?file=${encodeURIComponent(f.name)}&mode=${mode}" class="${isActive ? 'active' : ''}">${icon} ${f.name}</a>`;
126
129
  }).join('\n');
127
130
 
128
131
  return `<!DOCTYPE html>
@@ -135,6 +138,7 @@ function generatePage(files, currentFile) {
135
138
  :root {
136
139
  --bg: #0d1117;
137
140
  --bg-secondary: #161b22;
141
+ --bg-tertiary: #21262d;
138
142
  --text: #c9d1d9;
139
143
  --text-muted: #8b949e;
140
144
  --accent: #58a6ff;
@@ -142,6 +146,9 @@ function generatePage(files, currentFile) {
142
146
  --code-bg: #1f2428;
143
147
  --success: #3fb950;
144
148
  --warning: #d29922;
149
+ --error: #f85149;
150
+ --gradient-1: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
151
+ --gradient-2: linear-gradient(135deg, #3fb950 0%, #2ea043 100%);
145
152
  }
146
153
 
147
154
  * { box-sizing: border-box; margin: 0; padding: 0; }
@@ -197,12 +204,48 @@ function generatePage(files, currentFile) {
197
204
  color: var(--accent);
198
205
  }
199
206
 
207
+ /* Mode Toggle */
208
+ .mode-toggle {
209
+ display: flex;
210
+ background: var(--bg-tertiary);
211
+ border-radius: 20px;
212
+ padding: 4px;
213
+ margin-bottom: 20px;
214
+ }
215
+
216
+ .mode-toggle a {
217
+ flex: 1;
218
+ text-align: center;
219
+ padding: 8px 16px;
220
+ border-radius: 16px;
221
+ font-size: 13px;
222
+ font-weight: 500;
223
+ transition: all 0.3s ease;
224
+ margin: 0;
225
+ }
226
+
227
+ .mode-toggle a.active {
228
+ background: var(--gradient-1);
229
+ color: white;
230
+ box-shadow: 0 2px 8px rgba(102, 126, 234, 0.4);
231
+ }
232
+
233
+ .mode-toggle a:not(.active) {
234
+ background: transparent;
235
+ color: var(--text-muted);
236
+ }
237
+
238
+ .mode-toggle a:not(.active):hover {
239
+ color: var(--text);
240
+ background: var(--border);
241
+ }
242
+
200
243
  /* Main content */
201
244
  .main {
202
245
  flex: 1;
203
246
  margin-left: 280px;
204
247
  padding: 40px 60px;
205
- max-width: 900px;
248
+ max-width: 100%;
206
249
  }
207
250
 
208
251
  .main h1 {
@@ -293,9 +336,125 @@ function generatePage(files, currentFile) {
293
336
  background: var(--bg-secondary);
294
337
  }
295
338
 
296
- /* Status badges */
297
- .status-pending { color: var(--warning); }
298
- .status-completed { color: var(--success); }
339
+ /* Editor */
340
+ .editor-container {
341
+ display: ${isEditMode ? 'flex' : 'none'};
342
+ gap: 20px;
343
+ height: calc(100vh - 200px);
344
+ }
345
+
346
+ .editor-pane {
347
+ flex: 1;
348
+ display: flex;
349
+ flex-direction: column;
350
+ }
351
+
352
+ .editor-pane h3 {
353
+ margin: 0 0 12px 0;
354
+ font-size: 14px;
355
+ color: var(--text-muted);
356
+ text-transform: uppercase;
357
+ letter-spacing: 0.5px;
358
+ }
359
+
360
+ .editor {
361
+ flex: 1;
362
+ width: 100%;
363
+ background: var(--code-bg);
364
+ border: 1px solid var(--border);
365
+ border-radius: 8px;
366
+ padding: 16px;
367
+ color: var(--text);
368
+ font-family: 'SF Mono', Monaco, 'Cascadia Code', monospace;
369
+ font-size: 14px;
370
+ line-height: 1.6;
371
+ resize: none;
372
+ outline: none;
373
+ }
374
+
375
+ .editor:focus {
376
+ border-color: var(--accent);
377
+ box-shadow: 0 0 0 3px rgba(88, 166, 255, 0.1);
378
+ }
379
+
380
+ .preview-pane {
381
+ flex: 1;
382
+ background: var(--bg-secondary);
383
+ border: 1px solid var(--border);
384
+ border-radius: 8px;
385
+ padding: 20px;
386
+ overflow-y: auto;
387
+ }
388
+
389
+ .preview-content {
390
+ display: ${isEditMode ? 'none' : 'block'};
391
+ }
392
+
393
+ /* Toolbar */
394
+ .toolbar {
395
+ display: flex;
396
+ gap: 10px;
397
+ margin-bottom: 20px;
398
+ align-items: center;
399
+ }
400
+
401
+ .save-btn {
402
+ background: var(--gradient-2);
403
+ color: white;
404
+ border: none;
405
+ padding: 10px 24px;
406
+ border-radius: 8px;
407
+ font-size: 14px;
408
+ font-weight: 500;
409
+ cursor: pointer;
410
+ transition: all 0.2s;
411
+ display: flex;
412
+ align-items: center;
413
+ gap: 8px;
414
+ }
415
+
416
+ .save-btn:hover {
417
+ transform: translateY(-1px);
418
+ box-shadow: 0 4px 12px rgba(63, 185, 80, 0.3);
419
+ }
420
+
421
+ .save-btn:active {
422
+ transform: translateY(0);
423
+ }
424
+
425
+ .save-btn:disabled {
426
+ opacity: 0.5;
427
+ cursor: not-allowed;
428
+ transform: none;
429
+ }
430
+
431
+ .status {
432
+ font-size: 13px;
433
+ color: var(--text-muted);
434
+ margin-left: auto;
435
+ }
436
+
437
+ .status.saved {
438
+ color: var(--success);
439
+ }
440
+
441
+ .status.saving {
442
+ color: var(--warning);
443
+ }
444
+
445
+ .status.error {
446
+ color: var(--error);
447
+ }
448
+
449
+ /* Keyboard shortcut hint */
450
+ .shortcut {
451
+ font-size: 11px;
452
+ color: var(--text-muted);
453
+ background: var(--bg-tertiary);
454
+ padding: 2px 6px;
455
+ border-radius: 4px;
456
+ margin-left: 8px;
457
+ }
299
458
 
300
459
  /* Footer */
301
460
  .footer {
@@ -304,6 +463,8 @@ function generatePage(files, currentFile) {
304
463
  border-top: 1px solid var(--border);
305
464
  color: var(--text-muted);
306
465
  font-size: 12px;
466
+ display: flex;
467
+ justify-content: space-between;
307
468
  }
308
469
 
309
470
  /* Responsive */
@@ -311,37 +472,212 @@ function generatePage(files, currentFile) {
311
472
  .sidebar { width: 100%; height: auto; position: relative; }
312
473
  .main { margin-left: 0; padding: 20px; }
313
474
  body { flex-direction: column; }
475
+ .editor-container { flex-direction: column; height: auto; }
476
+ .editor { min-height: 300px; }
477
+ .preview-pane { min-height: 300px; }
314
478
  }
315
479
  </style>
316
480
  </head>
317
481
  <body>
318
482
  <nav class="sidebar">
319
483
  <h2>šŸ“ ${path.basename(fullPlanPath)}</h2>
484
+
485
+ <div class="mode-toggle">
486
+ <a href="?file=${encodeURIComponent(currentFileName)}&mode=preview" class="${!isEditMode ? 'active' : ''}">šŸ‘ļø Preview</a>
487
+ <a href="?file=${encodeURIComponent(currentFileName)}&mode=edit" class="${isEditMode ? 'active' : ''}">āœļø Edit</a>
488
+ </div>
489
+
320
490
  ${nav}
321
491
  </nav>
492
+
322
493
  <main class="main">
323
- <article>
494
+ ${isEditMode ? `
495
+ <div class="toolbar">
496
+ <button class="save-btn" onclick="saveFile()" id="saveBtn">
497
+ šŸ’¾ Save
498
+ <span class="shortcut">⌘S</span>
499
+ </button>
500
+ <span class="status" id="status">Ready to edit</span>
501
+ </div>
502
+
503
+ <div class="editor-container">
504
+ <div class="editor-pane">
505
+ <h3>šŸ“ Markdown Editor</h3>
506
+ <textarea class="editor" id="editor" spellcheck="false">${escapeHtml(content)}</textarea>
507
+ </div>
508
+ <div class="editor-pane">
509
+ <h3>šŸ‘ļø Live Preview</h3>
510
+ <div class="preview-pane" id="preview">${htmlContent}</div>
511
+ </div>
512
+ </div>
513
+ ` : `
514
+ <article class="preview-content">
324
515
  ${htmlContent}
325
516
  </article>
517
+ `}
518
+
326
519
  <footer class="footer">
327
- <p>Plan Preview Server • <a href="javascript:location.reload()">↻ Refresh</a></p>
520
+ <span>Plan Preview Server • Press <kbd>Ctrl+C</kbd> in terminal to stop</span>
521
+ <span><a href="javascript:location.reload()">↻ Refresh</a></span>
328
522
  </footer>
329
523
  </main>
524
+
330
525
  <script>
331
- // Auto-refresh every 5 seconds (optional)
332
- // setTimeout(() => location.reload(), 5000);
526
+ const currentFile = '${escapeHtml(currentFileName)}';
527
+ let originalContent = '';
528
+ let hasChanges = false;
529
+
530
+ ${isEditMode ? `
531
+ const editor = document.getElementById('editor');
532
+ const preview = document.getElementById('preview');
533
+ const status = document.getElementById('status');
534
+ const saveBtn = document.getElementById('saveBtn');
535
+
536
+ originalContent = editor.value;
537
+
538
+ // Live preview update
539
+ let updateTimeout;
540
+ editor.addEventListener('input', () => {
541
+ hasChanges = editor.value !== originalContent;
542
+ updateStatus();
543
+
544
+ clearTimeout(updateTimeout);
545
+ updateTimeout = setTimeout(() => {
546
+ preview.innerHTML = simpleMarkdown(editor.value);
547
+ }, 300);
548
+ });
549
+
550
+ // Simple client-side markdown (for live preview)
551
+ function simpleMarkdown(md) {
552
+ return md
553
+ .replace(/\`\`\`(\\w+)?\\n([\\s\\S]*?)\`\`\`/g, '<pre><code>$2</code></pre>')
554
+ .replace(/\`([^\`]+)\`/g, '<code class="inline">$1</code>')
555
+ .replace(/^### (.*$)/gm, '<h3>$1</h3>')
556
+ .replace(/^## (.*$)/gm, '<h2>$1</h2>')
557
+ .replace(/^# (.*$)/gm, '<h1>$1</h1>')
558
+ .replace(/\\*\\*([^*]+)\\*\\*/g, '<strong>$1</strong>')
559
+ .replace(/\\*([^*]+)\\*/g, '<em>$1</em>')
560
+ .replace(/\\[([^\\]]+)\\]\\(([^)]+)\\)/g, '<a href="$2">$1</a>')
561
+ .replace(/^\\s*[-*] (.*$)/gm, '<li>$1</li>')
562
+ .replace(/^> (.*$)/gm, '<blockquote>$1</blockquote>')
563
+ .replace(/^---+$/gm, '<hr>')
564
+ .replace(/\\n\\n/g, '</p><p>');
565
+ }
566
+
567
+ // Update status indicator
568
+ function updateStatus() {
569
+ if (hasChanges) {
570
+ status.textContent = 'ā— Unsaved changes';
571
+ status.className = 'status';
572
+ } else {
573
+ status.textContent = 'āœ“ Saved';
574
+ status.className = 'status saved';
575
+ }
576
+ }
577
+
578
+ // Save file
579
+ async function saveFile() {
580
+ if (!hasChanges) return;
581
+
582
+ saveBtn.disabled = true;
583
+ status.textContent = 'Saving...';
584
+ status.className = 'status saving';
585
+
586
+ try {
587
+ const response = await fetch('/save', {
588
+ method: 'POST',
589
+ headers: { 'Content-Type': 'application/json' },
590
+ body: JSON.stringify({
591
+ file: currentFile,
592
+ content: editor.value
593
+ })
594
+ });
595
+
596
+ const result = await response.json();
597
+
598
+ if (result.success) {
599
+ originalContent = editor.value;
600
+ hasChanges = false;
601
+ status.textContent = 'āœ“ Saved successfully';
602
+ status.className = 'status saved';
603
+
604
+ setTimeout(() => {
605
+ if (!hasChanges) {
606
+ status.textContent = 'āœ“ Saved';
607
+ }
608
+ }, 2000);
609
+ } else {
610
+ throw new Error(result.error || 'Save failed');
611
+ }
612
+ } catch (err) {
613
+ status.textContent = 'āœ— ' + err.message;
614
+ status.className = 'status error';
615
+ } finally {
616
+ saveBtn.disabled = false;
617
+ }
618
+ }
619
+
620
+ // Keyboard shortcut: Cmd/Ctrl + S to save
621
+ document.addEventListener('keydown', (e) => {
622
+ if ((e.metaKey || e.ctrlKey) && e.key === 's') {
623
+ e.preventDefault();
624
+ saveFile();
625
+ }
626
+ });
627
+
628
+ // Warn before leaving with unsaved changes
629
+ window.addEventListener('beforeunload', (e) => {
630
+ if (hasChanges) {
631
+ e.preventDefault();
632
+ e.returnValue = '';
633
+ }
634
+ });
635
+ ` : ''}
333
636
  </script>
334
637
  </body>
335
638
  </html>`;
336
639
  }
337
640
 
338
- // Create HTTP server
641
+ // Create HTTP server with save endpoint
339
642
  const server = http.createServer((req, res) => {
340
643
  const url = new URL(req.url, `http://localhost:${PORT}`);
644
+
645
+ // Handle save endpoint
646
+ if (req.method === 'POST' && url.pathname === '/save') {
647
+ let body = '';
648
+ req.on('data', chunk => { body += chunk; });
649
+ req.on('end', () => {
650
+ try {
651
+ const { file, content } = JSON.parse(body);
652
+ const files = getPlanFiles(fullPlanPath);
653
+ const targetFile = files.find(f => f.name === file);
654
+
655
+ if (!targetFile) {
656
+ res.writeHead(404, { 'Content-Type': 'application/json' });
657
+ res.end(JSON.stringify({ success: false, error: 'File not found' }));
658
+ return;
659
+ }
660
+
661
+ // Write file
662
+ fs.writeFileSync(targetFile.path, content, 'utf-8');
663
+ console.log(`šŸ’¾ Saved: ${file}`);
664
+
665
+ res.writeHead(200, { 'Content-Type': 'application/json' });
666
+ res.end(JSON.stringify({ success: true }));
667
+ } catch (err) {
668
+ res.writeHead(500, { 'Content-Type': 'application/json' });
669
+ res.end(JSON.stringify({ success: false, error: err.message }));
670
+ }
671
+ });
672
+ return;
673
+ }
674
+
675
+ // Handle GET requests
341
676
  const currentFile = url.searchParams.get('file');
677
+ const mode = url.searchParams.get('mode') || 'preview';
342
678
 
343
679
  const files = getPlanFiles(fullPlanPath);
344
- const html = generatePage(files, currentFile);
680
+ const html = generatePage(files, currentFile, mode);
345
681
 
346
682
  res.writeHead(200, { 'Content-Type': 'text/html; charset=utf-8' });
347
683
  res.end(html);
@@ -349,9 +685,13 @@ const server = http.createServer((req, res) => {
349
685
 
350
686
  server.listen(PORT, () => {
351
687
  const url = `http://localhost:${PORT}`;
352
- console.log(`\nšŸ“‹ Plan Preview Server`);
688
+ console.log(`\nšŸ“‹ Plan Preview Server (with Edit Support)`);
353
689
  console.log(` Plan: ${planPath}`);
354
690
  console.log(` URL: ${url}`);
691
+ console.log(`\n Features:`);
692
+ console.log(` • Preview mode: View rendered markdown`);
693
+ console.log(` • Edit mode: Edit with live preview`);
694
+ console.log(` • Save: Cmd/Ctrl+S or click Save button`);
355
695
  console.log(`\n Press Ctrl+C to stop\n`);
356
696
 
357
697
  // Open browser