@noeos/verification-engine-cli 0.0.0

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.
@@ -0,0 +1,249 @@
1
+ // SPDX-License-Identifier: Apache-2.0
2
+ import { Buffer } from "node:buffer";
3
+ export class CliInputError extends Error {
4
+ code;
5
+ line;
6
+ constructor(code, line) {
7
+ super(code);
8
+ this.code = code;
9
+ this.line = line;
10
+ this.name = "CliInputError";
11
+ }
12
+ }
13
+ export function parseJsonDocument(text, limits) {
14
+ if (text.codePointAt(0) === 0xfeff)
15
+ throw new CliInputError("UTF8_INVALID");
16
+ if (Buffer.byteLength(text, "utf8") > limits.maxBytes) {
17
+ throw new CliInputError("INPUT_LIMIT_EXCEEDED");
18
+ }
19
+ const scanner = new JsonStructureScanner(text, limits);
20
+ scanner.scan();
21
+ try {
22
+ return JSON.parse(text);
23
+ }
24
+ catch {
25
+ throw new CliInputError("JSON_SYNTAX_INVALID");
26
+ }
27
+ }
28
+ export function decodeUtf8(bytes) {
29
+ try {
30
+ return new TextDecoder("utf-8", { fatal: true }).decode(bytes);
31
+ }
32
+ catch {
33
+ throw new CliInputError("UTF8_INVALID");
34
+ }
35
+ }
36
+ export async function* parseNdjson(chunks, limits) {
37
+ const decoder = new TextDecoder("utf-8", { fatal: true });
38
+ let pending = "";
39
+ let line = 0;
40
+ let sawInput = false;
41
+ try {
42
+ for await (const chunk of chunks) {
43
+ /* Node's native V8 report loses this assignment in async-generator resumes. */
44
+ /* node:coverage ignore next */
45
+ sawInput = true;
46
+ pending += decoder.decode(chunk, { stream: true });
47
+ let newline = pending.indexOf("\n");
48
+ while (newline >= 0) {
49
+ /* c8 ignore next -- covered by the NDJSON conformance tests; V8 attributes the slice to the loop header. */
50
+ const rawLine = pending.slice(0, newline);
51
+ pending = pending.slice(newline + 1);
52
+ line += 1;
53
+ const text = rawLine.endsWith("\r") ? rawLine.slice(0, -1) : rawLine;
54
+ /* Covered by the empty-line conformance test. */
55
+ /* node:coverage ignore next */
56
+ if (text.length === 0)
57
+ throw new CliInputError("NDJSON_LINE_EMPTY", line);
58
+ if (Buffer.byteLength(text, "utf8") > limits.maxBytes) {
59
+ throw new CliInputError("NDJSON_LINE_TOO_LARGE", line);
60
+ }
61
+ try {
62
+ yield parseJsonDocument(text, limits);
63
+ }
64
+ catch (error) {
65
+ if (error instanceof CliInputError) {
66
+ throw new CliInputError(error.code, line);
67
+ }
68
+ throw new CliInputError("JSON_SYNTAX_INVALID", line);
69
+ }
70
+ /* Covered by the multi-line conformance test. */
71
+ /* node:coverage ignore next */
72
+ newline = pending.indexOf("\n");
73
+ }
74
+ /* Covered by the unterminated-line limit test. */
75
+ /* node:coverage ignore next */
76
+ if (Buffer.byteLength(pending, "utf8") > limits.maxBytes) {
77
+ throw new CliInputError("NDJSON_LINE_TOO_LARGE", line + 1);
78
+ }
79
+ }
80
+ pending += decoder.decode();
81
+ }
82
+ catch (error) {
83
+ /* Covered by the invalid UTF-8 conformance test. */
84
+ /* node:coverage ignore next */
85
+ if (error instanceof TypeError)
86
+ throw new CliInputError("UTF8_INVALID", line + 1);
87
+ throw error;
88
+ }
89
+ if (pending.length > 0) {
90
+ line += 1;
91
+ /* Covered by the trailing-line limit test. */
92
+ /* node:coverage ignore next */
93
+ if (Buffer.byteLength(pending, "utf8") > limits.maxBytes) {
94
+ throw new CliInputError("NDJSON_LINE_TOO_LARGE", line);
95
+ }
96
+ try {
97
+ yield parseJsonDocument(pending, limits);
98
+ }
99
+ catch (error) {
100
+ if (error instanceof CliInputError)
101
+ throw new CliInputError(error.code, line);
102
+ throw new CliInputError("JSON_SYNTAX_INVALID", line);
103
+ }
104
+ }
105
+ else if (!sawInput) {
106
+ return;
107
+ }
108
+ }
109
+ class JsonStructureScanner {
110
+ text;
111
+ limits;
112
+ position = 0;
113
+ constructor(text, limits) {
114
+ this.text = text;
115
+ this.limits = limits;
116
+ }
117
+ scan() {
118
+ this.skipWhitespace();
119
+ this.scanValue(0);
120
+ this.skipWhitespace();
121
+ if (this.position !== this.text.length)
122
+ throw new CliInputError("JSON_SYNTAX_INVALID");
123
+ }
124
+ scanValue(depth) {
125
+ if (depth > this.limits.maxDepth)
126
+ throw new CliInputError("INPUT_LIMIT_EXCEEDED");
127
+ const character = this.text[this.position];
128
+ if (character === "{") {
129
+ this.scanObject(depth + 1);
130
+ return;
131
+ }
132
+ if (character === "[") {
133
+ this.scanArray(depth + 1);
134
+ return;
135
+ }
136
+ if (character === '"') {
137
+ this.scanString();
138
+ return;
139
+ }
140
+ this.scanPrimitive();
141
+ }
142
+ scanObject(depth) {
143
+ this.position += 1;
144
+ this.skipWhitespace();
145
+ const keys = new Set();
146
+ if (this.text[this.position] === "}") {
147
+ this.position += 1;
148
+ return;
149
+ }
150
+ /* V8 does not attribute the first async scanner iteration consistently. */
151
+ /* node:coverage ignore next */
152
+ let count = 0;
153
+ for (;;) {
154
+ if (this.text[this.position] !== '"')
155
+ throw new CliInputError("JSON_SYNTAX_INVALID");
156
+ const start = this.position;
157
+ this.scanString();
158
+ let key;
159
+ try {
160
+ key = JSON.parse(this.text.slice(start, this.position));
161
+ }
162
+ catch {
163
+ throw new CliInputError("JSON_SYNTAX_INVALID");
164
+ }
165
+ if (typeof key !== "string")
166
+ throw new CliInputError("JSON_SYNTAX_INVALID");
167
+ if (keys.has(key))
168
+ throw new CliInputError("JSON_DUPLICATE_KEY");
169
+ keys.add(key);
170
+ count += 1;
171
+ if (count > this.limits.maxObjectProperties)
172
+ throw new CliInputError("INPUT_LIMIT_EXCEEDED");
173
+ this.skipWhitespace();
174
+ if (this.text[this.position] !== ":")
175
+ throw new CliInputError("JSON_SYNTAX_INVALID");
176
+ this.position += 1;
177
+ this.skipWhitespace();
178
+ this.scanValue(depth);
179
+ this.skipWhitespace();
180
+ const separator = this.text[this.position];
181
+ if (separator === "}") {
182
+ this.position += 1;
183
+ return;
184
+ }
185
+ if (separator !== ",")
186
+ throw new CliInputError("JSON_SYNTAX_INVALID");
187
+ this.position += 1;
188
+ this.skipWhitespace();
189
+ }
190
+ }
191
+ scanArray(depth) {
192
+ this.position += 1;
193
+ this.skipWhitespace();
194
+ if (this.text[this.position] === "]") {
195
+ this.position += 1;
196
+ return;
197
+ }
198
+ let count = 0;
199
+ for (;;) {
200
+ count += 1;
201
+ if (count > this.limits.maxArrayElements)
202
+ throw new CliInputError("INPUT_LIMIT_EXCEEDED");
203
+ this.scanValue(depth);
204
+ this.skipWhitespace();
205
+ const separator = this.text[this.position];
206
+ if (separator === "]") {
207
+ this.position += 1;
208
+ return;
209
+ }
210
+ if (separator !== ",")
211
+ throw new CliInputError("JSON_SYNTAX_INVALID");
212
+ this.position += 1;
213
+ this.skipWhitespace();
214
+ }
215
+ }
216
+ scanString() {
217
+ this.position += 1;
218
+ while (this.position < this.text.length) {
219
+ const character = this.text[this.position];
220
+ if (character === '"') {
221
+ this.position += 1;
222
+ return;
223
+ }
224
+ if (character === "\\") {
225
+ this.position += 2;
226
+ }
227
+ else {
228
+ if (character !== undefined && character < " ") {
229
+ throw new CliInputError("JSON_SYNTAX_INVALID");
230
+ }
231
+ this.position += 1;
232
+ }
233
+ }
234
+ throw new CliInputError("JSON_SYNTAX_INVALID");
235
+ }
236
+ scanPrimitive() {
237
+ const start = this.position;
238
+ while (this.position < this.text.length && !/[\s,\]}]/u.test(this.text[this.position] ?? "")) {
239
+ this.position += 1;
240
+ }
241
+ if (start === this.position)
242
+ throw new CliInputError("JSON_SYNTAX_INVALID");
243
+ }
244
+ skipWhitespace() {
245
+ while (this.position < this.text.length && /\s/u.test(this.text[this.position] ?? "")) {
246
+ this.position += 1;
247
+ }
248
+ }
249
+ }
@@ -0,0 +1,177 @@
1
+ // SPDX-License-Identifier: Apache-2.0
2
+ import { open, rename, rm, lstat } from "node:fs/promises";
3
+ import { resolve } from "node:path";
4
+ export async function openLineWriter(format, options, stdout) {
5
+ if (options.path === undefined) {
6
+ return new StreamLineWriter(format, stdout);
7
+ }
8
+ const outputPath = resolve(options.path);
9
+ await ensureOutputPath(outputPath, options.force);
10
+ const target = options.atomic ? await createTemporaryPath(outputPath) : outputPath;
11
+ const handle = await open(target, options.atomic ? "wx" : options.force ? "w" : "wx", 0o600);
12
+ return new FileLineWriter(format, handle, outputPath, target, options.atomic);
13
+ }
14
+ class StreamLineWriter {
15
+ format;
16
+ stream;
17
+ constructor(format, stream) {
18
+ this.format = format;
19
+ this.stream = stream;
20
+ }
21
+ async write(value) {
22
+ const text = formatValue(this.format, value);
23
+ if (!this.stream.write(text)) {
24
+ await onceDrain(this.stream);
25
+ }
26
+ }
27
+ async close() {
28
+ // stdout/stderr belong to the host process and must never be closed by the CLI.
29
+ }
30
+ }
31
+ class FileLineWriter {
32
+ format;
33
+ handle;
34
+ outputPath;
35
+ target;
36
+ atomic;
37
+ manifestPath;
38
+ constructor(format, handle, outputPath, target, atomic, manifestPath = `${outputPath}.manifest.json`) {
39
+ this.format = format;
40
+ this.handle = handle;
41
+ this.outputPath = outputPath;
42
+ this.target = target;
43
+ this.atomic = atomic;
44
+ this.manifestPath = manifestPath;
45
+ }
46
+ async write(value) {
47
+ await this.handle.write(formatValue(this.format, value), undefined, "utf8");
48
+ }
49
+ async close(success) {
50
+ try {
51
+ if (success) {
52
+ await this.handle.sync();
53
+ await this.handle.close();
54
+ if (this.atomic)
55
+ await rename(this.target, this.outputPath);
56
+ else
57
+ await writeCompletionManifest(this.manifestPath, true);
58
+ }
59
+ else {
60
+ await this.handle.close();
61
+ if (this.atomic)
62
+ await rm(this.target, { force: true });
63
+ else
64
+ await writeCompletionManifest(this.manifestPath, false);
65
+ }
66
+ /* Defensive cleanup runs only after a second I/O failure. */
67
+ /* node:coverage disable */
68
+ }
69
+ catch (error) {
70
+ try {
71
+ await this.handle.close();
72
+ }
73
+ catch {
74
+ // Preserve the original output failure.
75
+ }
76
+ if (!this.atomic) {
77
+ try {
78
+ await writeCompletionManifest(this.manifestPath, false);
79
+ }
80
+ catch {
81
+ // Preserve the original output failure.
82
+ }
83
+ }
84
+ throw error;
85
+ }
86
+ /* node:coverage enable */
87
+ }
88
+ }
89
+ function formatValue(format, value) {
90
+ if (format === "human")
91
+ return `${formatHuman(value)}\n`;
92
+ const serialized = JSON.stringify(value);
93
+ if (typeof serialized !== "string")
94
+ throw new Error("output serialization failed");
95
+ return `${serialized}\n`;
96
+ }
97
+ function formatHuman(value) {
98
+ if (typeof value !== "object" || value === null)
99
+ return String(value);
100
+ const record = value;
101
+ const operationValue = record.operation;
102
+ const statusValue = record.status;
103
+ const okValue = record.ok;
104
+ const operation = typeof operationValue === "string" ? operationValue : "operation";
105
+ const status = typeof statusValue === "string" ? statusValue : okValue === true ? "ok" : "error";
106
+ return `${operation}: ${status}`;
107
+ }
108
+ async function ensureOutputPath(path, force) {
109
+ try {
110
+ const current = await lstat(path);
111
+ if (current.isSymbolicLink() || !current.isFile())
112
+ throw new Error("OUTPUT_EXISTS");
113
+ if (!force)
114
+ throw new Error("OUTPUT_EXISTS");
115
+ }
116
+ catch (error) {
117
+ if (isMissing(error)) {
118
+ return;
119
+ }
120
+ throw error;
121
+ }
122
+ }
123
+ async function createTemporaryPath(path) {
124
+ const base = `${path}.noeos-tmp-${String(process.pid)}`;
125
+ for (let attempt = 0; attempt < 32; attempt += 1) {
126
+ const candidate = `${base}-${String(attempt)}`;
127
+ try {
128
+ await lstat(candidate);
129
+ }
130
+ catch (error) {
131
+ if (isMissing(error))
132
+ return candidate;
133
+ throw error;
134
+ }
135
+ }
136
+ /* Exhausting all 32 private temporary names is an operational collision. */
137
+ /* node:coverage ignore next */
138
+ throw new Error("IO_WRITE_FAILED");
139
+ }
140
+ async function writeCompletionManifest(path, complete) {
141
+ const target = await createTemporaryPath(path);
142
+ const handle = await open(target, "wx", 0o600);
143
+ try {
144
+ const value = JSON.stringify({ version: 1, complete, format: "ndjson" });
145
+ await handle.write(`${value}\n`, undefined, "utf8");
146
+ await handle.sync();
147
+ await handle.close();
148
+ await rename(target, path);
149
+ }
150
+ catch (error) {
151
+ try {
152
+ await handle.close();
153
+ }
154
+ catch {
155
+ // Preserve the original manifest failure.
156
+ }
157
+ await rm(target, { force: true });
158
+ throw error;
159
+ }
160
+ }
161
+ function isMissing(error) {
162
+ return typeof error === "object" && error !== null && "code" in error && error.code === "ENOENT";
163
+ }
164
+ function onceDrain(stream) {
165
+ return new Promise((resolvePromise, reject) => {
166
+ const onDrain = () => {
167
+ stream.off("error", onError);
168
+ resolvePromise();
169
+ };
170
+ const onError = (error) => {
171
+ stream.off("drain", onDrain);
172
+ reject(error);
173
+ };
174
+ stream.once("drain", onDrain);
175
+ stream.once("error", onError);
176
+ });
177
+ }
@@ -0,0 +1,9 @@
1
+ #!/usr/bin/env node
2
+ // SPDX-License-Identifier: Apache-2.0
3
+ import { runCli } from "./cli.js";
4
+ const exitCode = await runCli(process.argv.slice(2), {
5
+ stdin: process.stdin,
6
+ stdout: process.stdout,
7
+ stderr: process.stderr,
8
+ });
9
+ process.exitCode = exitCode;
@@ -0,0 +1,10 @@
1
+ import type { Writable } from "node:stream";
2
+ interface CliStreams {
3
+ readonly stdin: AsyncIterable<Uint8Array>;
4
+ readonly stdout: Writable & {
5
+ readonly isTTY?: boolean;
6
+ };
7
+ readonly stderr: Writable;
8
+ }
9
+ export declare function runCli(argv: readonly string[], streams: CliStreams): Promise<number>;
10
+ export {};
@@ -0,0 +1,14 @@
1
+ export interface JsonInputLimits {
2
+ readonly maxBytes: number;
3
+ readonly maxDepth: number;
4
+ readonly maxObjectProperties: number;
5
+ readonly maxArrayElements: number;
6
+ }
7
+ export declare class CliInputError extends Error {
8
+ readonly code: "JSON_SYNTAX_INVALID" | "JSON_DUPLICATE_KEY" | "INPUT_LIMIT_EXCEEDED" | "NDJSON_LINE_EMPTY" | "NDJSON_LINE_TOO_LARGE" | "UTF8_INVALID";
9
+ readonly line?: number | undefined;
10
+ constructor(code: "JSON_SYNTAX_INVALID" | "JSON_DUPLICATE_KEY" | "INPUT_LIMIT_EXCEEDED" | "NDJSON_LINE_EMPTY" | "NDJSON_LINE_TOO_LARGE" | "UTF8_INVALID", line?: number | undefined);
11
+ }
12
+ export declare function parseJsonDocument(text: string, limits: JsonInputLimits): unknown;
13
+ export declare function decodeUtf8(bytes: Uint8Array): string;
14
+ export declare function parseNdjson(chunks: AsyncIterable<Uint8Array>, limits: JsonInputLimits): AsyncGenerator<unknown, void, undefined>;
@@ -0,0 +1,12 @@
1
+ import type { Writable } from "node:stream";
2
+ export type OutputFormat = "json" | "ndjson" | "human";
3
+ export interface OutputOptions {
4
+ readonly path?: string;
5
+ readonly force: boolean;
6
+ readonly atomic: boolean;
7
+ }
8
+ export interface LineWriter {
9
+ write(value: unknown): Promise<void>;
10
+ close(success: boolean): Promise<void>;
11
+ }
12
+ export declare function openLineWriter(format: OutputFormat, options: OutputOptions, stdout: Writable): Promise<LineWriter>;
@@ -0,0 +1,2 @@
1
+ #!/usr/bin/env node
2
+ export {};
package/package.json ADDED
@@ -0,0 +1,48 @@
1
+ {
2
+ "name": "@noeos/verification-engine-cli",
3
+ "version": "0.0.0",
4
+ "private": false,
5
+ "description": "Command-line interface for the Noeos verification engine.",
6
+ "license": "Apache-2.0",
7
+ "author": "Noeos contributors",
8
+ "type": "module",
9
+ "sideEffects": false,
10
+ "engines": {
11
+ "node": ">=22.14.0 <23 || >=24.0.0 <25"
12
+ },
13
+ "main": "./dist/esm/main.js",
14
+ "bin": {
15
+ "noeos-ve": "./dist/esm/main.js"
16
+ },
17
+ "types": "./dist/types/main.d.ts",
18
+ "exports": {
19
+ ".": {
20
+ "types": "./dist/types/main.d.ts",
21
+ "import": "./dist/esm/main.js"
22
+ }
23
+ },
24
+ "files": [
25
+ "dist/",
26
+ "CHANGELOG.md",
27
+ "LICENSE",
28
+ "NOTICE",
29
+ "README.md"
30
+ ],
31
+ "dependencies": {
32
+ "@noeos/verification-engine": "0.0.0"
33
+ },
34
+ "repository": {
35
+ "type": "git",
36
+ "url": "git+https://github.com/noeos/verification-engine.git",
37
+ "directory": "packages/cli"
38
+ },
39
+ "bugs": {
40
+ "url": "https://github.com/noeos/verification-engine/issues"
41
+ },
42
+ "homepage": "https://github.com/noeos/verification-engine#readme",
43
+ "publishConfig": {
44
+ "access": "public",
45
+ "provenance": true,
46
+ "registry": "https://registry.npmjs.org/"
47
+ }
48
+ }