@covro/covro 0.1.1-dev.2 → 0.1.1-dev.3

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.
@@ -0,0 +1,128 @@
1
+ import { randomUUID } from "node:crypto";
2
+ import { StringDecoder } from "node:string_decoder";
3
+ import { CodexError } from "../codex/transport.js";
4
+ import { AppError } from "../shared/errors.js";
5
+ import {
6
+ shellActive,
7
+ SHELL_OUTPUT_BYTES
8
+ } from "../shared/shell.js";
9
+ class ShellService {
10
+ constructor(codex) {
11
+ this.codex = codex;
12
+ }
13
+ codex;
14
+ epoch = randomUUID();
15
+ revision = 0;
16
+ commands = /* @__PURE__ */ new Map();
17
+ snapshot(epoch, after = 0) {
18
+ return {
19
+ epoch: this.epoch,
20
+ revision: this.revision,
21
+ commands: [...this.commands.values()].filter(
22
+ (run) => epoch !== this.epoch || run.revision > after
23
+ )
24
+ };
25
+ }
26
+ active(projectId) {
27
+ return [...this.commands.values()].some(
28
+ (run) => shellActive(run) && (!projectId || run.projectId === projectId)
29
+ );
30
+ }
31
+ changed(run) {
32
+ run.revision = ++this.revision;
33
+ }
34
+ start(project, input) {
35
+ if (this.active(project.id))
36
+ throw new AppError("SHELL_BUSY", "\u6B64\u9879\u76EE\u5DF2\u6709\u547D\u4EE4\u8FD0\u884C\uFF0C\u8BF7\u5148\u4E2D\u65AD\u6216\u7B49\u5F85\u7ED3\u675F", 409);
37
+ if ([...this.commands.values()].filter(shellActive).length >= 4)
38
+ throw new AppError("SHELL_BUSY", "\u6700\u591A\u540C\u65F6\u8FD0\u884C 4 \u4E2A\u9879\u76EE\u7684\u547D\u4EE4", 409);
39
+ if (!this.codex.executeCommand || !this.codex.terminateCommand)
40
+ throw new AppError("PROTOCOL_INCOMPATIBLE", "\u5F53\u524D Codex \u4E0D\u652F\u6301 Shell \u547D\u4EE4\u6267\u884C", 409);
41
+ const run = {
42
+ id: input.clientOperationId,
43
+ projectId: project.id,
44
+ projectName: project.name,
45
+ cwd: project.canonicalRoot,
46
+ command: input.command,
47
+ status: "running",
48
+ startedAt: Date.now(),
49
+ output: [],
50
+ truncated: false,
51
+ revision: 0
52
+ };
53
+ this.commands.set(run.id, run);
54
+ this.changed(run);
55
+ while (this.commands.size > 20) {
56
+ const oldest = [...this.commands.values()].find((entry) => !shellActive(entry));
57
+ if (!oldest) break;
58
+ this.commands.delete(oldest.id);
59
+ }
60
+ const decoders = { stdout: new StringDecoder("utf8"), stderr: new StringDecoder("utf8") };
61
+ const append = (stream, text) => {
62
+ if (!text) return;
63
+ const last = run.output.at(-1);
64
+ if (last?.stream === stream) last.text += text;
65
+ else run.output.push({ stream, text });
66
+ let size = run.output.reduce((total, chunk) => total + Buffer.byteLength(chunk.text), 0);
67
+ while (run.output.length > 200 || size > SHELL_OUTPUT_BYTES) {
68
+ run.truncated = true;
69
+ const first = run.output[0];
70
+ const bytes = Buffer.byteLength(first.text);
71
+ if (run.output.length > 1) {
72
+ run.output.shift();
73
+ size -= bytes;
74
+ } else {
75
+ const buffer = Buffer.from(first.text);
76
+ let start = size - SHELL_OUTPUT_BYTES;
77
+ while (start < buffer.length && (buffer[start] & 192) === 128) start++;
78
+ first.text = buffer.subarray(start).toString("utf8");
79
+ break;
80
+ }
81
+ }
82
+ this.changed(run);
83
+ };
84
+ void this.codex.executeCommand(run.id, run.cwd, run.command, (stream, bytes, capReached) => {
85
+ append(stream, decoders[stream].write(bytes));
86
+ if (capReached) {
87
+ run.truncated = true;
88
+ this.changed(run);
89
+ }
90
+ }).then((result) => {
91
+ append("stdout", decoders.stdout.end() + result.stdout);
92
+ append("stderr", decoders.stderr.end() + result.stderr);
93
+ run.exitCode = result.exitCode;
94
+ run.status = run.status === "stopping" ? "interrupted" : result.exitCode === 0 ? "completed" : "failed";
95
+ }).catch((error) => {
96
+ append("stdout", decoders.stdout.end());
97
+ append("stderr", decoders.stderr.end());
98
+ run.status = error instanceof CodexError && error.uncertain ? "unknown" : "failed";
99
+ run.error = error instanceof Error ? error.message : "\u547D\u4EE4\u6267\u884C\u5931\u8D25";
100
+ if (run.status === "unknown") run.error += "\uFF1B\u6267\u884C\u7ED3\u679C\u672A\u77E5\uFF0C\u8BF7\u68C0\u67E5\u670D\u52A1\u7AEF\uFF0C\u52FF\u76F4\u63A5\u91CD\u590D\u6267\u884C\u3002";
101
+ }).finally(() => {
102
+ run.finishedAt = Date.now();
103
+ this.changed(run);
104
+ });
105
+ return structuredClone(run);
106
+ }
107
+ async interrupt(id) {
108
+ const run = this.commands.get(id);
109
+ if (!run) throw new AppError("SHELL_NOT_FOUND", "\u547D\u4EE4\u8BB0\u5F55\u5DF2\u8FC7\u671F\u6216\u670D\u52A1\u5DF2\u91CD\u542F", 404);
110
+ if (!shellActive(run)) return;
111
+ run.status = "stopping";
112
+ this.changed(run);
113
+ try {
114
+ await this.codex.terminateCommand(id);
115
+ } catch (error) {
116
+ if (run.status === "stopping") {
117
+ run.status = "running";
118
+ run.error = "\u4E2D\u65AD\u8BF7\u6C42\u672A\u786E\u8BA4\uFF0C\u8BF7\u91CD\u8BD5\u6216\u68C0\u67E5\u670D\u52A1\u7AEF\u3002";
119
+ this.changed(run);
120
+ }
121
+ throw error;
122
+ }
123
+ }
124
+ }
125
+ export {
126
+ ShellService
127
+ };
128
+ //# sourceMappingURL=shell.js.map
@@ -0,0 +1,7 @@
1
+ {
2
+ "version": 3,
3
+ "sources": ["../../src/server/shell.ts"],
4
+ "sourcesContent": ["import { randomUUID } from 'node:crypto';\nimport { StringDecoder } from 'node:string_decoder';\nimport type { CodexClient } from '../codex/types.js';\nimport { CodexError } from '../codex/transport.js';\nimport { AppError } from '../shared/errors.js';\nimport {\n shellActive,\n SHELL_OUTPUT_BYTES,\n type ShellCommand,\n type StartShellInput,\n} from '../shared/shell.js';\nimport type { Project } from '../shared/types.js';\n\n/** Bounded, server-owned runs. Browser navigation never owns a process lifetime. */\nexport class ShellService {\n private readonly epoch = randomUUID();\n private revision = 0;\n private readonly commands = new Map<string, ShellCommand>();\n constructor(private readonly codex: CodexClient) {}\n\n snapshot(epoch?: string, after = 0) {\n return {\n epoch: this.epoch,\n revision: this.revision,\n commands: [...this.commands.values()].filter(\n (run) => epoch !== this.epoch || run.revision > after,\n ),\n };\n }\n active(projectId?: string) {\n return [...this.commands.values()].some(\n (run) => shellActive(run) && (!projectId || run.projectId === projectId),\n );\n }\n private changed(run: ShellCommand) {\n run.revision = ++this.revision;\n }\n\n start(project: Project, input: StartShellInput): ShellCommand {\n if (this.active(project.id))\n throw new AppError('SHELL_BUSY', '\u6B64\u9879\u76EE\u5DF2\u6709\u547D\u4EE4\u8FD0\u884C\uFF0C\u8BF7\u5148\u4E2D\u65AD\u6216\u7B49\u5F85\u7ED3\u675F', 409);\n if ([...this.commands.values()].filter(shellActive).length >= 4)\n throw new AppError('SHELL_BUSY', '\u6700\u591A\u540C\u65F6\u8FD0\u884C 4 \u4E2A\u9879\u76EE\u7684\u547D\u4EE4', 409);\n if (!this.codex.executeCommand || !this.codex.terminateCommand)\n throw new AppError('PROTOCOL_INCOMPATIBLE', '\u5F53\u524D Codex \u4E0D\u652F\u6301 Shell \u547D\u4EE4\u6267\u884C', 409);\n const run: ShellCommand = {\n id: input.clientOperationId,\n projectId: project.id,\n projectName: project.name,\n cwd: project.canonicalRoot,\n command: input.command,\n status: 'running',\n startedAt: Date.now(),\n output: [],\n truncated: false,\n revision: 0,\n };\n this.commands.set(run.id, run);\n this.changed(run);\n while (this.commands.size > 20) {\n const oldest = [...this.commands.values()].find((entry) => !shellActive(entry));\n if (!oldest) break;\n this.commands.delete(oldest.id);\n }\n const decoders = { stdout: new StringDecoder('utf8'), stderr: new StringDecoder('utf8') };\n const append = (stream: 'stdout' | 'stderr', text: string) => {\n if (!text) return;\n const last = run.output.at(-1);\n if (last?.stream === stream) last.text += text;\n else run.output.push({ stream, text });\n // Bound both bytes and chunk count even for alternating single-byte writes.\n let size = run.output.reduce((total, chunk) => total + Buffer.byteLength(chunk.text), 0);\n while (run.output.length > 200 || size > SHELL_OUTPUT_BYTES) {\n run.truncated = true;\n const first = run.output[0]!;\n const bytes = Buffer.byteLength(first.text);\n if (run.output.length > 1) {\n run.output.shift();\n size -= bytes;\n } else {\n const buffer = Buffer.from(first.text);\n let start = size - SHELL_OUTPUT_BYTES;\n // Start on a complete code point; replacement bytes can otherwise exceed the cap.\n while (start < buffer.length && (buffer[start]! & 0xc0) === 0x80) start++;\n first.text = buffer.subarray(start).toString('utf8');\n break;\n }\n }\n this.changed(run);\n };\n void this.codex\n .executeCommand(run.id, run.cwd, run.command, (stream, bytes, capReached) => {\n append(stream, decoders[stream].write(bytes));\n if (capReached) {\n run.truncated = true;\n this.changed(run);\n }\n })\n .then((result) => {\n append('stdout', decoders.stdout.end() + result.stdout);\n append('stderr', decoders.stderr.end() + result.stderr);\n run.exitCode = result.exitCode;\n run.status =\n run.status === 'stopping'\n ? 'interrupted'\n : result.exitCode === 0\n ? 'completed'\n : 'failed';\n })\n .catch((error: unknown) => {\n append('stdout', decoders.stdout.end());\n append('stderr', decoders.stderr.end());\n run.status = error instanceof CodexError && error.uncertain ? 'unknown' : 'failed';\n run.error = error instanceof Error ? error.message : '\u547D\u4EE4\u6267\u884C\u5931\u8D25';\n if (run.status === 'unknown') run.error += '\uFF1B\u6267\u884C\u7ED3\u679C\u672A\u77E5\uFF0C\u8BF7\u68C0\u67E5\u670D\u52A1\u7AEF\uFF0C\u52FF\u76F4\u63A5\u91CD\u590D\u6267\u884C\u3002';\n })\n .finally(() => {\n run.finishedAt = Date.now();\n this.changed(run);\n });\n return structuredClone(run);\n }\n async interrupt(id: string) {\n const run = this.commands.get(id);\n if (!run) throw new AppError('SHELL_NOT_FOUND', '\u547D\u4EE4\u8BB0\u5F55\u5DF2\u8FC7\u671F\u6216\u670D\u52A1\u5DF2\u91CD\u542F', 404);\n if (!shellActive(run)) return;\n run.status = 'stopping';\n this.changed(run);\n try {\n await this.codex.terminateCommand!(id);\n } catch (error) {\n if (run.status === 'stopping') {\n run.status = 'running';\n run.error = '\u4E2D\u65AD\u8BF7\u6C42\u672A\u786E\u8BA4\uFF0C\u8BF7\u91CD\u8BD5\u6216\u68C0\u67E5\u670D\u52A1\u7AEF\u3002';\n this.changed(run);\n }\n throw error;\n }\n }\n}\n"],
5
+ "mappings": "AAAA,SAAS,kBAAkB;AAC3B,SAAS,qBAAqB;AAE9B,SAAS,kBAAkB;AAC3B,SAAS,gBAAgB;AACzB;AAAA,EACE;AAAA,EACA;AAAA,OAGK;AAIA,MAAM,aAAa;AAAA,EAIxB,YAA6B,OAAoB;AAApB;AAAA,EAAqB;AAAA,EAArB;AAAA,EAHZ,QAAQ,WAAW;AAAA,EAC5B,WAAW;AAAA,EACF,WAAW,oBAAI,IAA0B;AAAA,EAG1D,SAAS,OAAgB,QAAQ,GAAG;AAClC,WAAO;AAAA,MACL,OAAO,KAAK;AAAA,MACZ,UAAU,KAAK;AAAA,MACf,UAAU,CAAC,GAAG,KAAK,SAAS,OAAO,CAAC,EAAE;AAAA,QACpC,CAAC,QAAQ,UAAU,KAAK,SAAS,IAAI,WAAW;AAAA,MAClD;AAAA,IACF;AAAA,EACF;AAAA,EACA,OAAO,WAAoB;AACzB,WAAO,CAAC,GAAG,KAAK,SAAS,OAAO,CAAC,EAAE;AAAA,MACjC,CAAC,QAAQ,YAAY,GAAG,MAAM,CAAC,aAAa,IAAI,cAAc;AAAA,IAChE;AAAA,EACF;AAAA,EACQ,QAAQ,KAAmB;AACjC,QAAI,WAAW,EAAE,KAAK;AAAA,EACxB;AAAA,EAEA,MAAM,SAAkB,OAAsC;AAC5D,QAAI,KAAK,OAAO,QAAQ,EAAE;AACxB,YAAM,IAAI,SAAS,cAAc,sHAAuB,GAAG;AAC7D,QAAI,CAAC,GAAG,KAAK,SAAS,OAAO,CAAC,EAAE,OAAO,WAAW,EAAE,UAAU;AAC5D,YAAM,IAAI,SAAS,cAAc,+EAAmB,GAAG;AACzD,QAAI,CAAC,KAAK,MAAM,kBAAkB,CAAC,KAAK,MAAM;AAC5C,YAAM,IAAI,SAAS,yBAAyB,wEAA2B,GAAG;AAC5E,UAAM,MAAoB;AAAA,MACxB,IAAI,MAAM;AAAA,MACV,WAAW,QAAQ;AAAA,MACnB,aAAa,QAAQ;AAAA,MACrB,KAAK,QAAQ;AAAA,MACb,SAAS,MAAM;AAAA,MACf,QAAQ;AAAA,MACR,WAAW,KAAK,IAAI;AAAA,MACpB,QAAQ,CAAC;AAAA,MACT,WAAW;AAAA,MACX,UAAU;AAAA,IACZ;AACA,SAAK,SAAS,IAAI,IAAI,IAAI,GAAG;AAC7B,SAAK,QAAQ,GAAG;AAChB,WAAO,KAAK,SAAS,OAAO,IAAI;AAC9B,YAAM,SAAS,CAAC,GAAG,KAAK,SAAS,OAAO,CAAC,EAAE,KAAK,CAAC,UAAU,CAAC,YAAY,KAAK,CAAC;AAC9E,UAAI,CAAC,OAAQ;AACb,WAAK,SAAS,OAAO,OAAO,EAAE;AAAA,IAChC;AACA,UAAM,WAAW,EAAE,QAAQ,IAAI,cAAc,MAAM,GAAG,QAAQ,IAAI,cAAc,MAAM,EAAE;AACxF,UAAM,SAAS,CAAC,QAA6B,SAAiB;AAC5D,UAAI,CAAC,KAAM;AACX,YAAM,OAAO,IAAI,OAAO,GAAG,EAAE;AAC7B,UAAI,MAAM,WAAW,OAAQ,MAAK,QAAQ;AAAA,UACrC,KAAI,OAAO,KAAK,EAAE,QAAQ,KAAK,CAAC;AAErC,UAAI,OAAO,IAAI,OAAO,OAAO,CAAC,OAAO,UAAU,QAAQ,OAAO,WAAW,MAAM,IAAI,GAAG,CAAC;AACvF,aAAO,IAAI,OAAO,SAAS,OAAO,OAAO,oBAAoB;AAC3D,YAAI,YAAY;AAChB,cAAM,QAAQ,IAAI,OAAO,CAAC;AAC1B,cAAM,QAAQ,OAAO,WAAW,MAAM,IAAI;AAC1C,YAAI,IAAI,OAAO,SAAS,GAAG;AACzB,cAAI,OAAO,MAAM;AACjB,kBAAQ;AAAA,QACV,OAAO;AACL,gBAAM,SAAS,OAAO,KAAK,MAAM,IAAI;AACrC,cAAI,QAAQ,OAAO;AAEnB,iBAAO,QAAQ,OAAO,WAAW,OAAO,KAAK,IAAK,SAAU,IAAM;AAClE,gBAAM,OAAO,OAAO,SAAS,KAAK,EAAE,SAAS,MAAM;AACnD;AAAA,QACF;AAAA,MACF;AACA,WAAK,QAAQ,GAAG;AAAA,IAClB;AACA,SAAK,KAAK,MACP,eAAe,IAAI,IAAI,IAAI,KAAK,IAAI,SAAS,CAAC,QAAQ,OAAO,eAAe;AAC3E,aAAO,QAAQ,SAAS,MAAM,EAAE,MAAM,KAAK,CAAC;AAC5C,UAAI,YAAY;AACd,YAAI,YAAY;AAChB,aAAK,QAAQ,GAAG;AAAA,MAClB;AAAA,IACF,CAAC,EACA,KAAK,CAAC,WAAW;AAChB,aAAO,UAAU,SAAS,OAAO,IAAI,IAAI,OAAO,MAAM;AACtD,aAAO,UAAU,SAAS,OAAO,IAAI,IAAI,OAAO,MAAM;AACtD,UAAI,WAAW,OAAO;AACtB,UAAI,SACF,IAAI,WAAW,aACX,gBACA,OAAO,aAAa,IAClB,cACA;AAAA,IACV,CAAC,EACA,MAAM,CAAC,UAAmB;AACzB,aAAO,UAAU,SAAS,OAAO,IAAI,CAAC;AACtC,aAAO,UAAU,SAAS,OAAO,IAAI,CAAC;AACtC,UAAI,SAAS,iBAAiB,cAAc,MAAM,YAAY,YAAY;AAC1E,UAAI,QAAQ,iBAAiB,QAAQ,MAAM,UAAU;AACrD,UAAI,IAAI,WAAW,UAAW,KAAI,SAAS;AAAA,IAC7C,CAAC,EACA,QAAQ,MAAM;AACb,UAAI,aAAa,KAAK,IAAI;AAC1B,WAAK,QAAQ,GAAG;AAAA,IAClB,CAAC;AACH,WAAO,gBAAgB,GAAG;AAAA,EAC5B;AAAA,EACA,MAAM,UAAU,IAAY;AAC1B,UAAM,MAAM,KAAK,SAAS,IAAI,EAAE;AAChC,QAAI,CAAC,IAAK,OAAM,IAAI,SAAS,mBAAmB,kFAAiB,GAAG;AACpE,QAAI,CAAC,YAAY,GAAG,EAAG;AACvB,QAAI,SAAS;AACb,SAAK,QAAQ,GAAG;AAChB,QAAI;AACF,YAAM,KAAK,MAAM,iBAAkB,EAAE;AAAA,IACvC,SAAS,OAAO;AACd,UAAI,IAAI,WAAW,YAAY;AAC7B,YAAI,SAAS;AACb,YAAI,QAAQ;AACZ,aAAK,QAAQ,GAAG;AAAA,MAClB;AACA,YAAM;AAAA,IACR;AAAA,EACF;AACF;",
6
+ "names": []
7
+ }
@@ -0,0 +1,15 @@
1
+ import { z } from "zod";
2
+ const startShellSchema = z.object({
3
+ clientOperationId: z.string().uuid(),
4
+ command: z.string().min(1).max(8192).refine((value) => Boolean(value.trim()) && !value.includes("\0"))
5
+ }).strict();
6
+ const shellActive = (command) => command.status === "running" || command.status === "stopping";
7
+ const SHELL_TIMEOUT_MS = 10 * 60 * 1e3;
8
+ const SHELL_OUTPUT_BYTES = 64 * 1024;
9
+ export {
10
+ SHELL_OUTPUT_BYTES,
11
+ SHELL_TIMEOUT_MS,
12
+ shellActive,
13
+ startShellSchema
14
+ };
15
+ //# sourceMappingURL=shell.js.map
@@ -0,0 +1,7 @@
1
+ {
2
+ "version": 3,
3
+ "sources": ["../../src/shared/shell.ts"],
4
+ "sourcesContent": ["import { z } from 'zod';\n\nexport const startShellSchema = z\n .object({\n clientOperationId: z.string().uuid(),\n command: z\n .string()\n .min(1)\n .max(8192)\n .refine((value) => Boolean(value.trim()) && !value.includes('\\0')),\n })\n .strict();\nexport type StartShellInput = z.infer<typeof startShellSchema>;\nexport type ShellStatus =\n 'running' | 'stopping' | 'completed' | 'failed' | 'interrupted' | 'unknown';\nexport interface ShellCommand {\n id: string;\n projectId: string;\n projectName: string;\n cwd: string;\n command: string;\n status: ShellStatus;\n startedAt: number;\n finishedAt?: number;\n exitCode?: number;\n output: { stream: 'stdout' | 'stderr'; text: string }[];\n truncated: boolean;\n error?: string;\n revision: number;\n}\nexport interface ShellSnapshot {\n epoch: string;\n revision: number;\n commands: ShellCommand[];\n}\nexport const shellActive = (command: Pick<ShellCommand, 'status'>) =>\n command.status === 'running' || command.status === 'stopping';\nexport const SHELL_TIMEOUT_MS = 10 * 60 * 1000;\nexport const SHELL_OUTPUT_BYTES = 64 * 1024;\n"],
5
+ "mappings": "AAAA,SAAS,SAAS;AAEX,MAAM,mBAAmB,EAC7B,OAAO;AAAA,EACN,mBAAmB,EAAE,OAAO,EAAE,KAAK;AAAA,EACnC,SAAS,EACN,OAAO,EACP,IAAI,CAAC,EACL,IAAI,IAAI,EACR,OAAO,CAAC,UAAU,QAAQ,MAAM,KAAK,CAAC,KAAK,CAAC,MAAM,SAAS,IAAI,CAAC;AACrE,CAAC,EACA,OAAO;AAwBH,MAAM,cAAc,CAAC,YAC1B,QAAQ,WAAW,aAAa,QAAQ,WAAW;AAC9C,MAAM,mBAAmB,KAAK,KAAK;AACnC,MAAM,qBAAqB,KAAK;",
6
+ "names": []
7
+ }