@aragon-agent/cli 0.5.9 → 0.5.10

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (52) hide show
  1. package/CHANGELOG.md +1157 -1026
  2. package/README.md +123 -0
  3. package/dist/boot/guard.d.ts +135 -0
  4. package/dist/boot/guard.js +218 -0
  5. package/dist/boot/rollback.d.ts +86 -0
  6. package/dist/boot/rollback.js +185 -0
  7. package/dist/cli.js +283 -2
  8. package/dist/commands/builtins.js +118 -0
  9. package/dist/commands/registry.d.ts +17 -0
  10. package/dist/config/app-paths.d.ts +31 -0
  11. package/dist/config/app-paths.js +37 -0
  12. package/dist/config/cli-commands.d.ts +27 -1
  13. package/dist/config/cli-commands.js +51 -2
  14. package/dist/config/env.d.ts +10 -1
  15. package/dist/config/env.js +41 -1
  16. package/dist/config/load.d.ts +17 -0
  17. package/dist/config/load.js +25 -1
  18. package/dist/config/schema.d.ts +56 -0
  19. package/dist/config/schema.js +81 -0
  20. package/dist/config/store.js +13 -1
  21. package/dist/launcher.d.ts +14 -0
  22. package/dist/launcher.js +43 -0
  23. package/dist/logging/logger.d.ts +1 -1
  24. package/dist/ui/App.d.ts +11 -1
  25. package/dist/ui/App.js +88 -2
  26. package/dist/ui/BottomStatusRow.d.ts +25 -3
  27. package/dist/ui/BottomStatusRow.js +10 -7
  28. package/dist/ui/UpdateLine.d.ts +44 -0
  29. package/dist/ui/UpdateLine.js +129 -0
  30. package/dist/update/classify-failure.d.ts +32 -0
  31. package/dist/update/classify-failure.js +60 -0
  32. package/dist/update/install-lock.d.ts +55 -0
  33. package/dist/update/install-lock.js +184 -0
  34. package/dist/update/install-source.d.ts +97 -0
  35. package/dist/update/install-source.js +218 -0
  36. package/dist/update/installer.d.ts +108 -0
  37. package/dist/update/installer.js +242 -0
  38. package/dist/update/limits.d.ts +111 -0
  39. package/dist/update/limits.js +111 -0
  40. package/dist/update/npm-view.d.ts +62 -0
  41. package/dist/update/npm-view.js +133 -0
  42. package/dist/update/registry.d.ts +52 -0
  43. package/dist/update/registry.js +161 -0
  44. package/dist/update/semver.d.ts +89 -0
  45. package/dist/update/semver.js +295 -0
  46. package/dist/update/service.d.ts +193 -0
  47. package/dist/update/service.js +636 -0
  48. package/dist/update/state.d.ts +103 -0
  49. package/dist/update/state.js +142 -0
  50. package/dist/update/types.d.ts +169 -0
  51. package/dist/update/types.js +68 -0
  52. package/package.json +3 -3
@@ -0,0 +1,295 @@
1
+ /**
2
+ * Semver comparison and the update decision (cli-auto-update section 3.4).
3
+ *
4
+ * ASCII ONLY - this tree is inside the glyph scanner's scope (C-2 / C-3).
5
+ *
6
+ * HAND-WRITTEN AND NOT `semver`. The package has no such dependency
7
+ * (`package.json`), and this feature needs three questions answered - "is B
8
+ * newer than A", "is B a prerelease", "does this Node satisfy that range" - on a
9
+ * document the registry writes and npm itself only warns about. Adding a
10
+ * transitive dependency to an auto-updater, of all things, is the one place the
11
+ * supply-chain argument in R-1 cuts hardest.
12
+ *
13
+ * PURE: no I/O, no clock, no process. Every function here is total and none of
14
+ * them throws.
15
+ */
16
+ /**
17
+ * The form a version must have before it is interpolated into `npm install`'s
18
+ * argv (section 3.5 / AC-24).
19
+ *
20
+ * ANCHORED AND WITHOUT `v`: a registry that answered `"1.0.0; rm -rf /"` must
21
+ * never reach `execFile`. With `shell: false` this is not an injection surface
22
+ * to begin with, but the rejection is what makes that claim checkable rather
23
+ * than reasoned, and it also satisfies `fetch-source.ts` rule 3 (no leading `-`)
24
+ * by construction.
25
+ */
26
+ export const STRICT_VERSION_RE = /^\d+\.\d+\.\d+(?:-[0-9A-Za-z.-]+)?(?:\+[0-9A-Za-z.-]+)?$/;
27
+ const SEMVER_RE = /^v?(\d+)\.(\d+)\.(\d+)(?:-([0-9A-Za-z.-]+))?(?:\+[0-9A-Za-z.-]+)?$/;
28
+ /** Parse a semver string, or `null` when it is not one. Never throws. */
29
+ export function parseSemver(raw) {
30
+ if (typeof raw !== 'string')
31
+ return null;
32
+ const match = SEMVER_RE.exec(raw.trim());
33
+ if (!match)
34
+ return null;
35
+ const [, major, minor, patch, pre] = match;
36
+ return {
37
+ major: Number.parseInt(major, 10),
38
+ minor: Number.parseInt(minor, 10),
39
+ patch: Number.parseInt(patch, 10),
40
+ prerelease: pre ? pre.split('.') : [],
41
+ };
42
+ }
43
+ export function isPrerelease(raw) {
44
+ return (parseSemver(raw)?.prerelease.length ?? 0) > 0;
45
+ }
46
+ /**
47
+ * Compare prerelease identifier lists per semver section 11.
48
+ *
49
+ * THE EMPTY LIST SORTS HIGHEST, which is the rule that keeps `1.0.0-rc.1` below
50
+ * `1.0.0`. Getting this backwards would let a `latest` tag pointing at a release
51
+ * candidate read as newer than the release it precedes.
52
+ */
53
+ function comparePrerelease(a, b) {
54
+ if (a.length === 0 && b.length === 0)
55
+ return 0;
56
+ if (a.length === 0)
57
+ return 1;
58
+ if (b.length === 0)
59
+ return -1;
60
+ const len = Math.max(a.length, b.length);
61
+ for (let i = 0; i < len; i += 1) {
62
+ const x = a[i];
63
+ const y = b[i];
64
+ if (x === undefined)
65
+ return -1;
66
+ if (y === undefined)
67
+ return 1;
68
+ const xNum = /^\d+$/.test(x);
69
+ const yNum = /^\d+$/.test(y);
70
+ if (xNum && yNum) {
71
+ const diff = Number.parseInt(x, 10) - Number.parseInt(y, 10);
72
+ if (diff !== 0)
73
+ return diff < 0 ? -1 : 1;
74
+ continue;
75
+ }
76
+ // Numeric identifiers always sort below alphanumeric ones.
77
+ if (xNum !== yNum)
78
+ return xNum ? -1 : 1;
79
+ if (x !== y)
80
+ return x < y ? -1 : 1;
81
+ }
82
+ return 0;
83
+ }
84
+ /**
85
+ * `-1 | 0 | 1`. An UNPARSEABLE version sorts BELOW a parseable one and equal to
86
+ * another unparseable one, so a garbage registry answer can never read as newer
87
+ * than what is installed.
88
+ */
89
+ export function compareSemver(a, b) {
90
+ const left = parseSemver(a);
91
+ const right = parseSemver(b);
92
+ if (!left && !right)
93
+ return 0;
94
+ if (!left)
95
+ return -1;
96
+ if (!right)
97
+ return 1;
98
+ if (left.major !== right.major)
99
+ return left.major < right.major ? -1 : 1;
100
+ if (left.minor !== right.minor)
101
+ return left.minor < right.minor ? -1 : 1;
102
+ if (left.patch !== right.patch)
103
+ return left.patch < right.patch ? -1 : 1;
104
+ return comparePrerelease(left.prerelease, right.prerelease);
105
+ }
106
+ function parseClause(raw) {
107
+ const text = raw.trim();
108
+ if (text.length === 0)
109
+ return null;
110
+ const ops = [
111
+ ['>=', '>='],
112
+ ['<=', '<='],
113
+ ['>', '>'],
114
+ ['<', '<'],
115
+ ['^', '^'],
116
+ ['~', '~'],
117
+ ['=', '='],
118
+ ];
119
+ for (const [prefix, op] of ops) {
120
+ if (text.startsWith(prefix)) {
121
+ const rest = text.slice(prefix.length).trim();
122
+ const parsed = parsePartial(rest);
123
+ return parsed ? { op, ...parsed } : null;
124
+ }
125
+ }
126
+ const parsed = parsePartial(text);
127
+ return parsed ? { op: '=', ...parsed } : null;
128
+ }
129
+ /**
130
+ * `18`, `18.x`, `18.19`, `18.19.0` - the partial forms `engines.node` uses in
131
+ * practice. Missing components read as `0`, which is what makes `>=18` mean
132
+ * `>=18.0.0`, and `specified` records how many were really named.
133
+ */
134
+ function parsePartial(raw) {
135
+ const text = raw.trim().replace(/^v/, '');
136
+ if (text.length === 0)
137
+ return null;
138
+ const parts = text.split('.');
139
+ if (parts.length > 3)
140
+ return null;
141
+ const nums = [];
142
+ let specified = 0;
143
+ let sawWildcard = false;
144
+ for (const part of parts) {
145
+ if (part === 'x' || part === 'X' || part === '*') {
146
+ sawWildcard = true;
147
+ nums.push(0);
148
+ continue;
149
+ }
150
+ if (!/^\d+$/.test(part))
151
+ return null;
152
+ nums.push(Number.parseInt(part, 10));
153
+ // A concrete component AFTER a wildcard (`18.x.3`) names nothing further:
154
+ // the wildcard has already made everything below it free.
155
+ if (!sawWildcard)
156
+ specified += 1;
157
+ }
158
+ return {
159
+ version: {
160
+ major: nums[0] ?? 0,
161
+ minor: nums[1] ?? 0,
162
+ patch: nums[2] ?? 0,
163
+ prerelease: [],
164
+ },
165
+ specified,
166
+ };
167
+ }
168
+ function compareParsed(a, b) {
169
+ if (a.major !== b.major)
170
+ return a.major < b.major ? -1 : 1;
171
+ if (a.minor !== b.minor)
172
+ return a.minor < b.minor ? -1 : 1;
173
+ if (a.patch !== b.patch)
174
+ return a.patch < b.patch ? -1 : 1;
175
+ return comparePrerelease(a.prerelease, b.prerelease);
176
+ }
177
+ function satisfiesClause(actual, clause) {
178
+ const cmp = compareParsed(actual, clause.version);
179
+ switch (clause.op) {
180
+ case '>=':
181
+ return cmp >= 0;
182
+ case '>':
183
+ return cmp > 0;
184
+ case '<=':
185
+ return cmp <= 0;
186
+ case '<':
187
+ return cmp < 0;
188
+ case '=':
189
+ // A BARE PARTIAL IS A RANGE, NOT AN EQUALITY. `18` and `18.x` mean "any
190
+ // 18.y.z"; only a fully-specified `18.19.0` means that exact version.
191
+ if (clause.specified === 0)
192
+ return true;
193
+ if (actual.major !== clause.version.major)
194
+ return false;
195
+ if (clause.specified === 1)
196
+ return true;
197
+ if (actual.minor !== clause.version.minor)
198
+ return false;
199
+ if (clause.specified === 2)
200
+ return true;
201
+ return actual.patch === clause.version.patch;
202
+ case '^':
203
+ return cmp >= 0 && actual.major === clause.version.major;
204
+ case '~':
205
+ return (cmp >= 0 &&
206
+ actual.major === clause.version.major &&
207
+ actual.minor === clause.version.minor);
208
+ default:
209
+ return true;
210
+ }
211
+ }
212
+ /**
213
+ * Does `nodeVersion` satisfy `engines.node`?
214
+ *
215
+ * FAILS OPEN, and that is a decision rather than an oversight (D-15). Handled
216
+ * forms are `>=X`, `>=X.Y.Z`, `>X`, `<X`, `<=X`, `=X`, `^X`, `~X`, `X.x`, and
217
+ * `||`-joined unions of those, with space-separated clauses inside a union
218
+ * ANDed together. Anything this cannot parse returns `true`, matching npm's own
219
+ * default of warning rather than refusing.
220
+ *
221
+ * Failing CLOSED here would mean a syntax we did not anticipate silently freezes
222
+ * every user's updates forever, with no message and no way to find out - the
223
+ * worse of the two errors by a wide margin. A range we DID understand and that
224
+ * genuinely excludes this Node yields `false`, and the caller turns that into a
225
+ * `node-too-old` notice so the user is told to upgrade Node rather than left
226
+ * wondering.
227
+ */
228
+ export function satisfiesNodeRange(range, nodeVersion) {
229
+ if (range === undefined)
230
+ return true;
231
+ const text = range.trim();
232
+ if (text.length === 0 || text === '*')
233
+ return true;
234
+ const actual = parseSemver(nodeVersion);
235
+ if (!actual)
236
+ return true;
237
+ for (const union of text.split('||')) {
238
+ const clauses = union.trim().split(/\s+/).filter((c) => c.length > 0);
239
+ if (clauses.length === 0)
240
+ continue;
241
+ let allParsed = true;
242
+ let allSatisfied = true;
243
+ for (const raw of clauses) {
244
+ const clause = parseClause(raw);
245
+ if (!clause) {
246
+ allParsed = false;
247
+ break;
248
+ }
249
+ if (!satisfiesClause(actual, clause))
250
+ allSatisfied = false;
251
+ }
252
+ // An unparseable clause makes the WHOLE range unknown, not just this union:
253
+ // a partially-understood range is exactly the input that would make a
254
+ // fail-open guard silently strict.
255
+ if (!allParsed)
256
+ return true;
257
+ if (allSatisfied)
258
+ return true;
259
+ }
260
+ return false;
261
+ }
262
+ /**
263
+ * Should we install, merely report, or say nothing? (section 3.4)
264
+ *
265
+ * ORDER IS SEMANTIC. `up-to-date` is answered first because it is the
266
+ * overwhelmingly common case and the cheapest; `skipped` comes before the
267
+ * quality gates so a user who typed `/update skip` is not told about a
268
+ * prerelease they already declined. Only a range we UNDERSTOOD and that excludes
269
+ * this Node produces `notify`; every other rejection is silent (`none`).
270
+ *
271
+ * The SOURCE and the MODE are deliberately NOT inputs here. This function
272
+ * answers "is that version worth having"; whether this machine may install it is
273
+ * the service's question, and folding the two would make a pure decision depend
274
+ * on the filesystem.
275
+ */
276
+ export function decideUpdate(input) {
277
+ const { local, manifest, nodeVersion, skippedVersion } = input;
278
+ const remote = manifest.version;
279
+ if (compareSemver(remote, local) <= 0)
280
+ return { action: 'none', reason: 'up-to-date' };
281
+ if (skippedVersion.length > 0 && remote === skippedVersion) {
282
+ return { action: 'none', reason: 'skipped' };
283
+ }
284
+ // A stable install is never moved onto a prerelease, even if someone points
285
+ // `latest` at one. A local prerelease is already opted in, so it may move.
286
+ if (isPrerelease(remote) && !isPrerelease(local)) {
287
+ return { action: 'none', reason: 'prerelease' };
288
+ }
289
+ if (manifest.deprecated !== undefined)
290
+ return { action: 'none', reason: 'deprecated' };
291
+ if (!satisfiesNodeRange(manifest.engines?.node, nodeVersion)) {
292
+ return { action: 'notify', reason: 'node-too-old' };
293
+ }
294
+ return { action: 'install', reason: 'newer-available' };
295
+ }
@@ -0,0 +1,193 @@
1
+ /**
2
+ * `UpdateService` - the state machine, the scheduler and the event stream
3
+ * (cli-auto-update section 3.6).
4
+ *
5
+ * ASCII ONLY - this tree is inside the glyph scanner's scope (C-2 / C-3).
6
+ *
7
+ * idle --check()--> checking --none--> idle
8
+ * |
9
+ * |-notify--> available (terminal for the session)
10
+ * `-install-> installing --ok--> ready (terminal)
11
+ * `-err--> failed --backoff--> idle
12
+ *
13
+ * EVERY FAILURE IS SILENT (D-7). A background updater that cannot reach the
14
+ * registry behind a corporate proxy, or cannot write to `/usr/lib/node_modules`
15
+ * without sudo, must not turn into a recurring interruption: it logs, backs off
16
+ * exponentially, and stays quiet. The single exception is a failure the user can
17
+ * actually fix, after `UPDATE_LIMITS.failuresBeforeNotice` consecutive
18
+ * occurrences.
19
+ *
20
+ * THIS MODULE IS REACHED ONLY THROUGH A DYNAMIC `import()` (section 3.8 / D-25).
21
+ * `runOneShot` and `runInteractive` live in the same `cli.tsx` module and there
22
+ * is no bundler (C-16), so a static import here would put the whole subsystem in
23
+ * `dist/cli.js`'s graph and `aragon -p` would evaluate every module under
24
+ * `update/`.
25
+ */
26
+ import type { ScopedLogger } from '../logging/logger.js';
27
+ import type { UpdateConfig } from '../config/schema.js';
28
+ import { type NpmViewDeps } from './npm-view.js';
29
+ import { type InstallDeps, type SpawnImpl } from './installer.js';
30
+ import type { InstallSource, UpdateCommandPort, UpdateSnapshot } from './types.js';
31
+ /** A timer handle we only ever cancel and `unref`. */
32
+ interface TimerHandle {
33
+ unref?: () => void;
34
+ }
35
+ export interface UpdateServiceDeps {
36
+ config: UpdateConfig;
37
+ /** The running version - `cli.tsx`'s `VERSION`. */
38
+ currentVersion: string;
39
+ now?: () => number;
40
+ fetchImpl?: typeof fetch;
41
+ spawnImpl?: SpawnImpl;
42
+ logger?: ScopedLogger;
43
+ env?: NodeJS.ProcessEnv;
44
+ /** `process.versions.node`, injected so every engine test is offline. */
45
+ nodeVersion?: string;
46
+ /** Overrides `selfPackageRoot()`; tests point it at a synthetic tree. */
47
+ packageRoot?: string | null;
48
+ /** Overrides the name read from `<root>/package.json`. */
49
+ packageName?: string;
50
+ /** Pre-resolved classification, so a test need not build a real tree. */
51
+ source?: InstallSource;
52
+ setTimeoutImpl?: (fn: () => void, ms: number) => TimerHandle;
53
+ clearTimeoutImpl?: (handle: TimerHandle) => void;
54
+ /** Deterministic jitter for tests. */
55
+ random?: () => number;
56
+ /** Forwarded to `runNpmInstall` and `tryAcquireInstallLock`. */
57
+ install?: InstallDeps;
58
+ /** Forwarded to `fetchLatestViaNpm`, so no test spawns npm (H3). */
59
+ npmView?: NpmViewDeps;
60
+ lockPath?: string;
61
+ isProcessAlive?: (pid: number) => boolean;
62
+ }
63
+ type Listener = (snapshot: UpdateSnapshot) => void;
64
+ export declare class UpdateService implements UpdateCommandPort {
65
+ private readonly deps;
66
+ private readonly now;
67
+ private readonly log;
68
+ private readonly listeners;
69
+ private phase;
70
+ private latestVersion;
71
+ private reason;
72
+ private advice;
73
+ /** Set only on the `node-too-old` path; both, or neither (section 6.2). */
74
+ private requiredNode;
75
+ private runningNode;
76
+ private nextAt;
77
+ private failures;
78
+ /**
79
+ * The rollback notice, held for THIS SESSION and cleared from the file the
80
+ * first time it is read (section 5.2a / D-38). Both, or neither.
81
+ */
82
+ private rolledBackFrom;
83
+ private rolledBackTo;
84
+ /** What `aragon update --rollback` would reinstall, for `/update status`. */
85
+ private lastGoodVersion;
86
+ /** Which probe answered the last successful check (H3). */
87
+ private probe;
88
+ private timer;
89
+ private started;
90
+ private disposed;
91
+ private inFlight;
92
+ private resolved;
93
+ constructor(deps: UpdateServiceDeps);
94
+ /**
95
+ * Schedule the first check. Idempotent.
96
+ *
97
+ * The first check fires `UPDATE_LIMITS.startupDelayMs` after this call so it
98
+ * can never contend with first paint, and every timer is `unref()`ed so a
99
+ * pending check never keeps `aragon` from exiting.
100
+ */
101
+ start(): void;
102
+ snapshot(): UpdateSnapshot;
103
+ subscribe(fn: Listener): () => void;
104
+ nextCheckAt(): number | null;
105
+ /**
106
+ * Run a check now, subject to the machine-wide throttle unless forced.
107
+ *
108
+ * SINGLE-FLIGHT. Two `/update now` in a row, or a slash command landing on top
109
+ * of the scheduled tick, must produce one registry request and one install -
110
+ * the in-process half of what `lastCheckAt` does across processes.
111
+ */
112
+ checkNow(opts?: {
113
+ force?: boolean;
114
+ }): Promise<UpdateSnapshot>;
115
+ /**
116
+ * Install a specific version now, bypassing the decision gates.
117
+ *
118
+ * The explicit escape hatch behind `aragon update --to <version>` (non-goal 4:
119
+ * there is no AUTOMATIC downgrade, but a deliberate one is exactly what a user
120
+ * needs when a release misbehaves).
121
+ */
122
+ installNow(version?: string): Promise<UpdateSnapshot>;
123
+ /** Persist `skippedVersion`; the line disappears until a newer one appears. */
124
+ skip(version: string): void;
125
+ /**
126
+ * Clear timers and drop listeners. NEVER KILLS THE INSTALLER CHILD (U-3).
127
+ *
128
+ * Killing a running `npm install -g` is the one action in this feature that
129
+ * can leave the user's global installation broken, and `dispose()` is called
130
+ * by the most ordinary event there is - the user pressing Ctrl+C. The child is
131
+ * detached and `unref()`ed precisely so it can finish without us.
132
+ *
133
+ * Idempotent, and called on BOTH of `runInteractive`'s teardown branches.
134
+ */
135
+ dispose(): void;
136
+ private cancelTimer;
137
+ /**
138
+ * Arm the next check.
139
+ *
140
+ * The jitter is not decoration (D-12 / R-6): machine fleets started by the
141
+ * same automation otherwise arrive at the registry together, and the throttle
142
+ * in `update-state.json` only de-duplicates within ONE machine.
143
+ */
144
+ private schedule;
145
+ /** `backoffBaseMs * 2^(n-1)`, capped. `n` is the consecutive-failure count. */
146
+ private backoffMs;
147
+ private scheduleNext;
148
+ private setPhase;
149
+ /**
150
+ * Step 11 of the happy path: the new version is now the running one, so the
151
+ * pending marker has served its purpose.
152
+ *
153
+ * Silent by design - the user restarted, which is the acknowledgement. The log
154
+ * record is what makes "when did this machine actually pick up 0.6.0"
155
+ * answerable after the fact.
156
+ */
157
+ private reconcilePendingRestart;
158
+ /**
159
+ * Read the rollback notice, hold it for this session, and clear the file
160
+ * (cli-auto-update-hardening section 5.2a / D-38 / D-40).
161
+ *
162
+ * CONSUMING ON READ IS WHAT MAKES "ONE SESSION" TRUE WITHOUT A SECOND
163
+ * TIMESTAMP. It also means a launch that never constructs a service -
164
+ * `update.mode: 'off'`, a non-TTY, CI - does NOT burn the notice, so it
165
+ * survives to the first session that could actually have shown it.
166
+ *
167
+ * `rolledBackTo` is the running version and is therefore DERIVED, not stored:
168
+ * we are, by definition, the target of the rollback that wrote this field. The
169
+ * "both or neither" rule the pair inherits from `requiredNode` / `runningNode`
170
+ * is satisfied by construction rather than by discipline.
171
+ *
172
+ * `lastGoodVersion` is picked up in the same read - it is what
173
+ * `aragon update --rollback` would reinstall, and `/update status` is the only
174
+ * place a user can find that out before running the command.
175
+ */
176
+ private consumeRollbackNotice;
177
+ /** Classify this installation once. Cheap, but it stats the filesystem. */
178
+ private resolve;
179
+ private runCheck;
180
+ /**
181
+ * Report an available version without installing it.
182
+ *
183
+ * `dev-monorepo` and `npx` are SILENT even here (AC-6 / AC-7): a developer's
184
+ * clone is not out of date, it is checked out, and `npx` resolved `latest`
185
+ * seconds ago. Both fall back to `idle`, which `shouldRenderUpdateLine`
186
+ * answers `false` for.
187
+ */
188
+ private offer;
189
+ private performInstall;
190
+ private recordFailure;
191
+ private clearFailures;
192
+ }
193
+ export {};