@cluesmith/codev 1.1.0 → 1.2.0

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.
Files changed (146) hide show
  1. package/dist/agent-farm/cli.d.ts.map +1 -1
  2. package/dist/agent-farm/cli.js +19 -0
  3. package/dist/agent-farm/cli.js.map +1 -1
  4. package/dist/agent-farm/commands/cleanup.d.ts.map +1 -1
  5. package/dist/agent-farm/commands/cleanup.js +18 -1
  6. package/dist/agent-farm/commands/cleanup.js.map +1 -1
  7. package/dist/agent-farm/commands/consult.d.ts +16 -0
  8. package/dist/agent-farm/commands/consult.d.ts.map +1 -0
  9. package/dist/agent-farm/commands/consult.js +51 -0
  10. package/dist/agent-farm/commands/consult.js.map +1 -0
  11. package/dist/agent-farm/commands/open.js +6 -6
  12. package/dist/agent-farm/commands/open.js.map +1 -1
  13. package/dist/agent-farm/commands/spawn.d.ts.map +1 -1
  14. package/dist/agent-farm/commands/spawn.js +51 -42
  15. package/dist/agent-farm/commands/spawn.js.map +1 -1
  16. package/dist/agent-farm/commands/start.d.ts.map +1 -1
  17. package/dist/agent-farm/commands/start.js +9 -14
  18. package/dist/agent-farm/commands/start.js.map +1 -1
  19. package/dist/agent-farm/commands/util.js +2 -2
  20. package/dist/agent-farm/commands/util.js.map +1 -1
  21. package/dist/agent-farm/db/errors.d.ts +4 -0
  22. package/dist/agent-farm/db/errors.d.ts.map +1 -1
  23. package/dist/agent-farm/db/errors.js +8 -0
  24. package/dist/agent-farm/db/errors.js.map +1 -1
  25. package/dist/agent-farm/servers/dashboard-server.js +125 -71
  26. package/dist/agent-farm/servers/dashboard-server.js.map +1 -1
  27. package/dist/agent-farm/servers/open-server.d.ts +9 -0
  28. package/dist/agent-farm/servers/open-server.d.ts.map +1 -0
  29. package/dist/agent-farm/servers/{annotate-server.js → open-server.js} +17 -15
  30. package/dist/agent-farm/servers/open-server.js.map +1 -0
  31. package/dist/agent-farm/servers/tower-server.js +4 -7
  32. package/dist/agent-farm/servers/tower-server.js.map +1 -1
  33. package/dist/agent-farm/state.d.ts +5 -0
  34. package/dist/agent-farm/state.d.ts.map +1 -1
  35. package/dist/agent-farm/state.js +17 -0
  36. package/dist/agent-farm/state.js.map +1 -1
  37. package/dist/agent-farm/types.d.ts +1 -1
  38. package/dist/agent-farm/types.d.ts.map +1 -1
  39. package/dist/agent-farm/utils/config.d.ts.map +1 -1
  40. package/dist/agent-farm/utils/config.js +13 -7
  41. package/dist/agent-farm/utils/config.js.map +1 -1
  42. package/dist/agent-farm/utils/port-registry.d.ts +1 -1
  43. package/dist/agent-farm/utils/port-registry.d.ts.map +1 -1
  44. package/dist/agent-farm/utils/port-registry.js +1 -1
  45. package/dist/agent-farm/utils/port-registry.js.map +1 -1
  46. package/dist/agent-farm/utils/shell.d.ts +19 -0
  47. package/dist/agent-farm/utils/shell.d.ts.map +1 -1
  48. package/dist/agent-farm/utils/shell.js +28 -0
  49. package/dist/agent-farm/utils/shell.js.map +1 -1
  50. package/dist/cli.d.ts.map +1 -1
  51. package/dist/cli.js +33 -0
  52. package/dist/cli.js.map +1 -1
  53. package/dist/commands/adopt.d.ts +3 -0
  54. package/dist/commands/adopt.d.ts.map +1 -1
  55. package/dist/commands/adopt.js +31 -25
  56. package/dist/commands/adopt.js.map +1 -1
  57. package/dist/commands/consult/index.d.ts +3 -2
  58. package/dist/commands/consult/index.d.ts.map +1 -1
  59. package/dist/commands/consult/index.js +128 -54
  60. package/dist/commands/consult/index.js.map +1 -1
  61. package/dist/commands/doctor.d.ts.map +1 -1
  62. package/dist/commands/doctor.js +88 -36
  63. package/dist/commands/doctor.js.map +1 -1
  64. package/dist/commands/eject.d.ts +18 -0
  65. package/dist/commands/eject.d.ts.map +1 -0
  66. package/dist/commands/eject.js +149 -0
  67. package/dist/commands/eject.js.map +1 -0
  68. package/dist/commands/import.d.ts +16 -0
  69. package/dist/commands/import.d.ts.map +1 -0
  70. package/dist/commands/import.js +278 -0
  71. package/dist/commands/import.js.map +1 -0
  72. package/dist/commands/init.d.ts +3 -0
  73. package/dist/commands/init.d.ts.map +1 -1
  74. package/dist/commands/init.js +32 -27
  75. package/dist/commands/init.js.map +1 -1
  76. package/dist/lib/projectlist-parser.d.ts +70 -0
  77. package/dist/lib/projectlist-parser.d.ts.map +1 -0
  78. package/dist/lib/projectlist-parser.js +200 -0
  79. package/dist/lib/projectlist-parser.js.map +1 -0
  80. package/dist/lib/skeleton.d.ts +41 -0
  81. package/dist/lib/skeleton.d.ts.map +1 -0
  82. package/dist/lib/skeleton.js +110 -0
  83. package/dist/lib/skeleton.js.map +1 -0
  84. package/dist/lib/templates.d.ts +2 -1
  85. package/dist/lib/templates.d.ts.map +1 -1
  86. package/dist/lib/templates.js +11 -10
  87. package/dist/lib/templates.js.map +1 -1
  88. package/package.json +5 -4
  89. package/{templates → skeleton}/DEPENDENCIES.md +3 -48
  90. package/skeleton/bin/agent-farm +7 -0
  91. package/skeleton/docs/commands/agent-farm.md +469 -0
  92. package/skeleton/docs/commands/codev.md +253 -0
  93. package/skeleton/docs/commands/consult.md +286 -0
  94. package/skeleton/docs/commands/overview.md +108 -0
  95. package/skeleton/maintain/.gitkeep +2 -0
  96. package/{templates → skeleton}/protocols/experiment/protocol.md +2 -2
  97. package/skeleton/protocols/maintain/protocol.md +502 -0
  98. package/skeleton/protocols/maintain/templates/maintenance-run.md +64 -0
  99. package/{templates → skeleton}/protocols/spider/protocol.md +9 -9
  100. package/{templates/protocols/spider-solo → skeleton/protocols/spider}/templates/plan.md +22 -1
  101. package/{templates/protocols/spider-solo → skeleton/protocols/spider}/templates/spec.md +30 -1
  102. package/skeleton/protocols/tick/protocol.md +277 -0
  103. package/skeleton/resources/lessons-learned.md +30 -0
  104. package/skeleton/resources/workflow-reference.md +242 -0
  105. package/skeleton/roles/architect.md +283 -0
  106. package/{templates → skeleton}/roles/builder.md +2 -0
  107. package/skeleton/roles/review-types/impl-review.md +56 -0
  108. package/skeleton/roles/review-types/integration-review.md +68 -0
  109. package/skeleton/roles/review-types/plan-review.md +59 -0
  110. package/skeleton/roles/review-types/pr-ready.md +72 -0
  111. package/skeleton/roles/review-types/spec-review.md +55 -0
  112. package/skeleton/templates/lessons-learned.md +28 -0
  113. package/{templates → skeleton}/templates/projectlist.md +17 -16
  114. package/dist/agent-farm/servers/annotate-server.d.ts +0 -9
  115. package/dist/agent-farm/servers/annotate-server.d.ts.map +0 -1
  116. package/dist/agent-farm/servers/annotate-server.js.map +0 -1
  117. package/templates/agents/architecture-documenter.md +0 -189
  118. package/templates/agents/codev-updater.md +0 -276
  119. package/templates/agents/spider-protocol-updater.md +0 -118
  120. package/templates/annotate.html +0 -903
  121. package/templates/bin/agent-farm +0 -18
  122. package/templates/bin/annotate-server.js +0 -140
  123. package/templates/dashboard-split.html +0 -1679
  124. package/templates/dashboard.html +0 -149
  125. package/templates/protocols/maintain/protocol.md +0 -235
  126. package/templates/protocols/spider/templates/plan.md +0 -169
  127. package/templates/protocols/spider/templates/review.md +0 -207
  128. package/templates/protocols/spider/templates/spec.md +0 -140
  129. package/templates/protocols/spider-solo/protocol.md +0 -619
  130. package/templates/protocols/tick/protocol.md +0 -250
  131. package/templates/roles/architect.md +0 -230
  132. package/templates/tower.html +0 -1032
  133. /package/{templates/AGENTS.md → skeleton/AGENTS.md.template} +0 -0
  134. /package/{templates/CLAUDE.md → skeleton/CLAUDE.md.template} +0 -0
  135. /package/{templates → skeleton}/bin/codev-doctor +0 -0
  136. /package/{templates → skeleton}/builders.md +0 -0
  137. /package/{templates → skeleton}/config.json +0 -0
  138. /package/{templates → skeleton}/plans/.gitkeep +0 -0
  139. /package/{templates → skeleton}/protocols/experiment/templates/notes.md +0 -0
  140. /package/{templates/protocols/spider-solo → skeleton/protocols/spider}/templates/review.md +0 -0
  141. /package/{templates → skeleton}/protocols/tick/templates/plan.md +0 -0
  142. /package/{templates → skeleton}/protocols/tick/templates/review.md +0 -0
  143. /package/{templates → skeleton}/protocols/tick/templates/spec.md +0 -0
  144. /package/{templates → skeleton}/reviews/.gitkeep +0 -0
  145. /package/{templates → skeleton}/roles/consultant.md +0 -0
  146. /package/{templates → skeleton}/specs/.gitkeep +0 -0
@@ -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, '&amp;')
1009
- .replace(/</g, '&lt;')
1010
- .replace(/>/g, '&gt;')
1011
- .replace(/"/g, '&quot;')
1012
- .replace(/'/g, '&#39;');
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>