feat(FN-3766): add cli press sqlite store and credential helpers

Implements SQLite-based storage and credential management for the CLI printing press plugin, including type definitions, a persistent `cli-press-store`, and credential helpers, with updated routes, tests, and documentation.

Fusion-Task-Id: FN-3766
This commit is contained in:
Fusion
2026-05-10 18:11:03 -07:00
committed by gsxdsm
parent f7450ba3a2
commit 5a1518acd8
11 changed files with 1004 additions and 98 deletions

View File

@@ -3,6 +3,7 @@ import { createServer } from "node:http";
import { tmpdir } from "node:os";
import { join } from "node:path";
import { afterEach, describe, expect, it } from "vitest";
import { Database } from "@fusion/core";
import { createCliPrintingPressRoutes } from "../routes/wizard-routes.js";
import type { ServiceDraft } from "../wizard/types.js";
@@ -49,7 +50,9 @@ describe("run routes", () => {
res.end("pong");
});
const rootDir = await mkdtemp(join(tmpdir(), "cli-printing-press-run-routes-"));
const ctx = { taskStore: { getRootDir: () => rootDir } } as any;
const db = new Database(join(rootDir, ".fusion"), { inMemory: true });
db.init();
const ctx = { taskStore: { getRootDir: () => rootDir, getDatabase: () => db } } as any;
const createRes = await route("POST", "/drafts").handler({ params: {}, body: makeDraft(baseUrl) }, ctx);
const id = (createRes.body as { id: string }).id;
@@ -71,7 +74,9 @@ describe("run routes", () => {
}, 50);
});
const rootDir = await mkdtemp(join(tmpdir(), "cli-printing-press-run-routes-"));
const ctx = { taskStore: { getRootDir: () => rootDir } } as any;
const db = new Database(join(rootDir, ".fusion"), { inMemory: true });
db.init();
const ctx = { taskStore: { getRootDir: () => rootDir, getDatabase: () => db } } as any;
const createRes = await route("POST", "/drafts").handler({ params: {}, body: makeDraft(baseUrl) }, ctx);
const id = (createRes.body as { id: string }).id;

View File

@@ -2,6 +2,7 @@ import { mkdtemp } from "node:fs/promises";
import { tmpdir } from "node:os";
import { join } from "node:path";
import { describe, expect, it } from "vitest";
import { Database } from "@fusion/core";
import { createCliPrintingPressRoutes } from "../routes/wizard-routes";
import type { ServiceDraft } from "../wizard/types";
@@ -19,7 +20,9 @@ function route(method: string, path: string) {
describe("wizard routes", () => {
it("handles create/get/delete lifecycle", async () => {
const rootDir = await mkdtemp(join(tmpdir(), "cli-printing-press-routes-"));
const ctx = { taskStore: { getRootDir: () => rootDir } } as any;
const db = new Database(join(rootDir, ".fusion"), { inMemory: true });
db.init();
const ctx = { taskStore: { getRootDir: () => rootDir, getDatabase: () => db } } as any;
const createRes = await route("POST", "/drafts").handler({ params: {}, body: makeDraft() }, ctx);
expect(createRes.status).toBe(201);

View File

@@ -1,5 +1,6 @@
import { definePlugin } from "@fusion/plugin-sdk";
import { createCliPrintingPressRoutes } from "./routes/wizard-routes.js";
import { ensureCliPressSchema } from "./store/cli-press-store.js";
const plugin = definePlugin({
manifest: {
@@ -9,7 +10,9 @@ const plugin = definePlugin({
description: "Guided wizard for drafting external service CLI definitions",
},
state: "installed",
hooks: {},
hooks: {
onSchemaInit: ensureCliPressSchema,
},
routes: createCliPrintingPressRoutes(),
dashboardViews: [
{
@@ -35,3 +38,5 @@ export default plugin;
export { CliPrintingPressWizardView } from "./dashboard-view.js";
export { CliPrintingPressManageView } from "./manage-view.js";
export { CliPrintingPressTestRunner } from "./run/TestRunnerPanel.js";
export { createCliPressStore, ensureCliPressSchema } from "./store/cli-press-store.js";
export * from "./store/cli-press-types.js";

View File

@@ -2,7 +2,8 @@ import type { PluginContext, PluginRouteDefinition, PluginRouteResult } from "@f
import { generateCli } from "../generation/generator.js";
import { runGeneratedCli } from "../generation/runner.js";
import type { GeneratedCliArtifact, RunRequest } from "../generation/types.js";
import { createDraftStore, getArtifactDir, NotFoundError } from "../storage/draft-store.js";
import { createCliPressStore } from "../store/cli-press-store.js";
import type { CliSpec, Service } from "../store/cli-press-types.js";
import type { ServiceDraft } from "../wizard/types.js";
import { validateDraft } from "../wizard/validation.js";
@@ -25,6 +26,32 @@ function asArtifact(draft: ServiceDraft): GeneratedCliArtifact | null {
};
}
function getArtifactDir(id: string, projectRoot: string): string {
return `${projectRoot}/.fusion/plugins/cli-printing-press/artifacts/${id}`;
}
function getStore(ctx: PluginContext) {
return createCliPressStore(ctx.taskStore.getDatabase());
}
function toDraft(service: Service, spec: CliSpec | undefined, endpoints: ServiceDraft["endpoints"]): ServiceDraft {
return {
id: service.id,
name: service.displayName,
slug: service.slug,
description: service.description ?? "",
baseUrl: service.baseUrl,
transport: "http",
endpoints,
credential: { kind: "none" },
createdAt: service.createdAt,
updatedAt: service.updatedAt,
generatedAt: spec?.generatedAt,
regeneratedAt: spec?.generatedAt,
artifactPath: spec?.status === "generated" ? spec.specJson : undefined,
};
}
function isPrimitive(value: unknown): value is string | number | boolean {
return typeof value === "string" || typeof value === "number" || typeof value === "boolean";
}
@@ -69,8 +96,27 @@ export function createCliPrintingPressRoutes(): PluginRouteDefinition[] {
const draft = request.body as ServiceDraft;
const result = validateDraft(draft);
if (!result.ok) return { status: 400, body: { error: Object.values(result.errors)[0] ?? "Validation failed", errors: result.errors } };
const store = createDraftStore({ rootDir: ctx.taskStore.getRootDir() });
const created = await store.create(draft);
const store = getStore(ctx);
const createdService = store.createService({
slug: draft.slug,
displayName: draft.name,
description: draft.description,
baseUrl: draft.baseUrl,
sourceKind: "manual",
sourceRef: undefined,
});
const createdSpec = store.createSpec({
serviceId: createdService.id,
name: `${draft.slug}-cli`,
version: "0.1.0",
generatorVersion: "cli-printing-press",
specJson: JSON.stringify(draft),
status: "draft",
generatedAt: undefined,
lastGenerationError: undefined,
});
store.setSetting({ serviceId: createdService.id, key: "endpoints", value: JSON.stringify(draft.endpoints), scope: "wizard" });
const created = toDraft(createdService, createdSpec, draft.endpoints);
return ok(created, 201);
},
},
@@ -78,8 +124,14 @@ export function createCliPrintingPressRoutes(): PluginRouteDefinition[] {
method: "GET",
path: "/drafts",
handler: async (_req, ctx: PluginContext) => {
const store = createDraftStore({ rootDir: ctx.taskStore.getRootDir() });
return ok(await store.list());
const store = getStore(ctx);
const drafts = store.listServices().map((service) => ({
id: service.id,
name: service.displayName,
slug: service.slug,
updatedAt: service.updatedAt,
}));
return ok(drafts);
},
},
{
@@ -87,9 +139,13 @@ export function createCliPrintingPressRoutes(): PluginRouteDefinition[] {
path: "/drafts/:id",
handler: async (req, ctx: PluginContext) => {
const request = asRequest(req);
const store = createDraftStore({ rootDir: ctx.taskStore.getRootDir() });
const draft = await store.get(request.params.id);
return draft ? ok(draft) : ok({ error: "Draft not found" }, 404);
const store = getStore(ctx);
const service = store.getService(request.params.id);
if (!service) return ok({ error: "Draft not found" }, 404);
const spec = store.listSpecs(service.id)[0];
const endpointsSetting = store.listSettings(service.id).find((entry) => entry.key === "endpoints" && entry.scope === "wizard");
const endpoints = endpointsSetting ? (JSON.parse(endpointsSetting.value) as ServiceDraft["endpoints"]) : [];
return ok(toDraft(service, spec, endpoints));
},
},
{
@@ -100,14 +156,30 @@ export function createCliPrintingPressRoutes(): PluginRouteDefinition[] {
const draft = request.body as ServiceDraft;
const result = validateDraft(draft);
if (!result.ok) return { status: 400, body: { error: Object.values(result.errors)[0] ?? "Validation failed", errors: result.errors } };
const store = createDraftStore({ rootDir: ctx.taskStore.getRootDir() });
try {
const updated = await store.update(request.params.id, draft);
return ok(updated);
} catch (error) {
if (error instanceof NotFoundError) return ok({ error: "Draft not found" }, 404);
throw error;
}
const store = getStore(ctx);
const service = store.getService(request.params.id);
if (!service) return ok({ error: "Draft not found" }, 404);
const updatedService = store.updateService(service.id, {
displayName: draft.name,
description: draft.description,
baseUrl: draft.baseUrl,
sourceKind: "manual",
});
const existingSpec = store.listSpecs(service.id)[0];
const updatedSpec = existingSpec
? store.updateSpec(existingSpec.id, { specJson: JSON.stringify(draft), status: "draft", lastGenerationError: undefined })
: store.createSpec({
serviceId: service.id,
name: `${draft.slug}-cli`,
version: "0.1.0",
generatorVersion: "cli-printing-press",
specJson: JSON.stringify(draft),
status: "draft",
generatedAt: undefined,
lastGenerationError: undefined,
});
store.setSetting({ serviceId: service.id, key: "endpoints", value: JSON.stringify(draft.endpoints), scope: "wizard" });
return ok(toDraft(updatedService, updatedSpec, draft.endpoints));
},
},
{
@@ -116,17 +188,28 @@ export function createCliPrintingPressRoutes(): PluginRouteDefinition[] {
handler: async (req, ctx: PluginContext) => {
const request = asRequest(req);
const projectRoot = ctx.taskStore.getRootDir();
const store = createDraftStore({ rootDir: projectRoot });
const existing = await store.get(request.params.id);
if (!existing) return ok({ error: "Draft not found" }, 404);
const artifact = await generateCli({ draft: existing, outDir: getArtifactDir(existing.id, projectRoot) });
const draft = await store.update(request.params.id, {
regeneratedAt: artifact.generatedAt,
const store = getStore(ctx);
const service = store.getService(request.params.id);
if (!service) return ok({ error: "Draft not found" }, 404);
const spec = store.listSpecs(service.id)[0];
if (!spec) return ok({ error: "Draft not found" }, 404);
const draft = JSON.parse(spec.specJson) as ServiceDraft;
const artifact = await generateCli({ draft, outDir: getArtifactDir(draft.id, projectRoot) });
const updatedSpec = store.updateSpec(spec.id, {
specJson: JSON.stringify({ ...draft, regeneratedAt: artifact.generatedAt, generatedAt: artifact.generatedAt, artifactPath: artifact.binPath }),
generatedAt: artifact.generatedAt,
artifactPath: artifact.binPath,
status: "generated",
lastGenerationError: undefined,
});
return ok({ draft, artifact });
store.createArtifact({
cliSpecId: spec.id,
kind: "script",
path: artifact.binPath.replace(`${projectRoot}/.fusion/`, ""),
executable: true,
checksum: undefined,
sizeBytes: undefined,
});
return ok({ draft: JSON.parse(updatedSpec.specJson) as ServiceDraft, artifact });
},
},
{
@@ -137,10 +220,12 @@ export function createCliPrintingPressRoutes(): PluginRouteDefinition[] {
const parsed = validateRunRequest(request.body);
if (!parsed.ok) return ok({ error: parsed.error }, 400);
const store = createDraftStore({ rootDir: ctx.taskStore.getRootDir() });
const draft = await store.get(request.params.id);
if (!draft) return ok({ error: "Draft not found" }, 404);
const store = getStore(ctx);
const service = store.getService(request.params.id);
if (!service) return ok({ error: "Draft not found" }, 404);
const spec = store.listSpecs(service.id)[0];
if (!spec) return ok({ error: "Draft not found" }, 404);
const draft = JSON.parse(spec.specJson) as ServiceDraft;
const artifact = asArtifact(draft);
if (!artifact) return ok({ error: "Draft has not been generated yet" }, 409);
@@ -163,9 +248,13 @@ export function createCliPrintingPressRoutes(): PluginRouteDefinition[] {
path: "/drafts/:id/artifact",
handler: async (req, ctx: PluginContext) => {
const request = asRequest(req);
const store = createDraftStore({ rootDir: ctx.taskStore.getRootDir() });
const draft = await store.get(request.params.id);
const artifact = draft ? asArtifact(draft) : null;
const store = getStore(ctx);
const service = store.getService(request.params.id);
if (!service) return ok({ error: "Artifact not found" }, 404);
const spec = store.listSpecs(service.id)[0];
if (!spec) return ok({ error: "Artifact not found" }, 404);
const draft = JSON.parse(spec.specJson) as ServiceDraft;
const artifact = asArtifact(draft);
return artifact ? ok({ artifact }) : ok({ error: "Artifact not found" }, 404);
},
},
@@ -174,8 +263,8 @@ export function createCliPrintingPressRoutes(): PluginRouteDefinition[] {
path: "/drafts/:id",
handler: async (req, ctx: PluginContext) => {
const request = asRequest(req);
const store = createDraftStore({ rootDir: ctx.taskStore.getRootDir() });
await store.delete(request.params.id);
const store = getStore(ctx);
store.deleteService(request.params.id);
return { status: 204 };
},
},

View File

@@ -0,0 +1,128 @@
import { mkdtempSync } from "node:fs";
import { rm } from "node:fs/promises";
import { tmpdir } from "node:os";
import { join } from "node:path";
import { Database } from "@fusion/core";
import { afterEach, beforeEach, describe, expect, it } from "vitest";
import { createCliPressStore, ensureCliPressSchema } from "../cli-press-store.js";
import { encodeCredentialValue } from "../credentials.js";
describe("cli-press-store", () => {
let rootDir: string;
let db: Database;
let store: ReturnType<typeof createCliPressStore>;
beforeEach(() => {
rootDir = mkdtempSync(join(tmpdir(), "cli-press-store-"));
db = new Database(join(rootDir, ".fusion"), { inMemory: true });
db.init();
ensureCliPressSchema(db);
ensureCliPressSchema(db);
store = createCliPressStore(db);
});
afterEach(async () => {
db.close();
await rm(rootDir, { recursive: true, force: true });
});
it("creates schema idempotently and runs full CRUD", () => {
const service = store.createService({
slug: "demo",
displayName: "Demo",
description: "Demo service",
baseUrl: "https://example.com",
sourceKind: "manual",
sourceRef: "wizard",
});
expect(service.id).toMatch(/^svc_/);
const updatedService = store.updateService(service.id, { displayName: "Demo Updated" });
expect(updatedService.displayName).toBe("Demo Updated");
const spec = store.createSpec({
serviceId: service.id,
name: "demo-cli",
version: "0.1.0",
generatorVersion: "1.0.0",
specJson: JSON.stringify({ hello: "world" }),
status: "draft",
generatedAt: undefined,
lastGenerationError: undefined,
});
expect(store.getSpec(spec.id)?.specJson).toBe(JSON.stringify({ hello: "world" }));
const updatedSpec = store.updateSpec(spec.id, { status: "generated" });
expect(updatedSpec.status).toBe("generated");
const artifact = store.createArtifact({
cliSpecId: spec.id,
kind: "script",
path: "plugins/cli-printing-press/artifacts/demo.sh",
executable: true,
checksum: "abc",
sizeBytes: 42,
});
expect(artifact.id).toMatch(/^art_/);
const cred = store.createCredential({
serviceId: service.id,
name: "api",
kind: "api_key",
value: encodeCredentialValue("secret"),
placement: { kind: "api_key", header: "X-API-Key" },
});
expect(cred.id).toMatch(/^cred_/);
const setting = store.setSetting({
serviceId: service.id,
key: "region",
value: "us-east-1",
scope: "runtime",
});
expect(setting.id).toMatch(/^set_/);
expect(store.listServices()).toHaveLength(1);
expect(store.listSpecs(service.id)).toHaveLength(1);
expect(store.listArtifacts(spec.id)).toHaveLength(1);
expect(store.listCredentials(service.id)).toHaveLength(1);
expect(store.listSettings(service.id)).toHaveLength(1);
store.deleteService(service.id);
expect(store.listServices()).toHaveLength(0);
expect(store.listSpecs(service.id)).toHaveLength(0);
expect(store.listCredentials(service.id)).toHaveLength(0);
expect(store.listSettings(service.id)).toHaveLength(0);
});
it("rejects oauth and invalid placement", () => {
const service = store.createService({
slug: "oauth-demo",
displayName: "OAuth Demo",
description: "",
baseUrl: "https://example.com",
sourceKind: "manual",
sourceRef: "wizard",
});
expect(() =>
store.createCredential({
serviceId: service.id,
name: "bad",
kind: "oauth" as never,
value: encodeCredentialValue("x"),
placement: { kind: "header", header: "Authorization" },
}),
).toThrow("not supported");
expect(() =>
store.createCredential({
serviceId: service.id,
name: "bad2",
kind: "api_key",
value: encodeCredentialValue("x"),
placement: { kind: "api_key", header: "X", queryParam: "token" },
}),
).toThrow("Invalid credential placement");
});
});

View File

@@ -0,0 +1,62 @@
import { describe, expect, it } from "vitest";
import type { Credential } from "../cli-press-types.js";
import { applyCredentialToRequest, decodeCredentialValue, encodeCredentialValue } from "../credentials.js";
function baseCredential(partial: Partial<Credential>): Credential {
return {
id: "cred_1",
serviceId: "svc_1",
name: "cred",
kind: "header",
value: encodeCredentialValue("secret"),
placement: { kind: "header", header: "X-Custom-Token" },
createdAt: new Date().toISOString(),
updatedAt: new Date().toISOString(),
...partial,
} as Credential;
}
describe("credentials", () => {
it("encodes and decodes", () => {
const encoded = encodeCredentialValue("alice:s3cret");
expect(encoded.encoding).toBe("base64");
expect(decodeCredentialValue(encoded)).toBe("alice:s3cret");
});
it("applies all credential kinds", () => {
const request = { headers: {}, query: {}, env: {} };
applyCredentialToRequest(baseCredential({ kind: "header", placement: { kind: "header", header: "X-Custom-Token" } }), request);
applyCredentialToRequest(baseCredential({ kind: "query_param", placement: { kind: "query_param", queryParam: "api_token" } }), request);
applyCredentialToRequest(baseCredential({ kind: "env_var", placement: { kind: "env_var", envVar: "GITHUB_TOKEN" } }), request);
applyCredentialToRequest(baseCredential({ kind: "bearer_token", placement: { kind: "bearer_token", header: "Authorization" } }), request);
applyCredentialToRequest(baseCredential({ kind: "api_key", placement: { kind: "api_key", header: "X-API-Key" } }), request);
applyCredentialToRequest(baseCredential({ kind: "api_key", placement: { kind: "api_key", queryParam: "api_key" } }), request);
applyCredentialToRequest(
baseCredential({
kind: "basic_auth",
value: encodeCredentialValue("alice:s3cret"),
placement: { kind: "basic_auth", header: "Authorization" },
}),
request,
);
expect(request.headers["X-Custom-Token"]).toBe("secret");
expect(request.query.api_token).toBe("secret");
expect(request.env.GITHUB_TOKEN).toBe("secret");
expect(request.headers.Authorization).toBe("Basic YWxpY2U6czNjcmV0");
expect(request.headers["X-API-Key"]).toBe("secret");
expect(request.query.api_key).toBe("secret");
});
it("throws on placement mismatch and oauth", () => {
const request = { headers: {}, query: {}, env: {} };
expect(() =>
applyCredentialToRequest(baseCredential({ kind: "header", placement: { kind: "query_param", queryParam: "x" } as any }), request),
).toThrow("Invalid credential placement");
expect(() =>
applyCredentialToRequest(baseCredential({ kind: "oauth" as any, placement: { kind: "oauth", header: "Authorization" } as any }), request),
).toThrow("not supported");
});
});

View File

@@ -0,0 +1,427 @@
import { randomUUID } from "node:crypto";
import type { Database } from "@fusion/core";
import {
InvalidCredentialPlacementError,
OAuthNotSupportedError,
type CliArtifact,
type CliArtifactCreateInput,
type CliArtifactUpdateInput,
type CliSpec,
type CliSpecCreateInput,
type CliSpecUpdateInput,
type Credential,
type CredentialCreateInput,
type CredentialUpdateInput,
type Service,
type ServiceCreateInput,
type ServiceSetting,
type ServiceSettingCreateInput,
type ServiceUpdateInput,
} from "./cli-press-types.js";
interface ServiceRow {
id: string;
slug: string;
displayName: string;
description: string | null;
baseUrl: string;
sourceKind: Service["sourceKind"];
sourceRef: string | null;
createdAt: string;
updatedAt: string;
}
interface CliSpecRow {
id: string;
serviceId: string;
name: string;
version: string;
generatorVersion: string;
specJson: string;
generatedAt: string | null;
status: CliSpec["status"];
lastGenerationError: string | null;
createdAt: string;
updatedAt: string;
}
interface CliArtifactRow {
id: string;
cliSpecId: string;
kind: CliArtifact["kind"];
path: string;
executable: number;
checksum: string | null;
sizeBytes: number | null;
createdAt: string;
updatedAt: string;
}
interface CredentialRow {
id: string;
serviceId: string;
name: string;
kind: Credential["kind"];
value: string;
placement: string;
createdAt: string;
updatedAt: string;
}
interface ServiceSettingRow {
id: string;
serviceId: string;
key: string;
value: string;
scope: ServiceSetting["scope"];
createdAt: string;
updatedAt: string;
}
const OAUTH_KINDS = new Set(["oauth", "oauth2"]);
function parseJson<T>(value: string): T {
return JSON.parse(value) as T;
}
function nowIso(): string {
return new Date().toISOString();
}
function createId(prefix: "svc" | "cli" | "art" | "cred" | "set"): string {
return `${prefix}_${randomUUID()}`;
}
function assertCredentialSupported(kind: string): void {
if (OAUTH_KINDS.has(kind)) {
throw new OAuthNotSupportedError(kind);
}
}
function assertPlacementConsistency(
kind: Credential["kind"] | string,
placement: Credential["placement"] | { kind?: string; header?: string; queryParam?: string },
): void {
const placementKind = (placement as { kind?: string }).kind;
if (placementKind !== kind) {
throw new InvalidCredentialPlacementError({ credentialKind: kind, placementKind: String(placementKind) });
}
if (kind === "api_key") {
const candidate = placement as { kind?: string; header?: string; queryParam?: string };
const hasHeader = typeof candidate.header === "string" && candidate.header.trim().length > 0;
const hasQuery = typeof candidate.queryParam === "string" && candidate.queryParam.trim().length > 0;
if ((hasHeader ? 1 : 0) + (hasQuery ? 1 : 0) !== 1) {
throw new InvalidCredentialPlacementError({ credentialKind: kind, placementKind: String(placementKind) });
}
}
}
export function ensureCliPressSchema(db: Database): void {
db.exec(`
CREATE TABLE IF NOT EXISTS cli_press_services (
id TEXT PRIMARY KEY,
slug TEXT NOT NULL UNIQUE,
displayName TEXT NOT NULL,
description TEXT,
baseUrl TEXT NOT NULL,
sourceKind TEXT NOT NULL,
sourceRef TEXT,
createdAt TEXT NOT NULL,
updatedAt TEXT NOT NULL
);
CREATE TABLE IF NOT EXISTS cli_press_cli_specs (
id TEXT PRIMARY KEY,
serviceId TEXT NOT NULL,
name TEXT NOT NULL,
version TEXT NOT NULL,
generatorVersion TEXT NOT NULL,
specJson TEXT NOT NULL,
generatedAt TEXT,
status TEXT NOT NULL,
lastGenerationError TEXT,
createdAt TEXT NOT NULL,
updatedAt TEXT NOT NULL,
FOREIGN KEY (serviceId) REFERENCES cli_press_services(id) ON DELETE CASCADE,
UNIQUE(serviceId, name)
);
CREATE TABLE IF NOT EXISTS cli_press_artifacts (
id TEXT PRIMARY KEY,
cliSpecId TEXT NOT NULL,
kind TEXT NOT NULL,
path TEXT NOT NULL,
executable INTEGER NOT NULL,
checksum TEXT,
sizeBytes INTEGER,
createdAt TEXT NOT NULL,
updatedAt TEXT NOT NULL,
FOREIGN KEY (cliSpecId) REFERENCES cli_press_cli_specs(id) ON DELETE CASCADE
);
CREATE TABLE IF NOT EXISTS cli_press_credentials (
id TEXT PRIMARY KEY,
serviceId TEXT NOT NULL,
name TEXT NOT NULL,
kind TEXT NOT NULL,
value TEXT NOT NULL,
placement TEXT NOT NULL,
createdAt TEXT NOT NULL,
updatedAt TEXT NOT NULL,
FOREIGN KEY (serviceId) REFERENCES cli_press_services(id) ON DELETE CASCADE,
UNIQUE(serviceId, name)
);
CREATE TABLE IF NOT EXISTS cli_press_service_settings (
id TEXT PRIMARY KEY,
serviceId TEXT NOT NULL,
key TEXT NOT NULL,
value TEXT NOT NULL,
scope TEXT NOT NULL,
createdAt TEXT NOT NULL,
updatedAt TEXT NOT NULL,
FOREIGN KEY (serviceId) REFERENCES cli_press_services(id) ON DELETE CASCADE,
UNIQUE(serviceId, key, scope)
);
CREATE INDEX IF NOT EXISTS idx_cli_press_specs_service ON cli_press_cli_specs(serviceId);
CREATE INDEX IF NOT EXISTS idx_cli_press_artifacts_spec ON cli_press_artifacts(cliSpecId);
CREATE INDEX IF NOT EXISTS idx_cli_press_credentials_service ON cli_press_credentials(serviceId);
CREATE INDEX IF NOT EXISTS idx_cli_press_settings_service ON cli_press_service_settings(serviceId);
`);
}
export function createCliPressStore(db: Database) {
ensureCliPressSchema(db);
const mapService = (row: ServiceRow): Service => ({
id: row.id,
slug: row.slug,
displayName: row.displayName,
description: row.description ?? undefined,
baseUrl: row.baseUrl,
sourceKind: row.sourceKind,
sourceRef: row.sourceRef ?? undefined,
createdAt: row.createdAt,
updatedAt: row.updatedAt,
});
const mapSpec = (row: CliSpecRow): CliSpec => ({
id: row.id,
serviceId: row.serviceId,
name: row.name,
version: row.version,
generatorVersion: row.generatorVersion,
specJson: row.specJson,
generatedAt: row.generatedAt ?? undefined,
status: row.status,
lastGenerationError: row.lastGenerationError ?? undefined,
createdAt: row.createdAt,
updatedAt: row.updatedAt,
});
const mapArtifact = (row: CliArtifactRow): CliArtifact => ({
id: row.id,
cliSpecId: row.cliSpecId,
kind: row.kind,
path: row.path,
executable: Boolean(row.executable),
checksum: row.checksum ?? undefined,
sizeBytes: row.sizeBytes ?? undefined,
createdAt: row.createdAt,
updatedAt: row.updatedAt,
});
const mapCredential = (row: CredentialRow): Credential => ({
id: row.id,
serviceId: row.serviceId,
name: row.name,
kind: row.kind,
value: parseJson(row.value),
placement: parseJson(row.placement),
createdAt: row.createdAt,
updatedAt: row.updatedAt,
});
const mapSetting = (row: ServiceSettingRow): ServiceSetting => ({
id: row.id,
serviceId: row.serviceId,
key: row.key,
value: row.value,
scope: row.scope,
createdAt: row.createdAt,
updatedAt: row.updatedAt,
});
return {
listServices(): Service[] {
const rows = db.prepare("SELECT * FROM cli_press_services ORDER BY createdAt DESC").all() as unknown as ServiceRow[];
return rows.map(mapService);
},
getService(id: string): Service | undefined {
const row = db.prepare("SELECT * FROM cli_press_services WHERE id = ?").get(id) as unknown as ServiceRow | undefined;
return row ? mapService(row) : undefined;
},
createService(input: ServiceCreateInput): Service {
const service: Service = { id: createId("svc"), ...input, createdAt: nowIso(), updatedAt: nowIso() };
db.prepare(`INSERT INTO cli_press_services (id, slug, displayName, description, baseUrl, sourceKind, sourceRef, createdAt, updatedAt)
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)`)
.run(service.id, service.slug, service.displayName, service.description ?? null, service.baseUrl, service.sourceKind, service.sourceRef ?? null, service.createdAt, service.updatedAt);
db.bumpLastModified();
return service;
},
updateService(id: string, updates: ServiceUpdateInput): Service {
const existing = this.getService(id);
if (!existing) throw new Error(`Service ${id} not found`);
const updated: Service = { ...existing, ...updates, id: existing.id, slug: existing.slug, createdAt: existing.createdAt, updatedAt: nowIso() };
db.prepare(`UPDATE cli_press_services SET displayName = ?, description = ?, baseUrl = ?, sourceKind = ?, sourceRef = ?, updatedAt = ? WHERE id = ?`)
.run(updated.displayName, updated.description ?? null, updated.baseUrl, updated.sourceKind, updated.sourceRef ?? null, updated.updatedAt, id);
db.bumpLastModified();
return updated;
},
deleteService(id: string): void {
db.transaction(() => {
db.prepare("DELETE FROM cli_press_services WHERE id = ?").run(id);
});
db.bumpLastModified();
},
listSpecs(serviceId: string): CliSpec[] {
const rows = db.prepare("SELECT * FROM cli_press_cli_specs WHERE serviceId = ? ORDER BY createdAt DESC").all(serviceId) as unknown as CliSpecRow[];
return rows.map(mapSpec);
},
getSpec(id: string): CliSpec | undefined {
const row = db.prepare("SELECT * FROM cli_press_cli_specs WHERE id = ?").get(id) as unknown as CliSpecRow | undefined;
return row ? mapSpec(row) : undefined;
},
createSpec(input: CliSpecCreateInput): CliSpec {
const spec: CliSpec = { id: createId("cli"), ...input, createdAt: nowIso(), updatedAt: nowIso() };
db.prepare(`INSERT INTO cli_press_cli_specs (id, serviceId, name, version, generatorVersion, specJson, generatedAt, status, lastGenerationError, createdAt, updatedAt)
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`)
.run(spec.id, spec.serviceId, spec.name, spec.version, spec.generatorVersion, spec.specJson, spec.generatedAt ?? null, spec.status, spec.lastGenerationError ?? null, spec.createdAt, spec.updatedAt);
db.bumpLastModified();
return spec;
},
updateSpec(id: string, updates: CliSpecUpdateInput): CliSpec {
const existing = this.getSpec(id);
if (!existing) throw new Error(`Spec ${id} not found`);
const updated: CliSpec = { ...existing, ...updates, id: existing.id, serviceId: existing.serviceId, createdAt: existing.createdAt, updatedAt: nowIso() };
db.prepare(`UPDATE cli_press_cli_specs SET name=?, version=?, generatorVersion=?, specJson=?, generatedAt=?, status=?, lastGenerationError=?, updatedAt=? WHERE id=?`)
.run(updated.name, updated.version, updated.generatorVersion, updated.specJson, updated.generatedAt ?? null, updated.status, updated.lastGenerationError ?? null, updated.updatedAt, id);
db.bumpLastModified();
return updated;
},
deleteSpec(id: string): void {
db.prepare("DELETE FROM cli_press_cli_specs WHERE id = ?").run(id);
db.bumpLastModified();
},
listArtifacts(specId: string): CliArtifact[] {
const rows = db.prepare("SELECT * FROM cli_press_artifacts WHERE cliSpecId = ? ORDER BY createdAt DESC").all(specId) as unknown as CliArtifactRow[];
return rows.map(mapArtifact);
},
createArtifact(input: CliArtifactCreateInput): CliArtifact {
const artifact: CliArtifact = { id: createId("art"), ...input, createdAt: nowIso(), updatedAt: nowIso() };
db.prepare(`INSERT INTO cli_press_artifacts (id, cliSpecId, kind, path, executable, checksum, sizeBytes, createdAt, updatedAt)
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)`)
.run(artifact.id, artifact.cliSpecId, artifact.kind, artifact.path, artifact.executable ? 1 : 0, artifact.checksum ?? null, artifact.sizeBytes ?? null, artifact.createdAt, artifact.updatedAt);
db.bumpLastModified();
return artifact;
},
updateArtifact(id: string, updates: CliArtifactUpdateInput): CliArtifact {
const existing = db.prepare("SELECT * FROM cli_press_artifacts WHERE id = ?").get(id) as unknown as CliArtifactRow | undefined;
if (!existing) throw new Error(`Artifact ${id} not found`);
const updated = { ...mapArtifact(existing), ...updates, id: existing.id, cliSpecId: existing.cliSpecId, createdAt: existing.createdAt, updatedAt: nowIso() };
db.prepare("UPDATE cli_press_artifacts SET path=?, executable=?, checksum=?, sizeBytes=?, updatedAt=? WHERE id=?")
.run(updated.path, updated.executable ? 1 : 0, updated.checksum ?? null, updated.sizeBytes ?? null, updated.updatedAt, id);
db.bumpLastModified();
return updated;
},
deleteArtifact(id: string): void {
db.prepare("DELETE FROM cli_press_artifacts WHERE id = ?").run(id);
db.bumpLastModified();
},
listCredentials(serviceId: string): Credential[] {
const rows = db.prepare("SELECT * FROM cli_press_credentials WHERE serviceId = ? ORDER BY createdAt DESC").all(serviceId) as unknown as CredentialRow[];
return rows.map(mapCredential);
},
createCredential(input: CredentialCreateInput): Credential {
assertCredentialSupported(input.kind);
assertPlacementConsistency(input.kind, input.placement);
const cred: Credential = { id: createId("cred"), ...input, createdAt: nowIso(), updatedAt: nowIso() };
db.prepare(`INSERT INTO cli_press_credentials (id, serviceId, name, kind, value, placement, createdAt, updatedAt)
VALUES (?, ?, ?, ?, ?, ?, ?, ?)`)
.run(cred.id, cred.serviceId, cred.name, cred.kind, JSON.stringify(cred.value), JSON.stringify(cred.placement), cred.createdAt, cred.updatedAt);
db.bumpLastModified();
return cred;
},
updateCredential(id: string, updates: CredentialUpdateInput): Credential {
const existing = db.prepare("SELECT * FROM cli_press_credentials WHERE id = ?").get(id) as unknown as CredentialRow | undefined;
if (!existing) throw new Error(`Credential ${id} not found`);
const mapped = mapCredential(existing);
const updated: Credential = {
...mapped,
...updates,
id: mapped.id,
serviceId: mapped.serviceId,
kind: mapped.kind,
createdAt: mapped.createdAt,
updatedAt: nowIso(),
};
assertCredentialSupported(updated.kind);
assertPlacementConsistency(updated.kind, updated.placement);
db.prepare("UPDATE cli_press_credentials SET name=?, value=?, placement=?, updatedAt=? WHERE id=?")
.run(updated.name, JSON.stringify(updated.value), JSON.stringify(updated.placement), updated.updatedAt, id);
db.bumpLastModified();
return updated;
},
deleteCredential(id: string): void {
db.prepare("DELETE FROM cli_press_credentials WHERE id = ?").run(id);
db.bumpLastModified();
},
listSettings(serviceId: string): ServiceSetting[] {
const rows = db.prepare("SELECT * FROM cli_press_service_settings WHERE serviceId = ? ORDER BY createdAt DESC").all(serviceId) as unknown as ServiceSettingRow[];
return rows.map(mapSetting);
},
setSetting(input: ServiceSettingCreateInput): ServiceSetting {
const existing = db.prepare("SELECT * FROM cli_press_service_settings WHERE serviceId = ? AND key = ? AND scope = ?")
.get(input.serviceId, input.key, input.scope) as unknown as ServiceSettingRow | undefined;
const now = nowIso();
if (existing) {
db.prepare("UPDATE cli_press_service_settings SET value = ?, updatedAt = ? WHERE id = ?").run(input.value, now, existing.id);
db.bumpLastModified();
return mapSetting({ ...existing, value: input.value, updatedAt: now });
}
const setting: ServiceSetting = { id: createId("set"), ...input, createdAt: now, updatedAt: now };
db.prepare(`INSERT INTO cli_press_service_settings (id, serviceId, key, value, scope, createdAt, updatedAt)
VALUES (?, ?, ?, ?, ?, ?, ?)`)
.run(setting.id, setting.serviceId, setting.key, setting.value, setting.scope, setting.createdAt, setting.updatedAt);
db.bumpLastModified();
return setting;
},
deleteSetting(id: string): void {
db.prepare("DELETE FROM cli_press_service_settings WHERE id = ?").run(id);
db.bumpLastModified();
},
};
}
export type CliPressStore = ReturnType<typeof createCliPressStore>;

View File

@@ -0,0 +1,110 @@
export type ServiceSourceKind = "openapi" | "manual" | "other";
export type CredentialKind = "api_key" | "bearer_token" | "basic_auth" | "header" | "query_param" | "env_var";
export type CredentialPlacement =
| { kind: "header"; header: string }
| { kind: "query_param"; queryParam: string }
| { kind: "env_var"; envVar: string }
| { kind: "bearer_token"; header: string }
| { kind: "api_key"; header?: string; queryParam?: string }
| { kind: "basic_auth"; header: string };
export type ServiceSettingScope = "runtime" | "wizard" | "metadata";
export interface Service {
id: string;
slug: string;
displayName: string;
description?: string;
baseUrl: string;
sourceKind: ServiceSourceKind;
sourceRef?: string;
createdAt: string;
updatedAt: string;
}
export interface CliSpec {
id: string;
serviceId: string;
name: string;
version: string;
generatorVersion: string;
specJson: string;
generatedAt?: string;
status: "draft" | "generated" | "failed";
lastGenerationError?: string;
createdAt: string;
updatedAt: string;
}
export interface CliArtifact {
id: string;
cliSpecId: string;
kind: "binary" | "script" | "package";
path: string;
executable: boolean;
checksum?: string;
sizeBytes?: number;
createdAt: string;
updatedAt: string;
}
export interface EncodedCredentialValue {
encoding: "base64";
value: string;
}
export interface Credential {
id: string;
serviceId: string;
name: string;
kind: CredentialKind;
value: EncodedCredentialValue;
placement: CredentialPlacement;
createdAt: string;
updatedAt: string;
}
export interface ServiceSetting {
id: string;
serviceId: string;
key: string;
value: string;
scope: ServiceSettingScope;
createdAt: string;
updatedAt: string;
}
export class OAuthNotSupportedError extends Error {
constructor(kind: string) {
super(`Credential kind \"${kind}\" is not supported. OAuth/OAuth2 are deferred.`);
this.name = "OAuthNotSupportedError";
}
}
export class InvalidCredentialPlacementError extends Error {
constructor(input: { credentialKind: string; placementKind: string }) {
super(
`Invalid credential placement: credential kind \"${input.credentialKind}\" does not match placement kind \"${input.placementKind}\".`,
);
this.name = "InvalidCredentialPlacementError";
}
}
export type ServiceCreateInput = Omit<Service, "id" | "createdAt" | "updatedAt">;
export type ServiceUpdateInput = Partial<Pick<Service, "displayName" | "description" | "baseUrl" | "sourceKind" | "sourceRef">>;
export type CliSpecCreateInput = Omit<CliSpec, "id" | "createdAt" | "updatedAt">;
export type CliSpecUpdateInput = Partial<
Pick<CliSpec, "name" | "version" | "generatorVersion" | "specJson" | "status" | "lastGenerationError" | "generatedAt">
>;
export type CliArtifactCreateInput = Omit<CliArtifact, "id" | "createdAt" | "updatedAt">;
export type CliArtifactUpdateInput = Partial<Pick<CliArtifact, "path" | "executable" | "checksum" | "sizeBytes">>;
export type CredentialCreateInput = Omit<Credential, "id" | "createdAt" | "updatedAt">;
export type CredentialUpdateInput = Partial<Pick<Credential, "name" | "value" | "placement">>;
export type ServiceSettingCreateInput = Omit<ServiceSetting, "id" | "createdAt" | "updatedAt">;
export type ServiceSettingUpdateInput = Pick<ServiceSetting, "value">;

View File

@@ -0,0 +1,95 @@
import { Buffer } from "node:buffer";
import type { Credential, EncodedCredentialValue } from "./cli-press-types.js";
import { InvalidCredentialPlacementError, OAuthNotSupportedError } from "./cli-press-types.js";
export type RequestShape = {
headers: Record<string, string>;
query: Record<string, string>;
env: Record<string, string>;
};
export function encodeCredentialValue(raw: string): EncodedCredentialValue {
return {
encoding: "base64",
value: Buffer.from(raw, "utf8").toString("base64"),
};
}
export function decodeCredentialValue(encoded: EncodedCredentialValue): string {
if (encoded.encoding !== "base64") {
throw new Error(`Unsupported credential encoding: ${String((encoded as { encoding?: string }).encoding)}`);
}
return Buffer.from(encoded.value, "base64").toString("utf8");
}
function assertNoOAuth(kind: string): void {
if (kind === "oauth" || kind === "oauth2") {
throw new OAuthNotSupportedError(kind);
}
}
function assertPlacement(credential: Credential): void {
const credentialKind = credential.kind;
const placementKind = credential.placement.kind;
if (placementKind !== credentialKind) {
throw new InvalidCredentialPlacementError({ credentialKind, placementKind });
}
if (credential.kind === "api_key") {
const placement = credential.placement;
if (placement.kind !== "api_key") {
throw new InvalidCredentialPlacementError({ credentialKind, placementKind });
}
const hasHeader = typeof placement.header === "string" && placement.header.trim().length > 0;
const hasQuery = typeof placement.queryParam === "string" && placement.queryParam.trim().length > 0;
if ((hasHeader ? 1 : 0) + (hasQuery ? 1 : 0) !== 1) {
throw new InvalidCredentialPlacementError({ credentialKind, placementKind });
}
}
}
export function applyCredentialToRequest(credential: Credential, request: RequestShape): RequestShape {
assertNoOAuth((credential as { kind: string }).kind);
assertPlacement(credential);
const value = decodeCredentialValue(credential.value);
switch (credential.kind) {
case "header": {
const placement = credential.placement as { kind: "header"; header: string };
request.headers[placement.header] = value;
break;
}
case "query_param": {
const placement = credential.placement as { kind: "query_param"; queryParam: string };
request.query[placement.queryParam] = value;
break;
}
case "env_var": {
const placement = credential.placement as { kind: "env_var"; envVar: string };
request.env[placement.envVar] = value;
break;
}
case "bearer_token": {
const placement = credential.placement as { kind: "bearer_token"; header: string };
request.headers[placement.header] = `Bearer ${value}`;
break;
}
case "api_key": {
const placement = credential.placement as { kind: "api_key"; header?: string; queryParam?: string };
if (placement.header) {
request.headers[placement.header] = value;
} else if (placement.queryParam) {
request.query[placement.queryParam] = value;
}
break;
}
case "basic_auth": {
const placement = credential.placement as { kind: "basic_auth"; header: string };
const basicAuth = Buffer.from(value, "utf8").toString("base64");
request.headers[placement.header] = `Basic ${basicAuth}`;
break;
}
}
return request;
}