From 28ee2519a81c9cca300c85f07d9eb2339ab18a84 Mon Sep 17 00:00:00 2001 From: Ben Bernard Date: Tue, 24 Feb 2026 17:11:25 -0800 Subject: [PATCH 1/3] feat: expand no-private lint to all src/ and tests/, remove # private fields Refactor all JS private class fields (#) to plain properties and remove TypeScript visibility modifiers (private/protected/public) project-wide. Expand check-no-private.ts to scan all of src/ and tests/ instead of just the explorer subdirectory. Co-Authored-By: Claude Opus 4.6 --- scripts/check-no-private.ts | 6 +- src/Accumulator.ts | 8 +-- src/BaseRegistry.ts | 20 +++--- src/DomainLanguage.ts | 12 ++-- src/Executor.ts | 34 ++++----- src/InputStream.ts | 100 +++++++++++++-------------- src/KeyGroups.ts | 40 +++++------ src/KeySpec.ts | 36 +++++----- src/Operation.ts | 48 ++++++------- src/OutputStream.ts | 32 ++++----- src/Record.ts | 38 +++++----- src/RecordStream.ts | 44 ++++++------ src/operations/transform/annotate.ts | 27 ++++++-- src/operations/transform/assert.ts | 25 ++++--- src/operations/transform/chain.ts | 6 +- src/operations/transform/eval.ts | 23 ++++-- src/operations/transform/generate.ts | 25 ++++--- src/operations/transform/grep.ts | 36 ++++++---- src/operations/transform/xform.ts | 31 ++++++--- src/snippets/JsSnippetRunner.ts | 34 ++++----- src/snippets/PerlSnippetRunner.ts | 10 +-- src/snippets/PythonSnippetRunner.ts | 10 +-- tests/perf/bench.ts | 32 ++++----- 23 files changed, 368 insertions(+), 309 deletions(-) diff --git a/scripts/check-no-private.ts b/scripts/check-no-private.ts index 59fa93f..aa7356a 100644 --- a/scripts/check-no-private.ts +++ b/scripts/check-no-private.ts @@ -1,7 +1,7 @@ /** * CI/lint script that ensures no TypeScript visibility modifiers (private, * protected, public) or JavaScript private class fields (#) appear in the - * Explorer codebase. + * codebase. * * Usage: bun scripts/check-no-private.ts * Exit code 0 = all good, non-zero = violations found. @@ -12,8 +12,8 @@ import { join, relative } from "node:path"; const ROOT = join(import.meta.dir, ".."); const SCAN_DIRS = [ - join(ROOT, "src", "explorer"), - join(ROOT, "tests", "explorer"), + join(ROOT, "src"), + join(ROOT, "tests"), ]; const EXTENSIONS = new Set([".ts", ".tsx"]); diff --git a/src/Accumulator.ts b/src/Accumulator.ts index 269fd8a..555c1f8 100644 --- a/src/Accumulator.ts +++ b/src/Accumulator.ts @@ -7,21 +7,21 @@ import type { Record } from "./Record.ts"; * Analogous to App::RecordStream::Accumulator in Perl. */ export class Accumulator { - #records: Record[] = []; + records: Record[] = []; acceptRecord(record: Record): void { this.accumulateRecord(record); } accumulateRecord(record: Record): void { - this.#records.push(record); + this.records.push(record); } getRecords(): Record[] { - return this.#records; + return this.records; } clear(): void { - this.#records = []; + this.records = []; } } diff --git a/src/BaseRegistry.ts b/src/BaseRegistry.ts index 09f3886..4c96edb 100644 --- a/src/BaseRegistry.ts +++ b/src/BaseRegistry.ts @@ -20,7 +20,7 @@ export interface RegistryEntry { } export class BaseRegistry { - #implementations = new Map>(); + implementations = new Map>(); readonly typeName: string; constructor(typeName: string) { @@ -31,10 +31,10 @@ export class BaseRegistry { * Register an implementation under a name. */ register(name: string, entry: RegistryEntry): void { - this.#implementations.set(name, entry); + this.implementations.set(name, entry); if (entry.aliases) { for (const alias of entry.aliases) { - this.#implementations.set(alias, entry); + this.implementations.set(alias, entry); } } } @@ -51,7 +51,7 @@ export class BaseRegistry { const name = parts[0]!; const args = parts.slice(1); - const entry = this.#implementations.get(name); + const entry = this.implementations.get(name); if (!entry) { throw new Error(`Bad ${this.typeName}: ${name}`); } @@ -71,14 +71,14 @@ export class BaseRegistry { * Check if a name is registered. */ has(name: string): boolean { - return this.#implementations.has(name); + return this.implementations.has(name); } /** * Get an entry by name. */ get(name: string): RegistryEntry | undefined { - return this.#implementations.get(name); + return this.implementations.get(name); } /** @@ -89,9 +89,9 @@ export class BaseRegistry { const entryToNames = new Map, string[]>(); const entries: RegistryEntry[] = []; - const sortedNames = [...this.#implementations.keys()].sort(); + const sortedNames = [...this.implementations.keys()].sort(); for (const name of sortedNames) { - const entry = this.#implementations.get(name)!; + const entry = this.implementations.get(name)!; let names = entryToNames.get(entry); if (!names) { names = []; @@ -113,7 +113,7 @@ export class BaseRegistry { * Get the detailed usage for a specific implementation. */ showImplementation(name: string): string { - const entry = this.#implementations.get(name); + const entry = this.implementations.get(name); if (!entry) { return `Bad ${this.typeName}: ${name}\n`; } @@ -124,6 +124,6 @@ export class BaseRegistry { * Get all registered names. */ names(): string[] { - return [...this.#implementations.keys()]; + return [...this.implementations.keys()]; } } diff --git a/src/DomainLanguage.ts b/src/DomainLanguage.ts index 6028b9d..c89a3ca 100644 --- a/src/DomainLanguage.ts +++ b/src/DomainLanguage.ts @@ -22,12 +22,12 @@ export interface Valuation { /** A valuation that extracts a field value via KeySpec */ export class KeySpecValuation implements Valuation { - #field: string; + field: string; constructor(field: string) { - this.#field = field; + this.field = field; } evaluateRecord(record: Record): JsonValue { - const v = findKey(record.dataRef(), this.#field, true); + const v = findKey(record.dataRef(), this.field, true); return v === undefined ? null : v; } } @@ -41,12 +41,12 @@ export class RecordValuation implements Valuation { /** A valuation from a JS function */ export class FunctionValuation implements Valuation { - #fn: (r: Record) => JsonValue; + fn: (r: Record) => JsonValue; constructor(fn: (r: Record) => JsonValue) { - this.#fn = fn; + this.fn = fn; } evaluateRecord(record: Record): JsonValue { - return this.#fn(record); + return this.fn(record); } } diff --git a/src/Executor.ts b/src/Executor.ts index 984ba7b..d9c7f91 100644 --- a/src/Executor.ts +++ b/src/Executor.ts @@ -18,31 +18,31 @@ export interface SnippetDef { } export class Executor { - #snippets: Map; - #lineCounter = 0; - #currentFilename = "NONE"; - #state: Record = {}; + snippets: Map; + lineCounter = 0; + currentFilename = "NONE"; + state: Record = {}; constructor(codeOrSnippets: string | { [name: string]: SnippetDef }) { - this.#snippets = new Map(); + this.snippets = new Map(); if (typeof codeOrSnippets === "string") { - this.#addSnippet("__DEFAULT", { + this.addSnippet("__DEFAULT", { code: codeOrSnippets, argNames: ["r"], }); } else { for (const [name, def] of Object.entries(codeOrSnippets)) { - this.#addSnippet(name, def); + this.addSnippet(name, def); } } } - #addSnippet(name: string, def: SnippetDef): void { + addSnippet(name: string, def: SnippetDef): void { const transformedCode = transformCode(def.code); const argNames = def.argNames ?? ["r"]; - const fn = compileSnippet(transformedCode, argNames, this.#state); - this.#snippets.set(name, { fn, argNames }); + const fn = compileSnippet(transformedCode, argNames, this.state); + this.snippets.set(name, { fn, argNames }); } /** @@ -57,29 +57,29 @@ export class Executor { * Execute a named snippet with the given arguments. */ executeMethod(name: string, ...args: unknown[]): unknown { - const snippet = this.#snippets.get(name); + const snippet = this.snippets.get(name); if (!snippet) { throw new Error(`No such snippet: ${name}`); } - this.#lineCounter++; - return snippet.fn(...args, this.#lineCounter, this.#currentFilename); + this.lineCounter++; + return snippet.fn(...args, this.lineCounter, this.currentFilename); } setCurrentFilename(filename: string): void { - this.#currentFilename = filename; + this.currentFilename = filename; } getCurrentFilename(): string { - return this.#currentFilename; + return this.currentFilename; } getLine(): number { - return this.#lineCounter; + return this.lineCounter; } resetLine(): void { - this.#lineCounter = 0; + this.lineCounter = 0; } } diff --git a/src/InputStream.ts b/src/InputStream.ts index c205901..5c94c82 100644 --- a/src/InputStream.ts +++ b/src/InputStream.ts @@ -7,23 +7,23 @@ import { Record } from "./Record.ts"; * Analogous to App::RecordStream::InputStream in Perl. */ export class InputStream { - #lines: string[] | null = null; - #lineIndex = 0; + lines: string[] | null = null; + lineIndex = 0; // Using inline type to avoid Bun-specific ReadableStreamDefaultReader incompatibility - #byteReader: { read(): Promise<{ done: boolean; value?: Uint8Array }> } | null = null; - #decoder = new TextDecoder(); - #buffer = ""; - #bufferOffset = 0; - #done = false; - #next: InputStream | null; - #filename: string; + byteReader: { read(): Promise<{ done: boolean; value?: Uint8Array }> } | null = null; + decoder = new TextDecoder(); + buffer = ""; + bufferOffset = 0; + done = false; + next: InputStream | null; + filename: string; constructor(options: { next?: InputStream | null; filename?: string; }) { - this.#next = options.next ?? null; - this.#filename = options.filename ?? "UNKNOWN"; + this.next = options.next ?? null; + this.filename = options.filename ?? "UNKNOWN"; } /** @@ -31,7 +31,7 @@ export class InputStream { */ static fromString(str: string, next?: InputStream): InputStream { const stream = new InputStream({ next, filename: "STRING_INPUT" }); - stream.#lines = str.split("\n").filter((l) => l.trim() !== ""); + stream.lines = str.split("\n").filter((l) => l.trim() !== ""); return stream; } @@ -40,7 +40,7 @@ export class InputStream { */ static fromFile(filePath: string, next?: InputStream): InputStream { const stream = new InputStream({ next, filename: filePath }); - stream.#initFile(filePath); + stream.initFile(filePath); return stream; } @@ -52,7 +52,7 @@ export class InputStream { next?: InputStream ): InputStream { const stream = new InputStream({ next, filename: "STREAM_INPUT" }); - stream.#byteReader = readable.getReader(); + stream.byteReader = readable.getReader(); return stream; } @@ -84,9 +84,9 @@ export class InputStream { return lastStream!; } - #initFile(filePath: string): void { + initFile(filePath: string): void { const file = Bun.file(filePath); - this.#byteReader = file.stream().getReader(); + this.byteReader = file.stream().getReader(); } /** @@ -94,29 +94,29 @@ export class InputStream { * Returns null when all streams are exhausted. */ async getRecord(): Promise { - if (this.#done) { - return this.#callNextRecord(); + if (this.done) { + return this.callNextRecord(); } // String-based input - if (this.#lines !== null) { - if (this.#lineIndex < this.#lines.length) { - const line = this.#lines[this.#lineIndex]!; - this.#lineIndex++; + if (this.lines !== null) { + if (this.lineIndex < this.lines.length) { + const line = this.lines[this.lineIndex]!; + this.lineIndex++; return Record.fromJSON(line); } - this.#done = true; - return this.#callNextRecord(); + this.done = true; + return this.callNextRecord(); } // Stream-based input - if (this.#byteReader) { - const line = await this.#readLine(); + if (this.byteReader) { + const line = await this.readLine(); if (line !== null) { return Record.fromJSON(line); } - this.#done = true; - return this.#callNextRecord(); + this.done = true; + return this.callNextRecord(); } return null; @@ -128,44 +128,44 @@ export class InputStream { // is 2x slower due to per-segment TextDecoder overhead; Bun native stdin // adds subprocess cost. Line reading is ~10% of getRecord() time; JSON // parsing dominates. - async #readLine(): Promise { + async readLine(): Promise { while (true) { - const newlineIndex = this.#buffer.indexOf("\n", this.#bufferOffset); + const newlineIndex = this.buffer.indexOf("\n", this.bufferOffset); if (newlineIndex >= 0) { - const line = this.#buffer.slice(this.#bufferOffset, newlineIndex).trim(); - this.#bufferOffset = newlineIndex + 1; + const line = this.buffer.slice(this.bufferOffset, newlineIndex).trim(); + this.bufferOffset = newlineIndex + 1; if (line !== "") return line; continue; } - if (!this.#byteReader) return null; - const { value, done } = await this.#byteReader.read(); + if (!this.byteReader) return null; + const { value, done } = await this.byteReader.read(); if (done) { // Return any remaining content - const remaining = this.#buffer.slice(this.#bufferOffset).trim(); - this.#buffer = ""; - this.#bufferOffset = 0; + const remaining = this.buffer.slice(this.bufferOffset).trim(); + this.buffer = ""; + this.bufferOffset = 0; return remaining !== "" ? remaining : null; } // Compact consumed portion before appending new data - if (this.#bufferOffset > 0) { - this.#buffer = this.#buffer.slice(this.#bufferOffset); - this.#bufferOffset = 0; + if (this.bufferOffset > 0) { + this.buffer = this.buffer.slice(this.bufferOffset); + this.bufferOffset = 0; } - this.#buffer += this.#decoder.decode(value, { stream: true }); + this.buffer += this.decoder.decode(value, { stream: true }); } } - async #callNextRecord(): Promise { - if (!this.#next) return null; + async callNextRecord(): Promise { + if (!this.next) return null; // Flatten chain to prevent deep recursion - if (this.#next.#done) { - this.#next = this.#next.#next; + if (this.next.done) { + this.next = this.next.next; } - if (!this.#next) return null; + if (!this.next) return null; - return this.#next.getRecord(); + return this.next.getRecord(); } /** @@ -191,8 +191,8 @@ export class InputStream { } getFilename(): string { - if (!this.#done) return this.#filename; - if (this.#next) return this.#next.getFilename(); - return this.#filename; + if (!this.done) return this.filename; + if (this.next) return this.next.getFilename(); + return this.filename; } } diff --git a/src/KeyGroups.ts b/src/KeyGroups.ts index 1dfafb1..6dcde13 100644 --- a/src/KeyGroups.ts +++ b/src/KeyGroups.ts @@ -29,8 +29,8 @@ const VALID_OPTIONS: Record = { }; export class KeyGroups { - #groups: GroupMember[] = []; - #cachedSpecs: string[] | null = null; + groups: GroupMember[] = []; + cachedSpecs: string[] | null = null; constructor(...args: string[]) { for (const arg of args) { @@ -39,19 +39,19 @@ export class KeyGroups { } hasAnyGroup(): boolean { - return this.#groups.length > 0; + return this.groups.length > 0; } addGroups(groups: string): void { for (const groupSpec of groups.split(",")) { if (groupSpec.startsWith("!")) { - this.#groups.push(new KeyGroupRegex(groupSpec)); + this.groups.push(new KeyGroupRegex(groupSpec)); } else { - this.#groups.push(new KeyGroupKeySpec(groupSpec)); + this.groups.push(new KeyGroupKeySpec(groupSpec)); } } // Invalidate cache when groups change - this.#cachedSpecs = null; + this.cachedSpecs = null; } /** @@ -59,7 +59,7 @@ export class KeyGroups { */ getKeyspecsForRecord(record: JsonObject): string[] { const specs: string[] = []; - for (const group of this.#groups) { + for (const group of this.groups) { specs.push(...group.getFields(record)); } return specs; @@ -69,10 +69,10 @@ export class KeyGroups { * Get keyspecs, caching after first call. */ getKeyspecs(record: JsonObject): string[] { - if (this.#cachedSpecs === null) { - this.#cachedSpecs = this.getKeyspecsForRecord(record); + if (this.cachedSpecs === null) { + this.cachedSpecs = this.getKeyspecsForRecord(record); } - return this.#cachedSpecs; + return this.cachedSpecs; } } @@ -80,15 +80,15 @@ export class KeyGroups { * A plain key spec group member - resolves a single key spec. */ class KeyGroupKeySpec implements GroupMember { - #keySpec: KeySpec; + keySpec: KeySpec; constructor(spec: string) { - this.#keySpec = new KeySpec(spec); + this.keySpec = new KeySpec(spec); } getFields(record: JsonObject): string[] { - if (this.#keySpec.hasKeySpec(record)) { - const keyList = this.#keySpec.getKeyListForSpec(record); + if (this.keySpec.hasKeySpec(record)) { + const keyList = this.keySpec.getKeyListForSpec(record); if (keyList.length > 0) { return [keyList.join("/")]; } @@ -187,7 +187,7 @@ class KeyGroupRegex implements GroupMember { getFields(record: JsonObject): string[] { const specs: string[] = []; const regex = new RegExp(this.regex); - const allSpecs = this.#getSpecs(record); + const allSpecs = this.getSpecs(record); for (const spec of allSpecs) { if (regex.test(spec)) { @@ -202,7 +202,7 @@ class KeyGroupRegex implements GroupMember { return specs; } - #getSpecs(record: JsonObject): string[] { + getSpecs(record: JsonObject): string[] { let minDepth = 1; let maxDepth = 1; @@ -215,11 +215,11 @@ class KeyGroupRegex implements GroupMember { } const paths: string[][] = []; - this.#getPaths(record, 1, minDepth, maxDepth, [], paths); + this.getPaths(record, 1, minDepth, maxDepth, [], paths); return paths.map((p) => p.join("/")); } - #getPaths( + getPaths( data: JsonValue, currentDepth: number, minDepth: number, @@ -243,7 +243,7 @@ class KeyGroupRegex implements GroupMember { if (Array.isArray(data)) { for (let index = 0; index < data.length; index++) { if (currentDepth <= maxDepth || maxDepth === -1) { - this.#getPaths( + this.getPaths( data[index]!, currentDepth + 1, minDepth, @@ -259,7 +259,7 @@ class KeyGroupRegex implements GroupMember { if (typeof data === "object" && data !== null && !Array.isArray(data)) { for (const key of Object.keys(data as JsonObject)) { if (currentDepth <= maxDepth || maxDepth === -1) { - this.#getPaths( + this.getPaths( (data as JsonObject)[key]!, currentDepth + 1, minDepth, diff --git a/src/KeySpec.ts b/src/KeySpec.ts index 369a57b..12a6e6f 100644 --- a/src/KeySpec.ts +++ b/src/KeySpec.ts @@ -159,8 +159,8 @@ export class KeySpec { readonly spec: string; readonly parsedKeys: string[]; readonly fuzzy: boolean; - private _compiledGetter: CompiledGetter | null = null; - private _compiledSetter: CompiledSetter | null = null; + compiledGetter: CompiledGetter | null = null; + compiledSetter: CompiledSetter | null = null; constructor(spec: string) { // Check cache first @@ -169,8 +169,8 @@ export class KeySpec { this.spec = cached.spec; this.parsedKeys = cached.parsedKeys; this.fuzzy = cached.fuzzy; - this._compiledGetter = cached._compiledGetter; - this._compiledSetter = cached._compiledSetter; + this.compiledGetter = cached.compiledGetter; + this.compiledSetter = cached.compiledSetter; return; } @@ -186,8 +186,8 @@ export class KeySpec { // Eagerly compile for non-fuzzy multi-key specs if (!this.fuzzy && this.parsedKeys.length > 1) { - this._compiledGetter = compileGetter(this.parsedKeys); - this._compiledSetter = compileSetter(this.parsedKeys); + this.compiledGetter = compileGetter(this.parsedKeys); + this.compiledSetter = compileSetter(this.parsedKeys); } specRegistry.set(spec, this); @@ -216,8 +216,8 @@ export class KeySpec { // Compiled getter fast path (read-only traversal, no throwError // to preserve original error types for scalar-in-path cases) - if (this._compiledGetter && noVivify && !throwError) { - const value = this._compiledGetter(data); + if (this.compiledGetter && noVivify && !throwError) { + const value = this.compiledGetter(data); if (value !== undefined) return { value, found: true }; return { value: undefined, found: false }; } @@ -234,11 +234,11 @@ export class KeySpec { ) as { value: JsonValue | undefined; found: boolean }; // Lazy compile for fuzzy specs after first successful resolution - if (this.fuzzy && !this._compiledGetter && result.found) { + if (this.fuzzy && !this.compiledGetter && result.found) { const resolvedKeys = this.getKeyListForSpec(data); if (resolvedKeys.length > 0) { - this._compiledGetter = compileGetter(resolvedKeys); - this._compiledSetter = compileSetter(resolvedKeys); + this.compiledGetter = compileGetter(resolvedKeys); + this.compiledSetter = compileSetter(resolvedKeys); } } @@ -256,8 +256,8 @@ export class KeySpec { return data[key]; } // Compiled getter - if (this._compiledGetter) { - const value = this._compiledGetter(data); + if (this.compiledGetter) { + const value = this.compiledGetter(data); if (value !== undefined) return value; if (throwError) throw new NoSuchKeyError(); return undefined; @@ -276,19 +276,19 @@ export class KeySpec { } // Compiled setter fast path - if (this._compiledSetter) { - this._compiledSetter(data, value); + if (this.compiledSetter) { + this.compiledSetter(data, value); return; } setNestedValue(data, this.parsedKeys, value, this.fuzzy); // Lazy compile for fuzzy specs after first set - if (this.fuzzy && !this._compiledSetter) { + if (this.fuzzy && !this.compiledSetter) { const resolvedKeys = this.getKeyListForSpec(data); if (resolvedKeys.length > 0) { - this._compiledGetter = compileGetter(resolvedKeys); - this._compiledSetter = compileSetter(resolvedKeys); + this.compiledGetter = compileGetter(resolvedKeys); + this.compiledSetter = compileSetter(resolvedKeys); } } } diff --git a/src/Operation.ts b/src/Operation.ts index 38f90fb..78f261e 100644 --- a/src/Operation.ts +++ b/src/Operation.ts @@ -89,19 +89,19 @@ interface HelpType { export abstract class Operation implements RecordReceiver { next: RecordReceiver; - #filenameKey: string | null = null; - #currentFilename = "NONE"; - #wantsHelp = false; - #exitValue = 0; - #helpTypes: Map; + filenameKey: string | null = null; + currentFilename = "NONE"; + wantsHelp = false; + exitValue = 0; + helpTypes: Map; constructor(next?: RecordReceiver) { this.next = next ?? new PrinterReceiver(); - this.#helpTypes = new Map([ + this.helpTypes = new Map([ ["all", { use: false, skipInAll: true, - code: () => this.#allHelp(), + code: () => this.allHelp(), description: "Output all help for this script", }], ["snippet", { @@ -191,8 +191,8 @@ export abstract class Operation implements RecordReceiver { * Emit a record downstream. */ pushRecord(record: Record): boolean { - if (this.#filenameKey) { - record.set(this.#filenameKey, this.#currentFilename); + if (this.filenameKey) { + record.set(this.filenameKey, this.currentFilename); } return this.next.acceptRecord(record); } @@ -224,18 +224,18 @@ export abstract class Operation implements RecordReceiver { * Set the filename key for annotating records with source filename. */ setFilenameKey(key: string): void { - this.#filenameKey = key; + this.filenameKey = key; } /** * Update the current input filename. */ updateCurrentFilename(filename: string): void { - this.#currentFilename = filename; + this.currentFilename = filename; } getCurrentFilename(): string { - return this.#currentFilename; + return this.currentFilename; } /** @@ -247,19 +247,19 @@ export abstract class Operation implements RecordReceiver { } setWantsHelp(val: boolean): void { - this.#wantsHelp = val; + this.wantsHelp = val; } getWantsHelp(): boolean { - return this.#wantsHelp; + return this.wantsHelp; } setExitValue(val: number): void { - this.#exitValue = val; + this.exitValue = val; } getExitValue(): number { - return this.#exitValue; + return this.exitValue; } /** @@ -274,12 +274,12 @@ export abstract class Operation implements RecordReceiver { * Enable a built-in help type (e.g. "snippet", "keyspecs"). */ useHelpType(type: string): void { - const entry = this.#helpTypes.get(type); + const entry = this.helpTypes.get(type); if (entry) { entry.use = true; } // Enabling any help type also enables --help-all - const allEntry = this.#helpTypes.get("all"); + const allEntry = this.helpTypes.get("all"); if (allEntry) { allEntry.use = true; } @@ -295,7 +295,7 @@ export abstract class Operation implements RecordReceiver { skipInAll = false, optionName?: string, ): void { - this.#helpTypes.set(type, { + this.helpTypes.set(type, { use: true, skipInAll, code, @@ -303,7 +303,7 @@ export abstract class Operation implements RecordReceiver { optionName, }); // Also ensure --help-all is available - const allEntry = this.#helpTypes.get("all"); + const allEntry = this.helpTypes.get("all"); if (allEntry) { allEntry.use = true; } @@ -312,9 +312,9 @@ export abstract class Operation implements RecordReceiver { /** * Generate --help-all output: all enabled help types combined. */ - #allHelp(): string { + allHelp(): string { const parts: string[] = []; - for (const [type, info] of this.#helpTypes) { + for (const [type, info] of this.helpTypes) { if (!info.use || info.skipInAll) continue; parts.push(`Help from: --help-${type}:\n`); parts.push(info.code()); @@ -354,7 +354,7 @@ export abstract class Operation implements RecordReceiver { // Build help option handlers const helpHandlers = new Map void>(); - for (const [type, info] of this.#helpTypes) { + for (const [type, info] of this.helpTypes) { if (!info.use) continue; const optName = info.optionName ?? `help-${type}`; helpHandlers.set(`--${optName}`, () => { @@ -371,7 +371,7 @@ export abstract class Operation implements RecordReceiver { // Check help flags first if (arg === "--help" || arg === "-h") { - this.#wantsHelp = true; + this.wantsHelp = true; i++; continue; } diff --git a/src/OutputStream.ts b/src/OutputStream.ts index 0d00bc1..f46fb89 100644 --- a/src/OutputStream.ts +++ b/src/OutputStream.ts @@ -7,14 +7,14 @@ import type { FileSink } from "bun"; * Analogous to App::RecordStream::OutputStream in Perl. */ export class OutputStream { - #writer: WritableStreamDefaultWriter | null = null; - #sink: FileSink | null = null; + writer: WritableStreamDefaultWriter | null = null; + sink: FileSink | null = null; constructor(writable?: WritableStream) { if (writable) { - this.#writer = writable.getWriter(); + this.writer = writable.getWriter(); } else { - this.#sink = Bun.stdout.writer(); + this.sink = Bun.stdout.writer(); } } @@ -23,10 +23,10 @@ export class OutputStream { */ async write(record: Record): Promise { const line = record.toString() + "\n"; - if (this.#sink) { - this.#sink.write(line); - } else if (this.#writer) { - await this.#writer.write(line); + if (this.sink) { + this.sink.write(line); + } else if (this.writer) { + await this.writer.write(line); } } @@ -35,10 +35,10 @@ export class OutputStream { */ async writeLine(line: string): Promise { const output = line.endsWith("\n") ? line : line + "\n"; - if (this.#sink) { - this.#sink.write(output); - } else if (this.#writer) { - await this.#writer.write(output); + if (this.sink) { + this.sink.write(output); + } else if (this.writer) { + await this.writer.write(output); } } @@ -46,10 +46,10 @@ export class OutputStream { * Close the output stream. */ async close(): Promise { - if (this.#sink) { - await this.#sink.flush(); - } else if (this.#writer) { - await this.#writer.close(); + if (this.sink) { + await this.sink.flush(); + } else if (this.writer) { + await this.writer.close(); } } diff --git a/src/Record.ts b/src/Record.ts index e73c6d5..91c6077 100644 --- a/src/Record.ts +++ b/src/Record.ts @@ -8,25 +8,25 @@ import type { JsonValue, JsonObject } from "./types/json.ts"; * Analogous to App::RecordStream::Record in Perl. */ export class Record { - #data: JsonObject; + data: JsonObject; constructor(data?: JsonObject) { - this.#data = data ?? {}; + this.data = data ?? {}; } /** * Get a top-level field value. For nested access, use getKeySpec(). */ get(key: string): JsonValue | undefined { - return this.#data[key]; + return this.data[key]; } /** * Set a top-level field value. Returns the old value. */ set(key: string, value: JsonValue): JsonValue | undefined { - const old = this.#data[key]; - this.#data[key] = value; + const old = this.data[key]; + this.data[key] = value; return old; } @@ -35,8 +35,8 @@ export class Record { */ remove(...keys: string[]): (JsonValue | undefined)[] { return keys.map((key) => { - const old = this.#data[key]; - delete this.#data[key]; // eslint-disable-line @typescript-eslint/no-dynamic-delete + const old = this.data[key]; + delete this.data[key]; // eslint-disable-line @typescript-eslint/no-dynamic-delete return old; }); } @@ -45,14 +45,14 @@ export class Record { * Check if a top-level field exists. */ has(key: string): boolean { - return key in this.#data; + return key in this.data; } /** * Rename a field. If the old field did not exist, creates the new field with null. */ rename(oldKey: string, newKey: string): void { - const value = this.has(oldKey) ? this.#data[oldKey] : null; + const value = this.has(oldKey) ? this.data[oldKey] : null; this.set(newKey, value!); this.remove(oldKey); } @@ -64,7 +64,7 @@ export class Record { const keep = new Set(keys); for (const key of this.keys()) { if (!keep.has(key)) { - delete this.#data[key]; // eslint-disable-line @typescript-eslint/no-dynamic-delete + delete this.data[key]; // eslint-disable-line @typescript-eslint/no-dynamic-delete } } } @@ -73,7 +73,7 @@ export class Record { * Return all top-level field names. */ keys(): string[] { - return Object.keys(this.#data); + return Object.keys(this.data); } /** @@ -81,14 +81,14 @@ export class Record { * Uses a fast JSON-specific clone (no circular-ref handling needed). */ clone(): Record { - return new Record(cloneJsonObject(this.#data)); + return new Record(cloneJsonObject(this.data)); } /** * Return the underlying data as a plain JSON object (shallow copy). */ toJSON(): JsonObject { - return { ...this.#data }; + return { ...this.data }; } /** @@ -96,14 +96,14 @@ export class Record { * Mutations to the returned object will affect the record. */ dataRef(): JsonObject { - return this.#data; + return this.data; } /** * Serialize to a JSON line (no trailing newline). */ toString(): string { - return JSON.stringify(this.#data); + return JSON.stringify(this.data); } /** @@ -211,18 +211,18 @@ export class Record { if (isSimple && !allHack && !reverse) { // Fast path: simple field, ascending, no ALL hack comparator = (a: Record, b: Record): number => { - return cmpFn(a.#data[simpleKey], b.#data[simpleKey]); + return cmpFn(a.data[simpleKey], b.data[simpleKey]); }; } else if (isSimple && !allHack && reverse) { // Fast path: simple field, descending, no ALL hack comparator = (a: Record, b: Record): number => { - return -cmpFn(a.#data[simpleKey], b.#data[simpleKey]); + return -cmpFn(a.data[simpleKey], b.data[simpleKey]); }; } else { // General path: nested keys or ALL hack comparator = (a: Record, b: Record): number => { - const aVal = isSimple ? a.#data[simpleKey] : getNestedValueFromParts(a.#data, parts); - const bVal = isSimple ? b.#data[simpleKey] : getNestedValueFromParts(b.#data, parts); + const aVal = isSimple ? a.data[simpleKey] : getNestedValueFromParts(a.data, parts); + const bVal = isSimple ? b.data[simpleKey] : getNestedValueFromParts(b.data, parts); let val: number | undefined; diff --git a/src/RecordStream.ts b/src/RecordStream.ts index 305359c..9e47407 100644 --- a/src/RecordStream.ts +++ b/src/RecordStream.ts @@ -21,10 +21,10 @@ export interface SnippetOptions { * Internally uses async iterables for lazy evaluation. */ export class RecordStream { - #source: AsyncIterable; + source: AsyncIterable; constructor(source: AsyncIterable) { - this.#source = source; + this.source = source; } // ─── Static Constructors ─────────────────────────────────────── @@ -87,13 +87,13 @@ export class RecordStream { grep(predicate: string | ((r: Record) => boolean), options?: SnippetOptions): RecordStream { if (typeof predicate === "string" && options?.lang && !isJsLang(options.lang)) { const runner = createSnippetRunner(options.lang); - return new RecordStream(runnerGrepAsync(this.#source, predicate, runner)); + return new RecordStream(runnerGrepAsync(this.source, predicate, runner)); } const fn = typeof predicate === "string" ? makeRecordPredicate(predicate) : predicate; - const src = this.#source; + const src = this.source; return new RecordStream(filterAsync(src, fn)); } @@ -104,10 +104,10 @@ export class RecordStream { eval(snippet: string, options?: SnippetOptions): RecordStream { if (options?.lang && !isJsLang(options.lang)) { const runner = createSnippetRunner(options.lang); - return new RecordStream(runnerEvalAsync(this.#source, snippet, runner)); + return new RecordStream(runnerEvalAsync(this.source, snippet, runner)); } const executor = new Executor(`${snippet}; return r;`); - const src = this.#source; + const src = this.source; return new RecordStream( mapAsync(src, (r) => { executor.executeCode(r); @@ -126,13 +126,13 @@ export class RecordStream { ): RecordStream { if (typeof snippetOrFn === "string" && options?.lang && !isJsLang(options.lang)) { const runner = createSnippetRunner(options.lang); - return new RecordStream(runnerXformAsync(this.#source, snippetOrFn, runner)); + return new RecordStream(runnerXformAsync(this.source, snippetOrFn, runner)); } const fn = typeof snippetOrFn === "string" ? makeRecordXform(snippetOrFn) : snippetOrFn; - const src = this.#source; + const src = this.source; return new RecordStream(flatMapAsync(src, fn)); } @@ -141,7 +141,7 @@ export class RecordStream { * This is a buffering operation (must consume all input). */ sort(...keys: string[]): RecordStream { - const src = this.#source; + const src = this.source; return new RecordStream(sortAsync(src, keys)); } @@ -150,7 +150,7 @@ export class RecordStream { * Records must be sorted by the given keys for correct results. */ uniq(...keys: string[]): RecordStream { - const src = this.#source; + const src = this.source; return new RecordStream(uniqAsync(src, keys)); } @@ -158,7 +158,7 @@ export class RecordStream { * Take the first N records. */ head(n: number): RecordStream { - const src = this.#source; + const src = this.source; return new RecordStream(takeAsync(src, n)); } @@ -166,7 +166,7 @@ export class RecordStream { * Skip the first N records and emit the rest. */ tail(n: number): RecordStream { - const src = this.#source; + const src = this.source; return new RecordStream(skipAsync(src, n)); } @@ -174,7 +174,7 @@ export class RecordStream { * Apply aggregators grouped by keys. */ collate(options: CollateOptions): RecordStream { - const src = this.#source; + const src = this.source; return new RecordStream(collateAsync(src, options)); } @@ -182,7 +182,7 @@ export class RecordStream { * Map each record to a new record using a function. */ map(fn: (r: Record) => Record): RecordStream { - const src = this.#source; + const src = this.source; return new RecordStream(mapAsync(src, fn)); } @@ -190,7 +190,7 @@ export class RecordStream { * Reverse the order of records (buffering operation). */ reverse(): RecordStream { - const src = this.#source; + const src = this.source; return new RecordStream(reverseAsync(src)); } @@ -198,7 +198,7 @@ export class RecordStream { * Flatten array fields into separate records. */ decollate(field: string): RecordStream { - const src = this.#source; + const src = this.source; return new RecordStream( flatMapAsync(src, (r) => { const val = findKey(r.dataRef(), field); @@ -216,8 +216,8 @@ export class RecordStream { * Chain another RecordStream after this one. */ concat(other: RecordStream): RecordStream { - const src1 = this.#source; - const src2 = other.#source; + const src1 = this.source; + const src2 = other.source; return new RecordStream(concatAsync(src1, src2)); } @@ -228,7 +228,7 @@ export class RecordStream { */ async toArray(): Promise { const result: Record[] = []; - for await (const record of this.#source) { + for await (const record of this.source) { result.push(record); } return result; @@ -247,7 +247,7 @@ export class RecordStream { */ async toJsonLines(): Promise { const lines: string[] = []; - for await (const record of this.#source) { + for await (const record of this.source) { lines.push(record.toString()); } return lines.join("\n") + "\n"; @@ -274,7 +274,7 @@ export class RecordStream { async pipe(writable: WritableStream): Promise { const writer = writable.getWriter(); try { - for await (const record of this.#source) { + for await (const record of this.source) { await writer.write(record.toString() + "\n"); } } finally { @@ -286,7 +286,7 @@ export class RecordStream { * Get the async iterable source for manual iteration. */ [Symbol.asyncIterator](): AsyncIterator { - return this.#source[Symbol.asyncIterator](); + return this.source[Symbol.asyncIterator](); } } diff --git a/src/operations/transform/annotate.ts b/src/operations/transform/annotate.ts index 6de7338..9bd55de 100644 --- a/src/operations/transform/annotate.ts +++ b/src/operations/transform/annotate.ts @@ -16,12 +16,13 @@ import { createSnippetRunner, isJsLang, langOptionDef } from "../../snippets/ind * Analogous to App::RecordStream::Operation::annotate in Perl. */ export class AnnotateOperation extends Operation { + extraArgs: string[] = []; executor!: Executor; keyGroups = new KeyGroups(); annotations = new Map>(); lang: string | null = null; runner: SnippetRunner | null = null; - #uncachedBatch: { index: number; record: Record; syntheticKey: string }[] = []; + uncachedBatch: { index: number; record: Record; syntheticKey: string }[] = []; override addHelpTypes(): void { this.useHelpType("snippet"); @@ -53,7 +54,19 @@ export class AnnotateOperation extends Operation { ]; const remaining = this.parseOptions(args, defs); - const expression = exprSnippet ?? fileSnippet ?? remaining.join(" "); + + let expression: string; + if (fileSnippet ?? exprSnippet) { + expression = (fileSnippet ?? exprSnippet)!; + this.extraArgs = remaining; + } else { + if (remaining.length === 0) { + expression = ""; + } else { + expression = remaining[0]!; + this.extraArgs = remaining.slice(1); + } + } if (!this.keyGroups.hasAnyGroup()) { throw new Error("Must specify at least one --key, maybe you want xform instead?"); @@ -93,8 +106,8 @@ export class AnnotateOperation extends Operation { if (this.runner) { // Queue this uncached record for batch processing - this.#uncachedBatch.push({ - index: this.#uncachedBatch.length, + this.uncachedBatch.push({ + index: this.uncachedBatch.length, record, syntheticKey, }); @@ -124,13 +137,13 @@ export class AnnotateOperation extends Operation { } override streamDone(): void { - if (this.runner && this.#uncachedBatch.length > 0) { - const records = this.#uncachedBatch.map((entry) => entry.record); + if (this.runner && this.uncachedBatch.length > 0) { + const records = this.uncachedBatch.map((entry) => entry.record); const results = this.runner.executeBatch(records); for (let i = 0; i < results.length; i++) { const result = results[i]!; - const entry = this.#uncachedBatch[i]!; + const entry = this.uncachedBatch[i]!; if (result.error) { process.stderr.write(`annotate: ${result.error}\n`); diff --git a/src/operations/transform/assert.ts b/src/operations/transform/assert.ts index c15fb43..bfbb139 100644 --- a/src/operations/transform/assert.ts +++ b/src/operations/transform/assert.ts @@ -11,13 +11,14 @@ import { createSnippetRunner, isJsLang, langOptionDef } from "../../snippets/ind * Analogous to App::RecordStream::Operation::assert in Perl. */ export class AssertOperation extends Operation { + extraArgs: string[] = []; executor!: Executor; assertion = ""; diagnostic = ""; verbose = false; lang: string | null = null; runner: SnippetRunner | null = null; - #bufferedRecords: Record[] = []; + bufferedRecords: Record[] = []; override addHelpTypes(): void { this.useHelpType("snippet"); @@ -55,9 +56,17 @@ export class AssertOperation extends Operation { ]; const remaining = this.parseOptions(args, defs); - const expression = exprSnippet ?? fileSnippet ?? remaining.join(" "); - if (!expression) { - throw new Error("assert requires an expression argument"); + + let expression: string; + if (fileSnippet ?? exprSnippet) { + expression = (fileSnippet ?? exprSnippet)!; + this.extraArgs = remaining; + } else { + if (remaining.length === 0) { + throw new Error("assert requires an expression argument"); + } + expression = remaining[0]!; + this.extraArgs = remaining.slice(1); } this.assertion = expression; @@ -72,7 +81,7 @@ export class AssertOperation extends Operation { acceptRecord(record: Record): boolean { if (this.runner) { - this.#bufferedRecords.push(record); + this.bufferedRecords.push(record); return true; } @@ -94,11 +103,11 @@ export class AssertOperation extends Operation { } override streamDone(): void { - if (this.runner && this.#bufferedRecords.length > 0) { - const results = this.runner.executeBatch(this.#bufferedRecords); + if (this.runner && this.bufferedRecords.length > 0) { + const results = this.runner.executeBatch(this.bufferedRecords); for (let i = 0; i < results.length; i++) { const result = results[i]!; - const record = this.#bufferedRecords[i]!; + const record = this.bufferedRecords[i]!; if (result.error) { throw new Error(`Assertion failed! ${this.diagnostic}\n` + diff --git a/src/operations/transform/chain.ts b/src/operations/transform/chain.ts index 404c61f..8da559e 100644 --- a/src/operations/transform/chain.ts +++ b/src/operations/transform/chain.ts @@ -232,7 +232,7 @@ export class ChainOperation extends Operation { /** * Whether the first operation overrides acceptLine (line-oriented input op). */ - private firstOpHasCustomAcceptLine(): boolean { + firstOpHasCustomAcceptLine(): boolean { const first = this.operations[0]; if (!first) return false; const proto = Object.getPrototypeOf(first) as { [key: string]: unknown }; @@ -244,7 +244,7 @@ export class ChainOperation extends Operation { * Whether the first operation has a parseContent method and no file args * (bulk-content input ops like fromcsv, fromjsonarray that need stdin). */ - private firstOpNeedsBulkStdin(): boolean { + firstOpNeedsBulkStdin(): boolean { const first = this.operations[0]; if (!first) return false; const opAny = first as unknown as { [key: string]: unknown }; @@ -255,7 +255,7 @@ export class ChainOperation extends Operation { } /** Buffer for bulk stdin content when first op needs parseContent */ - private bulkStdinLines: string[] = []; + bulkStdinLines: string[] = []; acceptRecord(record: Record): boolean { if (this.operations.length > 0) { diff --git a/src/operations/transform/eval.ts b/src/operations/transform/eval.ts index 139b2d6..5e15aaf 100644 --- a/src/operations/transform/eval.ts +++ b/src/operations/transform/eval.ts @@ -19,7 +19,8 @@ export class EvalOperation extends Operation { chomp = false; lang: string | null = null; runner: SnippetRunner | null = null; - #bufferedRecords: Record[] = []; + bufferedRecords: Record[] = []; + extraArgs: string[] = []; override addHelpTypes(): void { this.useHelpType("snippet"); @@ -49,9 +50,17 @@ export class EvalOperation extends Operation { ]; const remaining = this.parseOptions(args, defs); - const expression = fileSnippet ?? exprSnippet ?? remaining.join(" "); - if (!expression) { - throw new Error("eval requires an expression argument"); + + let expression: string; + if (fileSnippet ?? exprSnippet) { + expression = (fileSnippet ?? exprSnippet)!; + this.extraArgs = remaining; + } else { + if (remaining.length === 0) { + throw new Error("eval requires an expression argument"); + } + expression = remaining[0]!; + this.extraArgs = remaining.slice(1); } if (this.lang && !isJsLang(this.lang)) { @@ -64,7 +73,7 @@ export class EvalOperation extends Operation { acceptRecord(record: Record): boolean { if (this.runner) { - this.#bufferedRecords.push(record); + this.bufferedRecords.push(record); return true; } @@ -80,8 +89,8 @@ export class EvalOperation extends Operation { } override streamDone(): void { - if (this.runner && this.#bufferedRecords.length > 0) { - const results = this.runner.executeBatch(this.#bufferedRecords); + if (this.runner && this.bufferedRecords.length > 0) { + const results = this.runner.executeBatch(this.bufferedRecords); for (const result of results) { if (result.error) { process.stderr.write(`eval: ${result.error}\n`); diff --git a/src/operations/transform/generate.ts b/src/operations/transform/generate.ts index 29c1e9e..228c85b 100644 --- a/src/operations/transform/generate.ts +++ b/src/operations/transform/generate.ts @@ -18,12 +18,13 @@ import { createSnippetRunner, isJsLang, langOptionDef } from "../../snippets/ind * Analogous to App::RecordStream::Operation::generate in Perl. */ export class GenerateOperation extends Operation { + extraArgs: string[] = []; executor!: Executor; keychain = "_chain"; passthrough = false; lang: string | null = null; runner: SnippetRunner | null = null; - #bufferedRecords: Record[] = []; + bufferedRecords: Record[] = []; override addHelpTypes(): void { this.useHelpType("snippet"); @@ -59,9 +60,17 @@ export class GenerateOperation extends Operation { ]; const remaining = this.parseOptions(args, defs); - const expression = fileSnippet ?? exprSnippet ?? remaining.join(" "); - if (!expression) { - throw new Error("generate requires an expression argument"); + + let expression: string; + if (fileSnippet ?? exprSnippet) { + expression = (fileSnippet ?? exprSnippet)!; + this.extraArgs = remaining; + } else { + if (remaining.length === 0) { + throw new Error("generate requires an expression argument"); + } + expression = remaining[0]!; + this.extraArgs = remaining.slice(1); } if (this.lang && !isJsLang(this.lang)) { @@ -74,7 +83,7 @@ export class GenerateOperation extends Operation { acceptRecord(record: Record): boolean { if (this.runner) { - this.#bufferedRecords.push(record); + this.bufferedRecords.push(record); return true; } @@ -110,11 +119,11 @@ export class GenerateOperation extends Operation { } override streamDone(): void { - if (this.runner && this.#bufferedRecords.length > 0) { - const results = this.runner.executeBatch(this.#bufferedRecords); + if (this.runner && this.bufferedRecords.length > 0) { + const results = this.runner.executeBatch(this.bufferedRecords); for (let i = 0; i < results.length; i++) { const result = results[i]!; - const inputRecord = this.#bufferedRecords[i]!; + const inputRecord = this.bufferedRecords[i]!; if (result.error) { process.stderr.write(`generate: ${result.error}\n`); diff --git a/src/operations/transform/grep.ts b/src/operations/transform/grep.ts index 9201534..afc2039 100644 --- a/src/operations/transform/grep.ts +++ b/src/operations/transform/grep.ts @@ -20,7 +20,8 @@ export class GrepOperation extends Operation { seenRecord = false; lang: string | null = null; runner: SnippetRunner | null = null; - #bufferedRecords: Record[] = []; + bufferedRecords: Record[] = []; + extraArgs: string[] = []; override addHelpTypes(): void { this.useHelpType("snippet"); @@ -79,9 +80,18 @@ export class GrepOperation extends Operation { this.beforeCount = context; } - const expression = fileSnippet ?? exprSnippet ?? remaining.join(" "); - if (!expression) { - throw new Error("grep requires an expression argument"); + let expression: string; + if (fileSnippet ?? exprSnippet) { + // Expression provided via -e or -E; remaining args are file paths + expression = (fileSnippet ?? exprSnippet)!; + this.extraArgs = remaining; + } else { + // First positional arg is expression, rest are file paths + if (remaining.length === 0) { + throw new Error("grep requires an expression argument"); + } + expression = remaining[0]!; + this.extraArgs = remaining.slice(1); } if (this.lang && !isJsLang(this.lang)) { @@ -92,13 +102,13 @@ export class GrepOperation extends Operation { } } - #hasContext(): boolean { + hasContext(): boolean { return this.afterCount > 0 || this.beforeCount > 0; } acceptRecord(record: Record): boolean { - if (this.runner && !this.#hasContext()) { - this.#bufferedRecords.push(record); + if (this.runner && !this.hasContext()) { + this.bufferedRecords.push(record); return true; } @@ -113,7 +123,7 @@ export class GrepOperation extends Operation { return true; } if (this.antiMatch) passed = !passed; - return this.#applyContext(record, passed); + return this.applyContext(record, passed); } let matched = this.executor.executeCode(record); @@ -121,10 +131,10 @@ export class GrepOperation extends Operation { matched = !matched; } - return this.#applyContext(record, !!matched); + return this.applyContext(record, !!matched); } - #applyContext(record: Record, matched: boolean): boolean { + applyContext(record: Record, matched: boolean): boolean { let pushedRecord = false; if (matched) { @@ -158,8 +168,8 @@ export class GrepOperation extends Operation { } override streamDone(): void { - if (this.runner && this.#bufferedRecords.length > 0) { - const results = this.runner.executeBatch(this.#bufferedRecords); + if (this.runner && this.bufferedRecords.length > 0) { + const results = this.runner.executeBatch(this.bufferedRecords); for (let i = 0; i < results.length; i++) { const result = results[i]!; if (result.error) { @@ -169,7 +179,7 @@ export class GrepOperation extends Operation { let passed = result.passed ?? false; if (this.antiMatch) passed = !passed; if (passed) { - this.pushRecord(this.#bufferedRecords[i]!); + this.pushRecord(this.bufferedRecords[i]!); this.seenRecord = true; } } diff --git a/src/operations/transform/xform.ts b/src/operations/transform/xform.ts index 5ac9213..688f4f2 100644 --- a/src/operations/transform/xform.ts +++ b/src/operations/transform/xform.ts @@ -24,7 +24,8 @@ export class XformOperation extends Operation { suppressR = false; lang: string | null = null; runner: SnippetRunner | null = null; - #bufferedRecords: Record[] = []; + bufferedRecords: Record[] = []; + extraArgs: string[] = []; override addHelpTypes(): void { this.useHelpType("snippet"); @@ -83,9 +84,17 @@ export class XformOperation extends Operation { ]; const remaining = this.parseOptions(args, defs); - const expression = fileSnippet ?? exprSnippet ?? remaining.join(" "); - if (!expression) { - throw new Error("xform requires an expression argument"); + + let expression: string; + if (fileSnippet ?? exprSnippet) { + expression = (fileSnippet ?? exprSnippet)!; + this.extraArgs = remaining; + } else { + if (remaining.length === 0) { + throw new Error("xform requires an expression argument"); + } + expression = remaining[0]!; + this.extraArgs = remaining.slice(1); } if (this.lang && !isJsLang(this.lang)) { @@ -126,20 +135,20 @@ export class XformOperation extends Operation { return executor; } - #hasContext(): boolean { + hasContext(): boolean { return this.beforeCount > 0 || this.afterCount > 0; } acceptRecord(record: Record): boolean { - if (this.runner && !this.#hasContext()) { - this.#bufferedRecords.push(record); + if (this.runner && !this.hasContext()) { + this.bufferedRecords.push(record); return true; } if (this.runner) { // Context mode with non-JS lang: process one record at a time // so the context sliding window logic below can work - this.#runSingleWithRunner(record); + this.runSingleWithRunner(record); return true; } @@ -179,7 +188,7 @@ export class XformOperation extends Operation { return this.runRecordWithContext(this.currentRecord, this.beforeArray, this.afterArray); } - #runSingleWithRunner(record: Record): void { + runSingleWithRunner(record: Record): void { if (!this.runner) return; const results = this.runner.executeBatch([record]); const result = results[0]; @@ -195,8 +204,8 @@ export class XformOperation extends Operation { } override streamDone(): void { - if (this.runner && this.#bufferedRecords.length > 0) { - const results = this.runner.executeBatch(this.#bufferedRecords); + if (this.runner && this.bufferedRecords.length > 0) { + const results = this.runner.executeBatch(this.bufferedRecords); for (const result of results) { if (result.error) { process.stderr.write(`xform: ${result.error}\n`); diff --git a/src/snippets/JsSnippetRunner.ts b/src/snippets/JsSnippetRunner.ts index 1f087cf..484feef 100644 --- a/src/snippets/JsSnippetRunner.ts +++ b/src/snippets/JsSnippetRunner.ts @@ -17,57 +17,57 @@ import type { export class JsSnippetRunner implements SnippetRunner { name = "javascript"; - #executor: Executor | null = null; - #mode: SnippetMode = "eval"; + executor: Executor | null = null; + mode: SnippetMode = "eval"; async init(code: string, context: SnippetContext): Promise { - this.#mode = context.mode; + this.mode = context.mode; switch (context.mode) { case "eval": // Eval mode: run code, return modified record - this.#executor = new Executor(`${code}\n; return r;`); + this.executor = new Executor(`${code}\n; return r;`); break; case "grep": // Grep mode: evaluate as expression, return boolean - this.#executor = new Executor(autoReturn(code)); + this.executor = new Executor(autoReturn(code)); break; case "xform": // Xform mode: run code, return modified record (or array) - this.#executor = new Executor(`${code}\n; return r;`); + this.executor = new Executor(`${code}\n; return r;`); break; case "generate": // Generate mode: evaluate expression, return array of records - this.#executor = new Executor(autoReturn(code)); + this.executor = new Executor(autoReturn(code)); break; } } async executeRecord(record: Record): Promise { - return this.#executeRecordSync(record); + return this.executeRecordSync(record); } executeBatch(records: Record[]): SnippetResult[] { - return records.map((r) => this.#executeRecordSync(r)); + return records.map((r) => this.executeRecordSync(r)); } - #executeRecordSync(record: Record): SnippetResult { - if (!this.#executor) { + executeRecordSync(record: Record): SnippetResult { + if (!this.executor) { return { error: "Runner not initialized" }; } try { - switch (this.#mode) { + switch (this.mode) { case "eval": { - this.#executor.executeCode(record); + this.executor.executeCode(record); return { record: record.toJSON() }; } case "grep": { - const result = this.#executor.executeCode(record); + const result = this.executor.executeCode(record); return { passed: !!result }; } case "xform": { - const result = this.#executor.executeCode(record); + const result = this.executor.executeCode(record); if (Array.isArray(result)) { const records = result .filter((item): item is JsonObject | Record => @@ -87,7 +87,7 @@ export class JsSnippetRunner implements SnippetRunner { return { records: [record.toJSON()] }; } case "generate": { - const result = this.#executor.executeCode(record); + const result = this.executor.executeCode(record); const items = Array.isArray(result) ? result : result ? [result] : []; const records = items .filter((item): item is JsonObject | Record => @@ -109,6 +109,6 @@ export class JsSnippetRunner implements SnippetRunner { } async shutdown(): Promise { - this.#executor = null; + this.executor = null; } } diff --git a/src/snippets/PerlSnippetRunner.ts b/src/snippets/PerlSnippetRunner.ts index 67c2e6a..774fd39 100644 --- a/src/snippets/PerlSnippetRunner.ts +++ b/src/snippets/PerlSnippetRunner.ts @@ -25,12 +25,12 @@ const RUNNER_PATH = join(RUNNER_DIR, "runner.pl"); export class PerlSnippetRunner implements SnippetRunner { name = "perl"; - #code = ""; - #mode: SnippetMode = "eval"; + code = ""; + mode: SnippetMode = "eval"; async init(code: string, context: SnippetContext): Promise { - this.#code = transformCode(code, "lvalue"); - this.#mode = context.mode; + this.code = transformCode(code, "lvalue"); + this.mode = context.mode; } async executeRecord(record: Record): Promise { @@ -40,7 +40,7 @@ export class PerlSnippetRunner implements SnippetRunner { executeBatch(records: Record[]): SnippetResult[] { const lines: string[] = [ - JSON.stringify({ type: "init", code: this.#code, mode: this.#mode }), + JSON.stringify({ type: "init", code: this.code, mode: this.mode }), ...records.map((r) => JSON.stringify({ type: "record", data: r.toJSON() }) ), diff --git a/src/snippets/PythonSnippetRunner.ts b/src/snippets/PythonSnippetRunner.ts index 302e048..28511c4 100644 --- a/src/snippets/PythonSnippetRunner.ts +++ b/src/snippets/PythonSnippetRunner.ts @@ -26,12 +26,12 @@ const RUNNER_PATH = join(RUNNER_DIR, "runner.py"); export class PythonSnippetRunner implements SnippetRunner { name = "python"; - #code = ""; - #mode: SnippetMode = "eval"; + code = ""; + mode: SnippetMode = "eval"; async init(code: string, context: SnippetContext): Promise { - this.#code = transformCode(code, "accessor"); - this.#mode = context.mode; + this.code = transformCode(code, "accessor"); + this.mode = context.mode; } async executeRecord(record: Record): Promise { @@ -41,7 +41,7 @@ export class PythonSnippetRunner implements SnippetRunner { executeBatch(records: Record[]): SnippetResult[] { const lines: string[] = [ - JSON.stringify({ type: "init", code: this.#code, mode: this.#mode }), + JSON.stringify({ type: "init", code: this.code, mode: this.mode }), ...records.map((r) => JSON.stringify({ type: "record", data: r.toJSON() }) ), diff --git a/tests/perf/bench.ts b/tests/perf/bench.ts index 878b98b..f047db5 100644 --- a/tests/perf/bench.ts +++ b/tests/perf/bench.ts @@ -166,13 +166,13 @@ function fmtDeltaMarkdown(pct: number, threshold: number = 10): string { export class BenchmarkSuite { name: string; - #entries: BenchEntry[] = []; - #results: BenchmarkResult[] = []; - #suiteOptions: SuiteOptions; + entries: BenchEntry[] = []; + results: BenchmarkResult[] = []; + suiteOptions: SuiteOptions; constructor(name: string, options?: SuiteOptions) { this.name = name; - this.#suiteOptions = options ?? {}; + this.suiteOptions = options ?? {}; } /** @@ -183,17 +183,17 @@ export class BenchmarkSuite { fn: () => void | Promise, options?: BenchmarkOptions, ): void { - this.#entries.push({ name, fn, options: options ?? {} }); + this.entries.push({ name, fn, options: options ?? {} }); } /** * Run all registered benchmarks and print results. */ async run(): Promise { - const filter = this.#suiteOptions.filter; + const filter = this.suiteOptions.filter; const entries = filter - ? this.#entries.filter((e) => e.name.includes(filter)) - : this.#entries; + ? this.entries.filter((e) => e.name.includes(filter)) + : this.entries; if (entries.length === 0) { console.log(`\nSuite: ${this.name} — no benchmarks matched filter\n`); @@ -207,15 +207,15 @@ export class BenchmarkSuite { const baseline = loadBaseline(); for (const entry of entries) { - const result = await this.#runOne(entry); - this.#results.push(result); - this.#printResult(result, baseline); + const result = await this.runOne(entry); + this.results.push(result); + this.printResult(result, baseline); } - return this.#results; + return this.results; } - async #runOne(entry: BenchEntry): Promise { + async runOne(entry: BenchEntry): Promise { const warmup = entry.options.warmup ?? 3; const iterations = entry.options.iterations ?? 10; @@ -261,7 +261,7 @@ export class BenchmarkSuite { return result; } - #printResult(result: BenchmarkResult, baseline: BaselineData | null): void { + printResult(result: BenchmarkResult, baseline: BaselineData | null): void { const parts: string[] = [ ` ${result.name}`, ` min=${fmtMs(result.min)} median=${fmtMs(result.median)} p95=${fmtMs(result.p95)} max=${fmtMs(result.max)}`, @@ -290,12 +290,12 @@ export class BenchmarkSuite { * Save current results as the new baseline. */ saveBaseline(): void { - saveBaseline(this.#results); + saveBaseline(this.results); console.log(`\nBaseline saved to ${BASELINE_FILE}`); } getResults(): BenchmarkResult[] { - return [...this.#results]; + return [...this.results]; } } From fd626fa2f47101885abd89d75e9639c7b74f1719 Mon Sep 17 00:00:00 2001 From: Ben Bernard Date: Tue, 24 Feb 2026 17:12:05 -0800 Subject: [PATCH 2/3] feat: refactor dispatcher for universal stdin/file input support All commands now transparently support both stdin and file arguments. The dispatcher handles file reading so operations don't need to. Transform ops (grep, eval, sort, etc.) now accept file args. Removed duplicated file-reading logic from individual from* operations. Co-Authored-By: Claude Opus 4.6 --- src/cli/dispatcher.ts | 127 ++++++++++++++++------ src/operations/input/fromapache.ts | 3 + src/operations/input/fromcsv.ts | 18 --- src/operations/input/fromjsonarray.ts | 15 --- src/operations/input/fromkv.ts | 3 +- src/operations/input/frommultire.ts | 3 +- src/operations/input/fromre.ts | 22 ---- src/operations/input/fromsplit.ts | 22 ---- src/operations/input/fromxferlog.ts | 6 +- src/operations/input/fromxml.ts | 38 +++---- src/operations/transform/collate.ts | 3 +- src/operations/transform/decollate.ts | 3 +- src/operations/transform/delta.ts | 3 +- src/operations/transform/expandjson.ts | 3 +- src/operations/transform/flatten.ts | 3 +- src/operations/transform/normalizetime.ts | 3 +- src/operations/transform/parsedate.ts | 3 +- src/operations/transform/sort.ts | 3 +- src/operations/transform/stream2table.ts | 3 +- src/operations/transform/substream.ts | 3 +- src/operations/transform/topn.ts | 3 +- tests/operations/input/fromcsv.test.ts | 7 ++ tests/operations/input/fromsplit.test.ts | 25 +++++ 23 files changed, 174 insertions(+), 148 deletions(-) diff --git a/src/cli/dispatcher.ts b/src/cli/dispatcher.ts index 3e1796b..e4f2d80 100644 --- a/src/cli/dispatcher.ts +++ b/src/cli/dispatcher.ts @@ -122,10 +122,11 @@ for (const [name, Ctor] of operationRegistry) { } /** - * Input operations that can consume bulk stdin content via parseContent(). - * When they have no file args, we read all of stdin and call parseContent(). + * Input operations that consume bulk content via parseContent()/parseXml(). + * For these ops, when no file args are given we read all of stdin as one string. + * When file args are given, the dispatcher reads each file and calls parseContent(). */ -const BULK_STDIN_OPS = new Set(["fromcsv", "fromjsonarray", "fromkv", "fromxml"]); +const BULK_CONTENT_OPS = new Set(["fromcsv", "fromjsonarray", "fromxml"]); /** * Read all of stdin as a string. @@ -175,6 +176,26 @@ async function readStdinLines(callback: (line: string) => boolean): Promise boolean): void { + const content = readFileSync(path); + for (const line of content.split("\n")) { + if (line === "") continue; + if (!callback(line)) return; + } +} + /** * Look up an operation constructor by name. */ @@ -240,79 +261,113 @@ export async function runOperation(command: string, args: string[]): Promise { - if (op.wantsInput()) { - // Determine if this is a line-oriented input op (has custom acceptLine) - // or a record-oriented transform op - const isLineOriented = hasCustomAcceptLine(op); + const fileArgs = getFileArgs(op); + const isBulk = BULK_CONTENT_OPS.has(command); + const isLineOriented = !isBulk && hasCustomAcceptLine(op); + if (fileArgs.length > 0 && (op.wantsInput() || isBulk)) { + // --- Feed from files --- + if (isBulk) { + for (const file of fileArgs) { + op.updateCurrentFilename(file); + const content = readFileSync(file); + callParseContent(command, op, content); + } + } else if (isLineOriented) { + for (const file of fileArgs) { + op.updateCurrentFilename(file); + feedFileLines(file, (line) => op.acceptLine(line)); + } + } else { + // Record-oriented (transform ops) + for (const file of fileArgs) { + op.updateCurrentFilename(file); + feedFileLines(file, (line) => { + try { + const record = Record.fromJSON(line); + return op.acceptRecord(record); + } catch { + return true; + } + }); + } + } + await op.finish(); + } else if (op.wantsInput()) { + // --- Feed from stdin (line or record oriented) --- if (isLineOriented) { - // Feed raw lines from stdin — used by fromre, fromsplit, fromapache, etc. - await readStdinLines((line) => { - if (op.acceptLine) { - return op.acceptLine(line); - } - return true; - }); + await readStdinLines((line) => op.acceptLine(line)); } else { - // Feed JSON records from stdin or file args - // Parse remaining args (after options) to find file paths - // Since init() already consumed options, we check if the operation - // has stashed extra args that look like file paths. await readStdinLines((line) => { try { const record = Record.fromJSON(line); return op.acceptRecord(record); } catch { - // If it's not JSON, skip it return true; } }); } await op.finish(); - } else if (BULK_STDIN_OPS.has(command) && needsStdinContent(op)) { - // Input op that needs bulk stdin content (no file args given) + } else if (isBulk && needsStdinContent(op)) { + // --- Bulk stdin (fromcsv, fromjsonarray, fromxml with no args) --- const content = await readAllStdin(); if (content.trim()) { callParseContent(command, op, content); } await op.finish(); } else { - // Self-contained operation (fromps, fromdb, etc.) or has file args + // --- Self-contained (fromps, fromdb, fromtcpdump, fromxls, etc.) --- await op.finish(); } } +/** + * Get file args from an operation's extraArgs property. + */ +function getFileArgs(op: Operation): string[] { + const opRecord = op as unknown as { [key: string]: unknown }; + const extraArgs = opRecord["extraArgs"]; + if (Array.isArray(extraArgs)) { + return extraArgs as string[]; + } + return []; +} + /** * Check if the operation has a custom acceptLine implementation * (indicating it processes raw text lines, not JSON records). */ function hasCustomAcceptLine(op: Operation): boolean { - // Check if the prototype overrides acceptLine const proto = Object.getPrototypeOf(op) as { [key: string]: unknown }; return typeof proto["acceptLine"] === "function" && proto["acceptLine"] !== Operation.prototype.acceptLine; } /** - * Check if an input operation needs stdin content (has no file args to process). - * We detect this by checking the extraArgs property which input ops use for files. + * Check if an input operation needs stdin content (has no file args or URL args). */ function needsStdinContent(op: Operation): boolean { - // Input ops store file args in extraArgs; if empty, they need stdin const opRecord = op as unknown as { [key: string]: unknown }; const extraArgs = opRecord["extraArgs"]; - if (Array.isArray(extraArgs)) { - return extraArgs.length === 0; - } + if (Array.isArray(extraArgs) && extraArgs.length > 0) return false; + // fromxml stores URL args separately; if it has URLs it doesn't need stdin + const urlArgs = opRecord["urlArgs"]; + if (Array.isArray(urlArgs) && urlArgs.length > 0) return false; return true; } diff --git a/src/operations/input/fromapache.ts b/src/operations/input/fromapache.ts index 8809167..eca82c4 100644 --- a/src/operations/input/fromapache.ts +++ b/src/operations/input/fromapache.ts @@ -31,6 +31,7 @@ type ParseMode = "fast" | "strict"; export class FromApache extends Operation { mode: ParseMode = "fast"; strictFormats: string[] | null = null; + extraArgs: string[] = []; acceptRecord(_record: Record): boolean { return true; @@ -121,6 +122,8 @@ export class FromApache extends Operation { } } } + + this.extraArgs = remaining; } processLine(line: string): void { diff --git a/src/operations/input/fromcsv.ts b/src/operations/input/fromcsv.ts index 395bdfd..2927fbc 100644 --- a/src/operations/input/fromcsv.ts +++ b/src/operations/input/fromcsv.ts @@ -96,19 +96,6 @@ export class FromCsv extends Operation { return false; } - override streamDone(): void { - if (this.extraArgs.length > 0) { - for (const file of this.extraArgs) { - this.updateCurrentFilename(file); - const content = readFileSync(file); - this.parseContent(content); - } - } else { - // For programmatic use, stdin should be passed as content - // In CLI mode, stdin will be handled by the dispatcher - } - } - /** * Parse CSV content and push records. * This is the main entry point for both file and stdin processing. @@ -153,11 +140,6 @@ export class FromCsv extends Operation { } } -function readFileSync(path: string): string { - const fs = require("node:fs") as typeof import("node:fs"); - return fs.readFileSync(path, "utf-8"); -} - import type { CommandDoc } from "../../types/CommandDoc.ts"; export const documentation: CommandDoc = { diff --git a/src/operations/input/fromjsonarray.ts b/src/operations/input/fromjsonarray.ts index 2e29fc6..5f3b3b9 100644 --- a/src/operations/input/fromjsonarray.ts +++ b/src/operations/input/fromjsonarray.ts @@ -36,16 +36,6 @@ export class FromJsonArray extends Operation { return false; } - override streamDone(): void { - if (this.extraArgs.length > 0) { - for (const file of this.extraArgs) { - this.updateCurrentFilename(file); - const content = readFileSync(file); - this.parseContent(content); - } - } - } - parseContent(content: string): void { const parsed: unknown = JSON.parse(content); @@ -77,11 +67,6 @@ export class FromJsonArray extends Operation { } } -function readFileSync(path: string): string { - const fs = require("node:fs") as typeof import("node:fs"); - return fs.readFileSync(path, "utf-8"); -} - import type { CommandDoc } from "../../types/CommandDoc.ts"; export const documentation: CommandDoc = { diff --git a/src/operations/input/fromkv.ts b/src/operations/input/fromkv.ts index f7e3b3a..26e4097 100644 --- a/src/operations/input/fromkv.ts +++ b/src/operations/input/fromkv.ts @@ -12,6 +12,7 @@ export class FromKv extends Operation { entryDelim = "\n"; recordDelim = "END\n"; acc: string | null = null; + extraArgs: string[] = []; acceptRecord(_record: Record): boolean { return true; @@ -48,7 +49,7 @@ export class FromKv extends Operation { }, ]; - this.parseOptions(args, defs); + this.extraArgs = this.parseOptions(args, defs); } override acceptLine(line: string): boolean { diff --git a/src/operations/input/frommultire.ts b/src/operations/input/frommultire.ts index f51368b..9a5ecd0 100644 --- a/src/operations/input/frommultire.ts +++ b/src/operations/input/frommultire.ts @@ -20,6 +20,7 @@ export class FromMultiRe extends Operation { keepAll = false; keepFields: Set = new Set(); currentRecord: Record = new Record(); + extraArgs: string[] = []; acceptRecord(_record: Record): boolean { return true; @@ -127,7 +128,7 @@ export class FromMultiRe extends Operation { }, ]; - this.parseOptions(args, defs); + this.extraArgs = this.parseOptions(args, defs); } addRegex( diff --git a/src/operations/input/fromre.ts b/src/operations/input/fromre.ts index 091203b..889a31a 100644 --- a/src/operations/input/fromre.ts +++ b/src/operations/input/fromre.ts @@ -47,23 +47,6 @@ export class FromRe extends Operation { return true; } - override wantsInput(): boolean { - return this.extraArgs.length === 0; - } - - override streamDone(): void { - if (this.extraArgs.length > 0) { - for (const file of this.extraArgs) { - this.updateCurrentFilename(file); - const content = readFileSync(file); - for (const line of content.split("\n")) { - if (line === "") continue; - this.processLine(line); - } - } - } - } - processLine(line: string): void { if (!this.pattern) return; @@ -89,11 +72,6 @@ export class FromRe extends Operation { } } -function readFileSync(path: string): string { - const fs = require("node:fs") as typeof import("node:fs"); - return fs.readFileSync(path, "utf-8"); -} - import type { CommandDoc } from "../../types/CommandDoc.ts"; export const documentation: CommandDoc = { diff --git a/src/operations/input/fromsplit.ts b/src/operations/input/fromsplit.ts index fc4e4ef..7fe44bf 100644 --- a/src/operations/input/fromsplit.ts +++ b/src/operations/input/fromsplit.ts @@ -68,23 +68,6 @@ export class FromSplit extends Operation { return true; } - override wantsInput(): boolean { - return this.extraArgs.length === 0; - } - - override streamDone(): void { - if (this.extraArgs.length > 0) { - for (const file of this.extraArgs) { - this.updateCurrentFilename(file); - const content = readFileSync(file); - for (const line of content.split("\n")) { - if (line === "") continue; - this.processLine(line); - } - } - } - } - processLine(line: string): void { if (this.header) { const values = this.splitLine(line); @@ -122,11 +105,6 @@ export class FromSplit extends Operation { } } -function readFileSync(path: string): string { - const fs = require("node:fs") as typeof import("node:fs"); - return fs.readFileSync(path, "utf-8"); -} - import type { CommandDoc } from "../../types/CommandDoc.ts"; export const documentation: CommandDoc = { diff --git a/src/operations/input/fromxferlog.ts b/src/operations/input/fromxferlog.ts index 99a1020..7a62d32 100644 --- a/src/operations/input/fromxferlog.ts +++ b/src/operations/input/fromxferlog.ts @@ -18,12 +18,14 @@ const XFERLOG_RE = * Analogous to App::RecordStream::Operation::fromxferlog in Perl. */ export class FromXferlog extends Operation { + extraArgs: string[] = []; + acceptRecord(_record: Record): boolean { return true; } - init(_args: string[]): void { - // No options + init(args: string[]): void { + this.extraArgs = this.parseOptions(args, []); } processLine(line: string): void { diff --git a/src/operations/input/fromxml.ts b/src/operations/input/fromxml.ts index b895381..0af2a1e 100644 --- a/src/operations/input/fromxml.ts +++ b/src/operations/input/fromxml.ts @@ -12,18 +12,12 @@ export class FromXml extends Operation { elements: string[] = []; nested = false; extraArgs: string[] = []; - stdinLines: string[] = []; + urlArgs: string[] = []; acceptRecord(_record: Record): boolean { return true; } - override acceptLine(line: string): boolean { - // When reading from stdin, accumulate raw XML lines - this.stdinLines.push(line); - return true; - } - init(args: string[]): void { const defs: OptionDef[] = [ { @@ -44,29 +38,33 @@ export class FromXml extends Operation { }, ]; - this.extraArgs = this.parseOptions(args, defs); + const allArgs = this.parseOptions(args, defs); + + // Separate file paths (handled by dispatcher) from URIs (handled here) + for (const arg of allArgs) { + if (arg.startsWith("http://") || arg.startsWith("https://") || arg.startsWith("file:")) { + this.urlArgs.push(arg); + } else { + this.extraArgs.push(arg); + } + } // Deduplicate elements this.elements = [...new Set(this.elements)]; } override wantsInput(): boolean { - return this.extraArgs.length === 0; + return false; } override streamDone(): void { - if (this.extraArgs.length > 0) { - for (const uri of this.extraArgs) { - this.updateCurrentFilename(uri); - const xml = this.getXmlString(uri); - if (xml) { - this.parseXml(xml); - } + // Only handle URI args here — file paths are handled by the dispatcher + for (const uri of this.urlArgs) { + this.updateCurrentFilename(uri); + const xml = this.getXmlString(uri); + if (xml) { + this.parseXml(xml); } - } else if (this.stdinLines.length > 0) { - this.updateCurrentFilename("STDIN"); - const xml = this.stdinLines.join("\n"); - this.parseXml(xml); } } diff --git a/src/operations/transform/collate.ts b/src/operations/transform/collate.ts index ff31f59..aaf8228 100644 --- a/src/operations/transform/collate.ts +++ b/src/operations/transform/collate.ts @@ -116,6 +116,7 @@ interface CollateCookie { * Analogous to App::RecordStream::Operation::collate in Perl. */ export class CollateOperation extends Operation { + extraArgs: string[] = []; clumperOptions!: ClumperOptions; incremental = false; @@ -302,7 +303,7 @@ export class CollateOperation extends Operation { }, ]; - this.parseOptions(args, defs); + this.extraArgs = this.parseOptions(args, defs); // Build the aggregators map: start with -a specs, then merge DL aggregators const aggregators = makeAggregators(...aggregatorSpecs); diff --git a/src/operations/transform/decollate.ts b/src/operations/transform/decollate.ts index fca9233..da45733 100644 --- a/src/operations/transform/decollate.ts +++ b/src/operations/transform/decollate.ts @@ -12,6 +12,7 @@ import type { JsonValue } from "../../types/json.ts"; * Analogous to App::RecordStream::Operation::decollate in Perl. */ export class DecollateOperation extends Operation { + extraArgs: string[] = []; deaggregators: Deaggregator[] = []; onlyDeaggregated = false; @@ -46,7 +47,7 @@ export class DecollateOperation extends Operation { }, ]; - this.parseOptions(args, defs); + this.extraArgs = this.parseOptions(args, defs); for (const spec of deaggSpecs) { this.deaggregators.push(deaggregatorRegistry.parse(spec)); diff --git a/src/operations/transform/delta.ts b/src/operations/transform/delta.ts index 4b71c45..edafcc5 100644 --- a/src/operations/transform/delta.ts +++ b/src/operations/transform/delta.ts @@ -11,6 +11,7 @@ import type { JsonObject } from "../../types/json.ts"; * Analogous to App::RecordStream::Operation::delta in Perl. */ export class DeltaOperation extends Operation { + extraArgs: string[] = []; keyGroups = new KeyGroups(); lastRecord: Record | null = null; @@ -25,7 +26,7 @@ export class DeltaOperation extends Operation { }, ]; - this.parseOptions(args, defs); + this.extraArgs = this.parseOptions(args, defs); if (!this.keyGroups.hasAnyGroup()) { throw new Error("Must specify --key"); diff --git a/src/operations/transform/expandjson.ts b/src/operations/transform/expandjson.ts index 921eb20..51a06d2 100644 --- a/src/operations/transform/expandjson.ts +++ b/src/operations/transform/expandjson.ts @@ -11,6 +11,7 @@ import type { CommandDoc } from "../../types/CommandDoc.ts"; * Analogous to App::RecordStream::Operation::expandjson (new operation). */ export class ExpandJsonOperation extends Operation { + extraArgs: string[] = []; keys: string[] = []; recursive = false; @@ -36,7 +37,7 @@ export class ExpandJsonOperation extends Operation { }, ]; - this.parseOptions(args, defs); + this.extraArgs = this.parseOptions(args, defs); } acceptRecord(record: Record): boolean { diff --git a/src/operations/transform/flatten.ts b/src/operations/transform/flatten.ts index eb3277b..fe98bb4 100644 --- a/src/operations/transform/flatten.ts +++ b/src/operations/transform/flatten.ts @@ -16,6 +16,7 @@ interface FlattenField { * Analogous to App::RecordStream::Operation::flatten in Perl. */ export class FlattenOperation extends Operation { + extraArgs: string[] = []; fields: FlattenField[] = []; separator = "-"; defaultDepth = 1; @@ -76,7 +77,7 @@ export class FlattenOperation extends Operation { i++; } - this.parseOptions(filteredArgs, defs); + this.extraArgs = this.parseOptions(filteredArgs, defs); } acceptRecord(record: Record): boolean { diff --git a/src/operations/transform/normalizetime.ts b/src/operations/transform/normalizetime.ts index 1bb6179..709797b 100644 --- a/src/operations/transform/normalizetime.ts +++ b/src/operations/transform/normalizetime.ts @@ -11,6 +11,7 @@ import type { JsonObject } from "../../types/json.ts"; * Analogous to App::RecordStream::Operation::normalizetime in Perl. */ export class NormalizeTimeOperation extends Operation { + extraArgs: string[] = []; key = ""; sanitizedKey = ""; threshold = 0; @@ -58,7 +59,7 @@ export class NormalizeTimeOperation extends Operation { }, ]; - this.parseOptions(args, defs); + this.extraArgs = this.parseOptions(args, defs); if (!this.key) { throw new Error("Must specify --key"); diff --git a/src/operations/transform/parsedate.ts b/src/operations/transform/parsedate.ts index b7adf33..47e8d4a 100644 --- a/src/operations/transform/parsedate.ts +++ b/src/operations/transform/parsedate.ts @@ -12,6 +12,7 @@ import type { JsonObject } from "../../types/json.ts"; * Inspired by the Perl PR #74 parsedate operation. */ export class ParseDateOperation extends Operation { + extraArgs: string[] = []; inputKey = ""; outputKey = ""; inputFormat: string | null = null; @@ -73,7 +74,7 @@ export class ParseDateOperation extends Operation { }, ]; - this.parseOptions(args, defs); + this.extraArgs = this.parseOptions(args, defs); if (!this.inputKey) { throw new Error("Must specify --key"); diff --git a/src/operations/transform/sort.ts b/src/operations/transform/sort.ts index 9716937..dedae5c 100644 --- a/src/operations/transform/sort.ts +++ b/src/operations/transform/sort.ts @@ -12,6 +12,7 @@ export class SortOperation extends Operation { accumulator = new Accumulator(); keys: string[] = []; reverse = false; + extraArgs: string[] = []; init(args: string[]): void { const defs: OptionDef[] = [ @@ -33,7 +34,7 @@ export class SortOperation extends Operation { }, ]; - this.parseOptions(args, defs); + this.extraArgs = this.parseOptions(args, defs); } acceptRecord(record: Record): boolean { diff --git a/src/operations/transform/stream2table.ts b/src/operations/transform/stream2table.ts index 4a4be4a..1426380 100644 --- a/src/operations/transform/stream2table.ts +++ b/src/operations/transform/stream2table.ts @@ -11,6 +11,7 @@ import type { JsonObject, JsonValue } from "../../types/json.ts"; * Analogous to App::RecordStream::Operation::stream2table in Perl. */ export class Stream2TableOperation extends Operation { + extraArgs: string[] = []; field = ""; removeField = false; groups = new Map(); @@ -26,7 +27,7 @@ export class Stream2TableOperation extends Operation { }, ]; - this.parseOptions(args, defs); + this.extraArgs = this.parseOptions(args, defs); if (!this.field) { throw new Error("You must specify a --field option"); diff --git a/src/operations/transform/substream.ts b/src/operations/transform/substream.ts index b89efd0..164f495 100644 --- a/src/operations/transform/substream.ts +++ b/src/operations/transform/substream.ts @@ -11,6 +11,7 @@ import { Record } from "../../Record.ts"; * Analogous to App::RecordStream::Operation::substream in Perl. */ export class SubstreamOperation extends Operation { + extraArgs: string[] = []; beginExecutor: Executor | null = null; endExecutor: Executor | null = null; inSubstream = false; @@ -37,7 +38,7 @@ export class SubstreamOperation extends Operation { }, ]; - this.parseOptions(args, defs); + this.extraArgs = this.parseOptions(args, defs); if (beginExpr) { this.beginExecutor = new Executor(autoReturn(beginExpr)); diff --git a/src/operations/transform/topn.ts b/src/operations/transform/topn.ts index 548b3d0..7db5018 100644 --- a/src/operations/transform/topn.ts +++ b/src/operations/transform/topn.ts @@ -11,6 +11,7 @@ import type { JsonObject } from "../../types/json.ts"; * Analogous to App::RecordStream::Operation::topn in Perl. */ export class TopnOperation extends Operation { + extraArgs: string[] = []; keyGroups = new KeyGroups(); num = 10; delimiter = "9t%7Oz%]"; @@ -41,7 +42,7 @@ export class TopnOperation extends Operation { }, ]; - this.parseOptions(args, defs); + this.extraArgs = this.parseOptions(args, defs); if (!this.num) { throw new Error("Must specify --topn "); diff --git a/tests/operations/input/fromcsv.test.ts b/tests/operations/input/fromcsv.test.ts index 9c489ab..60526b8 100644 --- a/tests/operations/input/fromcsv.test.ts +++ b/tests/operations/input/fromcsv.test.ts @@ -70,6 +70,7 @@ describe("FromCsv", () => { }); test("reads from file with header and static key", () => { + const fs = require("node:fs") as typeof import("node:fs"); const collector = new CollectorReceiver(); const op = new FromCsv(collector); op.init([ @@ -79,6 +80,12 @@ describe("FromCsv", () => { "tests/fixtures/data3.csv", "tests/fixtures/data4.csv", ]); + // Simulate dispatcher file reading: read each file and call parseContent + for (const file of op.extraArgs) { + op.updateCurrentFilename(file); + const content = fs.readFileSync(file, "utf-8"); + op.parseContent(content); + } op.finish(); const result = collector.records.map((r) => r.toJSON()); expect(result).toEqual([ diff --git a/tests/operations/input/fromsplit.test.ts b/tests/operations/input/fromsplit.test.ts index 1c7068f..327fd59 100644 --- a/tests/operations/input/fromsplit.test.ts +++ b/tests/operations/input/fromsplit.test.ts @@ -19,9 +19,18 @@ function runFromSplit( describe("FromSplit", () => { test("split file with custom delimiter and field name", () => { + const fs = require("node:fs") as typeof import("node:fs"); const collector = new CollectorReceiver(); const op = new FromSplit(collector); op.init(["-f", "f1", "-d", " ", "tests/fixtures/splitfile"]); + // Simulate dispatcher: read file lines and call acceptLine + for (const file of op.extraArgs) { + const content = fs.readFileSync(file, "utf-8"); + for (const line of content.split("\n")) { + if (line === "") continue; + op.acceptLine(line); + } + } op.finish(); const result = collector.records.map((r) => r.toJSON()); expect(result).toEqual([ @@ -31,9 +40,17 @@ describe("FromSplit", () => { }); test("split file with default delimiter (comma regex)", () => { + const fs = require("node:fs") as typeof import("node:fs"); const collector = new CollectorReceiver(); const op = new FromSplit(collector); op.init(["tests/fixtures/splitfile"]); + for (const file of op.extraArgs) { + const content = fs.readFileSync(file, "utf-8"); + for (const line of content.split("\n")) { + if (line === "") continue; + op.acceptLine(line); + } + } op.finish(); const result = collector.records.map((r) => r.toJSON()); expect(result).toEqual([ @@ -43,9 +60,17 @@ describe("FromSplit", () => { }); test("split file with header", () => { + const fs = require("node:fs") as typeof import("node:fs"); const collector = new CollectorReceiver(); const op = new FromSplit(collector); op.init(["--header", "tests/fixtures/splitfile"]); + for (const file of op.extraArgs) { + const content = fs.readFileSync(file, "utf-8"); + for (const line of content.split("\n")) { + if (line === "") continue; + op.acceptLine(line); + } + } op.finish(); const result = collector.records.map((r) => r.toJSON()); expect(result).toEqual([ From 8ae89f1d83c706bd0b5cc9af550e53b22d8c6adb Mon Sep 17 00:00:00 2001 From: Ben Bernard Date: Tue, 24 Feb 2026 17:12:42 -0800 Subject: [PATCH 3/3] feat: add PostgreSQL, MySQL, and Oracle support to fromdb Add --type pg/mysql/oracle with dynamic imports for pg, mysql2, and oracledb packages. PostgreSQL uses Unix socket by default when --host is omitted (matching Perl PR #84). Add live integration tests for pg and mysql that auto-skip when databases are unavailable (CI-safe). Co-Authored-By: Claude Opus 4.6 --- bun.lock | 237 ++++++++++++++++++-- package.json | 5 + src/operations/input/fromdb.ts | 304 +++++++++++++++++++++++++- tests/operations/input/fromdb.test.ts | 191 ++++++++++++++++ 4 files changed, 711 insertions(+), 26 deletions(-) diff --git a/bun.lock b/bun.lock index 683ad0a..914a319 100644 --- a/bun.lock +++ b/bun.lock @@ -7,23 +7,28 @@ "dependencies": { "@types/react": "^19.2.14", "better-sqlite3": "^12.6.2", + "exceljs": "^4.4.0", "fast-xml-parser": "^5.3.7", "ink": "^6.8.0", "ink-select-input": "^6.2.0", "ink-spinner": "^5.0.0", "ink-text-input": "^6.0.0", "mongodb": "^7.1.0", + "mysql2": "^3.18.0", "nanoid": "^5.1.6", "openai": "^6.22.0", + "oracledb": "^6.10.0", "papaparse": "^5.5.3", + "pg": "^8.18.0", "react": "^19.2.4", "string-width": "^8.2.0", - "xlsx": "^0.18.5", }, "devDependencies": { "@types/better-sqlite3": "^7.6.13", "@types/bun": "latest", + "@types/oracledb": "^6.10.1", "@types/papaparse": "^5.5.2", + "@types/pg": "^8.16.0", "lefthook": "^2.1.1", "oxlint": "latest", "typescript": "^5", @@ -130,6 +135,10 @@ "@esbuild/win32-x64": ["@esbuild/win32-x64@0.21.5", "", { "os": "win32", "cpu": "x64" }, "sha512-tQd/1efJuzPC6rCFwEvLtci/xNFcTZknmXs98FYDfGE4wP9ClFV98nyKrzJKVPMhdDnjzLhdUyMX4PsQAPjwIw=="], + "@fast-csv/format": ["@fast-csv/format@4.3.5", "", { "dependencies": { "@types/node": "^14.0.1", "lodash.escaperegexp": "^4.1.2", "lodash.isboolean": "^3.0.3", "lodash.isequal": "^4.5.0", "lodash.isfunction": "^3.0.9", "lodash.isnil": "^4.0.0" } }, "sha512-8iRn6QF3I8Ak78lNAa+Gdl5MJJBM5vRHivFtMRUWINdevNo00K7OXxS2PshawLKTejVwieIlPmK5YlLu6w4u8A=="], + + "@fast-csv/parse": ["@fast-csv/parse@4.3.6", "", { "dependencies": { "@types/node": "^14.0.1", "lodash.escaperegexp": "^4.1.2", "lodash.groupby": "^4.6.0", "lodash.isfunction": "^3.0.9", "lodash.isnil": "^4.0.0", "lodash.isundefined": "^3.0.1", "lodash.uniq": "^4.5.0" } }, "sha512-uRsLYksqpbDmWaSmzvJcuApSEe38+6NQZBUsuAyMZKqHxH0g1wcJgsKUvN3WC8tewaqFjBMMGrkHmC+T7k8LvA=="], + "@iconify-json/simple-icons": ["@iconify-json/simple-icons@1.2.71", "", { "dependencies": { "@iconify/types": "*" } }, "sha512-rNoDFbq1fAYiEexBvrw613/xiUOPEu5MKVV/X8lI64AgdTzLQUUemr9f9fplxUMPoxCBP2rWzlhOEeTHk/Sf0Q=="], "@iconify/types": ["@iconify/types@2.0.0", "", {}, "sha512-+wluvCrRhXrhyOmRDJ3q8mux9JkKy5SJ/v8ol2tu4FVjyYvtEzkc/3pK15ET6RKg4b4w4BmTk1+gsCUhf21Ykg=="], @@ -260,8 +269,12 @@ "@types/node": ["@types/node@25.3.0", "", { "dependencies": { "undici-types": "~7.18.0" } }, "sha512-4K3bqJpXpqfg2XKGK9bpDTc6xO/xoUP/RBWS7AtRMug6zZFaRekiLzjVtAoZMquxoAbzBvy5nxQ7veS5eYzf8A=="], + "@types/oracledb": ["@types/oracledb@6.10.1", "", { "dependencies": { "@types/node": "*" } }, "sha512-/P478CfP8rYvXi6897OLn/BgXOP/RnDcKYCX3JRRNSZ/J94gmKJAT1vWiLA+HDRopBmqe0pzzud6hKhKQguJcw=="], + "@types/papaparse": ["@types/papaparse@5.5.2", "", { "dependencies": { "@types/node": "*" } }, "sha512-gFnFp/JMzLHCwRf7tQHrNnfhN4eYBVYYI897CGX4MY1tzY9l2aLkVyx2IlKZ/SAqDbB3I1AOZW5gTMGGsqWliA=="], + "@types/pg": ["@types/pg@8.16.0", "", { "dependencies": { "@types/node": "*", "pg-protocol": "*", "pg-types": "^2.2.0" } }, "sha512-RmhMd/wD+CF8Dfo+cVIy3RR5cl8CyfXQ0tGgW6XBL8L4LM/UTEbNXYRbLwU6w+CgrKBNbrQWt4FUtTfaU5jSYQ=="], + "@types/react": ["@types/react@19.2.14", "", { "dependencies": { "csstype": "^3.2.2" } }, "sha512-ilcTH/UniCkMdtexkoCN0bI7pMcJDvmQFPvuPvmEaYA/NSfFTAgdUSLAoVjaRJm7+6PvcM+q1zYOwS4wTYMF9w=="], "@types/unist": ["@types/unist@3.0.3", "", {}, "sha512-ko/gIFJRv177XgZsZcBwnqJN5x/Gien8qNOn0D5bQU/zAzVf9Zt3BlcUiLqhV9y4ARk0GbT3tnUiPNgnTXzc/Q=="], @@ -308,8 +321,6 @@ "@vueuse/shared": ["@vueuse/shared@12.8.2", "", { "dependencies": { "vue": "^3.5.13" } }, "sha512-dznP38YzxZoNloI0qpEfpkms8knDtaoQ6Y/sfS0L7Yki4zh40LFHEhur0odJC6xTHG5dxWVPiUWBXn+wCG2s5w=="], - "adler-32": ["adler-32@1.3.1", "", {}, "sha512-ynZ4w/nUUv5rrsR8UUGoe1VC9hZj6V5hU9Qw1HlMDJGEJw5S7TfTErWTjMys6M7vr0YWcPqs3qAr4ss0nDfP+A=="], - "algoliasearch": ["algoliasearch@5.49.0", "", { "dependencies": { "@algolia/abtesting": "1.15.0", "@algolia/client-abtesting": "5.49.0", "@algolia/client-analytics": "5.49.0", "@algolia/client-common": "5.49.0", "@algolia/client-insights": "5.49.0", "@algolia/client-personalization": "5.49.0", "@algolia/client-query-suggestions": "5.49.0", "@algolia/client-search": "5.49.0", "@algolia/ingestion": "1.49.0", "@algolia/monitoring": "1.49.0", "@algolia/recommend": "5.49.0", "@algolia/requester-browser-xhr": "5.49.0", "@algolia/requester-fetch": "5.49.0", "@algolia/requester-node-http": "5.49.0" } }, "sha512-Tse7vx7WOvbU+kpq/L3BrBhSWTPbtMa59zIEhMn+Z2NoxZlpcCRUDCRxQ7kDFs1T3CHxDgvb+mDuILiBBpBaAA=="], "ansi-escapes": ["ansi-escapes@7.3.0", "", { "dependencies": { "environment": "^1.0.0" } }, "sha512-BvU8nYgGQBxcmMuEeUEmNTvrMVjJNSH7RgW24vXexN4Ven6qCvy4TntnvlnwnMLTVlcRQQdbRY8NKnaIoeWDNg=="], @@ -318,27 +329,51 @@ "ansi-styles": ["ansi-styles@6.2.3", "", {}, "sha512-4Dj6M28JB+oAH8kFkTLUo+a2jwOFkuqb3yucU0CANcRRUbxS0cP0nZYCGjcc3BNXwRIsUVmDGgzawme7zvJHvg=="], + "archiver": ["archiver@5.3.2", "", { "dependencies": { "archiver-utils": "^2.1.0", "async": "^3.2.4", "buffer-crc32": "^0.2.1", "readable-stream": "^3.6.0", "readdir-glob": "^1.1.2", "tar-stream": "^2.2.0", "zip-stream": "^4.1.0" } }, "sha512-+25nxyyznAXF7Nef3y0EbBeqmGZgeN/BxHX29Rs39djAfaFalmQ89SE6CWyDCHzGL0yt/ycBtNOmGTW0FyGWNw=="], + + "archiver-utils": ["archiver-utils@2.1.0", "", { "dependencies": { "glob": "^7.1.4", "graceful-fs": "^4.2.0", "lazystream": "^1.0.0", "lodash.defaults": "^4.2.0", "lodash.difference": "^4.5.0", "lodash.flatten": "^4.4.0", "lodash.isplainobject": "^4.0.6", "lodash.union": "^4.6.0", "normalize-path": "^3.0.0", "readable-stream": "^2.0.0" } }, "sha512-bEL/yUb/fNNiNTuUz979Z0Yg5L+LzLxGJz8x79lYmR54fmTIb6ob/hNQgkQnIUDWIFjZVQwl9Xs356I6BAMHfw=="], + + "async": ["async@3.2.6", "", {}, "sha512-htCUDlxyyCLMgaM3xXg0C0LW2xqfuQ6p05pCEIsXuyQ+a1koYKTuBMzRNwmybfLgvJDMd0r1LTn4+E0Ti6C2AA=="], + "auto-bind": ["auto-bind@5.0.1", "", {}, "sha512-ooviqdwwgfIfNmDwo94wlshcdzfO64XV0Cg6oDsDYBJfITDz1EngD2z7DkbvCWn+XIMsIqW27sEVF6qcpJrRcg=="], + "aws-ssl-profiles": ["aws-ssl-profiles@1.1.2", "", {}, "sha512-NZKeq9AfyQvEeNlN0zSYAaWrmBffJh3IELMZfRpJVWgrpEbtEpnjvzqBPf+mxoI287JohRDoa+/nsfqqiZmF6g=="], + + "balanced-match": ["balanced-match@1.0.2", "", {}, "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw=="], + "base64-js": ["base64-js@1.5.1", "", {}, "sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA=="], "better-sqlite3": ["better-sqlite3@12.6.2", "", { "dependencies": { "bindings": "^1.5.0", "prebuild-install": "^7.1.1" } }, "sha512-8VYKM3MjCa9WcaSAI3hzwhmyHVlH8tiGFwf0RlTsZPWJ1I5MkzjiudCo4KC4DxOaL/53A5B1sI/IbldNFDbsKA=="], + "big-integer": ["big-integer@1.6.52", "", {}, "sha512-QxD8cf2eVqJOOz63z6JIN9BzvVs/dlySa5HGSBH5xtR8dPteIRQnBxxKqkNTiT6jbDTF6jAfrd4oMcND9RGbQg=="], + + "binary": ["binary@0.3.0", "", { "dependencies": { "buffers": "~0.1.1", "chainsaw": "~0.1.0" } }, "sha512-D4H1y5KYwpJgK8wk1Cue5LLPgmwHKYSChkbspQg5JtVuR5ulGckxfR62H3AE9UDkdMC8yyXlqYihuz3Aqg2XZg=="], + "bindings": ["bindings@1.5.0", "", { "dependencies": { "file-uri-to-path": "1.0.0" } }, "sha512-p2q/t/mhvuOj/UeLlV6566GD/guowlr0hHxClI0W9m7MWYkL1F0hLo+0Aexs9HSPCtR1SXQ0TD3MMKrXZajbiQ=="], "birpc": ["birpc@2.9.0", "", {}, "sha512-KrayHS5pBi69Xi9JmvoqrIgYGDkD6mcSe/i6YKi3w5kekCLzrX4+nawcXqrj2tIp50Kw/mT/s3p+GVK0A0sKxw=="], "bl": ["bl@4.1.0", "", { "dependencies": { "buffer": "^5.5.0", "inherits": "^2.0.4", "readable-stream": "^3.4.0" } }, "sha512-1W07cM9gS6DcLperZfFSj+bWLtaPGSOHWhPiGzXmvVJbRLdG82sH/Kn8EtW1VqWVA54AKf2h5k5BbnIbwF3h6w=="], + "bluebird": ["bluebird@3.4.7", "", {}, "sha512-iD3898SR7sWVRHbiQv+sHUtHnMvC1o3nW5rAcqnq3uOn07DSAppZYUkIGslDz6gXC7HfunPe7YVBgoEJASPcHA=="], + + "brace-expansion": ["brace-expansion@2.0.2", "", { "dependencies": { "balanced-match": "^1.0.0" } }, "sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ=="], + "bson": ["bson@7.2.0", "", {}, "sha512-YCEo7KjMlbNlyHhz7zAZNDpIpQbd+wOEHJYezv0nMYTn4x31eIUM2yomNNubclAt63dObUzKHWsBLJ9QcZNSnQ=="], "buffer": ["buffer@5.7.1", "", { "dependencies": { "base64-js": "^1.3.1", "ieee754": "^1.1.13" } }, "sha512-EHcyIPBQ4BSGlvjB16k5KgAJ27CIsHY/2JBmCRReo48y9rQ3MaUzWX3KVlBa4U7MyX02HdVj0K7C3WaB3ju7FQ=="], + "buffer-crc32": ["buffer-crc32@0.2.13", "", {}, "sha512-VO9Ht/+p3SN7SKWqcrgEzjGbRSJYTx+Q1pTQC0wrWqHx0vpJraQ6GtHx8tvcg1rlK1byhU5gccxgOgj7B0TDkQ=="], + + "buffer-indexof-polyfill": ["buffer-indexof-polyfill@1.0.2", "", {}, "sha512-I7wzHwA3t1/lwXQh+A5PbNvJxgfo5r3xulgpYDB5zckTu/Z9oUK9biouBKQUjEqzaz3HnAT6TYoovmE+GqSf7A=="], + + "buffers": ["buffers@0.1.1", "", {}, "sha512-9q/rDEGSb/Qsvv2qvzIzdluL5k7AaJOTrw23z9reQthrbF7is4CtlT0DXyO1oei2DCp4uojjzQ7igaSHp1kAEQ=="], + "bun-types": ["bun-types@1.3.9", "", { "dependencies": { "@types/node": "*" } }, "sha512-+UBWWOakIP4Tswh0Bt0QD0alpTY8cb5hvgiYeWCMet9YukHbzuruIEeXC2D7nMJPB12kbh8C7XJykSexEqGKJg=="], "ccount": ["ccount@2.0.1", "", {}, "sha512-eyrF0jiFpY+3drT6383f1qhkbGsLSifNAjA61IUjZjmLCWjItY6LB9ft9YhoDgwfmclB2zhu51Lc7+95b8NRAg=="], - "cfb": ["cfb@1.2.2", "", { "dependencies": { "adler-32": "~1.3.0", "crc-32": "~1.2.0" } }, "sha512-KfdUZsSOw19/ObEWasvBP/Ac4reZvAGauZhs6S/gqNhXhI7cKwvlH7ulj+dOEYnca4bm4SGo8C1bTAQvnTjgQA=="], + "chainsaw": ["chainsaw@0.1.0", "", { "dependencies": { "traverse": ">=0.3.0 <0.4" } }, "sha512-75kWfWt6MEKNC8xYXIdRpDehRYY/tNSgwKaJq+dbbDcxORuVrrQ+SEHoWsniVn9XPYfP4gmdWIeDk/4YNp1rNQ=="], "chalk": ["chalk@5.6.2", "", {}, "sha512-7NzBL0rN6fMUW+f7A6Io4h40qQlG+xGmtMxfbnH/K7TAtt8JQWVQK+6g0UXKMeVJoyV5EkkNsErQ8pVD3bLHbA=="], @@ -358,28 +393,40 @@ "code-excerpt": ["code-excerpt@4.0.0", "", { "dependencies": { "convert-to-spaces": "^2.0.1" } }, "sha512-xxodCmBen3iy2i0WtAK8FlFNrRzjUqjRsMfho58xT/wvZU1YTM3fCnRjcy1gJPMepaRlgm/0e6w8SpWHpn3/cA=="], - "codepage": ["codepage@1.15.0", "", {}, "sha512-3g6NUTPd/YtuuGrhMnOMRjFc+LJw/bnMp3+0r/Wcz3IXUuCosKRJvMphm5+Q+bvTVGcJJuRvVLuYba+WojaFaA=="], - "comma-separated-tokens": ["comma-separated-tokens@2.0.3", "", {}, "sha512-Fu4hJdvzeylCfQPp9SGWidpzrMs7tTrlu6Vb8XGaRGck8QSNZJJp538Wrb60Lax4fPwR64ViY468OIUTbRlGZg=="], + "compress-commons": ["compress-commons@4.1.2", "", { "dependencies": { "buffer-crc32": "^0.2.13", "crc32-stream": "^4.0.2", "normalize-path": "^3.0.0", "readable-stream": "^3.6.0" } }, "sha512-D3uMHtGc/fcO1Gt1/L7i1e33VOvD4A9hfQLP+6ewd+BvG/gQ84Yh4oftEhAdjSMgBgwGL+jsppT7JYNpo6MHHg=="], + + "concat-map": ["concat-map@0.0.1", "", {}, "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg=="], + "convert-to-spaces": ["convert-to-spaces@2.0.1", "", {}, "sha512-rcQ1bsQO9799wq24uE5AM2tAILy4gXGIK/njFWcVQkGNZ96edlpY+A7bjwvzjYvLDyzmG1MmMLZhpcsb+klNMQ=="], "copy-anything": ["copy-anything@4.0.5", "", { "dependencies": { "is-what": "^5.2.0" } }, "sha512-7Vv6asjS4gMOuILabD3l739tsaxFQmC+a7pLZm02zyvs8p977bL3zEgq3yDk5rn9B0PbYgIv++jmHcuUab4RhA=="], + "core-util-is": ["core-util-is@1.0.3", "", {}, "sha512-ZQBvi1DcpJ4GDqanjucZ2Hj3wEO5pZDS89BWbkcrvdxksJorwUDDZamX9ldFkp9aw2lmBDLgkObEA4DWNJ9FYQ=="], + "crc-32": ["crc-32@1.2.2", "", { "bin": { "crc32": "bin/crc32.njs" } }, "sha512-ROmzCKrTnOwybPcJApAA6WBWij23HVfGVNKqqrZpuyZOHqK2CwHSvpGuyt/UNNvaIjEd8X5IFGp4Mh+Ie1IHJQ=="], + "crc32-stream": ["crc32-stream@4.0.3", "", { "dependencies": { "crc-32": "^1.2.0", "readable-stream": "^3.4.0" } }, "sha512-NT7w2JVU7DFroFdYkeq8cywxrgjPHWkdX1wjpRQXPX5Asews3tA+Ght6lddQO5Mkumffp3X7GEqku3epj2toIw=="], + "csstype": ["csstype@3.2.3", "", {}, "sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ=="], + "dayjs": ["dayjs@1.11.19", "", {}, "sha512-t5EcLVS6QPBNqM2z8fakk/NKel+Xzshgt8FFKAn+qwlD1pzZWxh0nVCrvFK7ZDb6XucZeF9z8C7CBWTRIVApAw=="], + "decompress-response": ["decompress-response@6.0.0", "", { "dependencies": { "mimic-response": "^3.1.0" } }, "sha512-aW35yZM6Bb/4oJlZncMH2LCoZtJXTRxES17vE3hoRiowU2kWHaJKFkSBDnDR+cm9J+9QhXmREyIfv0pji9ejCQ=="], "deep-extend": ["deep-extend@0.6.0", "", {}, "sha512-LOHxIOaPYdHlJRtCQfDIVZtfw/ufM8+rVj649RIHzcm/vGwQRXFt6OPqIFWsm2XEMrNIEtWR64sY1LEKD2vAOA=="], + "denque": ["denque@2.1.0", "", {}, "sha512-HVQE3AAb/pxF8fQAoiqpvg9i3evqug3hoiwakOyZAwJm+6vZehbkYXZ0l4JxS+I3QxM97v5aaRNhj8v5oBhekw=="], + "dequal": ["dequal@2.0.3", "", {}, "sha512-0je+qPKHEMohvfRTCEo3CrPG6cAzAYgmzKyxRiYSSDkS6eGJdyVJm7WaYA5ECaAD9wLB2T4EEeymA5aFVcYXCA=="], "detect-libc": ["detect-libc@2.1.2", "", {}, "sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ=="], "devlop": ["devlop@1.1.0", "", { "dependencies": { "dequal": "^2.0.0" } }, "sha512-RWmIqhcFf1lRYBvNmr7qTNuyCt/7/ns2jbpp1+PalgE/rDQcBT0fioSMUpJ93irlUhC5hrg4cYqe6U+0ImW0rA=="], + "duplexer2": ["duplexer2@0.1.4", "", { "dependencies": { "readable-stream": "^2.0.2" } }, "sha512-asLFVfWWtJ90ZyOUHMqk7/S2w2guQKxUI2itj3d92ADHhxUSbCMGi1f1cBcJ7xM1To+pE/Khbwo1yuNbMEPKeA=="], + "emoji-regex": ["emoji-regex@10.6.0", "", {}, "sha512-toUI84YS5YmxW219erniWD0CIVOo46xGKColeNQRgOzDorgBi1v4D71/OFzgD9GO2UGKIv1C3Sp8DAn0+j5w7A=="], "emoji-regex-xs": ["emoji-regex-xs@1.0.0", "", {}, "sha512-LRlerrMYoIDrT6jgpeZ2YYl/L8EulRTt5hQcYjy5AInh7HWXKimpqx68aknBFpGL2+/IcogTcaydJEgaTmOpDg=="], @@ -398,8 +445,12 @@ "estree-walker": ["estree-walker@2.0.2", "", {}, "sha512-Rfkk/Mp/DL7JVje3u18FxFujQlTNR2q6QfMSMB7AvCBx91NGj/ba3kCfza0f6dVDbw7YlRf/nDrn7pQrCCyQ/w=="], + "exceljs": ["exceljs@4.4.0", "", { "dependencies": { "archiver": "^5.0.0", "dayjs": "^1.8.34", "fast-csv": "^4.3.1", "jszip": "^3.10.1", "readable-stream": "^3.6.0", "saxes": "^5.0.1", "tmp": "^0.2.0", "unzipper": "^0.10.11", "uuid": "^8.3.0" } }, "sha512-XctvKaEMaj1Ii9oDOqbW/6e1gXknSY4g/aLCDicOXqBE4M0nRWkUu0PTp++UPNzoFY12BNHMfs/VadKIS6llvg=="], + "expand-template": ["expand-template@2.0.3", "", {}, "sha512-XYfuKMvj4O35f/pOXLObndIRvyQ+/+6AhODh+OKWj9S9498pHHn/IMszH+gt0fBCRWMNfk1ZSp5x3AifmnI2vg=="], + "fast-csv": ["fast-csv@4.3.6", "", { "dependencies": { "@fast-csv/format": "4.3.5", "@fast-csv/parse": "4.3.6" } }, "sha512-2RNSpuwwsJGP0frGsOmTb9oUF+VkFSM4SyLTDgwf2ciHWTarN0lQTC+F2f/t5J9QjW+c65VFIAAu85GsvMIusw=="], + "fast-xml-parser": ["fast-xml-parser@5.3.7", "", { "dependencies": { "strnum": "^2.1.2" }, "bin": { "fxparser": "src/cli/cli.js" } }, "sha512-JzVLro9NQv92pOM/jTCR6mHlJh2FGwtomH8ZQjhFj/R29P2Fnj38OgPJVtcvYw6SuKClhgYuwUZf5b3rd8u2mA=="], "figures": ["figures@6.1.0", "", { "dependencies": { "is-unicode-supported": "^2.0.0" } }, "sha512-d+l3qxjSesT4V7v2fh+QnmFnUWv9lSpjarhShNTgBOfA0ttejbQUAlHLitbjkoRiDulW0OPoQPYIGhIC8ohejg=="], @@ -408,16 +459,24 @@ "focus-trap": ["focus-trap@7.8.0", "", { "dependencies": { "tabbable": "^6.4.0" } }, "sha512-/yNdlIkpWbM0ptxno3ONTuf+2g318kh2ez3KSeZN5dZ8YC6AAmgeWz+GasYYiBJPFaYcSAPeu4GfhUaChzIJXA=="], - "frac": ["frac@1.1.2", "", {}, "sha512-w/XBfkibaTl3YDqASwfDUqkna4Z2p9cFSr1aHDt0WoMTECnRfBOv2WArlZILlqgWlmdIlALXGpM2AOhEk5W3IA=="], - "fs-constants": ["fs-constants@1.0.0", "", {}, "sha512-y6OAwoSIf7FyjMIv94u+b5rdheZEjzR63GTyZJm5qh4Bi+2YgwLCcI/fPFZkL5PSixOt6ZNKm+w+Hfp/Bciwow=="], + "fs.realpath": ["fs.realpath@1.0.0", "", {}, "sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw=="], + "fsevents": ["fsevents@2.3.3", "", { "os": "darwin" }, "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw=="], + "fstream": ["fstream@1.0.12", "", { "dependencies": { "graceful-fs": "^4.1.2", "inherits": "~2.0.0", "mkdirp": ">=0.5 0", "rimraf": "2" } }, "sha512-WvJ193OHa0GHPEL+AycEJgxvBEwyfRkN1vhjca23OaPVMCaLCXTd5qAu82AjTcgP1UJmytkOKb63Ypde7raDIg=="], + + "generate-function": ["generate-function@2.3.1", "", { "dependencies": { "is-property": "^1.0.2" } }, "sha512-eeB5GfMNeevm/GRYq20ShmsaGcmI81kIX2K9XQx5miC8KdHaC6Jm0qQ8ZNeGOi7wYB8OsdxKs+Y2oVuTFuVwKQ=="], + "get-east-asian-width": ["get-east-asian-width@1.5.0", "", {}, "sha512-CQ+bEO+Tva/qlmw24dCejulK5pMzVnUOFOijVogd3KQs07HnRIgp8TGipvCCRT06xeYEbpbgwaCxglFyiuIcmA=="], "github-from-package": ["github-from-package@0.0.0", "", {}, "sha512-SyHy3T1v2NUXn29OsWdxmK6RwHD+vkj3v8en8AOBZ1wBQ/hCAQ5bAQTD02kW4W9tUp/3Qh6J8r9EvntiyCmOOw=="], + "glob": ["glob@7.2.3", "", { "dependencies": { "fs.realpath": "^1.0.0", "inflight": "^1.0.4", "inherits": "2", "minimatch": "^3.1.1", "once": "^1.3.0", "path-is-absolute": "^1.0.0" } }, "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q=="], + + "graceful-fs": ["graceful-fs@4.2.11", "", {}, "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ=="], + "hast-util-to-html": ["hast-util-to-html@9.0.5", "", { "dependencies": { "@types/hast": "^3.0.0", "@types/unist": "^3.0.0", "ccount": "^2.0.0", "comma-separated-tokens": "^2.0.0", "hast-util-whitespace": "^3.0.0", "html-void-elements": "^3.0.0", "mdast-util-to-hast": "^13.0.0", "property-information": "^7.0.0", "space-separated-tokens": "^2.0.0", "stringify-entities": "^4.0.0", "zwitch": "^2.0.4" } }, "sha512-OguPdidb+fbHQSU4Q4ZiLKnzWo8Wwsf5bZfbvu7//a9oTYoqD/fWpe96NuHkoS9h0ccGOTe0C4NGXdtS0iObOw=="], "hast-util-whitespace": ["hast-util-whitespace@3.0.0", "", { "dependencies": { "@types/hast": "^3.0.0" } }, "sha512-88JUN06ipLwsnv+dVn+OIYOvAuvBMy/Qoi6O7mQHxdPXpjy+Cd6xRkWwux7DKO+4sYILtLBRIKgsdpS2gQc7qw=="], @@ -426,10 +485,16 @@ "html-void-elements": ["html-void-elements@3.0.0", "", {}, "sha512-bEqo66MRXsUGxWHV5IP0PUiAWwoEjba4VCzg0LjFJBpchPaTfyfCKTG6bc5F8ucKec3q5y6qOdGyYTSBEvhCrg=="], + "iconv-lite": ["iconv-lite@0.7.2", "", { "dependencies": { "safer-buffer": ">= 2.1.2 < 3.0.0" } }, "sha512-im9DjEDQ55s9fL4EYzOAv0yMqmMBSZp6G0VvFyTMPKWxiSBHUj9NW/qqLmXUwXrrM7AvqSlTCfvqRb0cM8yYqw=="], + "ieee754": ["ieee754@1.2.1", "", {}, "sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA=="], + "immediate": ["immediate@3.0.6", "", {}, "sha512-XXOFtyqDjNDAQxVfYxuF7g9Il/IbWmmlQg2MYKOH8ExIT1qg6xc4zyS3HaEEATgs1btfzxq15ciUiY7gjSXRGQ=="], + "indent-string": ["indent-string@5.0.0", "", {}, "sha512-m6FAo/spmsW2Ab2fU35JTYwtOKa2yAwXSwgjSv1TJzh4Mh7mC3lzAOVLBprb72XsTrgkEIsl7YrFNAiDiRhIGg=="], + "inflight": ["inflight@1.0.6", "", { "dependencies": { "once": "^1.3.0", "wrappy": "1" } }, "sha512-k92I/b08q4wvFscXCLvqfsHCrjrF7yiXsQuIVvVE7N82W3+aqpzuUdBbfhWcy/FZR3/4IgflMgKLOsvPDrGCJA=="], + "inherits": ["inherits@2.0.4", "", {}, "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ=="], "ini": ["ini@1.3.8", "", {}, "sha512-JV/yugV2uzW5iMRSiZAyDtQd+nxtUnjeLt0acNdw98kKLrvuRVyB80tsREOE7yvGVgalhZ6RNXCmEHkUKBKxew=="], @@ -446,10 +511,18 @@ "is-in-ci": ["is-in-ci@2.0.0", "", { "bin": { "is-in-ci": "cli.js" } }, "sha512-cFeerHriAnhrQSbpAxL37W1wcJKUUX07HyLWZCW1URJT/ra3GyUTzBgUnh24TMVfNTV2Hij2HLxkPHFZfOZy5w=="], + "is-property": ["is-property@1.0.2", "", {}, "sha512-Ks/IoX00TtClbGQr4TWXemAnktAQvYB7HzcCxDGqEZU6oCmb2INHuOoKxbtR+HFkmYWBKv/dOZtGRiAjDhj92g=="], + "is-unicode-supported": ["is-unicode-supported@2.1.0", "", {}, "sha512-mE00Gnza5EEB3Ds0HfMyllZzbBrmLOX3vfWoj9A9PEnTfratQ/BcaJOuMhnkhjXvb2+FkY3VuHqtAGpTPmglFQ=="], "is-what": ["is-what@5.5.0", "", {}, "sha512-oG7cgbmg5kLYae2N5IVd3jm2s+vldjxJzK1pcu9LfpGuQ93MQSzo0okvRna+7y5ifrD+20FE8FvjusyGaz14fw=="], + "isarray": ["isarray@1.0.0", "", {}, "sha512-VLghIWNM6ELQzo7zwmcg0NmTVyWKYjvIeM83yjp0wRDTmUnrM678fQbcKBo6n2CJEF0szoG//ytg+TKla89ALQ=="], + + "jszip": ["jszip@3.10.1", "", { "dependencies": { "lie": "~3.3.0", "pako": "~1.0.2", "readable-stream": "~2.3.6", "setimmediate": "^1.0.5" } }, "sha512-xXDvecyTpGLrqFrvkrUSoxxfJI5AH7U8zxxtVclpsUtMCq4JQ290LY8AW5c7Ggnr/Y/oK+bQMbqK2qmtk3pN4g=="], + + "lazystream": ["lazystream@1.0.1", "", { "dependencies": { "readable-stream": "^2.0.5" } }, "sha512-b94GiNHQNy6JNTrt5w6zNyffMrNkXZb3KTkCZJb2V1xaEGCk093vkZ2jk3tpaeP33/OiXC+WvK9AxUebnf5nbw=="], + "lefthook": ["lefthook@2.1.1", "", { "optionalDependencies": { "lefthook-darwin-arm64": "2.1.1", "lefthook-darwin-x64": "2.1.1", "lefthook-freebsd-arm64": "2.1.1", "lefthook-freebsd-x64": "2.1.1", "lefthook-linux-arm64": "2.1.1", "lefthook-linux-x64": "2.1.1", "lefthook-openbsd-arm64": "2.1.1", "lefthook-openbsd-x64": "2.1.1", "lefthook-windows-arm64": "2.1.1", "lefthook-windows-x64": "2.1.1" }, "bin": { "lefthook": "bin/index.js" } }, "sha512-Tl9h9c+sG3ShzTHKuR3LAIblnnh+Mgxnm2Ul7yu9cu260Z27LEbO3V6Zw4YZFP59/2rlD42pt/llYsQCkkCFzw=="], "lefthook-darwin-arm64": ["lefthook-darwin-arm64@2.1.1", "", { "os": "darwin", "cpu": "arm64" }, "sha512-O/RS1j03/Fnq5zCzEb2r7UOBsqPeBuf1C5pMkIJcO4TSE6hf3rhLUkcorKc2M5ni/n5zLGtzQUXHV08/fSAT3Q=="], @@ -472,6 +545,40 @@ "lefthook-windows-x64": ["lefthook-windows-x64@2.1.1", "", { "os": "win32", "cpu": "x64" }, "sha512-1L2oGIzmhfOTxfwbe5mpSQ+m3ilpvGNymwIhn4UHq6hwHsUL6HEhODqx02GfBn6OXpVIr56bvdBAusjL/SVYGQ=="], + "lie": ["lie@3.3.0", "", { "dependencies": { "immediate": "~3.0.5" } }, "sha512-UaiMJzeWRlEujzAuw5LokY1L5ecNQYZKfmyZ9L7wDHb/p5etKaxXhohBcrw0EYby+G/NA52vRSN4N39dxHAIwQ=="], + + "listenercount": ["listenercount@1.0.1", "", {}, "sha512-3mk/Zag0+IJxeDrxSgaDPy4zZ3w05PRZeJNnlWhzFz5OkX49J4krc+A8X2d2M69vGMBEX0uyl8M+W+8gH+kBqQ=="], + + "lodash.defaults": ["lodash.defaults@4.2.0", "", {}, "sha512-qjxPLHd3r5DnsdGacqOMU6pb/avJzdh9tFX2ymgoZE27BmjXrNy/y4LoaiTeAb+O3gL8AfpJGtqfX/ae2leYYQ=="], + + "lodash.difference": ["lodash.difference@4.5.0", "", {}, "sha512-dS2j+W26TQ7taQBGN8Lbbq04ssV3emRw4NY58WErlTO29pIqS0HmoT5aJ9+TUQ1N3G+JOZSji4eugsWwGp9yPA=="], + + "lodash.escaperegexp": ["lodash.escaperegexp@4.1.2", "", {}, "sha512-TM9YBvyC84ZxE3rgfefxUWiQKLilstD6k7PTGt6wfbtXF8ixIJLOL3VYyV/z+ZiPLsVxAsKAFVwWlWeb2Y8Yyw=="], + + "lodash.flatten": ["lodash.flatten@4.4.0", "", {}, "sha512-C5N2Z3DgnnKr0LOpv/hKCgKdb7ZZwafIrsesve6lmzvZIRZRGaZ/l6Q8+2W7NaT+ZwO3fFlSCzCzrDCFdJfZ4g=="], + + "lodash.groupby": ["lodash.groupby@4.6.0", "", {}, "sha512-5dcWxm23+VAoz+awKmBaiBvzox8+RqMgFhi7UvX9DHZr2HdxHXM/Wrf8cfKpsW37RNrvtPn6hSwNqurSILbmJw=="], + + "lodash.isboolean": ["lodash.isboolean@3.0.3", "", {}, "sha512-Bz5mupy2SVbPHURB98VAcw+aHh4vRV5IPNhILUCsOzRmsTmSQ17jIuqopAentWoehktxGd9e/hbIXq980/1QJg=="], + + "lodash.isequal": ["lodash.isequal@4.5.0", "", {}, "sha512-pDo3lu8Jhfjqls6GkMgpahsF9kCyayhgykjyLMNFTKWrpVdAQtYyB4muAMWozBB4ig/dtWAmsMxLEI8wuz+DYQ=="], + + "lodash.isfunction": ["lodash.isfunction@3.0.9", "", {}, "sha512-AirXNj15uRIMMPihnkInB4i3NHeb4iBtNg9WRWuK2o31S+ePwwNmDPaTL3o7dTJ+VXNZim7rFs4rxN4YU1oUJw=="], + + "lodash.isnil": ["lodash.isnil@4.0.0", "", {}, "sha512-up2Mzq3545mwVnMhTDMdfoG1OurpA/s5t88JmQX809eH3C8491iu2sfKhTfhQtKY78oPNhiaHJUpT/dUDAAtng=="], + + "lodash.isplainobject": ["lodash.isplainobject@4.0.6", "", {}, "sha512-oSXzaWypCMHkPC3NvBEaPHf0KsA5mvPrOPgQWDsbg8n7orZ290M0BmC/jgRZ4vcJ6DTAhjrsSYgdsW/F+MFOBA=="], + + "lodash.isundefined": ["lodash.isundefined@3.0.1", "", {}, "sha512-MXB1is3s899/cD8jheYYE2V9qTHwKvt+npCwpD+1Sxm3Q3cECXCiYHjeHWXNwr6Q0SOBPrYUDxendrO6goVTEA=="], + + "lodash.union": ["lodash.union@4.6.0", "", {}, "sha512-c4pB2CdGrGdjMKYLA+XiRDO7Y0PRQbm/Gzg8qMj+QH+pFVAoTp5sBpO0odL3FjoPCGjK96p6qsP+yQoiLoOBcw=="], + + "lodash.uniq": ["lodash.uniq@4.5.0", "", {}, "sha512-xfBaXQd9ryd9dlSDvnvI0lvxfLJlYAZzXomUYzLKtUeOQvOP5piqAWuGtrhWeqaXK9hhoM/iyJc5AV+XfsX3HQ=="], + + "long": ["long@5.3.2", "", {}, "sha512-mNAgZ1GmyNhD7AuqnTG3/VQ26o760+ZYBPKjPvugO8+nLbYfX6TVpJPseBvopbdY+qpZ/lKUnmEc1LeZYS3QAA=="], + + "lru.min": ["lru.min@1.1.4", "", {}, "sha512-DqC6n3QQ77zdFpCMASA1a3Jlb64Hv2N2DciFGkO/4L9+q/IpIAuRlKOvCXabtRW6cQf8usbmM6BE/TOPysCdIA=="], + "magic-string": ["magic-string@0.30.21", "", { "dependencies": { "@jridgewell/sourcemap-codec": "^1.5.5" } }, "sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ=="], "mark.js": ["mark.js@8.11.1", "", {}, "sha512-1I+1qpDt4idfgLQG+BNWmrqku+7/2bi5nLf4YwF8y8zXvmfiTBY3PV3ZibfrjBueCByROpuBjLLFCajqkgYoLQ=="], @@ -494,24 +601,34 @@ "mimic-response": ["mimic-response@3.1.0", "", {}, "sha512-z0yWI+4FDrrweS8Zmt4Ej5HdJmky15+L2e6Wgn3+iK5fWzb6T3fhNFq2+MeTRb064c6Wr4N/wv0DzQTjNzHNGQ=="], + "minimatch": ["minimatch@5.1.8", "", { "dependencies": { "brace-expansion": "^2.0.1" } }, "sha512-7RN35vit8DeBclkofOVmBY0eDAZZQd1HzmukRdSyz95CRh8FT54eqnbj0krQr3mrHR6sfRyYkyhwBWjoV5uqlQ=="], + "minimist": ["minimist@1.2.8", "", {}, "sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA=="], "minisearch": ["minisearch@7.2.0", "", {}, "sha512-dqT2XBYUOZOiC5t2HRnwADjhNS2cecp9u+TJRiJ1Qp/f5qjkeT5APcGPjHw+bz89Ms8Jp+cG4AlE+QZ/QnDglg=="], "mitt": ["mitt@3.0.1", "", {}, "sha512-vKivATfr97l2/QBCYAkXYDbrIWPM2IIKEl7YPhjCvKlG3kE2gm+uBo6nEXK3M5/Ffh/FLpKExzOQ3JJoJGFKBw=="], + "mkdirp": ["mkdirp@3.0.1", "", { "bin": { "mkdirp": "dist/cjs/src/bin.js" } }, "sha512-+NsyUUAZDmo6YVHzL/stxSu3t9YS1iljliy3BSDrXJ/dkn1KYdmtZODGGjLcc9XLgVVpH4KshHB8XmZgMhaBXg=="], + "mkdirp-classic": ["mkdirp-classic@0.5.3", "", {}, "sha512-gKLcREMhtuZRwRAfqP3RFW+TK4JqApVBtOIftVgjuABpAtpxhPGaDcfvbhNvD0B8iD1oUr/txX35NjcaY6Ns/A=="], "mongodb": ["mongodb@7.1.0", "", { "dependencies": { "@mongodb-js/saslprep": "^1.3.0", "bson": "^7.1.1", "mongodb-connection-string-url": "^7.0.0" }, "peerDependencies": { "@aws-sdk/credential-providers": "^3.806.0", "@mongodb-js/zstd": "^7.0.0", "gcp-metadata": "^7.0.1", "kerberos": "^7.0.0", "mongodb-client-encryption": ">=7.0.0 <7.1.0", "snappy": "^7.3.2", "socks": "^2.8.6" }, "optionalPeers": ["@aws-sdk/credential-providers", "@mongodb-js/zstd", "gcp-metadata", "kerberos", "mongodb-client-encryption", "snappy", "socks"] }, "sha512-kMfnKunbolQYwCIyrkxNJFB4Ypy91pYqua5NargS/f8ODNSJxT03ZU3n1JqL4mCzbSih8tvmMEMLpKTT7x5gCg=="], "mongodb-connection-string-url": ["mongodb-connection-string-url@7.0.1", "", { "dependencies": { "@types/whatwg-url": "^13.0.0", "whatwg-url": "^14.1.0" } }, "sha512-h0AZ9A7IDVwwHyMxmdMXKy+9oNlF0zFoahHiX3vQ8e3KFcSP3VmsmfvtRSuLPxmyv2vjIDxqty8smTgie/SNRQ=="], + "mysql2": ["mysql2@3.18.0", "", { "dependencies": { "aws-ssl-profiles": "^1.1.2", "denque": "^2.1.0", "generate-function": "^2.3.1", "iconv-lite": "^0.7.2", "long": "^5.3.2", "lru.min": "^1.1.4", "named-placeholders": "^1.1.6", "sql-escaper": "^1.3.3" }, "peerDependencies": { "@types/node": ">= 8" } }, "sha512-3rupyOFks7Vq0jcjBpmg1gtgfGuCcmgrRJPEfpGzzrB/ydutupbjKkoDJGsGkrJRU6j44o2tb0McduL03/v/dQ=="], + + "named-placeholders": ["named-placeholders@1.1.6", "", { "dependencies": { "lru.min": "^1.1.0" } }, "sha512-Tz09sEL2EEuv5fFowm419c1+a/jSMiBjI9gHxVLrVdbUkkNUUfjsVYs9pVZu5oCon/kmRh9TfLEObFtkVxmY0w=="], + "nanoid": ["nanoid@5.1.6", "", { "bin": { "nanoid": "bin/nanoid.js" } }, "sha512-c7+7RQ+dMB5dPwwCp4ee1/iV/q2P6aK1mTZcfr1BTuVlyW9hJYiMPybJCcnBlQtuSmTIWNeazm/zqNoZSSElBg=="], "napi-build-utils": ["napi-build-utils@2.0.0", "", {}, "sha512-GEbrYkbfF7MoNaoh2iGG84Mnf/WZfB0GdGEsM8wz7Expx/LlWf5U8t9nvJKXSp3qr5IsEbK04cBGhol/KwOsWA=="], "node-abi": ["node-abi@3.87.0", "", { "dependencies": { "semver": "^7.3.5" } }, "sha512-+CGM1L1CgmtheLcBuleyYOn7NWPVu0s0EJH2C4puxgEZb9h8QpR9G2dBfZJOAUhi7VQxuBPMd0hiISWcTyiYyQ=="], + "normalize-path": ["normalize-path@3.0.0", "", {}, "sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA=="], + "once": ["once@1.4.0", "", { "dependencies": { "wrappy": "1" } }, "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w=="], "onetime": ["onetime@5.1.2", "", { "dependencies": { "mimic-fn": "^2.1.0" } }, "sha512-kbpaSSGJTWdAY5KPVeMOKXSrPtr8C8C7wodJbcsd51jRnmD+GZu8Y0VoU6Dm5Z4vWr0Ig/1NKuWRKf7j5aaYSg=="], @@ -520,22 +637,54 @@ "openai": ["openai@6.22.0", "", { "peerDependencies": { "ws": "^8.18.0", "zod": "^3.25 || ^4.0" }, "optionalPeers": ["ws", "zod"], "bin": { "openai": "bin/cli" } }, "sha512-7Yvy17F33Bi9RutWbsaYt5hJEEJ/krRPOrwan+f9aCPuMat1WVsb2VNSII5W1EksKT6fF69TG/xj4XzodK3JZw=="], + "oracledb": ["oracledb@6.10.0", "", {}, "sha512-kGUumXmrEWbSpBuKJyb9Ip3rXcNgKK6grunI3/cLPzrRvboZ6ZoLi9JQ+z6M/RIG924tY8BLflihL4CKKQAYMA=="], + "oxlint": ["oxlint@1.49.0", "", { "optionalDependencies": { "@oxlint/binding-android-arm-eabi": "1.49.0", "@oxlint/binding-android-arm64": "1.49.0", "@oxlint/binding-darwin-arm64": "1.49.0", "@oxlint/binding-darwin-x64": "1.49.0", "@oxlint/binding-freebsd-x64": "1.49.0", "@oxlint/binding-linux-arm-gnueabihf": "1.49.0", "@oxlint/binding-linux-arm-musleabihf": "1.49.0", "@oxlint/binding-linux-arm64-gnu": "1.49.0", "@oxlint/binding-linux-arm64-musl": "1.49.0", "@oxlint/binding-linux-ppc64-gnu": "1.49.0", "@oxlint/binding-linux-riscv64-gnu": "1.49.0", "@oxlint/binding-linux-riscv64-musl": "1.49.0", "@oxlint/binding-linux-s390x-gnu": "1.49.0", "@oxlint/binding-linux-x64-gnu": "1.49.0", "@oxlint/binding-linux-x64-musl": "1.49.0", "@oxlint/binding-openharmony-arm64": "1.49.0", "@oxlint/binding-win32-arm64-msvc": "1.49.0", "@oxlint/binding-win32-ia32-msvc": "1.49.0", "@oxlint/binding-win32-x64-msvc": "1.49.0" }, "peerDependencies": { "oxlint-tsgolint": ">=0.14.1" }, "optionalPeers": ["oxlint-tsgolint"], "bin": { "oxlint": "bin/oxlint" } }, "sha512-YZffp0gM+63CJoRhHjtjRnwKtAgUnXM6j63YQ++aigji2NVvLGsUlrXo9gJUXZOdcbfShLYtA6RuTu8GZ4lzOQ=="], + "pako": ["pako@1.0.11", "", {}, "sha512-4hLB8Py4zZce5s4yd9XzopqwVv/yGNhV1Bl8NTmCq1763HeK2+EwVTv+leGeL13Dnh2wfbqowVPXCIO0z4taYw=="], + "papaparse": ["papaparse@5.5.3", "", {}, "sha512-5QvjGxYVjxO59MGU2lHVYpRWBBtKHnlIAcSe1uNFCkkptUh63NFRj0FJQm7nR67puEruUci/ZkjmEFrjCAyP4A=="], "patch-console": ["patch-console@2.0.0", "", {}, "sha512-0YNdUceMdaQwoKce1gatDScmMo5pu/tfABfnzEqeG0gtTmd7mh/WcwgUjtAeOU7N8nFFlbQBnFK2gXW5fGvmMA=="], + "path-is-absolute": ["path-is-absolute@1.0.1", "", {}, "sha512-AVbw3UJ2e9bq64vSaS9Am0fje1Pa8pbGqTTsmXfaIiMpnr5DlDhfJOuLj9Sf95ZPVDAUerDfEk88MPmPe7UCQg=="], + "perfect-debounce": ["perfect-debounce@1.0.0", "", {}, "sha512-xCy9V055GLEqoFaHoC1SoLIaLmWctgCUaBaWxDZ7/Zx4CTyX7cJQLJOok/orfjZAh9kEYpjJa4d0KcJmCbctZA=="], + "pg": ["pg@8.18.0", "", { "dependencies": { "pg-connection-string": "^2.11.0", "pg-pool": "^3.11.0", "pg-protocol": "^1.11.0", "pg-types": "2.2.0", "pgpass": "1.0.5" }, "optionalDependencies": { "pg-cloudflare": "^1.3.0" }, "peerDependencies": { "pg-native": ">=3.0.1" }, "optionalPeers": ["pg-native"] }, "sha512-xqrUDL1b9MbkydY/s+VZ6v+xiMUmOUk7SS9d/1kpyQxoJ6U9AO1oIJyUWVZojbfe5Cc/oluutcgFG4L9RDP1iQ=="], + + "pg-cloudflare": ["pg-cloudflare@1.3.0", "", {}, "sha512-6lswVVSztmHiRtD6I8hw4qP/nDm1EJbKMRhf3HCYaqud7frGysPv7FYJ5noZQdhQtN2xJnimfMtvQq21pdbzyQ=="], + + "pg-connection-string": ["pg-connection-string@2.11.0", "", {}, "sha512-kecgoJwhOpxYU21rZjULrmrBJ698U2RxXofKVzOn5UDj61BPj/qMb7diYUR1nLScCDbrztQFl1TaQZT0t1EtzQ=="], + + "pg-int8": ["pg-int8@1.0.1", "", {}, "sha512-WCtabS6t3c8SkpDBUlb1kjOs7l66xsGdKpIPZsg4wR+B3+u9UAum2odSsF9tnvxg80h4ZxLWMy4pRjOsFIqQpw=="], + + "pg-pool": ["pg-pool@3.11.0", "", { "peerDependencies": { "pg": ">=8.0" } }, "sha512-MJYfvHwtGp870aeusDh+hg9apvOe2zmpZJpyt+BMtzUWlVqbhFmMK6bOBXLBUPd7iRtIF9fZplDc7KrPN3PN7w=="], + + "pg-protocol": ["pg-protocol@1.11.0", "", {}, "sha512-pfsxk2M9M3BuGgDOfuy37VNRRX3jmKgMjcvAcWqNDpZSf4cUmv8HSOl5ViRQFsfARFn0KuUQTgLxVMbNq5NW3g=="], + + "pg-types": ["pg-types@2.2.0", "", { "dependencies": { "pg-int8": "1.0.1", "postgres-array": "~2.0.0", "postgres-bytea": "~1.0.0", "postgres-date": "~1.0.4", "postgres-interval": "^1.1.0" } }, "sha512-qTAAlrEsl8s4OiEQY69wDvcMIdQN6wdz5ojQiOy6YRMuynxenON0O5oCpJI6lshc6scgAY8qvJ2On/p+CXY0GA=="], + + "pgpass": ["pgpass@1.0.5", "", { "dependencies": { "split2": "^4.1.0" } }, "sha512-FdW9r/jQZhSeohs1Z3sI1yxFQNFvMcnmfuj4WBMUTxOrAyLMaTcE1aAMBiTlbMNaXvBCQuVi0R7hd8udDSP7ug=="], + "picocolors": ["picocolors@1.1.1", "", {}, "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA=="], "postcss": ["postcss@8.5.6", "", { "dependencies": { "nanoid": "^3.3.11", "picocolors": "^1.1.1", "source-map-js": "^1.2.1" } }, "sha512-3Ybi1tAuwAP9s0r1UQ2J4n5Y0G05bJkpUIO0/bI9MhwmD70S5aTWbXGBwxHrelT+XM1k6dM0pk+SwNkpTRN7Pg=="], + "postgres-array": ["postgres-array@2.0.0", "", {}, "sha512-VpZrUqU5A69eQyW2c5CA1jtLecCsN2U/bD6VilrFDWq5+5UIEVO7nazS3TEcHf1zuPYO/sqGvUvW62g86RXZuA=="], + + "postgres-bytea": ["postgres-bytea@1.0.1", "", {}, "sha512-5+5HqXnsZPE65IJZSMkZtURARZelel2oXUEO8rH83VS/hxH5vv1uHquPg5wZs8yMAfdv971IU+kcPUczi7NVBQ=="], + + "postgres-date": ["postgres-date@1.0.7", "", {}, "sha512-suDmjLVQg78nMK2UZ454hAG+OAW+HQPZ6n++TNDUX+L0+uUlLywnoxJKDou51Zm+zTCjrCl0Nq6J9C5hP9vK/Q=="], + + "postgres-interval": ["postgres-interval@1.2.0", "", { "dependencies": { "xtend": "^4.0.0" } }, "sha512-9ZhXKM/rw350N1ovuWHbGxnGh/SNJ4cnxHiM0rxE4VN41wsg8P8zWn9hv/buK00RP4WvlOyr/RBDiptyxVbkZQ=="], + "preact": ["preact@10.28.4", "", {}, "sha512-uKFfOHWuSNpRFVTnljsCluEFq57OKT+0QdOiQo8XWnQ/pSvg7OpX5eNOejELXJMWy+BwM2nobz0FkvzmnpCNsQ=="], "prebuild-install": ["prebuild-install@7.1.3", "", { "dependencies": { "detect-libc": "^2.0.0", "expand-template": "^2.0.3", "github-from-package": "0.0.0", "minimist": "^1.2.3", "mkdirp-classic": "^0.5.3", "napi-build-utils": "^2.0.0", "node-abi": "^3.3.0", "pump": "^3.0.0", "rc": "^1.2.7", "simple-get": "^4.0.0", "tar-fs": "^2.0.0", "tunnel-agent": "^0.6.0" }, "bin": { "prebuild-install": "bin.js" } }, "sha512-8Mf2cbV7x1cXPUILADGI3wuhfqWvtiLA1iclTDbFRZkgRQS0NqsPZphna9V+HyTEadheuPmjaJMsbzKQFOzLug=="], + "process-nextick-args": ["process-nextick-args@2.0.1", "", {}, "sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag=="], + "property-information": ["property-information@7.1.0", "", {}, "sha512-TwEZ+X+yCJmYfL7TPUOcvBZ4QfoT5YenQiJuX//0th53DE6w0xxLEtfK3iyryQFddXuvkIk51EEgrJQ0WJkOmQ=="], "pump": ["pump@3.0.3", "", { "dependencies": { "end-of-stream": "^1.1.0", "once": "^1.3.1" } }, "sha512-todwxLMY7/heScKmntwQG8CXVkWUOdYxIvY2s0VWAAMh/nd8SoYiRaKjlr7+iCs984f2P8zvrfWcDDYVb73NfA=="], @@ -552,6 +701,8 @@ "readable-stream": ["readable-stream@3.6.2", "", { "dependencies": { "inherits": "^2.0.3", "string_decoder": "^1.1.1", "util-deprecate": "^1.0.1" } }, "sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA=="], + "readdir-glob": ["readdir-glob@1.1.3", "", { "dependencies": { "minimatch": "^5.1.0" } }, "sha512-v05I2k7xN8zXvPD9N+z/uhXPaj0sUFCe2rcWZIpBsqxfP7xXFQ0tipAd/wjj1YxWyWtUS5IDJpOG82JKt2EAVA=="], + "regex": ["regex@6.1.0", "", { "dependencies": { "regex-utilities": "^2.3.0" } }, "sha512-6VwtthbV4o/7+OaAF9I5L5V3llLEsoPyq9P1JVXkedTP33c7MfCG0/5NOPcSJn0TzXcG9YUrR0gQSWioew3LDg=="], "regex-recursion": ["regex-recursion@6.0.2", "", { "dependencies": { "regex-utilities": "^2.3.0" } }, "sha512-0YCaSCq2VRIebiaUviZNs0cBz1kg5kVS2UKUfNIx8YVs1cN3AV7NTctO5FOKBA+UT2BPJIWZauYHPqJODG50cg=="], @@ -562,16 +713,24 @@ "rfdc": ["rfdc@1.4.1", "", {}, "sha512-q1b3N5QkRUWUl7iyylaaj3kOpIT0N2i9MqIEQXP73GVsN9cw3fdx8X63cEmWhJGi2PPCF23Ijp7ktmd39rawIA=="], + "rimraf": ["rimraf@2.7.1", "", { "dependencies": { "glob": "^7.1.3" }, "bin": { "rimraf": "./bin.js" } }, "sha512-uWjbaKIK3T1OSVptzX7Nl6PvQ3qAGtKEtVRjRuazjfL3Bx5eI409VZSqgND+4UNnmzLVdPj9FqFJNPqBZFve4w=="], + "rollup": ["rollup@4.58.0", "", { "dependencies": { "@types/estree": "1.0.8" }, "optionalDependencies": { "@rollup/rollup-android-arm-eabi": "4.58.0", "@rollup/rollup-android-arm64": "4.58.0", "@rollup/rollup-darwin-arm64": "4.58.0", "@rollup/rollup-darwin-x64": "4.58.0", "@rollup/rollup-freebsd-arm64": "4.58.0", "@rollup/rollup-freebsd-x64": "4.58.0", "@rollup/rollup-linux-arm-gnueabihf": "4.58.0", "@rollup/rollup-linux-arm-musleabihf": "4.58.0", "@rollup/rollup-linux-arm64-gnu": "4.58.0", "@rollup/rollup-linux-arm64-musl": "4.58.0", "@rollup/rollup-linux-loong64-gnu": "4.58.0", "@rollup/rollup-linux-loong64-musl": "4.58.0", "@rollup/rollup-linux-ppc64-gnu": "4.58.0", "@rollup/rollup-linux-ppc64-musl": "4.58.0", "@rollup/rollup-linux-riscv64-gnu": "4.58.0", "@rollup/rollup-linux-riscv64-musl": "4.58.0", "@rollup/rollup-linux-s390x-gnu": "4.58.0", "@rollup/rollup-linux-x64-gnu": "4.58.0", "@rollup/rollup-linux-x64-musl": "4.58.0", "@rollup/rollup-openbsd-x64": "4.58.0", "@rollup/rollup-openharmony-arm64": "4.58.0", "@rollup/rollup-win32-arm64-msvc": "4.58.0", "@rollup/rollup-win32-ia32-msvc": "4.58.0", "@rollup/rollup-win32-x64-gnu": "4.58.0", "@rollup/rollup-win32-x64-msvc": "4.58.0", "fsevents": "~2.3.2" }, "bin": { "rollup": "dist/bin/rollup" } }, "sha512-wbT0mBmWbIvvq8NeEYWWvevvxnOyhKChir47S66WCxw1SXqhw7ssIYejnQEVt7XYQpsj2y8F9PM+Cr3SNEa0gw=="], "safe-buffer": ["safe-buffer@5.2.1", "", {}, "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ=="], + "safer-buffer": ["safer-buffer@2.1.2", "", {}, "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg=="], + + "saxes": ["saxes@5.0.1", "", { "dependencies": { "xmlchars": "^2.2.0" } }, "sha512-5LBh1Tls8c9xgGjw3QrMwETmTMVk0oFgvrFSvWx62llR2hcEInrKNZ2GZCCuuy2lvWrdl5jhbpeqc5hRYKFOcw=="], + "scheduler": ["scheduler@0.27.0", "", {}, "sha512-eNv+WrVbKu1f3vbYJT/xtiF5syA5HPIMtf9IgY/nKg0sWqzAUEvqY/xm7OcZc/qafLx/iO9FgOmeSAp4v5ti/Q=="], "search-insights": ["search-insights@2.17.3", "", {}, "sha512-RQPdCYTa8A68uM2jwxoY842xDhvx3E5LFL1LxvxCNMev4o5mLuokczhzjAgGwUZBAmOKZknArSxLKmXtIi2AxQ=="], "semver": ["semver@7.7.4", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-vFKC2IEtQnVhpT78h1Yp8wzwrf8CM+MzKMHGJZfBtzhZNycRFnXsHk6E5TxIkkMsgNS7mdX3AGB7x2QM2di4lA=="], + "setimmediate": ["setimmediate@1.0.5", "", {}, "sha512-MATJdZp8sLqDl/68LfQmbP8zKPLQNV6BIZoIgrscFDQ+RsvK/BxeDQOgyxKKoh0y/8h3BqVFnCqQ/gd+reiIXA=="], + "shell-quote": ["shell-quote@1.8.3", "", {}, "sha512-ObmnIF4hXNg1BqhnHmgbDETF8dLPCggZWBjkQfhZpbszZnYur5DUljTcCHii5LC3J5E0yeO/1LIMyH+UvHQgyw=="], "shiki": ["shiki@2.5.0", "", { "dependencies": { "@shikijs/core": "2.5.0", "@shikijs/engine-javascript": "2.5.0", "@shikijs/engine-oniguruma": "2.5.0", "@shikijs/langs": "2.5.0", "@shikijs/themes": "2.5.0", "@shikijs/types": "2.5.0", "@shikijs/vscode-textmate": "^10.0.2", "@types/hast": "^3.0.4" } }, "sha512-mI//trrsaiCIPsja5CNfsyNOqgAZUb6VpJA+340toL42UpzQlXpwRV9nch69X6gaUxrr9kaOOa6e3y3uAkGFxQ=="], @@ -592,7 +751,9 @@ "speakingurl": ["speakingurl@14.0.1", "", {}, "sha512-1POYv7uv2gXoyGFpBCmpDVSNV74IfsWlDW216UPjbWufNf+bSU6GdbDsxdcxtfwb4xlI3yxzOTKClUosxARYrQ=="], - "ssf": ["ssf@0.11.2", "", { "dependencies": { "frac": "~1.1.2" } }, "sha512-+idbmIXoYET47hH+d7dfm2epdOMUDjqcB4648sTZ+t2JwoyBFL/insLfB/racrDmsKB3diwsDA696pZMieAC5g=="], + "split2": ["split2@4.2.0", "", {}, "sha512-UcjcJOWknrNkF6PLX83qcHM6KHgVKNkV62Y8a5uYDVv9ydGQVwAHMKqHdJje1VTWpljG0WYpCDhrCdAOYH4TWg=="], + + "sql-escaper": ["sql-escaper@1.3.3", "", {}, "sha512-BsTCV265VpTp8tm1wyIm1xqQCS+Q9NHx2Sr+WcnUrgLrQ6yiDIvHYJV5gHxsj1lMBy2zm5twLaZao8Jd+S8JJw=="], "stack-utils": ["stack-utils@2.0.6", "", { "dependencies": { "escape-string-regexp": "^2.0.0" } }, "sha512-XlkWvfIm6RmsWtNJx+uqtKLS8eqFbxUg0ZzLXqY0caEy9l7hruX8IpiDnjsLavoBgqCCR71TqWO8MaXYheJ3RQ=="], @@ -620,10 +781,14 @@ "terminal-size": ["terminal-size@4.0.1", "", {}, "sha512-avMLDQpUI9I5XFrklECw1ZEUPJhqzcwSWsyyI8blhRLT+8N1jLJWLWWYQpB2q2xthq8xDvjZPISVh53T/+CLYQ=="], + "tmp": ["tmp@0.2.5", "", {}, "sha512-voyz6MApa1rQGUxT3E+BK7/ROe8itEx7vD8/HEvt4xwXucvQ5G5oeEiHkmHZJuBO21RpOf+YYm9MOivj709jow=="], + "to-rotated": ["to-rotated@1.0.0", "", {}, "sha512-KsEID8AfgUy+pxVRLsWp0VzCa69wxzUDZnzGbyIST/bcgcrMvTYoFBX/QORH4YApoD89EDuUovx4BTdpOn319Q=="], "tr46": ["tr46@5.1.1", "", { "dependencies": { "punycode": "^2.3.1" } }, "sha512-hdF5ZgjTqgAntKkklYw0R03MG2x/bSzTtkxmIRw/sTNV8YXsCJ1tfLAX23lhxhHJlEf3CRCOCGGWw3vI3GaSPw=="], + "traverse": ["traverse@0.3.9", "", {}, "sha512-iawgk0hLP3SxGKDfnDJf8wTz4p2qImnyihM5Hh/sGvQ3K37dPi/w8sRhdNIxYA1TwFwc5mDhIJq+O0RsvXBKdQ=="], + "trim-lines": ["trim-lines@3.0.1", "", {}, "sha512-kRj8B+YHZCc9kQYdWfJB2/oUl9rA99qbowYYBtr4ui4mZyAQ2JpvVBd/6U2YloATfqBhBTSMhTpgBHtU0Mf3Rg=="], "tunnel-agent": ["tunnel-agent@0.6.0", "", { "dependencies": { "safe-buffer": "^5.0.1" } }, "sha512-McnNiV1l8RYeY8tBgEpuodCC1mLUdbSN+CYBL7kJsJNInOP8UjDDEwdk6Mw60vdLLrr5NHKZhMAOSrR2NZuQ+w=="], @@ -644,8 +809,12 @@ "unist-util-visit-parents": ["unist-util-visit-parents@6.0.2", "", { "dependencies": { "@types/unist": "^3.0.0", "unist-util-is": "^6.0.0" } }, "sha512-goh1s1TBrqSqukSc8wrjwWhL0hiJxgA8m4kFxGlQ+8FYQ3C/m11FcTs4YYem7V664AhHVvgoQLk890Ssdsr2IQ=="], + "unzipper": ["unzipper@0.10.14", "", { "dependencies": { "big-integer": "^1.6.17", "binary": "~0.3.0", "bluebird": "~3.4.1", "buffer-indexof-polyfill": "~1.0.0", "duplexer2": "~0.1.4", "fstream": "^1.0.12", "graceful-fs": "^4.2.2", "listenercount": "~1.0.1", "readable-stream": "~2.3.6", "setimmediate": "~1.0.4" } }, "sha512-ti4wZj+0bQTiX2KmKWuwj7lhV+2n//uXEotUmGuQqrbVZSEGFMbI68+c6JCQ8aAmUWYvtHEz2A8K6wXvueR/6g=="], + "util-deprecate": ["util-deprecate@1.0.2", "", {}, "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw=="], + "uuid": ["uuid@8.3.2", "", { "bin": { "uuid": "dist/bin/uuid" } }, "sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg=="], + "vfile": ["vfile@6.0.3", "", { "dependencies": { "@types/unist": "^3.0.0", "vfile-message": "^4.0.0" } }, "sha512-KzIbH/9tXat2u30jf+smMwFCsno4wHVdNmzFyL+T/L3UGqqk6JKfVqOFOZEpZSHADH1k40ab6NUIXZq422ov3Q=="], "vfile-message": ["vfile-message@4.0.3", "", { "dependencies": { "@types/unist": "^3.0.0", "unist-util-stringify-position": "^4.0.0" } }, "sha512-QTHzsGd1EhbZs4AsQ20JX1rC3cOlt/IWJruk893DfLRr57lcnOeMaWG4K0JrRta4mIJZKth2Au3mM3u03/JWKw=="], @@ -662,32 +831,72 @@ "widest-line": ["widest-line@6.0.0", "", { "dependencies": { "string-width": "^8.1.0" } }, "sha512-U89AsyEeAsyoF0zVJBkG9zBgekjgjK7yk9sje3F4IQpXBJ10TF6ByLlIfjMhcmHMJgHZI4KHt4rdNfktzxIAMA=="], - "wmf": ["wmf@1.0.2", "", {}, "sha512-/p9K7bEh0Dj6WbXg4JG0xvLQmIadrner1bi45VMJTfnbVHsc7yIajZyoSoK60/dtVBs12Fm6WkUI5/3WAVsNMw=="], - - "word": ["word@0.3.0", "", {}, "sha512-OELeY0Q61OXpdUfTp+oweA/vtLVg5VDOXh+3he3PNzLGG/y0oylSOC1xRVj0+l4vQ3tj/bB1HVHv1ocXkQceFA=="], - "wrap-ansi": ["wrap-ansi@9.0.2", "", { "dependencies": { "ansi-styles": "^6.2.1", "string-width": "^7.0.0", "strip-ansi": "^7.1.0" } }, "sha512-42AtmgqjV+X1VpdOfyTGOYRi0/zsoLqtXQckTmqTeybT+BDIbM/Guxo7x3pE2vtpr1ok6xRqM9OpBe+Jyoqyww=="], "wrappy": ["wrappy@1.0.2", "", {}, "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ=="], "ws": ["ws@8.19.0", "", { "peerDependencies": { "bufferutil": "^4.0.1", "utf-8-validate": ">=5.0.2" }, "optionalPeers": ["bufferutil", "utf-8-validate"] }, "sha512-blAT2mjOEIi0ZzruJfIhb3nps74PRWTCz1IjglWEEpQl5XS/UNama6u2/rjFkDDouqr4L67ry+1aGIALViWjDg=="], - "xlsx": ["xlsx@0.18.5", "", { "dependencies": { "adler-32": "~1.3.0", "cfb": "~1.2.1", "codepage": "~1.15.0", "crc-32": "~1.2.1", "ssf": "~0.11.2", "wmf": "~1.0.1", "word": "~0.3.0" }, "bin": { "xlsx": "bin/xlsx.njs" } }, "sha512-dmg3LCjBPHZnQp5/F/+nnTa+miPJxUXB6vtk42YjBBKayDNagxGEeIdWApkYPOf3Z3pm3k62Knjzp7lMeTEtFQ=="], + "xmlchars": ["xmlchars@2.2.0", "", {}, "sha512-JZnDKK8B0RCDw84FNdDAIpZK+JuJw+s7Lz8nksI7SIuU3UXJJslUthsi+uWBUYOwPFwW7W7PRLRfUKpxjtjFCw=="], + + "xtend": ["xtend@4.0.2", "", {}, "sha512-LKYU1iAXJXUgAXn9URjiu+MWhyUXHsvfp7mcuYm9dSUKK0/CjtrUwFAxD82/mCWbtLsGjFIad0wIsod4zrTAEQ=="], "yoga-layout": ["yoga-layout@3.2.1", "", {}, "sha512-0LPOt3AxKqMdFBZA3HBAt/t/8vIKq7VaQYbuA8WxCgung+p9TVyKRYdpvCb80HcdTN2NkbIKbhNwKUfm3tQywQ=="], + "zip-stream": ["zip-stream@4.1.1", "", { "dependencies": { "archiver-utils": "^3.0.4", "compress-commons": "^4.1.2", "readable-stream": "^3.6.0" } }, "sha512-9qv4rlDiopXg4E69k+vMHjNN63YFMe9sZMrdlvKnCjlCRWeCBswPPMPUfx+ipsAWq1LXHe70RcbaHdJJpS6hyQ=="], + "zod": ["zod@3.25.76", "", {}, "sha512-gzUt/qt81nXsFGKIFcC3YnfEAx5NkunCfnDlvuBSSFS02bcXu4Lmea0AFIUwbLWxWPx3d9p8S5QoaujKcNQxcQ=="], "zwitch": ["zwitch@2.0.4", "", {}, "sha512-bXE4cR/kVZhKZX/RjPEflHaKVhUVl85noU3v6b8apfQEc1x4A+zBxjZ4lN8LqGd6WZ3dl98pY4o717VFmoPp+A=="], + "@fast-csv/format/@types/node": ["@types/node@14.18.63", "", {}, "sha512-fAtCfv4jJg+ExtXhvCkCqUKZ+4ok/JQk01qDKhL5BDDoS3AxKXhV5/MAVUZyQnSEd2GT92fkgZl0pz0Q0AzcIQ=="], + + "@fast-csv/parse/@types/node": ["@types/node@14.18.63", "", {}, "sha512-fAtCfv4jJg+ExtXhvCkCqUKZ+4ok/JQk01qDKhL5BDDoS3AxKXhV5/MAVUZyQnSEd2GT92fkgZl0pz0Q0AzcIQ=="], + + "archiver-utils/readable-stream": ["readable-stream@2.3.8", "", { "dependencies": { "core-util-is": "~1.0.0", "inherits": "~2.0.3", "isarray": "~1.0.0", "process-nextick-args": "~2.0.0", "safe-buffer": "~5.1.1", "string_decoder": "~1.1.1", "util-deprecate": "~1.0.1" } }, "sha512-8p0AUk4XODgIewSi0l8Epjs+EVnWiK7NoDIEGU0HhE7+ZyY8D1IMY7odu5lRrFXGg71L15KG8QrPmum45RTtdA=="], + "cli-truncate/slice-ansi": ["slice-ansi@7.1.2", "", { "dependencies": { "ansi-styles": "^6.2.1", "is-fullwidth-code-point": "^5.0.0" } }, "sha512-iOBWFgUX7caIZiuutICxVgX1SdxwAVFFKwt1EvMYYec/NWO5meOJ6K5uQxhrYBdQJne4KxiqZc+KptFOWFSI9w=="], + "duplexer2/readable-stream": ["readable-stream@2.3.8", "", { "dependencies": { "core-util-is": "~1.0.0", "inherits": "~2.0.3", "isarray": "~1.0.0", "process-nextick-args": "~2.0.0", "safe-buffer": "~5.1.1", "string_decoder": "~1.1.1", "util-deprecate": "~1.0.1" } }, "sha512-8p0AUk4XODgIewSi0l8Epjs+EVnWiK7NoDIEGU0HhE7+ZyY8D1IMY7odu5lRrFXGg71L15KG8QrPmum45RTtdA=="], + + "glob/minimatch": ["minimatch@3.1.3", "", { "dependencies": { "brace-expansion": "^1.1.7" } }, "sha512-M2GCs7Vk83NxkUyQV1bkABc4yxgz9kILhHImZiBPAZ9ybuvCb0/H7lEl5XvIg3g+9d4eNotkZA5IWwYl0tibaA=="], + "ink-text-input/type-fest": ["type-fest@4.41.0", "", {}, "sha512-TeTSQ6H5YHvpqVwBRcnLDCBnDOHWYu7IvGbHT6N8AOymcr9PJGjc1GTtiWZTYg0NCgYwvnYWEkVChQAr9bjfwA=="], + "jszip/readable-stream": ["readable-stream@2.3.8", "", { "dependencies": { "core-util-is": "~1.0.0", "inherits": "~2.0.3", "isarray": "~1.0.0", "process-nextick-args": "~2.0.0", "safe-buffer": "~5.1.1", "string_decoder": "~1.1.1", "util-deprecate": "~1.0.1" } }, "sha512-8p0AUk4XODgIewSi0l8Epjs+EVnWiK7NoDIEGU0HhE7+ZyY8D1IMY7odu5lRrFXGg71L15KG8QrPmum45RTtdA=="], + + "lazystream/readable-stream": ["readable-stream@2.3.8", "", { "dependencies": { "core-util-is": "~1.0.0", "inherits": "~2.0.3", "isarray": "~1.0.0", "process-nextick-args": "~2.0.0", "safe-buffer": "~5.1.1", "string_decoder": "~1.1.1", "util-deprecate": "~1.0.1" } }, "sha512-8p0AUk4XODgIewSi0l8Epjs+EVnWiK7NoDIEGU0HhE7+ZyY8D1IMY7odu5lRrFXGg71L15KG8QrPmum45RTtdA=="], + "postcss/nanoid": ["nanoid@3.3.11", "", { "bin": { "nanoid": "bin/nanoid.cjs" } }, "sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w=="], "react-devtools-core/ws": ["ws@7.5.10", "", { "peerDependencies": { "bufferutil": "^4.0.1", "utf-8-validate": "^5.0.2" }, "optionalPeers": ["bufferutil", "utf-8-validate"] }, "sha512-+dbF1tHwZpXcbOJdVOkzLDxZP1ailvSxM6ZweXTegylPny803bFhA+vqBYw4s31NSAk4S2Qz+AKXK9a4wkdjcQ=="], + "unzipper/readable-stream": ["readable-stream@2.3.8", "", { "dependencies": { "core-util-is": "~1.0.0", "inherits": "~2.0.3", "isarray": "~1.0.0", "process-nextick-args": "~2.0.0", "safe-buffer": "~5.1.1", "string_decoder": "~1.1.1", "util-deprecate": "~1.0.1" } }, "sha512-8p0AUk4XODgIewSi0l8Epjs+EVnWiK7NoDIEGU0HhE7+ZyY8D1IMY7odu5lRrFXGg71L15KG8QrPmum45RTtdA=="], + "wrap-ansi/string-width": ["string-width@7.2.0", "", { "dependencies": { "emoji-regex": "^10.3.0", "get-east-asian-width": "^1.0.0", "strip-ansi": "^7.1.0" } }, "sha512-tsaTIkKW9b4N+AEj+SVA+WhJzV7/zMhcSu78mLKWSk7cXMOSHsBKFWUs0fWwq8QyK3MgJBQRX6Gbi4kYbdvGkQ=="], + + "zip-stream/archiver-utils": ["archiver-utils@3.0.4", "", { "dependencies": { "glob": "^7.2.3", "graceful-fs": "^4.2.0", "lazystream": "^1.0.0", "lodash.defaults": "^4.2.0", "lodash.difference": "^4.5.0", "lodash.flatten": "^4.4.0", "lodash.isplainobject": "^4.0.6", "lodash.union": "^4.6.0", "normalize-path": "^3.0.0", "readable-stream": "^3.6.0" } }, "sha512-KVgf4XQVrTjhyWmx6cte4RxonPLR9onExufI1jhvw/MQ4BB6IsZD5gT8Lq+u/+pRkWna/6JoHpiQioaqFP5Rzw=="], + + "archiver-utils/readable-stream/safe-buffer": ["safe-buffer@5.1.2", "", {}, "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g=="], + + "archiver-utils/readable-stream/string_decoder": ["string_decoder@1.1.1", "", { "dependencies": { "safe-buffer": "~5.1.0" } }, "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg=="], + + "duplexer2/readable-stream/safe-buffer": ["safe-buffer@5.1.2", "", {}, "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g=="], + + "duplexer2/readable-stream/string_decoder": ["string_decoder@1.1.1", "", { "dependencies": { "safe-buffer": "~5.1.0" } }, "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg=="], + + "glob/minimatch/brace-expansion": ["brace-expansion@1.1.12", "", { "dependencies": { "balanced-match": "^1.0.0", "concat-map": "0.0.1" } }, "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg=="], + + "jszip/readable-stream/safe-buffer": ["safe-buffer@5.1.2", "", {}, "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g=="], + + "jszip/readable-stream/string_decoder": ["string_decoder@1.1.1", "", { "dependencies": { "safe-buffer": "~5.1.0" } }, "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg=="], + + "lazystream/readable-stream/safe-buffer": ["safe-buffer@5.1.2", "", {}, "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g=="], + + "lazystream/readable-stream/string_decoder": ["string_decoder@1.1.1", "", { "dependencies": { "safe-buffer": "~5.1.0" } }, "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg=="], + + "unzipper/readable-stream/safe-buffer": ["safe-buffer@5.1.2", "", {}, "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g=="], + + "unzipper/readable-stream/string_decoder": ["string_decoder@1.1.1", "", { "dependencies": { "safe-buffer": "~5.1.0" } }, "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg=="], } } diff --git a/package.json b/package.json index 3c7f236..228b033 100644 --- a/package.json +++ b/package.json @@ -22,7 +22,9 @@ "devDependencies": { "@types/better-sqlite3": "^7.6.13", "@types/bun": "latest", + "@types/oracledb": "^6.10.1", "@types/papaparse": "^5.5.2", + "@types/pg": "^8.16.0", "lefthook": "^2.1.1", "oxlint": "latest", "typescript": "^5", @@ -38,9 +40,12 @@ "ink-spinner": "^5.0.0", "ink-text-input": "^6.0.0", "mongodb": "^7.1.0", + "mysql2": "^3.18.0", "nanoid": "^5.1.6", "openai": "^6.22.0", + "oracledb": "^6.10.0", "papaparse": "^5.5.3", + "pg": "^8.18.0", "react": "^19.2.4", "string-width": "^8.2.0" } diff --git a/src/operations/input/fromdb.ts b/src/operations/input/fromdb.ts index f658277..2a24209 100644 --- a/src/operations/input/fromdb.ts +++ b/src/operations/input/fromdb.ts @@ -6,6 +6,8 @@ import type { JsonObject, JsonValue } from "../../types/json.ts"; /** * Execute SQL queries and produce records from results. * + * Supports SQLite (default) and PostgreSQL (--type pg). + * * Analogous to App::RecordStream::Operation::fromdb in Perl. */ export class FromDb extends Operation { @@ -13,6 +15,11 @@ export class FromDb extends Operation { sql: string | null = null; dbFile: string | null = null; dbType = "sqlite"; + host: string | null = null; + port: string | null = null; + dbName: string | null = null; + user: string | null = null; + password: string | null = null; acceptRecord(_record: Record): boolean { return true; @@ -42,7 +49,7 @@ export class FromDb extends Operation { handler: (v) => { this.dbFile = v as string; }, - description: "Path to the database file", + description: "Path to the database file (sqlite)", }, { long: "type", @@ -50,7 +57,55 @@ export class FromDb extends Operation { handler: (v) => { this.dbType = v as string; }, - description: "Database type (default: sqlite)", + description: "Database type: sqlite (default), pg, mysql, oracle", + }, + { + long: "host", + type: "string", + handler: (v) => { + this.host = v as string; + }, + description: "Hostname for database connection (pg, mysql)", + }, + { + long: "port", + type: "string", + handler: (v) => { + this.port = v as string; + }, + description: "Port for database connection (pg, mysql)", + }, + { + long: "db", + type: "string", + handler: (v) => { + this.dbName = v as string; + }, + description: "Database name (pg, mysql, oracle tnsname)", + }, + { + long: "dbname", + type: "string", + handler: (v) => { + this.dbName = v as string; + }, + description: "Database name (alias for --db)", + }, + { + long: "user", + type: "string", + handler: (v) => { + this.user = v as string; + }, + description: "Database user", + }, + { + long: "password", + type: "string", + handler: (v) => { + this.password = v as string; + }, + description: "Database password", }, ]; @@ -63,6 +118,18 @@ export class FromDb extends Operation { if (!this.sql) { this.sql = `SELECT * FROM ${this.tableName}`; } + + if (this.dbType === "pg" && !this.dbName) { + throw new Error("--db is required for PostgreSQL databases"); + } + + if (this.dbType === "mysql" && (!this.host || !this.dbName)) { + throw new Error("--host and --db are required for MySQL databases"); + } + + if (this.dbType === "oracle" && !this.dbName) { + throw new Error("--db (tnsname) is required for Oracle databases"); + } } override wantsInput(): boolean { @@ -70,12 +137,53 @@ export class FromDb extends Operation { } override streamDone(): void { - if (this.dbType !== "sqlite") { - throw new Error( - `Database type '${this.dbType}' is not supported yet. Only sqlite is currently supported.` - ); + if (this.dbType === "sqlite") { + this.runSqlite(); + } + // pg, mysql, oracle are async - handled via streamDoneAsync in finish() + } + + async streamDoneAsync(): Promise { + switch (this.dbType) { + case "sqlite": + this.runSqlite(); + break; + case "pg": + await this.runPostgres(); + break; + case "mysql": + await this.runMysql(); + break; + case "oracle": + await this.runOracle(); + break; + default: + throw new Error( + `Database type '${this.dbType}' is not supported. Supported types: sqlite, pg, mysql, oracle` + ); + } + } + + /** + * Override finish to support async database types. + * The dispatcher and executor both call `await op.finish()`, so returning + * a Promise here is correctly awaited at runtime even though the base + * class types this as void. + */ + override finish(): void { + if (this.dbType === "sqlite") { + this.streamDone(); + this.next.finish(); + return; } + // For async types (pg, mysql, oracle), return the Promise so the + // dispatcher's `await op.finish()` waits for it at runtime. + return this.streamDoneAsync().then(() => { + this.next.finish(); + }) as unknown as void; + } + runSqlite(): void { if (!this.dbFile) { throw new Error("--dbfile is required for sqlite databases"); } @@ -101,6 +209,130 @@ export class FromDb extends Operation { db.close(); } } + + async runPostgres(): Promise { + const { Client } = await import("pg"); + + const config: { + database: string; + host?: string; + port?: number; + user?: string; + password?: string; + } = { + database: this.dbName!, + }; + + // When --host is NOT provided, leave it unset so the driver uses Unix domain socket + if (this.host) { + config.host = this.host; + } + + // Only include port when explicitly provided + if (this.port) { + config.port = parseInt(this.port, 10); + } + + if (this.user) { + config.user = this.user; + } + + if (this.password) { + config.password = this.password; + } + + const client = new Client(config); + try { + await client.connect(); + const result = await client.query(this.sql!); + + for (const row of result.rows) { + this.pushRecord(new Record(row as JsonObject)); + } + } finally { + await client.end(); + } + } + + async runMysql(): Promise { + const mysql = await import("mysql2/promise"); + + const config: { + host: string; + database: string; + port?: number; + user?: string; + password?: string; + } = { + host: this.host!, + database: this.dbName!, + }; + + if (this.port) { + config.port = parseInt(this.port, 10); + } + + if (this.user) { + config.user = this.user; + } + + if (this.password) { + config.password = this.password; + } + + const connection = await mysql.createConnection(config); + try { + const [rows] = await connection.execute(this.sql!); + + for (const row of rows as JsonObject[]) { + this.pushRecord(new Record(row)); + } + } finally { + await connection.end(); + } + } + + async runOracle(): Promise { + let oracledb; + try { + oracledb = await import("oracledb"); + } catch { + throw new Error( + "Oracle support requires the 'oracledb' package. Install with: bun add oracledb" + ); + } + + const config: { + connectString: string; + user?: string; + password?: string; + } = { + connectString: this.dbName!, + }; + + if (this.user) { + config.user = this.user; + } + + if (this.password) { + config.password = this.password; + } + + const connection = await oracledb.default.getConnection(config); + try { + const result = await connection.execute(this.sql!, [], { + outFormat: oracledb.default.OUT_FORMAT_OBJECT, + }); + + if (result.rows) { + for (const row of result.rows) { + this.pushRecord(new Record(row as JsonObject)); + } + } + } finally { + await connection.close(); + } + } } import type { CommandDoc } from "../../types/CommandDoc.ts"; @@ -110,7 +342,7 @@ export const documentation: CommandDoc = { category: "input", synopsis: "recs fromdb [options]", description: - "Execute a select statement on a database and create a record stream from the results. The keys of the record will be the column names and the values the row values.", + "Execute a select statement on a database and create a record stream from the results. The keys of the record will be the column names and the values the row values.\n\nSupports SQLite (default), PostgreSQL (--type pg), MySQL (--type mysql), and Oracle (--type oracle).\n\nPostgreSQL and MySQL require the 'pg' and 'mysql2' packages respectively. Oracle requires the 'oracledb' package (optional, install with: bun add oracledb).", options: [ { flags: ["--table"], @@ -125,22 +357,70 @@ export const documentation: CommandDoc = { { flags: ["--dbfile"], argument: "", - description: "Path to the database file.", + description: "Path to the database file (sqlite).", }, { flags: ["--type"], argument: "", - description: "Database type (default: sqlite).", + description: + "Database type: sqlite (default), pg, mysql, oracle.", + }, + { + flags: ["--host"], + argument: "", + description: + "Hostname for database connection. For pg, omit to use Unix domain socket. Required for mysql.", + }, + { + flags: ["--port"], + argument: "", + description: "Port for database connection (pg, mysql).", + }, + { + flags: ["--db", "--dbname"], + argument: "", + description: + "Database name. Required for pg, mysql. For oracle, this is the TNS name.", + }, + { + flags: ["--user"], + argument: "", + description: "Database user for authentication.", + }, + { + flags: ["--password"], + argument: "", + description: "Database password for authentication.", }, ], examples: [ { - description: "Dump a table", + description: "Dump a SQLite table", command: "recs fromdb --type sqlite --dbfile testDb --table recs", }, { - description: "Run a select statement", - command: "recs fromdb --dbfile testDb --sql 'SELECT * FROM recs WHERE id > 9'", + description: "Run a SQLite select statement", + command: + "recs fromdb --dbfile testDb --sql 'SELECT * FROM recs WHERE id > 9'", + }, + { + description: "Query PostgreSQL via Unix socket", + command: "recs fromdb --type pg --db mydb --table users", + }, + { + description: "Query PostgreSQL with host and credentials", + command: + "recs fromdb --type pg --host db.example.com --port 5432 --db mydb --user admin --password secret --sql 'SELECT * FROM users'", + }, + { + description: "Query MySQL", + command: + "recs fromdb --type mysql --host localhost --db mydb --user root --table orders", + }, + { + description: "Query Oracle", + command: + "recs fromdb --type oracle --db MYDB --user scott --password tiger --table emp", }, ], }; diff --git a/tests/operations/input/fromdb.test.ts b/tests/operations/input/fromdb.test.ts index b3f6689..034a8e2 100644 --- a/tests/operations/input/fromdb.test.ts +++ b/tests/operations/input/fromdb.test.ts @@ -11,6 +11,64 @@ function runFromDb(args: string[]): JsonObject[] { return collector.records.map((r) => r.toJSON()); } +async function runFromDbAsync(args: string[]): Promise { + const collector = new CollectorReceiver(); + const op = new FromDb(collector); + op.init(args); + await op.finish(); + return collector.records.map((r) => r.toJSON()); +} + +// -- Detect available databases at module load time -- + +const TEST_ROWS = Array.from({ length: 10 }, (_, i) => ({ + id: i + 1, + foo: String(i + 1), +})); + +const INSERT_SQL = `INSERT INTO recs (id, foo) VALUES ${TEST_ROWS.map((r) => `(${r.id}, '${r.foo}')`).join(", ")}`; + +let pgAvailable = false; +try { + const { Client } = await import("pg"); + const client = new Client({ database: "recs_test" }); + await client.connect(); + await client.query( + `CREATE TABLE IF NOT EXISTS recs (id INTEGER, foo TEXT)` + ); + const { rows } = await client.query("SELECT COUNT(*)::int AS n FROM recs"); + if (rows[0].n === 0) { + await client.query(INSERT_SQL); + } + await client.end(); + pgAvailable = true; +} catch { + // pg not available — tests will be skipped +} + +let mysqlAvailable = false; +try { + const mysql = await import("mysql2/promise"); + const conn = await mysql.createConnection({ + host: "127.0.0.1", + user: "root", + database: "recs_test", + }); + await conn.execute( + `CREATE TABLE IF NOT EXISTS recs (id INTEGER, foo VARCHAR(255))` + ); + const [countRows] = await conn.execute("SELECT COUNT(*) AS n FROM recs"); + if ((countRows as Array<{ n: number }>)[0]!.n === 0) { + await conn.execute(INSERT_SQL); + } + await conn.end(); + mysqlAvailable = true; +} catch { + // mysql not available — tests will be skipped +} + +// -- SQLite tests (always run) -- + describe("FromDb", () => { test("dump entire table", () => { const result = runFromDb([ @@ -51,3 +109,136 @@ describe("FromDb", () => { expect(op.wantsInput()).toBe(false); }); }); + +// -- Option validation tests (always run, no DB needed) -- + +describe("FromDb option validation", () => { + test("requires --db for pg type", () => { + expect(() => { + const op = new FromDb(); + op.init(["--type", "pg", "--table", "recs"]); + }).toThrow("--db is required for PostgreSQL databases"); + }); + + test("requires --host and --db for mysql type", () => { + expect(() => { + const op = new FromDb(); + op.init(["--type", "mysql", "--table", "recs"]); + }).toThrow("--host and --db are required for MySQL databases"); + }); + + test("requires --host for mysql type (--db alone insufficient)", () => { + expect(() => { + const op = new FromDb(); + op.init(["--type", "mysql", "--db", "mydb", "--table", "recs"]); + }).toThrow("--host and --db are required for MySQL databases"); + }); + + test("requires --db for oracle type", () => { + expect(() => { + const op = new FromDb(); + op.init(["--type", "oracle", "--table", "recs"]); + }).toThrow("--db (tnsname) is required for Oracle databases"); + }); + + test("rejects unsupported --type", async () => { + const collector = new CollectorReceiver(); + const op = new FromDb(collector); + op.init(["--type", "bogus", "--table", "t", "--db", "x"]); + await expect(op.finish()).rejects.toThrow("not supported"); + }); +}); + +// -- PostgreSQL integration tests (skipped if pg unavailable) -- + +describe.skipIf(!pgAvailable)("FromDb PostgreSQL", () => { + test("dump entire table via --type pg", async () => { + const result = await runFromDbAsync([ + "--type", "pg", + "--db", "recs_test", + "--table", "recs", + ]); + + expect(result.length).toBe(10); + expect(result[0]).toEqual({ id: 1, foo: "1" }); + expect(result[9]).toEqual({ id: 10, foo: "10" }); + }); + + test("run SQL query with WHERE clause", async () => { + const result = await runFromDbAsync([ + "--type", "pg", + "--db", "recs_test", + "--sql", "SELECT * FROM recs WHERE id > 6 ORDER BY id", + ]); + + expect(result.length).toBe(4); + expect(result[0]).toEqual({ id: 7, foo: "7" }); + expect(result[3]).toEqual({ id: 10, foo: "10" }); + }); + + test("connects via Unix socket by default (no --host)", async () => { + const result = await runFromDbAsync([ + "--type", "pg", + "--db", "recs_test", + "--sql", "SELECT 1 AS val", + ]); + + expect(result).toEqual([{ val: 1 }]); + }); + + test("supports --port option", async () => { + const result = await runFromDbAsync([ + "--type", "pg", + "--db", "recs_test", + "--port", "5432", + "--sql", "SELECT 1 AS val", + ]); + + expect(result).toEqual([{ val: 1 }]); + }); +}); + +// -- MySQL integration tests (skipped if mysql unavailable) -- + +describe.skipIf(!mysqlAvailable)("FromDb MySQL", () => { + test("dump entire table via --type mysql", async () => { + const result = await runFromDbAsync([ + "--type", "mysql", + "--host", "127.0.0.1", + "--db", "recs_test", + "--user", "root", + "--table", "recs", + ]); + + expect(result.length).toBe(10); + expect(result[0]).toEqual({ id: 1, foo: "1" }); + expect(result[9]).toEqual({ id: 10, foo: "10" }); + }); + + test("run SQL query with WHERE clause", async () => { + const result = await runFromDbAsync([ + "--type", "mysql", + "--host", "127.0.0.1", + "--db", "recs_test", + "--user", "root", + "--sql", "SELECT * FROM recs WHERE id > 6 ORDER BY id", + ]); + + expect(result.length).toBe(4); + expect(result[0]).toEqual({ id: 7, foo: "7" }); + expect(result[3]).toEqual({ id: 10, foo: "10" }); + }); + + test("supports --port option", async () => { + const result = await runFromDbAsync([ + "--type", "mysql", + "--host", "127.0.0.1", + "--port", "3306", + "--db", "recs_test", + "--user", "root", + "--sql", "SELECT 1 AS val", + ]); + + expect(result[0]).toHaveProperty("val", 1); + }); +});