@aventara/cli 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,184 @@
1
1
  # @aventara/cli
2
2
 
3
+ ## 1.0.0-rc.13
4
+
5
+ ### Patch Changes
6
+
7
+ - 6c3e25c: `AvZ.oneOf(["USD", "EUR"])` is a string field validator for a closed set of values. It builds an anchored pattern of
8
+ the values, each one's pattern syntax escaped (`"C++"` and `"a.b"` match only themselves), so the Contract gains no
9
+ new check: the generated client runs it like any `AvZ.regex`, and the field's type stays `string`. A refused value
10
+ answers `V1003` with the message `Value must be one of "USD", "EUR".` rather than the pattern. An empty list, or a
11
+ value that is not a string, is refused when the check is built, and the types refuse both. When the types should name
12
+ the set as a union, use a Prisma `enum`: Aventara carries it into the Contract and the generated client as a literal
13
+ union.
14
+ - 33419de: `aventara new` and `aventara init` now work inside a monorepo. Inside a git repository, `aventara new` gives the project no `.git` of its own (`nest new --skip-git`, create-next-app `--disable-git`). Under a pnpm workspace, the scaffold's `allowBuilds` go into the root's `pnpm-workspace.yaml` (confirmed like any change to existing content), the install runs at the root, and the project gets no workspace file or lockfile of its own; a warning says when the root's `packages` do not match the new folder.
15
+
16
+ `aventara new --skip-lint` leaves out `nest new`'s oxlint and prettier (their files, scripts and devDependencies). Inside a workspace whose root has its own Biome, ESLint or Prettier configuration, a one-line hint suggests it.
17
+
18
+ `aventara new` and `aventara init` print the Aventara version they pin, and, when the registry's `rc` or `latest` tag names a newer one, that version and a command that takes it (`pnpm --config.minimum-release-age=0 dlx …`; naming the exact version alone does not pass a delay you set yourself). A release-age delay (pnpm 11 and later default to one day) is the usual reason `@rc` or `@latest` resolves an older CLI. The lookup runs once the command passes its checks, reads the `@aventara` registry's tags only, with a short timeout, and prints nothing more when offline. The frontend's next step now names `@aventara/client` at the CLI's exact version instead of a dist-tag.
19
+
20
+ Inside a pnpm workspace, a folder with its own `pnpm-workspace.yaml` is treated as its own root; the root's `allowBuilds` is edited only for a member its `packages` match, only when something is missing, and only when the file is plain YAML the CLI reads exactly, with `allowBuilds` as a block of `name: true` or `name: false` entries (otherwise nothing is written and the lines to add are printed); the edit is read back and refused if any entry already there would change, keeping explicit `false` entries, comments and CRLF line endings.
21
+
22
+ - 5cdc922: The agent skill spells its commands for the project's package manager: `pnpm run aventara:prepare`, `pnpm exec tsc`
23
+ and `pnpm dlx @aventara/cli@… skill` in a pnpm project, the npm forms otherwise. `aventara new` and `aventara init` use
24
+ the package manager they install with. `aventara skill` reads the nearest lockfile, up to the repository's root, so a
25
+ server app inside a pnpm workspace gets the pnpm forms. When the nearest lockfile is neither npm's nor pnpm's (a yarn
26
+ or bun one), or there is none, it uses the package manager that ran it if that is pnpm, and npm's forms otherwise; the
27
+ CLI supports npm and pnpm only. `reference/errors.md` follows too.
28
+
29
+ The skill's guidance also covers more of what agents got wrong:
30
+
31
+ - A rule that reads the database is re-checked in `hooks.before` under `forceTransaction`, or made the write's own
32
+ condition with `update.first`: a guard runs before the transaction opens, so its check can be stale at commit. A new
33
+ compare-and-set recipe shows it.
34
+ - Nest guards, interceptors and pipes (`APP_GUARD`, Passport's `AuthGuard`, `@nestjs/throttler`) never run on
35
+ Aventara's routes; only `app.use` middleware does. There is no built-in rate limit.
36
+ - Logic that is not one of a model's operations still belongs in your own Nest controller or service.
37
+ - `ctx` calls and `ctx.$args` are typed without configuration: narrow with a type guard there, never cast.
38
+ - A computed value a validator refuses answers `500 A3000` when the caller's layer hides the field.
39
+ - `forceTransaction` needs `transactionsStyle: "interactive"`; `maxTransactionOperations` counts reads too.
40
+ - New sections: scalars (wire forms, `bigint` and `Date` from a controller), the adapter's transaction options and
41
+ the SQLite and PostgreSQL differences, server surfaces (`application`, `serverClient`, `client`, the three `Tx`
42
+ builders), testing (Nest testing module, `framework.client`, `framework.clientTx`, `createFakeAdapter`), the query
43
+ language next to Prisma's, Resource transaction stages with an example, deploying, the full stage order, what
44
+ changes the contract hash, `diagnostics` behind an `A3000`, and where the full docs are for agents.
45
+ - The recipes no longer break on a `String` id.
46
+
47
+ - 5c65195: `aventara new` and `aventara init` now add a `typecheck` script, so a monorepo's `turbo typecheck` reaches the new project: `tsc --noEmit` in a NestJS project, `next typegen && tsc --noEmit` in a Next.js one. A `typecheck` script the project already has is kept as it is. The agent skill's "Before you finish" runs it.
48
+
49
+ `aventara-build` no longer leaves a project whose `tsc` reports `Type 'AvModel' does not satisfy the constraint 'AvAdapterModel'` in every Resource file, which hid the real error. A run that fails (or is interrupted) now changes no generated file, the Next.js browser client included: each keeps what the last successful run wrote. The failure prints before the run's other lines. Each file is replaced in one step, and a second run started while one is writing the same project is refused. When the configuration's computed fields cannot be read and the project does not type-check (read as `tsc -p .` reads it), the build prints those type errors first and keeps `computed-fields.generated.ts` as it was, instead of rewriting it to state nothing. A file that does state nothing (a configuration that is not a literal) now types its model correctly. If you see that cascade from an older build, run `aventara:prepare` again and fix the first error it prints.
50
+
51
+ - 2aaa27d: A stage's `ctx` create no longer has to write a field that the root's or the `application` overlay's `compute.create`
52
+ fills. A `ctx` call runs that compute, and the generated types already state which fields it fills (the
53
+ `computed-fields.generated.ts` file `aventara-build` writes), so `ctx.Payment.create.one({ data })` type-checks without
54
+ the computed `employeeId`. A field that only the `client` or `agent` overlay computes is still required, because that
55
+ compute does not run for a `ctx` call. A model whose types state no computed fields keeps every requirement. The agent
56
+ skill says so.
57
+ - 2aaa27d: Exemptions take one more value, `"read"`, on a field and on a Resource's `fields`. It names every read capability of
58
+ the field that the layer above offers: a scalar's `select`, `filter`, `order` and `orderNulls`, a relation's `select`,
59
+ `include`, `filter`, `order`, `pagination` and `reducers`. On `fields` it applies to every field of the Resource.
60
+
61
+ - Under `exposed: false` it exposes reading and no write, so a read-only Resource no longer needs every field listed:
62
+ `Category: { operations: { find: true }, fields: "read" }`.
63
+ - Under `exposed: true` it hides reading and keeps the writes, which is the write-only field:
64
+ `passwordHash: "read"`.
65
+
66
+ It compiles to the same Contracts as the capability lists it stands for, hash included. Like every exemption it only
67
+ narrows: it never brings back a capability the layer above hid, and naming a field the layer above hid is refused as
68
+ before. `fields: "read"` names relations too, so a relation into a Resource the layer leaves out still fails with
69
+ `COMPILER_DANGLING_RELATION`. The agent skill documents it.
70
+
71
+ Read-only now means no nested write either. A relation does not write rows of a Resource its layer cannot write: one
72
+ that creates nothing (no create or upsert operation, and no field writable on create) takes no `$create`,
73
+ `$connectOrCreate` or `$upsert`; one that updates nothing (no update or upsert operation, and no field writable on
74
+ update) takes no `$update` or `$upsert`; one with no delete operation takes no `$delete`. The fields count with the
75
+ operations for creates and updates, so a Resource whose create operation is hidden but whose fields stay writable on
76
+ create is still created through its parent. Relation fields count too, even link-only (`$connect`): if a Resource must
77
+ create nothing, hide its relations' `create` as well. This holds however the Resource was made read-only, `"read"` or listed
78
+ capabilities, and also when it has no required field, where a nested `$create: {}` was accepted before. `$connect`,
79
+ `$disconnect` and `$set` stay.
80
+
81
+ What happens follows the layer's `exposed`. Under `exposed: false` the relation's directives are the ones the layer
82
+ grants (by `true` on the Resource or the field, by the relation's `create` or `update`, or by naming the directive), so
83
+ such a grant is refused at startup with `COMPILER_READ_ONLY_NESTED_WRITE`: let the relation only link,
84
+ `author: { select: true, create: ["$connect"] }`. Under `exposed: true` the relation inherits its directives, so they
85
+ are dropped from it and startup succeeds; a call that sends one gets `V1006`, or "Relation … is not writable during
86
+ update." when the relation's update had nothing else. Dropping one can leave another Resource with nothing writable, and
87
+ the nested writes into it go the same way. A layer with `exposed: false` below that names a dropped directive, or grants
88
+ a member that was removed, is refused with `COMPILER_READ_ONLY_NESTED_WRITE`.
89
+
90
+ A configuration under `exposed: true` that hides a Resource's deletes, or every way to create or update it, therefore
91
+ serves Contracts with fewer directives on the relations into it, and its ClientContract hash changes: regenerate the
92
+ frontend client. Such a configuration was refused at startup before when a hidden field was required, and otherwise
93
+ accepted nested creates.
94
+
95
+ - 6c3e25c: Field validators now judge every value the caller sent before write compute, and each value once. A value the caller
96
+ sent is judged after the guards, the pipes and its field pipes, before any compute stage runs, and that includes a
97
+ field that has a `compute.create` or `compute.update` stage of its own. After compute, only a value compute set or
98
+ changed is judged: a field the caller left out that compute filled, or a caller's value compute replaced. A compute
99
+ stage that returns `undefined` or the caller's own value changes nothing, so that value is not judged again. The order
100
+ is now guards → pipes → validators of the caller's values → write compute → validators of what compute set or changed
101
+ → hooks.
102
+
103
+ A compute stage never sees a value its own validators refuse. A caller value that also breaks a compute stage (a
104
+ quantity that overflows a total, an unknown currency code handed to `Intl.NumberFormat`) answered `500 A3000` before
105
+ this release; it now answers `422 A2004` with the issue at the field, and no compute stage runs. A bad caller value is
106
+ refused even when compute would have replaced it: before this release that value was dropped silently. Codes and
107
+ issue shapes are unchanged. One answer no longer mixes the two kinds: when a caller value is refused, the computed
108
+ values are not judged yet. In a transaction plan or a forced call, every literal value is judged before the
109
+ transaction opens; a resolved `$ref` value and a value a Resource transaction pipe changed are judged at their step,
110
+ before write compute. The agent skill's stage order says so.
111
+
112
+ - 6c3e25c: A field the configuration adds (`lifecycle: ["VIRTUAL"]`) with `nullable: false` is now a create requirement, like the
113
+ adapter's own non-null columns: in each layer that lets a caller write it at create, and where no `compute.create` (the
114
+ root's or that layer's) fills it, it joins the Contract's `createRequirements.required`. A create without it answers
115
+ `422 A2004` with `V1000` at the field, before any guard runs, and `framework.application`, `framework.client` and the
116
+ generated client type it as required. A layer that hides the field creates without it, and an update is unchanged. A
117
+ nullable added field stays optional. A `compute.create` stage that fills such a field and returns `undefined` answers
118
+ `500 A3000` with an `AvComputedValueError` in the `diagnostics` sink, as it does for a required database column, and
119
+ nothing is written.
120
+
121
+ This changes the Client Contract, and so its hash, for every configuration with such a field: regenerate the frontend
122
+ client. A configuration that sent a `nullable: false` virtual input only sometimes must now send it on every create, or
123
+ state it `nullable: true`.
124
+
125
+ - 5571ebb: `aventara new --host next` runs `create-next-app@16` instead of `@latest`, so a new Next.js major cannot scaffold an
126
+ app outside `@aventara/next`'s supported range.
127
+
128
+ The newest-release lookup of `aventara new` and `aventara init` now asks the registry npm would install
129
+ `@aventara/cli` from, also when `aventara` runs on its own rather than through npm or pnpm: the `@aventara:registry`
130
+ setting, then `registry`, from the environment, the project's `.npmrc` or your user `.npmrc`. Only those two settings
131
+ are read from the files; nothing else in them, auth tokens included, is read or printed. A `${…}` variable in the
132
+ project's `.npmrc` is not expanded (that value is skipped), and a file that is not a regular file or is over 64 KiB is
133
+ skipped. The lookup keeps its short
134
+ timeout and still says nothing when it cannot reach the registry.
135
+
136
+ - 73aa797: A generated client takes three hooks, one function each: `new AvClient({ onCall, onSuccess, onError })`. They see
137
+ every call the client makes: an operation, a cached read, a transaction (once, whatever its steps) and a `$access`
138
+ call, as an `AvCall` (its `kind`, its `label`, the operation's `resource`, `family` and `variant` or the `$access`
139
+ verb, and its `requestId` when it sends one; never its arguments).
140
+
141
+ - `onCall(call)` runs when the call is made, before it is sent and before grouping. It may return, or resolve to,
142
+ `AvCallAdditions` for that call's request: `headers`, `credentials`, `cache`, `keepalive`, `priority` and
143
+ `referrerPolicy`. A header the client sets itself (`Content-Type`, `Accept`, the three `Aventara-*` headers), in
144
+ its canonical or lower-case spelling, and `method`, `body`, `signal`, `redirect` and `mode` are type errors, each
145
+ typed as the sentence that refuses it. At runtime such a header in any spelling, a name that is not a valid header
146
+ 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
147
+ `AVENTARA_CLIENT_HOOK_HEADER_DROPPED`), once per header name for each `onCall`. The call's own `headers` option
148
+ wins over `onCall`'s.
149
+ - Calls share one `POST /_all` only when their additions are equal (in any order). An async `onCall` holds back only
150
+ its own call: the call is sent when it settles, with the calls queued then. Calls group when their `onCall` settles
151
+ together with equal additions; an `onCall` doing its own I/O per call sends each call alone, and one awaiting a
152
+ 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
153
+ call made from inside a hook never waits on the call that ran it.
154
+ - `onSuccess(call, result)` and `onError(call, error)` are sinks, typed as returning `void`, so any function fits
155
+ (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`
156
+ sees a refusal, a validation error, a transport failure and an abort, sent alone or inside an Operation Set.
157
+ - A hook that throws or rejects is reported once as an `AventaraWarning` (code `AVENTARA_CLIENT_HOOK_THREW`, or
158
+ `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
159
+ the call, the calls sent with it or the process.
160
+
161
+ `fetch` stays a transport swap: it must return the `Response` unread. A token every call needs now goes in `onCall`,
162
+ read through one shared in-flight promise so the calls still group; the skill and the README say so.
163
+
164
+ - e6495a0: The agent skills cover more of this release. The server skill says that a `state` rejecting with a `401` or `403`
165
+ status answers `A4000` or `A4001`, that `forceTransaction` also runs under `transactionsStyle: "server"` (the way to
166
+ keep forced writes atomic without client plans), that an enum field takes no validator, and how SQLite's one
167
+ connection makes a plain operation wait for an open transaction (`AVENTARA_PRISMA_SQLITE_TRANSACTION_JOINED`, the
168
+ `driver` check). It no longer says that a `ctx` create must write a field the root's or the `application` overlay's
169
+ `compute.create` fills. The client guide, in both skills, adds `AvCall` and `AvCallAdditions` for typing a hook, that
170
+ a hanging `onCall` stalls only its own call, the `AVENTARA_CLIENT_OPTIONS_NO_DEFAULT` warning, the refusal of an
171
+ entrypoint carrying credentials, and how a client of your own computes a transaction step's `fingerprint` with
172
+ `AvProtocol.fingerprint`.
173
+
174
+ - a9260d2: The agent skills cover what an agent needs without opening `node_modules`:
175
+
176
+ - 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.
177
+ - 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.
178
+ - 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`).
179
+ - 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`.
180
+ - `CONFIG_FORBIDDEN_IN_OVERLAY` in the error catalog now names `managedElsewhere` as well as a field `definition`.
181
+
3
182
  ## 1.0.0-rc.12
4
183
 
5
184
  ### Patch Changes
@@ -22,7 +201,7 @@
22
201
  compound identifier goes with any one of its fields, and the generated client and the server façades no longer offer
23
202
  it. A layer that keeps a unique variant with no filterable identifier left fails at startup with
24
203
  `COMPILER_MISSING_IDENTIFIER`, as hiding the identifier already did: under `exposed: false`, pick `id: ["select",
25
- "filter"]`, not `id: ["select"]`, to keep `find.unique`. The agent skill's Pick example says so.
204
+ "filter"]`, not `id: ["select"]`, to keep `find.unique`. The agent skill's Pick example says so.
26
205
 
27
206
  ## 1.0.0-rc.11
28
207
 
package/README.md CHANGED
@@ -26,7 +26,7 @@ without `.env` (it is in `.gitignore`). No commit is made, and one line says why
26
26
 
27
27
  ### `aventara new <name> --host next`
28
28
 
29
- Runs `create-next-app@latest <name> --ts --app --src-dir --import-alias "@/*" --use-<npm|pnpm> --skip-install`, then
29
+ Runs `create-next-app@16 <name> --ts --app --src-dir --import-alias "@/*" --use-<npm|pnpm> --skip-install`, then
30
30
  `aventara init`'s pipeline over the new app, then the install. create-next-app asks its own questions on your terminal;
31
31
  with `--yes` it takes its defaults instead (and is given `--yes` too), which is also what a run with no terminal needs.
32
32
  Its own flags go after `--`, as they are: `aventara new my-app --host next --yes -- --tailwind --eslint`. `--skip-git`
@@ -10,7 +10,10 @@ export async function confirmConflicts(conflicts, consent) {
10
10
  }
11
11
  const them = conflicts.length === 1 ? "it" : "them";
12
12
  if (!consent.prompter.interactive) {
13
- throw new AvConflictsNotConfirmedError(`not initializing, and nothing was touched: ${conflicts.join(", ")} already ${conflicts.length === 1 ? "has" : "have"} other content and nobody can be asked (stdin is not a terminal); run \`aventara init --yes\` to replace ${them}`);
13
+ const rerun = consent.command === "init"
14
+ ? "run `aventara init --yes`"
15
+ : "run `aventara new` again with `--yes`";
16
+ throw new AvConflictsNotConfirmedError(`not ${consent.command === "init" ? "initializing" : "creating the project"}, and nothing was touched: ${conflicts.join(", ")} already ${conflicts.length === 1 ? "has" : "have"} other content and nobody can be asked (stdin is not a terminal); ${rerun} to replace ${them}`);
14
17
  }
15
18
  consent.prompter.say(`aventara: warning: ${conflictWarning(conflicts)}.`);
16
19
  const answer = await consent.prompter.ask({
@@ -0,0 +1,32 @@
1
+ import { readFileSync, rmSync, writeFileSync } from "node:fs";
2
+ import path from "node:path";
3
+ const FILES = [".oxlintrc.json", ".prettierrc"];
4
+ const SCRIPTS = ["lint", "format"];
5
+ const DEV_DEPENDENCIES = ["oxlint", "oxlint-tsgolint", "prettier"];
6
+ export function stripLintTooling(directory) {
7
+ const removed = [];
8
+ for (const file of FILES) {
9
+ try {
10
+ rmSync(path.join(directory, file));
11
+ removed.push(file);
12
+ }
13
+ catch {
14
+ }
15
+ }
16
+ const manifestFile = path.join(directory, "package.json");
17
+ const manifest = JSON.parse(readFileSync(manifestFile, "utf8"));
18
+ for (const [field, names] of [
19
+ ["scripts", SCRIPTS],
20
+ ["devDependencies", DEV_DEPENDENCIES],
21
+ ]) {
22
+ const record = manifest[field];
23
+ for (const name of names) {
24
+ if (record !== undefined && name in record) {
25
+ delete record[name];
26
+ removed.push(`package.json's ${field}["${name}"]`);
27
+ }
28
+ }
29
+ }
30
+ writeFileSync(manifestFile, `${JSON.stringify(manifest, null, 2)}\n`);
31
+ return removed;
32
+ }
@@ -1,3 +1,4 @@
1
+ import { readPnpmWorkspace, } from "./pnpm-workspace.reader.js";
1
2
  function sorted(record) {
2
3
  return Object.fromEntries(Object.entries(record).sort(([left], [right]) => left < right ? -1 : left > right ? 1 : 0));
3
4
  }
@@ -33,6 +34,9 @@ export function mergePackageManifest(text, changes, replaceConflicts) {
33
34
  }
34
35
  scripts[name] = command;
35
36
  }
37
+ for (const [name, command] of Object.entries(changes.defaults)) {
38
+ scripts[name] ??= command;
39
+ }
36
40
  for (const [name, { from, to }] of Object.entries(changes.rewrites)) {
37
41
  const existing = scripts[name];
38
42
  if (existing === undefined || existing === to) {
@@ -99,31 +103,70 @@ export function mergeGitignore(text, additions) {
99
103
  conflicts: [],
100
104
  };
101
105
  }
106
+ export function allowBuildsLines(names) {
107
+ return ["allowBuilds:", allowBuildsEntries(names)].join("\n");
108
+ }
109
+ export function allowBuildsGuidance(text, names) {
110
+ return text !== undefined &&
111
+ /^(?:\?[ ]*)?(?:allowBuilds|"allowBuilds"|'allowBuilds')[ ]*(?::|$)/m.test(text.replace(/^\uFEFF/, ""))
112
+ ? `add whichever of these are missing under your \`allowBuilds:\` (an explicit \`false\` is yours to keep):\n${allowBuildsEntries(names)}`
113
+ : `allow these packages' install scripts in it:\n${allowBuildsLines(names)}`;
114
+ }
115
+ export function allowBuildsEntries(names) {
116
+ return names.map((name) => ` ${quotedYamlKey(name)}: true`).join("\n");
117
+ }
118
+ function quotedYamlKey(name) {
119
+ return /^[A-Za-z0-9_-][A-Za-z0-9._-]*$/.test(name) ? name : `"${name}"`;
120
+ }
121
+ function onlyInserted(before, after, added) {
122
+ const was = before.allowBuilds?.entries ?? new Map();
123
+ const now = after.allowBuilds?.entries;
124
+ if (after.bom !== before.bom ||
125
+ after.eol !== before.eol ||
126
+ now === undefined ||
127
+ now.size !== was.size + added.length ||
128
+ [...was].some(([name, value]) => now.get(name) !== value) ||
129
+ added.some((name) => was.has(name) || now.get(name) !== true)) {
130
+ return false;
131
+ }
132
+ const inserted = added.length + (before.allowBuilds === undefined ? 1 : 0);
133
+ const at = before.allowBuilds === undefined
134
+ ? before.lines.length
135
+ : before.allowBuilds.end;
136
+ const kept = [
137
+ ...after.lines.slice(0, at),
138
+ ...after.lines.slice(at + inserted),
139
+ ];
140
+ return (after.lines.length === before.lines.length + inserted &&
141
+ kept.every((line, index) => line === before.lines[index]));
142
+ }
102
143
  export function mergePnpmWorkspace(text, packages) {
103
- const quoted = (name) => /^[A-Za-z0-9_-][A-Za-z0-9._-]*$/.test(name) ? name : `"${name}"`;
104
- const lines = text === undefined || text === ""
105
- ? []
106
- : text.replace(/\n$/, "").split("\n");
107
- const header = lines.findIndex((line) => /^allowBuilds:\s*$/.test(line));
108
- const present = new Set();
109
- if (header !== -1) {
110
- for (let at = header + 1; at < lines.length && /^\s+\S/.test(lines[at]); at += 1) {
111
- const name = /^\s+["']?([^"':]+)["']?\s*:/.exec(lines[at])?.[1];
112
- if (name !== undefined) {
113
- present.add(name);
114
- }
115
- }
144
+ const unsafe = (why) => ({
145
+ text: text ?? "",
146
+ conflicts: [],
147
+ added: [],
148
+ unsafe: why,
149
+ });
150
+ const reading = readPnpmWorkspace(text ?? "");
151
+ if ("unsafe" in reading) {
152
+ return unsafe(reading.unsafe);
116
153
  }
117
- const missing = packages.filter((name) => !present.has(name));
154
+ const { document } = reading;
155
+ const block = document.allowBuilds;
156
+ const missing = [...new Set(packages)].filter((name) => block?.entries.has(name) !== true);
118
157
  if (missing.length === 0 && text !== undefined) {
119
- return { text, conflicts: [] };
120
- }
121
- const entries = missing.map((name) => ` ${quoted(name)}: true`);
122
- if (header === -1) {
123
- lines.push("allowBuilds:", ...entries);
158
+ return { text, conflicts: [], added: [] };
124
159
  }
125
- else {
126
- lines.splice(header + 1, 0, ...entries);
160
+ const indent = block?.indent ?? " ";
161
+ const entries = missing.map((name) => `${indent}${quotedYamlKey(name)}: true`);
162
+ const { lines } = document;
163
+ const merged = block === undefined
164
+ ? [...lines, "allowBuilds:", ...entries]
165
+ : [...lines.slice(0, block.end), ...entries, ...lines.slice(block.end)];
166
+ const after = `${document.bom ? "\uFEFF" : ""}${merged.join(document.eol)}${document.eol}`;
167
+ const check = readPnpmWorkspace(after);
168
+ if ("unsafe" in check || !onlyInserted(document, check.document, missing)) {
169
+ return unsafe("adding to its allowBuilds would not keep it valid");
127
170
  }
128
- return { text: `${lines.join("\n")}\n`, conflicts: [] };
171
+ return { text: after, conflicts: [], added: missing };
129
172
  }
@@ -0,0 +1,179 @@
1
+ const TOP_LEVEL_KEY = /^([A-Za-z_][A-Za-z0-9_-]*):(?=[ ]|$)(.*)$/;
2
+ const ENTRY = /^([ ]+)(?:"([^"\\]*)"|'([^']*)'|([A-Za-z0-9_][A-Za-z0-9@/._-]*)):[ ]+(true|false)(?:[ ]+#.*)?[ ]*$/;
3
+ const NOT_A_STRING = /^(?:true|false|null|yes|no|on|off|y|n|[0-9][0-9._a-fA-FoOxX+-]*)$/i;
4
+ const DOUBLE_QUOTED = /^"(?:[^"\\]|\\.)*"[ ]*(?:#.*)?$/;
5
+ const SINGLE_QUOTED = /^'(?:[^']|'')*'[ ]*(?:#.*)?$/;
6
+ function flowClosesOnItsLine(value) {
7
+ let depth = 0;
8
+ let quote;
9
+ for (let at = 0; at < value.length; at += 1) {
10
+ const char = value[at];
11
+ if (quote !== undefined) {
12
+ if (quote === '"' && char === "\\") {
13
+ at += 1;
14
+ }
15
+ else if (char === quote) {
16
+ quote = undefined;
17
+ }
18
+ }
19
+ else if (char === '"' || char === "'") {
20
+ quote = char;
21
+ }
22
+ else if (char === "[" || char === "{") {
23
+ depth += 1;
24
+ }
25
+ else if (char === "]" || char === "}") {
26
+ depth -= 1;
27
+ if (depth === 0) {
28
+ return /^[ ]*(?:#.*)?$/.test(value.slice(at + 1));
29
+ }
30
+ }
31
+ }
32
+ return false;
33
+ }
34
+ const PROPERTY = /^(?:&[^ ,[\]{}]+|!(?:<[^>]*>|[^ ]*))(?:[ ]+|$)/;
35
+ function endsOnItsLine(inline) {
36
+ let value = inline;
37
+ for (let property = PROPERTY.exec(value); property !== null;) {
38
+ value = value.slice(property[0].length);
39
+ property = PROPERTY.exec(value);
40
+ }
41
+ switch (value[0]) {
42
+ case '"':
43
+ return DOUBLE_QUOTED.test(value);
44
+ case "'":
45
+ return SINGLE_QUOTED.test(value);
46
+ case "[":
47
+ case "{":
48
+ return flowClosesOnItsLine(value);
49
+ default:
50
+ return true;
51
+ }
52
+ }
53
+ export function readPnpmWorkspace(text) {
54
+ const unsafe = (why) => ({ unsafe: why });
55
+ const bom = text.startsWith("\uFEFF");
56
+ const body = bom ? text.slice(1) : text;
57
+ if (body.includes("\uFEFF")) {
58
+ return unsafe("it holds a byte-order mark after its start");
59
+ }
60
+ const crlf = body.includes("\r\n");
61
+ if (crlf ? /\r(?!\n)|(?<!\r)\n/.test(body) : body.includes("\r")) {
62
+ return unsafe("it mixes line endings");
63
+ }
64
+ const lines = body === "" ? [] : body.replace(/\r?\n$/, "").split(/\r?\n/);
65
+ const keys = new Set();
66
+ let current;
67
+ let started = false;
68
+ let directive = false;
69
+ let header;
70
+ let inBlock = false;
71
+ let end;
72
+ let indent;
73
+ const entries = new Map();
74
+ for (const [at, line] of lines.entries()) {
75
+ const number = at + 1;
76
+ if (/^[ ]*\t/.test(line)) {
77
+ return unsafe(`line ${number} indents with a tab`);
78
+ }
79
+ if (/^[ ]*(?:#.*)?$/.test(line)) {
80
+ continue;
81
+ }
82
+ if (line.startsWith(" ")) {
83
+ if (current === undefined) {
84
+ return unsafe(`line ${number} is indented before any top-level key`);
85
+ }
86
+ if (!inBlock) {
87
+ continue;
88
+ }
89
+ const entry = ENTRY.exec(line);
90
+ const name = entry?.[2] ?? entry?.[3] ?? entry?.[4];
91
+ if (entry === null ||
92
+ name === undefined ||
93
+ (entry[4] !== undefined && NOT_A_STRING.test(name))) {
94
+ return unsafe(`line ${number} in its allowBuilds is not a \`name: true\` or \`name: false\` entry`);
95
+ }
96
+ if (indent !== undefined && entry[1] !== indent) {
97
+ return unsafe("its allowBuilds entries are not at one indentation");
98
+ }
99
+ if (entries.has(name)) {
100
+ return unsafe(`its allowBuilds states ${name} more than once`);
101
+ }
102
+ indent = entry[1];
103
+ entries.set(name, entry[5] === "true");
104
+ end = at + 1;
105
+ continue;
106
+ }
107
+ inBlock = false;
108
+ if (line.startsWith("%")) {
109
+ if (started) {
110
+ return unsafe(`line ${number} is a directive after the document's start`);
111
+ }
112
+ directive = true;
113
+ continue;
114
+ }
115
+ if (/^---(?:[ ]|$)/.test(line)) {
116
+ if (started) {
117
+ return unsafe("it holds more than one YAML document");
118
+ }
119
+ if (!/^---[ ]*(?:#.*)?$/.test(line)) {
120
+ return unsafe(`line ${number} has content after its \`---\``);
121
+ }
122
+ started = true;
123
+ directive = false;
124
+ continue;
125
+ }
126
+ if (/^\.\.\.(?:[ ]|$)/.test(line)) {
127
+ return unsafe(`line ${number} ends the document (\`...\`)`);
128
+ }
129
+ if (directive) {
130
+ return unsafe("its directive is not followed by `---`");
131
+ }
132
+ started = true;
133
+ if (/^-(?:[ ]|$)/.test(line)) {
134
+ if (current === undefined || current.inline) {
135
+ return unsafe(`line ${number} is a sequence item outside a key's value`);
136
+ }
137
+ if (current.name === "allowBuilds") {
138
+ return unsafe("its allowBuilds is not a block of one entry per line");
139
+ }
140
+ continue;
141
+ }
142
+ const key = TOP_LEVEL_KEY.exec(line);
143
+ if (key === null) {
144
+ return unsafe(`its line ${number}, \`${line.trim()}\`, is not a plain top-level key`);
145
+ }
146
+ const name = key[1];
147
+ const value = key[2].trim();
148
+ const inline = value !== "" && !value.startsWith("#");
149
+ if (keys.has(name)) {
150
+ return unsafe(`it states ${name} more than once`);
151
+ }
152
+ keys.add(name);
153
+ if (inline && !endsOnItsLine(value)) {
154
+ return unsafe(`line ${number}'s value continues past its line`);
155
+ }
156
+ if (name === "allowBuilds") {
157
+ if (inline) {
158
+ return unsafe("its allowBuilds is not a block of one entry per line");
159
+ }
160
+ header = at;
161
+ end = at + 1;
162
+ inBlock = true;
163
+ }
164
+ current = { name, inline };
165
+ }
166
+ if (directive) {
167
+ return unsafe("its directive is not followed by `---`");
168
+ }
169
+ return {
170
+ document: {
171
+ bom,
172
+ eol: crlf ? "\r\n" : "\n",
173
+ lines,
174
+ allowBuilds: header === undefined
175
+ ? undefined
176
+ : { header, end: end, indent, entries },
177
+ },
178
+ };
179
+ }
package/dist/cli.js CHANGED
@@ -2,7 +2,10 @@ import { readFileSync } from "node:fs";
2
2
  import { AvConflictsNotConfirmedError } from "./apply/conflict.confirmer.js";
3
3
  import { ADAPTER_CATALOG } from "./catalog/adapter.catalog.generated.js";
4
4
  import { AvCliCommandError, commandUsage, parseCliCommand, usage, } from "./command/command.parser.js";
5
+ import { detectPackageManager } from "./project/package-manager.detector.js";
5
6
  import { AvProjectRefusedError } from "./project/project.inspector.js";
7
+ import { registryNewestRelease, releaseLines, } from "./release/newest-release.reader.js";
8
+ import { npmRegistryOf } from "./release/npm-registry.resolver.js";
6
9
  import { runCommand } from "./run/command.runner.js";
7
10
  import { AvInstallFailedError, runInit } from "./run/init.orchestrator.js";
8
11
  import { runNew } from "./run/new.orchestrator.js";
@@ -43,11 +46,21 @@ export async function runCli(argv, io) {
43
46
  if (command.command === "skill") {
44
47
  return runSkill(command, {
45
48
  directory: io.cwd,
49
+ env: io.env,
46
50
  stdout: io.stdout,
47
51
  stderr: io.stderr,
48
52
  cliVersion: ownVersion(),
49
53
  });
50
54
  }
55
+ const scaffoldCommand = command.command;
56
+ const release = () => releaseLines({
57
+ pinned: ownVersion(),
58
+ command: scaffoldCommand,
59
+ packageManager: detectPackageManager({
60
+ userAgent: io.env.npm_config_user_agent,
61
+ }),
62
+ read: io.newestRelease,
63
+ });
51
64
  if (command.command === "init") {
52
65
  return await runInit(command, {
53
66
  directory: io.cwd,
@@ -59,6 +72,7 @@ export async function runCli(argv, io) {
59
72
  catalog: io.catalog ?? ADAPTER_CATALOG,
60
73
  aventaraVersion: ownVersion(),
61
74
  progressMode: io.progress ?? "off",
75
+ release,
62
76
  });
63
77
  }
64
78
  return await runNew(command, {
@@ -71,6 +85,7 @@ export async function runCli(argv, io) {
71
85
  catalog: io.catalog ?? ADAPTER_CATALOG,
72
86
  aventaraVersion: ownVersion(),
73
87
  progressMode: io.progress ?? "off",
88
+ release,
74
89
  });
75
90
  }
76
91
  catch (error) {
@@ -102,6 +117,7 @@ export async function runFromProcess() {
102
117
  },
103
118
  prompter,
104
119
  progress: progressModeOf(process.stderr.isTTY, process.env),
120
+ newestRelease: registryNewestRelease(npmRegistryOf({ cwd: process.cwd(), env: process.env })),
105
121
  });
106
122
  }
107
123
  finally {
@@ -16,6 +16,12 @@ const OPTIONS = [
16
16
  commands: ["new"],
17
17
  summary: "create no git repository (passed to nest new)",
18
18
  },
19
+ {
20
+ flag: "--skip-lint",
21
+ key: "skipLint",
22
+ commands: ["new"],
23
+ summary: "leave out nest new's linter and formatter (oxlint, prettier, their files and scripts)",
24
+ },
19
25
  {
20
26
  flag: "--yes",
21
27
  alias: "-y",
@@ -236,6 +242,7 @@ export function parseCliCommand(argv) {
236
242
  given,
237
243
  skipInstall: options.skipInstall === true,
238
244
  skipGit: options.skipGit === true,
245
+ ...(options.skipLint === true ? { skipLint: true } : {}),
239
246
  yes: options.yes === true,
240
247
  ...(options.verbose === true ? { verbose: true } : {}),
241
248
  ...(host === undefined ? {} : { host }),
@@ -38,9 +38,8 @@ const E2E_SCRIPTS = {
38
38
  };
39
39
  const NEST = hostOf("nest");
40
40
  export function frontendInitCommand(packageManager, cliVersion) {
41
- const tag = /^\d+\.\d+\.\d+-([0-9A-Za-z-]+)/.exec(cliVersion)?.[1];
42
41
  const runner = packageManager === "pnpm" ? "pnpm dlx" : "npx";
43
- return `${runner} @aventara/client${tag === undefined ? "" : `@${tag}`} init`;
42
+ return `${runner} @aventara/client@${cliVersion} init`;
44
43
  }
45
44
  export function planNestHost(builder, input) {
46
45
  const { inspection, scaffold, packageManager } = input;
@@ -102,6 +101,7 @@ export function planNestHost(builder, input) {
102
101
  `pre${script}`,
103
102
  `${packageManager} run ${PREPARE_SCRIPT}`,
104
103
  ])),
104
+ typecheck: "tsc --noEmit",
105
105
  rewrites: inspection.usesNestConfig
106
106
  ? {}
107
107
  : {
@@ -122,6 +122,7 @@ export function planNextHost(builder, input) {
122
122
  },
123
123
  build: "aventara-build --client",
124
124
  scripts: { prebuild: `${input.packageManager} run ${PREPARE_SCRIPT}` },
125
+ typecheck: "next typegen && tsc --noEmit",
125
126
  rewrites: {},
126
127
  gitignore: [
127
128
  `/${layout.generated}/`,