open source is social v-it.org
0
fork

Configure Feed

Select the types of activity you want to include in your feed.

Add --beacon flag to vit init for .vit/beacon management

- vit init --beacon <git-url> derives a beacon URI and writes it to .vit/beacon
- vit init --beacon . reads the git remote origin URL automatically
- vit init (no flag) reports current beacon status
- vit doctor now reports beacon URI alongside existing checks
- Tests cover URL conversion, git remote reading, error cases, and status reporting

+164 -8
+9 -1
src/cmd/doctor.js
··· 1 1 // SPDX-License-Identifier: AGPL-3.0-only 2 2 // Copyright (c) 2026 sol pbc 3 3 4 - import { existsSync } from 'node:fs'; 4 + import { existsSync, readFileSync } from 'node:fs'; 5 5 import { join } from 'node:path'; 6 6 import { loadConfig } from '../lib/config.js'; 7 7 ··· 24 24 console.log('.vit: found'); 25 25 } else { 26 26 console.log('.vit: not found'); 27 + } 28 + 29 + const beaconPath = join(vitDir, 'beacon'); 30 + if (existsSync(beaconPath)) { 31 + const uri = readFileSync(beaconPath, 'utf-8').trim(); 32 + console.log(`beacon: ${uri}`); 33 + } else { 34 + console.log('beacon: not set'); 27 35 } 28 36 } catch (err) { 29 37 console.error(err.message);
+44 -7
src/cmd/init.js
··· 1 1 // SPDX-License-Identifier: AGPL-3.0-only 2 2 // Copyright (c) 2026 sol pbc 3 3 4 - import { existsSync } from 'node:fs'; 4 + import { existsSync, mkdirSync, readFileSync, writeFileSync } from 'node:fs'; 5 + import { execSync } from 'node:child_process'; 5 6 import { join } from 'node:path'; 7 + import { toBeacon } from '../lib/beacon.js'; 6 8 7 9 export default function register(program) { 8 10 program 9 11 .command('init') 10 - .description('Check for local .vit directory') 11 - .action(async () => { 12 + .description('Initialize .vit directory and set project beacon. Use the most official upstream or well-known git URL so all contributors converge on the same beacon.') 13 + .option('--beacon <url>', 'Git URL (or "." to read from git remote origin) to derive the beacon URI') 14 + .action(async (opts) => { 12 15 try { 13 16 const vitDir = join(process.cwd(), '.vit'); 14 - if (existsSync(vitDir)) { 15 - console.log('.vit directory found'); 16 - } else { 17 - console.log('.vit directory not found'); 17 + const beaconPath = join(vitDir, 'beacon'); 18 + 19 + if (!opts.beacon) { 20 + // No --beacon flag: report status 21 + if (existsSync(beaconPath)) { 22 + const uri = readFileSync(beaconPath, 'utf-8').trim(); 23 + console.log(`beacon: ${uri}`); 24 + } else if (existsSync(vitDir)) { 25 + console.log('beacon: not set'); 26 + } else { 27 + console.log('.vit directory not found'); 28 + } 29 + return; 30 + } 31 + 32 + // Resolve git URL 33 + let gitUrl = opts.beacon; 34 + if (gitUrl === '.') { 35 + try { 36 + gitUrl = execSync('git config --get remote.origin.url', { 37 + encoding: 'utf-8', 38 + stdio: ['pipe', 'pipe', 'pipe'], 39 + }).trim(); 40 + } catch { 41 + console.error('No git remote origin found. Set a remote or provide a git URL directly.'); 42 + process.exitCode = 1; 43 + return; 44 + } 45 + if (!gitUrl) { 46 + console.error('No git remote origin found. Set a remote or provide a git URL directly.'); 47 + process.exitCode = 1; 48 + return; 49 + } 18 50 } 51 + 52 + const beacon = 'vit:' + toBeacon(gitUrl); 53 + mkdirSync(vitDir, { recursive: true }); 54 + writeFileSync(beaconPath, beacon + '\n'); 55 + console.log(`beacon: ${beacon}`); 19 56 } catch (err) { 20 57 console.error(err.message); 21 58 process.exitCode = 1;
+111
test/init.test.js
··· 1 + // SPDX-License-Identifier: AGPL-3.0-only 2 + // Copyright (c) 2026 sol pbc 3 + 4 + import { describe, test, expect, beforeEach, afterEach } from 'bun:test'; 5 + import { mkdirSync, rmSync, readFileSync, existsSync } from 'node:fs'; 6 + import { tmpdir } from 'node:os'; 7 + import { join } from 'node:path'; 8 + import { execSync } from 'node:child_process'; 9 + 10 + const vitBin = join(import.meta.dir, '..', 'bin', 'vit.js'); 11 + 12 + function run(args, cwd) { 13 + try { 14 + return { 15 + stdout: execSync(`bun ${vitBin} ${args}`, { cwd, encoding: 'utf-8', stdio: ['pipe', 'pipe', 'pipe'] }).trim(), 16 + exitCode: 0, 17 + }; 18 + } catch (err) { 19 + return { 20 + stdout: (err.stdout || '').trim(), 21 + stderr: (err.stderr || '').trim(), 22 + exitCode: err.status, 23 + }; 24 + } 25 + } 26 + 27 + describe('vit init --beacon', () => { 28 + let tmpDir; 29 + 30 + beforeEach(() => { 31 + tmpDir = join(tmpdir(), '.test-tmp-' + Math.random().toString(36).slice(2)); 32 + mkdirSync(tmpDir, { recursive: true }); 33 + }); 34 + 35 + afterEach(() => { 36 + rmSync(tmpDir, { recursive: true, force: true }); 37 + }); 38 + 39 + test('writes beacon from HTTPS URL', () => { 40 + const result = run('init --beacon https://github.com/solpbc/vit.git', tmpDir); 41 + expect(result.exitCode).toBe(0); 42 + expect(result.stdout).toBe('beacon: vit:github.com/solpbc/vit'); 43 + 44 + const content = readFileSync(join(tmpDir, '.vit', 'beacon'), 'utf-8'); 45 + expect(content).toBe('vit:github.com/solpbc/vit\n'); 46 + }); 47 + 48 + test('writes beacon from SSH URL', () => { 49 + const result = run('init --beacon git@github.com:solpbc/vit.git', tmpDir); 50 + expect(result.exitCode).toBe(0); 51 + expect(result.stdout).toBe('beacon: vit:github.com/solpbc/vit'); 52 + 53 + const content = readFileSync(join(tmpDir, '.vit', 'beacon'), 'utf-8'); 54 + expect(content).toBe('vit:github.com/solpbc/vit\n'); 55 + }); 56 + 57 + test('creates .vit directory if missing', () => { 58 + expect(existsSync(join(tmpDir, '.vit'))).toBe(false); 59 + run('init --beacon https://github.com/solpbc/vit.git', tmpDir); 60 + expect(existsSync(join(tmpDir, '.vit'))).toBe(true); 61 + }); 62 + 63 + test('overwrites existing beacon silently', () => { 64 + run('init --beacon https://github.com/old/repo.git', tmpDir); 65 + run('init --beacon https://github.com/solpbc/vit.git', tmpDir); 66 + 67 + const content = readFileSync(join(tmpDir, '.vit', 'beacon'), 'utf-8'); 68 + expect(content).toBe('vit:github.com/solpbc/vit\n'); 69 + }); 70 + 71 + test('reads beacon from git remote with --beacon .', () => { 72 + // Set up a git repo with a remote origin 73 + execSync('git init', { cwd: tmpDir, stdio: 'pipe' }); 74 + execSync('git remote add origin https://github.com/solpbc/vit.git', { cwd: tmpDir, stdio: 'pipe' }); 75 + 76 + const result = run('init --beacon .', tmpDir); 77 + expect(result.exitCode).toBe(0); 78 + expect(result.stdout).toBe('beacon: vit:github.com/solpbc/vit'); 79 + }); 80 + 81 + test('errors when --beacon . has no git remote', () => { 82 + // tmpDir is not a git repo 83 + const result = run('init --beacon .', tmpDir); 84 + expect(result.exitCode).not.toBe(0); 85 + }); 86 + 87 + test('reports beacon when no flag and beacon exists', () => { 88 + run('init --beacon https://github.com/solpbc/vit.git', tmpDir); 89 + const result = run('init', tmpDir); 90 + expect(result.exitCode).toBe(0); 91 + expect(result.stdout).toBe('beacon: vit:github.com/solpbc/vit'); 92 + }); 93 + 94 + test('reports not set when no flag and .vit exists but no beacon', () => { 95 + mkdirSync(join(tmpDir, '.vit'), { recursive: true }); 96 + const result = run('init', tmpDir); 97 + expect(result.exitCode).toBe(0); 98 + expect(result.stdout).toBe('beacon: not set'); 99 + }); 100 + 101 + test('reports .vit not found when no flag and no .vit dir', () => { 102 + const result = run('init', tmpDir); 103 + expect(result.exitCode).toBe(0); 104 + expect(result.stdout).toBe('.vit directory not found'); 105 + }); 106 + 107 + test('errors on invalid git URL', () => { 108 + const result = run('init --beacon notaurl', tmpDir); 109 + expect(result.exitCode).not.toBe(0); 110 + }); 111 + });