@agentage/extension 0.0.0-stage → 0.1.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (85) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +11 -2
  3. package/dist/agcid/agcid.d.ts +4 -0
  4. package/dist/agcid/agcid.js +15 -0
  5. package/dist/agcid/scope.d.ts +4 -0
  6. package/dist/agcid/scope.js +16 -0
  7. package/dist/context/context.d.ts +36 -0
  8. package/dist/context/context.js +25 -0
  9. package/dist/discovery/discovery.d.ts +47 -0
  10. package/dist/discovery/discovery.js +52 -0
  11. package/dist/discovery/discovery.schema.d.ts +106 -0
  12. package/dist/discovery/discovery.schema.js +56 -0
  13. package/dist/envelope/codes.d.ts +16 -0
  14. package/dist/envelope/codes.js +15 -0
  15. package/dist/envelope/envelope.d.ts +34 -0
  16. package/dist/envelope/envelope.js +23 -0
  17. package/dist/envelope/envelope.schema.d.ts +24 -0
  18. package/dist/envelope/envelope.schema.js +18 -0
  19. package/dist/envelope/parse.d.ts +17 -0
  20. package/dist/envelope/parse.js +12 -0
  21. package/dist/health/health.d.ts +18 -0
  22. package/dist/health/health.js +43 -0
  23. package/dist/health/health.schema.d.ts +46 -0
  24. package/dist/health/health.schema.js +20 -0
  25. package/dist/index.d.ts +19 -0
  26. package/dist/index.js +19 -0
  27. package/dist/log/log.d.ts +13 -0
  28. package/dist/log/log.js +21 -0
  29. package/dist/manifest/manifest.schema.d.ts +195 -0
  30. package/dist/manifest/manifest.schema.js +88 -0
  31. package/dist/manifest/parts.d.ts +11 -0
  32. package/dist/manifest/parts.js +24 -0
  33. package/dist/manifest/primitives.schema.d.ts +20 -0
  34. package/dist/manifest/primitives.schema.js +52 -0
  35. package/dist/manifest/read-manifest.d.ts +5 -0
  36. package/dist/manifest/read-manifest.js +19 -0
  37. package/dist/runtime/build-info.d.ts +4 -0
  38. package/dist/runtime/build-info.js +16 -0
  39. package/dist/runtime/main.d.ts +2 -0
  40. package/dist/runtime/main.js +10 -0
  41. package/dist/runtime/run-part.d.ts +13 -0
  42. package/dist/runtime/run-part.js +57 -0
  43. package/dist/schemas/json-schemas.d.ts +2 -0
  44. package/dist/schemas/json-schemas.js +29 -0
  45. package/dist/schemas/write-schemas.d.ts +1 -0
  46. package/dist/schemas/write-schemas.js +8 -0
  47. package/dist/server/app.d.ts +18 -0
  48. package/dist/server/app.js +47 -0
  49. package/dist/server/errors.d.ts +6 -0
  50. package/dist/server/errors.js +29 -0
  51. package/dist/server/part.types.d.ts +7 -0
  52. package/dist/server/part.types.js +2 -0
  53. package/dist/server/serve.d.ts +14 -0
  54. package/dist/server/serve.js +27 -0
  55. package/dist/testing/test-server.d.ts +12 -0
  56. package/dist/testing/test-server.js +26 -0
  57. package/dist/tool/build-info.d.ts +4 -0
  58. package/dist/tool/build-info.js +18 -0
  59. package/dist/tool/build.d.ts +35 -0
  60. package/dist/tool/build.js +83 -0
  61. package/dist/tool/cli.d.ts +2 -0
  62. package/dist/tool/cli.js +3 -0
  63. package/dist/tool/docker.d.ts +15 -0
  64. package/dist/tool/docker.js +26 -0
  65. package/dist/tool/exec.d.ts +12 -0
  66. package/dist/tool/exec.js +24 -0
  67. package/dist/tool/format.d.ts +2 -0
  68. package/dist/tool/format.js +14 -0
  69. package/dist/tool/index.d.ts +11 -0
  70. package/dist/tool/index.js +11 -0
  71. package/dist/tool/labels.d.ts +4 -0
  72. package/dist/tool/labels.js +25 -0
  73. package/dist/tool/main.d.ts +8 -0
  74. package/dist/tool/main.js +54 -0
  75. package/dist/tool/recipe.d.ts +3 -0
  76. package/dist/tool/recipe.js +32 -0
  77. package/dist/tool/stage.d.ts +5 -0
  78. package/dist/tool/stage.js +52 -0
  79. package/package.json +61 -4
  80. package/schemas/error-envelope.schema.json +70 -0
  81. package/schemas/extension-discovery.schema.json +182 -0
  82. package/schemas/extension-package.schema.json +309 -0
  83. package/schemas/extension-record.schema.json +176 -0
  84. package/schemas/health.schema.json +100 -0
  85. package/schemas/ok-envelope.schema.json +25 -0
@@ -0,0 +1,57 @@
1
+ import { join } from 'node:path';
2
+ import { pathToFileURL } from 'node:url';
3
+ import {} from 'node:http';
4
+ import { createContext } from '../context/context.js';
5
+ import { DEFAULT_RECORD_FILE, loadRecord } from '../discovery/discovery.js';
6
+ import { createLog } from '../log/log.js';
7
+ import { entryOf } from '../manifest/parts.js';
8
+ import { readManifest } from '../manifest/read-manifest.js';
9
+ import { createApiApp } from '../server/app.js';
10
+ import {} from '../server/part.types.js';
11
+ import { serve } from '../server/serve.js';
12
+ import { buildInfoFromEnv, portFromEnv } from './build-info.js';
13
+ const importFile = (file) => import(pathToFileURL(file).href);
14
+ export const createOf = (mod, file) => {
15
+ const create = mod?.create;
16
+ if (typeof create !== 'function')
17
+ throw new Error(`${file} does not export create(ctx)`);
18
+ return create;
19
+ };
20
+ const recordFor = async (env, log) => {
21
+ const file = env['AGENTAGE_RECORD_FILE'] || DEFAULT_RECORD_FILE;
22
+ const record = await loadRecord(file);
23
+ if (!record)
24
+ log.warn('no release record mounted; discovery lists this image by tag', { file });
25
+ return record;
26
+ };
27
+ // The image entry for a server part: manifest, ctx, create(ctx), then the kit's own server.
28
+ export const runPart = async (options) => {
29
+ if (options.part !== 'api')
30
+ throw new Error(`part ${options.part} is not served by kit 0.1`);
31
+ const { env } = options;
32
+ const pkg = await readManifest(options.dir);
33
+ const log = createLog({ extension: pkg.agentage.id, part: 'api' });
34
+ const port = portFromEnv(env);
35
+ const origin = env['AGENTAGE_ORIGIN'] || undefined;
36
+ const ctx = createContext({
37
+ pkg,
38
+ part: 'api',
39
+ origin: origin ?? `http://127.0.0.1:${port}`,
40
+ log,
41
+ });
42
+ const file = join(options.dir, entryOf(pkg, 'api'));
43
+ const mod = await (options.importModule ?? importFile)(file);
44
+ const router = await createOf(mod, file)(ctx);
45
+ const build = buildInfoFromEnv(env);
46
+ const record = await recordFor(env, log);
47
+ const app = createApiApp({
48
+ pkg,
49
+ build,
50
+ router,
51
+ log,
52
+ record,
53
+ origin,
54
+ ownImage: env['AGENTAGE_IMAGE'],
55
+ });
56
+ return serve({ app, port, log, ...options.serve });
57
+ };
@@ -0,0 +1,2 @@
1
+ export declare const jsonSchemas: () => Record<string, Record<string, unknown>>;
2
+ export declare const schemaText: (schema: Record<string, unknown>) => string;
@@ -0,0 +1,29 @@
1
+ import { z } from 'zod';
2
+ import { DiscoveryDocumentSchema, ExtensionRecordSchema } from '../discovery/discovery.schema.js';
3
+ import { ErrorEnvelopeSchema, OkEnvelopeSchema } from '../envelope/envelope.schema.js';
4
+ import { HealthSchema } from '../health/health.schema.js';
5
+ import { PackageSchema } from '../manifest/manifest.schema.js';
6
+ const BASE = 'https://agentage.io/schemas/';
7
+ const SOURCES = {
8
+ 'extension-package.schema.json': {
9
+ schema: PackageSchema,
10
+ title: 'agentage extension package.json',
11
+ },
12
+ 'extension-record.schema.json': {
13
+ schema: ExtensionRecordSchema,
14
+ title: 'agentage-extension.json build record (in the package and the release asset)',
15
+ },
16
+ 'extension-discovery.schema.json': {
17
+ schema: DiscoveryDocumentSchema,
18
+ title: 'GET /.well-known/agentage-extension.json',
19
+ },
20
+ 'health.schema.json': { schema: HealthSchema, title: 'GET /health of every part container' },
21
+ 'error-envelope.schema.json': { schema: ErrorEnvelopeSchema, title: 'REST error envelope' },
22
+ 'ok-envelope.schema.json': { schema: OkEnvelopeSchema, title: 'REST success envelope' },
23
+ };
24
+ // Generated from the Zod schemas, the source of truth; cross-field rules live in Zod only.
25
+ export const jsonSchemas = () => Object.fromEntries(Object.entries(SOURCES).map(([file, { schema, title }]) => [
26
+ file,
27
+ { $id: `${BASE}${file}`, title, ...z.toJSONSchema(schema, { io: 'input' }) },
28
+ ]));
29
+ export const schemaText = (schema) => `${JSON.stringify(schema, null, 2)}\n`;
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,8 @@
1
+ import { mkdir, writeFile } from 'node:fs/promises';
2
+ import { join } from 'node:path';
3
+ import { jsonSchemas, schemaText } from './json-schemas.js';
4
+ const outDir = process.argv[2] ?? 'schemas';
5
+ await mkdir(outDir, { recursive: true });
6
+ for (const [file, schema] of Object.entries(jsonSchemas())) {
7
+ await writeFile(join(outDir, file), schemaText(schema));
8
+ }
@@ -0,0 +1,18 @@
1
+ import { type CorsOptions } from 'cors';
2
+ import { type Express } from 'express';
3
+ import { type BuildInfo } from '../discovery/discovery.js';
4
+ import { type ExtensionRecord } from '../discovery/discovery.schema.js';
5
+ import { type Log } from '../log/log.js';
6
+ import { type ExtensionPackage } from '../manifest/manifest.schema.js';
7
+ import { type ApiHandler } from './part.types.js';
8
+ export interface ApiAppOptions {
9
+ pkg: ExtensionPackage;
10
+ build: BuildInfo;
11
+ router: ApiHandler;
12
+ log: Log;
13
+ record?: ExtensionRecord | null;
14
+ ownImage?: string;
15
+ origin?: string;
16
+ }
17
+ export declare const PUBLIC_CORS: CorsOptions;
18
+ export declare const createApiApp: (options: ApiAppOptions) => Express;
@@ -0,0 +1,47 @@
1
+ import cors, {} from 'cors';
2
+ import express, {} from 'express';
3
+ import helmet from 'helmet';
4
+ import { agcidMiddleware } from '../agcid/scope.js';
5
+ import { discoveryDocument, recordOf, } from '../discovery/discovery.js';
6
+ import {} from '../discovery/discovery.schema.js';
7
+ import { createHealthHandler } from '../health/health.js';
8
+ import {} from '../log/log.js';
9
+ import {} from '../manifest/manifest.schema.js';
10
+ import { errorHandler, notFound } from './errors.js';
11
+ import {} from './part.types.js';
12
+ // api-standards section 6: CORS * without credentials on /.well-known/ and /api/.
13
+ export const PUBLIC_CORS = {
14
+ origin: '*',
15
+ credentials: false,
16
+ allowedHeaders: ['Authorization', 'Content-Type', 'agcid', 'traceparent', 'If-Match'],
17
+ exposedHeaders: ['agcid', 'ETag', 'Retry-After', 'RateLimit', 'WWW-Authenticate'],
18
+ };
19
+ const BODY_LIMIT = '1mb';
20
+ const requestOrigin = (req) => `${req.protocol}://${req.get('host') ?? ''}`;
21
+ const discoveryHandler = (options) => {
22
+ const images = options.ownImage ? { api: options.ownImage } : {};
23
+ const record = options.record ?? recordOf(options.pkg, options.build, images);
24
+ return (req, res) => {
25
+ res.json(discoveryDocument(record, options.origin ?? requestOrigin(req)));
26
+ };
27
+ };
28
+ const healthHandler = (options) => createHealthHandler({
29
+ extension: options.pkg.agentage.id,
30
+ part: 'api',
31
+ version: options.pkg.version,
32
+ build: options.build,
33
+ });
34
+ // The kit owns the app: agcid, helmet, CORS, /health, discovery, the body limit, 404 and errors.
35
+ export const createApiApp = (options) => {
36
+ const app = express();
37
+ app.disable('x-powered-by');
38
+ app.set('trust proxy', 'loopback, linklocal, uniquelocal');
39
+ app.use(agcidMiddleware, helmet());
40
+ app.get('/health', healthHandler(options));
41
+ app.use('/.well-known', cors(PUBLIC_CORS));
42
+ app.get('/.well-known/agentage-extension.json', discoveryHandler(options));
43
+ app.use('/api/v1', cors(PUBLIC_CORS), express.json({ limit: BODY_LIMIT }), options.router);
44
+ app.use(notFound);
45
+ app.use(errorHandler(options.log));
46
+ return app;
47
+ };
@@ -0,0 +1,6 @@
1
+ import { type ErrorRequestHandler, type RequestHandler } from 'express';
2
+ import { type Failure } from '../envelope/envelope.js';
3
+ import { type Log } from '../log/log.js';
4
+ export declare const toFailure: (err: unknown) => Failure;
5
+ export declare const notFound: RequestHandler;
6
+ export declare const errorHandler: (log: Log) => ErrorRequestHandler;
@@ -0,0 +1,29 @@
1
+ import {} from 'express';
2
+ import { agcidOfResponse } from '../agcid/scope.js';
3
+ import { errorEnvelope, fail, isFailure } from '../envelope/envelope.js';
4
+ import {} from '../log/log.js';
5
+ const bodyParserFailure = (err) => {
6
+ const type = err?.type;
7
+ if (type === 'entity.too.large')
8
+ return fail('PAYLOAD_TOO_LARGE', 'Body too large');
9
+ if (type === 'entity.parse.failed')
10
+ return fail('VALIDATION_ERROR', 'Invalid JSON body');
11
+ return null;
12
+ };
13
+ // Anything that is not a kit failure is an INTERNAL 500 with a generic message; the log has it whole.
14
+ export const toFailure = (err) => {
15
+ if (isFailure(err))
16
+ return err;
17
+ return bodyParserFailure(err) ?? fail('INTERNAL', 'Internal error');
18
+ };
19
+ export const notFound = () => {
20
+ throw fail('NOT_FOUND', 'Not found');
21
+ };
22
+ export const errorHandler = (log) => (err, _req, res, _next) => {
23
+ const failure = toFailure(err);
24
+ if (failure.status >= 500)
25
+ log.error(err);
26
+ if (failure.retryAfter !== undefined)
27
+ res.setHeader('Retry-After', String(failure.retryAfter));
28
+ res.status(failure.status).json(errorEnvelope(failure, agcidOfResponse(res)));
29
+ };
@@ -0,0 +1,7 @@
1
+ import { type RequestHandler, type Router } from 'express';
2
+ import { type PartContext } from '../context/context.js';
3
+ export type ApiHandler = Router | RequestHandler;
4
+ export type ApiCreate<C extends PartContext = PartContext> = (ctx: C) => ApiHandler | Promise<ApiHandler>;
5
+ export interface ApiPart {
6
+ create: ApiCreate;
7
+ }
@@ -0,0 +1,2 @@
1
+ import {} from 'express';
2
+ import {} from '../context/context.js';
@@ -0,0 +1,14 @@
1
+ import { type Server } from 'node:http';
2
+ import { type Express } from 'express';
3
+ import { type Log } from '../log/log.js';
4
+ export interface ServeOptions {
5
+ app: Express;
6
+ port: number;
7
+ log: Log;
8
+ graceMs?: number;
9
+ exit?: (code: number) => void;
10
+ signals?: NodeJS.Signals[];
11
+ }
12
+ export declare const listen: (app: Express, port: number, host?: string) => Promise<Server>;
13
+ export declare const shutdownOn: (server: Server, options: ServeOptions) => (() => void);
14
+ export declare const serve: (options: ServeOptions) => Promise<Server>;
@@ -0,0 +1,27 @@
1
+ import {} from 'node:http';
2
+ import {} from 'node:net';
3
+ import {} from 'express';
4
+ import {} from '../log/log.js';
5
+ export const listen = (app, port, host) => new Promise((resolve, reject) => {
6
+ const done = (err) => (err ? reject(err) : resolve(server));
7
+ const server = host === undefined ? app.listen(port, done) : app.listen(port, host, done);
8
+ });
9
+ // SIGTERM drains: no new connections, idle ones closed, a hard exit after the grace period.
10
+ export const shutdownOn = (server, options) => {
11
+ const exit = options.exit ?? ((code) => process.exit(code));
12
+ const stop = () => {
13
+ options.log.info('shutting down');
14
+ server.close(() => exit(0));
15
+ server.closeIdleConnections();
16
+ setTimeout(() => exit(0), options.graceMs ?? 10_000).unref();
17
+ };
18
+ for (const signal of options.signals ?? ['SIGTERM', 'SIGINT'])
19
+ process.once(signal, stop);
20
+ return stop;
21
+ };
22
+ export const serve = async (options) => {
23
+ const server = await listen(options.app, options.port);
24
+ shutdownOn(server, options);
25
+ options.log.info('listening', { port: server.address().port });
26
+ return server;
27
+ };
@@ -0,0 +1,12 @@
1
+ import { type PartContext } from '../context/context.js';
2
+ import { type ApiCreate } from '../server/part.types.js';
3
+ export interface TestServerOptions<C extends PartContext> {
4
+ create: ApiCreate<C>;
5
+ ctx?: C;
6
+ }
7
+ export interface TestServer {
8
+ url: string;
9
+ fetch(path: string, init?: RequestInit): Promise<Response>;
10
+ close(): Promise<void>;
11
+ }
12
+ export declare const startTestServer: <C extends PartContext = PartContext>(options: TestServerOptions<C>) => Promise<TestServer>;
@@ -0,0 +1,26 @@
1
+ import {} from 'node:net';
2
+ import { createTestContext } from '../context/context.js';
3
+ import {} from '../manifest/manifest.schema.js';
4
+ import { createApiApp } from '../server/app.js';
5
+ import {} from '../server/part.types.js';
6
+ import { listen } from '../server/serve.js';
7
+ const TEST_BUILD = { commit: '0'.repeat(40), buildTime: '2026-01-01T00:00:00Z' };
8
+ const testPackage = (ctx) => ({
9
+ name: `@agentage/extension-${ctx.extension.id}`,
10
+ version: ctx.extension.version,
11
+ engines: { agentage: '^0.1.0' },
12
+ agentage: { id: ctx.extension.id, parts: { api: true } },
13
+ });
14
+ // Runs an api part's create(ctx) inside the kit's real app on 127.0.0.1, port chosen by the OS.
15
+ export const startTestServer = async (options) => {
16
+ const ctx = options.ctx ?? createTestContext();
17
+ const router = await options.create(ctx);
18
+ const app = createApiApp({ pkg: testPackage(ctx), build: TEST_BUILD, router, log: ctx.log });
19
+ const server = await listen(app, 0, '127.0.0.1');
20
+ const url = `http://127.0.0.1:${server.address().port}`;
21
+ return {
22
+ url,
23
+ fetch: (path, init) => fetch(`${url}${path}`, init),
24
+ close: () => new Promise((resolve) => server.close(() => resolve())),
25
+ };
26
+ };
@@ -0,0 +1,4 @@
1
+ import { type BuildInfo } from '../discovery/discovery.js';
2
+ import { type Env } from '../runtime/build-info.js';
3
+ import { type Run } from './exec.js';
4
+ export declare const resolveBuildInfo: (dir: string, env: Env, run: Run, now?: Date) => Promise<BuildInfo>;
@@ -0,0 +1,18 @@
1
+ import {} from '../discovery/discovery.js';
2
+ import {} from '../runtime/build-info.js';
3
+ import { mustRun } from './exec.js';
4
+ const FULL_SHA = /^[0-9a-f]{40}$/;
5
+ const isoSeconds = (date) => `${date.toISOString().slice(0, 19)}Z`;
6
+ const commitOf = async (dir, env, run) => {
7
+ const given = env['COMMIT_SHA'] || env['GITHUB_SHA'];
8
+ if (given)
9
+ return given;
10
+ return (await mustRun(run, 'git', ['rev-parse', 'HEAD'], { cwd: dir })).trim();
11
+ };
12
+ // The commit every image label, /health and the record carry: CI's sha, else the checkout's HEAD.
13
+ export const resolveBuildInfo = async (dir, env, run, now = new Date()) => {
14
+ const commit = await commitOf(dir, env, run);
15
+ if (!FULL_SHA.test(commit))
16
+ throw new Error(`commit is not a full 40-hex sha: ${commit}`);
17
+ return { commit, buildTime: env['BUILD_TIME'] || isoSeconds(now) };
18
+ };
@@ -0,0 +1,35 @@
1
+ import { type ExtensionPackage } from '../manifest/manifest.schema.js';
2
+ import { type ImagePartKind } from '../manifest/primitives.schema.js';
3
+ import { type Env } from '../runtime/build-info.js';
4
+ import { type Docker } from './docker.js';
5
+ import { type Run } from './exec.js';
6
+ export declare const DEFAULT_REGISTRY = "ghcr.io/agentage";
7
+ export declare const SUPPORTED_PARTS: readonly ImagePartKind[];
8
+ export interface BuildOptions {
9
+ dir?: string;
10
+ push?: boolean;
11
+ registry?: string;
12
+ env?: Env;
13
+ now?: Date;
14
+ }
15
+ export interface BuildDeps {
16
+ run: Run;
17
+ docker?: Docker;
18
+ tmpRoot?: string;
19
+ }
20
+ export interface BuiltImage {
21
+ part: ImagePartKind;
22
+ tag: string;
23
+ digest?: string;
24
+ image?: string;
25
+ }
26
+ export interface BuildResult {
27
+ id: string;
28
+ version: string;
29
+ commit: string;
30
+ buildTime: string;
31
+ images: BuiltImage[];
32
+ recordFile?: string;
33
+ }
34
+ export declare const imageTag: (registry: string, pkg: ExtensionPackage, part: ImagePartKind) => string;
35
+ export declare const build: (options?: BuildOptions, deps?: BuildDeps) => Promise<BuildResult>;
@@ -0,0 +1,83 @@
1
+ import { rm, writeFile } from 'node:fs/promises';
2
+ import { join, resolve } from 'node:path';
3
+ import { recordOf, validateRecord } from '../discovery/discovery.js';
4
+ import {} from '../manifest/manifest.schema.js';
5
+ import { imageParts } from '../manifest/parts.js';
6
+ import {} from '../manifest/primitives.schema.js';
7
+ import { readManifest } from '../manifest/read-manifest.js';
8
+ import {} from '../runtime/build-info.js';
9
+ import { resolveBuildInfo } from './build-info.js';
10
+ import { createDocker } from './docker.js';
11
+ import { mustRun, runCommand } from './exec.js';
12
+ import { labelsOf } from './labels.js';
13
+ import { RECORD_FILE, stageContext } from './stage.js';
14
+ export const DEFAULT_REGISTRY = 'ghcr.io/agentage';
15
+ export const SUPPORTED_PARTS = ['api'];
16
+ export const imageTag = (registry, pkg, part) => `${registry}/${pkg.agentage.id}-${part}:${pkg.version}`;
17
+ const partsToBuild = (pkg) => {
18
+ const parts = imageParts(pkg);
19
+ const unsupported = parts.filter((part) => !SUPPORTED_PARTS.includes(part));
20
+ if (unsupported.length > 0) {
21
+ throw new Error(`@agentage/extension 0.1 builds the api part only, not: ${unsupported.join(', ')}`);
22
+ }
23
+ return parts;
24
+ };
25
+ const compile = async (dir, pkg, run) => {
26
+ const scripts = pkg.scripts;
27
+ if (typeof scripts?.['build'] === 'string')
28
+ await mustRun(run, 'npm', ['run', 'build'], { cwd: dir });
29
+ };
30
+ const buildImage = async (job, docker, tmpRoot) => {
31
+ const context = await stageContext(job.dir, job.pkg, job.part, tmpRoot);
32
+ try {
33
+ const buildArgs = {
34
+ COMMIT_SHA: job.info.commit,
35
+ BUILD_TIME: job.info.buildTime,
36
+ IMAGE: job.tag,
37
+ };
38
+ await docker.build({
39
+ context,
40
+ tag: job.tag,
41
+ buildArgs,
42
+ labels: labelsOf(job.pkg, job.part, job.info),
43
+ });
44
+ }
45
+ finally {
46
+ await rm(context, { recursive: true, force: true });
47
+ }
48
+ };
49
+ const pushImage = async (job, docker) => {
50
+ const digest = await docker.push(job.tag);
51
+ const repository = job.tag.slice(0, job.tag.lastIndexOf(':'));
52
+ return { part: job.part, tag: job.tag, digest, image: `${repository}@${digest}` };
53
+ };
54
+ const writeRecord = async (dir, pkg, info, images) => {
55
+ const byPart = Object.fromEntries(images.map((built) => [built.part, built.image]));
56
+ const record = validateRecord(recordOf(pkg, info, byPart));
57
+ const file = join(dir, 'dist', RECORD_FILE);
58
+ await writeFile(file, `${JSON.stringify(record, null, 2)}\n`);
59
+ return file;
60
+ };
61
+ const buildParts = async (base, options, deps) => {
62
+ const docker = deps.docker ?? createDocker(deps.run);
63
+ const registry = options.registry ?? DEFAULT_REGISTRY;
64
+ const images = [];
65
+ for (const part of partsToBuild(base.pkg)) {
66
+ const job = { ...base, part, tag: imageTag(registry, base.pkg, part) };
67
+ await buildImage(job, docker, deps.tmpRoot);
68
+ images.push(options.push ? await pushImage(job, docker) : { part, tag: job.tag });
69
+ }
70
+ return images;
71
+ };
72
+ // One image per server part from the kit recipe; with push, the digests go into the build record.
73
+ export const build = async (options = {}, deps = { run: runCommand }) => {
74
+ const dir = resolve(options.dir ?? '.');
75
+ const pkg = await readManifest(dir);
76
+ partsToBuild(pkg);
77
+ const info = await resolveBuildInfo(dir, options.env ?? process.env, deps.run, options.now);
78
+ await compile(dir, pkg, deps.run);
79
+ const images = await buildParts({ dir, pkg, info }, options, deps);
80
+ const recordFile = options.push ? await writeRecord(dir, pkg, info, images) : undefined;
81
+ const result = { id: pkg.agentage.id, version: pkg.version, ...info, images };
82
+ return recordFile ? { ...result, recordFile } : result;
83
+ };
@@ -0,0 +1,2 @@
1
+ #!/usr/bin/env node
2
+ export {};
@@ -0,0 +1,3 @@
1
+ #!/usr/bin/env node
2
+ import { main } from './main.js';
3
+ process.exitCode = await main(process.argv.slice(2));
@@ -0,0 +1,15 @@
1
+ import { type Run } from './exec.js';
2
+ export interface DockerBuild {
3
+ context: string;
4
+ tag: string;
5
+ buildArgs: Record<string, string>;
6
+ labels: Record<string, string>;
7
+ }
8
+ export interface Docker {
9
+ build(options: DockerBuild): Promise<void>;
10
+ push(tag: string): Promise<string>;
11
+ }
12
+ export declare const PLATFORM = "linux/amd64";
13
+ export declare const buildArgsOf: (options: DockerBuild) => string[];
14
+ export declare const digestOf: (pushOutput: string) => string;
15
+ export declare const createDocker: (run: Run) => Docker;
@@ -0,0 +1,26 @@
1
+ import { mustRun } from './exec.js';
2
+ export const PLATFORM = 'linux/amd64';
3
+ const flags = (flag, values) => Object.entries(values).flatMap(([key, value]) => [flag, `${key}=${value}`]);
4
+ export const buildArgsOf = (options) => [
5
+ 'build',
6
+ '--platform',
7
+ PLATFORM,
8
+ '--provenance=false',
9
+ '--tag',
10
+ options.tag,
11
+ ...flags('--build-arg', options.buildArgs),
12
+ ...flags('--label', options.labels),
13
+ options.context,
14
+ ];
15
+ export const digestOf = (pushOutput) => {
16
+ const match = /digest: (sha256:[0-9a-f]{64})/.exec(pushOutput);
17
+ if (!match?.[1])
18
+ throw new Error('docker push printed no digest');
19
+ return match[1];
20
+ };
21
+ export const createDocker = (run) => ({
22
+ build: async (options) => {
23
+ await mustRun(run, 'docker', buildArgsOf(options));
24
+ },
25
+ push: async (tag) => digestOf(await mustRun(run, 'docker', ['push', tag], { echo: true })),
26
+ });
@@ -0,0 +1,12 @@
1
+ export interface RunOptions {
2
+ cwd?: string;
3
+ env?: NodeJS.ProcessEnv;
4
+ echo?: boolean;
5
+ }
6
+ export interface RunResult {
7
+ code: number;
8
+ stdout: string;
9
+ }
10
+ export type Run = (command: string, args: string[], options?: RunOptions) => Promise<RunResult>;
11
+ export declare const runCommand: Run;
12
+ export declare const mustRun: (run: Run, command: string, args: string[], options?: RunOptions) => Promise<string>;
@@ -0,0 +1,24 @@
1
+ import { spawn } from 'node:child_process';
2
+ // stdout is captured (and echoed to stderr with `echo`); stderr streams through as progress.
3
+ export const runCommand = (command, args, options = {}) => new Promise((resolve, reject) => {
4
+ const child = spawn(command, args, {
5
+ cwd: options.cwd,
6
+ env: options.env ?? process.env,
7
+ stdio: ['ignore', 'pipe', 'inherit'],
8
+ });
9
+ let stdout = '';
10
+ child.stdout.on('data', (chunk) => {
11
+ stdout += chunk.toString();
12
+ if (options.echo)
13
+ process.stderr.write(chunk);
14
+ });
15
+ child.on('error', reject);
16
+ child.on('close', (code) => resolve({ code: code ?? 1, stdout }));
17
+ });
18
+ export const mustRun = async (run, command, args, options) => {
19
+ const result = await run(command, args, options);
20
+ if (result.code !== 0) {
21
+ throw new Error(`${command} ${args[0] ?? ''} failed with exit code ${result.code}`);
22
+ }
23
+ return result.stdout;
24
+ };
@@ -0,0 +1,2 @@
1
+ import { type BuildResult } from './build.js';
2
+ export declare const buildLines: (result: BuildResult, cwd?: string) => string[];
@@ -0,0 +1,14 @@
1
+ import { relative } from 'node:path';
2
+ import {} from './build.js';
3
+ const shortDigest = (digest) => digest.slice(0, 'sha256:'.length + 8);
4
+ // One line per image, the record path last: what `agentage extension build` prints.
5
+ export const buildLines = (result, cwd = process.cwd()) => {
6
+ const width = Math.max(...result.images.map((image) => image.tag.length)) + 2;
7
+ const lines = result.images.map((image) => {
8
+ const state = image.digest ? `${shortDigest(image.digest)} pushed` : 'built';
9
+ return `${image.part.padEnd(8)}${image.tag.padEnd(width)}${state}`;
10
+ });
11
+ if (result.recordFile)
12
+ lines.push(`${'record'.padEnd(8)}${relative(cwd, result.recordFile)}`);
13
+ return lines;
14
+ };
@@ -0,0 +1,11 @@
1
+ export { build, DEFAULT_REGISTRY, imageTag, SUPPORTED_PARTS, type BuildDeps, type BuildOptions, type BuildResult, type BuiltImage, } from './build.js';
2
+ export { resolveBuildInfo } from './build-info.js';
3
+ export { createDocker, PLATFORM, type Docker, type DockerBuild } from './docker.js';
4
+ export { runCommand, type Run, type RunOptions, type RunResult } from './exec.js';
5
+ export { buildLines } from './format.js';
6
+ export { labelsOf } from './labels.js';
7
+ export { main as runTool, USAGE as TOOL_USAGE } from './main.js';
8
+ export { PART_PORT, serverPartRecipe } from './recipe.js';
9
+ export { RECORD_FILE, stageContext } from './stage.js';
10
+ export { readManifest, validateManifest } from '../manifest/read-manifest.js';
11
+ export { type ExtensionPackage } from '../manifest/manifest.schema.js';
@@ -0,0 +1,11 @@
1
+ export { build, DEFAULT_REGISTRY, imageTag, SUPPORTED_PARTS, } from './build.js';
2
+ export { resolveBuildInfo } from './build-info.js';
3
+ export { createDocker, PLATFORM } from './docker.js';
4
+ export { runCommand } from './exec.js';
5
+ export { buildLines } from './format.js';
6
+ export { labelsOf } from './labels.js';
7
+ export { main as runTool, USAGE as TOOL_USAGE } from './main.js';
8
+ export { PART_PORT, serverPartRecipe } from './recipe.js';
9
+ export { RECORD_FILE, stageContext } from './stage.js';
10
+ export { readManifest, validateManifest } from '../manifest/read-manifest.js';
11
+ export {} from '../manifest/manifest.schema.js';
@@ -0,0 +1,4 @@
1
+ import { type BuildInfo } from '../discovery/discovery.js';
2
+ import { type ExtensionPackage } from '../manifest/manifest.schema.js';
3
+ import { type ImagePartKind } from '../manifest/primitives.schema.js';
4
+ export declare const labelsOf: (pkg: ExtensionPackage, part: ImagePartKind, build: BuildInfo) => Record<string, string>;
@@ -0,0 +1,25 @@
1
+ import {} from '../discovery/discovery.js';
2
+ import {} from '../manifest/manifest.schema.js';
3
+ import {} from '../manifest/primitives.schema.js';
4
+ const repositoryUrl = (pkg) => {
5
+ const repo = pkg.repository;
6
+ const url = typeof repo === 'string' ? repo : repo?.url;
7
+ if (typeof url !== 'string')
8
+ return undefined;
9
+ return url.replace(/^git\+/, '').replace(/\.git$/, '');
10
+ };
11
+ // OCI labels: revision is the full sha, created the build time; source links the GHCR package.
12
+ export const labelsOf = (pkg, part, build) => {
13
+ const labels = {
14
+ 'org.opencontainers.image.revision': build.commit,
15
+ 'org.opencontainers.image.created': build.buildTime,
16
+ 'org.opencontainers.image.version': pkg.version,
17
+ 'org.opencontainers.image.title': `${pkg.agentage.id}-${part}`,
18
+ 'io.agentage.extension': pkg.agentage.id,
19
+ 'io.agentage.part': part,
20
+ };
21
+ const source = repositoryUrl(pkg);
22
+ if (source)
23
+ labels['org.opencontainers.image.source'] = source;
24
+ return labels;
25
+ };
@@ -0,0 +1,8 @@
1
+ import { type BuildOptions, type BuildResult } from './build.js';
2
+ export interface MainDeps {
3
+ build: (options: BuildOptions) => Promise<BuildResult>;
4
+ out: (line: string) => void;
5
+ err: (line: string) => void;
6
+ }
7
+ export declare const USAGE = "usage: agentage-extension build [--push] [--dir <path>] [--registry <host/owner>]\n\n build one image per server part from the kit recipe\n --push push the images and write dist/agentage-extension.json with their digests\n --dir the extension package folder (default: the current folder)\n --registry image registry and owner (default: ghcr.io/agentage)";
8
+ export declare const main: (argv: string[], deps?: MainDeps) => Promise<number>;