@acid-cat/cli 0.8.0 → 0.10.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.
@@ -2,7 +2,7 @@
2
2
  var fixture_acidcat_default = {
3
3
  acidcat: "0.1",
4
4
  title: "AcidCat conformance fixture",
5
- description: "One capability for each behavior that acid-cat conform tests.",
5
+ description: "One capability or event for each behavior that acid-cat conform tests.",
6
6
  targets: {
7
7
  "api": { "bindings": ["http"] },
8
8
  "app": { "bindings": ["typescript", "webmcp"] },
@@ -167,6 +167,28 @@ var fixture_acidcat_default = {
167
167
  "type": "object"
168
168
  }
169
169
  },
170
+ "fixture.signal": {
171
+ "description": "Give an occurrence of fixture.watch to each subscription that watches the key. Return the number of these subscriptions.",
172
+ "audience": [
173
+ "app",
174
+ "mcp",
175
+ "cli",
176
+ "api"
177
+ ],
178
+ "input": {
179
+ "properties": {
180
+ "key": { "type": "string" },
181
+ "value": { "type": "string" }
182
+ },
183
+ "required": ["key", "value"],
184
+ "type": "object"
185
+ },
186
+ "output": {
187
+ "properties": { "delivered": { "type": "integer" } },
188
+ "required": ["delivered"],
189
+ "type": "object"
190
+ }
191
+ },
170
192
  "fixture.switch.set": {
171
193
  "description": "Turn the switch on or off. The switch controls the availability of fixture.toggle.",
172
194
  "audience": [
@@ -193,6 +215,100 @@ var fixture_acidcat_default = {
193
215
  "properties": {},
194
216
  "type": "object"
195
217
  }
218
+ },
219
+ "fixture.watching": {
220
+ "description": "Return the keys of the subscriptions to fixture.watch that are active now.",
221
+ "audience": [
222
+ "app",
223
+ "mcp",
224
+ "cli",
225
+ "api"
226
+ ],
227
+ "output": {
228
+ "properties": { "keys": {
229
+ "items": { "type": "string" },
230
+ "type": "array"
231
+ } },
232
+ "required": ["keys"],
233
+ "type": "object"
234
+ },
235
+ "readOnly": true
236
+ }
237
+ },
238
+ events: {
239
+ "fixture.broken": {
240
+ "description": "Give the occurrence n = 1, then fail with a defect.",
241
+ "audience": [
242
+ "app",
243
+ "mcp",
244
+ "cli",
245
+ "api"
246
+ ],
247
+ "payload": {
248
+ "properties": { "n": { "type": "integer" } },
249
+ "required": ["n"],
250
+ "type": "object"
251
+ }
252
+ },
253
+ "fixture.secret": {
254
+ "description": "Only the app target can follow this event. It never gives an occurrence.",
255
+ "audience": ["app"]
256
+ },
257
+ "fixture.ticks": {
258
+ "description": "Give the occurrences n = 1 to count, with the eventId and the cursor String(n), then end. With a cursor c, start at c + 1. With a gap g, the occurrence n = g has truncated: true, as if the source skipped occurrences before it. The key changes nothing: it gives the subscriptions of each test their own input.",
259
+ "audience": [
260
+ "app",
261
+ "mcp",
262
+ "cli",
263
+ "api"
264
+ ],
265
+ "input": {
266
+ "properties": {
267
+ "count": {
268
+ "maximum": 100,
269
+ "minimum": 0,
270
+ "type": "integer"
271
+ },
272
+ "gap": {
273
+ "maximum": 100,
274
+ "minimum": 1,
275
+ "type": "integer"
276
+ },
277
+ "key": { "type": "string" }
278
+ },
279
+ "required": ["count"],
280
+ "type": "object"
281
+ },
282
+ "payload": {
283
+ "properties": { "n": { "type": "integer" } },
284
+ "required": ["n"],
285
+ "type": "object"
286
+ }
287
+ },
288
+ "fixture.watch": {
289
+ "description": "Give an occurrence each time that fixture.signal gets the key. Never end. With a marker, write a file at that path when the subscription starts.",
290
+ "audience": [
291
+ "app",
292
+ "mcp",
293
+ "cli",
294
+ "api"
295
+ ],
296
+ "input": {
297
+ "properties": {
298
+ "key": { "type": "string" },
299
+ "marker": { "type": "string" }
300
+ },
301
+ "required": ["key"],
302
+ "type": "object"
303
+ },
304
+ "payload": {
305
+ "properties": {
306
+ "key": { "type": "string" },
307
+ "value": { "type": "string" }
308
+ },
309
+ "required": ["key", "value"],
310
+ "type": "object"
311
+ }
196
312
  }
197
313
  }
198
314
  };
@@ -1,4 +1,4 @@
1
1
  //#region package.json
2
- var version = "0.8.0";
2
+ var version = "0.10.0";
3
3
  //#endregion
4
4
  export { version };
package/dist/main.js CHANGED
@@ -70,29 +70,78 @@ const viewCommand = Command.make("view", {
70
70
  const url = (name, description) => Flag.String(name).pipe(Flag.withDescription(description), Flag.optional);
71
71
  const conformCommand = Command.make("conform", {
72
72
  mcp: url("mcp", "The URL of the MCP server"),
73
+ mcpHandshake: Flag.Boolean("mcp-handshake").pipe(Flag.withDescription("Check the initialize handshake of MCP revision 2025-11-25 through --mcp too"), Flag.withDefault(false)),
74
+ mcpStdio: url("mcp-stdio", "The command that starts the MCP server through standard input and output, for example \"node fixture-mcp-stdio.ts\""),
73
75
  http: url("http", "The base URL of the HTTP API, before /capabilities"),
74
76
  cli: url("cli", "The command that starts the command line, for example \"node fixture-cli.ts\""),
75
77
  webmcp: url("webmcp", "The URL of a web page that serves the app target. It runs in Chromium, through Playwright"),
78
+ pollBuffer: Flag.Int("poll-buffer").pipe(Flag.withDescription("The number of occurrences that a poll lease of the deployment keeps, from 1 to 97. With it, conform tests a cursor older than the buffer"), Flag.optional),
79
+ pollLeaseMs: Flag.Int("poll-lease-ms").pipe(Flag.withDescription("The lease time of the deployment, in milliseconds, shorter than the deadline. With it, conform tests that a lease with no poll ends"), Flag.optional),
80
+ webhookPrincipal: url("webhook-principal", "Test webhook delivery through --mcp. The deployment gives this principal to a request with the header Authorization: Bearer <principal>"),
81
+ webhookCert: url("webhook-cert", "The PEM file of the certificate of the HTTPS receiver of the webhook tests. It must be valid for 127.0.0.1, and the deployment must trust it"),
82
+ webhookKey: url("webhook-key", "The PEM file of the private key of --webhook-cert"),
83
+ webhookOtherPrincipal: url("webhook-other-principal", "A second principal that the deployment gives to a request with Authorization: Bearer <principal>, for the check of the limit"),
84
+ webhookMaxTtlMs: Flag.Int("webhook-max-ttl-ms").pipe(Flag.withDescription("The maximum lifetime of a webhook subscription in the deployment. The default is 3600000"), Flag.optional),
85
+ webhookMinTtlMs: Flag.Int("webhook-min-ttl-ms").pipe(Flag.withDescription("The minimum lifetime of a webhook subscription in the deployment, shorter than the deadline. With it, conform tests that a subscription with no renewal ends"), Flag.optional),
86
+ webhookMaxSubscriptions: Flag.Int("webhook-max-subscriptions").pipe(Flag.withDescription("The maximum number of webhook subscriptions of one principal in the deployment. With it, conform tests the limit"), Flag.optional),
76
87
  maximumBytes: Flag.Int("max-bytes").pipe(Flag.withDescription("The body limit of the remote bindings"), Flag.withDefault(1048576)),
77
88
  deadline: Flag.Int("deadline").pipe(Flag.withDescription("The time, in milliseconds, that each check has to settle. A check that does not settle in this time fails"), Flag.withDefault(DEADLINE))
78
89
  }, Effect.fn(function* (options) {
79
90
  const value = (option) => Option.isSome(option) ? option.value : void 0;
80
- const command = value(options.cli)?.split(/\s+/).filter((word) => word !== "");
91
+ const words = (option) => value(option)?.split(/\s+/).filter((word) => word !== "");
92
+ const optionalInt = (option) => Option.isSome(option) ? option.value : void 0;
93
+ const fs = yield* FileSystem.FileSystem;
94
+ const given = [
95
+ options.webhookPrincipal,
96
+ options.webhookCert,
97
+ options.webhookKey
98
+ ].filter(Option.isSome).length;
99
+ if (given !== 0 && given !== 3) {
100
+ yield* Console.error("Give --webhook-principal, --webhook-cert and --webhook-key together.");
101
+ return yield* fail;
102
+ }
103
+ const settings = [
104
+ Option.isSome(options.webhookOtherPrincipal),
105
+ Option.isSome(options.webhookMaxTtlMs),
106
+ Option.isSome(options.webhookMinTtlMs),
107
+ Option.isSome(options.webhookMaxSubscriptions)
108
+ ];
109
+ if (given === 0 && settings.includes(true)) {
110
+ yield* Console.error("The other --webhook-* options need --webhook-principal, --webhook-cert and --webhook-key.");
111
+ return yield* fail;
112
+ }
113
+ const webhook = Option.isSome(options.webhookPrincipal) && Option.isSome(options.webhookCert) && Option.isSome(options.webhookKey) ? {
114
+ principal: options.webhookPrincipal.value,
115
+ cert: yield* fs.readFileString(options.webhookCert.value),
116
+ key: yield* fs.readFileString(options.webhookKey.value),
117
+ otherPrincipal: value(options.webhookOtherPrincipal),
118
+ maxTtlMs: optionalInt(options.webhookMaxTtlMs),
119
+ minTtlMs: optionalInt(options.webhookMinTtlMs),
120
+ maxSubscriptions: optionalInt(options.webhookMaxSubscriptions)
121
+ } : void 0;
81
122
  const results = yield* Effect.promise(() => conform({
82
123
  mcp: value(options.mcp),
124
+ mcpHandshake: options.mcpHandshake,
125
+ mcpStdio: words(options.mcpStdio),
83
126
  http: value(options.http),
84
- cli: command,
127
+ cli: words(options.cli),
85
128
  webmcp: value(options.webmcp),
129
+ poll: {
130
+ bufferLimit: optionalInt(options.pollBuffer),
131
+ leaseMs: optionalInt(options.pollLeaseMs)
132
+ },
133
+ webhook,
86
134
  maximumBytes: options.maximumBytes,
87
135
  deadline: options.deadline
88
136
  }));
89
137
  if (results.length === 0) {
90
- yield* Console.error("Give at least one binding: --mcp, --http, --cli or --webmcp.");
138
+ yield* Console.error("Give at least one binding: --mcp, --mcp-stdio, --http, --cli or --webmcp.");
91
139
  return yield* fail;
92
140
  }
93
- for (const result of results) yield* Console.log(`${result.passed ? "✓" : "✗"} ${result.binding}: ${result.name} (${result.rule})${result.detail ? `\n ${result.detail}` : ""}`);
141
+ for (const result of results) yield* Console.log(`${result.warning === true ? "!" : result.passed ? "✓" : "✗"} ${result.binding}: ${result.name} (${result.rule})${result.detail ? `\n ${result.detail}` : ""}`);
94
142
  const failed = results.filter((result) => !result.passed).length;
95
- yield* Console.log(`\n${results.length - failed} passed, ${failed} failed.`);
143
+ const warnings = results.filter((result) => result.warning === true).length;
144
+ yield* Console.log(`\n${results.length - failed} passed, ${failed} failed${warnings === 0 ? "" : `, ${warnings} with a warning`}.`);
96
145
  if (failed > 0) return yield* fail;
97
146
  })).pipe(Command.withDescription("Test a running implementation of the conformance fixture through its bindings."));
98
147
  Command.make("acid-cat").pipe(Command.withDescription("Check, format and view AcidCat catalogs, and test implementations."), Command.withSubcommands([
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@acid-cat/cli",
3
- "version": "0.8.0",
3
+ "version": "0.10.0",
4
4
  "description": "The AcidCat command line: check, format and view catalogs, and test implementations.",
5
5
  "keywords": [
6
6
  "acidcat",
@@ -37,12 +37,12 @@
37
37
  "access": "public"
38
38
  },
39
39
  "dependencies": {
40
- "@acid-cat/catalog": "0.8.0",
41
- "@acid-cat/spec-ts": "0.8.0",
42
- "@effect/platform-node": "4.0.6",
43
- "@effect/platform-node-shared": "4.0.6",
40
+ "@acid-cat/catalog": "0.10.0",
41
+ "@acid-cat/spec-ts": "0.10.0",
42
+ "@effect/platform-node": "^4.1.1",
43
+ "@effect/platform-node-shared": "^4.1.1",
44
44
  "@modelcontextprotocol/client": "2.3.1",
45
- "effect": "4.0.6",
45
+ "effect": "^4.1.1",
46
46
  "fast-check": "4.10.2"
47
47
  },
48
48
  "peerDependencies": {