@acid-cat/cli 0.0.0-stage → 0.1.0

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.
@@ -0,0 +1,28 @@
1
+ import type { CatalogDocument } from "@acid-cat/spec-ts";
2
+ export interface Check {
3
+ readonly binding: string;
4
+ readonly name: string;
5
+ /** The part of the specification that the check tests, for example `spec/bindings/mcp.md#tools`. */
6
+ readonly rule: string;
7
+ readonly passed: boolean;
8
+ readonly detail?: string | undefined;
9
+ }
10
+ /** The conformance fixture: the catalog that each implementation serves. */
11
+ export declare const fixture: CatalogDocument;
12
+ /** The ids of the capabilities in the view of a target of the fixture. */
13
+ export declare const idsOf: (target: string) => string[];
14
+ /** Collects checks for one binding. */
15
+ export declare class Checks {
16
+ readonly results: Check[];
17
+ readonly binding: string;
18
+ constructor(binding: string);
19
+ run(name: string, rule: string, body: () => Promise<void> | void): Promise<void>;
20
+ }
21
+ /** Fail a check unless `condition` is true. */
22
+ export declare function expect(condition: unknown, message: string): asserts condition;
23
+ /** Fail a check unless the two values have the same JSON. */
24
+ export declare function expectEqual(actual: unknown, expected: unknown, what: string): void;
25
+ /** A sorted copy of a list. */
26
+ export declare const sorted: (values: Iterable<string>) => string[];
27
+ /** An origin that no deployment allows. */
28
+ export declare const FORBIDDEN_ORIGIN = "https://not-allowed.acid-cat.invalid";
@@ -0,0 +1,2 @@
1
+ import { Checks } from "./check.js";
2
+ export declare function checkCli(command: readonly string[]): Promise<Checks>;
@@ -0,0 +1,5 @@
1
+ /** Conformance checks for the HTTP binding (spec/bindings/http.md). */
2
+ import { Checks } from "./check.js";
3
+ export declare function checkHttp(base: string, options: {
4
+ readonly maximumBytes: number;
5
+ }): Promise<Checks>;
@@ -0,0 +1,29 @@
1
+ /**
2
+ * `acid-cat conform`: test a running implementation through its bindings (spec/conformance.md). The implementation
3
+ * serves the conformance fixture (conformance/fixture.acidcat.json), with its switch on.
4
+ */
5
+ import type { Check } from "./check.js";
6
+ import type { WebMcpInstall } from "./webmcp.js";
7
+ export type { Check } from "./check.js";
8
+ export { fixture } from "./check.js";
9
+ export { serve } from "./serve.js";
10
+ export type { Served, WebHandler } from "./serve.js";
11
+ export { createTestHost } from "./webmcp.js";
12
+ export type { TestHost, WebMcpInstall } from "./webmcp.js";
13
+ export interface ConformOptions {
14
+ /** The URL of the MCP server, for example `http://localhost:8787/mcp`. */
15
+ readonly mcp?: string | undefined;
16
+ /** The base URL of the HTTP API, before `/capabilities`. */
17
+ readonly http?: string | undefined;
18
+ /** The command that starts the command line, for example `["node", "fixture-cli.ts"]`. */
19
+ readonly cli?: readonly string[] | undefined;
20
+ /**
21
+ * The WebMCP binding: the URL of a page that serves the `app` target, or a function that installs the implementation
22
+ * on a host in the same process. A page runs in Chromium, through Playwright.
23
+ */
24
+ readonly webmcp?: string | WebMcpInstall | undefined;
25
+ /** The body limit of the remote bindings, in bytes. The default is 1 MiB. */
26
+ readonly maximumBytes?: number | undefined;
27
+ }
28
+ /** Run all checks for the bindings that the options give. */
29
+ export declare function conform(options: ConformOptions): Promise<readonly Check[]>;
@@ -0,0 +1,11 @@
1
+ /**
2
+ * Conformance checks for the MCP binding (spec/bindings/mcp.md). The protocol checks use the official MCP client,
3
+ * with revision 2026-07-28. The rejection checks send requests that a client never sends.
4
+ */
5
+ import { Client } from "@modelcontextprotocol/client";
6
+ import { Checks } from "./check.js";
7
+ /** A connected MCP client. */
8
+ export declare function mcpClient(url: string): Promise<Client>;
9
+ export declare function checkMcp(url: string, options: {
10
+ readonly maximumBytes: number;
11
+ }): Promise<Checks>;
@@ -0,0 +1,14 @@
1
+ /**
2
+ * Property-based checks for the guarantees of spec/invocation.md#cancellation-and-concurrency, through each remote
3
+ * binding. fast-check makes random sets of concurrent calls and cancellations.
4
+ */
5
+ import type { Outcome } from "@acid-cat/spec-ts";
6
+ import { Checks } from "./check.js";
7
+ /** A call through one binding. A cancelled call can reject. */
8
+ export type Invoke = (id: string, input: object, signal?: AbortSignal) => Promise<Outcome<unknown, unknown>>;
9
+ export declare function httpInvoke(base: string): Invoke;
10
+ export declare function mcpInvoke(url: string): Promise<{
11
+ readonly invoke: Invoke;
12
+ close(): Promise<void>;
13
+ }>;
14
+ export declare function checkProperties(binding: string, invoke: Invoke): Promise<Checks>;
@@ -0,0 +1,10 @@
1
+ export interface Served {
2
+ /** The base URL, for example `http://127.0.0.1:40123`. */
3
+ readonly url: string;
4
+ close(): Promise<void>;
5
+ }
6
+ export type WebHandler = (request: Request) => Promise<Response>;
7
+ /** Serve handlers by path prefix, for example `{ "/mcp": mcp, "/api": http }`. */
8
+ export declare function serve(routes: {
9
+ readonly [prefix: string]: WebHandler;
10
+ }): Promise<Served>;
@@ -0,0 +1,51 @@
1
+ import { Checks } from "./check.js";
2
+ /** A tool as the host records it. */
3
+ export interface RecordedTool {
4
+ readonly name: string;
5
+ readonly description: string;
6
+ readonly inputSchema: unknown;
7
+ }
8
+ /** What a call to a tool gives: the value that `execute` resolves with, or the error that it rejects with. */
9
+ export type ToolCall = {
10
+ readonly resolved: unknown;
11
+ } | {
12
+ readonly rejected: {
13
+ readonly name: string;
14
+ readonly kind: unknown;
15
+ readonly message: string;
16
+ };
17
+ } | {
18
+ readonly missing: true;
19
+ };
20
+ /** The host that the checks give to the page. */
21
+ export interface TestHost {
22
+ readonly modelContext: {
23
+ registerTool(tool: object, options: {
24
+ readonly signal: AbortSignal;
25
+ }): void;
26
+ };
27
+ tools(): RecordedTool[];
28
+ /** Call a tool. If `cancelAfter` is given, abort the signal of the call after that many milliseconds. */
29
+ call(name: string, input: unknown, cancelAfter?: number): Promise<ToolCall>;
30
+ }
31
+ /**
32
+ * Make a test host. The function uses no imports and no outer variables, because the browser driver sends its
33
+ * source text to the page.
34
+ */
35
+ export declare function createTestHost(): TestHost;
36
+ /** The view from the checks: the tools of the page, and calls to them. */
37
+ export interface WebMcpDriver {
38
+ tools(): Promise<RecordedTool[]>;
39
+ call(name: string, input: unknown, cancelAfter?: number): Promise<ToolCall>;
40
+ close(): Promise<void>;
41
+ }
42
+ /**
43
+ * Install the implementation on a host in the same process. Return a function that removes it, or nothing. The
44
+ * implementation serves the `app` target of the fixture, with its switch on.
45
+ */
46
+ export type WebMcpInstall = (modelContext: TestHost["modelContext"]) => void | (() => void | Promise<void>) | Promise<void | (() => void | Promise<void>)>;
47
+ /** A driver for an implementation in the same process. Values pass through JSON, as they do from a page. */
48
+ export declare function processDriver(install: WebMcpInstall): Promise<WebMcpDriver>;
49
+ /** A driver for a page in Chromium, through Playwright. The page loads with the test host as its `modelContext`. */
50
+ export declare function browserDriver(url: string): Promise<WebMcpDriver>;
51
+ export declare function checkWebMcp(driver: WebMcpDriver): Promise<Checks>;
@@ -0,0 +1,5 @@
1
+ /** The library of `@acid-cat/cli`: catalog checks, and the conformance tests. */
2
+ export { bundle, check, viewOf } from "./catalog.js";
3
+ export type { Catalog, Report } from "./catalog.js";
4
+ export { conform, fixture, serve } from "./conform/index.js";
5
+ export type { Check, ConformOptions, Served, WebHandler } from "./conform/index.js";
package/dist/index.js ADDED
@@ -0,0 +1,16 @@
1
+ import {
2
+ bundle,
3
+ check,
4
+ conform,
5
+ fixture,
6
+ serve,
7
+ viewOf
8
+ } from "./chunks/chunk-VZ6KDZEL.js";
9
+ export {
10
+ bundle,
11
+ check,
12
+ conform,
13
+ fixture,
14
+ serve,
15
+ viewOf
16
+ };
package/dist/main.d.ts ADDED
@@ -0,0 +1,2 @@
1
+ #!/usr/bin/env node
2
+ export {};
package/dist/main.js ADDED
@@ -0,0 +1,191 @@
1
+ #!/usr/bin/env node
2
+ import {
3
+ check,
4
+ conform,
5
+ viewOf
6
+ } from "./chunks/chunk-VZ6KDZEL.js";
7
+
8
+ // src/main.ts
9
+ import { NodeRuntime, NodeServices } from "@effect/platform-node";
10
+ import { canonicalText } from "@acid-cat/catalog";
11
+ import { Console, Effect, FileSystem, Option } from "effect";
12
+ import { Argument, Command, Flag } from "effect/cli";
13
+
14
+ // package.json
15
+ var package_default = {
16
+ name: "@acid-cat/cli",
17
+ version: "0.1.0",
18
+ description: "The AcidCat command line: check, format and view catalogs, and test implementations.",
19
+ keywords: [
20
+ "acidcat",
21
+ "cli",
22
+ "conformance"
23
+ ],
24
+ license: "MIT",
25
+ repository: {
26
+ type: "git",
27
+ url: "git+https://github.com/Hebilicious/acidcat.git",
28
+ directory: "packages/cli"
29
+ },
30
+ homepage: "https://github.com/Hebilicious/acidcat/tree/main/packages/cli#readme",
31
+ bugs: "https://github.com/Hebilicious/acidcat/issues",
32
+ type: "module",
33
+ engines: {
34
+ node: ">=24"
35
+ },
36
+ files: [
37
+ "dist",
38
+ "conformance/fixture.acidcat.json"
39
+ ],
40
+ bin: {
41
+ "acid-cat": "./src/main.ts"
42
+ },
43
+ exports: {
44
+ ".": "./src/index.ts",
45
+ "./conformance/fixture.acidcat.json": "./conformance/fixture.acidcat.json"
46
+ },
47
+ publishConfig: {
48
+ access: "public",
49
+ exports: {
50
+ ".": {
51
+ types: "./dist/index.d.ts",
52
+ default: "./dist/index.js"
53
+ },
54
+ "./conformance/fixture.acidcat.json": "./conformance/fixture.acidcat.json"
55
+ },
56
+ bin: {
57
+ "acid-cat": "./dist/main.js"
58
+ }
59
+ },
60
+ dependencies: {
61
+ "@acid-cat/catalog": "workspace:*",
62
+ "@acid-cat/spec-ts": "workspace:*",
63
+ "@effect/platform-node": "catalog:",
64
+ "@modelcontextprotocol/client": "catalog:",
65
+ effect: "catalog:",
66
+ "fast-check": "catalog:"
67
+ },
68
+ peerDependencies: {
69
+ playwright: "catalog:peers"
70
+ },
71
+ peerDependenciesMeta: {
72
+ playwright: {
73
+ optional: true
74
+ }
75
+ },
76
+ devDependencies: {
77
+ "@types/node": "catalog:",
78
+ playwright: "catalog:",
79
+ typescript: "catalog:"
80
+ }
81
+ };
82
+
83
+ // src/main.ts
84
+ var readCatalog = Effect.fn("readCatalog")(function* (file) {
85
+ const fs = yield* FileSystem.FileSystem;
86
+ const text = yield* fs.readFileString(file);
87
+ return { text, document: JSON.parse(text) };
88
+ });
89
+ var fail = Effect.sync(() => {
90
+ process.exitCode = 1;
91
+ });
92
+ var files = Argument.String("catalog").pipe(
93
+ Argument.withDescription("A catalog file, such as acidcat.catalog.json"),
94
+ Argument.variadic({ min: 1 })
95
+ );
96
+ var checkCommand = Command.make(
97
+ "check",
98
+ {
99
+ files,
100
+ strict: Flag.Boolean("strict").pipe(Flag.withDescription("Fail on warnings too"), Flag.withDefault(false))
101
+ },
102
+ Effect.fn(function* ({ files: files2, strict }) {
103
+ let failed = false;
104
+ for (const file of files2) {
105
+ const { text, document } = yield* readCatalog(file);
106
+ const report = check(document, text);
107
+ for (const error of report.errors) yield* Console.error(`${file}: error: ${error}`);
108
+ for (const warning of report.warnings) yield* Console.error(`${file}: warning: ${warning}`);
109
+ failed ||= report.errors.length > 0 || strict && report.warnings.length > 0;
110
+ }
111
+ if (failed) return yield* fail;
112
+ yield* Console.log(files2.length === 1 ? "The catalog conforms." : "Every catalog conforms.");
113
+ })
114
+ ).pipe(Command.withDescription("Check that catalogs conform to the specification."));
115
+ var formatCommand = Command.make(
116
+ "format",
117
+ {
118
+ files,
119
+ write: Flag.Boolean("write").pipe(Flag.withDescription("Rewrite each file instead of printing it"), Flag.withDefault(false))
120
+ },
121
+ Effect.fn(function* ({ files: files2, write }) {
122
+ const fs = yield* FileSystem.FileSystem;
123
+ for (const file of files2) {
124
+ const { document } = yield* readCatalog(file);
125
+ const text = canonicalText(document);
126
+ if (write) yield* fs.writeFileString(file, text);
127
+ else yield* Effect.sync(() => process.stdout.write(text));
128
+ }
129
+ })
130
+ ).pipe(Command.withDescription("Write catalogs in canonical form."));
131
+ var viewCommand = Command.make(
132
+ "view",
133
+ {
134
+ file: Argument.String("catalog").pipe(Argument.withDescription("A catalog file")),
135
+ target: Argument.String("target").pipe(Argument.withDescription("The name of a target in the catalog"))
136
+ },
137
+ Effect.fn(function* ({ file, target }) {
138
+ const { document } = yield* readCatalog(file);
139
+ const catalog = document;
140
+ if (!(target in catalog.targets)) {
141
+ yield* Console.error(`${file}: error: there is no target ${target}`);
142
+ return yield* fail;
143
+ }
144
+ yield* Effect.sync(() => process.stdout.write(canonicalText(viewOf(catalog, target))));
145
+ })
146
+ ).pipe(Command.withDescription("Print the view of one target."));
147
+ var url = (name, description) => Flag.String(name).pipe(Flag.withDescription(description), Flag.optional);
148
+ var conformCommand = Command.make(
149
+ "conform",
150
+ {
151
+ mcp: url("mcp", "The URL of the MCP server"),
152
+ http: url("http", "The base URL of the HTTP API, before /capabilities"),
153
+ cli: url("cli", 'The command that starts the command line, for example "node fixture-cli.ts"'),
154
+ webmcp: url("webmcp", "The URL of a web page that serves the app target. It runs in Chromium, through Playwright"),
155
+ maximumBytes: Flag.Int("max-bytes").pipe(Flag.withDescription("The body limit of the remote bindings"), Flag.withDefault(1048576))
156
+ },
157
+ Effect.fn(function* (options) {
158
+ const value = (option) => Option.isSome(option) ? option.value : void 0;
159
+ const command = value(options.cli)?.split(/\s+/).filter((word) => word !== "");
160
+ const results = yield* Effect.promise(
161
+ () => conform({
162
+ mcp: value(options.mcp),
163
+ http: value(options.http),
164
+ cli: command,
165
+ webmcp: value(options.webmcp),
166
+ maximumBytes: options.maximumBytes
167
+ })
168
+ );
169
+ if (results.length === 0) {
170
+ yield* Console.error("Give at least one binding: --mcp, --http, --cli or --webmcp.");
171
+ return yield* fail;
172
+ }
173
+ for (const result of results) {
174
+ yield* Console.log(`${result.passed ? "\u2713" : "\u2717"} ${result.binding}: ${result.name} (${result.rule})${result.detail ? `
175
+ ${result.detail}` : ""}`);
176
+ }
177
+ const failed = results.filter((result) => !result.passed).length;
178
+ yield* Console.log(`
179
+ ${results.length - failed} passed, ${failed} failed.`);
180
+ if (failed > 0) return yield* fail;
181
+ })
182
+ ).pipe(Command.withDescription("Test a running implementation of the conformance fixture through its bindings."));
183
+ var acidCat = Command.make("acid-cat").pipe(
184
+ Command.withDescription("Check, format and view AcidCat catalogs, and test implementations."),
185
+ Command.withSubcommands([checkCommand, formatCommand, viewCommand, conformCommand])
186
+ );
187
+ acidCat.pipe(
188
+ Command.run({ version: package_default.version }),
189
+ Effect.provide(NodeServices.layer),
190
+ NodeRuntime.runMain
191
+ );
package/package.json CHANGED
@@ -1,6 +1,60 @@
1
1
  {
2
2
  "name": "@acid-cat/cli",
3
- "version": "0.0.0-stage",
4
- "stub": true,
5
- "description": "Temporary package placeholder for staged publishing"
3
+ "version": "0.1.0",
4
+ "description": "The AcidCat command line: check, format and view catalogs, and test implementations.",
5
+ "keywords": [
6
+ "acidcat",
7
+ "cli",
8
+ "conformance"
9
+ ],
10
+ "license": "MIT",
11
+ "repository": {
12
+ "type": "git",
13
+ "url": "git+https://github.com/Hebilicious/acidcat.git",
14
+ "directory": "packages/cli"
15
+ },
16
+ "homepage": "https://github.com/Hebilicious/acidcat/tree/main/packages/cli#readme",
17
+ "bugs": "https://github.com/Hebilicious/acidcat/issues",
18
+ "type": "module",
19
+ "engines": {
20
+ "node": ">=24"
21
+ },
22
+ "files": [
23
+ "dist",
24
+ "conformance/fixture.acidcat.json"
25
+ ],
26
+ "bin": {
27
+ "acid-cat": "./dist/main.js"
28
+ },
29
+ "exports": {
30
+ ".": {
31
+ "types": "./dist/index.d.ts",
32
+ "default": "./dist/index.js"
33
+ },
34
+ "./conformance/fixture.acidcat.json": "./conformance/fixture.acidcat.json"
35
+ },
36
+ "publishConfig": {
37
+ "access": "public"
38
+ },
39
+ "dependencies": {
40
+ "@acid-cat/catalog": "0.1.0",
41
+ "@acid-cat/spec-ts": "0.1.0",
42
+ "@effect/platform-node": "4.0.3",
43
+ "@modelcontextprotocol/client": "2.3.1",
44
+ "effect": "4.0.3",
45
+ "fast-check": "4.10.2"
46
+ },
47
+ "peerDependencies": {
48
+ "playwright": "^1.64.0"
49
+ },
50
+ "peerDependenciesMeta": {
51
+ "playwright": {
52
+ "optional": true
53
+ }
54
+ },
55
+ "devDependencies": {
56
+ "@types/node": "26.6.5",
57
+ "playwright": "1.64.0",
58
+ "typescript": "7.0.2"
59
+ }
6
60
  }