open source is social v-it.org
1// SPDX-License-Identifier: MIT
2// Copyright (c) 2026 sol pbc
3
4import { loadConfig } from '../lib/config.js';
5import { restoreAgent } from '../lib/oauth.js';
6import { readProjectConfig } from '../lib/vit-dir.js';
7import { existsSync, lstatSync, readdirSync, readFileSync } from 'node:fs';
8import { join } from 'node:path';
9import { homedir } from 'node:os';
10import { mark, name } from '../lib/brand.js';
11import { which } from '../lib/compat.js';
12import { jsonOk, jsonError } from '../lib/json-output.js';
13import { configPath } from '../lib/paths.js';
14import { errorMessage, formatError } from '../lib/error-format.js';
15
16function scanSkillDir(dir) {
17 const skills = [];
18 if (!existsSync(dir)) return skills;
19 try {
20 const entries = readdirSync(dir, { withFileTypes: true });
21 for (const entry of entries) {
22 if (!entry.isDirectory()) continue;
23 const skillMd = join(dir, entry.name, 'SKILL.md');
24 if (existsSync(skillMd)) {
25 let version = null;
26 try {
27 const content = readFileSync(skillMd, 'utf-8');
28 const match = content.match(/^---\r?\n([\s\S]*?)\r?\n---/);
29 if (match) {
30 const versionMatch = match[1].match(/^version:\s*(.+)$/m);
31 if (versionMatch) version = versionMatch[1].trim();
32 }
33 } catch (err) {
34 console.warn(`warning: failed to read ${skillMd}: ${errorMessage(err)}`);
35 }
36 skills.push({ name: entry.name, version });
37 }
38 }
39 } catch (err) {
40 console.warn(`warning: failed to read skill directory ${dir}: ${errorMessage(err)}`);
41 }
42 return skills;
43}
44
45export default function register(program) {
46 async function checkHealth(opts) {
47 try {
48 const config = loadConfig();
49 let installType = 'not on PATH';
50 let vitPath = which(name);
51 let installPath = vitPath || null;
52 let beacon = null;
53 let skillInstalled = false;
54 let projectSkills = [];
55 let userSkills = [];
56 let blueskyOk = false;
57 let pds = null;
58
59 if (!vitPath) {
60 installType = 'not on PATH';
61 if (!opts.json) console.log(`${mark} install: not on PATH`);
62 } else {
63 try {
64 if (lstatSync(vitPath).isSymbolicLink()) {
65 installType = 'linked';
66 if (!opts.json) console.log(`${mark} install: linked (${vitPath})`);
67 } else if (vitPath.includes('node_modules')) {
68 installType = 'global';
69 if (!opts.json) console.log(`${mark} install: global`);
70 } else {
71 installType = 'source';
72 if (!opts.json) console.log(`${mark} install: source (${vitPath})`);
73 }
74 } catch (err) {
75 console.warn(`warning: failed to inspect install path ${vitPath}: ${errorMessage(err)}`);
76 installType = 'source';
77 if (!opts.json) console.log(`${mark} install: source (${vitPath})`);
78 }
79 }
80
81 const projConfig = readProjectConfig();
82 beacon = projConfig.beacon || null;
83 if (projConfig.beacon) {
84 if (!opts.json) console.log(`${mark} beacon: ${projConfig.beacon}`);
85 } else {
86 if (!opts.json) console.log(`${mark} beacon: not set (run vit init)`);
87 }
88
89 const projectSkillPath = join(process.cwd(), '.claude', 'skills', 'using-vit', 'SKILL.md');
90 const userSkillPath = join(homedir(), '.claude', 'skills', 'using-vit', 'SKILL.md');
91 skillInstalled = existsSync(projectSkillPath) || existsSync(userSkillPath);
92 if (skillInstalled) {
93 if (!opts.json) console.log(`${mark} skill: ok (using-vit)`);
94 } else {
95 if (!opts.json) console.log(`${mark} skill: not installed (reinstall vit)`);
96 }
97
98 // Report installed skills
99 const projectSkillDir = join(process.cwd(), '.claude', 'skills');
100 projectSkills = scanSkillDir(projectSkillDir);
101 const userSkillDir = join(homedir(), '.claude', 'skills');
102 userSkills = scanSkillDir(userSkillDir);
103
104 if (!opts.json && projectSkills.length > 0) {
105 console.log(`${mark} project skills: ${projectSkills.length} installed`);
106 for (const s of projectSkills) {
107 const ver = s.version ? ` v${s.version}` : '';
108 console.log(` ${s.name}${ver}`);
109 }
110 }
111 if (!opts.json && userSkills.length > 0) {
112 console.log(`${mark} user skills: ${userSkills.length} installed`);
113 for (const s of userSkills) {
114 const ver = s.version ? ` v${s.version}` : '';
115 console.log(` ${s.name}${ver}`);
116 }
117 }
118
119 let effectiveDid = config.did;
120 let identitySource = effectiveDid ? 'global' : null;
121 let authType = 'oauth';
122
123 const localLoginPath = join(process.cwd(), '.vit', 'login.json');
124 try {
125 if (existsSync(localLoginPath)) {
126 const local = JSON.parse(readFileSync(localLoginPath, 'utf-8'));
127 if (local.did) {
128 effectiveDid = local.did;
129 identitySource = 'local';
130 authType = local.type || 'oauth';
131 }
132 }
133 } catch (err) {
134 console.warn(`warning: failed to read ${localLoginPath}: ${errorMessage(err)}`);
135 }
136
137 if (!identitySource && effectiveDid) identitySource = 'global';
138
139 if (identitySource === 'global' && effectiveDid) {
140 const sessionFile = configPath('session.json');
141 try {
142 if (existsSync(sessionFile)) {
143 const raw = readFileSync(sessionFile, 'utf-8');
144 const sessionData = JSON.parse(raw);
145 if (sessionData[effectiveDid]?.type === 'app-password') authType = 'app-password';
146 }
147 } catch (err) {
148 console.warn(`warning: failed to read ${sessionFile}: ${errorMessage(err)}`);
149 }
150 }
151
152 if (!effectiveDid) {
153 if (!opts.json) console.log(`${mark} bluesky: not logged in (run ${name} login <handle>)`);
154 } else {
155 try {
156 const { session } = await restoreAgent(effectiveDid);
157 blueskyOk = true;
158 pds = session.serverMetadata?.issuer || null;
159 if (!opts.json) console.log(`${mark} bluesky: ok (${session.did || effectiveDid}${pds ? ', ' + pds : ''})`);
160 } catch (err) {
161 console.warn(`warning: failed to validate Bluesky session: ${errorMessage(err)}`);
162 if (!opts.json) console.log(`${mark} bluesky: token expired or invalid (run ${name} login <handle>)`);
163 }
164 if (!opts.json) console.log(`${mark} identity: ${identitySource} (${authType})`);
165 }
166
167 if (opts.json) {
168 jsonOk({
169 install: { type: installType, path: installPath },
170 beacon,
171 skill: skillInstalled,
172 projectSkills,
173 userSkills,
174 bluesky: { ok: blueskyOk, did: effectiveDid || null, pds, source: identitySource, authType },
175 });
176 }
177 } catch (err) {
178 if (opts.json) {
179 jsonError(err);
180 return;
181 }
182 console.error(formatError(err, { verbose: false }));
183 process.exitCode = 1;
184 }
185 }
186
187 program.command('doctor')
188 .description('Verify vit environment and project configuration')
189 .option('--json', 'Output as JSON')
190 .action(checkHealth);
191 program.command('status')
192 .description('Alias for doctor')
193 .option('--json', 'Output as JSON')
194 .action(checkHealth);
195}