@vzn/vx 0.0.645 → 0.0.648
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 +39 -50
- package/launcher.cjs +9 -2
- package/launcher.sh +2 -2
- package/package.json +5 -5
- package/schemas/cache.json +5 -5
- package/schemas/error.json +1 -1
- package/schemas/info.json +16 -2
- package/schemas/last.json +127 -5
- package/schemas/plan.json +28 -2
- package/schemas/show.json +10 -1
- package/skills/vx/SKILL.md +19 -3
- package/src/bin.ts +54 -13
- package/src/cache/archive.ts +324 -23
- package/src/cache/cache.ts +330 -65
- package/src/cache/git-inputs.ts +274 -97
- package/src/cache/index.ts +7 -2
- package/src/cache/inputs.ts +515 -83
- package/src/cache/key-fold.ts +15 -6
- package/src/cache/layer.ts +74 -11
- package/src/cache/layered-cache.ts +154 -59
- package/src/cache/linked-inputs.ts +1 -1
- package/src/cache/output-index.ts +44 -14
- package/src/cache/plugin-cache.ts +49 -23
- package/src/cache/remote-hits.ts +72 -0
- package/src/cache/remote-lane.ts +100 -15
- package/src/cache/run-history.ts +77 -23
- package/src/cache/run-store.ts +11 -10
- package/src/cache/schema.ts +42 -6
- package/src/cache/tar-stream.ts +5 -0
- package/src/cache/umask.ts +1 -2
- package/src/cli/cache.ts +58 -27
- package/src/cli/completions.ts +12 -5
- package/src/cli/core-alias.ts +5 -2
- package/src/cli/docs.ts +42 -16
- package/src/cli/help.ts +62 -25
- package/src/cli/index.ts +79 -45
- package/src/cli/info.ts +12 -6
- package/src/cli/init.ts +222 -34
- package/src/cli/last.ts +443 -47
- package/src/cli/lock.ts +197 -52
- package/src/cli/plan-format.ts +91 -49
- package/src/cli/plugin-commands.ts +160 -11
- package/src/cli/plugin-templates.ts +13 -13
- package/src/cli/registry.ts +209 -0
- package/src/cli/run.ts +111 -41
- package/src/cli/select.ts +148 -49
- package/src/cli/show.ts +59 -16
- package/src/cli/task-verb.ts +16 -6
- package/src/cli/upgrade.ts +155 -54
- package/src/cli/watch-cycle.ts +10 -5
- package/src/cli/watch-filter.ts +71 -3
- package/src/cli/watch-fs.ts +22 -6
- package/src/cli/watch-judge.ts +48 -6
- package/src/cli/watch.ts +163 -42
- package/src/cli/why.ts +211 -73
- package/src/cli/workspace-config.ts +54 -15
- package/src/config.ts +13 -1
- package/src/exec/env.ts +7 -2
- package/src/exec/executor.ts +262 -15
- package/src/exec/index.ts +8 -2
- package/src/exec/kill-tree.ts +15 -0
- package/src/exec/local-executor.ts +1 -1
- package/src/exec/proc-sample.ts +70 -6
- package/src/exec/runner.ts +256 -53
- package/src/exec/sandbox-binds.ts +7 -3
- package/src/exec/sandbox-paths.ts +1 -1
- package/src/exec/sandbox-runtime.ts +222 -28
- package/src/exec/sandbox-violations.ts +52 -20
- package/src/graph/critical-path.ts +79 -0
- package/src/graph/default-build.ts +19 -0
- package/src/graph/definition.ts +38 -0
- package/src/graph/folds.ts +38 -5
- package/src/graph/index.ts +4 -1
- package/src/graph/priorities.ts +74 -8
- package/src/graph/scheduler.ts +326 -65
- package/src/graph/task-graph.ts +228 -35
- package/src/handoff.ts +97 -9
- package/src/history/failure-mode.ts +6 -3
- package/src/history/history.ts +268 -50
- package/src/history/index.ts +2 -0
- package/src/history/metrics.ts +142 -45
- package/src/index.ts +42 -10
- package/src/orchestrator/admission.ts +10 -6
- package/src/orchestrator/affected-tasks.ts +11 -6
- package/src/orchestrator/deferred-outputs.ts +2 -0
- package/src/orchestrator/doctor.ts +134 -42
- package/src/orchestrator/execute-args.ts +6 -0
- package/src/orchestrator/execute-task.ts +155 -42
- package/src/orchestrator/fingerprint-watch.ts +1 -1
- package/src/orchestrator/graph-memo.ts +140 -0
- package/src/orchestrator/graph-stage.ts +18 -1
- package/src/orchestrator/held-servers.ts +86 -0
- package/src/orchestrator/hit-restore.ts +227 -122
- package/src/orchestrator/index.ts +20 -3
- package/src/orchestrator/local-floor.ts +198 -0
- package/src/orchestrator/local-shortcircuit.ts +51 -8
- package/src/orchestrator/lockfile-claim.ts +96 -16
- package/src/orchestrator/miss-save.ts +52 -12
- package/src/orchestrator/options.ts +8 -6
- package/src/orchestrator/output-host.ts +43 -14
- package/src/orchestrator/output-log.ts +90 -10
- package/src/orchestrator/persistent-task.ts +97 -28
- package/src/orchestrator/persistent.ts +16 -4
- package/src/orchestrator/placement.ts +257 -14
- package/src/orchestrator/plan.ts +64 -7
- package/src/orchestrator/plugin-host.ts +230 -40
- package/src/orchestrator/plugin.ts +150 -37
- package/src/orchestrator/prepare.ts +178 -32
- package/src/orchestrator/projects.ts +65 -40
- package/src/orchestrator/remote-prefetch.ts +119 -25
- package/src/orchestrator/run-artifacts.ts +95 -32
- package/src/orchestrator/run-context.ts +39 -41
- package/src/orchestrator/run-failures.ts +65 -23
- package/src/orchestrator/run-id.ts +6 -3
- package/src/orchestrator/run-keys.ts +300 -0
- package/src/orchestrator/run-live.ts +294 -0
- package/src/orchestrator/run-lock.ts +13 -7
- package/src/orchestrator/run-records.ts +35 -23
- package/src/orchestrator/run-report.ts +3 -3
- package/src/orchestrator/run-steps.ts +58 -22
- package/src/orchestrator/run-stop.ts +22 -5
- package/src/orchestrator/run.ts +409 -79
- package/src/orchestrator/sandbox-fix.ts +7 -5
- package/src/orchestrator/sandbox-request.ts +78 -22
- package/src/orchestrator/shell-verdict.ts +5 -5
- package/src/orchestrator/signals.ts +26 -1
- package/src/orchestrator/stable-keys.ts +92 -8
- package/src/orchestrator/steps.ts +2 -2
- package/src/orchestrator/task-artifacts.ts +206 -0
- package/src/orchestrator/task-hash.ts +89 -14
- package/src/orchestrator/task-log-buffer.ts +72 -36
- package/src/orchestrator/task-process.ts +22 -0
- package/src/orchestrator/telemetry-host.ts +24 -3
- package/src/orchestrator/telemetry.ts +113 -38
- package/src/orchestrator/upstream.ts +15 -0
- package/src/report/events.ts +32 -13
- package/src/report/forecast.ts +36 -35
- package/src/report/framed-output.ts +173 -46
- package/src/report/index.ts +1 -0
- package/src/report/logger.ts +217 -186
- package/src/report/path-links.ts +12 -4
- package/src/report/plain-output.ts +13 -5
- package/src/report/status-line.ts +94 -46
- package/src/report/summary.ts +174 -38
- package/src/report/terminal-signals.ts +13 -2
- package/src/settings.ts +14 -6
- package/src/util/bun-version.ts +4 -1
- package/src/util/colors.ts +20 -3
- package/src/util/config-frame.ts +41 -4
- package/src/{graph → util}/dependency-spec.ts +22 -13
- package/src/util/edit-distance.ts +13 -1
- package/src/util/env-flag.ts +9 -0
- package/src/util/errors.ts +59 -3
- package/src/util/essential-env.ts +57 -0
- package/src/util/git-ignored.ts +1 -1
- package/src/util/hash.ts +35 -0
- package/src/util/hide-env.ts +45 -5
- package/src/util/index.ts +22 -5
- package/src/util/num.ts +8 -0
- package/src/util/paths.ts +121 -21
- package/src/util/report.ts +6 -1
- package/src/util/secret-mask.ts +62 -1
- package/src/util/secrets.ts +7 -2
- package/src/util/size.ts +11 -3
- package/src/util/tail.ts +6 -1
- package/src/util/timing.ts +13 -1
- package/src/util/ui.ts +65 -4
- package/src/util/which.ts +1 -1
- package/src/util/write-file.ts +67 -0
- package/src/workspace/affected.ts +710 -107
- package/src/workspace/bracket-literal.ts +84 -0
- package/src/workspace/canonical-dir.ts +37 -0
- package/src/workspace/config-bytecode.ts +282 -0
- package/src/workspace/config-eval.ts +29 -8
- package/src/workspace/config-imports.ts +199 -58
- package/src/workspace/config-schema.ts +242 -39
- package/src/workspace/filter.ts +37 -8
- package/src/workspace/index.ts +11 -2
- package/src/workspace/json-data.ts +3 -4
- package/src/workspace/lockfile.ts +57 -8
- package/src/workspace/migration.ts +72 -49
- package/src/workspace/package-graph.ts +5 -4
- package/src/workspace/project-loader.ts +314 -47
- package/src/workspace/repo-id.ts +1 -1
- package/src/workspace/workspace.ts +176 -33
- package/src/cli/foreign-flags.ts +0 -727
package/src/cli/docs.ts
CHANGED
|
@@ -1,10 +1,11 @@
|
|
|
1
1
|
// `vx docs <query> [--limit N] [--format pretty|json]` — search vx's
|
|
2
2
|
// reference as this version wrote it: packages/vx/docs at its git tag on
|
|
3
|
-
// GitHub, read online (
|
|
3
|
+
// GitHub, read online (2026-10-17: nothing is embedded). Each page is
|
|
4
4
|
// cut at its headings, and the sections holding every query word print
|
|
5
5
|
// whole, best first.
|
|
6
6
|
|
|
7
7
|
import { VERSION } from '../version.js'
|
|
8
|
+
import { detectColors, paint, parseDecimalInt, ui } from '../util/index.js'
|
|
8
9
|
import { flagHint, formatValue, refuse, refusedWord, seeHelp } from './help.js'
|
|
9
10
|
|
|
10
11
|
const SITE = 'https://vznjs.github.io/vx/'
|
|
@@ -19,7 +20,7 @@ interface DocsArgs {
|
|
|
19
20
|
error?: string
|
|
20
21
|
}
|
|
21
22
|
|
|
22
|
-
function parseDocsArgs(args: readonly string[]): DocsArgs {
|
|
23
|
+
export function parseDocsArgs(args: readonly string[]): DocsArgs {
|
|
23
24
|
const out: DocsArgs = { terms: [], limit: DEFAULT_LIMIT, json: false }
|
|
24
25
|
for (let i = 0; i < args.length; i++) {
|
|
25
26
|
const a = args[i]!
|
|
@@ -29,11 +30,17 @@ function parseDocsArgs(args: readonly string[]): DocsArgs {
|
|
|
29
30
|
out.json = fv === 'json'
|
|
30
31
|
} else if (a === '--limit' || a.startsWith('--limit=')) {
|
|
31
32
|
const v = a === '--limit' ? args[++i] : a.slice('--limit='.length)
|
|
32
|
-
|
|
33
|
-
if (!Number.isInteger(n) || n < 1) {
|
|
33
|
+
if (v === undefined || v === '') {
|
|
34
34
|
return {
|
|
35
35
|
...out,
|
|
36
|
-
error: `--limit
|
|
36
|
+
error: `--limit requires a value: a whole number above 0${seeHelp('docs')}`,
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
const n = parseDecimalInt(v)
|
|
40
|
+
if (n === null || n < 1) {
|
|
41
|
+
return {
|
|
42
|
+
...out,
|
|
43
|
+
error: `--limit takes a whole number above 0 (got ${v})${seeHelp('docs')}`,
|
|
37
44
|
}
|
|
38
45
|
}
|
|
39
46
|
out.limit = n
|
|
@@ -52,7 +59,7 @@ export interface DocsHit {
|
|
|
52
59
|
}
|
|
53
60
|
|
|
54
61
|
function words(s: string): string[] {
|
|
55
|
-
return s.toLowerCase().match(/[
|
|
62
|
+
return s.toLowerCase().match(/[\p{L}\p{N}][\p{L}\p{N}_.-]*/gu) ?? []
|
|
56
63
|
}
|
|
57
64
|
|
|
58
65
|
/** GitHub's heading anchor, which Starlight shares: lowercase, punctuation dropped, spaces to `-`. */
|
|
@@ -63,7 +70,7 @@ function anchor(heading: string): string {
|
|
|
63
70
|
.replace(/ /g, '-')
|
|
64
71
|
}
|
|
65
72
|
|
|
66
|
-
/** A page's URL on the site, as
|
|
73
|
+
/** A page's URL on the site, as private/vx-docs/scripts/import-docs.ts serves it. */
|
|
67
74
|
function pageUrl(page: string): string {
|
|
68
75
|
const served: Record<string, string> = {
|
|
69
76
|
README: 'overview',
|
|
@@ -117,6 +124,8 @@ export async function loadPages(
|
|
|
117
124
|
interface Section {
|
|
118
125
|
page: string
|
|
119
126
|
heading: string
|
|
127
|
+
/** The heading's id on the site; absent for the page's opening. */
|
|
128
|
+
id?: string
|
|
120
129
|
text: string
|
|
121
130
|
}
|
|
122
131
|
|
|
@@ -124,18 +133,31 @@ interface Section {
|
|
|
124
133
|
function sections(page: string, md: string): Section[] {
|
|
125
134
|
const out: Section[] = []
|
|
126
135
|
let heading = ''
|
|
136
|
+
let id: string | undefined
|
|
127
137
|
let lines: string[] = []
|
|
128
138
|
let fence = false
|
|
139
|
+
// The site numbers a repeated id (`usage`, `usage-1`) over every `##` to
|
|
140
|
+
// `######` heading of the page, as import-docs.ts's `headingIds` does:
|
|
141
|
+
// cli.md repeats Usage per verb, and each linked the first.
|
|
142
|
+
const seen = new Map<string, number>()
|
|
129
143
|
const flush = () => {
|
|
130
144
|
const text = lines.join('\n').trim()
|
|
131
|
-
if (text !== '') out.push({ page, heading, text })
|
|
145
|
+
if (text !== '') out.push({ page, heading, ...(id !== undefined ? { id } : {}), text })
|
|
132
146
|
}
|
|
133
147
|
for (const line of md.split('\n')) {
|
|
134
148
|
if (/^\s*(```|~~~)/.test(line)) fence = !fence
|
|
135
|
-
const h = fence ? null :
|
|
136
|
-
|
|
149
|
+
const h = fence ? null : /^(#{1,6}) (.+)$/.exec(line)
|
|
150
|
+
let slug: string | undefined
|
|
151
|
+
if (h !== null && h[1]!.length > 1) {
|
|
152
|
+
const base = anchor(h[2]!.replace(/`/g, '').trim())
|
|
153
|
+
const n = seen.get(base) ?? 0
|
|
154
|
+
seen.set(base, n + 1)
|
|
155
|
+
slug = n === 0 ? base : `${base}-${n}`
|
|
156
|
+
}
|
|
157
|
+
if (h !== null && h[1]!.length <= 4) {
|
|
137
158
|
flush()
|
|
138
|
-
heading = h[
|
|
159
|
+
heading = h[2]!.replace(/`/g, '').trim()
|
|
160
|
+
id = slug ?? anchor(heading)
|
|
139
161
|
lines = [line]
|
|
140
162
|
} else lines.push(line)
|
|
141
163
|
}
|
|
@@ -182,7 +204,7 @@ export function searchDocs(
|
|
|
182
204
|
return (exact.length > 0 ? exact : scored).slice(0, limit).map(({ s }) => ({
|
|
183
205
|
page: s.page,
|
|
184
206
|
heading: s.heading,
|
|
185
|
-
url: s.
|
|
207
|
+
url: s.id === undefined ? pageUrl(s.page) : `${pageUrl(s.page)}#${s.id}`,
|
|
186
208
|
text: s.text,
|
|
187
209
|
}))
|
|
188
210
|
}
|
|
@@ -220,16 +242,20 @@ export async function docsCmd(
|
|
|
220
242
|
return 0
|
|
221
243
|
}
|
|
222
244
|
if (hits.length === 0) {
|
|
223
|
-
|
|
245
|
+
ui.say(
|
|
246
|
+
'warn',
|
|
224
247
|
`vx docs: no section of the docs at ${ref} holds every word of "${query}"; try fewer words, or ${SITE}llms.txt\n`,
|
|
248
|
+
'out',
|
|
225
249
|
)
|
|
226
250
|
return 0
|
|
227
251
|
}
|
|
252
|
+
const c = detectColors(process.stdout)
|
|
228
253
|
process.stdout.write(
|
|
229
254
|
hits
|
|
230
|
-
.map(
|
|
231
|
-
|
|
232
|
-
|
|
255
|
+
.map((h) =>
|
|
256
|
+
c.enabled
|
|
257
|
+
? `${ui.render([ui.excerpt(`${h.page}.md${h.heading === '' ? '' : ` § ${h.heading}`}`, h.text)], c)}${paint('', h.url, c, { dim: true })}\n`
|
|
258
|
+
: `── ${h.page}.md${h.heading === '' ? '' : ` § ${h.heading}`} · ${h.url}\n\n${h.text}\n`,
|
|
233
259
|
)
|
|
234
260
|
.join('\n'),
|
|
235
261
|
)
|
package/src/cli/help.ts
CHANGED
|
@@ -114,14 +114,14 @@ export function helpText(pluginCommands: readonly string[] = []): string {
|
|
|
114
114
|
'Usage:',
|
|
115
115
|
' vx run [OPTIONS] [TASK | PKG#TASK ...] [-- forwarded-args...]',
|
|
116
116
|
' vx watch [OPTIONS] [--poll] TASK [-- forwarded-args...]',
|
|
117
|
-
' vx cache clean [--older-than <duration>] [--max-size <size>] [--global] [--dry
|
|
117
|
+
' vx cache clean [--older-than <duration>] [--max-size <size>] [--global] [--dry] [--format pretty|json] [--cache-dir <path>]',
|
|
118
118
|
' vx lock [--check] [--format pretty|json]',
|
|
119
|
-
' vx init [--dry [--format pretty|json]] [--force] [--mjs] [--native|--keep] [--plugin <
|
|
119
|
+
' vx init [--dry [--format pretty|json]] [--force] [--mjs] [--native|--keep] [--plugin <hook>]',
|
|
120
120
|
' vx upgrade [TAG] [-g|--global]',
|
|
121
121
|
' vx show [PROJECT[#TASK] | TASK] [--filter <pattern>] [--affected[=<ref>]] [--format pretty|json]',
|
|
122
122
|
' vx info [--format pretty|json] [--cache-dir <path>]',
|
|
123
123
|
' vx why (TASK | PKG#TASK) [--run RUNID] [--format pretty|json] [--cache-dir <path>]',
|
|
124
|
-
' vx last [RUNID] [--list[=N]] [--failed] [--log <task>] [--stats [<task>] [--branch <name>]] [--format pretty|json] [--cache-dir <path>]',
|
|
124
|
+
' vx last [RUNID] [--list[=N]] [--failed] [--log <task>] [--stats [<task>] [--branch <name>]] [--sql <query>] [--live [--follow]] [--format pretty|json] [--cache-dir <path>]',
|
|
125
125
|
' vx docs <query> [--limit N] [--format pretty|json]',
|
|
126
126
|
' vx completions bash|zsh|fish',
|
|
127
127
|
' vx help [VERB]',
|
|
@@ -186,7 +186,7 @@ export function helpText(pluginCommands: readonly string[] = []): string {
|
|
|
186
186
|
' summary table after the run.',
|
|
187
187
|
'',
|
|
188
188
|
'Planning (for run — skips execution):',
|
|
189
|
-
" --dry[=
|
|
189
|
+
" --dry[=pretty|json] Print the task graph and each task's predicted hit",
|
|
190
190
|
' or miss.',
|
|
191
191
|
'',
|
|
192
192
|
'Artifacts (for run):',
|
|
@@ -197,13 +197,7 @@ export function helpText(pluginCommands: readonly string[] = []): string {
|
|
|
197
197
|
' cache, distributed execution, and telemetry export are provided by',
|
|
198
198
|
' PLUGINS declared in .vx/workspace.ts — first-party or your own. Core',
|
|
199
199
|
' depends on none of them; a plain `vx run` never requires a plugin.',
|
|
200
|
-
' See the plugin guide: https://vznjs.github.io/vx/
|
|
201
|
-
'',
|
|
202
|
-
'Turbo and Nx spellings (for run):',
|
|
203
|
-
' What turbo run / nx run-many / nx affected take works here, or is refused',
|
|
204
|
-
' naming the vx spelling: -t build,test, -p app, --exclude docs, --parallel 4,',
|
|
205
|
-
' and --base main, --dry-run, --skip-nx-cache. `vx build` names `vx run build`.',
|
|
206
|
-
' Every flag: https://vznjs.github.io/vx/cli/#turbo-and-nx-flags',
|
|
200
|
+
' See the plugin guide: https://vznjs.github.io/vx/docs/plugins/',
|
|
207
201
|
'',
|
|
208
202
|
'Argument forwarding (for run):',
|
|
209
203
|
" Anything after `--` is forwarded (shell-quoted) to the task's exec",
|
|
@@ -221,11 +215,13 @@ export function helpText(pluginCommands: readonly string[] = []): string {
|
|
|
221
215
|
'Cache management:',
|
|
222
216
|
' vx cache clean --older-than 30d Evict entries last accessed > 30 days ago.',
|
|
223
217
|
' vx cache clean --max-size 1G Keep total cache under 1 GB (LRU eviction).',
|
|
224
|
-
" vx cache clean Clear .vx/cache and this repo's store
|
|
218
|
+
" vx cache clean Clear .vx/cache and this repo's store",
|
|
219
|
+
' ~/.vx/<id>/cache.',
|
|
225
220
|
" --global Clear every repo's store under ~/.vx.",
|
|
226
|
-
' --dry
|
|
221
|
+
' --dry Say what the policy would reap; delete nothing.',
|
|
227
222
|
' --format <fmt> pretty (default) | json.',
|
|
228
|
-
' --cache-dir <path> The cache a run with the same flag uses (why,
|
|
223
|
+
' --cache-dir <path> The cache a run with the same flag uses (why,',
|
|
224
|
+
' last, info too).',
|
|
229
225
|
'',
|
|
230
226
|
' Duration units: ms, s, m, h, d. Size units: K, M, G, T (powers of 1024).',
|
|
231
227
|
' Cache statistics — directory, entry count, size — are part of `vx info`.',
|
|
@@ -259,6 +255,10 @@ export function helpText(pluginCommands: readonly string[] = []): string {
|
|
|
259
255
|
' --stats [<task>] Per-task p50/p99, pass and hit rate, failure mode, peak',
|
|
260
256
|
' RSS and CPU over the last 50 runs; a task id or name narrows.',
|
|
261
257
|
" --branch <name> The branch --stats' p50 forecasts from (default: the run's).",
|
|
258
|
+
' --sql <query> One read-only SELECT over the history (runs, invocations,',
|
|
259
|
+
' entries, task_outcomes, ...): filter, group, trend.',
|
|
260
|
+
' --live The run in progress: running, ended, waiting tasks, warnings.',
|
|
261
|
+
" --follow With --live, the run's output and events until it ends.",
|
|
262
262
|
' --format <fmt> pretty (default) | json.',
|
|
263
263
|
' info, why and last:',
|
|
264
264
|
' --cache-dir <path> Read the cache a run with the same flag uses.',
|
|
@@ -274,7 +274,7 @@ export function helpText(pluginCommands: readonly string[] = []): string {
|
|
|
274
274
|
' --mjs Write .mjs files (no type import) instead of .ts.',
|
|
275
275
|
' --native Turbo or Nx: write a vx.config.ts per package (no question).',
|
|
276
276
|
' --keep Turbo or Nx: only .vx/workspace.ts, declaring turbo() or nx().',
|
|
277
|
-
' --plugin <
|
|
277
|
+
' --plugin <hook> Instead: write plugins/<hook>.ts, a runnable plugin, and its',
|
|
278
278
|
' test (executor, cache, telemetry, schedule, admit, commands,',
|
|
279
279
|
' project, graph, key).',
|
|
280
280
|
'',
|
|
@@ -304,11 +304,11 @@ export function helpText(pluginCommands: readonly string[] = []): string {
|
|
|
304
304
|
' into .vx/projects.lock.json, the only place a run reads configs',
|
|
305
305
|
' from. A run never evaluates one, never checks the rules',
|
|
306
306
|
' and never writes the lock: with none it fails.',
|
|
307
|
-
' vx lock --check Audit the lock:
|
|
308
|
-
'
|
|
309
|
-
' graph rules. Exits 1 on drift (e.g. configs that
|
|
310
|
-
' vars). Run it before the run in CI. With --format json
|
|
311
|
-
' {upToDate, audited, notAudited, problems}.',
|
|
307
|
+
' vx lock --check Audit the lock: re-evaluate every config here and compare',
|
|
308
|
+
' it with the frozen object, find a config added or gone, and',
|
|
309
|
+
' check the graph rules. Exits 1 on drift (e.g. configs that',
|
|
310
|
+
' read env vars). Run it before the run in CI. With --format json',
|
|
311
|
+
' it prints {upToDate, audited, notAudited, problems}.',
|
|
312
312
|
'',
|
|
313
313
|
...(pluginCommands.length > 0 ? ['Plugin commands:', ...pluginCommands, ''] : []),
|
|
314
314
|
].join('\n')
|
|
@@ -323,6 +323,27 @@ export function seeHelp(verb: string): string {
|
|
|
323
323
|
return ` (see \`vx ${verb} --help\`)`
|
|
324
324
|
}
|
|
325
325
|
|
|
326
|
+
/**
|
|
327
|
+
* The refusal for a `project#task` spec with an empty half, or undefined.
|
|
328
|
+
* An empty task wins over an empty project: `#` names no task to run in
|
|
329
|
+
* every project, so `bare(task)` would print a hint with no task in it.
|
|
330
|
+
*/
|
|
331
|
+
export function anchoredSpecError(
|
|
332
|
+
spec: string,
|
|
333
|
+
bare: (task: string) => string,
|
|
334
|
+
): string | undefined {
|
|
335
|
+
const idx = spec.indexOf('#')
|
|
336
|
+
if (idx < 0) return undefined
|
|
337
|
+
const project = spec.slice(0, idx)
|
|
338
|
+
const task = spec.slice(idx + 1)
|
|
339
|
+
if (!task) {
|
|
340
|
+
const list = project ? `vx show ${project}` : 'vx show'
|
|
341
|
+
return `missing task name after '#' in "${spec}" (${list} lists the tasks)`
|
|
342
|
+
}
|
|
343
|
+
if (!project) return `missing project name before '#' in "${spec}" (${bare(task)})`
|
|
344
|
+
return undefined
|
|
345
|
+
}
|
|
346
|
+
|
|
326
347
|
/** `--format`'s value, or the one refusal every verb gives for a bad one. */
|
|
327
348
|
export function formatValue(
|
|
328
349
|
v: string | undefined,
|
|
@@ -343,8 +364,8 @@ export function refusedWord(arg: string): string {
|
|
|
343
364
|
}
|
|
344
365
|
|
|
345
366
|
/**
|
|
346
|
-
* `(did you mean --concurrency?)` for a flag within two edits
|
|
347
|
-
* accepts; empty for anything else (a positional included). A third edit
|
|
367
|
+
* `(did you mean --concurrency?)` for a flag within two edits (one, for a
|
|
368
|
+
* name of four letters or fewer) of one `verb` accepts; empty for anything else (a positional included). A third edit
|
|
348
369
|
* is allowed only between flags that share their first five characters,
|
|
349
370
|
* so `--timeouts` reaches `--timeout` while `--zzz` does not reach `--all`.
|
|
350
371
|
*/
|
|
@@ -356,9 +377,25 @@ export function flagHint(verb: string, arg: string): string {
|
|
|
356
377
|
// `--json` is gh's and Nx's spelling; a verb with `--format` reads it
|
|
357
378
|
// there, and no edit budget reaches `--format json` from `--json`.
|
|
358
379
|
if (name === '--json' && flags.includes('--format')) return ' (did you mean --format json?)'
|
|
359
|
-
//
|
|
360
|
-
//
|
|
361
|
-
|
|
380
|
+
// `vx run` prints no JSON of its own, and edit distance reached `--force`
|
|
381
|
+
// from `--format`: a run with its cache reads turned off.
|
|
382
|
+
if ((name === '--format' || name === '--json') && flags.includes('--dry'))
|
|
383
|
+
return ' (the run as JSON is `vx last --format json` after it; the plan is --dry=json)'
|
|
384
|
+
// Watch takes run's flags but --dry, so the branch above missed it.
|
|
385
|
+
if ((name === '--format' || name === '--json') && verb === 'watch')
|
|
386
|
+
return " (vx watch prints no JSON; each cycle's run is `vx last --format json` after it)"
|
|
387
|
+
// A bare flag handed a value (`--check=yes`) is no edit from itself.
|
|
388
|
+
if (name !== arg && flags.includes(name)) return ` (did you mean ${name}?)`
|
|
389
|
+
// Another verb's own flag is no typo: `vx run --poll` was told "did you
|
|
390
|
+
// mean --all?". Named only when one other verb takes it.
|
|
391
|
+
const owners = CORE_VERBS.filter(
|
|
392
|
+
(v) => v !== verb.split(' ')[0] && acceptedFlags(v).includes(name),
|
|
393
|
+
)
|
|
394
|
+
if (owners.length === 1)
|
|
395
|
+
return `. \`vx ${owners[0] === 'cache' ? 'cache clean' : owners[0]}\` takes it`
|
|
396
|
+
// A flag one word too long (`--dry-run` for `--dry`) is too many edits
|
|
397
|
+
// away for either budget, and so is one cut short at a word.
|
|
398
|
+
const cut = flags.filter((f) => f.startsWith(`${name}-`) || name.startsWith(`${f}-`))
|
|
362
399
|
const best =
|
|
363
400
|
nearest(name, flags) ??
|
|
364
401
|
nearest(
|
package/src/cli/index.ts
CHANGED
|
@@ -3,9 +3,9 @@
|
|
|
3
3
|
|
|
4
4
|
import { VERSION } from '../version.js'
|
|
5
5
|
import { checkSettingsEnv } from '../settings.js'
|
|
6
|
-
import { CORE_VERBS, flagHint, printHelp, refusedWord, seeHelp } from './help.js'
|
|
7
|
-
import { FOREIGN_VERBS } from './foreign-flags.js'
|
|
6
|
+
import { CORE_VERBS, flagHint, printHelp, refuse, refusedWord, seeHelp } from './help.js'
|
|
8
7
|
import {
|
|
8
|
+
detectColors,
|
|
9
9
|
errorDocument,
|
|
10
10
|
hideEnvFromTasks,
|
|
11
11
|
sayNotice,
|
|
@@ -15,6 +15,7 @@ import {
|
|
|
15
15
|
maskedLine,
|
|
16
16
|
nearest,
|
|
17
17
|
refusalNotice,
|
|
18
|
+
thrownMessage,
|
|
18
19
|
ui,
|
|
19
20
|
UserError,
|
|
20
21
|
wantsJson,
|
|
@@ -29,6 +30,7 @@ import {
|
|
|
29
30
|
const plugins = () => import('./plugin-commands.js')
|
|
30
31
|
|
|
31
32
|
export async function run(argv: readonly string[]): Promise<number> {
|
|
33
|
+
refuseLossyArgs(argv)
|
|
32
34
|
const [command, ...rest] = argv
|
|
33
35
|
await hideEnvFromTasks()
|
|
34
36
|
|
|
@@ -42,8 +44,11 @@ export async function run(argv: readonly string[]): Promise<number> {
|
|
|
42
44
|
return 0
|
|
43
45
|
}
|
|
44
46
|
// Each setting's variable once, before any verb: a renamed one is named,
|
|
45
|
-
// and a deep reader trusts what it reads.
|
|
46
|
-
|
|
47
|
+
// and a deep reader trusts what it reads. Not for help, as `vx run
|
|
48
|
+
// --help` above is not: help is where a setting's values are read. Nor
|
|
49
|
+
// for `vx info`, which reports a refused one (doctor.ts).
|
|
50
|
+
if (command !== 'help' && command !== '--help' && command !== '-h' && command !== 'info')
|
|
51
|
+
checkSettingsEnv()
|
|
47
52
|
|
|
48
53
|
try {
|
|
49
54
|
return await dispatch(command, rest)
|
|
@@ -52,9 +57,23 @@ export async function run(argv: readonly string[]): Promise<number> {
|
|
|
52
57
|
}
|
|
53
58
|
}
|
|
54
59
|
|
|
60
|
+
/**
|
|
61
|
+
* Bun decodes argv as lossy UTF-8 and exposes no raw bytes on macOS, so
|
|
62
|
+
* `-- $'a\xfe'` and `-- $'a\xff'` both became `a` U+FFFD: the task got
|
|
63
|
+
* bytes nobody typed and the two runs shared a key (a stale hit). A real
|
|
64
|
+
* U+FFFD cannot be told from a replaced byte, so any arg holding one is refused.
|
|
65
|
+
*/
|
|
66
|
+
function refuseLossyArgs(argv: readonly string[]): void {
|
|
67
|
+
const at = argv.findIndex((a) => a.includes('�'))
|
|
68
|
+
if (at === -1) return
|
|
69
|
+
throw new UserError(
|
|
70
|
+
`argument ${at + 1} (${JSON.stringify(argv[at])}) is not valid UTF-8, or holds U+FFFD; vx cannot pass it on byte for byte`,
|
|
71
|
+
)
|
|
72
|
+
}
|
|
73
|
+
|
|
55
74
|
/**
|
|
56
75
|
* A crash from a hand-broken lock entry is that entry's fault: runs validate
|
|
57
|
-
* no entry, so it surfaced as a raw TypeError (
|
|
76
|
+
* no entry, so it surfaced as a raw TypeError (2026-10-14). The lock is
|
|
58
77
|
* read only on the crash path, so a normal run pays nothing.
|
|
59
78
|
*/
|
|
60
79
|
async function brokenLockNamed(err: unknown): Promise<unknown> {
|
|
@@ -62,7 +81,8 @@ async function brokenLockNamed(err: unknown): Promise<unknown> {
|
|
|
62
81
|
try {
|
|
63
82
|
const { findWorkspaceRoot, LOCK_INVALID, lockEntryBroken } =
|
|
64
83
|
await import('../workspace/index.js')
|
|
65
|
-
if (lockEntryBroken(await findWorkspaceRoot(process.cwd())))
|
|
84
|
+
if (lockEntryBroken(await findWorkspaceRoot(process.cwd())))
|
|
85
|
+
return new UserError(LOCK_INVALID, 'VX_E_LOCK')
|
|
66
86
|
} catch {}
|
|
67
87
|
return err
|
|
68
88
|
}
|
|
@@ -86,41 +106,45 @@ async function dispatch(command: string | undefined, rest: string[]): Promise<nu
|
|
|
86
106
|
}
|
|
87
107
|
case '--help':
|
|
88
108
|
case '-h':
|
|
109
|
+
if (rest.length > 0) return dispatch('help', rest)
|
|
89
110
|
await printHelp(await (await plugins()).pluginCommandHelp())
|
|
90
111
|
return 0
|
|
91
112
|
case 'help': {
|
|
92
|
-
// `vx help run` is the same question as `vx run --help
|
|
93
|
-
//
|
|
94
|
-
|
|
95
|
-
|
|
113
|
+
// `vx help run` is the same question as `vx run --help`, and so is
|
|
114
|
+
// `vx help <plugin verb>`: a plugin verb owns its own help.
|
|
115
|
+
const [verb, extra] = rest
|
|
116
|
+
if (extra !== undefined) {
|
|
117
|
+
return refuse(
|
|
118
|
+
'help',
|
|
119
|
+
rest,
|
|
120
|
+
`${refusedWord(extra)}: ${extra}${flagHint('help', extra)} (see \`vx help\`)`,
|
|
121
|
+
'VX_E_USAGE',
|
|
122
|
+
)
|
|
123
|
+
}
|
|
96
124
|
const core = verb !== undefined && (CORE_VERBS as readonly string[]).includes(verb)
|
|
97
125
|
if (verb !== undefined && !core) {
|
|
98
126
|
// A name that is no verb here is the typo `vx rnu` is, not a request
|
|
99
127
|
// for the whole reference: it printed 145 lines and no hint.
|
|
100
128
|
const resolved = await (await plugins()).resolvePluginCommand(verb)
|
|
101
129
|
const unknown = resolved === null || 'declaredVerbs' in resolved
|
|
102
|
-
const pointer = FOREIGN_VERBS[verb]
|
|
103
|
-
if (unknown && pointer !== undefined) {
|
|
104
|
-
sayNotice('info', 'vx', pointer)
|
|
105
|
-
return 1
|
|
106
|
-
}
|
|
107
130
|
if (unknown && verb.startsWith('-')) {
|
|
108
|
-
|
|
109
|
-
'
|
|
110
|
-
|
|
131
|
+
return refuse(
|
|
132
|
+
'help',
|
|
133
|
+
rest,
|
|
111
134
|
`unknown flag: ${verb}${flagHint('help', verb)} (see \`vx help\`)`,
|
|
135
|
+
'VX_E_USAGE',
|
|
112
136
|
)
|
|
113
|
-
return 1
|
|
114
137
|
}
|
|
115
138
|
if (unknown) {
|
|
116
139
|
const declared = resolved === null ? [] : resolved.declaredVerbs
|
|
117
|
-
|
|
118
|
-
'
|
|
119
|
-
|
|
140
|
+
return refuse(
|
|
141
|
+
'help',
|
|
142
|
+
rest,
|
|
120
143
|
`unknown command: ${verb}${didYouMeanVerb(verb, declared)} (see \`vx help\`)`,
|
|
144
|
+
'VX_E_UNKNOWN_COMMAND',
|
|
121
145
|
)
|
|
122
|
-
return 1
|
|
123
146
|
}
|
|
147
|
+
if (resolved !== null && !('loadError' in resolved)) return dispatch(verb, ['--help'])
|
|
124
148
|
}
|
|
125
149
|
await printHelp(await (await plugins()).pluginCommandHelp(), core ? verb : undefined)
|
|
126
150
|
return 0
|
|
@@ -131,14 +155,17 @@ async function dispatch(command: string | undefined, rest: string[]): Promise<nu
|
|
|
131
155
|
// takes no more of is refused, as every other verb refuses one.
|
|
132
156
|
const extra = rest[0]
|
|
133
157
|
if (extra !== undefined) {
|
|
134
|
-
|
|
135
|
-
'
|
|
136
|
-
|
|
158
|
+
return refuse(
|
|
159
|
+
'version',
|
|
160
|
+
rest,
|
|
137
161
|
`${refusedWord(extra)}: ${extra}${flagHint('version', extra)}${seeHelp('version')}`,
|
|
162
|
+
'VX_E_USAGE',
|
|
138
163
|
)
|
|
139
|
-
return 1
|
|
140
164
|
}
|
|
141
|
-
|
|
165
|
+
// Painted, and the same words piped: a script reads `vx <version>`.
|
|
166
|
+
process.stdout.write(
|
|
167
|
+
`${ui.inline([ui.cmd('vx'), ' ', ui.tone('info', VERSION)], detectColors(process.stdout))}\n`,
|
|
168
|
+
)
|
|
142
169
|
return 0
|
|
143
170
|
}
|
|
144
171
|
case 'run':
|
|
@@ -180,7 +207,7 @@ async function dispatch(command: string | undefined, rest: string[]): Promise<nu
|
|
|
180
207
|
// A verb's own refusal is its one line; anything else is the
|
|
181
208
|
// plugin's crash, named as every other stage names one.
|
|
182
209
|
if (isUserError(err)) throw err
|
|
183
|
-
const message =
|
|
210
|
+
const message = thrownMessage(err)
|
|
184
211
|
throw new UserError(
|
|
185
212
|
`plugin '${resolved.plugin.name}' failed in command '${command}': ${message}`,
|
|
186
213
|
)
|
|
@@ -207,22 +234,18 @@ async function dispatch(command: string | undefined, rest: string[]): Promise<nu
|
|
|
207
234
|
text: string,
|
|
208
235
|
code = 'VX_E_UNKNOWN_COMMAND',
|
|
209
236
|
tone: 'error' | 'info' = 'error',
|
|
237
|
+
json = text.split('\n')[0]!,
|
|
210
238
|
): number => {
|
|
211
239
|
sayNotice(tone, 'vx', text.replace(/^vx: /, '').trimEnd())
|
|
212
|
-
|
|
213
|
-
|
|
240
|
+
// `vx --format json` puts the flag where the verb goes.
|
|
241
|
+
if (wantsJson([command, ...rest]))
|
|
242
|
+
process.stdout.write(errorDocument(code, maskedLine(json)))
|
|
214
243
|
return 1
|
|
215
244
|
}
|
|
216
245
|
const loadNote =
|
|
217
246
|
resolved !== null && 'loadError' in resolved
|
|
218
247
|
? `\n (plugin verbs could not be looked up: .vx/workspace failed to load: ${resolved.loadError})`
|
|
219
248
|
: ''
|
|
220
|
-
// Another tool's verb: the pointer, but only after the plugins had
|
|
221
|
-
// their chance — a workspace that declares a plugin verb of that name keeps it.
|
|
222
|
-
const moved = FOREIGN_VERBS[command]
|
|
223
|
-
if (moved !== undefined) {
|
|
224
|
-
return unknown(`${moved}${loadNote}\n`, undefined, 'info')
|
|
225
|
-
}
|
|
226
249
|
// A task typed where the verb goes (`turbo build`, `nx build app`),
|
|
227
250
|
// `turbo dev` included: a repo's own `dev` task beats the no-service note.
|
|
228
251
|
const declaredVerbs =
|
|
@@ -244,12 +267,13 @@ async function dispatch(command: string | undefined, rest: string[]): Promise<nu
|
|
|
244
267
|
// provided by PLUGINS (declared in .vx/workspace.ts), never by core.
|
|
245
268
|
// We keep this neutral hint for the common muscle-memory verbs, but
|
|
246
269
|
// core names no specific plugin package: any package can provide
|
|
247
|
-
// these.
|
|
270
|
+
// these. The notice prints a line per fact, so each line is a
|
|
271
|
+
// whole sentence.
|
|
248
272
|
return unknown(
|
|
249
|
-
`vx:
|
|
250
|
-
` vx core runs tasks in-process
|
|
251
|
-
` distributed execution come from plugins
|
|
252
|
-
` guide: https://vznjs.github.io/vx/
|
|
273
|
+
`vx: \`${command}\` is not a vx core command.\n` +
|
|
274
|
+
` vx core runs tasks in-process.\n` +
|
|
275
|
+
` A dashboard, remote cache and distributed execution come from plugins, not core.\n` +
|
|
276
|
+
` See the plugin guide: https://vznjs.github.io/vx/docs/plugins/\n`,
|
|
253
277
|
)
|
|
254
278
|
}
|
|
255
279
|
// One line, as a verb's own unknown flag or subcommand gets; the full
|
|
@@ -271,9 +295,19 @@ async function dispatch(command: string | undefined, rest: string[]): Promise<nu
|
|
|
271
295
|
'VX_E_USAGE',
|
|
272
296
|
)
|
|
273
297
|
}
|
|
298
|
+
// A workspace that does not load is the refusal to fix first: the
|
|
299
|
+
// verb may be its own plugin's.
|
|
300
|
+
if (resolved !== null && 'loadError' in resolved) {
|
|
301
|
+
return unknown(
|
|
302
|
+
`vx: unknown command: ${command}${guess}${loadNote} (see \`vx help\`)\n`,
|
|
303
|
+
'VX_E_CONFIG',
|
|
304
|
+
'error',
|
|
305
|
+
`vx: unknown command: ${command}; .vx/workspace failed to load: ${resolved.loadError}`,
|
|
306
|
+
)
|
|
307
|
+
}
|
|
274
308
|
return unknown(
|
|
275
|
-
`vx: unknown command: ${command}${guess}
|
|
276
|
-
(guess === ''
|
|
309
|
+
`vx: unknown command: ${command}${guess} (see \`vx help\`)\n` +
|
|
310
|
+
(guess === '' ? verbSourceNote(resolved, declaredVerbs) : ''),
|
|
277
311
|
)
|
|
278
312
|
}
|
|
279
313
|
}
|
|
@@ -310,6 +344,6 @@ function verbSourceNote(resolved: unknown, declaredVerbs: readonly string[]): st
|
|
|
310
344
|
if (declaredVerbs.length > 0) {
|
|
311
345
|
return ` This workspace's plugins declare: ${[...declaredVerbs].sort().join(', ')}.\n`
|
|
312
346
|
}
|
|
313
|
-
const here = resolved === null ? 'there is no workspace here' : '
|
|
347
|
+
const here = resolved === null ? 'there is no workspace here' : 'no plugin here adds one'
|
|
314
348
|
return ` A plugin declared in .vx/workspace.ts can add verbs; ${here}.\n`
|
|
315
349
|
}
|
package/src/cli/info.ts
CHANGED
|
@@ -113,20 +113,24 @@ export function renderInfo(f: InfoFacts, colors: ColorSupport = { enabled: false
|
|
|
113
113
|
// the hits are a share of.
|
|
114
114
|
[
|
|
115
115
|
'task runs (24h)',
|
|
116
|
-
`${f.runs24h} (${f.hits24h} cache
|
|
116
|
+
`${f.runs24h} (${f.hits24h} cache hit${f.hits24h === 1 ? '' : 's'}` +
|
|
117
117
|
(f.hits24h > 0
|
|
118
118
|
? `: ${f.hits24h - f.restored24h} up-to-date, ${f.restored24h} restored)`
|
|
119
119
|
: ')'),
|
|
120
120
|
],
|
|
121
121
|
['flaky tasks', describeFlakyTasks(f.flakyTasks)],
|
|
122
122
|
['sandbox', describeSandbox(f.sandbox)],
|
|
123
|
-
[
|
|
123
|
+
[
|
|
124
|
+
'projects lock',
|
|
125
|
+
typeof f.lockError === 'string' ? `refused: ${f.lockError}` : f.lockfile ? 'yes' : 'no',
|
|
126
|
+
],
|
|
124
127
|
]
|
|
125
128
|
return ui.report('vx info', rows).lines(colors).join('\n')
|
|
126
129
|
}
|
|
127
130
|
|
|
128
|
-
/** `812 (1.2 GB)
|
|
131
|
+
/** `812 (1.2 GB)`, or why the index cannot be read and its fix. */
|
|
129
132
|
function describeEntries(f: InfoFacts): string {
|
|
133
|
+
if (f.cacheIndexError !== null) return `unreadable: ${f.cacheIndexError}`
|
|
130
134
|
return `${f.cacheEntries} (${formatBytes(f.cacheBytes)})`
|
|
131
135
|
}
|
|
132
136
|
|
|
@@ -139,7 +143,7 @@ function describeSettings(settings: InfoFacts['settings']): string {
|
|
|
139
143
|
.filter(([, s]) => s.source !== 'default')
|
|
140
144
|
.map(
|
|
141
145
|
([path, s]) =>
|
|
142
|
-
`${path} ${String(s.value)} (${s.source === 'env' ? envName(path) : '.vx/workspace'})`,
|
|
146
|
+
`${path} ${String(s.value)} (${s.source === 'env' ? envName(path) : '.vx/workspace'}${s.refused === undefined ? '' : `, refused: ${s.refused}`})`,
|
|
143
147
|
)
|
|
144
148
|
.join(', ')
|
|
145
149
|
}
|
|
@@ -169,9 +173,11 @@ function describeSandbox(s: InfoFacts['sandbox']): string {
|
|
|
169
173
|
/** `4 — the CPU count`, `2 — cgroup CPU quota 2 of 8 cores`, `8 — .vx/workspace.ts (4 cores)`. */
|
|
170
174
|
export function describeWorkers(w: InfoFacts['workers']): string {
|
|
171
175
|
const quota = w.cpuQuota === null ? '' : `, cgroup CPU quota ${trimCores(w.cpuQuota)}`
|
|
172
|
-
|
|
176
|
+
const cores = `${w.cores} core${w.cores === 1 ? '' : 's'}`
|
|
177
|
+
if (w.source === 'workspace') return `${w.count} — .vx/workspace.ts (${cores}${quota})`
|
|
178
|
+
if (w.source === 'env') return `${w.count} — ${envName('run.concurrency')} (${cores}${quota})`
|
|
173
179
|
if (w.source === 'cgroup')
|
|
174
|
-
return `${w.count} — cgroup CPU quota ${trimCores(w.cpuQuota ?? w.count)} of ${
|
|
180
|
+
return `${w.count} — cgroup CPU quota ${trimCores(w.cpuQuota ?? w.count)} of ${cores}`
|
|
175
181
|
return `${w.count} — the CPU count${quota}`
|
|
176
182
|
}
|
|
177
183
|
|