@musashishao/folderforge 1.2.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.
Files changed (64) hide show
  1. package/README.md +181 -0
  2. package/dist/adapters/child-mcp/client.js +114 -0
  3. package/dist/adapters/child-mcp/registry.js +66 -0
  4. package/dist/audit/audit-log.js +45 -0
  5. package/dist/audit/event-types.js +1 -0
  6. package/dist/core/config.js +211 -0
  7. package/dist/core/container.js +51 -0
  8. package/dist/core/errors.js +37 -0
  9. package/dist/core/logger.js +8 -0
  10. package/dist/core/types.js +4 -0
  11. package/dist/dashboard/server.js +191 -0
  12. package/dist/lsp/protocol.js +116 -0
  13. package/dist/main.js +190 -0
  14. package/dist/managers/db-manager.js +161 -0
  15. package/dist/managers/lsp-manager.js +269 -0
  16. package/dist/managers/process-manager.js +140 -0
  17. package/dist/policy/approvals.js +143 -0
  18. package/dist/policy/command-policy.js +99 -0
  19. package/dist/policy/glob-match.js +61 -0
  20. package/dist/policy/path-policy.js +73 -0
  21. package/dist/policy/policy-engine.js +156 -0
  22. package/dist/policy/rate-limiter.js +96 -0
  23. package/dist/policy/risk.js +112 -0
  24. package/dist/policy/secret-policy.js +132 -0
  25. package/dist/server/mcp-server.js +144 -0
  26. package/dist/server/transports/http.js +133 -0
  27. package/dist/server/transports/stdio.js +14 -0
  28. package/dist/tools/adapter-tools.js +62 -0
  29. package/dist/tools/browser-tools.js +76 -0
  30. package/dist/tools/build-tools.js +78 -0
  31. package/dist/tools/code-tools.js +250 -0
  32. package/dist/tools/coverage-tools.js +135 -0
  33. package/dist/tools/db-tools.js +130 -0
  34. package/dist/tools/diff-util.js +45 -0
  35. package/dist/tools/error-parser.js +57 -0
  36. package/dist/tools/file-tools.js +319 -0
  37. package/dist/tools/format-tools.js +118 -0
  38. package/dist/tools/git-tools.js +371 -0
  39. package/dist/tools/index.js +63 -0
  40. package/dist/tools/memory-tools.js +54 -0
  41. package/dist/tools/output-schemas.js +100 -0
  42. package/dist/tools/pagination.js +92 -0
  43. package/dist/tools/pkg-tools.js +260 -0
  44. package/dist/tools/process-tools.js +128 -0
  45. package/dist/tools/registry.js +194 -0
  46. package/dist/tools/schema-lock.js +152 -0
  47. package/dist/tools/search-tools.js +176 -0
  48. package/dist/tools/security-tools.js +147 -0
  49. package/dist/tools/terminal-tools.js +57 -0
  50. package/dist/tools/workspace-tools.js +186 -0
  51. package/dist/workspace/memory-store.js +67 -0
  52. package/dist/workspace/onboarding.js +46 -0
  53. package/dist/workspace/project-detector.js +95 -0
  54. package/dist/workspace/workspace-manager.js +106 -0
  55. package/docs/adapters.md +76 -0
  56. package/docs/architecture.md +66 -0
  57. package/docs/roadmap.md +172 -0
  58. package/docs/security.md +94 -0
  59. package/docs/tools.md +129 -0
  60. package/examples/claude-desktop.json +18 -0
  61. package/examples/codex.toml +18 -0
  62. package/examples/config.basic.yaml +37 -0
  63. package/examples/config.full.yaml +120 -0
  64. package/package.json +74 -0
@@ -0,0 +1,161 @@
1
+ /**
2
+ * Read-only database manager. MVP supports SQLite via better-sqlite3 if present
3
+ * and Postgres via 'pg' if present; both are optional peer deps loaded lazily.
4
+ * If a driver is missing, the tools return a clear, actionable error instead of
5
+ * crashing the server.
6
+ */
7
+ const WRITE_KEYWORDS = /\b(INSERT|UPDATE|DELETE|DROP|ALTER|CREATE|TRUNCATE|GRANT|REVOKE|REPLACE|MERGE|VACUUM)\b/i;
8
+ export function isReadOnlyQuery(sql) {
9
+ const trimmed = sql.trim().replace(/;+\s*$/, '');
10
+ if (WRITE_KEYWORDS.test(trimmed))
11
+ return false;
12
+ return /^(SELECT|WITH|EXPLAIN|PRAGMA|SHOW)\b/i.test(trimmed);
13
+ }
14
+ const SECRET_COL = /pass(word)?|secret|token|api[_-]?key|private[_-]?key/i;
15
+ export function maskRow(row) {
16
+ const out = {};
17
+ for (const [k, v] of Object.entries(row)) {
18
+ out[k] = SECRET_COL.test(k) ? '[MASKED]' : v;
19
+ }
20
+ return out;
21
+ }
22
+ export class DbManager {
23
+ connections = new Map();
24
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
25
+ handles = new Map();
26
+ list() {
27
+ return [...this.connections.values()];
28
+ }
29
+ require(id) {
30
+ const conn = this.connections.get(id);
31
+ const handle = this.handles.get(id);
32
+ if (!conn || !handle)
33
+ throw new Error(`Unknown db connection: ${id}`);
34
+ return { conn, handle };
35
+ }
36
+ async connect(id, kind, target) {
37
+ if (/prod|production/i.test(target)) {
38
+ throw new Error('Refusing to connect to a production-looking database target.');
39
+ }
40
+ if (kind === 'sqlite') {
41
+ const mod = await this.tryImport('better-sqlite3', 'SQLite');
42
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
43
+ const Database = mod.default ?? mod;
44
+ const db = new Database(target, { readonly: true, fileMustExist: true });
45
+ this.handles.set(id, db);
46
+ }
47
+ else {
48
+ const mod = await this.tryImport('pg', 'Postgres');
49
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
50
+ const Client = mod.Client;
51
+ const client = new Client({ connectionString: target });
52
+ await client.connect();
53
+ this.handles.set(id, client);
54
+ }
55
+ const conn = { id, kind, target, readonly: true };
56
+ this.connections.set(id, conn);
57
+ return conn;
58
+ }
59
+ async listTables(id) {
60
+ const { conn, handle } = this.require(id);
61
+ if (conn.kind === 'sqlite') {
62
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
63
+ const rows = handle
64
+ .prepare("SELECT name FROM sqlite_master WHERE type='table' ORDER BY name")
65
+ .all();
66
+ return rows.map((r) => r.name);
67
+ }
68
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
69
+ const res = await handle.query("SELECT table_name FROM information_schema.tables WHERE table_schema='public' ORDER BY table_name");
70
+ return res.rows.map((r) => r.table_name);
71
+ }
72
+ async describeTable(id, table) {
73
+ const { conn, handle } = this.require(id);
74
+ if (!/^[A-Za-z0-9_]+$/.test(table))
75
+ throw new Error('Invalid table name');
76
+ if (conn.kind === 'sqlite') {
77
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
78
+ return handle.prepare(`PRAGMA table_info(${table})`).all();
79
+ }
80
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
81
+ const res = await handle.query('SELECT column_name, data_type, is_nullable FROM information_schema.columns WHERE table_name=$1 ORDER BY ordinal_position', [table]);
82
+ return res.rows;
83
+ }
84
+ async queryReadonly(id, sql, limit = 200) {
85
+ if (!isReadOnlyQuery(sql))
86
+ throw new Error('Only read-only queries (SELECT/EXPLAIN/WITH) are allowed.');
87
+ const { conn, handle } = this.require(id);
88
+ let rows;
89
+ if (conn.kind === 'sqlite') {
90
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
91
+ rows = handle.prepare(sql).all().slice(0, limit);
92
+ }
93
+ else {
94
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
95
+ const res = await handle.query(sql);
96
+ rows = res.rows.slice(0, limit);
97
+ }
98
+ return rows.map(maskRow);
99
+ }
100
+ async explain(id, sql) {
101
+ const cleaned = sql.trim().replace(/;+\s*$/, '');
102
+ return this.queryReadonly(id, `EXPLAIN ${cleaned}`);
103
+ }
104
+ /**
105
+ * Execute a single write statement (INSERT/UPDATE/DELETE) against a dev
106
+ * connection. Refuses read-only-looking statements and DDL; migrations go
107
+ * through {@link runMigration}. HIGH-risk: gated by policy/approval upstream.
108
+ */
109
+ async write(id, sql) {
110
+ if (isReadOnlyQuery(sql)) {
111
+ throw new Error('db_write expects a write statement (INSERT/UPDATE/DELETE), not a read-only query.');
112
+ }
113
+ const { conn, handle } = this.require(id);
114
+ if (conn.readonly) {
115
+ throw new Error(`Connection ${id} is read-only; db_write is not permitted.`);
116
+ }
117
+ if (conn.kind === 'sqlite') {
118
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
119
+ const info = handle.prepare(sql).run();
120
+ return { changes: Number(info.changes ?? 0) };
121
+ }
122
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
123
+ const res = await handle.query(sql);
124
+ return { changes: Number(res.rowCount ?? 0) };
125
+ }
126
+ /**
127
+ * Run a multi-statement migration script in a single transaction. HIGH-risk:
128
+ * gated by policy/approval upstream.
129
+ */
130
+ async runMigration(id, sql) {
131
+ const { conn, handle } = this.require(id);
132
+ if (conn.readonly) {
133
+ throw new Error(`Connection ${id} is read-only; db_run_migration is not permitted.`);
134
+ }
135
+ if (conn.kind === 'sqlite') {
136
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
137
+ handle.exec(sql);
138
+ return { applied: true };
139
+ }
140
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
141
+ const client = handle;
142
+ try {
143
+ await client.query('BEGIN');
144
+ await client.query(sql);
145
+ await client.query('COMMIT');
146
+ return { applied: true };
147
+ }
148
+ catch (err) {
149
+ await client.query('ROLLBACK').catch(() => undefined);
150
+ throw err;
151
+ }
152
+ }
153
+ async tryImport(mod, label) {
154
+ try {
155
+ return await import(mod);
156
+ }
157
+ catch {
158
+ throw new Error(`${label} driver "${mod}" is not installed. Run: npm install ${mod} (optional dependency).`);
159
+ }
160
+ }
161
+ }
@@ -0,0 +1,269 @@
1
+ import { spawn } from 'node:child_process';
2
+ import { pathToFileURL, fileURLToPath } from 'node:url';
3
+ import { readFileSync } from 'node:fs';
4
+ import { join, isAbsolute } from 'node:path';
5
+ import { MessageBuffer, encodeMessage, isResponse, SYMBOL_KIND, lspSeverity, } from '../lsp/protocol.js';
6
+ /** Built-in language server definitions. Overridable via config. */
7
+ export const DEFAULT_LANGUAGE_SERVERS = [
8
+ {
9
+ id: 'typescript',
10
+ command: 'typescript-language-server',
11
+ args: ['--stdio'],
12
+ extensions: ['ts', 'tsx', 'js', 'jsx', 'mjs', 'cjs'],
13
+ languageId: 'typescript',
14
+ },
15
+ {
16
+ id: 'python',
17
+ command: 'pyright-langserver',
18
+ args: ['--stdio'],
19
+ extensions: ['py', 'pyi'],
20
+ languageId: 'python',
21
+ },
22
+ ];
23
+ export const DEFAULT_LSP_CONFIG = {
24
+ enabled: true,
25
+ requestTimeoutMs: 15_000,
26
+ };
27
+ /** A live, initialized language-server connection. */
28
+ class LspConnection {
29
+ def;
30
+ root;
31
+ requestTimeoutMs;
32
+ child;
33
+ nextId = 1;
34
+ pending = new Map();
35
+ buffer;
36
+ opened = new Set();
37
+ /** Latest published diagnostics per file URI. */
38
+ diagnostics = new Map();
39
+ exited = false;
40
+ constructor(def, root, requestTimeoutMs) {
41
+ this.def = def;
42
+ this.root = root;
43
+ this.requestTimeoutMs = requestTimeoutMs;
44
+ this.child = spawn(def.command, def.args, { cwd: root });
45
+ this.buffer = new MessageBuffer((msg) => this.onMessage(msg));
46
+ this.child.stdout.on('data', (c) => this.buffer.append(c));
47
+ this.child.stderr.on('data', () => { }); // language servers log noisily; ignore
48
+ this.child.on('exit', () => {
49
+ this.exited = true;
50
+ for (const [, p] of this.pending) {
51
+ clearTimeout(p.timer);
52
+ p.reject(new Error(`${this.def.id} language server exited`));
53
+ }
54
+ this.pending.clear();
55
+ });
56
+ this.child.on('error', () => {
57
+ this.exited = true;
58
+ });
59
+ }
60
+ onMessage(msg) {
61
+ if (isResponse(msg)) {
62
+ const p = this.pending.get(msg.id);
63
+ if (!p)
64
+ return;
65
+ this.pending.delete(msg.id);
66
+ clearTimeout(p.timer);
67
+ if (msg.error)
68
+ p.reject(new Error(msg.error.message));
69
+ else
70
+ p.resolve(msg.result);
71
+ return;
72
+ }
73
+ // Notification from the server.
74
+ if ('method' in msg && msg.method === 'textDocument/publishDiagnostics') {
75
+ const params = msg.params;
76
+ if (params?.uri)
77
+ this.diagnostics.set(params.uri, params.diagnostics ?? []);
78
+ }
79
+ // Server->client requests (e.g. workspace/configuration) are answered with
80
+ // a null result so the server doesn't stall waiting on us.
81
+ if ('method' in msg && 'id' in msg) {
82
+ this.send({ jsonrpc: '2.0', id: msg.id, result: null });
83
+ }
84
+ }
85
+ send(msg) {
86
+ if (this.exited)
87
+ return;
88
+ this.child.stdin.write(encodeMessage(msg));
89
+ }
90
+ request(method, params) {
91
+ if (this.exited)
92
+ return Promise.reject(new Error(`${this.def.id} server not running`));
93
+ const id = this.nextId++;
94
+ return new Promise((resolve, reject) => {
95
+ const timer = setTimeout(() => {
96
+ this.pending.delete(id);
97
+ reject(new Error(`LSP request ${method} timed out after ${this.requestTimeoutMs}ms`));
98
+ }, this.requestTimeoutMs);
99
+ this.pending.set(id, { resolve, reject, timer });
100
+ this.send({ jsonrpc: '2.0', id, method, params });
101
+ });
102
+ }
103
+ notify(method, params) {
104
+ this.send({ jsonrpc: '2.0', method, params });
105
+ }
106
+ async initialize() {
107
+ await this.request('initialize', {
108
+ processId: process.pid,
109
+ rootUri: pathToFileURL(this.root).href,
110
+ capabilities: {
111
+ textDocument: {
112
+ documentSymbol: { hierarchicalDocumentSymbolSupport: true },
113
+ definition: {},
114
+ references: {},
115
+ rename: {},
116
+ publishDiagnostics: {},
117
+ },
118
+ workspace: { symbol: {}, workspaceFolders: true },
119
+ },
120
+ workspaceFolders: [{ uri: pathToFileURL(this.root).href, name: 'root' }],
121
+ });
122
+ this.notify('initialized', {});
123
+ }
124
+ /** Ensure a file is open on the server (didOpen once per URI). */
125
+ ensureOpen(relativePath) {
126
+ const abs = isAbsolute(relativePath) ? relativePath : join(this.root, relativePath);
127
+ const uri = pathToFileURL(abs).href;
128
+ if (this.opened.has(uri))
129
+ return uri;
130
+ let text = '';
131
+ try {
132
+ text = readFileSync(abs, 'utf8');
133
+ }
134
+ catch {
135
+ text = '';
136
+ }
137
+ this.notify('textDocument/didOpen', {
138
+ textDocument: { uri, languageId: this.def.languageId, version: 1, text },
139
+ });
140
+ this.opened.add(uri);
141
+ return uri;
142
+ }
143
+ dispose() {
144
+ try {
145
+ this.send({ jsonrpc: '2.0', id: this.nextId++, method: 'shutdown', params: null });
146
+ this.notify('exit', null);
147
+ }
148
+ catch {
149
+ /* ignore */
150
+ }
151
+ try {
152
+ this.child.kill('SIGTERM');
153
+ }
154
+ catch {
155
+ /* ignore */
156
+ }
157
+ this.exited = true;
158
+ }
159
+ }
160
+ /**
161
+ * Manages native LSP connections, one per (language, projectRoot). Lazily
162
+ * spawns and initializes a server on first use, reusing it afterwards. When the
163
+ * configured binary is not installed, callers get a clear "unavailable" signal
164
+ * and degrade gracefully (Serena adapter / regex search).
165
+ */
166
+ export class LspManager {
167
+ config;
168
+ connections = new Map();
169
+ servers;
170
+ constructor(config = DEFAULT_LSP_CONFIG) {
171
+ this.config = config;
172
+ this.config = config ?? DEFAULT_LSP_CONFIG;
173
+ this.servers = this.config.servers ?? DEFAULT_LANGUAGE_SERVERS;
174
+ }
175
+ /** Pick the server definition that handles a given file path, if any. */
176
+ serverForPath(relativePath) {
177
+ const ext = relativePath.split('.').pop()?.toLowerCase() ?? '';
178
+ return this.servers.find((s) => s.extensions.includes(ext));
179
+ }
180
+ serverById(id) {
181
+ return this.servers.find((s) => s.id === id);
182
+ }
183
+ isEnabled() {
184
+ return this.config.enabled;
185
+ }
186
+ /**
187
+ * Get (or lazily create + initialize) a connection for a language at a root.
188
+ * Returns null if LSP is disabled or the binary cannot be spawned.
189
+ */
190
+ async ensure(def, root) {
191
+ if (!this.config.enabled)
192
+ return null;
193
+ const key = `${def.id}::${root}`;
194
+ const existing = this.connections.get(key);
195
+ if (existing)
196
+ return existing;
197
+ try {
198
+ const conn = new LspConnection(def, root, this.config.requestTimeoutMs);
199
+ await conn.initialize();
200
+ this.connections.set(key, conn);
201
+ return conn;
202
+ }
203
+ catch {
204
+ return null; // binary missing or init failed -> caller falls back
205
+ }
206
+ }
207
+ disposeAll() {
208
+ for (const [, c] of this.connections)
209
+ c.dispose();
210
+ this.connections.clear();
211
+ }
212
+ }
213
+ /** Normalize an LSP Location into a {file, line, column} record (1-based line). */
214
+ export function normalizeLocation(loc) {
215
+ const l = loc;
216
+ if (!l?.uri || !l.range?.start)
217
+ return null;
218
+ let file = l.uri;
219
+ try {
220
+ file = fileURLToPath(l.uri);
221
+ }
222
+ catch {
223
+ /* keep raw uri */
224
+ }
225
+ return {
226
+ file,
227
+ line: (l.range.start.line ?? 0) + 1,
228
+ column: (l.range.start.character ?? 0) + 1,
229
+ };
230
+ }
231
+ /** Flatten a documentSymbol tree (hierarchical or flat) into named entries. */
232
+ export function flattenSymbols(symbols) {
233
+ const out = [];
234
+ const visit = (nodes) => {
235
+ for (const n of nodes) {
236
+ const s = n;
237
+ if (s?.name) {
238
+ const line = (s.range?.start?.line ?? s.location?.range?.start?.line ?? 0) + 1;
239
+ out.push({ name: s.name, kind: SYMBOL_KIND[s.kind ?? 0] ?? 'symbol', line });
240
+ }
241
+ if (Array.isArray(s?.children))
242
+ visit(s.children);
243
+ }
244
+ };
245
+ if (Array.isArray(symbols))
246
+ visit(symbols);
247
+ return out;
248
+ }
249
+ /** Convert LSP diagnostics for a file into the shared error-item shape. */
250
+ export function normalizeDiagnostics(uri, diags) {
251
+ let file = uri;
252
+ try {
253
+ file = fileURLToPath(uri);
254
+ }
255
+ catch {
256
+ /* keep raw uri */
257
+ }
258
+ return diags.map((d) => {
259
+ const x = d;
260
+ return {
261
+ file,
262
+ line: (x.range?.start?.line ?? 0) + 1,
263
+ column: (x.range?.start?.character ?? 0) + 1,
264
+ severity: lspSeverity(x.severity),
265
+ message: x.message ?? '',
266
+ ...(x.code !== undefined ? { code: String(x.code) } : {}),
267
+ };
268
+ });
269
+ }
@@ -0,0 +1,140 @@
1
+ import { spawn } from 'node:child_process';
2
+ import { randomUUID } from 'node:crypto';
3
+ /**
4
+ * Manages long-running child processes (dev servers, watchers, compose).
5
+ */
6
+ export class ProcessManager {
7
+ sessions = new Map();
8
+ maxBuffer = 1_000_000;
9
+ start(command, cwd, shell) {
10
+ const sessionId = `proc_${randomUUID().slice(0, 8)}`;
11
+ const child = spawn(shell, ['-lc', command], {
12
+ cwd,
13
+ env: process.env,
14
+ });
15
+ const session = {
16
+ sessionId,
17
+ pid: child.pid,
18
+ command,
19
+ cwd,
20
+ status: 'running',
21
+ exitCode: null,
22
+ startedAt: Date.now(),
23
+ child,
24
+ output: '',
25
+ cursor: 0,
26
+ waiters: [],
27
+ };
28
+ const wake = () => {
29
+ const ws = session.waiters;
30
+ session.waiters = [];
31
+ for (const w of ws)
32
+ w();
33
+ };
34
+ const append = (chunk) => {
35
+ session.output += chunk.toString('utf8');
36
+ if (session.output.length > this.maxBuffer) {
37
+ session.output = session.output.slice(-this.maxBuffer);
38
+ }
39
+ wake();
40
+ };
41
+ child.stdout.on('data', append);
42
+ child.stderr.on('data', append);
43
+ child.on('exit', (code) => {
44
+ session.status = session.status === 'killed' ? 'killed' : 'exited';
45
+ session.exitCode = code;
46
+ wake();
47
+ });
48
+ this.sessions.set(sessionId, session);
49
+ return this.publicView(session);
50
+ }
51
+ read(sessionId) {
52
+ const s = this.require(sessionId);
53
+ const out = s.output.slice(s.cursor);
54
+ s.cursor = s.output.length;
55
+ return { output: out, status: s.status, cursor: s.cursor };
56
+ }
57
+ /**
58
+ * Long-poll read: resolve as soon as new output is available or the process
59
+ * exits, or after `timeoutMs` with whatever (possibly empty) output arrived.
60
+ * This backs streaming tails without busy-waiting. `done` is true once the
61
+ * process has exited and all buffered output has been drained.
62
+ */
63
+ readUntil(sessionId, timeoutMs = 2000, signal) {
64
+ const s = this.require(sessionId);
65
+ const drain = () => {
66
+ const output = s.output.slice(s.cursor);
67
+ s.cursor = s.output.length;
68
+ const done = s.status !== 'running' && s.cursor >= s.output.length;
69
+ return { output, status: s.status, cursor: s.cursor, done };
70
+ };
71
+ // Immediate return if there is already new output, the process is finished,
72
+ // or the caller has already cancelled (P6).
73
+ if (s.output.length > s.cursor || s.status !== 'running' || signal?.aborted) {
74
+ return Promise.resolve(drain());
75
+ }
76
+ return new Promise((resolveOut) => {
77
+ let settled = false;
78
+ const finish = () => {
79
+ if (settled)
80
+ return;
81
+ settled = true;
82
+ clearTimeout(timer);
83
+ if (signal)
84
+ signal.removeEventListener('abort', finish);
85
+ resolveOut(drain());
86
+ };
87
+ const timer = setTimeout(finish, timeoutMs);
88
+ // Wake immediately on cancellation so a long tail does not block the
89
+ // client after it has cancelled the request.
90
+ if (signal)
91
+ signal.addEventListener('abort', finish, { once: true });
92
+ s.waiters.push(finish);
93
+ });
94
+ }
95
+ write(sessionId, input) {
96
+ const s = this.require(sessionId);
97
+ if (s.status !== 'running')
98
+ throw new Error('Process is not running');
99
+ s.child.stdin.write(input.endsWith('\n') ? input : input + '\n');
100
+ }
101
+ stop(sessionId) {
102
+ const s = this.require(sessionId);
103
+ if (s.status === 'running') {
104
+ s.child.kill('SIGTERM');
105
+ s.status = 'killed';
106
+ }
107
+ return this.publicView(s);
108
+ }
109
+ kill(sessionId) {
110
+ const s = this.require(sessionId);
111
+ if (s.status === 'running') {
112
+ s.child.kill('SIGKILL');
113
+ s.status = 'killed';
114
+ }
115
+ return this.publicView(s);
116
+ }
117
+ list() {
118
+ return [...this.sessions.values()].map((s) => this.publicView(s));
119
+ }
120
+ isManaged(sessionId) {
121
+ return this.sessions.has(sessionId);
122
+ }
123
+ require(sessionId) {
124
+ const s = this.sessions.get(sessionId);
125
+ if (!s)
126
+ throw new Error(`Unknown process session: ${sessionId}`);
127
+ return s;
128
+ }
129
+ publicView(s) {
130
+ return {
131
+ sessionId: s.sessionId,
132
+ pid: s.pid,
133
+ command: s.command,
134
+ cwd: s.cwd,
135
+ status: s.status,
136
+ exitCode: s.exitCode,
137
+ startedAt: s.startedAt,
138
+ };
139
+ }
140
+ }