@aventara/client 0.0.0-stage → 0.1.0-pilot.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.
Files changed (84) hide show
  1. package/LICENSE +91 -0
  2. package/LICENSE-ADDITIONAL-PERMISSION.md +9 -0
  3. package/README.md +234 -2
  4. package/dist/avclient.bin.d.ts +2 -0
  5. package/dist/avclient.bin.js +15 -0
  6. package/dist/cli/command.parser.d.ts +37 -0
  7. package/dist/cli/command.parser.js +177 -0
  8. package/dist/cli/generate.command.d.ts +24 -0
  9. package/dist/cli/generate.command.js +41 -0
  10. package/dist/cli/generation-failure.renderer.d.ts +6 -0
  11. package/dist/cli/generation-failure.renderer.js +52 -0
  12. package/dist/cli/generation-success.renderer.d.ts +32 -0
  13. package/dist/cli/generation-success.renderer.js +47 -0
  14. package/dist/cli/terminal.prompter.d.ts +13 -0
  15. package/dist/cli/terminal.prompter.js +53 -0
  16. package/dist/cli/warning.renderer.d.ts +10 -0
  17. package/dist/cli/warning.renderer.js +14 -0
  18. package/dist/cli.d.ts +29 -0
  19. package/dist/cli.js +75 -0
  20. package/dist/config/client-config.interface.d.ts +62 -0
  21. package/dist/config/client-config.interface.js +14 -0
  22. package/dist/config/config.loader.d.ts +41 -0
  23. package/dist/config/config.loader.js +95 -0
  24. package/dist/config/config.resolver.d.ts +50 -0
  25. package/dist/config/config.resolver.js +126 -0
  26. package/dist/config/env.cascade.d.ts +84 -0
  27. package/dist/config/env.cascade.js +126 -0
  28. package/dist/contract/contract.acceptance.d.ts +77 -0
  29. package/dist/contract/contract.acceptance.js +124 -0
  30. package/dist/contract/contract.fetcher.d.ts +64 -0
  31. package/dist/contract/contract.fetcher.js +85 -0
  32. package/dist/contract/contract.loader.d.ts +32 -0
  33. package/dist/contract/contract.loader.js +32 -0
  34. package/dist/emit/banner.emitter.d.ts +31 -0
  35. package/dist/emit/banner.emitter.js +42 -0
  36. package/dist/emit/client-surface.emitter.d.ts +32 -0
  37. package/dist/emit/client-surface.emitter.js +236 -0
  38. package/dist/emit/client-tree.emitter.d.ts +37 -0
  39. package/dist/emit/client-tree.emitter.js +103 -0
  40. package/dist/emit/contract-carrier.emitter.d.ts +13 -0
  41. package/dist/emit/contract-carrier.emitter.js +60 -0
  42. package/dist/emit/derivation.emitter.d.ts +45 -0
  43. package/dist/emit/derivation.emitter.js +233 -0
  44. package/dist/emit/descriptor.emitter.d.ts +4 -0
  45. package/dist/emit/descriptor.emitter.js +97 -0
  46. package/dist/emit/emitted-tree.interface.d.ts +61 -0
  47. package/dist/emit/emitted-tree.interface.js +18 -0
  48. package/dist/emit/enum.emitter.d.ts +24 -0
  49. package/dist/emit/enum.emitter.js +42 -0
  50. package/dist/emit/name.deriver.d.ts +153 -0
  51. package/dist/emit/name.deriver.js +411 -0
  52. package/dist/emit/named-type.emitter.d.ts +32 -0
  53. package/dist/emit/named-type.emitter.js +50 -0
  54. package/dist/emit/runtime.emitter.d.ts +87 -0
  55. package/dist/emit/runtime.emitter.js +707 -0
  56. package/dist/emit/scalar.codec.d.ts +63 -0
  57. package/dist/emit/scalar.codec.js +498 -0
  58. package/dist/emit/transaction.emitter.d.ts +17 -0
  59. package/dist/emit/transaction.emitter.js +438 -0
  60. package/dist/generate.d.ts +123 -0
  61. package/dist/generate.js +98 -0
  62. package/dist/index.d.ts +8 -0
  63. package/dist/index.js +8 -0
  64. package/dist/init/client-config.template.d.ts +11 -0
  65. package/dist/init/client-config.template.js +27 -0
  66. package/dist/init/client-init.errors.d.ts +9 -0
  67. package/dist/init/client-init.errors.js +9 -0
  68. package/dist/init/client-init.orchestrator.d.ts +3 -0
  69. package/dist/init/client-init.orchestrator.js +86 -0
  70. package/dist/init/client-init.planner.d.ts +27 -0
  71. package/dist/init/client-init.planner.js +99 -0
  72. package/dist/init/client-init.questions.d.ts +52 -0
  73. package/dist/init/client-init.questions.js +124 -0
  74. package/dist/init/client-project.inspector.d.ts +15 -0
  75. package/dist/init/client-project.inspector.js +32 -0
  76. package/dist/init/command.runner.d.ts +8 -0
  77. package/dist/init/command.runner.js +17 -0
  78. package/dist/node-version.guard.d.ts +8 -0
  79. package/dist/node-version.guard.js +59 -0
  80. package/dist/output/output.validator.d.ts +75 -0
  81. package/dist/output/output.validator.js +262 -0
  82. package/dist/output/output.writer.d.ts +162 -0
  83. package/dist/output/output.writer.js +499 -0
  84. package/package.json +47 -3
package/LICENSE ADDED
@@ -0,0 +1,91 @@
1
+ Required Notice: Copyright 2026 Mohamed Ragheb
2
+
3
+ # PolyForm Shield License 1.0.0
4
+
5
+ <https://polyformproject.org/licenses/shield/1.0.0>
6
+
7
+ ## Acceptance
8
+
9
+ In order to get any license under these terms, you must agree to them as both strict obligations and conditions to all your licenses.
10
+
11
+ ## Copyright License
12
+
13
+ The licensor grants you a copyright license for the software to do everything you might do with the software that would otherwise infringe the licensor's copyright in it for any permitted purpose. However, you may only distribute the software according to [Distribution License](#distribution-license) and make changes or new works based on the software according to [Changes and New Works License](#changes-and-new-works-license).
14
+
15
+ ## Distribution License
16
+
17
+ The licensor grants you an additional copyright license to distribute copies of the software. Your license to distribute covers distributing the software with changes and new works permitted by [Changes and New Works License](#changes-and-new-works-license).
18
+
19
+ ## Notices
20
+
21
+ You must ensure that anyone who gets a copy of any part of the software from you also gets a copy of these terms or the URL for them above, as well as copies of any plain-text lines beginning with `Required Notice:` that the licensor provided with the software. For example:
22
+
23
+ > Required Notice: Copyright Yoyodyne, Inc. (http://example.com)
24
+
25
+ ## Changes and New Works License
26
+
27
+ The licensor grants you an additional copyright license to make changes and new works based on the software for any permitted purpose.
28
+
29
+ ## Patent License
30
+
31
+ The licensor grants you a patent license for the software that covers patent claims the licensor can license, or becomes able to license, that you would infringe by using the software.
32
+
33
+ ## Noncompete
34
+
35
+ Any purpose is a permitted purpose, except for providing any product that competes with the software or any product the licensor or any of its affiliates provides using the software.
36
+
37
+ ## Competition
38
+
39
+ Goods and services compete even when they provide functionality through different kinds of interfaces or for different technical platforms. Applications can compete with services, libraries with plugins, frameworks with development tools, and so on, even if they're written in different programming languages or for different computer architectures. Goods and services compete even when provided free of charge. If you market a product as a practical substitute for the software or another product, it definitely competes.
40
+
41
+ ## New Products
42
+
43
+ If you are using the software to provide a product that does not compete, but the licensor or any of its affiliates brings your product into competition by providing a new version of the software or another product using the software, you may continue using versions of the software available under these terms beforehand to provide your competing product, but not any later versions.
44
+
45
+ ## Discontinued Products
46
+
47
+ You may begin using the software to compete with a product or service that the licensor or any of its affiliates has stopped providing, unless the licensor includes a plain-text line beginning with `Licensor Line of Business:` with the software that mentions that line of business. For example:
48
+
49
+ > Licensor Line of Business: YoyodyneCMS Content Management System (http://example.com/cms)
50
+
51
+ ## Sales of Business
52
+
53
+ If the licensor or any of its affiliates sells a line of business developing the software or using the software to provide a product, the buyer can also enforce [Noncompete](#noncompete) for that product.
54
+
55
+ ## Fair Use
56
+
57
+ You may have "fair use" rights for the software under the law. These terms do not limit them.
58
+
59
+ ## No Other Rights
60
+
61
+ These terms do not allow you to sublicense or transfer any of your licenses to anyone else, or prevent the licensor from granting licenses to anyone else. These terms do not imply any other licenses.
62
+
63
+ ## Patent Defense
64
+
65
+ If you make any written claim that the software infringes or contributes to infringement of any patent, your patent license for the software granted under these terms ends immediately. If your company makes such a claim, your patent license ends immediately for work on behalf of your company.
66
+
67
+ ## Violations
68
+
69
+ The first time you are notified in writing that you have violated any of these terms, or done anything with the software not covered by your licenses, your licenses can nonetheless continue if you come into full compliance with these terms, and take practical steps to correct past violations, within 32 days of receiving notice. Otherwise, all your licenses end immediately.
70
+
71
+ ## No Liability
72
+
73
+ ***As far as the law allows, the software comes as is, without any warranty or condition, and the licensor will not be liable to you for any damages arising out of these terms or the use or nature of the software, under any kind of legal claim.***
74
+
75
+ ## Definitions
76
+
77
+ The **licensor** is the individual or entity offering these terms, and the **software** is the software the licensor makes available under these terms.
78
+
79
+ A **product** can be a good or service, or a combination of them.
80
+
81
+ **You** refers to the individual or entity agreeing to these terms.
82
+
83
+ **Your company** is any legal entity, sole proprietorship, or other kind of organization that you work for, plus all its affiliates.
84
+
85
+ **Affiliates** means the other organizations than an organization has control over, is under the control of, or is under common control with.
86
+
87
+ **Control** means ownership of substantially all the assets of an entity, or the power to direct its management and policies by vote, contract, or otherwise. Control can be direct or indirect.
88
+
89
+ **Your licenses** are all the licenses granted to you for the software under these terms.
90
+
91
+ **Use** means anything you do with the software requiring one of your licenses.
@@ -0,0 +1,9 @@
1
+ # Additional Permission — Aventara
2
+
3
+ Copyright 2026 Mohamed Ragheb
4
+
5
+ In addition to the permissions of the PolyForm Shield License 1.0.0 in `LICENSE`, the licensor grants the following additional permission:
6
+
7
+ The Noncompete section applies only to providing a product that competes with the software itself — the Aventara framework, its packages and its tooling. Providing a product that competes with any other product the licensor or its affiliates provide using the software is a permitted purpose.
8
+
9
+ This additional permission only adds to your permissions; it does not restrict or replace any term of `LICENSE`.
package/README.md CHANGED
@@ -1,3 +1,235 @@
1
- # Temporary Holding Version
1
+ # `@aventara/client`
2
2
 
3
- This version is a temporary placeholder for this package. An operational version to replace this has been submitted for review and is awaiting a staged release.
3
+ The development-time generator for Aventara typed remote clients. It fetches a deployed framework's `ClientContract`
4
+ from `<entrypoint>/_contract`, verifies it, and writes a standalone TypeScript client into your project. The generated
5
+ client is the product: it imports nothing from this package, or from `@aventara/core`, at runtime.
6
+
7
+ Every argument and result type in the generated client is core's own derivation — `@aventara/core`'s published
8
+ declarations, copied into the tree and instantiated over the deployment's ClientContract — so the client agrees with the
9
+ server by construction, and that agreement is gated over both pilot schemas.
10
+
11
+ ## Setting up a frontend: `avclient init`
12
+
13
+ ```bash
14
+ npx @aventara/client@pilot init # in your frontend project; pnpm: pnpm dlx @aventara/client@pilot init
15
+ ```
16
+
17
+ It asks — or takes from flags, or with `--yes` takes every default — four things, and writes the setup:
18
+
19
+ | Flag | Question | Default |
20
+ |---|---|---|
21
+ | `--entrypoint <url>` | The server's framework entrypoint | `http://localhost:3000/api` |
22
+ | `--env-var <NAME>` / `--no-env-var` | Read it from this variable, or write it as a literal | `AVENTARA_API_URL` |
23
+ | `--generate-at <dir>` | Where the client goes | `./src/api` |
24
+ | `--package-manager <npm\|pnpm>` | | the lockfile's, else the launching one, else npm |
25
+
26
+ It writes `framework.client.mts`, the variable into `.env` (only with a variable), `"avclient:generate": "avclient
27
+ generate"` into the scripts and `@aventara/client` as an **exact** devDependency at its own version; runs the install
28
+ (`--skip-install` prints it instead); and generates the client right away (`--skip-generate` to skip; a server that does
29
+ not answer leaves the files and names `avclient generate`). Existing content that differs is listed and replaced only
30
+ when you confirm, or with `--yes`; where nobody can be asked, nothing is touched. The generated tree is meant to be
31
+ committed: regenerating it needs a running server.
32
+
33
+ ## Generating a client
34
+
35
+ Install it as a dev dependency (or let `avclient init` do it), then add `framework.client.mts` to the directory you run
36
+ the generator from:
37
+
38
+ ```ts
39
+ import { defineClientConfig, env } from "@aventara/client";
40
+
41
+ export default defineClientConfig({
42
+ entrypoint: env("AVENTARA_API_URL"),
43
+ generateAt: "./src/api",
44
+ });
45
+ ```
46
+
47
+ and run:
48
+
49
+ ```text
50
+ npx avclient generate [--yes]
51
+ ```
52
+
53
+ **Why `.mts`.** `npm init -y` writes `"type": "commonjs"`, and under it Node reads a `.ts` file as CommonJS — the
54
+ config's `import` would stop the run. A `.mts` file is an ES module in every project. A `framework.client.ts` written
55
+ before 0.1.0-pilot.1 is still read; with both files present the generator refuses, and `avclient init` moves the old one
56
+ to `framework.client.mts` (asking first if you edited it).
57
+
58
+ - **`entrypoint`** is the deployment's framework entrypoint: origin plus mount path, one absolute `http(s)` URL. It
59
+ may not carry credentials — the platform's `fetch` refuses such a URL, and the entrypoint ships inside the client.
60
+ - **`generateAt`** is a directory you may share with your own files. The generator owns exactly two entries in it —
61
+ `AvClient.ts` and `generated/` — and never reads, moves or removes anything else there.
62
+ - **The `.env` cascade** is read from the current directory before the config is evaluated, highest precedence first:
63
+ the process environment, `.env.<mode>.local`, `.env.<mode>`, `.env.local`, `.env`; `mode` is `NODE_ENV`, or
64
+ `development`. The config reads it through `env("NAME")`; `process.env` is never written.
65
+ - **Content the generator did not produce** in `AvClient.ts` or `generated/` is listed and you are asked before it is
66
+ overwritten or removed — including what a killed run left in the `generated/` it moved aside. `--yes` answers yes.
67
+ Where nobody can answer — stdin is not a terminal, as in CI — the run is refused and nothing is touched. A cancelled
68
+ or refused run still prints every warning it raised.
69
+
70
+ ### What a run guarantees
71
+
72
+ - **A failed run leaves the previous output exactly as it was.** The whole tree is written to a staging directory inside
73
+ `generateAt`, validated there as one program, and only then moved into place. A run killed part-way is repaired by the
74
+ next run that writes, before it writes anything. A first run that fails removes the directories it created.
75
+ - **The validate step is a real type check** when the optional peer `typescript` (5.5 to 6) is installed, under a
76
+ consumer's strictest plausible settings and with nothing outside the tree resolvable. Without it — or under
77
+ TypeScript 7, which has no classic compiler API to check with — the check degrades to a parse with Node's own
78
+ TypeScript parser, and says so loudly in one warning; it is never skipped, and the client is still written. The peer
79
+ range has no upper bound, so a frontend on TypeScript 7 installs it; your own `tsc` checks the tree when it compiles.
80
+ - **Determinism.** Two runs over one ClientContract write the same bytes; deleting the client and regenerating it gives
81
+ the same bytes. Nothing volatile — no timestamp, generator version, host or path — is emitted.
82
+ - **Failure is a sentence.** A refusal is one line and exit code 1, with no stack; anything the run warned about before
83
+ it stopped is printed first. A stack means a defect in the generator.
84
+
85
+ ## Calling the API
86
+
87
+ ```ts
88
+ import avClient, { AvClient, type User, type UserWhere } from "./api/AvClient";
89
+
90
+ const where: UserWhere = { email: { equals: "ada@example.com" } };
91
+ const users = await avClient.User.find.many({ where, select: ["id", "email", { posts: { select: ["$count"] } }] });
92
+ const one: User = await avClient.User.find.unique({ where: { id } });
93
+ const maybe = await avClient.User.find.first({ where }); // User | null — a miss is null
94
+ ```
95
+
96
+ - **`avClient`** is the default export: the client of the deployment the tree was generated from.
97
+ `new AvClient({ entrypoint, fetch })` makes another — `entrypoint` for a deployment serving **exactly the same**
98
+ ClientContract (tests, SSR), `fetch` for a custom fetch.
99
+ - **One property per Resource**, one per family under it, one per advertised variant: only what the ClientContract
100
+ advertises exists, in the type and at runtime. A Resource named like one of the client's own members (`tx`,
101
+ `transaction`, `then`, `constructor`, `Object.prototype`'s names) is reached as `<name>Model`, with one warning; its
102
+ wire name is unchanged.
103
+ - **A call resolves to the data**: the record, a list, a count; a first-style miss (`A1001`) is `null`. Every other
104
+ outcome throws — the `FrameworkError` subclass of its code (`NotFoundError` for a strict-unique miss,
105
+ `ContractMismatchError` for a stale client, …), or `TransportError` when no framework answer arrived.
106
+ - **Values are application values**: `bigint`, `Date`, `Uint8Array`, the client's `Decimal`, JSON values as they are.
107
+ Arguments are encoded and results revived for you; a value that cannot be read throws `TransportError`.
108
+ - **Per-call options** (`CallOptions`), never sent in the body: `signal` (an abort rejects with what `fetch` rejected
109
+ with), `headers` (the framework's own identity headers are refused), and `requestId`, sent as `Aventara-Request-Id`
110
+ and winning over that header in `headers`.
111
+ - **Named types** (`User`, `UserWhere`, `UserUniqueWhere`, `UserOrderBy`, `UserCreateData`, `UserUpdateData`,
112
+ `UserSelect`, `UserInclude`) are named exports, each only when the operation it reads is advertised. A name that would
113
+ clash walks the rename ladder (`User` beside a Resource `UserWhere` becomes `UserModel`), one warning each.
114
+
115
+ ## Transactions
116
+
117
+ When the ClientContract advertises `interactive` transactions, the client has `avClient.tx` and `avClient.transaction`
118
+ (otherwise neither exists):
119
+
120
+ ```ts
121
+ const created = avClient.tx.User.create.one({ data: { email: "ada@example.com" }, select: ["id"] });
122
+ const profile = avClient.tx.Profile.create.one({ data: { bio: "…", userId: created.$ref("id") } });
123
+ const [user, row] = await avClient.transaction([created, profile]);
124
+ ```
125
+
126
+ `avClient.tx.…` builds a handle and sends nothing. `avClient.transaction([...])` sends the plan once and resolves one
127
+ result per handle, typed, in order; a failing step throws its error with `cause.operation` naming the step. `$ref` binds
128
+ to the handle's position in the list it runs with. Two mistakes are refused before anything is sent, as core refuses
129
+ them in process: one handle twice (`ValidationError`, `A2004`/`V1001`), and a `$ref` to a handle not in the list
130
+ (`ValidationError`, `A2007`/`V1010`). A handle is plain data: any client of the same generated tree may run it.
131
+
132
+ ## Custom fetch and authentication
133
+
134
+ `fetch` is typed as **your platform's own `fetch`** when your `lib` declares one (DOM, `@types/node`), so the platform
135
+ `fetch` is accepted without a cast; with neither, a structural fetch type stands in. The client never adds headers of its
136
+ own beyond the protocol's; authentication is a custom fetch:
137
+
138
+ ```ts
139
+ const client = new AvClient({
140
+ fetch: (input, init) => fetch(input, { ...init, headers: { ...init?.headers, Authorization: `Bearer ${token}` } }),
141
+ });
142
+ ```
143
+
144
+ The platform `fetch` is read when a call is made, not when the client is imported, so importing the client never fails
145
+ where there is none — a call does, saying why. An entrypoint carrying credentials is refused when the config is
146
+ resolved.
147
+
148
+ ## Up to date
149
+
150
+ A rerun asks the deployment conditionally (`If-None-Match`, the ClientContract hash of the output it
151
+ finds — only when that output is intact and its carrier re-verifies). On `304 Not Modified` it re-emits from that
152
+ stored ClientContract with **this** generator and **this** entrypoint: identical bytes print
153
+ `avclient: up to date: … nothing was written.` and exit 0; otherwise the output is replaced, and the line says
154
+ `the ClientContract is unchanged; the generator or the entrypoint changed`.
155
+
156
+ ## What is emitted
157
+
158
+ Every file opens with a banner stating that it is generated and must not be edited, and with the Biome suppressions a
159
+ generated file needs — emitted, not configured, so the output stays correct in a repository whose formatter nobody here
160
+ chose. The `.d.ts` files are core's copied declarations under `generated/derivation/` and the named types in
161
+ `generated/types.d.ts`; the `.ts` sources carry their own types.
162
+
163
+ - `AvClient.ts` — the entry point you import: `avClient` (the default export), `AvClient`, `AvClientOptions`,
164
+ `CallOptions`, `Fetch`, `Operation`, the named Resource types, the enums, `Decimal`, `FrameworkError` and its
165
+ subclasses, `TransportError`, and the types `Cause`, `ValidationIssue`, `OperationCode` and `ValidationCode`.
166
+ - `generated/client.ts` — the typed client: the class `AvClient`, `AvClientOptions`, `CallOptions` and the ready
167
+ instance `avClient`; every argument and result type an instantiation of core's own derivation.
168
+ - `generated/contract.ts` — the ClientContract the client was generated against: its served canonical bytes, verbatim,
169
+ as the type `ClientContractShape` (no runtime byte).
170
+ - `generated/derivation/` — `@aventara/core`'s published declarations of the argument/result derivation and the
171
+ call grammar, copied byte for byte (bannered, source-map trailer dropped): the generated types are core's own.
172
+ - `generated/enums.ts` — each enum as a union type and a same-named `as const` object.
173
+ - `generated/metadata.ts` — the ClientContract hash and protocol version the client is bound to, and the entrypoint it
174
+ was generated from, its default deployment (an entrypoint carrying credentials is refused at resolution).
175
+ - `generated/runtime/codec.ts` — the nine scalars' wire codecs, and the request-body serializer.
176
+ - `generated/runtime/decimal.ts` — the framework's `Decimal`: a constructor, `toString` and `toJSON`.
177
+ - `generated/runtime/descriptor.ts` — what the runtime reads of the contract: the result fields it revives and the
178
+ relations it follows (the decode table), and the advertised operations.
179
+ - `generated/runtime/errors.ts` — `FrameworkError`, its subclasses by code class (`AuthError`, `ConflictError`,
180
+ `ContractMismatchError`, `InternalError`, `NotFoundError`, `ProtocolError`, `ValidationError`), `TransportError`, and
181
+ the code unions, emitted from core's code arrays rather than retyped.
182
+ - `generated/runtime/fingerprint.ts` — the operation fingerprint (RFC 8785 canonical JSON, SHA-256,
183
+ base64url), dependency-free and pinned to core's; emitted only when transactions are `interactive`.
184
+ - `generated/runtime/transaction.ts` — `avClient.tx`'s deferred handles and `avClient.transaction`'s plan assembly,
185
+ refusals and one POST to `/_transactions`; emitted only when transactions are `interactive`.
186
+ - `generated/runtime/transport.ts` — one POST per operation with the identity headers, and the response read by the
187
+ protocol's rule; arguments encoded by value, results revived by the decode table, the platform's own `fetch` type accepted.
188
+ Its `execute` primitive is module-private: no public file re-exports it; `AvClient`'s methods wrap it.
189
+ - `generated/types.d.ts` — the named Resource types, as a declaration file (`User`, `UserWhere`,
190
+ `UserUniqueWhere`, `UserOrderBy`, `UserCreateData`, `UserUpdateData`, `UserSelect`, `UserInclude`), each only when
191
+ its operation is advertised.
192
+
193
+ The tree is self-contained: every import in it names a file in it. That is gated twice in this package's tests — once
194
+ lexically, once by compiling the tree alone where nothing outside it can resolve — and each gate catches a planted
195
+ import of `@aventara/core` or `@aventara/client` on its own. Names a TypeScript declaration cannot carry as-is (a
196
+ reserved word, a name the runtime owns, an enum sharing a Resource's name) are renamed, with one warning each; the wire
197
+ name is kept.
198
+
199
+ ## What a generated client is bound to
200
+
201
+ A generated client is bound to the **exact ClientContract hash and protocol version** it was generated from. Both are
202
+ in `generated/metadata.ts` and both are sent on every request (`Aventara-Protocol-Version`, `Aventara-Contract-Hash`).
203
+ A deployment whose ClientContract differs refuses the request with `A2005`, which the client throws as
204
+ `ContractMismatchError`.
205
+
206
+ The hash covers everything the deployment advertises, not only its schema: two deployments of one schema can advertise
207
+ different ClientContracts, and a client generated against one is refused by the other. Whatever the cause, the remedy
208
+ is the same — **generate against the deployment the client will call, and regenerate when it changes.**
209
+
210
+ That holds for an operation the deployment **no longer advertises**, too: the server checks the client's
211
+ identity before it routes, so a stale client calling a removed operation also hears `A2005` and throws
212
+ `ContractMismatchError`. A response that is not a framework envelope at all — a host's own 404 for a path it does not
213
+ mount — is a `TransportError`, which names no remedy: nothing in it says the client is stale.
214
+
215
+ The embedded entrypoint is a **default**: `new AvClient({ entrypoint })` may point a client at another deployment, and
216
+ doing so re-binds nothing — the ClientContract hash still decides whether that deployment accepts the client.
217
+
218
+ ## Type-checking cost in a consumer
219
+
220
+ A consumer pays for the calls it type-checks, not for the schema — under `skipLibCheck: true` (the common default),
221
+ where the named types (`generated/types.d.ts`) and core's derivation are declaration files checked only where read:
222
+ about 33,000 instantiations for a 50-Resource schema and seven typed calls. Under `skipLibCheck: false` every named type
223
+ is resolved where it is declared, about 2,900 instantiations per Resource: about 204,000 for the same
224
+ program, the 500,000 line near 150 Resources.
225
+
226
+ ## Before 1.0
227
+
228
+ - **The `Aventara` wire prefix** of the protocol's headers (`Aventara-Protocol-Version`, `Aventara-Contract-Hash`,
229
+ `Aventara-Request-Id`) may still change before the first stable release. It is one constant in the generated
230
+ `generated/runtime/transport.ts`; regenerating picks up a change.
231
+
232
+ ## License
233
+
234
+ PolyForm Shield 1.0.0 with an additional permission — free to use, including in commercial applications; you may not
235
+ use it to build a product that competes with Aventara itself. See `LICENSE` and `LICENSE-ADDITIONAL-PERMISSION.md`.
@@ -0,0 +1,2 @@
1
+ #!/usr/bin/env node
2
+ export {};
@@ -0,0 +1,15 @@
1
+ #!/usr/bin/env node
2
+ import { refuseUnsupportedNode } from "./node-version.guard.js";
3
+ /**
4
+ * `avclient`'s entry, what the manifest's `bin` names (F-855). It always runs — no
5
+ * `import.meta.main` — and checks this Node against the package's
6
+ * `engines.node` before it loads anything else: the program is imported only
7
+ * once the guard admits this Node, so an older Node meets one sentence and exit
8
+ * 1, never a silent exit 0 or a parse error from a module it cannot run.
9
+ *
10
+ * A promise chain rather than a top-level `await`, which Node 12 cannot parse:
11
+ * this file is read by the Nodes the package does not support.
12
+ */
13
+ if (!refuseUnsupportedNode("avclient", new URL("../package.json", import.meta.url))) {
14
+ void import("./cli.js").then((program) => program.runFromProcess());
15
+ }
@@ -0,0 +1,37 @@
1
+ /**
2
+ * The `avclient` command line: one command, its one flag, and help. Everything
3
+ * the generator needs is in `framework.client.ts` and the `.env` cascade (§15.2),
4
+ * so `generate` takes no flag that would duplicate a config member — a second
5
+ * source of the same fact. `--yes` is not one: it answers the one question the
6
+ * generator asks (architect, 2026-10-04).
7
+ */
8
+ export declare const USAGE = "avclient \u2014 generate a typed Aventara client from a deployed ClientContract.\n\nUsage:\n avclient generate [--yes] Read framework.client.mts (or framework.client.ts) in the\n current directory, fetch <entrypoint>/_contract, and\n write AvClient.ts and generated/ into its generateAt\n directory.\n avclient init [options] Set up this frontend: write framework.client.mts, 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.mts 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\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.mts 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";
9
+ /** `avclient generate --help` (pilot.1): that command's usage alone. */
10
+ export declare const GENERATE_USAGE = "Usage: avclient generate [--yes]\n\nRead framework.client.mts (or framework.client.ts) in the current directory, fetch\n<entrypoint>/_contract, and write AvClient.ts and generated/ into its generateAt\ndirectory.\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";
11
+ /** `avclient init --help` (pilot.1): that command's usage alone. */
12
+ export declare const INIT_USAGE = "Usage: avclient init [options]\n\nSet up this frontend: write framework.client.mts, 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.mts 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\nOn a terminal every unanswered question is asked; anywhere else, pass its flag\nor --yes, or the run stops before writing anything.\n";
13
+ /** `avclient init`'s command line (R4, Q16 rows 12–15). */
14
+ export type ClientInitCommand = {
15
+ readonly command: "init";
16
+ readonly given: Readonly<Partial<Record<"entrypoint" | "envVar" | "generateAt" | "packageManager", string>>>;
17
+ readonly noEnvVar: boolean;
18
+ readonly skipInstall: boolean;
19
+ readonly skipGenerate: boolean;
20
+ readonly yes: boolean;
21
+ };
22
+ /** What the command line asked for. */
23
+ export type CliCommand =
24
+ /** `--help`; with `topic`, `avclient <topic> --help` (pilot.1). */
25
+ {
26
+ readonly command: "help";
27
+ readonly topic?: "generate" | "init";
28
+ } | {
29
+ readonly command: "generate";
30
+ readonly yes: boolean;
31
+ } | ClientInitCommand;
32
+ /** The command line cannot be understood. A refusal: one sentence, exit 1. */
33
+ export declare class CliCommandError extends Error {
34
+ readonly name = "CliCommandError";
35
+ }
36
+ /** @throws CliCommandError when `argv` is not `generate [--yes|-y]`, `--help` or `-h`. */
37
+ export declare function parseCliCommand(argv: readonly string[]): CliCommand;
@@ -0,0 +1,177 @@
1
+ import { CLIENT_CONFIG_FILE, LEGACY_CLIENT_CONFIG_FILE, } from "../config/config.loader.js";
2
+ /**
3
+ * The `avclient` command line: one command, its one flag, and help. Everything
4
+ * the generator needs is in `framework.client.ts` and the `.env` cascade (§15.2),
5
+ * so `generate` takes no flag that would duplicate a config member — a second
6
+ * source of the same fact. `--yes` is not one: it answers the one question the
7
+ * generator asks (architect, 2026-10-04).
8
+ */
9
+ export const USAGE = `avclient — generate a typed Aventara client from a deployed ClientContract.
10
+
11
+ Usage:
12
+ avclient generate [--yes] Read ${CLIENT_CONFIG_FILE} (or ${LEGACY_CLIENT_CONFIG_FILE}) in the
13
+ current directory, fetch <entrypoint>/_contract, and
14
+ write AvClient.ts and generated/ into its generateAt
15
+ directory.
16
+ avclient init [options] Set up this frontend: write ${CLIENT_CONFIG_FILE}, the
17
+ .env entry, the avclient:generate script and the
18
+ @aventara/client devDependency; install; generate.
19
+ --entrypoint <url> The server's entrypoint [http://localhost:3000/api].
20
+ --env-var <NAME> Read it from this variable [AVENTARA_API_URL].
21
+ --no-env-var Write it into ${CLIENT_CONFIG_FILE} as a literal.
22
+ --generate-at <dir> Where the client goes [./src/api].
23
+ --package-manager <npm|pnpm> [the lockfile's, else the launching one, else npm]
24
+ --skip-install Print the install instead of running it.
25
+ --skip-generate Do not generate now.
26
+ -y, --yes Accept every default, and replace differing content.
27
+ avclient --help Print this and exit 0.
28
+ avclient <command> --help Print that command's usage and exit 0.
29
+
30
+ Options:
31
+ -y, --yes Overwrite or remove content in AvClient.ts and generated/ that the
32
+ generator did not produce, without asking. Without it, such content
33
+ is listed and you are asked; where nobody can answer (stdin is not a
34
+ terminal, as in CI), the run is refused and nothing is touched.
35
+
36
+ The generator owns AvClient.ts and generated/ in generateAt, and nothing else
37
+ there: your own files beside them are never read, moved or removed. Generated
38
+ files are replaced on every run; do not edit them.
39
+
40
+ Before ${CLIENT_CONFIG_FILE} is evaluated, the .env cascade is read from the
41
+ current directory, highest precedence first: the process environment,
42
+ .env.<mode>.local, .env.<mode>, .env.local, .env — where mode is NODE_ENV, or
43
+ "development".
44
+ `;
45
+ /** `avclient generate --help` (pilot.1): that command's usage alone. */
46
+ export const GENERATE_USAGE = `Usage: avclient generate [--yes]
47
+
48
+ Read ${CLIENT_CONFIG_FILE} (or ${LEGACY_CLIENT_CONFIG_FILE}) in the current directory, fetch
49
+ <entrypoint>/_contract, and write AvClient.ts and generated/ into its generateAt
50
+ directory.
51
+
52
+ Options:
53
+ -y, --yes Overwrite or remove content in AvClient.ts and generated/ that the
54
+ generator did not produce, without asking. Without it, such content
55
+ is listed and you are asked; where nobody can answer (stdin is not a
56
+ terminal, as in CI), the run is refused and nothing is touched.
57
+
58
+ The generator owns AvClient.ts and generated/ in generateAt, and nothing else
59
+ there: your own files beside them are never read, moved or removed.
60
+
61
+ Before the config is evaluated, the .env cascade is read from the current
62
+ directory, highest precedence first: the process environment, .env.<mode>.local,
63
+ .env.<mode>, .env.local, .env — where mode is NODE_ENV, or "development".
64
+ `;
65
+ /** `avclient init --help` (pilot.1): that command's usage alone. */
66
+ export const INIT_USAGE = `Usage: avclient init [options]
67
+
68
+ Set up this frontend: write ${CLIENT_CONFIG_FILE}, the .env entry, the avclient:generate
69
+ script and the @aventara/client devDependency; install; generate.
70
+
71
+ Options:
72
+ --entrypoint <url> The server's entrypoint [http://localhost:3000/api].
73
+ --env-var <NAME> Read it from this variable [AVENTARA_API_URL].
74
+ --no-env-var Write it into ${CLIENT_CONFIG_FILE} as a literal.
75
+ --generate-at <dir> Where the client goes [./src/api].
76
+ --package-manager <npm|pnpm> [the lockfile's, else the launching one, else npm]
77
+ --skip-install Print the install instead of running it.
78
+ --skip-generate Do not generate now.
79
+ -y, --yes Accept every default, and replace differing content.
80
+
81
+ On a terminal every unanswered question is asked; anywhere else, pass its flag
82
+ or --yes, or the run stops before writing anything.
83
+ `;
84
+ /** The command line cannot be understood. A refusal: one sentence, exit 1. */
85
+ export class CliCommandError extends Error {
86
+ name = "CliCommandError";
87
+ }
88
+ /** @throws CliCommandError when `argv` is not `generate [--yes|-y]`, `--help` or `-h`. */
89
+ export function parseCliCommand(argv) {
90
+ const [command, ...rest] = argv;
91
+ const help = "run `avclient --help` for usage.";
92
+ if (command === undefined) {
93
+ throw new CliCommandError(`a command is required; ${help}`);
94
+ }
95
+ if (command === "--help" || command === "-h") {
96
+ return { command: "help" };
97
+ }
98
+ if ((command === "init" || command === "generate") &&
99
+ (rest.includes("--help") || rest.includes("-h"))) {
100
+ return { command: "help", topic: command };
101
+ }
102
+ if (command === "init") {
103
+ return parseInit(rest, help);
104
+ }
105
+ if (command !== "generate") {
106
+ throw new CliCommandError(`unknown command ${JSON.stringify(command)}; ${help}`);
107
+ }
108
+ let yes = false;
109
+ for (const argument of rest) {
110
+ if ((argument === "--yes" || argument === "-y") && !yes) {
111
+ yes = true;
112
+ continue;
113
+ }
114
+ throw new CliCommandError(`unexpected argument ${JSON.stringify(argument)}: generate takes only --yes, ` +
115
+ `everything else it needs is in ${CLIENT_CONFIG_FILE}; ${help}`);
116
+ }
117
+ return { command: "generate", yes };
118
+ }
119
+ const INIT_VALUES = {
120
+ "--entrypoint": "entrypoint",
121
+ "--env-var": "envVar",
122
+ "--generate-at": "generateAt",
123
+ "--package-manager": "packageManager",
124
+ };
125
+ const INIT_SWITCHES = {
126
+ "--no-env-var": "noEnvVar",
127
+ "--skip-install": "skipInstall",
128
+ "--skip-generate": "skipGenerate",
129
+ "--yes": "yes",
130
+ "-y": "yes",
131
+ };
132
+ function parseInit(argv, help) {
133
+ const given = {};
134
+ const switches = {
135
+ noEnvVar: false,
136
+ skipInstall: false,
137
+ skipGenerate: false,
138
+ yes: false,
139
+ };
140
+ const seen = new Set();
141
+ for (let index = 0; index < argv.length; index += 1) {
142
+ const argument = argv[index];
143
+ const equals = argument.indexOf("=");
144
+ const flag = equals === -1 ? argument : argument.slice(0, equals);
145
+ const inline = equals === -1 ? undefined : argument.slice(equals + 1);
146
+ const value = INIT_VALUES[flag];
147
+ const toggle = INIT_SWITCHES[flag];
148
+ if (value === undefined && toggle === undefined) {
149
+ throw new CliCommandError(`unexpected argument ${JSON.stringify(argument)} for init; ${help}`);
150
+ }
151
+ const canonical = toggle === "yes" ? "--yes" : flag;
152
+ if (seen.has(canonical)) {
153
+ throw new CliCommandError(`${canonical} was given twice; ${help}`);
154
+ }
155
+ seen.add(canonical);
156
+ if (toggle !== undefined) {
157
+ if (inline !== undefined) {
158
+ throw new CliCommandError(`${canonical} takes no value; ${help}`);
159
+ }
160
+ switches[toggle] = true;
161
+ continue;
162
+ }
163
+ const taken = inline ?? argv[index + 1];
164
+ if (taken === undefined ||
165
+ (inline === undefined && taken.startsWith("-"))) {
166
+ throw new CliCommandError(`${canonical} takes a value; ${help}`);
167
+ }
168
+ if (inline === undefined) {
169
+ index += 1;
170
+ }
171
+ given[value] = taken;
172
+ }
173
+ if (switches.noEnvVar && given.envVar !== undefined) {
174
+ throw new CliCommandError(`--env-var and --no-env-var contradict each other; ${help}`);
175
+ }
176
+ return { command: "init", given, ...switches };
177
+ }
@@ -0,0 +1,24 @@
1
+ import type { ProcessEnvInput } from "../config/env.cascade.js";
2
+ import type { CommandRunner } from "../init/command.runner.js";
3
+ /** What `runCli` reads and writes instead of the process globals. */
4
+ export interface CliIo {
5
+ readonly cwd: string;
6
+ readonly env: ProcessEnvInput;
7
+ readonly stdout: (text: string) => void;
8
+ readonly stderr: (text: string) => void;
9
+ /** Whether someone can answer a question: stdin is a terminal. */
10
+ readonly interactive: boolean;
11
+ /** Asks `question`; resolves true for yes. Called only when `interactive`. */
12
+ readonly confirm: (question: string) => Promise<boolean>;
13
+ /** Asks for a line; resolves with what was typed. Called only when `interactive` (`init`). */
14
+ readonly ask?: (prompt: string) => Promise<string>;
15
+ /** Runs a command with stdout piped (`init`'s install); a child process by default. */
16
+ readonly run?: CommandRunner;
17
+ }
18
+ /**
19
+ * `avclient generate` — the generation, the generateAt rule's question, and the
20
+ * report. Shared by `init`, which generates once the project is set up.
21
+ *
22
+ * @throws whatever the generation raises; the caller renders it.
23
+ */
24
+ export declare function runGenerate(io: CliIo, yes: boolean): Promise<number>;
@@ -0,0 +1,41 @@
1
+ import { generateClient } from "../generate.js";
2
+ import { FOREIGN_CONTENT_QUESTION, renderForeignContentCancellation, renderForeignContentWarning, renderGenerationSuccess, renderUpToDate, } from "./generation-success.renderer.js";
3
+ /**
4
+ * `avclient generate` — the generation, the generateAt rule's question, and the
5
+ * report. Shared by `init`, which generates once the project is set up.
6
+ *
7
+ * @throws whatever the generation raises; the caller renders it.
8
+ */
9
+ export async function runGenerate(io, yes) {
10
+ const outcome = await generateClient({
11
+ directory: io.cwd,
12
+ processEnv: io.env,
13
+ overrideForeign: yes,
14
+ });
15
+ if (outcome.kind === "up-to-date") {
16
+ const report = renderUpToDate(outcome);
17
+ io.stderr(report.stderr);
18
+ io.stdout(report.stdout);
19
+ return 0;
20
+ }
21
+ let result;
22
+ if (outcome.kind === "foreign-content") {
23
+ if (!io.interactive) {
24
+ io.stderr(renderForeignContentCancellation(outcome, false));
25
+ return 1;
26
+ }
27
+ io.stderr(renderForeignContentWarning(outcome));
28
+ if (!(await io.confirm(FOREIGN_CONTENT_QUESTION))) {
29
+ io.stderr(renderForeignContentCancellation(outcome, true));
30
+ return 1;
31
+ }
32
+ result = await outcome.proceed();
33
+ }
34
+ else {
35
+ result = outcome;
36
+ }
37
+ const report = renderGenerationSuccess(result);
38
+ io.stderr(report.stderr);
39
+ io.stdout(report.stdout);
40
+ return 0;
41
+ }