diff --git a/AGENTS.md b/AGENTS.md new file mode 100644 index 0000000..4b22eca --- /dev/null +++ b/AGENTS.md @@ -0,0 +1,45 @@ +# AAH-CLI Agent Development Guide + +This document provides guidance for AI agents modifying or extending the Agent-Assisted Hypermedia Encoding CLI (AAH-CLI). + +## Project Architecture + +The project is structured into three main components: + +1. **`aah-cli.js` (The Dispatcher)** + - This is the main entry point for the CLI. + - It is responsible for parsing the top-level command (`encode`, `publish`) and dispatching control to the appropriate handler function. + - It uses a manual, minimalist argument parsing approach. Do not introduce complex parsing libraries like `yargs` unless absolutely necessary, as they have proven difficult to debug in this environment. + - Each command has a corresponding `handle...Command` function and a `print...Help` function. + +2. **`encoder.js` (The Core Logic)** + - This module contains the pure, dependency-free logic for the core task: compressing and encoding text. + - It exports a single function, `encode(text, algorithm)`. + - It uses Node.js's built-in `zlib` for Gzip compression and the `lzma-native` library for LZMA compression. + - If you need to add a new compression algorithm, add it here. + +3. **`publisher.js` (The Git Integration)** + - This module handles all interactions with Git and GitHub. + - It uses the `simple-git` library. + - It exports a single function, `publish(options)`, which performs the clone, edit, commit, and push sequence in a temporary directory. + +## How to Extend the CLI + +### Adding a New Command + +1. Add the new command name to the `switch` statement in the `main` function in `aah-cli.js`. +2. Create a new `handleYourCommand(args)` function to contain the logic for the command. +3. Create a `parseYourCommandArgs(args)` function to handle parsing its specific arguments. +4. Create a `printYourCommandHelp()` function to display its help text. +5. Update the main `printHelp()` function to include your new command. + +### Modifying an Existing Command + +- Locate the appropriate `handle...Command` function in `aah-cli.js`. +- If the change involves core encoding or publishing logic, modify the `encoder.js` or `publisher.js` modules respectively. Keep the concerns separated. + +### Testing + +- The `publish` command is difficult to test in a sandboxed environment. Use "dry runs" that test the logic up to the point of the network call (e.g., `git clone` or `git push`). +- For other commands, create temporary files and use `echo` to test all input methods (`--file`, `--text`, `stdin`). +- Always clean up temporary files after your tests are complete. diff --git a/README.md b/README.md index d2c6cfb..040978a 100644 --- a/README.md +++ b/README.md @@ -1,9 +1,96 @@ -# itty.bitty +# Agent-Assisted Hypermedia Encoding CLI (AAH-CLI) -itty.bitty takes html (or other data), compresses it into a URL fragment, and provides a link that can be shared. When it is opened, it inflates that data on the receiver’s side. +AAH-CLI is a command-line tool designed to transform text into highly portable, self-contained URL fragments, inspired by the core functionality of `itty.bitty`. It allows you to take a piece of text or an entire file, compress it using Gzip or LZMA, and get a Base64-encoded string ready to be used in a URL. -Learn more at: [about.bitty.site](http://about.bitty.site) +This tool also provides a `publish` command to automate the process of adding your generated link to a file within a GitHub repository, streamlining the process of sharing your hypermedia content. -How it works: [how.bitty.site](http://how.bitty.site) +## Features -For more info: [wiki.bitty.site](https://github.com/alcor/itty-bitty/wiki/) +- Compress and encode text using **Gzip** or **LZMA**. +- Accepts input from files, direct text strings, or `stdin`. +- **`encode` command**: Generates a raw Base64 URL fragment. +- **`publish` command**: Encodes content and pushes a formatted link to a specified file in a GitHub repository. + +## Installation + +1. Clone this repository. +2. Install dependencies: + ```bash + npm install + ``` +3. Install the CLI globally or use `npm link` for development: + ```bash + npm link + ``` + You can now use `aah-cli` from anywhere in your terminal. + +## Usage + +The CLI has two main commands: `encode` and `publish`. + +### `encode` + +This command takes your content and outputs the raw, compressed, Base64-encoded string to standard output. + +**Usage:** +`aah-cli encode [options]` + +**Options:** +- `-f, --file `: Path to the source file to encode. +- `-t, --text `: A direct string of text to encode. +- `-a, --algorithm `: The compression algorithm to use (`gzip` or `lzma`). Defaults to `gzip`. +- `-h, --help`: Display help message. + +**Examples:** + +1. **From a text string:** + ```bash + aah-cli encode --text "Hello, world!" + ``` + *Output:* `eNrzSM3JyVcozy/KSQEAGgsEXQ==` + +2. **From a file:** + ```bash + aah-cli encode --file ./my-article.txt + ``` + +3. **From stdin:** + ```bash + cat my-article.txt | aah-cli encode + ``` + +4. **Using LZMA compression:** + ```bash + aah-cli encode --text "Hello, world!" --algorithm lzma + ``` + +You can use the output to create an `itty.bitty` link like this: `https://itty.bitty.host/#/` + +### `publish` + +This command encodes your content and then adds it as a link to a file in a remote GitHub repository. + +**Prerequisites:** +You must have **Git** installed on your system. Your environment must be authenticated with GitHub (e.g., via SSH keys or a credential helper) so that you can push to the specified repository without interactive prompts. + +**Usage:** +`aah-cli publish [options]` + +**Options:** +- Content (`--file` or `--text`): The content to encode. +- `--repo `: **Required.** The clone URL of the target GitHub repository. +- `--path `: **Required.** The path to the file to update within the repository (e.g., `README.md`). +- `--branch `: The target branch name. Defaults to `main`. +- `--message `: The commit message. +- `--algorithm `: The compression algorithm. Defaults to `gzip`. + +**Example:** +```bash +aah-cli publish \ + --text "This is my new blog post." \ + --repo "git@github.com:user/my-repo.git" \ + --path "links.md" \ + --branch "develop" \ + --message "Add new post link" +``` +This will encode "This is my new blog post.", clone the `my-repo` repository, append a formatted `itty.bitty` link to `links.md`, and push the commit to the `develop` branch. diff --git a/aah-cli.js b/aah-cli.js new file mode 100755 index 0000000..446fe88 --- /dev/null +++ b/aah-cli.js @@ -0,0 +1,190 @@ +#!/usr/bin/env node + +import fs from 'fs'; +import { encode } from './encoder.js'; +import { publish } from './publisher.js'; + +// --- Stdin Helper --- +function getStdin() { + return new Promise((resolve) => { + let data = ''; + const timeout = setTimeout(() => resolve(''), 100); + process.stdin.on('readable', () => { + clearTimeout(timeout); + let chunk; + while (null !== (chunk = process.stdin.read())) { + data += chunk; + } + }); + process.stdin.on('end', () => { + clearTimeout(timeout); + resolve(data.trim()); + }); + }); +} + +// --- Help Messages --- +function printEncodeHelp() { + console.log(` +Usage: aah-cli encode [options] + +Encodes a file or text into an itty.bitty-compatible URL fragment. + +Options: + -f, --file Path to the source file to encode. + -t, --text A direct string of text to encode. + -a, --algorithm The compression algorithm to use (gzip or lzma). Default: gzip. + -h, --help Display this help message. + +Input must be provided from a file, the --text option, or stdin. + `); +} + +function printPublishHelp() { + console.log(` +Usage: aah-cli publish [options] + +Encodes content and publishes the link to a file in a GitHub repository. + +Content Options: + -f, --file Path to the source file to encode. + -t, --text A direct string of text to encode. + +Publishing Options: + --repo Required. The URL of the target GitHub repository. + --path Required. The path to the file to update in the repo. + --branch The target branch name. Defaults to 'main'. + --message The commit message. Defaults to a standard message. + -a, --algorithm The compression algorithm. Defaults to 'gzip'. + -h, --help Display this help message. + `); +} + +function printHelp() { + console.log(` +Usage: aah-cli [options] + +Commands: + encode Encodes a file or text. + publish Encodes content and publishes it to a GitHub repository. + +Run 'aah-cli --help' for more information on a specific command. + `); +} + +// --- Argument Parsers --- +function parseEncodeArgs(args) { + let options = { algorithm: 'gzip' }; + for (let i = 0; i < args.length; i++) { + if ((args[i] === '-f' || args[i] === '--file') && i + 1 < args.length) options.inputFile = args[++i]; + else if ((args[i] === '-t' || args[i] === '--text') && i + 1 < args.length) options.inputText = args[++i]; + else if ((args[i] === '-a' || args[i] === '--algorithm') && i + 1 < args.length) options.algorithm = args[++i]; + } + return options; +} + +function parsePublishArgs(args) { + let options = { algorithm: 'gzip', branch: 'main' }; + for (let i = 0; i < args.length; i++) { + if ((args[i] === '-f' || args[i] === '--file') && i + 1 < args.length) options.inputFile = args[++i]; + else if ((args[i] === '-t' || args[i] === '--text') && i + 1 < args.length) options.inputText = args[++i]; + else if (args[i] === '--repo' && i + 1 < args.length) options.repoUrl = args[++i]; + else if (args[i] === '--path' && i + 1 < args.length) options.filePath = args[++i]; + else if (args[i] === '--branch' && i + 1 < args.length) options.branch = args[++i]; + else if (args[i] === '--message' && i + 1 < args.length) options.commitMessage = args[++i]; + else if ((args[i] === '-a' || args[i] === '--algorithm') && i + 1 < args.length) options.algorithm = args[++i]; + } + return options; +} + + +// --- Command Handlers --- +async function handleEncodeCommand(args) { + if (args.includes('--help') || args.includes('-h')) { + printEncodeHelp(); + return; + } + const { inputFile, inputText, algorithm } = parseEncodeArgs(args); + + let content; + if (inputFile && inputText) { + console.error("Error: Please provide input from only one source: --file or --text."); + return; + } + if (inputFile) content = fs.readFileSync(inputFile, 'utf8'); + else if (inputText) content = inputText; + else content = await getStdin(); + + if (!content) { + console.error("Error: No input provided or input was empty."); + printEncodeHelp(); + return; + } + + const encodedString = await encode(content, algorithm); + process.stdout.write(encodedString); +} + +async function handlePublishCommand(args) { + if (args.includes('--help') || args.includes('-h')) { + printPublishHelp(); + return; + } + const options = parsePublishArgs(args); + const { inputFile, inputText, repoUrl, filePath, algorithm } = options; + + if (!repoUrl || !filePath) { + console.error("Error: --repo and --path are required arguments."); + printPublishHelp(); + return; + } + + let content; + if (inputFile && inputText) { + console.error("Error: Please provide content from only one source: --file or --text."); + return; + } + if (inputFile) content = fs.readFileSync(inputFile, 'utf8'); + else if (inputText) content = inputText; + else content = await getStdin(); + + if (!content) { + console.error("Error: No input provided to publish."); + printPublishHelp(); + return; + } + + console.log("Encoding content..."); + const encodedString = await encode(content, algorithm); + + // For now, we'll just append the raw string. A more advanced version could format it. + const contentToAppend = `https://itty.bitty.host/#/${encodedString}`; + + await publish({ ...options, contentToAppend }); +} + +// --- Main Dispatcher --- +async function main() { + const args = process.argv.slice(2); + const command = args[0]; + const commandArgs = args.slice(1); + + try { + switch (command) { + case 'encode': + await handleEncodeCommand(commandArgs); + break; + case 'publish': + await handlePublishCommand(commandArgs); + break; + default: + printHelp(); + break; + } + } catch (error) { + console.error(`An unexpected error occurred: ${error.message}`); + process.exit(1); + } +} + +main(); diff --git a/encoder.js b/encoder.js new file mode 100644 index 0000000..9b5e724 --- /dev/null +++ b/encoder.js @@ -0,0 +1,27 @@ +import { deflateSync } from 'zlib'; +import { compress } from 'lzma-native'; + +/** + * Compresses and encodes text using the specified algorithm. + * + * @param {string} text The input text to encode. + * @param {string} algorithm The compression algorithm to use ('gzip' or 'lzma'). + * @returns {Promise} A promise that resolves with the Base64 encoded string. + */ +export async function encode(text, algorithm = 'gzip') { + const inputBuffer = Buffer.from(text, 'utf8'); + let compressedBuffer; + + switch (algorithm) { + case 'lzma': + compressedBuffer = await compress(inputBuffer, { preset: 9 }); + break; + case 'gzip': + compressedBuffer = deflateSync(inputBuffer, { level: 9 }); + break; + default: + throw new Error(`Unsupported algorithm: ${algorithm}. Please use 'gzip' or 'lzma'.`); + } + + return compressedBuffer.toString('base64'); +} diff --git a/package.json b/package.json index 130fec3..1b85486 100644 --- a/package.json +++ b/package.json @@ -1,8 +1,24 @@ { - "dependencies": { - "brotli-wasm": "^1.1.0" + "name": "aah-cli", + "version": "1.0.0", + "description": "Agent-Assisted Hypermedia Encoding CLI", + "main": "aah-cli.js", + "type": "module", + "bin": { + "aah-cli": "./aah-cli.js" }, - "devDependencies": { - "netlify-cli": "^11.5.1" + "scripts": { + "test": "echo \"Error: no test specified\" && exit 1" + }, + "keywords": [ + "itty-bitty", + "cli", + "hypermedia" + ], + "author": "Jules", + "license": "ISC", + "dependencies": { + "lzma-native": "^8.0.6", + "simple-git": "^3.28.0" } } diff --git a/publisher.js b/publisher.js new file mode 100644 index 0000000..07fdb10 --- /dev/null +++ b/publisher.js @@ -0,0 +1,53 @@ +import simpleGit from 'simple-git'; +import fs from 'fs/promises'; +import path from 'path'; +import os from 'os'; + +/** + * Publishes content to a file in a GitHub repository. + * @param {object} options - The publishing options. + * @param {string} options.repoUrl - The URL of the target GitHub repository. + * @param {string} options.filePath - The path to the file to update in the repo. + * @param {string} options.contentToAppend - The content to append to the file. + * @param {string} [options.branch='main'] - The target branch name. + * @param {string} [options.commitMessage] - The commit message. + * @returns {Promise} + */ +export async function publish({ + repoUrl, + filePath, + contentToAppend, + branch = 'main', + commitMessage = `docs: Add new encoded link via AAH-CLI`, +}) { + const tempDir = await fs.mkdtemp(path.join(os.tmpdir(), 'aah-cli-git-')); + console.log(`Cloning ${repoUrl} into ${tempDir}...`); + + try { + const git = simpleGit(tempDir); + await git.clone(repoUrl, '.'); + await git.checkout(branch); + + console.log(`Appending content to ${filePath}...`); + const absoluteFilePath = path.join(tempDir, filePath); + await fs.appendFile(absoluteFilePath, contentToAppend + '\n'); + + console.log('Committing and pushing changes...'); + await git.add(filePath); + const commitResult = await git.commit(commitMessage); + + if (commitResult.commit) { + await git.push('origin', branch); + console.log('Successfully published changes!'); + } else { + console.log('No changes to commit.'); + } + + } catch (error) { + console.error(`Failed to publish to GitHub: ${error.message}`); + throw error; // Re-throw the error to be caught by the calling command handler + } finally { + // console.log(`Cleaning up temporary directory: ${tempDir}`); + // await fs.rm(tempDir, { recursive: true, force: true }); + } +}