@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 +16 -0
- package/dist/tool/check.d.ts +5 -5
- package/dist/tool/check.js +31 -25
- package/dist/tool/rules/store.js +4 -5
- package/dist/tool/rules/surface.js +9 -8
- package/package.json +1 -1
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.
|
package/dist/tool/check.d.ts
CHANGED
|
@@ -5,16 +5,16 @@ export interface CheckOptions {
|
|
|
5
5
|
requireDocker?: boolean;
|
|
6
6
|
rules?: readonly CheckRule[];
|
|
7
7
|
}
|
|
8
|
-
export
|
|
8
|
+
export type CheckInput = CheckOptions & {
|
|
9
9
|
dir: string;
|
|
10
10
|
pkg: ExtensionPackage;
|
|
11
11
|
run: Run;
|
|
12
|
-
}
|
|
13
|
-
export
|
|
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: (
|
|
36
|
+
export declare const check: (at: string, options?: CheckOptions) => Promise<CheckReport>;
|
|
37
37
|
export declare const assertConforms: (dir: string, options?: CheckOptions) => Promise<void>;
|
package/dist/tool/check.js
CHANGED
|
@@ -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'))
|
|
23
|
-
|
|
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
|
|
35
|
-
const
|
|
36
|
-
const mod = (await import(
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
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
|
|
42
|
-
|
|
43
|
-
|
|
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
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
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 =
|
|
59
|
-
return
|
|
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
|
-
|
|
91
|
-
|
|
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 = [];
|
package/dist/tool/rules/store.js
CHANGED
|
@@ -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
|
|
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 = (
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
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.
|
|
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",
|