open source is social v-it.org
0
fork

Configure Feed

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

Merge branch 'hopper-f3egwvkx-init-agent-ux'

+169 -15
+91 -10
src/cmd/init.js
··· 11 11 program 12 12 .command('init') 13 13 .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.') 14 - .option('--beacon <url>', 'Git URL (or "." to read from git remote origin) to derive the beacon URI') 14 + .option('--beacon <url>', 'Git URL (or "." to read from git remote upstream/origin) to derive the beacon URI') 15 15 .option('-v, --verbose', 'Show step-by-step details') 16 16 .action(async (opts) => { 17 17 try { ··· 31 31 const config = readProjectConfig(); 32 32 if (config.beacon) { 33 33 console.log(`beacon: ${config.beacon}`); 34 - } else if (existsSync(dir)) { 35 - console.log('beacon: not set'); 34 + console.log('hint: to change the beacon, run: vit init --beacon <git-url>'); 35 + return; 36 + } 37 + 38 + let isGitRepo = false; 39 + try { 40 + execSync('git rev-parse --is-inside-work-tree', { 41 + encoding: 'utf-8', 42 + stdio: ['pipe', 'pipe', 'pipe'], 43 + }); 44 + isGitRepo = true; 45 + } catch {} 46 + if (verbose) console.log(`[verbose] in git repo: ${isGitRepo ? 'yes' : 'no'}`); 47 + 48 + const hasVitDir = existsSync(dir); 49 + if (!isGitRepo) { 50 + console.log(hasVitDir ? 'status: no beacon' : 'status: not initialized'); 51 + console.log('git: false'); 52 + if (hasVitDir) { 53 + console.log('hint: run: vit init --beacon <canonical-git-url>'); 54 + } else { 55 + console.log('hint: run vit init from inside a git repository.'); 56 + } 57 + return; 58 + } 59 + 60 + console.log(hasVitDir ? 'status: no beacon' : 'status: not initialized'); 61 + console.log('git: true'); 62 + 63 + let remoteNames = []; 64 + try { 65 + remoteNames = execSync('git remote', { 66 + encoding: 'utf-8', 67 + stdio: ['pipe', 'pipe', 'pipe'], 68 + }) 69 + .trim() 70 + .split('\n') 71 + .filter(Boolean); 72 + } catch { 73 + remoteNames = []; 74 + } 75 + if (verbose) console.log(`[verbose] remotes detected: ${remoteNames.length > 0 ? remoteNames.join(', ') : 'none'}`); 76 + 77 + const remotes = []; 78 + for (const name of remoteNames) { 79 + try { 80 + const url = execSync(`git config --get remote.${name}.url`, { 81 + encoding: 'utf-8', 82 + stdio: ['pipe', 'pipe', 'pipe'], 83 + }).trim(); 84 + if (url) remotes.push({ name, url }); 85 + } catch {} 86 + } 87 + if (verbose && remotes.length > 0) { 88 + console.log(`[verbose] remote urls: ${remotes.map(r => `${r.name}=${r.url}`).join(' ')}`); 89 + } 90 + 91 + const remotesDisplay = remotes.length > 0 92 + ? remotes.map(remote => `${remote.name}=${remote.url}`).join(' ') 93 + : 'none'; 94 + console.log(`remotes: ${remotesDisplay}`); 95 + 96 + const upstream = remotes.find(remote => remote.name === 'upstream'); 97 + const origin = remotes.find(remote => remote.name === 'origin'); 98 + if (upstream) { 99 + console.log('hint: detected upstream remote. upstream points to the canonical repo.'); 100 + console.log(`hint: run: vit init --beacon ${upstream.url}`); 101 + } else if (origin) { 102 + console.log(`hint: run: vit init --beacon ${origin.url}`); 36 103 } else { 37 - console.log('.vit directory not found'); 104 + console.log('hint: no git remotes found. run: vit init --beacon <canonical-git-url>'); 38 105 } 39 106 return; 40 107 } 41 108 42 109 let gitUrl = opts.beacon; 43 110 if (gitUrl === '.') { 111 + if (verbose) console.log('[verbose] resolving --beacon . via remote.upstream.url then remote.origin.url'); 112 + let usedRemote = ''; 44 113 try { 45 - gitUrl = execSync('git config --get remote.origin.url', { 114 + gitUrl = execSync('git config --get remote.upstream.url', { 46 115 encoding: 'utf-8', 47 116 stdio: ['pipe', 'pipe', 'pipe'], 48 117 }).trim(); 49 - if (verbose) console.log(`[verbose] Read git remote origin: ${gitUrl}`); 118 + if (gitUrl) usedRemote = 'upstream'; 50 119 } catch { 51 - console.error('No git remote origin found. Set a remote or provide a git URL directly.'); 52 - process.exitCode = 1; 53 - return; 120 + gitUrl = ''; 121 + } 122 + 123 + if (!gitUrl) { 124 + try { 125 + gitUrl = execSync('git config --get remote.origin.url', { 126 + encoding: 'utf-8', 127 + stdio: ['pipe', 'pipe', 'pipe'], 128 + }).trim(); 129 + if (gitUrl) usedRemote = 'origin'; 130 + } catch { 131 + gitUrl = ''; 132 + } 54 133 } 134 + 55 135 if (!gitUrl) { 56 - console.error('No git remote origin found. Set a remote or provide a git URL directly.'); 136 + console.error('No git remote found. Set a remote or provide a git URL directly.'); 57 137 process.exitCode = 1; 58 138 return; 59 139 } 140 + if (verbose) console.log(`[verbose] Read git remote ${usedRemote}: ${gitUrl}`); 60 141 } 61 142 62 143 const beacon = 'vit:' + toBeacon(gitUrl);
+78 -5
test/init.test.js
··· 8 8 import { join } from 'node:path'; 9 9 import { execSync } from 'node:child_process'; 10 10 11 - describe('vit init --beacon', () => { 11 + describe('vit init', () => { 12 12 let tmpDir; 13 13 14 14 beforeEach(() => { ··· 72 72 run('init --beacon https://github.com/solpbc/vit.git', tmpDir, { CLAUDECODE: '1' }); 73 73 const result = run('init', tmpDir, { CLAUDECODE: '1' }); 74 74 expect(result.exitCode).toBe(0); 75 - expect(result.stdout).toBe('beacon: vit:github.com/solpbc/vit'); 75 + expect(result.stdout).toContain('beacon: vit:github.com/solpbc/vit'); 76 + expect(result.stdout).toContain('hint: to change the beacon, run: vit init --beacon <git-url>'); 76 77 }); 77 78 78 - test('reports not set when no flag and .vit exists but no beacon', () => { 79 + test('reports no beacon when .vit exists but directory is not a git repo', () => { 79 80 mkdirSync(join(tmpDir, '.vit'), { recursive: true }); 80 81 const result = run('init', tmpDir, { CLAUDECODE: '1' }); 81 82 expect(result.exitCode).toBe(0); 82 - expect(result.stdout).toBe('beacon: not set'); 83 + expect(result.stdout).toContain('status: no beacon'); 84 + expect(result.stdout).toContain('git: false'); 85 + expect(result.stdout).toContain('hint: run: vit init --beacon <canonical-git-url>'); 83 86 }); 84 87 85 88 test('reports .vit not found when no flag and no .vit dir', () => { 86 89 const result = run('init', tmpDir, { CLAUDECODE: '1' }); 87 90 expect(result.exitCode).toBe(0); 88 - expect(result.stdout).toBe('.vit directory not found'); 91 + expect(result.stdout).toContain('status: not initialized'); 92 + expect(result.stdout).toContain('git: false'); 93 + expect(result.stdout).toContain('hint: run vit init from inside a git repository'); 94 + }); 95 + 96 + test('guides agent in fork repo with upstream and origin remotes', () => { 97 + execSync('git init', { cwd: tmpDir, stdio: 'pipe' }); 98 + execSync('git remote add origin https://github.com/agent/vit.git', { cwd: tmpDir, stdio: 'pipe' }); 99 + execSync('git remote add upstream https://github.com/solpbc/vit.git', { cwd: tmpDir, stdio: 'pipe' }); 100 + 101 + const result = run('init', tmpDir, { CLAUDECODE: '1' }); 102 + expect(result.exitCode).toBe(0); 103 + expect(result.stdout).toContain('status: not initialized'); 104 + expect(result.stdout).toContain('git: true'); 105 + expect(result.stdout).toContain('origin='); 106 + expect(result.stdout).toContain('upstream='); 107 + expect(result.stdout).toContain('hint: detected upstream remote'); 108 + expect(result.stdout).toContain('vit init --beacon https://github.com/solpbc/vit.git'); 109 + }); 110 + 111 + test('guides agent in repo with only origin remote', () => { 112 + execSync('git init', { cwd: tmpDir, stdio: 'pipe' }); 113 + execSync('git remote add origin https://github.com/solpbc/vit.git', { cwd: tmpDir, stdio: 'pipe' }); 114 + 115 + const result = run('init', tmpDir, { CLAUDECODE: '1' }); 116 + expect(result.exitCode).toBe(0); 117 + expect(result.stdout).toContain('status: not initialized'); 118 + expect(result.stdout).toContain('git: true'); 119 + expect(result.stdout).toContain('origin='); 120 + expect(result.stdout).toContain('vit init --beacon https://github.com/solpbc/vit.git'); 121 + }); 122 + 123 + test('guides agent in git repo with no remotes', () => { 124 + execSync('git init', { cwd: tmpDir, stdio: 'pipe' }); 125 + 126 + const result = run('init', tmpDir, { CLAUDECODE: '1' }); 127 + expect(result.exitCode).toBe(0); 128 + expect(result.stdout).toContain('status: not initialized'); 129 + expect(result.stdout).toContain('git: true'); 130 + expect(result.stdout).toContain('remotes: none'); 131 + expect(result.stdout).toContain('hint: no git remotes found'); 132 + }); 133 + 134 + test('guides agent in git repo with .vit but no beacon', () => { 135 + execSync('git init', { cwd: tmpDir, stdio: 'pipe' }); 136 + mkdirSync(join(tmpDir, '.vit'), { recursive: true }); 137 + 138 + const result = run('init', tmpDir, { CLAUDECODE: '1' }); 139 + expect(result.exitCode).toBe(0); 140 + expect(result.stdout).toContain('status: no beacon'); 141 + expect(result.stdout).toContain('git: true'); 142 + expect(result.stdout).toContain('remotes: none'); 143 + expect(result.stdout).toContain('hint: no git remotes found'); 144 + }); 145 + 146 + test('--beacon . prefers upstream over origin in fork', () => { 147 + execSync('git init', { cwd: tmpDir, stdio: 'pipe' }); 148 + execSync('git remote add origin https://github.com/agent/fork.git', { cwd: tmpDir, stdio: 'pipe' }); 149 + execSync('git remote add upstream https://github.com/solpbc/vit.git', { cwd: tmpDir, stdio: 'pipe' }); 150 + 151 + const result = run('init --beacon .', tmpDir, { CLAUDECODE: '1' }); 152 + expect(result.exitCode).toBe(0); 153 + expect(result.stdout).toBe('beacon: vit:github.com/solpbc/vit'); 154 + }); 155 + 156 + test('shows guidance for already initialized repo', () => { 157 + run('init --beacon https://github.com/solpbc/vit.git', tmpDir, { CLAUDECODE: '1' }); 158 + const result = run('init', tmpDir, { CLAUDECODE: '1' }); 159 + expect(result.exitCode).toBe(0); 160 + expect(result.stdout).toContain('beacon: vit:github.com/solpbc/vit'); 161 + expect(result.stdout).toContain('hint: to change the beacon'); 89 162 }); 90 163 91 164 test('errors on invalid git URL', () => {