Skip to content

Commit 1f54410

Browse files
Merge pull request #1163 from objectstack-ai/claude/evaluate-dynamic-package-installation
Evaluating remote package installation strategies
2 parents 56a54b2 + cd3838c commit 1f54410

File tree

10 files changed

+510
-2
lines changed

10 files changed

+510
-2
lines changed

apps/server/objectstack.config.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@ import { MetadataPlugin } from '@objectstack/metadata';
2222
import { AIServicePlugin } from '@objectstack/service-ai';
2323
import { AutomationServicePlugin } from '@objectstack/service-automation';
2424
import { AnalyticsServicePlugin } from '@objectstack/service-analytics';
25+
import { PackageServicePlugin } from '@objectstack/service-package';
2526
import CrmApp from '../../examples/app-crm/objectstack.config';
2627
import TodoApp from '../../examples/app-todo/objectstack.config';
2728
import BiPluginManifest from '../../examples/plugin-bi/objectstack.config';
@@ -73,6 +74,7 @@ export default defineStack({
7374
},
7475
new DriverPlugin(new InMemoryDriver(), 'memory'),
7576
new DriverPlugin(tursoDriver, 'turso'),
77+
new PackageServicePlugin(), // Package management service
7678
new AppPlugin(CrmApp),
7779
new AppPlugin(TodoApp),
7880
new AppPlugin(BiPluginManifest),

apps/server/package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,7 @@
3535
"@objectstack/service-analytics": "workspace:*",
3636
"@objectstack/service-automation": "workspace:*",
3737
"@objectstack/service-feed": "workspace:*",
38+
"@objectstack/service-package": "workspace:*",
3839
"@objectstack/spec": "workspace:*",
3940
"hono": "^4.12.12",
4041
"pino": "^10.3.1",
Lines changed: 101 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,101 @@
1+
// Copyright (c) 2025 ObjectStack. Licensed under the Apache-2.0 license.
2+
3+
import { Args, Command, Flags } from '@oclif/core';
4+
import { loadConfig } from '../utils/config.js';
5+
import { printHeader, printKV, printSuccess, printError, printStep } from '../utils/format.js';
6+
7+
export default class Publish extends Command {
8+
static override description = 'Publish package to ObjectStack server';
9+
10+
static override args = {
11+
config: Args.string({ description: 'Configuration file path', required: false }),
12+
};
13+
14+
static override flags = {
15+
server: Flags.string({
16+
char: 's',
17+
description: 'Server URL',
18+
env: 'OBJECTSTACK_SERVER_URL',
19+
default: 'http://localhost:3000',
20+
}),
21+
token: Flags.string({
22+
char: 't',
23+
description: 'Auth token',
24+
env: 'OBJECTSTACK_AUTH_TOKEN',
25+
}),
26+
};
27+
28+
async run(): Promise<void> {
29+
const { args, flags } = await this.parse(Publish);
30+
31+
printHeader('Publish Package');
32+
33+
try {
34+
// 1. Load config
35+
printStep('Loading configuration...');
36+
const { config, absolutePath } = await loadConfig(args.config);
37+
38+
if (!config || !config.manifest) {
39+
printError('Invalid config: missing manifest');
40+
this.exit(1);
41+
}
42+
43+
const manifest = config.manifest;
44+
45+
printSuccess(`Loaded: ${absolutePath}`);
46+
47+
// 2. Collect metadata
48+
printStep('Collecting metadata...');
49+
const metadata = {
50+
objects: config.objects || [],
51+
views: config.views || [],
52+
apps: config.apps || [],
53+
flows: config.flows || [],
54+
agents: config.agents || [],
55+
tools: config.tools || [],
56+
translations: config.translations || [],
57+
};
58+
59+
console.log('');
60+
printKV(' Package', `${manifest.id}@${manifest.version}`);
61+
printKV(' Objects', metadata.objects.length.toString());
62+
printKV(' Views', metadata.views.length.toString());
63+
printKV(' Apps', metadata.apps.length.toString());
64+
printKV(' Flows', metadata.flows.length.toString());
65+
printKV(' Agents', metadata.agents.length.toString());
66+
printKV(' Tools', metadata.tools.length.toString());
67+
printKV(' Translations', metadata.translations.length.toString());
68+
69+
// 3. Publish to server
70+
const serverUrl = `${flags.server}/api/v1/packages`;
71+
printStep(`Publishing to ${serverUrl}...`);
72+
73+
const response = await fetch(serverUrl, {
74+
method: 'POST',
75+
headers: {
76+
'Content-Type': 'application/json',
77+
...(flags.token && { 'Authorization': `Bearer ${flags.token}` }),
78+
},
79+
body: JSON.stringify({ manifest, metadata }),
80+
});
81+
82+
if (!response.ok) {
83+
const error = await response.json();
84+
printError(`Publish failed: ${error.error || response.statusText}`);
85+
this.exit(1);
86+
}
87+
88+
const result = await response.json();
89+
const size = (JSON.stringify(metadata).length / 1024).toFixed(2);
90+
91+
console.log('');
92+
printSuccess(result.message);
93+
printKV(' Size', `${size} KB`);
94+
printKV(' Server', flags.server);
95+
96+
} catch (error) {
97+
printError((error as Error).message);
98+
this.exit(1);
99+
}
100+
}
101+
}

packages/rest/package.json

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,8 @@
2121
"dependencies": {
2222
"@objectstack/core": "workspace:*",
2323
"@objectstack/spec": "workspace:*",
24-
"zod": "^4.3.6"
24+
"zod": "^4.3.6",
25+
"@objectstack/service-package": "workspace:*"
2526
},
2627
"devDependencies": {
2728
"typescript": "^6.0.2",
Lines changed: 100 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,100 @@
1+
// Copyright (c) 2025 ObjectStack. Licensed under the Apache-2.0 license.
2+
3+
import { IHttpServer } from '@objectstack/core';
4+
import type { PackageService } from '@objectstack/service-package';
5+
6+
/**
7+
* Register package management API routes
8+
*
9+
* Provides endpoints for publishing, retrieving, and managing packages.
10+
* Routes:
11+
* - POST /api/v1/packages - Publish a package
12+
* - GET /api/v1/packages - List all packages
13+
* - GET /api/v1/packages/:id - Get a specific package
14+
* - DELETE /api/v1/packages/:id - Delete a package
15+
*/
16+
export function registerPackageRoutes(server: IHttpServer, packageService: PackageService, basePath: string = '/api/v1') {
17+
const packagesPath = `${basePath}/packages`;
18+
19+
// POST /api/v1/packages - Publish a package
20+
server.post(packagesPath, async (c) => {
21+
try {
22+
const body = await c.req.json();
23+
const { manifest, metadata } = body;
24+
25+
if (!manifest || !metadata) {
26+
return c.json({ error: 'Missing required fields: manifest, metadata' }, 400);
27+
}
28+
29+
if (!manifest.id || !manifest.version) {
30+
return c.json({ error: 'Invalid manifest: id and version are required' }, 400);
31+
}
32+
33+
const result = await packageService.publish({ manifest, metadata });
34+
35+
if (result.success) {
36+
return c.json({
37+
success: true,
38+
message: `Published ${manifest.id}@${manifest.version}`,
39+
package: {
40+
id: manifest.id,
41+
version: manifest.version,
42+
},
43+
});
44+
}
45+
46+
return c.json({ success: false, error: result.error }, 400);
47+
} catch (error) {
48+
return c.json({ error: (error as Error).message }, 500);
49+
}
50+
});
51+
52+
// GET /api/v1/packages - List all packages (latest versions)
53+
server.get(packagesPath, async (c) => {
54+
try {
55+
const packages = await packageService.list();
56+
return c.json({ packages });
57+
} catch (error) {
58+
return c.json({ error: (error as Error).message }, 500);
59+
}
60+
});
61+
62+
// GET /api/v1/packages/:id - Get a specific package
63+
server.get(`${packagesPath}/:id`, async (c) => {
64+
try {
65+
const packageId = c.req.param('id');
66+
const version = c.req.query('version') || 'latest';
67+
68+
const pkg = await packageService.get(packageId, version);
69+
70+
if (!pkg) {
71+
return c.json({ error: 'Package not found' }, 404);
72+
}
73+
74+
return c.json({ package: pkg });
75+
} catch (error) {
76+
return c.json({ error: (error as Error).message }, 500);
77+
}
78+
});
79+
80+
// DELETE /api/v1/packages/:id - Delete a package
81+
server.delete(`${packagesPath}/:id`, async (c) => {
82+
try {
83+
const packageId = c.req.param('id');
84+
const version = c.req.query('version');
85+
86+
const result = await packageService.delete(packageId, version);
87+
88+
if (result.success) {
89+
return c.json({
90+
success: true,
91+
message: `Deleted ${packageId}${version ? `@${version}` : ''}`,
92+
});
93+
}
94+
95+
return c.json({ success: false }, 400);
96+
} catch (error) {
97+
return c.json({ error: (error as Error).message }, 500);
98+
}
99+
});
100+
}

packages/rest/src/rest-api-plugin.ts

Lines changed: 17 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,8 @@
33
import { Plugin, PluginContext, IHttpServer } from '@objectstack/core';
44
import { RestServer } from './rest-server.js';
55
import { ObjectStackProtocol, RestServerConfig } from '@objectstack/spec/api';
6+
import { registerPackageRoutes } from './package-routes.js';
7+
import type { PackageService } from '@objectstack/service-package';
68

79
export interface RestApiPluginConfig {
810
serverServiceName?: string;
@@ -61,12 +63,26 @@ export function createRestApiPlugin(config: RestApiPluginConfig = {}): Plugin {
6163
try {
6264
const restServer = new RestServer(server, protocol, config.api as any);
6365
restServer.registerRoutes();
64-
66+
6567
ctx.logger.info('REST API successfully registered');
6668
} catch (err: any) {
6769
ctx.logger.error('Failed to register REST API routes', { error: err.message } as any);
6870
throw err;
6971
}
72+
73+
// Register package management routes if service is available
74+
try {
75+
const packageService = ctx.getService<PackageService>('package');
76+
if (packageService) {
77+
const basePath = config.api?.api?.basePath || '/api';
78+
const version = config.api?.api?.version || 'v1';
79+
registerPackageRoutes(server, packageService, `${basePath}/${version}`);
80+
ctx.logger.info('Package management routes registered');
81+
}
82+
} catch (e) {
83+
// Package service not available, skip
84+
ctx.logger.debug('Package service not available, package routes skipped');
85+
}
7086
}
7187
};
7288
}
Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
{
2+
"name": "@objectstack/service-package",
3+
"version": "1.0.0",
4+
"license": "Apache-2.0",
5+
"description": "Package management service for ObjectStack — publish, install, and manage packages",
6+
"type": "module",
7+
"main": "dist/index.js",
8+
"types": "dist/index.d.ts",
9+
"exports": {
10+
".": {
11+
"types": "./dist/index.d.ts",
12+
"import": "./dist/index.js",
13+
"require": "./dist/index.cjs"
14+
}
15+
},
16+
"scripts": {
17+
"build": "tsup --config ../../../tsup.config.ts",
18+
"test": "vitest run --passWithNoTests"
19+
},
20+
"dependencies": {
21+
"@objectstack/core": "workspace:*",
22+
"@objectstack/spec": "workspace:*"
23+
},
24+
"devDependencies": {
25+
"@types/node": "^25.6.0",
26+
"typescript": "^6.0.2",
27+
"vitest": "^4.1.4"
28+
}
29+
}

0 commit comments

Comments
 (0)