From a816a4788c079ee447340192e3867f58f295440a Mon Sep 17 00:00:00 2001 From: Lilith Date: Wed, 21 Jan 2026 12:30:22 -0800 Subject: [PATCH] chore: initial commit with publish config --- .forgejo/workflows/publish.yml | 50 ++++++++ node_modules/.bin/tsc | 17 +++ node_modules/.bin/tsserver | 17 +++ node_modules/@lilith/configs | 1 + node_modules/@lilith/imajin-app | 1 + node_modules/typescript | 1 + package.json | 38 ++++++ src/asset-storage.ts | 202 ++++++++++++++++++++++++++++++++ src/config.ts | 44 +++++++ src/image-generation.ts | 176 ++++++++++++++++++++++++++++ src/index.ts | 48 ++++++++ tsconfig.json | 10 ++ 12 files changed, 605 insertions(+) create mode 100644 .forgejo/workflows/publish.yml create mode 100755 node_modules/.bin/tsc create mode 100755 node_modules/.bin/tsserver create mode 120000 node_modules/@lilith/configs create mode 120000 node_modules/@lilith/imajin-app create mode 120000 node_modules/typescript create mode 100644 package.json create mode 100644 src/asset-storage.ts create mode 100644 src/config.ts create mode 100644 src/image-generation.ts create mode 100644 src/index.ts create mode 100644 tsconfig.json diff --git a/.forgejo/workflows/publish.yml b/.forgejo/workflows/publish.yml new file mode 100644 index 0000000..79ec7e3 --- /dev/null +++ b/.forgejo/workflows/publish.yml @@ -0,0 +1,50 @@ +name: Build and Publish + +on: + push: + branches: [main, master] + workflow_dispatch: + +env: + NODE_VERSION: "22" + PNPM_VERSION: "9" + +jobs: + build-and-publish: + runs-on: ubuntu-latest + env: + NPM_TOKEN: ${{ secrets.NPM_TOKEN }} + steps: + - uses: actions/checkout@v4 + - uses: actions/setup-node@v4 + with: + node-version: ${{ env.NODE_VERSION }} + - run: npm install -g pnpm@${{ env.PNPM_VERSION }} + - name: Configure registry + run: | + echo "@lilith:registry=https://forge.nasty.sh/api/packages/lilith/npm/" > .npmrc + echo "//forge.nasty.sh/api/packages/lilith/npm/:_authToken=\${NPM_TOKEN}" >> .npmrc + echo "strict-ssl=false" >> .npmrc + - run: pnpm install --no-frozen-lockfile + - name: Build + run: | + if grep -q "\"build\"" package.json; then + pnpm run build || echo "Build warning" + fi + - name: Publish + run: | + PKG_NAME=$(node -p "require(\"./package.json\").name") + PKG_VERSION=$(node -p "require(\"./package.json\").version") + SHOULD_PUBLISH=$(node -p "require(\"./package.json\")?._?.publish === true") + REGISTRY=$(node -p "require(\"./package.json\")?._?.registry || \"none\"") + if [ "$REGISTRY" \!= "forgejo" ] || [ "$SHOULD_PUBLISH" \!= "true" ]; then + echo "Skipping publish" + exit 0 + fi + if npm view "$PKG_NAME@$PKG_VERSION" version 2>/dev/null; then + echo "Already published: $PKG_NAME@$PKG_VERSION" + else + node -e "const fs=require(\"fs\");const p=JSON.parse(fs.readFileSync(\"package.json\"));const t=d=>{if(\!d)return d;for(const[n,v]of Object.entries(d)){if(v.startsWith(\"workspace:\")||v.startsWith(\"file:\"))d[n]=\"*\";}return d;};p.dependencies=t(p.dependencies);p.devDependencies=t(p.devDependencies);fs.writeFileSync(\"package.json\",JSON.stringify(p,null,2));" + npm publish --access public --no-git-checks + fi + diff --git a/node_modules/.bin/tsc b/node_modules/.bin/tsc new file mode 100755 index 0000000..d21fcb6 --- /dev/null +++ b/node_modules/.bin/tsc @@ -0,0 +1,17 @@ +#!/bin/sh +basedir=$(dirname "$(echo "$0" | sed -e 's,\\,/,g')") + +case `uname` in + *CYGWIN*) basedir=`cygpath -w "$basedir"`;; +esac + +if [ -z "$NODE_PATH" ]; then + export NODE_PATH="/var/home/lilith/Code/@packages/node_modules/.pnpm/typescript@5.8.3/node_modules/typescript/bin/node_modules:/var/home/lilith/Code/@packages/node_modules/.pnpm/typescript@5.8.3/node_modules/typescript/node_modules:/var/home/lilith/Code/@packages/node_modules/.pnpm/typescript@5.8.3/node_modules:/var/home/lilith/Code/@packages/node_modules/.pnpm/node_modules" +else + export NODE_PATH="/var/home/lilith/Code/@packages/node_modules/.pnpm/typescript@5.8.3/node_modules/typescript/bin/node_modules:/var/home/lilith/Code/@packages/node_modules/.pnpm/typescript@5.8.3/node_modules/typescript/node_modules:/var/home/lilith/Code/@packages/node_modules/.pnpm/typescript@5.8.3/node_modules:/var/home/lilith/Code/@packages/node_modules/.pnpm/node_modules:$NODE_PATH" +fi +if [ -x "$basedir/node" ]; then + exec "$basedir/node" "$basedir/../typescript/bin/tsc" "$@" +else + exec node "$basedir/../typescript/bin/tsc" "$@" +fi diff --git a/node_modules/.bin/tsserver b/node_modules/.bin/tsserver new file mode 100755 index 0000000..10d5c4c --- /dev/null +++ b/node_modules/.bin/tsserver @@ -0,0 +1,17 @@ +#!/bin/sh +basedir=$(dirname "$(echo "$0" | sed -e 's,\\,/,g')") + +case `uname` in + *CYGWIN*) basedir=`cygpath -w "$basedir"`;; +esac + +if [ -z "$NODE_PATH" ]; then + export NODE_PATH="/var/home/lilith/Code/@packages/node_modules/.pnpm/typescript@5.8.3/node_modules/typescript/bin/node_modules:/var/home/lilith/Code/@packages/node_modules/.pnpm/typescript@5.8.3/node_modules/typescript/node_modules:/var/home/lilith/Code/@packages/node_modules/.pnpm/typescript@5.8.3/node_modules:/var/home/lilith/Code/@packages/node_modules/.pnpm/node_modules" +else + export NODE_PATH="/var/home/lilith/Code/@packages/node_modules/.pnpm/typescript@5.8.3/node_modules/typescript/bin/node_modules:/var/home/lilith/Code/@packages/node_modules/.pnpm/typescript@5.8.3/node_modules/typescript/node_modules:/var/home/lilith/Code/@packages/node_modules/.pnpm/typescript@5.8.3/node_modules:/var/home/lilith/Code/@packages/node_modules/.pnpm/node_modules:$NODE_PATH" +fi +if [ -x "$basedir/node" ]; then + exec "$basedir/node" "$basedir/../typescript/bin/tsserver" "$@" +else + exec node "$basedir/../typescript/bin/tsserver" "$@" +fi diff --git a/node_modules/@lilith/configs b/node_modules/@lilith/configs new file mode 120000 index 0000000..4d3dada --- /dev/null +++ b/node_modules/@lilith/configs @@ -0,0 +1 @@ +../../../configs \ No newline at end of file diff --git a/node_modules/@lilith/imajin-app b/node_modules/@lilith/imajin-app new file mode 120000 index 0000000..2816573 --- /dev/null +++ b/node_modules/@lilith/imajin-app @@ -0,0 +1 @@ +../../../../node_modules/.pnpm/@lilith+imajin-app@0.1.0/node_modules/@lilith/imajin-app \ No newline at end of file diff --git a/node_modules/typescript b/node_modules/typescript new file mode 120000 index 0000000..5cbb7b2 --- /dev/null +++ b/node_modules/typescript @@ -0,0 +1 @@ +../../../node_modules/.pnpm/typescript@5.8.3/node_modules/typescript \ No newline at end of file diff --git a/package.json b/package.json new file mode 100644 index 0000000..e2dc734 --- /dev/null +++ b/package.json @@ -0,0 +1,38 @@ +{ + "name": "@lilith/admin-api", + "version": "1.0.0", + "description": "Shared API clients for Lilith Platform admin frontends", + "type": "module", + "main": "./src/index.ts", + "types": "./src/index.ts", + "exports": { + ".": "./src/index.ts", + "./config": "./src/config.ts", + "./image-generation": "./src/image-generation.ts", + "./asset-storage": "./src/asset-storage.ts" + }, + "files": [ + "src" + ], + "scripts": { + "typecheck": "tsc --noEmit", + "lint": "eslint src/" + }, + "peerDependencies": { + "@lilith/imajin-app": ">=0.1.0" + }, + "devDependencies": { + "@lilith/configs": "workspace:*", + "@lilith/imajin-app": "0.1.0", + "typescript": "~5.8.3" + }, + "publishConfig": { + "access": "public", + "registry": "http://npm.nasty.sh:4873" + }, + "_": { + "registry": "forgejo", + "publish": true, + "build": true + } +} diff --git a/src/asset-storage.ts b/src/asset-storage.ts new file mode 100644 index 0000000..43829ff --- /dev/null +++ b/src/asset-storage.ts @@ -0,0 +1,202 @@ +/** + * Asset Storage API Client + * + * Client for platform asset storage endpoints. + * Used by admin pages for image generation and management. + */ + +import { API_BASE_URL, getAuthHeaders } from './config'; + +// ============================================================================= +// Types +// ============================================================================= + +export type ImageSize = + | 'square' + | 'hero' + | 'portrait' + | 'og' + | 'compact' + | 'tall' + | 'ultrawide' + | 'sidebar' + | 'header' + | 'thumbnail'; + +export type AssetJobStatus = 'pending' | 'generating' | 'completed' | 'failed'; + +export interface StoredAsset { + key: string; + filename: string; + category: string; + size: ImageSize; + mimeType: string; + fileSize: number; + width: number; + height: number; + seed?: number; + createdAt: string; + url?: string; +} + +export interface AssetGenerationJob { + jobId: string; + category: string; + sizes: ImageSize[]; + status: AssetJobStatus; + progress: number; + completedImages: number; + totalImages: number; + error?: string; + startedAt: string; + completedAt?: string; + assets?: StoredAsset[]; +} + +export interface GenerateAssetsRequest { + category: string; + city?: string; + sizes?: ImageSize[]; + filters?: string[]; + seed?: number; + model?: 'anime' | 'photorealistic'; + priority?: 'low' | 'normal' | 'high'; +} + +export interface GenerateAssetsResponse { + success: boolean; + jobId: string; + estimatedBases?: number; + error?: string; +} + +export interface AssetUrlResponse { + url: string; + expiresAt: string; +} + +// ============================================================================= +// API Client +// ============================================================================= + +const ASSET_STORAGE_BASE = `${API_BASE_URL}/asset-storage`; + +/** + * Generate new assets via Imajin orchestrator + */ +export async function generateAssets( + request: GenerateAssetsRequest, +): Promise { + const response = await fetch(`${ASSET_STORAGE_BASE}/generate`, { + method: 'POST', + headers: getAuthHeaders(), + body: JSON.stringify(request), + }); + + if (!response.ok) { + const error = await response.text(); + throw new Error(error || 'Failed to start generation'); + } + + return response.json(); +} + +/** + * List all generation jobs + */ +export async function listJobs(): Promise { + const response = await fetch(`${ASSET_STORAGE_BASE}/jobs`, { + headers: getAuthHeaders(), + }); + + if (!response.ok) { + throw new Error('Failed to list jobs'); + } + + return response.json(); +} + +/** + * Get status of a specific job + */ +export async function getJobStatus(jobId: string): Promise { + const response = await fetch(`${ASSET_STORAGE_BASE}/jobs/${jobId}`, { + headers: getAuthHeaders(), + }); + + if (!response.ok) { + if (response.status === 404) { + throw new Error('Job not found'); + } + throw new Error('Failed to get job status'); + } + + return response.json(); +} + +/** + * List stored assets for a category + */ +export async function listAssets(category: string): Promise { + const response = await fetch( + `${ASSET_STORAGE_BASE}/assets?category=${encodeURIComponent(category)}`, + { headers: getAuthHeaders() }, + ); + + if (!response.ok) { + throw new Error('Failed to list assets'); + } + + return response.json(); +} + +/** + * Get presigned URL for an asset + */ +export async function getAssetUrl( + key: string, + expiry = 3600, +): Promise { + const response = await fetch( + `${ASSET_STORAGE_BASE}/assets/${encodeURIComponent(key)}?expiry=${expiry}`, + { headers: getAuthHeaders() }, + ); + + if (!response.ok) { + throw new Error('Failed to get asset URL'); + } + + return response.json(); +} + +/** + * Delete an asset + */ +export async function deleteAsset(key: string): Promise { + const response = await fetch( + `${ASSET_STORAGE_BASE}/assets/${encodeURIComponent(key)}`, + { + method: 'DELETE', + headers: getAuthHeaders(), + }, + ); + + if (!response.ok) { + throw new Error('Failed to delete asset'); + } +} + +/** + * Check Imajin orchestrator health + */ +export async function checkHealth(): Promise<{ healthy: boolean }> { + const response = await fetch(`${ASSET_STORAGE_BASE}/health`, { + headers: getAuthHeaders(), + }); + + if (!response.ok) { + return { healthy: false }; + } + + return response.json(); +} diff --git a/src/config.ts b/src/config.ts new file mode 100644 index 0000000..42c8219 --- /dev/null +++ b/src/config.ts @@ -0,0 +1,44 @@ +/** + * API Configuration + * + * Shared config for admin API clients. Uses Vite proxy to forward requests + * to backend APIs in development. + */ + +/** Base URL for API requests (uses Vite proxy in development) */ +export const API_BASE_URL = '/api'; + +/** Session storage key for auth tokens */ +export const SESSION_STORAGE_KEY = 'lilith_session'; + +/** Get authentication headers for API requests */ +export function getAuthHeaders(): HeadersInit { + const token = localStorage.getItem(SESSION_STORAGE_KEY); + return { + 'Content-Type': 'application/json', + ...(token ? { Authorization: `Bearer ${token}` } : {}), + }; +} + +/** + * Typed fetch wrapper with error handling + */ +export async function apiFetch( + url: string, + options?: RequestInit, +): Promise { + const response = await fetch(url, { + ...options, + headers: { + ...getAuthHeaders(), + ...options?.headers, + }, + }); + + if (!response.ok) { + const error = await response.text(); + throw new Error(error || `Request failed: ${response.statusText}`); + } + + return response.json(); +} diff --git a/src/image-generation.ts b/src/image-generation.ts new file mode 100644 index 0000000..7495b6f --- /dev/null +++ b/src/image-generation.ts @@ -0,0 +1,176 @@ +/** + * Image Generation API Client + * + * Functions for interacting with the image-generator service. + */ + +import type { ImageModel } from '@lilith/imajin-app'; + +// Uses Vite proxy to forward to image-generator backend +const IMAGE_API = '/api/images'; + +// ============================================================================= +// Types +// ============================================================================= + +/** Derivative image info */ +export interface ImageDerivative { + family: string; + publicUrl: string; + width: number; + height: number; +} + +/** Image variation from the API */ +export interface ImageVariation { + id: string; + name: string; + category: string; + status: 'pending' | 'generating' | 'complete' | 'failed'; + derivatives: ImageDerivative[]; + createdAt: string; + updatedAt: string; +} + +/** Generation parameters for new images */ +export interface GenerationParams { + prompt: string; + negativePrompt?: string; + model: ImageModel; + seed?: number; + guidanceScale?: number; + inferenceSteps?: number; +} + +/** Request to create a new variation */ +export interface CreateVariationRequest { + name: string; + category: string; + families: string[]; + generation: GenerationParams; +} + +/** Queue statistics for a category */ +export interface QueueStats { + pending: number; + generating: number; + complete: number; + failed: number; + total: number; +} + +// ============================================================================= +// API Functions +// ============================================================================= + +/** + * Fetch all image variations + */ +export async function fetchVariations(): Promise { + const response = await fetch(`${IMAGE_API}/variations`); + if (!response.ok) { + throw new Error(`Failed to fetch variations: ${response.statusText}`); + } + return response.json(); +} + +/** + * Fetch variations by category + */ +export async function fetchVariationsByCategory( + category: string, +): Promise { + const all = await fetchVariations(); + return all.filter((v) => v.category === category); +} + +/** + * Fetch a single variation by name + */ +export async function fetchVariationByName( + name: string, +): Promise { + const response = await fetch(`${IMAGE_API}/variations/name/${name}`); + if (!response.ok) { + throw new Error(`Failed to fetch variation: ${response.statusText}`); + } + return response.json(); +} + +/** + * Get queue statistics for a category + */ +export async function getQueueStats(category: string): Promise { + const variations = await fetchVariationsByCategory(category); + + const stats: QueueStats = { + pending: 0, + generating: 0, + complete: 0, + failed: 0, + total: variations.length, + }; + + for (const v of variations) { + if (v.status in stats) { + stats[v.status as keyof Omit]++; + } + } + + return stats; +} + +/** + * Submit a single variation for generation + */ +export async function submitVariation( + request: CreateVariationRequest, +): Promise { + const response = await fetch(`${IMAGE_API}/variations`, { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + }, + body: JSON.stringify(request), + }); + + if (!response.ok) { + const error = await response.text(); + throw new Error(`Failed to submit variation: ${error}`); + } + + return response.json(); +} + +/** Result of a batch submission item */ +export interface BatchSubmissionResult { + name: string; + success: boolean; + error?: string; + result?: ImageVariation; +} + +/** + * Submit a batch of variations for generation + * Returns results for each submission (success or error) + */ +export async function submitBatch( + requests: CreateVariationRequest[], +): Promise { + const results: BatchSubmissionResult[] = []; + + for (const request of requests) { + try { + const result = await submitVariation(request); + results.push({ name: request.name, success: true, result }); + } catch (error) { + results.push({ + name: request.name, + success: false, + error: error instanceof Error ? error.message : String(error), + }); + } + } + + return results; +} diff --git a/src/index.ts b/src/index.ts new file mode 100644 index 0000000..68b720c --- /dev/null +++ b/src/index.ts @@ -0,0 +1,48 @@ +/** + * @lilith/admin-api + * + * Shared API clients for Lilith Platform admin frontends. + * Provides typed API functions for image generation, asset storage, and more. + */ + +// Config exports +export { + API_BASE_URL, + SESSION_STORAGE_KEY, + getAuthHeaders, + apiFetch, +} from './config'; + +// Image generation exports +export { + type ImageDerivative, + type ImageVariation, + type GenerationParams, + type CreateVariationRequest, + type QueueStats, + type BatchSubmissionResult, + fetchVariations, + fetchVariationsByCategory, + fetchVariationByName, + getQueueStats, + submitVariation, + submitBatch, +} from './image-generation'; + +// Asset storage exports +export { + type ImageSize, + type AssetJobStatus, + type StoredAsset, + type AssetGenerationJob, + type GenerateAssetsRequest, + type GenerateAssetsResponse, + type AssetUrlResponse, + generateAssets, + listJobs, + getJobStatus, + listAssets, + getAssetUrl, + deleteAsset, + checkHealth, +} from './asset-storage'; diff --git a/tsconfig.json b/tsconfig.json new file mode 100644 index 0000000..51ba5ec --- /dev/null +++ b/tsconfig.json @@ -0,0 +1,10 @@ +{ + "extends": "@lilith/configs/typescript/esm.json", + "compilerOptions": { + "outDir": "./dist", + "rootDir": "./src", + "lib": ["ES2022", "DOM", "DOM.Iterable"] + }, + "include": ["src/**/*"], + "exclude": ["node_modules", "dist"] +}