remcodex 0.1.0-beta.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +331 -0
- package/dist/server/src/app.js +186 -0
- package/dist/server/src/cli.js +270 -0
- package/dist/server/src/controllers/codex-options.controller.js +199 -0
- package/dist/server/src/controllers/message.controller.js +21 -0
- package/dist/server/src/controllers/project.controller.js +44 -0
- package/dist/server/src/controllers/session.controller.js +175 -0
- package/dist/server/src/db/client.js +10 -0
- package/dist/server/src/db/migrations.js +32 -0
- package/dist/server/src/gateways/ws.gateway.js +60 -0
- package/dist/server/src/services/codex-app-server-runner.js +363 -0
- package/dist/server/src/services/codex-exec-runner.js +147 -0
- package/dist/server/src/services/codex-rollout-sync.js +977 -0
- package/dist/server/src/services/codex-runner.js +11 -0
- package/dist/server/src/services/codex-stream-events.js +478 -0
- package/dist/server/src/services/event-store.js +328 -0
- package/dist/server/src/services/project-manager.js +130 -0
- package/dist/server/src/services/pty-runner.js +72 -0
- package/dist/server/src/services/session-manager.js +1586 -0
- package/dist/server/src/services/session-timeline-service.js +181 -0
- package/dist/server/src/types/codex-launch.js +2 -0
- package/dist/server/src/types/models.js +37 -0
- package/dist/server/src/utils/ansi.js +143 -0
- package/dist/server/src/utils/codex-launch.js +102 -0
- package/dist/server/src/utils/codex-quota.js +179 -0
- package/dist/server/src/utils/codex-status.js +163 -0
- package/dist/server/src/utils/codex-ui-options.js +114 -0
- package/dist/server/src/utils/command.js +46 -0
- package/dist/server/src/utils/errors.js +16 -0
- package/dist/server/src/utils/ids.js +7 -0
- package/dist/server/src/utils/node-pty.js +29 -0
- package/package.json +36 -0
- package/scripts/fix-node-pty-helper.js +36 -0
- package/web/api.js +175 -0
- package/web/app.js +8082 -0
- package/web/components/composer.js +627 -0
- package/web/components/session-workbench.js +173 -0
- package/web/i18n/index.js +171 -0
- package/web/i18n/locales/de.js +50 -0
- package/web/i18n/locales/en.js +320 -0
- package/web/i18n/locales/es.js +50 -0
- package/web/i18n/locales/fr.js +50 -0
- package/web/i18n/locales/ja.js +50 -0
- package/web/i18n/locales/ko.js +50 -0
- package/web/i18n/locales/pt-BR.js +50 -0
- package/web/i18n/locales/ru.js +50 -0
- package/web/i18n/locales/zh-CN.js +320 -0
- package/web/i18n/locales/zh-Hant.js +53 -0
- package/web/index.html +23 -0
- package/web/message-rich-text.js +218 -0
- package/web/session-command-activity.js +980 -0
- package/web/session-event-adapter.js +826 -0
- package/web/session-timeline-reducer.js +728 -0
- package/web/session-timeline-renderer.js +656 -0
- package/web/session-ws.js +31 -0
- package/web/styles.css +5665 -0
- package/web/vendor/markdown-it.js +6969 -0
|
@@ -0,0 +1,328 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.EventStore = void 0;
|
|
4
|
+
const node_events_1 = require("node:events");
|
|
5
|
+
const ids_1 = require("../utils/ids");
|
|
6
|
+
class EventStore {
|
|
7
|
+
db;
|
|
8
|
+
emitter = new node_events_1.EventEmitter();
|
|
9
|
+
latestQuotaCache = new Map();
|
|
10
|
+
constructor(db) {
|
|
11
|
+
this.db = db;
|
|
12
|
+
}
|
|
13
|
+
append(sessionId, input) {
|
|
14
|
+
const seq = this.nextSeq(sessionId);
|
|
15
|
+
const id = input.id?.trim() || (0, ids_1.createId)("evt");
|
|
16
|
+
const timestamp = input.timestamp?.trim() || new Date().toISOString();
|
|
17
|
+
const payloadJson = JSON.stringify(input.payload ?? {});
|
|
18
|
+
const stream = this.normalizeStream(input.stream);
|
|
19
|
+
this.db
|
|
20
|
+
.prepare(`
|
|
21
|
+
INSERT INTO session_events (
|
|
22
|
+
id,
|
|
23
|
+
session_id,
|
|
24
|
+
turn_id,
|
|
25
|
+
seq,
|
|
26
|
+
event_type,
|
|
27
|
+
message_id,
|
|
28
|
+
call_id,
|
|
29
|
+
request_id,
|
|
30
|
+
phase,
|
|
31
|
+
stream,
|
|
32
|
+
payload_json,
|
|
33
|
+
created_at
|
|
34
|
+
)
|
|
35
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
|
36
|
+
`)
|
|
37
|
+
.run(id, sessionId, input.turnId, seq, input.type, input.messageId, input.callId, input.requestId, input.phase, stream, payloadJson, timestamp);
|
|
38
|
+
const row = this.db
|
|
39
|
+
.prepare(`
|
|
40
|
+
SELECT
|
|
41
|
+
id,
|
|
42
|
+
session_id,
|
|
43
|
+
turn_id,
|
|
44
|
+
seq,
|
|
45
|
+
event_type,
|
|
46
|
+
message_id,
|
|
47
|
+
call_id,
|
|
48
|
+
request_id,
|
|
49
|
+
phase,
|
|
50
|
+
stream,
|
|
51
|
+
payload_json,
|
|
52
|
+
created_at
|
|
53
|
+
FROM session_events
|
|
54
|
+
WHERE id = ?
|
|
55
|
+
`)
|
|
56
|
+
.get(id);
|
|
57
|
+
const event = this.toPayload(row);
|
|
58
|
+
this.captureLatestQuota(sessionId, event);
|
|
59
|
+
this.emitter.emit(this.channel(sessionId), event);
|
|
60
|
+
return event;
|
|
61
|
+
}
|
|
62
|
+
list(sessionId, options = {}) {
|
|
63
|
+
const safeLimit = Math.max(1, Math.min(options.limit ?? 200, 200));
|
|
64
|
+
const after = Math.max(0, options.after ?? 0);
|
|
65
|
+
const before = Math.max(0, options.before ?? 0);
|
|
66
|
+
if (before > 0) {
|
|
67
|
+
const rows = this.db
|
|
68
|
+
.prepare(`
|
|
69
|
+
SELECT
|
|
70
|
+
id,
|
|
71
|
+
session_id,
|
|
72
|
+
turn_id,
|
|
73
|
+
seq,
|
|
74
|
+
event_type,
|
|
75
|
+
message_id,
|
|
76
|
+
call_id,
|
|
77
|
+
request_id,
|
|
78
|
+
phase,
|
|
79
|
+
stream,
|
|
80
|
+
payload_json,
|
|
81
|
+
created_at
|
|
82
|
+
FROM session_events
|
|
83
|
+
WHERE session_id = ? AND seq < ?
|
|
84
|
+
ORDER BY seq DESC
|
|
85
|
+
LIMIT ?
|
|
86
|
+
`)
|
|
87
|
+
.all(sessionId, before, safeLimit + 1);
|
|
88
|
+
const hasMoreBefore = rows.length > safeLimit;
|
|
89
|
+
const pageRows = rows.slice(0, safeLimit).reverse();
|
|
90
|
+
return {
|
|
91
|
+
items: pageRows.map((row) => this.toPayload(row)),
|
|
92
|
+
nextCursor: pageRows.length > 0 ? pageRows[pageRows.length - 1].seq : after,
|
|
93
|
+
beforeCursor: pageRows.length > 0 ? pageRows[0].seq : before,
|
|
94
|
+
hasMoreBefore,
|
|
95
|
+
};
|
|
96
|
+
}
|
|
97
|
+
if (after > 0) {
|
|
98
|
+
const rows = this.db
|
|
99
|
+
.prepare(`
|
|
100
|
+
SELECT
|
|
101
|
+
id,
|
|
102
|
+
session_id,
|
|
103
|
+
turn_id,
|
|
104
|
+
seq,
|
|
105
|
+
event_type,
|
|
106
|
+
message_id,
|
|
107
|
+
call_id,
|
|
108
|
+
request_id,
|
|
109
|
+
phase,
|
|
110
|
+
stream,
|
|
111
|
+
payload_json,
|
|
112
|
+
created_at
|
|
113
|
+
FROM session_events
|
|
114
|
+
WHERE session_id = ? AND seq > ?
|
|
115
|
+
ORDER BY seq ASC
|
|
116
|
+
LIMIT ?
|
|
117
|
+
`)
|
|
118
|
+
.all(sessionId, after, safeLimit);
|
|
119
|
+
return {
|
|
120
|
+
items: rows.map((row) => this.toPayload(row)),
|
|
121
|
+
nextCursor: rows.length > 0 ? rows[rows.length - 1].seq : after,
|
|
122
|
+
beforeCursor: rows.length > 0 ? rows[0].seq : 0,
|
|
123
|
+
hasMoreBefore: rows.length > 0 ? rows[0].seq > 1 : false,
|
|
124
|
+
};
|
|
125
|
+
}
|
|
126
|
+
const rows = this.db
|
|
127
|
+
.prepare(`
|
|
128
|
+
SELECT
|
|
129
|
+
id,
|
|
130
|
+
session_id,
|
|
131
|
+
turn_id,
|
|
132
|
+
seq,
|
|
133
|
+
event_type,
|
|
134
|
+
message_id,
|
|
135
|
+
call_id,
|
|
136
|
+
request_id,
|
|
137
|
+
phase,
|
|
138
|
+
stream,
|
|
139
|
+
payload_json,
|
|
140
|
+
created_at
|
|
141
|
+
FROM session_events
|
|
142
|
+
WHERE session_id = ?
|
|
143
|
+
ORDER BY seq DESC
|
|
144
|
+
LIMIT ?
|
|
145
|
+
`)
|
|
146
|
+
.all(sessionId, safeLimit + 1);
|
|
147
|
+
const hasMoreBefore = rows.length > safeLimit;
|
|
148
|
+
const pageRows = rows.slice(0, safeLimit).reverse();
|
|
149
|
+
return {
|
|
150
|
+
items: pageRows.map((row) => this.toPayload(row)),
|
|
151
|
+
nextCursor: pageRows.length > 0 ? pageRows[pageRows.length - 1].seq : after,
|
|
152
|
+
beforeCursor: pageRows.length > 0 ? pageRows[0].seq : 0,
|
|
153
|
+
hasMoreBefore,
|
|
154
|
+
};
|
|
155
|
+
}
|
|
156
|
+
listAll(sessionId) {
|
|
157
|
+
const rows = this.db
|
|
158
|
+
.prepare(`
|
|
159
|
+
SELECT
|
|
160
|
+
id,
|
|
161
|
+
session_id,
|
|
162
|
+
turn_id,
|
|
163
|
+
seq,
|
|
164
|
+
event_type,
|
|
165
|
+
message_id,
|
|
166
|
+
call_id,
|
|
167
|
+
request_id,
|
|
168
|
+
phase,
|
|
169
|
+
stream,
|
|
170
|
+
payload_json,
|
|
171
|
+
created_at
|
|
172
|
+
FROM session_events
|
|
173
|
+
WHERE session_id = ?
|
|
174
|
+
ORDER BY seq ASC
|
|
175
|
+
`)
|
|
176
|
+
.all(sessionId);
|
|
177
|
+
return rows.map((row) => this.toPayload(row));
|
|
178
|
+
}
|
|
179
|
+
latestQuota(sessionId) {
|
|
180
|
+
const cached = this.latestQuotaCache.get(sessionId);
|
|
181
|
+
if (cached) {
|
|
182
|
+
return cached;
|
|
183
|
+
}
|
|
184
|
+
const rows = this.db
|
|
185
|
+
.prepare(`
|
|
186
|
+
SELECT payload_json
|
|
187
|
+
FROM session_events
|
|
188
|
+
WHERE session_id = ?
|
|
189
|
+
AND event_type = 'token_count'
|
|
190
|
+
ORDER BY seq DESC
|
|
191
|
+
`)
|
|
192
|
+
.all(sessionId);
|
|
193
|
+
for (const row of rows) {
|
|
194
|
+
const payload = this.tryParse(row.payload_json ?? null);
|
|
195
|
+
if (payload && this.hasUsableQuota(payload)) {
|
|
196
|
+
this.latestQuotaCache.set(sessionId, payload);
|
|
197
|
+
return payload;
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
return null;
|
|
201
|
+
}
|
|
202
|
+
latestPendingApproval(sessionId) {
|
|
203
|
+
const rows = this.db
|
|
204
|
+
.prepare(`
|
|
205
|
+
SELECT event_type, request_id, payload_json, seq
|
|
206
|
+
FROM session_events
|
|
207
|
+
WHERE session_id = ?
|
|
208
|
+
AND event_type IN ('approval.requested', 'approval.resolved')
|
|
209
|
+
ORDER BY seq ASC
|
|
210
|
+
`)
|
|
211
|
+
.all(sessionId);
|
|
212
|
+
if (rows.length === 0) {
|
|
213
|
+
return null;
|
|
214
|
+
}
|
|
215
|
+
const pending = new Map();
|
|
216
|
+
for (const row of rows) {
|
|
217
|
+
if (row.event_type === "approval.requested") {
|
|
218
|
+
const payload = this.tryParse(row.payload_json);
|
|
219
|
+
const requestId = row.request_id || payload?.requestId || "";
|
|
220
|
+
if (requestId) {
|
|
221
|
+
pending.set(requestId, {
|
|
222
|
+
...payload,
|
|
223
|
+
requestId,
|
|
224
|
+
});
|
|
225
|
+
}
|
|
226
|
+
continue;
|
|
227
|
+
}
|
|
228
|
+
const payload = this.tryParse(row.payload_json);
|
|
229
|
+
const requestId = row.request_id || payload?.requestId || "";
|
|
230
|
+
if (requestId) {
|
|
231
|
+
pending.delete(requestId);
|
|
232
|
+
}
|
|
233
|
+
}
|
|
234
|
+
const unresolved = [...pending.values()].sort((a, b) => String(a.createdAt || "").localeCompare(String(b.createdAt || "")));
|
|
235
|
+
return unresolved[0] ?? null;
|
|
236
|
+
}
|
|
237
|
+
subscribe(sessionId, listener) {
|
|
238
|
+
const channel = this.channel(sessionId);
|
|
239
|
+
this.emitter.on(channel, listener);
|
|
240
|
+
return () => {
|
|
241
|
+
this.emitter.off(channel, listener);
|
|
242
|
+
};
|
|
243
|
+
}
|
|
244
|
+
nextSeq(sessionId) {
|
|
245
|
+
const row = this.db
|
|
246
|
+
.prepare(`
|
|
247
|
+
SELECT COALESCE(MAX(seq), 0) AS current_seq
|
|
248
|
+
FROM session_events
|
|
249
|
+
WHERE session_id = ?
|
|
250
|
+
`)
|
|
251
|
+
.get(sessionId);
|
|
252
|
+
return row.current_seq + 1;
|
|
253
|
+
}
|
|
254
|
+
toPayload(row) {
|
|
255
|
+
return {
|
|
256
|
+
id: row.id,
|
|
257
|
+
sessionId: row.session_id,
|
|
258
|
+
type: row.event_type,
|
|
259
|
+
seq: row.seq,
|
|
260
|
+
timestamp: row.created_at,
|
|
261
|
+
turnId: row.turn_id,
|
|
262
|
+
messageId: row.message_id,
|
|
263
|
+
callId: row.call_id,
|
|
264
|
+
requestId: row.request_id,
|
|
265
|
+
phase: row.phase,
|
|
266
|
+
stream: this.normalizeStream(row.stream),
|
|
267
|
+
payload: this.tryParse(row.payload_json) ?? {},
|
|
268
|
+
};
|
|
269
|
+
}
|
|
270
|
+
tryParse(raw) {
|
|
271
|
+
if (!raw) {
|
|
272
|
+
return null;
|
|
273
|
+
}
|
|
274
|
+
try {
|
|
275
|
+
return JSON.parse(raw);
|
|
276
|
+
}
|
|
277
|
+
catch {
|
|
278
|
+
return null;
|
|
279
|
+
}
|
|
280
|
+
}
|
|
281
|
+
normalizeStream(stream) {
|
|
282
|
+
switch (stream) {
|
|
283
|
+
case "stdout":
|
|
284
|
+
case "stderr":
|
|
285
|
+
return stream;
|
|
286
|
+
default:
|
|
287
|
+
return null;
|
|
288
|
+
}
|
|
289
|
+
}
|
|
290
|
+
channel(sessionId) {
|
|
291
|
+
return `session:${sessionId}`;
|
|
292
|
+
}
|
|
293
|
+
captureLatestQuota(sessionId, event) {
|
|
294
|
+
if (event.type !== "token_count") {
|
|
295
|
+
return;
|
|
296
|
+
}
|
|
297
|
+
const payload = event.payload;
|
|
298
|
+
if (payload && this.hasUsableQuota(payload)) {
|
|
299
|
+
this.latestQuotaCache.set(sessionId, payload);
|
|
300
|
+
}
|
|
301
|
+
}
|
|
302
|
+
hasUsableQuota(payload) {
|
|
303
|
+
const rateLimits = payload.rateLimits && typeof payload.rateLimits === "object"
|
|
304
|
+
? payload.rateLimits
|
|
305
|
+
: {};
|
|
306
|
+
const primary = rateLimits.primary && typeof rateLimits.primary === "object"
|
|
307
|
+
? rateLimits.primary
|
|
308
|
+
: {};
|
|
309
|
+
const secondary = rateLimits.secondary && typeof rateLimits.secondary === "object"
|
|
310
|
+
? rateLimits.secondary
|
|
311
|
+
: {};
|
|
312
|
+
return (this.readQuotaField(primary.used_percent) != null ||
|
|
313
|
+
this.readQuotaField(primary.resets_at) != null ||
|
|
314
|
+
this.readQuotaField(secondary.used_percent) != null ||
|
|
315
|
+
this.readQuotaField(secondary.resets_at) != null);
|
|
316
|
+
}
|
|
317
|
+
readQuotaField(input) {
|
|
318
|
+
if (typeof input === "number" && Number.isFinite(input)) {
|
|
319
|
+
return input;
|
|
320
|
+
}
|
|
321
|
+
if (typeof input === "string" && input.trim()) {
|
|
322
|
+
const parsed = Number.parseFloat(input);
|
|
323
|
+
return Number.isFinite(parsed) ? parsed : null;
|
|
324
|
+
}
|
|
325
|
+
return null;
|
|
326
|
+
}
|
|
327
|
+
}
|
|
328
|
+
exports.EventStore = EventStore;
|
|
@@ -0,0 +1,130 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
3
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
|
+
};
|
|
5
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
+
exports.ProjectManager = void 0;
|
|
7
|
+
const node_fs_1 = require("node:fs");
|
|
8
|
+
const node_os_1 = require("node:os");
|
|
9
|
+
const node_path_1 = __importDefault(require("node:path"));
|
|
10
|
+
const errors_1 = require("../utils/errors");
|
|
11
|
+
const ids_1 = require("../utils/ids");
|
|
12
|
+
class ProjectManager {
|
|
13
|
+
db;
|
|
14
|
+
allowedRoots;
|
|
15
|
+
constructor(db, projectRootsEnv, repoRoot) {
|
|
16
|
+
this.db = db;
|
|
17
|
+
this.allowedRoots = (projectRootsEnv ?? (0, node_os_1.homedir)())
|
|
18
|
+
.split(",")
|
|
19
|
+
.map((item) => item.trim())
|
|
20
|
+
.filter(Boolean)
|
|
21
|
+
.map((item) => node_path_1.default.resolve(item));
|
|
22
|
+
}
|
|
23
|
+
listProjects() {
|
|
24
|
+
return this.db
|
|
25
|
+
.prepare(`
|
|
26
|
+
SELECT id, name, path, created_at
|
|
27
|
+
FROM projects
|
|
28
|
+
ORDER BY created_at DESC
|
|
29
|
+
`)
|
|
30
|
+
.all();
|
|
31
|
+
}
|
|
32
|
+
getProject(projectId) {
|
|
33
|
+
return (this.db
|
|
34
|
+
.prepare(`
|
|
35
|
+
SELECT id, name, path, created_at
|
|
36
|
+
FROM projects
|
|
37
|
+
WHERE id = ?
|
|
38
|
+
`)
|
|
39
|
+
.get(projectId) ?? null);
|
|
40
|
+
}
|
|
41
|
+
createProject(input) {
|
|
42
|
+
const name = input.name.trim();
|
|
43
|
+
const resolvedPath = node_path_1.default.resolve(input.path.trim());
|
|
44
|
+
if (!name) {
|
|
45
|
+
throw new errors_1.AppError(400, "Project name is required.");
|
|
46
|
+
}
|
|
47
|
+
if (!input.path.trim()) {
|
|
48
|
+
throw new errors_1.AppError(400, "Project path is required.");
|
|
49
|
+
}
|
|
50
|
+
if (!(0, node_fs_1.existsSync)(resolvedPath) && input.createMissing) {
|
|
51
|
+
(0, node_fs_1.mkdirSync)(resolvedPath, { recursive: true });
|
|
52
|
+
}
|
|
53
|
+
if (!(0, node_fs_1.existsSync)(resolvedPath) || !(0, node_fs_1.statSync)(resolvedPath).isDirectory()) {
|
|
54
|
+
throw new errors_1.AppError(400, "Project path does not exist or is not a directory.");
|
|
55
|
+
}
|
|
56
|
+
if (!this.isAllowed(resolvedPath)) {
|
|
57
|
+
throw new errors_1.AppError(400, `Project path is outside allowed roots: ${this.allowedRoots.join(", ")}`);
|
|
58
|
+
}
|
|
59
|
+
const duplicated = this.db
|
|
60
|
+
.prepare(`
|
|
61
|
+
SELECT id
|
|
62
|
+
FROM projects
|
|
63
|
+
WHERE path = ?
|
|
64
|
+
`)
|
|
65
|
+
.get(resolvedPath);
|
|
66
|
+
if (duplicated) {
|
|
67
|
+
throw new errors_1.AppError(409, "Project path is already registered.");
|
|
68
|
+
}
|
|
69
|
+
const project = {
|
|
70
|
+
id: (0, ids_1.createId)("proj"),
|
|
71
|
+
name,
|
|
72
|
+
path: resolvedPath,
|
|
73
|
+
created_at: new Date().toISOString(),
|
|
74
|
+
};
|
|
75
|
+
this.db
|
|
76
|
+
.prepare(`
|
|
77
|
+
INSERT INTO projects (id, name, path, created_at)
|
|
78
|
+
VALUES (?, ?, ?, ?)
|
|
79
|
+
`)
|
|
80
|
+
.run(project.id, project.name, project.path, project.created_at);
|
|
81
|
+
return project;
|
|
82
|
+
}
|
|
83
|
+
listAllowedRoots() {
|
|
84
|
+
return [...this.allowedRoots];
|
|
85
|
+
}
|
|
86
|
+
browseDirectories(targetPath) {
|
|
87
|
+
const rawTarget = String(targetPath || "").trim();
|
|
88
|
+
if (!rawTarget) {
|
|
89
|
+
return {
|
|
90
|
+
currentPath: null,
|
|
91
|
+
parentPath: null,
|
|
92
|
+
items: this.allowedRoots.map((root) => ({
|
|
93
|
+
name: node_path_1.default.basename(root) || root,
|
|
94
|
+
path: root,
|
|
95
|
+
})),
|
|
96
|
+
};
|
|
97
|
+
}
|
|
98
|
+
const resolvedPath = node_path_1.default.resolve(rawTarget);
|
|
99
|
+
const root = this.findAllowedRoot(resolvedPath);
|
|
100
|
+
if (!root) {
|
|
101
|
+
throw new errors_1.AppError(400, `Project path is outside allowed roots: ${this.allowedRoots.join(", ")}`);
|
|
102
|
+
}
|
|
103
|
+
if (!(0, node_fs_1.existsSync)(resolvedPath) || !(0, node_fs_1.statSync)(resolvedPath).isDirectory()) {
|
|
104
|
+
throw new errors_1.AppError(400, "Project path does not exist or is not a directory.");
|
|
105
|
+
}
|
|
106
|
+
const items = (0, node_fs_1.readdirSync)(resolvedPath, { withFileTypes: true })
|
|
107
|
+
.filter((entry) => entry.isDirectory())
|
|
108
|
+
.map((entry) => ({
|
|
109
|
+
name: entry.name,
|
|
110
|
+
path: node_path_1.default.join(resolvedPath, entry.name),
|
|
111
|
+
}))
|
|
112
|
+
.sort((left, right) => left.name.localeCompare(right.name, undefined, { sensitivity: "base" }));
|
|
113
|
+
const parentPath = resolvedPath === root ? null : node_path_1.default.dirname(resolvedPath);
|
|
114
|
+
return {
|
|
115
|
+
currentPath: resolvedPath,
|
|
116
|
+
parentPath,
|
|
117
|
+
items,
|
|
118
|
+
};
|
|
119
|
+
}
|
|
120
|
+
isAllowed(targetPath) {
|
|
121
|
+
return Boolean(this.findAllowedRoot(targetPath));
|
|
122
|
+
}
|
|
123
|
+
findAllowedRoot(targetPath) {
|
|
124
|
+
return (this.allowedRoots.find((root) => {
|
|
125
|
+
const relative = node_path_1.default.relative(root, targetPath);
|
|
126
|
+
return relative === "" || (!relative.startsWith("..") && !node_path_1.default.isAbsolute(relative));
|
|
127
|
+
}) ?? null);
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
exports.ProjectManager = ProjectManager;
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.PtyRunner = void 0;
|
|
4
|
+
const node_pty_1 = require("node-pty");
|
|
5
|
+
const node_pty_2 = require("../utils/node-pty");
|
|
6
|
+
class PtyRunner {
|
|
7
|
+
command;
|
|
8
|
+
cwd;
|
|
9
|
+
process = null;
|
|
10
|
+
dataListeners = new Set();
|
|
11
|
+
exitListeners = new Set();
|
|
12
|
+
stopTimer = null;
|
|
13
|
+
constructor(command, cwd) {
|
|
14
|
+
this.command = command;
|
|
15
|
+
this.cwd = cwd;
|
|
16
|
+
}
|
|
17
|
+
start() {
|
|
18
|
+
(0, node_pty_2.ensureNodePtyHelperExecutable)();
|
|
19
|
+
const shell = process.env.SHELL ?? "/bin/zsh";
|
|
20
|
+
this.process = (0, node_pty_1.spawn)(shell, ["-lc", this.command], {
|
|
21
|
+
cwd: this.cwd,
|
|
22
|
+
cols: 120,
|
|
23
|
+
rows: 30,
|
|
24
|
+
name: "xterm-256color",
|
|
25
|
+
env: {
|
|
26
|
+
...process.env,
|
|
27
|
+
TERM: "xterm-256color",
|
|
28
|
+
},
|
|
29
|
+
});
|
|
30
|
+
this.process.onData((chunk) => {
|
|
31
|
+
this.dataListeners.forEach((listener) => listener(chunk));
|
|
32
|
+
});
|
|
33
|
+
this.process.onExit(({ exitCode }) => {
|
|
34
|
+
if (this.stopTimer) {
|
|
35
|
+
clearTimeout(this.stopTimer);
|
|
36
|
+
this.stopTimer = null;
|
|
37
|
+
}
|
|
38
|
+
this.process = null;
|
|
39
|
+
this.exitListeners.forEach((listener) => listener(exitCode));
|
|
40
|
+
});
|
|
41
|
+
return this.process.pid;
|
|
42
|
+
}
|
|
43
|
+
write(input) {
|
|
44
|
+
this.process?.write(input);
|
|
45
|
+
}
|
|
46
|
+
stop() {
|
|
47
|
+
if (!this.process) {
|
|
48
|
+
return;
|
|
49
|
+
}
|
|
50
|
+
this.process.write("\u0003");
|
|
51
|
+
this.stopTimer = setTimeout(() => {
|
|
52
|
+
this.process?.kill();
|
|
53
|
+
}, 1500);
|
|
54
|
+
this.stopTimer.unref();
|
|
55
|
+
}
|
|
56
|
+
onData(listener) {
|
|
57
|
+
this.dataListeners.add(listener);
|
|
58
|
+
return () => {
|
|
59
|
+
this.dataListeners.delete(listener);
|
|
60
|
+
};
|
|
61
|
+
}
|
|
62
|
+
onExit(listener) {
|
|
63
|
+
this.exitListeners.add(listener);
|
|
64
|
+
return () => {
|
|
65
|
+
this.exitListeners.delete(listener);
|
|
66
|
+
};
|
|
67
|
+
}
|
|
68
|
+
isAlive() {
|
|
69
|
+
return this.process !== null;
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
exports.PtyRunner = PtyRunner;
|