@aventara/cli 1.0.0-rc.12 → 1.0.0-rc.14

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,204 @@
1
1
  # @aventara/cli
2
2
 
3
+ ## 1.0.0-rc.14
4
+
5
+ ### Patch Changes
6
+
7
+ - 1c5eaa3: The agent skill's stage order in `reference/stages.md` now lists every stage `SKILL.md` and the pipelines docs list: policy admission before the guards, the policy write check after the re-check, and output validation before the response. It says where a nested write's targets run their guards, field pipes, validators and write compute (after the top-level operation's stages of the same kind), lists the check of a value compute set in a field the caller's layer hides (`A3000`), and gives the `forceTransaction` order in full: framework validation and policy admission before `BEGIN`, the policy write check and output validation inside the transaction.
8
+
9
+ The error catalog's `A4000` and `A4001` entries now state the whole rule for a `state` that rejects with an authentication library's refusal: the envelope it answers, that nothing of the library's error is sent and nothing goes to `diagnostics`, which shapes are not a refusal (`500 A3000`), and every route, MCP tool call and in-process call it applies to.
10
+
11
+ - 0895a93: `avDehydrate` and `avHydrate` are typed: data handed from a Server Component to a Client Component as props keeps its type, with no cast.
12
+
13
+ - `avDehydrate(data)` (`@aventara/next`) returns an `AvDehydrated<T>` for data of type `T`, and `avHydrate(value)` (`@/aventara/client`) of an `AvDehydrated<T>` returns the `T`, its `Date`, `bigint`, `Uint8Array` and `AvDecimal` values included. Type an SWR `fallback` prop `AvDehydrated<T>` and pass `avHydrate(fallback)` to `SWRConfig` as it is.
14
+ - `AvDehydrated<T>` is new, exported by `@aventara/next` and by the `src/aventara/client.ts` that `aventara-build --client` writes; the two are one type. It is opaque: a plain object, the data itself, or the dehydrated form of another type is a compile error where an `AvDehydrated<T>` is expected.
15
+ - A Resource or enum named `AvDehydrated` or `avHydrate` is now emitted under a renamed identifier (`AvDehydratedModel`, `avHydrateModel`), with a warning, as other names the generated client owns are. Before, `src/aventara/client.ts`'s own declaration hid a Resource type of that name, which could not be imported from it.
16
+ - Read data you dehydrate through `@/aventara/universal`: `@/aventara/server` types a decimal as core's `AvDecimal`, while `avHydrate` revives it as the browser client's.
17
+ - Pass `avHydrate(fallback)` to `SWRConfig`, never the dehydrated value itself: SWR's `fallback` is typed `any`, so the compiler cannot catch it.
18
+ - `avHydrate` still takes `unknown` and returns `unknown`; `avDehydrate` of an `unknown` value returns an `AvDehydrated<unknown>`. `serializeData: avDehydrate` and `deserializeData: avHydrate` in TanStack Query are written as before.
19
+ - The agent skills show the SWR props pattern typed, with no cast. The frontend skill's refresh line no longer names `npx`, and says a file you edited is kept with a warning naming it, and replaced with `--force`.
20
+
21
+ - ec0d332: `avclient skill` no longer ends with "the client skill is up to date" after keeping a file you edited: it says the skill is up to date apart from what the warnings above name, as `aventara skill` does. The server skill now says what the frontend skill holds: a `SKILL.md` with `reference/client.md`'s content, and `reference/client-setup.md`.
22
+
23
+ ## 1.0.0-rc.13
24
+
25
+ ### Patch Changes
26
+
27
+ - 6c3e25c: `AvZ.oneOf(["USD", "EUR"])` is a string field validator for a closed set of values. It builds an anchored pattern of
28
+ the values, each one's pattern syntax escaped (`"C++"` and `"a.b"` match only themselves), so the Contract gains no
29
+ new check: the generated client runs it like any `AvZ.regex`, and the field's type stays `string`. A refused value
30
+ answers `V1003` with the message `Value must be one of "USD", "EUR".` rather than the pattern. An empty list, or a
31
+ value that is not a string, is refused when the check is built, and the types refuse both. When the types should name
32
+ the set as a union, use a Prisma `enum`: Aventara carries it into the Contract and the generated client as a literal
33
+ union.
34
+ - 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.
35
+
36
+ `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.
37
+
38
+ `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.
39
+
40
+ 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.
41
+
42
+ - 5cdc922: The agent skill spells its commands for the project's package manager: `pnpm run aventara:prepare`, `pnpm exec tsc`
43
+ and `pnpm dlx @aventara/cli@… skill` in a pnpm project, the npm forms otherwise. `aventara new` and `aventara init` use
44
+ the package manager they install with. `aventara skill` reads the nearest lockfile, up to the repository's root, so a
45
+ server app inside a pnpm workspace gets the pnpm forms. When the nearest lockfile is neither npm's nor pnpm's (a yarn
46
+ 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
47
+ CLI supports npm and pnpm only. `reference/errors.md` follows too.
48
+
49
+ The skill's guidance also covers more of what agents got wrong:
50
+
51
+ - A rule that reads the database is re-checked in `hooks.before` under `forceTransaction`, or made the write's own
52
+ condition with `update.first`: a guard runs before the transaction opens, so its check can be stale at commit. A new
53
+ compare-and-set recipe shows it.
54
+ - Nest guards, interceptors and pipes (`APP_GUARD`, Passport's `AuthGuard`, `@nestjs/throttler`) never run on
55
+ Aventara's routes; only `app.use` middleware does. There is no built-in rate limit.
56
+ - Logic that is not one of a model's operations still belongs in your own Nest controller or service.
57
+ - `ctx` calls and `ctx.$args` are typed without configuration: narrow with a type guard there, never cast.
58
+ - A computed value a validator refuses answers `500 A3000` when the caller's layer hides the field.
59
+ - `forceTransaction` needs `transactionsStyle: "interactive"`; `maxTransactionOperations` counts reads too.
60
+ - New sections: scalars (wire forms, `bigint` and `Date` from a controller), the adapter's transaction options and
61
+ the SQLite and PostgreSQL differences, server surfaces (`application`, `serverClient`, `client`, the three `Tx`
62
+ builders), testing (Nest testing module, `framework.client`, `framework.clientTx`, `createFakeAdapter`), the query
63
+ language next to Prisma's, Resource transaction stages with an example, deploying, the full stage order, what
64
+ changes the contract hash, `diagnostics` behind an `A3000`, and where the full docs are for agents.
65
+ - The recipes no longer break on a `String` id.
66
+
67
+ - 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.
68
+
69
+ `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.
70
+
71
+ - 2aaa27d: A stage's `ctx` create no longer has to write a field that the root's or the `application` overlay's `compute.create`
72
+ fills. A `ctx` call runs that compute, and the generated types already state which fields it fills (the
73
+ `computed-fields.generated.ts` file `aventara-build` writes), so `ctx.Payment.create.one({ data })` type-checks without
74
+ the computed `employeeId`. A field that only the `client` or `agent` overlay computes is still required, because that
75
+ compute does not run for a `ctx` call. A model whose types state no computed fields keeps every requirement. The agent
76
+ skill says so.
77
+ - 2aaa27d: Exemptions take one more value, `"read"`, on a field and on a Resource's `fields`. It names every read capability of
78
+ the field that the layer above offers: a scalar's `select`, `filter`, `order` and `orderNulls`, a relation's `select`,
79
+ `include`, `filter`, `order`, `pagination` and `reducers`. On `fields` it applies to every field of the Resource.
80
+
81
+ - Under `exposed: false` it exposes reading and no write, so a read-only Resource no longer needs every field listed:
82
+ `Category: { operations: { find: true }, fields: "read" }`.
83
+ - Under `exposed: true` it hides reading and keeps the writes, which is the write-only field:
84
+ `passwordHash: "read"`.
85
+
86
+ It compiles to the same Contracts as the capability lists it stands for, hash included. Like every exemption it only
87
+ narrows: it never brings back a capability the layer above hid, and naming a field the layer above hid is refused as
88
+ before. `fields: "read"` names relations too, so a relation into a Resource the layer leaves out still fails with
89
+ `COMPILER_DANGLING_RELATION`. The agent skill documents it.
90
+
91
+ Read-only now means no nested write either. A relation does not write rows of a Resource its layer cannot write: one
92
+ that creates nothing (no create or upsert operation, and no field writable on create) takes no `$create`,
93
+ `$connectOrCreate` or `$upsert`; one that updates nothing (no update or upsert operation, and no field writable on
94
+ update) takes no `$update` or `$upsert`; one with no delete operation takes no `$delete`. The fields count with the
95
+ operations for creates and updates, so a Resource whose create operation is hidden but whose fields stay writable on
96
+ create is still created through its parent. Relation fields count too, even link-only (`$connect`): if a Resource must
97
+ create nothing, hide its relations' `create` as well. This holds however the Resource was made read-only, `"read"` or listed
98
+ capabilities, and also when it has no required field, where a nested `$create: {}` was accepted before. `$connect`,
99
+ `$disconnect` and `$set` stay.
100
+
101
+ What happens follows the layer's `exposed`. Under `exposed: false` the relation's directives are the ones the layer
102
+ grants (by `true` on the Resource or the field, by the relation's `create` or `update`, or by naming the directive), so
103
+ such a grant is refused at startup with `COMPILER_READ_ONLY_NESTED_WRITE`: let the relation only link,
104
+ `author: { select: true, create: ["$connect"] }`. Under `exposed: true` the relation inherits its directives, so they
105
+ are dropped from it and startup succeeds; a call that sends one gets `V1006`, or "Relation … is not writable during
106
+ update." when the relation's update had nothing else. Dropping one can leave another Resource with nothing writable, and
107
+ the nested writes into it go the same way. A layer with `exposed: false` below that names a dropped directive, or grants
108
+ a member that was removed, is refused with `COMPILER_READ_ONLY_NESTED_WRITE`.
109
+
110
+ A configuration under `exposed: true` that hides a Resource's deletes, or every way to create or update it, therefore
111
+ serves Contracts with fewer directives on the relations into it, and its ClientContract hash changes: regenerate the
112
+ frontend client. Such a configuration was refused at startup before when a hidden field was required, and otherwise
113
+ accepted nested creates.
114
+
115
+ - 6c3e25c: Field validators now judge every value the caller sent before write compute, and each value once. A value the caller
116
+ sent is judged after the guards, the pipes and its field pipes, before any compute stage runs, and that includes a
117
+ field that has a `compute.create` or `compute.update` stage of its own. After compute, only a value compute set or
118
+ changed is judged: a field the caller left out that compute filled, or a caller's value compute replaced. A compute
119
+ stage that returns `undefined` or the caller's own value changes nothing, so that value is not judged again. The order
120
+ is now guards → pipes → validators of the caller's values → write compute → validators of what compute set or changed
121
+ → hooks.
122
+
123
+ A compute stage never sees a value its own validators refuse. A caller value that also breaks a compute stage (a
124
+ quantity that overflows a total, an unknown currency code handed to `Intl.NumberFormat`) answered `500 A3000` before
125
+ this release; it now answers `422 A2004` with the issue at the field, and no compute stage runs. A bad caller value is
126
+ refused even when compute would have replaced it: before this release that value was dropped silently. Codes and
127
+ issue shapes are unchanged. One answer no longer mixes the two kinds: when a caller value is refused, the computed
128
+ values are not judged yet. In a transaction plan or a forced call, every literal value is judged before the
129
+ transaction opens; a resolved `$ref` value and a value a Resource transaction pipe changed are judged at their step,
130
+ before write compute. The agent skill's stage order says so.
131
+
132
+ - 6c3e25c: A field the configuration adds (`lifecycle: ["VIRTUAL"]`) with `nullable: false` is now a create requirement, like the
133
+ adapter's own non-null columns: in each layer that lets a caller write it at create, and where no `compute.create` (the
134
+ root's or that layer's) fills it, it joins the Contract's `createRequirements.required`. A create without it answers
135
+ `422 A2004` with `V1000` at the field, before any guard runs, and `framework.application`, `framework.client` and the
136
+ generated client type it as required. A layer that hides the field creates without it, and an update is unchanged. A
137
+ nullable added field stays optional. A `compute.create` stage that fills such a field and returns `undefined` answers
138
+ `500 A3000` with an `AvComputedValueError` in the `diagnostics` sink, as it does for a required database column, and
139
+ nothing is written.
140
+
141
+ This changes the Client Contract, and so its hash, for every configuration with such a field: regenerate the frontend
142
+ client. A configuration that sent a `nullable: false` virtual input only sometimes must now send it on every create, or
143
+ state it `nullable: true`.
144
+
145
+ - 5571ebb: `aventara new --host next` runs `create-next-app@16` instead of `@latest`, so a new Next.js major cannot scaffold an
146
+ app outside `@aventara/next`'s supported range.
147
+
148
+ The newest-release lookup of `aventara new` and `aventara init` now asks the registry npm would install
149
+ `@aventara/cli` from, also when `aventara` runs on its own rather than through npm or pnpm: the `@aventara:registry`
150
+ setting, then `registry`, from the environment, the project's `.npmrc` or your user `.npmrc`. Only those two settings
151
+ are read from the files; nothing else in them, auth tokens included, is read or printed. A `${…}` variable in the
152
+ 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
153
+ skipped. The lookup keeps its short
154
+ timeout and still says nothing when it cannot reach the registry.
155
+
156
+ - 73aa797: A generated client takes three hooks, one function each: `new AvClient({ onCall, onSuccess, onError })`. They see
157
+ every call the client makes: an operation, a cached read, a transaction (once, whatever its steps) and a `$access`
158
+ call, as an `AvCall` (its `kind`, its `label`, the operation's `resource`, `family` and `variant` or the `$access`
159
+ verb, and its `requestId` when it sends one; never its arguments).
160
+
161
+ - `onCall(call)` runs when the call is made, before it is sent and before grouping. It may return, or resolve to,
162
+ `AvCallAdditions` for that call's request: `headers`, `credentials`, `cache`, `keepalive`, `priority` and
163
+ `referrerPolicy`. A header the client sets itself (`Content-Type`, `Accept`, the three `Aventara-*` headers), in
164
+ its canonical or lower-case spelling, and `method`, `body`, `signal`, `redirect` and `mode` are type errors, each
165
+ typed as the sentence that refuses it. At runtime such a header in any spelling, a name that is not a valid header
166
+ 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
167
+ `AVENTARA_CLIENT_HOOK_HEADER_DROPPED`), once per header name for each `onCall`. The call's own `headers` option
168
+ wins over `onCall`'s.
169
+ - Calls share one `POST /_all` only when their additions are equal (in any order). An async `onCall` holds back only
170
+ its own call: the call is sent when it settles, with the calls queued then. Calls group when their `onCall` settles
171
+ together with equal additions; an `onCall` doing its own I/O per call sends each call alone, and one awaiting a
172
+ 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
173
+ call made from inside a hook never waits on the call that ran it.
174
+ - `onSuccess(call, result)` and `onError(call, error)` are sinks, typed as returning `void`, so any function fits
175
+ (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`
176
+ sees a refusal, a validation error, a transport failure and an abort, sent alone or inside an Operation Set.
177
+ - A hook that throws or rejects is reported once as an `AventaraWarning` (code `AVENTARA_CLIENT_HOOK_THREW`, or
178
+ `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
179
+ the call, the calls sent with it or the process.
180
+
181
+ `fetch` stays a transport swap: it must return the `Response` unread. A token every call needs now goes in `onCall`,
182
+ read through one shared in-flight promise so the calls still group; the skill and the README say so.
183
+
184
+ - e6495a0: The agent skills cover more of this release. The server skill says that a `state` rejecting with a `401` or `403`
185
+ status answers `A4000` or `A4001`, that `forceTransaction` also runs under `transactionsStyle: "server"` (the way to
186
+ keep forced writes atomic without client plans), that an enum field takes no validator, and how SQLite's one
187
+ connection makes a plain operation wait for an open transaction (`AVENTARA_PRISMA_SQLITE_TRANSACTION_JOINED`, the
188
+ `driver` check). It no longer says that a `ctx` create must write a field the root's or the `application` overlay's
189
+ `compute.create` fills. The client guide, in both skills, adds `AvCall` and `AvCallAdditions` for typing a hook, that
190
+ a hanging `onCall` stalls only its own call, the `AVENTARA_CLIENT_OPTIONS_NO_DEFAULT` warning, the refusal of an
191
+ entrypoint carrying credentials, and how a client of your own computes a transaction step's `fingerprint` with
192
+ `AvProtocol.fingerprint`.
193
+
194
+ - a9260d2: The agent skills cover what an agent needs without opening `node_modules`:
195
+
196
+ - 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.
197
+ - 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.
198
+ - 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`).
199
+ - 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`.
200
+ - `CONFIG_FORBIDDEN_IN_OVERLAY` in the error catalog now names `managedElsewhere` as well as a field `definition`.
201
+
3
202
  ## 1.0.0-rc.12
4
203
 
5
204
  ### Patch Changes
@@ -22,7 +221,7 @@
22
221
  compound identifier goes with any one of its fields, and the generated client and the server façades no longer offer
23
222
  it. A layer that keeps a unique variant with no filterable identifier left fails at startup with
24
223
  `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.
224
+ "filter"]`, not `id: ["select"]`, to keep `find.unique`. The agent skill's Pick example says so.
26
225
 
27
226
  ## 1.0.0-rc.11
28
227
 
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 }),