@bcts/provenance-mark-cli 1.0.0-alpha.13

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/src/cmd/new.ts ADDED
@@ -0,0 +1,235 @@
1
+ /**
2
+ * New command - 1:1 port of new.rs
3
+ *
4
+ * Initialize a directory with a new provenance mark chain.
5
+ */
6
+
7
+ /* eslint-disable no-console, no-undef */
8
+
9
+ import * as fs from "fs";
10
+ import * as path from "path";
11
+ import {
12
+ ProvenanceMarkGenerator,
13
+ ProvenanceMarkInfo,
14
+ ProvenanceMarkResolution,
15
+ type ProvenanceSeed,
16
+ } from "@bcts/provenance-mark";
17
+
18
+ import type { Exec } from "../exec.js";
19
+ import { readNewPath } from "../utils.js";
20
+ import { type InfoArgs, parseInfoArgs } from "./info.js";
21
+
22
+ /**
23
+ * Output format for the creation summary.
24
+ *
25
+ * Corresponds to Rust `OutputFormat`
26
+ */
27
+ export enum OutputFormat {
28
+ Markdown = "markdown",
29
+ Ur = "ur",
30
+ Json = "json",
31
+ }
32
+
33
+ /**
34
+ * Resolution level for the provenance mark chain.
35
+ *
36
+ * Corresponds to Rust `Resolution`
37
+ */
38
+ export enum Resolution {
39
+ /** Good for physical works of art and applications requiring minimal mark size. */
40
+ Low = "low",
41
+ /** Good for digital works of art. */
42
+ Medium = "medium",
43
+ /** Good for general use. */
44
+ Quartile = "quartile",
45
+ /** Industrial strength, largest mark. */
46
+ High = "high",
47
+ }
48
+
49
+ /**
50
+ * Convert Resolution to ProvenanceMarkResolution.
51
+ */
52
+ function resolutionToProvenanceMarkResolution(res: Resolution): ProvenanceMarkResolution {
53
+ switch (res) {
54
+ case Resolution.Low:
55
+ return ProvenanceMarkResolution.Low;
56
+ case Resolution.Medium:
57
+ return ProvenanceMarkResolution.Medium;
58
+ case Resolution.Quartile:
59
+ return ProvenanceMarkResolution.Quartile;
60
+ case Resolution.High:
61
+ return ProvenanceMarkResolution.High;
62
+ }
63
+ }
64
+
65
+ /**
66
+ * Parse a resolution string.
67
+ */
68
+ export function parseResolution(value: string): Resolution {
69
+ switch (value.toLowerCase()) {
70
+ case "low":
71
+ return Resolution.Low;
72
+ case "medium":
73
+ return Resolution.Medium;
74
+ case "quartile":
75
+ return Resolution.Quartile;
76
+ case "high":
77
+ return Resolution.High;
78
+ default:
79
+ throw new Error(`Invalid resolution: ${value}. Must be one of: low, medium, quartile, high`);
80
+ }
81
+ }
82
+
83
+ /**
84
+ * Parse an output format string.
85
+ */
86
+ export function parseOutputFormat(value: string): OutputFormat {
87
+ switch (value.toLowerCase()) {
88
+ case "markdown":
89
+ return OutputFormat.Markdown;
90
+ case "ur":
91
+ return OutputFormat.Ur;
92
+ case "json":
93
+ return OutputFormat.Json;
94
+ default:
95
+ throw new Error(`Invalid format: ${value}. Must be one of: markdown, ur, json`);
96
+ }
97
+ }
98
+
99
+ /**
100
+ * Arguments for the new command.
101
+ *
102
+ * Corresponds to Rust `CommandArgs`
103
+ */
104
+ export interface NewCommandArgs {
105
+ /** Path to directory to be created. Must not already exist. */
106
+ path: string;
107
+ /** A seed to use for the provenance mark chain, encoded as base64. */
108
+ seed?: ProvenanceSeed;
109
+ /** The resolution of the provenance mark chain. */
110
+ resolution: Resolution;
111
+ /** A comment to be included for the genesis mark. */
112
+ comment: string;
113
+ /** The date of the genesis mark. If not supplied, the current date is used. */
114
+ date?: Date;
115
+ /** Suppress informational status output on stderr/stdout. */
116
+ quiet: boolean;
117
+ /** Output format for the creation summary. */
118
+ format: OutputFormat;
119
+ /** Info args for the mark. */
120
+ info: InfoArgs;
121
+ }
122
+
123
+ /**
124
+ * Create default args for the new command.
125
+ */
126
+ export function defaultNewCommandArgs(): NewCommandArgs {
127
+ return {
128
+ path: "",
129
+ resolution: Resolution.Quartile,
130
+ comment: "Genesis mark.",
131
+ quiet: false,
132
+ format: OutputFormat.Markdown,
133
+ info: {},
134
+ };
135
+ }
136
+
137
+ /**
138
+ * New command implementation.
139
+ *
140
+ * Corresponds to Rust `impl Exec for CommandArgs`
141
+ */
142
+ export class NewCommand implements Exec {
143
+ private readonly args: NewCommandArgs;
144
+
145
+ constructor(args: NewCommandArgs) {
146
+ this.args = args;
147
+ }
148
+
149
+ exec(): string {
150
+ // Create the directory, ensuring it doesn't already exist.
151
+ const dirPath = this.createDir();
152
+
153
+ // Create the `marks` subdirectory inside `path`.
154
+ const marksPath = path.join(dirPath, "marks");
155
+ fs.mkdirSync(marksPath);
156
+
157
+ // Create the generator
158
+ const resolution = resolutionToProvenanceMarkResolution(this.args.resolution);
159
+ let generator: ProvenanceMarkGenerator;
160
+ if (this.args.seed !== undefined) {
161
+ generator = ProvenanceMarkGenerator.newWithSeed(resolution, this.args.seed);
162
+ } else {
163
+ generator = ProvenanceMarkGenerator.newRandom(resolution);
164
+ }
165
+
166
+ // Generate the genesis mark.
167
+ const date = this.args.date ?? new Date();
168
+ const info = parseInfoArgs(this.args.info);
169
+ const mark = generator.next(date, info);
170
+ const markInfo = ProvenanceMarkInfo.new(mark, this.args.comment);
171
+
172
+ // Serialize the mark to JSON and write it as `mark-seq.json` to `path/marks`.
173
+ const markJson = JSON.stringify(markInfo.toJSON(), null, 2);
174
+ const markPath = path.join(marksPath, `mark-${mark.seq()}.json`);
175
+ fs.writeFileSync(markPath, markJson);
176
+
177
+ // Serialize `generator` to JSON and write it as `generator.json` to `path`.
178
+ const generatorJson = JSON.stringify(generator.toJSON(), null, 2);
179
+ const generatorPath = path.join(dirPath, "generator.json");
180
+ fs.writeFileSync(generatorPath, generatorJson);
181
+
182
+ // Return output based on format.
183
+ const statusLines = [
184
+ `Provenance mark chain created at: ${dirPath}`,
185
+ `Mark ${mark.seq()} written to: ${markPath}`,
186
+ ];
187
+
188
+ switch (this.args.format) {
189
+ case OutputFormat.Markdown: {
190
+ const paragraphs: string[] = [];
191
+ if (!this.args.quiet) {
192
+ paragraphs.push(...statusLines);
193
+ }
194
+ paragraphs.push(markInfo.markdownSummary());
195
+ return paragraphs.join("\n\n");
196
+ }
197
+ case OutputFormat.Ur: {
198
+ if (!this.args.quiet) {
199
+ for (const line of statusLines) {
200
+ console.error(line);
201
+ }
202
+ }
203
+ return markInfo.ur().toString();
204
+ }
205
+ case OutputFormat.Json: {
206
+ if (!this.args.quiet) {
207
+ for (const line of statusLines) {
208
+ console.error(line);
209
+ }
210
+ }
211
+ return JSON.stringify(markInfo.toJSON(), null, 2);
212
+ }
213
+ }
214
+ }
215
+
216
+ private createDir(): string {
217
+ const dirPath = readNewPath(this.args.path);
218
+
219
+ // Ensure the directory doesn't already exist.
220
+ if (fs.existsSync(dirPath)) {
221
+ throw new Error(`Path already exists: ${dirPath}`);
222
+ }
223
+
224
+ // Ensure the parent directory exists.
225
+ const parent = path.dirname(dirPath);
226
+ if (!fs.existsSync(parent)) {
227
+ throw new Error(`Parent directory does not exist: ${parent}`);
228
+ }
229
+
230
+ // Create the new directory.
231
+ fs.mkdirSync(dirPath);
232
+
233
+ return dirPath;
234
+ }
235
+ }
@@ -0,0 +1,119 @@
1
+ /**
2
+ * Next command - 1:1 port of next.rs
3
+ *
4
+ * Generate the next provenance mark in a chain.
5
+ */
6
+
7
+ /* eslint-disable no-console, no-undef */
8
+
9
+ import * as fs from "fs";
10
+ import * as path from "path";
11
+ import { ProvenanceMarkGenerator, ProvenanceMarkInfo } from "@bcts/provenance-mark";
12
+
13
+ import type { Exec } from "../exec.js";
14
+ import { readExistingDirectoryPath } from "../utils.js";
15
+ import { type InfoArgs, parseInfoArgs } from "./info.js";
16
+ import { OutputFormat, parseOutputFormat } from "./new.js";
17
+
18
+ /**
19
+ * Arguments for the next command.
20
+ *
21
+ * Corresponds to Rust `CommandArgs`
22
+ */
23
+ export interface NextCommandArgs {
24
+ /** Path to the chain's directory. Must already exist. */
25
+ path: string;
26
+ /** A comment to be included for the mark. */
27
+ comment: string;
28
+ /** The date of the next mark. If not supplied, the current date is used. */
29
+ date?: Date;
30
+ /** Suppress informational status output on stderr/stdout. */
31
+ quiet: boolean;
32
+ /** Output format for the mark. */
33
+ format: OutputFormat;
34
+ /** Info args for the mark. */
35
+ info: InfoArgs;
36
+ }
37
+
38
+ /**
39
+ * Create default args for the next command.
40
+ */
41
+ export function defaultNextCommandArgs(): NextCommandArgs {
42
+ return {
43
+ path: "",
44
+ comment: "Blank.",
45
+ quiet: false,
46
+ format: OutputFormat.Markdown,
47
+ info: {},
48
+ };
49
+ }
50
+
51
+ /**
52
+ * Next command implementation.
53
+ *
54
+ * Corresponds to Rust `impl Exec for CommandArgs`
55
+ */
56
+ export class NextCommand implements Exec {
57
+ private readonly args: NextCommandArgs;
58
+
59
+ constructor(args: NextCommandArgs) {
60
+ this.args = args;
61
+ }
62
+
63
+ exec(): string {
64
+ // Get the chain's directory path.
65
+ const dirPath = readExistingDirectoryPath(this.args.path);
66
+
67
+ // Read the generator from `path/generator.json`.
68
+ const generatorPath = path.join(dirPath, "generator.json");
69
+ const generatorJson = fs.readFileSync(generatorPath, "utf-8");
70
+ const generator = ProvenanceMarkGenerator.fromJSON(
71
+ JSON.parse(generatorJson) as Record<string, unknown>,
72
+ );
73
+
74
+ // Generate the next mark.
75
+ const date = this.args.date ?? new Date();
76
+ const info = parseInfoArgs(this.args.info);
77
+ const mark = generator.next(date, info);
78
+ const markInfo = ProvenanceMarkInfo.new(mark, this.args.comment);
79
+
80
+ // Serialize the mark to JSON and write it as `mark-seq.json` to `path/marks`.
81
+ const marksPath = path.join(dirPath, "marks");
82
+ const markJson = JSON.stringify(markInfo.toJSON(), null, 2);
83
+ const markPath = path.join(marksPath, `mark-${mark.seq()}.json`);
84
+ fs.writeFileSync(markPath, markJson);
85
+
86
+ // Serialize `generator` to JSON and write it back to `path/generator.json`.
87
+ const newGeneratorJson = JSON.stringify(generator.toJSON(), null, 2);
88
+ fs.writeFileSync(generatorPath, newGeneratorJson);
89
+
90
+ // Return output based on format.
91
+ const statusLine = `Mark ${mark.seq()} written to: ${markPath}`;
92
+
93
+ switch (this.args.format) {
94
+ case OutputFormat.Markdown: {
95
+ const paragraphs: string[] = [];
96
+ if (!this.args.quiet) {
97
+ paragraphs.push(statusLine);
98
+ }
99
+ paragraphs.push(markInfo.markdownSummary());
100
+ return paragraphs.join("\n\n");
101
+ }
102
+ case OutputFormat.Ur: {
103
+ if (!this.args.quiet) {
104
+ console.error(statusLine);
105
+ }
106
+ return markInfo.ur().toString();
107
+ }
108
+ case OutputFormat.Json: {
109
+ if (!this.args.quiet) {
110
+ console.error(statusLine);
111
+ }
112
+ return JSON.stringify(markInfo.toJSON(), null, 2);
113
+ }
114
+ }
115
+ }
116
+ }
117
+
118
+ // Re-export for convenience
119
+ export { OutputFormat, parseOutputFormat };
@@ -0,0 +1,108 @@
1
+ /**
2
+ * Print command - 1:1 port of print.rs
3
+ *
4
+ * Prints provenance marks in a chain.
5
+ */
6
+
7
+ import * as fs from "fs";
8
+ import * as path from "path";
9
+ import { ProvenanceMarkGenerator, ProvenanceMarkInfo } from "@bcts/provenance-mark";
10
+
11
+ import type { Exec } from "../exec.js";
12
+ import { readExistingDirectoryPath } from "../utils.js";
13
+ import { OutputFormat, parseOutputFormat } from "./new.js";
14
+
15
+ /**
16
+ * Arguments for the print command.
17
+ *
18
+ * Corresponds to Rust `CommandArgs`
19
+ */
20
+ export interface PrintCommandArgs {
21
+ /** Path to the chain's directory. Must already exist. */
22
+ path: string;
23
+ /** The sequence number of the first mark to print. */
24
+ start: number;
25
+ /** The sequence number of the last mark to print. */
26
+ end?: number;
27
+ /** Output format for the rendered marks. */
28
+ format: OutputFormat;
29
+ }
30
+
31
+ /**
32
+ * Create default args for the print command.
33
+ */
34
+ export function defaultPrintCommandArgs(): PrintCommandArgs {
35
+ return {
36
+ path: "",
37
+ start: 0,
38
+ format: OutputFormat.Markdown,
39
+ };
40
+ }
41
+
42
+ /**
43
+ * Print command implementation.
44
+ *
45
+ * Corresponds to Rust `impl Exec for CommandArgs`
46
+ */
47
+ export class PrintCommand implements Exec {
48
+ private readonly args: PrintCommandArgs;
49
+
50
+ constructor(args: PrintCommandArgs) {
51
+ this.args = args;
52
+ }
53
+
54
+ exec(): string {
55
+ // Get the chain's directory path.
56
+ const dirPath = readExistingDirectoryPath(this.args.path);
57
+
58
+ // Read the generator from `path/generator.json`.
59
+ const generatorPath = path.join(dirPath, "generator.json");
60
+ const generatorJson = fs.readFileSync(generatorPath, "utf-8");
61
+ const generator = ProvenanceMarkGenerator.fromJSON(
62
+ JSON.parse(generatorJson) as Record<string, unknown>,
63
+ );
64
+
65
+ // Validate the start and end sequence numbers.
66
+ const lastValidSeq = generator.nextSeq() - 1;
67
+ const startSeq = this.args.start;
68
+ const endSeq = this.args.end ?? lastValidSeq;
69
+
70
+ if (startSeq > endSeq) {
71
+ throw new Error(
72
+ "The start sequence number must be less than or equal to the end sequence number.",
73
+ );
74
+ }
75
+ if (endSeq > lastValidSeq) {
76
+ throw new Error(
77
+ "The end sequence number must be less than or equal to the last valid sequence number.",
78
+ );
79
+ }
80
+
81
+ // Collect the requested marks.
82
+ const markInfos: ProvenanceMarkInfo[] = [];
83
+ for (let seq = startSeq; seq <= endSeq; seq++) {
84
+ const markPath = path.join(dirPath, "marks", `mark-${seq}.json`);
85
+ const markJson = fs.readFileSync(markPath, "utf-8");
86
+ const markInfo = ProvenanceMarkInfo.fromJSON(JSON.parse(markJson) as Record<string, unknown>);
87
+ markInfos.push(markInfo);
88
+ }
89
+
90
+ switch (this.args.format) {
91
+ case OutputFormat.Markdown: {
92
+ const summaries = markInfos.map((info) => info.markdownSummary());
93
+ return summaries.join("\n");
94
+ }
95
+ case OutputFormat.Ur: {
96
+ const urs = markInfos.map((info) => info.ur().toString());
97
+ return urs.join("\n");
98
+ }
99
+ case OutputFormat.Json: {
100
+ const jsonArray = markInfos.map((info) => info.toJSON());
101
+ return JSON.stringify(jsonArray, null, 2);
102
+ }
103
+ }
104
+ }
105
+ }
106
+
107
+ // Re-export for convenience
108
+ export { OutputFormat, parseOutputFormat };
@@ -0,0 +1,114 @@
1
+ /**
2
+ * Seed parsing - 1:1 port of seed.rs
3
+ *
4
+ * Functions for parsing provenance seeds from various formats.
5
+ */
6
+
7
+ import { UR } from "@bcts/uniform-resources";
8
+ import { Seed } from "@bcts/components";
9
+ import { ProvenanceSeed, PROVENANCE_SEED_LENGTH } from "@bcts/provenance-mark";
10
+ import { fromBase64, hexToBytes } from "../utils.js";
11
+
12
+ /**
13
+ * Parse a seed from a string.
14
+ *
15
+ * Supports the following formats:
16
+ * - `ur:seed/...` - UR-encoded seed
17
+ * - `0x...` or hex string - Hex-encoded seed
18
+ * - Base64 string - Base64-encoded seed
19
+ *
20
+ * Corresponds to Rust `parse_seed()`
21
+ */
22
+ export function parseSeed(input: string): ProvenanceSeed {
23
+ const trimmed = input.trim();
24
+ if (trimmed === "") {
25
+ throw new Error("seed string is empty");
26
+ }
27
+
28
+ // Try UR format first
29
+ if (trimmed.toLowerCase().startsWith("ur:")) {
30
+ return parseSeedUr(trimmed);
31
+ }
32
+
33
+ // Try hex format
34
+ const hexResult = parseSeedHex(trimmed);
35
+ if (hexResult !== undefined) {
36
+ return hexResult;
37
+ }
38
+
39
+ // Fall back to base64
40
+ return parseSeedBase64(trimmed);
41
+ }
42
+
43
+ /**
44
+ * Parse a seed from a UR string.
45
+ *
46
+ * Corresponds to Rust `parse_seed_ur()`
47
+ */
48
+ function parseSeedUr(input: string): ProvenanceSeed {
49
+ try {
50
+ const ur = UR.fromURString(input);
51
+ const seed = Seed.fromUR(ur);
52
+ return seedFromExact(seed.toData());
53
+ } catch (e: unknown) {
54
+ const message = e instanceof Error ? e.message : String(e);
55
+ throw new Error(`failed to parse seed UR: ${message}`);
56
+ }
57
+ }
58
+
59
+ /**
60
+ * Parse a seed from a hex string.
61
+ *
62
+ * Returns undefined if the string is not valid hex format.
63
+ *
64
+ * Corresponds to Rust `parse_seed_hex()`
65
+ */
66
+ function parseSeedHex(input: string): ProvenanceSeed | undefined {
67
+ const source = input.startsWith("0x") ? input.slice(2) : input;
68
+ if (source === "") {
69
+ return undefined;
70
+ }
71
+
72
+ // Check if it's valid hex
73
+ if (source.length % 2 !== 0) {
74
+ return undefined;
75
+ }
76
+ if (!/^[0-9a-fA-F]+$/.test(source)) {
77
+ return undefined;
78
+ }
79
+
80
+ try {
81
+ const bytes = hexToBytes(source);
82
+ return seedFromExact(bytes);
83
+ } catch (e) {
84
+ const message = e instanceof Error ? e.message : String(e);
85
+ throw new Error(`failed to decode hex seed: ${message}`);
86
+ }
87
+ }
88
+
89
+ /**
90
+ * Parse a seed from a base64 string.
91
+ *
92
+ * Corresponds to Rust `parse_seed_base64()`
93
+ */
94
+ function parseSeedBase64(input: string): ProvenanceSeed {
95
+ try {
96
+ const bytes = fromBase64(input);
97
+ return seedFromExact(bytes);
98
+ } catch (e) {
99
+ const message = e instanceof Error ? e.message : String(e);
100
+ throw new Error(`failed to decode base64 seed: ${message}`);
101
+ }
102
+ }
103
+
104
+ /**
105
+ * Create a seed from exactly PROVENANCE_SEED_LENGTH bytes.
106
+ *
107
+ * Corresponds to Rust `seed_from_exact()`
108
+ */
109
+ function seedFromExact(bytes: Uint8Array): ProvenanceSeed {
110
+ if (bytes.length !== PROVENANCE_SEED_LENGTH) {
111
+ throw new Error(`seed must be ${PROVENANCE_SEED_LENGTH} bytes but found ${bytes.length}`);
112
+ }
113
+ return ProvenanceSeed.fromBytes(bytes);
114
+ }