@agentage/extension 0.4.0 → 0.4.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.
package/README.md CHANGED
@@ -9,5 +9,21 @@ correlation id, the response envelope, the store pool, the scheduler and argumen
9
9
  npm install @agentage/extension express zod
10
10
  ```
11
11
 
12
+ `agentage extension check [dir] --json` prints the conformance report (exit 1 when `conforms` is
13
+ false); `file` and `line` are there only when a problem has them:
14
+
15
+ ```json
16
+ {
17
+ "id": "probe",
18
+ "version": "0.4.0",
19
+ "conforms": false,
20
+ "rules": ["manifest", "entries", "surface", "stack", "store", "sealing"],
21
+ "problems": [
22
+ { "rule": "sealing", "file": "src/api/index.ts", "line": 7, "problem": "reads process.env" }
23
+ ],
24
+ "skipped": {}
25
+ }
26
+ ```
27
+
12
28
  See the [platform README](https://github.com/agentage/platform#readme) for every part kind, the
13
29
  manifest, the tool and how a part image runs.
@@ -5,16 +5,16 @@ export interface CheckOptions {
5
5
  requireDocker?: boolean;
6
6
  rules?: readonly CheckRule[];
7
7
  }
8
- export interface CheckInput extends CheckOptions {
8
+ export type CheckInput = CheckOptions & {
9
9
  dir: string;
10
10
  pkg: ExtensionPackage;
11
11
  run: Run;
12
- }
13
- export interface Finding {
12
+ };
13
+ export type Finding = {
14
14
  file?: string;
15
15
  line?: number;
16
16
  problem: string;
17
- }
17
+ };
18
18
  export type CheckProblem = Finding & {
19
19
  rule: string;
20
20
  };
@@ -33,5 +33,5 @@ export interface CheckReport {
33
33
  skipped: Record<string, string>;
34
34
  }
35
35
  export declare const RULES: readonly CheckRule[];
36
- export declare const check: (dir: string, options?: CheckOptions) => Promise<CheckReport>;
36
+ export declare const check: (at: string, options?: CheckOptions) => Promise<CheckReport>;
37
37
  export declare const assertConforms: (dir: string, options?: CheckOptions) => Promise<void>;
@@ -1,5 +1,5 @@
1
1
  import { readdir } from 'node:fs/promises';
2
- import { join } from 'node:path';
2
+ import { join, normalize, resolve } from 'node:path';
3
3
  import { pathToFileURL } from 'node:url';
4
4
  import semver from 'semver';
5
5
  import { checkCli } from '../command/check-cli.js';
@@ -14,14 +14,15 @@ import { sealing } from './rules/sealing.js';
14
14
  import { store } from './rules/store.js';
15
15
  import { surface } from './rules/surface.js';
16
16
  import { KIT_VERSION } from './kit-version.js';
17
- import { assertBuilt } from './stage.js';
17
+ import { assertBuilt, exists } from './stage.js';
18
18
  const RECIPE = /^(?:dockerfile|containerfile)(?:\..+)?$|^(?:docker-)?compose(?:\.[\w-]+)?\.ya?ml$/i;
19
+ const NO_PACKAGE = 'is missing: run it in an extension package folder';
19
20
  const manifest = {
20
21
  name: 'manifest',
21
22
  run: async ({ dir }) => {
22
- const value = await readJsonIfExists(join(dir, 'package.json')).catch((err) => {
23
- throw new Error(messageOf(err).replace(`${dir}/`, ''));
24
- });
23
+ const value = await readJsonIfExists(join(dir, 'package.json'));
24
+ if (value === undefined)
25
+ return [{ file: 'package.json', problem: NO_PACKAGE }];
25
26
  const issues = PackageSchema.safeParse(value).error?.issues ?? [];
26
27
  const recipes = (await readdir(dir)).filter((file) => RECIPE.test(file)).sort();
27
28
  const recipe = 'is not allowed: the kit writes every image recipe';
@@ -31,32 +32,35 @@ const manifest = {
31
32
  ];
32
33
  },
33
34
  };
34
- const problemsOf = (work) => work.then((found) => (Array.isArray(found) ? found : []), (err) => [messageOf(err)]);
35
- const exportProblems = async (dir, pkg, part) => {
36
- const mod = (await import(pathToFileURL(join(dir, entryOf(pkg, part))).href));
37
- const names = Object.keys(mod);
38
- if (!names.includes('create'))
39
- return [`part ${part} does not export create`];
35
+ const exportProblem = async (dir, pkg, part) => {
36
+ const href = pathToFileURL(join(dir, entryOf(pkg, part))).href;
37
+ const mod = (await import(href).catch((err) => {
38
+ throw new Error(`part ${part} fails at import: ${messageOf(err)}`);
39
+ }));
40
+ const others = Object.keys(mod).filter((name) => name !== 'create');
40
41
  if (typeof mod.create !== 'function')
41
- return [`part ${part} create is not a function`];
42
- const others = names.filter((name) => name !== 'create');
43
- return others.length > 0 ? [`part ${part} exports ${others.join(', ')} beside create`] : [];
42
+ return `part ${part} does not export create${'create' in mod ? ' as a function' : ''}`;
43
+ if (others.length > 0)
44
+ return `part ${part} exports ${others.join(', ')} beside create`;
45
+ const cli = part === 'cli' ? await checkCli(dir) : null;
46
+ return cli && `cli part: ${cli}`;
44
47
  };
45
- const partProblems = async ({ dir, pkg }, part) => {
46
- const built = part === 'cli' ? [] : await problemsOf(assertBuilt(dir, pkg, part));
47
- if (built.length > 0 || part === 'ui')
48
- return built;
49
- const own = await problemsOf(exportProblems(dir, pkg, part));
50
- const cli = own.length === 0 && part === 'cli' ? await checkCli(dir) : null;
51
- return cli ? [`cli part: ${cli}`] : own;
48
+ const partProblem = async ({ dir, pkg }, part) => {
49
+ if (part !== 'cli')
50
+ await assertBuilt(dir, pkg, part);
51
+ else if (!(await exists(join(dir, entryOf(pkg, part)))))
52
+ return `${normalize(entryOf(pkg, part))} not found: the package build must write the cli part there`;
53
+ return part === 'ui' ? null : exportProblem(dir, pkg, part);
52
54
  };
53
55
  // Every part built where its image looks, exporting create and nothing else; cli handlers = commands.
54
56
  const entries = {
55
57
  name: 'entries',
56
58
  run: async (input) => {
59
+ if (!(await exists(join(input.dir, 'dist'))))
60
+ return [{ file: 'dist', problem: 'is missing: build the package first (npm run build)' }];
57
61
  const parts = Object.keys(normalizeParts(input.pkg.agentage.parts).code);
58
- const found = await Promise.all(parts.map((part) => partProblems(input, part)));
59
- return found.flat().map((problem) => ({ problem }));
62
+ const found = parts.map((part) => partProblem(input, part).catch(messageOf));
63
+ return (await Promise.all(found)).flatMap((problem) => (problem ? [{ problem }] : []));
60
64
  },
61
65
  };
62
66
  const MAJORS = { express: '^5', zod: '^4', '@modelcontextprotocol/server': '^2' };
@@ -87,9 +91,11 @@ const stack = {
87
91
  };
88
92
  // The closed list of extension-kit section 7, in its order; build runs it before any image.
89
93
  export const RULES = [manifest, entries, surface, stack, store, sealing];
90
- const runRule = (rule, input) => rule.run(input).catch((err) => [{ problem: messageOf(err) }]);
91
- export const check = async (dir, options = {}) => {
94
+ // A thrown message names its file relative to the package, as every problem line does.
95
+ const runRule = (rule, input) => rule.run(input).catch((err) => [{ problem: messageOf(err).replace(`${input.dir}/`, '') }]);
96
+ export const check = async (at, options = {}) => {
92
97
  const { rules = RULES, run = runCommand } = options;
98
+ const dir = resolve(at);
93
99
  const read = readJsonIfExists(join(dir, 'package.json')).catch(() => ({}));
94
100
  const pkg = ((await read) ?? {});
95
101
  const problems = [];
@@ -1,6 +1,5 @@
1
1
  import { readFile } from 'node:fs/promises';
2
2
  import { join, resolve } from 'node:path';
3
- import { messageOf } from '../../log/log.js';
4
3
  import { hasPostgres } from '../../manifest/parts.js';
5
4
  import { sqlFiles } from '../../store/migrations.js';
6
5
  import {} from '../check.js';
@@ -18,6 +17,9 @@ const runArgs = (dir, files) => [
18
17
  ...[STORE_IMAGE, '-c', SCRIPT, 'sh', ...files],
19
18
  ];
20
19
  const failure = (stderr = '') => {
20
+ const daemon = /^docker: (.*)$/m.exec(stderr)?.[1];
21
+ if (daemon)
22
+ return { problem: `${STORE_IMAGE} does not run: ${daemon}` };
21
23
  const [, file, line, why] = /^psql:\/tmp\/m\/([^:]+):(\d+): (?:ERROR:\s+)?(.*)$/m.exec(stderr) ?? [];
22
24
  const problem = `fails on ${STORE_IMAGE}: ${why ? `ERROR: ${why}` : stderr.trim().split('\n').pop()}`;
23
25
  return file ? { file: `migrations/${file}`, line: Number(line), problem } : { problem };
@@ -36,15 +38,12 @@ const metaCommands = async (dir, files) => {
36
38
  });
37
39
  };
38
40
  const dockerAnswers = (run) => run('docker', ['version'], { quiet: true }).then(({ code }) => code === 0, () => false);
39
- const migrationFiles = (dir) => sqlFiles(join(dir, 'migrations')).catch((err) => {
40
- throw new Error(messageOf(err).replace(join(dir, 'migrations'), 'migrations'));
41
- });
42
41
  export const store = {
43
42
  name: 'store',
44
43
  run: async ({ dir, pkg, run, requireDocker }) => {
45
44
  if (!hasPostgres(pkg))
46
45
  return { skipped: 'no store' };
47
- const files = await migrationFiles(dir);
46
+ const files = await sqlFiles(join(dir, 'migrations'));
48
47
  const meta = await metaCommands(dir, files);
49
48
  if (files.length === 0 || meta.length > 0)
50
49
  return meta;
@@ -14,13 +14,15 @@ const routeKeys = ({ path, stack }) => [path].flat().flatMap((one) => {
14
14
  // router.all names no method, so it has no operation to compare.
15
15
  return stack.flatMap(({ method }) => (method ? [`${method.toUpperCase()} ${at}`] : []));
16
16
  });
17
- const stackOf = (router) => router?.stack;
18
17
  // Express routes (of a Router or an express() app) as OpenAPI keys; a sub-router hides its routes, so it never matches one.
19
- const servedRoutes = (router) => (stackOf(router) ?? stackOf(router?.router) ?? []).flatMap(({ name, route, handle }) => {
20
- if (route)
21
- return routeKeys(route);
22
- return handle?.stack ? [`${name} sub-router`] : [];
23
- });
18
+ const servedRoutes = (app) => {
19
+ const { stack, router } = (app ?? {});
20
+ return (stack ?? router?.stack ?? []).flatMap(({ name, route, handle }) => {
21
+ if (route)
22
+ return routeKeys(route);
23
+ return handle?.stack ? [`${name} sub-router`] : [];
24
+ });
25
+ };
24
26
  const METHODS = new Set(['get', 'put', 'post', 'delete', 'options', 'head', 'patch', 'trace']);
25
27
  const operations = (doc) => Object.entries((doc['paths'] ?? {})).flatMap(([path, ops]) => Object.entries(ops ?? {})
26
28
  .filter(([method]) => METHODS.has(method))
@@ -33,8 +35,7 @@ const apiProblems = (doc, routes, version) => {
33
35
  const served = new Set([`info.version ${version}`, ...routes]);
34
36
  const info = doc['info']?.['version'];
35
37
  const documented = new Set([`info.version ${String(info)}`, ...ops.map(([key]) => key)]);
36
- const diff = [...served].filter((k) => !documented.has(k));
37
- diff.push(...[...documented].filter((k) => !served.has(k)));
38
+ const diff = [...served, ...documented].filter((k) => !served.has(k) || !documented.has(k));
38
39
  const open = ops.filter(([, op]) => !declaresAccess(op['security'] ?? doc['security']));
39
40
  return [
40
41
  ...(diff.length > 0 ? [`differs from the generated contract (${diff.join(', ')})`] : []),
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@agentage/extension",
3
- "version": "0.4.0",
3
+ "version": "0.4.1",
4
4
  "description": "The agentage extension kit: create(ctx) per part (pages, ui, api, mcp, worker, cli), the store, /health, discovery, the envelope, and the build tool with up and token",
5
5
  "type": "module",
6
6
  "license": "MIT",