git-nostr-hook 0.0.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (4) hide show
  1. package/README.md +59 -0
  2. package/bin/cli.js +131 -0
  3. package/lib/hook.js +122 -0
  4. package/package.json +30 -0
package/README.md ADDED
@@ -0,0 +1,59 @@
1
+ # git-nostr-hook
2
+
3
+ Git hook that publishes repository state to Nostr (NIP-34) on every commit.
4
+
5
+ ## Install
6
+
7
+ ```bash
8
+ npm install -g git-nostr-hook
9
+ git-nostr-hook install
10
+ ```
11
+
12
+ ## Setup
13
+
14
+ Set your Nostr private key:
15
+
16
+ ```bash
17
+ git config --global nostr.privkey <64-char-hex-key>
18
+ ```
19
+
20
+ Generate a key if needed:
21
+
22
+ ```bash
23
+ npx noskey
24
+ ```
25
+
26
+ ## Usage
27
+
28
+ Once installed, every `git commit` automatically publishes a Kind 30617 event to Nostr relays with:
29
+
30
+ - Repository name
31
+ - Branch refs (like `git ls-remote`)
32
+ - Clone/web URLs
33
+ - Latest commit message
34
+
35
+ ## Commands
36
+
37
+ ```bash
38
+ git-nostr-hook install # Install global git hook
39
+ git-nostr-hook uninstall # Remove global git hook
40
+ git-nostr-hook run # Run manually
41
+ ```
42
+
43
+ ## How It Works
44
+
45
+ 1. Git runs `.git-hooks/post-commit` after every commit
46
+ 2. Hook reads private key from `git config nostr.privkey`
47
+ 3. Builds a Kind 30617 (NIP-34 repository announcement) event
48
+ 4. Signs with Schnorr signature
49
+ 5. Publishes to relays: `relay.damus.io`, `nos.lol`, `relay.nostr.band`
50
+
51
+ ## NIP-34
52
+
53
+ This implements [NIP-34](https://github.com/nostr-protocol/nips/blob/master/34.md) - Git repositories on Nostr.
54
+
55
+ Kind 30617 is a replaceable event, so each commit updates the previous announcement.
56
+
57
+ ## License
58
+
59
+ MIT
package/bin/cli.js ADDED
@@ -0,0 +1,131 @@
1
+ #!/usr/bin/env node
2
+
3
+ /**
4
+ * git-nostr-hook CLI
5
+ *
6
+ * Usage:
7
+ * git-nostr-hook install Install global git hook
8
+ * git-nostr-hook uninstall Remove global git hook
9
+ * git-nostr-hook run Run the hook manually
10
+ */
11
+
12
+ import { existsSync, mkdirSync, writeFileSync, unlinkSync, readFileSync } from 'fs';
13
+ import { execSync } from 'child_process';
14
+ import { dirname, join } from 'path';
15
+ import { fileURLToPath } from 'url';
16
+ import { homedir } from 'os';
17
+
18
+ const __dirname = dirname(fileURLToPath(import.meta.url));
19
+ const HOOKS_DIR = join(homedir(), '.git-hooks');
20
+ const HOOK_PATH = join(HOOKS_DIR, 'post-commit');
21
+
22
+ const HOOK_SCRIPT = `#!/usr/bin/env node
23
+ import { run } from 'git-nostr-hook/lib/hook.js';
24
+ run().catch(() => {});
25
+ `;
26
+
27
+ function install() {
28
+ console.log('Installing git-nostr-hook...\n');
29
+
30
+ // Create hooks directory
31
+ if (!existsSync(HOOKS_DIR)) {
32
+ mkdirSync(HOOKS_DIR, { recursive: true });
33
+ console.log(`✓ Created ${HOOKS_DIR}`);
34
+ }
35
+
36
+ // Create package.json for ES modules
37
+ const pkgPath = join(HOOKS_DIR, 'package.json');
38
+ if (!existsSync(pkgPath)) {
39
+ writeFileSync(pkgPath, JSON.stringify({
40
+ name: "git-hooks",
41
+ type: "module",
42
+ private: true
43
+ }, null, 2));
44
+ console.log('✓ Created package.json');
45
+ }
46
+
47
+ // Write the hook
48
+ writeFileSync(HOOK_PATH, HOOK_SCRIPT);
49
+ execSync(`chmod +x "${HOOK_PATH}"`);
50
+ console.log('✓ Installed post-commit hook');
51
+
52
+ // Set global hooks path
53
+ execSync('git config --global core.hooksPath ~/.git-hooks');
54
+ console.log('✓ Set global core.hooksPath');
55
+
56
+ console.log('\n✅ Installation complete!\n');
57
+ console.log('Next steps:');
58
+ console.log(' 1. Set your Nostr private key:');
59
+ console.log(' git config --global nostr.privkey <64-char-hex-key>\n');
60
+ console.log(' 2. Generate a key if needed:');
61
+ console.log(' npx noskey\n');
62
+ console.log(' 3. Make a commit in any repo to test!');
63
+ }
64
+
65
+ function uninstall() {
66
+ console.log('Uninstalling git-nostr-hook...\n');
67
+
68
+ // Remove the hook
69
+ if (existsSync(HOOK_PATH)) {
70
+ unlinkSync(HOOK_PATH);
71
+ console.log('✓ Removed post-commit hook');
72
+ }
73
+
74
+ // Unset global hooks path
75
+ try {
76
+ execSync('git config --global --unset core.hooksPath');
77
+ console.log('✓ Unset global core.hooksPath');
78
+ } catch {
79
+ // Already unset
80
+ }
81
+
82
+ console.log('\n✅ Uninstalled successfully!');
83
+ }
84
+
85
+ async function runHook() {
86
+ const { run } = await import('../lib/hook.js');
87
+ await run();
88
+ }
89
+
90
+ function help() {
91
+ console.log(`
92
+ git-nostr-hook v0.0.1
93
+
94
+ Publish repository state to Nostr (NIP-34) on every commit.
95
+
96
+ Usage:
97
+ git-nostr-hook install Install global git hook
98
+ git-nostr-hook uninstall Remove global git hook
99
+ git-nostr-hook run Run the hook manually
100
+ git-nostr-hook help Show this help
101
+
102
+ Setup:
103
+ git config --global nostr.privkey <hex-private-key>
104
+
105
+ Learn more: https://github.com/nostrapps/git-nostr-hook
106
+ `);
107
+ }
108
+
109
+ const command = process.argv[2];
110
+
111
+ switch (command) {
112
+ case 'install':
113
+ install();
114
+ break;
115
+ case 'uninstall':
116
+ uninstall();
117
+ break;
118
+ case 'run':
119
+ runHook().catch(err => {
120
+ console.error('Error:', err.message);
121
+ process.exit(1);
122
+ });
123
+ break;
124
+ case 'help':
125
+ case '--help':
126
+ case '-h':
127
+ help();
128
+ break;
129
+ default:
130
+ help();
131
+ }
package/lib/hook.js ADDED
@@ -0,0 +1,122 @@
1
+ #!/usr/bin/env node
2
+
3
+ /**
4
+ * Git post-commit hook that publishes repository state to Nostr (NIP-34)
5
+ *
6
+ * This hook:
7
+ * 1. Reads the private key from: git config nostr.privkey
8
+ * 2. Builds a Kind 30617 event (repository announcement)
9
+ * 3. Signs and publishes to Nostr relays
10
+ */
11
+
12
+ import { execSync } from 'child_process';
13
+ import { finalizeEvent } from 'nostr-tools/pure';
14
+ import { Relay } from 'nostr-tools/relay';
15
+
16
+ const RELAYS = [
17
+ 'wss://relay.damus.io',
18
+ 'wss://nos.lol',
19
+ 'wss://relay.nostr.band'
20
+ ];
21
+
22
+ function git(cmd) {
23
+ try {
24
+ return execSync(`git ${cmd}`, { encoding: 'utf8' }).trim();
25
+ } catch {
26
+ return null;
27
+ }
28
+ }
29
+
30
+ function getBranchRefs() {
31
+ const refs = [];
32
+ const branches = git('for-each-ref --format="%(refname:short) %(objectname)" refs/heads/');
33
+ if (branches) {
34
+ for (const line of branches.split('\n')) {
35
+ const [branch, sha] = line.split(' ');
36
+ if (branch && sha) {
37
+ refs.push([`refs/heads/${branch}`, sha]);
38
+ }
39
+ }
40
+ }
41
+ return refs;
42
+ }
43
+
44
+ function buildRepoEvent(secretKey) {
45
+ const repoName = git('rev-parse --show-toplevel')?.split('/').pop() || 'unknown';
46
+ const remoteUrl = git('remote get-url origin');
47
+ const currentBranch = git('symbolic-ref --short HEAD') || 'main';
48
+ const commitMsg = git('log -1 --pretty=%s');
49
+
50
+ const tags = [
51
+ ['d', repoName],
52
+ ['name', repoName],
53
+ ['HEAD', `ref: refs/heads/${currentBranch}`],
54
+ ];
55
+
56
+ if (remoteUrl) {
57
+ tags.push(['clone', remoteUrl]);
58
+ const webUrl = remoteUrl.replace(/\.git$/, '').replace('git@github.com:', 'https://github.com/');
59
+ if (webUrl.startsWith('https://')) {
60
+ tags.push(['web', webUrl]);
61
+ }
62
+ }
63
+
64
+ for (const ref of getBranchRefs()) {
65
+ tags.push(ref);
66
+ }
67
+
68
+ const event = {
69
+ kind: 30617,
70
+ created_at: Math.floor(Date.now() / 1000),
71
+ tags,
72
+ content: `Latest commit: ${commitMsg}`
73
+ };
74
+
75
+ return finalizeEvent(event, secretKey);
76
+ }
77
+
78
+ async function publishToRelay(relayUrl, event) {
79
+ try {
80
+ const relay = await Relay.connect(relayUrl);
81
+ await relay.publish(event);
82
+ console.log(`✓ Published to ${relayUrl}`);
83
+ relay.close();
84
+ return true;
85
+ } catch (err) {
86
+ console.log(`✗ Failed ${relayUrl}: ${err.message}`);
87
+ return false;
88
+ }
89
+ }
90
+
91
+ export async function run() {
92
+ console.log('\n📡 git-nostr-hook\n');
93
+
94
+ const privkeyHex = git('config nostr.privkey');
95
+ if (!privkeyHex) {
96
+ console.log('⚠ No nostr.privkey configured. Skipping.');
97
+ console.log(' Set with: git config nostr.privkey <hex-key>');
98
+ process.exit(0);
99
+ }
100
+
101
+ const secretKey = Uint8Array.from(Buffer.from(privkeyHex, 'hex'));
102
+ const event = buildRepoEvent(secretKey);
103
+
104
+ console.log('Event ID:', event.id);
105
+ console.log('Pubkey:', event.pubkey);
106
+ console.log('');
107
+
108
+ const results = await Promise.allSettled(
109
+ RELAYS.map(relay => publishToRelay(relay, event))
110
+ );
111
+
112
+ const succeeded = results.filter(r => r.status === 'fulfilled' && r.value).length;
113
+ console.log(`\n✓ Published to ${succeeded}/${RELAYS.length} relays\n`);
114
+ }
115
+
116
+ // Run if called directly
117
+ if (import.meta.url === `file://${process.argv[1]}`) {
118
+ run().catch(err => {
119
+ console.error('Error:', err.message);
120
+ process.exit(1);
121
+ });
122
+ }
package/package.json ADDED
@@ -0,0 +1,30 @@
1
+ {
2
+ "name": "git-nostr-hook",
3
+ "version": "0.0.1",
4
+ "description": "Git hook that publishes repository state to Nostr (NIP-34)",
5
+ "type": "module",
6
+ "bin": {
7
+ "git-nostr-hook": "./bin/cli.js"
8
+ },
9
+ "files": [
10
+ "bin/",
11
+ "lib/"
12
+ ],
13
+ "keywords": [
14
+ "git",
15
+ "nostr",
16
+ "nip34",
17
+ "hook",
18
+ "post-commit"
19
+ ],
20
+ "author": "nostrapps",
21
+ "license": "MIT",
22
+ "repository": {
23
+ "type": "git",
24
+ "url": "https://github.com/nostrapps/git-nostr-hook.git"
25
+ },
26
+ "homepage": "https://github.com/nostrapps/git-nostr-hook",
27
+ "dependencies": {
28
+ "nostr-tools": "^2.7.0"
29
+ }
30
+ }