@adityaaria/spark 6.0.19 → 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.
- package/bin/spark-install.sh +86 -0
- package/package.json +1 -1
- package/skills/project-scanner/SKILL.md +2 -2
- package/src/cli/index.js +6 -0
- package/src/dashboard/public/index.html +657 -222
- package/src/dashboard/server.js +100 -0
package/bin/spark-install.sh
CHANGED
|
@@ -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
|
@@ -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.
|
|
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:**
|
|
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,12 @@ 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
|
+
|
|
17
23
|
if (command === 'dashboard' || command === 'ui') {
|
|
18
24
|
const { startDashboard } = await import('../dashboard/server.js');
|
|
19
25
|
startDashboard();
|
|
@@ -3,204 +3,222 @@
|
|
|
3
3
|
<head>
|
|
4
4
|
<meta charset="UTF-8">
|
|
5
5
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
6
|
-
<title>SPARK | Command Center</title>
|
|
6
|
+
<title>SPARK | Command Center v2</title>
|
|
7
7
|
<!-- Google Fonts -->
|
|
8
8
|
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@300;400;600;700&display=swap" rel="stylesheet">
|
|
9
9
|
<!-- Marked.js for Markdown parsing -->
|
|
10
10
|
<script src="https://cdn.jsdelivr.net/npm/marked/marked.min.js"></script>
|
|
11
|
-
<!--
|
|
12
|
-
<script src="https://
|
|
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>
|
|
13
15
|
<style>
|
|
14
16
|
:root {
|
|
15
|
-
--bg-color: #
|
|
16
|
-
--
|
|
17
|
-
--glass-border:
|
|
18
|
-
--accent
|
|
19
|
-
--accent: #
|
|
20
|
-
--danger: #
|
|
21
|
-
--
|
|
22
|
-
--text-
|
|
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;
|
|
23
28
|
}
|
|
24
29
|
|
|
25
|
-
* {
|
|
26
|
-
box-sizing: border-box;
|
|
27
|
-
margin: 0;
|
|
28
|
-
padding: 0;
|
|
29
|
-
}
|
|
30
|
+
* { box-sizing: border-box; margin: 0; padding: 0; }
|
|
30
31
|
|
|
31
32
|
body {
|
|
32
|
-
font-family:
|
|
33
|
+
font-family: var(--font-main);
|
|
33
34
|
background-color: var(--bg-color);
|
|
34
35
|
color: var(--text-main);
|
|
35
36
|
min-height: 100vh;
|
|
36
37
|
display: flex;
|
|
37
|
-
|
|
38
|
-
radial-gradient(circle at 15% 50%, var(--accent-glow), transparent 25%),
|
|
39
|
-
radial-gradient(circle at 85% 30%, rgba(255, 71, 87, 0.1), transparent 25%);
|
|
38
|
+
-webkit-font-smoothing: antialiased;
|
|
40
39
|
}
|
|
41
40
|
|
|
42
41
|
/* Sidebar */
|
|
43
42
|
.sidebar {
|
|
44
43
|
width: 280px;
|
|
45
|
-
background: var(--
|
|
44
|
+
background: var(--bg-color);
|
|
46
45
|
border-right: 1px solid var(--glass-border);
|
|
47
|
-
|
|
48
|
-
padding: 24px;
|
|
46
|
+
padding: 32px 24px;
|
|
49
47
|
display: flex;
|
|
50
48
|
flex-direction: column;
|
|
51
49
|
}
|
|
52
50
|
|
|
53
51
|
.logo {
|
|
54
|
-
font-size:
|
|
55
|
-
font-weight:
|
|
56
|
-
letter-spacing:
|
|
57
|
-
margin-bottom:
|
|
52
|
+
font-size: 20px;
|
|
53
|
+
font-weight: 400;
|
|
54
|
+
letter-spacing: 1px;
|
|
55
|
+
margin-bottom: 48px;
|
|
58
56
|
display: flex;
|
|
59
57
|
align-items: center;
|
|
60
|
-
gap:
|
|
58
|
+
gap: 12px;
|
|
59
|
+
color: var(--text-main);
|
|
61
60
|
}
|
|
62
61
|
|
|
63
|
-
.logo
|
|
64
|
-
|
|
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;
|
|
65
73
|
}
|
|
66
74
|
|
|
67
75
|
.nav-item {
|
|
68
|
-
padding: 12px
|
|
69
|
-
margin-bottom:
|
|
70
|
-
border-radius:
|
|
76
|
+
padding: 10px 12px;
|
|
77
|
+
margin-bottom: 4px;
|
|
78
|
+
border-radius: 6px;
|
|
71
79
|
cursor: pointer;
|
|
72
80
|
transition: all 0.2s ease;
|
|
73
81
|
color: var(--text-muted);
|
|
74
|
-
font-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
color: var(--text-main);
|
|
82
|
+
font-size: 14px;
|
|
83
|
+
font-weight: 400;
|
|
84
|
+
display: flex;
|
|
85
|
+
align-items: center;
|
|
86
|
+
gap: 12px;
|
|
80
87
|
}
|
|
81
88
|
|
|
89
|
+
.nav-item:hover { color: var(--text-main); }
|
|
82
90
|
.nav-item.active {
|
|
83
|
-
background: rgba(
|
|
84
|
-
color: var(--
|
|
85
|
-
border: 1px solid rgba(0, 255, 170, 0.2);
|
|
91
|
+
background: rgba(255, 255, 255, 0.05);
|
|
92
|
+
color: var(--text-main);
|
|
86
93
|
}
|
|
87
94
|
|
|
88
95
|
/* Main Content */
|
|
89
96
|
.content {
|
|
90
97
|
flex: 1;
|
|
91
|
-
padding:
|
|
98
|
+
padding: 48px;
|
|
92
99
|
overflow-y: auto;
|
|
93
100
|
height: 100vh;
|
|
101
|
+
display: flex;
|
|
102
|
+
flex-direction: column;
|
|
94
103
|
}
|
|
95
104
|
|
|
96
105
|
.header {
|
|
97
106
|
display: flex;
|
|
98
107
|
justify-content: space-between;
|
|
99
|
-
align-items:
|
|
100
|
-
margin-bottom:
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
.header h1 {
|
|
104
|
-
font-size: 32px;
|
|
105
|
-
font-weight: 700;
|
|
108
|
+
align-items: flex-end;
|
|
109
|
+
margin-bottom: 48px;
|
|
110
|
+
padding-bottom: 24px;
|
|
111
|
+
border-bottom: 1px solid var(--glass-border);
|
|
106
112
|
}
|
|
107
113
|
|
|
114
|
+
.header h1 { font-size: 28px; font-weight: 400; letter-spacing: -0.5px; }
|
|
115
|
+
|
|
108
116
|
.status-badge {
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
border-radius: 20px;
|
|
113
|
-
font-size: 14px;
|
|
114
|
-
font-weight: 600;
|
|
115
|
-
border: 1px solid rgba(0, 255, 170, 0.2);
|
|
117
|
+
color: var(--text-muted);
|
|
118
|
+
font-family: var(--font-mono);
|
|
119
|
+
font-size: 12px;
|
|
116
120
|
display: flex;
|
|
117
121
|
align-items: center;
|
|
118
|
-
gap:
|
|
122
|
+
gap: 8px;
|
|
119
123
|
}
|
|
120
|
-
|
|
121
124
|
.status-badge::before {
|
|
122
|
-
content: '';
|
|
123
|
-
|
|
124
|
-
width: 8px;
|
|
125
|
-
height: 8px;
|
|
126
|
-
border-radius: 50%;
|
|
127
|
-
background: var(--accent);
|
|
128
|
-
box-shadow: 0 0 8px var(--accent);
|
|
125
|
+
content: ''; display: block; width: 6px; height: 6px; border-radius: 50%;
|
|
126
|
+
background: var(--text-muted);
|
|
129
127
|
}
|
|
128
|
+
.status-badge.active::before { background: var(--accent); }
|
|
130
129
|
|
|
131
|
-
/* Cards / Panels */
|
|
132
130
|
.panel {
|
|
133
|
-
background: var(--
|
|
131
|
+
background: var(--panel-bg);
|
|
134
132
|
border: 1px solid var(--glass-border);
|
|
135
|
-
border-radius:
|
|
136
|
-
padding:
|
|
137
|
-
|
|
138
|
-
margin-bottom: 30px;
|
|
139
|
-
box-shadow: 0 8px 32px rgba(0, 0, 0, 0.2);
|
|
133
|
+
border-radius: 12px;
|
|
134
|
+
padding: 32px;
|
|
135
|
+
margin-bottom: 24px;
|
|
140
136
|
animation: fadeIn 0.4s ease forwards;
|
|
137
|
+
flex: 1;
|
|
141
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
142
|
|
|
143
143
|
@keyframes fadeIn {
|
|
144
|
-
from { opacity: 0; transform: translateY(
|
|
144
|
+
from { opacity: 0; transform: translateY(5px); }
|
|
145
145
|
to { opacity: 1; transform: translateY(0); }
|
|
146
146
|
}
|
|
147
147
|
|
|
148
|
-
/*
|
|
149
|
-
.
|
|
150
|
-
|
|
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;
|
|
151
159
|
}
|
|
152
|
-
.
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
font-weight:
|
|
160
|
+
.widget-value {
|
|
161
|
+
font-family: var(--font-mono);
|
|
162
|
+
font-size: 42px;
|
|
163
|
+
font-weight: 300;
|
|
156
164
|
color: var(--text-main);
|
|
165
|
+
margin-bottom: 4px;
|
|
166
|
+
letter-spacing: -1px;
|
|
157
167
|
}
|
|
158
|
-
.
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
.markdown-body ul, .markdown-body ol { margin-bottom: 16px; padding-left: 24px; color: var(--text-muted); }
|
|
162
|
-
.markdown-body li { margin-bottom: 8px; }
|
|
163
|
-
.markdown-body code {
|
|
164
|
-
background: rgba(0,0,0,0.3);
|
|
165
|
-
padding: 3px 6px;
|
|
166
|
-
border-radius: 4px;
|
|
167
|
-
font-family: monospace;
|
|
168
|
-
font-size: 14px;
|
|
169
|
-
color: #ff9e64;
|
|
170
|
-
}
|
|
171
|
-
.markdown-body pre {
|
|
172
|
-
background: #111520;
|
|
173
|
-
padding: 16px;
|
|
174
|
-
border-radius: 8px;
|
|
175
|
-
overflow-x: auto;
|
|
176
|
-
border: 1px solid var(--glass-border);
|
|
177
|
-
margin-bottom: 16px;
|
|
168
|
+
.widget-label {
|
|
169
|
+
font-size: 12px;
|
|
170
|
+
color: var(--text-muted);
|
|
178
171
|
}
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
172
|
+
|
|
173
|
+
.widget.accent-card {
|
|
174
|
+
background: var(--accent);
|
|
175
|
+
border: none;
|
|
176
|
+
color: #111;
|
|
183
177
|
}
|
|
178
|
+
.widget.accent-card .widget-value,
|
|
179
|
+
.widget.accent-card .widget-label { color: #111; font-weight: 500; }
|
|
184
180
|
|
|
185
|
-
/*
|
|
186
|
-
.
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
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;
|
|
190
199
|
}
|
|
191
|
-
.
|
|
192
|
-
.
|
|
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
|
+
|
|
193
203
|
.btn {
|
|
194
|
-
background: var(--text-main);
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
transition: transform 0.1s ease;
|
|
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;
|
|
202
211
|
}
|
|
203
|
-
.btn:hover {
|
|
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; }
|
|
204
222
|
|
|
205
223
|
</style>
|
|
206
224
|
</head>
|
|
@@ -208,156 +226,573 @@
|
|
|
208
226
|
|
|
209
227
|
<div class="sidebar">
|
|
210
228
|
<div class="logo">
|
|
211
|
-
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="var(--accent)" stroke-width="2"
|
|
212
|
-
<path d="M13 2L3 14h9l-1 8 10-12h-9l1-8z"/>
|
|
213
|
-
</svg>
|
|
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>
|
|
214
230
|
SPARK<span>UI</span>
|
|
215
231
|
</div>
|
|
216
|
-
|
|
217
|
-
|
|
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>
|
|
218
250
|
</div>
|
|
219
251
|
</div>
|
|
220
252
|
|
|
221
253
|
<div class="content">
|
|
222
254
|
<div class="header">
|
|
223
|
-
<
|
|
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>
|
|
224
259
|
<div class="status-badge" id="agent-status">Checking Agents...</div>
|
|
225
260
|
</div>
|
|
226
261
|
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
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>
|
|
231
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>
|
|
232
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
|
+
|
|
233
384
|
</div>
|
|
234
385
|
|
|
235
386
|
<script>
|
|
236
|
-
// Initialize Mermaid
|
|
237
|
-
mermaid.initialize({
|
|
238
|
-
startOnLoad: false,
|
|
239
|
-
theme: 'dark',
|
|
240
|
-
fontFamily: 'Inter'
|
|
241
|
-
});
|
|
242
|
-
|
|
243
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);
|
|
244
422
|
|
|
245
423
|
async function fetchDocs() {
|
|
246
424
|
try {
|
|
247
425
|
const res = await fetch('/api/docs');
|
|
248
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;
|
|
249
478
|
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
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;
|
|
253
508
|
}
|
|
254
509
|
|
|
255
|
-
|
|
256
|
-
|
|
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);
|
|
257
515
|
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
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>';
|
|
263
523
|
} else {
|
|
264
|
-
|
|
524
|
+
document.getElementById('danger-list').innerHTML = dangerListHTML;
|
|
265
525
|
}
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
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) {}
|
|
269
552
|
}
|
|
270
553
|
|
|
271
|
-
async function
|
|
554
|
+
async function fetchSkills() {
|
|
272
555
|
try {
|
|
273
|
-
const res = await fetch('/api/
|
|
556
|
+
const res = await fetch('/api/skills');
|
|
274
557
|
const data = await res.json();
|
|
275
|
-
const
|
|
276
|
-
|
|
277
|
-
|
|
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
|
+
});
|
|
278
570
|
} else {
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
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);
|
|
282
607
|
}
|
|
283
608
|
} catch (e) {
|
|
284
|
-
|
|
609
|
+
alert("Failed to save");
|
|
285
610
|
}
|
|
286
611
|
}
|
|
287
612
|
|
|
288
|
-
function
|
|
289
|
-
|
|
290
|
-
nav.innerHTML = '';
|
|
613
|
+
function renderGraph() {
|
|
614
|
+
if(networkInstance) return; // already rendered
|
|
291
615
|
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
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);
|
|
299
657
|
}
|
|
300
658
|
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
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
|
+
}
|
|
308
674
|
}
|
|
309
675
|
});
|
|
310
676
|
|
|
311
|
-
const
|
|
312
|
-
if (
|
|
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
|
+
}
|
|
313
690
|
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
const container = document.getElementById('content-container');
|
|
318
|
-
const parsedHTML = marked.parse(doc.content);
|
|
319
|
-
|
|
320
|
-
container.innerHTML = `
|
|
321
|
-
<div class="panel markdown-body">
|
|
322
|
-
${parsedHTML}
|
|
323
|
-
</div>
|
|
324
|
-
`;
|
|
691
|
+
function toggleAllEndpoints(cb) {
|
|
692
|
+
document.querySelectorAll('.ep-checkbox').forEach(el => el.checked = cb.checked);
|
|
693
|
+
}
|
|
325
694
|
|
|
326
|
-
|
|
327
|
-
const
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
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);
|
|
336
720
|
|
|
337
|
-
//
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
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);
|
|
344
778
|
});
|
|
779
|
+
|
|
780
|
+
downloadFile(JSON.stringify(collection, null, 2), 'SPARK_API_Collection.json', 'application/json');
|
|
345
781
|
}
|
|
346
782
|
|
|
347
|
-
function
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
`;
|
|
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);
|
|
361
796
|
}
|
|
362
797
|
|
|
363
798
|
// Init
|
package/src/dashboard/server.js
CHANGED
|
@@ -14,6 +14,15 @@ const LOCK_FILE = path.join(process.cwd(), '.spark-lock.json');
|
|
|
14
14
|
const server = http.createServer((req, res) => {
|
|
15
15
|
res.setHeader('Access-Control-Allow-Origin', '*');
|
|
16
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
|
+
|
|
17
26
|
// API Route: Get all markdown docs
|
|
18
27
|
if (req.url === '/api/docs' && req.method === 'GET') {
|
|
19
28
|
res.setHeader('Content-Type', 'application/json');
|
|
@@ -33,6 +42,97 @@ const server = http.createServer((req, res) => {
|
|
|
33
42
|
}
|
|
34
43
|
}
|
|
35
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
|
+
|
|
36
136
|
// API Route: Get Spark Lock status
|
|
37
137
|
if (req.url === '/api/status' && req.method === 'GET') {
|
|
38
138
|
res.setHeader('Content-Type', 'application/json');
|