@usefractal/fracctl-core
v0.3.1
Published
Core implementation package for building Fractal CLIs and developer tools.
Readme
@usefractal/fracctl-core
Core implementation package for building Fractal CLIs and developer tools.
fracctl-core contains the implementation for Fractal auth, deployment, database, and cloud storage operations. A CLI wrapper should own command routing, help text, flag parsing, frac.json lookup, and output formatting.
Install
npm install @usefractal/fracctl-corepnpm add @usefractal/fracctl-coreTypeScript
The package ships TypeScript declarations. Consumers can import it directly:
import { createFracctlCore, get_runtime_config, type FracctlEnvironment } from "@usefractal/fracctl-core";The published package exposes dist/index.d.ts through both types and the package exports map.
Runtime Config
The Fractal API URL and WorkOS CLI client ID have package defaults. Both are still configurable: pass explicit values to createFracctlCore(...) or set env vars and read them with get_runtime_config(...).
import { createFracctlCore } from "@usefractal/fracctl-core";
const core = createFracctlCore();To override either value explicitly:
const core = createFracctlCore({
baseUrl: "https://your-fractal-api.example.com",
workosClientId: "client_your_workos_client_id",
});If your CLI wants to read Fractal environment variables, use the exported helper explicitly:
import { get_runtime_config } from "@usefractal/fracctl-core";
const runtime = get_runtime_config(process.env);
const baseUrl = runtime.baseUrl;
const workosClientId = runtime.workosClientId;
const projectId = runtime.projectId;
const environment = runtime.environment ?? "dev";get_runtime_config(env) reads:
FRACTAL_API_URLFRACTAL_WORKOS_CLIENT_IDFRACTAL_PROJECT_IDFRACTAL_CLOUD_ENVIRONMENT
This is optional. Callers can pass their own values from flags, config files, UI inputs, tests, or any other source.
The same helper is also available on the core object:
const runtime = core.get_runtime_config(process.env);API
type FracctlCore = {
get_runtime_config(env?: NodeJS.ProcessEnv): FracctlRuntimeConfig;
auth_login(): Promise<void>;
auth_status(): Promise<void>;
auth_logout(): Promise<void>;
login(): Promise<void>;
status(): Promise<void>;
logout(): Promise<void>;
list_projects(): Promise<Project[]>;
create_deployment_project(options: { name: string }): Promise<Project>;
assert_project_exists(options: { projectId: string }): Promise<Project>;
deploy_project(options: { projectId: string; sourceDir: string }): Promise<{ deploymentId?: string }>;
create_cloud_storage_token(options: {
projectId: string;
environment: "dev" | "prod";
expiresInSeconds?: number;
}): Promise<{ token: string; expiresAt: string; origin: string }>;
get_database_connection_url(options: {
projectId: string;
environment: "dev" | "prod";
}): Promise<string>;
open_database_shell(options: {
projectId: string;
environment: "dev" | "prod";
}): Promise<void>;
enable_cloud_storage(options: { projectId: string }): Promise<Project>;
cloud_storage_put(options: {
projectId: string;
environment: "dev" | "prod";
localFile: string;
remotePath: string;
contentType?: string;
expiresInSeconds?: number;
}): Promise<{ key: string; size: number }>;
cloud_storage_get(options: {
projectId: string;
environment: "dev" | "prod";
remotePath: string;
expiresInSeconds?: number;
}): Promise<{ body: Uint8Array }>;
cloud_storage_list(options: {
projectId: string;
environment: "dev" | "prod";
prefix?: string;
limit?: number;
expiresInSeconds?: number;
}): Promise<CloudStorageListResponse>;
};Implementing CLI Commands
The wrapper CLI should parse flags and then call the corresponding core method with explicit values.
CLI Wrapper Example
This example shows the expected split: the wrapper reads env vars, parses flags, reads frac.json, resolves projectId, then passes explicit values into core.
#!/usr/bin/env node
import { readFile } from "node:fs/promises";
import { dirname, join, resolve } from "node:path";
import { CliError, createFracctlCore, get_runtime_config, type FracctlEnvironment } from "@usefractal/fracctl-core";
type FracJson = {
projectId: string;
};
function readOption(args: string[], names: string[]): string | undefined {
for (let index = 0; index < args.length; index += 1) {
const arg = args[index];
for (const name of names) {
if (arg === name) return args[index + 1];
if (arg.startsWith(`${name}=`)) return arg.slice(name.length + 1);
}
}
return undefined;
}
function readProjectOption(args: string[]): string | undefined {
return readOption(args, ["--project", "--project-id", "-p"]);
}
function readEnvironment(args: string[], fallback?: FracctlEnvironment): FracctlEnvironment {
const environment = readOption(args, ["--env", "-e"]) || fallback || "dev";
if (environment !== "dev" && environment !== "prod") {
throw new CliError("--env must be either dev or prod.");
}
return environment;
}
async function findFracJson(startDir: string): Promise<FracJson | null> {
let dir = resolve(startDir);
while (true) {
const candidate = join(dir, "frac.json");
try {
const parsed = JSON.parse(await readFile(candidate, "utf8")) as Partial<FracJson>;
if (!parsed.projectId || typeof parsed.projectId !== "string") {
throw new CliError(`${candidate} is missing projectId.`);
}
return { projectId: parsed.projectId };
} catch (error) {
if (error instanceof CliError) throw error;
const code = typeof error === "object" && error && "code" in error ? (error as { code?: string }).code : undefined;
if (code !== "ENOENT") {
throw new CliError(`Could not read ${candidate}: ${error instanceof Error ? error.message : String(error)}`);
}
}
const parent = dirname(dir);
if (parent === dir) return null;
dir = parent;
}
}
async function resolveProjectId(args: string[], envProjectId?: string): Promise<string> {
const flagProjectId = readProjectOption(args);
if (flagProjectId) return flagProjectId;
const fracJson = await findFracJson(process.cwd());
if (fracJson) return fracJson.projectId;
if (envProjectId) return envProjectId;
throw new CliError("Project ID is required. Pass --project <id>, add frac.json, or set FRACTAL_PROJECT_ID.");
}
async function main(argv: string[]): Promise<void> {
const runtime = get_runtime_config(process.env);
const core = createFracctlCore({
baseUrl: runtime.baseUrl,
workosClientId: runtime.workosClientId,
});
const [command, ...args] = argv;
if (command === "auth:login") {
await core.auth_login();
return;
}
if (command === "token") {
const projectId = await resolveProjectId(args, runtime.projectId);
const environment = readEnvironment(args, runtime.environment);
const token = await core.create_cloud_storage_token({ projectId, environment });
console.log(token.token);
return;
}
if (command === "sql") {
const projectId = await resolveProjectId(args, runtime.projectId);
const environment = readEnvironment(args, runtime.environment);
console.log(await core.get_database_connection_url({ projectId, environment }));
return;
}
throw new CliError(`Unknown command: ${command || ""}`);
}
main(process.argv.slice(2)).catch((error: unknown) => {
console.error(error instanceof Error ? error.message : String(error));
process.exitCode = 1;
});Auth
await core.auth_login();
await core.auth_status();
await core.auth_logout();auth_login() starts the WorkOS device login flow and stores the session in macOS Keychain. auth_status() prints the current session status. auth_logout() removes the stored session.
Token
const token = await core.create_cloud_storage_token({
projectId: "project_123",
environment: "dev",
expiresInSeconds: 3600,
});
console.log(`export FRACTAL_CLOUD_TOKEN='${token.token}'`);
console.log(`export FRACTAL_CLOUD_STORAGE_ORIGIN='${token.origin}'`);A CLI can source projectId from --project, frac.json, FRACTAL_PROJECT_ID, or a prompt. Core just receives the final value.
SQL
const connectionUrl = await core.get_database_connection_url({
projectId: "project_123",
environment: "dev",
});
console.log(connectionUrl);To open psql directly:
await core.open_database_shell({
projectId: "project_123",
environment: "dev",
});Deploy
await core.assert_project_exists({ projectId: "project_123" });
const deployment = await core.deploy_project({
projectId: "project_123",
sourceDir: process.cwd(),
});
console.log(deployment.deploymentId);If a CLI wants to create a deployment-only project when no project is configured:
const project = await core.create_deployment_project({
name: "My Deployment",
});
await core.deploy_project({
projectId: project.id,
sourceDir: process.cwd(),
});deploy_project() packages sourceDir, uploads it to Fractal, and starts a deployment. It uses .gitignore when packaging from a git worktree and excludes common secret/build paths such as .env, .ssh, .aws, .wrangler, node_modules, dist, and private key files.
Cloud Storage
Enable cloud storage:
await core.enable_cloud_storage({
projectId: "project_123",
});Upload an object:
const uploaded = await core.cloud_storage_put({
projectId: "project_123",
environment: "dev",
localFile: "./hello.txt",
remotePath: "hello.txt",
});
console.log(uploaded.key, uploaded.size);Download an object:
import { writeFile } from "node:fs/promises";
const downloaded = await core.cloud_storage_get({
projectId: "project_123",
environment: "dev",
remotePath: "hello.txt",
});
await writeFile("./downloaded-hello.txt", downloaded.body);List objects:
const listing = await core.cloud_storage_list({
projectId: "project_123",
environment: "dev",
prefix: "uploads/",
limit: 100,
});
for (const object of listing.objects) {
console.log(object.key, object.size, object.uploadedAt);
}Error Handling
Core throws CliError for expected user-facing failures. A wrapper CLI should catch errors at the process boundary and print the message.
try {
await core.cloud_storage_put({
projectId: "project_123",
environment: "dev",
localFile: "./hello.txt",
remotePath: "hello.txt",
});
} catch (error) {
if (error instanceof CliError) {
console.error(error.message);
process.exitCode = 1;
} else {
throw error;
}
}Runtime Requirements
- Node.js 20 or newer.
- macOS for Keychain-backed auth storage.
psqlonPATHforopen_database_shell().- A valid Fractal API URL and WorkOS client ID provided by the caller.
