open source is social v-it.org
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}