Browse Source

Sync from gitrepublic-web monorepo

master
Silberengel 3 weeks ago
parent
commit
deb1109a7f
  1. 20
      README.md
  2. 10
      scripts/git-commit-msg-hook.js
  3. 78
      scripts/git-wrapper.js
  4. 429
      scripts/gitrepublic.js
  5. 20
      scripts/postinstall.js

20
README.md

@ -16,10 +16,15 @@ export NOSTRGIT_SECRET_KEY="nsec1..."
# Setup (configures credential helper and commit hook) # Setup (configures credential helper and commit hook)
gitrep-setup 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 clone https://your-domain.com/api/git/npub1.../repo.git gitrepublic-web
gitrep push gitrepublic-web main 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. # 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" # We suggest using "gitrepublic-web" as the remote name instead of "origin"
# because "origin" is often already set to GitHub, GitLab, or other services. # because "origin" is often already set to GitHub, GitLab, or other services.
@ -27,11 +32,14 @@ gitrep push gitrepublic-web main
## Commands ## Commands
- **`gitrepublic`** or **`gitrep`** - Git wrapper with enhanced error messages (use instead of `git`) - **`gitrepublic`** or **`gitrep`** - Unified command for both git operations and API access
- **`gitrepublic-api`** or **`gitrep-api`** - Access GitRepublic APIs from command line - 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-setup`** or **`gitrep-setup`** - Automatic setup script
- **`gitrepublic-uninstall`** or **`gitrep-uninstall`** - Remove all configuration - **`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. Run any command with `--help` or `-h` for detailed usage information.
## Uninstall ## Uninstall
@ -94,8 +102,10 @@ export NOSTR_RELAYS="wss://relay1.com,wss://relay2.com" # Optional, has default
## Documentation ## Documentation
For detailed documentation, run: For detailed documentation, run:
- `gitrep --help` or `gitrepublic --help` - Git wrapper usage - `gitrep --help` or `gitrepublic --help` - General help and git commands
- `gitrep-api --help` or `gitrepublic-api --help` - API 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-setup --help` or `gitrepublic-setup --help` - Setup options
- `gitrep-uninstall --help` or `gitrepublic-uninstall --help` - Uninstall options - `gitrep-uninstall --help` or `gitrepublic-uninstall --help` - Uninstall options

10
scripts/git-commit-msg-hook.js

@ -337,9 +337,15 @@ async function signCommitMessage(commitMessageFile) {
try { try {
const relaysEnv = process.env.NOSTR_RELAYS; const relaysEnv = process.env.NOSTR_RELAYS;
const relays = relaysEnv ? relaysEnv.split(',').map(r => r.trim()).filter(r => r.length > 0) : [ 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://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(); const pool = new SimplePool();

78
scripts/git-wrapper.js

@ -1,25 +1,37 @@
#!/usr/bin/env node #!/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 * This script handles both git commands (with enhanced error messages) and
* operations fail, especially for authentication and permission errors. * API commands (push-all, repos, publish, etc.). It delegates API commands
* to gitrepublic.js and handles git commands directly.
* *
* Usage: * Usage:
* gitrepublic <git-command> [arguments...] * gitrepublic <command> [arguments...]
* gitrep <git-command> [arguments...] (shorter alias) * gitrep <command> [arguments...] (shorter alias)
* *
* Examples: * Git Commands:
* gitrep clone https://domain.com/api/git/npub1.../repo.git gitrepublic-web * gitrep clone https://domain.com/api/git/npub1.../repo.git gitrepublic-web
* gitrep push gitrepublic-web main * gitrep push gitrepublic-web main
* gitrep pull 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 <subcommand>
*/ */
import { spawn, execSync } from 'child_process'; import { spawn, execSync } from 'child_process';
import { createHash } from 'crypto'; import { createHash } from 'crypto';
import { finalizeEvent } from 'nostr-tools'; import { finalizeEvent } from 'nostr-tools';
import { decode } from 'nostr-tools/nip19'; 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 // NIP-98 auth event kind
const KIND_NIP98_AUTH = 27235; const KIND_NIP98_AUTH = 27235;
@ -27,6 +39,17 @@ const KIND_NIP98_AUTH = 27235;
// Commands that interact with remotes (need error handling) // Commands that interact with remotes (need error handling)
const REMOTE_COMMANDS = ['clone', 'push', 'pull', 'fetch', 'ls-remote']; 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 // Get git remote URL
function getRemoteUrl(remote = 'origin') { function getRemoteUrl(remote = 'origin') {
try { try {
@ -244,7 +267,7 @@ Usage:
gitrepublic <git-command> [arguments...] gitrepublic <git-command> [arguments...]
gitrep <git-command> [arguments...] (shorter alias) gitrep <git-command> [arguments...] (shorter alias)
Examples: Git Commands:
gitrep clone https://domain.com/api/git/npub1.../repo.git gitrepublic-web gitrep clone https://domain.com/api/git/npub1.../repo.git gitrepublic-web
gitrep push gitrepublic-web main gitrep push gitrepublic-web main
gitrep pull gitrepublic-web main gitrep pull gitrepublic-web main
@ -252,6 +275,15 @@ Examples:
gitrep branch gitrep branch
gitrep commit -m "My commit" 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 <npub> <repo> Get repository info
gitrep publish <subcommand> Publish Nostr events
gitrep search <query> Search repositories
gitrep verify <event-file> Verify Nostr events
gitrep config [server] Show configuration
Note: "gitrep" is a shorter alias for "gitrepublic" - both work the same way. 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" We suggest using "gitrepublic-web" as the remote name instead of "origin"
because "origin" is often already set to GitHub, GitLab, or other services. because "origin" is often already set to GitHub, GitLab, or other services.
@ -261,12 +293,15 @@ Features:
- Enhanced error messages for GitRepublic repositories - Enhanced error messages for GitRepublic repositories
- Detailed authentication and permission error information - Detailed authentication and permission error information
- Transparent pass-through for non-GitRepublic repositories (GitHub, GitLab, etc.) - 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: For GitRepublic repositories, the wrapper provides:
- Detailed 401/403 error messages with pubkeys and maintainer information - Detailed 401/403 error messages with pubkeys and maintainer information
- Helpful guidance on how to fix authentication issues - Helpful guidance on how to fix authentication issues
- Automatic fetching of error details from the server - Automatic fetching of error details from the server
Run any command with --help for detailed usage information.
Documentation: https://github.com/silberengel/gitrepublic-cli Documentation: https://github.com/silberengel/gitrepublic-cli
GitCitadel: Visit us on GitHub: https://github.com/ShadowySupercode or on our homepage: https://gitcitadel.com 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() { async function main() {
const args = process.argv.slice(2); 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')) { if (args.length === 0 || args.includes('--help') || args.includes('-h')) {
showHelp(); showHelp();
process.exit(0); process.exit(0);
} }
const command = args[0];
const commandArgs = args.slice(1);
// For clone, check if URL is GitRepublic // For clone, check if URL is GitRepublic
if (command === 'clone' && commandArgs.length > 0) { if (command === 'clone' && commandArgs.length > 0) {
const url = commandArgs[commandArgs.length - 1]; const url = commandArgs[commandArgs.length - 1];

429
scripts/gitrepublic.js

@ -1,13 +1,22 @@
#!/usr/bin/env node #!/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: * Usage (via gitrep/gitrepublic):
* gitrepublic <command> [options] * gitrep push-all [branch] [options]
* gitrep repos list
* gitrep publish <subcommand>
*
* Usage (direct, for backward compatibility):
* gitrep-api push-all [branch] [options]
* gitrepublic-api repos list
* *
* Commands: * Commands:
* push-all [branch] [--force] [--tags] [--dry-run] Push to all remotes
* repos list List repositories * repos list List repositories
* repos get <npub> <repo> Get repository info * repos get <npub> <repo> Get repository info
* repos settings <npub> <repo> Get/update repository settings * repos settings <npub> <repo> Get/update repository settings
@ -20,6 +29,9 @@
* file put <npub> <repo> <path> Create/update file * file put <npub> <repo> <path> Create/update file
* file delete <npub> <repo> <path> Delete file * file delete <npub> <repo> <path> Delete file
* search <query> Search repositories * search <query> Search repositories
* publish <subcommand> Publish Nostr events
* verify <event-file> Verify Nostr event signatures
* config [server] Show configuration
* *
* Options: * Options:
* --server <url> GitRepublic server URL (default: http://localhost:5173) * --server <url> GitRepublic server URL (default: http://localhost:5173)
@ -29,12 +41,48 @@
*/ */
import { createHash } from 'crypto'; 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 { decode } from 'nostr-tools/nip19';
import { readFileSync, writeFileSync, existsSync } from 'fs'; import { readFileSync, writeFileSync, existsSync } from 'fs';
import { execSync } from 'child_process'; import { execSync } from 'child_process';
import { join, dirname, resolve } from 'path'; 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 // NIP-98 auth event kind
const KIND_NIP98_AUTH = 27235; const KIND_NIP98_AUTH = 27235;
@ -46,7 +94,18 @@ const DEFAULT_SERVER = process.env.GITREPUBLIC_SERVER || 'http://localhost:5173'
/** /**
* Decode Nostr key and get private key bytes * 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) { function getPrivateKeyBytes(key) {
if (!key || typeof key !== 'string') {
throw new Error('Invalid key: key must be a string');
}
try {
if (key.startsWith('nsec')) { if (key.startsWith('nsec')) {
const decoded = decode(key); const decoded = decode(key);
if (decoded.type === 'nsec') { if (decoded.type === 'nsec') {
@ -62,6 +121,13 @@ function getPrivateKeyBytes(key) {
return keyBytes; return keyBytes;
} }
throw new Error('Invalid key format. Use nsec or hex.'); 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.');
}
throw error;
}
} }
/** /**
@ -180,38 +246,106 @@ function storeEventInJsonl(event) {
/** /**
* Publish event to Nostr relays using SimplePool * 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 pool = new SimplePool();
const success = []; const success = [];
const failed = []; const failed = [];
// 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 { try {
// Publish to all relays - SimplePool handles this automatically // Publish to a single relay with timeout
// Returns a Set of relays that accepted the event const publishPromise = pool.publish([relayUrl], event);
const results = await pool.publish(relays, event); const timeoutPromise = new Promise((_, reject) =>
setTimeout(() => reject(new Error('Publish timeout after 8 seconds')), 8000)
// Check which relays succeeded );
for (const relay of relays) {
if (results && results.has && results.has(relay)) { const successfulRelays = await Promise.race([publishPromise, timeoutPromise]);
success.push(relay);
} else { // Check if this relay succeeded
failed.push({ relay, error: 'Relay did not accept event' }); 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) { } catch (error) {
// If publish fails entirely, mark all relays as failed const errorMessage = error instanceof Error ? error.message : String(error);
for (const relay of relays) { const sanitizedError = sanitizeErrorMessage(errorMessage);
failed.push({ relay, error: String(error) });
if (errorMessage.includes('timeout')) {
return { relay: relayUrl, success: false, error: 'Publish timeout - relay did not respond' };
} else {
return { relay: relayUrl, success: false, error: sanitizedError };
}
}
});
// 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 }; 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 * Make authenticated API request
*/ */
@ -245,7 +379,10 @@ async function apiRequest(server, endpoint, method = 'GET', body = null, options
} }
if (!response.ok) { 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; return data;
@ -641,7 +778,7 @@ const commands = {
console.log(` console.log(`
Publish Nostr Git Events Publish Nostr Git Events
Usage: gitrep-api publish <subcommand> [options] Usage: gitrep publish <subcommand> [options]
Subcommands: Subcommands:
repo-announcement <repo-name> [options] repo-announcement <repo-name> [options]
@ -654,7 +791,7 @@ Subcommands:
--relay <url> Custom relay URL (can be specified multiple times) --relay <url> Custom relay URL (can be specified multiple times)
Example: Example:
gitrep-api publish repo-announcement myrepo \\ gitrep publish repo-announcement myrepo \\
--description "My awesome repo" \\ --description "My awesome repo" \\
--clone-url "https://gitrepublic.com/api/git/npub1.../myrepo.git" \\ --clone-url "https://gitrepublic.com/api/git/npub1.../myrepo.git" \\
--maintainer "npub1..." --maintainer "npub1..."
@ -664,7 +801,7 @@ Subcommands:
Note: You must be the current owner (signing with NOSTRGIT_SECRET_KEY) Note: You must be the current owner (signing with NOSTRGIT_SECRET_KEY)
Example: Example:
gitrep-api publish ownership-transfer myrepo npub1... --self-transfer gitrep publish ownership-transfer myrepo npub1... --self-transfer
pr <owner-npub> <repo> <title> [options] pr <owner-npub> <repo> <title> [options]
Create a pull request (kind 1618) Create a pull request (kind 1618)
@ -674,7 +811,7 @@ Subcommands:
--head <branch> Head branch (default: main) --head <branch> Head branch (default: main)
Example: Example:
gitrep-api publish pr npub1... myrepo "Fix bug" \\ gitrep publish pr npub1... myrepo "Fix bug" \\
--content "This PR fixes a critical bug" \\ --content "This PR fixes a critical bug" \\
--base main --head feature-branch --base main --head feature-branch
@ -685,14 +822,14 @@ Subcommands:
--label <label> Label (can be specified multiple times) --label <label> Label (can be specified multiple times)
Example: Example:
gitrep-api publish issue npub1... myrepo "Bug report" \\ gitrep publish issue npub1... myrepo "Bug report" \\
--content "Found a bug" --label bug --label critical --content "Found a bug" --label bug --label critical
status <event-id> <open|applied|closed|draft> [--content <text>] status <event-id> <open|applied|closed|draft> [--content <text>]
Update PR/issue status (kinds 1630-1633) Update PR/issue status (kinds 1630-1633)
Example: 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] patch <owner-npub> <repo> <patch-file> [options]
Publish a git patch (kind 1617) Publish a git patch (kind 1617)
@ -706,7 +843,7 @@ Subcommands:
--mention <npub> Mention user (can be specified multiple times) --mention <npub> Mention user (can be specified multiple times)
Example: Example:
gitrep-api publish patch npub1... myrepo patch-0001.patch \\ gitrep publish patch npub1... myrepo patch-0001.patch \\
--earliest-commit abc123 --commit def456 --root --earliest-commit abc123 --commit def456 --root
repo-state <repo> [options] repo-state <repo> [options]
@ -716,12 +853,27 @@ Subcommands:
--head <branch> Set HEAD branch --head <branch> Set HEAD branch
Example: Example:
gitrep-api publish repo-state myrepo \\ gitrep publish repo-state myrepo \\
--ref refs/heads/main abc123 def456 \\ --ref refs/heads/main abc123 def456 \\
--ref refs/tags/v1.0.0 xyz789 \\ --ref refs/tags/v1.0.0 xyz789 \\
--head main --head main
pr-update <owner-npub> <repo> <pr-event-id> <commit-id> [options] 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) Update pull request tip commit (kind 1619)
Options: Options:
--pr-author <npub> PR author pubkey (for NIP-22 tags) --pr-author <npub> PR author pubkey (for NIP-22 tags)
@ -731,7 +883,7 @@ Subcommands:
--mention <npub> Mention user (can be specified multiple times) --mention <npub> Mention user (can be specified multiple times)
Example: 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... \\ --pr-author npub1... \\
--clone-url "https://gitrepublic.com/api/git/npub1.../myrepo.git" \\ --clone-url "https://gitrepublic.com/api/git/npub1.../myrepo.git" \\
--merge-base base-commit-id --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 // Get relays from environment or use defaults
const relaysEnv = process.env.NOSTR_RELAYS; const relaysEnv = process.env.NOSTR_RELAYS;
const relays = relaysEnv ? relaysEnv.split(',').map(r => r.trim()).filter(r => r.length > 0) : [ 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://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') { if (subcommand === 'repo-announcement') {
@ -816,6 +974,9 @@ For more information, see: https://github.com/silberengel/gitrepublic-cli
tags.push(['p', maintainer]); tags.push(['p', maintainer]);
} }
// Add client tag unless --no-client-tag is specified
addClientTag(tags, args);
const event = finalizeEvent({ const event = finalizeEvent({
kind: 30617, // REPO_ANNOUNCEMENT kind: 30617, // REPO_ANNOUNCEMENT
created_at: Math.floor(Date.now() / 1000), 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 // Store event in JSONL file
storeEventInJsonl(event); storeEventInJsonl(event);
const result = await publishToRelays(event, relays); const result = await publishToRelays(event, relays, privateKeyBytes, pubkey);
if (json) { if (json) {
console.log(JSON.stringify({ event, published: result }, null, 2)); 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']); tags.push(['t', 'self-transfer']);
} }
// Add client tag unless --no-client-tag is specified
addClientTag(tags, args);
const event = finalizeEvent({ const event = finalizeEvent({
kind: 1641, // OWNERSHIP_TRANSFER kind: 1641, // OWNERSHIP_TRANSFER
created_at: Math.floor(Date.now() / 1000), 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 // Store event in JSONL file
storeEventInJsonl(event); storeEventInJsonl(event);
const result = await publishToRelays(event, relays); const result = await publishToRelays(event, relays, privateKeyBytes, pubkey);
if (json) { if (json) {
console.log(JSON.stringify({ event, published: result }, null, 2)); 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]); tags.push(['head', headBranch]);
} }
// Add client tag unless --no-client-tag is specified
addClientTag(tags, args);
const event = finalizeEvent({ const event = finalizeEvent({
kind: 1618, // PULL_REQUEST kind: 1618, // PULL_REQUEST
created_at: Math.floor(Date.now() / 1000), 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 // Store event in JSONL file
storeEventInJsonl(event); storeEventInJsonl(event);
const result = await publishToRelays(event, relays); const result = await publishToRelays(event, relays, privateKeyBytes, pubkey);
if (json) { if (json) {
console.log(JSON.stringify({ event, published: result }, null, 2)); 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]); tags.push(['t', label]);
} }
// Add client tag unless --no-client-tag is specified
addClientTag(tags, args);
const event = finalizeEvent({ const event = finalizeEvent({
kind: 1621, // ISSUE kind: 1621, // ISSUE
created_at: Math.floor(Date.now() / 1000), 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 // Store event in JSONL file
storeEventInJsonl(event); storeEventInJsonl(event);
const result = await publishToRelays(event, relays); const result = await publishToRelays(event, relays, privateKeyBytes, pubkey);
if (json) { if (json) {
console.log(JSON.stringify({ event, published: result }, null, 2)); 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({ const event = finalizeEvent({
kind, kind,
created_at: Math.floor(Date.now() / 1000), 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 // Store event in JSONL file
storeEventInJsonl(event); storeEventInJsonl(event);
const result = await publishToRelays(event, relays); const result = await publishToRelays(event, relays, privateKeyBytes, pubkey);
if (json) { if (json) {
console.log(JSON.stringify({ event, published: result }, null, 2)); console.log(JSON.stringify({ event, published: result }, null, 2));
@ -1186,6 +1359,9 @@ For more information, see: https://github.com/silberengel/gitrepublic-cli
tags.push(['p', mentionPubkey]); tags.push(['p', mentionPubkey]);
} }
// Add client tag unless --no-client-tag is specified
addClientTag(tags, args);
const event = finalizeEvent({ const event = finalizeEvent({
kind: 1617, // PATCH kind: 1617, // PATCH
created_at: Math.floor(Date.now() / 1000), 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 // Store event in JSONL file
storeEventInJsonl(event); storeEventInJsonl(event);
const result = await publishToRelays(event, relays); const result = await publishToRelays(event, relays, privateKeyBytes, pubkey);
if (json) { if (json) {
console.log(JSON.stringify({ event, published: result }, null, 2)); 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({ const event = finalizeEvent({
kind: 30618, // REPO_STATE kind: 30618, // REPO_STATE
created_at: Math.floor(Date.now() / 1000), 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 // Store event in JSONL file
storeEventInJsonl(event); storeEventInJsonl(event);
const result = await publishToRelays(event, relays); const result = await publishToRelays(event, relays, privateKeyBytes, pubkey);
if (json) { if (json) {
console.log(JSON.stringify({ event, published: result }, null, 2)); 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]); tags.push(['merge-base', mergeBase]);
} }
// Add client tag unless --no-client-tag is specified
addClientTag(tags, args);
const event = finalizeEvent({ const event = finalizeEvent({
kind: 1619, // PULL_REQUEST_UPDATE kind: 1619, // PULL_REQUEST_UPDATE
created_at: Math.floor(Date.now() / 1000), 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 // Store event in JSONL file
storeEventInJsonl(event); storeEventInJsonl(event);
const result = await publishToRelays(event, relays); const result = await publishToRelays(event, relays, privateKeyBytes, pubkey);
if (json) { if (json) {
console.log(JSON.stringify({ event, published: result }, null, 2)); console.log(JSON.stringify({ event, published: result }, null, 2));
@ -1400,9 +1582,146 @@ For more information, see: https://github.com/silberengel/gitrepublic-cli
result.failed.forEach(f => console.log(` ${f.relay}: ${f.error}`)); 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.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 { } else {
console.error(`Error: Unknown publish subcommand: ${subcommand}`); 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('Use: publish repo-announcement|ownership-transfer|pr|pr-update|issue|status|patch|repo-state|event');
console.error('Run: publish --help for detailed usage'); console.error('Run: publish --help for detailed usage');
process.exit(1); process.exit(1);
} }
@ -1491,7 +1810,7 @@ For more information, see: https://github.com/silberengel/gitrepublic-cli
if (args.includes('--help') || args.includes('-h')) { if (args.includes('--help') || args.includes('-h')) {
console.log(`Push to All Remotes console.log(`Push to All Remotes
Usage: gitrep-api push-all [branch] [options] Usage: gitrep push-all [branch] [options]
Description: Description:
Pushes the current branch (or specified branch) to all configured git remotes. Pushes the current branch (or specified branch) to all configured git remotes.
@ -1508,11 +1827,11 @@ Options:
--help, -h Show this help message --help, -h Show this help message
Examples: Examples:
gitrep-api push-all Push all branches to all remotes gitrep push-all Push all branches to all remotes
gitrep-api push-all main Push main branch to all remotes gitrep push-all main Push main branch to all remotes
gitrep-api push-all main --force Force push main branch to all remotes gitrep 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 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 main --dry-run Show what would be pushed without pushing
Notes: Notes:
- This command requires you to be in a git repository - 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 - If any remote fails, the command will exit with an error code
- Use --dry-run to test before actually pushing - Use --dry-run to test before actually pushing
`); `);
process.exit(0); return; // Exit the function (process.exit is handled by the caller)
} }
// Parse arguments // Parse arguments
@ -1625,10 +1944,10 @@ const commandArgs = commandIndex >= 0 ? args.slice(commandIndex + 1) : [];
const serverIndex = args.indexOf('--server'); const serverIndex = args.indexOf('--server');
const server = serverIndex >= 0 && args[serverIndex + 1] ? args[serverIndex + 1] : DEFAULT_SERVER; const server = serverIndex >= 0 && args[serverIndex + 1] ? args[serverIndex + 1] : DEFAULT_SERVER;
const json = args.includes('--json'); 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')); 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 // Only treat as general help if --help or -h is before the command or there's no command
const help = !commandHelpRequested && args.includes('--help'); const help = !commandHelpRequested && (args.includes('--help') || args.includes('-h'));
// Add config command // Add config command
if (command === 'config') { if (command === 'config') {
@ -1649,7 +1968,7 @@ if (command === 'config') {
} }
console.log(''); console.log('');
console.log('To change the server:'); 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>'); console.log(' export GITREPUBLIC_SERVER=<url>');
} }
process.exit(0); process.exit(0);
@ -1677,7 +1996,7 @@ if (commandHelpRequested && commandHandler) {
if (help || !command || !commandHandler) { if (help || !command || !commandHandler) {
console.log(`GitRepublic CLI console.log(`GitRepublic CLI
Usage: gitrep-api <command> [options] (or gitrepublic-api) Usage: gitrep <command> [options] (or gitrepublic)
Commands: Commands:
config [server] Show configuration (server URL) config [server] Show configuration (server URL)

20
scripts/postinstall.js

@ -15,24 +15,32 @@ Quick Start:
2. Run setup to configure git: 2. Run setup to configure git:
gitrep-setup 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 clone https://your-domain.com/api/git/npub1.../repo.git gitrepublic-web
gitrep push gitrepublic-web main 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. Note: "gitrep" is a shorter alias for "gitrepublic" - both work the same way.
Use "gitrepublic-web" as the remote name (not "origin") since Use "gitrepublic-web" as the remote name (not "origin") since
"origin" is often already set to GitHub, GitLab, or other services. "origin" is often already set to GitHub, GitLab, or other services.
Commands: Commands:
gitrepublic / gitrep Git wrapper with enhanced error messages gitrepublic / gitrep Unified command for git and API operations
gitrepublic-api / gitrep-api Access GitRepublic APIs gitrepublic-api / gitrep-api (Alias to gitrep/gitrepublic for backward compatibility)
gitrepublic-setup / gitrep-setup Configure git credential helper and commit hook gitrepublic-setup / gitrep-setup Configure git credential helper and commit hook
gitrepublic-uninstall / gitrep-uninstall Remove all configuration gitrepublic-uninstall / gitrep-uninstall Remove all configuration
Get Help: Get Help:
gitrep --help (or gitrepublic --help) gitrep --help General help and git commands
gitrep-api --help (or gitrepublic-api --help) gitrep push-all --help Push to all remotes
gitrep-setup --help (or gitrepublic-setup --help) gitrep repos --help Repository management
gitrep publish --help Publish Nostr events
gitrep-setup --help Setup options
Documentation: https://github.com/silberengel/gitrepublic-cli Documentation: https://github.com/silberengel/gitrepublic-cli
GitCitadel: Visit us on GitHub: https://github.com/ShadowySupercode or on our homepage: https://gitcitadel.com GitCitadel: Visit us on GitHub: https://github.com/ShadowySupercode or on our homepage: https://gitcitadel.com

Loading…
Cancel
Save