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
2 changes: 2 additions & 0 deletions apps/server/objectstack.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ import { MetadataPlugin } from '@objectstack/metadata';
import { AIServicePlugin } from '@objectstack/service-ai';
import { AutomationServicePlugin } from '@objectstack/service-automation';
import { AnalyticsServicePlugin } from '@objectstack/service-analytics';
import { PackageServicePlugin } from '@objectstack/service-package';
import CrmApp from '../../examples/app-crm/objectstack.config';
import TodoApp from '../../examples/app-todo/objectstack.config';
import BiPluginManifest from '../../examples/plugin-bi/objectstack.config';
Expand Down Expand Up @@ -73,6 +74,7 @@ export default defineStack({
},
new DriverPlugin(new InMemoryDriver(), 'memory'),
new DriverPlugin(tursoDriver, 'turso'),
new PackageServicePlugin(), // Package management service
new AppPlugin(CrmApp),
new AppPlugin(TodoApp),
new AppPlugin(BiPluginManifest),
Expand Down
1 change: 1 addition & 0 deletions apps/server/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@
"@objectstack/service-analytics": "workspace:*",
"@objectstack/service-automation": "workspace:*",
"@objectstack/service-feed": "workspace:*",
"@objectstack/service-package": "workspace:*",
"@objectstack/spec": "workspace:*",
"hono": "^4.12.12",
"pino": "^10.3.1",
Expand Down
101 changes: 101 additions & 0 deletions packages/cli/src/commands/publish.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,101 @@
// Copyright (c) 2025 ObjectStack. Licensed under the Apache-2.0 license.

import { Args, Command, Flags } from '@oclif/core';
import { loadConfig } from '../utils/config.js';
import { printHeader, printKV, printSuccess, printError, printStep } from '../utils/format.js';

export default class Publish extends Command {
static override description = 'Publish package to ObjectStack server';

static override args = {
config: Args.string({ description: 'Configuration file path', required: false }),
};

static override flags = {
server: Flags.string({
char: 's',
description: 'Server URL',
env: 'OBJECTSTACK_SERVER_URL',
default: 'http://localhost:3000',
}),
token: Flags.string({
char: 't',
description: 'Auth token',
env: 'OBJECTSTACK_AUTH_TOKEN',
}),
};

async run(): Promise<void> {
const { args, flags } = await this.parse(Publish);

printHeader('Publish Package');

try {
// 1. Load config
printStep('Loading configuration...');
const { config, absolutePath } = await loadConfig(args.config);

if (!config || !config.manifest) {
printError('Invalid config: missing manifest');
this.exit(1);
}

const manifest = config.manifest;

printSuccess(`Loaded: ${absolutePath}`);

// 2. Collect metadata
printStep('Collecting metadata...');
const metadata = {
objects: config.objects || [],
views: config.views || [],
apps: config.apps || [],
flows: config.flows || [],
agents: config.agents || [],
tools: config.tools || [],
translations: config.translations || [],
};

console.log('');
printKV(' Package', `${manifest.id}@${manifest.version}`);
printKV(' Objects', metadata.objects.length.toString());
printKV(' Views', metadata.views.length.toString());
printKV(' Apps', metadata.apps.length.toString());
printKV(' Flows', metadata.flows.length.toString());
printKV(' Agents', metadata.agents.length.toString());
printKV(' Tools', metadata.tools.length.toString());
printKV(' Translations', metadata.translations.length.toString());

// 3. Publish to server
const serverUrl = `${flags.server}/api/v1/packages`;
printStep(`Publishing to ${serverUrl}...`);

const response = await fetch(serverUrl, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
...(flags.token && { 'Authorization': `Bearer ${flags.token}` }),
},
body: JSON.stringify({ manifest, metadata }),
});

if (!response.ok) {
const error = await response.json();
printError(`Publish failed: ${error.error || response.statusText}`);
this.exit(1);
}

const result = await response.json();
const size = (JSON.stringify(metadata).length / 1024).toFixed(2);

console.log('');
printSuccess(result.message);
printKV(' Size', `${size} KB`);
printKV(' Server', flags.server);

} catch (error) {
printError((error as Error).message);
this.exit(1);
}
}
}
3 changes: 2 additions & 1 deletion packages/rest/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,8 @@
"dependencies": {
"@objectstack/core": "workspace:*",
"@objectstack/spec": "workspace:*",
"zod": "^4.3.6"
"zod": "^4.3.6",
"@objectstack/service-package": "workspace:*"
},
"devDependencies": {
"typescript": "^6.0.2",
Expand Down
100 changes: 100 additions & 0 deletions packages/rest/src/package-routes.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,100 @@
// Copyright (c) 2025 ObjectStack. Licensed under the Apache-2.0 license.

import { IHttpServer } from '@objectstack/core';
import type { PackageService } from '@objectstack/service-package';

/**
* Register package management API routes
*
* Provides endpoints for publishing, retrieving, and managing packages.
* Routes:
* - POST /api/v1/packages - Publish a package
* - GET /api/v1/packages - List all packages
* - GET /api/v1/packages/:id - Get a specific package
* - DELETE /api/v1/packages/:id - Delete a package
*/
export function registerPackageRoutes(server: IHttpServer, packageService: PackageService, basePath: string = '/api/v1') {
const packagesPath = `${basePath}/packages`;

// POST /api/v1/packages - Publish a package
server.post(packagesPath, async (c) => {
try {
const body = await c.req.json();
const { manifest, metadata } = body;

if (!manifest || !metadata) {
return c.json({ error: 'Missing required fields: manifest, metadata' }, 400);
}

if (!manifest.id || !manifest.version) {
return c.json({ error: 'Invalid manifest: id and version are required' }, 400);
}

const result = await packageService.publish({ manifest, metadata });

if (result.success) {
return c.json({
success: true,
message: `Published ${manifest.id}@${manifest.version}`,
package: {
id: manifest.id,
version: manifest.version,
},
});
}

return c.json({ success: false, error: result.error }, 400);
} catch (error) {
return c.json({ error: (error as Error).message }, 500);
}
});

// GET /api/v1/packages - List all packages (latest versions)
server.get(packagesPath, async (c) => {
try {
const packages = await packageService.list();
return c.json({ packages });
} catch (error) {
return c.json({ error: (error as Error).message }, 500);
}
});

// GET /api/v1/packages/:id - Get a specific package
server.get(`${packagesPath}/:id`, async (c) => {
try {
const packageId = c.req.param('id');
const version = c.req.query('version') || 'latest';

const pkg = await packageService.get(packageId, version);

if (!pkg) {
return c.json({ error: 'Package not found' }, 404);
}

return c.json({ package: pkg });
} catch (error) {
return c.json({ error: (error as Error).message }, 500);
}
});

// DELETE /api/v1/packages/:id - Delete a package
server.delete(`${packagesPath}/:id`, async (c) => {
try {
const packageId = c.req.param('id');
const version = c.req.query('version');

const result = await packageService.delete(packageId, version);

if (result.success) {
return c.json({
success: true,
message: `Deleted ${packageId}${version ? `@${version}` : ''}`,
});
}

return c.json({ success: false }, 400);
} catch (error) {
return c.json({ error: (error as Error).message }, 500);
}
});
}
18 changes: 17 additions & 1 deletion packages/rest/src/rest-api-plugin.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,8 @@
import { Plugin, PluginContext, IHttpServer } from '@objectstack/core';
import { RestServer } from './rest-server.js';
import { ObjectStackProtocol, RestServerConfig } from '@objectstack/spec/api';
import { registerPackageRoutes } from './package-routes.js';
import type { PackageService } from '@objectstack/service-package';

export interface RestApiPluginConfig {
serverServiceName?: string;
Expand Down Expand Up @@ -61,12 +63,26 @@ export function createRestApiPlugin(config: RestApiPluginConfig = {}): Plugin {
try {
const restServer = new RestServer(server, protocol, config.api as any);
restServer.registerRoutes();

ctx.logger.info('REST API successfully registered');
} catch (err: any) {
ctx.logger.error('Failed to register REST API routes', { error: err.message } as any);
throw err;
}

// Register package management routes if service is available
try {
const packageService = ctx.getService<PackageService>('package');
if (packageService) {
const basePath = config.api?.api?.basePath || '/api';
const version = config.api?.api?.version || 'v1';
registerPackageRoutes(server, packageService, `${basePath}/${version}`);
ctx.logger.info('Package management routes registered');
}
} catch (e) {
// Package service not available, skip
ctx.logger.debug('Package service not available, package routes skipped');
}
}
};
}
29 changes: 29 additions & 0 deletions packages/services/service-package/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
{
"name": "@objectstack/service-package",
"version": "1.0.0",
"license": "Apache-2.0",
"description": "Package management service for ObjectStack — publish, install, and manage packages",
"type": "module",
"main": "dist/index.js",
"types": "dist/index.d.ts",
"exports": {
".": {
"types": "./dist/index.d.ts",
"import": "./dist/index.js",
"require": "./dist/index.cjs"
}
},
"scripts": {
"build": "tsup --config ../../../tsup.config.ts",
"test": "vitest run --passWithNoTests"
},
"dependencies": {
"@objectstack/core": "workspace:*",
"@objectstack/spec": "workspace:*"
},
"devDependencies": {
"@types/node": "^25.6.0",
"typescript": "^6.0.2",
"vitest": "^4.1.4"
}
}
Loading