@autifyhq/aximo-cli 0.2.0 → 0.4.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 +117 -50
- package/dist/cli.js +3 -1
- package/dist/commands/case/case.test.d.ts +1 -0
- package/dist/commands/case/case.test.js +120 -0
- package/dist/commands/case/create.d.ts +53 -0
- package/dist/commands/case/create.js +71 -0
- package/dist/commands/case/delete.d.ts +20 -0
- package/dist/commands/case/delete.js +39 -0
- package/dist/commands/case/index.d.ts +2 -0
- package/dist/commands/case/index.js +8 -0
- package/dist/commands/device/doctor.d.ts +1 -0
- package/dist/commands/device/doctor.js +13 -4
- package/dist/commands/device/doctor.test.js +9 -3
- package/dist/commands/device/paths.d.ts +1 -0
- package/dist/commands/device/paths.js +3 -0
- package/dist/commands/mcp/server.test.js +9 -3
- package/dist/commands/mcp/tools.js +81 -0
- package/dist/commands/mcp/tools.test.js +77 -1
- package/dist/commands/plan/add-case.d.ts +30 -0
- package/dist/commands/plan/add-case.js +58 -0
- package/dist/commands/plan/create.d.ts +31 -0
- package/dist/commands/plan/create.js +56 -0
- package/dist/commands/plan/index.d.ts +2 -0
- package/dist/commands/plan/index.js +9 -0
- package/dist/commands/plan/plan.test.d.ts +1 -0
- package/dist/commands/plan/plan.test.js +104 -0
- package/dist/commands/plan/remove-case.d.ts +26 -0
- package/dist/commands/plan/remove-case.js +43 -0
- package/dist/commands/schema.js +40 -1
- package/dist/commands/session/index.js +1 -0
- package/dist/commands/session/save-as-case.d.ts +43 -0
- package/dist/commands/session/save-as-case.js +69 -0
- package/dist/commands/session/save-as-case.test.d.ts +1 -0
- package/dist/commands/session/save-as-case.test.js +58 -0
- package/dist/desktop-server/setup-tls.test.d.ts +1 -0
- package/dist/desktop-server/setup-tls.test.js +83 -0
- package/dist/desktop-server/setup.d.ts +17 -0
- package/dist/desktop-server/setup.js +53 -1
- package/dist/index.d.ts +12 -7
- package/dist/index.js +12 -7
- package/dist/types.d.ts +40 -0
- package/package.json +1 -1
package/dist/commands/schema.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { defineCommand } from "citty";
|
|
2
|
-
import { formatSuccess, formatError, printResult } from "../output.js";
|
|
3
2
|
import pkg from "../../package.json" with { type: "json" };
|
|
3
|
+
import { formatError, formatSuccess, printResult } from "../output.js";
|
|
4
4
|
function arg(name, type, description, required = false) {
|
|
5
5
|
return { name, type, description, required };
|
|
6
6
|
}
|
|
@@ -41,6 +41,45 @@ export function getSchema() {
|
|
|
41
41
|
cmd("get", "GET /api/sessions/{sessionId}", [
|
|
42
42
|
arg("sessionId", "positional", "Session ID", true),
|
|
43
43
|
]),
|
|
44
|
+
cmd("save-as-case", "POST /api/sessions/{sessionId}/save-as-case", [
|
|
45
|
+
arg("sessionId", "positional", "Session ID", true),
|
|
46
|
+
arg("title", "string", ""),
|
|
47
|
+
arg("type", "string", "web | mobile | desktop"),
|
|
48
|
+
arg("scenario", "string", ""),
|
|
49
|
+
arg("format", "string", "gherkin | markdown | plain_text"),
|
|
50
|
+
...MUTATE_ARGS,
|
|
51
|
+
]),
|
|
52
|
+
]),
|
|
53
|
+
cmd("case", "Test case commands", [], [
|
|
54
|
+
cmd("create", "POST /api/cases", [
|
|
55
|
+
arg("project-id", "string", "Project ID"),
|
|
56
|
+
arg("scenario", "string", "Test scenario text"),
|
|
57
|
+
arg("type", "string", "web | mobile | desktop"),
|
|
58
|
+
arg("title", "string", ""),
|
|
59
|
+
arg("description", "string", ""),
|
|
60
|
+
arg("format", "string", "gherkin | markdown | plain_text"),
|
|
61
|
+
arg("start-url", "string", ""),
|
|
62
|
+
arg("directory-id", "string", ""),
|
|
63
|
+
...MUTATE_ARGS,
|
|
64
|
+
]),
|
|
65
|
+
]),
|
|
66
|
+
cmd("plan", "Test plan commands", [], [
|
|
67
|
+
cmd("create", "POST /api/plans", [
|
|
68
|
+
arg("project-id", "string", "Project ID"),
|
|
69
|
+
arg("title", "string", "Plan title"),
|
|
70
|
+
arg("description", "string", ""),
|
|
71
|
+
...MUTATE_ARGS,
|
|
72
|
+
]),
|
|
73
|
+
cmd("add-case", "POST /api/plans/{planId}/cases", [
|
|
74
|
+
arg("planId", "positional", "Plan ID", true),
|
|
75
|
+
arg("case-id", "string", "Case ID to add"),
|
|
76
|
+
...MUTATE_ARGS,
|
|
77
|
+
]),
|
|
78
|
+
cmd("remove-case", "DELETE /api/plans/{planId}/cases/{caseId}", [
|
|
79
|
+
arg("planId", "positional", "Plan ID", true),
|
|
80
|
+
arg("caseId", "positional", "Case ID", true),
|
|
81
|
+
arg("dry-run", "boolean", "Validate and show request without calling API"),
|
|
82
|
+
]),
|
|
44
83
|
]),
|
|
45
84
|
cmd("trigger", "Trigger commands", [], [
|
|
46
85
|
cmd("case", "Case trigger commands", [], [
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
import { type ClientDeps } from "../../client.js";
|
|
2
|
+
import { type DryRunResult } from "../../mutate.js";
|
|
3
|
+
import type { Envelope, SaveSessionAsCaseResult } from "../../types.js";
|
|
4
|
+
export interface SessionSaveAsCaseOptions {
|
|
5
|
+
sessionId?: unknown;
|
|
6
|
+
title?: unknown;
|
|
7
|
+
type?: unknown;
|
|
8
|
+
scenario?: unknown;
|
|
9
|
+
format?: unknown;
|
|
10
|
+
json?: unknown;
|
|
11
|
+
dryRun?: unknown;
|
|
12
|
+
}
|
|
13
|
+
export declare function runSessionSaveAsCase(options: SessionSaveAsCaseOptions, deps?: ClientDeps): Promise<Envelope<SaveSessionAsCaseResult | DryRunResult>>;
|
|
14
|
+
declare const _default: import("citty").CommandDef<{
|
|
15
|
+
readonly sessionId: {
|
|
16
|
+
readonly type: "positional";
|
|
17
|
+
readonly description: "Session ID";
|
|
18
|
+
readonly required: true;
|
|
19
|
+
};
|
|
20
|
+
readonly title: {
|
|
21
|
+
readonly type: "string";
|
|
22
|
+
};
|
|
23
|
+
readonly type: {
|
|
24
|
+
readonly type: "string";
|
|
25
|
+
readonly description: "web | mobile | desktop";
|
|
26
|
+
};
|
|
27
|
+
readonly scenario: {
|
|
28
|
+
readonly type: "string";
|
|
29
|
+
};
|
|
30
|
+
readonly format: {
|
|
31
|
+
readonly type: "string";
|
|
32
|
+
readonly description: "gherkin | markdown | plain_text";
|
|
33
|
+
};
|
|
34
|
+
readonly json: {
|
|
35
|
+
readonly type: "string";
|
|
36
|
+
readonly description: "Raw JSON request body (replaces individual flags)";
|
|
37
|
+
};
|
|
38
|
+
readonly "dry-run": {
|
|
39
|
+
readonly type: "boolean";
|
|
40
|
+
readonly description: "Validate and show request without calling API";
|
|
41
|
+
};
|
|
42
|
+
}>;
|
|
43
|
+
export default _default;
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
import { defineCommand } from "citty";
|
|
2
|
+
import { request } from "../../client.js";
|
|
3
|
+
import { handleApiError } from "../../errors.js";
|
|
4
|
+
import { dryRunEnvelope, resolveJsonOverride } from "../../mutate.js";
|
|
5
|
+
import { formatSuccess, printResult, validationError } from "../../output.js";
|
|
6
|
+
import { optionalString, requireId } from "../../validate.js";
|
|
7
|
+
export async function runSessionSaveAsCase(options, deps) {
|
|
8
|
+
let sessionId;
|
|
9
|
+
let body;
|
|
10
|
+
const override = resolveJsonOverride(options.json);
|
|
11
|
+
if (override && "error" in override)
|
|
12
|
+
return override.error;
|
|
13
|
+
try {
|
|
14
|
+
sessionId = requireId("sessionId", options.sessionId);
|
|
15
|
+
}
|
|
16
|
+
catch (err) {
|
|
17
|
+
return validationError(err);
|
|
18
|
+
}
|
|
19
|
+
if (override) {
|
|
20
|
+
body = override.body;
|
|
21
|
+
}
|
|
22
|
+
else {
|
|
23
|
+
try {
|
|
24
|
+
body = {
|
|
25
|
+
title: optionalString("title", options.title),
|
|
26
|
+
type: optionalString("type", options.type),
|
|
27
|
+
scenario: optionalString("scenario", options.scenario),
|
|
28
|
+
format: optionalString("format", options.format),
|
|
29
|
+
};
|
|
30
|
+
}
|
|
31
|
+
catch (err) {
|
|
32
|
+
return validationError(err);
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
const path = `/api/sessions/${encodeURIComponent(sessionId)}/save-as-case`;
|
|
36
|
+
if (options.dryRun) {
|
|
37
|
+
return dryRunEnvelope("POST", path, body);
|
|
38
|
+
}
|
|
39
|
+
try {
|
|
40
|
+
const res = await request(path, { method: "POST", body }, deps ?? {});
|
|
41
|
+
return formatSuccess(res);
|
|
42
|
+
}
|
|
43
|
+
catch (err) {
|
|
44
|
+
return handleApiError(err);
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
export default defineCommand({
|
|
48
|
+
meta: { name: "save-as-case", description: "Save a finished session as a test case" },
|
|
49
|
+
args: {
|
|
50
|
+
sessionId: { type: "positional", description: "Session ID", required: true },
|
|
51
|
+
title: { type: "string" },
|
|
52
|
+
type: { type: "string", description: "web | mobile | desktop" },
|
|
53
|
+
scenario: { type: "string" },
|
|
54
|
+
format: { type: "string", description: "gherkin | markdown | plain_text" },
|
|
55
|
+
json: { type: "string", description: "Raw JSON request body (replaces individual flags)" },
|
|
56
|
+
"dry-run": { type: "boolean", description: "Validate and show request without calling API" },
|
|
57
|
+
},
|
|
58
|
+
async run({ args }) {
|
|
59
|
+
printResult(await runSessionSaveAsCase({
|
|
60
|
+
sessionId: args.sessionId,
|
|
61
|
+
title: args.title,
|
|
62
|
+
type: args.type,
|
|
63
|
+
scenario: args.scenario,
|
|
64
|
+
format: args.format,
|
|
65
|
+
json: args.json,
|
|
66
|
+
dryRun: args["dry-run"],
|
|
67
|
+
}));
|
|
68
|
+
},
|
|
69
|
+
});
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
import { afterAll, afterEach, beforeAll, describe, expect, it } from "vitest";
|
|
2
|
+
import { HttpResponse, http, server, TEST_BASE_URL } from "../../test-utils.js";
|
|
3
|
+
import { runSessionSaveAsCase } from "./save-as-case.js";
|
|
4
|
+
beforeAll(() => {
|
|
5
|
+
process.env["AXIMO_API_KEY"] = "test-key";
|
|
6
|
+
process.env["AXIMO_BASE_URL"] = TEST_BASE_URL;
|
|
7
|
+
server.listen();
|
|
8
|
+
});
|
|
9
|
+
afterEach(() => server.resetHandlers());
|
|
10
|
+
afterAll(() => server.close());
|
|
11
|
+
describe("runSessionSaveAsCase", () => {
|
|
12
|
+
it("POSTs to the save-as-case path and returns the new case", async () => {
|
|
13
|
+
let seen;
|
|
14
|
+
server.use(http.post(`${TEST_BASE_URL}/api/sessions/s1/save-as-case`, async ({ request: req }) => {
|
|
15
|
+
seen = await req.json();
|
|
16
|
+
return HttpResponse.json({ success: true, caseId: "c1", projectId: "p1" }, { status: 201 });
|
|
17
|
+
}));
|
|
18
|
+
const result = await runSessionSaveAsCase({ sessionId: "s1", title: "Saved" });
|
|
19
|
+
expect(result.ok).toBe(true);
|
|
20
|
+
if (result.ok && !("dryRun" in result.data)) {
|
|
21
|
+
expect(result.data.caseId).toBe("c1");
|
|
22
|
+
expect(result.data.projectId).toBe("p1");
|
|
23
|
+
}
|
|
24
|
+
expect(seen).toMatchObject({ title: "Saved" });
|
|
25
|
+
});
|
|
26
|
+
it("works with no body fields (derives from session)", async () => {
|
|
27
|
+
server.use(http.post(`${TEST_BASE_URL}/api/sessions/s1/save-as-case`, () => HttpResponse.json({ success: true, caseId: "c2", projectId: "p1" }, { status: 201 })));
|
|
28
|
+
const result = await runSessionSaveAsCase({ sessionId: "s1" });
|
|
29
|
+
expect(result.ok).toBe(true);
|
|
30
|
+
});
|
|
31
|
+
it("rejects an empty sessionId", async () => {
|
|
32
|
+
const result = await runSessionSaveAsCase({ sessionId: "" });
|
|
33
|
+
expect(result.ok).toBe(false);
|
|
34
|
+
if (!result.ok)
|
|
35
|
+
expect(result.error.code).toBe("validation_error");
|
|
36
|
+
});
|
|
37
|
+
it("rejects an injected sessionId (requireId)", async () => {
|
|
38
|
+
const result = await runSessionSaveAsCase({ sessionId: "s1?x=1" });
|
|
39
|
+
expect(result.ok).toBe(false);
|
|
40
|
+
if (!result.ok)
|
|
41
|
+
expect(result.error.code).toBe("validation_error");
|
|
42
|
+
});
|
|
43
|
+
it("--dry-run returns request without calling API", async () => {
|
|
44
|
+
const result = await runSessionSaveAsCase({ sessionId: "s1", dryRun: true });
|
|
45
|
+
expect(result.ok).toBe(true);
|
|
46
|
+
if (result.ok && "dryRun" in result.data) {
|
|
47
|
+
expect(result.data.method).toBe("POST");
|
|
48
|
+
expect(result.data.url).toBe(`${TEST_BASE_URL}/api/sessions/s1/save-as-case`);
|
|
49
|
+
}
|
|
50
|
+
});
|
|
51
|
+
it("maps 400 to validation_error", async () => {
|
|
52
|
+
server.use(http.post(`${TEST_BASE_URL}/api/sessions/s1/save-as-case`, () => HttpResponse.json({ error: "Session is still in progress" }, { status: 400 })));
|
|
53
|
+
const result = await runSessionSaveAsCase({ sessionId: "s1" });
|
|
54
|
+
expect(result.ok).toBe(false);
|
|
55
|
+
if (!result.ok)
|
|
56
|
+
expect(result.error.code).toBe("validation_error");
|
|
57
|
+
});
|
|
58
|
+
});
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
import { mkdtempSync, readFileSync, rmSync, writeFileSync } from "node:fs";
|
|
2
|
+
import { tmpdir } from "node:os";
|
|
3
|
+
import { join } from "node:path";
|
|
4
|
+
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
|
|
5
|
+
import { resolveAgentSslCertFile, writeCertBundle } from "./setup.js";
|
|
6
|
+
let tempDir;
|
|
7
|
+
const savedEnv = {};
|
|
8
|
+
beforeEach(() => {
|
|
9
|
+
tempDir = mkdtempSync(join(tmpdir(), "aximo-tls-"));
|
|
10
|
+
for (const key of ["SSL_CERT_FILE", "NODE_EXTRA_CA_CERTS", "AXIMO_HOME"]) {
|
|
11
|
+
savedEnv[key] = process.env[key];
|
|
12
|
+
delete process.env[key];
|
|
13
|
+
}
|
|
14
|
+
process.env["AXIMO_HOME"] = tempDir;
|
|
15
|
+
});
|
|
16
|
+
afterEach(() => {
|
|
17
|
+
for (const [key, value] of Object.entries(savedEnv)) {
|
|
18
|
+
if (value === undefined)
|
|
19
|
+
delete process.env[key];
|
|
20
|
+
else
|
|
21
|
+
process.env[key] = value;
|
|
22
|
+
}
|
|
23
|
+
rmSync(tempDir, { recursive: true, force: true });
|
|
24
|
+
vi.doUnmock("node:child_process");
|
|
25
|
+
vi.resetModules();
|
|
26
|
+
});
|
|
27
|
+
describe("writeCertBundle", () => {
|
|
28
|
+
it("concatenates certifi roots and the custom CA into one bundle", () => {
|
|
29
|
+
const certifi = join(tempDir, "certifi.pem");
|
|
30
|
+
const customCa = join(tempDir, "ca.pem");
|
|
31
|
+
const out = join(tempDir, "bundle.pem");
|
|
32
|
+
writeFileSync(certifi, "-----CERTIFI ROOT-----\n");
|
|
33
|
+
writeFileSync(customCa, "-----AXIMO CA-----\n");
|
|
34
|
+
writeCertBundle(out, certifi, customCa);
|
|
35
|
+
const result = readFileSync(out, "utf-8");
|
|
36
|
+
expect(result).toContain("-----CERTIFI ROOT-----");
|
|
37
|
+
expect(result).toContain("-----AXIMO CA-----");
|
|
38
|
+
// certifi first, custom CA second, single trailing newline
|
|
39
|
+
expect(result).toBe("-----CERTIFI ROOT-----\n-----AXIMO CA-----\n");
|
|
40
|
+
});
|
|
41
|
+
});
|
|
42
|
+
describe("resolveAgentSslCertFile", () => {
|
|
43
|
+
it("respects an explicit SSL_CERT_FILE override", () => {
|
|
44
|
+
process.env["SSL_CERT_FILE"] = "/custom/bundle.pem";
|
|
45
|
+
process.env["NODE_EXTRA_CA_CERTS"] = join(tempDir, "ca.pem");
|
|
46
|
+
writeFileSync(join(tempDir, "ca.pem"), "ca");
|
|
47
|
+
expect(resolveAgentSslCertFile("/no/python")).toBe("/custom/bundle.pem");
|
|
48
|
+
});
|
|
49
|
+
it("returns undefined when no custom CA is configured (public-CA servers)", () => {
|
|
50
|
+
expect(resolveAgentSslCertFile("/no/python")).toBeUndefined();
|
|
51
|
+
});
|
|
52
|
+
it("returns undefined when NODE_EXTRA_CA_CERTS points at a missing file", () => {
|
|
53
|
+
process.env["NODE_EXTRA_CA_CERTS"] = join(tempDir, "does-not-exist.pem");
|
|
54
|
+
expect(resolveAgentSslCertFile("/no/python")).toBeUndefined();
|
|
55
|
+
});
|
|
56
|
+
it("degrades gracefully (no throw) when the embedded certifi store can't be located", () => {
|
|
57
|
+
process.env["NODE_EXTRA_CA_CERTS"] = join(tempDir, "ca.pem");
|
|
58
|
+
writeFileSync(join(tempDir, "ca.pem"), "-----AXIMO CA-----\n");
|
|
59
|
+
const errSpy = vi.spyOn(console, "error").mockImplementation(() => { });
|
|
60
|
+
// A non-existent python makes `python -m certifi` fail, so no bundle is built.
|
|
61
|
+
expect(resolveAgentSslCertFile(join(tempDir, "no-such-python"))).toBeUndefined();
|
|
62
|
+
expect(errSpy).toHaveBeenCalled();
|
|
63
|
+
errSpy.mockRestore();
|
|
64
|
+
});
|
|
65
|
+
it("builds and returns the certifi+CA bundle when a custom CA and certifi store are present", async () => {
|
|
66
|
+
const certifiPath = join(tempDir, "certifi.pem");
|
|
67
|
+
const customCaPath = join(tempDir, "ca.pem");
|
|
68
|
+
writeFileSync(certifiPath, "-----CERTIFI ROOT-----\n");
|
|
69
|
+
writeFileSync(customCaPath, "-----AXIMO CA-----\n");
|
|
70
|
+
process.env["NODE_EXTRA_CA_CERTS"] = customCaPath;
|
|
71
|
+
// Stub `python -m certifi` so it resolves to the fixture certifi store above.
|
|
72
|
+
vi.resetModules();
|
|
73
|
+
vi.doMock("node:child_process", () => ({
|
|
74
|
+
spawn: vi.fn(),
|
|
75
|
+
spawnSync: vi.fn(() => ({ status: 0, stdout: `${certifiPath}\n` })),
|
|
76
|
+
}));
|
|
77
|
+
const { resolveAgentSslCertFile: resolve } = await import("./setup.js");
|
|
78
|
+
const bundlePath = resolve("/fake/python");
|
|
79
|
+
expect(bundlePath).toBe(join(tempDir, "agent-ca-bundle.pem"));
|
|
80
|
+
// The bundle is the certifi roots followed by the custom CA, in that order.
|
|
81
|
+
expect(readFileSync(bundlePath, "utf-8")).toBe("-----CERTIFI ROOT-----\n-----AXIMO CA-----\n");
|
|
82
|
+
});
|
|
83
|
+
});
|
|
@@ -15,5 +15,22 @@ interface RemoteOptions {
|
|
|
15
15
|
deviceToken: string;
|
|
16
16
|
deviceName: string;
|
|
17
17
|
}
|
|
18
|
+
/** Path to the embedded interpreter's certifi CA store, or undefined if unavailable. */
|
|
19
|
+
export declare function embeddedCertifiPath(pythonPath: string): string | undefined;
|
|
20
|
+
/** Write a combined trust store (certifi roots followed by the custom CA) to outPath. */
|
|
21
|
+
export declare function writeCertBundle(outPath: string, certifiPath: string, customCaPath: string): void;
|
|
22
|
+
/**
|
|
23
|
+
* Resolve the SSL_CERT_FILE for the Python desktop agent.
|
|
24
|
+
*
|
|
25
|
+
* The Node CLI trusts a custom CA (e.g. an on-prem self-signed server) via
|
|
26
|
+
* NODE_EXTRA_CA_CERTS, but Python ignores that variable, so the agent's wss://
|
|
27
|
+
* relay connection would fail verification. Mirror the trust by building a
|
|
28
|
+
* bundle of the embedded interpreter's certifi roots PLUS the custom CA and
|
|
29
|
+
* pointing SSL_CERT_FILE at it. SSL_CERT_FILE replaces the whole trust store, so
|
|
30
|
+
* the bundle must contain both — the custom CA alone would break every other TLS
|
|
31
|
+
* the agent does. No-op when no custom CA is configured (public-CA servers
|
|
32
|
+
* already verify via certifi).
|
|
33
|
+
*/
|
|
34
|
+
export declare function resolveAgentSslCertFile(pythonPath: string): string | undefined;
|
|
18
35
|
export declare function startDesktopRemote(pythonPath: string, scriptPath: string, options: RemoteOptions): ChildProcess;
|
|
19
36
|
export {};
|
|
@@ -5,7 +5,7 @@ import { arch as osArch, platform } from "node:os";
|
|
|
5
5
|
import { join } from "node:path";
|
|
6
6
|
import { Readable } from "node:stream";
|
|
7
7
|
import { pipeline } from "node:stream/promises";
|
|
8
|
-
import { getAximoDir, getCacheDir, getDepsMarkerPath, getEmbeddedPythonBinary, getPythonDir, getPythonMarkerPath, getRemoteLogPath, } from "../commands/device/paths.js";
|
|
8
|
+
import { getAgentCaBundlePath, getAximoDir, getCacheDir, getDepsMarkerPath, getEmbeddedPythonBinary, getPythonDir, getPythonMarkerPath, getRemoteLogPath, } from "../commands/device/paths.js";
|
|
9
9
|
import { ensureAgent } from "./agent-sync.js";
|
|
10
10
|
export const PYTHON_RELEASE = "20260414";
|
|
11
11
|
export const PYTHON_VERSION = "3.12.13";
|
|
@@ -174,7 +174,58 @@ export async function ensurePythonReady(deps) {
|
|
|
174
174
|
agentChanged: agent.changed,
|
|
175
175
|
};
|
|
176
176
|
}
|
|
177
|
+
/** Path to the embedded interpreter's certifi CA store, or undefined if unavailable. */
|
|
178
|
+
export function embeddedCertifiPath(pythonPath) {
|
|
179
|
+
const result = spawnSync(pythonPath, ["-m", "certifi"], { encoding: "utf-8" });
|
|
180
|
+
if (result.status !== 0)
|
|
181
|
+
return undefined;
|
|
182
|
+
const path = (result.stdout ?? "").trim();
|
|
183
|
+
return path && existsSync(path) ? path : undefined;
|
|
184
|
+
}
|
|
185
|
+
/** Write a combined trust store (certifi roots followed by the custom CA) to outPath. */
|
|
186
|
+
export function writeCertBundle(outPath, certifiPath, customCaPath) {
|
|
187
|
+
const certifi = readFileSync(certifiPath, "utf-8");
|
|
188
|
+
const customCa = readFileSync(customCaPath, "utf-8");
|
|
189
|
+
writeFileSync(outPath, `${certifi.trimEnd()}\n${customCa.trimEnd()}\n`, { mode: 0o644 });
|
|
190
|
+
}
|
|
191
|
+
/**
|
|
192
|
+
* Resolve the SSL_CERT_FILE for the Python desktop agent.
|
|
193
|
+
*
|
|
194
|
+
* The Node CLI trusts a custom CA (e.g. an on-prem self-signed server) via
|
|
195
|
+
* NODE_EXTRA_CA_CERTS, but Python ignores that variable, so the agent's wss://
|
|
196
|
+
* relay connection would fail verification. Mirror the trust by building a
|
|
197
|
+
* bundle of the embedded interpreter's certifi roots PLUS the custom CA and
|
|
198
|
+
* pointing SSL_CERT_FILE at it. SSL_CERT_FILE replaces the whole trust store, so
|
|
199
|
+
* the bundle must contain both — the custom CA alone would break every other TLS
|
|
200
|
+
* the agent does. No-op when no custom CA is configured (public-CA servers
|
|
201
|
+
* already verify via certifi).
|
|
202
|
+
*/
|
|
203
|
+
export function resolveAgentSslCertFile(pythonPath) {
|
|
204
|
+
// Respect an explicit override — the user may already point Python at a bundle.
|
|
205
|
+
if (process.env["SSL_CERT_FILE"])
|
|
206
|
+
return process.env["SSL_CERT_FILE"];
|
|
207
|
+
const customCa = process.env["NODE_EXTRA_CA_CERTS"];
|
|
208
|
+
if (!customCa || !existsSync(customCa))
|
|
209
|
+
return undefined;
|
|
210
|
+
const certifiPath = embeddedCertifiPath(pythonPath);
|
|
211
|
+
if (!certifiPath) {
|
|
212
|
+
console.error("[remote] NODE_EXTRA_CA_CERTS is set but the embedded Python's certifi store " +
|
|
213
|
+
"could not be located; the desktop agent may fail TLS verification against a " +
|
|
214
|
+
"custom-CA server. Set SSL_CERT_FILE to a certifi+CA bundle to override.");
|
|
215
|
+
return undefined;
|
|
216
|
+
}
|
|
217
|
+
try {
|
|
218
|
+
const bundlePath = getAgentCaBundlePath();
|
|
219
|
+
writeCertBundle(bundlePath, certifiPath, customCa);
|
|
220
|
+
return bundlePath;
|
|
221
|
+
}
|
|
222
|
+
catch (err) {
|
|
223
|
+
console.error(`[remote] Failed to build the agent CA bundle: ${err instanceof Error ? err.message : String(err)}`);
|
|
224
|
+
return undefined;
|
|
225
|
+
}
|
|
226
|
+
}
|
|
177
227
|
export function startDesktopRemote(pythonPath, scriptPath, options) {
|
|
228
|
+
const sslCertFile = resolveAgentSslCertFile(pythonPath);
|
|
178
229
|
const child = spawn(pythonPath, [scriptPath], {
|
|
179
230
|
env: {
|
|
180
231
|
...process.env,
|
|
@@ -183,6 +234,7 @@ export function startDesktopRemote(pythonPath, scriptPath, options) {
|
|
|
183
234
|
RELAY_URL: options.relayUrl,
|
|
184
235
|
DEVICE_TOKEN: options.deviceToken,
|
|
185
236
|
DEVICE_NAME: options.deviceName,
|
|
237
|
+
...(sslCertFile ? { SSL_CERT_FILE: sslCertFile } : {}),
|
|
186
238
|
},
|
|
187
239
|
stdio: "pipe",
|
|
188
240
|
});
|
package/dist/index.d.ts
CHANGED
|
@@ -1,19 +1,24 @@
|
|
|
1
|
-
export { request, getApiKey, getBaseUrl } from "./client.js";
|
|
2
1
|
export type { ClientDeps, RequestOptions } from "./client.js";
|
|
3
|
-
export {
|
|
4
|
-
export {
|
|
2
|
+
export { getApiKey, getBaseUrl, request } from "./client.js";
|
|
3
|
+
export { runCaseCreate } from "./commands/case/create.js";
|
|
4
|
+
export { runDeviceList } from "./commands/device/list.js";
|
|
5
5
|
export { runHealthCheck } from "./commands/health/check.js";
|
|
6
|
+
export { runPlanAddCase } from "./commands/plan/add-case.js";
|
|
7
|
+
export { runPlanCreate } from "./commands/plan/create.js";
|
|
8
|
+
export { runPlanRemoveCase } from "./commands/plan/remove-case.js";
|
|
6
9
|
export { runProjectList } from "./commands/project/list.js";
|
|
7
|
-
export { runDeviceList } from "./commands/device/list.js";
|
|
8
10
|
export { runSessionCreate } from "./commands/session/create.js";
|
|
9
11
|
export { runSessionGet } from "./commands/session/get.js";
|
|
10
|
-
export {
|
|
12
|
+
export { runSessionSaveAsCase } from "./commands/session/save-as-case.js";
|
|
11
13
|
export { runTriggerCaseGet } from "./commands/trigger/case/get.js";
|
|
12
|
-
export {
|
|
14
|
+
export { runTriggerCaseRun } from "./commands/trigger/case/run.js";
|
|
13
15
|
export { runTriggerPlanGet } from "./commands/trigger/plan/get.js";
|
|
16
|
+
export { runTriggerPlanRun } from "./commands/trigger/plan/run.js";
|
|
14
17
|
export { runUploadCreate } from "./commands/upload/create.js";
|
|
15
|
-
export { runUploadList } from "./commands/upload/list.js";
|
|
16
18
|
export { runUploadGet } from "./commands/upload/get.js";
|
|
19
|
+
export { runUploadList } from "./commands/upload/list.js";
|
|
17
20
|
export { runUploadPut } from "./commands/upload/put.js";
|
|
18
21
|
export { runUploadSubmit } from "./commands/upload/submit.js";
|
|
22
|
+
export { handleApiError } from "./errors.js";
|
|
23
|
+
export { formatError, formatSuccess, printResult, validationError } from "./output.js";
|
|
19
24
|
export type * from "./types.js";
|
package/dist/index.js
CHANGED
|
@@ -1,17 +1,22 @@
|
|
|
1
|
-
export {
|
|
2
|
-
export {
|
|
3
|
-
export {
|
|
1
|
+
export { getApiKey, getBaseUrl, request } from "./client.js";
|
|
2
|
+
export { runCaseCreate } from "./commands/case/create.js";
|
|
3
|
+
export { runDeviceList } from "./commands/device/list.js";
|
|
4
4
|
export { runHealthCheck } from "./commands/health/check.js";
|
|
5
|
+
export { runPlanAddCase } from "./commands/plan/add-case.js";
|
|
6
|
+
export { runPlanCreate } from "./commands/plan/create.js";
|
|
7
|
+
export { runPlanRemoveCase } from "./commands/plan/remove-case.js";
|
|
5
8
|
export { runProjectList } from "./commands/project/list.js";
|
|
6
|
-
export { runDeviceList } from "./commands/device/list.js";
|
|
7
9
|
export { runSessionCreate } from "./commands/session/create.js";
|
|
8
10
|
export { runSessionGet } from "./commands/session/get.js";
|
|
9
|
-
export {
|
|
11
|
+
export { runSessionSaveAsCase } from "./commands/session/save-as-case.js";
|
|
10
12
|
export { runTriggerCaseGet } from "./commands/trigger/case/get.js";
|
|
11
|
-
export {
|
|
13
|
+
export { runTriggerCaseRun } from "./commands/trigger/case/run.js";
|
|
12
14
|
export { runTriggerPlanGet } from "./commands/trigger/plan/get.js";
|
|
15
|
+
export { runTriggerPlanRun } from "./commands/trigger/plan/run.js";
|
|
13
16
|
export { runUploadCreate } from "./commands/upload/create.js";
|
|
14
|
-
export { runUploadList } from "./commands/upload/list.js";
|
|
15
17
|
export { runUploadGet } from "./commands/upload/get.js";
|
|
18
|
+
export { runUploadList } from "./commands/upload/list.js";
|
|
16
19
|
export { runUploadPut } from "./commands/upload/put.js";
|
|
17
20
|
export { runUploadSubmit } from "./commands/upload/submit.js";
|
|
21
|
+
export { handleApiError } from "./errors.js";
|
|
22
|
+
export { formatError, formatSuccess, printResult, validationError } from "./output.js";
|
package/dist/types.d.ts
CHANGED
|
@@ -119,6 +119,46 @@ export interface Project {
|
|
|
119
119
|
name?: string | undefined;
|
|
120
120
|
[key: string]: unknown;
|
|
121
121
|
}
|
|
122
|
+
export interface CreateCaseInput {
|
|
123
|
+
projectId: string;
|
|
124
|
+
scenario: string;
|
|
125
|
+
type?: string | undefined;
|
|
126
|
+
title?: string | undefined;
|
|
127
|
+
description?: string | undefined;
|
|
128
|
+
format?: string | undefined;
|
|
129
|
+
startUrl?: string | undefined;
|
|
130
|
+
directoryId?: string | undefined;
|
|
131
|
+
}
|
|
132
|
+
export interface CreateCaseResult {
|
|
133
|
+
success: boolean;
|
|
134
|
+
caseId: string;
|
|
135
|
+
}
|
|
136
|
+
export interface DeleteCaseResult {
|
|
137
|
+
success: boolean;
|
|
138
|
+
}
|
|
139
|
+
export interface CreatePlanInput {
|
|
140
|
+
projectId: string;
|
|
141
|
+
title: string;
|
|
142
|
+
description?: string | undefined;
|
|
143
|
+
}
|
|
144
|
+
export interface CreatePlanResult {
|
|
145
|
+
success: boolean;
|
|
146
|
+
planId: string;
|
|
147
|
+
}
|
|
148
|
+
export interface PlanCaseResult {
|
|
149
|
+
success: boolean;
|
|
150
|
+
}
|
|
151
|
+
export interface SaveSessionAsCaseInput {
|
|
152
|
+
title?: string | undefined;
|
|
153
|
+
type?: string | undefined;
|
|
154
|
+
scenario?: string | undefined;
|
|
155
|
+
format?: string | undefined;
|
|
156
|
+
}
|
|
157
|
+
export interface SaveSessionAsCaseResult {
|
|
158
|
+
success: boolean;
|
|
159
|
+
caseId: string;
|
|
160
|
+
projectId: string;
|
|
161
|
+
}
|
|
122
162
|
export interface PaginatedList<T> {
|
|
123
163
|
items: T[];
|
|
124
164
|
totalCount: number;
|