@longshot/cli 0.0.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/LICENSE +8 -0
- package/dist/agent.js +214 -0
- package/dist/cli.js +172 -0
- package/dist/git.js +291 -0
- package/dist/index.js +1250 -0
- package/dist/profile.js +79 -0
- package/dist/projects.js +337 -0
- package/dist/queue.js +868 -0
- package/dist/services.js +194 -0
- package/dist/store.js +612 -0
- package/dist/views/agent-progress.js +242 -0
- package/dist/views/branches.js +191 -0
- package/dist/views/chat.js +386 -0
- package/dist/views/diff.js +321 -0
- package/dist/views/history.js +124 -0
- package/dist/views/layout.js +121 -0
- package/dist/views/run.js +92 -0
- package/dist/views/services.js +230 -0
- package/dist/views/spec.js +18 -0
- package/dist/views/tasks.js +898 -0
- package/dist/views/verify.js +209 -0
- package/package.json +36 -0
- package/public/style.css +2088 -0
|
@@ -0,0 +1,230 @@
|
|
|
1
|
+
import { layout } from "./layout.js";
|
|
2
|
+
function escapeHtml(str) {
|
|
3
|
+
return str
|
|
4
|
+
.replace(/&/g, "&")
|
|
5
|
+
.replace(/</g, "<")
|
|
6
|
+
.replace(/>/g, ">")
|
|
7
|
+
.replace(/"/g, """);
|
|
8
|
+
}
|
|
9
|
+
function statusDot(status) {
|
|
10
|
+
const colors = {
|
|
11
|
+
running: "var(--accent-green)",
|
|
12
|
+
stopped: "var(--text-muted)",
|
|
13
|
+
crashed: "var(--accent-red)",
|
|
14
|
+
};
|
|
15
|
+
return `<span class="service-dot" style="background:${colors[status]}"></span>`;
|
|
16
|
+
}
|
|
17
|
+
function formatUptime(startedAt) {
|
|
18
|
+
const ms = Date.now() - new Date(startedAt).getTime();
|
|
19
|
+
const seconds = Math.floor(ms / 1000);
|
|
20
|
+
if (seconds < 60)
|
|
21
|
+
return `${seconds}s`;
|
|
22
|
+
const minutes = Math.floor(seconds / 60);
|
|
23
|
+
if (minutes < 60)
|
|
24
|
+
return `${minutes}m`;
|
|
25
|
+
const hours = Math.floor(minutes / 60);
|
|
26
|
+
const mins = minutes % 60;
|
|
27
|
+
return `${hours}h ${mins}m`;
|
|
28
|
+
}
|
|
29
|
+
function renderServiceCard(service) {
|
|
30
|
+
const statusClass = `service-status-${service.status}`;
|
|
31
|
+
const actions = [];
|
|
32
|
+
if (service.status === "stopped" || service.status === "crashed") {
|
|
33
|
+
actions.push(`<button class="btn btn-small btn-service-start" onclick="startService('${service.id}')">Start</button>`);
|
|
34
|
+
}
|
|
35
|
+
if (service.status === "running") {
|
|
36
|
+
actions.push(`<button class="btn btn-small btn-service-stop" onclick="stopService('${service.id}')">Stop</button>`);
|
|
37
|
+
actions.push(`<button class="btn btn-small" onclick="restartService('${service.id}')">Restart</button>`);
|
|
38
|
+
}
|
|
39
|
+
actions.push(`<a href="/services/${service.id}/logs" class="btn btn-small">Logs</a>`);
|
|
40
|
+
if (service.status !== "running") {
|
|
41
|
+
actions.push(`<button class="btn btn-small" onclick="editService('${service.id}')">Edit</button>`);
|
|
42
|
+
actions.push(`<button class="btn btn-small btn-service-delete" onclick="deleteService('${service.id}')">Delete</button>`);
|
|
43
|
+
}
|
|
44
|
+
const statusInfo = service.status === "running" && service.startedAt
|
|
45
|
+
? `<span class="service-uptime">up ${formatUptime(service.startedAt)}</span>`
|
|
46
|
+
: service.status === "crashed" && service.exitCode !== undefined
|
|
47
|
+
? `<span class="service-exit-code">exit ${service.exitCode}</span>`
|
|
48
|
+
: "";
|
|
49
|
+
return `<div class="service-card ${statusClass}" data-id="${service.id}" data-name="${escapeHtml(service.name)}" data-command="${escapeHtml(service.command)}" data-cwd="${escapeHtml(service.cwd || "")}">
|
|
50
|
+
<div class="service-card-top">
|
|
51
|
+
${statusDot(service.status)}
|
|
52
|
+
<span class="service-name">${escapeHtml(service.name)}</span>
|
|
53
|
+
<span class="service-status-label">${service.status}</span>
|
|
54
|
+
${statusInfo}
|
|
55
|
+
</div>
|
|
56
|
+
<div class="service-command">${escapeHtml(service.command)}</div>
|
|
57
|
+
<div class="service-actions">${actions.join("\n")}</div>
|
|
58
|
+
</div>`;
|
|
59
|
+
}
|
|
60
|
+
export function servicesPage(services) {
|
|
61
|
+
const body = `
|
|
62
|
+
<div class="services-container">
|
|
63
|
+
<div class="services-header">
|
|
64
|
+
<h1>Services</h1>
|
|
65
|
+
<button class="btn btn-primary btn-small" id="add-service-btn" onclick="showAddForm()">Add Service</button>
|
|
66
|
+
</div>
|
|
67
|
+
|
|
68
|
+
<div class="service-form" id="service-form" style="display:none">
|
|
69
|
+
<input type="hidden" id="service-edit-id" value="">
|
|
70
|
+
<input type="text" id="service-name" placeholder="Service name" class="service-input">
|
|
71
|
+
<input type="text" id="service-command" placeholder="Command (e.g. npm run dev)" class="service-input">
|
|
72
|
+
<input type="text" id="service-cwd" placeholder="Working directory (optional)" class="service-input">
|
|
73
|
+
<div class="service-form-actions">
|
|
74
|
+
<button class="btn btn-primary btn-small" onclick="saveService()">Save</button>
|
|
75
|
+
<button class="btn btn-small" onclick="hideForm()">Cancel</button>
|
|
76
|
+
</div>
|
|
77
|
+
</div>
|
|
78
|
+
|
|
79
|
+
<div class="service-list" id="service-list">
|
|
80
|
+
${services.length
|
|
81
|
+
? services.map(renderServiceCard).join("\n")
|
|
82
|
+
: '<p class="empty-state">No services defined. Add one to get started.</p>'}
|
|
83
|
+
</div>
|
|
84
|
+
</div>
|
|
85
|
+
<script>
|
|
86
|
+
function showAddForm() {
|
|
87
|
+
document.getElementById('service-edit-id').value = '';
|
|
88
|
+
document.getElementById('service-name').value = '';
|
|
89
|
+
document.getElementById('service-command').value = '';
|
|
90
|
+
document.getElementById('service-cwd').value = '';
|
|
91
|
+
document.getElementById('service-form').style.display = 'block';
|
|
92
|
+
document.getElementById('service-name').focus();
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
function editService(id) {
|
|
96
|
+
const card = document.querySelector('[data-id="' + id + '"]');
|
|
97
|
+
document.getElementById('service-edit-id').value = id;
|
|
98
|
+
document.getElementById('service-name').value = card.dataset.name;
|
|
99
|
+
document.getElementById('service-command').value = card.dataset.command;
|
|
100
|
+
document.getElementById('service-cwd').value = card.dataset.cwd;
|
|
101
|
+
document.getElementById('service-form').style.display = 'block';
|
|
102
|
+
document.getElementById('service-name').focus();
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
function hideForm() {
|
|
106
|
+
document.getElementById('service-form').style.display = 'none';
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
async function saveService() {
|
|
110
|
+
const editId = document.getElementById('service-edit-id').value;
|
|
111
|
+
const name = document.getElementById('service-name').value.trim();
|
|
112
|
+
const command = document.getElementById('service-command').value.trim();
|
|
113
|
+
const cwd = document.getElementById('service-cwd').value.trim();
|
|
114
|
+
if (!name || !command) return;
|
|
115
|
+
|
|
116
|
+
const body = { name, command, cwd: cwd || undefined };
|
|
117
|
+
if (editId) {
|
|
118
|
+
await fetch('/api/services/' + editId, {
|
|
119
|
+
method: 'PUT',
|
|
120
|
+
headers: { 'Content-Type': 'application/json' },
|
|
121
|
+
body: JSON.stringify(body),
|
|
122
|
+
});
|
|
123
|
+
} else {
|
|
124
|
+
await fetch('/api/services', {
|
|
125
|
+
method: 'POST',
|
|
126
|
+
headers: { 'Content-Type': 'application/json' },
|
|
127
|
+
body: JSON.stringify(body),
|
|
128
|
+
});
|
|
129
|
+
}
|
|
130
|
+
location.reload();
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
async function startService(id) {
|
|
134
|
+
const btn = event.target;
|
|
135
|
+
btn.disabled = true;
|
|
136
|
+
btn.textContent = 'Starting...';
|
|
137
|
+
await fetch('/api/services/' + id + '/start', { method: 'POST' });
|
|
138
|
+
location.reload();
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
async function stopService(id) {
|
|
142
|
+
const btn = event.target;
|
|
143
|
+
btn.disabled = true;
|
|
144
|
+
btn.textContent = 'Stopping...';
|
|
145
|
+
await fetch('/api/services/' + id + '/stop', { method: 'POST' });
|
|
146
|
+
location.reload();
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
async function restartService(id) {
|
|
150
|
+
const btn = event.target;
|
|
151
|
+
btn.disabled = true;
|
|
152
|
+
btn.textContent = '...';
|
|
153
|
+
await fetch('/api/services/' + id + '/restart', { method: 'POST' });
|
|
154
|
+
location.reload();
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
async function deleteService(id) {
|
|
158
|
+
if (!confirm('Delete this service?')) return;
|
|
159
|
+
await fetch('/api/services/' + id, { method: 'DELETE' });
|
|
160
|
+
location.reload();
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
// Poll for status updates every 5s
|
|
164
|
+
setInterval(async () => {
|
|
165
|
+
try {
|
|
166
|
+
const resp = await fetch('/api/services');
|
|
167
|
+
const services = await resp.json();
|
|
168
|
+
// Update status dots and labels without full reload
|
|
169
|
+
for (const s of services) {
|
|
170
|
+
const card = document.querySelector('[data-id="' + s.id + '"]');
|
|
171
|
+
if (!card) continue;
|
|
172
|
+
const dot = card.querySelector('.service-dot');
|
|
173
|
+
const label = card.querySelector('.service-status-label');
|
|
174
|
+
const colors = { running: 'var(--accent-green)', stopped: 'var(--text-muted)', crashed: 'var(--accent-red)' };
|
|
175
|
+
if (dot) dot.style.background = colors[s.status] || colors.stopped;
|
|
176
|
+
if (label) label.textContent = s.status;
|
|
177
|
+
}
|
|
178
|
+
} catch {}
|
|
179
|
+
}, 5000);
|
|
180
|
+
</script>`;
|
|
181
|
+
return layout("Services", "services", body);
|
|
182
|
+
}
|
|
183
|
+
export function serviceLogsPage(service, logs) {
|
|
184
|
+
const body = `
|
|
185
|
+
<div class="service-logs-container">
|
|
186
|
+
<div class="service-logs-header">
|
|
187
|
+
<a href="/services" class="back-link">Services</a>
|
|
188
|
+
<h1>${escapeHtml(service.name)} — Logs</h1>
|
|
189
|
+
<div class="service-logs-meta">
|
|
190
|
+
${statusDot(service.status)}
|
|
191
|
+
<span>${service.status}</span>
|
|
192
|
+
<span class="service-command-small">${escapeHtml(service.command)}</span>
|
|
193
|
+
</div>
|
|
194
|
+
</div>
|
|
195
|
+
<div class="service-logs-actions">
|
|
196
|
+
<button class="btn btn-small" onclick="clearLogs()">Clear Logs</button>
|
|
197
|
+
<label class="service-autoscroll-label">
|
|
198
|
+
<input type="checkbox" id="autoscroll" checked> Auto-scroll
|
|
199
|
+
</label>
|
|
200
|
+
</div>
|
|
201
|
+
<pre class="service-logs" id="logs">${escapeHtml(logs)}</pre>
|
|
202
|
+
</div>
|
|
203
|
+
<script>
|
|
204
|
+
const logsEl = document.getElementById('logs');
|
|
205
|
+
const autoscrollEl = document.getElementById('autoscroll');
|
|
206
|
+
|
|
207
|
+
function scrollToBottom() {
|
|
208
|
+
if (autoscrollEl.checked) {
|
|
209
|
+
logsEl.scrollTop = logsEl.scrollHeight;
|
|
210
|
+
}
|
|
211
|
+
}
|
|
212
|
+
scrollToBottom();
|
|
213
|
+
|
|
214
|
+
// Poll for new log content
|
|
215
|
+
setInterval(async () => {
|
|
216
|
+
try {
|
|
217
|
+
const resp = await fetch('/api/services/${service.id}/logs');
|
|
218
|
+
const data = await resp.json();
|
|
219
|
+
logsEl.textContent = data.logs;
|
|
220
|
+
scrollToBottom();
|
|
221
|
+
} catch {}
|
|
222
|
+
}, 2000);
|
|
223
|
+
|
|
224
|
+
async function clearLogs() {
|
|
225
|
+
await fetch('/api/services/${service.id}/logs/clear', { method: 'POST' });
|
|
226
|
+
logsEl.textContent = '';
|
|
227
|
+
}
|
|
228
|
+
</script>`;
|
|
229
|
+
return layout("Logs — " + service.name, "services", body);
|
|
230
|
+
}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import { marked } from "marked";
|
|
2
|
+
import { layout } from "./layout.js";
|
|
3
|
+
export function specPage(specContent) {
|
|
4
|
+
const rendered = specContent
|
|
5
|
+
? marked.parse(specContent, { async: false })
|
|
6
|
+
: '<p class="empty-state">No spec yet. Start chatting to build one.</p>';
|
|
7
|
+
const body = `
|
|
8
|
+
<div class="spec-container">
|
|
9
|
+
<div class="spec-header">
|
|
10
|
+
<h1>Project Spec</h1>
|
|
11
|
+
<button class="btn btn-small" onclick="location.reload()">Refresh</button>
|
|
12
|
+
</div>
|
|
13
|
+
<div class="spec-content">
|
|
14
|
+
${rendered}
|
|
15
|
+
</div>
|
|
16
|
+
</div>`;
|
|
17
|
+
return layout("Spec", "spec", body);
|
|
18
|
+
}
|