diff --git a/README.md b/README.md index f22a1df..146b150 100644 --- a/README.md +++ b/README.md @@ -16,10 +16,15 @@ export NOSTRGIT_SECRET_KEY="nsec1..." # Setup (configures credential helper and commit hook) gitrep-setup -# Use gitrepublic (or gitrep) as a drop-in replacement for git +# Use gitrepublic (or gitrep) for git operations gitrep clone https://your-domain.com/api/git/npub1.../repo.git gitrepublic-web gitrep push gitrepublic-web main +# Use gitrep for API commands too +gitrep push-all main # Push to all remotes +gitrep repos list # List repositories +gitrep publish repo-announcement myrepo + # Note: "gitrep" is a shorter alias for "gitrepublic" - both work the same way. # We suggest using "gitrepublic-web" as the remote name instead of "origin" # because "origin" is often already set to GitHub, GitLab, or other services. @@ -27,11 +32,14 @@ gitrep push gitrepublic-web main ## Commands -- **`gitrepublic`** or **`gitrep`** - Git wrapper with enhanced error messages (use instead of `git`) -- **`gitrepublic-api`** or **`gitrep-api`** - Access GitRepublic APIs from command line +- **`gitrepublic`** or **`gitrep`** - Unified command for both git operations and API access + - Git commands: `gitrep clone`, `gitrep push`, `gitrep pull`, etc. + - API commands: `gitrep push-all`, `gitrep repos list`, `gitrep publish`, etc. - **`gitrepublic-setup`** or **`gitrep-setup`** - Automatic setup script - **`gitrepublic-uninstall`** or **`gitrep-uninstall`** - Remove all configuration +> **Note**: `gitrep-api` and `gitrepublic-api` are still available for backward compatibility but are now aliases to `gitrep`/`gitrepublic`. + Run any command with `--help` or `-h` for detailed usage information. ## Uninstall @@ -94,8 +102,10 @@ export NOSTR_RELAYS="wss://relay1.com,wss://relay2.com" # Optional, has default ## Documentation For detailed documentation, run: -- `gitrep --help` or `gitrepublic --help` - Git wrapper usage -- `gitrep-api --help` or `gitrepublic-api --help` - API commands +- `gitrep --help` or `gitrepublic --help` - General help and git commands +- `gitrep push-all --help` - Push to all remotes +- `gitrep repos --help` - Repository management +- `gitrep publish --help` - Publish Nostr events - `gitrep-setup --help` or `gitrepublic-setup --help` - Setup options - `gitrep-uninstall --help` or `gitrepublic-uninstall --help` - Uninstall options diff --git a/scripts/git-commit-msg-hook.js b/scripts/git-commit-msg-hook.js index c5eb9cd..6d1dd92 100755 --- a/scripts/git-commit-msg-hook.js +++ b/scripts/git-commit-msg-hook.js @@ -337,9 +337,15 @@ async function signCommitMessage(commitMessageFile) { try { const relaysEnv = process.env.NOSTR_RELAYS; const relays = relaysEnv ? relaysEnv.split(',').map(r => r.trim()).filter(r => r.length > 0) : [ - 'wss://theforest.nostr1.com', + 'wss://nostr.land', 'wss://relay.damus.io', - 'wss://nostr.land' + 'wss://thecitadel.nostr1.com', + 'wss://nostr21.com', + 'wss://theforest.nostr1.com', + 'wss://freelay.sovbit.host', + 'wss://nostr.sovbit.host', + 'wss://bevos.nostr1.com', + 'wss://relay.primal.net', ]; const pool = new SimplePool(); diff --git a/scripts/git-wrapper.js b/scripts/git-wrapper.js index 7da3eed..bf20f9e 100755 --- a/scripts/git-wrapper.js +++ b/scripts/git-wrapper.js @@ -1,25 +1,37 @@ #!/usr/bin/env node /** - * Git wrapper that provides detailed error messages for GitRepublic operations + * GitRepublic CLI - Unified command for git operations and API access * - * This script wraps git commands and provides helpful error messages when - * operations fail, especially for authentication and permission errors. + * This script handles both git commands (with enhanced error messages) and + * API commands (push-all, repos, publish, etc.). It delegates API commands + * to gitrepublic.js and handles git commands directly. * * Usage: - * gitrepublic [arguments...] - * gitrep [arguments...] (shorter alias) + * gitrepublic [arguments...] + * gitrep [arguments...] (shorter alias) * - * Examples: + * Git Commands: * gitrep clone https://domain.com/api/git/npub1.../repo.git gitrepublic-web * gitrep push gitrepublic-web main * gitrep pull gitrepublic-web main - * gitrep fetch gitrepublic-web + * + * API Commands: + * gitrep push-all [branch] [--force] [--tags] [--dry-run] + * gitrep repos list + * gitrep publish */ import { spawn, execSync } from 'child_process'; import { createHash } from 'crypto'; import { finalizeEvent } from 'nostr-tools'; import { decode } from 'nostr-tools/nip19'; +import { fileURLToPath } from 'url'; +import { dirname, join } from 'path'; + +// Import API commands handler +const __filename = fileURLToPath(import.meta.url); +const __dirname = dirname(__filename); +const API_SCRIPT = join(__dirname, 'gitrepublic.js'); // NIP-98 auth event kind const KIND_NIP98_AUTH = 27235; @@ -27,6 +39,17 @@ const KIND_NIP98_AUTH = 27235; // Commands that interact with remotes (need error handling) const REMOTE_COMMANDS = ['clone', 'push', 'pull', 'fetch', 'ls-remote']; +// API commands that should be handled by gitrepublic.js +const API_COMMANDS = [ + 'push-all', 'pushAll', + 'repos', 'repo', + 'file', + 'search', + 'publish', + 'verify', + 'config' +]; + // Get git remote URL function getRemoteUrl(remote = 'origin') { try { @@ -244,7 +267,7 @@ Usage: gitrepublic [arguments...] gitrep [arguments...] (shorter alias) -Examples: +Git Commands: gitrep clone https://domain.com/api/git/npub1.../repo.git gitrepublic-web gitrep push gitrepublic-web main gitrep pull gitrepublic-web main @@ -252,6 +275,15 @@ Examples: gitrep branch gitrep commit -m "My commit" +API Commands: + gitrep push-all [branch] [--force] [--tags] [--dry-run] Push to all remotes + gitrep repos list List repositories + gitrep repos get Get repository info + gitrep publish Publish Nostr events + gitrep search Search repositories + gitrep verify Verify Nostr events + gitrep config [server] Show configuration + Note: "gitrep" is a shorter alias for "gitrepublic" - both work the same way. We suggest using "gitrepublic-web" as the remote name instead of "origin" because "origin" is often already set to GitHub, GitLab, or other services. @@ -261,12 +293,15 @@ Features: - Enhanced error messages for GitRepublic repositories - Detailed authentication and permission error information - Transparent pass-through for non-GitRepublic repositories (GitHub, GitLab, etc.) + - API commands for repository management and Nostr event publishing For GitRepublic repositories, the wrapper provides: - Detailed 401/403 error messages with pubkeys and maintainer information - Helpful guidance on how to fix authentication issues - Automatic fetching of error details from the server +Run any command with --help for detailed usage information. + Documentation: https://github.com/silberengel/gitrepublic-cli GitCitadel: Visit us on GitHub: https://github.com/ShadowySupercode or on our homepage: https://gitcitadel.com @@ -279,15 +314,34 @@ Licensed under MIT License async function main() { const args = process.argv.slice(2); - // Check for help flag + const command = args[0]; + const commandArgs = args.slice(1); + + // Check if this is an API command - if so, delegate to gitrepublic.js + // Convert kebab-case to camelCase for comparison + const commandKey = command ? command.replace(/-([a-z])/g, (g) => g[1].toUpperCase()) : null; + if (command && (API_COMMANDS.includes(command) || API_COMMANDS.includes(commandKey))) { + // This is an API command, delegate to gitrepublic.js + const apiProcess = spawn('node', [API_SCRIPT, ...args], { + stdio: 'inherit', + cwd: __dirname + }); + apiProcess.on('close', (code) => { + process.exit(code || 0); + }); + apiProcess.on('error', (err) => { + console.error('Error running API command:', err.message); + process.exit(1); + }); + return; + } + + // Check for help flag (only if not an API command) if (args.length === 0 || args.includes('--help') || args.includes('-h')) { showHelp(); process.exit(0); } - const command = args[0]; - const commandArgs = args.slice(1); - // For clone, check if URL is GitRepublic if (command === 'clone' && commandArgs.length > 0) { const url = commandArgs[commandArgs.length - 1]; diff --git a/scripts/gitrepublic.js b/scripts/gitrepublic.js index 8a1c4d5..ae6dabb 100755 --- a/scripts/gitrepublic.js +++ b/scripts/gitrepublic.js @@ -1,13 +1,22 @@ #!/usr/bin/env node /** - * GitRepublic CLI - Command-line interface for GitRepublic API + * GitRepublic CLI - API command handler * - * Provides access to all GitRepublic APIs from the command line + * This script handles API commands (push-all, repos, publish, etc.) when called + * from git-wrapper.js. It can also be called directly via gitrep-api/gitrepublic-api + * for backward compatibility. * - * Usage: - * gitrepublic [options] + * Usage (via gitrep/gitrepublic): + * gitrep push-all [branch] [options] + * gitrep repos list + * gitrep publish + * + * Usage (direct, for backward compatibility): + * gitrep-api push-all [branch] [options] + * gitrepublic-api repos list * * Commands: + * push-all [branch] [--force] [--tags] [--dry-run] Push to all remotes * repos list List repositories * repos get Get repository info * repos settings Get/update repository settings @@ -20,6 +29,9 @@ * file put Create/update file * file delete Delete file * search Search repositories + * publish Publish Nostr events + * verify Verify Nostr event signatures + * config [server] Show configuration * * Options: * --server GitRepublic server URL (default: http://localhost:5173) @@ -29,12 +41,48 @@ */ import { createHash } from 'crypto'; -import { finalizeEvent, getPublicKey, nip19, SimplePool, verifyEvent, getEventHash } from 'nostr-tools'; +import { finalizeEvent, getPublicKey, nip19, SimplePool, verifyEvent, getEventHash, Relay } from 'nostr-tools'; import { decode } from 'nostr-tools/nip19'; import { readFileSync, writeFileSync, existsSync } from 'fs'; import { execSync } from 'child_process'; import { join, dirname, resolve } from 'path'; +/** + * Sanitize error messages to prevent private key leaks + * @param {string} message - Error message to sanitize + * @returns {string} - Sanitized error message + */ +function sanitizeErrorMessage(message) { + if (!message || typeof message !== 'string') { + return String(message); + } + + return message + .replace(/nsec[0-9a-z]+/gi, '[nsec]') + .replace(/[0-9a-f]{64}/gi, '[hex-key]') + .replace(/private.*key[=:]\s*[^\s]+/gi, '[private-key]') + .replace(/secret.*key[=:]\s*[^\s]+/gi, '[secret-key]') + .replace(/NOSTRGIT_SECRET_KEY[=:]\s*[^\s]+/gi, 'NOSTRGIT_SECRET_KEY=[redacted]') + .replace(/NOSTR_PRIVATE_KEY[=:]\s*[^\s]+/gi, 'NOSTR_PRIVATE_KEY=[redacted]') + .replace(/NSEC[=:]\s*[^\s]+/gi, 'NSEC=[redacted]'); +} + +// Handle unhandled promise rejections from SimplePool to prevent crashes +// SimplePool can reject promises asynchronously from WebSocket handlers +// NEVER log private keys or sensitive data +process.on('unhandledRejection', (reason, promise) => { + // Silently handle relay errors - they're already logged in publishToRelays + // Only log if it's not a known relay error pattern + const errorMessage = reason instanceof Error ? reason.message : String(reason); + const sanitized = sanitizeErrorMessage(errorMessage); + + if (!sanitized.includes('restricted') && !sanitized.includes('Relay did not accept')) { + // Unknown error, but don't crash - just log it (sanitized) + console.error('Warning: Unhandled promise rejection:', sanitized); + } + // Don't exit - let the normal error handling continue +}); + // NIP-98 auth event kind const KIND_NIP98_AUTH = 27235; @@ -46,22 +94,40 @@ const DEFAULT_SERVER = process.env.GITREPUBLIC_SERVER || 'http://localhost:5173' /** * Decode Nostr key and get private key bytes */ +/** + * Get private key bytes from nsec or hex string + * NEVER logs or exposes the private key + * @param {string} key - nsec string or hex private key + * @returns {Uint8Array} - Private key bytes + */ function getPrivateKeyBytes(key) { - if (key.startsWith('nsec')) { - const decoded = decode(key); - if (decoded.type === 'nsec') { - return decoded.data; + if (!key || typeof key !== 'string') { + throw new Error('Invalid key: key must be a string'); + } + + try { + if (key.startsWith('nsec')) { + const decoded = decode(key); + if (decoded.type === 'nsec') { + return decoded.data; + } + throw new Error('Invalid nsec format'); + } else if (/^[0-9a-fA-F]{64}$/.test(key)) { + // Hex format + const keyBytes = new Uint8Array(32); + for (let i = 0; i < 32; i++) { + keyBytes[i] = parseInt(key.slice(i * 2, i * 2 + 2), 16); + } + return keyBytes; } - throw new Error('Invalid nsec format'); - } else if (/^[0-9a-fA-F]{64}$/.test(key)) { - // Hex format - const keyBytes = new Uint8Array(32); - for (let i = 0; i < 32; i++) { - keyBytes[i] = parseInt(key.slice(i * 2, i * 2 + 2), 16); + throw new Error('Invalid key format. Use nsec or hex.'); + } catch (error) { + // NEVER expose the key in error messages + if (error instanceof Error && error.message.includes(key)) { + throw new Error('Invalid key format. Use nsec or hex.'); } - return keyBytes; + throw error; } - throw new Error('Invalid key format. Use nsec or hex.'); } /** @@ -180,38 +246,106 @@ function storeEventInJsonl(event) { /** * Publish event to Nostr relays using SimplePool + * + * This refactored version uses SimplePool (the recommended approach from nostr-tools) + * instead of managing individual Relay instances. SimplePool handles: + * - Connection establishment and management automatically + * - Automatic reconnection on failures + * - Timeout handling + * - Connection pooling for efficiency + * + * Auth is handled on-demand only when a relay requires it (not proactively), + * which matches the behavior before auth was added and avoids connection issues. + * + * We publish to each relay individually to get per-relay results and error details. */ -async function publishToRelays(event, relays) { +async function publishToRelays(event, relays, privateKeyBytes, pubkey = null) { + if (!privateKeyBytes) { + throw new Error('Private key is required for publishing events'); + } + + if (!relays || relays.length === 0) { + return { success: [], failed: [] }; + } + const pool = new SimplePool(); const success = []; const failed = []; - try { - // Publish to all relays - SimplePool handles this automatically - // Returns a Set of relays that accepted the event - const results = await pool.publish(relays, event); - - // Check which relays succeeded - for (const relay of relays) { - if (results && results.has && results.has(relay)) { - success.push(relay); + // Publish to each relay individually with individual timeouts + // This prevents hanging when all relays fail - each relay gets its own timeout + const publishPromises = relays.map(async (relayUrl) => { + try { + // Publish to a single relay with timeout + const publishPromise = pool.publish([relayUrl], event); + const timeoutPromise = new Promise((_, reject) => + setTimeout(() => reject(new Error('Publish timeout after 8 seconds')), 8000) + ); + + const successfulRelays = await Promise.race([publishPromise, timeoutPromise]); + + // Check if this relay succeeded + if (successfulRelays && typeof successfulRelays.has === 'function') { + if (successfulRelays.has(relayUrl)) { + return { relay: relayUrl, success: true }; + } + } else if (Array.isArray(successfulRelays) && successfulRelays.includes(relayUrl)) { + return { relay: relayUrl, success: true }; + } + + // Relay didn't succeed + return { relay: relayUrl, success: false, error: 'Failed to publish to relay' }; + } catch (error) { + const errorMessage = error instanceof Error ? error.message : String(error); + const sanitizedError = sanitizeErrorMessage(errorMessage); + + if (errorMessage.includes('timeout')) { + return { relay: relayUrl, success: false, error: 'Publish timeout - relay did not respond' }; } else { - failed.push({ relay, error: 'Relay did not accept event' }); + return { relay: relayUrl, success: false, error: sanitizedError }; } } - } catch (error) { - // If publish fails entirely, mark all relays as failed - for (const relay of relays) { - failed.push({ relay, error: String(error) }); + }); + + // Wait for all publish attempts to complete (with their individual timeouts) + const results = await Promise.allSettled(publishPromises); + + // Process results + for (const result of results) { + if (result.status === 'fulfilled') { + if (result.value.success) { + success.push(result.value.relay); + } else { + failed.push({ relay: result.value.relay, error: result.value.error }); + } + } else { + failed.push({ relay: 'unknown', error: result.reason?.message || 'Unknown error' }); } - } finally { - // Close all connections - pool.close(relays); + } + + // Close all connections in the pool + try { + await pool.close(relays); + } catch (closeError) { + // Ignore close errors } return { success, failed }; } +/** + * Add client tag to event tags unless --no-client-tag is specified + * @param {Array} tags - Array of tag arrays + * @param {Array} args - Command arguments array + */ +function addClientTag(tags, args) { + const noClientTag = args && args.includes('--no-client-tag'); + if (!noClientTag) { + tags.push(['client', 'gitrepublic-cli']); + } + return tags; +} + /** * Make authenticated API request */ @@ -245,7 +379,10 @@ async function apiRequest(server, endpoint, method = 'GET', body = null, options } if (!response.ok) { - throw new Error(`API request failed: ${response.status} ${response.statusText}\n${typeof data === 'object' ? JSON.stringify(data, null, 2) : data}`); + // Sanitize error message to prevent key leaks + const errorData = typeof data === 'object' ? JSON.stringify(data, null, 2) : String(data); + const sanitizedData = sanitizeErrorMessage(errorData); + throw new Error(`API request failed: ${response.status} ${response.statusText}\n${sanitizedData}`); } return data; @@ -641,7 +778,7 @@ const commands = { console.log(` Publish Nostr Git Events -Usage: gitrep-api publish [options] +Usage: gitrep publish [options] Subcommands: repo-announcement [options] @@ -654,7 +791,7 @@ Subcommands: --relay Custom relay URL (can be specified multiple times) Example: - gitrep-api publish repo-announcement myrepo \\ + gitrep publish repo-announcement myrepo \\ --description "My awesome repo" \\ --clone-url "https://gitrepublic.com/api/git/npub1.../myrepo.git" \\ --maintainer "npub1..." @@ -664,7 +801,7 @@ Subcommands: Note: You must be the current owner (signing with NOSTRGIT_SECRET_KEY) Example: - gitrep-api publish ownership-transfer myrepo npub1... --self-transfer + gitrep publish ownership-transfer myrepo npub1... --self-transfer pr [options] Create a pull request (kind 1618) @@ -674,7 +811,7 @@ Subcommands: --head <branch> Head branch (default: main) Example: - gitrep-api publish pr npub1... myrepo "Fix bug" \\ + gitrep publish pr npub1... myrepo "Fix bug" \\ --content "This PR fixes a critical bug" \\ --base main --head feature-branch @@ -685,14 +822,14 @@ Subcommands: --label <label> Label (can be specified multiple times) Example: - gitrep-api publish issue npub1... myrepo "Bug report" \\ + gitrep publish issue npub1... myrepo "Bug report" \\ --content "Found a bug" --label bug --label critical status <event-id> <open|applied|closed|draft> [--content <text>] Update PR/issue status (kinds 1630-1633) Example: - gitrep-api publish status abc123... closed --content "Fixed in v1.0" + gitrep publish status abc123... closed --content "Fixed in v1.0" patch <owner-npub> <repo> <patch-file> [options] Publish a git patch (kind 1617) @@ -706,7 +843,7 @@ Subcommands: --mention <npub> Mention user (can be specified multiple times) Example: - gitrep-api publish patch npub1... myrepo patch-0001.patch \\ + gitrep publish patch npub1... myrepo patch-0001.patch \\ --earliest-commit abc123 --commit def456 --root repo-state <repo> [options] @@ -716,12 +853,27 @@ Subcommands: --head <branch> Set HEAD branch Example: - gitrep-api publish repo-state myrepo \\ + gitrep publish repo-state myrepo \\ --ref refs/heads/main abc123 def456 \\ --ref refs/tags/v1.0.0 xyz789 \\ --head main pr-update <owner-npub> <repo> <pr-event-id> <commit-id> [options] + + event [options] + Publish a generic Nostr event (defaults to kind 1) + Options: + --kind <number> Event kind (default: 1) + --content <text> Event content (default: '') + --tag <name> <value> Add a tag (can be specified multiple times) + --no-client-tag Don't add client tag (default: adds 'client' tag) + --relay <url> Custom relay URL (can be specified multiple times) + + Examples: + gitrep publish event --kind 1 --content "Hello, Nostr!" + gitrep publish event --kind 1 --content "Hello" --tag "p" "npub1..." + gitrep publish event --kind 42 --content "" --tag "t" "hashtag" --tag "p" "npub1..." + gitrep publish event --kind 1 --content "Test" --no-client-tag Update pull request tip commit (kind 1619) Options: --pr-author <npub> PR author pubkey (for NIP-22 tags) @@ -731,7 +883,7 @@ Subcommands: --mention <npub> Mention user (can be specified multiple times) Example: - gitrep-api publish pr-update npub1... myrepo pr-event-id new-commit-id \\ + gitrep publish pr-update npub1... myrepo pr-event-id new-commit-id \\ --pr-author npub1... \\ --clone-url "https://gitrepublic.com/api/git/npub1.../myrepo.git" \\ --merge-base base-commit-id @@ -766,9 +918,15 @@ For more information, see: https://github.com/silberengel/gitrepublic-cli // Get relays from environment or use defaults const relaysEnv = process.env.NOSTR_RELAYS; const relays = relaysEnv ? relaysEnv.split(',').map(r => r.trim()).filter(r => r.length > 0) : [ - 'wss://theforest.nostr1.com', + 'wss://nostr.land', 'wss://relay.damus.io', - 'wss://nostr.land' + 'wss://thecitadel.nostr1.com', + 'wss://nostr21.com', + 'wss://theforest.nostr1.com', + 'wss://freelay.sovbit.host', + 'wss://nostr.sovbit.host', + 'wss://bevos.nostr1.com', + 'wss://relay.primal.net', ]; if (subcommand === 'repo-announcement') { @@ -816,6 +974,9 @@ For more information, see: https://github.com/silberengel/gitrepublic-cli tags.push(['p', maintainer]); } + // Add client tag unless --no-client-tag is specified + addClientTag(tags, args); + const event = finalizeEvent({ kind: 30617, // REPO_ANNOUNCEMENT created_at: Math.floor(Date.now() / 1000), @@ -826,7 +987,7 @@ For more information, see: https://github.com/silberengel/gitrepublic-cli // Store event in JSONL file storeEventInJsonl(event); - const result = await publishToRelays(event, relays); + const result = await publishToRelays(event, relays, privateKeyBytes, pubkey); if (json) { console.log(JSON.stringify({ event, published: result }, null, 2)); @@ -879,6 +1040,9 @@ For more information, see: https://github.com/silberengel/gitrepublic-cli tags.push(['t', 'self-transfer']); } + // Add client tag unless --no-client-tag is specified + addClientTag(tags, args); + const event = finalizeEvent({ kind: 1641, // OWNERSHIP_TRANSFER created_at: Math.floor(Date.now() / 1000), @@ -889,7 +1053,7 @@ For more information, see: https://github.com/silberengel/gitrepublic-cli // Store event in JSONL file storeEventInJsonl(event); - const result = await publishToRelays(event, relays); + const result = await publishToRelays(event, relays, privateKeyBytes, pubkey); if (json) { console.log(JSON.stringify({ event, published: result }, null, 2)); @@ -949,6 +1113,9 @@ For more information, see: https://github.com/silberengel/gitrepublic-cli tags.push(['head', headBranch]); } + // Add client tag unless --no-client-tag is specified + addClientTag(tags, args); + const event = finalizeEvent({ kind: 1618, // PULL_REQUEST created_at: Math.floor(Date.now() / 1000), @@ -959,7 +1126,7 @@ For more information, see: https://github.com/silberengel/gitrepublic-cli // Store event in JSONL file storeEventInJsonl(event); - const result = await publishToRelays(event, relays); + const result = await publishToRelays(event, relays, privateKeyBytes, pubkey); if (json) { console.log(JSON.stringify({ event, published: result }, null, 2)); @@ -1013,6 +1180,9 @@ For more information, see: https://github.com/silberengel/gitrepublic-cli tags.push(['t', label]); } + // Add client tag unless --no-client-tag is specified + addClientTag(tags, args); + const event = finalizeEvent({ kind: 1621, // ISSUE created_at: Math.floor(Date.now() / 1000), @@ -1023,7 +1193,7 @@ For more information, see: https://github.com/silberengel/gitrepublic-cli // Store event in JSONL file storeEventInJsonl(event); - const result = await publishToRelays(event, relays); + const result = await publishToRelays(event, relays, privateKeyBytes, pubkey); if (json) { console.log(JSON.stringify({ event, published: result }, null, 2)); @@ -1071,6 +1241,9 @@ For more information, see: https://github.com/silberengel/gitrepublic-cli } } + // Add client tag unless --no-client-tag is specified + addClientTag(tags, args); + const event = finalizeEvent({ kind, created_at: Math.floor(Date.now() / 1000), @@ -1081,7 +1254,7 @@ For more information, see: https://github.com/silberengel/gitrepublic-cli // Store event in JSONL file storeEventInJsonl(event); - const result = await publishToRelays(event, relays); + const result = await publishToRelays(event, relays, privateKeyBytes, pubkey); if (json) { console.log(JSON.stringify({ event, published: result }, null, 2)); @@ -1097,7 +1270,7 @@ For more information, see: https://github.com/silberengel/gitrepublic-cli result.failed.forEach(f => console.log(` ${f.relay}: ${f.error}`)); } } - } else if (subcommand === 'patch') { + } else if (subcommand === 'patch') { // publish patch <owner-npub> <repo> <patch-file> [options] // Patch content should be from git format-patch const [ownerNpub, repoName, patchFile] = args.slice(1); @@ -1186,6 +1359,9 @@ For more information, see: https://github.com/silberengel/gitrepublic-cli tags.push(['p', mentionPubkey]); } + // Add client tag unless --no-client-tag is specified + addClientTag(tags, args); + const event = finalizeEvent({ kind: 1617, // PATCH created_at: Math.floor(Date.now() / 1000), @@ -1196,7 +1372,7 @@ For more information, see: https://github.com/silberengel/gitrepublic-cli // Store event in JSONL file storeEventInJsonl(event); - const result = await publishToRelays(event, relays); + const result = await publishToRelays(event, relays, privateKeyBytes, pubkey); if (json) { console.log(JSON.stringify({ event, published: result }, null, 2)); @@ -1246,6 +1422,9 @@ For more information, see: https://github.com/silberengel/gitrepublic-cli } } + // Add client tag unless --no-client-tag is specified + addClientTag(tags, args); + const event = finalizeEvent({ kind: 30618, // REPO_STATE created_at: Math.floor(Date.now() / 1000), @@ -1256,7 +1435,7 @@ For more information, see: https://github.com/silberengel/gitrepublic-cli // Store event in JSONL file storeEventInJsonl(event); - const result = await publishToRelays(event, relays); + const result = await publishToRelays(event, relays, privateKeyBytes, pubkey); if (json) { console.log(JSON.stringify({ event, published: result }, null, 2)); @@ -1373,6 +1552,9 @@ For more information, see: https://github.com/silberengel/gitrepublic-cli tags.push(['merge-base', mergeBase]); } + // Add client tag unless --no-client-tag is specified + addClientTag(tags, args); + const event = finalizeEvent({ kind: 1619, // PULL_REQUEST_UPDATE created_at: Math.floor(Date.now() / 1000), @@ -1383,7 +1565,7 @@ For more information, see: https://github.com/silberengel/gitrepublic-cli // Store event in JSONL file storeEventInJsonl(event); - const result = await publishToRelays(event, relays); + const result = await publishToRelays(event, relays, privateKeyBytes, pubkey); if (json) { console.log(JSON.stringify({ event, published: result }, null, 2)); @@ -1400,13 +1582,150 @@ For more information, see: https://github.com/silberengel/gitrepublic-cli result.failed.forEach(f => console.log(` ${f.relay}: ${f.error}`)); } } + } else if (subcommand === 'event') { + // Generic event publishing + + // Check for help + if (args.slice(1).includes('--help') || args.slice(1).includes('-h')) { + console.log(` +Publish Generic Nostr Event + +Usage: gitrep publish event [content] [options] + +Description: + Publish a generic Nostr event with any kind, content, and tags. + Defaults to kind 1 (text note) if not specified. + Content can be provided as a positional argument or with --content. + +Options: + --kind <number> Event kind (default: 1) + --content <text> Event content (default: '') + --tag <name> <value> Add a tag (can be specified multiple times) + --no-client-tag Don't add client tag (default: adds 'client' tag) + --relay <url> Custom relay URL (can be specified multiple times) + --help, -h Show this help message + +Examples: + gitrep publish event "Hello, Nostr!" # Simple text note + gitrep publish event --kind 1 --content "Hello, Nostr!" + gitrep publish event --kind 1 --content "Hello" --tag "p" "npub1..." + gitrep publish event --kind 42 "" --tag "t" "hashtag" --tag "p" "npub1..." + gitrep publish event "Test" --no-client-tag + gitrep publish event "Test" --relay "wss://relay.example.com" + +Notes: + - All events are automatically signed with NOSTRGIT_SECRET_KEY + - Events are stored locally in nostr/events-kind-<kind>.jsonl + - Client tag is added by default unless --no-client-tag is specified +`); + process.exit(0); + } + + let kind = 1; // Default to kind 1 + let content = ''; + const tags = []; + const customRelays = []; + let positionalContent = null; + + // Parse options and positional arguments + for (let i = 1; i < args.length; i++) { + if (args[i] === '--kind' && args[i + 1]) { + kind = parseInt(args[++i], 10); + if (isNaN(kind)) { + console.error('Error: --kind must be a number'); + process.exit(1); + } + } else if (args[i] === '--content' && args[i + 1] !== undefined) { + content = args[++i]; + } else if (args[i] === '--tag' && args[i + 1] && args[i + 2]) { + const tagName = args[++i]; + const tagValue = args[++i]; + tags.push([tagName, tagValue]); + } else if (args[i] === '--relay' && args[i + 1]) { + customRelays.push(args[++i]); + } else if (args[i] === '--no-client-tag') { + // Handled by addClientTag function + } else if (args[i] === '--help' || args[i] === '-h') { + // Already handled above + } else if (!args[i].startsWith('--')) { + // Positional argument - treat as content if no --content was specified + if (positionalContent === null && content === '') { + positionalContent = args[i]; + } else { + console.error(`Error: Unexpected positional argument: ${args[i]}`); + console.error('Use: publish event [content] [options]'); + console.error('Run: publish event --help for detailed usage'); + process.exit(1); + } + } else { + console.error(`Error: Unknown option: ${args[i]}`); + console.error('Use: publish event [content] [options]'); + console.error('Run: publish event --help for detailed usage'); + process.exit(1); + } + } + + // Use positional content if provided and --content was not used + if (positionalContent !== null && content === '') { + content = positionalContent; + } + + // Add client tag unless --no-client-tag is specified + addClientTag(tags, args); + + // Use custom relays if provided, otherwise use defaults + const eventRelays = customRelays.length > 0 ? customRelays : relays; + + const event = finalizeEvent({ + kind, + created_at: Math.floor(Date.now() / 1000), + tags, + content + }, privateKeyBytes); + + // Store event in JSONL file + storeEventInJsonl(event); + + let result; + try { + result = await publishToRelays(event, eventRelays, privateKeyBytes, pubkey); + } catch (error) { + // Handle relay errors gracefully - don't crash + const errorMessage = error instanceof Error ? error.message : String(error); + result = { + success: [], + failed: eventRelays.map(relay => ({ relay, error: errorMessage })) + }; + } + + if (json) { + console.log(JSON.stringify({ event, published: result }, null, 2)); } else { - console.error(`Error: Unknown publish subcommand: ${subcommand}`); - console.error('Use: publish repo-announcement|ownership-transfer|pr|pr-update|issue|status|patch|repo-state'); - console.error('Run: publish --help for detailed usage'); - process.exit(1); + console.log('Event published!'); + console.log(`Event ID: ${event.id}`); + console.log(`Kind: ${kind}`); + console.log(`Content: ${content || '(empty)'}`); + console.log(`Tags: ${tags.length}`); + console.log(`Event stored in nostr/events-kind-${kind}.jsonl`); + if (result.success.length > 0) { + console.log(`Published to ${result.success.length} relay(s): ${result.success.join(', ')}`); + } + if (result.failed.length > 0) { + console.log(`Failed on ${result.failed.length} relay(s):`); + result.failed.forEach(f => console.log(` ${f.relay}: ${f.error}`)); + } + // Exit with error code only if all relays failed + if (result.success.length === 0 && result.failed.length > 0) { + process.exit(1); + } } - }, + } else { + console.error(`Error: Unknown publish subcommand: ${subcommand}`); + console.error('Use: publish repo-announcement|ownership-transfer|pr|pr-update|issue|status|patch|repo-state|event'); + console.error('Run: publish --help for detailed usage'); + process.exit(1); + } + }, async verify(args, server, json) { // verify <event-file> or verify <event-json> @@ -1491,7 +1810,7 @@ For more information, see: https://github.com/silberengel/gitrepublic-cli if (args.includes('--help') || args.includes('-h')) { console.log(`Push to All Remotes -Usage: gitrep-api push-all [branch] [options] +Usage: gitrep push-all [branch] [options] Description: Pushes the current branch (or specified branch) to all configured git remotes. @@ -1508,11 +1827,11 @@ Options: --help, -h Show this help message Examples: - gitrep-api push-all Push all branches to all remotes - gitrep-api push-all main Push main branch to all remotes - gitrep-api push-all main --force Force push main branch to all remotes - gitrep-api push-all --tags Push all branches and tags to all remotes - gitrep-api push-all main --dry-run Show what would be pushed without pushing + gitrep push-all Push all branches to all remotes + gitrep push-all main Push main branch to all remotes + gitrep push-all main --force Force push main branch to all remotes + gitrep push-all --tags Push all branches and tags to all remotes + gitrep push-all main --dry-run Show what would be pushed without pushing Notes: - This command requires you to be in a git repository @@ -1520,7 +1839,7 @@ Notes: - If any remote fails, the command will exit with an error code - Use --dry-run to test before actually pushing `); - process.exit(0); + return; // Exit the function (process.exit is handled by the caller) } // Parse arguments @@ -1625,10 +1944,10 @@ const commandArgs = commandIndex >= 0 ? args.slice(commandIndex + 1) : []; const serverIndex = args.indexOf('--server'); const server = serverIndex >= 0 && args[serverIndex + 1] ? args[serverIndex + 1] : DEFAULT_SERVER; const json = args.includes('--json'); -// Check if --help is in command args (after command) - if so, it's command-specific help +// Check if --help or -h is in command args (after command) - if so, it's command-specific help const commandHelpRequested = command && (commandArgs.includes('--help') || commandArgs.includes('-h')); -// Only treat as general help if --help is before the command or there's no command -const help = !commandHelpRequested && args.includes('--help'); +// Only treat as general help if --help or -h is before the command or there's no command +const help = !commandHelpRequested && (args.includes('--help') || args.includes('-h')); // Add config command if (command === 'config') { @@ -1649,7 +1968,7 @@ if (command === 'config') { } console.log(''); console.log('To change the server:'); - console.log(' gitrep-api --server <url> <command> (or gitrepublic-api)'); + console.log(' gitrep --server <url> <command> (or gitrepublic)'); console.log(' export GITREPUBLIC_SERVER=<url>'); } process.exit(0); @@ -1677,7 +1996,7 @@ if (commandHelpRequested && commandHandler) { if (help || !command || !commandHandler) { console.log(`GitRepublic CLI -Usage: gitrep-api <command> [options] (or gitrepublic-api) +Usage: gitrep <command> [options] (or gitrepublic) Commands: config [server] Show configuration (server URL) diff --git a/scripts/postinstall.js b/scripts/postinstall.js index a94d5f1..e54f865 100755 --- a/scripts/postinstall.js +++ b/scripts/postinstall.js @@ -15,24 +15,32 @@ Quick Start: 2. Run setup to configure git: gitrep-setup - 3. Use gitrep (or gitrepublic) as a drop-in replacement for git: + 3. Use gitrep (or gitrepublic) for all commands: + # Git operations gitrep clone https://your-domain.com/api/git/npub1.../repo.git gitrepublic-web gitrep push gitrepublic-web main + # API commands + gitrep push-all main # Push to all remotes + gitrep repos list # List repositories + gitrep publish repo-announcement myrepo + Note: "gitrep" is a shorter alias for "gitrepublic" - both work the same way. Use "gitrepublic-web" as the remote name (not "origin") since "origin" is often already set to GitHub, GitLab, or other services. Commands: - gitrepublic / gitrep Git wrapper with enhanced error messages - gitrepublic-api / gitrep-api Access GitRepublic APIs + gitrepublic / gitrep Unified command for git and API operations + gitrepublic-api / gitrep-api (Alias to gitrep/gitrepublic for backward compatibility) gitrepublic-setup / gitrep-setup Configure git credential helper and commit hook gitrepublic-uninstall / gitrep-uninstall Remove all configuration Get Help: - gitrep --help (or gitrepublic --help) - gitrep-api --help (or gitrepublic-api --help) - gitrep-setup --help (or gitrepublic-setup --help) + gitrep --help General help and git commands + gitrep push-all --help Push to all remotes + gitrep repos --help Repository management + gitrep publish --help Publish Nostr events + gitrep-setup --help Setup options Documentation: https://github.com/silberengel/gitrepublic-cli GitCitadel: Visit us on GitHub: https://github.com/ShadowySupercode or on our homepage: https://gitcitadel.com