* feat(hub): deploy canonical workflow bundles Keep CLI deployment aligned with Hub's authored bundle boundary so validation and activation receive the same deterministic source files. Public documentation now teaches that boundary as the only configuration format. * test(hub): assert validation diagnostics directly * refactor(hub): deepen HTTP client module * feat(hub): validate named agent configurations * docs(hub): align Claude mode example
118 lines
3.9 KiB
TypeScript
118 lines
3.9 KiB
TypeScript
#!/usr/bin/env npx tsx
|
|
|
|
import assert from "node:assert/strict";
|
|
import { createServer } from "node:http";
|
|
import type { AddressInfo } from "node:net";
|
|
import { mkdir, mkdtemp, rm, writeFile } from "node:fs/promises";
|
|
import { tmpdir } from "node:os";
|
|
import path from "node:path";
|
|
import { runLocalPaseo } from "./helpers/local-cli.js";
|
|
|
|
const cwd = await mkdtemp(path.join(tmpdir(), "paseo-hub-installed-"));
|
|
const requests: Array<{ url: string | undefined; body: unknown }> = [];
|
|
const server = createServer((request, response) => {
|
|
let body = "";
|
|
request.setEncoding("utf8");
|
|
request.on("data", (chunk: string) => {
|
|
body += chunk;
|
|
});
|
|
request.on("end", () => {
|
|
requests.push({ url: request.url, body: JSON.parse(body) });
|
|
const validation = request.url?.endsWith("/validate") === true;
|
|
response.writeHead(validation ? 200 : 201, { "content-type": "application/json" });
|
|
response.end(
|
|
JSON.stringify(
|
|
validation
|
|
? { projectSlug: "studio", valid: true }
|
|
: {
|
|
projectSlug: "studio",
|
|
version: 1,
|
|
versionId: "2de5b143-1c88-42db-8a8d-71ca2af97830",
|
|
active: true,
|
|
},
|
|
),
|
|
);
|
|
});
|
|
});
|
|
|
|
try {
|
|
const workflows = path.join(cwd, ".paseo", "workflows");
|
|
await mkdir(path.join(workflows, "partials"), { recursive: true });
|
|
const files = [
|
|
{
|
|
path: ".paseo/hub.yml",
|
|
content:
|
|
"environments:\n studio:\n kind: daemon\n daemon: local\n cwd: /workspace\nagents:\n codex-safe:\n provider: codex\n options:\n sandbox_workspace_write:\n writable_roots: [/var/cache/npm]\n network_access: false\n",
|
|
},
|
|
{
|
|
path: ".paseo/workflows/run.yml",
|
|
content:
|
|
"name: run\non: manual.run\nmax_runtime: 1h\ninputs:\n repo:\n type: string\n choices: [studio]\n agent:\n type: string\n choices: [codex-safe]\nsteps:\n - id: work\n environment: ${{ paseo.inputs.repo }}\n max_runtime: 30m\n idle_timeout: 5m\n agent: ${{ paseo.inputs.agent }}\n prompt:\n - include: partials/instructions.md\n - text: ${{ paseo.prompt }}\n",
|
|
},
|
|
{
|
|
path: ".paseo/workflows/partials/instructions.md",
|
|
content: "Keep structured provider options unchanged.\n",
|
|
},
|
|
];
|
|
for (const file of files) await writeFile(path.join(cwd, file.path), file.content);
|
|
|
|
await new Promise<void>((resolve) => server.listen(0, "127.0.0.1", resolve));
|
|
const address = server.address() as AddressInfo;
|
|
const origin = `http://127.0.0.1:${address.port}`;
|
|
const validate = await runLocalPaseo(
|
|
[
|
|
"hub",
|
|
"deploy",
|
|
"-p",
|
|
"studio",
|
|
"--dry-run",
|
|
"--hub",
|
|
origin,
|
|
"--api-key",
|
|
"test-secret",
|
|
"--json",
|
|
],
|
|
{},
|
|
cwd,
|
|
);
|
|
assert.equal(validate.exitCode, 0, validate.stderr);
|
|
assert.deepEqual(JSON.parse(validate.stdout), {
|
|
projectSlug: "studio",
|
|
valid: true,
|
|
workflows: 1,
|
|
origin,
|
|
});
|
|
|
|
const install = await runLocalPaseo(
|
|
["hub", "deploy", "-p", "studio", "--hub", origin, "--api-key", "test-secret", "--json"],
|
|
{},
|
|
cwd,
|
|
);
|
|
assert.equal(install.exitCode, 0, install.stderr);
|
|
|
|
assert.deepEqual(
|
|
requests.map(({ url, body }) => ({ url, body })),
|
|
[
|
|
{
|
|
url: "/api/v1/configurations/validate",
|
|
body: { projectSlug: "studio", files: files.toSorted(compareFiles) },
|
|
},
|
|
{
|
|
url: "/api/v1/configurations/install",
|
|
body: { projectSlug: "studio", files: files.toSorted(compareFiles) },
|
|
},
|
|
],
|
|
);
|
|
console.log("✅ installed Hub CLI deploys and validates the canonical bundle");
|
|
} finally {
|
|
server.closeAllConnections();
|
|
await new Promise<void>((resolve) => server.close(() => resolve()));
|
|
await rm(cwd, { recursive: true });
|
|
}
|
|
|
|
function compareFiles(left: { path: string }, right: { path: string }): number {
|
|
if (left.path < right.path) return -1;
|
|
if (left.path > right.path) return 1;
|
|
return 0;
|
|
}
|