@burdenoff/vibe-agent 2.1.1 → 2.3.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.
- package/dist/app-sm6n9xst.js +1165 -0
- package/dist/app-sm6n9xst.js.map +19 -0
- package/dist/cli.js +167 -2050
- package/dist/cli.js.map +8 -30
- package/dist/index-05qfwz8r.js +122 -0
- package/dist/index-05qfwz8r.js.map +10 -0
- package/dist/index-30p492yv.js +294 -0
- package/dist/index-30p492yv.js.map +13 -0
- package/dist/index-3rjnbp97.js +268 -0
- package/dist/index-3rjnbp97.js.map +13 -0
- package/dist/index-6vry08rz.js +285 -0
- package/dist/index-6vry08rz.js.map +13 -0
- package/dist/index-88ym10cs.js +194 -0
- package/dist/index-88ym10cs.js.map +10 -0
- package/dist/index-a9g7hbj9.js +229 -0
- package/dist/index-a9g7hbj9.js.map +13 -0
- package/dist/index-atjhkm74.js +149 -0
- package/dist/index-atjhkm74.js.map +10 -0
- package/dist/index-b1eq3qvs.js +515 -0
- package/dist/index-b1eq3qvs.js.map +19 -0
- package/dist/index-c7zy3n33.js +167 -0
- package/dist/index-c7zy3n33.js.map +13 -0
- package/dist/index-fm6gqenc.js +338 -0
- package/dist/index-fm6gqenc.js.map +13 -0
- package/dist/index-hefqxwht.js +270 -0
- package/dist/index-hefqxwht.js.map +13 -0
- package/dist/index-k9hb0b93.js +280 -0
- package/dist/index-k9hb0b93.js.map +13 -0
- package/dist/index-npmvh1x9.js +385 -0
- package/dist/index-npmvh1x9.js.map +13 -0
- package/dist/index-rdm6e3rr.js +587 -0
- package/dist/index-rdm6e3rr.js.map +13 -0
- package/dist/index-s7ff1fj1.js +415 -0
- package/dist/index-s7ff1fj1.js.map +11 -0
- package/dist/index-t4qgjy5w.js +287 -0
- package/dist/index-t4qgjy5w.js.map +13 -0
- package/dist/index-wmvkjcjj.js +301 -0
- package/dist/index-wmvkjcjj.js.map +13 -0
- package/dist/{app-31chs2a1.js → index-wr0mkm57.js} +8 -3201
- package/dist/{app-31chs2a1.js.map → index-wr0mkm57.js.map} +4 -25
- package/dist/index-xmeskdnb.js +292 -0
- package/dist/index-xmeskdnb.js.map +11 -0
- package/dist/index.js +9 -6
- package/dist/index.js.map +2 -2
- package/dist/{package-hb6db316.js → package-04nkt49b.js} +5 -3
- package/dist/{package-hb6db316.js.map → package-04nkt49b.js.map} +1 -1
- package/dist/plugin-system-7r9mb1tb.js +479 -0
- package/dist/plugin-system-7r9mb1tb.js.map +10 -0
- package/package.json +3 -1
- package/dist/index-t06ktmx9.js +0 -216
- package/dist/index-t06ktmx9.js.map +0 -11
- package/dist/plugin-system-bg1pzjj9.js +0 -450
- package/dist/plugin-system-bg1pzjj9.js.map +0 -11
|
@@ -1,18 +1,9 @@
|
|
|
1
1
|
// @bun
|
|
2
|
-
import {
|
|
3
|
-
ServiceRegistry,
|
|
4
|
-
checkDependencies,
|
|
5
|
-
installDependencies
|
|
6
|
-
} from "./index-t06ktmx9.js";
|
|
7
2
|
import {
|
|
8
3
|
__export,
|
|
9
4
|
__require,
|
|
10
5
|
__toESM
|
|
11
6
|
} from "./index-g8dczzvv.js";
|
|
12
|
-
import {
|
|
13
|
-
PluginManager,
|
|
14
|
-
logger
|
|
15
|
-
} from "./plugin-system-bg1pzjj9.js";
|
|
16
7
|
|
|
17
8
|
// node_modules/@sinclair/typebox/build/esm/type/guard/value.mjs
|
|
18
9
|
var exports_value = {};
|
|
@@ -1058,7 +1049,7 @@ function Literal(value, options) {
|
|
|
1058
1049
|
}
|
|
1059
1050
|
|
|
1060
1051
|
// node_modules/@sinclair/typebox/build/esm/type/boolean/boolean.mjs
|
|
1061
|
-
function
|
|
1052
|
+
function Boolean(options) {
|
|
1062
1053
|
return CreateType({ [Kind]: "Boolean", type: "boolean" }, options);
|
|
1063
1054
|
}
|
|
1064
1055
|
|
|
@@ -1080,7 +1071,7 @@ function String2(options) {
|
|
|
1080
1071
|
// node_modules/@sinclair/typebox/build/esm/type/template-literal/syntax.mjs
|
|
1081
1072
|
function* FromUnion(syntax) {
|
|
1082
1073
|
const trim = syntax.trim().replace(/"|'/g, "");
|
|
1083
|
-
return trim === "boolean" ? yield
|
|
1074
|
+
return trim === "boolean" ? yield Boolean() : trim === "number" ? yield Number2() : trim === "bigint" ? yield BigInt2() : trim === "string" ? yield String2() : yield (() => {
|
|
1084
1075
|
const literals = trim.split("|").map((literal) => Literal(literal.trim()));
|
|
1085
1076
|
return literals.length === 0 ? Never() : literals.length === 1 ? literals[0] : UnionEvaluated(literals);
|
|
1086
1077
|
})();
|
|
@@ -2693,7 +2684,7 @@ __export(exports_type3, {
|
|
|
2693
2684
|
Const: () => Const,
|
|
2694
2685
|
Composite: () => Composite,
|
|
2695
2686
|
Capitalize: () => Capitalize,
|
|
2696
|
-
Boolean: () =>
|
|
2687
|
+
Boolean: () => Boolean,
|
|
2697
2688
|
BigInt: () => BigInt2,
|
|
2698
2689
|
Awaited: () => Awaited,
|
|
2699
2690
|
AsyncIterator: () => AsyncIterator,
|
|
@@ -13966,8 +13957,8 @@ class Elysia {
|
|
|
13966
13957
|
}
|
|
13967
13958
|
handle = async (request) => this.fetch(request);
|
|
13968
13959
|
get fetch() {
|
|
13969
|
-
let
|
|
13970
|
-
return Object.defineProperty(this, "fetch", { value:
|
|
13960
|
+
let fetch = this.config.aot ? composeGeneralHandler(this) : createDynamicHandler(this);
|
|
13961
|
+
return Object.defineProperty(this, "fetch", { value: fetch, configurable: true, writable: true }), fetch;
|
|
13971
13962
|
}
|
|
13972
13963
|
handleError = async (context, error) => {
|
|
13973
13964
|
return (this.handleError = this.config.aot ? composeErrorHandler(this) : createDynamicErrorHandler(this))(context, error);
|
|
@@ -13987,3191 +13978,7 @@ class Elysia {
|
|
|
13987
13978
|
}
|
|
13988
13979
|
}
|
|
13989
13980
|
|
|
13990
|
-
|
|
13991
|
-
var isBun2 = typeof new Headers()?.toJSON === "function";
|
|
13992
|
-
var processHeaders = (headers) => {
|
|
13993
|
-
if (isBun2)
|
|
13994
|
-
return Object.keys(headers.toJSON()).join(", ");
|
|
13995
|
-
let keys = "";
|
|
13996
|
-
let i = 0;
|
|
13997
|
-
headers.forEach((_2, key) => {
|
|
13998
|
-
if (i)
|
|
13999
|
-
keys = keys + ", " + key;
|
|
14000
|
-
else
|
|
14001
|
-
keys = key;
|
|
14002
|
-
i++;
|
|
14003
|
-
});
|
|
14004
|
-
return keys;
|
|
14005
|
-
};
|
|
14006
|
-
var cors = (config) => {
|
|
14007
|
-
let {
|
|
14008
|
-
aot = true,
|
|
14009
|
-
origin = true,
|
|
14010
|
-
methods = true,
|
|
14011
|
-
allowedHeaders = true,
|
|
14012
|
-
exposeHeaders = true,
|
|
14013
|
-
credentials = true,
|
|
14014
|
-
maxAge = 5,
|
|
14015
|
-
preflight = true
|
|
14016
|
-
} = config ?? {};
|
|
14017
|
-
if (Array.isArray(allowedHeaders))
|
|
14018
|
-
allowedHeaders = allowedHeaders.join(", ");
|
|
14019
|
-
if (Array.isArray(exposeHeaders))
|
|
14020
|
-
exposeHeaders = exposeHeaders.join(", ");
|
|
14021
|
-
const origins = typeof origin === "boolean" ? undefined : Array.isArray(origin) ? origin : [origin];
|
|
14022
|
-
const app = new Elysia({
|
|
14023
|
-
name: "@elysiajs/cors",
|
|
14024
|
-
seed: config,
|
|
14025
|
-
aot
|
|
14026
|
-
});
|
|
14027
|
-
const anyOrigin = origins?.some((o) => o === "*");
|
|
14028
|
-
const originMap = {};
|
|
14029
|
-
if (origins) {
|
|
14030
|
-
for (const origin2 of origins)
|
|
14031
|
-
if (typeof origin2 === "string")
|
|
14032
|
-
originMap[origin2] = true;
|
|
14033
|
-
}
|
|
14034
|
-
const processOrigin = (origin2, request, from) => {
|
|
14035
|
-
if (Array.isArray(origin2))
|
|
14036
|
-
return origin2.some((o) => processOrigin(o, request, from));
|
|
14037
|
-
switch (typeof origin2) {
|
|
14038
|
-
case "string":
|
|
14039
|
-
if (from in originMap)
|
|
14040
|
-
return true;
|
|
14041
|
-
const fromProtocol = from.indexOf("://");
|
|
14042
|
-
if (fromProtocol !== -1)
|
|
14043
|
-
from = from.slice(fromProtocol + 3);
|
|
14044
|
-
return origin2 === from;
|
|
14045
|
-
case "function":
|
|
14046
|
-
return origin2(request) === true;
|
|
14047
|
-
case "object":
|
|
14048
|
-
if (origin2 instanceof RegExp)
|
|
14049
|
-
return origin2.test(from);
|
|
14050
|
-
}
|
|
14051
|
-
return false;
|
|
14052
|
-
};
|
|
14053
|
-
const handleOrigin = (set, request) => {
|
|
14054
|
-
if (origin === true) {
|
|
14055
|
-
set.headers.vary = "*";
|
|
14056
|
-
set.headers["access-control-allow-origin"] = request.headers.get("Origin") || "*";
|
|
14057
|
-
return;
|
|
14058
|
-
}
|
|
14059
|
-
if (anyOrigin) {
|
|
14060
|
-
set.headers.vary = "*";
|
|
14061
|
-
set.headers["access-control-allow-origin"] = "*";
|
|
14062
|
-
return;
|
|
14063
|
-
}
|
|
14064
|
-
if (!origins?.length)
|
|
14065
|
-
return;
|
|
14066
|
-
if (origins.length) {
|
|
14067
|
-
const from = request.headers.get("Origin") ?? "";
|
|
14068
|
-
for (let i = 0;i < origins.length; i++) {
|
|
14069
|
-
const value = processOrigin(origins[i], request, from);
|
|
14070
|
-
if (value === true) {
|
|
14071
|
-
set.headers.vary = origin ? "Origin" : "*";
|
|
14072
|
-
set.headers["access-control-allow-origin"] = from || "*";
|
|
14073
|
-
return;
|
|
14074
|
-
}
|
|
14075
|
-
}
|
|
14076
|
-
}
|
|
14077
|
-
set.headers.vary = "Origin";
|
|
14078
|
-
};
|
|
14079
|
-
const handleMethod = (set, method) => {
|
|
14080
|
-
if (!method)
|
|
14081
|
-
return;
|
|
14082
|
-
if (methods === true)
|
|
14083
|
-
return set.headers["access-control-allow-methods"] = method ?? "*";
|
|
14084
|
-
if (methods === false || !methods?.length)
|
|
14085
|
-
return;
|
|
14086
|
-
if (methods === "*")
|
|
14087
|
-
return set.headers["access-control-allow-methods"] = "*";
|
|
14088
|
-
if (!Array.isArray(methods))
|
|
14089
|
-
return set.headers["access-control-allow-methods"] = methods;
|
|
14090
|
-
set.headers["access-control-allow-methods"] = methods.join(", ");
|
|
14091
|
-
};
|
|
14092
|
-
const defaultHeaders = {};
|
|
14093
|
-
if (typeof exposeHeaders === "string")
|
|
14094
|
-
defaultHeaders["access-control-expose-headers"] = exposeHeaders;
|
|
14095
|
-
if (typeof allowedHeaders === "string")
|
|
14096
|
-
defaultHeaders["access-control-allow-headers"] = allowedHeaders;
|
|
14097
|
-
if (credentials === true)
|
|
14098
|
-
defaultHeaders["access-control-allow-credentials"] = "true";
|
|
14099
|
-
app.headers(defaultHeaders);
|
|
14100
|
-
function handleOption({ set, request, headers }) {
|
|
14101
|
-
handleOrigin(set, request);
|
|
14102
|
-
handleMethod(set, request.headers.get("access-control-request-method"));
|
|
14103
|
-
if (allowedHeaders === true || exposeHeaders === true) {
|
|
14104
|
-
if (allowedHeaders === true)
|
|
14105
|
-
set.headers["access-control-allow-headers"] = headers["access-control-request-headers"];
|
|
14106
|
-
if (exposeHeaders === true)
|
|
14107
|
-
set.headers["access-control-expose-headers"] = Object.keys(headers).join(",");
|
|
14108
|
-
}
|
|
14109
|
-
if (maxAge)
|
|
14110
|
-
set.headers["access-control-max-age"] = maxAge.toString();
|
|
14111
|
-
return new Response(null, {
|
|
14112
|
-
status: 204
|
|
14113
|
-
});
|
|
14114
|
-
}
|
|
14115
|
-
if (preflight)
|
|
14116
|
-
app.options("/", handleOption).options("/*", handleOption);
|
|
14117
|
-
return app.onRequest(function processCors({ set, request }) {
|
|
14118
|
-
handleOrigin(set, request);
|
|
14119
|
-
if (preflight && request.method === "OPTIONS") {
|
|
14120
|
-
return handleOption({
|
|
14121
|
-
set,
|
|
14122
|
-
request,
|
|
14123
|
-
headers: isBun2 ? request.headers.toJSON() : Object.fromEntries(request.headers.entries())
|
|
14124
|
-
});
|
|
14125
|
-
}
|
|
14126
|
-
handleMethod(set, request.method);
|
|
14127
|
-
if (allowedHeaders === true || exposeHeaders === true) {
|
|
14128
|
-
const headers = processHeaders(request.headers);
|
|
14129
|
-
if (allowedHeaders === true)
|
|
14130
|
-
set.headers["access-control-allow-headers"] = headers;
|
|
14131
|
-
if (exposeHeaders === true)
|
|
14132
|
-
set.headers["access-control-expose-headers"] = headers;
|
|
14133
|
-
}
|
|
14134
|
-
});
|
|
14135
|
-
};
|
|
14136
|
-
|
|
14137
|
-
// src/db/database.ts
|
|
14138
|
-
import { Database } from "bun:sqlite";
|
|
14139
|
-
import { join } from "path";
|
|
14140
|
-
import os from "os";
|
|
14141
|
-
import { existsSync, mkdirSync } from "fs";
|
|
14142
|
-
var VIBECONTROLS_DIR = join(os.homedir(), ".vibecontrols");
|
|
14143
|
-
var DEFAULT_DB_PATH = join(VIBECONTROLS_DIR, "agent.db");
|
|
14144
|
-
|
|
14145
|
-
class AgentDatabase {
|
|
14146
|
-
db;
|
|
14147
|
-
constructor(dbPath = DEFAULT_DB_PATH) {
|
|
14148
|
-
const dir = join(dbPath, "..");
|
|
14149
|
-
if (!existsSync(dir)) {
|
|
14150
|
-
mkdirSync(dir, { recursive: true });
|
|
14151
|
-
}
|
|
14152
|
-
this.db = new Database(dbPath);
|
|
14153
|
-
this.db.exec("PRAGMA journal_mode = WAL");
|
|
14154
|
-
this.db.exec("PRAGMA busy_timeout = 5000");
|
|
14155
|
-
this.db.exec("PRAGMA synchronous = NORMAL");
|
|
14156
|
-
this.db.exec("PRAGMA foreign_keys = ON");
|
|
14157
|
-
this.initializeSchema();
|
|
14158
|
-
}
|
|
14159
|
-
initializeSchema() {
|
|
14160
|
-
this.db.exec(`
|
|
14161
|
-
CREATE TABLE IF NOT EXISTS tasks (
|
|
14162
|
-
id TEXT PRIMARY KEY,
|
|
14163
|
-
type TEXT CHECK(type IN ('command', 'script', 'file_operation')) NOT NULL,
|
|
14164
|
-
status TEXT CHECK(status IN ('pending', 'running', 'completed', 'failed')) DEFAULT 'pending',
|
|
14165
|
-
payload TEXT NOT NULL,
|
|
14166
|
-
result TEXT,
|
|
14167
|
-
error TEXT,
|
|
14168
|
-
createdAt TEXT DEFAULT (datetime('now')),
|
|
14169
|
-
updatedAt TEXT DEFAULT (datetime('now'))
|
|
14170
|
-
);
|
|
14171
|
-
|
|
14172
|
-
CREATE TABLE IF NOT EXISTS agent_config (
|
|
14173
|
-
id TEXT PRIMARY KEY,
|
|
14174
|
-
key TEXT UNIQUE NOT NULL,
|
|
14175
|
-
value TEXT NOT NULL,
|
|
14176
|
-
updatedAt TEXT DEFAULT (datetime('now'))
|
|
14177
|
-
);
|
|
14178
|
-
|
|
14179
|
-
CREATE TABLE IF NOT EXISTS git_repositories (
|
|
14180
|
-
id TEXT PRIMARY KEY,
|
|
14181
|
-
path TEXT UNIQUE NOT NULL,
|
|
14182
|
-
name TEXT NOT NULL,
|
|
14183
|
-
parentPath TEXT,
|
|
14184
|
-
isSubmodule INTEGER DEFAULT 0,
|
|
14185
|
-
projectType TEXT,
|
|
14186
|
-
vitePort INTEGER,
|
|
14187
|
-
lastScanned TEXT DEFAULT (datetime('now')),
|
|
14188
|
-
createdAt TEXT DEFAULT (datetime('now'))
|
|
14189
|
-
);
|
|
14190
|
-
|
|
14191
|
-
CREATE TABLE IF NOT EXISTS bookmarked_commands (
|
|
14192
|
-
id TEXT PRIMARY KEY,
|
|
14193
|
-
projectId TEXT,
|
|
14194
|
-
command TEXT NOT NULL,
|
|
14195
|
-
description TEXT,
|
|
14196
|
-
category TEXT,
|
|
14197
|
-
createdAt TEXT DEFAULT (datetime('now'))
|
|
14198
|
-
);
|
|
14199
|
-
|
|
14200
|
-
CREATE TABLE IF NOT EXISTS notifications (
|
|
14201
|
-
id TEXT PRIMARY KEY,
|
|
14202
|
-
sessionName TEXT,
|
|
14203
|
-
projectId TEXT,
|
|
14204
|
-
type TEXT CHECK(type IN ('info', 'success', 'warning', 'error')) DEFAULT 'info',
|
|
14205
|
-
title TEXT NOT NULL,
|
|
14206
|
-
message TEXT NOT NULL,
|
|
14207
|
-
status TEXT CHECK(status IN ('unread', 'read')) DEFAULT 'unread',
|
|
14208
|
-
createdAt TEXT DEFAULT (datetime('now'))
|
|
14209
|
-
);
|
|
14210
|
-
|
|
14211
|
-
CREATE TABLE IF NOT EXISTS plugin_state (
|
|
14212
|
-
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
14213
|
-
pluginName TEXT NOT NULL,
|
|
14214
|
-
key TEXT NOT NULL,
|
|
14215
|
-
value TEXT NOT NULL,
|
|
14216
|
-
updatedAt TEXT DEFAULT (datetime('now')),
|
|
14217
|
-
UNIQUE(pluginName, key)
|
|
14218
|
-
);
|
|
14219
|
-
|
|
14220
|
-
-- Indexes
|
|
14221
|
-
CREATE INDEX IF NOT EXISTS idx_tasks_status ON tasks(status);
|
|
14222
|
-
CREATE INDEX IF NOT EXISTS idx_tasks_type ON tasks(type);
|
|
14223
|
-
CREATE INDEX IF NOT EXISTS idx_git_repositories_path ON git_repositories(path);
|
|
14224
|
-
CREATE INDEX IF NOT EXISTS idx_git_repositories_parentPath ON git_repositories(parentPath);
|
|
14225
|
-
CREATE INDEX IF NOT EXISTS idx_bookmarked_commands_projectId ON bookmarked_commands(projectId);
|
|
14226
|
-
CREATE INDEX IF NOT EXISTS idx_notifications_status ON notifications(status);
|
|
14227
|
-
CREATE INDEX IF NOT EXISTS idx_notifications_projectId ON notifications(projectId);
|
|
14228
|
-
CREATE INDEX IF NOT EXISTS idx_plugin_state_plugin ON plugin_state(pluginName);
|
|
14229
|
-
`);
|
|
14230
|
-
}
|
|
14231
|
-
createTask(task) {
|
|
14232
|
-
const stmt = this.db.prepare(`
|
|
14233
|
-
INSERT INTO tasks (id, type, status, payload, result, error)
|
|
14234
|
-
VALUES ($id, $type, $status, $payload, $result, $error)
|
|
14235
|
-
`);
|
|
14236
|
-
stmt.run({
|
|
14237
|
-
$id: task.id,
|
|
14238
|
-
$type: task.type,
|
|
14239
|
-
$status: task.status,
|
|
14240
|
-
$payload: task.payload,
|
|
14241
|
-
$result: task.result ?? null,
|
|
14242
|
-
$error: task.error ?? null
|
|
14243
|
-
});
|
|
14244
|
-
return this.getTask(task.id);
|
|
14245
|
-
}
|
|
14246
|
-
getTask(id) {
|
|
14247
|
-
return this.db.prepare("SELECT * FROM tasks WHERE id = ?").get(id);
|
|
14248
|
-
}
|
|
14249
|
-
getAllTasks() {
|
|
14250
|
-
return this.db.prepare("SELECT * FROM tasks ORDER BY createdAt DESC").all();
|
|
14251
|
-
}
|
|
14252
|
-
getPendingTasks() {
|
|
14253
|
-
return this.db.prepare("SELECT * FROM tasks WHERE status = 'pending' ORDER BY createdAt").all();
|
|
14254
|
-
}
|
|
14255
|
-
updateTask(id, updates) {
|
|
14256
|
-
const fields = Object.keys(updates).filter((k2) => k2 !== "id" && k2 !== "createdAt").map((k2) => `${k2} = $${k2}`).join(", ");
|
|
14257
|
-
if (!fields)
|
|
14258
|
-
return;
|
|
14259
|
-
const stmt = this.db.prepare(`UPDATE tasks SET ${fields}, updatedAt = datetime('now') WHERE id = $id`);
|
|
14260
|
-
const params = { $id: id };
|
|
14261
|
-
for (const [k2, v] of Object.entries(updates)) {
|
|
14262
|
-
params[`$${k2}`] = v ?? null;
|
|
14263
|
-
}
|
|
14264
|
-
stmt.run(params);
|
|
14265
|
-
}
|
|
14266
|
-
cancelTask(id) {
|
|
14267
|
-
const result = this.db.prepare("UPDATE tasks SET status = 'failed', error = 'Cancelled', updatedAt = datetime('now') WHERE id = ? AND status IN ('pending', 'running')").run(id);
|
|
14268
|
-
return result.changes > 0;
|
|
14269
|
-
}
|
|
14270
|
-
getConfig(key) {
|
|
14271
|
-
const row = this.db.prepare("SELECT value FROM agent_config WHERE key = ?").get(key);
|
|
14272
|
-
if (row?.value === "__DELETED__")
|
|
14273
|
-
return;
|
|
14274
|
-
return row?.value;
|
|
14275
|
-
}
|
|
14276
|
-
setConfig(key, value) {
|
|
14277
|
-
this.db.prepare(`
|
|
14278
|
-
INSERT INTO agent_config (id, key, value)
|
|
14279
|
-
VALUES ($id, $key, $value)
|
|
14280
|
-
ON CONFLICT(key) DO UPDATE SET value = $value, updatedAt = datetime('now')
|
|
14281
|
-
`).run({ $id: key, $key: key, $value: value });
|
|
14282
|
-
}
|
|
14283
|
-
deleteConfig(key) {
|
|
14284
|
-
const result = this.db.prepare("DELETE FROM agent_config WHERE key = ?").run(key);
|
|
14285
|
-
return result.changes > 0;
|
|
14286
|
-
}
|
|
14287
|
-
getAllConfig() {
|
|
14288
|
-
const rows = this.db.prepare("SELECT key, value FROM agent_config").all();
|
|
14289
|
-
return rows.reduce((acc, row) => {
|
|
14290
|
-
if (row.value !== "__DELETED__") {
|
|
14291
|
-
acc[row.key] = row.value;
|
|
14292
|
-
}
|
|
14293
|
-
return acc;
|
|
14294
|
-
}, {});
|
|
14295
|
-
}
|
|
14296
|
-
bulkSetConfig(entries) {
|
|
14297
|
-
const stmt = this.db.prepare(`
|
|
14298
|
-
INSERT INTO agent_config (id, key, value)
|
|
14299
|
-
VALUES ($id, $key, $value)
|
|
14300
|
-
ON CONFLICT(key) DO UPDATE SET value = $value, updatedAt = datetime('now')
|
|
14301
|
-
`);
|
|
14302
|
-
const transaction = this.db.transaction((entries2) => {
|
|
14303
|
-
for (const [key, value] of entries2) {
|
|
14304
|
-
stmt.run({ $id: key, $key: key, $value: value });
|
|
14305
|
-
}
|
|
14306
|
-
});
|
|
14307
|
-
transaction(Object.entries(entries));
|
|
14308
|
-
}
|
|
14309
|
-
getConfigStatus() {
|
|
14310
|
-
const count = this.db.prepare("SELECT COUNT(*) as count FROM agent_config").get();
|
|
14311
|
-
const latest = this.db.prepare("SELECT MAX(updatedAt) as latest FROM agent_config").get();
|
|
14312
|
-
return { totalKeys: count.count, lastUpdated: latest.latest };
|
|
14313
|
-
}
|
|
14314
|
-
createGitRepository(repo) {
|
|
14315
|
-
this.db.prepare(`
|
|
14316
|
-
INSERT INTO git_repositories (id, path, name, parentPath, isSubmodule, projectType, vitePort)
|
|
14317
|
-
VALUES ($id, $path, $name, $parentPath, $isSubmodule, $projectType, $vitePort)
|
|
14318
|
-
`).run({
|
|
14319
|
-
$id: repo.id,
|
|
14320
|
-
$path: repo.path,
|
|
14321
|
-
$name: repo.name,
|
|
14322
|
-
$parentPath: repo.parentPath ?? null,
|
|
14323
|
-
$isSubmodule: repo.isSubmodule ? 1 : 0,
|
|
14324
|
-
$projectType: repo.projectType ?? null,
|
|
14325
|
-
$vitePort: repo.vitePort ?? null
|
|
14326
|
-
});
|
|
14327
|
-
return this.getGitRepository(repo.id);
|
|
14328
|
-
}
|
|
14329
|
-
getGitRepository(id) {
|
|
14330
|
-
const row = this.db.prepare("SELECT * FROM git_repositories WHERE id = ?").get(id);
|
|
14331
|
-
if (row)
|
|
14332
|
-
row.isSubmodule = Boolean(row.isSubmodule);
|
|
14333
|
-
return row ?? undefined;
|
|
14334
|
-
}
|
|
14335
|
-
getGitRepositoryByPath(path) {
|
|
14336
|
-
const row = this.db.prepare("SELECT * FROM git_repositories WHERE path = ?").get(path);
|
|
14337
|
-
if (row)
|
|
14338
|
-
row.isSubmodule = Boolean(row.isSubmodule);
|
|
14339
|
-
return row ?? undefined;
|
|
14340
|
-
}
|
|
14341
|
-
getAllGitRepositories() {
|
|
14342
|
-
const rows = this.db.prepare("SELECT * FROM git_repositories ORDER BY path").all();
|
|
14343
|
-
return rows.map((r) => ({ ...r, isSubmodule: Boolean(r.isSubmodule) }));
|
|
14344
|
-
}
|
|
14345
|
-
updateGitRepository(id, updates) {
|
|
14346
|
-
const fields = Object.keys(updates).filter((k2) => k2 !== "id" && k2 !== "createdAt").map((k2) => `${k2} = $${k2}`).join(", ");
|
|
14347
|
-
if (!fields)
|
|
14348
|
-
return;
|
|
14349
|
-
const params = { $id: id };
|
|
14350
|
-
for (const [k2, v] of Object.entries(updates)) {
|
|
14351
|
-
if (k2 === "isSubmodule")
|
|
14352
|
-
params[`$${k2}`] = v ? 1 : 0;
|
|
14353
|
-
else
|
|
14354
|
-
params[`$${k2}`] = v ?? null;
|
|
14355
|
-
}
|
|
14356
|
-
this.db.prepare(`UPDATE git_repositories SET ${fields}, lastScanned = datetime('now') WHERE id = $id`).run(params);
|
|
14357
|
-
}
|
|
14358
|
-
deleteGitRepository(id) {
|
|
14359
|
-
const result = this.db.prepare("DELETE FROM git_repositories WHERE id = ?").run(id);
|
|
14360
|
-
return result.changes > 0;
|
|
14361
|
-
}
|
|
14362
|
-
fixGitHierarchy() {
|
|
14363
|
-
const repos = this.getAllGitRepositories();
|
|
14364
|
-
let fixed = 0;
|
|
14365
|
-
for (const repo of repos) {
|
|
14366
|
-
if (repo.isSubmodule)
|
|
14367
|
-
continue;
|
|
14368
|
-
const parent = repos.find((r) => r.id !== repo.id && repo.path.startsWith(r.path + "/") && !r.isSubmodule);
|
|
14369
|
-
if (parent && repo.parentPath !== parent.path) {
|
|
14370
|
-
this.updateGitRepository(repo.id, { parentPath: parent.path });
|
|
14371
|
-
fixed++;
|
|
14372
|
-
}
|
|
14373
|
-
}
|
|
14374
|
-
return { fixed };
|
|
14375
|
-
}
|
|
14376
|
-
createBookmarkedCommand(cmd) {
|
|
14377
|
-
this.db.prepare(`
|
|
14378
|
-
INSERT INTO bookmarked_commands (id, projectId, command, description, category)
|
|
14379
|
-
VALUES ($id, $projectId, $command, $description, $category)
|
|
14380
|
-
`).run({
|
|
14381
|
-
$id: cmd.id,
|
|
14382
|
-
$projectId: cmd.projectId ?? null,
|
|
14383
|
-
$command: cmd.command,
|
|
14384
|
-
$description: cmd.description ?? null,
|
|
14385
|
-
$category: cmd.category ?? null
|
|
14386
|
-
});
|
|
14387
|
-
return this.getBookmarkedCommand(cmd.id);
|
|
14388
|
-
}
|
|
14389
|
-
getBookmarkedCommand(id) {
|
|
14390
|
-
return this.db.prepare("SELECT * FROM bookmarked_commands WHERE id = ?").get(id);
|
|
14391
|
-
}
|
|
14392
|
-
getAllBookmarkedCommands() {
|
|
14393
|
-
return this.db.prepare("SELECT * FROM bookmarked_commands ORDER BY createdAt DESC").all();
|
|
14394
|
-
}
|
|
14395
|
-
getBookmarkedCommandsByProject(projectId) {
|
|
14396
|
-
if (projectId === null) {
|
|
14397
|
-
return this.db.prepare("SELECT * FROM bookmarked_commands WHERE projectId IS NULL ORDER BY createdAt DESC").all();
|
|
14398
|
-
}
|
|
14399
|
-
return this.db.prepare("SELECT * FROM bookmarked_commands WHERE projectId = ? ORDER BY createdAt DESC").all(projectId);
|
|
14400
|
-
}
|
|
14401
|
-
getBookmarkedCommandsByCategory(category) {
|
|
14402
|
-
return this.db.prepare("SELECT * FROM bookmarked_commands WHERE category = ? ORDER BY createdAt DESC").all(category);
|
|
14403
|
-
}
|
|
14404
|
-
updateBookmarkedCommand(id, updates) {
|
|
14405
|
-
const fields = Object.keys(updates).filter((k2) => k2 !== "id" && k2 !== "createdAt").map((k2) => `${k2} = $${k2}`).join(", ");
|
|
14406
|
-
if (!fields)
|
|
14407
|
-
return;
|
|
14408
|
-
const params = { $id: id };
|
|
14409
|
-
for (const [k2, v] of Object.entries(updates)) {
|
|
14410
|
-
params[`$${k2}`] = v ?? null;
|
|
14411
|
-
}
|
|
14412
|
-
this.db.prepare(`UPDATE bookmarked_commands SET ${fields} WHERE id = $id`).run(params);
|
|
14413
|
-
}
|
|
14414
|
-
deleteBookmarkedCommand(id) {
|
|
14415
|
-
const result = this.db.prepare("DELETE FROM bookmarked_commands WHERE id = ?").run(id);
|
|
14416
|
-
return result.changes > 0;
|
|
14417
|
-
}
|
|
14418
|
-
executeBookmarkedCommand(id) {
|
|
14419
|
-
return this.getBookmarkedCommand(id);
|
|
14420
|
-
}
|
|
14421
|
-
createNotification(notification) {
|
|
14422
|
-
this.db.prepare(`
|
|
14423
|
-
INSERT INTO notifications (id, sessionName, projectId, type, title, message, status)
|
|
14424
|
-
VALUES ($id, $sessionName, $projectId, $type, $title, $message, $status)
|
|
14425
|
-
`).run({
|
|
14426
|
-
$id: notification.id,
|
|
14427
|
-
$sessionName: notification.sessionName ?? null,
|
|
14428
|
-
$projectId: notification.projectId ?? null,
|
|
14429
|
-
$type: notification.type,
|
|
14430
|
-
$title: notification.title,
|
|
14431
|
-
$message: notification.message,
|
|
14432
|
-
$status: notification.status
|
|
14433
|
-
});
|
|
14434
|
-
return this.getNotification(notification.id);
|
|
14435
|
-
}
|
|
14436
|
-
getNotification(id) {
|
|
14437
|
-
return this.db.prepare("SELECT * FROM notifications WHERE id = ?").get(id);
|
|
14438
|
-
}
|
|
14439
|
-
getAllNotifications() {
|
|
14440
|
-
return this.db.prepare("SELECT * FROM notifications ORDER BY createdAt DESC").all();
|
|
14441
|
-
}
|
|
14442
|
-
getNotificationsByProject(projectId) {
|
|
14443
|
-
if (projectId === null) {
|
|
14444
|
-
return this.db.prepare("SELECT * FROM notifications WHERE projectId IS NULL ORDER BY createdAt DESC").all();
|
|
14445
|
-
}
|
|
14446
|
-
return this.db.prepare("SELECT * FROM notifications WHERE projectId = ? ORDER BY createdAt DESC").all(projectId);
|
|
14447
|
-
}
|
|
14448
|
-
getGlobalNotifications() {
|
|
14449
|
-
return this.db.prepare("SELECT * FROM notifications WHERE projectId IS NULL ORDER BY createdAt DESC").all();
|
|
14450
|
-
}
|
|
14451
|
-
getUnreadNotifications() {
|
|
14452
|
-
return this.db.prepare("SELECT * FROM notifications WHERE status = 'unread' ORDER BY createdAt DESC").all();
|
|
14453
|
-
}
|
|
14454
|
-
updateNotificationStatus(id, status2) {
|
|
14455
|
-
this.db.prepare("UPDATE notifications SET status = ? WHERE id = ?").run(status2, id);
|
|
14456
|
-
}
|
|
14457
|
-
markAllNotificationsRead() {
|
|
14458
|
-
const result = this.db.prepare("UPDATE notifications SET status = 'read' WHERE status = 'unread'").run();
|
|
14459
|
-
return result.changes;
|
|
14460
|
-
}
|
|
14461
|
-
deleteNotification(id) {
|
|
14462
|
-
const result = this.db.prepare("DELETE FROM notifications WHERE id = ?").run(id);
|
|
14463
|
-
return result.changes > 0;
|
|
14464
|
-
}
|
|
14465
|
-
clearOldNotifications(olderThanDays = 30) {
|
|
14466
|
-
const result = this.db.prepare("DELETE FROM notifications WHERE createdAt < datetime('now', '-' || ? || ' days')").run(olderThanDays);
|
|
14467
|
-
return result.changes;
|
|
14468
|
-
}
|
|
14469
|
-
getPluginState(pluginName, key) {
|
|
14470
|
-
const row = this.db.prepare("SELECT value FROM plugin_state WHERE pluginName = ? AND key = ?").get(pluginName, key);
|
|
14471
|
-
return row?.value;
|
|
14472
|
-
}
|
|
14473
|
-
setPluginState(pluginName, key, value) {
|
|
14474
|
-
this.db.prepare(`
|
|
14475
|
-
INSERT INTO plugin_state (pluginName, key, value)
|
|
14476
|
-
VALUES ($pluginName, $key, $value)
|
|
14477
|
-
ON CONFLICT(pluginName, key) DO UPDATE SET value = $value, updatedAt = datetime('now')
|
|
14478
|
-
`).run({ $pluginName: pluginName, $key: key, $value: value });
|
|
14479
|
-
}
|
|
14480
|
-
deletePluginState(pluginName, key) {
|
|
14481
|
-
const result = this.db.prepare("DELETE FROM plugin_state WHERE pluginName = ? AND key = ?").run(pluginName, key);
|
|
14482
|
-
return result.changes > 0;
|
|
14483
|
-
}
|
|
14484
|
-
getAllPluginState(pluginName) {
|
|
14485
|
-
return this.db.prepare("SELECT key, value, updatedAt FROM plugin_state WHERE pluginName = ? ORDER BY key").all(pluginName);
|
|
14486
|
-
}
|
|
14487
|
-
deleteAllPluginState(pluginName) {
|
|
14488
|
-
const result = this.db.prepare("DELETE FROM plugin_state WHERE pluginName = ?").run(pluginName);
|
|
14489
|
-
return result.changes;
|
|
14490
|
-
}
|
|
14491
|
-
close() {
|
|
14492
|
-
this.db.close();
|
|
14493
|
-
}
|
|
14494
|
-
}
|
|
14495
|
-
|
|
14496
|
-
// src/middleware/auth.ts
|
|
14497
|
-
import crypto2 from "crypto";
|
|
14498
|
-
import { join as join2 } from "path";
|
|
14499
|
-
import { homedir } from "os";
|
|
14500
|
-
import { existsSync as existsSync2, readFileSync } from "fs";
|
|
14501
|
-
var AUTH_EXEMPT_PREFIXES = [
|
|
14502
|
-
"/health",
|
|
14503
|
-
"/api/agent/identity",
|
|
14504
|
-
"/api/agent/api-key",
|
|
14505
|
-
"/api/agent/tunnel",
|
|
14506
|
-
"/terminal/",
|
|
14507
|
-
"/ws/"
|
|
14508
|
-
];
|
|
14509
|
-
function isExempt(path) {
|
|
14510
|
-
return AUTH_EXEMPT_PREFIXES.some((prefix) => path.startsWith(prefix));
|
|
14511
|
-
}
|
|
14512
|
-
function generateApiKey() {
|
|
14513
|
-
const bytes = crypto2.randomBytes(32);
|
|
14514
|
-
return `vcak_${bytes.toString("base64url")}`;
|
|
14515
|
-
}
|
|
14516
|
-
function loadStaticApiKey() {
|
|
14517
|
-
const configFile = join2(homedir(), ".vibecontrols", "config.json");
|
|
14518
|
-
if (existsSync2(configFile)) {
|
|
14519
|
-
try {
|
|
14520
|
-
const config = JSON.parse(readFileSync(configFile, "utf8"));
|
|
14521
|
-
return config["static-api-key"] || undefined;
|
|
14522
|
-
} catch {
|
|
14523
|
-
return;
|
|
14524
|
-
}
|
|
14525
|
-
}
|
|
14526
|
-
return;
|
|
14527
|
-
}
|
|
14528
|
-
function createAuthPlugin() {
|
|
14529
|
-
const envKey = process.env.AGENT_API_KEY;
|
|
14530
|
-
const staticKey = loadStaticApiKey();
|
|
14531
|
-
const apiKey = envKey || staticKey || generateApiKey();
|
|
14532
|
-
logger.info("auth", `Agent API key: ${apiKey.substring(0, 12)}...`);
|
|
14533
|
-
return new Elysia({ name: "plugin/auth" }).decorate("apiKey", apiKey).derive(({ headers, path }) => {
|
|
14534
|
-
return {
|
|
14535
|
-
isAuthenticated: (() => {
|
|
14536
|
-
if (isExempt(path))
|
|
14537
|
-
return true;
|
|
14538
|
-
const providedKey = headers["x-agent-api-key"];
|
|
14539
|
-
return providedKey === apiKey;
|
|
14540
|
-
})()
|
|
14541
|
-
};
|
|
14542
|
-
}).onBeforeHandle(({ isAuthenticated, path, set }) => {
|
|
14543
|
-
if (!isAuthenticated && !isExempt(path)) {
|
|
14544
|
-
set.status = 401;
|
|
14545
|
-
return { error: "Unauthorized", message: "Invalid or missing API key" };
|
|
14546
|
-
}
|
|
14547
|
-
});
|
|
14548
|
-
}
|
|
14549
|
-
|
|
14550
|
-
// src/services/gateway-client.ts
|
|
14551
|
-
class GatewayClient {
|
|
14552
|
-
config = null;
|
|
14553
|
-
token = null;
|
|
14554
|
-
configure(config) {
|
|
14555
|
-
this.config = config;
|
|
14556
|
-
this.token = null;
|
|
14557
|
-
logger.info("gateway-client", "Gateway client configured", {
|
|
14558
|
-
globalGatewayUrl: config.globalGatewayUrl,
|
|
14559
|
-
clientId: config.clientId
|
|
14560
|
-
});
|
|
14561
|
-
}
|
|
14562
|
-
isConfigured() {
|
|
14563
|
-
return this.config !== null;
|
|
14564
|
-
}
|
|
14565
|
-
getConfig() {
|
|
14566
|
-
if (!this.config)
|
|
14567
|
-
return null;
|
|
14568
|
-
const { clientSecret: _2, ...rest } = this.config;
|
|
14569
|
-
return rest;
|
|
14570
|
-
}
|
|
14571
|
-
async globalQuery(query, variables) {
|
|
14572
|
-
return this.executeQuery("global", query, variables);
|
|
14573
|
-
}
|
|
14574
|
-
async workspaceQuery(query, variables) {
|
|
14575
|
-
return this.executeQuery("workspaces", query, variables);
|
|
14576
|
-
}
|
|
14577
|
-
async executeQuery(supergraph, query, variables) {
|
|
14578
|
-
if (!this.config) {
|
|
14579
|
-
throw new Error("Gateway client not configured. Call configure() first.");
|
|
14580
|
-
}
|
|
14581
|
-
const token = await this.ensureToken();
|
|
14582
|
-
const baseUrl = supergraph === "workspaces" ? this.config.workspaceGatewayUrl ?? this.config.globalGatewayUrl : this.config.globalGatewayUrl;
|
|
14583
|
-
const url = `${baseUrl.replace(/\/$/, "")}/${supergraph}/graphql`;
|
|
14584
|
-
logger.debug("gateway-client", `GraphQL ${supergraph} request`, {
|
|
14585
|
-
url,
|
|
14586
|
-
queryLength: query.length
|
|
14587
|
-
});
|
|
14588
|
-
const response = await fetch(url, {
|
|
14589
|
-
method: "POST",
|
|
14590
|
-
headers: {
|
|
14591
|
-
"Content-Type": "application/json",
|
|
14592
|
-
Authorization: `Bearer ${token}`
|
|
14593
|
-
},
|
|
14594
|
-
body: JSON.stringify({ query, variables })
|
|
14595
|
-
});
|
|
14596
|
-
const result = await response.json();
|
|
14597
|
-
if (result.errors?.length) {
|
|
14598
|
-
logger.warn("gateway-client", "GraphQL errors", {
|
|
14599
|
-
supergraph,
|
|
14600
|
-
errors: result.errors.map((e) => e.message)
|
|
14601
|
-
});
|
|
14602
|
-
}
|
|
14603
|
-
return result;
|
|
14604
|
-
}
|
|
14605
|
-
async ensureToken() {
|
|
14606
|
-
if (this.token && this.token.expiresAt > Date.now() + 60000) {
|
|
14607
|
-
return this.token.accessToken;
|
|
14608
|
-
}
|
|
14609
|
-
return this.authenticate();
|
|
14610
|
-
}
|
|
14611
|
-
async authenticate() {
|
|
14612
|
-
if (!this.config) {
|
|
14613
|
-
throw new Error("Gateway client not configured.");
|
|
14614
|
-
}
|
|
14615
|
-
logger.info("gateway-client", "Authenticating with gateway", {
|
|
14616
|
-
clientId: this.config.clientId
|
|
14617
|
-
});
|
|
14618
|
-
const url = `${this.config.globalGatewayUrl.replace(/\/$/, "")}/global/graphql`;
|
|
14619
|
-
const response = await fetch(url, {
|
|
14620
|
-
method: "POST",
|
|
14621
|
-
headers: { "Content-Type": "application/json" },
|
|
14622
|
-
body: JSON.stringify({
|
|
14623
|
-
query: `
|
|
14624
|
-
mutation AuthenticateApp($input: AuthenticateAppInput!) {
|
|
14625
|
-
authenticateApp(input: $input) {
|
|
14626
|
-
accessToken
|
|
14627
|
-
tokenType
|
|
14628
|
-
expiresIn
|
|
14629
|
-
scopes
|
|
14630
|
-
}
|
|
14631
|
-
}
|
|
14632
|
-
`,
|
|
14633
|
-
variables: {
|
|
14634
|
-
input: {
|
|
14635
|
-
clientId: this.config.clientId,
|
|
14636
|
-
clientSecret: this.config.clientSecret,
|
|
14637
|
-
scopes: this.config.scopes ?? []
|
|
14638
|
-
}
|
|
14639
|
-
}
|
|
14640
|
-
})
|
|
14641
|
-
});
|
|
14642
|
-
const result = await response.json();
|
|
14643
|
-
if (result.errors?.length || !result.data?.authenticateApp) {
|
|
14644
|
-
const msg = result.errors?.[0]?.message ?? "Authentication failed";
|
|
14645
|
-
logger.error("gateway-client", `Authentication failed: ${msg}`);
|
|
14646
|
-
throw new Error(`Gateway authentication failed: ${msg}`);
|
|
14647
|
-
}
|
|
14648
|
-
const auth = result.data.authenticateApp;
|
|
14649
|
-
this.token = {
|
|
14650
|
-
accessToken: auth.accessToken,
|
|
14651
|
-
expiresAt: Date.now() + auth.expiresIn * 1000,
|
|
14652
|
-
scopes: auth.scopes
|
|
14653
|
-
};
|
|
14654
|
-
logger.info("gateway-client", "Authenticated successfully", {
|
|
14655
|
-
expiresIn: auth.expiresIn,
|
|
14656
|
-
scopes: auth.scopes
|
|
14657
|
-
});
|
|
14658
|
-
return this.token.accessToken;
|
|
14659
|
-
}
|
|
14660
|
-
}
|
|
14661
|
-
var gatewayClient = new GatewayClient;
|
|
14662
|
-
|
|
14663
|
-
// src/routes/health.routes.ts
|
|
14664
|
-
function createHealthRoutes(_serviceRegistry) {
|
|
14665
|
-
return new Elysia({ prefix: "/health" }).get("/", () => ({
|
|
14666
|
-
status: "ok",
|
|
14667
|
-
timestamp: new Date().toISOString(),
|
|
14668
|
-
uptime: process.uptime()
|
|
14669
|
-
})).get("/live", () => ({ status: "ok" })).get("/ready", () => ({ status: "ok" }));
|
|
14670
|
-
}
|
|
14671
|
-
|
|
14672
|
-
// src/routes/agent.routes.ts
|
|
14673
|
-
import os2 from "os";
|
|
14674
|
-
import crypto3 from "crypto";
|
|
14675
|
-
import { readFileSync as readFileSync2 } from "fs";
|
|
14676
|
-
function getMachineId() {
|
|
14677
|
-
const hostname = os2.hostname();
|
|
14678
|
-
const platform = os2.platform();
|
|
14679
|
-
const arch = os2.arch();
|
|
14680
|
-
return crypto3.createHash("sha256").update(`${hostname}-${platform}-${arch}`).digest("hex").substring(0, 16);
|
|
14681
|
-
}
|
|
14682
|
-
function getAgentVersion() {
|
|
14683
|
-
try {
|
|
14684
|
-
const pkgPath = new URL("../../package.json", import.meta.url);
|
|
14685
|
-
const pkg = JSON.parse(readFileSync2(new URL(pkgPath), "utf8"));
|
|
14686
|
-
return pkg.version || "0.0.0";
|
|
14687
|
-
} catch {
|
|
14688
|
-
return "0.0.0";
|
|
14689
|
-
}
|
|
14690
|
-
}
|
|
14691
|
-
function createAgentRoutes(db, serviceRegistry) {
|
|
14692
|
-
const version = getAgentVersion();
|
|
14693
|
-
const machineId = getMachineId();
|
|
14694
|
-
return new Elysia({ prefix: "/api/agent" }).get("/identity", () => ({
|
|
14695
|
-
machineId,
|
|
14696
|
-
hostname: os2.hostname(),
|
|
14697
|
-
platform: os2.platform(),
|
|
14698
|
-
arch: os2.arch(),
|
|
14699
|
-
version,
|
|
14700
|
-
uptime: process.uptime()
|
|
14701
|
-
})).get("/api-key", ({ store }) => ({
|
|
14702
|
-
apiKey: store.apiKey ?? "unknown"
|
|
14703
|
-
})).get("/tunnel", async () => {
|
|
14704
|
-
const tunnelProvider = serviceRegistry.getProvider("tunnel");
|
|
14705
|
-
if (!tunnelProvider) {
|
|
14706
|
-
return { tunnelUrl: null, message: "No tunnel provider registered" };
|
|
14707
|
-
}
|
|
14708
|
-
const url = await tunnelProvider.getActiveTunnelUrl();
|
|
14709
|
-
return { tunnelUrl: url };
|
|
14710
|
-
}).post("/tunnel/start", async ({ set }) => {
|
|
14711
|
-
const tunnelProvider = serviceRegistry.getProvider("tunnel");
|
|
14712
|
-
if (!tunnelProvider) {
|
|
14713
|
-
set.status = 400;
|
|
14714
|
-
return { error: "No tunnel provider registered" };
|
|
14715
|
-
}
|
|
14716
|
-
const port = parseInt(process.env.PORT || "3005");
|
|
14717
|
-
const tunnel = await tunnelProvider.start({ port, name: "agent" });
|
|
14718
|
-
return { success: true, tunnel };
|
|
14719
|
-
}).post("/tunnel/stop", async ({ set }) => {
|
|
14720
|
-
const tunnelProvider = serviceRegistry.getProvider("tunnel");
|
|
14721
|
-
if (!tunnelProvider) {
|
|
14722
|
-
set.status = 400;
|
|
14723
|
-
return { error: "No tunnel provider registered" };
|
|
14724
|
-
}
|
|
14725
|
-
const tunnels = await tunnelProvider.list();
|
|
14726
|
-
const agentTunnel = tunnels.find((t2) => t2.metadata?.isAgent);
|
|
14727
|
-
if (agentTunnel) {
|
|
14728
|
-
await tunnelProvider.stop(agentTunnel.id);
|
|
14729
|
-
}
|
|
14730
|
-
return { success: true };
|
|
14731
|
-
}).get("/setup/check", () => {
|
|
14732
|
-
const deps = checkDependencies();
|
|
14733
|
-
const allInstalled = deps.filter((d) => d.required).every((d) => d.installed);
|
|
14734
|
-
return { ready: allInstalled, dependencies: deps };
|
|
14735
|
-
}).post("/setup/install", async () => {
|
|
14736
|
-
const result = await installDependencies();
|
|
14737
|
-
return result;
|
|
14738
|
-
}).post("/gateway-auth", async ({ body, set }) => {
|
|
14739
|
-
try {
|
|
14740
|
-
gatewayClient.configure({
|
|
14741
|
-
globalGatewayUrl: body.tenantApiUrl || body.globalGatewayUrl || "",
|
|
14742
|
-
workspaceGatewayUrl: body.workspacesApiUrl || body.workspaceGatewayUrl,
|
|
14743
|
-
clientId: body.appClientId || body.clientId || "",
|
|
14744
|
-
clientSecret: body.appClientSecret || body.clientSecret || ""
|
|
14745
|
-
});
|
|
14746
|
-
if (body.appClientId || body.clientId) {
|
|
14747
|
-
db.setConfig("gateway-auth:clientId", body.appClientId || body.clientId || "");
|
|
14748
|
-
}
|
|
14749
|
-
if (body.appClientSecret || body.clientSecret) {
|
|
14750
|
-
db.setConfig("gateway-auth:clientSecret", body.appClientSecret || body.clientSecret || "");
|
|
14751
|
-
}
|
|
14752
|
-
if (body.workspacesApiUrl || body.workspaceGatewayUrl) {
|
|
14753
|
-
db.setConfig("gateway-auth:workspaceGatewayUrl", body.workspacesApiUrl || body.workspaceGatewayUrl || "");
|
|
14754
|
-
}
|
|
14755
|
-
if (body.tenantApiUrl || body.globalGatewayUrl) {
|
|
14756
|
-
db.setConfig("gateway-auth:globalGatewayUrl", body.tenantApiUrl || body.globalGatewayUrl || "");
|
|
14757
|
-
}
|
|
14758
|
-
if (body.workspaceId) {
|
|
14759
|
-
db.setConfig("gateway-auth:workspaceId", body.workspaceId);
|
|
14760
|
-
}
|
|
14761
|
-
return { success: true, message: "Gateway auth configured" };
|
|
14762
|
-
} catch (err) {
|
|
14763
|
-
set.status = 500;
|
|
14764
|
-
return {
|
|
14765
|
-
error: "Failed to configure gateway auth",
|
|
14766
|
-
message: String(err)
|
|
14767
|
-
};
|
|
14768
|
-
}
|
|
14769
|
-
}, {
|
|
14770
|
-
body: t.Object({
|
|
14771
|
-
workspacesApiUrl: t.Optional(t.String()),
|
|
14772
|
-
tenantApiUrl: t.Optional(t.String()),
|
|
14773
|
-
globalGatewayUrl: t.Optional(t.String()),
|
|
14774
|
-
workspaceGatewayUrl: t.Optional(t.String()),
|
|
14775
|
-
appClientId: t.Optional(t.String()),
|
|
14776
|
-
appClientSecret: t.Optional(t.String()),
|
|
14777
|
-
clientId: t.Optional(t.String()),
|
|
14778
|
-
clientSecret: t.Optional(t.String()),
|
|
14779
|
-
workspaceId: t.Optional(t.String())
|
|
14780
|
-
})
|
|
14781
|
-
}).get("/gateway-auth", () => {
|
|
14782
|
-
const config = gatewayClient.getConfig();
|
|
14783
|
-
return { configured: gatewayClient.isConfigured(), config };
|
|
14784
|
-
}).get("/version", () => ({
|
|
14785
|
-
version,
|
|
14786
|
-
runtime: "bun",
|
|
14787
|
-
runtimeVersion: typeof Bun !== "undefined" ? Bun.version : "unknown"
|
|
14788
|
-
})).get("/system", () => ({
|
|
14789
|
-
hostname: os2.hostname(),
|
|
14790
|
-
platform: os2.platform(),
|
|
14791
|
-
arch: os2.arch(),
|
|
14792
|
-
release: os2.release(),
|
|
14793
|
-
cpus: os2.cpus().length,
|
|
14794
|
-
totalMemory: os2.totalmem(),
|
|
14795
|
-
freeMemory: os2.freemem(),
|
|
14796
|
-
uptime: os2.uptime(),
|
|
14797
|
-
bunVersion: typeof Bun !== "undefined" ? Bun.version : undefined,
|
|
14798
|
-
agentVersion: version,
|
|
14799
|
-
homeDir: os2.homedir(),
|
|
14800
|
-
cwd: process.cwd(),
|
|
14801
|
-
environment: "development"
|
|
14802
|
-
})).get("/status", () => ({
|
|
14803
|
-
status: "online",
|
|
14804
|
-
timestamp: new Date().toISOString(),
|
|
14805
|
-
version,
|
|
14806
|
-
providers: serviceRegistry.listProviders(),
|
|
14807
|
-
services: serviceRegistry.listServices()
|
|
14808
|
-
})).get("/url", async () => {
|
|
14809
|
-
const tunnelProvider = serviceRegistry.getProvider("tunnel");
|
|
14810
|
-
const tunnelUrl = tunnelProvider ? await tunnelProvider.getActiveTunnelUrl() : null;
|
|
14811
|
-
const localUrl = `http://localhost:${process.env.PORT || 3005}`;
|
|
14812
|
-
return {
|
|
14813
|
-
url: tunnelUrl || localUrl,
|
|
14814
|
-
tunnelUrl,
|
|
14815
|
-
localUrl,
|
|
14816
|
-
source: tunnelUrl ? "tunnel" : "local"
|
|
14817
|
-
};
|
|
14818
|
-
});
|
|
14819
|
-
}
|
|
14820
|
-
|
|
14821
|
-
// src/routes/session.routes.ts
|
|
14822
|
-
function getSessionProvider(registry) {
|
|
14823
|
-
const provider = registry.getProvider("session");
|
|
14824
|
-
if (!provider) {
|
|
14825
|
-
throw new Error("No session provider registered");
|
|
14826
|
-
}
|
|
14827
|
-
return provider;
|
|
14828
|
-
}
|
|
14829
|
-
function createSessionRoutes(serviceRegistry) {
|
|
14830
|
-
return new Elysia({ prefix: "/api/sessions" }).get("/", async ({ set }) => {
|
|
14831
|
-
try {
|
|
14832
|
-
const provider = getSessionProvider(serviceRegistry);
|
|
14833
|
-
const sessions = await provider.list();
|
|
14834
|
-
return { sessions };
|
|
14835
|
-
} catch (err) {
|
|
14836
|
-
set.status = 500;
|
|
14837
|
-
return { error: "Failed to list sessions", details: String(err) };
|
|
14838
|
-
}
|
|
14839
|
-
}).get("/system", async ({ set }) => {
|
|
14840
|
-
try {
|
|
14841
|
-
const provider = getSessionProvider(serviceRegistry);
|
|
14842
|
-
const sessions = await provider.listSystem();
|
|
14843
|
-
return { sessions };
|
|
14844
|
-
} catch (err) {
|
|
14845
|
-
set.status = 500;
|
|
14846
|
-
return {
|
|
14847
|
-
error: "Failed to get system sessions",
|
|
14848
|
-
details: String(err)
|
|
14849
|
-
};
|
|
14850
|
-
}
|
|
14851
|
-
}).get("/system/terminals", async ({ set }) => {
|
|
14852
|
-
try {
|
|
14853
|
-
const provider = getSessionProvider(serviceRegistry);
|
|
14854
|
-
const processes = await provider.listSystemTerminals();
|
|
14855
|
-
return { processes };
|
|
14856
|
-
} catch (err) {
|
|
14857
|
-
set.status = 500;
|
|
14858
|
-
return {
|
|
14859
|
-
error: "Failed to get system terminal processes",
|
|
14860
|
-
details: String(err)
|
|
14861
|
-
};
|
|
14862
|
-
}
|
|
14863
|
-
}).post("/system/kill", async ({ body, set }) => {
|
|
14864
|
-
try {
|
|
14865
|
-
const provider = getSessionProvider(serviceRegistry);
|
|
14866
|
-
const results = [];
|
|
14867
|
-
for (const sessionId of body.sessionIds) {
|
|
14868
|
-
try {
|
|
14869
|
-
await provider.killSystem(sessionId);
|
|
14870
|
-
results.push({ target: sessionId, success: true });
|
|
14871
|
-
} catch (err) {
|
|
14872
|
-
results.push({
|
|
14873
|
-
target: sessionId,
|
|
14874
|
-
success: false,
|
|
14875
|
-
error: String(err)
|
|
14876
|
-
});
|
|
14877
|
-
}
|
|
14878
|
-
}
|
|
14879
|
-
return { results };
|
|
14880
|
-
} catch (err) {
|
|
14881
|
-
set.status = 500;
|
|
14882
|
-
return {
|
|
14883
|
-
error: "Failed to kill system sessions",
|
|
14884
|
-
details: String(err)
|
|
14885
|
-
};
|
|
14886
|
-
}
|
|
14887
|
-
}, {
|
|
14888
|
-
body: t.Object({
|
|
14889
|
-
sessionIds: t.Array(t.String()),
|
|
14890
|
-
force: t.Optional(t.Boolean())
|
|
14891
|
-
})
|
|
14892
|
-
}).post("/system/terminals/kill", async ({ body, set }) => {
|
|
14893
|
-
try {
|
|
14894
|
-
const provider = getSessionProvider(serviceRegistry);
|
|
14895
|
-
const results = [];
|
|
14896
|
-
for (const pid of body.pids) {
|
|
14897
|
-
try {
|
|
14898
|
-
await provider.killSystemTerminal(pid);
|
|
14899
|
-
results.push({ target: String(pid), success: true });
|
|
14900
|
-
} catch (err) {
|
|
14901
|
-
results.push({
|
|
14902
|
-
target: String(pid),
|
|
14903
|
-
success: false,
|
|
14904
|
-
error: String(err)
|
|
14905
|
-
});
|
|
14906
|
-
}
|
|
14907
|
-
}
|
|
14908
|
-
return { results };
|
|
14909
|
-
} catch (err) {
|
|
14910
|
-
set.status = 500;
|
|
14911
|
-
return {
|
|
14912
|
-
error: "Failed to kill system terminal processes",
|
|
14913
|
-
details: String(err)
|
|
14914
|
-
};
|
|
14915
|
-
}
|
|
14916
|
-
}, {
|
|
14917
|
-
body: t.Object({
|
|
14918
|
-
pids: t.Array(t.Number()),
|
|
14919
|
-
force: t.Optional(t.Boolean())
|
|
14920
|
-
})
|
|
14921
|
-
}).post("/health-check", async ({ body, set }) => {
|
|
14922
|
-
try {
|
|
14923
|
-
const provider = getSessionProvider(serviceRegistry);
|
|
14924
|
-
const results = [];
|
|
14925
|
-
for (const sessionId of body.sessionIds) {
|
|
14926
|
-
try {
|
|
14927
|
-
const session = await provider.get(sessionId);
|
|
14928
|
-
if (!session) {
|
|
14929
|
-
results.push({ sessionId, status: "unknown" });
|
|
14930
|
-
} else {
|
|
14931
|
-
results.push({
|
|
14932
|
-
sessionId,
|
|
14933
|
-
status: session.status === "active" ? "running" : "dead"
|
|
14934
|
-
});
|
|
14935
|
-
}
|
|
14936
|
-
} catch {
|
|
14937
|
-
results.push({ sessionId, status: "unknown" });
|
|
14938
|
-
}
|
|
14939
|
-
}
|
|
14940
|
-
return { results };
|
|
14941
|
-
} catch (err) {
|
|
14942
|
-
set.status = 500;
|
|
14943
|
-
return {
|
|
14944
|
-
error: "Failed to health check sessions",
|
|
14945
|
-
details: String(err)
|
|
14946
|
-
};
|
|
14947
|
-
}
|
|
14948
|
-
}, {
|
|
14949
|
-
body: t.Object({
|
|
14950
|
-
sessionIds: t.Array(t.String())
|
|
14951
|
-
})
|
|
14952
|
-
}).post("/create", async ({ body, set }) => {
|
|
14953
|
-
try {
|
|
14954
|
-
const provider = getSessionProvider(serviceRegistry);
|
|
14955
|
-
if (body.sessionId) {
|
|
14956
|
-
const existing = await provider.get(body.sessionId);
|
|
14957
|
-
if (existing && existing.status === "active") {
|
|
14958
|
-
return { session: existing, reused: true };
|
|
14959
|
-
}
|
|
14960
|
-
}
|
|
14961
|
-
const session = await provider.create({
|
|
14962
|
-
name: body.sessionName || `vibecontrols-${Date.now()}`,
|
|
14963
|
-
command: body.command,
|
|
14964
|
-
workingDirectory: body.startDirectory,
|
|
14965
|
-
projectId: body.projectId
|
|
14966
|
-
});
|
|
14967
|
-
return { session, reused: false };
|
|
14968
|
-
} catch (err) {
|
|
14969
|
-
set.status = 500;
|
|
14970
|
-
return { error: "Failed to create session", details: String(err) };
|
|
14971
|
-
}
|
|
14972
|
-
}, {
|
|
14973
|
-
body: t.Object({
|
|
14974
|
-
sessionId: t.Optional(t.String()),
|
|
14975
|
-
projectId: t.Optional(t.String()),
|
|
14976
|
-
sessionName: t.Optional(t.String()),
|
|
14977
|
-
windowName: t.Optional(t.String()),
|
|
14978
|
-
command: t.Optional(t.String()),
|
|
14979
|
-
startDirectory: t.Optional(t.String())
|
|
14980
|
-
})
|
|
14981
|
-
}).get("/:id", async ({ params, set }) => {
|
|
14982
|
-
try {
|
|
14983
|
-
const provider = getSessionProvider(serviceRegistry);
|
|
14984
|
-
const session = await provider.get(params.id);
|
|
14985
|
-
if (!session) {
|
|
14986
|
-
set.status = 404;
|
|
14987
|
-
return { error: "Session not found" };
|
|
14988
|
-
}
|
|
14989
|
-
return { session };
|
|
14990
|
-
} catch (err) {
|
|
14991
|
-
set.status = 500;
|
|
14992
|
-
return { error: "Failed to get session", details: String(err) };
|
|
14993
|
-
}
|
|
14994
|
-
}).delete("/:id", async ({ params, set }) => {
|
|
14995
|
-
try {
|
|
14996
|
-
const provider = getSessionProvider(serviceRegistry);
|
|
14997
|
-
const session = await provider.get(params.id);
|
|
14998
|
-
if (!session) {
|
|
14999
|
-
set.status = 404;
|
|
15000
|
-
return { error: "Session not found" };
|
|
15001
|
-
}
|
|
15002
|
-
await provider.kill(params.id);
|
|
15003
|
-
return { success: true, sessionName: session.name };
|
|
15004
|
-
} catch (err) {
|
|
15005
|
-
set.status = 500;
|
|
15006
|
-
return { error: "Failed to kill session", details: String(err) };
|
|
15007
|
-
}
|
|
15008
|
-
}).post("/:id/command", async ({ params, body, set }) => {
|
|
15009
|
-
try {
|
|
15010
|
-
const provider = getSessionProvider(serviceRegistry);
|
|
15011
|
-
const session = await provider.get(params.id);
|
|
15012
|
-
if (!session) {
|
|
15013
|
-
set.status = 404;
|
|
15014
|
-
return { error: "Session not found" };
|
|
15015
|
-
}
|
|
15016
|
-
await provider.sendCommand(params.id, body.command);
|
|
15017
|
-
return { success: true };
|
|
15018
|
-
} catch (err) {
|
|
15019
|
-
set.status = 500;
|
|
15020
|
-
return { error: "Failed to execute command", details: String(err) };
|
|
15021
|
-
}
|
|
15022
|
-
}, {
|
|
15023
|
-
body: t.Object({
|
|
15024
|
-
command: t.String()
|
|
15025
|
-
})
|
|
15026
|
-
}).post("/:id/keys", async ({ params, body, set }) => {
|
|
15027
|
-
try {
|
|
15028
|
-
const provider = getSessionProvider(serviceRegistry);
|
|
15029
|
-
const session = await provider.get(params.id);
|
|
15030
|
-
if (!session) {
|
|
15031
|
-
set.status = 404;
|
|
15032
|
-
return { error: "Session not found" };
|
|
15033
|
-
}
|
|
15034
|
-
await provider.sendKeys(params.id, body.keys);
|
|
15035
|
-
return { success: true };
|
|
15036
|
-
} catch (err) {
|
|
15037
|
-
set.status = 500;
|
|
15038
|
-
return { error: "Failed to send keys", details: String(err) };
|
|
15039
|
-
}
|
|
15040
|
-
}, {
|
|
15041
|
-
body: t.Object({
|
|
15042
|
-
keys: t.String()
|
|
15043
|
-
})
|
|
15044
|
-
}).post("/:id/interrupt", async ({ params, set }) => {
|
|
15045
|
-
try {
|
|
15046
|
-
const provider = getSessionProvider(serviceRegistry);
|
|
15047
|
-
const session = await provider.get(params.id);
|
|
15048
|
-
if (!session) {
|
|
15049
|
-
set.status = 404;
|
|
15050
|
-
return { error: "Session not found" };
|
|
15051
|
-
}
|
|
15052
|
-
await provider.interrupt(params.id);
|
|
15053
|
-
return { success: true };
|
|
15054
|
-
} catch (err) {
|
|
15055
|
-
set.status = 500;
|
|
15056
|
-
return { error: "Failed to send interrupt", details: String(err) };
|
|
15057
|
-
}
|
|
15058
|
-
}).get("/:id/capture", async ({ params, set }) => {
|
|
15059
|
-
try {
|
|
15060
|
-
const provider = getSessionProvider(serviceRegistry);
|
|
15061
|
-
const session = await provider.get(params.id);
|
|
15062
|
-
if (!session) {
|
|
15063
|
-
set.status = 404;
|
|
15064
|
-
return { error: "Session not found" };
|
|
15065
|
-
}
|
|
15066
|
-
const output = await provider.capture(params.id);
|
|
15067
|
-
return { output };
|
|
15068
|
-
} catch (err) {
|
|
15069
|
-
set.status = 500;
|
|
15070
|
-
return { error: "Failed to capture output", details: String(err) };
|
|
15071
|
-
}
|
|
15072
|
-
}).put("/:id/rename", async ({ params, body, set }) => {
|
|
15073
|
-
try {
|
|
15074
|
-
const provider = getSessionProvider(serviceRegistry);
|
|
15075
|
-
const session = await provider.get(params.id);
|
|
15076
|
-
if (!session) {
|
|
15077
|
-
set.status = 404;
|
|
15078
|
-
return { error: "Session not found" };
|
|
15079
|
-
}
|
|
15080
|
-
await provider.rename(params.id, body.newName);
|
|
15081
|
-
return { success: true };
|
|
15082
|
-
} catch (err) {
|
|
15083
|
-
set.status = 500;
|
|
15084
|
-
return { error: "Failed to rename session", details: String(err) };
|
|
15085
|
-
}
|
|
15086
|
-
}, {
|
|
15087
|
-
body: t.Object({
|
|
15088
|
-
newName: t.String()
|
|
15089
|
-
})
|
|
15090
|
-
}).post("/:id/toggle-mouse", async ({ params, set }) => {
|
|
15091
|
-
try {
|
|
15092
|
-
const provider = getSessionProvider(serviceRegistry);
|
|
15093
|
-
const session = await provider.get(params.id);
|
|
15094
|
-
if (!session) {
|
|
15095
|
-
set.status = 404;
|
|
15096
|
-
return { error: "Session not found" };
|
|
15097
|
-
}
|
|
15098
|
-
await provider.toggleMouse(params.id);
|
|
15099
|
-
return { success: true };
|
|
15100
|
-
} catch (err) {
|
|
15101
|
-
set.status = 500;
|
|
15102
|
-
return { error: "Failed to toggle mouse mode", details: String(err) };
|
|
15103
|
-
}
|
|
15104
|
-
}).get("/:id/termination-status", async ({ params, set }) => {
|
|
15105
|
-
try {
|
|
15106
|
-
const provider = getSessionProvider(serviceRegistry);
|
|
15107
|
-
const session = await provider.get(params.id);
|
|
15108
|
-
if (!session) {
|
|
15109
|
-
set.status = 404;
|
|
15110
|
-
return { error: "Session not found" };
|
|
15111
|
-
}
|
|
15112
|
-
const termStatus = await provider.getTerminationStatus(params.id);
|
|
15113
|
-
return {
|
|
15114
|
-
sessionId: params.id,
|
|
15115
|
-
sessionName: session.name,
|
|
15116
|
-
databaseStatus: session.status,
|
|
15117
|
-
...termStatus,
|
|
15118
|
-
isFullyTerminated: termStatus.exited && session.status === "terminated"
|
|
15119
|
-
};
|
|
15120
|
-
} catch (err) {
|
|
15121
|
-
set.status = 500;
|
|
15122
|
-
return {
|
|
15123
|
-
error: "Failed to verify termination",
|
|
15124
|
-
details: String(err)
|
|
15125
|
-
};
|
|
15126
|
-
}
|
|
15127
|
-
}).get("/:id/terminal", async ({ params, set }) => {
|
|
15128
|
-
try {
|
|
15129
|
-
const provider = getSessionProvider(serviceRegistry);
|
|
15130
|
-
const session = await provider.get(params.id);
|
|
15131
|
-
if (!session) {
|
|
15132
|
-
set.status = 404;
|
|
15133
|
-
return { error: "Session not found" };
|
|
15134
|
-
}
|
|
15135
|
-
const terminal = await provider.getTerminalInfo(params.id);
|
|
15136
|
-
if (!terminal) {
|
|
15137
|
-
set.status = 404;
|
|
15138
|
-
return { error: "Terminal not running for this session" };
|
|
15139
|
-
}
|
|
15140
|
-
const tunnelProvider = serviceRegistry.getProvider("tunnel");
|
|
15141
|
-
const tunnelUrl = tunnelProvider ? await tunnelProvider.getActiveTunnelUrl() : null;
|
|
15142
|
-
const terminalProxyUrl = tunnelUrl ? `${tunnelUrl}/terminal/${params.id}/` : null;
|
|
15143
|
-
return {
|
|
15144
|
-
port: terminal.port,
|
|
15145
|
-
url: `http://localhost:${terminal.port}`,
|
|
15146
|
-
pid: terminal.pid,
|
|
15147
|
-
terminalUrl: terminalProxyUrl
|
|
15148
|
-
};
|
|
15149
|
-
} catch (err) {
|
|
15150
|
-
set.status = 500;
|
|
15151
|
-
return { error: "Failed to get terminal info", details: String(err) };
|
|
15152
|
-
}
|
|
15153
|
-
}).post("/:id/terminal", async ({ params, set }) => {
|
|
15154
|
-
try {
|
|
15155
|
-
const provider = getSessionProvider(serviceRegistry);
|
|
15156
|
-
const session = await provider.get(params.id);
|
|
15157
|
-
if (!session) {
|
|
15158
|
-
set.status = 404;
|
|
15159
|
-
return { error: "Session not found" };
|
|
15160
|
-
}
|
|
15161
|
-
const existingTerminal = await provider.getTerminalInfo(params.id);
|
|
15162
|
-
if (existingTerminal) {
|
|
15163
|
-
const tunnelProvider2 = serviceRegistry.getProvider("tunnel");
|
|
15164
|
-
const tunnelUrl2 = tunnelProvider2 ? await tunnelProvider2.getActiveTunnelUrl() : null;
|
|
15165
|
-
const terminalProxyUrl2 = tunnelUrl2 ? `${tunnelUrl2}/terminal/${params.id}/` : null;
|
|
15166
|
-
return {
|
|
15167
|
-
port: existingTerminal.port,
|
|
15168
|
-
pid: existingTerminal.pid,
|
|
15169
|
-
url: `http://localhost:${existingTerminal.port}`,
|
|
15170
|
-
terminalUrl: terminalProxyUrl2
|
|
15171
|
-
};
|
|
15172
|
-
}
|
|
15173
|
-
const terminal = await provider.startTerminal(params.id);
|
|
15174
|
-
const tunnelProvider = serviceRegistry.getProvider("tunnel");
|
|
15175
|
-
const tunnelUrl = tunnelProvider ? await tunnelProvider.getActiveTunnelUrl() : null;
|
|
15176
|
-
const terminalProxyUrl = tunnelUrl ? `${tunnelUrl}/terminal/${params.id}/` : null;
|
|
15177
|
-
return {
|
|
15178
|
-
port: terminal.port,
|
|
15179
|
-
pid: terminal.pid,
|
|
15180
|
-
url: `http://localhost:${terminal.port}`,
|
|
15181
|
-
terminalUrl: terminalProxyUrl
|
|
15182
|
-
};
|
|
15183
|
-
} catch (err) {
|
|
15184
|
-
set.status = 500;
|
|
15185
|
-
return { error: "Failed to start terminal", details: String(err) };
|
|
15186
|
-
}
|
|
15187
|
-
}).post("/:id/terminal/stop", async ({ params, set }) => {
|
|
15188
|
-
try {
|
|
15189
|
-
const provider = getSessionProvider(serviceRegistry);
|
|
15190
|
-
const session = await provider.get(params.id);
|
|
15191
|
-
if (!session) {
|
|
15192
|
-
set.status = 404;
|
|
15193
|
-
return { error: "Session not found" };
|
|
15194
|
-
}
|
|
15195
|
-
await provider.stopTerminal(params.id);
|
|
15196
|
-
return { success: true };
|
|
15197
|
-
} catch (err) {
|
|
15198
|
-
set.status = 500;
|
|
15199
|
-
return { error: "Failed to stop terminal", details: String(err) };
|
|
15200
|
-
}
|
|
15201
|
-
});
|
|
15202
|
-
}
|
|
15203
|
-
|
|
15204
|
-
// src/routes/tunnel.routes.ts
|
|
15205
|
-
function getTunnelProvider(registry) {
|
|
15206
|
-
const provider = registry.getProvider("tunnel");
|
|
15207
|
-
if (!provider) {
|
|
15208
|
-
throw new Error("No tunnel provider registered");
|
|
15209
|
-
}
|
|
15210
|
-
return provider;
|
|
15211
|
-
}
|
|
15212
|
-
function createTunnelRoutes(serviceRegistry) {
|
|
15213
|
-
return new Elysia({ prefix: "/api/tunnels" }).get("/", async ({ set }) => {
|
|
15214
|
-
try {
|
|
15215
|
-
const provider = getTunnelProvider(serviceRegistry);
|
|
15216
|
-
const tunnels = await provider.list();
|
|
15217
|
-
return { tunnels };
|
|
15218
|
-
} catch (err) {
|
|
15219
|
-
set.status = 500;
|
|
15220
|
-
return { error: "Failed to list tunnels", details: String(err) };
|
|
15221
|
-
}
|
|
15222
|
-
}).get("/status", async ({ set }) => {
|
|
15223
|
-
try {
|
|
15224
|
-
const provider = getTunnelProvider(serviceRegistry);
|
|
15225
|
-
const tunnels = await provider.list();
|
|
15226
|
-
const active = tunnels.filter((t2) => t2.status === "active");
|
|
15227
|
-
const inactive = tunnels.filter((t2) => t2.status === "stopped");
|
|
15228
|
-
const errored = tunnels.filter((t2) => t2.status === "error");
|
|
15229
|
-
return {
|
|
15230
|
-
total: tunnels.length,
|
|
15231
|
-
active: active.length,
|
|
15232
|
-
inactive: inactive.length,
|
|
15233
|
-
errored: errored.length,
|
|
15234
|
-
tunnels: active.map((t2) => ({
|
|
15235
|
-
id: t2.id,
|
|
15236
|
-
port: t2.port,
|
|
15237
|
-
url: t2.url,
|
|
15238
|
-
pid: t2.pid
|
|
15239
|
-
}))
|
|
15240
|
-
};
|
|
15241
|
-
} catch (err) {
|
|
15242
|
-
set.status = 500;
|
|
15243
|
-
return { error: "Failed to get tunnel status", details: String(err) };
|
|
15244
|
-
}
|
|
15245
|
-
}).post("/start", async ({ body, set }) => {
|
|
15246
|
-
if (body.localPort < 1 || body.localPort > 65535) {
|
|
15247
|
-
set.status = 400;
|
|
15248
|
-
return { error: "Invalid localPort: must be between 1 and 65535" };
|
|
15249
|
-
}
|
|
15250
|
-
try {
|
|
15251
|
-
const provider = getTunnelProvider(serviceRegistry);
|
|
15252
|
-
const existing = await provider.list();
|
|
15253
|
-
const portTunnel = existing.find((t2) => t2.port === body.localPort && t2.status === "active");
|
|
15254
|
-
if (portTunnel) {
|
|
15255
|
-
set.status = 409;
|
|
15256
|
-
return {
|
|
15257
|
-
error: `An active tunnel already exists for port ${body.localPort}`,
|
|
15258
|
-
tunnel: portTunnel
|
|
15259
|
-
};
|
|
15260
|
-
}
|
|
15261
|
-
const tunnel = await provider.start({
|
|
15262
|
-
port: body.localPort,
|
|
15263
|
-
name: body.subdomain,
|
|
15264
|
-
metadata: body.sessionId ? { sessionId: body.sessionId } : undefined
|
|
15265
|
-
});
|
|
15266
|
-
return {
|
|
15267
|
-
id: tunnel.id,
|
|
15268
|
-
localPort: tunnel.port,
|
|
15269
|
-
publicUrl: tunnel.url,
|
|
15270
|
-
pid: tunnel.pid,
|
|
15271
|
-
status: tunnel.status,
|
|
15272
|
-
sessionId: body.sessionId || null
|
|
15273
|
-
};
|
|
15274
|
-
} catch (err) {
|
|
15275
|
-
set.status = 500;
|
|
15276
|
-
return { error: "Failed to start tunnel", details: String(err) };
|
|
15277
|
-
}
|
|
15278
|
-
}, {
|
|
15279
|
-
body: t.Object({
|
|
15280
|
-
localPort: t.Number(),
|
|
15281
|
-
subdomain: t.Optional(t.String()),
|
|
15282
|
-
sessionId: t.Optional(t.String())
|
|
15283
|
-
})
|
|
15284
|
-
}).get("/:id", async ({ params, set }) => {
|
|
15285
|
-
try {
|
|
15286
|
-
const provider = getTunnelProvider(serviceRegistry);
|
|
15287
|
-
const tunnel = await provider.getStatus(params.id);
|
|
15288
|
-
if (!tunnel) {
|
|
15289
|
-
set.status = 404;
|
|
15290
|
-
return { error: "Tunnel not found" };
|
|
15291
|
-
}
|
|
15292
|
-
return { tunnel };
|
|
15293
|
-
} catch (err) {
|
|
15294
|
-
set.status = 500;
|
|
15295
|
-
return { error: "Failed to get tunnel", details: String(err) };
|
|
15296
|
-
}
|
|
15297
|
-
}).post("/:id/stop", async ({ params, set }) => {
|
|
15298
|
-
try {
|
|
15299
|
-
const provider = getTunnelProvider(serviceRegistry);
|
|
15300
|
-
const tunnel = await provider.getStatus(params.id);
|
|
15301
|
-
if (!tunnel) {
|
|
15302
|
-
set.status = 404;
|
|
15303
|
-
return { error: "Tunnel not found" };
|
|
15304
|
-
}
|
|
15305
|
-
if (tunnel.status !== "active") {
|
|
15306
|
-
set.status = 400;
|
|
15307
|
-
return { error: "Tunnel is not active" };
|
|
15308
|
-
}
|
|
15309
|
-
await provider.stop(params.id);
|
|
15310
|
-
return { success: true };
|
|
15311
|
-
} catch (err) {
|
|
15312
|
-
set.status = 500;
|
|
15313
|
-
return { error: "Failed to stop tunnel", details: String(err) };
|
|
15314
|
-
}
|
|
15315
|
-
}).delete("/:id", async ({ params, set }) => {
|
|
15316
|
-
try {
|
|
15317
|
-
const provider = getTunnelProvider(serviceRegistry);
|
|
15318
|
-
const tunnel = await provider.getStatus(params.id);
|
|
15319
|
-
if (!tunnel) {
|
|
15320
|
-
set.status = 404;
|
|
15321
|
-
return { error: "Tunnel not found" };
|
|
15322
|
-
}
|
|
15323
|
-
if (tunnel.status === "active") {
|
|
15324
|
-
await provider.stop(params.id);
|
|
15325
|
-
}
|
|
15326
|
-
await provider.delete(params.id);
|
|
15327
|
-
return { success: true };
|
|
15328
|
-
} catch (err) {
|
|
15329
|
-
set.status = 500;
|
|
15330
|
-
return { error: "Failed to delete tunnel", details: String(err) };
|
|
15331
|
-
}
|
|
15332
|
-
});
|
|
15333
|
-
}
|
|
15334
|
-
|
|
15335
|
-
// src/routes/task.routes.ts
|
|
15336
|
-
import crypto4 from "crypto";
|
|
15337
|
-
import { promises as fs } from "fs";
|
|
15338
|
-
import path from "path";
|
|
15339
|
-
import os3 from "os";
|
|
15340
|
-
async function executeCommand(payload) {
|
|
15341
|
-
const proc = Bun.spawn(["sh", "-c", payload.command], {
|
|
15342
|
-
cwd: payload.cwd,
|
|
15343
|
-
env: { ...process.env, ...payload.env },
|
|
15344
|
-
stdout: "pipe",
|
|
15345
|
-
stderr: "pipe"
|
|
15346
|
-
});
|
|
15347
|
-
const [stdout, stderr] = await Promise.all([
|
|
15348
|
-
new Response(proc.stdout).text(),
|
|
15349
|
-
new Response(proc.stderr).text()
|
|
15350
|
-
]);
|
|
15351
|
-
await proc.exited;
|
|
15352
|
-
return { stdout, stderr };
|
|
15353
|
-
}
|
|
15354
|
-
async function executeScript(payload) {
|
|
15355
|
-
const interpreter = payload.interpreter || "bash";
|
|
15356
|
-
const tmpDir = os3.tmpdir();
|
|
15357
|
-
const scriptFile = path.join(tmpDir, `vibecontrols-script-${Date.now()}.sh`);
|
|
15358
|
-
await fs.writeFile(scriptFile, payload.script, { mode: 493 });
|
|
15359
|
-
try {
|
|
15360
|
-
const proc = Bun.spawn([interpreter, scriptFile], {
|
|
15361
|
-
cwd: payload.cwd,
|
|
15362
|
-
stdout: "pipe",
|
|
15363
|
-
stderr: "pipe"
|
|
15364
|
-
});
|
|
15365
|
-
const [stdout, stderr] = await Promise.all([
|
|
15366
|
-
new Response(proc.stdout).text(),
|
|
15367
|
-
new Response(proc.stderr).text()
|
|
15368
|
-
]);
|
|
15369
|
-
await proc.exited;
|
|
15370
|
-
return { stdout, stderr };
|
|
15371
|
-
} finally {
|
|
15372
|
-
await fs.unlink(scriptFile).catch(() => {});
|
|
15373
|
-
}
|
|
15374
|
-
}
|
|
15375
|
-
async function executeFileOperation(payload) {
|
|
15376
|
-
switch (payload.operation) {
|
|
15377
|
-
case "read": {
|
|
15378
|
-
const data = await fs.readFile(payload.path, "utf-8");
|
|
15379
|
-
return { content: data };
|
|
15380
|
-
}
|
|
15381
|
-
case "write": {
|
|
15382
|
-
if (!payload.content)
|
|
15383
|
-
throw new Error("Content is required for write operation");
|
|
15384
|
-
await fs.writeFile(payload.path, payload.content);
|
|
15385
|
-
return { success: true };
|
|
15386
|
-
}
|
|
15387
|
-
case "delete": {
|
|
15388
|
-
await fs.unlink(payload.path);
|
|
15389
|
-
return { success: true };
|
|
15390
|
-
}
|
|
15391
|
-
case "exists": {
|
|
15392
|
-
try {
|
|
15393
|
-
await fs.access(payload.path);
|
|
15394
|
-
return { exists: true };
|
|
15395
|
-
} catch {
|
|
15396
|
-
return { exists: false };
|
|
15397
|
-
}
|
|
15398
|
-
}
|
|
15399
|
-
case "list": {
|
|
15400
|
-
const files = await fs.readdir(payload.path);
|
|
15401
|
-
return { files };
|
|
15402
|
-
}
|
|
15403
|
-
default:
|
|
15404
|
-
throw new Error(`Unknown file operation: ${payload.operation}`);
|
|
15405
|
-
}
|
|
15406
|
-
}
|
|
15407
|
-
async function processTask(db, task) {
|
|
15408
|
-
try {
|
|
15409
|
-
db.updateTask(task.id, { status: "running" });
|
|
15410
|
-
const payload = JSON.parse(task.payload);
|
|
15411
|
-
let result;
|
|
15412
|
-
switch (task.type) {
|
|
15413
|
-
case "command":
|
|
15414
|
-
result = await executeCommand(payload);
|
|
15415
|
-
break;
|
|
15416
|
-
case "script":
|
|
15417
|
-
result = await executeScript(payload);
|
|
15418
|
-
break;
|
|
15419
|
-
case "file_operation":
|
|
15420
|
-
result = await executeFileOperation(payload);
|
|
15421
|
-
break;
|
|
15422
|
-
default:
|
|
15423
|
-
throw new Error(`Unknown task type: ${task.type}`);
|
|
15424
|
-
}
|
|
15425
|
-
db.updateTask(task.id, {
|
|
15426
|
-
status: "completed",
|
|
15427
|
-
result: JSON.stringify(result)
|
|
15428
|
-
});
|
|
15429
|
-
} catch (err) {
|
|
15430
|
-
db.updateTask(task.id, {
|
|
15431
|
-
status: "failed",
|
|
15432
|
-
error: err instanceof Error ? err.message : String(err)
|
|
15433
|
-
});
|
|
15434
|
-
}
|
|
15435
|
-
}
|
|
15436
|
-
function createTaskRoutes(db) {
|
|
15437
|
-
return new Elysia({ prefix: "/api/tasks" }).get("/", ({ query }) => {
|
|
15438
|
-
const status2 = query.status;
|
|
15439
|
-
if (status2 === "pending") {
|
|
15440
|
-
return { tasks: db.getPendingTasks() };
|
|
15441
|
-
}
|
|
15442
|
-
return { tasks: db.getAllTasks() };
|
|
15443
|
-
}).get("/:id", ({ params, set }) => {
|
|
15444
|
-
const task = db.getTask(params.id);
|
|
15445
|
-
if (!task) {
|
|
15446
|
-
set.status = 404;
|
|
15447
|
-
return { error: "Task not found" };
|
|
15448
|
-
}
|
|
15449
|
-
return { task };
|
|
15450
|
-
}).post("/", async ({ body, set }) => {
|
|
15451
|
-
try {
|
|
15452
|
-
const task = db.createTask({
|
|
15453
|
-
id: crypto4.randomUUID(),
|
|
15454
|
-
type: body.type,
|
|
15455
|
-
status: "pending",
|
|
15456
|
-
payload: JSON.stringify(body.payload)
|
|
15457
|
-
});
|
|
15458
|
-
processTask(db, task);
|
|
15459
|
-
return { task };
|
|
15460
|
-
} catch (err) {
|
|
15461
|
-
set.status = 500;
|
|
15462
|
-
return { error: "Failed to create task", details: String(err) };
|
|
15463
|
-
}
|
|
15464
|
-
}, {
|
|
15465
|
-
body: t.Object({
|
|
15466
|
-
type: t.Union([
|
|
15467
|
-
t.Literal("command"),
|
|
15468
|
-
t.Literal("script"),
|
|
15469
|
-
t.Literal("file_operation")
|
|
15470
|
-
]),
|
|
15471
|
-
payload: t.Object({
|
|
15472
|
-
command: t.Optional(t.String()),
|
|
15473
|
-
cwd: t.Optional(t.String()),
|
|
15474
|
-
env: t.Optional(t.Record(t.String(), t.String())),
|
|
15475
|
-
script: t.Optional(t.String()),
|
|
15476
|
-
interpreter: t.Optional(t.String()),
|
|
15477
|
-
operation: t.Optional(t.Union([
|
|
15478
|
-
t.Literal("read"),
|
|
15479
|
-
t.Literal("write"),
|
|
15480
|
-
t.Literal("delete"),
|
|
15481
|
-
t.Literal("exists"),
|
|
15482
|
-
t.Literal("list")
|
|
15483
|
-
])),
|
|
15484
|
-
path: t.Optional(t.String()),
|
|
15485
|
-
content: t.Optional(t.String())
|
|
15486
|
-
})
|
|
15487
|
-
})
|
|
15488
|
-
}).post("/:id/cancel", ({ params, set }) => {
|
|
15489
|
-
const task = db.getTask(params.id);
|
|
15490
|
-
if (!task) {
|
|
15491
|
-
set.status = 404;
|
|
15492
|
-
return { error: "Task not found" };
|
|
15493
|
-
}
|
|
15494
|
-
if (task.status !== "pending") {
|
|
15495
|
-
set.status = 400;
|
|
15496
|
-
return { error: "Only pending tasks can be cancelled" };
|
|
15497
|
-
}
|
|
15498
|
-
db.updateTask(params.id, {
|
|
15499
|
-
status: "failed",
|
|
15500
|
-
error: "Task cancelled by user"
|
|
15501
|
-
});
|
|
15502
|
-
return { success: true };
|
|
15503
|
-
});
|
|
15504
|
-
}
|
|
15505
|
-
|
|
15506
|
-
// src/routes/config.routes.ts
|
|
15507
|
-
function createConfigRoutes(db) {
|
|
15508
|
-
return new Elysia({ prefix: "/api/config" }).get("/", () => {
|
|
15509
|
-
const config = db.getAllConfig();
|
|
15510
|
-
return { config };
|
|
15511
|
-
}).get("/status", () => {
|
|
15512
|
-
const status2 = db.getConfigStatus();
|
|
15513
|
-
return status2;
|
|
15514
|
-
}).get("/:key", ({ params, set }) => {
|
|
15515
|
-
const value = db.getConfig(params.key);
|
|
15516
|
-
if (value === undefined) {
|
|
15517
|
-
set.status = 404;
|
|
15518
|
-
return { error: "Configuration key not found" };
|
|
15519
|
-
}
|
|
15520
|
-
return { key: params.key, value };
|
|
15521
|
-
}).put("/:key", ({ params, body, set }) => {
|
|
15522
|
-
try {
|
|
15523
|
-
db.setConfig(params.key, body.value);
|
|
15524
|
-
return { success: true };
|
|
15525
|
-
} catch (err) {
|
|
15526
|
-
set.status = 500;
|
|
15527
|
-
return {
|
|
15528
|
-
error: "Failed to set configuration",
|
|
15529
|
-
details: String(err)
|
|
15530
|
-
};
|
|
15531
|
-
}
|
|
15532
|
-
}, {
|
|
15533
|
-
body: t.Object({
|
|
15534
|
-
value: t.String()
|
|
15535
|
-
})
|
|
15536
|
-
}).delete("/:key", ({ params, set }) => {
|
|
15537
|
-
const current = db.getConfig(params.key);
|
|
15538
|
-
if (current === undefined) {
|
|
15539
|
-
set.status = 404;
|
|
15540
|
-
return { error: "Configuration key not found" };
|
|
15541
|
-
}
|
|
15542
|
-
try {
|
|
15543
|
-
db.deleteConfig(params.key);
|
|
15544
|
-
return { success: true };
|
|
15545
|
-
} catch (err) {
|
|
15546
|
-
set.status = 500;
|
|
15547
|
-
return {
|
|
15548
|
-
error: "Failed to delete configuration",
|
|
15549
|
-
details: String(err)
|
|
15550
|
-
};
|
|
15551
|
-
}
|
|
15552
|
-
}).post("/bulk", ({ body, set }) => {
|
|
15553
|
-
try {
|
|
15554
|
-
db.bulkSetConfig(body.configs);
|
|
15555
|
-
return { success: true };
|
|
15556
|
-
} catch (err) {
|
|
15557
|
-
set.status = 500;
|
|
15558
|
-
return {
|
|
15559
|
-
error: "Failed to set configuration",
|
|
15560
|
-
details: String(err)
|
|
15561
|
-
};
|
|
15562
|
-
}
|
|
15563
|
-
}, {
|
|
15564
|
-
body: t.Object({
|
|
15565
|
-
configs: t.Record(t.String(), t.String())
|
|
15566
|
-
})
|
|
15567
|
-
});
|
|
15568
|
-
}
|
|
15569
|
-
|
|
15570
|
-
// src/routes/git.routes.ts
|
|
15571
|
-
import crypto5 from "crypto";
|
|
15572
|
-
import { promises as fs2 } from "fs";
|
|
15573
|
-
import path2 from "path";
|
|
15574
|
-
async function detectProjectType(directory) {
|
|
15575
|
-
try {
|
|
15576
|
-
const entries = await fs2.readdir(directory);
|
|
15577
|
-
if (entries.includes("package.json")) {
|
|
15578
|
-
try {
|
|
15579
|
-
const packageJson = JSON.parse(await fs2.readFile(path2.join(directory, "package.json"), "utf8"));
|
|
15580
|
-
const deps = {
|
|
15581
|
-
...packageJson.dependencies,
|
|
15582
|
-
...packageJson.devDependencies
|
|
15583
|
-
};
|
|
15584
|
-
if (deps.react || deps["@types/react"])
|
|
15585
|
-
return "react";
|
|
15586
|
-
if (deps.vue || deps["@vue/cli"])
|
|
15587
|
-
return "vue";
|
|
15588
|
-
if (deps.angular || deps["@angular/core"])
|
|
15589
|
-
return "angular";
|
|
15590
|
-
if (deps.next || deps["@types/next"])
|
|
15591
|
-
return "nextjs";
|
|
15592
|
-
if (deps.nuxt || deps["@nuxt/core"])
|
|
15593
|
-
return "nuxtjs";
|
|
15594
|
-
if (deps.svelte || deps["@sveltejs/kit"])
|
|
15595
|
-
return "svelte";
|
|
15596
|
-
if (deps.express || deps.fastify || deps.koa)
|
|
15597
|
-
return "nodejs-backend";
|
|
15598
|
-
if (deps.electron)
|
|
15599
|
-
return "electron";
|
|
15600
|
-
if (deps.vite)
|
|
15601
|
-
return "vite";
|
|
15602
|
-
} catch {}
|
|
15603
|
-
return "nodejs";
|
|
15604
|
-
}
|
|
15605
|
-
if (entries.includes("setup.py") || entries.includes("requirements.txt") || entries.includes("pyproject.toml") || entries.includes("Pipfile")) {
|
|
15606
|
-
if (entries.includes("manage.py"))
|
|
15607
|
-
return "django";
|
|
15608
|
-
if (entries.includes("app.py"))
|
|
15609
|
-
return "flask";
|
|
15610
|
-
return "python";
|
|
15611
|
-
}
|
|
15612
|
-
if (entries.includes("go.mod"))
|
|
15613
|
-
return "go";
|
|
15614
|
-
if (entries.includes("Cargo.toml"))
|
|
15615
|
-
return "rust";
|
|
15616
|
-
if (entries.includes("pom.xml"))
|
|
15617
|
-
return "maven";
|
|
15618
|
-
if (entries.includes("build.gradle") || entries.includes("build.gradle.kts"))
|
|
15619
|
-
return "gradle";
|
|
15620
|
-
if (entries.includes("Gemfile"))
|
|
15621
|
-
return "ruby";
|
|
15622
|
-
if (entries.includes("composer.json"))
|
|
15623
|
-
return "php";
|
|
15624
|
-
if (entries.some((e) => e.endsWith(".csproj") || e.endsWith(".sln")))
|
|
15625
|
-
return "dotnet";
|
|
15626
|
-
if (entries.includes("CMakeLists.txt") || entries.includes("Makefile"))
|
|
15627
|
-
return "cpp";
|
|
15628
|
-
if (entries.includes("Package.swift"))
|
|
15629
|
-
return "swift";
|
|
15630
|
-
if (entries.includes("pubspec.yaml"))
|
|
15631
|
-
return "flutter";
|
|
15632
|
-
if (entries.some((e) => e.endsWith(".tf")))
|
|
15633
|
-
return "terraform";
|
|
15634
|
-
if (entries.includes("Dockerfile"))
|
|
15635
|
-
return "docker";
|
|
15636
|
-
} catch {}
|
|
15637
|
-
return;
|
|
15638
|
-
}
|
|
15639
|
-
async function detectVitePort(directory) {
|
|
15640
|
-
try {
|
|
15641
|
-
const variants = ["vite.config.ts", "vite.config.js"];
|
|
15642
|
-
for (const name of variants) {
|
|
15643
|
-
try {
|
|
15644
|
-
const content = await fs2.readFile(path2.join(directory, name), "utf-8");
|
|
15645
|
-
const match = content.match(/port:\s*(\d+)/);
|
|
15646
|
-
if (match)
|
|
15647
|
-
return parseInt(match[1], 10);
|
|
15648
|
-
} catch {}
|
|
15649
|
-
}
|
|
15650
|
-
} catch {}
|
|
15651
|
-
return;
|
|
15652
|
-
}
|
|
15653
|
-
var SKIP_DIRS = new Set([
|
|
15654
|
-
".git",
|
|
15655
|
-
"node_modules",
|
|
15656
|
-
"__pycache__",
|
|
15657
|
-
".venv",
|
|
15658
|
-
"venv",
|
|
15659
|
-
"dist",
|
|
15660
|
-
"build"
|
|
15661
|
-
]);
|
|
15662
|
-
async function scanForGitRepositories(directory, includeSubmodules, parentPath) {
|
|
15663
|
-
const repositories = [];
|
|
15664
|
-
try {
|
|
15665
|
-
const entries = await fs2.readdir(directory, { withFileTypes: true });
|
|
15666
|
-
const hasGit = entries.some((e) => e.name === ".git" && e.isDirectory());
|
|
15667
|
-
if (hasGit) {
|
|
15668
|
-
repositories.push({
|
|
15669
|
-
path: directory,
|
|
15670
|
-
name: path2.basename(directory),
|
|
15671
|
-
parentPath,
|
|
15672
|
-
isSubmodule: !!parentPath,
|
|
15673
|
-
projectType: await detectProjectType(directory),
|
|
15674
|
-
vitePort: await detectVitePort(directory)
|
|
15675
|
-
});
|
|
15676
|
-
if (!includeSubmodules)
|
|
15677
|
-
return repositories;
|
|
15678
|
-
parentPath = directory;
|
|
15679
|
-
}
|
|
15680
|
-
for (const entry of entries) {
|
|
15681
|
-
if (entry.isDirectory() && !SKIP_DIRS.has(entry.name) && !entry.name.startsWith(".")) {
|
|
15682
|
-
const subPath = path2.join(directory, entry.name);
|
|
15683
|
-
const subRepos = await scanForGitRepositories(subPath, includeSubmodules, parentPath);
|
|
15684
|
-
repositories.push(...subRepos);
|
|
15685
|
-
}
|
|
15686
|
-
}
|
|
15687
|
-
} catch {}
|
|
15688
|
-
return repositories;
|
|
15689
|
-
}
|
|
15690
|
-
function createGitRoutes(db) {
|
|
15691
|
-
return new Elysia({ prefix: "/api/git" }).get("/", () => {
|
|
15692
|
-
const repositories = db.getAllGitRepositories();
|
|
15693
|
-
return { repositories };
|
|
15694
|
-
}).get("/:id", ({ params, set }) => {
|
|
15695
|
-
const repository = db.getGitRepository(params.id);
|
|
15696
|
-
if (!repository) {
|
|
15697
|
-
set.status = 404;
|
|
15698
|
-
return { error: "Repository not found" };
|
|
15699
|
-
}
|
|
15700
|
-
return { repository };
|
|
15701
|
-
}).post("/scan", async ({ body, set }) => {
|
|
15702
|
-
try {
|
|
15703
|
-
const stats = await fs2.stat(body.directory);
|
|
15704
|
-
if (!stats.isDirectory()) {
|
|
15705
|
-
set.status = 400;
|
|
15706
|
-
return { error: "Path is not a directory" };
|
|
15707
|
-
}
|
|
15708
|
-
const repositories = await scanForGitRepositories(body.directory, body.includeSubmodules ?? true);
|
|
15709
|
-
const savedRepos = [];
|
|
15710
|
-
for (const repo of repositories) {
|
|
15711
|
-
const existing = db.getGitRepositoryByPath(repo.path);
|
|
15712
|
-
if (existing) {
|
|
15713
|
-
db.updateGitRepository(existing.id, repo);
|
|
15714
|
-
savedRepos.push({ ...existing, ...repo });
|
|
15715
|
-
} else {
|
|
15716
|
-
const newRepo = db.createGitRepository({
|
|
15717
|
-
id: crypto5.randomUUID(),
|
|
15718
|
-
...repo
|
|
15719
|
-
});
|
|
15720
|
-
savedRepos.push(newRepo);
|
|
15721
|
-
}
|
|
15722
|
-
}
|
|
15723
|
-
return {
|
|
15724
|
-
repositories: savedRepos,
|
|
15725
|
-
scannedPath: body.directory,
|
|
15726
|
-
totalFound: savedRepos.length
|
|
15727
|
-
};
|
|
15728
|
-
} catch (err) {
|
|
15729
|
-
set.status = 500;
|
|
15730
|
-
return { error: "Failed to scan directory", details: String(err) };
|
|
15731
|
-
}
|
|
15732
|
-
}, {
|
|
15733
|
-
body: t.Object({
|
|
15734
|
-
directory: t.String(),
|
|
15735
|
-
includeSubmodules: t.Optional(t.Boolean())
|
|
15736
|
-
})
|
|
15737
|
-
}).put("/:id", ({ params, body, set }) => {
|
|
15738
|
-
const repository = db.getGitRepository(params.id);
|
|
15739
|
-
if (!repository) {
|
|
15740
|
-
set.status = 404;
|
|
15741
|
-
return { error: "Repository not found" };
|
|
15742
|
-
}
|
|
15743
|
-
try {
|
|
15744
|
-
db.updateGitRepository(params.id, body);
|
|
15745
|
-
return { success: true };
|
|
15746
|
-
} catch (err) {
|
|
15747
|
-
set.status = 500;
|
|
15748
|
-
return {
|
|
15749
|
-
error: "Failed to update repository",
|
|
15750
|
-
details: String(err)
|
|
15751
|
-
};
|
|
15752
|
-
}
|
|
15753
|
-
}, {
|
|
15754
|
-
body: t.Object({
|
|
15755
|
-
name: t.Optional(t.String()),
|
|
15756
|
-
projectType: t.Optional(t.String()),
|
|
15757
|
-
vitePort: t.Optional(t.Number())
|
|
15758
|
-
})
|
|
15759
|
-
}).delete("/:id", ({ params, set }) => {
|
|
15760
|
-
const repository = db.getGitRepository(params.id);
|
|
15761
|
-
if (!repository) {
|
|
15762
|
-
set.status = 404;
|
|
15763
|
-
return { error: "Repository not found" };
|
|
15764
|
-
}
|
|
15765
|
-
try {
|
|
15766
|
-
db.deleteGitRepository(params.id);
|
|
15767
|
-
return { success: true };
|
|
15768
|
-
} catch (err) {
|
|
15769
|
-
set.status = 500;
|
|
15770
|
-
return { error: "Failed to delete repository", details: String(err) };
|
|
15771
|
-
}
|
|
15772
|
-
}).post("/fix-hierarchy", ({ set }) => {
|
|
15773
|
-
try {
|
|
15774
|
-
const result = db.fixGitHierarchy();
|
|
15775
|
-
const repos = db.getAllGitRepositories();
|
|
15776
|
-
return { success: true, fixed: result.fixed, total: repos.length };
|
|
15777
|
-
} catch (err) {
|
|
15778
|
-
set.status = 500;
|
|
15779
|
-
return { error: "Failed to fix hierarchy", details: String(err) };
|
|
15780
|
-
}
|
|
15781
|
-
});
|
|
15782
|
-
}
|
|
15783
|
-
|
|
15784
|
-
// src/routes/file.routes.ts
|
|
15785
|
-
import { promises as fs3 } from "fs";
|
|
15786
|
-
import nodePath from "path";
|
|
15787
|
-
var SENSITIVE_PATTERNS = [
|
|
15788
|
-
/\.env$/,
|
|
15789
|
-
/\.pem$/,
|
|
15790
|
-
/\.key$/,
|
|
15791
|
-
/id_rsa/,
|
|
15792
|
-
/\.ssh\//,
|
|
15793
|
-
/\.git\/config$/,
|
|
15794
|
-
/\.npmrc$/
|
|
15795
|
-
];
|
|
15796
|
-
var PROTECTED_PATHS = ["/", "/etc", "/usr", "/bin", "/sbin", "/var", "/opt"];
|
|
15797
|
-
function validatePath(filePath) {
|
|
15798
|
-
const normalized = nodePath.normalize(filePath);
|
|
15799
|
-
if (normalized.includes("..")) {
|
|
15800
|
-
return {
|
|
15801
|
-
valid: false,
|
|
15802
|
-
normalized,
|
|
15803
|
-
error: "Directory traversal not allowed"
|
|
15804
|
-
};
|
|
15805
|
-
}
|
|
15806
|
-
return { valid: true, normalized };
|
|
15807
|
-
}
|
|
15808
|
-
function isSensitiveFile(filePath) {
|
|
15809
|
-
return SENSITIVE_PATTERNS.some((p) => p.test(filePath));
|
|
15810
|
-
}
|
|
15811
|
-
function isProtectedPath(filePath) {
|
|
15812
|
-
return PROTECTED_PATHS.includes(filePath) || PROTECTED_PATHS.some((p) => filePath.startsWith(p + "/"));
|
|
15813
|
-
}
|
|
15814
|
-
function createFileRoutes() {
|
|
15815
|
-
return new Elysia({ prefix: "/api/files" }).post("/read", async ({ body, set }) => {
|
|
15816
|
-
const { valid, normalized, error } = validatePath(body.path);
|
|
15817
|
-
if (!valid) {
|
|
15818
|
-
set.status = 403;
|
|
15819
|
-
return { error };
|
|
15820
|
-
}
|
|
15821
|
-
if (isSensitiveFile(normalized)) {
|
|
15822
|
-
set.status = 403;
|
|
15823
|
-
return { error: "Access to sensitive files not allowed" };
|
|
15824
|
-
}
|
|
15825
|
-
try {
|
|
15826
|
-
const content = await fs3.readFile(normalized, "utf-8");
|
|
15827
|
-
return { content, path: normalized, size: content.length };
|
|
15828
|
-
} catch (err) {
|
|
15829
|
-
const nodeErr = err;
|
|
15830
|
-
if (nodeErr.code === "ENOENT") {
|
|
15831
|
-
set.status = 404;
|
|
15832
|
-
return { error: "File not found" };
|
|
15833
|
-
}
|
|
15834
|
-
if (nodeErr.code === "EACCES") {
|
|
15835
|
-
set.status = 403;
|
|
15836
|
-
return { error: "Permission denied" };
|
|
15837
|
-
}
|
|
15838
|
-
set.status = 500;
|
|
15839
|
-
return { error: "Failed to read file", details: String(err) };
|
|
15840
|
-
}
|
|
15841
|
-
}, {
|
|
15842
|
-
body: t.Object({
|
|
15843
|
-
path: t.String(),
|
|
15844
|
-
projectId: t.Optional(t.String())
|
|
15845
|
-
})
|
|
15846
|
-
}).post("/write", async ({ body, set }) => {
|
|
15847
|
-
const { valid, normalized, error } = validatePath(body.path);
|
|
15848
|
-
if (!valid) {
|
|
15849
|
-
set.status = 403;
|
|
15850
|
-
return { error };
|
|
15851
|
-
}
|
|
15852
|
-
try {
|
|
15853
|
-
const dir = nodePath.dirname(normalized);
|
|
15854
|
-
await fs3.mkdir(dir, { recursive: true });
|
|
15855
|
-
await fs3.writeFile(normalized, body.content, "utf-8");
|
|
15856
|
-
const stats = await fs3.stat(normalized);
|
|
15857
|
-
return { success: true, path: normalized, size: stats.size };
|
|
15858
|
-
} catch (err) {
|
|
15859
|
-
const nodeErr = err;
|
|
15860
|
-
if (nodeErr.code === "EACCES") {
|
|
15861
|
-
set.status = 403;
|
|
15862
|
-
return { error: "Permission denied" };
|
|
15863
|
-
}
|
|
15864
|
-
set.status = 500;
|
|
15865
|
-
return { error: "Failed to write file", details: String(err) };
|
|
15866
|
-
}
|
|
15867
|
-
}, {
|
|
15868
|
-
body: t.Object({
|
|
15869
|
-
path: t.String(),
|
|
15870
|
-
content: t.String(),
|
|
15871
|
-
projectId: t.Optional(t.String())
|
|
15872
|
-
})
|
|
15873
|
-
}).post("/list", async ({ body, set }) => {
|
|
15874
|
-
const { valid, normalized, error } = validatePath(body.path);
|
|
15875
|
-
if (!valid) {
|
|
15876
|
-
set.status = 403;
|
|
15877
|
-
return { error };
|
|
15878
|
-
}
|
|
15879
|
-
try {
|
|
15880
|
-
const entries = await fs3.readdir(normalized, {
|
|
15881
|
-
withFileTypes: true
|
|
15882
|
-
});
|
|
15883
|
-
const files = await Promise.all(entries.map(async (entry) => {
|
|
15884
|
-
const fullPath = nodePath.join(normalized, entry.name);
|
|
15885
|
-
let stats;
|
|
15886
|
-
try {
|
|
15887
|
-
stats = await fs3.stat(fullPath);
|
|
15888
|
-
} catch {
|
|
15889
|
-
stats = null;
|
|
15890
|
-
}
|
|
15891
|
-
return {
|
|
15892
|
-
name: entry.name,
|
|
15893
|
-
path: fullPath,
|
|
15894
|
-
type: entry.isDirectory() ? "directory" : entry.isFile() ? "file" : entry.isSymbolicLink() ? "symlink" : "other",
|
|
15895
|
-
size: stats?.size || 0,
|
|
15896
|
-
modified: stats?.mtime || null,
|
|
15897
|
-
permissions: stats?.mode || null
|
|
15898
|
-
};
|
|
15899
|
-
}));
|
|
15900
|
-
files.sort((a, b) => {
|
|
15901
|
-
if (a.type === "directory" && b.type !== "directory")
|
|
15902
|
-
return -1;
|
|
15903
|
-
if (a.type !== "directory" && b.type === "directory")
|
|
15904
|
-
return 1;
|
|
15905
|
-
return a.name.localeCompare(b.name);
|
|
15906
|
-
});
|
|
15907
|
-
return { path: normalized, files, count: files.length };
|
|
15908
|
-
} catch (err) {
|
|
15909
|
-
const nodeErr = err;
|
|
15910
|
-
if (nodeErr.code === "ENOENT") {
|
|
15911
|
-
set.status = 404;
|
|
15912
|
-
return { error: "Directory not found" };
|
|
15913
|
-
}
|
|
15914
|
-
if (nodeErr.code === "ENOTDIR") {
|
|
15915
|
-
set.status = 400;
|
|
15916
|
-
return { error: "Path is not a directory" };
|
|
15917
|
-
}
|
|
15918
|
-
if (nodeErr.code === "EACCES") {
|
|
15919
|
-
set.status = 403;
|
|
15920
|
-
return { error: "Permission denied" };
|
|
15921
|
-
}
|
|
15922
|
-
set.status = 500;
|
|
15923
|
-
return { error: "Failed to list directory", details: String(err) };
|
|
15924
|
-
}
|
|
15925
|
-
}, {
|
|
15926
|
-
body: t.Object({
|
|
15927
|
-
path: t.String(),
|
|
15928
|
-
projectId: t.Optional(t.String())
|
|
15929
|
-
})
|
|
15930
|
-
}).post("/exists", async ({ body, set }) => {
|
|
15931
|
-
const { valid, normalized, error } = validatePath(body.path);
|
|
15932
|
-
if (!valid) {
|
|
15933
|
-
set.status = 403;
|
|
15934
|
-
return { error };
|
|
15935
|
-
}
|
|
15936
|
-
try {
|
|
15937
|
-
const stats = await fs3.stat(normalized);
|
|
15938
|
-
return {
|
|
15939
|
-
exists: true,
|
|
15940
|
-
path: normalized,
|
|
15941
|
-
type: stats.isDirectory() ? "directory" : stats.isFile() ? "file" : "other",
|
|
15942
|
-
size: stats.size,
|
|
15943
|
-
modified: stats.mtime
|
|
15944
|
-
};
|
|
15945
|
-
} catch (err) {
|
|
15946
|
-
const nodeErr = err;
|
|
15947
|
-
if (nodeErr.code === "ENOENT") {
|
|
15948
|
-
return { exists: false, path: normalized };
|
|
15949
|
-
}
|
|
15950
|
-
set.status = 500;
|
|
15951
|
-
return { error: "Failed to check path", details: String(err) };
|
|
15952
|
-
}
|
|
15953
|
-
}, {
|
|
15954
|
-
body: t.Object({
|
|
15955
|
-
path: t.String()
|
|
15956
|
-
})
|
|
15957
|
-
}).post("/delete", async ({ body, set }) => {
|
|
15958
|
-
const { valid, normalized, error } = validatePath(body.path);
|
|
15959
|
-
if (!valid) {
|
|
15960
|
-
set.status = 403;
|
|
15961
|
-
return { error };
|
|
15962
|
-
}
|
|
15963
|
-
if (isProtectedPath(normalized)) {
|
|
15964
|
-
set.status = 403;
|
|
15965
|
-
return { error: "Cannot delete system directories" };
|
|
15966
|
-
}
|
|
15967
|
-
try {
|
|
15968
|
-
const stats = await fs3.stat(normalized);
|
|
15969
|
-
if (stats.isDirectory()) {
|
|
15970
|
-
await fs3.rm(normalized, { recursive: true, force: true });
|
|
15971
|
-
} else {
|
|
15972
|
-
await fs3.unlink(normalized);
|
|
15973
|
-
}
|
|
15974
|
-
return {
|
|
15975
|
-
success: true,
|
|
15976
|
-
path: normalized
|
|
15977
|
-
};
|
|
15978
|
-
} catch (err) {
|
|
15979
|
-
const nodeErr = err;
|
|
15980
|
-
if (nodeErr.code === "ENOENT") {
|
|
15981
|
-
set.status = 404;
|
|
15982
|
-
return { error: "Path not found" };
|
|
15983
|
-
}
|
|
15984
|
-
if (nodeErr.code === "EACCES") {
|
|
15985
|
-
set.status = 403;
|
|
15986
|
-
return { error: "Permission denied" };
|
|
15987
|
-
}
|
|
15988
|
-
set.status = 500;
|
|
15989
|
-
return { error: "Failed to delete path", details: String(err) };
|
|
15990
|
-
}
|
|
15991
|
-
}, {
|
|
15992
|
-
body: t.Object({
|
|
15993
|
-
path: t.String()
|
|
15994
|
-
})
|
|
15995
|
-
}).post("/readme", async ({ body, set }) => {
|
|
15996
|
-
const { valid, normalized, error } = validatePath(body.path);
|
|
15997
|
-
if (!valid) {
|
|
15998
|
-
set.status = 403;
|
|
15999
|
-
return { error };
|
|
16000
|
-
}
|
|
16001
|
-
const readmeVariants = [
|
|
16002
|
-
"README.md",
|
|
16003
|
-
"readme.md",
|
|
16004
|
-
"README.MD",
|
|
16005
|
-
"README",
|
|
16006
|
-
"readme",
|
|
16007
|
-
"README.txt",
|
|
16008
|
-
"readme.txt",
|
|
16009
|
-
"README.rst",
|
|
16010
|
-
"readme.rst"
|
|
16011
|
-
];
|
|
16012
|
-
for (const variant of readmeVariants) {
|
|
16013
|
-
try {
|
|
16014
|
-
const readmePath = nodePath.join(normalized, variant);
|
|
16015
|
-
const content = await fs3.readFile(readmePath, "utf-8");
|
|
16016
|
-
return {
|
|
16017
|
-
content,
|
|
16018
|
-
path: readmePath,
|
|
16019
|
-
variant,
|
|
16020
|
-
size: content.length
|
|
16021
|
-
};
|
|
16022
|
-
} catch {}
|
|
16023
|
-
}
|
|
16024
|
-
set.status = 404;
|
|
16025
|
-
return { error: "No README file found" };
|
|
16026
|
-
}, {
|
|
16027
|
-
body: t.Object({
|
|
16028
|
-
path: t.String()
|
|
16029
|
-
})
|
|
16030
|
-
});
|
|
16031
|
-
}
|
|
16032
|
-
|
|
16033
|
-
// src/routes/bookmark.routes.ts
|
|
16034
|
-
import crypto6 from "crypto";
|
|
16035
|
-
function createBookmarkRoutes(db, serviceRegistry) {
|
|
16036
|
-
return new Elysia({ prefix: "/api/bookmarks" }).get("/", ({ query }) => {
|
|
16037
|
-
const q = query;
|
|
16038
|
-
const bookmarks = q.projectId !== undefined ? db.getBookmarkedCommandsByProject(q.projectId) : db.getAllBookmarkedCommands();
|
|
16039
|
-
return { bookmarks };
|
|
16040
|
-
}).get("/global", () => {
|
|
16041
|
-
const bookmarks = db.getBookmarkedCommandsByProject(null);
|
|
16042
|
-
return { bookmarks };
|
|
16043
|
-
}).get("/project/:projectId", ({ params }) => {
|
|
16044
|
-
const bookmarks = db.getBookmarkedCommandsByProject(params.projectId);
|
|
16045
|
-
return { bookmarks };
|
|
16046
|
-
}).get("/category/:category", ({ params }) => {
|
|
16047
|
-
const bookmarks = db.getBookmarkedCommandsByCategory(params.category);
|
|
16048
|
-
return { bookmarks, category: params.category };
|
|
16049
|
-
}).get("/:id", ({ params, set }) => {
|
|
16050
|
-
const bookmark = db.getBookmarkedCommand(params.id);
|
|
16051
|
-
if (!bookmark) {
|
|
16052
|
-
set.status = 404;
|
|
16053
|
-
return { error: "Bookmarked command not found" };
|
|
16054
|
-
}
|
|
16055
|
-
return { bookmark };
|
|
16056
|
-
}).post("/", ({ body, set }) => {
|
|
16057
|
-
try {
|
|
16058
|
-
const bookmark = db.createBookmarkedCommand({
|
|
16059
|
-
id: crypto6.randomUUID(),
|
|
16060
|
-
projectId: body.projectId,
|
|
16061
|
-
command: body.command,
|
|
16062
|
-
description: body.description,
|
|
16063
|
-
category: body.category
|
|
16064
|
-
});
|
|
16065
|
-
return { bookmark };
|
|
16066
|
-
} catch (err) {
|
|
16067
|
-
set.status = 500;
|
|
16068
|
-
return {
|
|
16069
|
-
error: "Failed to create bookmarked command",
|
|
16070
|
-
details: String(err)
|
|
16071
|
-
};
|
|
16072
|
-
}
|
|
16073
|
-
}, {
|
|
16074
|
-
body: t.Object({
|
|
16075
|
-
projectId: t.Optional(t.String()),
|
|
16076
|
-
command: t.String(),
|
|
16077
|
-
description: t.Optional(t.String()),
|
|
16078
|
-
category: t.Optional(t.String())
|
|
16079
|
-
})
|
|
16080
|
-
}).put("/:id", ({ params, body, set }) => {
|
|
16081
|
-
const bookmark = db.getBookmarkedCommand(params.id);
|
|
16082
|
-
if (!bookmark) {
|
|
16083
|
-
set.status = 404;
|
|
16084
|
-
return { error: "Bookmarked command not found" };
|
|
16085
|
-
}
|
|
16086
|
-
try {
|
|
16087
|
-
const updates = {};
|
|
16088
|
-
if (body.command !== undefined)
|
|
16089
|
-
updates.command = body.command;
|
|
16090
|
-
if (body.description !== undefined)
|
|
16091
|
-
updates.description = body.description;
|
|
16092
|
-
if (body.category !== undefined)
|
|
16093
|
-
updates.category = body.category;
|
|
16094
|
-
db.updateBookmarkedCommand(params.id, updates);
|
|
16095
|
-
const updatedBookmark = db.getBookmarkedCommand(params.id);
|
|
16096
|
-
return { bookmark: updatedBookmark };
|
|
16097
|
-
} catch (err) {
|
|
16098
|
-
set.status = 500;
|
|
16099
|
-
return {
|
|
16100
|
-
error: "Failed to update bookmarked command",
|
|
16101
|
-
details: String(err)
|
|
16102
|
-
};
|
|
16103
|
-
}
|
|
16104
|
-
}, {
|
|
16105
|
-
body: t.Object({
|
|
16106
|
-
command: t.Optional(t.String()),
|
|
16107
|
-
description: t.Optional(t.String()),
|
|
16108
|
-
category: t.Optional(t.String())
|
|
16109
|
-
})
|
|
16110
|
-
}).delete("/:id", ({ params, set }) => {
|
|
16111
|
-
const bookmark = db.getBookmarkedCommand(params.id);
|
|
16112
|
-
if (!bookmark) {
|
|
16113
|
-
set.status = 404;
|
|
16114
|
-
return { error: "Bookmarked command not found" };
|
|
16115
|
-
}
|
|
16116
|
-
try {
|
|
16117
|
-
db.deleteBookmarkedCommand(params.id);
|
|
16118
|
-
return { success: true };
|
|
16119
|
-
} catch (err) {
|
|
16120
|
-
set.status = 500;
|
|
16121
|
-
return {
|
|
16122
|
-
error: "Failed to delete bookmarked command",
|
|
16123
|
-
details: String(err)
|
|
16124
|
-
};
|
|
16125
|
-
}
|
|
16126
|
-
}).post("/:id/execute", async ({ params, body, set }) => {
|
|
16127
|
-
const bookmark = db.getBookmarkedCommand(params.id);
|
|
16128
|
-
if (!bookmark) {
|
|
16129
|
-
set.status = 404;
|
|
16130
|
-
return { error: "Bookmarked command not found" };
|
|
16131
|
-
}
|
|
16132
|
-
try {
|
|
16133
|
-
if (body.sessionId) {
|
|
16134
|
-
const sessionProvider = serviceRegistry.getProvider("session");
|
|
16135
|
-
if (!sessionProvider) {
|
|
16136
|
-
set.status = 400;
|
|
16137
|
-
return { error: "No session provider registered" };
|
|
16138
|
-
}
|
|
16139
|
-
const session = await sessionProvider.get(body.sessionId);
|
|
16140
|
-
if (!session) {
|
|
16141
|
-
set.status = 404;
|
|
16142
|
-
return { error: "Session not found" };
|
|
16143
|
-
}
|
|
16144
|
-
await sessionProvider.sendCommand(body.sessionId, bookmark.command);
|
|
16145
|
-
return {
|
|
16146
|
-
success: true,
|
|
16147
|
-
executedIn: "session",
|
|
16148
|
-
sessionId: body.sessionId
|
|
16149
|
-
};
|
|
16150
|
-
}
|
|
16151
|
-
const task = db.createTask({
|
|
16152
|
-
id: crypto6.randomUUID(),
|
|
16153
|
-
type: "command",
|
|
16154
|
-
status: "pending",
|
|
16155
|
-
payload: JSON.stringify({
|
|
16156
|
-
command: bookmark.command,
|
|
16157
|
-
cwd: body.cwd || process.cwd()
|
|
16158
|
-
})
|
|
16159
|
-
});
|
|
16160
|
-
return {
|
|
16161
|
-
success: true,
|
|
16162
|
-
executedIn: "task",
|
|
16163
|
-
taskId: task.id
|
|
16164
|
-
};
|
|
16165
|
-
} catch (err) {
|
|
16166
|
-
set.status = 500;
|
|
16167
|
-
return {
|
|
16168
|
-
error: "Failed to execute bookmarked command",
|
|
16169
|
-
details: String(err)
|
|
16170
|
-
};
|
|
16171
|
-
}
|
|
16172
|
-
}, {
|
|
16173
|
-
body: t.Object({
|
|
16174
|
-
sessionId: t.Optional(t.String()),
|
|
16175
|
-
cwd: t.Optional(t.String())
|
|
16176
|
-
})
|
|
16177
|
-
});
|
|
16178
|
-
}
|
|
16179
|
-
|
|
16180
|
-
// src/routes/notification.routes.ts
|
|
16181
|
-
import crypto7 from "crypto";
|
|
16182
|
-
function createNotificationRoutes(db) {
|
|
16183
|
-
return new Elysia({ prefix: "/api/notifications" }).get("/", ({ query }) => {
|
|
16184
|
-
const q = query;
|
|
16185
|
-
if (q.status === "unread") {
|
|
16186
|
-
return { notifications: db.getUnreadNotifications() };
|
|
16187
|
-
}
|
|
16188
|
-
if (q.projectId !== undefined) {
|
|
16189
|
-
return { notifications: db.getNotificationsByProject(q.projectId) };
|
|
16190
|
-
}
|
|
16191
|
-
return { notifications: db.getAllNotifications() };
|
|
16192
|
-
}).get("/unread", () => {
|
|
16193
|
-
const notifications = db.getUnreadNotifications();
|
|
16194
|
-
return { notifications, count: notifications.length };
|
|
16195
|
-
}).get("/global", () => {
|
|
16196
|
-
const notifications = db.getGlobalNotifications();
|
|
16197
|
-
return { notifications, count: notifications.length };
|
|
16198
|
-
}).get("/project/:projectId", ({ params }) => {
|
|
16199
|
-
const notifications = db.getNotificationsByProject(params.projectId);
|
|
16200
|
-
return { notifications };
|
|
16201
|
-
}).put("/read-all", ({ set }) => {
|
|
16202
|
-
try {
|
|
16203
|
-
const count = db.markAllNotificationsRead();
|
|
16204
|
-
return { success: true, markedCount: count };
|
|
16205
|
-
} catch (err) {
|
|
16206
|
-
set.status = 500;
|
|
16207
|
-
return {
|
|
16208
|
-
error: "Failed to mark all notifications as read",
|
|
16209
|
-
details: String(err)
|
|
16210
|
-
};
|
|
16211
|
-
}
|
|
16212
|
-
}).delete("/clear/old", ({ query }) => {
|
|
16213
|
-
const q = query;
|
|
16214
|
-
const days = q.days ? parseInt(q.days, 10) : 30;
|
|
16215
|
-
const count = db.clearOldNotifications(days);
|
|
16216
|
-
return { success: true, deletedCount: count };
|
|
16217
|
-
}).get("/:id", ({ params, set }) => {
|
|
16218
|
-
const notification = db.getNotification(params.id);
|
|
16219
|
-
if (!notification) {
|
|
16220
|
-
set.status = 404;
|
|
16221
|
-
return { error: "Notification not found" };
|
|
16222
|
-
}
|
|
16223
|
-
return { notification };
|
|
16224
|
-
}).post("/", ({ body, set }) => {
|
|
16225
|
-
try {
|
|
16226
|
-
const notification = db.createNotification({
|
|
16227
|
-
id: crypto7.randomUUID(),
|
|
16228
|
-
sessionName: body.sessionName,
|
|
16229
|
-
projectId: body.projectId,
|
|
16230
|
-
type: body.type || "info",
|
|
16231
|
-
title: body.title,
|
|
16232
|
-
message: body.message,
|
|
16233
|
-
status: "unread"
|
|
16234
|
-
});
|
|
16235
|
-
return { notification };
|
|
16236
|
-
} catch (err) {
|
|
16237
|
-
set.status = 500;
|
|
16238
|
-
return {
|
|
16239
|
-
error: "Failed to create notification",
|
|
16240
|
-
details: String(err)
|
|
16241
|
-
};
|
|
16242
|
-
}
|
|
16243
|
-
}, {
|
|
16244
|
-
body: t.Object({
|
|
16245
|
-
sessionName: t.Optional(t.String()),
|
|
16246
|
-
projectId: t.Optional(t.String()),
|
|
16247
|
-
type: t.Optional(t.Union([
|
|
16248
|
-
t.Literal("info"),
|
|
16249
|
-
t.Literal("success"),
|
|
16250
|
-
t.Literal("warning"),
|
|
16251
|
-
t.Literal("error")
|
|
16252
|
-
])),
|
|
16253
|
-
title: t.String(),
|
|
16254
|
-
message: t.String()
|
|
16255
|
-
})
|
|
16256
|
-
}).put("/:id/read", ({ params, set }) => {
|
|
16257
|
-
const notification = db.getNotification(params.id);
|
|
16258
|
-
if (!notification) {
|
|
16259
|
-
set.status = 404;
|
|
16260
|
-
return { error: "Notification not found" };
|
|
16261
|
-
}
|
|
16262
|
-
db.updateNotificationStatus(params.id, "read");
|
|
16263
|
-
return { success: true };
|
|
16264
|
-
}).put("/:id/unread", ({ params, set }) => {
|
|
16265
|
-
const notification = db.getNotification(params.id);
|
|
16266
|
-
if (!notification) {
|
|
16267
|
-
set.status = 404;
|
|
16268
|
-
return { error: "Notification not found" };
|
|
16269
|
-
}
|
|
16270
|
-
db.updateNotificationStatus(params.id, "unread");
|
|
16271
|
-
return { success: true };
|
|
16272
|
-
}).delete("/:id", ({ params, set }) => {
|
|
16273
|
-
const notification = db.getNotification(params.id);
|
|
16274
|
-
if (!notification) {
|
|
16275
|
-
set.status = 404;
|
|
16276
|
-
return { error: "Notification not found" };
|
|
16277
|
-
}
|
|
16278
|
-
try {
|
|
16279
|
-
db.deleteNotification(params.id);
|
|
16280
|
-
return { success: true };
|
|
16281
|
-
} catch (err) {
|
|
16282
|
-
set.status = 500;
|
|
16283
|
-
return {
|
|
16284
|
-
error: "Failed to delete notification",
|
|
16285
|
-
details: String(err)
|
|
16286
|
-
};
|
|
16287
|
-
}
|
|
16288
|
-
}).post("/webhook/:sessionId", ({ params, body, set }) => {
|
|
16289
|
-
try {
|
|
16290
|
-
const typeMap = {
|
|
16291
|
-
task_completed: "success",
|
|
16292
|
-
info: "info",
|
|
16293
|
-
success: "success",
|
|
16294
|
-
warning: "warning",
|
|
16295
|
-
error: "error"
|
|
16296
|
-
};
|
|
16297
|
-
const mappedType = typeMap[body.type || "info"] || "info";
|
|
16298
|
-
const message = body.metadata ? `${body.message}
|
|
16299
|
-
|
|
16300
|
-
Metadata: ${JSON.stringify(body.metadata, null, 2)}` : body.message;
|
|
16301
|
-
const notification = db.createNotification({
|
|
16302
|
-
id: crypto7.randomUUID(),
|
|
16303
|
-
sessionName: body.projectName,
|
|
16304
|
-
projectId: params.sessionId,
|
|
16305
|
-
type: mappedType,
|
|
16306
|
-
title: body.title,
|
|
16307
|
-
message,
|
|
16308
|
-
status: "unread"
|
|
16309
|
-
});
|
|
16310
|
-
return {
|
|
16311
|
-
notification,
|
|
16312
|
-
webhook: true,
|
|
16313
|
-
originalType: body.type || "info",
|
|
16314
|
-
mappedType
|
|
16315
|
-
};
|
|
16316
|
-
} catch (err) {
|
|
16317
|
-
set.status = 500;
|
|
16318
|
-
return {
|
|
16319
|
-
error: "Failed to create webhook notification",
|
|
16320
|
-
details: String(err)
|
|
16321
|
-
};
|
|
16322
|
-
}
|
|
16323
|
-
}, {
|
|
16324
|
-
body: t.Object({
|
|
16325
|
-
type: t.Optional(t.String()),
|
|
16326
|
-
title: t.String(),
|
|
16327
|
-
message: t.String(),
|
|
16328
|
-
metadata: t.Optional(t.Record(t.String(), t.Unknown())),
|
|
16329
|
-
projectName: t.Optional(t.String())
|
|
16330
|
-
})
|
|
16331
|
-
});
|
|
16332
|
-
}
|
|
16333
|
-
|
|
16334
|
-
// src/routes/project.routes.ts
|
|
16335
|
-
import { promises as fs4 } from "fs";
|
|
16336
|
-
import nodePath2 from "path";
|
|
16337
|
-
var FORBIDDEN_PATHS = [
|
|
16338
|
-
"/System",
|
|
16339
|
-
"/usr",
|
|
16340
|
-
"/bin",
|
|
16341
|
-
"/sbin",
|
|
16342
|
-
"/etc",
|
|
16343
|
-
"/var/root",
|
|
16344
|
-
"/private/var/root",
|
|
16345
|
-
"/tmp",
|
|
16346
|
-
"/Windows",
|
|
16347
|
-
"/Program Files",
|
|
16348
|
-
"C:\\Windows",
|
|
16349
|
-
"C:\\Program Files"
|
|
16350
|
-
];
|
|
16351
|
-
var ALLOWED_PREFIXES = [
|
|
16352
|
-
"/Users",
|
|
16353
|
-
"/home",
|
|
16354
|
-
"/opt",
|
|
16355
|
-
process.env.HOME || "",
|
|
16356
|
-
"C:\\Users",
|
|
16357
|
-
"D:\\",
|
|
16358
|
-
"E:\\"
|
|
16359
|
-
].filter(Boolean);
|
|
16360
|
-
function isAllowedPath(scanPath) {
|
|
16361
|
-
const normalized = nodePath2.resolve(scanPath);
|
|
16362
|
-
if (FORBIDDEN_PATHS.some((fp) => normalized.startsWith(fp)))
|
|
16363
|
-
return false;
|
|
16364
|
-
return ALLOWED_PREFIXES.some((prefix) => normalized.startsWith(prefix));
|
|
16365
|
-
}
|
|
16366
|
-
async function detectProject(dirPath, dirName) {
|
|
16367
|
-
try {
|
|
16368
|
-
const files = await fs4.readdir(dirPath);
|
|
16369
|
-
const hasPackageJson = files.includes("package.json");
|
|
16370
|
-
const hasPyproject = files.includes("pyproject.toml");
|
|
16371
|
-
const hasRequirements = files.includes("requirements.txt");
|
|
16372
|
-
const hasCargoToml = files.includes("Cargo.toml");
|
|
16373
|
-
const hasGitDir = files.includes(".git");
|
|
16374
|
-
if (!hasPackageJson && !hasPyproject && !hasRequirements && !hasCargoToml && !hasGitDir) {
|
|
16375
|
-
return null;
|
|
16376
|
-
}
|
|
16377
|
-
let projectType = "unknown";
|
|
16378
|
-
let vitePort;
|
|
16379
|
-
if (hasPackageJson) {
|
|
16380
|
-
try {
|
|
16381
|
-
const content = await fs4.readFile(nodePath2.join(dirPath, "package.json"), "utf-8");
|
|
16382
|
-
if (content.trim()) {
|
|
16383
|
-
const pkg = JSON.parse(content);
|
|
16384
|
-
const deps = { ...pkg.dependencies, ...pkg.devDependencies };
|
|
16385
|
-
if (deps.vite) {
|
|
16386
|
-
projectType = "vite";
|
|
16387
|
-
const devScript = pkg.scripts?.dev;
|
|
16388
|
-
if (typeof devScript === "string") {
|
|
16389
|
-
const portMatch = devScript.match(/--port[=\s]+(\d+)/);
|
|
16390
|
-
if (portMatch)
|
|
16391
|
-
vitePort = parseInt(portMatch[1], 10);
|
|
16392
|
-
}
|
|
16393
|
-
} else if (deps.react)
|
|
16394
|
-
projectType = "react";
|
|
16395
|
-
else if (deps.next)
|
|
16396
|
-
projectType = "nextjs";
|
|
16397
|
-
else if (deps.express)
|
|
16398
|
-
projectType = "express";
|
|
16399
|
-
else if (deps.fastify)
|
|
16400
|
-
projectType = "fastify";
|
|
16401
|
-
else
|
|
16402
|
-
projectType = "nodejs";
|
|
16403
|
-
}
|
|
16404
|
-
} catch {
|
|
16405
|
-
projectType = "nodejs";
|
|
16406
|
-
}
|
|
16407
|
-
} else if (hasPyproject || hasRequirements) {
|
|
16408
|
-
projectType = "python";
|
|
16409
|
-
} else if (hasCargoToml) {
|
|
16410
|
-
projectType = "rust";
|
|
16411
|
-
}
|
|
16412
|
-
const stats = await fs4.stat(dirPath);
|
|
16413
|
-
const project = {
|
|
16414
|
-
id: `local-${Buffer.from(dirPath).toString("base64url").slice(0, 8)}-${Date.now()}`,
|
|
16415
|
-
name: dirName,
|
|
16416
|
-
path: dirPath,
|
|
16417
|
-
type: projectType,
|
|
16418
|
-
lastUpdated: stats.mtime.getTime(),
|
|
16419
|
-
status: "INACTIVE",
|
|
16420
|
-
tags: [projectType]
|
|
16421
|
-
};
|
|
16422
|
-
if (vitePort)
|
|
16423
|
-
project.vitePort = vitePort;
|
|
16424
|
-
if (hasGitDir) {
|
|
16425
|
-
try {
|
|
16426
|
-
const gitConfig = await fs4.readFile(nodePath2.join(dirPath, ".git", "config"), "utf-8");
|
|
16427
|
-
const remoteMatch = gitConfig.match(/url = (.+)/);
|
|
16428
|
-
if (remoteMatch)
|
|
16429
|
-
project.repository = remoteMatch[1].trim();
|
|
16430
|
-
} catch {}
|
|
16431
|
-
}
|
|
16432
|
-
return project;
|
|
16433
|
-
} catch {
|
|
16434
|
-
return null;
|
|
16435
|
-
}
|
|
16436
|
-
}
|
|
16437
|
-
async function scanForProjects(basePath, recursive) {
|
|
16438
|
-
const projects = [];
|
|
16439
|
-
try {
|
|
16440
|
-
const entries = await fs4.readdir(basePath, { withFileTypes: true });
|
|
16441
|
-
for (const entry of entries) {
|
|
16442
|
-
if (!entry.isDirectory())
|
|
16443
|
-
continue;
|
|
16444
|
-
if (entry.name.startsWith(".") || entry.name === "node_modules")
|
|
16445
|
-
continue;
|
|
16446
|
-
const fullPath = nodePath2.join(basePath, entry.name);
|
|
16447
|
-
const project = await detectProject(fullPath, entry.name);
|
|
16448
|
-
if (project)
|
|
16449
|
-
projects.push(project);
|
|
16450
|
-
if (recursive) {
|
|
16451
|
-
try {
|
|
16452
|
-
const subProjects = await scanForProjects(fullPath, true);
|
|
16453
|
-
projects.push(...subProjects);
|
|
16454
|
-
} catch {}
|
|
16455
|
-
}
|
|
16456
|
-
}
|
|
16457
|
-
} catch {}
|
|
16458
|
-
return projects;
|
|
16459
|
-
}
|
|
16460
|
-
function createProjectRoutes() {
|
|
16461
|
-
return new Elysia({ prefix: "/api/projects" }).get("/", () => {
|
|
16462
|
-
return { projects: [] };
|
|
16463
|
-
}).post("/", () => {
|
|
16464
|
-
const id = `project-${Date.now()}`;
|
|
16465
|
-
return { id, success: true };
|
|
16466
|
-
}).post("/scan", async ({ body, set }) => {
|
|
16467
|
-
if (!body.scanPath) {
|
|
16468
|
-
set.status = 400;
|
|
16469
|
-
return { error: "scanPath is required", success: false };
|
|
16470
|
-
}
|
|
16471
|
-
const normalized = nodePath2.normalize(body.scanPath);
|
|
16472
|
-
if (normalized.includes("..")) {
|
|
16473
|
-
set.status = 403;
|
|
16474
|
-
return { error: "Directory traversal not allowed", success: false };
|
|
16475
|
-
}
|
|
16476
|
-
if (!isAllowedPath(normalized)) {
|
|
16477
|
-
set.status = 403;
|
|
16478
|
-
return {
|
|
16479
|
-
error: "Access to this path is not allowed",
|
|
16480
|
-
success: false
|
|
16481
|
-
};
|
|
16482
|
-
}
|
|
16483
|
-
try {
|
|
16484
|
-
const projects = await scanForProjects(normalized, body.recursive ?? false);
|
|
16485
|
-
return { projects, success: true, count: projects.length };
|
|
16486
|
-
} catch (err) {
|
|
16487
|
-
set.status = 500;
|
|
16488
|
-
return {
|
|
16489
|
-
error: "Failed to scan projects",
|
|
16490
|
-
details: String(err),
|
|
16491
|
-
success: false
|
|
16492
|
-
};
|
|
16493
|
-
}
|
|
16494
|
-
}, {
|
|
16495
|
-
body: t.Object({
|
|
16496
|
-
recursive: t.Optional(t.Boolean()),
|
|
16497
|
-
scanPath: t.Optional(t.String()),
|
|
16498
|
-
respectGitignore: t.Optional(t.Boolean())
|
|
16499
|
-
})
|
|
16500
|
-
}).post("/scan-remote", async ({ body, set }) => {
|
|
16501
|
-
if (!body.scanPath) {
|
|
16502
|
-
set.status = 400;
|
|
16503
|
-
return {
|
|
16504
|
-
error: "scanPath is required for remote scanning",
|
|
16505
|
-
success: false
|
|
16506
|
-
};
|
|
16507
|
-
}
|
|
16508
|
-
return {
|
|
16509
|
-
projects: [],
|
|
16510
|
-
success: true,
|
|
16511
|
-
count: 0,
|
|
16512
|
-
message: "Remote scanning not yet implemented"
|
|
16513
|
-
};
|
|
16514
|
-
}, {
|
|
16515
|
-
body: t.Object({
|
|
16516
|
-
recursive: t.Optional(t.Boolean()),
|
|
16517
|
-
connectionId: t.Optional(t.String()),
|
|
16518
|
-
scanPath: t.Optional(t.String()),
|
|
16519
|
-
respectGitignore: t.Optional(t.Boolean())
|
|
16520
|
-
})
|
|
16521
|
-
}).put("/:id", () => {
|
|
16522
|
-
return { success: true, changes: 1 };
|
|
16523
|
-
});
|
|
16524
|
-
}
|
|
16525
|
-
|
|
16526
|
-
// src/routes/plugin.routes.ts
|
|
16527
|
-
var AVAILABLE_PLUGINS = [
|
|
16528
|
-
{
|
|
16529
|
-
packageName: "@burdenoff/vibe-plugin-tunnel-cloudflare",
|
|
16530
|
-
name: "tunnel-cloudflare",
|
|
16531
|
-
description: "Cloudflare Tunnel provider for remote access",
|
|
16532
|
-
cliCommand: "tunnel",
|
|
16533
|
-
apiPrefix: "/api/tunnel-cloudflare",
|
|
16534
|
-
installed: false,
|
|
16535
|
-
category: "tunnel"
|
|
16536
|
-
},
|
|
16537
|
-
{
|
|
16538
|
-
packageName: "@burdenoff/vibe-plugin-session-tmux",
|
|
16539
|
-
name: "session-tmux",
|
|
16540
|
-
description: "tmux session provider for terminal management",
|
|
16541
|
-
cliCommand: "session",
|
|
16542
|
-
apiPrefix: "/api/session-tmux",
|
|
16543
|
-
installed: false,
|
|
16544
|
-
category: "session"
|
|
16545
|
-
},
|
|
16546
|
-
{
|
|
16547
|
-
packageName: "@burdenoff/vibe-plugin-ssh",
|
|
16548
|
-
name: "ssh",
|
|
16549
|
-
description: "SSH connection management and port forwarding",
|
|
16550
|
-
cliCommand: "ssh",
|
|
16551
|
-
apiPrefix: "/api/ssh",
|
|
16552
|
-
installed: false,
|
|
16553
|
-
category: "tool"
|
|
16554
|
-
},
|
|
16555
|
-
{
|
|
16556
|
-
packageName: "@burdenoff/vibe-plugin-ai",
|
|
16557
|
-
name: "ai",
|
|
16558
|
-
description: "AI tool management and integration",
|
|
16559
|
-
cliCommand: "ai",
|
|
16560
|
-
apiPrefix: undefined,
|
|
16561
|
-
installed: false,
|
|
16562
|
-
category: "integration"
|
|
16563
|
-
}
|
|
16564
|
-
];
|
|
16565
|
-
function createPluginRoutes(pluginManager) {
|
|
16566
|
-
return new Elysia({ prefix: "/api/plugins" }).get("/", () => {
|
|
16567
|
-
const plugins = pluginManager.getPluginDetails();
|
|
16568
|
-
return { plugins, count: plugins.length };
|
|
16569
|
-
}).get("/available", () => {
|
|
16570
|
-
const installed = pluginManager.getPluginDetails();
|
|
16571
|
-
const installedNames = new Set(installed.map((p) => p.packageName));
|
|
16572
|
-
return {
|
|
16573
|
-
plugins: AVAILABLE_PLUGINS.map((p) => ({
|
|
16574
|
-
...p,
|
|
16575
|
-
installed: installedNames.has(p.packageName)
|
|
16576
|
-
}))
|
|
16577
|
-
};
|
|
16578
|
-
}).post("/install", async ({ body, set }) => {
|
|
16579
|
-
if (!body.packageName || typeof body.packageName !== "string") {
|
|
16580
|
-
set.status = 400;
|
|
16581
|
-
return { error: "Missing required field: packageName" };
|
|
16582
|
-
}
|
|
16583
|
-
try {
|
|
16584
|
-
await pluginManager.install(body.packageName);
|
|
16585
|
-
return {
|
|
16586
|
-
success: true,
|
|
16587
|
-
message: `Plugin ${body.packageName} installed and loaded successfully`
|
|
16588
|
-
};
|
|
16589
|
-
} catch (err) {
|
|
16590
|
-
set.status = 500;
|
|
16591
|
-
return { error: "Failed to install plugin", details: String(err) };
|
|
16592
|
-
}
|
|
16593
|
-
}, {
|
|
16594
|
-
body: t.Object({
|
|
16595
|
-
packageName: t.String()
|
|
16596
|
-
})
|
|
16597
|
-
}).post("/remove", async ({ body, set }) => {
|
|
16598
|
-
if (!body.packageName || typeof body.packageName !== "string") {
|
|
16599
|
-
set.status = 400;
|
|
16600
|
-
return { error: "Missing required field: packageName" };
|
|
16601
|
-
}
|
|
16602
|
-
try {
|
|
16603
|
-
await pluginManager.remove(body.packageName);
|
|
16604
|
-
return {
|
|
16605
|
-
success: true,
|
|
16606
|
-
message: `Plugin ${body.packageName} removed successfully`
|
|
16607
|
-
};
|
|
16608
|
-
} catch (err) {
|
|
16609
|
-
set.status = 500;
|
|
16610
|
-
return { error: "Failed to remove plugin", details: String(err) };
|
|
16611
|
-
}
|
|
16612
|
-
}, {
|
|
16613
|
-
body: t.Object({
|
|
16614
|
-
packageName: t.String()
|
|
16615
|
-
})
|
|
16616
|
-
}).post("/reload", async ({ set }) => {
|
|
16617
|
-
try {
|
|
16618
|
-
await pluginManager.dispatchServerStop();
|
|
16619
|
-
await pluginManager.loadAll();
|
|
16620
|
-
const plugins = pluginManager.getPluginDetails();
|
|
16621
|
-
return {
|
|
16622
|
-
success: true,
|
|
16623
|
-
plugins,
|
|
16624
|
-
message: `Reloaded ${plugins.length} plugin(s)`
|
|
16625
|
-
};
|
|
16626
|
-
} catch (err) {
|
|
16627
|
-
set.status = 500;
|
|
16628
|
-
return { error: "Failed to reload plugins", details: String(err) };
|
|
16629
|
-
}
|
|
16630
|
-
});
|
|
16631
|
-
}
|
|
16632
|
-
|
|
16633
|
-
// src/routes/plugin-state.routes.ts
|
|
16634
|
-
class LocalPluginStateBackend {
|
|
16635
|
-
db;
|
|
16636
|
-
constructor(db) {
|
|
16637
|
-
this.db = db;
|
|
16638
|
-
}
|
|
16639
|
-
async getAll(pluginName) {
|
|
16640
|
-
return this.db.getAllPluginState(pluginName).map((e) => ({
|
|
16641
|
-
key: e.key,
|
|
16642
|
-
value: e.value
|
|
16643
|
-
}));
|
|
16644
|
-
}
|
|
16645
|
-
async get(pluginName, key) {
|
|
16646
|
-
return this.db.getPluginState(pluginName, key);
|
|
16647
|
-
}
|
|
16648
|
-
async set(pluginName, key, value) {
|
|
16649
|
-
this.db.setPluginState(pluginName, key, value);
|
|
16650
|
-
}
|
|
16651
|
-
async delete(pluginName, key) {
|
|
16652
|
-
return this.db.deletePluginState(pluginName, key);
|
|
16653
|
-
}
|
|
16654
|
-
async deleteAll(pluginName) {
|
|
16655
|
-
return this.db.deleteAllPluginState(pluginName);
|
|
16656
|
-
}
|
|
16657
|
-
}
|
|
16658
|
-
|
|
16659
|
-
class PluginStateRouter {
|
|
16660
|
-
db;
|
|
16661
|
-
_agentId;
|
|
16662
|
-
localBackend;
|
|
16663
|
-
externalConfig;
|
|
16664
|
-
constructor(db, _agentId) {
|
|
16665
|
-
this.db = db;
|
|
16666
|
-
this._agentId = _agentId;
|
|
16667
|
-
this.localBackend = new LocalPluginStateBackend(db);
|
|
16668
|
-
}
|
|
16669
|
-
configureExternal(config) {
|
|
16670
|
-
this.externalConfig = config;
|
|
16671
|
-
}
|
|
16672
|
-
resolve(backend) {
|
|
16673
|
-
switch (backend) {
|
|
16674
|
-
case "remote":
|
|
16675
|
-
logger.warn("plugin-state", "Remote backend not yet implemented, falling back to local");
|
|
16676
|
-
return this.localBackend;
|
|
16677
|
-
case "external":
|
|
16678
|
-
if (!this.externalConfig) {
|
|
16679
|
-
logger.warn("plugin-state", "External backend not configured, falling back to local");
|
|
16680
|
-
return this.localBackend;
|
|
16681
|
-
}
|
|
16682
|
-
logger.warn("plugin-state", "External backend not yet implemented, falling back to local");
|
|
16683
|
-
return this.localBackend;
|
|
16684
|
-
case "local":
|
|
16685
|
-
default:
|
|
16686
|
-
return this.localBackend;
|
|
16687
|
-
}
|
|
16688
|
-
}
|
|
16689
|
-
}
|
|
16690
|
-
function createPluginStateRoutes(db) {
|
|
16691
|
-
const agentId = db.getConfig("gateway-auth:clientId");
|
|
16692
|
-
const router = new PluginStateRouter(db, agentId);
|
|
16693
|
-
const externalConfigStr = db.getConfig("plugin-state:external-config");
|
|
16694
|
-
if (externalConfigStr) {
|
|
16695
|
-
try {
|
|
16696
|
-
const config = JSON.parse(externalConfigStr);
|
|
16697
|
-
router.configureExternal(config);
|
|
16698
|
-
} catch {
|
|
16699
|
-
logger.warn("plugin-state-routes", "Failed to parse saved external config");
|
|
16700
|
-
}
|
|
16701
|
-
}
|
|
16702
|
-
return new Elysia({ prefix: "/api/plugin-state" }).post("/config/external", ({ body, set }) => {
|
|
16703
|
-
if (!body.baseUrl) {
|
|
16704
|
-
set.status = 400;
|
|
16705
|
-
return { error: "Missing required field: baseUrl" };
|
|
16706
|
-
}
|
|
16707
|
-
router.configureExternal(body);
|
|
16708
|
-
db.setConfig("plugin-state:external-config", JSON.stringify(body));
|
|
16709
|
-
return { configured: true, baseUrl: body.baseUrl };
|
|
16710
|
-
}, {
|
|
16711
|
-
body: t.Object({
|
|
16712
|
-
baseUrl: t.String(),
|
|
16713
|
-
headers: t.Optional(t.Record(t.String(), t.String()))
|
|
16714
|
-
})
|
|
16715
|
-
}).get("/:pluginName", async ({ params, query }) => {
|
|
16716
|
-
const q = query;
|
|
16717
|
-
const backend = router.resolve(q.backend);
|
|
16718
|
-
const entries = await backend.getAll(params.pluginName);
|
|
16719
|
-
return {
|
|
16720
|
-
pluginName: params.pluginName,
|
|
16721
|
-
entries,
|
|
16722
|
-
count: entries.length,
|
|
16723
|
-
backend: q.backend ?? "local"
|
|
16724
|
-
};
|
|
16725
|
-
}).get("/:pluginName/:key", async ({ params, query, set }) => {
|
|
16726
|
-
const q = query;
|
|
16727
|
-
const backend = router.resolve(q.backend);
|
|
16728
|
-
const value = await backend.get(params.pluginName, params.key);
|
|
16729
|
-
if (value === undefined) {
|
|
16730
|
-
set.status = 404;
|
|
16731
|
-
return {
|
|
16732
|
-
error: "Not found",
|
|
16733
|
-
message: `Key "${params.key}" not found for plugin "${params.pluginName}"`
|
|
16734
|
-
};
|
|
16735
|
-
}
|
|
16736
|
-
return {
|
|
16737
|
-
pluginName: params.pluginName,
|
|
16738
|
-
key: params.key,
|
|
16739
|
-
value,
|
|
16740
|
-
backend: q.backend ?? "local"
|
|
16741
|
-
};
|
|
16742
|
-
}).put("/:pluginName/:key", async ({ params, body, query, set }) => {
|
|
16743
|
-
if (body.value === undefined || body.value === null) {
|
|
16744
|
-
set.status = 400;
|
|
16745
|
-
return { error: "Missing required field: value" };
|
|
16746
|
-
}
|
|
16747
|
-
const q = query;
|
|
16748
|
-
const strValue = typeof body.value === "string" ? body.value : JSON.stringify(body.value);
|
|
16749
|
-
const backend = router.resolve(q.backend);
|
|
16750
|
-
await backend.set(params.pluginName, params.key, strValue);
|
|
16751
|
-
return {
|
|
16752
|
-
pluginName: params.pluginName,
|
|
16753
|
-
key: params.key,
|
|
16754
|
-
value: strValue,
|
|
16755
|
-
updated: true,
|
|
16756
|
-
backend: q.backend ?? "local"
|
|
16757
|
-
};
|
|
16758
|
-
}, {
|
|
16759
|
-
body: t.Object({
|
|
16760
|
-
value: t.Union([t.String(), t.Any()])
|
|
16761
|
-
})
|
|
16762
|
-
}).delete("/:pluginName/:key", async ({ params, query, set }) => {
|
|
16763
|
-
const q = query;
|
|
16764
|
-
const backend = router.resolve(q.backend);
|
|
16765
|
-
const deleted = await backend.delete(params.pluginName, params.key);
|
|
16766
|
-
if (!deleted) {
|
|
16767
|
-
set.status = 404;
|
|
16768
|
-
return {
|
|
16769
|
-
error: "Not found",
|
|
16770
|
-
message: `Key "${params.key}" not found for plugin "${params.pluginName}"`
|
|
16771
|
-
};
|
|
16772
|
-
}
|
|
16773
|
-
return {
|
|
16774
|
-
pluginName: params.pluginName,
|
|
16775
|
-
key: params.key,
|
|
16776
|
-
deleted: true,
|
|
16777
|
-
backend: q.backend ?? "local"
|
|
16778
|
-
};
|
|
16779
|
-
}).delete("/:pluginName", async ({ params, query }) => {
|
|
16780
|
-
const q = query;
|
|
16781
|
-
const backend = router.resolve(q.backend);
|
|
16782
|
-
const count = await backend.deleteAll(params.pluginName);
|
|
16783
|
-
return {
|
|
16784
|
-
pluginName: params.pluginName,
|
|
16785
|
-
deletedCount: count,
|
|
16786
|
-
backend: q.backend ?? "local"
|
|
16787
|
-
};
|
|
16788
|
-
});
|
|
16789
|
-
}
|
|
16790
|
-
|
|
16791
|
-
// src/ws/events.ws.ts
|
|
16792
|
-
import { EventEmitter } from "events";
|
|
16793
|
-
var eventBus = new EventEmitter;
|
|
16794
|
-
eventBus.setMaxListeners(100);
|
|
16795
|
-
function createEventsWs() {
|
|
16796
|
-
const clients = new Set;
|
|
16797
|
-
eventBus.on("ws:event", (event) => {
|
|
16798
|
-
const json = JSON.stringify(event);
|
|
16799
|
-
for (const client of clients) {
|
|
16800
|
-
try {
|
|
16801
|
-
client.send(json);
|
|
16802
|
-
} catch {}
|
|
16803
|
-
}
|
|
16804
|
-
});
|
|
16805
|
-
return new Elysia().ws("/ws/events", {
|
|
16806
|
-
open(ws) {
|
|
16807
|
-
const client = {
|
|
16808
|
-
ws,
|
|
16809
|
-
channels: new Set,
|
|
16810
|
-
send: (data) => ws.send(data)
|
|
16811
|
-
};
|
|
16812
|
-
clients.add(client);
|
|
16813
|
-
ws._client = client;
|
|
16814
|
-
logger.info("ws-events", "Client connected", {
|
|
16815
|
-
id: String(ws.id || "unknown")
|
|
16816
|
-
});
|
|
16817
|
-
ws.send(JSON.stringify({
|
|
16818
|
-
type: "connected",
|
|
16819
|
-
timestamp: new Date().toISOString()
|
|
16820
|
-
}));
|
|
16821
|
-
},
|
|
16822
|
-
message(ws, message) {
|
|
16823
|
-
try {
|
|
16824
|
-
const data = typeof message === "string" ? JSON.parse(message) : message;
|
|
16825
|
-
switch (data.type) {
|
|
16826
|
-
case "subscribe": {
|
|
16827
|
-
const client = ws._client;
|
|
16828
|
-
if (client)
|
|
16829
|
-
client.channels.add(data.channel);
|
|
16830
|
-
logger.debug("ws-events", `Subscribed to ${data.channel}`);
|
|
16831
|
-
break;
|
|
16832
|
-
}
|
|
16833
|
-
case "unsubscribe": {
|
|
16834
|
-
const client = ws._client;
|
|
16835
|
-
if (client)
|
|
16836
|
-
client.channels.delete(data.channel);
|
|
16837
|
-
logger.debug("ws-events", `Unsubscribed from ${data.channel}`);
|
|
16838
|
-
break;
|
|
16839
|
-
}
|
|
16840
|
-
case "ping":
|
|
16841
|
-
ws.send(JSON.stringify({
|
|
16842
|
-
type: "pong",
|
|
16843
|
-
timestamp: new Date().toISOString()
|
|
16844
|
-
}));
|
|
16845
|
-
break;
|
|
16846
|
-
default:
|
|
16847
|
-
logger.debug("ws-events", `Unknown message type: ${data.type}`);
|
|
16848
|
-
}
|
|
16849
|
-
} catch {
|
|
16850
|
-
logger.warn("ws-events", "Failed to parse incoming message");
|
|
16851
|
-
}
|
|
16852
|
-
},
|
|
16853
|
-
close(ws) {
|
|
16854
|
-
const client = ws._client;
|
|
16855
|
-
if (client)
|
|
16856
|
-
clients.delete(client);
|
|
16857
|
-
logger.info("ws-events", "Client disconnected");
|
|
16858
|
-
}
|
|
16859
|
-
});
|
|
16860
|
-
}
|
|
16861
|
-
// src/ws/terminal.ws.ts
|
|
16862
|
-
async function proxyToTtyd(port, upstreamPath, method, headers, body) {
|
|
16863
|
-
const fetchHeaders = {};
|
|
16864
|
-
for (const [k2, v] of Object.entries(headers)) {
|
|
16865
|
-
if (v && !["host", "connection"].includes(k2.toLowerCase())) {
|
|
16866
|
-
fetchHeaders[k2] = v;
|
|
16867
|
-
}
|
|
16868
|
-
}
|
|
16869
|
-
const init = { method, headers: fetchHeaders };
|
|
16870
|
-
if (method !== "GET" && method !== "HEAD" && body) {
|
|
16871
|
-
init.body = typeof body === "string" ? body : JSON.stringify(body);
|
|
16872
|
-
}
|
|
16873
|
-
return fetch(`http://127.0.0.1:${port}${upstreamPath}`, init);
|
|
16874
|
-
}
|
|
16875
|
-
async function resolveTtydPort(serviceRegistry, sessionId) {
|
|
16876
|
-
const provider = serviceRegistry.getProvider("session");
|
|
16877
|
-
if (!provider)
|
|
16878
|
-
return null;
|
|
16879
|
-
const terminal = await provider.getTerminalInfo(sessionId);
|
|
16880
|
-
if (!terminal)
|
|
16881
|
-
return null;
|
|
16882
|
-
try {
|
|
16883
|
-
process.kill(terminal.pid, 0);
|
|
16884
|
-
return terminal.port;
|
|
16885
|
-
} catch {
|
|
16886
|
-
return null;
|
|
16887
|
-
}
|
|
16888
|
-
}
|
|
16889
|
-
function createTerminalProxy(serviceRegistry) {
|
|
16890
|
-
return new Elysia().all("/terminal/:sessionId", async ({ params, request, set }) => {
|
|
16891
|
-
const port = await resolveTtydPort(serviceRegistry, params.sessionId);
|
|
16892
|
-
if (!port) {
|
|
16893
|
-
set.status = 502;
|
|
16894
|
-
return { error: "Terminal not running for this session" };
|
|
16895
|
-
}
|
|
16896
|
-
const upstream = await proxyToTtyd(port, "/", request.method, Object.fromEntries(Object.entries(request.headers).map(([k2, v]) => [k2, v])));
|
|
16897
|
-
set.status = upstream.status;
|
|
16898
|
-
const buf = await upstream.arrayBuffer();
|
|
16899
|
-
return new Response(buf, {
|
|
16900
|
-
status: upstream.status,
|
|
16901
|
-
headers: upstream.headers
|
|
16902
|
-
});
|
|
16903
|
-
}).all("/terminal/:sessionId/*", async ({ params, request, set }) => {
|
|
16904
|
-
const sessionId = params.sessionId;
|
|
16905
|
-
const wildcardPath = params["*"] || "";
|
|
16906
|
-
const port = await resolveTtydPort(serviceRegistry, sessionId);
|
|
16907
|
-
if (!port) {
|
|
16908
|
-
set.status = 502;
|
|
16909
|
-
return { error: "Terminal not running for this session" };
|
|
16910
|
-
}
|
|
16911
|
-
const upstream = await proxyToTtyd(port, `/${wildcardPath}`, request.method, Object.fromEntries(Object.entries(request.headers).map(([k2, v]) => [k2, v])));
|
|
16912
|
-
set.status = upstream.status;
|
|
16913
|
-
const buf = await upstream.arrayBuffer();
|
|
16914
|
-
return new Response(buf, {
|
|
16915
|
-
status: upstream.status,
|
|
16916
|
-
headers: upstream.headers
|
|
16917
|
-
});
|
|
16918
|
-
}).ws("/terminal/:sessionId/ws", {
|
|
16919
|
-
open(ws) {
|
|
16920
|
-
const wsData = ws.data;
|
|
16921
|
-
const sessionId = wsData.params?.sessionId;
|
|
16922
|
-
if (!sessionId) {
|
|
16923
|
-
ws.close(1008, "Missing sessionId");
|
|
16924
|
-
return;
|
|
16925
|
-
}
|
|
16926
|
-
logger.info("terminal-ws", `Client connected for session ${sessionId}`);
|
|
16927
|
-
resolveTtydPort(serviceRegistry, sessionId).then((port) => {
|
|
16928
|
-
if (!port) {
|
|
16929
|
-
logger.warn("terminal-ws", `No ttyd running for session ${sessionId}`);
|
|
16930
|
-
ws.close(1011, "No terminal running");
|
|
16931
|
-
return;
|
|
16932
|
-
}
|
|
16933
|
-
const upstreamUrl = `ws://127.0.0.1:${port}/ws`;
|
|
16934
|
-
const upstreamWs = new WebSocket(upstreamUrl, ["tty"]);
|
|
16935
|
-
const upstreamBuffer = [];
|
|
16936
|
-
let upstreamReady = false;
|
|
16937
|
-
upstreamWs.addEventListener("open", () => {
|
|
16938
|
-
upstreamReady = true;
|
|
16939
|
-
logger.info("terminal-ws", `Bridge established \u2192 port ${port}`, {
|
|
16940
|
-
sessionId
|
|
16941
|
-
});
|
|
16942
|
-
for (const msg of upstreamBuffer) {
|
|
16943
|
-
upstreamWs.send(msg);
|
|
16944
|
-
}
|
|
16945
|
-
upstreamBuffer.length = 0;
|
|
16946
|
-
});
|
|
16947
|
-
upstreamWs.addEventListener("message", (event) => {
|
|
16948
|
-
try {
|
|
16949
|
-
ws.send(event.data);
|
|
16950
|
-
} catch {}
|
|
16951
|
-
});
|
|
16952
|
-
upstreamWs.addEventListener("close", (event) => {
|
|
16953
|
-
logger.info("terminal-ws", `Upstream closed (code=${event.code})`, { sessionId });
|
|
16954
|
-
try {
|
|
16955
|
-
ws.close(1000, "Upstream closed");
|
|
16956
|
-
} catch {}
|
|
16957
|
-
});
|
|
16958
|
-
upstreamWs.addEventListener("error", (event) => {
|
|
16959
|
-
logger.error("terminal-ws", `Upstream error`, { sessionId });
|
|
16960
|
-
try {
|
|
16961
|
-
ws.close(1011, "Upstream error");
|
|
16962
|
-
} catch {}
|
|
16963
|
-
});
|
|
16964
|
-
ws._upstream = upstreamWs;
|
|
16965
|
-
ws._upstreamReady = () => upstreamReady;
|
|
16966
|
-
ws._upstreamBuffer = upstreamBuffer;
|
|
16967
|
-
});
|
|
16968
|
-
},
|
|
16969
|
-
message(ws, message) {
|
|
16970
|
-
const upstreamWs = ws._upstream;
|
|
16971
|
-
const isReady = ws._upstreamReady?.();
|
|
16972
|
-
const buffer = ws._upstreamBuffer;
|
|
16973
|
-
if (upstreamWs && isReady && upstreamWs.readyState === WebSocket.OPEN) {
|
|
16974
|
-
upstreamWs.send(message);
|
|
16975
|
-
} else if (buffer) {
|
|
16976
|
-
buffer.push(message);
|
|
16977
|
-
}
|
|
16978
|
-
},
|
|
16979
|
-
close(ws) {
|
|
16980
|
-
const sessionId = ws.data.params?.sessionId;
|
|
16981
|
-
logger.info("terminal-ws", `Client disconnected`, { sessionId });
|
|
16982
|
-
const upstreamWs = ws._upstream;
|
|
16983
|
-
if (upstreamWs && upstreamWs.readyState === WebSocket.OPEN) {
|
|
16984
|
-
upstreamWs.close(1000, "Client disconnected");
|
|
16985
|
-
}
|
|
16986
|
-
}
|
|
16987
|
-
});
|
|
16988
|
-
}
|
|
16989
|
-
// src/ws/logs.ws.ts
|
|
16990
|
-
var LEVEL_PRIORITY = {
|
|
16991
|
-
debug: 0,
|
|
16992
|
-
info: 1,
|
|
16993
|
-
warn: 2,
|
|
16994
|
-
error: 3
|
|
16995
|
-
};
|
|
16996
|
-
function createLogRoutes() {
|
|
16997
|
-
return new Elysia({ prefix: "/api/logs" }).get("/", async ({ query }) => {
|
|
16998
|
-
const q = query;
|
|
16999
|
-
const entries = await logger.readHistory({
|
|
17000
|
-
level: q.level,
|
|
17001
|
-
source: q.source,
|
|
17002
|
-
from: q.from,
|
|
17003
|
-
to: q.to,
|
|
17004
|
-
limit: q.limit ? parseInt(q.limit, 10) : 200,
|
|
17005
|
-
offset: q.offset ? parseInt(q.offset, 10) : 0
|
|
17006
|
-
});
|
|
17007
|
-
return { entries, count: entries.length };
|
|
17008
|
-
}).get("/stream", ({ query, set }) => {
|
|
17009
|
-
const q = query;
|
|
17010
|
-
const minLevel = q.level ?? "info";
|
|
17011
|
-
const sourceFilter = q.source ?? "";
|
|
17012
|
-
set.headers["Content-Type"] = "text/event-stream";
|
|
17013
|
-
set.headers["Cache-Control"] = "no-cache";
|
|
17014
|
-
set.headers["Connection"] = "keep-alive";
|
|
17015
|
-
set.headers["X-Accel-Buffering"] = "no";
|
|
17016
|
-
return new ReadableStream({
|
|
17017
|
-
start(controller) {
|
|
17018
|
-
const encoder2 = new TextEncoder;
|
|
17019
|
-
controller.enqueue(encoder2.encode(`: connected
|
|
17020
|
-
|
|
17021
|
-
`));
|
|
17022
|
-
const heartbeat = setInterval(() => {
|
|
17023
|
-
try {
|
|
17024
|
-
controller.enqueue(encoder2.encode(`: heartbeat
|
|
17025
|
-
|
|
17026
|
-
`));
|
|
17027
|
-
} catch {
|
|
17028
|
-
cleanup();
|
|
17029
|
-
}
|
|
17030
|
-
}, 15000);
|
|
17031
|
-
const onLog = (entry) => {
|
|
17032
|
-
if (LEVEL_PRIORITY[entry.level] < LEVEL_PRIORITY[minLevel])
|
|
17033
|
-
return;
|
|
17034
|
-
if (sourceFilter && !entry.source.includes(sourceFilter))
|
|
17035
|
-
return;
|
|
17036
|
-
try {
|
|
17037
|
-
controller.enqueue(encoder2.encode(`event: log
|
|
17038
|
-
data: ${JSON.stringify(entry)}
|
|
17039
|
-
|
|
17040
|
-
`));
|
|
17041
|
-
} catch {
|
|
17042
|
-
cleanup();
|
|
17043
|
-
}
|
|
17044
|
-
};
|
|
17045
|
-
const cleanup = () => {
|
|
17046
|
-
clearInterval(heartbeat);
|
|
17047
|
-
logger.removeListener("log", onLog);
|
|
17048
|
-
try {
|
|
17049
|
-
controller.close();
|
|
17050
|
-
} catch {}
|
|
17051
|
-
};
|
|
17052
|
-
logger.on("log", onLog);
|
|
17053
|
-
return () => cleanup();
|
|
17054
|
-
},
|
|
17055
|
-
cancel() {}
|
|
17056
|
-
});
|
|
17057
|
-
});
|
|
17058
|
-
}
|
|
17059
|
-
function createLogWs() {
|
|
17060
|
-
return new Elysia().ws("/ws/logs", {
|
|
17061
|
-
open(ws) {
|
|
17062
|
-
logger.info("ws-logs", "Log stream client connected");
|
|
17063
|
-
const filters = {
|
|
17064
|
-
minLevel: "info",
|
|
17065
|
-
source: ""
|
|
17066
|
-
};
|
|
17067
|
-
ws._filters = filters;
|
|
17068
|
-
const onLog = (entry) => {
|
|
17069
|
-
const f = ws._filters;
|
|
17070
|
-
if (LEVEL_PRIORITY[entry.level] < LEVEL_PRIORITY[f.minLevel])
|
|
17071
|
-
return;
|
|
17072
|
-
if (f.source && !entry.source.includes(f.source))
|
|
17073
|
-
return;
|
|
17074
|
-
try {
|
|
17075
|
-
ws.send(JSON.stringify(entry));
|
|
17076
|
-
} catch {}
|
|
17077
|
-
};
|
|
17078
|
-
logger.on("log", onLog);
|
|
17079
|
-
ws._logHandler = onLog;
|
|
17080
|
-
ws.send(JSON.stringify({
|
|
17081
|
-
type: "connected",
|
|
17082
|
-
timestamp: new Date().toISOString()
|
|
17083
|
-
}));
|
|
17084
|
-
},
|
|
17085
|
-
message(ws, message) {
|
|
17086
|
-
try {
|
|
17087
|
-
const data = typeof message === "string" ? JSON.parse(message) : message;
|
|
17088
|
-
if (data.type === "filter") {
|
|
17089
|
-
const filters = ws._filters;
|
|
17090
|
-
if (data.level)
|
|
17091
|
-
filters.minLevel = data.level;
|
|
17092
|
-
if (data.source !== undefined)
|
|
17093
|
-
filters.source = data.source;
|
|
17094
|
-
ws.send(JSON.stringify({ type: "filter-updated", filters }));
|
|
17095
|
-
}
|
|
17096
|
-
} catch {}
|
|
17097
|
-
},
|
|
17098
|
-
close(ws) {
|
|
17099
|
-
const handler = ws._logHandler;
|
|
17100
|
-
if (handler) {
|
|
17101
|
-
logger.removeListener("log", handler);
|
|
17102
|
-
}
|
|
17103
|
-
logger.info("ws-logs", "Log stream client disconnected");
|
|
17104
|
-
}
|
|
17105
|
-
});
|
|
17106
|
-
}
|
|
17107
|
-
// src/app.ts
|
|
17108
|
-
async function createApp(options) {
|
|
17109
|
-
const { port, host, dbPath, apiKey, logLevel, corsOrigin } = options;
|
|
17110
|
-
const db = new AgentDatabase(dbPath);
|
|
17111
|
-
const serviceRegistry = new ServiceRegistry;
|
|
17112
|
-
const pluginManager = new PluginManager;
|
|
17113
|
-
try {
|
|
17114
|
-
const gwGlobalUrl = db.getConfig("gateway-auth:globalGatewayUrl");
|
|
17115
|
-
const gwClientId = db.getConfig("gateway-auth:clientId");
|
|
17116
|
-
const gwClientSecret = db.getConfig("gateway-auth:clientSecret");
|
|
17117
|
-
if (gwGlobalUrl && gwClientId && gwClientSecret) {
|
|
17118
|
-
const gwWorkspaceUrl = db.getConfig("gateway-auth:workspaceGatewayUrl");
|
|
17119
|
-
gatewayClient.configure({
|
|
17120
|
-
globalGatewayUrl: gwGlobalUrl,
|
|
17121
|
-
workspaceGatewayUrl: gwWorkspaceUrl,
|
|
17122
|
-
clientId: gwClientId,
|
|
17123
|
-
clientSecret: gwClientSecret
|
|
17124
|
-
});
|
|
17125
|
-
logger.info("app", "Restored gateway client config from database");
|
|
17126
|
-
}
|
|
17127
|
-
} catch (err) {
|
|
17128
|
-
logger.warn("app", "Failed to restore gateway client config", {
|
|
17129
|
-
error: String(err)
|
|
17130
|
-
});
|
|
17131
|
-
}
|
|
17132
|
-
const app = new Elysia().use(cors({
|
|
17133
|
-
origin: corsOrigin || true,
|
|
17134
|
-
credentials: true
|
|
17135
|
-
})).use(createAuthPlugin()).onAfterHandle(({ request, set }) => {
|
|
17136
|
-
const url = new URL(request.url).pathname;
|
|
17137
|
-
const isNoisy = url === "/health" || url.startsWith("/api/logs/stream");
|
|
17138
|
-
const status2 = typeof set.status === "number" ? set.status : 200;
|
|
17139
|
-
const level = isNoisy ? "debug" : status2 >= 400 ? "warn" : "info";
|
|
17140
|
-
logger[level]("http", `${request.method} ${url} \u2192 ${status2}`, {
|
|
17141
|
-
method: request.method,
|
|
17142
|
-
path: url,
|
|
17143
|
-
statusCode: status2
|
|
17144
|
-
});
|
|
17145
|
-
}).use(createHealthRoutes(serviceRegistry)).use(createAgentRoutes(db, serviceRegistry)).use(createSessionRoutes(serviceRegistry)).use(createTunnelRoutes(serviceRegistry)).use(createTaskRoutes(db)).use(createConfigRoutes(db)).use(createGitRoutes(db)).use(createFileRoutes()).use(createBookmarkRoutes(db, serviceRegistry)).use(createNotificationRoutes(db)).use(createProjectRoutes()).use(createPluginRoutes(pluginManager)).use(createPluginStateRoutes(db)).use(createLogRoutes()).use(createEventsWs()).use(createLogWs()).use(createTerminalProxy(serviceRegistry));
|
|
17146
|
-
try {
|
|
17147
|
-
await pluginManager.loadAll();
|
|
17148
|
-
logger.info("app", `Loaded ${pluginManager.getPluginDetails().length} plugin(s)`);
|
|
17149
|
-
} catch (err) {
|
|
17150
|
-
logger.warn("app", "Failed to load plugins", { error: String(err) });
|
|
17151
|
-
}
|
|
17152
|
-
return {
|
|
17153
|
-
app,
|
|
17154
|
-
db,
|
|
17155
|
-
serviceRegistry,
|
|
17156
|
-
pluginManager,
|
|
17157
|
-
async start() {
|
|
17158
|
-
app.listen({ port, hostname: host });
|
|
17159
|
-
logger.info("app", `Agent server listening on ${host}:${port}`);
|
|
17160
|
-
return app;
|
|
17161
|
-
},
|
|
17162
|
-
async stop() {
|
|
17163
|
-
try {
|
|
17164
|
-
await pluginManager.dispatchServerStop();
|
|
17165
|
-
} catch (err) {
|
|
17166
|
-
logger.warn("app", "Error stopping plugins", { error: String(err) });
|
|
17167
|
-
}
|
|
17168
|
-
db.close();
|
|
17169
|
-
logger.info("app", "Agent server stopped");
|
|
17170
|
-
}
|
|
17171
|
-
};
|
|
17172
|
-
}
|
|
17173
|
-
|
|
17174
|
-
export { createApp };
|
|
13981
|
+
export { t, Elysia };
|
|
17175
13982
|
|
|
17176
|
-
//# debugId=
|
|
17177
|
-
//# sourceMappingURL=
|
|
13983
|
+
//# debugId=12770D641240622664756E2164756E21
|
|
13984
|
+
//# sourceMappingURL=index-wr0mkm57.js.map
|