@cluesmith/codev 1.1.0 → 1.1.1
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/dist/agent-farm/cli.d.ts.map +1 -1
- package/dist/agent-farm/cli.js +19 -0
- package/dist/agent-farm/cli.js.map +1 -1
- package/dist/agent-farm/commands/cleanup.d.ts.map +1 -1
- package/dist/agent-farm/commands/cleanup.js +18 -1
- package/dist/agent-farm/commands/cleanup.js.map +1 -1
- package/dist/agent-farm/commands/consult.d.ts +16 -0
- package/dist/agent-farm/commands/consult.d.ts.map +1 -0
- package/dist/agent-farm/commands/consult.js +51 -0
- package/dist/agent-farm/commands/consult.js.map +1 -0
- package/dist/agent-farm/commands/open.js +6 -6
- package/dist/agent-farm/commands/open.js.map +1 -1
- package/dist/agent-farm/commands/spawn.d.ts.map +1 -1
- package/dist/agent-farm/commands/spawn.js +48 -42
- package/dist/agent-farm/commands/spawn.js.map +1 -1
- package/dist/agent-farm/commands/start.d.ts.map +1 -1
- package/dist/agent-farm/commands/start.js +8 -14
- package/dist/agent-farm/commands/start.js.map +1 -1
- package/dist/agent-farm/commands/util.js +2 -2
- package/dist/agent-farm/commands/util.js.map +1 -1
- package/dist/agent-farm/db/errors.d.ts +4 -0
- package/dist/agent-farm/db/errors.d.ts.map +1 -1
- package/dist/agent-farm/db/errors.js +8 -0
- package/dist/agent-farm/db/errors.js.map +1 -1
- package/dist/agent-farm/servers/dashboard-server.js +113 -71
- package/dist/agent-farm/servers/dashboard-server.js.map +1 -1
- package/dist/agent-farm/servers/open-server.d.ts +9 -0
- package/dist/agent-farm/servers/open-server.d.ts.map +1 -0
- package/dist/agent-farm/servers/{annotate-server.js → open-server.js} +17 -15
- package/dist/agent-farm/servers/open-server.js.map +1 -0
- package/dist/agent-farm/servers/tower-server.js +4 -7
- package/dist/agent-farm/servers/tower-server.js.map +1 -1
- package/dist/agent-farm/state.d.ts +5 -0
- package/dist/agent-farm/state.d.ts.map +1 -1
- package/dist/agent-farm/state.js +17 -0
- package/dist/agent-farm/state.js.map +1 -1
- package/dist/agent-farm/types.d.ts +1 -1
- package/dist/agent-farm/types.d.ts.map +1 -1
- package/dist/agent-farm/utils/config.d.ts.map +1 -1
- package/dist/agent-farm/utils/config.js +13 -7
- package/dist/agent-farm/utils/config.js.map +1 -1
- package/dist/agent-farm/utils/port-registry.d.ts +1 -1
- package/dist/agent-farm/utils/port-registry.d.ts.map +1 -1
- package/dist/agent-farm/utils/port-registry.js +1 -1
- package/dist/agent-farm/utils/port-registry.js.map +1 -1
- package/dist/agent-farm/utils/shell.d.ts +19 -0
- package/dist/agent-farm/utils/shell.d.ts.map +1 -1
- package/dist/agent-farm/utils/shell.js +28 -0
- package/dist/agent-farm/utils/shell.js.map +1 -1
- package/dist/cli.d.ts.map +1 -1
- package/dist/cli.js +18 -0
- package/dist/cli.js.map +1 -1
- package/dist/commands/adopt.d.ts +3 -0
- package/dist/commands/adopt.d.ts.map +1 -1
- package/dist/commands/adopt.js +31 -25
- package/dist/commands/adopt.js.map +1 -1
- package/dist/commands/consult/index.d.ts +3 -2
- package/dist/commands/consult/index.d.ts.map +1 -1
- package/dist/commands/consult/index.js +128 -54
- package/dist/commands/consult/index.js.map +1 -1
- package/dist/commands/doctor.d.ts.map +1 -1
- package/dist/commands/doctor.js +30 -34
- package/dist/commands/doctor.js.map +1 -1
- package/dist/commands/eject.d.ts +18 -0
- package/dist/commands/eject.d.ts.map +1 -0
- package/dist/commands/eject.js +149 -0
- package/dist/commands/eject.js.map +1 -0
- package/dist/commands/init.d.ts +3 -0
- package/dist/commands/init.d.ts.map +1 -1
- package/dist/commands/init.js +32 -27
- package/dist/commands/init.js.map +1 -1
- package/dist/lib/projectlist-parser.d.ts +70 -0
- package/dist/lib/projectlist-parser.d.ts.map +1 -0
- package/dist/lib/projectlist-parser.js +200 -0
- package/dist/lib/projectlist-parser.js.map +1 -0
- package/dist/lib/skeleton.d.ts +41 -0
- package/dist/lib/skeleton.d.ts.map +1 -0
- package/dist/lib/skeleton.js +110 -0
- package/dist/lib/skeleton.js.map +1 -0
- package/dist/lib/templates.d.ts +2 -1
- package/dist/lib/templates.d.ts.map +1 -1
- package/dist/lib/templates.js +11 -10
- package/dist/lib/templates.js.map +1 -1
- package/package.json +4 -3
- package/{templates → skeleton}/DEPENDENCIES.md +2 -48
- package/{templates → skeleton}/agents/codev-updater.md +4 -3
- package/skeleton/bin/agent-farm +7 -0
- package/skeleton/docs/commands/agent-farm.md +469 -0
- package/skeleton/docs/commands/codev.md +253 -0
- package/skeleton/docs/commands/consult.md +286 -0
- package/skeleton/docs/commands/overview.md +107 -0
- package/{templates → skeleton}/protocols/experiment/protocol.md +2 -2
- package/{templates → skeleton}/protocols/spider/protocol.md +7 -7
- package/{templates/protocols/spider-solo → skeleton/protocols/spider}/templates/plan.md +22 -1
- package/{templates/protocols/spider-solo → skeleton/protocols/spider}/templates/spec.md +30 -1
- package/skeleton/protocols/tick/protocol.md +277 -0
- package/skeleton/resources/lessons-learned.md +30 -0
- package/skeleton/resources/workflow-reference.md +229 -0
- package/{templates → skeleton}/roles/architect.md +2 -0
- package/{templates → skeleton}/roles/builder.md +2 -0
- package/skeleton/roles/review-types/impl-review.md +56 -0
- package/skeleton/roles/review-types/integration-review.md +68 -0
- package/skeleton/roles/review-types/plan-review.md +59 -0
- package/skeleton/roles/review-types/pr-ready.md +72 -0
- package/skeleton/roles/review-types/spec-review.md +55 -0
- package/{templates → skeleton}/templates/projectlist.md +17 -16
- package/dist/agent-farm/servers/annotate-server.d.ts +0 -9
- package/dist/agent-farm/servers/annotate-server.d.ts.map +0 -1
- package/dist/agent-farm/servers/annotate-server.js.map +0 -1
- package/templates/annotate.html +0 -903
- package/templates/bin/agent-farm +0 -18
- package/templates/bin/annotate-server.js +0 -140
- package/templates/dashboard-split.html +0 -1679
- package/templates/dashboard.html +0 -149
- package/templates/protocols/spider/templates/plan.md +0 -169
- package/templates/protocols/spider/templates/review.md +0 -207
- package/templates/protocols/spider/templates/spec.md +0 -140
- package/templates/protocols/spider-solo/protocol.md +0 -619
- package/templates/protocols/tick/protocol.md +0 -250
- package/templates/tower.html +0 -1032
- /package/{templates/AGENTS.md → skeleton/AGENTS.md.template} +0 -0
- /package/{templates/CLAUDE.md → skeleton/CLAUDE.md.template} +0 -0
- /package/{templates → skeleton}/agents/architecture-documenter.md +0 -0
- /package/{templates → skeleton}/agents/spider-protocol-updater.md +0 -0
- /package/{templates → skeleton}/bin/codev-doctor +0 -0
- /package/{templates → skeleton}/builders.md +0 -0
- /package/{templates → skeleton}/config.json +0 -0
- /package/{templates → skeleton}/plans/.gitkeep +0 -0
- /package/{templates → skeleton}/protocols/experiment/templates/notes.md +0 -0
- /package/{templates → skeleton}/protocols/maintain/protocol.md +0 -0
- /package/{templates/protocols/spider-solo → skeleton/protocols/spider}/templates/review.md +0 -0
- /package/{templates → skeleton}/protocols/tick/templates/plan.md +0 -0
- /package/{templates → skeleton}/protocols/tick/templates/review.md +0 -0
- /package/{templates → skeleton}/protocols/tick/templates/spec.md +0 -0
- /package/{templates → skeleton}/reviews/.gitkeep +0 -0
- /package/{templates → skeleton}/roles/consultant.md +0 -0
- /package/{templates → skeleton}/specs/.gitkeep +0 -0
package/templates/tower.html
DELETED
|
@@ -1,1032 +0,0 @@
|
|
|
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>AF Control Tower</title>
|
|
7
|
-
<style>
|
|
8
|
-
* {
|
|
9
|
-
box-sizing: border-box;
|
|
10
|
-
margin: 0;
|
|
11
|
-
padding: 0;
|
|
12
|
-
}
|
|
13
|
-
|
|
14
|
-
:root {
|
|
15
|
-
--bg-primary: #1a1a1a;
|
|
16
|
-
--bg-secondary: #252525;
|
|
17
|
-
--bg-tertiary: #2a2a2a;
|
|
18
|
-
--border: #333;
|
|
19
|
-
--text-primary: #fff;
|
|
20
|
-
--text-secondary: #ccc;
|
|
21
|
-
--text-muted: #666;
|
|
22
|
-
--accent: #3b82f6;
|
|
23
|
-
--status-running: #22c55e;
|
|
24
|
-
--status-stopped: #666;
|
|
25
|
-
}
|
|
26
|
-
|
|
27
|
-
body {
|
|
28
|
-
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
|
|
29
|
-
background: var(--bg-primary);
|
|
30
|
-
color: var(--text-primary);
|
|
31
|
-
min-height: 100vh;
|
|
32
|
-
padding: 0;
|
|
33
|
-
font-size: 16px;
|
|
34
|
-
}
|
|
35
|
-
|
|
36
|
-
/* Header */
|
|
37
|
-
.header {
|
|
38
|
-
display: flex;
|
|
39
|
-
justify-content: space-between;
|
|
40
|
-
align-items: center;
|
|
41
|
-
padding: 20px 32px;
|
|
42
|
-
background: var(--bg-secondary);
|
|
43
|
-
border-bottom: 1px solid var(--border);
|
|
44
|
-
position: sticky;
|
|
45
|
-
top: 0;
|
|
46
|
-
z-index: 100;
|
|
47
|
-
}
|
|
48
|
-
|
|
49
|
-
.header h1 {
|
|
50
|
-
font-size: 24px;
|
|
51
|
-
font-weight: 600;
|
|
52
|
-
display: flex;
|
|
53
|
-
align-items: center;
|
|
54
|
-
gap: 12px;
|
|
55
|
-
}
|
|
56
|
-
|
|
57
|
-
.header h1 .emoji {
|
|
58
|
-
font-size: 28px;
|
|
59
|
-
}
|
|
60
|
-
|
|
61
|
-
.header-actions {
|
|
62
|
-
display: flex;
|
|
63
|
-
gap: 12px;
|
|
64
|
-
}
|
|
65
|
-
|
|
66
|
-
.btn {
|
|
67
|
-
padding: 10px 20px;
|
|
68
|
-
border-radius: 6px;
|
|
69
|
-
border: 1px solid var(--border);
|
|
70
|
-
background: var(--bg-tertiary);
|
|
71
|
-
color: var(--text-secondary);
|
|
72
|
-
cursor: pointer;
|
|
73
|
-
font-size: 15px;
|
|
74
|
-
transition: all 0.15s ease;
|
|
75
|
-
}
|
|
76
|
-
|
|
77
|
-
.btn:hover {
|
|
78
|
-
background: #333;
|
|
79
|
-
border-color: #444;
|
|
80
|
-
}
|
|
81
|
-
|
|
82
|
-
.btn-primary {
|
|
83
|
-
background: var(--accent);
|
|
84
|
-
border-color: var(--accent);
|
|
85
|
-
color: white;
|
|
86
|
-
}
|
|
87
|
-
|
|
88
|
-
.btn-primary:hover {
|
|
89
|
-
background: #2563eb;
|
|
90
|
-
}
|
|
91
|
-
|
|
92
|
-
.btn-launch {
|
|
93
|
-
flex-shrink: 0;
|
|
94
|
-
min-width: 100px;
|
|
95
|
-
}
|
|
96
|
-
|
|
97
|
-
.btn-small {
|
|
98
|
-
padding: 6px 12px;
|
|
99
|
-
font-size: 13px;
|
|
100
|
-
}
|
|
101
|
-
|
|
102
|
-
.btn-danger {
|
|
103
|
-
background: #dc2626;
|
|
104
|
-
border-color: #dc2626;
|
|
105
|
-
color: white;
|
|
106
|
-
}
|
|
107
|
-
|
|
108
|
-
.btn-danger:hover {
|
|
109
|
-
background: #b91c1c;
|
|
110
|
-
}
|
|
111
|
-
|
|
112
|
-
/* Main content - left aligned */
|
|
113
|
-
.main {
|
|
114
|
-
padding: 32px;
|
|
115
|
-
}
|
|
116
|
-
|
|
117
|
-
/* Section headers */
|
|
118
|
-
.section-header {
|
|
119
|
-
font-size: 18px;
|
|
120
|
-
font-weight: 600;
|
|
121
|
-
margin-bottom: 16px;
|
|
122
|
-
color: var(--text-secondary);
|
|
123
|
-
}
|
|
124
|
-
|
|
125
|
-
/* Empty state */
|
|
126
|
-
.empty-state {
|
|
127
|
-
padding: 48px 24px;
|
|
128
|
-
color: var(--text-muted);
|
|
129
|
-
}
|
|
130
|
-
|
|
131
|
-
.empty-state .icon {
|
|
132
|
-
font-size: 48px;
|
|
133
|
-
margin-bottom: 16px;
|
|
134
|
-
}
|
|
135
|
-
|
|
136
|
-
.empty-state h2 {
|
|
137
|
-
font-size: 20px;
|
|
138
|
-
font-weight: 500;
|
|
139
|
-
margin-bottom: 8px;
|
|
140
|
-
color: var(--text-secondary);
|
|
141
|
-
}
|
|
142
|
-
|
|
143
|
-
.empty-state p {
|
|
144
|
-
font-size: 16px;
|
|
145
|
-
margin-bottom: 24px;
|
|
146
|
-
}
|
|
147
|
-
|
|
148
|
-
.empty-state code {
|
|
149
|
-
background: var(--bg-tertiary);
|
|
150
|
-
padding: 4px 10px;
|
|
151
|
-
border-radius: 4px;
|
|
152
|
-
font-size: 15px;
|
|
153
|
-
}
|
|
154
|
-
|
|
155
|
-
/* Instance cards - 4 column grid */
|
|
156
|
-
.instances {
|
|
157
|
-
display: grid;
|
|
158
|
-
grid-template-columns: repeat(4, 1fr);
|
|
159
|
-
gap: 16px;
|
|
160
|
-
margin-bottom: 32px;
|
|
161
|
-
}
|
|
162
|
-
|
|
163
|
-
@media (max-width: 1600px) {
|
|
164
|
-
.instances {
|
|
165
|
-
grid-template-columns: repeat(3, 1fr);
|
|
166
|
-
}
|
|
167
|
-
}
|
|
168
|
-
|
|
169
|
-
@media (max-width: 1200px) {
|
|
170
|
-
.instances {
|
|
171
|
-
grid-template-columns: repeat(2, 1fr);
|
|
172
|
-
}
|
|
173
|
-
}
|
|
174
|
-
|
|
175
|
-
@media (max-width: 800px) {
|
|
176
|
-
.instances {
|
|
177
|
-
grid-template-columns: 1fr;
|
|
178
|
-
}
|
|
179
|
-
}
|
|
180
|
-
|
|
181
|
-
.instance {
|
|
182
|
-
background: var(--bg-secondary);
|
|
183
|
-
border: 1px solid var(--border);
|
|
184
|
-
border-radius: 8px;
|
|
185
|
-
overflow: hidden;
|
|
186
|
-
}
|
|
187
|
-
|
|
188
|
-
.instance-header {
|
|
189
|
-
display: flex;
|
|
190
|
-
justify-content: space-between;
|
|
191
|
-
align-items: center;
|
|
192
|
-
padding: 16px 20px;
|
|
193
|
-
}
|
|
194
|
-
|
|
195
|
-
.instance-title {
|
|
196
|
-
display: flex;
|
|
197
|
-
align-items: center;
|
|
198
|
-
gap: 12px;
|
|
199
|
-
}
|
|
200
|
-
|
|
201
|
-
.instance-actions {
|
|
202
|
-
display: flex;
|
|
203
|
-
gap: 8px;
|
|
204
|
-
}
|
|
205
|
-
|
|
206
|
-
.instance-path-row {
|
|
207
|
-
padding: 0 20px 16px;
|
|
208
|
-
border-bottom: 1px solid var(--border);
|
|
209
|
-
}
|
|
210
|
-
|
|
211
|
-
.instance-name {
|
|
212
|
-
font-size: 20px;
|
|
213
|
-
font-weight: 600;
|
|
214
|
-
}
|
|
215
|
-
|
|
216
|
-
.status-badge {
|
|
217
|
-
display: inline-flex;
|
|
218
|
-
align-items: center;
|
|
219
|
-
gap: 6px;
|
|
220
|
-
padding: 6px 12px;
|
|
221
|
-
border-radius: 12px;
|
|
222
|
-
font-size: 14px;
|
|
223
|
-
font-weight: 500;
|
|
224
|
-
}
|
|
225
|
-
|
|
226
|
-
.status-badge.running {
|
|
227
|
-
background: rgba(34, 197, 94, 0.15);
|
|
228
|
-
color: var(--status-running);
|
|
229
|
-
}
|
|
230
|
-
|
|
231
|
-
.status-badge.stopped {
|
|
232
|
-
background: rgba(102, 102, 102, 0.15);
|
|
233
|
-
color: var(--status-stopped);
|
|
234
|
-
}
|
|
235
|
-
|
|
236
|
-
.status-dot {
|
|
237
|
-
width: 8px;
|
|
238
|
-
height: 8px;
|
|
239
|
-
border-radius: 50%;
|
|
240
|
-
}
|
|
241
|
-
|
|
242
|
-
.status-dot.running {
|
|
243
|
-
background: var(--status-running);
|
|
244
|
-
animation: pulse 2s ease-in-out infinite;
|
|
245
|
-
}
|
|
246
|
-
|
|
247
|
-
.status-dot.stopped {
|
|
248
|
-
background: var(--status-stopped);
|
|
249
|
-
}
|
|
250
|
-
|
|
251
|
-
@keyframes pulse {
|
|
252
|
-
0%, 100% { opacity: 1; }
|
|
253
|
-
50% { opacity: 0.5; }
|
|
254
|
-
}
|
|
255
|
-
|
|
256
|
-
.instance-path {
|
|
257
|
-
font-size: 14px;
|
|
258
|
-
color: var(--text-muted);
|
|
259
|
-
font-family: monospace;
|
|
260
|
-
}
|
|
261
|
-
|
|
262
|
-
.instance-body {
|
|
263
|
-
padding: 20px;
|
|
264
|
-
}
|
|
265
|
-
|
|
266
|
-
.ports-list {
|
|
267
|
-
display: flex;
|
|
268
|
-
flex-direction: column;
|
|
269
|
-
gap: 10px;
|
|
270
|
-
}
|
|
271
|
-
|
|
272
|
-
.port-item {
|
|
273
|
-
display: flex;
|
|
274
|
-
align-items: center;
|
|
275
|
-
justify-content: space-between;
|
|
276
|
-
padding: 12px 16px;
|
|
277
|
-
background: var(--bg-tertiary);
|
|
278
|
-
border-radius: 6px;
|
|
279
|
-
}
|
|
280
|
-
|
|
281
|
-
.port-info {
|
|
282
|
-
display: flex;
|
|
283
|
-
align-items: center;
|
|
284
|
-
gap: 12px;
|
|
285
|
-
}
|
|
286
|
-
|
|
287
|
-
.port-type {
|
|
288
|
-
font-size: 15px;
|
|
289
|
-
color: var(--text-secondary);
|
|
290
|
-
min-width: 100px;
|
|
291
|
-
}
|
|
292
|
-
|
|
293
|
-
.port-url {
|
|
294
|
-
font-size: 15px;
|
|
295
|
-
font-family: monospace;
|
|
296
|
-
color: var(--text-muted);
|
|
297
|
-
}
|
|
298
|
-
|
|
299
|
-
.port-status {
|
|
300
|
-
width: 8px;
|
|
301
|
-
height: 8px;
|
|
302
|
-
border-radius: 50%;
|
|
303
|
-
}
|
|
304
|
-
|
|
305
|
-
.port-status.active {
|
|
306
|
-
background: var(--status-running);
|
|
307
|
-
}
|
|
308
|
-
|
|
309
|
-
.port-status.inactive {
|
|
310
|
-
background: var(--status-stopped);
|
|
311
|
-
}
|
|
312
|
-
|
|
313
|
-
.port-actions {
|
|
314
|
-
display: flex;
|
|
315
|
-
gap: 8px;
|
|
316
|
-
}
|
|
317
|
-
|
|
318
|
-
.port-actions a {
|
|
319
|
-
padding: 6px 16px;
|
|
320
|
-
border-radius: 4px;
|
|
321
|
-
background: var(--bg-primary);
|
|
322
|
-
color: var(--accent);
|
|
323
|
-
text-decoration: none;
|
|
324
|
-
font-size: 14px;
|
|
325
|
-
transition: background 0.15s ease;
|
|
326
|
-
}
|
|
327
|
-
|
|
328
|
-
.port-actions a:hover {
|
|
329
|
-
background: #333;
|
|
330
|
-
}
|
|
331
|
-
|
|
332
|
-
.port-actions a.disabled {
|
|
333
|
-
color: var(--text-muted);
|
|
334
|
-
pointer-events: none;
|
|
335
|
-
}
|
|
336
|
-
|
|
337
|
-
.instance-meta {
|
|
338
|
-
margin-top: 16px;
|
|
339
|
-
padding-top: 16px;
|
|
340
|
-
border-top: 1px solid var(--border);
|
|
341
|
-
font-size: 14px;
|
|
342
|
-
color: var(--text-muted);
|
|
343
|
-
display: flex;
|
|
344
|
-
gap: 24px;
|
|
345
|
-
}
|
|
346
|
-
|
|
347
|
-
/* Recents section */
|
|
348
|
-
.recents-section {
|
|
349
|
-
margin-top: 32px;
|
|
350
|
-
padding-top: 32px;
|
|
351
|
-
border-top: 1px solid var(--border);
|
|
352
|
-
}
|
|
353
|
-
|
|
354
|
-
.recents-list {
|
|
355
|
-
display: flex;
|
|
356
|
-
flex-direction: column;
|
|
357
|
-
gap: 8px;
|
|
358
|
-
}
|
|
359
|
-
|
|
360
|
-
.recent-item {
|
|
361
|
-
display: flex;
|
|
362
|
-
align-items: center;
|
|
363
|
-
justify-content: space-between;
|
|
364
|
-
padding: 16px 20px;
|
|
365
|
-
background: var(--bg-secondary);
|
|
366
|
-
border: 1px solid var(--border);
|
|
367
|
-
border-radius: 8px;
|
|
368
|
-
}
|
|
369
|
-
|
|
370
|
-
.recent-info {
|
|
371
|
-
display: flex;
|
|
372
|
-
flex-direction: column;
|
|
373
|
-
gap: 4px;
|
|
374
|
-
}
|
|
375
|
-
|
|
376
|
-
.recent-name {
|
|
377
|
-
font-size: 16px;
|
|
378
|
-
font-weight: 500;
|
|
379
|
-
}
|
|
380
|
-
|
|
381
|
-
.recent-path {
|
|
382
|
-
font-size: 13px;
|
|
383
|
-
color: var(--text-muted);
|
|
384
|
-
font-family: monospace;
|
|
385
|
-
}
|
|
386
|
-
|
|
387
|
-
.recent-time {
|
|
388
|
-
font-size: 13px;
|
|
389
|
-
color: var(--text-muted);
|
|
390
|
-
}
|
|
391
|
-
|
|
392
|
-
/* Launch section */
|
|
393
|
-
.launch-section {
|
|
394
|
-
margin-top: 32px;
|
|
395
|
-
background: var(--bg-secondary);
|
|
396
|
-
border: 1px solid var(--border);
|
|
397
|
-
border-radius: 8px;
|
|
398
|
-
padding: 24px;
|
|
399
|
-
}
|
|
400
|
-
|
|
401
|
-
.launch-section h3 {
|
|
402
|
-
font-size: 18px;
|
|
403
|
-
font-weight: 600;
|
|
404
|
-
margin-bottom: 20px;
|
|
405
|
-
display: flex;
|
|
406
|
-
align-items: center;
|
|
407
|
-
gap: 10px;
|
|
408
|
-
}
|
|
409
|
-
|
|
410
|
-
.launch-form {
|
|
411
|
-
display: flex;
|
|
412
|
-
gap: 12px;
|
|
413
|
-
align-items: center;
|
|
414
|
-
width: 100%;
|
|
415
|
-
}
|
|
416
|
-
|
|
417
|
-
.launch-form input[type="text"] {
|
|
418
|
-
width: 100%;
|
|
419
|
-
padding: 12px 16px;
|
|
420
|
-
border-radius: 6px;
|
|
421
|
-
border: 1px solid var(--border);
|
|
422
|
-
background: var(--bg-tertiary);
|
|
423
|
-
color: var(--text-primary);
|
|
424
|
-
font-size: 16px;
|
|
425
|
-
}
|
|
426
|
-
|
|
427
|
-
.launch-form input[type="text"]:focus {
|
|
428
|
-
outline: none;
|
|
429
|
-
border-color: var(--accent);
|
|
430
|
-
}
|
|
431
|
-
|
|
432
|
-
.launch-form input[type="text"]::placeholder {
|
|
433
|
-
color: var(--text-muted);
|
|
434
|
-
}
|
|
435
|
-
|
|
436
|
-
/* Autocomplete dropdown */
|
|
437
|
-
.autocomplete-wrapper {
|
|
438
|
-
position: relative;
|
|
439
|
-
flex: 1 1 auto;
|
|
440
|
-
width: 100%;
|
|
441
|
-
}
|
|
442
|
-
|
|
443
|
-
.autocomplete-dropdown {
|
|
444
|
-
position: absolute;
|
|
445
|
-
top: 100%;
|
|
446
|
-
left: 0;
|
|
447
|
-
right: 0;
|
|
448
|
-
background: var(--bg-tertiary);
|
|
449
|
-
border: 1px solid var(--border);
|
|
450
|
-
border-top: none;
|
|
451
|
-
border-radius: 0 0 6px 6px;
|
|
452
|
-
max-height: 300px;
|
|
453
|
-
overflow-y: auto;
|
|
454
|
-
z-index: 100;
|
|
455
|
-
display: none;
|
|
456
|
-
}
|
|
457
|
-
|
|
458
|
-
.autocomplete-dropdown.show {
|
|
459
|
-
display: block;
|
|
460
|
-
}
|
|
461
|
-
|
|
462
|
-
.autocomplete-item {
|
|
463
|
-
padding: 10px 16px;
|
|
464
|
-
cursor: pointer;
|
|
465
|
-
display: flex;
|
|
466
|
-
align-items: center;
|
|
467
|
-
gap: 10px;
|
|
468
|
-
font-size: 14px;
|
|
469
|
-
font-family: monospace;
|
|
470
|
-
}
|
|
471
|
-
|
|
472
|
-
.autocomplete-item:hover,
|
|
473
|
-
.autocomplete-item.selected {
|
|
474
|
-
background: var(--bg-secondary);
|
|
475
|
-
}
|
|
476
|
-
|
|
477
|
-
.autocomplete-item .project-badge {
|
|
478
|
-
background: var(--status-running);
|
|
479
|
-
color: white;
|
|
480
|
-
padding: 2px 6px;
|
|
481
|
-
border-radius: 4px;
|
|
482
|
-
font-size: 11px;
|
|
483
|
-
font-family: system-ui;
|
|
484
|
-
}
|
|
485
|
-
|
|
486
|
-
.autocomplete-item .folder-icon {
|
|
487
|
-
color: var(--text-muted);
|
|
488
|
-
}
|
|
489
|
-
|
|
490
|
-
.launch-hint {
|
|
491
|
-
margin-top: 16px;
|
|
492
|
-
font-size: 14px;
|
|
493
|
-
color: var(--text-muted);
|
|
494
|
-
}
|
|
495
|
-
|
|
496
|
-
/* Loading state */
|
|
497
|
-
.loading {
|
|
498
|
-
padding: 48px;
|
|
499
|
-
color: var(--text-muted);
|
|
500
|
-
}
|
|
501
|
-
|
|
502
|
-
.loading .spinner {
|
|
503
|
-
width: 32px;
|
|
504
|
-
height: 32px;
|
|
505
|
-
border: 3px solid var(--border);
|
|
506
|
-
border-top-color: var(--accent);
|
|
507
|
-
border-radius: 50%;
|
|
508
|
-
animation: spin 1s linear infinite;
|
|
509
|
-
margin-bottom: 16px;
|
|
510
|
-
}
|
|
511
|
-
|
|
512
|
-
@keyframes spin {
|
|
513
|
-
to { transform: rotate(360deg); }
|
|
514
|
-
}
|
|
515
|
-
|
|
516
|
-
/* Toast notifications */
|
|
517
|
-
.toast-container {
|
|
518
|
-
position: fixed;
|
|
519
|
-
bottom: 24px;
|
|
520
|
-
right: 24px;
|
|
521
|
-
z-index: 1000;
|
|
522
|
-
display: flex;
|
|
523
|
-
flex-direction: column;
|
|
524
|
-
gap: 8px;
|
|
525
|
-
}
|
|
526
|
-
|
|
527
|
-
.toast {
|
|
528
|
-
padding: 14px 20px;
|
|
529
|
-
background: var(--bg-secondary);
|
|
530
|
-
border: 1px solid var(--border);
|
|
531
|
-
border-radius: 6px;
|
|
532
|
-
font-size: 15px;
|
|
533
|
-
animation: toast-in 0.3s ease-out;
|
|
534
|
-
}
|
|
535
|
-
|
|
536
|
-
.toast.error {
|
|
537
|
-
border-color: #ef4444;
|
|
538
|
-
}
|
|
539
|
-
|
|
540
|
-
.toast.success {
|
|
541
|
-
border-color: #22c55e;
|
|
542
|
-
}
|
|
543
|
-
|
|
544
|
-
@keyframes toast-in {
|
|
545
|
-
from {
|
|
546
|
-
opacity: 0;
|
|
547
|
-
transform: translateY(10px);
|
|
548
|
-
}
|
|
549
|
-
to {
|
|
550
|
-
opacity: 1;
|
|
551
|
-
transform: translateY(0);
|
|
552
|
-
}
|
|
553
|
-
}
|
|
554
|
-
|
|
555
|
-
/* Reduced motion */
|
|
556
|
-
@media (prefers-reduced-motion: reduce) {
|
|
557
|
-
.status-dot.running,
|
|
558
|
-
.loading .spinner {
|
|
559
|
-
animation: none;
|
|
560
|
-
}
|
|
561
|
-
}
|
|
562
|
-
</style>
|
|
563
|
-
</head>
|
|
564
|
-
<body>
|
|
565
|
-
<header class="header">
|
|
566
|
-
<h1>
|
|
567
|
-
<span class="emoji">🗼</span>
|
|
568
|
-
Agent Farm Control Tower
|
|
569
|
-
</h1>
|
|
570
|
-
<div class="header-actions">
|
|
571
|
-
<button class="btn" onclick="refresh()">Refresh</button>
|
|
572
|
-
</div>
|
|
573
|
-
</header>
|
|
574
|
-
|
|
575
|
-
<main class="main">
|
|
576
|
-
<div id="content">
|
|
577
|
-
<div class="loading">
|
|
578
|
-
<div class="spinner"></div>
|
|
579
|
-
<p>Loading instances...</p>
|
|
580
|
-
</div>
|
|
581
|
-
</div>
|
|
582
|
-
|
|
583
|
-
<div id="recents-container"></div>
|
|
584
|
-
|
|
585
|
-
<div class="launch-section">
|
|
586
|
-
<h3>
|
|
587
|
-
<span>+</span>
|
|
588
|
-
Launch New Instance
|
|
589
|
-
</h3>
|
|
590
|
-
<div class="launch-form">
|
|
591
|
-
<div class="autocomplete-wrapper">
|
|
592
|
-
<input type="text" id="project-path" placeholder="Start typing a path (e.g., ~/Development/)" autocomplete="off" />
|
|
593
|
-
<div class="autocomplete-dropdown" id="autocomplete-dropdown"></div>
|
|
594
|
-
</div>
|
|
595
|
-
<button class="btn btn-primary btn-launch" onclick="launchInstance()">Launch</button>
|
|
596
|
-
</div>
|
|
597
|
-
<p class="launch-hint">
|
|
598
|
-
Type a path to see suggestions. Directories with <code>codev/</code> are highlighted as valid projects.
|
|
599
|
-
</p>
|
|
600
|
-
</div>
|
|
601
|
-
</main>
|
|
602
|
-
|
|
603
|
-
<div class="toast-container" id="toast-container"></div>
|
|
604
|
-
|
|
605
|
-
<script>
|
|
606
|
-
// State
|
|
607
|
-
let runningInstances = [];
|
|
608
|
-
let recentInstances = [];
|
|
609
|
-
|
|
610
|
-
// Initialize
|
|
611
|
-
async function init() {
|
|
612
|
-
await refresh();
|
|
613
|
-
// Poll every 5 seconds
|
|
614
|
-
setInterval(refresh, 5000);
|
|
615
|
-
}
|
|
616
|
-
|
|
617
|
-
// Refresh data from API
|
|
618
|
-
async function refresh() {
|
|
619
|
-
try {
|
|
620
|
-
const response = await fetch('/api/status');
|
|
621
|
-
if (!response.ok) throw new Error('Failed to fetch status');
|
|
622
|
-
|
|
623
|
-
const data = await response.json();
|
|
624
|
-
runningInstances = (data.instances || []).filter(i => i.running);
|
|
625
|
-
recentInstances = (data.instances || []).filter(i => !i.running);
|
|
626
|
-
render();
|
|
627
|
-
} catch (err) {
|
|
628
|
-
console.error('Refresh error:', err);
|
|
629
|
-
showToast('Failed to refresh: ' + err.message, 'error');
|
|
630
|
-
}
|
|
631
|
-
}
|
|
632
|
-
|
|
633
|
-
// Render the UI
|
|
634
|
-
function render() {
|
|
635
|
-
const content = document.getElementById('content');
|
|
636
|
-
|
|
637
|
-
if (runningInstances.length === 0) {
|
|
638
|
-
content.innerHTML = `
|
|
639
|
-
<div class="empty-state">
|
|
640
|
-
<div class="icon">📭</div>
|
|
641
|
-
<h2>No running instances</h2>
|
|
642
|
-
<p>Start a new instance below or run <code>af start</code> in a project directory.</p>
|
|
643
|
-
</div>
|
|
644
|
-
`;
|
|
645
|
-
} else {
|
|
646
|
-
content.innerHTML = `
|
|
647
|
-
<h2 class="section-header">Running Instances</h2>
|
|
648
|
-
<div class="instances">
|
|
649
|
-
${runningInstances.map(renderInstance).join('')}
|
|
650
|
-
</div>
|
|
651
|
-
`;
|
|
652
|
-
}
|
|
653
|
-
|
|
654
|
-
// Render recents
|
|
655
|
-
const recentsContainer = document.getElementById('recents-container');
|
|
656
|
-
if (recentInstances.length > 0) {
|
|
657
|
-
recentsContainer.innerHTML = `
|
|
658
|
-
<div class="recents-section">
|
|
659
|
-
<h2 class="section-header">Recent Projects</h2>
|
|
660
|
-
<div class="recents-list">
|
|
661
|
-
${recentInstances.map(renderRecentItem).join('')}
|
|
662
|
-
</div>
|
|
663
|
-
</div>
|
|
664
|
-
`;
|
|
665
|
-
} else {
|
|
666
|
-
recentsContainer.innerHTML = '';
|
|
667
|
-
}
|
|
668
|
-
}
|
|
669
|
-
|
|
670
|
-
// Render a single instance card
|
|
671
|
-
function renderInstance(instance) {
|
|
672
|
-
const statusClass = instance.running ? 'running' : 'stopped';
|
|
673
|
-
const statusText = instance.running ? 'Running' : 'Stopped';
|
|
674
|
-
|
|
675
|
-
const portsHtml = instance.ports.map(port => `
|
|
676
|
-
<div class="port-item">
|
|
677
|
-
<div class="port-info">
|
|
678
|
-
<span class="port-status ${port.active ? 'active' : 'inactive'}"></span>
|
|
679
|
-
<span class="port-type">${escapeHtml(port.type)}</span>
|
|
680
|
-
<span class="port-url">${escapeHtml(port.url)}</span>
|
|
681
|
-
</div>
|
|
682
|
-
<div class="port-actions">
|
|
683
|
-
<a href="${escapeHtml(port.url)}" target="_blank" class="${port.active ? '' : 'disabled'}">
|
|
684
|
-
Open
|
|
685
|
-
</a>
|
|
686
|
-
</div>
|
|
687
|
-
</div>
|
|
688
|
-
`).join('');
|
|
689
|
-
|
|
690
|
-
const lastUsed = instance.lastUsed
|
|
691
|
-
? formatDate(instance.lastUsed)
|
|
692
|
-
: 'Never';
|
|
693
|
-
|
|
694
|
-
return `
|
|
695
|
-
<div class="instance">
|
|
696
|
-
<div class="instance-header">
|
|
697
|
-
<div class="instance-title">
|
|
698
|
-
<span class="instance-name">${escapeHtml(instance.projectName)}</span>
|
|
699
|
-
<span class="status-badge ${statusClass}">
|
|
700
|
-
<span class="status-dot ${statusClass}"></span>
|
|
701
|
-
${statusText}
|
|
702
|
-
</span>
|
|
703
|
-
</div>
|
|
704
|
-
<div class="instance-actions">
|
|
705
|
-
${instance.running ? `
|
|
706
|
-
<button class="btn btn-small" onclick="restartInstance(${instance.basePort}, '${escapeHtml(instance.projectPath)}')">Restart</button>
|
|
707
|
-
<button class="btn btn-small btn-danger" onclick="stopInstance(${instance.basePort})">Stop</button>
|
|
708
|
-
` : `
|
|
709
|
-
<button class="btn btn-small btn-primary" onclick="launchPath('${escapeHtml(instance.projectPath)}')">Start</button>
|
|
710
|
-
`}
|
|
711
|
-
</div>
|
|
712
|
-
</div>
|
|
713
|
-
<div class="instance-path-row">
|
|
714
|
-
<span class="instance-path" title="${escapeHtml(instance.projectPath)}">
|
|
715
|
-
${escapeHtml(instance.projectPath)}
|
|
716
|
-
</span>
|
|
717
|
-
</div>
|
|
718
|
-
<div class="instance-body">
|
|
719
|
-
<div class="ports-list">
|
|
720
|
-
${portsHtml}
|
|
721
|
-
</div>
|
|
722
|
-
<div class="instance-meta">
|
|
723
|
-
<span>Last active: ${lastUsed}</span>
|
|
724
|
-
<span>Port block: ${instance.basePort}-${instance.basePort + 99}</span>
|
|
725
|
-
</div>
|
|
726
|
-
</div>
|
|
727
|
-
</div>
|
|
728
|
-
`;
|
|
729
|
-
}
|
|
730
|
-
|
|
731
|
-
// Render a recent item
|
|
732
|
-
function renderRecentItem(instance) {
|
|
733
|
-
const lastUsed = instance.lastUsed ? formatDate(instance.lastUsed) : 'Never';
|
|
734
|
-
|
|
735
|
-
return `
|
|
736
|
-
<div class="recent-item">
|
|
737
|
-
<div class="recent-info">
|
|
738
|
-
<span class="recent-name">${escapeHtml(instance.projectName)}</span>
|
|
739
|
-
<span class="recent-path">${escapeHtml(instance.projectPath)}</span>
|
|
740
|
-
</div>
|
|
741
|
-
<div style="display: flex; align-items: center; gap: 16px;">
|
|
742
|
-
<span class="recent-time">${lastUsed}</span>
|
|
743
|
-
<button class="btn btn-primary" onclick="launchPath('${escapeHtml(instance.projectPath)}')">Start</button>
|
|
744
|
-
</div>
|
|
745
|
-
</div>
|
|
746
|
-
`;
|
|
747
|
-
}
|
|
748
|
-
|
|
749
|
-
// Autocomplete state
|
|
750
|
-
let suggestions = [];
|
|
751
|
-
let selectedIndex = -1;
|
|
752
|
-
let debounceTimer = null;
|
|
753
|
-
|
|
754
|
-
// Setup autocomplete
|
|
755
|
-
const pathInput = document.getElementById('project-path');
|
|
756
|
-
const dropdown = document.getElementById('autocomplete-dropdown');
|
|
757
|
-
|
|
758
|
-
pathInput.addEventListener('input', (e) => {
|
|
759
|
-
// Debounce to avoid too many requests
|
|
760
|
-
clearTimeout(debounceTimer);
|
|
761
|
-
debounceTimer = setTimeout(() => {
|
|
762
|
-
fetchSuggestions(e.target.value);
|
|
763
|
-
}, 150);
|
|
764
|
-
});
|
|
765
|
-
|
|
766
|
-
pathInput.addEventListener('keydown', (e) => {
|
|
767
|
-
if (e.key === 'ArrowDown') {
|
|
768
|
-
e.preventDefault();
|
|
769
|
-
selectedIndex = Math.min(selectedIndex + 1, suggestions.length - 1);
|
|
770
|
-
renderDropdown();
|
|
771
|
-
} else if (e.key === 'ArrowUp') {
|
|
772
|
-
e.preventDefault();
|
|
773
|
-
selectedIndex = Math.max(selectedIndex - 1, -1);
|
|
774
|
-
renderDropdown();
|
|
775
|
-
} else if (e.key === 'Enter') {
|
|
776
|
-
if (selectedIndex >= 0 && suggestions[selectedIndex]) {
|
|
777
|
-
e.preventDefault();
|
|
778
|
-
selectSuggestion(suggestions[selectedIndex]);
|
|
779
|
-
} else if (pathInput.value.trim()) {
|
|
780
|
-
launchInstance();
|
|
781
|
-
}
|
|
782
|
-
} else if (e.key === 'Escape') {
|
|
783
|
-
hideDropdown();
|
|
784
|
-
} else if (e.key === 'Tab' && suggestions.length > 0) {
|
|
785
|
-
e.preventDefault();
|
|
786
|
-
// Tab completes to common prefix or first suggestion
|
|
787
|
-
if (selectedIndex >= 0) {
|
|
788
|
-
selectSuggestion(suggestions[selectedIndex]);
|
|
789
|
-
} else if (suggestions.length > 0) {
|
|
790
|
-
selectSuggestion(suggestions[0]);
|
|
791
|
-
}
|
|
792
|
-
}
|
|
793
|
-
});
|
|
794
|
-
|
|
795
|
-
pathInput.addEventListener('focus', () => {
|
|
796
|
-
if (pathInput.value) {
|
|
797
|
-
fetchSuggestions(pathInput.value);
|
|
798
|
-
}
|
|
799
|
-
});
|
|
800
|
-
|
|
801
|
-
// Hide dropdown when clicking outside
|
|
802
|
-
document.addEventListener('click', (e) => {
|
|
803
|
-
if (!e.target.closest('.autocomplete-wrapper')) {
|
|
804
|
-
hideDropdown();
|
|
805
|
-
}
|
|
806
|
-
});
|
|
807
|
-
|
|
808
|
-
async function fetchSuggestions(inputPath) {
|
|
809
|
-
if (!inputPath) {
|
|
810
|
-
hideDropdown();
|
|
811
|
-
return;
|
|
812
|
-
}
|
|
813
|
-
|
|
814
|
-
try {
|
|
815
|
-
const response = await fetch('/api/browse?path=' + encodeURIComponent(inputPath));
|
|
816
|
-
const data = await response.json();
|
|
817
|
-
suggestions = data.suggestions || [];
|
|
818
|
-
selectedIndex = -1;
|
|
819
|
-
renderDropdown();
|
|
820
|
-
} catch (err) {
|
|
821
|
-
console.error('Fetch suggestions error:', err);
|
|
822
|
-
suggestions = [];
|
|
823
|
-
hideDropdown();
|
|
824
|
-
}
|
|
825
|
-
}
|
|
826
|
-
|
|
827
|
-
function renderDropdown() {
|
|
828
|
-
if (suggestions.length === 0) {
|
|
829
|
-
hideDropdown();
|
|
830
|
-
return;
|
|
831
|
-
}
|
|
832
|
-
|
|
833
|
-
dropdown.innerHTML = suggestions.map((s, i) => `
|
|
834
|
-
<div class="autocomplete-item ${i === selectedIndex ? 'selected' : ''}"
|
|
835
|
-
onclick="selectSuggestion(suggestions[${i}])">
|
|
836
|
-
<span class="folder-icon">📁</span>
|
|
837
|
-
<span>${escapeHtml(s.path)}</span>
|
|
838
|
-
${s.isProject ? '<span class="project-badge">codev</span>' : ''}
|
|
839
|
-
</div>
|
|
840
|
-
`).join('');
|
|
841
|
-
|
|
842
|
-
dropdown.classList.add('show');
|
|
843
|
-
|
|
844
|
-
// Scroll selected item into view
|
|
845
|
-
if (selectedIndex >= 0) {
|
|
846
|
-
const items = dropdown.querySelectorAll('.autocomplete-item');
|
|
847
|
-
if (items[selectedIndex]) {
|
|
848
|
-
items[selectedIndex].scrollIntoView({ block: 'nearest' });
|
|
849
|
-
}
|
|
850
|
-
}
|
|
851
|
-
}
|
|
852
|
-
|
|
853
|
-
function selectSuggestion(suggestion) {
|
|
854
|
-
pathInput.value = suggestion.path + '/';
|
|
855
|
-
hideDropdown();
|
|
856
|
-
// Fetch new suggestions for the selected directory
|
|
857
|
-
fetchSuggestions(pathInput.value);
|
|
858
|
-
pathInput.focus();
|
|
859
|
-
}
|
|
860
|
-
|
|
861
|
-
function hideDropdown() {
|
|
862
|
-
dropdown.classList.remove('show');
|
|
863
|
-
suggestions = [];
|
|
864
|
-
selectedIndex = -1;
|
|
865
|
-
}
|
|
866
|
-
|
|
867
|
-
// Launch a specific path (from recents)
|
|
868
|
-
async function launchPath(projectPath) {
|
|
869
|
-
try {
|
|
870
|
-
const response = await fetch('/api/launch', {
|
|
871
|
-
method: 'POST',
|
|
872
|
-
headers: { 'Content-Type': 'application/json' },
|
|
873
|
-
body: JSON.stringify({ projectPath })
|
|
874
|
-
});
|
|
875
|
-
|
|
876
|
-
const result = await response.json();
|
|
877
|
-
|
|
878
|
-
if (!result.success) {
|
|
879
|
-
throw new Error(result.error || 'Launch failed');
|
|
880
|
-
}
|
|
881
|
-
|
|
882
|
-
showToast('Instance launching...', 'success');
|
|
883
|
-
setTimeout(refresh, 2000);
|
|
884
|
-
} catch (err) {
|
|
885
|
-
showToast('Failed to launch: ' + err.message, 'error');
|
|
886
|
-
}
|
|
887
|
-
}
|
|
888
|
-
|
|
889
|
-
// Stop an instance by port
|
|
890
|
-
async function stopInstance(basePort) {
|
|
891
|
-
try {
|
|
892
|
-
const response = await fetch('/api/stop', {
|
|
893
|
-
method: 'POST',
|
|
894
|
-
headers: { 'Content-Type': 'application/json' },
|
|
895
|
-
body: JSON.stringify({ basePort })
|
|
896
|
-
});
|
|
897
|
-
|
|
898
|
-
const result = await response.json();
|
|
899
|
-
|
|
900
|
-
if (result.stopped && result.stopped.length > 0) {
|
|
901
|
-
showToast('Instance stopped', 'success');
|
|
902
|
-
} else {
|
|
903
|
-
showToast('No processes found to stop', 'info');
|
|
904
|
-
}
|
|
905
|
-
setTimeout(refresh, 1000);
|
|
906
|
-
} catch (err) {
|
|
907
|
-
showToast('Failed to stop: ' + err.message, 'error');
|
|
908
|
-
}
|
|
909
|
-
}
|
|
910
|
-
|
|
911
|
-
// Restart an instance
|
|
912
|
-
async function restartInstance(basePort, projectPath) {
|
|
913
|
-
try {
|
|
914
|
-
// First stop
|
|
915
|
-
await fetch('/api/stop', {
|
|
916
|
-
method: 'POST',
|
|
917
|
-
headers: { 'Content-Type': 'application/json' },
|
|
918
|
-
body: JSON.stringify({ basePort })
|
|
919
|
-
});
|
|
920
|
-
|
|
921
|
-
// Wait a moment for processes to die
|
|
922
|
-
await new Promise(r => setTimeout(r, 1000));
|
|
923
|
-
|
|
924
|
-
// Then start
|
|
925
|
-
const response = await fetch('/api/launch', {
|
|
926
|
-
method: 'POST',
|
|
927
|
-
headers: { 'Content-Type': 'application/json' },
|
|
928
|
-
body: JSON.stringify({ projectPath })
|
|
929
|
-
});
|
|
930
|
-
|
|
931
|
-
const result = await response.json();
|
|
932
|
-
|
|
933
|
-
if (!result.success) {
|
|
934
|
-
throw new Error(result.error || 'Restart failed');
|
|
935
|
-
}
|
|
936
|
-
|
|
937
|
-
showToast('Instance restarting...', 'success');
|
|
938
|
-
setTimeout(refresh, 2000);
|
|
939
|
-
} catch (err) {
|
|
940
|
-
showToast('Failed to restart: ' + err.message, 'error');
|
|
941
|
-
}
|
|
942
|
-
}
|
|
943
|
-
|
|
944
|
-
// Launch a new instance
|
|
945
|
-
async function launchInstance() {
|
|
946
|
-
const input = document.getElementById('project-path');
|
|
947
|
-
const projectPath = input.value.trim();
|
|
948
|
-
|
|
949
|
-
if (!projectPath) {
|
|
950
|
-
showToast('Please enter a project path', 'error');
|
|
951
|
-
return;
|
|
952
|
-
}
|
|
953
|
-
|
|
954
|
-
try {
|
|
955
|
-
const response = await fetch('/api/launch', {
|
|
956
|
-
method: 'POST',
|
|
957
|
-
headers: { 'Content-Type': 'application/json' },
|
|
958
|
-
body: JSON.stringify({ projectPath })
|
|
959
|
-
});
|
|
960
|
-
|
|
961
|
-
const result = await response.json();
|
|
962
|
-
|
|
963
|
-
if (!result.success) {
|
|
964
|
-
throw new Error(result.error || 'Launch failed');
|
|
965
|
-
}
|
|
966
|
-
|
|
967
|
-
input.value = '';
|
|
968
|
-
showToast('Instance launching... Please wait a moment.', 'success');
|
|
969
|
-
|
|
970
|
-
// Refresh after a delay to allow instance to start
|
|
971
|
-
setTimeout(refresh, 2000);
|
|
972
|
-
} catch (err) {
|
|
973
|
-
showToast('Failed to launch: ' + err.message, 'error');
|
|
974
|
-
}
|
|
975
|
-
}
|
|
976
|
-
|
|
977
|
-
// Format date for display
|
|
978
|
-
function formatDate(isoString) {
|
|
979
|
-
const date = new Date(isoString);
|
|
980
|
-
const now = new Date();
|
|
981
|
-
const diff = now.getTime() - date.getTime();
|
|
982
|
-
|
|
983
|
-
// Less than a minute
|
|
984
|
-
if (diff < 60000) {
|
|
985
|
-
return 'Just now';
|
|
986
|
-
}
|
|
987
|
-
|
|
988
|
-
// Less than an hour
|
|
989
|
-
if (diff < 3600000) {
|
|
990
|
-
const mins = Math.floor(diff / 60000);
|
|
991
|
-
return `${mins} minute${mins === 1 ? '' : 's'} ago`;
|
|
992
|
-
}
|
|
993
|
-
|
|
994
|
-
// Less than a day
|
|
995
|
-
if (diff < 86400000) {
|
|
996
|
-
const hours = Math.floor(diff / 3600000);
|
|
997
|
-
return `${hours} hour${hours === 1 ? '' : 's'} ago`;
|
|
998
|
-
}
|
|
999
|
-
|
|
1000
|
-
// Format as date
|
|
1001
|
-
return date.toLocaleDateString() + ' ' + date.toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' });
|
|
1002
|
-
}
|
|
1003
|
-
|
|
1004
|
-
// HTML escape
|
|
1005
|
-
function escapeHtml(str) {
|
|
1006
|
-
if (!str) return '';
|
|
1007
|
-
return str
|
|
1008
|
-
.replace(/&/g, '&')
|
|
1009
|
-
.replace(/</g, '<')
|
|
1010
|
-
.replace(/>/g, '>')
|
|
1011
|
-
.replace(/"/g, '"')
|
|
1012
|
-
.replace(/'/g, ''');
|
|
1013
|
-
}
|
|
1014
|
-
|
|
1015
|
-
// Toast notifications
|
|
1016
|
-
function showToast(message, type = 'info') {
|
|
1017
|
-
const container = document.getElementById('toast-container');
|
|
1018
|
-
const toast = document.createElement('div');
|
|
1019
|
-
toast.className = `toast ${type}`;
|
|
1020
|
-
toast.textContent = message;
|
|
1021
|
-
container.appendChild(toast);
|
|
1022
|
-
|
|
1023
|
-
setTimeout(() => {
|
|
1024
|
-
toast.remove();
|
|
1025
|
-
}, 4000);
|
|
1026
|
-
}
|
|
1027
|
-
|
|
1028
|
-
// Initialize
|
|
1029
|
-
init();
|
|
1030
|
-
</script>
|
|
1031
|
-
</body>
|
|
1032
|
-
</html>
|