@adityaaria/spark 6.0.18 → 6.0.20

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.
@@ -70,6 +70,7 @@ SCOPE="project"
70
70
  FORCE=false
71
71
  HELP=false
72
72
  DRY_RUN=false
73
+ UNINSTALL=false
73
74
 
74
75
  parse_args() {
75
76
  while [ $# -gt 0 ]; do
@@ -77,6 +78,7 @@ parse_args() {
77
78
  -g|--global) SCOPE="global" ;;
78
79
  --force) FORCE=true ;;
79
80
  --dry-run) DRY_RUN=true ;;
81
+ -u|--uninstall) UNINSTALL=true ;;
80
82
  -h|--help) HELP=true ;;
81
83
  *)
82
84
  error "Unknown argument: $1"
@@ -101,6 +103,7 @@ print_help() {
101
103
  Default: project scope (./.agent/skills/)
102
104
  --force Re-install even if already installed
103
105
  --dry-run Show what would be done without making changes
106
+ -u, --uninstall Safely remove SPARK from agent configs
104
107
  -h, --help Show this help message
105
108
 
106
109
  Examples:
@@ -750,6 +753,85 @@ check_existing_install() {
750
753
  fi
751
754
  }
752
755
 
756
+ # =============================================================================
757
+ # Uninstall
758
+ # =============================================================================
759
+
760
+ uninstall_for_agent() {
761
+ local agent_id="$1"
762
+ local target_dir
763
+ target_dir="$(get_target_dir "$agent_id")"
764
+
765
+ if [ ! -d "$target_dir" ]; then
766
+ return 0
767
+ fi
768
+
769
+ info "Removing SPARK from $agent_id at $target_dir"
770
+
771
+ # 1. Remove skills symlink/directory
772
+ if [ -e "$target_dir/skills" ]; then
773
+ rm -rf "$target_dir/skills"
774
+ fi
775
+
776
+ # 2. Remove hooks
777
+ local hook_files
778
+ hook_files="$(get_agent_hook_files "$agent_id")"
779
+ if [ -n "$hook_files" ]; then
780
+ while IFS= read -r hook_file; do
781
+ [ -z "$hook_file" ] && continue
782
+ rm -f "$target_dir/$hook_file"
783
+ done <<< "$hook_files"
784
+ # Try to remove hooks dir if empty
785
+ rmdir "$target_dir/hooks" 2>/dev/null || true
786
+ fi
787
+
788
+ # 3. Remove plugin manifests
789
+ local manifest_files
790
+ manifest_files="$(get_agent_manifest_files "$agent_id")"
791
+ if [ -n "$manifest_files" ]; then
792
+ while IFS= read -r manifest_file; do
793
+ [ -z "$manifest_file" ] && continue
794
+ rm -f "$target_dir/$manifest_file"
795
+ local manifest_dir
796
+ manifest_dir="$(dirname "$target_dir/$manifest_file")"
797
+ rmdir "$manifest_dir" 2>/dev/null || true
798
+ done <<< "$manifest_files"
799
+ fi
800
+
801
+ # 4. Remove agent dir if totally empty
802
+ rmdir "$target_dir" 2>/dev/null || true
803
+ }
804
+
805
+ perform_uninstall() {
806
+ header "SPARK Uninstaller"
807
+
808
+ local lock_dir
809
+ if [ "$SCOPE" = "global" ]; then
810
+ lock_dir="${HOME:-$(eval echo ~)}"
811
+ else
812
+ lock_dir="$(pwd)"
813
+ fi
814
+ local lock_path="$lock_dir/$LOCK_FILE_NAME"
815
+
816
+ # Detect agents
817
+ info "Detecting installed agents to clean up..."
818
+ detect_agents || true
819
+ build_selected_agents
820
+
821
+ for agent_id in "${SELECTED_AGENTS[@]}"; do
822
+ uninstall_for_agent "$agent_id"
823
+ done
824
+
825
+ if [ -f "$lock_path" ]; then
826
+ rm -f "$lock_path"
827
+ info "Removed lock file: $lock_path"
828
+ fi
829
+
830
+ echo ""
831
+ success "SPARK has been safely uninstalled."
832
+ exit $EXIT_SUCCESS
833
+ }
834
+
753
835
  # =============================================================================
754
836
  # Summary
755
837
  # =============================================================================
@@ -800,6 +882,10 @@ main() {
800
882
  exit $EXIT_SUCCESS
801
883
  fi
802
884
 
885
+ if $UNINSTALL; then
886
+ perform_uninstall
887
+ fi
888
+
803
889
  header "SPARK Native Installer"
804
890
 
805
891
  # Step 1: Resolve repo
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@adityaaria/spark",
3
- "version": "6.0.18",
3
+ "version": "6.0.20",
4
4
  "description": "SPARK skills and runtime bootstrap for coding agents",
5
5
  "type": "module",
6
6
  "publishConfig": {
@@ -11,7 +11,7 @@ Use this skill to scan and document a codebase's architecture, operational rules
11
11
 
12
12
  **Announce at start:** "spark detection šŸ’„ Using project-scanner to analyze repository DNA"
13
13
 
14
- **Save findings to:** The `.docs/` directory. Create it if it does not exist. For large projects, break down the documentation logically (e.g., `.docs/PROJECT_SCAN.md`, `.docs/API_CONTRACT.md`, `.docs/DOMAINS/`) instead of creating one massive monolithic file.
14
+ **Save findings to:** The `.docs/` directory. **You must physically create this directory and save the files to disk using your tools. Do NOT just print the output to the user.** For large projects, break down the documentation logically (e.g., `.docs/PROJECT_SCAN.md`, `.docs/API_CONTRACT.md`, `.docs/DOMAINS/`) instead of creating one massive monolithic file.
15
15
 
16
16
  ## Handling Massive Codebases (Subagent Delegation)
17
17
  If the repository is extremely large and analyzing all four pillars sequentially risks exceeding context limits or taking too long:
@@ -56,4 +56,4 @@ Identify conventions, rules, and technical debt enforced or found in the codebas
56
56
  - `[ ]` **Step 2:** Scan root configuration files and prioritize searching for Swagger/OpenAPI specifications.
57
57
  - `[ ]` **Step 3:** Scan source directory structures to infer the language, framework, and architectural patterns.
58
58
  - `[ ]` **Step 4:** Analyze testing frameworks, CI/CD workflows, and actively hunt for anti-patterns and legacy traps.
59
- - `[ ]` **Step 5:** Write the comprehensive report(s) into the `.docs/` directory.
59
+ - `[ ]` **Step 5 (CRITICAL):** You MUST use your file-writing tools to physically create the `.docs/` directory and save the markdown files (e.g. `PROJECT_SCAN.md`, `API_CONTRACT.md`) into it. DO NOT just print the summary into the chat! You must physically write the files to disk.
package/src/cli/index.js CHANGED
@@ -14,6 +14,18 @@ export async function run(argv = [], env = process.env) {
14
14
  return;
15
15
  }
16
16
 
17
+ if (command === 'uninstall') {
18
+ // Forward the args and append --uninstall
19
+ await runInstall([...args, '--uninstall'], env);
20
+ return;
21
+ }
22
+
23
+ if (command === 'dashboard' || command === 'ui') {
24
+ const { startDashboard } = await import('../dashboard/server.js');
25
+ startDashboard();
26
+ return;
27
+ }
28
+
17
29
  throw new Error(`Unknown command: ${command}`);
18
30
  }
19
31
 
@@ -0,0 +1,804 @@
1
+ <!DOCTYPE html>
2
+ <html lang="en">
3
+ <head>
4
+ <meta charset="UTF-8">
5
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
6
+ <title>SPARK | Command Center v2</title>
7
+ <!-- Google Fonts -->
8
+ <link href="https://fonts.googleapis.com/css2?family=Inter:wght@300;400;600;700&display=swap" rel="stylesheet">
9
+ <!-- Marked.js for Markdown parsing -->
10
+ <script src="https://cdn.jsdelivr.net/npm/marked/marked.min.js"></script>
11
+ <!-- Vis-Network for Interactive Graphs -->
12
+ <script type="text/javascript" src="https://unpkg.com/vis-network/standalone/umd/vis-network.min.js"></script>
13
+ <!-- Chart.js for Heatmap -->
14
+ <script src="https://cdn.jsdelivr.net/npm/chart.js"></script>
15
+ <style>
16
+ :root {
17
+ --bg-color: #0d0d0d;
18
+ --panel-bg: #151515;
19
+ --glass-border: #2a2a2a;
20
+ --accent: #dcedc1; /* Sage green from reference */
21
+ --accent-dark: #b8c99d;
22
+ --danger: #ff5e57;
23
+ --warning: #ffb86c;
24
+ --text-main: #f4f4f4;
25
+ --text-muted: #888888;
26
+ --font-main: 'Inter', sans-serif;
27
+ --font-mono: 'Courier New', Courier, monospace;
28
+ }
29
+
30
+ * { box-sizing: border-box; margin: 0; padding: 0; }
31
+
32
+ body {
33
+ font-family: var(--font-main);
34
+ background-color: var(--bg-color);
35
+ color: var(--text-main);
36
+ min-height: 100vh;
37
+ display: flex;
38
+ -webkit-font-smoothing: antialiased;
39
+ }
40
+
41
+ /* Sidebar */
42
+ .sidebar {
43
+ width: 280px;
44
+ background: var(--bg-color);
45
+ border-right: 1px solid var(--glass-border);
46
+ padding: 32px 24px;
47
+ display: flex;
48
+ flex-direction: column;
49
+ }
50
+
51
+ .logo {
52
+ font-size: 20px;
53
+ font-weight: 400;
54
+ letter-spacing: 1px;
55
+ margin-bottom: 48px;
56
+ display: flex;
57
+ align-items: center;
58
+ gap: 12px;
59
+ color: var(--text-main);
60
+ }
61
+
62
+ .logo svg { stroke: var(--text-main); }
63
+ .logo span { color: var(--text-muted); font-weight: 300; }
64
+
65
+ .nav-section { margin-bottom: 32px; }
66
+ .nav-title {
67
+ font-size: 11px;
68
+ text-transform: uppercase;
69
+ color: var(--text-muted);
70
+ letter-spacing: 1.5px;
71
+ margin-bottom: 16px;
72
+ padding-left: 12px;
73
+ }
74
+
75
+ .nav-item {
76
+ padding: 10px 12px;
77
+ margin-bottom: 4px;
78
+ border-radius: 6px;
79
+ cursor: pointer;
80
+ transition: all 0.2s ease;
81
+ color: var(--text-muted);
82
+ font-size: 14px;
83
+ font-weight: 400;
84
+ display: flex;
85
+ align-items: center;
86
+ gap: 12px;
87
+ }
88
+
89
+ .nav-item:hover { color: var(--text-main); }
90
+ .nav-item.active {
91
+ background: rgba(255, 255, 255, 0.05);
92
+ color: var(--text-main);
93
+ }
94
+
95
+ /* Main Content */
96
+ .content {
97
+ flex: 1;
98
+ padding: 48px;
99
+ overflow-y: auto;
100
+ height: 100vh;
101
+ display: flex;
102
+ flex-direction: column;
103
+ }
104
+
105
+ .header {
106
+ display: flex;
107
+ justify-content: space-between;
108
+ align-items: flex-end;
109
+ margin-bottom: 48px;
110
+ padding-bottom: 24px;
111
+ border-bottom: 1px solid var(--glass-border);
112
+ }
113
+
114
+ .header h1 { font-size: 28px; font-weight: 400; letter-spacing: -0.5px; }
115
+
116
+ .status-badge {
117
+ color: var(--text-muted);
118
+ font-family: var(--font-mono);
119
+ font-size: 12px;
120
+ display: flex;
121
+ align-items: center;
122
+ gap: 8px;
123
+ }
124
+ .status-badge::before {
125
+ content: ''; display: block; width: 6px; height: 6px; border-radius: 50%;
126
+ background: var(--text-muted);
127
+ }
128
+ .status-badge.active::before { background: var(--accent); }
129
+
130
+ .panel {
131
+ background: var(--panel-bg);
132
+ border: 1px solid var(--glass-border);
133
+ border-radius: 12px;
134
+ padding: 32px;
135
+ margin-bottom: 24px;
136
+ animation: fadeIn 0.4s ease forwards;
137
+ flex: 1;
138
+ }
139
+
140
+ .panel h2 { font-weight: 400; margin-bottom: 8px; font-size: 20px; }
141
+ .panel p { color: var(--text-muted); font-size: 14px; margin-bottom: 24px; }
142
+
143
+ @keyframes fadeIn {
144
+ from { opacity: 0; transform: translateY(5px); }
145
+ to { opacity: 1; transform: translateY(0); }
146
+ }
147
+
148
+ /* Widgets / Cards (Reference Style) */
149
+ .widget-row { display: flex; gap: 24px; margin-bottom: 24px; }
150
+ .widget {
151
+ flex: 1;
152
+ background: var(--panel-bg);
153
+ border: 1px solid var(--glass-border);
154
+ border-radius: 12px;
155
+ padding: 24px;
156
+ display: flex;
157
+ flex-direction: column;
158
+ justify-content: flex-end;
159
+ }
160
+ .widget-value {
161
+ font-family: var(--font-mono);
162
+ font-size: 42px;
163
+ font-weight: 300;
164
+ color: var(--text-main);
165
+ margin-bottom: 4px;
166
+ letter-spacing: -1px;
167
+ }
168
+ .widget-label {
169
+ font-size: 12px;
170
+ color: var(--text-muted);
171
+ }
172
+
173
+ .widget.accent-card {
174
+ background: var(--accent);
175
+ border: none;
176
+ color: #111;
177
+ }
178
+ .widget.accent-card .widget-value,
179
+ .widget.accent-card .widget-label { color: #111; font-weight: 500; }
180
+
181
+ /* Markdown Styling */
182
+ .markdown-body { line-height: 1.6; font-size: 15px; }
183
+ .markdown-body h1, .markdown-body h2, .markdown-body h3 { margin-top: 32px; margin-bottom: 16px; font-weight: 400; }
184
+ .markdown-body h1 { font-size: 24px; border-bottom: 1px solid var(--glass-border); padding-bottom: 12px; }
185
+ .markdown-body h2 { font-size: 20px; }
186
+ .markdown-body p { margin-bottom: 16px; color: var(--text-muted); }
187
+ .markdown-body ul { margin-bottom: 16px; padding-left: 24px; color: var(--text-muted); }
188
+ .markdown-body code { font-family: var(--font-mono); background: rgba(255,255,255,0.05); padding: 2px 6px; border-radius: 4px; font-size: 13px; }
189
+ .markdown-body pre { background: #000; border: 1px solid var(--glass-border); padding: 20px; border-radius: 8px; overflow-x: auto; margin-bottom: 20px; }
190
+
191
+ /* Form Styles */
192
+ .form-group { margin-bottom: 24px; }
193
+ .form-group label { display: block; margin-bottom: 8px; color: var(--text-muted); font-size: 13px; }
194
+ .form-group input, .form-group textarea {
195
+ width: 100%; padding: 14px; border-radius: 8px;
196
+ background: #0d0d0d; border: 1px solid var(--glass-border);
197
+ color: var(--text-main); font-family: var(--font-mono); font-size: 14px;
198
+ transition: border 0.2s;
199
+ }
200
+ .form-group input:focus, .form-group textarea:focus { outline: none; border-color: var(--text-muted); }
201
+ .form-group textarea { min-height: 180px; resize: vertical; }
202
+
203
+ .btn {
204
+ background: transparent; color: var(--text-main); font-weight: 400;
205
+ padding: 10px 20px; border-radius: 20px; border: 1px solid var(--glass-border);
206
+ cursor: pointer; transition: 0.2s; font-size: 13px;
207
+ }
208
+ .btn:hover { background: rgba(255,255,255,0.05); border-color: var(--text-muted); }
209
+ .btn-accent {
210
+ background: var(--accent); color: #111; border: none; font-weight: 500;
211
+ }
212
+ .btn-accent:hover { background: var(--accent-dark); }
213
+
214
+ /* Graph & Tree */
215
+ #network-graph { width: 100%; height: 500px; border: 1px solid var(--glass-border); border-radius: 12px; background: #0d0d0d; }
216
+ .tree-node { padding: 6px 0; margin-left: 24px; cursor: pointer; color: var(--text-muted); font-family: var(--font-mono); font-size: 13px; }
217
+ .tree-node:hover { color: var(--text-main); }
218
+ .tree-node.danger { color: var(--danger); }
219
+ .tree-node.danger::after { content: ' • FLAG'; font-size: 10px; letter-spacing: 1px; }
220
+
221
+ .hidden { display: none !important; }
222
+
223
+ </style>
224
+ </head>
225
+ <body>
226
+
227
+ <div class="sidebar">
228
+ <div class="logo">
229
+ <svg width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="var(--accent)" stroke-width="2"><path d="M13 2L3 14h9l-1 8 10-12h-9l1-8z"/></svg>
230
+ SPARK<span>UI</span>
231
+ </div>
232
+
233
+ <div class="nav-section">
234
+ <div class="nav-title">Views</div>
235
+ <div class="nav-item active" onclick="switchView('overview')">šŸ“„ Overview</div>
236
+ <div class="nav-item" onclick="switchView('heatmap')">šŸ”„ Heatmap</div>
237
+ <div class="nav-item" onclick="switchView('graph')">šŸ•ø Architecture Graph</div>
238
+ </div>
239
+
240
+ <div class="nav-section">
241
+ <div class="nav-title">Tools</div>
242
+ <div class="nav-item" onclick="switchView('studio')">āœļø Skill Studio</div>
243
+ <div class="nav-item" onclick="switchView('exporter')">šŸš€ API Exporter</div>
244
+ <div class="nav-item" onclick="switchView('guide')">šŸ“š Guide & Commands</div>
245
+ </div>
246
+
247
+ <div class="nav-section" id="docs-nav-section" style="display: none;">
248
+ <div class="nav-title">Scanned Docs</div>
249
+ <div id="nav-container"></div>
250
+ </div>
251
+ </div>
252
+
253
+ <div class="content">
254
+ <div class="header">
255
+ <div>
256
+ <h1 id="page-title">Dashboard Overview</h1>
257
+ <div style="font-family: var(--font-mono); color: var(--text-muted); font-size: 12px; margin-top: 8px;" id="current-time">11:37 AM Time</div>
258
+ </div>
259
+ <div class="status-badge" id="agent-status">Checking Agents...</div>
260
+ </div>
261
+
262
+ <!-- View: Overview (Markdown reader) -->
263
+ <div id="view-overview" class="panel markdown-body">
264
+ <h3>Welcome to SPARK UI</h3>
265
+ <p>Select a scanned document from the sidebar to view it.</p>
266
+ </div>
267
+
268
+ <!-- View: Heatmap -->
269
+ <div id="view-heatmap" class="panel hidden">
270
+ <div style="display: flex; justify-content: space-between; align-items: center; margin-bottom: 40px;">
271
+ <div>
272
+ <h2>Health Tracking</h2>
273
+ <p>AI-driven legacy trap and anti-pattern detection.</p>
274
+ </div>
275
+ <div style="text-align: right;">
276
+ <div style="font-family: var(--font-mono); font-size: 42px; font-weight: 300; color: var(--text-main);" id="health-score">--</div>
277
+ <div style="font-size: 12px; color: var(--text-muted);">Overall Score</div>
278
+ </div>
279
+ </div>
280
+
281
+ <div class="widget-row">
282
+ <div class="widget">
283
+ <div class="widget-value" id="stat-total">0</div>
284
+ <div class="widget-label">Total Scanned</div>
285
+ </div>
286
+ <div class="widget accent-card">
287
+ <div class="widget-value" id="stat-clean">0</div>
288
+ <div class="widget-label">Clean Code</div>
289
+ </div>
290
+ <div class="widget">
291
+ <div class="widget-value" style="color: var(--danger);" id="stat-danger">0</div>
292
+ <div class="widget-label">Anti-Patterns</div>
293
+ </div>
294
+ </div>
295
+
296
+ <div class="widget-row">
297
+ <div class="widget" style="flex: 1;">
298
+ <h3 style="margin-bottom: 24px; font-size: 14px; font-weight: 400;">Distribution</h3>
299
+ <canvas id="healthChart" style="max-height: 200px;"></canvas>
300
+ </div>
301
+ <div class="widget" style="flex: 2; overflow-y: auto; max-height: 290px; justify-content: flex-start;">
302
+ <h3 style="margin-bottom: 24px; font-size: 14px; font-weight: 400; display: flex; justify-content: space-between;">
303
+ <span>Detailed Report</span>
304
+ <span style="border: 1px solid var(--glass-border); padding: 4px 12px; border-radius: 20px; font-size: 11px;">Flagged Files</span>
305
+ </h3>
306
+ <div id="danger-list">
307
+ <p style="color: var(--text-muted); font-size: 13px;">Scanning...</p>
308
+ </div>
309
+ </div>
310
+ </div>
311
+
312
+ <div style="margin-top: 20px;">
313
+ <h3 style="margin-bottom: 16px; font-size: 14px; font-weight: 400;">Directory Tree</h3>
314
+ <div id="file-tree" style="background: var(--bg-color); padding: 24px; border-radius: 12px; border: 1px solid var(--glass-border); max-height: 250px; overflow-y: auto;"></div>
315
+ </div>
316
+ </div>
317
+
318
+ <!-- View: Graph -->
319
+ <div id="view-graph" class="panel hidden">
320
+ <h2>Interactive Architecture Graph</h2>
321
+ <p>Visualizing relationships between system components.</p>
322
+ <div id="network-graph"></div>
323
+ </div>
324
+
325
+ <!-- View: Skill Studio -->
326
+ <div id="view-studio" class="panel hidden" style="display: flex; gap: 40px;">
327
+ <div style="flex: 1;">
328
+ <h2>Create Custom Skill</h2>
329
+ <p>Define a new agent skill. This will be safely saved to <code>.agent/skills/</code> and protected from SPARK updates.</p>
330
+ <div style="margin-top: 20px;">
331
+ <div class="form-group">
332
+ <label>Skill Name (e.g., custom-deploy)</label>
333
+ <input type="text" id="skill-name" placeholder="lowercase-with-dashes">
334
+ </div>
335
+ <div class="form-group">
336
+ <label>Description</label>
337
+ <input type="text" id="skill-desc" placeholder="What does this skill do?">
338
+ </div>
339
+ <div class="form-group">
340
+ <label>Execution Steps (Markdown)</label>
341
+ <textarea id="skill-steps" placeholder="Enter markdown checklist..."></textarea>
342
+ </div>
343
+ <button class="btn btn-accent" onclick="saveSkill()">Save Skill</button>
344
+ <p id="skill-msg" style="margin-top: 16px; color: var(--text-muted); font-size: 13px;"></p>
345
+ </div>
346
+ </div>
347
+ <div style="width: 320px; padding-left: 32px; display: flex; flex-direction: column;">
348
+ <h3 style="font-size: 14px; font-weight: 400; margin-bottom: 24px;">Installed Skills</h3>
349
+ <div id="skill-list" style="flex: 1; overflow-y: auto;">
350
+ <p style="color: var(--text-muted); font-size: 13px;">Loading...</p>
351
+ </div>
352
+ </div>
353
+ </div>
354
+
355
+ <!-- View: Guide -->
356
+ <div id="view-guide" class="panel hidden markdown-body">
357
+ <h2>Loading Documentation...</h2>
358
+ </div>
359
+
360
+ <!-- View: API Exporter -->
361
+ <div id="view-exporter" class="panel hidden">
362
+ <div style="display: flex; justify-content: space-between; align-items: center; margin-bottom: 32px;">
363
+ <div>
364
+ <h2>API Exporter</h2>
365
+ <p>Select endpoints to generate a Postman Collection.</p>
366
+ </div>
367
+ <div style="display: flex; gap: 12px;">
368
+ <button class="btn" onclick="exportEnv()">.env</button>
369
+ <button class="btn btn-accent" onclick="exportPostman()">Export Collection</button>
370
+ </div>
371
+ </div>
372
+ <div style="display: flex; justify-content: space-between; padding: 16px 20px; background: var(--bg-color); border: 1px solid var(--glass-border); border-radius: 12px;">
373
+ <label style="cursor: pointer; display: flex; align-items: center; gap: 12px; font-size: 14px;">
374
+ <input type="checkbox" id="selectAllEndpoints" onchange="toggleAllEndpoints(this)">
375
+ Select All
376
+ </label>
377
+ <span id="endpoint-count" style="font-family: var(--font-mono); font-size: 13px; color: var(--text-muted);">0 items</span>
378
+ </div>
379
+ <div id="endpoint-list" style="margin-top: 16px; overflow-y: auto;">
380
+ <p style="color: var(--text-muted); padding: 20px; text-align: center; font-size: 14px;">No endpoints found.</p>
381
+ </div>
382
+ </div>
383
+
384
+ </div>
385
+
386
+ <script>
387
+ let docsData = [];
388
+ let treeData = [];
389
+ let networkInstance = null;
390
+ let chartInstance = null;
391
+
392
+ function switchView(viewName) {
393
+ document.querySelectorAll('.nav-item').forEach(el => el.classList.remove('active'));
394
+ event.currentTarget.classList.add('active');
395
+
396
+ ['overview', 'heatmap', 'graph', 'studio', 'guide', 'exporter'].forEach(v => {
397
+ document.getElementById(`view-${v}`).classList.add('hidden');
398
+ });
399
+ document.getElementById(`view-${viewName}`).classList.remove('hidden');
400
+
401
+ const titles = {
402
+ overview: 'Dashboard Overview',
403
+ heatmap: 'Codebase Heatmap',
404
+ graph: 'Architecture Graph',
405
+ studio: 'Skill Studio',
406
+ guide: 'Documentation & Guide',
407
+ exporter: 'API Exporter'
408
+ };
409
+ document.getElementById('page-title').innerText = titles[viewName];
410
+
411
+ if (viewName === 'graph') renderGraph();
412
+ if (viewName === 'heatmap') fetchTree();
413
+ if (viewName === 'guide') fetchReadme();
414
+ if (viewName === 'studio') fetchSkills();
415
+ if (viewName === 'exporter') extractEndpoints();
416
+ }
417
+
418
+ setInterval(() => {
419
+ const now = new Date();
420
+ document.getElementById('current-time').innerText = now.toLocaleTimeString([], {hour: '2-digit', minute:'2-digit'}) + ' Time';
421
+ }, 1000);
422
+
423
+ async function fetchDocs() {
424
+ try {
425
+ const res = await fetch('/api/docs');
426
+ const data = await res.json();
427
+ if (!data.error) {
428
+ docsData = data.docs;
429
+ const nav = document.getElementById('nav-container');
430
+ document.getElementById('docs-nav-section').style.display = 'block';
431
+ nav.innerHTML = '';
432
+ docsData.forEach(doc => {
433
+ const el = document.createElement('div');
434
+ el.className = 'nav-item';
435
+ el.innerText = 'šŸ“„ ' + doc.filename.replace('.md', '').replace(/_/g, ' ');
436
+ el.onclick = (e) => {
437
+ switchView('overview');
438
+ document.querySelectorAll('.nav-item').forEach(el => el.classList.remove('active'));
439
+ e.currentTarget.classList.add('active');
440
+ document.getElementById('page-title').innerText = doc.filename;
441
+ document.getElementById('view-overview').innerHTML = marked.parse(doc.content);
442
+ };
443
+ nav.appendChild(el);
444
+ });
445
+ }
446
+ } catch (err) {}
447
+ }
448
+
449
+ async function fetchStatus() {
450
+ try {
451
+ const res = await fetch('/api/status');
452
+ const data = await res.json();
453
+ const badge = document.getElementById('agent-status');
454
+ if (data.installed && data.agents && data.agents.length > 0) {
455
+ badge.innerHTML = `Agents Active: ${data.agents.join(', ')}`;
456
+ badge.classList.add('active');
457
+ } else {
458
+ badge.innerHTML = `No Agents Active`;
459
+ badge.classList.remove('active');
460
+ }
461
+ } catch (e) {}
462
+ }
463
+
464
+ async function fetchReadme() {
465
+ try {
466
+ const res = await fetch('/api/readme');
467
+ const data = await res.json();
468
+ document.getElementById('view-guide').innerHTML = marked.parse(data.content);
469
+ } catch (e) {}
470
+ }
471
+
472
+ async function fetchTree() {
473
+ if(treeData.length > 0) return; // already fetched
474
+ try {
475
+ const res = await fetch('/api/tree');
476
+ const data = await res.json();
477
+ treeData = data.tree;
478
+
479
+ // Naive simulation of finding legacy traps
480
+ const dangerousFiles = ['controller', 'utils', 'helper', 'api'];
481
+ let totalFiles = 0;
482
+ let dangerFilesCount = 0;
483
+ let dangerListHTML = '';
484
+
485
+ function buildTreeHtml(nodes) {
486
+ let html = '';
487
+ for(const node of nodes) {
488
+ const isDanger = node.type === 'file' && dangerousFiles.some(d => node.name.toLowerCase().includes(d));
489
+
490
+ if (node.type === 'file') {
491
+ totalFiles++;
492
+ if (isDanger) {
493
+ dangerFilesCount++;
494
+ dangerListHTML += `<div style="padding: 12px 16px; background: var(--bg-color); border: 1px solid var(--glass-border); border-radius: 8px; margin-bottom: 8px; display: flex; justify-content: space-between; align-items: center;">
495
+ <span style="font-family: var(--font-mono); font-size: 13px; color: var(--text-main);">${node.path}</span>
496
+ <span style="font-size: 10px; color: var(--text-muted); border: 1px solid var(--glass-border); padding: 4px 8px; border-radius: 20px;">Flagged</span>
497
+ </div>`;
498
+ }
499
+ }
500
+
501
+ const cls = `tree-node ${node.type} ${isDanger ? 'danger' : ''}`;
502
+ html += `<div class="${cls}">${node.name}</div>`;
503
+ if(node.children) {
504
+ html += `<div style="margin-left: 20px;">${buildTreeHtml(node.children)}</div>`;
505
+ }
506
+ }
507
+ return html;
508
+ }
509
+
510
+ document.getElementById('file-tree').innerHTML = buildTreeHtml(treeData);
511
+
512
+ // Update Widgets
513
+ const cleanFilesCount = totalFiles - dangerFilesCount;
514
+ const healthScore = totalFiles === 0 ? 100 : Math.round((cleanFilesCount / totalFiles) * 100);
515
+
516
+ document.getElementById('stat-total').innerText = totalFiles;
517
+ document.getElementById('stat-clean').innerText = cleanFilesCount;
518
+ document.getElementById('stat-danger').innerText = dangerFilesCount;
519
+ document.getElementById('health-score').innerText = healthScore;
520
+
521
+ if (dangerListHTML === '') {
522
+ document.getElementById('danger-list').innerHTML = '<p style="color: var(--text-muted); font-size: 13px;">No dangerous files found! Your codebase is healthy.</p>';
523
+ } else {
524
+ document.getElementById('danger-list').innerHTML = dangerListHTML;
525
+ }
526
+
527
+ // Render Chart
528
+ if (chartInstance) chartInstance.destroy();
529
+ const ctx = document.getElementById('healthChart').getContext('2d');
530
+ chartInstance = new Chart(ctx, {
531
+ type: 'doughnut',
532
+ data: {
533
+ labels: ['Clean Code', 'Anti-Patterns'],
534
+ datasets: [{
535
+ data: [cleanFilesCount, dangerFilesCount],
536
+ backgroundColor: ['#dcedc1', '#2a2a2a'],
537
+ borderWidth: 0,
538
+ hoverOffset: 4
539
+ }]
540
+ },
541
+ options: {
542
+ responsive: true,
543
+ maintainAspectRatio: false,
544
+ cutout: '75%',
545
+ plugins: {
546
+ legend: { display: false }
547
+ }
548
+ }
549
+ });
550
+
551
+ } catch (e) {}
552
+ }
553
+
554
+ async function fetchSkills() {
555
+ try {
556
+ const res = await fetch('/api/skills');
557
+ const data = await res.json();
558
+ const list = document.getElementById('skill-list');
559
+ list.innerHTML = '';
560
+
561
+ if (data.skills && data.skills.length > 0) {
562
+ data.skills.forEach(skill => {
563
+ const isCore = skill.type === 'core';
564
+ const badge = isCore ? 'Core' : 'Custom';
565
+ list.innerHTML += `<div style="padding: 16px 0; border-bottom: 1px solid var(--glass-border); display: flex; justify-content: space-between; align-items: center;">
566
+ <span style="font-size: 14px; color: var(--text-main);">${skill.name}</span>
567
+ <span style="font-size: 10px; color: var(--text-muted); border: 1px solid var(--glass-border); padding: 4px 8px; border-radius: 20px;">${badge}</span>
568
+ </div>`;
569
+ });
570
+ } else {
571
+ list.innerHTML = '<p style="color: var(--text-muted);">No skills found.</p>';
572
+ }
573
+ } catch (e) {}
574
+ }
575
+
576
+ async function saveSkill() {
577
+ const name = document.getElementById('skill-name').value;
578
+ const desc = document.getElementById('skill-desc').value;
579
+ const steps = document.getElementById('skill-steps').value;
580
+
581
+ if(!name || !steps) {
582
+ alert("Name and Steps are required!");
583
+ return;
584
+ }
585
+
586
+ const markdown = `---
587
+ name: ${name}
588
+ description: ${desc}
589
+ ---
590
+
591
+ # ${name}
592
+
593
+ ## Execution Checklist
594
+ ${steps}
595
+ `;
596
+
597
+ try {
598
+ const res = await fetch('/api/skills', {
599
+ method: 'POST',
600
+ headers: {'Content-Type': 'application/json'},
601
+ body: JSON.stringify({ name, content: markdown })
602
+ });
603
+ const data = await res.json();
604
+ if(data.success) {
605
+ document.getElementById('skill-msg').innerText = "āœ… Skill saved safely to: " + data.path;
606
+ setTimeout(() => document.getElementById('skill-msg').innerText = "", 5000);
607
+ }
608
+ } catch (e) {
609
+ alert("Failed to save");
610
+ }
611
+ }
612
+
613
+ function renderGraph() {
614
+ if(networkInstance) return; // already rendered
615
+
616
+ // Mock graph data, in a real scenario we'd parse .docs/
617
+ const nodes = new vis.DataSet([
618
+ { id: 1, label: 'Client App', color: '#00ffaa', shape: 'box' },
619
+ { id: 2, label: 'API Gateway', color: '#00ffaa', shape: 'box' },
620
+ { id: 3, label: 'Auth Service', color: '#ff4757', shape: 'box' },
621
+ { id: 4, label: 'Database', color: '#f1f5f9', shape: 'database' },
622
+ ]);
623
+ const edges = new vis.DataSet([
624
+ { from: 1, to: 2, arrows: 'to' },
625
+ { from: 2, to: 3, arrows: 'to', label: 'verify token' },
626
+ { from: 3, to: 4, arrows: 'to' },
627
+ ]);
628
+
629
+ const container = document.getElementById('network-graph');
630
+ const data = { nodes, edges };
631
+ const options = {
632
+ nodes: {
633
+ font: { color: '#f4f4f4', face: 'Inter', size: 14 },
634
+ margin: 12,
635
+ borderWidth: 1,
636
+ color: { background: '#151515', border: '#2a2a2a' },
637
+ shapeProperties: { borderRadius: 6 }
638
+ },
639
+ edges: {
640
+ color: { color: '#2a2a2a' },
641
+ font: { color: '#888888', align: 'horizontal', background: '#0d0d0d', strokeWidth: 0, face: 'Courier New', size: 11 },
642
+ smooth: { type: 'cubicBezier' }
643
+ },
644
+ layout: {
645
+ hierarchical: {
646
+ direction: 'UD',
647
+ sortMethod: 'directed',
648
+ nodeSpacing: 150,
649
+ levelSeparation: 150
650
+ }
651
+ },
652
+ physics: {
653
+ hierarchicalRepulsion: { nodeDistance: 150 }
654
+ }
655
+ };
656
+ networkInstance = new vis.Network(container, data, options);
657
+ }
658
+
659
+ // --- API Exporter Logic ---
660
+ let detectedEndpoints = [];
661
+
662
+ function extractEndpoints() {
663
+ detectedEndpoints = [];
664
+ // Parse through all docs to find patterns like: - **GET** `/api/users`
665
+ docsData.forEach(doc => {
666
+ const regex = /(GET|POST|PUT|PATCH|DELETE)[^a-zA-Z0-9/]+([/\w:-]+)/gi;
667
+ let match;
668
+ while ((match = regex.exec(doc.content)) !== null) {
669
+ const method = match[1].toUpperCase();
670
+ const path = match[2];
671
+ if (!detectedEndpoints.find(e => e.method === method && e.path === path) && path.includes('/')) {
672
+ detectedEndpoints.push({ method, path });
673
+ }
674
+ }
675
+ });
676
+
677
+ const list = document.getElementById('endpoint-list');
678
+ if (detectedEndpoints.length > 0) {
679
+ document.getElementById('endpoint-count').innerText = `${detectedEndpoints.length} Endpoints`;
680
+ list.innerHTML = '';
681
+ detectedEndpoints.forEach((ep, index) => {
682
+ list.innerHTML += `<div style="padding: 16px; border-bottom: 1px solid var(--glass-border); display: flex; align-items: center; gap: 16px;">
683
+ <input type="checkbox" class="ep-checkbox" value="${index}" checked>
684
+ <span style="font-family: var(--font-mono); font-size: 13px; color: var(--text-muted); width: 60px;">${ep.method}</span>
685
+ <span style="font-family: var(--font-mono); font-size: 14px; color: var(--text-main);">${ep.path}</span>
686
+ </div>`;
687
+ });
688
+ }
689
+ }
690
+
691
+ function toggleAllEndpoints(cb) {
692
+ document.querySelectorAll('.ep-checkbox').forEach(el => el.checked = cb.checked);
693
+ }
694
+
695
+ function getHeuristicMock(path) {
696
+ const p = path.toLowerCase();
697
+ if (p.includes('user')) return { id: "USR-123", name: "John Doe", email: "john@example.com", role: "admin", createdAt: "2026-07-01T10:00:00Z" };
698
+ if (p.includes('product') || p.includes('item')) return { id: "PRD-456", name: "Premium Widget", price: 99.99, stock: 50 };
699
+ if (p.includes('order') || p.includes('transaction')) return { id: "ORD-789", total: 99.99, status: "completed" };
700
+ if (p.includes('auth') || p.includes('login')) return { token: "eyJhbGci...", user: { id: 1, role: "user" } };
701
+ return { id: "123", status: "success", message: "Operation completed successfully." };
702
+ }
703
+
704
+ function exportPostman() {
705
+ const selectedIndices = Array.from(document.querySelectorAll('.ep-checkbox:checked')).map(cb => parseInt(cb.value));
706
+ if (selectedIndices.length === 0) return alert("Select at least one endpoint!");
707
+
708
+ const collection = {
709
+ info: {
710
+ name: "SPARK Generated Collection",
711
+ description: "Auto-generated API Collection with positive & negative cases using SPARK AI heuristics.",
712
+ schema: "https://schema.getpostman.com/json/collection/v2.1.0/collection.json"
713
+ },
714
+ item: []
715
+ };
716
+
717
+ selectedIndices.forEach(idx => {
718
+ const ep = detectedEndpoints[idx];
719
+ const mock = getHeuristicMock(ep.path);
720
+
721
+ // Construct URL
722
+ const urlParts = ep.path.split('/').filter(p => p);
723
+ const pathArr = urlParts.map(p => p.startsWith(':') ? `{{${p.replace(':','')}}}` : p);
724
+
725
+ const item = {
726
+ name: `${ep.method} ${ep.path}`,
727
+ item: [
728
+ {
729
+ name: "Positive Case (200 OK)",
730
+ request: {
731
+ method: ep.method,
732
+ header: [{ key: "Authorization", value: "Bearer {{token}}" }, { key: "Content-Type", value: "application/json" }],
733
+ body: ep.method !== 'GET' ? { mode: "raw", raw: JSON.stringify(mock, null, 2) } : undefined,
734
+ url: { raw: `{{baseUrl}}/${pathArr.join('/')}`, host: ["{{baseUrl}}"], path: pathArr }
735
+ },
736
+ response: [{
737
+ name: "Success",
738
+ status: "OK",
739
+ code: 200,
740
+ header: [{ key: "Content-Type", value: "application/json" }],
741
+ body: JSON.stringify({ success: true, data: mock }, null, 2)
742
+ }]
743
+ },
744
+ {
745
+ name: "Negative Case (400 Bad Request)",
746
+ request: {
747
+ method: ep.method,
748
+ header: [{ key: "Authorization", value: "Bearer {{token}}" }, { key: "Content-Type", value: "application/json" }],
749
+ body: ep.method !== 'GET' ? { mode: "raw", raw: "{}" } : undefined,
750
+ url: { raw: `{{baseUrl}}/${pathArr.join('/')}`, host: ["{{baseUrl}}"], path: pathArr }
751
+ },
752
+ response: [{
753
+ name: "Bad Request",
754
+ status: "Bad Request",
755
+ code: 400,
756
+ header: [{ key: "Content-Type", value: "application/json" }],
757
+ body: JSON.stringify({ success: false, error: { code: "VALIDATION_ERROR", message: "Invalid parameters provided" } }, null, 2)
758
+ }]
759
+ },
760
+ {
761
+ name: "Negative Case (401 Unauthorized)",
762
+ request: {
763
+ method: ep.method,
764
+ header: [], // No auth
765
+ url: { raw: `{{baseUrl}}/${pathArr.join('/')}`, host: ["{{baseUrl}}"], path: pathArr }
766
+ },
767
+ response: [{
768
+ name: "Unauthorized",
769
+ status: "Unauthorized",
770
+ code: 401,
771
+ header: [{ key: "Content-Type", value: "application/json" }],
772
+ body: JSON.stringify({ success: false, error: { code: "UNAUTHORIZED", message: "Missing or invalid token" } }, null, 2)
773
+ }]
774
+ }
775
+ ]
776
+ };
777
+ collection.item.push(item);
778
+ });
779
+
780
+ downloadFile(JSON.stringify(collection, null, 2), 'SPARK_API_Collection.json', 'application/json');
781
+ }
782
+
783
+ function exportEnv() {
784
+ const envContent = `BASE_URL=http://localhost:3000\nTOKEN=your_jwt_token_here\nNODE_ENV=development\n`;
785
+ downloadFile(envContent, '.env.example', 'text/plain');
786
+ }
787
+
788
+ function downloadFile(content, filename, contentType) {
789
+ const blob = new Blob([content], { type: contentType });
790
+ const url = URL.createObjectURL(blob);
791
+ const a = document.createElement('a');
792
+ a.href = url;
793
+ a.download = filename;
794
+ a.click();
795
+ URL.revokeObjectURL(url);
796
+ }
797
+
798
+ // Init
799
+ fetchStatus();
800
+ fetchDocs();
801
+
802
+ </script>
803
+ </body>
804
+ </html>
@@ -0,0 +1,186 @@
1
+ import fs from 'fs';
2
+ import path from 'path';
3
+ import http from 'http';
4
+ import { fileURLToPath } from 'url';
5
+
6
+ const __filename = fileURLToPath(import.meta.url);
7
+ const __dirname = path.dirname(__filename);
8
+
9
+ const PORT = 4321;
10
+ const PUBLIC_DIR = path.join(__dirname, 'public');
11
+ const DOCS_DIR = path.join(process.cwd(), '.docs');
12
+ const LOCK_FILE = path.join(process.cwd(), '.spark-lock.json');
13
+
14
+ const server = http.createServer((req, res) => {
15
+ res.setHeader('Access-Control-Allow-Origin', '*');
16
+
17
+ // Handle JSON body for POST
18
+ const getBody = (request) => new Promise((resolve) => {
19
+ let body = '';
20
+ request.on('data', chunk => body += chunk.toString());
21
+ request.on('end', () => {
22
+ try { resolve(JSON.parse(body)); } catch(e) { resolve({}); }
23
+ });
24
+ });
25
+
26
+ // API Route: Get all markdown docs
27
+ if (req.url === '/api/docs' && req.method === 'GET') {
28
+ res.setHeader('Content-Type', 'application/json');
29
+ if (!fs.existsSync(DOCS_DIR)) {
30
+ return res.end(JSON.stringify({ error: 'No .docs/ directory found in this project. Please run project-scanner skill first.' }));
31
+ }
32
+
33
+ try {
34
+ const files = fs.readdirSync(DOCS_DIR).filter(f => f.endsWith('.md'));
35
+ const docs = files.map(filename => {
36
+ const content = fs.readFileSync(path.join(DOCS_DIR, filename), 'utf-8');
37
+ return { filename, content };
38
+ });
39
+ return res.end(JSON.stringify({ docs }));
40
+ } catch (e) {
41
+ return res.end(JSON.stringify({ error: e.message }));
42
+ }
43
+ }
44
+
45
+ // API Route: Get File Tree (for Heatmap)
46
+ if (req.url === '/api/tree' && req.method === 'GET') {
47
+ res.setHeader('Content-Type', 'application/json');
48
+ const ignoreDirs = ['node_modules', '.git', '.docs', '.claude', '.cursor', '.codex', 'dist', 'build'];
49
+
50
+ function scanDir(dir, base = '') {
51
+ let results = [];
52
+ try {
53
+ const items = fs.readdirSync(dir);
54
+ for (const item of items) {
55
+ if (ignoreDirs.includes(item) || item.startsWith('.')) continue;
56
+ const fullPath = path.join(dir, item);
57
+ const relPath = path.join(base, item);
58
+ const stat = fs.statSync(fullPath);
59
+ if (stat.isDirectory()) {
60
+ results.push({ name: item, type: 'dir', path: relPath, children: scanDir(fullPath, relPath) });
61
+ } else {
62
+ results.push({ name: item, type: 'file', path: relPath });
63
+ }
64
+ }
65
+ } catch (e) {}
66
+ return results;
67
+ }
68
+
69
+ return res.end(JSON.stringify({ tree: scanDir(process.cwd()) }));
70
+ }
71
+
72
+ // API Route: Get SPARK Readme (for Guide)
73
+ if (req.url === '/api/readme' && req.method === 'GET') {
74
+ res.setHeader('Content-Type', 'application/json');
75
+ const readmePath = path.join(__dirname, '../../README.md');
76
+ if (fs.existsSync(readmePath)) {
77
+ return res.end(JSON.stringify({ content: fs.readFileSync(readmePath, 'utf-8') }));
78
+ }
79
+ return res.end(JSON.stringify({ content: '# Documentation not found' }));
80
+ }
81
+
82
+ // API Route: Save Custom Skill (for Skill Studio)
83
+ if (req.url === '/api/skills' && req.method === 'GET') {
84
+ res.setHeader('Content-Type', 'application/json');
85
+ let skills = [];
86
+
87
+ // 1. Core skills
88
+ const coreDir = path.join(__dirname, '../../skills');
89
+ if (fs.existsSync(coreDir)) {
90
+ try {
91
+ const items = fs.readdirSync(coreDir);
92
+ for (const item of items) {
93
+ const stat = fs.statSync(path.join(coreDir, item));
94
+ if (stat.isDirectory()) skills.push({ name: item, type: 'core' });
95
+ }
96
+ } catch (e) {}
97
+ }
98
+
99
+ // 2. Custom skills
100
+ const customDir = path.join(process.cwd(), '.agent', 'skills');
101
+ if (fs.existsSync(customDir)) {
102
+ try {
103
+ const items = fs.readdirSync(customDir);
104
+ for (const item of items) {
105
+ const stat = fs.statSync(path.join(customDir, item));
106
+ if (stat.isDirectory()) skills.push({ name: item, type: 'custom' });
107
+ }
108
+ } catch (e) {}
109
+ }
110
+
111
+ return res.end(JSON.stringify({ skills }));
112
+ }
113
+
114
+ if (req.url === '/api/skills' && req.method === 'POST') {
115
+ res.setHeader('Content-Type', 'application/json');
116
+ getBody(req).then(data => {
117
+ if (!data.name || !data.content) {
118
+ return res.end(JSON.stringify({ error: 'Missing name or content' }));
119
+ }
120
+
121
+ // Save to a safe, user-level directory that SPARK updates won't touch
122
+ // The Anthropic standard allows putting skills in .agent/skills/
123
+ const customSkillsDir = path.join(process.cwd(), '.agent', 'skills', data.name);
124
+
125
+ try {
126
+ fs.mkdirSync(customSkillsDir, { recursive: true });
127
+ fs.writeFileSync(path.join(customSkillsDir, 'SKILL.md'), data.content, 'utf-8');
128
+ return res.end(JSON.stringify({ success: true, path: customSkillsDir }));
129
+ } catch (e) {
130
+ return res.end(JSON.stringify({ error: e.message }));
131
+ }
132
+ });
133
+ return;
134
+ }
135
+
136
+ // API Route: Get Spark Lock status
137
+ if (req.url === '/api/status' && req.method === 'GET') {
138
+ res.setHeader('Content-Type', 'application/json');
139
+ let status = { installed: false, agents: [] };
140
+ if (fs.existsSync(LOCK_FILE)) {
141
+ try {
142
+ status = JSON.parse(fs.readFileSync(LOCK_FILE, 'utf-8'));
143
+ status.installed = true;
144
+ } catch (e) {}
145
+ }
146
+ return res.end(JSON.stringify(status));
147
+ }
148
+
149
+ // Static File Server
150
+ let filePath = req.url === '/' ? '/index.html' : req.url;
151
+ // Prevent path traversal
152
+ filePath = path.normalize(filePath).replace(/^(\.\.[\/\\])+/, '');
153
+ const ext = path.extname(filePath);
154
+
155
+ const contentTypes = {
156
+ '.html': 'text/html',
157
+ '.css': 'text/css',
158
+ '.js': 'text/javascript',
159
+ '.png': 'image/png',
160
+ '.svg': 'image/svg+xml'
161
+ };
162
+
163
+ const contentType = contentTypes[ext] || 'text/plain';
164
+ const fullPath = path.join(PUBLIC_DIR, filePath);
165
+
166
+ if (fs.existsSync(fullPath) && fs.statSync(fullPath).isFile()) {
167
+ res.writeHead(200, { 'Content-Type': contentType });
168
+ fs.createReadStream(fullPath).pipe(res);
169
+ } else {
170
+ res.writeHead(404);
171
+ res.end('404 Not Found');
172
+ }
173
+ });
174
+
175
+ export function startDashboard() {
176
+ server.listen(PORT, async () => {
177
+ console.log(`\nšŸš€ SPARK Dashboard running at http://localhost:${PORT}`);
178
+ console.log(`Open this URL in your browser to view the AI Knowledge Base.`);
179
+ console.log(`Press Ctrl+C to stop.\n`);
180
+
181
+ // Attempt to open browser automatically
182
+ const { exec } = await import('child_process');
183
+ const startCmd = process.platform == 'darwin' ? 'open' : process.platform == 'win32' ? 'start' : 'xdg-open';
184
+ exec(`${startCmd} http://localhost:${PORT}`);
185
+ });
186
+ }