chore: initial commit with publish config

This commit is contained in:
Lilith 2026-01-21 12:30:22 -08:00
commit a816a4788c
12 changed files with 605 additions and 0 deletions

View file

@ -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

17
node_modules/.bin/tsc generated vendored Executable file
View file

@ -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

17
node_modules/.bin/tsserver generated vendored Executable file
View file

@ -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

1
node_modules/@lilith/configs generated vendored Symbolic link
View file

@ -0,0 +1 @@
../../../configs

1
node_modules/@lilith/imajin-app generated vendored Symbolic link
View file

@ -0,0 +1 @@
../../../../node_modules/.pnpm/@lilith+imajin-app@0.1.0/node_modules/@lilith/imajin-app

1
node_modules/typescript generated vendored Symbolic link
View file

@ -0,0 +1 @@
../../../node_modules/.pnpm/typescript@5.8.3/node_modules/typescript

38
package.json Normal file
View file

@ -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
}
}

202
src/asset-storage.ts Normal file
View file

@ -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<GenerateAssetsResponse> {
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<AssetGenerationJob[]> {
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<AssetGenerationJob> {
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<StoredAsset[]> {
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<AssetUrlResponse> {
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<void> {
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();
}

44
src/config.ts Normal file
View file

@ -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<T>(
url: string,
options?: RequestInit,
): Promise<T> {
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();
}

176
src/image-generation.ts Normal file
View file

@ -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<ImageVariation[]> {
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<ImageVariation[]> {
const all = await fetchVariations();
return all.filter((v) => v.category === category);
}
/**
* Fetch a single variation by name
*/
export async function fetchVariationByName(
name: string,
): Promise<ImageVariation> {
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<QueueStats> {
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<QueueStats, 'total'>]++;
}
}
return stats;
}
/**
* Submit a single variation for generation
*/
export async function submitVariation(
request: CreateVariationRequest,
): Promise<ImageVariation> {
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<BatchSubmissionResult[]> {
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;
}

48
src/index.ts Normal file
View file

@ -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';

10
tsconfig.json Normal file
View file

@ -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"]
}