mirror of
https://github.com/arfct/itty-bitty.git
synced 2026-03-11 08:54:33 +00:00
feat: Implement Agent-Assisted Hypermedia Encoding CLI (AAH-CLI)
This commit introduces the AAH-CLI, a Node.js command-line tool for creating `itty.bitty`-style URL fragments. The CLI includes two main commands: - `encode`: Compresses text content using Gzip or LZMA and outputs a Base64-encoded string. It accepts input from files, direct text strings, or stdin. - `publish`: Encodes content and then publishes the resulting link to a specified file within a GitHub repository. This command uses Git to clone, modify, commit, and push the changes. The project is structured into modular components for encoding (`encoder.js`) and publishing (`publisher.js`), with a manual argument parser in the main `aah-cli.js` file for robustness. Includes comprehensive `README.md` and `AGENTS.md` documentation.
This commit is contained in:
parent
04d67cf243
commit
be79518f7f
6 changed files with 427 additions and 9 deletions
45
AGENTS.md
Normal file
45
AGENTS.md
Normal file
|
|
@ -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.
|
||||
97
README.md
97
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>`: Path to the source file to encode.
|
||||
- `-t, --text <string>`: A direct string of text to encode.
|
||||
- `-a, --algorithm <type>`: 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/#/<output_string>`
|
||||
|
||||
### `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 <url>`: **Required.** The clone URL of the target GitHub repository.
|
||||
- `--path <filepath>`: **Required.** The path to the file to update within the repository (e.g., `README.md`).
|
||||
- `--branch <name>`: The target branch name. Defaults to `main`.
|
||||
- `--message <msg>`: The commit message.
|
||||
- `--algorithm <type>`: 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.
|
||||
|
|
|
|||
190
aah-cli.js
Executable file
190
aah-cli.js
Executable file
|
|
@ -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> Path to the source file to encode.
|
||||
-t, --text <string> A direct string of text to encode.
|
||||
-a, --algorithm <type> 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> Path to the source file to encode.
|
||||
-t, --text <string> A direct string of text to encode.
|
||||
|
||||
Publishing Options:
|
||||
--repo <url> Required. The URL of the target GitHub repository.
|
||||
--path <filepath> Required. The path to the file to update in the repo.
|
||||
--branch <name> The target branch name. Defaults to 'main'.
|
||||
--message <msg> The commit message. Defaults to a standard message.
|
||||
-a, --algorithm <type> The compression algorithm. Defaults to 'gzip'.
|
||||
-h, --help Display this help message.
|
||||
`);
|
||||
}
|
||||
|
||||
function printHelp() {
|
||||
console.log(`
|
||||
Usage: aah-cli <command> [options]
|
||||
|
||||
Commands:
|
||||
encode Encodes a file or text.
|
||||
publish Encodes content and publishes it to a GitHub repository.
|
||||
|
||||
Run 'aah-cli <command> --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();
|
||||
27
encoder.js
Normal file
27
encoder.js
Normal file
|
|
@ -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<string>} 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');
|
||||
}
|
||||
24
package.json
24
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"
|
||||
}
|
||||
}
|
||||
|
|
|
|||
53
publisher.js
Normal file
53
publisher.js
Normal file
|
|
@ -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<void>}
|
||||
*/
|
||||
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 });
|
||||
}
|
||||
}
|
||||
Loading…
Reference in a new issue