open source is social v-it.org
0
fork

Configure Feed

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

Replace .vit/beacon with .vit/config.json and add JSONL logging

Migrate project-level beacon storage from a plain-text .vit/beacon
file to a structured .vit/config.json (JSON with "beacon" key).
Add shared vit-dir.js module for project config read/write and
JSONL append. Update init, doctor, and beacon commands. Add
caps.jsonl append to ship on successful PDS write. Update
.gitignore to track config.json while ignoring local state.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

+147 -39
+4
.gitignore
··· 9 9 # Environment 10 10 .env 11 11 bsky_session.json 12 + 13 + # vit project state (user-local) 14 + .vit/* 15 + !.vit/config.json
+1 -1
README.md
··· 18 18 19 19 ## Terminology 20 20 21 - - **beacon** — a git repo that uniquely represents a project for all vit users; all project groupings are based on a single beacon (one unified "upstream"); git urls are canonicalized into a uniform beacon; stored in `.vit/` 21 + - **beacon** — a git repo that uniquely represents a project for all vit users; all project groupings are based on a single beacon (one unified "upstream"); git urls are canonicalized into a uniform beacon; stored in `.vit/config.json` 22 22 - **init** — check environment readiness and configure vit for first use; alias: `doctor` 23 23 - **adopt** — adopt an existing project by its beacon; forks or clones the repo 24 24 - **follow** — atproto handles; stored in local project `.vit/`
+1 -1
VOCAB.md
··· 11 11 - Unifies forks and mirrors under one canonical reference 12 12 - Anchors feeds and cap lineage 13 13 - Defines project scope 14 - - Stored locally in `.vit/` 14 + - Stored in `.vit/config.json` 15 15 16 16 **Related Concepts** 17 17 - Beacon ID / URI — canonical identifier
+1 -1
skills/vit/SKILL.md
··· 67 67 68 68 ## Configuration 69 69 70 - - **`.vit/`** — local project directory, stores beacon and follows 70 + - **`.vit/`** — local project directory, stores config.json (beacon) and local state (JSONL logs) 71 71 - **`vit.json`** — credentials (`did`, `access_token`, etc.) and config, written by `vit login` and `vit config` 72 72 - **`vit config`** — read/write `vit.json` user-level config
+9 -4
src/cmd/beacon.js
··· 23 23 export default function register(program) { 24 24 program 25 25 .command('beacon') 26 - .description('Probe a remote repo for a .vit/beacon file') 26 + .description('Probe a remote repo for its beacon') 27 27 .argument('<target>', 'vit: URI or git URL to probe') 28 28 .action(async (target) => { 29 29 try { ··· 35 35 36 36 const head = await git.resolveRef({ fs, dir, ref: 'HEAD' }); 37 37 const commit = await git.readObject({ fs, dir, oid: head, format: 'parsed' }); 38 - const content = await readTreeFile(fs, dir, commit.object.tree, ['.vit', 'beacon']); 38 + const content = await readTreeFile(fs, dir, commit.object.tree, ['.vit', 'config.json']); 39 + 40 + let beacon; 41 + try { 42 + beacon = content && JSON.parse(content).beacon; 43 + } catch {} 39 44 40 - if (content && content.trim()) { 41 - console.log('beacon: lit ' + content.trim()); 45 + if (beacon) { 46 + console.log('beacon: lit ' + beacon); 42 47 } else { 43 48 console.log('beacon: unlit'); 44 49 }
+4 -13
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, readFileSync } from 'node:fs'; 5 - import { join } from 'node:path'; 6 4 import { loadConfig } from '../lib/config.js'; 5 + import { readProjectConfig } from '../lib/vit-dir.js'; 7 6 8 7 export default function register(program) { 9 8 program ··· 19 18 console.log('setup: not done (run vit setup)'); 20 19 } 21 20 22 - const vitDir = join(process.cwd(), '.vit'); 23 - if (existsSync(vitDir)) { 24 - console.log('.vit: found'); 25 - } else { 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}`); 21 + const projConfig = readProjectConfig(); 22 + if (projConfig.beacon) { 23 + console.log(`beacon: ${projConfig.beacon}`); 33 24 } else { 34 25 console.log('beacon: not set'); 35 26 }
+8 -12
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, mkdirSync, readFileSync, writeFileSync } from 'node:fs'; 4 + import { existsSync } from 'node:fs'; 5 5 import { execSync } from 'node:child_process'; 6 - import { join } from 'node:path'; 7 6 import { toBeacon } from '../lib/beacon.js'; 7 + import { vitDir, readProjectConfig, writeProjectConfig } from '../lib/vit-dir.js'; 8 8 9 9 export default function register(program) { 10 10 program ··· 13 13 .option('--beacon <url>', 'Git URL (or "." to read from git remote origin) to derive the beacon URI') 14 14 .action(async (opts) => { 15 15 try { 16 - const vitDir = join(process.cwd(), '.vit'); 17 - const beaconPath = join(vitDir, 'beacon'); 16 + const dir = vitDir(); 18 17 19 18 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)) { 19 + const config = readProjectConfig(); 20 + if (config.beacon) { 21 + console.log(`beacon: ${config.beacon}`); 22 + } else if (existsSync(dir)) { 25 23 console.log('beacon: not set'); 26 24 } else { 27 25 console.log('.vit directory not found'); ··· 29 27 return; 30 28 } 31 29 32 - // Resolve git URL 33 30 let gitUrl = opts.beacon; 34 31 if (gitUrl === '.') { 35 32 try { ··· 50 47 } 51 48 52 49 const beacon = 'vit:' + toBeacon(gitUrl); 53 - mkdirSync(vitDir, { recursive: true }); 54 - writeFileSync(beaconPath, beacon + '\n'); 50 + writeProjectConfig({ beacon }); 55 51 console.log(`beacon: ${beacon}`); 56 52 } catch (err) { 57 53 console.error(err.message);
+14
src/cmd/ship.js
··· 5 5 import { TID } from '@atproto/common-web'; 6 6 import { loadEnv } from '../lib/env.js'; 7 7 import { createOAuthClient, createSessionStore } from '../lib/oauth.js'; 8 + import { appendLog } from '../lib/vit-dir.js'; 8 9 9 10 export default function register(program) { 10 11 program ··· 45 46 validate: false, 46 47 }; 47 48 const putRes = await agent.com.atproto.repo.putRecord(putArgs); 49 + try { 50 + appendLog('caps.jsonl', { 51 + ts: new Date().toISOString(), 52 + did, 53 + rkey, 54 + collection: 'org.v-it.cap', 55 + pds: session.serverMetadata?.issuer, 56 + uri: putRes.data.uri, 57 + cid: putRes.data.cid, 58 + }); 59 + } catch (logErr) { 60 + console.error('warning: failed to write caps.jsonl:', logErr.message); 61 + } 48 62 console.log( 49 63 JSON.stringify({ 50 64 ts: new Date().toISOString(),
+31
src/lib/vit-dir.js
··· 1 + // SPDX-License-Identifier: AGPL-3.0-only 2 + // Copyright (c) 2026 sol pbc 3 + 4 + import { existsSync, mkdirSync, readFileSync, writeFileSync, appendFileSync } from 'node:fs'; 5 + import { join } from 'node:path'; 6 + 7 + export function vitDir() { 8 + return join(process.cwd(), '.vit'); 9 + } 10 + 11 + export function readProjectConfig() { 12 + const p = join(vitDir(), 'config.json'); 13 + if (!existsSync(p)) return {}; 14 + try { 15 + return JSON.parse(readFileSync(p, 'utf-8')); 16 + } catch { 17 + return {}; 18 + } 19 + } 20 + 21 + export function writeProjectConfig(obj) { 22 + const dir = vitDir(); 23 + mkdirSync(dir, { recursive: true }); 24 + writeFileSync(join(dir, 'config.json'), JSON.stringify(obj, null, 2) + '\n'); 25 + } 26 + 27 + export function appendLog(filename, record) { 28 + const dir = vitDir(); 29 + mkdirSync(dir, { recursive: true }); 30 + appendFileSync(join(dir, filename), JSON.stringify(record) + '\n'); 31 + }
+1 -1
test/beacon-cmd.test.js
··· 23 23 } 24 24 25 25 describe('vit beacon', () => { 26 - test('probes a public repo without .vit/beacon (unlit)', () => { 26 + test('probes a public repo without .vit/config.json beacon (unlit)', () => { 27 27 const result = run('beacon https://github.com/octocat/Hello-World.git'); 28 28 expect(result.exitCode).toBe(0); 29 29 expect(result.stdout).toBe('beacon: unlit');
+6 -6
test/init.test.js
··· 41 41 expect(result.exitCode).toBe(0); 42 42 expect(result.stdout).toBe('beacon: vit:github.com/solpbc/vit'); 43 43 44 - const content = readFileSync(join(tmpDir, '.vit', 'beacon'), 'utf-8'); 45 - expect(content).toBe('vit:github.com/solpbc/vit\n'); 44 + const content = readFileSync(join(tmpDir, '.vit', 'config.json'), 'utf-8'); 45 + expect(JSON.parse(content).beacon).toBe('vit:github.com/solpbc/vit'); 46 46 }); 47 47 48 48 test('writes beacon from SSH URL', () => { ··· 50 50 expect(result.exitCode).toBe(0); 51 51 expect(result.stdout).toBe('beacon: vit:github.com/solpbc/vit'); 52 52 53 - const content = readFileSync(join(tmpDir, '.vit', 'beacon'), 'utf-8'); 54 - expect(content).toBe('vit:github.com/solpbc/vit\n'); 53 + const content = readFileSync(join(tmpDir, '.vit', 'config.json'), 'utf-8'); 54 + expect(JSON.parse(content).beacon).toBe('vit:github.com/solpbc/vit'); 55 55 }); 56 56 57 57 test('creates .vit directory if missing', () => { ··· 64 64 run('init --beacon https://github.com/old/repo.git', tmpDir); 65 65 run('init --beacon https://github.com/solpbc/vit.git', tmpDir); 66 66 67 - const content = readFileSync(join(tmpDir, '.vit', 'beacon'), 'utf-8'); 68 - expect(content).toBe('vit:github.com/solpbc/vit\n'); 67 + const content = readFileSync(join(tmpDir, '.vit', 'config.json'), 'utf-8'); 68 + expect(JSON.parse(content).beacon).toBe('vit:github.com/solpbc/vit'); 69 69 }); 70 70 71 71 test('reads beacon from git remote with --beacon .', () => {
+67
test/vit-dir.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 + 9 + // vit-dir functions use process.cwd(), so we save/restore it 10 + const originalCwd = process.cwd(); 11 + 12 + describe('vit-dir', () => { 13 + let tmpDir; 14 + 15 + beforeEach(() => { 16 + tmpDir = join(tmpdir(), '.test-vit-dir-' + Math.random().toString(36).slice(2)); 17 + mkdirSync(tmpDir, { recursive: true }); 18 + process.chdir(tmpDir); 19 + }); 20 + 21 + afterEach(() => { 22 + process.chdir(originalCwd); 23 + rmSync(tmpDir, { recursive: true, force: true }); 24 + }); 25 + 26 + test('writeProjectConfig creates .vit/ and config.json', async () => { 27 + const { writeProjectConfig } = await import('../src/lib/vit-dir.js'); 28 + writeProjectConfig({ beacon: 'vit:github.com/org/repo' }); 29 + expect(existsSync(join(tmpDir, '.vit'))).toBe(true); 30 + const content = readFileSync(join(tmpDir, '.vit', 'config.json'), 'utf-8'); 31 + const parsed = JSON.parse(content); 32 + expect(parsed.beacon).toBe('vit:github.com/org/repo'); 33 + }); 34 + 35 + test('readProjectConfig reads written config', async () => { 36 + const { writeProjectConfig, readProjectConfig } = await import('../src/lib/vit-dir.js'); 37 + writeProjectConfig({ beacon: 'vit:github.com/org/repo' }); 38 + const config = readProjectConfig(); 39 + expect(config.beacon).toBe('vit:github.com/org/repo'); 40 + }); 41 + 42 + test('readProjectConfig returns {} when file missing', async () => { 43 + const { readProjectConfig } = await import('../src/lib/vit-dir.js'); 44 + const config = readProjectConfig(); 45 + expect(config).toEqual({}); 46 + }); 47 + 48 + test('appendLog creates file and appends JSONL line', async () => { 49 + const { appendLog } = await import('../src/lib/vit-dir.js'); 50 + appendLog('caps.jsonl', { ts: '2026-01-01T00:00:00Z', did: 'did:plc:test' }); 51 + const content = readFileSync(join(tmpDir, '.vit', 'caps.jsonl'), 'utf-8'); 52 + const lines = content.trim().split('\n'); 53 + expect(lines.length).toBe(1); 54 + expect(JSON.parse(lines[0]).did).toBe('did:plc:test'); 55 + }); 56 + 57 + test('appendLog appends to existing file', async () => { 58 + const { appendLog } = await import('../src/lib/vit-dir.js'); 59 + appendLog('caps.jsonl', { ts: '2026-01-01T00:00:00Z', n: 1 }); 60 + appendLog('caps.jsonl', { ts: '2026-01-02T00:00:00Z', n: 2 }); 61 + const content = readFileSync(join(tmpDir, '.vit', 'caps.jsonl'), 'utf-8'); 62 + const lines = content.trim().split('\n'); 63 + expect(lines.length).toBe(2); 64 + expect(JSON.parse(lines[0]).n).toBe(1); 65 + expect(JSON.parse(lines[1]).n).toBe(2); 66 + }); 67 + });