@vzn/vx 0.0.16 → 0.0.21
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/LICENSE +1 -1
- package/README.md +135 -124
- package/index.ts +10 -0
- package/launcher.mjs +10 -11
- package/package.json +11 -7
- package/src/bin.ts +14 -4
- package/src/cache/archive.ts +659 -0
- package/src/cache/cache.ts +769 -1101
- package/src/cache/chained-cache.ts +194 -0
- package/src/cache/config-evals.ts +89 -0
- package/src/cache/file-hashes.ts +262 -0
- package/src/cache/git-inputs.ts +637 -0
- package/src/cache/index.ts +24 -5
- package/src/cache/inputs.ts +373 -456
- package/src/cache/layer.ts +604 -0
- package/src/cache/layered-cache.ts +96 -43
- package/src/cache/output-index.ts +224 -0
- package/src/cache/policy.ts +84 -0
- package/src/cache/run-history.ts +147 -0
- package/src/cache/tar-stream.ts +358 -0
- package/src/cache/zstd.ts +160 -0
- package/src/cli/cache.ts +75 -21
- package/src/cli/completions.ts +104 -0
- package/src/cli/core-alias.ts +25 -0
- package/src/cli/help.ts +195 -128
- package/src/cli/index.ts +116 -46
- package/src/cli/info.ts +122 -63
- package/src/cli/init.ts +75 -0
- package/src/cli/last.ts +172 -0
- package/src/cli/lock.ts +2 -1
- package/src/cli/plan-format.ts +72 -6
- package/src/cli/plugin-commands.ts +103 -0
- package/src/cli/run.ts +245 -271
- package/src/cli/select.ts +332 -0
- package/src/cli/show.ts +144 -69
- package/src/cli/upgrade.ts +9 -7
- package/src/cli/watch.ts +858 -110
- package/src/cli/why.ts +223 -0
- package/src/cli/workspace-config.ts +126 -0
- package/src/config.ts +187 -150
- package/src/exec/env.ts +18 -1
- package/src/exec/executor.ts +224 -0
- package/src/exec/index.ts +24 -2
- package/src/exec/local-executor.ts +41 -0
- package/src/exec/runner.ts +166 -40
- package/src/exec/sandbox-binds.ts +182 -0
- package/src/exec/sandbox-paths.ts +50 -0
- package/src/exec/sandbox-runtime.ts +642 -260
- package/src/exec/sandbox-violations.ts +280 -0
- package/src/graph/index.ts +4 -10
- package/src/graph/scheduler.ts +232 -203
- package/src/graph/task-graph.ts +222 -8
- package/src/index.ts +146 -153
- package/src/orchestrator/admission.ts +137 -0
- package/src/orchestrator/deferred-outputs.ts +147 -0
- package/src/orchestrator/doctor.ts +255 -0
- package/src/orchestrator/download-policy.ts +171 -0
- package/src/orchestrator/events.ts +72 -8
- package/src/orchestrator/execute-task.ts +374 -549
- package/src/orchestrator/failure-mode.ts +202 -0
- package/src/orchestrator/framed-output.ts +126 -69
- package/src/orchestrator/history.ts +126 -59
- package/src/orchestrator/hit-restore.ts +221 -0
- package/src/orchestrator/index.ts +77 -93
- package/src/orchestrator/local-shortcircuit.ts +22 -0
- package/src/orchestrator/lockfile-claim.ts +281 -0
- package/src/orchestrator/logger.ts +208 -35
- package/src/orchestrator/metrics.ts +102 -1388
- package/src/orchestrator/miss-save.ts +169 -0
- package/src/orchestrator/options.ts +55 -34
- package/src/orchestrator/persistent.ts +79 -0
- package/src/orchestrator/placement.ts +182 -0
- package/src/orchestrator/plan.ts +144 -1
- package/src/orchestrator/plugin-host.ts +357 -120
- package/src/orchestrator/plugin.ts +298 -58
- package/src/orchestrator/prepare.ts +202 -106
- package/src/orchestrator/projects.ts +259 -0
- package/src/orchestrator/remote-prefetch.ts +12 -8
- package/src/orchestrator/run-artifacts.ts +63 -16
- package/src/orchestrator/run-context.ts +167 -29
- package/src/orchestrator/run-records.ts +149 -0
- package/src/orchestrator/run-report.ts +74 -47
- package/src/orchestrator/run.ts +534 -417
- package/src/orchestrator/sandbox-request.ts +243 -0
- package/src/orchestrator/save-lane.ts +47 -0
- package/src/orchestrator/signals.ts +111 -0
- package/src/orchestrator/stable-keys.ts +129 -31
- package/src/orchestrator/status-line.ts +49 -9
- package/src/orchestrator/summary.ts +90 -26
- package/src/orchestrator/tally.ts +78 -35
- package/src/orchestrator/task-hash.ts +111 -15
- package/src/orchestrator/task-log-buffer.ts +288 -0
- package/src/orchestrator/telemetry-host.ts +34 -6
- package/src/orchestrator/telemetry.ts +182 -36
- package/src/orchestrator/upstream.ts +72 -31
- package/src/util/cgroup.ts +160 -0
- package/src/util/edit-distance.ts +70 -0
- package/src/util/errors.ts +15 -1
- package/src/util/index.ts +15 -2
- package/src/util/num.ts +37 -0
- package/src/util/paths.ts +64 -0
- package/src/util/settle.ts +67 -0
- package/src/util/size.ts +5 -1
- package/src/util/tail.ts +66 -0
- package/src/util/timing.ts +63 -0
- package/src/util/verbs.ts +39 -0
- package/src/workspace/affected.ts +250 -23
- package/src/workspace/config-cache.ts +313 -0
- package/src/workspace/config-eval.ts +217 -0
- package/src/workspace/config-imports.ts +162 -0
- package/src/workspace/config-schema.ts +837 -0
- package/src/workspace/filter.ts +65 -10
- package/src/workspace/fingerprint.ts +53 -4
- package/src/workspace/index.ts +42 -3
- package/src/workspace/lockfile.ts +25 -9
- package/src/workspace/migrate-scripts.ts +134 -0
- package/src/workspace/migration.ts +314 -0
- package/src/workspace/nested-dirs.ts +21 -7
- package/src/workspace/package-graph.ts +114 -41
- package/src/workspace/project-loader.ts +224 -450
- package/src/workspace/workspace.ts +298 -72
- package/src/cache/cas-backend.ts +0 -97
- package/src/cache/digest.ts +0 -47
- package/src/cache/tar.ts +0 -343
- package/src/cli/backend.ts +0 -23
- package/src/cli/mcp-rpc.ts +0 -292
- package/src/cli/mcp.ts +0 -92
- package/src/cli/migrate-nx.ts +0 -450
- package/src/cli/migrate-turbo.ts +0 -364
- package/src/cli/migrate.ts +0 -219
- package/src/orchestrator/devframe-surface.ts +0 -74
- package/src/orchestrator/predict.ts +0 -106
- package/src/orchestrator/protocol.ts +0 -156
- package/src/orchestrator/resources.ts +0 -59
- package/src/orchestrator/run-state.ts +0 -145
- package/src/orchestrator/verify.ts +0 -164
- package/src/orchestrator/wire-render.ts +0 -92
- package/src/orchestrator/wire.ts +0 -212
|
@@ -1,92 +0,0 @@
|
|
|
1
|
-
// The inverse of `wireForwarder`: turn a stream of `WireEvent`s back into
|
|
2
|
-
// `Logger` calls on a concrete renderer. A `vx serve` client feeds the
|
|
3
|
-
// service's event stream through this into a normal `defaultLogger`, so a
|
|
4
|
-
// DELEGATED run renders identically to a local one — the terminal
|
|
5
|
-
// renderer stays completely untouched (it just receives reconstructed
|
|
6
|
-
// node-shaped objects instead of live graph nodes). This file is the
|
|
7
|
-
// whole adapter; it can be rewritten or removed without touching the
|
|
8
|
-
// renderer or the producer side.
|
|
9
|
-
|
|
10
|
-
import type { TaskNode, TaskOutcome } from '../graph/index.js'
|
|
11
|
-
import type { Logger } from './logger.js'
|
|
12
|
-
import type { OutcomeView, TaskView, WireEvent } from './events.js'
|
|
13
|
-
|
|
14
|
-
/**
|
|
15
|
-
* Rebuild a node-shaped object from a `TaskView`. The formatters only ever
|
|
16
|
-
* read `id` / `projectName` / `taskName` / `requested` / `surfaced` and
|
|
17
|
-
* `config.exec` (presence ⇒ not a group; `.command`; `.persistent`), so a
|
|
18
|
-
* structural stand-in is indistinguishable from a real `TaskNode` to them.
|
|
19
|
-
*/
|
|
20
|
-
function reconstructNode(view: TaskView): TaskNode {
|
|
21
|
-
const exec = view.isGroup
|
|
22
|
-
? undefined
|
|
23
|
-
: {
|
|
24
|
-
command: view.command ?? '',
|
|
25
|
-
...(view.persistent ? { persistent: { readyWhen: '' } } : {}),
|
|
26
|
-
}
|
|
27
|
-
return {
|
|
28
|
-
id: view.id,
|
|
29
|
-
projectName: view.project,
|
|
30
|
-
taskName: view.task,
|
|
31
|
-
requested: view.requested,
|
|
32
|
-
surfaced: view.surfaced,
|
|
33
|
-
config: exec === undefined ? {} : { exec },
|
|
34
|
-
} as unknown as TaskNode
|
|
35
|
-
}
|
|
36
|
-
|
|
37
|
-
function reconstructOutcome(view: OutcomeView, node: TaskNode): TaskOutcome {
|
|
38
|
-
return {
|
|
39
|
-
node,
|
|
40
|
-
status: view.status,
|
|
41
|
-
exitCode: view.exitCode,
|
|
42
|
-
durationMs: view.durationMs,
|
|
43
|
-
...(view.restored !== undefined ? { restored: view.restored } : {}),
|
|
44
|
-
...(view.sandboxViolations !== undefined ? { sandboxViolations: view.sandboxViolations } : {}),
|
|
45
|
-
...(view.sandboxViolationLines !== undefined
|
|
46
|
-
? { sandboxViolationLines: view.sandboxViolationLines }
|
|
47
|
-
: {}),
|
|
48
|
-
} as TaskOutcome
|
|
49
|
-
}
|
|
50
|
-
|
|
51
|
-
/**
|
|
52
|
-
* Build a `WireEvent` consumer that drives `sink` (typically a
|
|
53
|
-
* `defaultLogger`). Stateful: it accumulates the reconstructed nodes from
|
|
54
|
-
* `task:start` so later `task:*` events can resolve their node by id.
|
|
55
|
-
*/
|
|
56
|
-
export function createWireRenderer(sink: Logger): (event: WireEvent) => void {
|
|
57
|
-
const nodes = new Map<string, TaskNode>()
|
|
58
|
-
return (event) => {
|
|
59
|
-
switch (event.kind) {
|
|
60
|
-
case 'run:start':
|
|
61
|
-
sink.runStart?.(event.info)
|
|
62
|
-
return
|
|
63
|
-
case 'task:start': {
|
|
64
|
-
const node = reconstructNode(event.task)
|
|
65
|
-
nodes.set(node.id, node)
|
|
66
|
-
sink.taskStart?.(node)
|
|
67
|
-
return
|
|
68
|
-
}
|
|
69
|
-
case 'task:stdout': {
|
|
70
|
-
const node = nodes.get(event.taskId)
|
|
71
|
-
if (node) sink.taskStdout(node, event.chunk)
|
|
72
|
-
return
|
|
73
|
-
}
|
|
74
|
-
case 'task:stderr': {
|
|
75
|
-
const node = nodes.get(event.taskId)
|
|
76
|
-
if (node) sink.taskStderr(node, event.chunk)
|
|
77
|
-
return
|
|
78
|
-
}
|
|
79
|
-
case 'task:complete': {
|
|
80
|
-
const node = nodes.get(event.outcome.taskId)
|
|
81
|
-
if (node) sink.taskComplete(node, reconstructOutcome(event.outcome, node))
|
|
82
|
-
return
|
|
83
|
-
}
|
|
84
|
-
case 'run:status':
|
|
85
|
-
sink.status(event.line)
|
|
86
|
-
return
|
|
87
|
-
case 'run:end':
|
|
88
|
-
sink.runEnd?.()
|
|
89
|
-
return
|
|
90
|
-
}
|
|
91
|
-
}
|
|
92
|
-
}
|
package/src/orchestrator/wire.ts
DELETED
|
@@ -1,212 +0,0 @@
|
|
|
1
|
-
// JSON-RPC 2.0 envelope + OTel-LogRecord payload — the wire spec
|
|
2
|
-
// committed by docs/design/wire-protocol-2026-06.md. Lives alongside
|
|
3
|
-
// the existing protocol.ts (t-discriminated ServerMessage/ClientMessage)
|
|
4
|
-
// so vx serve can speak both formats during the transition.
|
|
5
|
-
//
|
|
6
|
-
// One envelope, three transports (WS / SSE / NDJSON), four channels
|
|
7
|
-
// (vx:events / vx:state / vx:rpc / vx:submit). Every external consumer
|
|
8
|
-
// that already speaks JSON-RPC works against vx out of the box.
|
|
9
|
-
//
|
|
10
|
-
// Pure types + a small adapter pair. No transport here; the Hono
|
|
11
|
-
// router in src/cli/serve.ts wires the byte frames.
|
|
12
|
-
|
|
13
|
-
import type { RunRequest, RunResult, ServerMessage, ClientMessage } from './protocol.js'
|
|
14
|
-
import type { WireEvent as InternalWireEvent } from './events.js'
|
|
15
|
-
|
|
16
|
-
/** Protocol version returned by GET /version. */
|
|
17
|
-
export const WIRE_PROTOCOL_VERSION = '1.0'
|
|
18
|
-
|
|
19
|
-
/** The four channels we expose; clients pick the subset they care about. */
|
|
20
|
-
export const WIRE_CHANNELS = ['vx:events', 'vx:state', 'vx:rpc', 'vx:submit'] as const
|
|
21
|
-
export type WireChannel = (typeof WIRE_CHANNELS)[number]
|
|
22
|
-
|
|
23
|
-
/** JSON-RPC 2.0 method names emitted by the server (notifications). */
|
|
24
|
-
export type ServerNotificationMethod = 'events.append' | 'state.patch'
|
|
25
|
-
|
|
26
|
-
/** JSON-RPC 2.0 method names dispatched from the client (request/response). */
|
|
27
|
-
export type ClientRequestMethod = 'state.snapshot' | 'submit.run' | `rpc.${string}`
|
|
28
|
-
|
|
29
|
-
/**
|
|
30
|
-
* The JSON-RPC 2.0 envelope. One of four shapes per the spec.
|
|
31
|
-
*
|
|
32
|
-
* - request: { jsonrpc, id, method, params? }
|
|
33
|
-
* - response: { jsonrpc, id, result }
|
|
34
|
-
* - error: { jsonrpc, id, error: { code, message, data? } }
|
|
35
|
-
* - notification: { jsonrpc, method, params? }
|
|
36
|
-
*/
|
|
37
|
-
export type Envelope = Request | Response | ErrorResponse | Notification
|
|
38
|
-
|
|
39
|
-
export interface Request {
|
|
40
|
-
jsonrpc: '2.0'
|
|
41
|
-
id: number | string
|
|
42
|
-
method: string
|
|
43
|
-
params?: unknown
|
|
44
|
-
}
|
|
45
|
-
|
|
46
|
-
export interface Response {
|
|
47
|
-
jsonrpc: '2.0'
|
|
48
|
-
id: number | string
|
|
49
|
-
result: unknown
|
|
50
|
-
}
|
|
51
|
-
|
|
52
|
-
export interface ErrorResponse {
|
|
53
|
-
jsonrpc: '2.0'
|
|
54
|
-
id: number | string
|
|
55
|
-
error: { code: number; message: string; data?: unknown }
|
|
56
|
-
}
|
|
57
|
-
|
|
58
|
-
export interface Notification {
|
|
59
|
-
jsonrpc: '2.0'
|
|
60
|
-
method: string
|
|
61
|
-
params?: unknown
|
|
62
|
-
}
|
|
63
|
-
|
|
64
|
-
/** JSON-RPC 2.0 error codes used by vx. The spec reserves -32000..-32099 for impl-defined. */
|
|
65
|
-
export const ENVELOPE_ERRORS = {
|
|
66
|
-
PARSE_ERROR: -32700,
|
|
67
|
-
INVALID_REQUEST: -32600,
|
|
68
|
-
METHOD_NOT_FOUND: -32601,
|
|
69
|
-
INVALID_PARAMS: -32602,
|
|
70
|
-
INTERNAL_ERROR: -32603,
|
|
71
|
-
USER_ERROR: -32000, // a vx UserError — clean message, no stack
|
|
72
|
-
TASK_HASH_UNKNOWN: -32001,
|
|
73
|
-
RUN_NOT_FOUND: -32002,
|
|
74
|
-
UNAUTHORIZED: -32003,
|
|
75
|
-
RATE_LIMITED: -32004,
|
|
76
|
-
} as const
|
|
77
|
-
|
|
78
|
-
/** Build a request envelope. */
|
|
79
|
-
export function makeRequest(id: number | string, method: string, params?: unknown): Request {
|
|
80
|
-
const req: Request = { jsonrpc: '2.0', id, method }
|
|
81
|
-
if (params !== undefined) req.params = params
|
|
82
|
-
return req
|
|
83
|
-
}
|
|
84
|
-
|
|
85
|
-
/** Build a notification envelope. */
|
|
86
|
-
export function makeNotification(method: string, params?: unknown): Notification {
|
|
87
|
-
const note: Notification = { jsonrpc: '2.0', method }
|
|
88
|
-
if (params !== undefined) note.params = params
|
|
89
|
-
return note
|
|
90
|
-
}
|
|
91
|
-
|
|
92
|
-
/** Build a success response envelope. */
|
|
93
|
-
export function makeResponse(id: number | string, result: unknown): Response {
|
|
94
|
-
return { jsonrpc: '2.0', id, result }
|
|
95
|
-
}
|
|
96
|
-
|
|
97
|
-
/** Build an error response envelope. */
|
|
98
|
-
export function makeError(
|
|
99
|
-
id: number | string,
|
|
100
|
-
code: number,
|
|
101
|
-
message: string,
|
|
102
|
-
data?: unknown,
|
|
103
|
-
): ErrorResponse {
|
|
104
|
-
const err: ErrorResponse = { jsonrpc: '2.0', id, error: { code, message } }
|
|
105
|
-
if (data !== undefined) err.error.data = data
|
|
106
|
-
return err
|
|
107
|
-
}
|
|
108
|
-
|
|
109
|
-
/** Type-guard: does a parsed object look like an Envelope? */
|
|
110
|
-
export function isEnvelope(value: unknown): value is Envelope {
|
|
111
|
-
if (value === null || typeof value !== 'object') return false
|
|
112
|
-
const v = value as { jsonrpc?: unknown }
|
|
113
|
-
return v.jsonrpc === '2.0'
|
|
114
|
-
}
|
|
115
|
-
|
|
116
|
-
/** Type-guard for a request envelope. */
|
|
117
|
-
export function isRequest(env: Envelope): env is Request {
|
|
118
|
-
return 'method' in env && 'id' in env
|
|
119
|
-
}
|
|
120
|
-
|
|
121
|
-
/** Type-guard for a notification envelope (request without id). */
|
|
122
|
-
export function isNotification(env: Envelope): env is Notification {
|
|
123
|
-
return 'method' in env && !('id' in env)
|
|
124
|
-
}
|
|
125
|
-
|
|
126
|
-
// ---------------------------------------------------------------------------
|
|
127
|
-
// Bidirectional adapters between the legacy t-discriminated wire and the
|
|
128
|
-
// JSON-RPC envelope. Lets vx serve accept both formats on the same WS
|
|
129
|
-
// endpoint during the transition: parse one, fall back to the other.
|
|
130
|
-
// ---------------------------------------------------------------------------
|
|
131
|
-
|
|
132
|
-
/**
|
|
133
|
-
* Project a ServerMessage to an envelope. Events become `events.append`
|
|
134
|
-
* notifications; results become `submit.run` responses; errors become
|
|
135
|
-
* error responses. The distributed coordinator messages map to the
|
|
136
|
-
* `coord.*` namespace in a distribution plugin's own adapters.
|
|
137
|
-
*/
|
|
138
|
-
export function serverMessageToEnvelope(msg: ServerMessage, id?: number | string): Envelope {
|
|
139
|
-
switch (msg.t) {
|
|
140
|
-
case 'event':
|
|
141
|
-
return makeNotification('events.append', msg.event)
|
|
142
|
-
case 'result':
|
|
143
|
-
// submit.run's response. id required from the caller.
|
|
144
|
-
return makeResponse(id ?? 0, msg.result)
|
|
145
|
-
case 'error':
|
|
146
|
-
return makeError(id ?? 0, ENVELOPE_ERRORS.USER_ERROR, msg.message)
|
|
147
|
-
}
|
|
148
|
-
}
|
|
149
|
-
|
|
150
|
-
/**
|
|
151
|
-
* Project an envelope back to a ServerMessage where one exists. Returns
|
|
152
|
-
* null for envelopes that don't have a mapping (e.g. state.snapshot
|
|
153
|
-
* responses — pure JSON-RPC, no submitter form).
|
|
154
|
-
*/
|
|
155
|
-
export function envelopeToServerMessage(env: Envelope): ServerMessage | null {
|
|
156
|
-
if (isNotification(env) && env.method === 'events.append') {
|
|
157
|
-
return { t: 'event', event: env.params as InternalWireEvent }
|
|
158
|
-
}
|
|
159
|
-
if ('result' in env) {
|
|
160
|
-
return { t: 'result', result: env.result as RunResult }
|
|
161
|
-
}
|
|
162
|
-
if ('error' in env) {
|
|
163
|
-
return { t: 'error', message: env.error.message }
|
|
164
|
-
}
|
|
165
|
-
return null
|
|
166
|
-
}
|
|
167
|
-
|
|
168
|
-
/** Project a ClientMessage to an envelope. `run` becomes a `submit.run` request. */
|
|
169
|
-
export function clientMessageToEnvelope(msg: ClientMessage, id?: number | string): Envelope {
|
|
170
|
-
return makeRequest(id ?? 1, 'submit.run', msg.request)
|
|
171
|
-
}
|
|
172
|
-
|
|
173
|
-
/** Project an envelope back to a ClientMessage. */
|
|
174
|
-
export function envelopeToClientMessage(env: Envelope): ClientMessage | null {
|
|
175
|
-
if (isRequest(env) && env.method === 'submit.run') {
|
|
176
|
-
return { t: 'run', request: env.params as RunRequest }
|
|
177
|
-
}
|
|
178
|
-
return null
|
|
179
|
-
}
|
|
180
|
-
|
|
181
|
-
// ---------------------------------------------------------------------------
|
|
182
|
-
// Transport encoders — single bus, one envelope, three byte framings.
|
|
183
|
-
// Helpers exposed for the Hono mounts in src/cli/serve.ts.
|
|
184
|
-
// ---------------------------------------------------------------------------
|
|
185
|
-
|
|
186
|
-
/** Encode an envelope for a WebSocket frame (one JSON object per frame). */
|
|
187
|
-
export function encodeForWS(env: Envelope): string {
|
|
188
|
-
return JSON.stringify(env)
|
|
189
|
-
}
|
|
190
|
-
|
|
191
|
-
/**
|
|
192
|
-
* Encode an envelope as an SSE event block. Each block is one
|
|
193
|
-
* `data: <json>\n\n` chunk so a `curl` or browser EventSource gets it
|
|
194
|
-
* cleanly.
|
|
195
|
-
*/
|
|
196
|
-
export function encodeForSSE(env: Envelope): string {
|
|
197
|
-
return `data: ${JSON.stringify(env)}\n\n`
|
|
198
|
-
}
|
|
199
|
-
|
|
200
|
-
/** Encode an envelope as a NDJSON line. One envelope per line. */
|
|
201
|
-
export function encodeForNDJSON(env: Envelope): string {
|
|
202
|
-
return JSON.stringify(env) + '\n'
|
|
203
|
-
}
|
|
204
|
-
|
|
205
|
-
/** Parse a UTF-8 string back to an envelope (and validate the shape). */
|
|
206
|
-
export function decodeEnvelope(raw: string): Envelope {
|
|
207
|
-
const value = JSON.parse(raw) as unknown
|
|
208
|
-
if (!isEnvelope(value)) {
|
|
209
|
-
throw new Error(`not a JSON-RPC 2.0 envelope: ${raw.slice(0, 100)}`)
|
|
210
|
-
}
|
|
211
|
-
return value
|
|
212
|
-
}
|