Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
133 changes: 133 additions & 0 deletions packages/cli/src/conversion.ts
Original file line number Diff line number Diff line change
Expand Up @@ -92,3 +92,136 @@ export async function decodeToJson(config: {
console.log(jsonOutput)
}
}

/**
* Read binary input from file or stdin
*/
export async function readBinaryInput(source: InputSource): Promise<Uint8Array> {
if (source.type === 'stdin') {
// For binary stdin, we need to read raw bytes
const { stdin } = process

if (stdin.readableEnded) {
return new Uint8Array(0)
}

return new Promise((resolve, reject) => {
const chunks: Uint8Array[] = []

const onData = (chunk: any) => {
chunks.push(new Uint8Array(chunk))
}

function cleanup() {
stdin.off('data', onData)
stdin.off('error', onError)
stdin.off('end', onEnd)
}

function onError(error: Error) {
cleanup()
reject(error)
}

function onEnd() {
cleanup()
const totalLength = chunks.reduce((sum, chunk) => sum + chunk.length, 0)
const result = new Uint8Array(totalLength)
let offset = 0
for (const chunk of chunks) {
result.set(chunk, offset)
offset += chunk.length
}
resolve(result)
}

// Read in binary mode (no encoding)
stdin.setRawMode?.(true)
stdin.on('data', onData)
stdin.once('error', onError)
stdin.once('end', onEnd)
stdin.resume()
})
}
else {
return fsp.readFile(source.path)
}
}

/**
* Encode JSON to binary TOON format
*/
export async function encodeToBinaryToon(config: {
input: InputSource
output?: string
delimiter: NonNullable<import('../../toon/src/binary/binary-types').BinaryEncodeOptions['delimiter']>
keyFolding?: NonNullable<import('../../toon/src/binary/binary-types').BinaryEncodeOptions['keyFolding']>
flattenDepth?: number
}): Promise<void> {
const jsonContent = await readInput(config.input)

let data: unknown
try {
data = JSON.parse(jsonContent)
}
catch (error) {
throw new Error(`Failed to parse JSON: ${error instanceof Error ? error.message : String(error)}`)
}

const { encodeBinary } = await import('../../toon/src')

const binaryOutput = encodeBinary(data, {
delimiter: config.delimiter,
keyFolding: config.keyFolding,
flattenDepth: config.flattenDepth,
})

if (config.output) {
await fsp.writeFile(config.output, binaryOutput)
const relativeInputPath = formatInputLabel(config.input)
const relativeOutputPath = path.relative(process.cwd(), config.output)
consola.success(`Encoded \`${relativeInputPath}\` → \`${relativeOutputPath}\` (binary)`)
}
else {
// For binary output to stdout, we can't just console.log
// Instead, write directly to stdout as binary
process.stdout.write(binaryOutput)
}
}

/**
* Decode binary TOON format to JSON
*/
export async function decodeBinaryToonToJson(config: {
input: InputSource
output?: string
indent: NonNullable<DecodeOptions['indent']>
strict: NonNullable<import('../../toon/src/binary/binary-types').BinaryDecodeOptions['strict']>
expandPaths?: NonNullable<import('../../toon/src/binary/binary-types').BinaryDecodeOptions['expandPaths']>
}): Promise<void> {
const binaryContent = await readBinaryInput(config.input)

let data: unknown
try {
const { decodeBinary } = await import('../../toon/src')
data = decodeBinary(binaryContent, {
strict: config.strict,
expandPaths: config.expandPaths,
})
}
catch (error) {
throw new Error(`Failed to decode binary TOON: ${error instanceof Error ? error.message : String(error)}`)
}

const jsonOutput = JSON.stringify(data, undefined, config.indent)

if (config.output) {
await fsp.writeFile(config.output, jsonOutput, 'utf-8')
const relativeInputPath = formatInputLabel(config.input)
const relativeOutputPath = path.relative(process.cwd(), config.output)
consola.success(`Decoded \`${relativeInputPath}\` → \`${relativeOutputPath}\` (from binary)`)
}
else {
console.log(jsonOutput)
}
}
39 changes: 35 additions & 4 deletions packages/cli/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ import { defineCommand } from 'citty'
import { consola } from 'consola'
import { DEFAULT_DELIMITER, DELIMITERS } from '../../toon/src'
import { name, version } from '../package.json' with { type: 'json' }
import { decodeToJson, encodeToToon } from './conversion'
import { decodeBinaryToonToJson, decodeToJson, encodeToBinaryToon, encodeToToon } from './conversion'
import { detectMode } from './utils'

export const mainCommand: CommandDef<{
Expand Down Expand Up @@ -65,6 +65,11 @@ export const mainCommand: CommandDef<{
description: string
default: false
}
binary: {
type: 'boolean'
description: 'Output binary TOON format (vs text TOON)'
default: false
}
}> = defineCommand({
meta: {
name,
Expand Down Expand Up @@ -126,6 +131,11 @@ export const mainCommand: CommandDef<{
description: 'Show token statistics',
default: false,
},
binary: {
type: 'boolean',
description: 'Output binary TOON format (vs text TOON)',
default: false,
},
},
async run({ args }) {
const input = args.input
Expand Down Expand Up @@ -168,10 +178,19 @@ export const mainCommand: CommandDef<{
throw new Error(`Invalid expandPaths value "${expandPaths}". Valid values are: off, safe`)
}

const mode = detectMode(inputSource, args.encode, args.decode)
const mode = detectMode(inputSource, args.encode, args.decode, args.binary)

try {
if (mode === 'encode') {
if (mode === 'encode_binary') {
await encodeToBinaryToon({
input: inputSource,
output: outputPath,
delimiter: delimiter as Delimiter,
keyFolding: keyFolding as NonNullable<import('../../toon/src/binary/binary-types').BinaryEncodeOptions['keyFolding']>,
flattenDepth,
})
}
else if (mode === 'encode') {
await encodeToToon({
input: inputSource,
output: outputPath,
Expand All @@ -182,7 +201,16 @@ export const mainCommand: CommandDef<{
printStats: args.stats === true,
})
}
else {
else if (mode === 'decode_binary') {
await decodeBinaryToonToJson({
input: inputSource,
output: outputPath,
indent,
strict: args.strict !== false,
expandPaths: expandPaths as NonNullable<import('../../toon/src/binary/binary-types').BinaryDecodeOptions['expandPaths']>,
})
}
else if (mode === 'decode') {
await decodeToJson({
input: inputSource,
output: outputPath,
Expand All @@ -191,6 +219,9 @@ export const mainCommand: CommandDef<{
expandPaths: expandPaths as NonNullable<DecodeOptions['expandPaths']>,
})
}
else {
throw new Error(`Unknown mode: ${mode}`)
}
}
catch (error) {
consola.error(error)
Expand Down
15 changes: 11 additions & 4 deletions packages/cli/src/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,23 +7,30 @@ export function detectMode(
input: InputSource,
encodeFlag?: boolean,
decodeFlag?: boolean,
): 'encode' | 'decode' {
binaryFlag?: boolean,
): 'encode' | 'decode' | 'encode_binary' | 'decode_binary' {
// Explicit flags take precedence
if (encodeFlag && binaryFlag)
return 'encode_binary'
if (encodeFlag)
return 'encode'
if (decodeFlag && binaryFlag)
return 'decode_binary'
if (decodeFlag)
return 'decode'

// Auto-detect based on file extension
if (input.type === 'file') {
if (input.path.endsWith('.json'))
return 'encode'
return encodeFlag || binaryFlag ? (binaryFlag ? 'encode_binary' : 'encode') : 'encode'
if (input.path.endsWith('.toon.bin') || binaryFlag)
return 'decode_binary'
if (input.path.endsWith('.toon'))
return 'decode'
return decodeFlag || binaryFlag ? (binaryFlag ? 'decode_binary' : 'decode') : 'decode'
}

// Default to encode
return 'encode'
return binaryFlag ? 'encode_binary' : 'encode'
}

export async function readInput(source: InputSource): Promise<string> {
Expand Down
Loading