@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
package/dist/services.js
ADDED
|
@@ -0,0 +1,194 @@
|
|
|
1
|
+
import { spawn } from "node:child_process";
|
|
2
|
+
import { appendFile, writeFile, readFile } from "node:fs/promises";
|
|
3
|
+
import { existsSync } from "node:fs";
|
|
4
|
+
import { join } from "node:path";
|
|
5
|
+
import * as store from "./store.js";
|
|
6
|
+
import { getProjectRoot } from "./projects.js";
|
|
7
|
+
const MAX_LOG_LINES = 10_000;
|
|
8
|
+
const TRUNCATE_CHECK_INTERVAL = 1000; // check every 1000 lines appended
|
|
9
|
+
// In-memory map of running services
|
|
10
|
+
const runningServices = new Map();
|
|
11
|
+
export function getServiceStatus(id) {
|
|
12
|
+
const running = runningServices.get(id);
|
|
13
|
+
if (running) {
|
|
14
|
+
return {
|
|
15
|
+
status: running.status,
|
|
16
|
+
pid: running.pid,
|
|
17
|
+
startedAt: running.startedAt,
|
|
18
|
+
exitCode: running.exitCode,
|
|
19
|
+
};
|
|
20
|
+
}
|
|
21
|
+
return { status: "stopped" };
|
|
22
|
+
}
|
|
23
|
+
export function getAllServiceStatuses() {
|
|
24
|
+
const result = new Map();
|
|
25
|
+
for (const [id, running] of runningServices) {
|
|
26
|
+
result.set(id, {
|
|
27
|
+
status: running.status,
|
|
28
|
+
pid: running.pid,
|
|
29
|
+
startedAt: running.startedAt,
|
|
30
|
+
exitCode: running.exitCode,
|
|
31
|
+
});
|
|
32
|
+
}
|
|
33
|
+
return result;
|
|
34
|
+
}
|
|
35
|
+
function logPath(id) {
|
|
36
|
+
return join(store.getServicesLogDir(), `${id}.log`);
|
|
37
|
+
}
|
|
38
|
+
async function appendLog(id, data) {
|
|
39
|
+
const dir = await store.ensureServicesLogDir();
|
|
40
|
+
const path = logPath(id);
|
|
41
|
+
const timestamp = new Date().toISOString();
|
|
42
|
+
const lines = data.split("\n").filter(Boolean);
|
|
43
|
+
const formatted = lines.map((line) => `[${timestamp}] ${line}\n`).join("");
|
|
44
|
+
await appendFile(path, formatted, "utf-8");
|
|
45
|
+
// Track lines for truncation
|
|
46
|
+
const running = runningServices.get(id);
|
|
47
|
+
if (running) {
|
|
48
|
+
running.linesAppended += lines.length;
|
|
49
|
+
if (running.linesAppended >= TRUNCATE_CHECK_INTERVAL) {
|
|
50
|
+
running.linesAppended = 0;
|
|
51
|
+
truncateLog(id).catch(() => { });
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
async function truncateLog(id) {
|
|
56
|
+
const path = logPath(id);
|
|
57
|
+
if (!existsSync(path))
|
|
58
|
+
return;
|
|
59
|
+
try {
|
|
60
|
+
const content = await readFile(path, "utf-8");
|
|
61
|
+
const lines = content.split("\n");
|
|
62
|
+
if (lines.length > MAX_LOG_LINES) {
|
|
63
|
+
const kept = lines.slice(lines.length - MAX_LOG_LINES).join("\n");
|
|
64
|
+
await writeFile(path, kept, "utf-8");
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
catch {
|
|
68
|
+
// Ignore truncation errors
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
export async function startService(id) {
|
|
72
|
+
if (runningServices.has(id)) {
|
|
73
|
+
const existing = runningServices.get(id);
|
|
74
|
+
if (existing.status === "running") {
|
|
75
|
+
return { ok: false, error: "Service already running" };
|
|
76
|
+
}
|
|
77
|
+
// Clean up crashed/stopped entry
|
|
78
|
+
runningServices.delete(id);
|
|
79
|
+
}
|
|
80
|
+
const service = await store.getService(id);
|
|
81
|
+
if (!service) {
|
|
82
|
+
return { ok: false, error: "Service not found" };
|
|
83
|
+
}
|
|
84
|
+
const cwd = service.cwd || getProjectRoot();
|
|
85
|
+
// Ensure log directory exists
|
|
86
|
+
await store.ensureServicesLogDir();
|
|
87
|
+
const child = spawn("sh", ["-c", service.command], {
|
|
88
|
+
cwd,
|
|
89
|
+
stdio: ["ignore", "pipe", "pipe"],
|
|
90
|
+
detached: false,
|
|
91
|
+
});
|
|
92
|
+
if (!child.pid) {
|
|
93
|
+
return { ok: false, error: "Failed to spawn process" };
|
|
94
|
+
}
|
|
95
|
+
const running = {
|
|
96
|
+
id,
|
|
97
|
+
process: child,
|
|
98
|
+
status: "running",
|
|
99
|
+
pid: child.pid,
|
|
100
|
+
startedAt: new Date().toISOString(),
|
|
101
|
+
linesAppended: 0,
|
|
102
|
+
};
|
|
103
|
+
runningServices.set(id, running);
|
|
104
|
+
// Pipe stdout/stderr to log file
|
|
105
|
+
child.stdout?.on("data", (data) => {
|
|
106
|
+
appendLog(id, data.toString()).catch(() => { });
|
|
107
|
+
});
|
|
108
|
+
child.stderr?.on("data", (data) => {
|
|
109
|
+
appendLog(id, data.toString()).catch(() => { });
|
|
110
|
+
});
|
|
111
|
+
child.on("exit", (code, signal) => {
|
|
112
|
+
const entry = runningServices.get(id);
|
|
113
|
+
if (entry) {
|
|
114
|
+
entry.exitCode = code ?? undefined;
|
|
115
|
+
if (entry.status === "running") {
|
|
116
|
+
// Was not manually stopped — it crashed
|
|
117
|
+
entry.status = code === 0 ? "stopped" : "crashed";
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
appendLog(id, `Process exited with code ${code} (signal: ${signal})`).catch(() => { });
|
|
121
|
+
});
|
|
122
|
+
child.on("error", (err) => {
|
|
123
|
+
const entry = runningServices.get(id);
|
|
124
|
+
if (entry) {
|
|
125
|
+
entry.status = "crashed";
|
|
126
|
+
}
|
|
127
|
+
appendLog(id, `Process error: ${err.message}`).catch(() => { });
|
|
128
|
+
});
|
|
129
|
+
await appendLog(id, `Started: ${service.command} (pid: ${child.pid})`);
|
|
130
|
+
return { ok: true };
|
|
131
|
+
}
|
|
132
|
+
export async function stopService(id) {
|
|
133
|
+
const running = runningServices.get(id);
|
|
134
|
+
if (!running || running.status !== "running") {
|
|
135
|
+
return { ok: false, error: "Service not running" };
|
|
136
|
+
}
|
|
137
|
+
running.status = "stopped";
|
|
138
|
+
return new Promise((resolve) => {
|
|
139
|
+
const child = running.process;
|
|
140
|
+
// Send SIGTERM
|
|
141
|
+
child.kill("SIGTERM");
|
|
142
|
+
// Escalate to SIGKILL after 5s
|
|
143
|
+
const killTimeout = setTimeout(() => {
|
|
144
|
+
try {
|
|
145
|
+
child.kill("SIGKILL");
|
|
146
|
+
}
|
|
147
|
+
catch {
|
|
148
|
+
// Already dead
|
|
149
|
+
}
|
|
150
|
+
}, 5000);
|
|
151
|
+
child.on("exit", () => {
|
|
152
|
+
clearTimeout(killTimeout);
|
|
153
|
+
resolve({ ok: true });
|
|
154
|
+
});
|
|
155
|
+
// Safety timeout — if exit never fires
|
|
156
|
+
setTimeout(() => {
|
|
157
|
+
clearTimeout(killTimeout);
|
|
158
|
+
resolve({ ok: true });
|
|
159
|
+
}, 10000);
|
|
160
|
+
});
|
|
161
|
+
}
|
|
162
|
+
export async function restartService(id) {
|
|
163
|
+
const running = runningServices.get(id);
|
|
164
|
+
if (running && running.status === "running") {
|
|
165
|
+
await stopService(id);
|
|
166
|
+
}
|
|
167
|
+
runningServices.delete(id);
|
|
168
|
+
return startService(id);
|
|
169
|
+
}
|
|
170
|
+
export async function getServiceLogs(id, tailLines = 200) {
|
|
171
|
+
const path = logPath(id);
|
|
172
|
+
if (!existsSync(path))
|
|
173
|
+
return "";
|
|
174
|
+
try {
|
|
175
|
+
const content = await readFile(path, "utf-8");
|
|
176
|
+
const lines = content.split("\n");
|
|
177
|
+
return lines.slice(-tailLines).join("\n");
|
|
178
|
+
}
|
|
179
|
+
catch {
|
|
180
|
+
return "";
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
export async function clearServiceLogs(id) {
|
|
184
|
+
const path = logPath(id);
|
|
185
|
+
await store.ensureServicesLogDir();
|
|
186
|
+
await writeFile(path, "", "utf-8");
|
|
187
|
+
}
|
|
188
|
+
export async function getServicesWithStatus() {
|
|
189
|
+
const services = await store.readServices();
|
|
190
|
+
return services.map((s) => {
|
|
191
|
+
const status = getServiceStatus(s.id);
|
|
192
|
+
return { ...s, ...status };
|
|
193
|
+
});
|
|
194
|
+
}
|