@proagentstore/cli 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/dist/browser-runner/index.js +58 -0
- package/dist/browser-runner/runner.js +203 -0
- package/dist/browser-runner/server.js +124 -0
- package/dist/browser-runner/store.js +82 -0
- package/dist/browser-runner/test-job-server-cli.js +35 -0
- package/dist/browser-runner/test-job-server.js +226 -0
- package/dist/browser-runner/types.js +1 -0
- package/dist/index.d.ts +1 -0
- package/dist/index.js +722 -0
- package/package.json +44 -0
|
@@ -0,0 +1,226 @@
|
|
|
1
|
+
import { createServer, } from "node:http";
|
|
2
|
+
export async function startTestJobServer(port = 0) {
|
|
3
|
+
const submissions = [];
|
|
4
|
+
const server = createServer(async (req, res) => {
|
|
5
|
+
try {
|
|
6
|
+
await route(req, res, submissions);
|
|
7
|
+
}
|
|
8
|
+
catch (error) {
|
|
9
|
+
html(res, 500, `<h1>Server Error</h1><pre>${escapeHtml(String(error))}</pre>`);
|
|
10
|
+
}
|
|
11
|
+
});
|
|
12
|
+
await new Promise((resolve) => {
|
|
13
|
+
server.listen(port, "127.0.0.1", resolve);
|
|
14
|
+
});
|
|
15
|
+
const address = server.address();
|
|
16
|
+
const url = `http://127.0.0.1:${address.port}`;
|
|
17
|
+
return {
|
|
18
|
+
url,
|
|
19
|
+
jobUrl: `${url}/jobs/software-engineer`,
|
|
20
|
+
submissions,
|
|
21
|
+
async close() {
|
|
22
|
+
await new Promise((resolve, reject) => {
|
|
23
|
+
server.close((error) => (error ? reject(error) : resolve()));
|
|
24
|
+
});
|
|
25
|
+
},
|
|
26
|
+
};
|
|
27
|
+
}
|
|
28
|
+
async function route(req, res, submissions) {
|
|
29
|
+
const url = new URL(req.url || "/", "http://127.0.0.1");
|
|
30
|
+
if (req.method === "GET" && url.pathname === "/") {
|
|
31
|
+
redirect(res, "/jobs/software-engineer");
|
|
32
|
+
return;
|
|
33
|
+
}
|
|
34
|
+
if (req.method === "GET" && url.pathname === "/jobs/software-engineer") {
|
|
35
|
+
html(res, 200, jobPage());
|
|
36
|
+
return;
|
|
37
|
+
}
|
|
38
|
+
if (req.method === "POST" && url.pathname === "/apply") {
|
|
39
|
+
const submission = await parseApplication(req);
|
|
40
|
+
submissions.unshift(submission);
|
|
41
|
+
redirect(res, `/success/${submission.id}`);
|
|
42
|
+
return;
|
|
43
|
+
}
|
|
44
|
+
if (req.method === "GET" && url.pathname.startsWith("/success/")) {
|
|
45
|
+
const id = url.pathname.split("/").pop() || "";
|
|
46
|
+
const submission = submissions.find((row) => row.id === id);
|
|
47
|
+
if (!submission) {
|
|
48
|
+
html(res, 404, "<h1>Application not found</h1>");
|
|
49
|
+
return;
|
|
50
|
+
}
|
|
51
|
+
html(res, 200, successPage(submission));
|
|
52
|
+
return;
|
|
53
|
+
}
|
|
54
|
+
if (req.method === "GET" && url.pathname === "/submissions") {
|
|
55
|
+
json(res, 200, { submissions });
|
|
56
|
+
return;
|
|
57
|
+
}
|
|
58
|
+
html(res, 404, "<h1>Not found</h1>");
|
|
59
|
+
}
|
|
60
|
+
function jobPage() {
|
|
61
|
+
return page("Senior Software Engineer", `
|
|
62
|
+
<main>
|
|
63
|
+
<section class="job">
|
|
64
|
+
<p class="eyebrow">PAGS Test Fixture</p>
|
|
65
|
+
<h1>Senior Software Engineer</h1>
|
|
66
|
+
<p class="company">Fixture Labs</p>
|
|
67
|
+
<p>Build workflow tools for safe browser agents. This role values TypeScript, automation, product judgment, and careful user approval flows.</p>
|
|
68
|
+
</section>
|
|
69
|
+
<form action="/apply" method="post" enctype="multipart/form-data" class="application-form">
|
|
70
|
+
<label>Full name <input name="fullName" autocomplete="name" required /></label>
|
|
71
|
+
<label>Email <input name="email" type="email" autocomplete="email" required /></label>
|
|
72
|
+
<label>Phone <input name="phone" autocomplete="tel" /></label>
|
|
73
|
+
<label>Location <input name="location" autocomplete="address-level2" /></label>
|
|
74
|
+
<label>LinkedIn <input name="linkedin" type="url" /></label>
|
|
75
|
+
<label>Portfolio <input name="portfolio" type="url" /></label>
|
|
76
|
+
<label>Work authorization
|
|
77
|
+
<select name="workAuthorization" required>
|
|
78
|
+
<option value="">Select one</option>
|
|
79
|
+
<option>Authorized to work in the United States</option>
|
|
80
|
+
<option>Require sponsorship</option>
|
|
81
|
+
</select>
|
|
82
|
+
</label>
|
|
83
|
+
<label>Resume <input name="resume" type="file" accept=".pdf,.txt,.doc,.docx" required /></label>
|
|
84
|
+
<label>Cover note <textarea name="coverNote" rows="5" required></textarea></label>
|
|
85
|
+
<button type="submit">Submit application</button>
|
|
86
|
+
</form>
|
|
87
|
+
</main>
|
|
88
|
+
`);
|
|
89
|
+
}
|
|
90
|
+
function successPage(submission) {
|
|
91
|
+
return page("Application Received", `
|
|
92
|
+
<main>
|
|
93
|
+
<section class="success">
|
|
94
|
+
<p class="eyebrow">Success</p>
|
|
95
|
+
<h1>Application received</h1>
|
|
96
|
+
<p id="application-id">${escapeHtml(submission.id)}</p>
|
|
97
|
+
<p>Thank you, ${escapeHtml(submission.fields.fullName || "candidate")}.</p>
|
|
98
|
+
<p>Resume: ${escapeHtml(submission.resume?.filename || "missing")} (${submission.resume?.size || 0} bytes)</p>
|
|
99
|
+
</section>
|
|
100
|
+
</main>
|
|
101
|
+
`);
|
|
102
|
+
}
|
|
103
|
+
function page(title, body) {
|
|
104
|
+
return `<!doctype html>
|
|
105
|
+
<html lang="en">
|
|
106
|
+
<head>
|
|
107
|
+
<meta charset="utf-8" />
|
|
108
|
+
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
|
109
|
+
<title>${escapeHtml(title)}</title>
|
|
110
|
+
<style>
|
|
111
|
+
body { margin: 0; font-family: system-ui, sans-serif; background: #f6f8fb; color: #16202a; }
|
|
112
|
+
main { width: min(880px, calc(100% - 32px)); margin: 40px auto; }
|
|
113
|
+
.job, .success, form { background: white; border: 1px solid #d7dee8; border-radius: 8px; padding: 24px; }
|
|
114
|
+
.application-form { display: grid; gap: 16px; margin-top: 20px; }
|
|
115
|
+
label { display: grid; gap: 6px; font-weight: 650; }
|
|
116
|
+
input, select, textarea { box-sizing: border-box; width: 100%; padding: 10px 12px; border: 1px solid #aab6c4; border-radius: 6px; font: inherit; }
|
|
117
|
+
button { width: fit-content; padding: 10px 16px; border: 0; border-radius: 6px; background: #166534; color: white; font-weight: 700; cursor: pointer; }
|
|
118
|
+
.eyebrow { color: #4f46e5; font-weight: 800; text-transform: uppercase; letter-spacing: .06em; }
|
|
119
|
+
.company { font-weight: 700; }
|
|
120
|
+
</style>
|
|
121
|
+
</head>
|
|
122
|
+
<body>${body}</body>
|
|
123
|
+
</html>`;
|
|
124
|
+
}
|
|
125
|
+
async function parseApplication(req) {
|
|
126
|
+
const contentType = req.headers["content-type"] || "";
|
|
127
|
+
if (!contentType.includes("multipart/form-data")) {
|
|
128
|
+
throw new Error("Expected multipart/form-data");
|
|
129
|
+
}
|
|
130
|
+
const boundary = contentType.match(/boundary=([^;]+)/)?.[1];
|
|
131
|
+
if (!boundary)
|
|
132
|
+
throw new Error("Missing multipart boundary");
|
|
133
|
+
const raw = await readBody(req);
|
|
134
|
+
const parsed = parseMultipart(raw, boundary);
|
|
135
|
+
return {
|
|
136
|
+
id: `fixture_app_${crypto.randomUUID()}`,
|
|
137
|
+
fields: parsed.fields,
|
|
138
|
+
resume: parsed.files.resume,
|
|
139
|
+
createdAt: new Date().toISOString(),
|
|
140
|
+
};
|
|
141
|
+
}
|
|
142
|
+
function parseMultipart(raw, boundary) {
|
|
143
|
+
const fields = {};
|
|
144
|
+
const files = {};
|
|
145
|
+
const delimiter = Buffer.from(`--${boundary}`);
|
|
146
|
+
let offset = 0;
|
|
147
|
+
while (offset < raw.length) {
|
|
148
|
+
const start = raw.indexOf(delimiter, offset);
|
|
149
|
+
if (start === -1)
|
|
150
|
+
break;
|
|
151
|
+
const partStart = start + delimiter.length;
|
|
152
|
+
if (raw.subarray(partStart, partStart + 2).toString() === "--")
|
|
153
|
+
break;
|
|
154
|
+
const headerStart = raw.subarray(partStart, partStart + 2).toString() === "\r\n"
|
|
155
|
+
? partStart + 2
|
|
156
|
+
: partStart;
|
|
157
|
+
const headerEnd = raw.indexOf(Buffer.from("\r\n\r\n"), headerStart);
|
|
158
|
+
if (headerEnd === -1)
|
|
159
|
+
break;
|
|
160
|
+
const next = raw.indexOf(delimiter, headerEnd + 4);
|
|
161
|
+
if (next === -1)
|
|
162
|
+
break;
|
|
163
|
+
const headers = raw.subarray(headerStart, headerEnd).toString("utf-8");
|
|
164
|
+
const body = trimPartBody(raw.subarray(headerEnd + 4, next));
|
|
165
|
+
const name = headers.match(/name="([^"]+)"/)?.[1];
|
|
166
|
+
const filename = headers.match(/filename="([^"]*)"/)?.[1];
|
|
167
|
+
const contentType = headers.match(/Content-Type:\s*([^\r\n]+)/i)?.[1] || "application/octet-stream";
|
|
168
|
+
if (name && filename) {
|
|
169
|
+
files[name] = {
|
|
170
|
+
filename,
|
|
171
|
+
contentType,
|
|
172
|
+
size: body.length,
|
|
173
|
+
};
|
|
174
|
+
}
|
|
175
|
+
else if (name) {
|
|
176
|
+
fields[name] = body.toString("utf-8");
|
|
177
|
+
}
|
|
178
|
+
offset = next;
|
|
179
|
+
}
|
|
180
|
+
return { fields, files };
|
|
181
|
+
}
|
|
182
|
+
function trimPartBody(value) {
|
|
183
|
+
if (value.subarray(-2).toString() === "\r\n")
|
|
184
|
+
return value.subarray(0, -2);
|
|
185
|
+
return value;
|
|
186
|
+
}
|
|
187
|
+
async function readBody(req) {
|
|
188
|
+
const chunks = [];
|
|
189
|
+
for await (const chunk of req)
|
|
190
|
+
chunks.push(Buffer.from(chunk));
|
|
191
|
+
return Buffer.concat(chunks);
|
|
192
|
+
}
|
|
193
|
+
function redirect(res, location) {
|
|
194
|
+
res.writeHead(303, { Location: location });
|
|
195
|
+
res.end();
|
|
196
|
+
}
|
|
197
|
+
function html(res, status, body) {
|
|
198
|
+
res.writeHead(status, {
|
|
199
|
+
"Content-Type": "text/html; charset=utf-8",
|
|
200
|
+
"X-Content-Type-Options": "nosniff",
|
|
201
|
+
});
|
|
202
|
+
res.end(body);
|
|
203
|
+
}
|
|
204
|
+
function json(res, status, body) {
|
|
205
|
+
res.writeHead(status, {
|
|
206
|
+
"Content-Type": "application/json; charset=utf-8",
|
|
207
|
+
"X-Content-Type-Options": "nosniff",
|
|
208
|
+
});
|
|
209
|
+
res.end(JSON.stringify(body));
|
|
210
|
+
}
|
|
211
|
+
function escapeHtml(value) {
|
|
212
|
+
return value.replace(/[&<>"']/g, (char) => {
|
|
213
|
+
switch (char) {
|
|
214
|
+
case "&":
|
|
215
|
+
return "&";
|
|
216
|
+
case "<":
|
|
217
|
+
return "<";
|
|
218
|
+
case ">":
|
|
219
|
+
return ">";
|
|
220
|
+
case '"':
|
|
221
|
+
return """;
|
|
222
|
+
default:
|
|
223
|
+
return "'";
|
|
224
|
+
}
|
|
225
|
+
});
|
|
226
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
#!/usr/bin/env node
|