@engineeros/connector 0.1.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/README.md ADDED
@@ -0,0 +1,25 @@
1
+ # EngineerOS Connector
2
+
3
+ The connector keeps an outbound WebSocket open from a repository-owning machine to EngineerOS and executes assigned Goals with Codex CLI.
4
+
5
+ ## Requirements
6
+
7
+ - Node.js 22 or newer
8
+ - Git
9
+ - Codex CLI installed and authenticated (`codex login`)
10
+
11
+ ## Pair once
12
+
13
+ Open the repository, choose **Connect a machine** in the EngineerOS Goal screen, and run the generated `npx @engineeros/connector pair ...` command. The one-time code expires after 10 minutes. A workspace-scoped connector token is stored below `~/.engineeros/connectors` with owner-only permissions where the operating system supports them.
14
+
15
+ ```shell
16
+ npx --yes @engineeros/connector@latest pair PAIRING-CODE --url https://engineeros.example.com --workspace /path/to/repository
17
+ ```
18
+
19
+ ## Reconnect
20
+
21
+ ```shell
22
+ npx --yes @engineeros/connector@latest start --workspace /path/to/repository
23
+ ```
24
+
25
+ Each Goal runs in an isolated worktree below `~/.engineeros/runs`. Cancellation stops Codex. The connector returns the changed paths, bounded diff, and repository ZIP to EngineerOS; a human still performs independent attestation.
@@ -0,0 +1,287 @@
1
+ #!/usr/bin/env node
2
+ import os from "node:os";
3
+ import path from "node:path";
4
+ import {
5
+ loadConfig,
6
+ resultUrl,
7
+ saveConfig,
8
+ socketUrl,
9
+ } from "../src/config.mjs";
10
+ import { executeAssignment, stopProcess } from "../src/runner.mjs";
11
+
12
+ const { command, positional, flags } = parseArgs(process.argv.slice(2));
13
+
14
+ if (command === "status") {
15
+ const config = await loadConfig(flags.workspace || process.cwd());
16
+ console.log(
17
+ config
18
+ ? `Paired as ${config.name} (${config.connector_id}) for ${config.workspace}`
19
+ : "Not paired",
20
+ );
21
+ process.exit(config ? 0 : 1);
22
+ }
23
+
24
+ let config;
25
+ let firstMessage;
26
+ if (command === "pair") {
27
+ const pairingCode = positional[0];
28
+ if (!pairingCode)
29
+ fail(
30
+ "Usage: engineeros-connector pair CODE --url URL [--name NAME] [--workspace PATH]",
31
+ );
32
+ const url = flags.url;
33
+ if (!url) fail("Pairing requires --url with the EngineerOS backend address.");
34
+ config = {
35
+ server_url: socketUrl(url),
36
+ workspace: path.resolve(flags.workspace || process.cwd()),
37
+ name:
38
+ flags.name ||
39
+ `${os.hostname()} - ${path.basename(path.resolve(flags.workspace || process.cwd()))}`,
40
+ };
41
+ firstMessage = {
42
+ type: "pair",
43
+ pairing_code: pairingCode,
44
+ name: config.name,
45
+ capabilities: {
46
+ codex_cli: true,
47
+ platform: process.platform,
48
+ workspace_name: path.basename(config.workspace),
49
+ },
50
+ };
51
+ } else if (command === "start") {
52
+ config = await loadConfig(flags.workspace || process.cwd());
53
+ if (!config)
54
+ fail(
55
+ "This connector is not paired. Create a pairing command in EngineerOS first.",
56
+ );
57
+ firstMessage = {
58
+ type: "authenticate",
59
+ connector_id: config.connector_id,
60
+ token: config.token,
61
+ };
62
+ } else {
63
+ fail("Use `engineeros-connector pair`, `start`, or `status`.");
64
+ }
65
+
66
+ let stopped = false;
67
+ let active = null;
68
+ const available = [];
69
+ let socket;
70
+ let pingTimer;
71
+ let reconnectDelay = 1_000;
72
+ let connectionRejected = false;
73
+
74
+ process.on("SIGINT", async () => {
75
+ stopped = true;
76
+ await stopProcess(active?.child);
77
+ socket?.close();
78
+ process.exit(0);
79
+ });
80
+
81
+ await connect();
82
+
83
+ async function connect() {
84
+ console.log(`Connecting ${config.name} to ${config.server_url}`);
85
+ connectionRejected = false;
86
+ socket = new WebSocket(config.server_url);
87
+ socket.addEventListener("open", () =>
88
+ socket.send(JSON.stringify(firstMessage)),
89
+ );
90
+ socket.addEventListener("message", async (event) => {
91
+ const message = JSON.parse(String(event.data));
92
+ if (message.type === "paired") {
93
+ config = {
94
+ ...config,
95
+ connector_id: message.connector.id,
96
+ token: message.token,
97
+ };
98
+ await saveConfig(config);
99
+ firstMessage = {
100
+ type: "authenticate",
101
+ connector_id: config.connector_id,
102
+ token: config.token,
103
+ };
104
+ console.log(`Paired. Connector ${config.connector_id} is online.`);
105
+ reconnectDelay = 1_000;
106
+ startPings();
107
+ return;
108
+ }
109
+ if (message.type === "authenticated") {
110
+ console.log("Connected and waiting for EngineerOS runs.");
111
+ reconnectDelay = 1_000;
112
+ startPings();
113
+ return;
114
+ }
115
+ if (message.type === "run.available") {
116
+ if (!available.includes(message.run_id)) available.push(message.run_id);
117
+ pump();
118
+ return;
119
+ }
120
+ if (message.type === "run.assignment") {
121
+ await execute(message);
122
+ return;
123
+ }
124
+ if (message.type === "run.cancelled" && active?.runId === message.run_id) {
125
+ console.log(`Run ${message.run_id} cancelled by EngineerOS.`);
126
+ active.cancelled = true;
127
+ await stopProcess(active.child);
128
+ return;
129
+ }
130
+ if (message.type === "connector.revoked") {
131
+ stopped = true;
132
+ console.error("This connector was revoked in EngineerOS.");
133
+ socket.close();
134
+ return;
135
+ }
136
+ if (message.type === "run.error") {
137
+ console.error(`EngineerOS: ${message.message}`);
138
+ if (active?.runId === message.run_id && !active.child) {
139
+ active = null;
140
+ pump();
141
+ }
142
+ }
143
+ if (message.type === "connection.error") {
144
+ connectionRejected = true;
145
+ console.error(`EngineerOS: ${message.message}`);
146
+ }
147
+ });
148
+ socket.addEventListener("close", () => {
149
+ clearInterval(pingTimer);
150
+ if (connectionRejected) {
151
+ stopped = true;
152
+ console.error(
153
+ "Connection rejected. Create a new pairing command in EngineerOS if this connector was revoked.",
154
+ );
155
+ return;
156
+ }
157
+ if (stopped) return;
158
+ console.error(
159
+ `Connection lost. Retrying in ${Math.round(reconnectDelay / 1_000)}s.`,
160
+ );
161
+ setTimeout(connect, reconnectDelay);
162
+ reconnectDelay = Math.min(30_000, reconnectDelay * 2);
163
+ });
164
+ socket.addEventListener("error", () => {});
165
+ }
166
+
167
+ function startPings() {
168
+ clearInterval(pingTimer);
169
+ pingTimer = setInterval(() => {
170
+ if (socket.readyState === WebSocket.OPEN) {
171
+ socket.send(
172
+ JSON.stringify({
173
+ type: "ping",
174
+ active_run_id: active?.runId ?? null,
175
+ }),
176
+ );
177
+ }
178
+ }, 10_000);
179
+ }
180
+
181
+ function pump() {
182
+ if (active || socket.readyState !== WebSocket.OPEN) return;
183
+ const runId = available.shift();
184
+ if (!runId) return;
185
+ active = { runId, child: null, cancelled: false };
186
+ socket.send(
187
+ JSON.stringify({
188
+ type: "run.claim",
189
+ run_id: runId,
190
+ runner_name: "Codex CLI",
191
+ metadata: {
192
+ hostname: os.hostname(),
193
+ workspace_name: path.basename(config.workspace),
194
+ },
195
+ }),
196
+ );
197
+ }
198
+
199
+ async function execute(assignment) {
200
+ const runId = assignment.run_id;
201
+ console.log(`Running Goal ${runId} with Codex CLI.`);
202
+ let progress = 10;
203
+ const heartbeat = setInterval(() => {
204
+ if (socket.readyState === WebSocket.OPEN && active?.runId === runId) {
205
+ progress = Math.min(90, progress + 5);
206
+ socket.send(
207
+ JSON.stringify({
208
+ type: "run.progress",
209
+ run_id: runId,
210
+ progress_percent: progress,
211
+ message: "Codex is working",
212
+ }),
213
+ );
214
+ }
215
+ }, 15_000);
216
+ try {
217
+ const result = await executeAssignment(assignment, config, {
218
+ onProcess: (child) => {
219
+ if (active?.runId === runId) active.child = child;
220
+ },
221
+ onEvent: (event) => {
222
+ const message =
223
+ event.message || event.item?.text || event.type || "Codex is working";
224
+ if (socket.readyState === WebSocket.OPEN) {
225
+ socket.send(
226
+ JSON.stringify({
227
+ type: "run.progress",
228
+ run_id: runId,
229
+ progress_percent: progress,
230
+ message: String(message).slice(0, 500),
231
+ }),
232
+ );
233
+ }
234
+ },
235
+ });
236
+ if (active?.cancelled) return;
237
+ const response = await fetch(
238
+ resultUrl(config.server_url, config.connector_id, runId),
239
+ {
240
+ method: "POST",
241
+ headers: {
242
+ "Content-Type": "application/json",
243
+ Authorization: `Bearer ${config.token}`,
244
+ },
245
+ body: JSON.stringify(result),
246
+ },
247
+ );
248
+ if (!response.ok)
249
+ throw new Error(
250
+ `EngineerOS rejected the result (${response.status}): ${await response.text()}`,
251
+ );
252
+ console.log(`Run submitted. Review workspace: ${result.run_workspace}`);
253
+ } catch (error) {
254
+ if (!active?.cancelled && socket.readyState === WebSocket.OPEN) {
255
+ socket.send(
256
+ JSON.stringify({
257
+ type: "run.failed",
258
+ run_id: runId,
259
+ message: error instanceof Error ? error.message : String(error),
260
+ }),
261
+ );
262
+ }
263
+ if (!active?.cancelled)
264
+ console.error(error instanceof Error ? error.message : String(error));
265
+ } finally {
266
+ clearInterval(heartbeat);
267
+ active = null;
268
+ pump();
269
+ }
270
+ }
271
+
272
+ function parseArgs(args) {
273
+ const command = args.shift();
274
+ const positional = [];
275
+ const flags = {};
276
+ while (args.length) {
277
+ const value = args.shift();
278
+ if (!value.startsWith("--")) positional.push(value);
279
+ else flags[value.slice(2)] = args.shift();
280
+ }
281
+ return { command, positional, flags };
282
+ }
283
+
284
+ function fail(message) {
285
+ console.error(message);
286
+ process.exit(1);
287
+ }
package/package.json ADDED
@@ -0,0 +1,35 @@
1
+ {
2
+ "name": "@engineeros/connector",
3
+ "version": "0.1.0",
4
+ "description": "Connect a local Codex CLI workspace to EngineerOS over an outbound WebSocket.",
5
+ "private": false,
6
+ "type": "module",
7
+ "license": "UNLICENSED",
8
+ "files": [
9
+ "bin",
10
+ "src",
11
+ "README.md"
12
+ ],
13
+ "bin": {
14
+ "engineeros-connector": "bin/engineeros-connector.mjs"
15
+ },
16
+ "scripts": {
17
+ "start": "node ./bin/engineeros-connector.mjs",
18
+ "test": "node --test",
19
+ "type-check": "node --check ./bin/engineeros-connector.mjs && node --check ./src/runner.mjs"
20
+ },
21
+ "engines": {
22
+ "node": ">=22"
23
+ },
24
+ "repository": {
25
+ "type": "git",
26
+ "url": "git+https://github.com/vinpuli/engineerosv2.git",
27
+ "directory": "packages/engineeros-connector"
28
+ },
29
+ "bugs": {
30
+ "url": "https://github.com/vinpuli/engineerosv2/issues"
31
+ },
32
+ "publishConfig": {
33
+ "access": "public"
34
+ }
35
+ }
package/src/config.mjs ADDED
@@ -0,0 +1,51 @@
1
+ import { createHash } from "node:crypto";
2
+ import { chmod, mkdir, readFile, writeFile } from "node:fs/promises";
3
+ import os from "node:os";
4
+ import path from "node:path";
5
+
6
+ export function configPath(workspace = process.cwd()) {
7
+ const key = createHash("sha256")
8
+ .update(path.resolve(workspace))
9
+ .digest("hex")
10
+ .slice(0, 24);
11
+ return path.join(os.homedir(), ".engineeros", "connectors", `${key}.json`);
12
+ }
13
+
14
+ export function socketUrl(value) {
15
+ const url = new URL(value);
16
+ if (url.protocol === "http:") url.protocol = "ws:";
17
+ if (url.protocol === "https:") url.protocol = "wss:";
18
+ if (!["ws:", "wss:"].includes(url.protocol)) {
19
+ throw new Error("EngineerOS URL must use http, https, ws, or wss.");
20
+ }
21
+ url.pathname = "/api/v1/agent-connectors/ws";
22
+ url.search = "";
23
+ url.hash = "";
24
+ return url.toString();
25
+ }
26
+
27
+ export function resultUrl(websocketUrl, connectorId, runId) {
28
+ const url = new URL(websocketUrl);
29
+ url.protocol = url.protocol === "wss:" ? "https:" : "http:";
30
+ url.pathname = `/api/v1/agent-connectors/${connectorId}/runs/${runId}/result`;
31
+ return url.toString();
32
+ }
33
+
34
+ export async function loadConfig(workspace = process.cwd()) {
35
+ try {
36
+ return JSON.parse(await readFile(configPath(workspace), "utf8"));
37
+ } catch (error) {
38
+ if (error?.code === "ENOENT") return null;
39
+ throw error;
40
+ }
41
+ }
42
+
43
+ export async function saveConfig(config) {
44
+ const target = configPath(config.workspace);
45
+ await mkdir(path.dirname(target), { recursive: true });
46
+ await writeFile(target, `${JSON.stringify(config, null, 2)}\n`, {
47
+ encoding: "utf8",
48
+ mode: 0o600,
49
+ });
50
+ if (process.platform !== "win32") await chmod(target, 0o600);
51
+ }
package/src/runner.mjs ADDED
@@ -0,0 +1,359 @@
1
+ import { createHash } from "node:crypto";
2
+ import { spawn } from "node:child_process";
3
+ import { mkdir, readFile, readdir, stat, writeFile } from "node:fs/promises";
4
+ import os from "node:os";
5
+ import path from "node:path";
6
+ import { promisify } from "node:util";
7
+ import { deflateRaw } from "node:zlib";
8
+
9
+ const deflate = promisify(deflateRaw);
10
+ const MAX_ARCHIVE_BYTES = 25 * 1024 * 1024;
11
+
12
+ export async function executeAssignment(assignment, config, callbacks) {
13
+ const runWorkspace = await prepareRunWorkspace(
14
+ config.workspace,
15
+ assignment.run_id,
16
+ assignment.base_revision,
17
+ );
18
+ const controller = launchCodex(
19
+ runWorkspace,
20
+ assignment.packet_markdown,
21
+ callbacks,
22
+ );
23
+ callbacks.onProcess?.(controller.child);
24
+ await controller.completed;
25
+ const changedFiles = await changedFilePaths(runWorkspace);
26
+ if (!changedFiles.length)
27
+ throw new Error("Codex completed without changing any files.");
28
+ const diffPatch = await boundedDiff(runWorkspace, changedFiles);
29
+ const archive = await repositoryArchive(runWorkspace);
30
+ const revision = `worktree-${createHash("sha256").update(diffPatch).digest("hex").slice(0, 55)}`;
31
+ return {
32
+ head_revision: revision,
33
+ repository_locator: assignment.repository_locator,
34
+ external_reference: `connector:${config.connector_id}/run:${assignment.run_id}`,
35
+ diff_patch: diffPatch,
36
+ changed_files: changedFiles,
37
+ repository_archive_base64: archive.toString("base64"),
38
+ repository_archive_media_type: "application/zip",
39
+ proof_evidence: [],
40
+ };
41
+ }
42
+
43
+ export async function stopProcess(child) {
44
+ if (!child || child.exitCode !== null) return;
45
+ if (process.platform === "win32") {
46
+ await run(
47
+ "taskkill",
48
+ ["/pid", String(child.pid), "/t", "/f"],
49
+ process.cwd(),
50
+ { allowFailure: true },
51
+ );
52
+ } else {
53
+ child.kill("SIGTERM");
54
+ }
55
+ }
56
+
57
+ async function prepareRunWorkspace(workspace, runId, baseRevision) {
58
+ const source = path.resolve(workspace);
59
+ const root = path.join(os.homedir(), ".engineeros", "runs");
60
+ const target = path.join(root, runId);
61
+ await mkdir(root, { recursive: true });
62
+ const isGit =
63
+ (
64
+ await run("git", ["rev-parse", "--is-inside-work-tree"], source, {
65
+ allowFailure: true,
66
+ })
67
+ ).code === 0;
68
+ if (isGit) {
69
+ const exists = await stat(target).then(
70
+ () => true,
71
+ () => false,
72
+ );
73
+ if (!exists) {
74
+ const base = await worktreeBase(source, baseRevision);
75
+ await run("git", ["worktree", "add", "--detach", target, base], source);
76
+ }
77
+ return target;
78
+ }
79
+ const exists = await stat(target).then(
80
+ () => true,
81
+ () => false,
82
+ );
83
+ if (!exists) {
84
+ await mkdir(target, { recursive: true });
85
+ await copyWorkspace(source, target);
86
+ await run("git", ["init"], target);
87
+ await run("git", ["config", "user.name", "EngineerOS Connector"], target);
88
+ await run(
89
+ "git",
90
+ ["config", "user.email", "connector@engineeros.local"],
91
+ target,
92
+ );
93
+ await run("git", ["add", "-A"], target);
94
+ await run(
95
+ "git",
96
+ ["commit", "--allow-empty", "-m", "EngineerOS run baseline"],
97
+ target,
98
+ );
99
+ }
100
+ return target;
101
+ }
102
+
103
+ function launchCodex(workspace, packet, callbacks) {
104
+ const command =
105
+ process.env.CODEX_BIN ||
106
+ (process.platform === "win32" ? "codex.cmd" : "codex");
107
+ const args = [
108
+ "exec",
109
+ "--json",
110
+ "--sandbox",
111
+ "workspace-write",
112
+ "-C",
113
+ workspace,
114
+ "-",
115
+ ];
116
+ const child = spawn(command, args, {
117
+ cwd: workspace,
118
+ env: process.env,
119
+ shell: process.platform === "win32",
120
+ stdio: ["pipe", "pipe", "pipe"],
121
+ });
122
+ const prompt = `${packet}\n\n## EngineerOS execution instruction\n\nImplement this frozen Goal completely in the current run workspace. Run the required verification. Do not commit, push, or modify files outside this workspace. End with a concise result and verification summary.\n`;
123
+ child.stdin.end(prompt);
124
+ let output = "";
125
+ let buffer = "";
126
+ child.stdout.setEncoding("utf8");
127
+ child.stdout.on("data", (chunk) => {
128
+ output += chunk;
129
+ buffer += chunk;
130
+ const lines = buffer.split(/\r?\n/);
131
+ buffer = lines.pop() ?? "";
132
+ for (const line of lines) {
133
+ if (!line.trim()) continue;
134
+ try {
135
+ const event = JSON.parse(line);
136
+ callbacks.onEvent?.(event);
137
+ } catch {
138
+ callbacks.onEvent?.({
139
+ type: "agent.output",
140
+ message: line.slice(0, 500),
141
+ });
142
+ }
143
+ }
144
+ });
145
+ child.stderr.setEncoding("utf8");
146
+ child.stderr.on("data", (chunk) => {
147
+ output += chunk;
148
+ callbacks.onEvent?.({
149
+ type: "agent.stderr",
150
+ message: chunk.trim().slice(0, 500),
151
+ });
152
+ });
153
+ const completed = new Promise((resolve, reject) => {
154
+ child.once("error", reject);
155
+ child.once("close", (code) => {
156
+ if (code === 0) resolve(output.slice(-20_000));
157
+ else
158
+ reject(
159
+ new Error(`Codex exited with code ${code}. ${output.slice(-1_000)}`),
160
+ );
161
+ });
162
+ });
163
+ return { child, completed };
164
+ }
165
+
166
+ async function changedFilePaths(workspace) {
167
+ const tracked = await run(
168
+ "git",
169
+ ["diff", "--name-only", "HEAD", "--"],
170
+ workspace,
171
+ );
172
+ const untracked = await run(
173
+ "git",
174
+ ["ls-files", "--others", "--exclude-standard"],
175
+ workspace,
176
+ );
177
+ return [
178
+ ...new Set(
179
+ `${tracked.stdout}\n${untracked.stdout}`
180
+ .split(/\r?\n/)
181
+ .map(normalizePath)
182
+ .filter(Boolean),
183
+ ),
184
+ ].sort();
185
+ }
186
+
187
+ async function boundedDiff(workspace, changedFiles) {
188
+ const tracked = await run(
189
+ "git",
190
+ ["diff", "--binary", "HEAD", "--"],
191
+ workspace,
192
+ );
193
+ const untracked = new Set(
194
+ (
195
+ await run(
196
+ "git",
197
+ ["ls-files", "--others", "--exclude-standard"],
198
+ workspace,
199
+ )
200
+ ).stdout
201
+ .split(/\r?\n/)
202
+ .map(normalizePath)
203
+ .filter(Boolean),
204
+ );
205
+ const parts = [tracked.stdout];
206
+ for (const relative of changedFiles.filter((item) => untracked.has(item))) {
207
+ const generated = await run(
208
+ "git",
209
+ ["diff", "--no-index", "--binary", "--", "/dev/null", relative],
210
+ workspace,
211
+ {
212
+ allowFailure: true,
213
+ },
214
+ );
215
+ parts.push(generated.stdout);
216
+ }
217
+ const diff = parts.filter(Boolean).join("\n");
218
+ if (!diff.trim()) throw new Error("The run produced no bounded diff.");
219
+ return diff;
220
+ }
221
+
222
+ async function repositoryArchive(workspace) {
223
+ const listed = await run(
224
+ "git",
225
+ ["ls-files", "-co", "--exclude-standard"],
226
+ workspace,
227
+ );
228
+ const files = [
229
+ ...new Set(listed.stdout.split(/\r?\n/).map(normalizePath).filter(Boolean)),
230
+ ].sort();
231
+ return createZip(
232
+ await Promise.all(
233
+ files.map(async (relative) => ({
234
+ name: relative,
235
+ data: await readFile(path.join(workspace, relative)),
236
+ })),
237
+ ),
238
+ );
239
+ }
240
+
241
+ async function worktreeBase(workspace, requested) {
242
+ if (!requested || requested.startsWith("greenfield:")) return "HEAD";
243
+ const exists = await run(
244
+ "git",
245
+ ["cat-file", "-e", `${requested}^{commit}`],
246
+ workspace,
247
+ { allowFailure: true },
248
+ );
249
+ if (exists.code !== 0) {
250
+ throw new Error(
251
+ `The frozen base revision ${requested} is not available in this repository.`,
252
+ );
253
+ }
254
+ return requested;
255
+ }
256
+
257
+ export async function createZip(files) {
258
+ const localParts = [];
259
+ const centralParts = [];
260
+ let offset = 0;
261
+ let total = 0;
262
+ for (const file of files) {
263
+ const name = Buffer.from(normalizePath(file.name), "utf8");
264
+ const data = Buffer.from(file.data);
265
+ total += data.length;
266
+ if (total > MAX_ARCHIVE_BYTES)
267
+ throw new Error("Repository archive exceeds the 25 MB connector limit.");
268
+ const compressed = await deflate(data);
269
+ const crc = crc32(data);
270
+ const local = Buffer.alloc(30);
271
+ local.writeUInt32LE(0x04034b50, 0);
272
+ local.writeUInt16LE(20, 4);
273
+ local.writeUInt16LE(0x0800, 6);
274
+ local.writeUInt16LE(8, 8);
275
+ local.writeUInt32LE(crc, 14);
276
+ local.writeUInt32LE(compressed.length, 18);
277
+ local.writeUInt32LE(data.length, 22);
278
+ local.writeUInt16LE(name.length, 26);
279
+ localParts.push(local, name, compressed);
280
+
281
+ const central = Buffer.alloc(46);
282
+ central.writeUInt32LE(0x02014b50, 0);
283
+ central.writeUInt16LE(20, 4);
284
+ central.writeUInt16LE(20, 6);
285
+ central.writeUInt16LE(0x0800, 8);
286
+ central.writeUInt16LE(8, 10);
287
+ central.writeUInt32LE(crc, 16);
288
+ central.writeUInt32LE(compressed.length, 20);
289
+ central.writeUInt32LE(data.length, 24);
290
+ central.writeUInt16LE(name.length, 28);
291
+ central.writeUInt32LE(offset, 42);
292
+ centralParts.push(central, name);
293
+ offset += local.length + name.length + compressed.length;
294
+ }
295
+ const central = Buffer.concat(centralParts);
296
+ const end = Buffer.alloc(22);
297
+ end.writeUInt32LE(0x06054b50, 0);
298
+ end.writeUInt16LE(files.length, 8);
299
+ end.writeUInt16LE(files.length, 10);
300
+ end.writeUInt32LE(central.length, 12);
301
+ end.writeUInt32LE(offset, 16);
302
+ return Buffer.concat([...localParts, central, end]);
303
+ }
304
+
305
+ async function copyWorkspace(source, target) {
306
+ for (const entry of await readdir(source, { withFileTypes: true })) {
307
+ if ([".git", ".engineeros", "node_modules"].includes(entry.name)) continue;
308
+ const from = path.join(source, entry.name);
309
+ const to = path.join(target, entry.name);
310
+ if (entry.isDirectory()) {
311
+ await mkdir(to, { recursive: true });
312
+ await copyWorkspace(from, to);
313
+ } else if (entry.isFile()) {
314
+ await writeFile(to, await readFile(from));
315
+ }
316
+ }
317
+ }
318
+
319
+ function normalizePath(value) {
320
+ return String(value ?? "")
321
+ .trim()
322
+ .replaceAll("\\", "/")
323
+ .replace(/^\.\//, "");
324
+ }
325
+
326
+ function run(command, args, cwd, options = {}) {
327
+ return new Promise((resolve, reject) => {
328
+ const child = spawn(command, args, {
329
+ cwd,
330
+ shell: false,
331
+ windowsHide: true,
332
+ });
333
+ let stdout = "";
334
+ let stderr = "";
335
+ child.stdout.setEncoding("utf8");
336
+ child.stderr.setEncoding("utf8");
337
+ child.stdout.on("data", (chunk) => (stdout += chunk));
338
+ child.stderr.on("data", (chunk) => (stderr += chunk));
339
+ child.once("error", reject);
340
+ child.once("close", (code) => {
341
+ const result = { code: code ?? 1, stdout, stderr };
342
+ if (code === 0 || options.allowFailure) resolve(result);
343
+ else
344
+ reject(
345
+ new Error(`${command} ${args.join(" ")} failed: ${stderr || stdout}`),
346
+ );
347
+ });
348
+ });
349
+ }
350
+
351
+ function crc32(buffer) {
352
+ let crc = 0xffffffff;
353
+ for (const byte of buffer) {
354
+ crc ^= byte;
355
+ for (let bit = 0; bit < 8; bit += 1)
356
+ crc = (crc >>> 1) ^ (0xedb88320 & -(crc & 1));
357
+ }
358
+ return (crc ^ 0xffffffff) >>> 0;
359
+ }