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.
- package/README.md +59 -0
- package/bin/cli.js +131 -0
- package/lib/hook.js +122 -0
- 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
|
+
}
|