Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,10 @@
"require": {
"types": "./lib/index.d.ts",
"default": "./lib/index.js"
},
"default": {
"types": "./lib/index.d.ts",
"default": "./lib/index.js"
}
}
},
Expand Down
11 changes: 7 additions & 4 deletions src/client.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,15 +9,18 @@ const DEFAULT_TIMEOUT = 5000;
export class OneCLI {
private containerClient: ContainerClient;

constructor(options: OneCLIOptions) {
if (!options.apiKey) {
throw new OneCLIError("apiKey is required.");
constructor(options: OneCLIOptions = {}) {
const apiKey = options.apiKey ?? process.env.ONECLI_API_KEY;
if (!apiKey) {
throw new OneCLIError(
"apiKey is required. Pass it in options or set the ONECLI_API_KEY environment variable.",
);
}

const url = options.url ?? process.env.ONECLI_URL ?? DEFAULT_URL;
const timeout = options.timeout ?? DEFAULT_TIMEOUT;

this.containerClient = new ContainerClient(url, options.apiKey, timeout);
this.containerClient = new ContainerClient(url, apiKey, timeout);
}

/**
Expand Down
27 changes: 0 additions & 27 deletions src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,30 +7,3 @@ export type {
ContainerConfig,
ApplyContainerConfigOptions,
} from "./container/types.js";

// ---------------------------------------------------------------------------
// Standalone convenience function
// ---------------------------------------------------------------------------

import { OneCLI } from "./client.js";

/**
* Standalone helper: fetch the container config from OneCLI and push the
* corresponding `-e` and `-v` flags onto a Docker `run` argument array.
*
* Returns `true` if OneCLI was reachable and config was applied,
* `false` otherwise (including when `apiKey` is falsy).
*
* @param args Docker `run` argument array to mutate.
* @param apiKey User API key (`oc_...`). Pass `undefined` / empty to skip.
* @param url Base URL of OneCLI. Defaults to `ONECLI_URL` env or `https://app.onecli.sh`.
*/
export async function applyOneCLIConfig(
args: string[],
apiKey?: string | null,
url?: string,
): Promise<boolean> {
if (!apiKey) return false;
const oc = new OneCLI({ apiKey, url });
return oc.applyContainerConfig(args);
}
3 changes: 2 additions & 1 deletion src/types.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,9 @@
export interface OneCLIOptions {
/**
* User API key from the OneCLI dashboard (starts with `oc_`).
* Falls back to `ONECLI_API_KEY` env var if not provided.
*/
apiKey: string;
apiKey?: string;

/**
* Base URL of the OneCLI instance.
Expand Down
53 changes: 46 additions & 7 deletions test/client.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,31 +3,71 @@ import { OneCLI } from "../src/client.js";
import { OneCLIError } from "../src/errors.js";

describe("OneCLI", () => {
const originalEnv = process.env.ONECLI_URL;
const originalUrl = process.env.ONECLI_URL;
const originalApiKey = process.env.ONECLI_API_KEY;

beforeEach(() => {
delete process.env.ONECLI_URL;
delete process.env.ONECLI_API_KEY;
});

afterEach(() => {
if (originalEnv !== undefined) {
process.env.ONECLI_URL = originalEnv;
if (originalUrl !== undefined) {
process.env.ONECLI_URL = originalUrl;
} else {
delete process.env.ONECLI_URL;
}
if (originalApiKey !== undefined) {
process.env.ONECLI_API_KEY = originalApiKey;
} else {
delete process.env.ONECLI_API_KEY;
}
});

describe("constructor", () => {
it("throws OneCLIError when apiKey is empty string", () => {
it("throws OneCLIError when no apiKey is provided and env var is not set", () => {
expect(() => new OneCLI()).toThrow(OneCLIError);
expect(() => new OneCLI()).toThrow("apiKey is required");
});

it("throws OneCLIError when apiKey is empty string and env var is not set", () => {
expect(() => new OneCLI({ apiKey: "" })).toThrow(OneCLIError);
expect(() => new OneCLI({ apiKey: "" })).toThrow("apiKey is required.");
});

it("accepts a valid apiKey", () => {
it("accepts apiKey from options", () => {
const oc = new OneCLI({ apiKey: "oc_test123" });
expect(oc).toBeInstanceOf(OneCLI);
});

it("falls back to ONECLI_API_KEY env var", () => {
process.env.ONECLI_API_KEY = "oc_from_env";
const oc = new OneCLI();
expect(oc).toBeInstanceOf(OneCLI);
});

it("prefers options.apiKey over ONECLI_API_KEY env var", () => {
process.env.ONECLI_API_KEY = "oc_from_env";

const fetchSpy = vi.spyOn(globalThis, "fetch").mockResolvedValue(
new Response(JSON.stringify({ env: {}, caCertificate: "", caCertificateContainerPath: "" })),
);

const oc = new OneCLI({
apiKey: "oc_from_options",
url: "http://localhost:3000",
});
oc.getContainerConfig();

expect(fetchSpy).toHaveBeenCalledWith(
expect.any(String),
expect.objectContaining({
headers: { Authorization: "Bearer oc_from_options" },
}),
);

fetchSpy.mockRestore();
});

it("uses url from options when provided", () => {
const oc = new OneCLI({
apiKey: "oc_test",
Expand All @@ -45,7 +85,6 @@ describe("OneCLI", () => {
it("prefers options.url over ONECLI_URL env var", () => {
process.env.ONECLI_URL = "http://env-url:3000";

// We can verify by checking that fetch is called with the right URL
const fetchSpy = vi.spyOn(globalThis, "fetch").mockResolvedValue(
new Response(JSON.stringify({ env: {}, caCertificate: "", caCertificateContainerPath: "" })),
);
Expand Down
105 changes: 9 additions & 96 deletions test/index.test.ts
Original file line number Diff line number Diff line change
@@ -1,103 +1,16 @@
import { describe, it, expect, vi, afterEach } from "vitest";
import { applyOneCLIConfig } from "../src/index.js";
import { describe, it, expect } from "vitest";
import { OneCLI, OneCLIError, ContainerClient } from "../src/index.js";

const MOCK_CONFIG = {
env: {
HTTPS_PROXY: "http://x:aoc_token@proxy:18080",
HTTP_PROXY: "http://x:aoc_token@proxy:18080",
NODE_EXTRA_CA_CERTS: "/tmp/onecli-proxy-ca.pem",
NODE_USE_ENV_PROXY: "1",
},
caCertificate:
"-----BEGIN CERTIFICATE-----\nTEST_CA\n-----END CERTIFICATE-----",
caCertificateContainerPath: "/tmp/onecli-proxy-ca.pem",
};

describe("applyOneCLIConfig", () => {
let fetchSpy: ReturnType<typeof vi.spyOn>;

afterEach(() => {
fetchSpy?.mockRestore();
});

it("returns false when apiKey is null", async () => {
const args: string[] = [];
const result = await applyOneCLIConfig(args, null);
expect(result).toBe(false);
expect(args).toEqual([]);
});

it("returns false when apiKey is undefined", async () => {
const args: string[] = [];
const result = await applyOneCLIConfig(args, undefined);
expect(result).toBe(false);
expect(args).toEqual([]);
});

it("returns false when apiKey is empty string", async () => {
const args: string[] = [];
const result = await applyOneCLIConfig(args, "");
expect(result).toBe(false);
expect(args).toEqual([]);
});

it("returns true and mutates args on success", async () => {
fetchSpy = vi.spyOn(globalThis, "fetch").mockResolvedValue(
new Response(JSON.stringify(MOCK_CONFIG)),
);

const args = ["run", "-i", "--rm"];
const result = await applyOneCLIConfig(
args,
"oc_test",
"http://localhost:3000",
);

expect(result).toBe(true);
expect(args.length).toBeGreaterThan(3);
expect(args).toContain("-e");
});

it("returns false when server is unreachable", async () => {
fetchSpy = vi
.spyOn(globalThis, "fetch")
.mockRejectedValue(new Error("ECONNREFUSED"));

const args = ["run", "-i", "--rm"];
const result = await applyOneCLIConfig(
args,
"oc_test",
"http://localhost:3000",
);

expect(result).toBe(false);
describe("package exports", () => {
it("exports OneCLI class", () => {
expect(OneCLI).toBeDefined();
});

it("passes url to the OneCLI client", async () => {
fetchSpy = vi.spyOn(globalThis, "fetch").mockResolvedValue(
new Response(JSON.stringify(MOCK_CONFIG)),
);

await applyOneCLIConfig([], "oc_test", "http://custom:4000");

expect(fetchSpy).toHaveBeenCalledWith(
"http://custom:4000/api/container-config",
expect.any(Object),
);
it("exports OneCLIError", () => {
expect(OneCLIError).toBeDefined();
});

it("passes apiKey as Bearer token", async () => {
fetchSpy = vi.spyOn(globalThis, "fetch").mockResolvedValue(
new Response(JSON.stringify(MOCK_CONFIG)),
);

await applyOneCLIConfig([], "oc_mykey123", "http://localhost:3000");

expect(fetchSpy).toHaveBeenCalledWith(
expect.any(String),
expect.objectContaining({
headers: { Authorization: "Bearer oc_mykey123" },
}),
);
it("exports ContainerClient", () => {
expect(ContainerClient).toBeDefined();
});
});
Loading