apphx 0.0.0-stage → 1.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/LICENSE +21 -0
- package/README.md +227 -2
- package/dist/apple-types.js +45 -0
- package/dist/apple-types.js.map +1 -0
- package/dist/apple.js +900 -0
- package/dist/apple.js.map +1 -0
- package/dist/cli.js +414 -0
- package/dist/cli.js.map +1 -0
- package/dist/config.js +80 -0
- package/dist/config.js.map +1 -0
- package/dist/dependencies.js +157 -0
- package/dist/dependencies.js.map +1 -0
- package/dist/export-options.js +49 -0
- package/dist/export-options.js.map +1 -0
- package/dist/flutter.js +171 -0
- package/dist/flutter.js.map +1 -0
- package/dist/inbox.js +128 -0
- package/dist/inbox.js.map +1 -0
- package/dist/integrations.js +435 -0
- package/dist/integrations.js.map +1 -0
- package/dist/preflight.js +43 -0
- package/dist/preflight.js.map +1 -0
- package/dist/process.js +260 -0
- package/dist/process.js.map +1 -0
- package/dist/remote.js +143 -0
- package/dist/remote.js.map +1 -0
- package/dist/requirements.js +37 -0
- package/dist/requirements.js.map +1 -0
- package/dist/runner.js +722 -0
- package/dist/runner.js.map +1 -0
- package/dist/scheduler.js +83 -0
- package/dist/scheduler.js.map +1 -0
- package/dist/schemas.js +232 -0
- package/dist/schemas.js.map +1 -0
- package/dist/store.js +361 -0
- package/dist/store.js.map +1 -0
- package/examples/factory.json +28 -0
- package/examples/luggage.json +39 -0
- package/examples/remote.json +6 -0
- package/package.json +30 -3
- package/skills/apphx/LICENSE +21 -0
- package/skills/apphx/SKILL.md +103 -0
- package/skills/apphx/resources/requirement.json +39 -0
- package/skills/apphx/resources/research-result.json +21 -0
- package/skills/apphx/resources/review-result.json +7 -0
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
import { toError } from "./schemas.js";
|
|
2
|
+
import { run } from "./process.js";
|
|
3
|
+
import { expandArgv } from "./config.js";
|
|
4
|
+
// src/preflight.ts
|
|
5
|
+
function prerequisiteFailure(argv, reason, detail, cause) {
|
|
6
|
+
return Object.assign(new Error(`Prerequisite ${JSON.stringify(argv)} ${detail}. Restore the configured worker environment before retrying.`, { cause }), {
|
|
7
|
+
code: "PREREQUISITE_FAILED",
|
|
8
|
+
prerequisite: {
|
|
9
|
+
argv: [...argv],
|
|
10
|
+
reason
|
|
11
|
+
}
|
|
12
|
+
});
|
|
13
|
+
}
|
|
14
|
+
async function checkPrerequisites(commands, { cwd, env = process.env, run: run$1 = run, signal } = {}) {
|
|
15
|
+
signal?.throwIfAborted();
|
|
16
|
+
for (const argv of commands) {
|
|
17
|
+
signal?.throwIfAborted();
|
|
18
|
+
let expanded;
|
|
19
|
+
try {
|
|
20
|
+
expanded = expandArgv(argv, env);
|
|
21
|
+
} catch (error) {
|
|
22
|
+
if (signal?.aborted || toError(error).code !== "SETUP_REQUIRED") throw error;
|
|
23
|
+
throw prerequisiteFailure(argv, "environment", "requires an allowed environment variable", error);
|
|
24
|
+
}
|
|
25
|
+
let result;
|
|
26
|
+
try {
|
|
27
|
+
result = await run$1(expanded, {
|
|
28
|
+
cwd,
|
|
29
|
+
env,
|
|
30
|
+
signal
|
|
31
|
+
});
|
|
32
|
+
} catch (value) {
|
|
33
|
+
const error = toError(value);
|
|
34
|
+
if (signal?.aborted || error.code !== "ENOENT" || !error.syscall?.startsWith("spawn")) throw value;
|
|
35
|
+
throw prerequisiteFailure(argv, "missing-command", "could not be started because its executable or working directory is missing", value);
|
|
36
|
+
}
|
|
37
|
+
signal?.throwIfAborted();
|
|
38
|
+
if (result.code !== 0) throw prerequisiteFailure(argv, "nonzero-exit", `exited with code ${result.code}`);
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
export { checkPrerequisites };
|
|
42
|
+
|
|
43
|
+
//# sourceMappingURL=preflight.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"preflight.js","names":["runCommand","run"],"sources":["../src/preflight.ts"],"sourcesContent":["import { expandArgv } from './config.js';\nimport { run as runCommand, type Run, type RunResult } from './process.js';\nimport { toError } from './schemas.js';\n\nexport interface PrerequisiteOptions {\n cwd?: string;\n env?: NodeJS.ProcessEnv;\n run?: Run;\n signal?: AbortSignal;\n}\n\nfunction prerequisiteFailure(argv: string[], reason: string, detail: string, cause?: unknown) {\n return Object.assign(new Error(`Prerequisite ${JSON.stringify(argv)} ${detail}. Restore the configured worker environment before retrying.`, { cause }), {\n code: 'PREREQUISITE_FAILED',\n prerequisite: { argv: [...argv], reason },\n });\n}\n\nexport async function checkPrerequisites(commands: string[][], { cwd, env = process.env, run = runCommand, signal }: PrerequisiteOptions = {}): Promise<void> {\n signal?.throwIfAborted();\n for (const argv of commands) {\n signal?.throwIfAborted();\n let expanded: string[];\n try {\n expanded = expandArgv(argv, env);\n } catch (error) {\n if (signal?.aborted || toError(error).code !== 'SETUP_REQUIRED') throw error;\n throw prerequisiteFailure(argv, 'environment', 'requires an allowed environment variable', error);\n }\n let result: RunResult;\n try {\n result = await run(expanded, { cwd, env, signal });\n } catch (value) {\n const error = toError(value);\n if (signal?.aborted || error.code !== 'ENOENT' || !error.syscall?.startsWith('spawn')) throw value;\n throw prerequisiteFailure(argv, 'missing-command', 'could not be started because its executable or working directory is missing', value);\n }\n signal?.throwIfAborted();\n if (result.code !== 0) throw prerequisiteFailure(argv, 'nonzero-exit', `exited with code ${result.code}`);\n }\n}\n"],"mappings":";;;;AAWA,SAAS,oBAAoB,MAAgB,QAAgB,QAAgB,OAAiB;CAC5F,OAAO,OAAO,OAAO,IAAI,MAAM,gBAAgB,KAAK,UAAU,IAAI,EAAE,GAAG,OAAO,+DAA+D,EAAE,MAAM,CAAC,GAAG;EACvJ,MAAM;EACN,cAAc;GAAE,MAAM,CAAC,GAAG,IAAI;GAAG;EAAO;CAC1C,CAAC;AACH;AAEA,eAAsB,mBAAmB,UAAsB,EAAE,KAAK,MAAM,QAAQ,KAAK,KAAA,QAAMA,KAAY,WAAgC,CAAC,GAAkB;CAC5J,QAAQ,eAAe;CACvB,KAAK,MAAM,QAAQ,UAAU;EAC3B,QAAQ,eAAe;EACvB,IAAI;EACJ,IAAI;GACF,WAAW,WAAW,MAAM,GAAG;EACjC,SAAS,OAAO;GACd,IAAI,QAAQ,WAAW,QAAQ,KAAK,CAAC,CAAC,SAAS,kBAAkB,MAAM;GACvE,MAAM,oBAAoB,MAAM,eAAe,4CAA4C,KAAK;EAClG;EACA,IAAI;EACJ,IAAI;GACF,SAAS,MAAMC,MAAI,UAAU;IAAE;IAAK;IAAK;GAAO,CAAC;EACnD,SAAS,OAAO;GACd,MAAM,QAAQ,QAAQ,KAAK;GAC3B,IAAI,QAAQ,WAAW,MAAM,SAAS,YAAY,CAAC,MAAM,SAAS,WAAW,OAAO,GAAG,MAAM;GAC7F,MAAM,oBAAoB,MAAM,mBAAmB,+EAA+E,KAAK;EACzI;EACA,QAAQ,eAAe;EACvB,IAAI,OAAO,SAAS,GAAG,MAAM,oBAAoB,MAAM,gBAAgB,oBAAoB,OAAO,MAAM;CAC1G;AACF"}
|
package/dist/process.js
ADDED
|
@@ -0,0 +1,260 @@
|
|
|
1
|
+
import { toError } from "./schemas.js";
|
|
2
|
+
import { randomUUID } from "node:crypto";
|
|
3
|
+
import { dirname } from "node:path";
|
|
4
|
+
import { spawn, spawnSync } from "node:child_process";
|
|
5
|
+
import { closeSync, mkdirSync, openSync, readFileSync, writeSync } from "node:fs";
|
|
6
|
+
import { setTimeout as setTimeout$1 } from "node:timers/promises";
|
|
7
|
+
// src/process.ts
|
|
8
|
+
const COMMAND_TOKEN = "APPLE_APP_FACTORY_COMMAND_TOKEN";
|
|
9
|
+
const TERMINATION_GRACE_MS = 1e3;
|
|
10
|
+
function groupMembers(pid) {
|
|
11
|
+
if (!Number.isSafeInteger(pid) || pid <= 1) return [];
|
|
12
|
+
try {
|
|
13
|
+
process.kill(process.platform === "win32" ? pid : -pid, 0);
|
|
14
|
+
} catch (error) {
|
|
15
|
+
if (toError(error).code === "ESRCH") return [];
|
|
16
|
+
}
|
|
17
|
+
if (process.platform === "win32") return [pid];
|
|
18
|
+
const result = spawnSync("/bin/ps", [
|
|
19
|
+
"-ax",
|
|
20
|
+
"-o",
|
|
21
|
+
"pid=,pgid=,stat="
|
|
22
|
+
], {
|
|
23
|
+
encoding: "utf8",
|
|
24
|
+
timeout: 1e3,
|
|
25
|
+
maxBuffer: 4194304
|
|
26
|
+
});
|
|
27
|
+
if (result.status !== 0 || result.error) return null;
|
|
28
|
+
const members = [];
|
|
29
|
+
for (const line of result.stdout.split("\n")) {
|
|
30
|
+
const fields = line.trim().split(/\s+/);
|
|
31
|
+
if (Number(fields[1]) === pid && !fields[2]?.startsWith("Z")) members.push(Number(fields[0]));
|
|
32
|
+
}
|
|
33
|
+
return members;
|
|
34
|
+
}
|
|
35
|
+
function processGroupAlive(pid) {
|
|
36
|
+
const members = groupMembers(pid);
|
|
37
|
+
return members === null || members.length > 0;
|
|
38
|
+
}
|
|
39
|
+
function processToken(pid) {
|
|
40
|
+
try {
|
|
41
|
+
if (process.platform === "linux") {
|
|
42
|
+
const prefix = `${COMMAND_TOKEN}=`;
|
|
43
|
+
return readFileSync(`/proc/${pid}/environ`, "utf8").split("\0").find((value) => value.startsWith(prefix))?.slice(prefix.length) ?? "";
|
|
44
|
+
}
|
|
45
|
+
if (process.platform === "darwin") {
|
|
46
|
+
const result = spawnSync("/bin/ps", [
|
|
47
|
+
"-E",
|
|
48
|
+
"-ww",
|
|
49
|
+
"-p",
|
|
50
|
+
String(pid),
|
|
51
|
+
"-o",
|
|
52
|
+
"command="
|
|
53
|
+
], {
|
|
54
|
+
encoding: "utf8",
|
|
55
|
+
timeout: 1e3,
|
|
56
|
+
maxBuffer: 1048576
|
|
57
|
+
});
|
|
58
|
+
if (result.status === 0 && !result.error) return result.stdout.match(new RegExp(`(?:^|\\s)${COMMAND_TOKEN}=([a-f0-9-]+)(?=\\s|$)`))?.[1] ?? "";
|
|
59
|
+
}
|
|
60
|
+
} catch {}
|
|
61
|
+
}
|
|
62
|
+
function inspectProcessGroup(pid, identity) {
|
|
63
|
+
const members = groupMembers(pid);
|
|
64
|
+
if (members?.length === 0) return "gone";
|
|
65
|
+
if (members === null || !identity?.token) return "unknown";
|
|
66
|
+
let readable = false;
|
|
67
|
+
const inspectionDeadline = Date.now() + 1e3;
|
|
68
|
+
for (const member of members) {
|
|
69
|
+
if (Date.now() >= inspectionDeadline) return "unknown";
|
|
70
|
+
const token = processToken(member);
|
|
71
|
+
if (token === identity.token) return "owned";
|
|
72
|
+
if (token !== void 0) readable = true;
|
|
73
|
+
}
|
|
74
|
+
if (!processGroupAlive(pid)) return "gone";
|
|
75
|
+
return readable ? "mismatch" : "unknown";
|
|
76
|
+
}
|
|
77
|
+
function cleanupError(pid, detail) {
|
|
78
|
+
return Object.assign(/* @__PURE__ */ new Error(`Command group ${pid} ${detail}. No replay is allowed; inspect this task's recorded command ownership before retrying.`), { code: "PROCESS_CLEANUP_BLOCKED" });
|
|
79
|
+
}
|
|
80
|
+
function sendSignal(pid, signal) {
|
|
81
|
+
try {
|
|
82
|
+
process.kill(process.platform === "win32" ? pid : -pid, signal);
|
|
83
|
+
} catch (value) {
|
|
84
|
+
const error = toError(value);
|
|
85
|
+
if (error.code !== "ESRCH") throw cleanupError(pid, `could not receive ${signal}: ${error.message}`);
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
function signalOwnedProcessGroup(pid, identity, signal) {
|
|
89
|
+
const ownership = inspectProcessGroup(pid, identity);
|
|
90
|
+
if (ownership === "gone") return false;
|
|
91
|
+
if (ownership !== "owned") throw cleanupError(pid, `has ${ownership === "mismatch" ? "a different process identity" : "unverifiable ownership"}`);
|
|
92
|
+
sendSignal(pid, signal);
|
|
93
|
+
return true;
|
|
94
|
+
}
|
|
95
|
+
async function terminateGroup(pid, identity, leaderAlive) {
|
|
96
|
+
const signal = (value) => {
|
|
97
|
+
if (leaderAlive()) sendSignal(pid, value);
|
|
98
|
+
else signalOwnedProcessGroup(pid, identity, value);
|
|
99
|
+
};
|
|
100
|
+
for (const value of ["SIGTERM", "SIGKILL"]) {
|
|
101
|
+
if (!processGroupAlive(pid)) return;
|
|
102
|
+
signal(value);
|
|
103
|
+
const deadline = Date.now() + TERMINATION_GRACE_MS;
|
|
104
|
+
do {
|
|
105
|
+
if (!processGroupAlive(pid)) return;
|
|
106
|
+
await setTimeout$1(50);
|
|
107
|
+
} while (Date.now() < deadline);
|
|
108
|
+
}
|
|
109
|
+
if (processGroupAlive(pid)) throw cleanupError(pid, "is still active after SIGKILL");
|
|
110
|
+
}
|
|
111
|
+
async function terminateOwnedProcessGroup(pid, identity) {
|
|
112
|
+
await terminateGroup(pid, identity, () => false);
|
|
113
|
+
}
|
|
114
|
+
function executionEnvironment(config, source = process.env) {
|
|
115
|
+
const env = {};
|
|
116
|
+
for (const key of [
|
|
117
|
+
"PATH",
|
|
118
|
+
"HOME",
|
|
119
|
+
"USER",
|
|
120
|
+
"LOGNAME",
|
|
121
|
+
"TMPDIR",
|
|
122
|
+
"LANG",
|
|
123
|
+
"LC_ALL",
|
|
124
|
+
"TERM",
|
|
125
|
+
...config.executionEnv
|
|
126
|
+
]) if (source[key] !== void 0 && !/^(ASC_|APPLE_|FASTLANE_|MATCH_)/.test(key)) env[key] = source[key];
|
|
127
|
+
env.CI = "1";
|
|
128
|
+
return env;
|
|
129
|
+
}
|
|
130
|
+
async function run(argv, { cwd, env = process.env, input, logPath, timeoutMs = 6e5, signal, onSpawn } = {}) {
|
|
131
|
+
if (!Array.isArray(argv) || !argv.length || argv.some((value) => typeof value !== "string")) throw new Error("Command must be a non-empty argv array");
|
|
132
|
+
const executable = argv[0];
|
|
133
|
+
if (executable === void 0) throw new Error("Command must be a non-empty argv array");
|
|
134
|
+
if (!Number.isFinite(timeoutMs) || timeoutMs <= 0) throw new Error("Command timeoutMs must be positive and finite");
|
|
135
|
+
signal?.throwIfAborted();
|
|
136
|
+
const deadline = Date.now() + timeoutMs;
|
|
137
|
+
const identity = { token: randomUUID() };
|
|
138
|
+
let fd;
|
|
139
|
+
if (logPath) {
|
|
140
|
+
mkdirSync(dirname(logPath), {
|
|
141
|
+
recursive: true,
|
|
142
|
+
mode: 448
|
|
143
|
+
});
|
|
144
|
+
fd = openSync(logPath, "a", 384);
|
|
145
|
+
}
|
|
146
|
+
try {
|
|
147
|
+
const { promise, resolve, reject } = Promise.withResolvers();
|
|
148
|
+
const child = spawn(executable, argv.slice(1), {
|
|
149
|
+
cwd,
|
|
150
|
+
env: {
|
|
151
|
+
...env,
|
|
152
|
+
[COMMAND_TOKEN]: identity.token
|
|
153
|
+
},
|
|
154
|
+
detached: process.platform !== "win32",
|
|
155
|
+
stdio: [
|
|
156
|
+
"pipe",
|
|
157
|
+
"pipe",
|
|
158
|
+
"pipe"
|
|
159
|
+
],
|
|
160
|
+
shell: false
|
|
161
|
+
});
|
|
162
|
+
let stdout = "", stderr = "";
|
|
163
|
+
let failure, exitCode;
|
|
164
|
+
let exited = false, closed = false, cleanupStarted = false, cleanupComplete = false, cleanupFailed = false, settled = false;
|
|
165
|
+
const finish = () => {
|
|
166
|
+
if (settled || !cleanupComplete || !closed && !cleanupFailed) return;
|
|
167
|
+
settled = true;
|
|
168
|
+
clearTimeout(timer);
|
|
169
|
+
clearTimeout(drainTimer);
|
|
170
|
+
signal?.removeEventListener("abort", onAbort);
|
|
171
|
+
if (failure) reject(failure);
|
|
172
|
+
else resolve({
|
|
173
|
+
code: exitCode ?? 1,
|
|
174
|
+
stdout,
|
|
175
|
+
stderr
|
|
176
|
+
});
|
|
177
|
+
};
|
|
178
|
+
const failCleanup = (error) => {
|
|
179
|
+
failure = error;
|
|
180
|
+
cleanupFailed = true;
|
|
181
|
+
child.unref();
|
|
182
|
+
child.stdin.destroy();
|
|
183
|
+
child.stdout.destroy();
|
|
184
|
+
child.stderr.destroy();
|
|
185
|
+
finish();
|
|
186
|
+
};
|
|
187
|
+
const cleanup = () => {
|
|
188
|
+
if (cleanupStarted) return;
|
|
189
|
+
cleanupStarted = true;
|
|
190
|
+
clearTimeout(timer);
|
|
191
|
+
(child.pid ? terminateGroup(child.pid, identity, () => !exited) : Promise.resolve()).catch(failCleanup).finally(() => {
|
|
192
|
+
cleanupComplete = true;
|
|
193
|
+
const pid = child.pid;
|
|
194
|
+
if (pid && !closed && !cleanupFailed) drainTimer = setTimeout(() => failCleanup(cleanupError(pid, "left stdout or stderr open after its verified process group exited; processes outside that group were not signaled")), TERMINATION_GRACE_MS);
|
|
195
|
+
finish();
|
|
196
|
+
});
|
|
197
|
+
};
|
|
198
|
+
const onAbort = () => {
|
|
199
|
+
failure ??= /* @__PURE__ */ new Error("Command interrupted; its task remains resumable");
|
|
200
|
+
cleanup();
|
|
201
|
+
};
|
|
202
|
+
let timer, drainTimer;
|
|
203
|
+
const onDeadline = () => {
|
|
204
|
+
const remaining = deadline - Date.now();
|
|
205
|
+
if (remaining > 0) timer = setTimeout(onDeadline, Math.min(remaining, 2147483647));
|
|
206
|
+
else {
|
|
207
|
+
failure ??= /* @__PURE__ */ new Error(`Command timed out after ${timeoutMs} ms: ${executable}`);
|
|
208
|
+
cleanup();
|
|
209
|
+
}
|
|
210
|
+
};
|
|
211
|
+
timer = setTimeout(onDeadline, Math.min(timeoutMs, 2147483647));
|
|
212
|
+
signal?.addEventListener("abort", onAbort, { once: true });
|
|
213
|
+
child.stdout.on("data", (chunk) => {
|
|
214
|
+
stdout = (stdout + chunk.toString()).slice(-262144);
|
|
215
|
+
if (fd !== void 0) writeSync(fd, chunk);
|
|
216
|
+
});
|
|
217
|
+
child.stderr.on("data", (chunk) => {
|
|
218
|
+
stderr = (stderr + chunk.toString()).slice(-262144);
|
|
219
|
+
if (fd !== void 0) writeSync(fd, chunk);
|
|
220
|
+
});
|
|
221
|
+
child.stdin.on("error", (error) => {
|
|
222
|
+
if (toError(error).code !== "EPIPE") {
|
|
223
|
+
failure ??= error;
|
|
224
|
+
cleanup();
|
|
225
|
+
}
|
|
226
|
+
});
|
|
227
|
+
child.on("error", (error) => {
|
|
228
|
+
failure ??= error;
|
|
229
|
+
cleanup();
|
|
230
|
+
});
|
|
231
|
+
child.on("exit", (code) => {
|
|
232
|
+
exited = true;
|
|
233
|
+
exitCode = code;
|
|
234
|
+
cleanup();
|
|
235
|
+
});
|
|
236
|
+
child.on("close", (code) => {
|
|
237
|
+
closed = true;
|
|
238
|
+
exitCode ??= code;
|
|
239
|
+
cleanup();
|
|
240
|
+
finish();
|
|
241
|
+
});
|
|
242
|
+
if (child.pid && onSpawn) try {
|
|
243
|
+
onSpawn(child.pid, {
|
|
244
|
+
deadline,
|
|
245
|
+
identity
|
|
246
|
+
});
|
|
247
|
+
} catch (error) {
|
|
248
|
+
failure = error;
|
|
249
|
+
cleanup();
|
|
250
|
+
}
|
|
251
|
+
if (signal?.aborted) onAbort();
|
|
252
|
+
child.stdin.end(input ?? "");
|
|
253
|
+
return await promise;
|
|
254
|
+
} finally {
|
|
255
|
+
if (fd !== void 0) closeSync(fd);
|
|
256
|
+
}
|
|
257
|
+
}
|
|
258
|
+
export { TERMINATION_GRACE_MS, executionEnvironment, inspectProcessGroup, processGroupAlive, run, signalOwnedProcessGroup, terminateOwnedProcessGroup };
|
|
259
|
+
|
|
260
|
+
//# sourceMappingURL=process.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"process.js","names":["sleep"],"sources":["../src/process.ts"],"sourcesContent":["import { spawn, spawnSync } from 'node:child_process';\nimport { mkdirSync, openSync, writeSync, closeSync, readFileSync } from 'node:fs';\nimport { dirname } from 'node:path';\nimport { randomUUID } from 'node:crypto';\nimport { setTimeout as sleep } from 'node:timers/promises';\nimport { toError, type CommandMetadata, type Config, type ProcessIdentity } from './schemas.js';\n\nexport interface RunOptions {\n cwd?: string;\n env?: NodeJS.ProcessEnv;\n input?: string;\n logPath?: string;\n timeoutMs?: number;\n signal?: AbortSignal;\n onSpawn?: (pid: number, metadata: CommandMetadata) => void;\n}\n\nexport interface RunResult {\n code: number;\n stdout: string;\n stderr: string;\n}\n\nexport type Run = (argv: string[], options?: RunOptions) => Promise<RunResult>;\n\nconst OUTPUT_LIMIT = 262144;\nconst COMMAND_TOKEN = 'APPLE_APP_FACTORY_COMMAND_TOKEN';\nexport const TERMINATION_GRACE_MS = 1000;\n\nfunction groupMembers(pid: number): number[] | null {\n if (!Number.isSafeInteger(pid) || pid <= 1) return [];\n try { process.kill(process.platform === 'win32' ? pid : -pid, 0); }\n catch (error) { if (toError(error).code === 'ESRCH') return []; }\n if (process.platform === 'win32') return [pid];\n const result = spawnSync('/bin/ps', ['-ax', '-o', 'pid=,pgid=,stat='], { encoding: 'utf8', timeout: 1000, maxBuffer: 4194304 });\n if (result.status !== 0 || result.error) return null;\n const members: number[] = [];\n for (const line of result.stdout.split('\\n')) {\n const fields = line.trim().split(/\\s+/);\n if (Number(fields[1]) === pid && !fields[2]?.startsWith('Z')) members.push(Number(fields[0]));\n }\n return members;\n}\n\nexport function processGroupAlive(pid: number): boolean {\n const members = groupMembers(pid);\n return members === null || members.length > 0;\n}\n\nfunction processToken(pid: number): string | undefined {\n try {\n if (process.platform === 'linux') {\n const prefix = `${COMMAND_TOKEN}=`;\n return readFileSync(`/proc/${pid}/environ`, 'utf8').split('\\0').find(value => value.startsWith(prefix))?.slice(prefix.length) ?? '';\n }\n if (process.platform === 'darwin') {\n const result = spawnSync('/bin/ps', ['-E', '-ww', '-p', String(pid), '-o', 'command='], { encoding: 'utf8', timeout: 1000, maxBuffer: 1048576 });\n if (result.status === 0 && !result.error) return result.stdout.match(new RegExp(`(?:^|\\\\s)${COMMAND_TOKEN}=([a-f0-9-]+)(?=\\\\s|$)`))?.[1] ?? '';\n }\n } catch {}\n return undefined;\n}\n\nexport function inspectProcessGroup(pid: number, identity?: ProcessIdentity): 'gone' | 'unknown' | 'owned' | 'mismatch' {\n const members = groupMembers(pid);\n if (members?.length === 0) return 'gone';\n if (members === null || !identity?.token) return 'unknown';\n let readable = false;\n const inspectionDeadline = Date.now() + 1000;\n for (const member of members) {\n if (Date.now() >= inspectionDeadline) return 'unknown';\n const token = processToken(member);\n if (token === identity.token) return 'owned';\n if (token !== undefined) readable = true;\n }\n if (!processGroupAlive(pid)) return 'gone';\n return readable ? 'mismatch' : 'unknown';\n}\n\nfunction cleanupError(pid: number, detail: string): Error & { code: string } {\n return Object.assign(new Error(`Command group ${pid} ${detail}. No replay is allowed; inspect this task's recorded command ownership before retrying.`), { code: 'PROCESS_CLEANUP_BLOCKED' });\n}\n\nfunction sendSignal(pid: number, signal: NodeJS.Signals): void {\n try { process.kill(process.platform === 'win32' ? pid : -pid, signal); }\n catch (value) {\n const error = toError(value);\n if (error.code !== 'ESRCH') throw cleanupError(pid, `could not receive ${signal}: ${error.message}`);\n }\n}\n\nexport function signalOwnedProcessGroup(pid: number, identity: ProcessIdentity | undefined, signal: NodeJS.Signals): boolean {\n const ownership = inspectProcessGroup(pid, identity);\n if (ownership === 'gone') return false;\n if (ownership !== 'owned') throw cleanupError(pid, `has ${ownership === 'mismatch' ? 'a different process identity' : 'unverifiable ownership'}`);\n sendSignal(pid, signal);\n return true;\n}\n\nasync function terminateGroup(pid: number, identity: ProcessIdentity | undefined, leaderAlive: () => boolean): Promise<void> {\n const signal = (value: NodeJS.Signals): void => {\n if (leaderAlive()) sendSignal(pid, value);\n else signalOwnedProcessGroup(pid, identity, value);\n };\n const signals: NodeJS.Signals[] = ['SIGTERM', 'SIGKILL'];\n for (const value of signals) {\n if (!processGroupAlive(pid)) return;\n signal(value);\n const deadline = Date.now() + TERMINATION_GRACE_MS;\n do {\n if (!processGroupAlive(pid)) return;\n await sleep(50);\n } while (Date.now() < deadline);\n }\n if (processGroupAlive(pid)) throw cleanupError(pid, 'is still active after SIGKILL');\n}\n\nexport async function terminateOwnedProcessGroup(pid: number, identity: ProcessIdentity | undefined): Promise<void> {\n await terminateGroup(pid, identity, () => false);\n}\n\nexport function executionEnvironment(config: Pick<Config, 'executionEnv'>, source: NodeJS.ProcessEnv = process.env): NodeJS.ProcessEnv {\n const env: NodeJS.ProcessEnv = {};\n for (const key of ['PATH', 'HOME', 'USER', 'LOGNAME', 'TMPDIR', 'LANG', 'LC_ALL', 'TERM', ...config.executionEnv]) {\n if (source[key] !== undefined && !/^(ASC_|APPLE_|FASTLANE_|MATCH_)/.test(key)) env[key] = source[key];\n }\n env.CI = '1';\n return env;\n}\n\nexport async function run(argv: string[], { cwd, env = process.env, input, logPath, timeoutMs = 600000, signal, onSpawn }: RunOptions = {}): Promise<RunResult> {\n if (!Array.isArray(argv) || !argv.length || argv.some(value => typeof value !== 'string')) throw new Error('Command must be a non-empty argv array');\n const executable = argv[0];\n if (executable === undefined) throw new Error('Command must be a non-empty argv array');\n if (!Number.isFinite(timeoutMs) || timeoutMs <= 0) throw new Error('Command timeoutMs must be positive and finite');\n signal?.throwIfAborted();\n const deadline = Date.now() + timeoutMs;\n const identity: ProcessIdentity = { token: randomUUID() };\n let fd: number | undefined;\n if (logPath) {\n mkdirSync(dirname(logPath), { recursive: true, mode: 0o700 });\n fd = openSync(logPath, 'a', 0o600);\n }\n try {\n const { promise, resolve, reject } = Promise.withResolvers<RunResult>();\n const child = spawn(executable, argv.slice(1), { cwd, env: { ...env, [COMMAND_TOKEN]: identity.token }, detached: process.platform !== 'win32', stdio: ['pipe', 'pipe', 'pipe'], shell: false });\n let stdout = '', stderr = '';\n let failure: unknown, exitCode: number | null | undefined;\n let exited = false, closed = false, cleanupStarted = false, cleanupComplete = false, cleanupFailed = false, settled = false;\n const finish = (): void => {\n if (settled || !cleanupComplete || (!closed && !cleanupFailed)) return;\n settled = true;\n clearTimeout(timer);\n clearTimeout(drainTimer);\n signal?.removeEventListener('abort', onAbort);\n if (failure) reject(failure);\n else resolve({ code: exitCode ?? 1, stdout, stderr });\n };\n const failCleanup = (error: unknown): void => {\n failure = error;\n cleanupFailed = true;\n child.unref();\n child.stdin.destroy();\n child.stdout.destroy();\n child.stderr.destroy();\n finish();\n };\n const cleanup = (): void => {\n if (cleanupStarted) return;\n cleanupStarted = true;\n clearTimeout(timer);\n const pending = child.pid ? terminateGroup(child.pid, identity, () => !exited) : Promise.resolve();\n pending.catch(failCleanup).finally(() => {\n cleanupComplete = true;\n const pid = child.pid;\n if (pid && !closed && !cleanupFailed) {\n drainTimer = setTimeout(() => failCleanup(cleanupError(pid, 'left stdout or stderr open after its verified process group exited; processes outside that group were not signaled')), TERMINATION_GRACE_MS);\n }\n finish();\n });\n };\n const onAbort = (): void => { failure ??= new Error('Command interrupted; its task remains resumable'); cleanup(); };\n let timer: NodeJS.Timeout | undefined, drainTimer: NodeJS.Timeout | undefined;\n const onDeadline = (): void => {\n const remaining = deadline - Date.now();\n if (remaining > 0) timer = setTimeout(onDeadline, Math.min(remaining, 2147483647));\n else { failure ??= new Error(`Command timed out after ${timeoutMs} ms: ${executable}`); cleanup(); }\n };\n timer = setTimeout(onDeadline, Math.min(timeoutMs, 2147483647));\n signal?.addEventListener('abort', onAbort, { once: true });\n child.stdout.on('data', (chunk: Buffer) => {\n stdout = (stdout + chunk.toString()).slice(-OUTPUT_LIMIT);\n if (fd !== undefined) writeSync(fd, chunk);\n });\n child.stderr.on('data', (chunk: Buffer) => {\n stderr = (stderr + chunk.toString()).slice(-OUTPUT_LIMIT);\n if (fd !== undefined) writeSync(fd, chunk);\n });\n child.stdin.on('error', (error: Error) => { if (toError(error).code !== 'EPIPE') { failure ??= error; cleanup(); } });\n child.on('error', error => { failure ??= error; cleanup(); });\n child.on('exit', code => { exited = true; exitCode = code; cleanup(); });\n child.on('close', code => { closed = true; exitCode ??= code; cleanup(); finish(); });\n if (child.pid && onSpawn) {\n try { onSpawn(child.pid, { deadline, identity }); }\n catch (error) { failure = error; cleanup(); }\n }\n if (signal?.aborted) onAbort();\n child.stdin.end(input ?? '');\n return await promise;\n } finally { if (fd !== undefined) closeSync(fd); }\n}\n"],"mappings":"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"}
|
package/dist/remote.js
ADDED
|
@@ -0,0 +1,143 @@
|
|
|
1
|
+
import { toError } from "./schemas.js";
|
|
2
|
+
import { loadJSON } from "./config.js";
|
|
3
|
+
import { parseRequirement } from "./requirements.js";
|
|
4
|
+
import { z } from "zod";
|
|
5
|
+
import { posix } from "node:path";
|
|
6
|
+
import { spawn } from "node:child_process";
|
|
7
|
+
// src/remote.ts
|
|
8
|
+
const remotePath = z.string().min(1).refine((value) => posix.isAbsolute(value) && !value.includes("\0") && !/[\r\n]/.test(value), "Use an absolute path on the Mac worker");
|
|
9
|
+
const RemoteSchema = z.strictObject({
|
|
10
|
+
host: z.string().regex(/^(?:[A-Za-z0-9_.-]+@)?[A-Za-z0-9][A-Za-z0-9_.-]*$/),
|
|
11
|
+
directory: remotePath,
|
|
12
|
+
executable: remotePath,
|
|
13
|
+
config: remotePath
|
|
14
|
+
});
|
|
15
|
+
const quote = (value) => `'${value.replaceAll("'", "'\\''")}'`;
|
|
16
|
+
function remoteCommand(endpoint, args) {
|
|
17
|
+
const remote = RemoteSchema.parse(endpoint);
|
|
18
|
+
const [action, ...rest] = args;
|
|
19
|
+
let input;
|
|
20
|
+
let forwarded;
|
|
21
|
+
switch (action) {
|
|
22
|
+
case "enqueue":
|
|
23
|
+
case "submit": {
|
|
24
|
+
if (rest.length !== 1) throw new Error(`remote ${action} requires one local requirements JSON file`);
|
|
25
|
+
const { spec, baseJobId } = parseRequirement(loadJSON(rest[0]));
|
|
26
|
+
input = JSON.stringify({
|
|
27
|
+
...spec,
|
|
28
|
+
...baseJobId ? { baseJobId } : {}
|
|
29
|
+
});
|
|
30
|
+
forwarded = [action, "-"];
|
|
31
|
+
break;
|
|
32
|
+
}
|
|
33
|
+
case "status":
|
|
34
|
+
if (rest.length > 1) throw new Error("remote status accepts at most one task ID");
|
|
35
|
+
forwarded = [action, ...rest];
|
|
36
|
+
break;
|
|
37
|
+
case "events":
|
|
38
|
+
case "retry":
|
|
39
|
+
case "next":
|
|
40
|
+
case "step":
|
|
41
|
+
if (rest.length !== 1 || !rest[0]) throw new Error(`remote ${action} requires one task ID`);
|
|
42
|
+
forwarded = [action, ...rest];
|
|
43
|
+
break;
|
|
44
|
+
case "complete":
|
|
45
|
+
if (rest.length < 2 || rest.length > 3 || !rest[0] || !rest[1]) throw new Error("remote complete requires a task ID, turn ID and optional local result JSON file");
|
|
46
|
+
if (rest[2]) input = JSON.stringify(loadJSON(rest[2]));
|
|
47
|
+
forwarded = [
|
|
48
|
+
action,
|
|
49
|
+
rest[0],
|
|
50
|
+
rest[1],
|
|
51
|
+
...rest[2] ? ["-"] : []
|
|
52
|
+
];
|
|
53
|
+
break;
|
|
54
|
+
case "abandon":
|
|
55
|
+
if (rest.length !== 2 || !rest[0] || !rest[1]) throw new Error("remote abandon requires a task ID and turn ID");
|
|
56
|
+
forwarded = [action, ...rest];
|
|
57
|
+
break;
|
|
58
|
+
case "scan":
|
|
59
|
+
if (rest.length > 1 || rest.length === 1 && rest[0] !== "--observe") throw new Error("remote scan accepts only --observe");
|
|
60
|
+
forwarded = [action, ...rest];
|
|
61
|
+
break;
|
|
62
|
+
case "inbox":
|
|
63
|
+
case "doctor":
|
|
64
|
+
if (rest.length) throw new Error(`remote ${action} accepts no arguments`);
|
|
65
|
+
forwarded = [action];
|
|
66
|
+
break;
|
|
67
|
+
default: throw new Error("remote supports enqueue, submit, scan, inbox, status, events, retry, next, complete, abandon, step and doctor; publication commands are forbidden");
|
|
68
|
+
}
|
|
69
|
+
const command = `cd -- ${quote(remote.directory)} && exec ${[
|
|
70
|
+
remote.executable,
|
|
71
|
+
"--build-only",
|
|
72
|
+
"--config",
|
|
73
|
+
remote.config,
|
|
74
|
+
...forwarded
|
|
75
|
+
].map(quote).join(" ")}`;
|
|
76
|
+
return {
|
|
77
|
+
argv: [
|
|
78
|
+
"ssh",
|
|
79
|
+
"-T",
|
|
80
|
+
"-o",
|
|
81
|
+
"BatchMode=yes",
|
|
82
|
+
"-o",
|
|
83
|
+
"StrictHostKeyChecking=yes",
|
|
84
|
+
"-o",
|
|
85
|
+
"PasswordAuthentication=no",
|
|
86
|
+
"-o",
|
|
87
|
+
"KbdInteractiveAuthentication=no",
|
|
88
|
+
"-o",
|
|
89
|
+
"ForwardAgent=no",
|
|
90
|
+
"-o",
|
|
91
|
+
"ClearAllForwardings=yes",
|
|
92
|
+
"-o",
|
|
93
|
+
"SendEnv=-*",
|
|
94
|
+
"-o",
|
|
95
|
+
"ConnectTimeout=10",
|
|
96
|
+
"-o",
|
|
97
|
+
"ServerAliveInterval=15",
|
|
98
|
+
"-o",
|
|
99
|
+
"ServerAliveCountMax=3",
|
|
100
|
+
"--",
|
|
101
|
+
remote.host,
|
|
102
|
+
command
|
|
103
|
+
],
|
|
104
|
+
input
|
|
105
|
+
};
|
|
106
|
+
}
|
|
107
|
+
async function dispatchRemote(endpoint, args) {
|
|
108
|
+
const command = remoteCommand(endpoint, args);
|
|
109
|
+
const { promise, resolve, reject } = Promise.withResolvers();
|
|
110
|
+
const child = spawn(command.argv[0], command.argv.slice(1), {
|
|
111
|
+
stdio: [
|
|
112
|
+
"pipe",
|
|
113
|
+
"inherit",
|
|
114
|
+
"inherit"
|
|
115
|
+
],
|
|
116
|
+
shell: false
|
|
117
|
+
});
|
|
118
|
+
const interrupt = () => {
|
|
119
|
+
child.kill("SIGTERM");
|
|
120
|
+
};
|
|
121
|
+
process.on("SIGINT", interrupt);
|
|
122
|
+
process.on("SIGTERM", interrupt);
|
|
123
|
+
const cleanup = () => {
|
|
124
|
+
process.removeListener("SIGINT", interrupt);
|
|
125
|
+
process.removeListener("SIGTERM", interrupt);
|
|
126
|
+
};
|
|
127
|
+
child.once("error", (error) => {
|
|
128
|
+
cleanup();
|
|
129
|
+
reject(error);
|
|
130
|
+
});
|
|
131
|
+
child.stdin.on("error", (error) => {
|
|
132
|
+
if (toError(error).code !== "EPIPE") child.kill("SIGTERM");
|
|
133
|
+
});
|
|
134
|
+
child.once("close", (code, signal) => {
|
|
135
|
+
cleanup();
|
|
136
|
+
resolve(code ?? (signal ? 128 : 1));
|
|
137
|
+
});
|
|
138
|
+
child.stdin.end(command.input ?? "");
|
|
139
|
+
return promise;
|
|
140
|
+
}
|
|
141
|
+
export { RemoteSchema, dispatchRemote, remoteCommand };
|
|
142
|
+
|
|
143
|
+
//# sourceMappingURL=remote.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"remote.js","names":[],"sources":["../src/remote.ts"],"sourcesContent":["import { spawn } from 'node:child_process';\nimport { posix } from 'node:path';\nimport { z } from 'zod';\nimport { loadJSON } from './config.js';\nimport { parseRequirement } from './requirements.js';\nimport { toError } from './schemas.js';\n\nconst remotePath = z.string().min(1).refine(value => posix.isAbsolute(value) && !value.includes('\\0') && !/[\\r\\n]/.test(value), 'Use an absolute path on the Mac worker');\nexport const RemoteSchema = z.strictObject({\n host: z.string().regex(/^(?:[A-Za-z0-9_.-]+@)?[A-Za-z0-9][A-Za-z0-9_.-]*$/),\n directory: remotePath,\n executable: remotePath,\n config: remotePath,\n});\nexport type Remote = z.infer<typeof RemoteSchema>;\nconst quote = (value: string): string => `'${value.replaceAll(\"'\", \"'\\\\''\")}'`;\n\nexport function remoteCommand(endpoint: unknown, args: string[]): { argv: string[]; input?: string } {\n const remote = RemoteSchema.parse(endpoint);\n const [action, ...rest] = args;\n let input: string | undefined;\n let forwarded: string[];\n switch (action) {\n case 'enqueue':\n case 'submit': {\n if (rest.length !== 1) throw new Error(`remote ${action} requires one local requirements JSON file`);\n const { spec, baseJobId } = parseRequirement(loadJSON(rest[0]));\n input = JSON.stringify({ ...spec, ...(baseJobId ? { baseJobId } : {}) });\n forwarded = [action, '-'];\n break;\n }\n case 'status':\n if (rest.length > 1) throw new Error('remote status accepts at most one task ID');\n forwarded = [action, ...rest];\n break;\n case 'events':\n case 'retry':\n case 'next':\n case 'step':\n if (rest.length !== 1 || !rest[0]) throw new Error(`remote ${action} requires one task ID`);\n forwarded = [action, ...rest];\n break;\n case 'complete':\n if (rest.length < 2 || rest.length > 3 || !rest[0] || !rest[1]) throw new Error('remote complete requires a task ID, turn ID and optional local result JSON file');\n if (rest[2]) input = JSON.stringify(loadJSON(rest[2]));\n forwarded = [action, rest[0], rest[1], ...(rest[2] ? ['-'] : [])];\n break;\n case 'abandon':\n if (rest.length !== 2 || !rest[0] || !rest[1]) throw new Error('remote abandon requires a task ID and turn ID');\n forwarded = [action, ...rest];\n break;\n case 'scan':\n if (rest.length > 1 || (rest.length === 1 && rest[0] !== '--observe')) throw new Error('remote scan accepts only --observe');\n forwarded = [action, ...rest];\n break;\n case 'inbox':\n case 'doctor':\n if (rest.length) throw new Error(`remote ${action} accepts no arguments`);\n forwarded = [action];\n break;\n default:\n throw new Error('remote supports enqueue, submit, scan, inbox, status, events, retry, next, complete, abandon, step and doctor; publication commands are forbidden');\n }\n const command = `cd -- ${quote(remote.directory)} && exec ${[remote.executable, '--build-only', '--config', remote.config, ...forwarded].map(quote).join(' ')}`;\n return {\n argv: ['ssh', '-T', '-o', 'BatchMode=yes', '-o', 'StrictHostKeyChecking=yes', '-o', 'PasswordAuthentication=no', '-o', 'KbdInteractiveAuthentication=no', '-o', 'ForwardAgent=no', '-o', 'ClearAllForwardings=yes', '-o', 'SendEnv=-*', '-o', 'ConnectTimeout=10', '-o', 'ServerAliveInterval=15', '-o', 'ServerAliveCountMax=3', '--', remote.host, command],\n input,\n };\n}\n\nexport async function dispatchRemote(endpoint: unknown, args: string[]): Promise<number> {\n const command = remoteCommand(endpoint, args);\n const { promise, resolve, reject } = Promise.withResolvers<number>();\n const child = spawn(command.argv[0], command.argv.slice(1), { stdio: ['pipe', 'inherit', 'inherit'], shell: false });\n const interrupt = (): void => { child.kill('SIGTERM'); };\n process.on('SIGINT', interrupt);\n process.on('SIGTERM', interrupt);\n const cleanup = (): void => { process.removeListener('SIGINT', interrupt); process.removeListener('SIGTERM', interrupt); };\n child.once('error', error => { cleanup(); reject(error); });\n child.stdin.on('error', error => { if (toError(error).code !== 'EPIPE') child.kill('SIGTERM'); });\n child.once('close', (code, signal) => { cleanup(); resolve(code ?? (signal ? 128 : 1)); });\n child.stdin.end(command.input ?? '');\n return promise;\n}\n"],"mappings":"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"}
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
import { RequirementSchema } from "./schemas.js";
|
|
2
|
+
import { closeSync, constants, fstatSync, lstatSync, openSync, readSync } from "node:fs";
|
|
3
|
+
// src/requirements.ts
|
|
4
|
+
const MAX_INPUT_BYTES = 1048576;
|
|
5
|
+
function parseRequirement(value) {
|
|
6
|
+
const { baseJobId, ...spec } = RequirementSchema.parse(value);
|
|
7
|
+
return {
|
|
8
|
+
spec,
|
|
9
|
+
...baseJobId === void 0 ? {} : { baseJobId }
|
|
10
|
+
};
|
|
11
|
+
}
|
|
12
|
+
function readInputFile(path, label = "Requirement") {
|
|
13
|
+
const info = lstatSync(path);
|
|
14
|
+
if (info.isSymbolicLink()) throw new Error(`${label} ${path} is a symbolic link; provide a regular file`);
|
|
15
|
+
if (!info.isFile()) throw new Error(`${label} ${path} is not a regular file; provide a regular file`);
|
|
16
|
+
const fd = openSync(path, constants.O_RDONLY | constants.O_NOFOLLOW | constants.O_NONBLOCK);
|
|
17
|
+
try {
|
|
18
|
+
const opened = fstatSync(fd);
|
|
19
|
+
if (!opened.isFile()) throw new Error(`${label} ${path} is not a regular file; provide a regular file`);
|
|
20
|
+
if (opened.size > 1048576) throw new Error(`${label} ${path} exceeds ${MAX_INPUT_BYTES} bytes; reduce its size`);
|
|
21
|
+
const bytes = Buffer.allocUnsafe(opened.size + 1);
|
|
22
|
+
let size = 0;
|
|
23
|
+
while (size < bytes.length) {
|
|
24
|
+
const count = readSync(fd, bytes, size, bytes.length - size, null);
|
|
25
|
+
if (count === 0) break;
|
|
26
|
+
size += count;
|
|
27
|
+
}
|
|
28
|
+
if (size > 1048576) throw new Error(`${label} ${path} exceeds ${MAX_INPUT_BYTES} bytes; reduce its size`);
|
|
29
|
+
if (size > opened.size) throw new Error(`${label} ${path} changed while being read; retry after writing is complete`);
|
|
30
|
+
return bytes.subarray(0, size);
|
|
31
|
+
} finally {
|
|
32
|
+
closeSync(fd);
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
export { MAX_INPUT_BYTES, parseRequirement, readInputFile };
|
|
36
|
+
|
|
37
|
+
//# sourceMappingURL=requirements.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"requirements.js","names":[],"sources":["../src/requirements.ts"],"sourcesContent":["import { closeSync, constants, fstatSync, lstatSync, openSync, readSync } from 'node:fs';\nimport { RequirementSchema, type Spec } from './schemas.js';\n\nexport const MAX_INPUT_BYTES = 1024 * 1024;\nexport type Requirement = { spec: Spec; baseJobId?: string };\n\nexport function parseRequirement(value: unknown): Requirement {\n const { baseJobId, ...spec } = RequirementSchema.parse(value);\n return { spec, ...(baseJobId === undefined ? {} : { baseJobId }) };\n}\n\nexport function readInputFile(path: string, label = 'Requirement'): Buffer {\n const info = lstatSync(path);\n if (info.isSymbolicLink()) throw new Error(`${label} ${path} is a symbolic link; provide a regular file`);\n if (!info.isFile()) throw new Error(`${label} ${path} is not a regular file; provide a regular file`);\n const fd = openSync(path, constants.O_RDONLY | constants.O_NOFOLLOW | constants.O_NONBLOCK);\n try {\n const opened = fstatSync(fd);\n if (!opened.isFile()) throw new Error(`${label} ${path} is not a regular file; provide a regular file`);\n if (opened.size > MAX_INPUT_BYTES) throw new Error(`${label} ${path} exceeds ${MAX_INPUT_BYTES} bytes; reduce its size`);\n const bytes = Buffer.allocUnsafe(opened.size + 1);\n let size = 0;\n while (size < bytes.length) {\n const count = readSync(fd, bytes, size, bytes.length - size, null);\n if (count === 0) break;\n size += count;\n }\n if (size > MAX_INPUT_BYTES) throw new Error(`${label} ${path} exceeds ${MAX_INPUT_BYTES} bytes; reduce its size`);\n if (size > opened.size) throw new Error(`${label} ${path} changed while being read; retry after writing is complete`);\n return bytes.subarray(0, size);\n } finally {\n closeSync(fd);\n }\n}\n"],"mappings":";;;AAGA,MAAa,kBAAkB;AAG/B,SAAgB,iBAAiB,OAA6B;CAC5D,MAAM,EAAE,WAAW,GAAG,SAAS,kBAAkB,MAAM,KAAK;CAC5D,OAAO;EAAE;EAAM,GAAI,cAAc,KAAA,IAAY,CAAC,IAAI,EAAE,UAAU;CAAG;AACnE;AAEA,SAAgB,cAAc,MAAc,QAAQ,eAAuB;CACzE,MAAM,OAAO,UAAU,IAAI;CAC3B,IAAI,KAAK,eAAe,GAAG,MAAM,IAAI,MAAM,GAAG,MAAM,GAAG,KAAK,4CAA4C;CACxG,IAAI,CAAC,KAAK,OAAO,GAAG,MAAM,IAAI,MAAM,GAAG,MAAM,GAAG,KAAK,+CAA+C;CACpG,MAAM,KAAK,SAAS,MAAM,UAAU,WAAW,UAAU,aAAa,UAAU,UAAU;CAC1F,IAAI;EACF,MAAM,SAAS,UAAU,EAAE;EAC3B,IAAI,CAAC,OAAO,OAAO,GAAG,MAAM,IAAI,MAAM,GAAG,MAAM,GAAG,KAAK,+CAA+C;EACtG,IAAI,OAAO,OAAA,SAAwB,MAAM,IAAI,MAAM,GAAG,MAAM,GAAG,KAAK,WAAW,gBAAgB,wBAAwB;EACvH,MAAM,QAAQ,OAAO,YAAY,OAAO,OAAO,CAAC;EAChD,IAAI,OAAO;EACX,OAAO,OAAO,MAAM,QAAQ;GAC1B,MAAM,QAAQ,SAAS,IAAI,OAAO,MAAM,MAAM,SAAS,MAAM,IAAI;GACjE,IAAI,UAAU,GAAG;GACjB,QAAQ;EACV;EACA,IAAI,OAAA,SAAwB,MAAM,IAAI,MAAM,GAAG,MAAM,GAAG,KAAK,WAAW,gBAAgB,wBAAwB;EAChH,IAAI,OAAO,OAAO,MAAM,MAAM,IAAI,MAAM,GAAG,MAAM,GAAG,KAAK,2DAA2D;EACpH,OAAO,MAAM,SAAS,GAAG,IAAI;CAC/B,UAAU;EACR,UAAU,EAAE;CACd;AACF"}
|