@aventara/client 1.0.0-rc.12 → 1.0.0-rc.13

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/CHANGELOG.md CHANGED
@@ -1,5 +1,84 @@
1
1
  # @aventara/client
2
2
 
3
+ ## 1.0.0-rc.13
4
+
5
+ ### Minor Changes
6
+
7
+ - f3b291f: `transactionsStyle` takes a third value, `"server"`: the server still opens transactions, and callers send no plans.
8
+ A forced call (`forceTransaction`), a write the policy checks after it runs and `ctx.$transaction` run in a
9
+ transaction as under `"interactive"`, while the layer has no `POST /_transactions` route, a plan sent to it answers as
10
+ that absent route (`404 A2002`, one `V1006` at `["operations"]`), `framework.transaction` refuses a plan of that
11
+ origin the same way, the generated client has no `tx` or `transaction`, and an MCP agent layer under it has no
12
+ `transaction` tool. `client: { transactionsStyle: "server" }` keeps forced writes atomic without letting remote
13
+ callers send multi-step plans. `forceTransaction` under `"none"` is still a startup error; under `"server"` it is
14
+ allowed. Like `"interactive"`, `"server"` needs an adapter with transactions (`COMPILER_UNAVAILABLE_CAPABILITY`
15
+ otherwise). The Contract's `transactions` carries the value, so a layer that states it gets a new Contract hash.
16
+
17
+ - 73aa797: A generated client takes three hooks, one function each: `new AvClient({ onCall, onSuccess, onError })`. They see
18
+ every call the client makes: an operation, a cached read, a transaction (once, whatever its steps) and a `$access`
19
+ call, as an `AvCall` (its `kind`, its `label`, the operation's `resource`, `family` and `variant` or the `$access`
20
+ verb, and its `requestId` when it sends one; never its arguments).
21
+
22
+ - `onCall(call)` runs when the call is made, before it is sent and before grouping. It may return, or resolve to,
23
+ `AvCallAdditions` for that call's request: `headers`, `credentials`, `cache`, `keepalive`, `priority` and
24
+ `referrerPolicy`. A header the client sets itself (`Content-Type`, `Accept`, the three `Aventara-*` headers), in
25
+ its canonical or lower-case spelling, and `method`, `body`, `signal`, `redirect` and `mode` are type errors, each
26
+ typed as the sentence that refuses it. At runtime such a header in any spelling, a name that is not a valid header
27
+ name and a value no request can carry (a line break, a control character, or a character outside Latin-1) are dropped with a warning (code
28
+ `AVENTARA_CLIENT_HOOK_HEADER_DROPPED`), once per header name for each `onCall`. The call's own `headers` option
29
+ wins over `onCall`'s.
30
+ - Calls share one `POST /_all` only when their additions are equal (in any order). An async `onCall` holds back only
31
+ its own call: the call is sent when it settles, with the calls queued then. Calls group when their `onCall` settles
32
+ together with equal additions; an `onCall` doing its own I/O per call sends each call alone, and one awaiting a
33
+ shared in-flight promise (cleared once it settles) keeps them in one `/_all`. A hanging `onCall` stalls only its own call, an abort settles that call at once (a `$access` call too), and a
34
+ call made from inside a hook never waits on the call that ran it.
35
+ - `onSuccess(call, result)` and `onError(call, error)` are sinks, typed as returning `void`, so any function fits
36
+ (an async one, `() => router.push("/login")`). What they return is ignored, an async one is not awaited, and the caller gets the same value or rejection, never delayed. `onError`
37
+ sees a refusal, a validation error, a transport failure and an abort, sent alone or inside an Operation Set.
38
+ - A hook that throws or rejects is reported once as an `AventaraWarning` (code `AVENTARA_CLIENT_HOOK_THREW`, or
39
+ `console.warn` where there is no `process.emitWarning`), naming the hook and the call, and skipped. A hook's failure, a value that cannot be read included, never reaches
40
+ the call, the calls sent with it or the process.
41
+
42
+ `fetch` stays a transport swap: it must return the `Response` unread. A token every call needs now goes in `onCall`,
43
+ read through one shared in-flight promise so the calls still group; the skill and the README say so.
44
+
45
+ ### Patch Changes
46
+
47
+ - 33419de: `avclient init` now writes a client-only agent skill into the frontend, `.claude/skills/aventara-client/SKILL.md`: how the generated client calls the API, for this `@aventara/client`. The new `avclient skill` command refreshes it after an upgrade; a file you edited is kept with a warning unless you pass `--force`. The generated client still needs no `@aventara/client` at run time.
48
+
49
+ - a6cebba: `avclient` refuses an entrypoint carrying credentials with a sentence that now says to add them through `onCall`,
50
+ not a custom `fetch`.
51
+ - e6495a0: The agent skills cover more of this release. The server skill says that a `state` rejecting with a `401` or `403`
52
+ status answers `A4000` or `A4001`, that `forceTransaction` also runs under `transactionsStyle: "server"` (the way to
53
+ keep forced writes atomic without client plans), that an enum field takes no validator, and how SQLite's one
54
+ connection makes a plain operation wait for an open transaction (`AVENTARA_PRISMA_SQLITE_TRANSACTION_JOINED`, the
55
+ `driver` check). It no longer says that a `ctx` create must write a field the root's or the `application` overlay's
56
+ `compute.create` fills. The client guide, in both skills, adds `AvCall` and `AvCallAdditions` for typing a hook, that
57
+ a hanging `onCall` stalls only its own call, the `AVENTARA_CLIENT_OPTIONS_NO_DEFAULT` warning, the refusal of an
58
+ entrypoint carrying credentials, and how a client of your own computes a transaction step's `fingerprint` with
59
+ `AvProtocol.fingerprint`.
60
+
61
+ - a9260d2: The agent skills cover what an agent needs without opening `node_modules`:
62
+
63
+ - The frontend skill (`avclient skill`) now explains what a call returns (`Date`, `bigint`, `AvDecimal`, `Uint8Array`), which error class each code throws (`AvTransportError` included), `error.cause.issues` for form errors, transactions with `$ref`, and a new `reference/client-setup.md`: `framework.client.ts` (`defineClientConfig`, `env`, `AVENTARA_API_URL`, `generateAt`), regenerating after the server changed, `avclient generate --yes` in CI, and the stale-client error (`A2005`, `AvContractMismatchError`). It also links the docs for agents.
64
+ - The server skill (`aventara skill`) adds `reference/avz.md` (every `AvZ` check and Json shape builder, `AvZ.infer`) and covers the `policy` and `access` keys, MCP token claims in `ctx.$state.mcp.claims`, the Next.js `state` example, `avDehydrate`/`avHydrate` and server transactions in Next.js, testing with `createFakeAdapter` (what `execute` receives and returns), switching the database or driver, `ctx.$sql`, `ctx.$transaction` and `ctx.$transport`, the compute stage types, and the `AVENTARA_PREBUILT_STALE` fallback.
65
+ - With `@aventara/access` installed, the skill now says to write Access's own skill with `avaccess skill` (`npx avaccess skill`, or `pnpm exec avaccess skill`).
66
+ - Corrected: a write-only field is written `"read"` everywhere; a field pipe or compute stage that returns `undefined` keeps the value; a read compute may be a plain function and gets `field`, `value`, `record` and `ctx`.
67
+ - `CONFIG_FORBIDDEN_IN_OVERLAY` in the error catalog now names `managedElsewhere` as well as a field `definition`.
68
+ - Updated dependencies [6c3e25c]
69
+ - Updated dependencies [2aaa27d]
70
+ - Updated dependencies [f3b291f]
71
+ - Updated dependencies [2aaa27d]
72
+ - Updated dependencies [e4e8abd]
73
+ - Updated dependencies [e662048]
74
+ - Updated dependencies [f3b291f]
75
+ - Updated dependencies [f3b291f]
76
+ - Updated dependencies [6c3e25c]
77
+ - Updated dependencies [6c3e25c]
78
+ - Updated dependencies [a708188]
79
+ - Updated dependencies [a9260d2]
80
+ - @aventara/core@1.0.0-rc.13
81
+
3
82
  ## 1.0.0-rc.12
4
83
 
5
84
  ### Patch Changes
package/README.md CHANGED
@@ -121,8 +121,9 @@ const maybe = await avClient.User.find.first({ where }); // User | null — a mi
121
121
  ```
122
122
 
123
123
  - `avClient` is the default export: the client of the deployment the tree was generated from.
124
- `new AvClient({ entrypoint, fetch })` makes another. `entrypoint` is for a deployment serving exactly the same
125
- ClientContract (tests, SSR), and `fetch` is for a custom fetch.
124
+ `new AvClient({ entrypoint, fetch, onCall, onSuccess, onError })` makes another. `entrypoint` is for a deployment
125
+ serving exactly the same ClientContract (tests, SSR), `fetch` swaps the transport, and the three hooks see every call
126
+ (see "Hooks" below).
126
127
  - The client has one property per Resource, one per family under it, and one per advertised variant. Only what the
127
128
  ClientContract advertises exists, in the type and at runtime. A Resource named like one of the client's own members
128
129
  (`all`, `tx`, `transaction`, `then`, `constructor`, `Object.prototype`'s names) is reached as `<name>Model`, with one
@@ -165,6 +166,7 @@ const [campaigns, unread, me] = await Promise.all([
165
166
  requests.
166
167
  - Sent alone, never grouped: a call with `headers` or a `requestId` (they belong to one request), a call made with
167
168
  `{ grouping: false }`, every call of a client made with `new AvClient({ grouping: false })`, and a transaction.
169
+ Calls whose `onCall` additions differ never share a request: each set of equal additions is its own `/_all`.
168
170
  - A server without `/_all` (it answers `A2002`) gets the calls again one by one, and that client stops grouping.
169
171
 
170
172
  ### Aborting a call
@@ -268,18 +270,66 @@ to the handle's position in the list it runs with. Two mistakes are refused befo
268
270
  them in process: one handle twice (`AvValidationError`, `A2004`/`V1001`), and a `$ref` to a handle not in the list
269
271
  (`AvValidationError`, `A2007`/`V1010`). A handle is plain data: any client of the same generated tree may run it.
270
272
 
271
- ## Custom fetch and authentication
273
+ ## Hooks: `onCall`, `onSuccess`, `onError`
272
274
 
273
- `fetch` is typed as your platform's own `fetch` when your `lib` declares one (DOM, `@types/node`), so the platform
274
- `fetch` is accepted without a cast; with neither, a structural fetch type stands in. The client adds no headers of its
275
- own beyond the protocol's, so authentication goes in a custom fetch:
275
+ Three options of `new AvClient(…)` see every call the client makes: an operation, a cached read, a transaction (once,
276
+ whatever its steps) and a `$access` call. Each takes the call as an `AvCall`: its `kind` (`"operation"`,
277
+ `"transaction"`, `"access"`), its `label` (`"Post" find.many`), the `resource`, `family` and `variant` of an
278
+ operation, the `verb` of a `$access` call, and its `requestId` when it sends one. Never its arguments.
276
279
 
277
280
  ```ts
281
+ // One read of the token for every call made while it is in flight, so those calls still group (see below).
282
+ let inFlight: Promise<string> | undefined;
283
+ const token = (): Promise<string> =>
284
+ (inFlight ??= session.token().finally(() => {
285
+ inFlight = undefined;
286
+ }));
287
+
278
288
  const client = new AvClient({
279
- fetch: (input, init) => fetch(input, { ...init, headers: { ...init?.headers, Authorization: `Bearer ${token}` } }),
289
+ onCall: async () => ({ headers: { Authorization: `Bearer ${await token()}` } }),
290
+ onSuccess: (call, result) => metrics.record(call.label, result),
291
+ onError: (call, error) => {
292
+ if (error instanceof AvAuthError) router.replace("/login");
293
+ else console.warn(call.label, error);
294
+ },
280
295
  });
281
296
  ```
282
297
 
298
+ - `onCall(call)` runs when the call is made, before it is sent and before the calls made with it are grouped. It may
299
+ return, or resolve to, `AvCallAdditions` for that call's request: `headers`, and `credentials`, `cache`, `keepalive`,
300
+ `priority` and `referrerPolicy`. Calls share an Operation Set only when their additions are equal (in any order).
301
+ - An async `onCall` holds back its own call and no other. The calls made together are sent once a microtask passes
302
+ with no new call; a call whose `onCall` has not settled by then is sent when it settles, together with the calls
303
+ queued at that moment. So calls group when their `onCall` settles together with equal additions. An `onCall` that
304
+ does its own I/O for each call (reads the token again for every call) settles each call at its own time and sends
305
+ each in its own request. To keep grouping, have `onCall` await one shared in-flight promise, cleared once it
306
+ settles, as `token()` above: every call made while it is in flight settles with it and shares one `/_all`. If
307
+ `token()` rejects, the call is sent without `Authorization` (the hook warning fires) and will likely reach `onError`
308
+ as a refusal.
309
+ A hanging `onCall` stalls only its own call, aborting that call settles it at once, and a call made from inside a
310
+ hook never waits on the call that ran the hook.
311
+ - What the request sends, in order: `onCall`'s additions, then the call's own `headers` option, which wins over them,
312
+ then avClient's own headers, which always win. A header avClient sets itself (`Content-Type`, `Accept`,
313
+ `Aventara-Protocol-Version`, `Aventara-Contract-Hash`, `Aventara-Request-Id`), in its canonical or lower-case
314
+ spelling, is a type error in `onCall`'s `headers`, and so are `method`, `body`, `signal`, `redirect` and `mode`.
315
+ At runtime such a header in any spelling, a name that is not a valid header name and a value no request can carry
316
+ (a line break, a control character, or a character outside Latin-1, such as an emoji or an em dash) are dropped, with a warning (code
317
+ `AVENTARA_CLIENT_HOOK_HEADER_DROPPED`) naming the header and the call, once per header name for each `onCall`.
318
+ - `onSuccess(call, result)` and `onError(call, error)` are sinks, for side effects only (log, report, navigate). They
319
+ are typed as returning `void`, so any function fits (`() => router.push("/login")`, an async one), and what they
320
+ return is ignored. An async one is not awaited: it never delays the caller's result or rejection, and its rejection
321
+ is reported like a throw. `onError` sees every rejection (a refusal, a validation error, a transport failure,
322
+ an abort), sent alone or in an Operation Set, and the caller still gets the same rejection.
323
+ - A hook that throws, or rejects, is reported once as a warning (`process.emitWarning`, an `AventaraWarning` with code
324
+ `AVENTARA_CLIENT_HOOK_THREW`, or `console.warn` where there is no `process`), naming the hook and the call, and is
325
+ skipped: the call goes on, with no additions when it was `onCall` (as when its value cannot be read). A hook's
326
+ failure never reaches the call, the calls sent with it or the process.
327
+
328
+ `fetch` swaps the transport and nothing else (a test's fake, a runtime's own fetch). It is typed as your platform's own
329
+ `fetch` when your `lib` declares one (DOM, `@types/node`), so the platform `fetch` is accepted without a cast; with
330
+ neither, a structural fetch type stands in. It must return the `Response` unread: the client reads the body itself, an
331
+ Operation Set's line by line as it streams. Headers and credentials for every call go in `onCall`.
332
+
283
333
  The platform `fetch` is read when a call is made, not when the client is imported. Importing the client never fails
284
334
  where there is no `fetch`; a call does, saying why. An entrypoint carrying credentials is refused when the config is
285
335
  resolved.
@@ -316,9 +366,9 @@ export default defineClientConfig({
316
366
  });
317
367
  ```
318
368
 
319
- A verb on another origin carries cookies only when your `fetch` sends them: pass one that does
320
- (`new AvClient({ fetch: (url, init) => fetch(url, { ...init, credentials: "include" }) })`), and have Access allow
321
- that origin with credentials (CORS).
369
+ A verb on another origin carries cookies only when its request asks for them: return `credentials: "include"` from
370
+ `onCall` (`new AvClient({ onCall: () => ({ credentials: "include" }) })`), and have Access allow that origin with
371
+ credentials (CORS).
322
372
 
323
373
  ## Up to date
324
374
 
@@ -1,4 +1,5 @@
1
- export declare const USAGE = "avclient \u2014 generate a typed Aventara client from a deployed ClientContract.\n\nUsage:\n avclient generate [--yes] Read framework.client.ts (or .mts, .cts, .js, .mjs,\n .cjs) in the current directory, fetch\n <entrypoint>/_contract, and write AvClient.ts and\n generated/ into its generateAt directory, its imports\n spelled as the project's tsconfig.json says.\n avclient init [options] Set up this frontend: write framework.client.ts, the\n .env entry, the avclient:generate script and the\n @aventara/client devDependency; install; generate.\n --entrypoint <url> The server's entrypoint [http://localhost:3000/api].\n --env-var <NAME> Read it from this variable [AVENTARA_API_URL].\n --no-env-var Write it into framework.client.ts as a literal.\n --generate-at <dir> Where the client goes [./src/api].\n --package-manager <npm|pnpm> [the lockfile's, else the launching one, else npm]\n --skip-install Print the install instead of running it.\n --skip-generate Do not generate now.\n -y, --yes Accept every default, and replace differing content.\n avclient --help Print this and exit 0.\n avclient <command> --help Print that command's usage and exit 0.\n avclient --version, -v Print this generator's version and exit 0.\n\nOptions:\n -y, --yes Overwrite or remove content in AvClient.ts and generated/ that the\n generator did not produce, without asking. Without it, such content\n is listed and you are asked; where nobody can answer (stdin is not a\n terminal, as in CI), the run is refused and nothing is touched.\n\nThe generator owns AvClient.ts and generated/ in generateAt, and nothing else\nthere: your own files beside them are never read, moved or removed. Generated\nfiles are replaced on every run; do not edit them.\n\nBefore framework.client.ts is evaluated, the .env cascade is read from the\ncurrent directory, highest precedence first: the process environment,\n.env.<mode>.local, .env.<mode>, .env.local, .env \u2014 where mode is NODE_ENV, or\n\"development\".\n";
1
+ export declare const USAGE = "avclient \u2014 generate a typed Aventara client from a deployed ClientContract.\n\nUsage:\n avclient generate [--yes] Read framework.client.ts (or .mts, .cts, .js, .mjs,\n .cjs) in the current directory, fetch\n <entrypoint>/_contract, and write AvClient.ts and\n generated/ into its generateAt directory, its imports\n spelled as the project's tsconfig.json says.\n avclient init [options] Set up this frontend: write framework.client.ts, the\n .env entry, the avclient:generate script and the\n @aventara/client devDependency; install; generate.\n --entrypoint <url> The server's entrypoint [http://localhost:3000/api].\n --env-var <NAME> Read it from this variable [AVENTARA_API_URL].\n --no-env-var Write it into framework.client.ts as a literal.\n --generate-at <dir> Where the client goes [./src/api].\n --package-manager <npm|pnpm> [the lockfile's, else the launching one, else npm]\n --skip-install Print the install instead of running it.\n --skip-generate Do not generate now.\n -y, --yes Accept every default, and replace differing content.\n avclient skill [--force] Write or refresh this frontend's agent skill\n (.claude/skills/aventara-client/); --force replaces\n a file you edited. avclient init writes it too.\n avclient --help Print this and exit 0.\n avclient <command> --help Print that command's usage and exit 0.\n avclient --version, -v Print this generator's version and exit 0.\n\nOptions:\n -y, --yes Overwrite or remove content in AvClient.ts and generated/ that the\n generator did not produce, without asking. Without it, such content\n is listed and you are asked; where nobody can answer (stdin is not a\n terminal, as in CI), the run is refused and nothing is touched.\n\nThe generator owns AvClient.ts and generated/ in generateAt, and nothing else\nthere: your own files beside them are never read, moved or removed. Generated\nfiles are replaced on every run; do not edit them.\n\nBefore framework.client.ts is evaluated, the .env cascade is read from the\ncurrent directory, highest precedence first: the process environment,\n.env.<mode>.local, .env.<mode>, .env.local, .env \u2014 where mode is NODE_ENV, or\n\"development\".\n";
2
+ export declare const SKILL_USAGE = "Usage: avclient skill [--force]\n\nWrite or refresh this frontend's agent skill, .claude/skills/aventara-client/,\nfor this @aventara/client: how the generated client calls the API. A file you\nedited is kept, with a warning.\n\nOptions:\n --force Replace a file you edited.\n";
2
3
  export declare const GENERATE_USAGE = "Usage: avclient generate [--yes]\n\nRead framework.client.ts (or .mts, .cts, .js, .mjs, .cjs) in the current directory,\nfetch <entrypoint>/_contract, and write AvClient.ts and generated/ into its\ngenerateAt directory, its imports spelled as the project's tsconfig.json says.\n\nOptions:\n -y, --yes Overwrite or remove content in AvClient.ts and generated/ that the\n generator did not produce, without asking. Without it, such content\n is listed and you are asked; where nobody can answer (stdin is not a\n terminal, as in CI), the run is refused and nothing is touched.\n\nThe generator owns AvClient.ts and generated/ in generateAt, and nothing else\nthere: your own files beside them are never read, moved or removed.\n\nBefore the config is evaluated, the .env cascade is read from the current\ndirectory, highest precedence first: the process environment, .env.<mode>.local,\n.env.<mode>, .env.local, .env \u2014 where mode is NODE_ENV, or \"development\".\n";
3
4
  export declare const INIT_USAGE = "Usage: avclient init [options]\n\nSet up this frontend: write framework.client.ts, the .env entry, the avclient:generate\nscript and the @aventara/client devDependency; install; generate.\n\nOptions:\n --entrypoint <url> The server's entrypoint [http://localhost:3000/api].\n --env-var <NAME> Read it from this variable [AVENTARA_API_URL].\n --no-env-var Write it into framework.client.ts as a literal.\n --generate-at <dir> Where the client goes [./src/api].\n --package-manager <npm|pnpm> [the lockfile's, else the launching one, else npm]\n --skip-install Print the install instead of running it.\n --skip-generate Do not generate now.\n --verbose Show the install's output as it runs.\n -y, --yes Accept every default, and replace differing content.\n\nOn a terminal every unanswered question is asked; anywhere else, pass its flag\nor --yes, or the run stops before writing anything.\n";
4
5
  export type ClientInitCommand = {
@@ -14,9 +15,12 @@ export type ClientInitCommand = {
14
15
  /** What the command line asked for. */
15
16
  type CliCommand = {
16
17
  readonly command: "help";
17
- readonly topic?: "generate" | "init";
18
+ readonly topic?: "generate" | "init" | "skill";
18
19
  } | {
19
20
  readonly command: "version";
21
+ } | {
22
+ readonly command: "skill";
23
+ readonly force: boolean;
20
24
  } | {
21
25
  readonly command: "generate";
22
26
  readonly yes: boolean;
@@ -18,6 +18,9 @@ Usage:
18
18
  --skip-install Print the install instead of running it.
19
19
  --skip-generate Do not generate now.
20
20
  -y, --yes Accept every default, and replace differing content.
21
+ avclient skill [--force] Write or refresh this frontend's agent skill
22
+ (.claude/skills/aventara-client/); --force replaces
23
+ a file you edited. avclient init writes it too.
21
24
  avclient --help Print this and exit 0.
22
25
  avclient <command> --help Print that command's usage and exit 0.
23
26
  avclient --version, -v Print this generator's version and exit 0.
@@ -37,6 +40,15 @@ current directory, highest precedence first: the process environment,
37
40
  .env.<mode>.local, .env.<mode>, .env.local, .env — where mode is NODE_ENV, or
38
41
  "development".
39
42
  `;
43
+ export const SKILL_USAGE = `Usage: avclient skill [--force]
44
+
45
+ Write or refresh this frontend's agent skill, .claude/skills/aventara-client/,
46
+ for this @aventara/client: how the generated client calls the API. A file you
47
+ edited is kept, with a warning.
48
+
49
+ Options:
50
+ --force Replace a file you edited.
51
+ `;
40
52
  export const GENERATE_USAGE = `Usage: avclient generate [--yes]
41
53
 
42
54
  Read ${CLIENT_CONFIG_FILE} (or .mts, .cts, .js, .mjs, .cjs) in the current directory,
@@ -90,13 +102,20 @@ export function parseCliCommand(argv) {
90
102
  if (command === "--version" || command === "-v") {
91
103
  return { command: "version" };
92
104
  }
93
- if ((command === "init" || command === "generate") &&
105
+ if ((command === "init" || command === "generate" || command === "skill") &&
94
106
  (rest.includes("--help") || rest.includes("-h"))) {
95
107
  return { command: "help", topic: command };
96
108
  }
97
109
  if (command === "init") {
98
110
  return parseInit(rest, help);
99
111
  }
112
+ if (command === "skill") {
113
+ const [extra] = rest.filter((argument) => argument !== "--force");
114
+ if (extra !== undefined || rest.length > 1) {
115
+ throw new AvCliCommandError(`unexpected argument ${JSON.stringify(extra ?? "--force")}: skill takes only --force; ${help}`);
116
+ }
117
+ return { command: "skill", force: rest.length === 1 };
118
+ }
100
119
  if (command !== "generate") {
101
120
  throw new AvCliCommandError(`unknown command ${JSON.stringify(command)}; ${help}`);
102
121
  }
package/dist/cli.js CHANGED
@@ -1,4 +1,4 @@
1
- import { GENERATE_USAGE, INIT_USAGE, parseCliCommand, USAGE, } from "./cli/command.parser.js";
1
+ import { GENERATE_USAGE, INIT_USAGE, parseCliCommand, SKILL_USAGE, USAGE, } from "./cli/command.parser.js";
2
2
  import { runGenerate } from "./cli/generate.command.js";
3
3
  import { renderGenerationFailure } from "./cli/generation-failure.renderer.js";
4
4
  import { progressModeOf } from "./cli/step.reporter.js";
@@ -6,6 +6,7 @@ import { createTerminalPrompter } from "./cli/terminal.prompter.js";
6
6
  import { AVENTARA_CLIENT_GENERATOR_VERSION } from "./index.js";
7
7
  import { runClientInit } from "./init/client-init.orchestrator.js";
8
8
  import { runCommand } from "./init/command.runner.js";
9
+ import { writeClientSkill } from "./skill/client-skill.writer.js";
9
10
  export async function runCli(argv, io) {
10
11
  try {
11
12
  const command = parseCliCommand(argv);
@@ -14,7 +15,9 @@ export async function runCli(argv, io) {
14
15
  ? USAGE
15
16
  : command.topic === "init"
16
17
  ? INIT_USAGE
17
- : GENERATE_USAGE);
18
+ : command.topic === "skill"
19
+ ? SKILL_USAGE
20
+ : GENERATE_USAGE);
18
21
  return 0;
19
22
  }
20
23
  if (command.command === "version") {
@@ -24,6 +27,18 @@ export async function runCli(argv, io) {
24
27
  if (command.command === "init") {
25
28
  return await runClientInit(command, io);
26
29
  }
30
+ if (command.command === "skill") {
31
+ const skill = writeClientSkill({
32
+ directory: io.cwd,
33
+ clientVersion: AVENTARA_CLIENT_GENERATOR_VERSION,
34
+ force: command.force,
35
+ });
36
+ for (const warning of skill.warnings) {
37
+ io.stderr(`avclient: warning: ${warning}.\n`);
38
+ }
39
+ io.stdout(`avclient: ${skill.written.length === 0 ? "the client skill is up to date" : `wrote ${skill.written.join(", ")}`}.\n`);
40
+ return 0;
41
+ }
27
42
  return await runGenerate(io, command.yes);
28
43
  }
29
44
  catch (error) {
@@ -76,7 +76,7 @@ export function resolveEntrypoint(value) {
76
76
  if (url.username !== "" || url.password !== "") {
77
77
  throw new AvClientConfigError("entrypoint must not carry credentials: the platform's fetch refuses a URL that " +
78
78
  "includes them, and the entrypoint is embedded in the generated client, which ships — " +
79
- "authenticate through a custom fetch instead.");
79
+ "add credentials through onCall instead.");
80
80
  }
81
81
  if (value.includes("?") || value.includes("#")) {
82
82
  throw new AvClientConfigError("entrypoint must not carry a query or fragment; it is the origin plus mount path only.");
@@ -26,7 +26,7 @@ export function emitAccessModules(contract, style) {
26
26
  {
27
27
  path: "runtime/access.ts",
28
28
  source: `import { AvTransportError } from ${from("./errors")};
29
- import type { AvCallOptions, PlatformSignal, TransportConnection } from ${from("./transport")};
29
+ import { type AvCallOptions, type PlatformSignal, type TransportConnection, withAdditions } from ${from("./transport")};
30
30
 
31
31
  /**
32
32
  * Aventara Access's verbs, as this deployment's ClientContract states them:
@@ -227,7 +227,7 @@ export async function callAccess(
227
227
  let answer: AccessAnswer;
228
228
  let text: string;
229
229
  try {
230
- answer = await fetch(routeOf(connection.entrypoint, path), init);
230
+ answer = await fetch(routeOf(connection.entrypoint, path), withAdditions(connection.additions, init));
231
231
  text = await answer.text();
232
232
  } catch (error) {
233
233
  if (options.signal?.aborted === true) {
@@ -60,18 +60,53 @@ export function emitClientSurfaceModule(contract, names, style) {
60
60
  ? "args: unknown, callOptions: AvCallOptions = {}"
61
61
  : "callOptions: AvCallOptions = {}";
62
62
  const args = shape.args ? "args" : "undefined";
63
+ const call = `observeCall(options, accessCall(${quoted}, callOptions), callOptions.signal, (additions) =>\n\t\t\t\t\t\tcallAccess({ ...connect(), additions }, ${quoted}, ${args}, callOptions),\n\t\t\t\t\t)`;
63
64
  return shape.resolves === "nothing"
64
- ? `\t\t\t\t${name}: async (${parameters}): Promise<void> => {\n\t\t\t\t\tawait callAccess(connect(), ${quoted}, ${args}, callOptions);\n\t\t\t\t},\n`
65
- : `\t\t\t\t${name}: async (${parameters}) => callAccess(connect(), ${quoted}, ${args}, callOptions),\n`;
65
+ ? `\t\t\t\t${name}: async (${parameters}): Promise<void> => {\n\t\t\t\t\tawait ${call};\n\t\t\t\t},\n`
66
+ : `\t\t\t\t${name}: async (${parameters}) =>\n\t\t\t\t\t${call},\n`;
66
67
  };
67
68
  const accessRuntime = access === undefined
68
69
  ? ""
69
70
  : '\t\tObject.defineProperty(this, "$access", {\n' +
70
71
  "\t\t\tvalue: Object.freeze({\n" +
71
- "\t\t\t\tcan: async (questions: readonly unknown[], callOptions: AvCallOptions = {}) => askCan(connect(), questions, callOptions),\n" +
72
+ "\t\t\t\tcan: async (questions: readonly unknown[], callOptions: AvCallOptions = {}) =>\n" +
73
+ '\t\t\t\t\tobserveCall(options, accessCall("can", callOptions), callOptions.signal, (additions) =>\n' +
74
+ "\t\t\t\t\t\taskCan({ ...connect(), additions }, questions, callOptions),\n" +
75
+ "\t\t\t\t\t),\n" +
72
76
  verbs.map(accessMember).join("") +
73
77
  "\t\t\t}),\n" +
74
78
  "\t\t});\n";
79
+ const callType = "/** One call, as avClient's hooks see it: what it is, never its arguments. */\n" +
80
+ "export type AvCall =\n" +
81
+ "\t| {\n" +
82
+ '\t\t\treadonly kind: "operation";\n' +
83
+ "\t\t\treadonly resource: Resource;\n" +
84
+ "\t\t\treadonly family: AvOperationFamily;\n" +
85
+ "\t\t\treadonly variant: string;\n" +
86
+ '\t\t\t/** How errors and warnings name it: `"Post" find.many`. */\n' +
87
+ "\t\t\treadonly label: string;\n" +
88
+ "\t\t\t/** Its requestId, when it sends one. */\n" +
89
+ "\t\t\treadonly requestId?: string;\n" +
90
+ "\t }" +
91
+ (interactive
92
+ ? "\n\t| {\n" +
93
+ "\t\t\t/** A transaction: one call, whatever its steps. */\n" +
94
+ '\t\t\treadonly kind: "transaction";\n' +
95
+ '\t\t\treadonly label: "the transaction";\n' +
96
+ "\t\t\treadonly requestId?: string;\n" +
97
+ "\t }"
98
+ : "") +
99
+ (access === undefined
100
+ ? ""
101
+ : "\n\t| {\n" +
102
+ '\t\t\treadonly kind: "access";\n' +
103
+ `\t\t\treadonly verb: ${["can", ...verbs].map((name) => JSON.stringify(name)).join(" | ")};\n` +
104
+ "\t\t\t/** `$access.can`, `$access.me`, … */\n" +
105
+ "\t\t\treadonly label: string;\n" +
106
+ "\t\t\t/** Its requestId, when it sends one (`can` alone does). */\n" +
107
+ "\t\t\treadonly requestId?: string;\n" +
108
+ "\t }") +
109
+ ";\n";
75
110
  const surface = (name, call) => `type ${name} = {\n` +
76
111
  `\treadonly [${resourceKey}]: {\n` +
77
112
  "\t\treadonly [F in keyof Grammar[R]]: {\n" +
@@ -107,18 +142,18 @@ export function emitClientSurfaceModule(contract, names, style) {
107
142
  (checked
108
143
  ? `import { ${interactive ? "checkOperation, checkPlan" : "checkOperation"} } from ${from("./runtime/field-validators")};\n`
109
144
  : "") +
110
- `import { all, CallQueue${interactive ? ", withSignal" : ""} } from ${from("./runtime/operation-set")};\n` +
145
+ `import { ${access === undefined ? "" : "accessCall, "}all, CallQueue${access === undefined ? "" : ", observeCall"}${interactive ? ", transactionCall, withSignal" : ""} } from ${from("./runtime/operation-set")};\n` +
111
146
  (interactive
112
147
  ? `import { deferOperation, runTransaction } from ${from("./runtime/transaction")};\n`
113
148
  : "") +
114
149
  (access === undefined
115
- ? `import type { AvCallOptions, AvFetch, PlatformSignal, TransportConnection } from ${from("./runtime/transport")};\n`
116
- : `import { askCan, type AvCallOptions, type AvFetch, type PlatformSignal, type TransportConnection } from ${from("./runtime/transport")};\n`) +
150
+ ? `import type { AvCallAdditions, AvCallOptions, AvFetch, PlatformSignal, TransportConnection } from ${from("./runtime/transport")};\n`
151
+ : `import { askCan, type AvCallAdditions, type AvCallOptions, type AvFetch, type PlatformSignal, type TransportConnection } from ${from("./runtime/transport")};\n`) +
117
152
  (verbs.includes("me")
118
153
  ? `import type { AvAccessMe } from ${from("./types")};\n`
119
154
  : "") +
120
155
  "\n" +
121
- `export type { AvCallOptions } from ${from("./runtime/transport")};\n` +
156
+ `export type { AvCallAdditions, AvCallOptions } from ${from("./runtime/transport")};\n` +
122
157
  "\n" +
123
158
  "/**\n" +
124
159
  " * The forms this client reads and writes: core's application forms\n" +
@@ -128,19 +163,51 @@ export function emitClientSurfaceModule(contract, names, style) {
128
163
  "\treadonly decimal: AvDecimal;\n" +
129
164
  "};\n" +
130
165
  "\n" +
131
- "/** How an `AvClient` reaches its deployment. */\n" +
166
+ "/** How an `AvClient` reaches its deployment, and what sees its calls. */\n" +
132
167
  "export interface AvClientOptions {\n" +
133
168
  "\t/** Another deployment serving exactly the same ClientContract; the generated default otherwise. */\n" +
134
169
  "\treadonly entrypoint?: string;\n" +
135
- "\t/** A custom fetch, for authentication or tests; the platform's otherwise. */\n" +
170
+ "\t/**\n" +
171
+ "\t * The fetch every request is sent with, in place of the platform's (a\n" +
172
+ "\t * test's fake, a runtime's own). It must return the Response unread: the\n" +
173
+ "\t * client reads the body itself, an Operation Set's as it streams. Headers\n" +
174
+ "\t * or credentials for every call go in onCall.\n" +
175
+ "\t */\n" +
136
176
  "\treadonly fetch?: AvFetch;\n" +
137
177
  "\t/**\n" +
138
178
  "\t * false sends every call to its own route. Otherwise the calls made\n" +
139
179
  "\t * together (in one tick) go out as one Operation Set, POST /_all.\n" +
140
180
  "\t */\n" +
141
181
  "\treadonly grouping?: boolean;\n" +
182
+ "\t/**\n" +
183
+ "\t * Runs once for every call, when it is made: before it is sent and before\n" +
184
+ "\t * the calls made with it are grouped. It may return, or resolve to,\n" +
185
+ "\t * additions for that call's request (a token, `credentials`); an async\n" +
186
+ "\t * onCall is awaited before the calls are grouped. Calls share an Operation\n" +
187
+ "\t * Set only when their additions are equal. The call's own `headers` option\n" +
188
+ "\t * wins over a header added here, and avClient's own headers always win.\n" +
189
+ "\t * A throw is reported as a warning, and the call goes on with no additions.\n" +
190
+ "\t */\n" +
191
+ "\treadonly onCall?: (call: AvCall) => AvCallAdditions | void | PromiseLike<AvCallAdditions | void>;\n" +
192
+ "\t/**\n" +
193
+ "\t * A sink for a call that resolved, and its result: side effects only (log,\n" +
194
+ "\t * report). What it returns is ignored, an async one is not awaited (it\n" +
195
+ "\t * never delays the caller's result), and a throw or a rejection is reported\n" +
196
+ "\t * as a warning and ignored.\n" +
197
+ "\t */\n" +
198
+ "\treadonly onSuccess?: (call: AvCall, result: unknown) => void;\n" +
199
+ "\t/**\n" +
200
+ "\t * A sink for a call that rejected (a refusal, a validation error, a\n" +
201
+ "\t * transport failure, an abort), sent alone or in an Operation Set: side\n" +
202
+ "\t * effects only (log, report, navigate). The caller still gets the same\n" +
203
+ "\t * rejection, never delayed: what it returns is ignored, an async one is not\n" +
204
+ "\t * awaited, and a throw or a rejection is reported as a warning and ignored.\n" +
205
+ "\t */\n" +
206
+ "\treadonly onError?: (call: AvCall, error: unknown) => void;\n" +
142
207
  "}\n" +
143
208
  "\n" +
209
+ callType +
210
+ "\n" +
144
211
  (shaped === undefined
145
212
  ? ""
146
213
  : "/** The ClientContract, with each shaped Json field's own type. */\n" +
@@ -238,7 +305,7 @@ export function emitClientSurfaceModule(contract, names, style) {
238
305
  '\t\tconst entrypoint = (options.entrypoint ?? DEFAULT_ENTRYPOINT).replace(/\\/+$/, "");\n' +
239
306
  "\t\tconst given = options.fetch;\n" +
240
307
  "\t\tconst connect = (): TransportConnection => ({ entrypoint, fetch: platformFetch(given) });\n" +
241
- "\t\tconst queue = new CallQueue(connect, options.grouping !== false);\n" +
308
+ "\t\tconst queue = new CallQueue(connect, options.grouping !== false, options);\n" +
242
309
  "\t\tfor (const [resource, families] of operationTree(\n" +
243
310
  "\t\t\t(resource, family, variant): OperationMethod =>\n" +
244
311
  (checked
@@ -263,11 +330,12 @@ export function emitClientSurfaceModule(contract, names, style) {
263
330
  "\t\t\tvalue: (steps: unknown, callOptions: AvCallOptions = {}) => {\n" +
264
331
  "\t\t\t\tconst plan = Array.isArray(steps) ? [...(steps as readonly unknown[])] : steps;\n" +
265
332
  "\t\t\t\treturn queue.alone(\n" +
266
- "\t\t\t\t\t(signal) =>\n" +
333
+ "\t\t\t\t\t(connection, signal) =>\n" +
267
334
  (checked
268
- ? "\t\t\t\t\t\trunTransaction(connect(), plan, withSignal(callOptions, signal), checkPlan),\n"
269
- : "\t\t\t\t\t\trunTransaction(connect(), plan, withSignal(callOptions, signal)),\n") +
335
+ ? "\t\t\t\t\t\trunTransaction(connection, plan, withSignal(callOptions, signal), checkPlan),\n"
336
+ : "\t\t\t\t\t\trunTransaction(connection, plan, withSignal(callOptions, signal)),\n") +
270
337
  "\t\t\t\t\tcallOptions.signal,\n" +
338
+ "\t\t\t\t\ttransactionCall(callOptions),\n" +
271
339
  "\t\t\t\t);\n" +
272
340
  "\t\t\t},\n" +
273
341
  "\t\t});\n"
@@ -33,7 +33,7 @@ export function emitClientTree(contract, entrypoint, style, regenerate = AVCLIEN
33
33
  emitDecimalModule(),
34
34
  emitErrorsModule(regenerate),
35
35
  emitTransportModule(style, contract.access !== undefined),
36
- emitOperationSetModule(style, contractHasCachedReads(contract)),
36
+ emitOperationSetModule(style, contractHasCachedReads(contract), contract.access !== undefined),
37
37
  ...(contractHasCachedReads(contract) ? emitCachedReadModules(style) : []),
38
38
  ...emitAccessModules(contract, style),
39
39
  emitNamedTypesModule(contract, names, style),
@@ -71,8 +71,8 @@ function generatedModuleSpecifier(style, module) {
71
71
  export function clientEntryReExports(style, clientClass = "value", accessVerbs = false) {
72
72
  const from = (module) => generatedModuleSpecifier(style, module);
73
73
  return ((clientClass === "value"
74
- ? `export {\n\tAvClient,\n\ttype AvClientOptions,\n\ttype AvCallOptions,\n} from ${from("client.ts")};\n`
75
- : `export type { AvClient, AvClientOptions, AvCallOptions } from ${from("client.ts")};\n`) +
74
+ ? `export {\n\tAvClient,\n\ttype AvClientOptions,\n\ttype AvCallOptions,\n\ttype AvCall,\n\ttype AvCallAdditions,\n} from ${from("client.ts")};\n`
75
+ : `export type { AvClient, AvClientOptions, AvCallOptions, AvCall, AvCallAdditions } from ${from("client.ts")};\n`) +
76
76
  `export type { AvFetch } from ${from("runtime/transport.ts")};\n` +
77
77
  `export type { AvOperation } from ${from("derivation/transactions/deferred-operation.d.ts")};\n` +
78
78
  `export type * from ${from("types.d.ts")};\n` +
@@ -6,6 +6,8 @@ export const RESERVED_EMITTED_NAMES = [
6
6
  "AvAccessMe",
7
7
  "AvClient",
8
8
  "AvClientOptions",
9
+ "AvCall",
10
+ "AvCallAdditions",
9
11
  "AvCallOptions",
10
12
  "AvCause",
11
13
  "AvDecimal",
@@ -1,3 +1,3 @@
1
1
  import type { EmittedModule } from "./emitted-tree.interface.js";
2
2
  import { type ClientModuleStyle } from "./module-style.interface.js";
3
- export declare function emitOperationSetModule(style: ClientModuleStyle, cached?: boolean): EmittedModule;
3
+ export declare function emitOperationSetModule(style: ClientModuleStyle, cached?: boolean, access?: boolean): EmittedModule;