@vendian/cli 0.0.36 → 0.0.37

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/cli-wrapper.mjs CHANGED
@@ -2363,7 +2363,7 @@ function buildLocalServeEventStreamArgs({ agentsDir: agentsDir2 = "./agents", co
2363
2363
  }
2364
2364
 
2365
2365
  // src/version.js
2366
- var CLI_VERSION = true ? "0.0.36" : process.env.npm_package_version || "0.0.0-dev";
2366
+ var CLI_VERSION = true ? "0.0.37" : process.env.npm_package_version || "0.0.0-dev";
2367
2367
 
2368
2368
  // src/dev-server.js
2369
2369
  var __dirname = path8.dirname(fileURLToPath(import.meta.url));
@@ -0,0 +1,259 @@
1
+ :root {
2
+ --bg: #ffffff;
3
+ --bg-page: #f8f9fb;
4
+ --bg-card: #ffffff;
5
+ --bg-elevated: #f4f5f7;
6
+ --bg-hover: #f0f1f3;
7
+ --bg-active: #e8eaed;
8
+ --border: #e3e5e8;
9
+ --border-hover: #cdd0d5;
10
+ --border-focus: #6366f1;
11
+ --text: #1a1d24;
12
+ --text-secondary: #555d6b;
13
+ --text-dim: #8a919e;
14
+ --accent: #6366f1;
15
+ --accent-solid: #6366f1;
16
+ --accent-bg: rgba(99,102,241,0.06);
17
+ --accent-border: rgba(99,102,241,0.18);
18
+ --green: #10b981;
19
+ --green-bg: rgba(16,185,129,0.07);
20
+ --green-border: rgba(16,185,129,0.2);
21
+ --red: #ef4444;
22
+ --red-bg: rgba(239,68,68,0.06);
23
+ --red-border: rgba(239,68,68,0.18);
24
+ --yellow: #f59e0b;
25
+ --yellow-bg: rgba(245,158,11,0.07);
26
+ --yellow-border: rgba(245,158,11,0.2);
27
+ --blue: #3b82f6;
28
+ --blue-bg: rgba(59,130,246,0.06);
29
+ --blue-border: rgba(59,130,246,0.2);
30
+ --radius: 10px;
31
+ --radius-lg: 14px;
32
+ --font: 'Inter', -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif;
33
+ --mono: 'JetBrains Mono', 'SF Mono', 'Fira Code', monospace;
34
+ --shadow: 0 1px 3px rgba(0,0,0,0.04), 0 2px 8px rgba(0,0,0,0.03);
35
+ --shadow-lg: 0 4px 16px rgba(0,0,0,0.06), 0 8px 32px rgba(0,0,0,0.04);
36
+ --transition: 0.15s ease;
37
+ }
38
+
39
+ * { margin: 0; padding: 0; box-sizing: border-box; }
40
+ body { font-family: var(--font); background: var(--bg-page); color: var(--text); min-height: 100vh; display: flex; flex-direction: column; font-size: 14px; line-height: 1.5; -webkit-font-smoothing: antialiased; }
41
+ ::selection { background: rgba(99,102,241,0.12); }
42
+
43
+ /* ─── Header ─── */
44
+ .header { display: flex; align-items: center; justify-content: space-between; padding: 12px 24px; border-bottom: 1px solid var(--border); background: var(--bg-card); position: sticky; top: 0; z-index: 50; min-height: 54px; }
45
+ .header-left { display: flex; align-items: center; gap: 12px; }
46
+ .logo { display: flex; align-items: center; gap: 9px; font-weight: 700; font-size: 15px; letter-spacing: -0.4px; color: var(--text); }
47
+ .logo svg { width: 26px; height: 26px; }
48
+ .header-right { display: flex; align-items: center; gap: 14px; }
49
+ .conn-badge { display: flex; align-items: center; gap: 7px; font-size: 12.5px; color: var(--text-dim); padding: 5px 12px; border-radius: 20px; background: var(--bg-elevated); border: 1px solid var(--border); }
50
+ .conn-badge .dot { width: 7px; height: 7px; border-radius: 50%; background: var(--text-dim); transition: background 0.3s; }
51
+ .conn-badge .dot.ok { background: var(--green); box-shadow: 0 0 5px rgba(16,185,129,0.3); }
52
+ .env-badge { font-size: 10.5px; font-weight: 700; padding: 3px 9px; border-radius: 4px; text-transform: uppercase; letter-spacing: 0.5px; background: var(--accent-bg); color: var(--accent); border: 1px solid var(--accent-border); cursor: pointer; transition: all var(--transition); }
53
+ .env-badge:hover { background: rgba(99,102,241,0.1); }
54
+
55
+ /* ─── Layout ─── */
56
+ .layout { display: flex; flex: 1; overflow: hidden; }
57
+
58
+ /* ─── Sidebar ─── */
59
+ .sidebar { width: 200px; min-width: 200px; border-right: 1px solid var(--border); background: var(--bg-card); display: flex; flex-direction: column; padding: 14px 0; gap: 1px; }
60
+ .nav-item { display: flex; align-items: center; gap: 10px; padding: 9px 16px; margin: 0 8px; border-radius: 7px; color: var(--text-secondary); font-size: 13px; font-weight: 500; cursor: pointer; transition: all var(--transition); user-select: none; }
61
+ .nav-item:hover { color: var(--text); background: var(--bg-hover); }
62
+ .nav-item.active { color: var(--accent); background: var(--accent-bg); font-weight: 600; }
63
+ .nav-item svg { width: 16px; height: 16px; flex-shrink: 0; opacity: 0.7; }
64
+ .nav-item.active svg { opacity: 1; }
65
+ .nav-sep { height: 1px; background: var(--border); margin: 8px 16px; }
66
+ .nav-label { font-size: 10px; font-weight: 700; color: var(--text-dim); padding: 8px 16px 3px; text-transform: uppercase; letter-spacing: 0.7px; margin: 0 8px; }
67
+
68
+ /* ─── Main ─── */
69
+ .main { flex: 1; overflow-y: auto; padding: 28px 36px; max-height: calc(100vh - 54px); }
70
+
71
+ /* ─── Page ─── */
72
+ .page { display: none; animation: fadeIn 0.15s ease; }
73
+ .page.active { display: block; }
74
+ @keyframes fadeIn { from { opacity: 0; transform: translateY(3px); } to { opacity: 1; transform: none; } }
75
+ .page-header { display: flex; justify-content: space-between; align-items: flex-start; margin-bottom: 20px; }
76
+ .page-title { font-size: 20px; font-weight: 700; letter-spacing: -0.4px; color: var(--text); }
77
+ .page-desc { font-size: 13px; color: var(--text-secondary); margin-top: 3px; }
78
+
79
+ /* ─── Folder Groups ─── */
80
+ .folder-group { margin-bottom: 28px; }
81
+ .folder-header { display: flex; align-items: center; gap: 7px; margin-bottom: 12px; font-size: 11px; font-weight: 700; color: var(--text-dim); text-transform: uppercase; letter-spacing: 0.5px; }
82
+ .folder-header svg { width: 13px; height: 13px; opacity: 0.5; }
83
+
84
+ /* ─── Cards / Grid ─── */
85
+ .grid { display: grid; grid-template-columns: repeat(auto-fill, minmax(300px, 1fr)); gap: 12px; }
86
+ .card { background: var(--bg-card); border: 1px solid var(--border); border-radius: var(--radius-lg); padding: 20px 22px; transition: all var(--transition); cursor: pointer; display: flex; flex-direction: column; box-shadow: var(--shadow); }
87
+ .card:hover { border-color: var(--border-hover); transform: translateY(-1px); box-shadow: var(--shadow-lg); }
88
+ .card-top { display: flex; align-items: flex-start; justify-content: space-between; margin-bottom: 3px; }
89
+ .card-title { font-size: 14px; font-weight: 650; color: var(--text); }
90
+ .card-dot { width: 9px; height: 9px; border-radius: 50%; flex-shrink: 0; margin-top: 4px; }
91
+ .card-dot.ready { background: var(--green); }
92
+ .card-dot.running { background: var(--blue); animation: pulse 1.5s infinite; }
93
+ .card-dot.error { background: var(--red); }
94
+ .card-dot.unknown { background: #d1d5db; }
95
+ @keyframes pulse { 0%,100% { opacity: 1; } 50% { opacity: 0.3; } }
96
+ .card-sub { font-size: 11.5px; font-family: var(--mono); color: var(--text-dim); margin-bottom: 8px; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
97
+ .card-desc { font-size: 12.5px; color: var(--text-secondary); line-height: 1.5; margin-bottom: 14px; display: -webkit-box; -webkit-line-clamp: 2; -webkit-box-orient: vertical; overflow: hidden; flex: 1; }
98
+ .card-meta { display: flex; flex-wrap: wrap; gap: 6px; margin-top: auto; }
99
+ .tag { display: inline-flex; align-items: center; gap: 4px; font-size: 11px; font-weight: 600; padding: 3px 8px; border-radius: 5px; background: var(--bg-elevated); border: 1px solid var(--border); color: var(--text-secondary); }
100
+ .tag svg { width: 11px; height: 11px; }
101
+ .tag.trigger { color: var(--accent); border-color: var(--accent-border); background: var(--accent-bg); }
102
+ .tag.cred { color: #92400e; border-color: var(--yellow-border); background: var(--yellow-bg); }
103
+ .tag.ok { color: var(--green); border-color: var(--green-border); background: var(--green-bg); }
104
+ .tag.missing { color: var(--red); border-color: var(--red-border); background: var(--red-bg); }
105
+
106
+ /* ─── Buttons ─── */
107
+ .btn { display: inline-flex; align-items: center; gap: 7px; padding: 9px 16px; border-radius: 7px; font-size: 12.5px; font-weight: 600; border: none; cursor: pointer; transition: all var(--transition); font-family: var(--font); }
108
+ .btn-primary { background: var(--accent-solid); color: white; }
109
+ .btn-primary:hover { background: #4f46e5; box-shadow: 0 3px 10px rgba(99,102,241,0.2); }
110
+ .btn-ghost { background: var(--bg-card); color: var(--text-secondary); border: 1px solid var(--border); }
111
+ .btn-ghost:hover { color: var(--text); border-color: var(--border-hover); background: var(--bg-hover); }
112
+ .btn-danger { background: var(--red-bg); color: var(--red); border: 1px solid var(--red-border); }
113
+ .btn-danger:hover { background: rgba(239,68,68,0.1); }
114
+ .btn-sm { padding: 6px 10px; font-size: 11.5px; border-radius: 5px; }
115
+ .btn-success { background: var(--green-bg); color: var(--green); border: 1px solid var(--green-border); }
116
+
117
+ /* ─── Form ─── */
118
+ .input { width: 100%; padding: 10px 13px; border-radius: 7px; border: 1px solid var(--border); background: var(--bg-card); color: var(--text); font-size: 13.5px; font-family: var(--font); outline: none; }
119
+ .input:focus { border-color: var(--border-focus); box-shadow: 0 0 0 3px rgba(99,102,241,0.07); }
120
+
121
+ /* ─── Validate ─── */
122
+ .val-grid { display: grid; grid-template-columns: repeat(auto-fill, minmax(280px, 1fr)); gap: 10px; margin-bottom: 20px; }
123
+ .val-card { background: var(--bg-card); border: 1px solid var(--border); border-radius: var(--radius); padding: 14px 16px; cursor: pointer; transition: all var(--transition); display: flex; align-items: center; gap: 12px; box-shadow: var(--shadow); }
124
+ .val-card:hover { border-color: var(--border-hover); background: var(--bg-hover); }
125
+ .val-card.selected { border-color: var(--accent); background: var(--accent-bg); }
126
+ .val-card .vc-icon { width: 34px; height: 34px; border-radius: 8px; display: flex; align-items: center; justify-content: center; background: var(--bg-elevated); border: 1px solid var(--border); flex-shrink: 0; font-size: 15px; }
127
+ .val-card .vc-info { flex: 1; min-width: 0; }
128
+ .val-card .vc-name { font-size: 13px; font-weight: 600; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
129
+ .val-card .vc-path { font-size: 11px; color: var(--text-dim); font-family: var(--mono); white-space: nowrap; overflow: hidden; text-overflow: ellipsis; margin-top: 1px; }
130
+ .val-card .vc-badge { font-size: 15px; flex-shrink: 0; }
131
+ .val-results { background: var(--bg-card); border: 1px solid var(--border); border-radius: var(--radius-lg); overflow: hidden; box-shadow: var(--shadow); }
132
+ .val-results-head { display: flex; align-items: center; justify-content: space-between; padding: 14px 20px; border-bottom: 1px solid var(--border); background: var(--bg-elevated); }
133
+ .val-results-head h3 { font-size: 13px; font-weight: 650; }
134
+ .val-results-body { padding: 16px 20px; max-height: 380px; overflow-y: auto; }
135
+ .val-section { margin-bottom: 14px; }
136
+ .val-section:last-child { margin-bottom: 0; }
137
+ .val-section-title { font-size: 10.5px; font-weight: 700; text-transform: uppercase; letter-spacing: 0.4px; margin-bottom: 6px; }
138
+ .val-section-title.errors { color: var(--red); }
139
+ .val-section-title.warnings { color: var(--yellow); }
140
+ .val-section-title.success { color: var(--green); }
141
+ .val-item { display: flex; align-items: flex-start; gap: 8px; padding: 8px 12px; margin-bottom: 3px; border-radius: 7px; font-size: 12.5px; line-height: 1.5; }
142
+ .val-item.error { background: var(--red-bg); color: #991b1b; border: 1px solid var(--red-border); }
143
+ .val-item.warning { background: var(--yellow-bg); color: #78350f; border: 1px solid var(--yellow-border); }
144
+ .val-item.success { background: var(--green-bg); color: #065f46; border: 1px solid var(--green-border); }
145
+ .val-item .vi-icon { flex-shrink: 0; padding-top: 1px; }
146
+
147
+ /* ─── Serve ─── */
148
+ .serve-controls { display: flex; align-items: center; gap: 12px; }
149
+ .serve-status { display: flex; align-items: center; gap: 8px; font-size: 13px; font-weight: 500; padding: 7px 14px; border-radius: 7px; background: var(--bg-card); border: 1px solid var(--border); color: var(--text-secondary); }
150
+ .serve-status .dot { width: 9px; height: 9px; border-radius: 50%; }
151
+ .serve-select-bar { display: flex; align-items: center; gap: 9px; padding: 10px 14px; border-radius: 8px; background: var(--bg-card); border: 1px solid var(--border); box-shadow: var(--shadow); margin-bottom: 14px; font-size: 13px; font-weight: 600; cursor: pointer; user-select: none; }
152
+ .serve-select-bar:hover { background: var(--bg-hover); }
153
+ .serve-select-bar input { width: 16px; height: 16px; accent-color: var(--accent-solid); cursor: pointer; }
154
+ .serve-select-bar .count { margin-left: auto; font-weight: 400; color: var(--text-dim); font-size: 12px; }
155
+ .serve-folder { margin-bottom: 20px; }
156
+ .serve-folder-title { display: flex; align-items: center; gap: 6px; margin-bottom: 8px; font-size: 10.5px; font-weight: 700; color: var(--text-dim); text-transform: uppercase; letter-spacing: 0.5px; }
157
+ .serve-folder-title svg { width: 12px; height: 12px; opacity: 0.5; }
158
+ .serve-grid { display: grid; grid-template-columns: repeat(auto-fill, minmax(300px, 1fr)); gap: 10px; }
159
+ .serve-card { background: var(--bg-card); border: 1px solid var(--border); border-radius: var(--radius-lg); padding: 16px 18px; cursor: pointer; transition: all var(--transition); display: flex; align-items: center; gap: 12px; user-select: none; box-shadow: var(--shadow); }
160
+ .serve-card:hover { border-color: var(--border-hover); }
161
+ .serve-card.checked { border-color: var(--accent); background: var(--accent-bg); }
162
+ .serve-card input { width: 17px; height: 17px; accent-color: var(--accent-solid); cursor: pointer; flex-shrink: 0; }
163
+ .serve-card .sc-info { flex: 1; min-width: 0; }
164
+ .serve-card .sc-name { font-size: 13.5px; font-weight: 600; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
165
+ .serve-card .sc-path { font-size: 11px; color: var(--text-dim); font-family: var(--mono); white-space: nowrap; overflow: hidden; text-overflow: ellipsis; margin-top: 2px; }
166
+ .serve-card .sc-triggers { font-size: 10.5px; color: var(--text-dim); margin-top: 3px; }
167
+
168
+ /* Live agent grid */
169
+ .live-grid { display: grid; grid-template-columns: repeat(auto-fill, minmax(280px, 1fr)); gap: 10px; margin-bottom: 20px; }
170
+ .live-card { background: var(--bg-card); border: 1px solid var(--border); border-radius: var(--radius-lg); padding: 14px 16px; display: flex; align-items: center; gap: 12px; box-shadow: var(--shadow); }
171
+ .live-card .lc-dot { width: 9px; height: 9px; border-radius: 50%; flex-shrink: 0; }
172
+ .live-card .lc-dot.ready { background: var(--green); }
173
+ .live-card .lc-dot.preparing { background: var(--yellow); animation: pulse 1.5s infinite; }
174
+ .live-card .lc-dot.running { background: var(--blue); animation: pulse 1.2s infinite; }
175
+ .live-card .lc-dot.error { background: var(--red); }
176
+ .live-card .lc-dot.unknown { background: #d1d5db; }
177
+ .live-card .lc-info { flex: 1; min-width: 0; }
178
+ .live-card .lc-name { font-size: 13px; font-weight: 600; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
179
+ .live-card .lc-detail { font-size: 11px; color: var(--text-dim); margin-top: 2px; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
180
+ .live-card .lc-detail.err { color: var(--red); }
181
+ .live-card .lc-badge { font-size: 10px; font-weight: 700; padding: 3px 8px; border-radius: 4px; text-transform: uppercase; letter-spacing: 0.2px; flex-shrink: 0; border: 1px solid; }
182
+ .live-card .lc-badge.ready { background: var(--green-bg); color: var(--green); border-color: var(--green-border); }
183
+ .live-card .lc-badge.preparing { background: var(--yellow-bg); color: #92400e; border-color: var(--yellow-border); }
184
+ .live-card .lc-badge.running { background: var(--blue-bg); color: var(--blue); border-color: var(--blue-border); }
185
+ .live-card .lc-badge.error { background: var(--red-bg); color: var(--red); border-color: var(--red-border); }
186
+ .live-card .lc-badge.unknown { background: var(--bg-elevated); color: var(--text-dim); border-color: var(--border); }
187
+
188
+ /* Activity feed */
189
+ .feed { background: var(--bg-card); border: 1px solid var(--border); border-radius: var(--radius-lg); box-shadow: var(--shadow); overflow: hidden; }
190
+ .feed-title { font-size: 10.5px; font-weight: 700; color: var(--text-dim); text-transform: uppercase; letter-spacing: 0.4px; padding: 12px 18px 8px; border-bottom: 1px solid var(--border); background: var(--bg-elevated); }
191
+ .feed-items { max-height: 280px; overflow-y: auto; padding: 6px 18px; }
192
+ .feed-item { display: flex; align-items: flex-start; gap: 9px; padding: 8px 0; border-bottom: 1px solid var(--border); font-size: 12.5px; }
193
+ .feed-item:last-child { border-bottom: none; }
194
+ .feed-time { font-size: 10.5px; font-family: var(--mono); color: var(--text-dim); white-space: nowrap; min-width: 52px; padding-top: 1px; }
195
+ .feed-icon { flex-shrink: 0; font-size: 11px; padding-top: 2px; }
196
+ .feed-body { flex: 1; min-width: 0; }
197
+ .feed-agent { font-weight: 600; font-size: 11.5px; color: var(--text); }
198
+ .feed-msg { color: var(--text-secondary); margin-top: 1px; font-size: 12px; }
199
+ .feed-msg.err { color: var(--red); }
200
+ .feed-msg.ok { color: var(--green); }
201
+
202
+ /* ─── Logs ─── */
203
+ .log-box { background: #1e1e2e; border: 1px solid var(--border); border-radius: var(--radius-lg); padding: 16px 18px; font-family: var(--mono); font-size: 12px; line-height: 1.8; max-height: 480px; overflow-y: auto; color: #cdd6f4; box-shadow: var(--shadow); }
204
+ .log-box .line { padding: 1px 0; }
205
+ .log-box .line.err { color: #f38ba8; }
206
+ .log-box .line.warn { color: #fab387; }
207
+ .log-box .line.info { color: #89b4fa; }
208
+
209
+ /* ─── Settings ─── */
210
+ .settings-section { margin-bottom: 28px; }
211
+ .settings-section-title { font-size: 10.5px; font-weight: 700; color: var(--text-dim); text-transform: uppercase; letter-spacing: 0.5px; margin-bottom: 10px; }
212
+ .conn-summary { display: flex; align-items: center; gap: 14px; margin-bottom: 18px; padding: 14px 18px; background: var(--bg-card); border: 1px solid var(--border); border-radius: var(--radius-lg); max-width: 560px; box-shadow: var(--shadow); }
213
+ .conn-summary .cs-item { display: flex; align-items: center; gap: 7px; font-size: 13px; color: var(--text-secondary); }
214
+ .conn-summary .cs-dot { width: 8px; height: 8px; border-radius: 50%; }
215
+ .conn-summary .cs-dot.green { background: var(--green); }
216
+ .conn-summary .cs-dot.red { background: var(--red); }
217
+ .settings-list { display: grid; gap: 8px; max-width: 560px; }
218
+ .settings-card { background: var(--bg-card); border: 1px solid var(--border); border-radius: var(--radius-lg); padding: 16px 18px; display: flex; align-items: center; gap: 14px; transition: all var(--transition); box-shadow: var(--shadow); cursor: pointer; }
219
+ .settings-card:hover { border-color: var(--border-hover); }
220
+ .settings-card.is-active { border-color: var(--green-border); background: var(--green-bg); }
221
+ .settings-card.needs-login { border-color: var(--border); opacity: 0.7; }
222
+ .settings-card .sc-left { flex: 1; min-width: 0; }
223
+ .settings-card .sc-label { font-size: 13.5px; font-weight: 650; }
224
+ .settings-card .sc-url { font-size: 11px; font-family: var(--mono); color: var(--text-dim); margin-top: 1px; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
225
+ .settings-card .sc-email { font-size: 11.5px; color: var(--text-secondary); margin-top: 2px; }
226
+ .settings-card .sc-right { display: flex; align-items: center; gap: 8px; flex-shrink: 0; }
227
+ .settings-badge { font-size: 10.5px; font-weight: 700; padding: 3px 9px; border-radius: 4px; white-space: nowrap; border: 1px solid; }
228
+ .settings-badge.active { background: var(--green-bg); color: var(--green); border-color: var(--green-border); }
229
+ .settings-badge.signed-in { background: var(--blue-bg); color: var(--blue); border-color: var(--blue-border); }
230
+ .settings-badge.disconnected { background: var(--bg-elevated); color: var(--text-dim); border-color: var(--border); }
231
+
232
+ /* ─── Empty ─── */
233
+ .empty { text-align: center; padding: 48px 20px; }
234
+ .empty-icon { font-size: 36px; margin-bottom: 14px; opacity: 0.35; }
235
+ .empty h3 { font-size: 16px; font-weight: 650; margin-bottom: 6px; }
236
+ .empty p { font-size: 13px; color: var(--text-secondary); margin-bottom: 18px; max-width: 320px; margin-left: auto; margin-right: auto; line-height: 1.5; }
237
+
238
+ /* ─── Modal ─── */
239
+ .modal-bg { display:none; position:fixed; inset:0; background:rgba(0,0,0,0.25); z-index:200; align-items:center; justify-content:center; backdrop-filter: blur(3px); }
240
+ .modal-bg.open { display:flex; }
241
+ .modal { background:var(--bg-card); border:1px solid var(--border); border-radius:var(--radius-lg); padding:24px; width:400px; max-width:90vw; box-shadow: var(--shadow-lg); }
242
+ .modal h3 { font-size: 16px; font-weight: 700; margin-bottom: 4px; }
243
+ .modal .modal-desc { font-size: 12.5px; color: var(--text-secondary); margin-bottom: 16px; }
244
+ .modal-actions { display:flex; justify-content:flex-end; gap:8px; margin-top:16px; }
245
+
246
+ /* ─── Scrollbar ─── */
247
+ ::-webkit-scrollbar { width: 5px; }
248
+ ::-webkit-scrollbar-track { background: transparent; }
249
+ ::-webkit-scrollbar-thumb { background: #d4d4d8; border-radius: 3px; }
250
+ ::-webkit-scrollbar-thumb:hover { background: #a1a1aa; }
251
+
252
+ @media (max-width: 840px) {
253
+ .sidebar { width: 52px; min-width: 52px; }
254
+ .nav-item span { display: none; }
255
+ .nav-item { justify-content: center; padding: 9px; margin: 0 5px; }
256
+ .nav-label { display: none; }
257
+ .main { padding: 16px; }
258
+ .grid, .serve-grid, .live-grid { grid-template-columns: 1fr; }
259
+ }
@@ -0,0 +1,156 @@
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>Vendian</title>
7
+ <link rel="stylesheet" href="css/app.css">
8
+ </head>
9
+ <body>
10
+
11
+ <div class="header">
12
+ <div class="header-left">
13
+ <div class="logo">
14
+ <svg viewBox="0 0 28 28" fill="none"><rect width="28" height="28" rx="7" fill="#6366f1"/><path d="M7 20L14 8L21 20H7Z" fill="white" fill-opacity="0.95"/></svg>
15
+ <span>Vendian</span>
16
+ </div>
17
+ </div>
18
+ <div class="header-right">
19
+ <div class="conn-badge"><span class="dot" id="conn-dot"></span><span id="conn-text">Idle</span></div>
20
+ <div class="env-badge" id="env-badge">—</div>
21
+ </div>
22
+ </div>
23
+
24
+ <div class="layout">
25
+ <aside class="sidebar">
26
+ <div class="nav-label">Workspace</div>
27
+ <div class="nav-item active" data-page="agents">
28
+ <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><rect x="3" y="3" width="7" height="7" rx="2"/><rect x="14" y="3" width="7" height="7" rx="2"/><rect x="3" y="14" width="7" height="7" rx="2"/><rect x="14" y="14" width="7" height="7" rx="2"/></svg>
29
+ <span>Agents</span>
30
+ </div>
31
+ <div class="nav-item" data-page="validate">
32
+ <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M9 12l2 2 4-4"/><circle cx="12" cy="12" r="9"/></svg>
33
+ <span>Validate</span>
34
+ </div>
35
+ <div class="nav-sep"></div>
36
+ <div class="nav-label">Runtime</div>
37
+ <div class="nav-item" data-page="serve">
38
+ <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><polygon points="6,3 20,12 6,21"/></svg>
39
+ <span>Serve</span>
40
+ </div>
41
+ <div class="nav-item" data-page="logs">
42
+ <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M14 2H6a2 2 0 00-2 2v16a2 2 0 002 2h12a2 2 0 002-2V8z"/><polyline points="14,2 14,8 20,8"/><line x1="16" y1="13" x2="8" y2="13"/><line x1="16" y1="17" x2="8" y2="17"/></svg>
43
+ <span>Logs</span>
44
+ </div>
45
+ <div class="nav-sep"></div>
46
+ <div class="nav-label">Account</div>
47
+ <div class="nav-item" data-page="settings">
48
+ <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><circle cx="12" cy="12" r="3"/><path d="M19.4 15a1.65 1.65 0 00.33 1.82l.06.06a2 2 0 010 2.83 2 2 0 01-2.83 0l-.06-.06a1.65 1.65 0 00-1.82-.33 1.65 1.65 0 00-1 1.51V21a2 2 0 01-4 0v-.09A1.65 1.65 0 009 19.4a1.65 1.65 0 00-1.82.33l-.06.06a2 2 0 01-2.83-2.83l.06-.06A1.65 1.65 0 004.68 15a1.65 1.65 0 00-1.51-1H3a2 2 0 010-4h.09A1.65 1.65 0 004.6 9a1.65 1.65 0 00-.33-1.82l-.06-.06a2 2 0 012.83-2.83l.06.06A1.65 1.65 0 009 4.68a1.65 1.65 0 001-1.51V3a2 2 0 014 0v.09a1.65 1.65 0 001 1.51 1.65 1.65 0 001.82-.33l.06-.06a2 2 0 012.83 2.83l-.06.06A1.65 1.65 0 0019.4 9a1.65 1.65 0 001.51 1H21a2 2 0 010 4h-.09a1.65 1.65 0 00-1.51 1z"/></svg>
49
+ <span>Settings</span>
50
+ </div>
51
+ </aside>
52
+
53
+ <div class="main">
54
+ <!-- ═══ Agents ═══ -->
55
+ <div class="page active" id="page-agents">
56
+ <div class="page-header">
57
+ <div><div class="page-title">Agents</div><div class="page-desc" id="agents-path">Scanning...</div></div>
58
+ <button class="btn btn-primary" id="btn-new-agent">
59
+ <svg width="13" height="13" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5"><line x1="12" y1="5" x2="12" y2="19"/><line x1="5" y1="12" x2="19" y2="12"/></svg>
60
+ New Agent
61
+ </button>
62
+ </div>
63
+ <div id="agents-content"></div>
64
+ <div class="empty" id="agents-empty" style="display:none">
65
+ <div class="empty-icon">📦</div>
66
+ <h3>No agents found</h3>
67
+ <p>Create your first agent or check that your agents directory has manifest.yaml files.</p>
68
+ <button class="btn btn-primary" id="btn-empty-create">Create Agent</button>
69
+ </div>
70
+ </div>
71
+
72
+ <!-- ═══ Validate ═══ -->
73
+ <div class="page" id="page-validate">
74
+ <div class="page-header">
75
+ <div><div class="page-title">Validate</div><div class="page-desc">Check manifests, credentials, imports, and deployment readiness.</div></div>
76
+ <button class="btn btn-primary" id="btn-validate-all">
77
+ <svg width="13" height="13" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5"><path d="M9 12l2 2 4-4"/><circle cx="12" cy="12" r="9"/></svg>
78
+ Validate All
79
+ </button>
80
+ </div>
81
+ <div class="val-grid" id="validate-grid"></div>
82
+ <div id="validate-results"></div>
83
+ </div>
84
+
85
+ <!-- ═══ Serve ═══ -->
86
+ <div class="page" id="page-serve">
87
+ <div class="page-header">
88
+ <div><div class="page-title">Local Serve</div><div class="page-desc">Run agents locally, connected to the Vendian backend.</div></div>
89
+ <div class="serve-controls">
90
+ <div class="serve-status"><span class="dot" id="serve-dot" style="background:#d1d5db"></span><span id="serve-label">Stopped</span></div>
91
+ <button class="btn btn-primary" id="btn-start">Start</button>
92
+ <button class="btn btn-danger" id="btn-stop" style="display:none">Stop</button>
93
+ </div>
94
+ </div>
95
+
96
+ <div id="serve-picker-section">
97
+ <div class="serve-select-bar" id="serve-select-bar">
98
+ <input type="checkbox" id="cb-all" checked>
99
+ <label for="cb-all" style="cursor:pointer">Select all</label>
100
+ <span class="count" id="serve-count">0</span>
101
+ </div>
102
+ <div id="serve-picker"></div>
103
+ </div>
104
+
105
+ <div id="serve-dashboard" style="display:none">
106
+ <div id="serve-activity-text" style="font-size:12.5px;color:var(--text-secondary);margin-bottom:14px"></div>
107
+ <div class="live-grid" id="live-cards"></div>
108
+ <div class="feed">
109
+ <div class="feed-title">Activity</div>
110
+ <div class="feed-items" id="feed-items"><div style="color:var(--text-dim);font-size:12px;padding:10px 0">Waiting for events...</div></div>
111
+ </div>
112
+ </div>
113
+ </div>
114
+
115
+ <!-- ═══ Logs ═══ -->
116
+ <div class="page" id="page-logs">
117
+ <div class="page-header">
118
+ <div><div class="page-title">Raw Logs</div><div class="page-desc">Event stream from the serve process.</div></div>
119
+ <button class="btn btn-ghost" id="btn-clear-logs">Clear</button>
120
+ </div>
121
+ <div class="log-box" id="all-logs"><div class="line" data-placeholder="1" style="color:#6c7086">Start serving to see logs here.</div></div>
122
+ </div>
123
+
124
+ <!-- ═══ Settings ═══ -->
125
+ <div class="page" id="page-settings">
126
+ <div class="page-header"><div><div class="page-title">Settings</div><div class="page-desc">Backend connections and authentication.</div></div></div>
127
+ <div class="conn-summary" id="conn-summary"><div class="cs-item"><span class="cs-dot"></span>Checking...</div></div>
128
+ <div class="settings-section">
129
+ <div class="settings-section-title">Environments</div>
130
+ <div class="settings-list" id="backends-list"></div>
131
+ </div>
132
+ <div class="settings-section">
133
+ <div class="settings-section-title">Sign In via Terminal</div>
134
+ <div style="font-size:12.5px;color:var(--text-secondary);margin-bottom:8px;max-width:520px">Run the login command to authenticate, then click an environment above to activate it.</div>
135
+ <div style="font-family:var(--mono);font-size:12px;padding:10px 14px;background:var(--bg-elevated);border-radius:7px;border:1px solid var(--border);color:var(--text-secondary);max-width:520px;display:inline-block">vendian login --backend <span style="color:var(--accent);font-weight:600">&lt;local|dev|staging|prod&gt;</span></div>
136
+ </div>
137
+ </div>
138
+ </div>
139
+ </div>
140
+
141
+ <!-- Create Agent Modal -->
142
+ <div class="modal-bg" id="modal-bg">
143
+ <div class="modal">
144
+ <h3>Create Agent</h3>
145
+ <div class="modal-desc">Scaffold a new agent with manifest, agent.py, and requirements.</div>
146
+ <input class="input" id="create-input" placeholder="Agent name (e.g. Invoice Processor)">
147
+ <div class="modal-actions">
148
+ <button class="btn btn-ghost" id="btn-modal-cancel">Cancel</button>
149
+ <button class="btn btn-primary" id="btn-modal-create">Create</button>
150
+ </div>
151
+ </div>
152
+ </div>
153
+
154
+ <script type="module" src="js/app.js"></script>
155
+ </body>
156
+ </html>
@@ -0,0 +1,48 @@
1
+ import { state, esc, groupByFolder, statusDotClass } from './state.js';
2
+
3
+ export function renderAgents() {
4
+ const content = document.getElementById('agents-content');
5
+ const empty = document.getElementById('agents-empty');
6
+ if (!state.agents.length) { content.innerHTML = ''; empty.style.display = ''; return; }
7
+ empty.style.display = 'none';
8
+
9
+ const groups = groupByFolder(state.agents);
10
+ const sortedFolders = Object.keys(groups).sort();
11
+ let html = '';
12
+
13
+ for (const folder of sortedFolders) {
14
+ const folderAgents = groups[folder];
15
+ html += `<div class="folder-group">`;
16
+ if (sortedFolders.length > 1 || folder !== '.') {
17
+ html += `<div class="folder-header"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M22 19a2 2 0 01-2 2H4a2 2 0 01-2-2V5a2 2 0 012-2h5l2 3h9a2 2 0 012 2z"/></svg>${esc(folder)}</div>`;
18
+ }
19
+ html += `<div class="grid">`;
20
+ for (const a of folderAgents) {
21
+ const tc = a.triggerCount || (a.triggers || []).length;
22
+ const cc = a.credentialCount || (a.credentials || []).length;
23
+ html += `<div class="card" data-path="${esc(a.path)}">
24
+ <div class="card-top">
25
+ <div class="card-title">${esc(a.displayName || a.name)}</div>
26
+ <div class="card-dot ${statusDotClass(a.runtimeStatus)}"></div>
27
+ </div>
28
+ <div class="card-sub">${esc(a.relativePath)}</div>
29
+ ${a.description ? `<div class="card-desc">${esc(a.description)}</div>` : '<div class="card-desc" style="color:var(--text-dim);font-style:italic">No description</div>'}
30
+ <div class="card-meta">
31
+ ${tc ? `<span class="tag trigger"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M13 2L3 14h9l-1 8 10-12h-9l1-8z"/></svg>${tc} trigger${tc>1?'s':''}</span>` : ''}
32
+ ${cc ? `<span class="tag cred"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><rect x="3" y="11" width="18" height="11" rx="2"/><path d="M7 11V7a5 5 0 0110 0v4"/></svg>${cc} cred${cc>1?'s':''}</span>` : ''}
33
+ ${a.hasAgentPy ? '<span class="tag ok">agent.py</span>' : '<span class="tag missing">no agent.py</span>'}
34
+ ${a.version ? `<span class="tag">v${esc(a.version)}</span>` : ''}
35
+ </div>
36
+ </div>`;
37
+ }
38
+ html += `</div></div>`;
39
+ }
40
+ content.innerHTML = html;
41
+
42
+ // Delegate click
43
+ content.querySelectorAll('.card[data-path]').forEach(card => {
44
+ card.addEventListener('click', () => {
45
+ window.dispatchEvent(new CustomEvent('navigate', { detail: { page: 'validate', path: card.dataset.path } }));
46
+ });
47
+ });
48
+ }
@@ -0,0 +1,97 @@
1
+ import { state, api } from './state.js';
2
+ import { renderAgents } from './agents.js';
3
+ import { populateValidateGrid, runValidateAll, triggerValidateAgent } from './validate.js';
4
+ import { populateServePicker, toggleAll, startServe, stopServe, setServing, clearLogs } from './serve.js';
5
+ import { loadAuth } from './settings.js';
6
+
7
+ // ─── Navigation ───
8
+ function nav(page) {
9
+ document.querySelectorAll('.nav-item').forEach(n => n.classList.toggle('active', n.dataset.page === page));
10
+ document.querySelectorAll('.page').forEach(p => p.classList.toggle('active', p.id === 'page-' + page));
11
+ }
12
+
13
+ document.querySelectorAll('.nav-item').forEach(el => {
14
+ el.addEventListener('click', () => nav(el.dataset.page));
15
+ });
16
+
17
+ window.addEventListener('navigate', (e) => {
18
+ const { page, path } = e.detail;
19
+ nav(page);
20
+ if (page === 'validate' && path) triggerValidateAgent(path);
21
+ });
22
+
23
+ // ─── Load agents ───
24
+ async function loadAgents() {
25
+ try {
26
+ const d = await api('/agents');
27
+ state.agents = d.agents || [];
28
+ document.getElementById('agents-path').textContent = d.agentsDir || '';
29
+ renderAgents();
30
+ populateValidateGrid();
31
+ populateServePicker();
32
+ } catch (e) { console.error(e); }
33
+ }
34
+
35
+ // ─── Status polling ───
36
+ async function checkStatus() {
37
+ try {
38
+ const d = await api('/status');
39
+ setServing(d.serving);
40
+ const dot = document.getElementById('conn-dot');
41
+ const text = document.getElementById('conn-text');
42
+ if (d.serving && d.connected) { dot.classList.add('ok'); text.textContent = 'Connected'; }
43
+ else if (d.serving) { dot.classList.add('ok'); text.textContent = d.activity || 'Starting...'; }
44
+ else { dot.classList.remove('ok'); text.textContent = 'Idle'; }
45
+ } catch {
46
+ document.getElementById('conn-dot').classList.remove('ok');
47
+ document.getElementById('conn-text').textContent = 'Offline';
48
+ }
49
+ }
50
+
51
+ // ─── Create agent modal ───
52
+ const modalBg = document.getElementById('modal-bg');
53
+ const createInput = document.getElementById('create-input');
54
+
55
+ document.getElementById('btn-new-agent').addEventListener('click', openModal);
56
+ document.getElementById('btn-empty-create').addEventListener('click', openModal);
57
+ document.getElementById('btn-modal-cancel').addEventListener('click', closeModal);
58
+ document.getElementById('btn-modal-create').addEventListener('click', createAgent);
59
+ modalBg.addEventListener('click', (e) => { if (e.target === modalBg) closeModal(); });
60
+ createInput.addEventListener('keydown', (e) => {
61
+ if (e.key === 'Enter') createAgent();
62
+ if (e.key === 'Escape') closeModal();
63
+ });
64
+
65
+ function openModal() { modalBg.classList.add('open'); createInput.value = ''; setTimeout(() => createInput.focus(), 80); }
66
+ function closeModal() { modalBg.classList.remove('open'); }
67
+
68
+ async function createAgent() {
69
+ const name = createInput.value.trim();
70
+ if (!name) return;
71
+ const d = await api('/create', { method: 'POST', body: JSON.stringify({ name }) });
72
+ if (d.ok) { closeModal(); await loadAgents(); }
73
+ else alert(d.error || 'Failed');
74
+ }
75
+
76
+ // ─── Wire up serve buttons ───
77
+ document.getElementById('btn-start').addEventListener('click', startServe);
78
+ document.getElementById('btn-stop').addEventListener('click', stopServe);
79
+ document.getElementById('btn-clear-logs').addEventListener('click', clearLogs);
80
+ document.getElementById('btn-validate-all').addEventListener('click', runValidateAll);
81
+ document.getElementById('env-badge').addEventListener('click', () => nav('settings'));
82
+
83
+ // Select all
84
+ const cbAll = document.getElementById('cb-all');
85
+ cbAll.addEventListener('change', () => toggleAll(cbAll.checked));
86
+ document.getElementById('serve-select-bar').addEventListener('click', (e) => {
87
+ if (e.target.tagName === 'INPUT') return;
88
+ cbAll.checked = !cbAll.checked;
89
+ toggleAll(cbAll.checked);
90
+ });
91
+
92
+ // ─── Init ───
93
+ loadAgents();
94
+ loadAuth();
95
+ checkStatus();
96
+ setInterval(checkStatus, 4000);
97
+ setInterval(loadAgents, 8000);
@@ -0,0 +1,228 @@
1
+ import { state, api, esc, groupByFolder, formatTime } from './state.js';
2
+
3
+ // ─── Picker ───
4
+ let userSelections = null; // null = all selected (default), Set = explicit selections
5
+
6
+ export function populateServePicker() {
7
+ const container = document.getElementById('serve-picker');
8
+ const countEl = document.getElementById('serve-count');
9
+ if (!state.agents.length) {
10
+ container.innerHTML = '<div style="color:var(--text-dim);font-size:13px">No agents found.</div>';
11
+ countEl.textContent = '0';
12
+ return;
13
+ }
14
+ countEl.textContent = `${state.agents.length} agent${state.agents.length === 1 ? '' : 's'}`;
15
+
16
+ // Determine which are selected
17
+ const isSelected = (path) => userSelections === null || userSelections.has(path);
18
+
19
+ const groups = groupByFolder(state.agents);
20
+ const sortedFolders = Object.keys(groups).sort();
21
+ let html = '';
22
+
23
+ for (const folder of sortedFolders) {
24
+ html += `<div class="serve-folder">`;
25
+ if (sortedFolders.length > 1 || folder !== '.') {
26
+ html += `<div class="serve-folder-title"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M22 19a2 2 0 01-2 2H4a2 2 0 01-2-2V5a2 2 0 012-2h5l2 3h9a2 2 0 012 2z"/></svg>${esc(folder)}</div>`;
27
+ }
28
+ html += `<div class="serve-grid">`;
29
+ for (const a of groups[folder]) {
30
+ const tc = a.triggerCount || (a.triggers || []).length;
31
+ const checked = isSelected(a.path);
32
+ html += `<div class="serve-card${checked ? ' checked' : ''}" data-path="${esc(a.path)}">
33
+ <input type="checkbox" class="agent-cb" ${checked ? 'checked' : ''} value="${esc(a.path)}">
34
+ <div class="sc-info">
35
+ <div class="sc-name">${esc(a.displayName || a.name)}</div>
36
+ <div class="sc-path">${esc(a.relativePath)}</div>
37
+ ${tc ? `<div class="sc-triggers">${tc} trigger${tc > 1 ? 's' : ''}</div>` : ''}
38
+ </div>
39
+ </div>`;
40
+ }
41
+ html += `</div></div>`;
42
+ }
43
+ container.innerHTML = html;
44
+
45
+ // Wire up clicks
46
+ container.querySelectorAll('.serve-card').forEach(card => {
47
+ card.addEventListener('click', (e) => {
48
+ if (e.target.tagName === 'INPUT') return;
49
+ const cb = card.querySelector('input');
50
+ cb.checked = !cb.checked;
51
+ card.classList.toggle('checked', cb.checked);
52
+ syncSelectAll();
53
+ });
54
+ card.querySelector('input').addEventListener('change', () => {
55
+ card.classList.toggle('checked', card.querySelector('input').checked);
56
+ syncSelectAll();
57
+ });
58
+ });
59
+
60
+ syncSelectAll();
61
+ }
62
+
63
+ function syncSelectAll() {
64
+ const all = document.querySelectorAll('.agent-cb');
65
+ const allChecked = [...all].every(c => c.checked);
66
+ document.getElementById('cb-all').checked = allChecked;
67
+ // Track user state
68
+ if (allChecked) {
69
+ userSelections = null; // all selected = default
70
+ } else {
71
+ userSelections = new Set([...all].filter(c => c.checked).map(c => c.value));
72
+ }
73
+ }
74
+
75
+ export function toggleAll(checked) {
76
+ document.querySelectorAll('.agent-cb').forEach(cb => { cb.checked = checked; });
77
+ document.querySelectorAll('.serve-card').forEach(card => { card.classList.toggle('checked', checked); });
78
+ userSelections = checked ? null : new Set();
79
+ }
80
+
81
+ export function getSelectedPaths() {
82
+ return [...document.querySelectorAll('.agent-cb:checked')].map(cb => cb.value);
83
+ }
84
+
85
+ // ─── Controls ───
86
+ export async function startServe() {
87
+ const selected = getSelectedPaths();
88
+ if (!selected.length) { alert('Select at least one agent.'); return; }
89
+ const body = {};
90
+ if (selected.length < state.agents.length) {
91
+ body.agentsDir = commonParent(selected);
92
+ }
93
+ const d = await api('/serve/start', { method: 'POST', body: JSON.stringify(body) });
94
+ if (d.ok) setServing(true);
95
+ else alert(d.error || 'Failed to start');
96
+ }
97
+
98
+ export async function stopServe() {
99
+ await api('/serve/stop', { method: 'POST' });
100
+ setServing(false);
101
+ }
102
+
103
+ export function setServing(v) {
104
+ state.isServing = v;
105
+ const dot = document.getElementById('serve-dot');
106
+ const label = document.getElementById('serve-label');
107
+ const btnStart = document.getElementById('btn-start');
108
+ const btnStop = document.getElementById('btn-stop');
109
+ const picker = document.getElementById('serve-picker-section');
110
+ const dashboard = document.getElementById('serve-dashboard');
111
+ if (v) {
112
+ dot.style.background = 'var(--green)'; dot.style.boxShadow = '0 0 6px rgba(16,185,129,0.3)';
113
+ label.textContent = 'Running';
114
+ btnStart.style.display = 'none'; btnStop.style.display = '';
115
+ picker.style.display = 'none'; dashboard.style.display = '';
116
+ startPoll();
117
+ } else {
118
+ dot.style.background = '#d1d5db'; dot.style.boxShadow = 'none';
119
+ label.textContent = 'Stopped';
120
+ btnStart.style.display = ''; btnStop.style.display = 'none';
121
+ picker.style.display = ''; dashboard.style.display = 'none';
122
+ stopPoll();
123
+ }
124
+ }
125
+
126
+ // ─── Live Dashboard ───
127
+ function startPoll() { if (!state.pollTimer) state.pollTimer = setInterval(pollState, 800); pollState(); }
128
+ function stopPoll() { if (state.pollTimer) { clearInterval(state.pollTimer); state.pollTimer = null; } }
129
+
130
+ async function pollState() {
131
+ try {
132
+ const d = await api('/serve/state');
133
+ renderLiveCards(d.agents || []);
134
+ renderFeed(d.recentActivity || []);
135
+ if (d.activity) {
136
+ const el = document.getElementById('serve-activity-text');
137
+ if (el) el.textContent = d.activity;
138
+ }
139
+ } catch {}
140
+ // Raw logs
141
+ try {
142
+ const d = await api('/logs?since=' + state.logOffset);
143
+ if (d.logs?.length) {
144
+ d.logs.forEach(l => appendLogLine(l));
145
+ state.logOffset = d.total;
146
+ }
147
+ } catch {}
148
+ }
149
+
150
+ function renderLiveCards(agents) {
151
+ const container = document.getElementById('live-cards');
152
+ if (!agents.length) { container.innerHTML = '<div style="color:var(--text-dim);font-size:12px">Discovering...</div>'; return; }
153
+ container.innerHTML = agents.map(a => {
154
+ const s = a.status || 'unknown';
155
+ const label = s === 'ready' || s === 'completed' ? 'Ready' : s === 'running' ? 'Running' : s === 'preparing' ? 'Preparing' : s === 'error' ? 'Error' : 'Offline';
156
+ const cls = s === 'ready' || s === 'completed' ? 'ready' : s === 'running' ? 'running' : s === 'preparing' ? 'preparing' : s === 'error' ? 'error' : 'unknown';
157
+ const detail = a.errorMessage ? (a.errorMessage.length > 70 ? a.errorMessage.slice(0,70)+'…' : a.errorMessage) : a.progressMessage || a.relativePath;
158
+ const detailCls = a.errorMessage ? 'lc-detail err' : 'lc-detail';
159
+ return `<div class="live-card"><div class="lc-dot ${cls}"></div><div class="lc-info"><div class="lc-name">${esc(a.name)}</div><div class="${detailCls}">${esc(detail)}</div></div><div class="lc-badge ${cls}">${label}</div></div>`;
160
+ }).join('');
161
+ }
162
+
163
+ function renderFeed(items) {
164
+ const container = document.getElementById('feed-items');
165
+ if (!items.length) return;
166
+ container.innerHTML = items.slice(-20).map(e => {
167
+ const time = formatTime(e.timestamp);
168
+ const agent = esc(e._agentName || '');
169
+ const msg = fmtMsg(e);
170
+ const icon = fmtIcon(e);
171
+ const cls = e.level === 'error' || (e.eventType === 'job_completed' && e.success === false) ? 'feed-msg err' : (e.eventType === 'job_completed' && e.success !== false) ? 'feed-msg ok' : 'feed-msg';
172
+ return `<div class="feed-item"><span class="feed-time">${time}</span><span class="feed-icon">${icon}</span><div class="feed-body"><div class="feed-agent">${agent}</div><div class="${cls}">${esc(msg)}</div></div></div>`;
173
+ }).join('');
174
+ container.scrollTop = container.scrollHeight;
175
+ }
176
+
177
+ function fmtMsg(e) {
178
+ switch (e.eventType) {
179
+ case 'job_started': return 'Started a new job';
180
+ case 'job_completed': return e.success === false ? `Failed — ${e.message||'error'}` : 'Completed';
181
+ case 'agent_prepare_started': return 'Preparing...';
182
+ case 'agent_prepare_progress': return e.message || 'Preparing...';
183
+ case 'agent_prepare_completed': return e.success === false ? (e.message||'Setup failed') : 'Ready';
184
+ case 'error': return e.message || 'Error';
185
+ default: return e.message || e.eventType || '';
186
+ }
187
+ }
188
+
189
+ function fmtIcon(e) {
190
+ if (e.eventType === 'job_started') return '▶';
191
+ if (e.eventType === 'job_completed' && e.success !== false) return '✔';
192
+ if (e.eventType === 'job_completed' || e.eventType === 'error' || e.level === 'error') return '✖';
193
+ if (e.eventType?.includes('prepare')) return '⚙';
194
+ return '•';
195
+ }
196
+
197
+ export function appendLogLine(text) {
198
+ const box = document.getElementById('all-logs');
199
+ if (box.children.length === 1 && box.children[0].dataset.placeholder) box.innerHTML = '';
200
+ const div = document.createElement('div');
201
+ const cls = /error|traceback|exception/i.test(text) ? ' err' : /warn/i.test(text) ? ' warn' : /connected|ready|registered/i.test(text) ? ' info' : '';
202
+ div.className = 'line' + cls;
203
+ try {
204
+ const p = JSON.parse(text);
205
+ div.textContent = `[${formatTime(p.timestamp)}] ${p.type||'event'} ${p.message||p.relativePath||''}`;
206
+ } catch { div.textContent = text; }
207
+ box.appendChild(div);
208
+ if (box.children.length > 1000) box.removeChild(box.firstChild);
209
+ box.scrollTop = box.scrollHeight;
210
+ }
211
+
212
+ export function clearLogs() {
213
+ document.getElementById('all-logs').innerHTML = '<div class="line" data-placeholder="1" style="color:#6c7086">Cleared.</div>';
214
+ }
215
+
216
+ // ─── Helpers ───
217
+ function commonParent(paths) {
218
+ if (!paths.length) return '';
219
+ const sep = paths[0].includes('/') ? '/' : '\\';
220
+ const split = paths.map(p => p.split(/[\\/]/));
221
+ const first = split[0];
222
+ let common = [];
223
+ for (let i = 0; i < first.length; i++) {
224
+ if (split.every(s => s[i] === first[i])) common.push(first[i]);
225
+ else break;
226
+ }
227
+ return common.join(sep);
228
+ }
@@ -0,0 +1,84 @@
1
+ import { state, api, esc, capitalize } from './state.js';
2
+
3
+ export async function loadAuth() {
4
+ try {
5
+ state.authData = await api('/auth');
6
+ renderEnvBadge();
7
+ renderSettings();
8
+ } catch {}
9
+ }
10
+
11
+ function renderEnvBadge() {
12
+ const badge = document.getElementById('env-badge');
13
+ const url = state.authData.apiUrl || '';
14
+ if (!url) { badge.textContent = '?'; return; }
15
+ if (url.includes('localhost')) badge.textContent = 'LOCAL';
16
+ else if (url.includes('.dev.')) badge.textContent = 'DEV';
17
+ else if (url.includes('staging')) badge.textContent = 'STAGING';
18
+ else badge.textContent = 'PROD';
19
+ }
20
+
21
+ function renderSettings() {
22
+ const backends = state.authData.backends || [];
23
+ const activeBackend = backends.find(b => b.active);
24
+ const connectedCount = backends.filter(b => b.authenticated).length;
25
+
26
+ // Summary
27
+ const summary = document.getElementById('conn-summary');
28
+ if (activeBackend) {
29
+ summary.innerHTML = `<div class="cs-item"><span class="cs-dot green"></span>Connected to <strong style="margin-left:4px">${capitalize(activeBackend.key)}</strong></div>
30
+ ${activeBackend.email ? `<div class="cs-item" style="color:var(--text-dim);font-size:11.5px;margin-left:auto">${esc(activeBackend.email)}</div>` : ''}`;
31
+ } else {
32
+ summary.innerHTML = `<div class="cs-item"><span class="cs-dot red"></span>Not connected — run <code style="font-family:var(--mono);background:var(--bg-elevated);padding:2px 6px;border-radius:3px;font-size:11px">vendian login</code></div>`;
33
+ }
34
+
35
+ // Backend list
36
+ const list = document.getElementById('backends-list');
37
+ list.innerHTML = backends.map(b => {
38
+ const isActive = b.active;
39
+ const cardClass = isActive ? 'is-active' : (!b.authenticated ? 'needs-login' : '');
40
+ const badgeClass = isActive ? 'active' : b.authenticated ? 'signed-in' : 'disconnected';
41
+ const badgeText = isActive ? '● Active' : b.authenticated ? 'Signed in' : 'Not connected';
42
+ const action = !b.authenticated
43
+ ? `<button class="btn btn-sm btn-ghost" data-login="${b.key}">Sign in →</button>`
44
+ : (!isActive ? `<button class="btn btn-sm btn-success" data-switch="${b.key}">Activate</button>` : '');
45
+ return `<div class="settings-card ${cardClass}" data-backend="${b.key}">
46
+ <div class="sc-left">
47
+ <div class="sc-label">${capitalize(b.key)}</div>
48
+ <div class="sc-url">${esc(b.url)}</div>
49
+ ${b.email ? `<div class="sc-email">${esc(b.email)}</div>` : ''}
50
+ </div>
51
+ <div class="sc-right">
52
+ <span class="settings-badge ${badgeClass}">${badgeText}</span>
53
+ ${action}
54
+ </div>
55
+ </div>`;
56
+ }).join('');
57
+
58
+ // Wire buttons
59
+ list.querySelectorAll('[data-login]').forEach(btn => {
60
+ btn.addEventListener('click', (e) => { e.stopPropagation(); promptLogin(btn.dataset.login); });
61
+ });
62
+ list.querySelectorAll('[data-switch]').forEach(btn => {
63
+ btn.addEventListener('click', (e) => { e.stopPropagation(); switchBackend(btn.dataset.switch); });
64
+ });
65
+ list.querySelectorAll('.settings-card').forEach(card => {
66
+ card.addEventListener('click', () => {
67
+ const key = card.dataset.backend;
68
+ const b = backends.find(x => x.key === key);
69
+ if (b && b.authenticated && !b.active) switchBackend(key);
70
+ else if (b && !b.authenticated) promptLogin(key);
71
+ });
72
+ });
73
+ }
74
+
75
+ function promptLogin(key) {
76
+ alert(`To sign in to ${capitalize(key)}, run:\n\n vendian login --backend ${key}\n\nThen refresh this page.`);
77
+ }
78
+
79
+ async function switchBackend(key) {
80
+ const d = await api('/auth/switch', { method: 'POST', body: JSON.stringify({ backend: key }) });
81
+ if (d.ok) await loadAuth();
82
+ else if (d.needsLogin) promptLogin(key);
83
+ else alert(d.error || 'Switch failed');
84
+ }
@@ -0,0 +1,52 @@
1
+ // ─── State ───
2
+ export const state = {
3
+ agents: [],
4
+ authData: {},
5
+ logOffset: 0,
6
+ pollTimer: null,
7
+ isServing: false,
8
+ validateResults: {},
9
+ selectedValidateAgent: null,
10
+ };
11
+
12
+ // ─── API ───
13
+ export const api = (path, opts) =>
14
+ fetch('/api' + path, { headers: { 'Content-Type': 'application/json' }, ...opts }).then(r => r.json());
15
+
16
+ // ─── Utils ───
17
+ export function esc(s) {
18
+ if (!s) return '';
19
+ const d = document.createElement('span');
20
+ d.textContent = s;
21
+ return d.innerHTML;
22
+ }
23
+
24
+ export function capitalize(s) {
25
+ return s ? s.charAt(0).toUpperCase() + s.slice(1) : '';
26
+ }
27
+
28
+ export function formatTime(ts) {
29
+ if (!ts) return '';
30
+ try {
31
+ const d = new Date(ts);
32
+ return `${String(d.getHours()).padStart(2,'0')}:${String(d.getMinutes()).padStart(2,'0')}:${String(d.getSeconds()).padStart(2,'0')}`;
33
+ } catch { return ''; }
34
+ }
35
+
36
+ export function groupByFolder(list) {
37
+ const groups = {};
38
+ list.forEach(a => {
39
+ const parts = a.relativePath.replace(/\\/g, '/').split('/');
40
+ const folder = parts.length > 1 ? parts.slice(0, -1).join('/') : '.';
41
+ if (!groups[folder]) groups[folder] = [];
42
+ groups[folder].push(a);
43
+ });
44
+ return groups;
45
+ }
46
+
47
+ export function statusDotClass(s) {
48
+ if (s === 'ready' || s === 'completed') return 'ready';
49
+ if (s === 'running') return 'running';
50
+ if (s === 'error') return 'error';
51
+ return 'unknown';
52
+ }
@@ -0,0 +1,111 @@
1
+ import { state, api, esc } from './state.js';
2
+
3
+ export function populateValidateGrid() {
4
+ const grid = document.getElementById('validate-grid');
5
+ if (!state.agents.length) {
6
+ grid.innerHTML = '<div style="color:var(--text-dim);font-size:13px">No agents found.</div>';
7
+ return;
8
+ }
9
+ grid.innerHTML = state.agents.map(a => {
10
+ const result = state.validateResults[a.path];
11
+ const sel = state.selectedValidateAgent === a.path ? ' selected' : '';
12
+ const badge = result
13
+ ? (result.valid ? '<span style="color:var(--green);font-size:16px;font-weight:700">✓</span>' : '<span style="color:var(--red);font-size:16px;font-weight:700">✗</span>')
14
+ : '';
15
+ return `<div class="val-card${sel}" data-path="${esc(a.path)}">
16
+ <div class="vc-icon">🤖</div>
17
+ <div class="vc-info"><div class="vc-name">${esc(a.displayName || a.name)}</div><div class="vc-path">${esc(a.relativePath)}</div></div>
18
+ <div class="vc-badge">${badge}</div>
19
+ </div>`;
20
+ }).join('');
21
+
22
+ grid.querySelectorAll('.val-card').forEach(card => {
23
+ card.addEventListener('click', () => selectAndValidate(card.dataset.path));
24
+ });
25
+ }
26
+
27
+ async function selectAndValidate(path) {
28
+ state.selectedValidateAgent = path;
29
+ populateValidateGrid();
30
+ await runValidateSingle(path);
31
+ }
32
+
33
+ export function triggerValidateAgent(path) {
34
+ state.selectedValidateAgent = path;
35
+ populateValidateGrid();
36
+ runValidateSingle(path);
37
+ }
38
+
39
+ async function runValidateSingle(path) {
40
+ const container = document.getElementById('validate-results');
41
+ container.innerHTML = `<div class="val-results"><div class="val-results-head"><h3>Validating...</h3></div><div class="val-results-body" style="color:var(--text-dim)">Running checks...</div></div>`;
42
+
43
+ const d = await api('/validate', { method: 'POST', body: JSON.stringify({ path }) });
44
+ state.validateResults[path] = d;
45
+ populateValidateGrid();
46
+ renderResults(d, path);
47
+ }
48
+
49
+ export async function runValidateAll() {
50
+ const container = document.getElementById('validate-results');
51
+ container.innerHTML = `<div class="val-results"><div class="val-results-head"><h3>Validating ${state.agents.length} agents...</h3></div><div class="val-results-body" style="color:var(--text-dim)">Running...</div></div>`;
52
+
53
+ let allErrors = [], allWarnings = [], passed = 0, failed = 0;
54
+ for (const a of state.agents) {
55
+ const d = await api('/validate', { method: 'POST', body: JSON.stringify({ path: a.path }) });
56
+ state.validateResults[a.path] = d;
57
+ if (d.valid) passed++; else failed++;
58
+ if (d.errors?.length) allErrors.push({ agent: a.displayName || a.name, errors: d.errors });
59
+ if (d.warnings?.length) allWarnings.push({ agent: a.displayName || a.name, warnings: d.warnings });
60
+ }
61
+ populateValidateGrid();
62
+ renderAllResults(passed, failed, allErrors, allWarnings);
63
+ }
64
+
65
+ function renderResults(d, path) {
66
+ const agent = state.agents.find(a => a.path === path);
67
+ const name = agent ? (agent.displayName || agent.name) : 'Agent';
68
+ const container = document.getElementById('validate-results');
69
+ let body = '';
70
+
71
+ if (d.valid) {
72
+ body += `<div class="val-section"><div class="val-section-title success">All checks passed</div><div class="val-item success"><span class="vi-icon">✓</span>${esc(name)} is valid and ready to deploy</div></div>`;
73
+ }
74
+ if (d.errors?.length) {
75
+ body += `<div class="val-section"><div class="val-section-title errors">Errors (${d.errors.length})</div>`;
76
+ d.errors.forEach(e => { body += `<div class="val-item error"><span class="vi-icon">✗</span>${esc(typeof e === 'string' ? e : e.message || JSON.stringify(e))}</div>`; });
77
+ body += `</div>`;
78
+ }
79
+ if (d.warnings?.length) {
80
+ body += `<div class="val-section"><div class="val-section-title warnings">Warnings (${d.warnings.length})</div>`;
81
+ d.warnings.forEach(w => { body += `<div class="val-item warning"><span class="vi-icon">⚠</span>${esc(typeof w === 'string' ? w : w.message || JSON.stringify(w))}</div>`; });
82
+ body += `</div>`;
83
+ }
84
+ if (d.agent_id) body += `<div style="margin-top:10px;font-size:11px;color:var(--text-dim)">ID: <span style="font-family:var(--mono)">${esc(d.agent_id)}</span></div>`;
85
+
86
+ const badge = d.valid
87
+ ? '<span style="color:var(--green);font-size:12px;font-weight:700">✓ Passed</span>'
88
+ : '<span style="color:var(--red);font-size:12px;font-weight:700">✗ Failed</span>';
89
+ container.innerHTML = `<div class="val-results"><div class="val-results-head"><h3>${esc(name)}</h3>${badge}</div><div class="val-results-body">${body}</div></div>`;
90
+ }
91
+
92
+ function renderAllResults(passed, failed, allErrors, allWarnings) {
93
+ const container = document.getElementById('validate-results');
94
+ let body = `<div style="display:flex;gap:16px;margin-bottom:16px"><span style="color:var(--green);font-weight:700">✓ ${passed} passed</span>${failed ? `<span style="color:var(--red);font-weight:700">✗ ${failed} failed</span>` : ''}</div>`;
95
+
96
+ for (const { agent, errors } of allErrors) {
97
+ body += `<div class="val-section"><div class="val-section-title errors">${esc(agent)}</div>`;
98
+ errors.forEach(e => { body += `<div class="val-item error"><span class="vi-icon">✗</span>${esc(typeof e === 'string' ? e : e.message || JSON.stringify(e))}</div>`; });
99
+ body += `</div>`;
100
+ }
101
+ for (const { agent, warnings } of allWarnings) {
102
+ body += `<div class="val-section"><div class="val-section-title warnings">${esc(agent)}</div>`;
103
+ warnings.forEach(w => { body += `<div class="val-item warning"><span class="vi-icon">⚠</span>${esc(typeof w === 'string' ? w : w.message || JSON.stringify(w))}</div>`; });
104
+ body += `</div>`;
105
+ }
106
+
107
+ const badge = failed === 0
108
+ ? '<span style="color:var(--green);font-size:12px;font-weight:700">✓ All passed</span>'
109
+ : `<span style="color:var(--red);font-size:12px;font-weight:700">✗ ${failed} failed</span>`;
110
+ container.innerHTML = `<div class="val-results"><div class="val-results-head"><h3>All Agents (${passed + failed})</h3>${badge}</div><div class="val-results-body">${body}</div></div>`;
111
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@vendian/cli",
3
- "version": "0.0.36",
3
+ "version": "0.0.37",
4
4
  "description": "Public Vendian CLI bootstrapper and launcher",
5
5
  "license": "UNLICENSED",
6
6
  "private": false,
@@ -15,6 +15,7 @@
15
15
  "files": [
16
16
  "bin/",
17
17
  "cli-wrapper.mjs",
18
+ "dev-ui/",
18
19
  "LICENSE",
19
20
  "README.md"
20
21
  ],