@longshot/cli 0.0.1 → 0.0.2
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/cli.js +82 -25
- package/dist/index.js +2 -2
- package/dist/projects.js +13 -5
- package/dist/queue.js +44 -14
- package/dist/tunnel.js +152 -0
- package/package.json +5 -4
package/dist/cli.js
CHANGED
|
@@ -3,6 +3,7 @@ import { parseArgs } from "node:util";
|
|
|
3
3
|
import { resolve, join, dirname } from "node:path";
|
|
4
4
|
import { existsSync, readFileSync, readdirSync } from "node:fs";
|
|
5
5
|
import { fileURLToPath } from "node:url";
|
|
6
|
+
import { homedir } from "node:os";
|
|
6
7
|
const __dirname = dirname(fileURLToPath(import.meta.url));
|
|
7
8
|
function getVersion() {
|
|
8
9
|
// Walk up from src/ or dist/ to find package.json
|
|
@@ -23,15 +24,19 @@ function printHelp() {
|
|
|
23
24
|
Usage: longshot [options]
|
|
24
25
|
|
|
25
26
|
Options:
|
|
26
|
-
-p, --port <number>
|
|
27
|
-
-d, --dir <path>
|
|
28
|
-
-
|
|
29
|
-
|
|
27
|
+
-p, --port <number> Port for the web server (default: 3333)
|
|
28
|
+
-d, --dir <path> Project root directory (default: current directory)
|
|
29
|
+
-c, --connect Connect to remote proxy tunnel
|
|
30
|
+
--proxy-url <url> Proxy WebSocket URL (overrides auth.json / env)
|
|
31
|
+
--token <token> Auth token (overrides auth.json / env)
|
|
32
|
+
-h, --help Show this help message
|
|
33
|
+
-v, --version Show version number
|
|
30
34
|
|
|
31
35
|
Examples:
|
|
32
36
|
longshot Start in current directory on port 3333
|
|
33
37
|
longshot -p 8080 Start on port 8080
|
|
34
38
|
longshot -d ~/my-project Start with a specific project directory
|
|
39
|
+
longshot --connect Start server and connect to proxy tunnel
|
|
35
40
|
|
|
36
41
|
First run:
|
|
37
42
|
If the target directory isn't a longshot project yet, you'll be guided
|
|
@@ -45,6 +50,9 @@ export function parseCliArgs(args) {
|
|
|
45
50
|
dir: { type: "string", short: "d" },
|
|
46
51
|
help: { type: "boolean", short: "h", default: false },
|
|
47
52
|
version: { type: "boolean", short: "v", default: false },
|
|
53
|
+
connect: { type: "boolean", short: "c", default: false },
|
|
54
|
+
"proxy-url": { type: "string" },
|
|
55
|
+
token: { type: "string" },
|
|
48
56
|
},
|
|
49
57
|
strict: true,
|
|
50
58
|
allowPositionals: false,
|
|
@@ -54,23 +62,44 @@ export function parseCliArgs(args) {
|
|
|
54
62
|
dir: values.dir ? resolve(values.dir) : process.cwd(),
|
|
55
63
|
help: values.help ?? false,
|
|
56
64
|
version: values.version ?? false,
|
|
65
|
+
connect: values.connect ?? false,
|
|
66
|
+
proxyUrl: values["proxy-url"],
|
|
67
|
+
token: values.token,
|
|
57
68
|
};
|
|
58
69
|
}
|
|
70
|
+
function loadAuthConfig() {
|
|
71
|
+
const authPath = join(homedir(), ".longshot", "auth.json");
|
|
72
|
+
try {
|
|
73
|
+
if (existsSync(authPath)) {
|
|
74
|
+
return JSON.parse(readFileSync(authPath, "utf-8"));
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
catch {
|
|
78
|
+
// Ignore malformed auth.json
|
|
79
|
+
}
|
|
80
|
+
return {};
|
|
81
|
+
}
|
|
82
|
+
export function resolveTunnelConfig(parsed) {
|
|
83
|
+
const auth = loadAuthConfig();
|
|
84
|
+
const proxyUrl = parsed.proxyUrl
|
|
85
|
+
|| process.env.LONGSHOT_PROXY_URL
|
|
86
|
+
|| auth.proxyUrl;
|
|
87
|
+
const token = parsed.token
|
|
88
|
+
|| process.env.LONGSHOT_TOKEN
|
|
89
|
+
|| auth.token;
|
|
90
|
+
return { proxyUrl, token };
|
|
91
|
+
}
|
|
59
92
|
class InquirerPrompter {
|
|
60
93
|
async selectMode() {
|
|
61
|
-
const
|
|
94
|
+
const { select } = await import("@inquirer/prompts");
|
|
62
95
|
try {
|
|
63
|
-
const
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
name: "
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
{ name: "Multi-project — manage multiple subdirectories", value: "multi" },
|
|
71
|
-
],
|
|
72
|
-
},
|
|
73
|
-
]);
|
|
96
|
+
const mode = await select({
|
|
97
|
+
message: "How would you like to use longshot?",
|
|
98
|
+
choices: [
|
|
99
|
+
{ name: "Single project — initialize this directory", value: "single" },
|
|
100
|
+
{ name: "Multi-project — manage multiple subdirectories", value: "multi" },
|
|
101
|
+
],
|
|
102
|
+
});
|
|
74
103
|
return mode;
|
|
75
104
|
}
|
|
76
105
|
catch {
|
|
@@ -78,16 +107,12 @@ class InquirerPrompter {
|
|
|
78
107
|
}
|
|
79
108
|
}
|
|
80
109
|
async selectFolders(folders) {
|
|
81
|
-
const
|
|
110
|
+
const { checkbox } = await import("@inquirer/prompts");
|
|
82
111
|
try {
|
|
83
|
-
const
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
message: "Select project directories:",
|
|
88
|
-
choices: folders.map((f) => ({ name: f, value: f })),
|
|
89
|
-
},
|
|
90
|
-
]);
|
|
112
|
+
const selected = await checkbox({
|
|
113
|
+
message: "Select project directories:",
|
|
114
|
+
choices: folders.map((f) => ({ name: f, value: f })),
|
|
115
|
+
});
|
|
91
116
|
return selected;
|
|
92
117
|
}
|
|
93
118
|
catch {
|
|
@@ -162,9 +187,41 @@ async function main() {
|
|
|
162
187
|
// Set environment for the server
|
|
163
188
|
process.env.PORT = String(parsed.port);
|
|
164
189
|
process.env.PROJECT_ROOT = parsed.dir;
|
|
190
|
+
process.env.LONGSHOT_CLI = "1";
|
|
165
191
|
// Start the server
|
|
166
192
|
const { startServer } = await import("./index.js");
|
|
167
193
|
await startServer();
|
|
194
|
+
// Start tunnel if --connect flag is used
|
|
195
|
+
if (parsed.connect) {
|
|
196
|
+
const { proxyUrl, token } = resolveTunnelConfig(parsed);
|
|
197
|
+
if (!proxyUrl || !token) {
|
|
198
|
+
console.error("Tunnel requires --proxy-url and --token (or ~/.longshot/auth.json or LONGSHOT_PROXY_URL/LONGSHOT_TOKEN env vars)");
|
|
199
|
+
process.exit(1);
|
|
200
|
+
}
|
|
201
|
+
const { TunnelClient } = await import("./tunnel.js");
|
|
202
|
+
const tunnel = new TunnelClient({
|
|
203
|
+
proxyUrl,
|
|
204
|
+
authToken: token,
|
|
205
|
+
localPort: parsed.port,
|
|
206
|
+
onConnected: () => console.log("Connected to proxy"),
|
|
207
|
+
onDisconnected: () => console.log("Disconnected from proxy, reconnecting..."),
|
|
208
|
+
onError: (err) => console.error("Tunnel error:", err.message),
|
|
209
|
+
});
|
|
210
|
+
const shutdown = () => {
|
|
211
|
+
console.log("Disconnecting tunnel...");
|
|
212
|
+
tunnel.disconnect();
|
|
213
|
+
process.exit(0);
|
|
214
|
+
};
|
|
215
|
+
process.on("SIGINT", shutdown);
|
|
216
|
+
process.on("SIGTERM", shutdown);
|
|
217
|
+
try {
|
|
218
|
+
await tunnel.connect();
|
|
219
|
+
}
|
|
220
|
+
catch (err) {
|
|
221
|
+
console.error("Failed to connect tunnel:", err.message);
|
|
222
|
+
process.exit(1);
|
|
223
|
+
}
|
|
224
|
+
}
|
|
168
225
|
}
|
|
169
226
|
main().catch((err) => {
|
|
170
227
|
console.error("Fatal error:", err);
|
package/dist/index.js
CHANGED
|
@@ -1244,7 +1244,7 @@ export async function startServer() {
|
|
|
1244
1244
|
}
|
|
1245
1245
|
});
|
|
1246
1246
|
}
|
|
1247
|
-
// Only start server when not imported
|
|
1248
|
-
if (!process.env.TESTING) {
|
|
1247
|
+
// Only start server when run directly (not imported by CLI or tests)
|
|
1248
|
+
if (!process.env.TESTING && !process.env.LONGSHOT_CLI) {
|
|
1249
1249
|
startServer();
|
|
1250
1250
|
}
|
package/dist/projects.js
CHANGED
|
@@ -64,17 +64,25 @@ export function detectMode() {
|
|
|
64
64
|
renameSync(remcDir, newDir);
|
|
65
65
|
console.log("Migrated .rem-c/ → .longshot/");
|
|
66
66
|
}
|
|
67
|
+
const hasProjectsJson = existsSync(join(ROOT_DIR, ".longshot", "projects.json"));
|
|
67
68
|
const hasLocalData = existsSync(join(ROOT_DIR, ".longshot"));
|
|
68
|
-
if (
|
|
69
|
-
//
|
|
69
|
+
if (hasProjectsJson) {
|
|
70
|
+
// Multi-project mode — parent dir has projects.json
|
|
71
|
+
multiProject = true;
|
|
72
|
+
autoDiscover();
|
|
73
|
+
}
|
|
74
|
+
else if (hasLocalData) {
|
|
75
|
+
// Single project mode — has .longshot/ but no projects.json
|
|
70
76
|
multiProject = false;
|
|
71
77
|
currentProjectRoot = ROOT_DIR;
|
|
72
78
|
currentProjectName = null;
|
|
73
79
|
return;
|
|
74
80
|
}
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
81
|
+
else {
|
|
82
|
+
// No data at all — look for child projects
|
|
83
|
+
multiProject = true;
|
|
84
|
+
autoDiscover();
|
|
85
|
+
}
|
|
78
86
|
// Switch to the first project if any exist
|
|
79
87
|
const projects = readProjects();
|
|
80
88
|
if (projects.length > 0) {
|
package/dist/queue.js
CHANGED
|
@@ -415,7 +415,6 @@ Context available if needed:
|
|
|
415
415
|
break;
|
|
416
416
|
}
|
|
417
417
|
case "start-work": {
|
|
418
|
-
await store.updateTaskStatus(item.params.taskId, "in_progress");
|
|
419
418
|
const task = await store.getTask(item.params.taskId);
|
|
420
419
|
if (!task) {
|
|
421
420
|
await store.updateQueueItem(item.id, {
|
|
@@ -425,6 +424,15 @@ Context available if needed:
|
|
|
425
424
|
});
|
|
426
425
|
return;
|
|
427
426
|
}
|
|
427
|
+
// Skip duplicate start-work for tasks already complete/rejected
|
|
428
|
+
if (task.meta.status === "complete" || task.meta.status === "rejected" || task.meta.completed || task.meta.checkpoint) {
|
|
429
|
+
await store.updateQueueItem(item.id, {
|
|
430
|
+
status: "done",
|
|
431
|
+
completedAt: new Date().toISOString(),
|
|
432
|
+
});
|
|
433
|
+
return;
|
|
434
|
+
}
|
|
435
|
+
await store.updateTaskStatus(item.params.taskId, "in_progress");
|
|
428
436
|
const prompt = `Implement task #${item.params.taskId}: ${task.meta.title}\n\nTask spec:\n${task.spec}`;
|
|
429
437
|
result = await runAgent(prompt, `Task #${item.params.taskId}: ${task.meta.title}`, task, item.params.taskId);
|
|
430
438
|
break;
|
|
@@ -440,6 +448,14 @@ Context available if needed:
|
|
|
440
448
|
});
|
|
441
449
|
return;
|
|
442
450
|
}
|
|
451
|
+
// Skip duplicate spec-update for tasks already complete
|
|
452
|
+
if (task.meta.status === "complete" || task.meta.status === "rejected") {
|
|
453
|
+
await store.updateQueueItem(item.id, {
|
|
454
|
+
status: "done",
|
|
455
|
+
completedAt: new Date().toISOString(),
|
|
456
|
+
});
|
|
457
|
+
return;
|
|
458
|
+
}
|
|
443
459
|
const padId = String(task.meta.id).padStart(3, "0");
|
|
444
460
|
const taskDir = `.longshot/tasks/${padId}-${task.meta.slug}`;
|
|
445
461
|
const specPrompt = `Task #${taskIdForUpdate} "${task.meta.title}" has just been implemented and approved.
|
|
@@ -598,9 +614,18 @@ async function revertTaskStatusOnFailure(item) {
|
|
|
598
614
|
if (!taskId)
|
|
599
615
|
return;
|
|
600
616
|
// Don't revert terminal statuses — a completed/rejected task should stay that way
|
|
617
|
+
// Also check for completed/checkpoint fields, since a duplicate start-work may have
|
|
618
|
+
// overwritten the status to in_progress even though the task was already done
|
|
601
619
|
const task = await store.getTask(taskId);
|
|
602
|
-
if (task
|
|
620
|
+
if (!task)
|
|
621
|
+
return;
|
|
622
|
+
if (task.meta.status === "complete" || task.meta.status === "rejected")
|
|
623
|
+
return;
|
|
624
|
+
if (task.meta.completed || task.meta.checkpoint) {
|
|
625
|
+
// Task was completed but status got overwritten — restore it
|
|
626
|
+
await store.updateTaskStatus(taskId, "complete");
|
|
603
627
|
return;
|
|
628
|
+
}
|
|
604
629
|
switch (item.type) {
|
|
605
630
|
case "start-work": {
|
|
606
631
|
// If there's a pending diff from a previous run, stay in_progress
|
|
@@ -666,18 +691,23 @@ export async function enqueue(type, params) {
|
|
|
666
691
|
params.taskSlug = task.slug;
|
|
667
692
|
}
|
|
668
693
|
const item = await store.addQueueItem({ type, params });
|
|
669
|
-
// Set transitional task status immediately on enqueue
|
|
670
|
-
|
|
671
|
-
|
|
672
|
-
|
|
673
|
-
|
|
674
|
-
|
|
675
|
-
|
|
676
|
-
|
|
677
|
-
|
|
678
|
-
|
|
679
|
-
|
|
680
|
-
|
|
694
|
+
// Set transitional task status immediately on enqueue — but only if the task
|
|
695
|
+
// is in the expected state. Never overwrite a "further along" or terminal status.
|
|
696
|
+
if (params.taskId) {
|
|
697
|
+
const task = await store.getTask(params.taskId);
|
|
698
|
+
const status = task?.meta.status;
|
|
699
|
+
if (type === "start-work" && status === "ready") {
|
|
700
|
+
await store.updateTaskStatus(params.taskId, "queued");
|
|
701
|
+
}
|
|
702
|
+
else if (type === "refine-task" && status === "drafting") {
|
|
703
|
+
await store.updateTaskStatus(params.taskId, "refining");
|
|
704
|
+
}
|
|
705
|
+
else if (type === "spec-update" && status === "in_progress") {
|
|
706
|
+
await store.updateTaskStatus(params.taskId, "approved");
|
|
707
|
+
}
|
|
708
|
+
else if (type === "fix-task" && status === "in_progress") {
|
|
709
|
+
await store.updateTaskStatus(params.taskId, "fixing");
|
|
710
|
+
}
|
|
681
711
|
}
|
|
682
712
|
// Start processing if not already running
|
|
683
713
|
processNext().catch((err) => console.error("Queue processor error:", err));
|
package/dist/tunnel.js
ADDED
|
@@ -0,0 +1,152 @@
|
|
|
1
|
+
import WebSocket from "ws";
|
|
2
|
+
const HOP_BY_HOP_HEADERS = new Set([
|
|
3
|
+
"connection",
|
|
4
|
+
"keep-alive",
|
|
5
|
+
"proxy-authenticate",
|
|
6
|
+
"proxy-authorization",
|
|
7
|
+
"te",
|
|
8
|
+
"trailers",
|
|
9
|
+
"transfer-encoding",
|
|
10
|
+
"upgrade",
|
|
11
|
+
]);
|
|
12
|
+
export class TunnelClient {
|
|
13
|
+
ws = null;
|
|
14
|
+
opts;
|
|
15
|
+
reconnectDelay = 1000;
|
|
16
|
+
reconnectTimer = null;
|
|
17
|
+
intentionalClose = false;
|
|
18
|
+
_connected = false;
|
|
19
|
+
constructor(opts) {
|
|
20
|
+
this.opts = opts;
|
|
21
|
+
}
|
|
22
|
+
connect() {
|
|
23
|
+
this.intentionalClose = false;
|
|
24
|
+
return new Promise((resolve, reject) => {
|
|
25
|
+
this.openConnection(resolve, reject);
|
|
26
|
+
});
|
|
27
|
+
}
|
|
28
|
+
disconnect() {
|
|
29
|
+
this.intentionalClose = true;
|
|
30
|
+
if (this.reconnectTimer) {
|
|
31
|
+
clearTimeout(this.reconnectTimer);
|
|
32
|
+
this.reconnectTimer = null;
|
|
33
|
+
}
|
|
34
|
+
if (this.ws) {
|
|
35
|
+
this.ws.close();
|
|
36
|
+
this.ws = null;
|
|
37
|
+
}
|
|
38
|
+
this._connected = false;
|
|
39
|
+
}
|
|
40
|
+
isConnected() {
|
|
41
|
+
return this._connected;
|
|
42
|
+
}
|
|
43
|
+
openConnection(onFirstConnect, onFirstError) {
|
|
44
|
+
const ws = new WebSocket(this.opts.proxyUrl);
|
|
45
|
+
this.ws = ws;
|
|
46
|
+
let registered = false;
|
|
47
|
+
ws.on("open", () => {
|
|
48
|
+
ws.send(JSON.stringify({ type: "register", token: this.opts.authToken }));
|
|
49
|
+
});
|
|
50
|
+
ws.on("message", (data) => {
|
|
51
|
+
let msg;
|
|
52
|
+
try {
|
|
53
|
+
msg = JSON.parse(data.toString());
|
|
54
|
+
}
|
|
55
|
+
catch {
|
|
56
|
+
return;
|
|
57
|
+
}
|
|
58
|
+
if (msg.type === "registered") {
|
|
59
|
+
registered = true;
|
|
60
|
+
this._connected = true;
|
|
61
|
+
this.reconnectDelay = 1000;
|
|
62
|
+
this.opts.onConnected?.();
|
|
63
|
+
onFirstConnect?.();
|
|
64
|
+
onFirstConnect = undefined;
|
|
65
|
+
onFirstError = undefined;
|
|
66
|
+
}
|
|
67
|
+
else if (msg.type === "error") {
|
|
68
|
+
const err = new Error(msg.message);
|
|
69
|
+
this.opts.onError?.(err);
|
|
70
|
+
if (!registered && onFirstError) {
|
|
71
|
+
onFirstError(err);
|
|
72
|
+
onFirstConnect = undefined;
|
|
73
|
+
onFirstError = undefined;
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
else if (msg.type === "ping") {
|
|
77
|
+
ws.send(JSON.stringify({ type: "pong" }));
|
|
78
|
+
}
|
|
79
|
+
else if (msg.type === "request") {
|
|
80
|
+
this.handleRequest(msg);
|
|
81
|
+
}
|
|
82
|
+
});
|
|
83
|
+
ws.on("close", () => {
|
|
84
|
+
this._connected = false;
|
|
85
|
+
this.opts.onDisconnected?.();
|
|
86
|
+
if (!this.intentionalClose) {
|
|
87
|
+
this.scheduleReconnect();
|
|
88
|
+
}
|
|
89
|
+
});
|
|
90
|
+
ws.on("error", (err) => {
|
|
91
|
+
this.opts.onError?.(err);
|
|
92
|
+
if (!registered && onFirstError) {
|
|
93
|
+
onFirstError(err);
|
|
94
|
+
onFirstConnect = undefined;
|
|
95
|
+
onFirstError = undefined;
|
|
96
|
+
}
|
|
97
|
+
});
|
|
98
|
+
}
|
|
99
|
+
scheduleReconnect() {
|
|
100
|
+
this.reconnectTimer = setTimeout(() => {
|
|
101
|
+
this.reconnectTimer = null;
|
|
102
|
+
this.openConnection();
|
|
103
|
+
}, this.reconnectDelay);
|
|
104
|
+
this.reconnectDelay = Math.min(this.reconnectDelay * 2, 30000);
|
|
105
|
+
}
|
|
106
|
+
async handleRequest(req) {
|
|
107
|
+
const url = `http://localhost:${this.opts.localPort}${req.path}`;
|
|
108
|
+
const headers = {};
|
|
109
|
+
if (req.headers) {
|
|
110
|
+
for (const [k, v] of Object.entries(req.headers)) {
|
|
111
|
+
if (!HOP_BY_HOP_HEADERS.has(k.toLowerCase())) {
|
|
112
|
+
headers[k] = v;
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
try {
|
|
117
|
+
const resp = await fetch(url, {
|
|
118
|
+
method: req.method,
|
|
119
|
+
headers,
|
|
120
|
+
body: req.method !== "GET" && req.method !== "HEAD" ? req.body : undefined,
|
|
121
|
+
});
|
|
122
|
+
const respHeaders = {};
|
|
123
|
+
resp.headers.forEach((v, k) => {
|
|
124
|
+
if (!HOP_BY_HOP_HEADERS.has(k.toLowerCase())) {
|
|
125
|
+
respHeaders[k] = v;
|
|
126
|
+
}
|
|
127
|
+
});
|
|
128
|
+
const body = await resp.text();
|
|
129
|
+
this.send({
|
|
130
|
+
type: "response",
|
|
131
|
+
id: req.id,
|
|
132
|
+
status: resp.status,
|
|
133
|
+
headers: respHeaders,
|
|
134
|
+
body,
|
|
135
|
+
});
|
|
136
|
+
}
|
|
137
|
+
catch {
|
|
138
|
+
this.send({
|
|
139
|
+
type: "response",
|
|
140
|
+
id: req.id,
|
|
141
|
+
status: 502,
|
|
142
|
+
headers: {},
|
|
143
|
+
body: "Local server unavailable",
|
|
144
|
+
});
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
send(msg) {
|
|
148
|
+
if (this.ws && this.ws.readyState === WebSocket.OPEN) {
|
|
149
|
+
this.ws.send(JSON.stringify(msg));
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@longshot/cli",
|
|
3
|
-
"version": "0.0.
|
|
3
|
+
"version": "0.0.2",
|
|
4
4
|
"description": "Mobile-first Claude orchestrator with spec-driven workflow",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
@@ -24,12 +24,13 @@
|
|
|
24
24
|
"@hono/node-server": "^1.19.9",
|
|
25
25
|
"diff2html": "^3.4.56",
|
|
26
26
|
"hono": "^4.11.9",
|
|
27
|
-
"inquirer": "^
|
|
28
|
-
"marked": "^17.0.2"
|
|
27
|
+
"@inquirer/prompts": "^7.0.0",
|
|
28
|
+
"marked": "^17.0.2",
|
|
29
|
+
"ws": "^8.19.0"
|
|
29
30
|
},
|
|
30
31
|
"devDependencies": {
|
|
31
|
-
"@types/inquirer": "^9.0.9",
|
|
32
32
|
"@types/node": "^25.2.3",
|
|
33
|
+
"@types/ws": "^8.18.1",
|
|
33
34
|
"tsx": "^4.21.0",
|
|
34
35
|
"typescript": "^5.9.3"
|
|
35
36
|
}
|