open source is social v-it.org
at main 242 lines 9.1 kB view raw
1// SPDX-License-Identifier: MIT 2// Copyright (c) 2026 sol pbc 3 4import { existsSync, writeFileSync } from 'node:fs'; 5import { execSync } from 'node:child_process'; 6import { join } from 'node:path'; 7import { toBeacon } from '../lib/beacon.js'; 8import { vitDir, readProjectConfig, writeProjectConfig } from '../lib/vit-dir.js'; 9import { requireAgent } from '../lib/agent.js'; 10import { mark, name, DOT_VIT_README } from '../lib/brand.js'; 11import { jsonOk, jsonError } from '../lib/json-output.js'; 12 13export default function register(program) { 14 program 15 .command('init') 16 .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.') 17 .option('--beacon <url>', 'Git URL (or "." to read from git remote upstream/origin) to derive the beacon URI') 18 .option('--secondary <url>', 'Secondary beacon URL for upstream cap discovery') 19 .option('--json', 'Output as JSON') 20 .option('-v, --verbose', 'Show step-by-step details') 21 .action(async (opts) => { 22 try { 23 const gate = requireAgent(); 24 if (!gate.ok) { 25 if (opts.json) { 26 jsonError('agent required', 'run vit init from a coding agent'); 27 return; 28 } 29 console.error(`${name} init should be run by a coding agent (e.g. claude code, gemini cli).`); 30 console.error(`open your agent and ask it to run '${name} init' for you.`); 31 process.exitCode = 1; 32 return; 33 } 34 35 const { verbose } = opts; 36 const vlog = opts.json ? (...a) => console.error(...a) : console.log; 37 const dir = vitDir(); 38 if (verbose) vlog(`[verbose] .vit dir: ${dir}`); 39 40 if (!opts.beacon && !opts.secondary) { 41 const config = readProjectConfig(); 42 if (config.beacon) { 43 if (opts.json) { 44 const out = { beacon: config.beacon }; 45 if (config.secondaryBeacon) out.secondaryBeacon = config.secondaryBeacon; 46 jsonOk(out); 47 return; 48 } 49 console.log(`${mark} beacon: ${config.beacon}`); 50 if (config.secondaryBeacon) { 51 console.log(`${mark} secondary beacon: ${config.secondaryBeacon}`); 52 } 53 console.log(`hint: to change the beacon, run: ${name} init --beacon <git-url>`); 54 return; 55 } 56 57 let isGitRepo = false; 58 try { 59 execSync('git rev-parse --is-inside-work-tree', { 60 encoding: 'utf-8', 61 stdio: ['pipe', 'pipe', 'pipe'], 62 }); 63 isGitRepo = true; 64 } catch {} 65 if (verbose) vlog(`[verbose] in git repo: ${isGitRepo ? 'yes' : 'no'}`); 66 67 const hasVitDir = existsSync(dir); 68 if (!isGitRepo) { 69 let remotes = []; 70 if (opts.json) { 71 jsonOk({ 72 status: hasVitDir ? 'no beacon' : 'not initialized', 73 git: false, 74 remotes, 75 }); 76 return; 77 } 78 console.log(hasVitDir ? 'status: no beacon' : 'status: not initialized'); 79 console.log('git: false'); 80 if (hasVitDir) { 81 console.log(`hint: run: ${name} init --beacon <canonical-git-url>`); 82 } else { 83 console.log(`hint: run ${name} init from inside a git repository.`); 84 } 85 return; 86 } 87 88 console.log(hasVitDir ? 'status: no beacon' : 'status: not initialized'); 89 console.log('git: true'); 90 91 let remoteNames = []; 92 try { 93 remoteNames = execSync('git remote', { 94 encoding: 'utf-8', 95 stdio: ['pipe', 'pipe', 'pipe'], 96 }) 97 .trim() 98 .split('\n') 99 .filter(Boolean); 100 } catch { 101 remoteNames = []; 102 } 103 if (verbose) vlog(`[verbose] remotes detected: ${remoteNames.length > 0 ? remoteNames.join(', ') : 'none'}`); 104 105 const remotes = []; 106 for (const name of remoteNames) { 107 try { 108 const url = execSync(`git config --get remote.${name}.url`, { 109 encoding: 'utf-8', 110 stdio: ['pipe', 'pipe', 'pipe'], 111 }).trim(); 112 if (url) remotes.push({ name, url }); 113 } catch {} 114 } 115 if (verbose && remotes.length > 0) { 116 vlog(`[verbose] remote urls: ${remotes.map(r => `${r.name}=${r.url}`).join(' ')}`); 117 } 118 119 if (opts.json) { 120 jsonOk({ 121 status: hasVitDir ? 'no beacon' : 'not initialized', 122 git: true, 123 remotes: remotes.map(r => ({ name: r.name, url: r.url })), 124 }); 125 return; 126 } 127 128 const remotesDisplay = remotes.length > 0 129 ? remotes.map(remote => `${remote.name}=${remote.url}`).join(' ') 130 : 'none'; 131 console.log(`remotes: ${remotesDisplay}`); 132 133 const upstream = remotes.find(remote => remote.name === 'upstream'); 134 const origin = remotes.find(remote => remote.name === 'origin'); 135 if (upstream) { 136 console.log('hint: detected upstream remote. upstream points to the canonical repo.'); 137 console.log(`hint: run: ${name} init --beacon ${upstream.url}`); 138 } else if (origin) { 139 console.log(`hint: run: ${name} init --beacon ${origin.url}`); 140 } else { 141 console.log(`hint: no git remotes found. run: ${name} init --beacon <canonical-git-url>`); 142 } 143 return; 144 } 145 146 if (opts.secondary && !opts.beacon) { 147 const config = readProjectConfig(); 148 if (!config.beacon) { 149 if (opts.json) { 150 jsonError("no primary beacon set — run 'vit init --beacon <url>' first"); 151 return; 152 } 153 console.error("no primary beacon set — run 'vit init --beacon <url>' first"); 154 process.exitCode = 1; 155 return; 156 } 157 158 const secondary = 'vit:' + toBeacon(opts.secondary); 159 const merged = { ...config, secondaryBeacon: secondary }; 160 writeProjectConfig(merged); 161 if (opts.json) { 162 jsonOk({ beacon: merged.beacon, secondaryBeacon: merged.secondaryBeacon }); 163 return; 164 } 165 console.log(`${mark} beacon: ${merged.beacon}`); 166 console.log(`${mark} secondary beacon: ${merged.secondaryBeacon}`); 167 return; 168 } 169 170 let gitUrl = opts.beacon; 171 if (gitUrl === '.') { 172 if (verbose) vlog('[verbose] resolving --beacon . via remote.upstream.url then remote.origin.url'); 173 let usedRemote = ''; 174 try { 175 gitUrl = execSync('git config --get remote.upstream.url', { 176 encoding: 'utf-8', 177 stdio: ['pipe', 'pipe', 'pipe'], 178 }).trim(); 179 if (gitUrl) usedRemote = 'upstream'; 180 } catch { 181 gitUrl = ''; 182 } 183 184 if (!gitUrl) { 185 try { 186 gitUrl = execSync('git config --get remote.origin.url', { 187 encoding: 'utf-8', 188 stdio: ['pipe', 'pipe', 'pipe'], 189 }).trim(); 190 if (gitUrl) usedRemote = 'origin'; 191 } catch { 192 gitUrl = ''; 193 } 194 } 195 196 if (!gitUrl) { 197 if (opts.json) { 198 jsonError('no git remote found', 'set a remote or provide a git URL directly'); 199 return; 200 } 201 console.error('No git remote found. Set a remote or provide a git URL directly.'); 202 process.exitCode = 1; 203 return; 204 } 205 if (verbose) vlog(`[verbose] Read git remote ${usedRemote}: ${gitUrl}`); 206 } 207 208 const beacon = 'vit:' + toBeacon(gitUrl); 209 if (verbose) vlog(`[verbose] Computed beacon: ${beacon}`); 210 const existing = readProjectConfig(); 211 const merged = { ...existing, beacon }; 212 if (opts.secondary) { 213 merged.secondaryBeacon = 'vit:' + toBeacon(opts.secondary); 214 } 215 writeProjectConfig(merged); 216 if (verbose) vlog(`[verbose] Wrote config.json`); 217 const readmePath = join(vitDir(), 'README.md'); 218 if (!existsSync(readmePath)) { 219 writeFileSync(readmePath, DOT_VIT_README); 220 if (verbose) vlog(`[verbose] Wrote .vit/README.md`); 221 } 222 if (opts.json) { 223 const out = { beacon: merged.beacon }; 224 if (merged.secondaryBeacon) out.secondaryBeacon = merged.secondaryBeacon; 225 jsonOk(out); 226 return; 227 } 228 console.log(`${mark} beacon: ${beacon}`); 229 if (merged.secondaryBeacon) { 230 console.log(`${mark} secondary beacon: ${merged.secondaryBeacon}`); 231 } 232 } catch (err) { 233 const msg = err instanceof Error ? err.message : String(err); 234 if (opts.json) { 235 jsonError(msg); 236 return; 237 } 238 console.error(msg); 239 process.exitCode = 1; 240 } 241 }); 242}