@hublo/sentinel 1.1.0-alpha.2 → 1.1.0-alpha.4
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/README.md +148 -35
- package/dist/bin/sentinel.js +119 -48
- package/dist/chunk-FLLX5MN3.js +3155 -0
- package/dist/index.d.ts +59 -37
- package/dist/index.js +1 -1
- package/oxlint/nest.json +184 -187
- package/oxlint/node.json +27 -30
- package/oxlint/react.json +189 -204
- package/oxlint/svelte.json +215 -0
- package/package.json +6 -4
- package/dist/chunk-EZCRU6XE.js +0 -2062
package/README.md
CHANGED
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
|
|
5
5
|
`sentinel` is a standalone, semver-versioned package (published to a registry, consumed by a repo as a normal dependency) that unifies a repo's tooling, config, and quality checks into one place, so projects stop copying config files everywhere and stop carrying a pile of duplicated tooling dependencies.
|
|
6
6
|
|
|
7
|
-
> Status: **foundation
|
|
7
|
+
> Status: **the foundation, the TypeScript tool and the lint tool are shipped** (`@hublo/sentinel` on npm). The rest of this README is the design reference for the tools still to come, added one at a time on top of this foundation.
|
|
8
8
|
|
|
9
9
|
---
|
|
10
10
|
|
|
@@ -19,7 +19,7 @@ A large monorepo accumulates:
|
|
|
19
19
|
|
|
20
20
|
## What it gives you
|
|
21
21
|
|
|
22
|
-
**The shift:** instead of today's **big-bang** (change every project and every tool at once, untested in isolation), evolution is **app-scoped and versioned**, evolving a project is a **version bump**, a new/swapped tool is just a **new runner**, and a new stack a **new
|
|
22
|
+
**The shift:** instead of today's **big-bang** (change every project and every tool at once, untested in isolation), evolution is **app-scoped and versioned**, evolving a project is a **version bump**, a new/swapped tool is just a **new runner**, and a new stack a **new preset**. No repo-wide edits.
|
|
23
23
|
|
|
24
24
|
- **One source of truth for config** — every project just `extends @hublo/sentinel/...`; the actual rules live in one versioned place. Change a rule once, everyone gets it on the next version bump.
|
|
25
25
|
- **One source of truth for tooling dependencies** — a project depends on `@hublo/sentinel`, not on a scattered pile of eslint / vitest / plugin devDeps. Bump one version and the whole toolchain moves, atomically, tested in isolation first.
|
|
@@ -43,12 +43,14 @@ A large monorepo accumulates:
|
|
|
43
43
|
|
|
44
44
|
sentinel writes **standard config files** into a project (each just `extends` a sentinel preset) and runs the checks. Your editor and the tools read those **normal files natively**, they never call sentinel at runtime, so nothing is coupled to it or brittle.
|
|
45
45
|
|
|
46
|
-
> **Shipped today:**
|
|
46
|
+
> **Shipped today:** the **TypeScript** and **lint** tools. The `--test` / `--build` snippets
|
|
47
|
+
> below illustrate the end state and land with their own ticket.
|
|
47
48
|
|
|
48
49
|
**Step 1 — put a module on sentinel** (once per module, by a dev; the files are committed). Run from the app dir; `--init` does it all, nothing is hand-edited:
|
|
49
50
|
|
|
50
51
|
```bash
|
|
51
|
-
sentinel --init --typescript --
|
|
52
|
+
sentinel --init --typescript --preset <react|nest|node|svelte>
|
|
53
|
+
sentinel --init --lint --preset <react|nest|node|svelte>
|
|
52
54
|
pnpm install # fetch what --init declared, then commit
|
|
53
55
|
```
|
|
54
56
|
|
|
@@ -56,10 +58,10 @@ pnpm install # fetch what --init declared, then commit
|
|
|
56
58
|
|
|
57
59
|
Those files are tiny, they just point at a sentinel preset. What gets committed:
|
|
58
60
|
|
|
59
|
-
```
|
|
60
|
-
//
|
|
61
|
-
|
|
62
|
-
|
|
61
|
+
```jsonc
|
|
62
|
+
// .oxlintrc.json — generated. Adoption REPLACES eslint rather than sitting beside it, so
|
|
63
|
+
// the module's eslint config is DELETED and this is the only linter config it keeps.
|
|
64
|
+
{ "extends": ["./node_modules/@hublo/sentinel/oxlint/react.json"] }
|
|
63
65
|
```
|
|
64
66
|
|
|
65
67
|
```jsonc
|
|
@@ -122,7 +124,7 @@ pnpm dlx @hublo/sentinel@<exact-version> --inspect --typescript --module <name>
|
|
|
122
124
|
- [`docs/typescript-adoption.md`](docs/typescript-adoption.md) — the adoption cheat sheet: the two adoption steps, the command model (verb x type x location), options, reading a report, and troubleshooting.
|
|
123
125
|
- [`docs/typescript-traces.md`](docs/typescript-traces.md) — a **generated, versioned** reference of live command + output traces (every verb, option, config result and edge case) against the mock monorepo. Regenerate after CLI changes with `pnpm docs:traces`.
|
|
124
126
|
|
|
125
|
-
## Architecture: `target → runner →
|
|
127
|
+
## Architecture: `target → runner → preset`
|
|
126
128
|
|
|
127
129
|
Every check is described by three layers:
|
|
128
130
|
|
|
@@ -130,12 +132,12 @@ Every check is described by three layers:
|
|
|
130
132
|
| -------------------- | --------------------------- | ------------------------------------------- | -------------------------------------------------------------------------------------- |
|
|
131
133
|
| **target** (role) | `--lint`, `--typescript`, … | the _kind_ of check, stable | `lint` `format` `typescript` `build` `test` `static-analysis` `runtime-analysis` |
|
|
132
134
|
| **runner** (adapter) | `--runner=<tool>` | the _tool_ behind the target, swappable | lint: `eslint`/`biome`/`oxlint` · types: `tsc`/`tsgo` · build: `vite` · test: `vitest` |
|
|
133
|
-
| **
|
|
135
|
+
| **preset** (preset) | detected / `--preset` | the _variant_ per stack (strict by default) | `react` `nest` `node` `svelte` (svelte: lint only, no tsconfig preset yet) |
|
|
134
136
|
|
|
135
|
-
A run is `target × runner ×
|
|
137
|
+
A run is `target × runner × preset`, e.g. `sentinel --run --lint --runner=eslint` from the `host-admin` dir.
|
|
136
138
|
|
|
137
139
|
- `--runner` is an **optional override on a central default**. `sentinel --lint` uses the configured default runner, so swapping a tool globally is a one-place change; `--runner=biome` overrides for a single run (great for benchmarking eslint vs biome vs oxlint, and for gradual migration).
|
|
138
|
-
- The **
|
|
140
|
+
- The **preset is detected** from the module's dependencies (deterministic: a framework dep → its preset, else `node`), and **`--preset` overrides it**. Detection can be wrong where deps are hoisted at the repo root (it returns `node`), so pass `--preset` for `--init` (that is where the preset is chosen). `--run`/`--report` don't depend on it, they run the tool on the committed config.
|
|
139
141
|
|
|
140
142
|
### Adapters & the engine (ports & adapters)
|
|
141
143
|
|
|
@@ -143,14 +145,14 @@ A run is `target × runner × flavour`, e.g. `sentinel --run --lint --runner=esl
|
|
|
143
145
|
|
|
144
146
|
- the **engine** (core) is tool-agnostic: it parses the CLI, resolves an adapter, and owns **all IO and repo structure**, finding the project root, reading, merging and writing files;
|
|
145
147
|
- an **adapter** is the boundary to one tool (eslint, tsc, vitest, …). It carries the tool knowledge (how to run it, what its config means) and implements one contract (types in `src/core/types.ts`, optional base in `src/core/base-adapter.ts`);
|
|
146
|
-
- a **context** is a plain data object the engine passes to an adapter for a run (`app`, `cwd`, optional `
|
|
148
|
+
- a **context** is a plain data object the engine passes to an adapter for a run (`app`, `cwd`, optional `preset`, `ci`, `fix`). It is data only, it never carries a filesystem capability.
|
|
147
149
|
|
|
148
150
|
`sentinel` does not reimplement tools. The contract:
|
|
149
151
|
|
|
150
|
-
- **`appliesTo(
|
|
151
|
-
- **`plan(
|
|
152
|
+
- **`appliesTo(preset)`** — which presets this adapter handles (resolution filters on it, so a React-only adapter is never picked for Nest)
|
|
153
|
+
- **`plan(preset)`** — PURE: returns a declarative `UpdatePlan` of file operations (used by `--init`). The adapter never touches the disk; the engine applies the plan.
|
|
152
154
|
- **`run(ctx)`** — invoke the tool's bin against the project (used by `--run`)
|
|
153
|
-
- **`inspect(
|
|
155
|
+
- **`inspect(preset)`** — the adapter's resolved base config (used by `--inspect`)
|
|
154
156
|
- **`report(ctx)`** — metrics (used by `--report`)
|
|
155
157
|
|
|
156
158
|
**`--init` is a declarative plan, not file-writing inside the adapter.** The adapter describes intent as operations; the engine executes them:
|
|
@@ -170,7 +172,7 @@ So the tool-_meaning_ lives in the adapter and the read/merge/write _mechanics_
|
|
|
170
172
|
```mermaid
|
|
171
173
|
flowchart LR
|
|
172
174
|
CLI["sentinel --verb --target<br/>[--runner]"] --> D[dispatch]
|
|
173
|
-
D --> R["registry.resolve<br/>(target,
|
|
175
|
+
D --> R["registry.resolve<br/>(target, preset, runner)"]
|
|
174
176
|
R --> A["adapter<br/>eslint / tsc / vitest / ..."]
|
|
175
177
|
A -->|"--run"| Run["tool binary on the project"]
|
|
176
178
|
A -->|"--init"| Upd["declarative plan → engine writes"]
|
|
@@ -200,6 +202,80 @@ The rules live in `sentinel`. Each module keeps a **thin, generated stub** per t
|
|
|
200
202
|
- **Per-module install** — `@hublo/sentinel` is added per module, so adoption is **gradual** (migrate lot by lot; a module can adopt sentinel while its neighbour still uses the old config). Root configs are removed only once the **last** module has migrated.
|
|
201
203
|
- **Runner binaries** (`eslint`, `typescript`, `vite`, `vitest`, …) are **resolved from the adopting module** at run time (sentinel looks for the tool in the module, then falls back to `PATH`), so the editor and nx keep using the exact binary the project already installs. sentinel does **not** declare them as dependencies today, so it does not pin their versions: the module still owns its own `typescript`. Having sentinel dictate those versions (as peer dependencies, so the whole toolchain rides the sentinel version) is the intended end state, and it lands with the tool tickets that actually bundle a runner.
|
|
202
204
|
|
|
205
|
+
## What a preset does not enforce
|
|
206
|
+
|
|
207
|
+
A preset that silently enforces less than the config it replaced is worse than no preset, so
|
|
208
|
+
every rule that is not enforced carries a reason and is listed by `sentinel --inspect --lint`:
|
|
209
|
+
|
|
210
|
+
| State | Meaning | Can it be switched on? |
|
|
211
|
+
| -------------- | -------------------------------------------------------------------- | ------------------------------- |
|
|
212
|
+
| **deferred** | scheduled for a later migration wave | yes, on schedule |
|
|
213
|
+
| **disabled** | a decision: it argues with the architecture, or only false-positives | yes, by revisiting the decision |
|
|
214
|
+
| **downgraded** | it runs and reports, it just cannot fail the build | yes, once the debt is paid |
|
|
215
|
+
|
|
216
|
+
A small number of rules sit outside those three states, because the preset cannot carry them at
|
|
217
|
+
any severity. They are documented here rather than reported by `--inspect`: oxlint validates
|
|
218
|
+
rule and plugin names when it **parses** a config, so naming one would make the config fail to
|
|
219
|
+
parse and the module would lint nothing at all. They are still recorded as data in the preset,
|
|
220
|
+
so the tests can count every rule a module enforces and fail on anything unaccounted for.
|
|
221
|
+
|
|
222
|
+
### Rules the presets do not carry (nest, react)
|
|
223
|
+
|
|
224
|
+
| Rule | Why |
|
|
225
|
+
| ---------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
|
226
|
+
| `typescript/member-ordering` | oxlint has no native rule, and the preset deliberately does not host `@typescript-eslint/eslint-plugin`: a plugin costs a dependency, a load step and a pass over every file, which is not worth one rule |
|
|
227
|
+
| `no-invalid-this` | no oxlint equivalent, and hosting does not help (the rule crashes on every file under oxlint's plugin API). Its coverage moves to TypeScript's `noImplicitThis` |
|
|
228
|
+
| `no-octal` | no oxlint equivalent |
|
|
229
|
+
| ~15 stylistic rules | formatting, which the formatter owns. They move to **oxfmt**, deliberately the last step of the migration, so nothing enforces them in between |
|
|
230
|
+
|
|
231
|
+
The formatting group is the only one that will ever shrink; the rest are settled losses.
|
|
232
|
+
|
|
233
|
+
`no-return-await` is **not** in that table, despite having no oxlint rule of that name. It is
|
|
234
|
+
deprecated in ESLint core and oxlint ships the typescript-eslint successor, so the presets carry
|
|
235
|
+
`typescript/return-await` with the `never` option, which is what the old rule meant. It is held
|
|
236
|
+
at `warn`: both modules set the old rule to error, but it was never actually reporting (the nx
|
|
237
|
+
lint target expanded to 2 files of 2888), and running it over every file finds 9 real violations
|
|
238
|
+
on bff-admin and 5 on host-admin. Fix those and it can be raised.
|
|
239
|
+
|
|
240
|
+
### Rules with no oxlint equivalent (svelte)
|
|
241
|
+
|
|
242
|
+
Oxlint parses a `.svelte` file's `<script>` block and **not its template**. Measured on oxlint
|
|
243
|
+
1.77 with a component carrying a violation in each half: the `<script>` violation is reported,
|
|
244
|
+
and `{@html}`, `{@debug}` and duplicate style properties in the template produce nothing.
|
|
245
|
+
Adding a svelte plugin does not fix this, because the missing piece is the **parser**, not the
|
|
246
|
+
rule implementations.
|
|
247
|
+
|
|
248
|
+
So sixteen rules the two svelte modules enforce today have **no oxlint equivalent**. Fourteen
|
|
249
|
+
read the template:
|
|
250
|
+
|
|
251
|
+
`svelte/comment-directive` · `svelte/no-at-debug-tags` · **`svelte/no-at-html-tags`** ·
|
|
252
|
+
`svelte/no-dupe-else-if-blocks` · `svelte/no-dupe-style-properties` ·
|
|
253
|
+
`svelte/no-dynamic-slot-name` · `svelte/no-inner-declarations` ·
|
|
254
|
+
`svelte/no-not-function-handler` · `svelte/no-object-in-text-mustaches` ·
|
|
255
|
+
`svelte/no-shorthand-style-property-overrides` · `svelte/no-unknown-style-directive-property` ·
|
|
256
|
+
`svelte/no-unused-svelte-ignore` · `svelte/system` · `svelte/valid-compile`
|
|
257
|
+
|
|
258
|
+
and two core rules oxlint does not implement: `no-dupe-args` (TypeScript rejects duplicate
|
|
259
|
+
parameter names anyway) and `no-octal`.
|
|
260
|
+
|
|
261
|
+
**The one worth knowing about is `svelte/no-at-html-tags`**, an XSS guard that fires on the
|
|
262
|
+
modules today. Adopting the svelte lint preset gives that check up.
|
|
263
|
+
|
|
264
|
+
These rules are **absent from the generated config rather than set to `off`**, and that is
|
|
265
|
+
forced, not stylistic. Oxlint rejects an unknown rule or plugin when it _parses_ a config, even
|
|
266
|
+
when the value is `off`:
|
|
267
|
+
|
|
268
|
+
```console
|
|
269
|
+
$ oxlint -c '{"rules": {"svelte/no-at-html-tags": "off"}}'
|
|
270
|
+
Failed to parse oxlint configuration file.
|
|
271
|
+
x Plugin 'svelte' not found
|
|
272
|
+
```
|
|
273
|
+
|
|
274
|
+
Listing them as disabled would not weaken the config, it would stop the config from parsing and
|
|
275
|
+
the module would lint nothing at all. They are recorded as data in the preset (`uncovered`), so
|
|
276
|
+
the preset tests can count every rule the modules enforce and fail on anything unaccounted for,
|
|
277
|
+
and they are documented here for the developers who own those modules.
|
|
278
|
+
|
|
203
279
|
## Composition & precedence
|
|
204
280
|
|
|
205
281
|
A repo often has a base config that is **structural**, not just tooling, e.g. a
|
|
@@ -264,11 +340,11 @@ A command **composes** three axes: **verb + type + location**.
|
|
|
264
340
|
```
|
|
265
341
|
sentinel <verb> [type] [options]
|
|
266
342
|
|
|
267
|
-
VERBS --run execute the target's tool
|
|
268
|
-
--inspect
|
|
269
|
-
--
|
|
270
|
-
--
|
|
271
|
-
--
|
|
343
|
+
VERBS --run execute the target's tool (--json for metrics + diagnostics)
|
|
344
|
+
--inspect the resolved configuration, what is deferred, adoption + drift
|
|
345
|
+
--init generate/apply the config stubs (writes; one module only)
|
|
346
|
+
--report deprecated → --run --json
|
|
347
|
+
--status deprecated → --inspect
|
|
272
348
|
|
|
273
349
|
TYPES --lint --format --typescript --build --test
|
|
274
350
|
--static-analysis --runtime-analysis --arch
|
|
@@ -278,27 +354,55 @@ LOCATION in a MODULE dir → that module (do NOT pass --module)
|
|
|
278
354
|
at the workspace ROOT → --module <name> (one) · --ci (affected) · else all
|
|
279
355
|
|
|
280
356
|
OPTIONS --module <name> from the root: scope to one module
|
|
281
|
-
--
|
|
357
|
+
--preset <name> override the detected stack preset (react, nest, ...)
|
|
282
358
|
--runner <tool> override the default runner
|
|
283
359
|
--ci from the root: affected only; non-zero exit on failure
|
|
284
360
|
--fix auto-fix where applicable
|
|
285
361
|
--dry-run preview a --init without writing
|
|
286
362
|
--json machine-readable output (report / inspect / status / --dry-run)
|
|
363
|
+
-- <tool options> --run only: pass the rest to the tool itself (one type)
|
|
287
364
|
|
|
288
365
|
EXAMPLES sentinel --run --typescript # in a module → that module
|
|
289
|
-
sentinel --
|
|
290
|
-
sentinel --
|
|
291
|
-
sentinel --
|
|
292
|
-
sentinel --
|
|
293
|
-
sentinel --
|
|
366
|
+
sentinel --run --typescript -- --noImplicitAny # ask tsc a question of your own
|
|
367
|
+
sentinel --run --typescript --module bff-admin # from root → one module
|
|
368
|
+
sentinel --run --json # from root → all types, machine output
|
|
369
|
+
sentinel --inspect --typescript # from root → adoption coverage
|
|
370
|
+
sentinel --inspect --ci # from root → fail CI on drift
|
|
371
|
+
sentinel --init --typescript --preset react # write stubs for the current module
|
|
294
372
|
```
|
|
295
373
|
|
|
296
374
|
`--run`/`--inspect`/`--report`/`--status` share one context rule (developer from a
|
|
297
375
|
module, or from the root for a name / affected / all); `--init` writes, so it targets
|
|
298
376
|
one module only (adopting every module at once is refused, adopt gradually).
|
|
299
377
|
|
|
378
|
+
### Asking the tool your own question (`--`)
|
|
379
|
+
|
|
380
|
+
Everything after `--` goes to the tool, not to sentinel. It applies to `--run` only, and
|
|
381
|
+
needs exactly one type named, since one tool's options are not another's:
|
|
382
|
+
|
|
383
|
+
```bash
|
|
384
|
+
sentinel --run --typescript -- --noImplicitAny # what would this deferred rule cost?
|
|
385
|
+
sentinel --run --lint -- --deny-warnings # what would zero warnings take?
|
|
386
|
+
```
|
|
387
|
+
|
|
388
|
+
This exists because a preset **defers** rules to keep first adoption non-breaking, and
|
|
389
|
+
`--inspect` can only say that a rule is deferred. "Deferred" is honest, and it is not the
|
|
390
|
+
answer anyone wants: the question is how much debt is behind it. Rather than sentinel
|
|
391
|
+
modelling that question one flag at a time, the tool's own options are forwarded, so
|
|
392
|
+
anything the tool can already answer is one command away.
|
|
393
|
+
|
|
394
|
+
Two things to know:
|
|
395
|
+
|
|
396
|
+
- **A modified run always says so**, on stderr and in the `--json` envelope (`toolArgs`).
|
|
397
|
+
These options can weaken a check as easily as strengthen it, and `--run` is what CI
|
|
398
|
+
calls, so a green result must never be mistakable for a clean standard check.
|
|
399
|
+
- **For TypeScript it changes how tsc is invoked**, because build mode refuses compiler
|
|
400
|
+
options (`error TS5094`). Sentinel switches to project mode and checks each project the
|
|
401
|
+
solution `references`, since the solution itself holds no files. Emit is forced off, so a
|
|
402
|
+
question about the code never writes build output.
|
|
403
|
+
|
|
300
404
|
**`--status` — adoption + conformity.** A cheap, workspace-wide read (no tool run, no
|
|
301
|
-
|
|
405
|
+
preset guessing): for each module it reads the committed `tsconfig` `extends` chain and
|
|
302
406
|
reports whether it is **adopted** (extends a sentinel preset), which preset, and whether
|
|
303
407
|
it is **conformant** (drift-free, a re-`update` would change nothing), plus a coverage
|
|
304
408
|
footer. This is the **drift guard** as a command, `--status --ci` exits non-zero when an
|
|
@@ -320,14 +424,14 @@ src/
|
|
|
320
424
|
core/
|
|
321
425
|
types.ts # the adapter contract, pure types (Adapter, FileOperation, UpdatePlan)
|
|
322
426
|
base-adapter.ts # optional convenience base class for adapters
|
|
323
|
-
domain.ts # vocabulary + derived types (verbs, targets,
|
|
427
|
+
domain.ts # vocabulary + derived types (verbs, targets, presets)
|
|
324
428
|
settings.ts # tunables (workspace-root marker, ...)
|
|
325
|
-
registry.ts # register +
|
|
429
|
+
registry.ts # register + preset-aware resolve
|
|
326
430
|
dispatch.ts # verb → adapter method
|
|
327
431
|
apply-plan.ts # the engine's filesystem port (applies --init operations)
|
|
328
432
|
roles/<config-role>/ # lint, format, typescript, build, test
|
|
329
433
|
adapters/<runner>/ # one adapter per tool (implements the contract)
|
|
330
|
-
|
|
434
|
+
presets/<stack>/ # config presets per stack (react, nest, svelte, ...)
|
|
331
435
|
roles/{static-analysis,runtime-analysis}/ # analysis roles
|
|
332
436
|
configs/ # fixed configs (internal, not exported)
|
|
333
437
|
runners/ # one runner per sub-tool (duplication, complexity, ...)
|
|
@@ -336,9 +440,18 @@ tests/ # unit tests + tests/e2e (runs the built dist binar
|
|
|
336
440
|
.github/workflows/ # ci.yml (PR checks) + publish.yml (manual, version-input publish)
|
|
337
441
|
```
|
|
338
442
|
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
443
|
+
Presets reach consumers two different ways, decided by the tool rather than by us:
|
|
444
|
+
|
|
445
|
+
- **tsconfig, as subpath exports**: `@hublo/sentinel/tsconfig/react` · `.../tsconfig/nest` ·
|
|
446
|
+
`.../tsconfig/node`. tsc resolves `extends` through the `exports` map.
|
|
447
|
+
- **oxlint, as physical paths at the package root**: `oxlint/react.json`, `oxlint/nest.json`,
|
|
448
|
+
`oxlint/node.json`, `oxlint/svelte.json`, referenced as
|
|
449
|
+
`./node_modules/@hublo/sentinel/oxlint/<preset>.json`. Oxlint resolves every `extends`
|
|
450
|
+
entry as a PATH and never as a package specifier, so it never consults `exports` and the
|
|
451
|
+
physical layout IS the contract. The same is true of `jsPlugins`, which is why the local
|
|
452
|
+
Hublo plugin ships beside them in `plugins/`.
|
|
453
|
+
|
|
454
|
+
Presets for the remaining tools land with their own ticket.
|
|
342
455
|
|
|
343
456
|
## FAQ
|
|
344
457
|
|