cesr-ts 0.6.0 → 0.8.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.
- package/README.md +17 -6
- package/esm/mod.js +1 -0
- package/esm/src/{annotate/cli.js → cli/commands/annotate.js} +11 -40
- package/esm/src/cli/commands/bench.js +137 -0
- package/esm/src/cli/commands/validate.js +154 -0
- package/esm/src/cli/io-node.js +45 -0
- package/esm/src/cli/main.js +42 -0
- package/esm/src/cli/node.js +14 -0
- package/esm/src/cli/types.js +1 -0
- package/esm/src/core/parser-frame-parser.js +3 -0
- package/esm/src/primitives/mapper.js +48 -41
- package/esm/src/primitives/matter.js +7 -0
- package/esm/src/serder/native.js +62 -29
- package/esm/src/serder/serder.js +35 -46
- package/esm/src/serder/serdery.js +7 -43
- package/esm/src/tables/counter.tables.generated.js +1 -5
- package/esm/src/tables/hard-code-tables.js +39 -0
- package/esm/src/tables/indexer.tables.generated.js +1 -9
- package/esm/src/tables/matter.tables.generated.js +1 -14
- package/esm/src/version.js +7 -4
- package/package.json +3 -9
- package/types/mod.d.ts +1 -0
- package/types/mod.d.ts.map +1 -1
- package/types/src/cli/commands/annotate.d.ts +9 -0
- package/types/src/cli/commands/annotate.d.ts.map +1 -0
- package/types/src/cli/commands/bench.d.ts +4 -0
- package/types/src/cli/commands/bench.d.ts.map +1 -0
- package/types/src/cli/commands/validate.d.ts +4 -0
- package/types/src/cli/commands/validate.d.ts.map +1 -0
- package/types/src/cli/io-node.d.ts +4 -0
- package/types/src/cli/io-node.d.ts.map +1 -0
- package/types/src/cli/main.d.ts +10 -0
- package/types/src/cli/main.d.ts.map +1 -0
- package/types/src/cli/node.d.ts +9 -0
- package/types/src/cli/node.d.ts.map +1 -0
- package/types/src/cli/types.d.ts +22 -0
- package/types/src/cli/types.d.ts.map +1 -0
- package/types/src/core/parser-frame-parser.d.ts.map +1 -1
- package/types/src/primitives/mapper.d.ts.map +1 -1
- package/types/src/primitives/matter.d.ts.map +1 -1
- package/types/src/serder/native.d.ts +9 -0
- package/types/src/serder/native.d.ts.map +1 -1
- package/types/src/serder/serder.d.ts.map +1 -1
- package/types/src/serder/serdery.d.ts.map +1 -1
- package/types/src/tables/counter.tables.generated.d.ts +1 -1
- package/types/src/tables/counter.tables.generated.d.ts.map +1 -1
- package/types/src/tables/hard-code-tables.d.ts +15 -0
- package/types/src/tables/hard-code-tables.d.ts.map +1 -0
- package/types/src/tables/indexer.tables.generated.d.ts +1 -1
- package/types/src/tables/indexer.tables.generated.d.ts.map +1 -1
- package/types/src/tables/matter.tables.generated.d.ts +1 -1
- package/types/src/tables/matter.tables.generated.d.ts.map +1 -1
- package/types/src/version.d.ts +7 -4
- package/types/src/version.d.ts.map +1 -1
- package/esm/src/annotate/cli-node.js +0 -53
- package/types/src/annotate/cli-node.d.ts +0 -2
- package/types/src/annotate/cli-node.d.ts.map +0 -1
- package/types/src/annotate/cli.d.ts +0 -23
- package/types/src/annotate/cli.d.ts.map +0 -1
package/README.md
CHANGED
|
@@ -25,16 +25,27 @@ const annotated = annotate(text, { domainHint: "txt", pretty: true });
|
|
|
25
25
|
## CLI usage
|
|
26
26
|
|
|
27
27
|
```bash
|
|
28
|
-
|
|
28
|
+
npm exec --package cesr-ts -- tephra annotate --in mystream.cesr --pretty
|
|
29
|
+
npm exec --package cesr-ts -- tephra validate --in mystream.cesr
|
|
30
|
+
npm exec --package cesr-ts -- tephra bench --in mystream.cesr --iterations 20 --warmup 3
|
|
29
31
|
```
|
|
30
32
|
|
|
31
|
-
|
|
33
|
+
After global install:
|
|
32
34
|
|
|
33
35
|
```bash
|
|
34
|
-
|
|
36
|
+
tephra annotate --in mystream.cesr --pretty
|
|
37
|
+
tephra validate --in mystream.cesr
|
|
38
|
+
tephra bench --in mystream.cesr --iterations 20 --warmup 3
|
|
35
39
|
```
|
|
36
40
|
|
|
37
|
-
##
|
|
41
|
+
## Deno CLI usage (from repository root)
|
|
42
|
+
|
|
43
|
+
```bash
|
|
44
|
+
deno task tephra:annotate --in mystream.cesr --pretty
|
|
45
|
+
deno task tephra:validate --in mystream.cesr
|
|
46
|
+
```
|
|
47
|
+
|
|
48
|
+
## Benchmarking (from repository root)
|
|
38
49
|
|
|
39
50
|
Run standard parser benchmark baselines:
|
|
40
51
|
|
|
@@ -45,8 +56,8 @@ deno task bench:cesr
|
|
|
45
56
|
Run a benchmark on an arbitrary stream:
|
|
46
57
|
|
|
47
58
|
```bash
|
|
48
|
-
deno task bench
|
|
49
|
-
cat ../../samples/cesr-streams/CESR_1_0-oor-auth-vc.cesr | deno task bench
|
|
59
|
+
deno task tephra:bench --in ../../samples/cesr-streams/CESR_1_0-oor-auth-vc.cesr
|
|
60
|
+
cat ../../samples/cesr-streams/CESR_1_0-oor-auth-vc.cesr | deno task tephra:bench --iterations 20 --warmup 3
|
|
50
61
|
```
|
|
51
62
|
|
|
52
63
|
## Using cesr-ts through tufa
|
package/esm/mod.js
CHANGED
|
@@ -1,5 +1,7 @@
|
|
|
1
|
-
import { annotate } from "
|
|
1
|
+
import { annotate } from "../../annotate/annotator.js";
|
|
2
2
|
const TEXT_DECODER = new TextDecoder();
|
|
3
|
+
const ANNOTATE_USAGE = "Usage: tephra annotate [--in <path>] [--out <path>] [--qb2] [--pretty]";
|
|
4
|
+
/** Parse `tephra annotate` command-line flags without performing IO. */
|
|
3
5
|
function parseArgs(args) {
|
|
4
6
|
const out = { qb2: false, pretty: false };
|
|
5
7
|
for (let i = 0; i < args.length; i++) {
|
|
@@ -28,52 +30,21 @@ function parseArgs(args) {
|
|
|
28
30
|
i++;
|
|
29
31
|
continue;
|
|
30
32
|
}
|
|
31
|
-
if (arg === "--help" || arg === "-h") {
|
|
32
|
-
throw new Error("Usage: cesr-annotate [--in <path>] [--out <path>] [--qb2] [--pretty]");
|
|
33
|
-
}
|
|
34
33
|
throw new Error(`Unknown argument: ${arg}`);
|
|
35
34
|
}
|
|
36
35
|
return out;
|
|
37
36
|
}
|
|
38
37
|
/**
|
|
39
|
-
*
|
|
38
|
+
* Execute `tephra annotate`.
|
|
40
39
|
*
|
|
41
|
-
*
|
|
42
|
-
*
|
|
40
|
+
* The command preserves the old standalone annotation behavior while moving it
|
|
41
|
+
* under the package-level dispatcher.
|
|
43
42
|
*/
|
|
44
|
-
export async function
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
try {
|
|
49
|
-
while (true) {
|
|
50
|
-
const { value, done } = await reader.read();
|
|
51
|
-
if (done)
|
|
52
|
-
break;
|
|
53
|
-
if (value) {
|
|
54
|
-
chunks.push(value);
|
|
55
|
-
total += value.length;
|
|
56
|
-
}
|
|
57
|
-
}
|
|
58
|
-
}
|
|
59
|
-
finally {
|
|
60
|
-
reader.releaseLock();
|
|
61
|
-
}
|
|
62
|
-
const out = new Uint8Array(total);
|
|
63
|
-
let offset = 0;
|
|
64
|
-
for (const chunk of chunks) {
|
|
65
|
-
out.set(chunk, offset);
|
|
66
|
-
offset += chunk.length;
|
|
43
|
+
export async function annotateCommand(args, io) {
|
|
44
|
+
if (args.includes("--help") || args.includes("-h")) {
|
|
45
|
+
await io.writeStdout(`${ANNOTATE_USAGE}\n`);
|
|
46
|
+
return 0;
|
|
67
47
|
}
|
|
68
|
-
return out;
|
|
69
|
-
}
|
|
70
|
-
/**
|
|
71
|
-
* Execute the standalone annotate CLI against the provided IO boundary.
|
|
72
|
-
*
|
|
73
|
-
* Error reporting is intentionally normalized here so runtime-specific entry
|
|
74
|
-
* points can stay as thin bootstraps.
|
|
75
|
-
*/
|
|
76
|
-
export async function annotateCli(args, io) {
|
|
77
48
|
try {
|
|
78
49
|
const options = parseArgs(args);
|
|
79
50
|
const inputBytes = options.inPath
|
|
@@ -95,7 +66,7 @@ export async function annotateCli(args, io) {
|
|
|
95
66
|
}
|
|
96
67
|
catch (error) {
|
|
97
68
|
const message = error instanceof Error ? error.message : String(error);
|
|
98
|
-
await io.writeStderr(`
|
|
69
|
+
await io.writeStderr(`tephra annotate error: ${message}\n`);
|
|
99
70
|
return 1;
|
|
100
71
|
}
|
|
101
72
|
}
|
|
@@ -0,0 +1,137 @@
|
|
|
1
|
+
import { benchmarkCesrParser } from "../../bench/parser-benchmark.js";
|
|
2
|
+
const BENCH_USAGE = "Usage: tephra bench [--in <path>] [--iterations <n>] [--warmup <n>] [--chunk-size <bytes>] [--framed] [--compat] [--allow-errors] [--json]";
|
|
3
|
+
/** Parse non-negative integer flags used for sizing and warmup controls. */
|
|
4
|
+
function parseNonNegativeInt(argName, value) {
|
|
5
|
+
const parsed = Number(value);
|
|
6
|
+
if (!Number.isFinite(parsed) || !Number.isInteger(parsed) || parsed < 0) {
|
|
7
|
+
throw new Error(`${argName} must be a non-negative integer`);
|
|
8
|
+
}
|
|
9
|
+
return parsed;
|
|
10
|
+
}
|
|
11
|
+
/** Parse strictly positive integer flags used for measured iteration count. */
|
|
12
|
+
function parsePositiveInt(argName, value) {
|
|
13
|
+
const parsed = parseNonNegativeInt(argName, value);
|
|
14
|
+
if (parsed <= 0) {
|
|
15
|
+
throw new Error(`${argName} must be greater than 0`);
|
|
16
|
+
}
|
|
17
|
+
return parsed;
|
|
18
|
+
}
|
|
19
|
+
/** Parse `tephra bench` command-line flags without performing IO. */
|
|
20
|
+
function parseArgs(args) {
|
|
21
|
+
const out = {
|
|
22
|
+
iterations: 50,
|
|
23
|
+
warmupIterations: 5,
|
|
24
|
+
chunkSize: 0,
|
|
25
|
+
framed: false,
|
|
26
|
+
attachmentDispatchMode: "strict",
|
|
27
|
+
allowErrors: false,
|
|
28
|
+
json: false,
|
|
29
|
+
};
|
|
30
|
+
for (let i = 0; i < args.length; i++) {
|
|
31
|
+
const arg = args[i];
|
|
32
|
+
if (arg === "--in") {
|
|
33
|
+
const next = args[i + 1];
|
|
34
|
+
if (!next)
|
|
35
|
+
throw new Error("Missing value for --in");
|
|
36
|
+
out.inPath = next;
|
|
37
|
+
i++;
|
|
38
|
+
continue;
|
|
39
|
+
}
|
|
40
|
+
if (arg === "--iterations") {
|
|
41
|
+
const next = args[i + 1];
|
|
42
|
+
if (!next)
|
|
43
|
+
throw new Error("Missing value for --iterations");
|
|
44
|
+
out.iterations = parsePositiveInt("--iterations", next);
|
|
45
|
+
i++;
|
|
46
|
+
continue;
|
|
47
|
+
}
|
|
48
|
+
if (arg === "--warmup") {
|
|
49
|
+
const next = args[i + 1];
|
|
50
|
+
if (!next)
|
|
51
|
+
throw new Error("Missing value for --warmup");
|
|
52
|
+
out.warmupIterations = parseNonNegativeInt("--warmup", next);
|
|
53
|
+
i++;
|
|
54
|
+
continue;
|
|
55
|
+
}
|
|
56
|
+
if (arg === "--chunk-size") {
|
|
57
|
+
const next = args[i + 1];
|
|
58
|
+
if (!next)
|
|
59
|
+
throw new Error("Missing value for --chunk-size");
|
|
60
|
+
out.chunkSize = parseNonNegativeInt("--chunk-size", next);
|
|
61
|
+
i++;
|
|
62
|
+
continue;
|
|
63
|
+
}
|
|
64
|
+
if (arg === "--framed") {
|
|
65
|
+
out.framed = true;
|
|
66
|
+
continue;
|
|
67
|
+
}
|
|
68
|
+
if (arg === "--compat") {
|
|
69
|
+
out.attachmentDispatchMode = "compat";
|
|
70
|
+
continue;
|
|
71
|
+
}
|
|
72
|
+
if (arg === "--allow-errors") {
|
|
73
|
+
out.allowErrors = true;
|
|
74
|
+
continue;
|
|
75
|
+
}
|
|
76
|
+
if (arg === "--json") {
|
|
77
|
+
out.json = true;
|
|
78
|
+
continue;
|
|
79
|
+
}
|
|
80
|
+
throw new Error(`Unknown argument: ${arg}`);
|
|
81
|
+
}
|
|
82
|
+
return out;
|
|
83
|
+
}
|
|
84
|
+
/** Render human-readable benchmark output for terminal workflows. */
|
|
85
|
+
function formatHumanReadable(sourceLabel, result) {
|
|
86
|
+
const chunkLabel = result.chunkSize === result.bytesPerIteration
|
|
87
|
+
? "full stream"
|
|
88
|
+
: `${result.chunkSize} bytes`;
|
|
89
|
+
return [
|
|
90
|
+
"CESR parser benchmark",
|
|
91
|
+
`source: ${sourceLabel}`,
|
|
92
|
+
`iterations: ${result.iterations} (warmup: ${result.warmupIterations})`,
|
|
93
|
+
`input bytes/iteration: ${result.bytesPerIteration}`,
|
|
94
|
+
`chunk size: ${chunkLabel}`,
|
|
95
|
+
`frames/iteration: ${(result.totalFrames / result.iterations).toFixed(2)}`,
|
|
96
|
+
`errors/iteration: ${(result.totalErrors / result.iterations).toFixed(2)}`,
|
|
97
|
+
`avg iteration: ${result.avgIterationMs.toFixed(3)} ms`,
|
|
98
|
+
`throughput: ${result.throughputMiBPerSec.toFixed(3)} MiB/s`,
|
|
99
|
+
`frame rate: ${result.framesPerSec.toFixed(2)} frames/s`,
|
|
100
|
+
].join("\n");
|
|
101
|
+
}
|
|
102
|
+
/** Execute `tephra bench` using the existing parser benchmark engine. */
|
|
103
|
+
export async function benchCommand(args, io) {
|
|
104
|
+
if (args.includes("--help") || args.includes("-h")) {
|
|
105
|
+
await io.writeStdout(`${BENCH_USAGE}\n`);
|
|
106
|
+
return 0;
|
|
107
|
+
}
|
|
108
|
+
try {
|
|
109
|
+
const options = parseArgs(args);
|
|
110
|
+
const sourceLabel = options.inPath ? options.inPath : "stdin";
|
|
111
|
+
const inputBytes = options.inPath
|
|
112
|
+
? await io.readFile(options.inPath)
|
|
113
|
+
: await io.readStdin();
|
|
114
|
+
const result = benchmarkCesrParser(inputBytes, {
|
|
115
|
+
iterations: options.iterations,
|
|
116
|
+
warmupIterations: options.warmupIterations,
|
|
117
|
+
chunkSize: options.chunkSize,
|
|
118
|
+
parserOptions: {
|
|
119
|
+
framed: options.framed,
|
|
120
|
+
attachmentDispatchMode: options.attachmentDispatchMode,
|
|
121
|
+
},
|
|
122
|
+
failOnParseError: !options.allowErrors,
|
|
123
|
+
});
|
|
124
|
+
if (options.json) {
|
|
125
|
+
await io.writeStdout(`${JSON.stringify({ source: sourceLabel, ...result })}\n`);
|
|
126
|
+
return 0;
|
|
127
|
+
}
|
|
128
|
+
const rendered = formatHumanReadable(sourceLabel, result);
|
|
129
|
+
await io.writeStdout(`${rendered}\n`);
|
|
130
|
+
return 0;
|
|
131
|
+
}
|
|
132
|
+
catch (error) {
|
|
133
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
134
|
+
await io.writeStderr(`tephra bench error: ${message}\n`);
|
|
135
|
+
return 1;
|
|
136
|
+
}
|
|
137
|
+
}
|
|
@@ -0,0 +1,154 @@
|
|
|
1
|
+
import { parseBytes } from "../../core/parser-engine.js";
|
|
2
|
+
const VALIDATE_USAGE = "Usage: tephra validate [--in <path>] [--framed] [--compat] [--json]";
|
|
3
|
+
/** Parse `tephra validate` command-line flags without performing IO. */
|
|
4
|
+
function parseArgs(args) {
|
|
5
|
+
const out = {
|
|
6
|
+
framed: false,
|
|
7
|
+
attachmentDispatchMode: "strict",
|
|
8
|
+
json: false,
|
|
9
|
+
};
|
|
10
|
+
for (let i = 0; i < args.length; i++) {
|
|
11
|
+
const arg = args[i];
|
|
12
|
+
if (arg === "--in") {
|
|
13
|
+
const next = args[i + 1];
|
|
14
|
+
if (!next)
|
|
15
|
+
throw new Error("Missing value for --in");
|
|
16
|
+
out.inPath = next;
|
|
17
|
+
i++;
|
|
18
|
+
continue;
|
|
19
|
+
}
|
|
20
|
+
if (arg === "--framed") {
|
|
21
|
+
out.framed = true;
|
|
22
|
+
continue;
|
|
23
|
+
}
|
|
24
|
+
if (arg === "--compat") {
|
|
25
|
+
out.attachmentDispatchMode = "compat";
|
|
26
|
+
continue;
|
|
27
|
+
}
|
|
28
|
+
if (arg === "--json") {
|
|
29
|
+
out.json = true;
|
|
30
|
+
continue;
|
|
31
|
+
}
|
|
32
|
+
throw new Error(`Unknown argument: ${arg}`);
|
|
33
|
+
}
|
|
34
|
+
return out;
|
|
35
|
+
}
|
|
36
|
+
/**
|
|
37
|
+
* Convert parser errors into stable validation report entries.
|
|
38
|
+
*
|
|
39
|
+
* Parser error classes may carry useful location fields, but the CLI should not
|
|
40
|
+
* expose entire internal error objects as JSON. This function keeps the stable
|
|
41
|
+
* public fields and includes offset/context only when the parser supplied them.
|
|
42
|
+
*/
|
|
43
|
+
function describeError(error) {
|
|
44
|
+
const detail = {
|
|
45
|
+
name: error.name || "Error",
|
|
46
|
+
message: error.message,
|
|
47
|
+
};
|
|
48
|
+
const maybeParserError = error;
|
|
49
|
+
if (typeof maybeParserError.offset === "number") {
|
|
50
|
+
detail.offset = maybeParserError.offset;
|
|
51
|
+
}
|
|
52
|
+
if (typeof maybeParserError.context === "string") {
|
|
53
|
+
detail.context = maybeParserError.context;
|
|
54
|
+
}
|
|
55
|
+
return detail;
|
|
56
|
+
}
|
|
57
|
+
/**
|
|
58
|
+
* Count frames, attachment groups, and parser errors for one input stream.
|
|
59
|
+
*
|
|
60
|
+
* `tephra validate` is strict by default because a validation command should fail
|
|
61
|
+
* on ambiguous attachment dispatch. `--compat` intentionally relaxes that mode
|
|
62
|
+
* for streams that still rely on legacy mixed-major attachment behavior.
|
|
63
|
+
*
|
|
64
|
+
* Empty input is reported as a validation failure even though the parser has no
|
|
65
|
+
* malformed bytes to diagnose. A zero-frame stream is not useful evidence that a
|
|
66
|
+
* CESR payload is valid.
|
|
67
|
+
*/
|
|
68
|
+
function validateBytes(bytes, source, options) {
|
|
69
|
+
const events = parseBytes(bytes, {
|
|
70
|
+
framed: options.framed,
|
|
71
|
+
attachmentDispatchMode: options.attachmentDispatchMode,
|
|
72
|
+
});
|
|
73
|
+
const frames = events.filter((event) => event.type === "frame");
|
|
74
|
+
const errors = events
|
|
75
|
+
.filter((event) => event.type === "error")
|
|
76
|
+
.map((event) => describeError(event.error));
|
|
77
|
+
const attachmentGroupCount = frames.reduce((count, event) => count + event.frame.attachments.length, 0);
|
|
78
|
+
if (frames.length === 0 && errors.length === 0) {
|
|
79
|
+
errors.push({
|
|
80
|
+
name: "NoFramesError",
|
|
81
|
+
message: "No CESR frames parsed",
|
|
82
|
+
});
|
|
83
|
+
}
|
|
84
|
+
return {
|
|
85
|
+
ok: errors.length === 0,
|
|
86
|
+
source,
|
|
87
|
+
bytes: bytes.length,
|
|
88
|
+
frameCount: frames.length,
|
|
89
|
+
attachmentGroupCount,
|
|
90
|
+
errorCount: errors.length,
|
|
91
|
+
errors,
|
|
92
|
+
};
|
|
93
|
+
}
|
|
94
|
+
/** Render successful human-readable validation output. */
|
|
95
|
+
function formatSuccess(report) {
|
|
96
|
+
return [
|
|
97
|
+
"CESR validation passed",
|
|
98
|
+
`source: ${report.source}`,
|
|
99
|
+
`bytes: ${report.bytes}`,
|
|
100
|
+
`frames: ${report.frameCount}`,
|
|
101
|
+
`attachment groups: ${report.attachmentGroupCount}`,
|
|
102
|
+
].join("\n");
|
|
103
|
+
}
|
|
104
|
+
/** Render human-readable validation failure output with per-error details. */
|
|
105
|
+
function formatFailure(report) {
|
|
106
|
+
const lines = [
|
|
107
|
+
"CESR validation failed",
|
|
108
|
+
`source: ${report.source}`,
|
|
109
|
+
`bytes: ${report.bytes}`,
|
|
110
|
+
`frames: ${report.frameCount}`,
|
|
111
|
+
`attachment groups: ${report.attachmentGroupCount}`,
|
|
112
|
+
`errors: ${report.errorCount}`,
|
|
113
|
+
];
|
|
114
|
+
for (const error of report.errors) {
|
|
115
|
+
lines.push(`- ${error.name}: ${error.message}`);
|
|
116
|
+
if (error.offset !== undefined) {
|
|
117
|
+
lines.push(` offset: ${error.offset}`);
|
|
118
|
+
}
|
|
119
|
+
if (error.context) {
|
|
120
|
+
lines.push(` context: ${error.context}`);
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
return lines.join("\n");
|
|
124
|
+
}
|
|
125
|
+
/** Execute `tephra validate` against file input or stdin. */
|
|
126
|
+
export async function validateCommand(args, io) {
|
|
127
|
+
if (args.includes("--help") || args.includes("-h")) {
|
|
128
|
+
await io.writeStdout(`${VALIDATE_USAGE}\n`);
|
|
129
|
+
return 0;
|
|
130
|
+
}
|
|
131
|
+
try {
|
|
132
|
+
const options = parseArgs(args);
|
|
133
|
+
const source = options.inPath ? options.inPath : "stdin";
|
|
134
|
+
const inputBytes = options.inPath
|
|
135
|
+
? await io.readFile(options.inPath)
|
|
136
|
+
: await io.readStdin();
|
|
137
|
+
const report = validateBytes(inputBytes, source, options);
|
|
138
|
+
if (options.json) {
|
|
139
|
+
await io.writeStdout(`${JSON.stringify(report)}\n`);
|
|
140
|
+
}
|
|
141
|
+
else if (report.ok) {
|
|
142
|
+
await io.writeStdout(`${formatSuccess(report)}\n`);
|
|
143
|
+
}
|
|
144
|
+
else {
|
|
145
|
+
await io.writeStderr(`${formatFailure(report)}\n`);
|
|
146
|
+
}
|
|
147
|
+
return report.ok ? 0 : 1;
|
|
148
|
+
}
|
|
149
|
+
catch (error) {
|
|
150
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
151
|
+
await io.writeStderr(`tephra validate error: ${message}\n`);
|
|
152
|
+
return 1;
|
|
153
|
+
}
|
|
154
|
+
}
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
import { readFile, writeFile } from "node:fs/promises";
|
|
2
|
+
import { stderr, stdin, stdout } from "node:process";
|
|
3
|
+
/** Read Node stdin into one contiguous byte buffer for command execution. */
|
|
4
|
+
async function readNodeStdin() {
|
|
5
|
+
const chunks = [];
|
|
6
|
+
let total = 0;
|
|
7
|
+
for await (const chunk of stdin) {
|
|
8
|
+
const bytes = chunk instanceof Uint8Array ? chunk : new Uint8Array(chunk);
|
|
9
|
+
chunks.push(bytes);
|
|
10
|
+
total += bytes.length;
|
|
11
|
+
}
|
|
12
|
+
const out = new Uint8Array(total);
|
|
13
|
+
let offset = 0;
|
|
14
|
+
for (const chunk of chunks) {
|
|
15
|
+
out.set(chunk, offset);
|
|
16
|
+
offset += chunk.length;
|
|
17
|
+
}
|
|
18
|
+
return out;
|
|
19
|
+
}
|
|
20
|
+
/** Write text to a Node stream and wait for completion or backpressure error. */
|
|
21
|
+
async function writeNodeStream(stream, text) {
|
|
22
|
+
await new Promise((resolve, reject) => {
|
|
23
|
+
stream.write(text, (error) => {
|
|
24
|
+
if (error)
|
|
25
|
+
reject(error);
|
|
26
|
+
else
|
|
27
|
+
resolve();
|
|
28
|
+
});
|
|
29
|
+
});
|
|
30
|
+
}
|
|
31
|
+
/** Create the Node runtime IO adapter for the package-level `tephra` CLI. */
|
|
32
|
+
export function createNodeCliIo() {
|
|
33
|
+
return {
|
|
34
|
+
readFile: async (path) => {
|
|
35
|
+
const data = await readFile(path);
|
|
36
|
+
return new Uint8Array(data);
|
|
37
|
+
},
|
|
38
|
+
writeTextFile: async (path, text) => {
|
|
39
|
+
await writeFile(path, text, "utf8");
|
|
40
|
+
},
|
|
41
|
+
readStdin: readNodeStdin,
|
|
42
|
+
writeStdout: (text) => writeNodeStream(stdout, text),
|
|
43
|
+
writeStderr: (text) => writeNodeStream(stderr, text),
|
|
44
|
+
};
|
|
45
|
+
}
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
import { annotateCommand } from "./commands/annotate.js";
|
|
2
|
+
import { benchCommand } from "./commands/bench.js";
|
|
3
|
+
import { validateCommand } from "./commands/validate.js";
|
|
4
|
+
const COMMANDS = {
|
|
5
|
+
annotate: annotateCommand,
|
|
6
|
+
validate: validateCommand,
|
|
7
|
+
bench: benchCommand,
|
|
8
|
+
};
|
|
9
|
+
const TEPHRA_USAGE = [
|
|
10
|
+
"Usage: tephra <command> [options]",
|
|
11
|
+
"",
|
|
12
|
+
"Commands:",
|
|
13
|
+
" annotate Annotate a CESR stream",
|
|
14
|
+
" validate Validate a CESR stream",
|
|
15
|
+
" bench Benchmark CESR parser throughput",
|
|
16
|
+
"",
|
|
17
|
+
"Run `tephra <command> --help` for command-specific options.",
|
|
18
|
+
].join("\n");
|
|
19
|
+
/**
|
|
20
|
+
* Execute the package-level `tephra` CLI dispatcher.
|
|
21
|
+
*
|
|
22
|
+
* This function owns only command selection and top-level usage behavior. Each
|
|
23
|
+
* subcommand receives the already-selected argument tail plus a runtime-neutral
|
|
24
|
+
* IO adapter, so command behavior remains identical under Deno and Node.
|
|
25
|
+
*/
|
|
26
|
+
export async function tephraCli(args, io) {
|
|
27
|
+
const [command, ...commandArgs] = args;
|
|
28
|
+
if (command === "--help" || command === "-h") {
|
|
29
|
+
await io.writeStdout(`${TEPHRA_USAGE}\n`);
|
|
30
|
+
return 0;
|
|
31
|
+
}
|
|
32
|
+
if (!command) {
|
|
33
|
+
await io.writeStderr(`${TEPHRA_USAGE}\n`);
|
|
34
|
+
return 1;
|
|
35
|
+
}
|
|
36
|
+
const handler = COMMANDS[command];
|
|
37
|
+
if (!handler) {
|
|
38
|
+
await io.writeStderr(`Unknown tephra command: ${command}\n${TEPHRA_USAGE}\n`);
|
|
39
|
+
return 1;
|
|
40
|
+
}
|
|
41
|
+
return await handler(commandArgs, io);
|
|
42
|
+
}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* npm executable entrypoint for the package-level `tephra` CLI.
|
|
4
|
+
*
|
|
5
|
+
* The CESR npm build discovers this file by the marker text in this comment and
|
|
6
|
+
* prepends a Node shebang after DNT emits JavaScript. Keep this launcher thin so
|
|
7
|
+
* Deno-only APIs do not leak into the Node executable path.
|
|
8
|
+
*/
|
|
9
|
+
import "../../_dnt.polyfills.js";
|
|
10
|
+
import { argv, exit } from "node:process";
|
|
11
|
+
import { createNodeCliIo } from "./io-node.js";
|
|
12
|
+
import { tephraCli } from "./main.js";
|
|
13
|
+
const code = await tephraCli(argv.slice(2), createNodeCliIo());
|
|
14
|
+
exit(code);
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -242,6 +242,9 @@ export class FrameParser {
|
|
|
242
242
|
};
|
|
243
243
|
}
|
|
244
244
|
catch (_error) {
|
|
245
|
+
// Non-native body groups wrap one Matter primitive. If Serder hydration
|
|
246
|
+
// fails, expose the primitive body bytes only; callers that inspect the
|
|
247
|
+
// fallback should not see the surrounding group counter envelope.
|
|
245
248
|
const raw = matter.raw;
|
|
246
249
|
return {
|
|
247
250
|
frame: {
|