open source is social v-it.org
0
fork

Configure Feed

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

add agent detection utility and command guards

+210 -24
+9
src/cmd/adopt.js
··· 6 6 import { execFileSync } from 'node:child_process'; 7 7 import { parseGitUrl, toBeacon, beaconToHttps } from '../lib/beacon.js'; 8 8 import { writeProjectConfig } from '../lib/vit-dir.js'; 9 + import { requireNotAgent } from '../lib/agent.js'; 9 10 10 11 export default function register(program) { 11 12 program ··· 16 17 .option('-v, --verbose', 'Show step-by-step details') 17 18 .action(async (beacon, name, opts) => { 18 19 try { 20 + const gate = requireNotAgent(); 21 + if (!gate.ok) { 22 + console.error(`vit adopt cannot run inside ${gate.name} (detected ${gate.envVar}=1).`); 23 + console.error('run vit adopt from your own terminal instead.'); 24 + process.exitCode = 1; 25 + return; 26 + } 27 + 19 28 const { verbose } = opts; 20 29 21 30 // resolve beacon
+9
src/cmd/init.js
··· 5 5 import { execSync } from 'node:child_process'; 6 6 import { toBeacon } from '../lib/beacon.js'; 7 7 import { vitDir, readProjectConfig, writeProjectConfig } from '../lib/vit-dir.js'; 8 + import { requireAgent } from '../lib/agent.js'; 8 9 9 10 export default function register(program) { 10 11 program ··· 14 15 .option('-v, --verbose', 'Show step-by-step details') 15 16 .action(async (opts) => { 16 17 try { 18 + const gate = requireAgent(); 19 + if (!gate.ok) { 20 + console.error('vit init should be run by a coding agent (e.g. claude code, gemini cli).'); 21 + console.error("open your agent and ask it to run 'vit init' for you."); 22 + process.exitCode = 1; 23 + return; 24 + } 25 + 17 26 const { verbose } = opts; 18 27 const dir = vitDir(); 19 28 if (verbose) console.log(`[verbose] .vit dir: ${dir}`);
+9
src/cmd/setup.js
··· 2 2 // Copyright (c) 2026 sol pbc 3 3 4 4 import { loadConfig, saveConfig } from '../lib/config.js'; 5 + import { requireNotAgent } from '../lib/agent.js'; 5 6 6 7 export default function register(program) { 7 8 program ··· 9 10 .description('Initialize user-level vit setup') 10 11 .action(async () => { 11 12 try { 13 + const gate = requireNotAgent(); 14 + if (!gate.ok) { 15 + console.error(`vit setup cannot run inside ${gate.name} (detected ${gate.envVar}=1).`); 16 + console.error('run vit setup from your own terminal instead.'); 17 + process.exitCode = 1; 18 + return; 19 + } 20 + 12 21 const config = loadConfig(); 13 22 if (config.setup_at) { 14 23 const when = new Date(config.setup_at * 1000).toISOString();
+9
src/cmd/skim.js
··· 5 5 import { CAP_COLLECTION, FOLLOW_COLLECTION } from '../lib/constants.js'; 6 6 import { restoreAgent } from '../lib/oauth.js'; 7 7 import { readProjectConfig } from '../lib/vit-dir.js'; 8 + import { requireAgent } from '../lib/agent.js'; 8 9 9 10 export default function register(program) { 10 11 program ··· 17 18 .option('-v, --verbose', 'Show step-by-step details') 18 19 .action(async (opts) => { 19 20 try { 21 + const gate = requireAgent(); 22 + if (!gate.ok) { 23 + console.error('vit skim should be run by a coding agent (e.g. claude code, gemini cli).'); 24 + console.error("open your agent and ask it to run 'vit skim' for you."); 25 + process.exitCode = 1; 26 + return; 27 + } 28 + 20 29 const { verbose } = opts; 21 30 const did = opts.did || loadConfig().did; 22 31 if (!did) {
+27
src/lib/agent.js
··· 1 + // SPDX-License-Identifier: AGPL-3.0-only 2 + // Copyright (c) 2026 sol pbc 3 + 4 + const CODING_AGENTS = { 5 + CLAUDECODE: 'claude code', 6 + GEMINI_CLI: 'gemini cli', 7 + CODEX_CI: 'codex', 8 + }; 9 + 10 + export function detectCodingAgent() { 11 + for (const [envVar, name] of Object.entries(CODING_AGENTS)) { 12 + if (process.env[envVar] === '1') return { name, envVar }; 13 + } 14 + return null; 15 + } 16 + 17 + export function requireAgent() { 18 + const agent = detectCodingAgent(); 19 + if (agent) return { ok: true, ...agent }; 20 + return { ok: false }; 21 + } 22 + 23 + export function requireNotAgent() { 24 + const agent = detectCodingAgent(); 25 + if (!agent) return { ok: true }; 26 + return { ok: false, ...agent }; 27 + }
+16 -8
test/adopt.test.js
··· 7 7 import { tmpdir } from 'node:os'; 8 8 import { join } from 'node:path'; 9 9 10 + const NON_AGENT_ENV = { CLAUDECODE: '', GEMINI_CLI: '', CODEX_CI: '' }; 11 + 10 12 describe('vit adopt', () => { 11 13 let tmpDir; 12 14 ··· 31 33 }); 32 34 33 35 test('fails with invalid beacon', () => { 34 - const result = run('adopt notaurl', tmpDir); 36 + const result = run('adopt notaurl', tmpDir, NON_AGENT_ENV); 35 37 expect(result.exitCode).not.toBe(0); 36 38 expect(result.stderr).toContain('Invalid git URL'); 37 39 }); 38 40 39 41 test('fails if directory already exists', () => { 40 42 mkdirSync(join(tmpDir, 'hello-world')); 41 - const result = run('adopt https://github.com/octocat/Hello-World', tmpDir); 43 + const result = run('adopt https://github.com/octocat/Hello-World', tmpDir, NON_AGENT_ENV); 42 44 expect(result.exitCode).not.toBe(0); 43 45 expect(result.stderr).toContain('already exists'); 46 + }); 47 + 48 + test('rejects when run inside a coding agent', () => { 49 + const result = run('adopt https://github.com/octocat/Hello-World', tmpDir, { CLAUDECODE: '1' }); 50 + expect(result.exitCode).toBe(1); 51 + expect(result.stderr).toContain('cannot run inside claude code'); 44 52 }); 45 53 46 54 test('clones repo and initializes .vit/', () => { 47 - const result = run('adopt https://github.com/octocat/Hello-World', tmpDir); 55 + const result = run('adopt https://github.com/octocat/Hello-World', tmpDir, NON_AGENT_ENV); 48 56 expect(result.exitCode).toBe(0); 49 57 expect(result.stdout).toContain('vit:github.com/octocat/hello-world'); 50 58 expect(result.stdout).toContain('hello-world'); ··· 56 64 }, 30000); 57 65 58 66 test('clones into custom directory name', () => { 59 - const result = run('adopt https://github.com/octocat/Hello-World my-copy', tmpDir); 67 + const result = run('adopt https://github.com/octocat/Hello-World my-copy', tmpDir, NON_AGENT_ENV); 60 68 expect(result.exitCode).toBe(0); 61 69 expect(result.stdout).toContain('my-copy'); 62 70 ··· 67 75 }, 30000); 68 76 69 77 test('handles vit: prefixed beacon', () => { 70 - const result = run('adopt vit:github.com/octocat/Hello-World', tmpDir); 78 + const result = run('adopt vit:github.com/octocat/Hello-World', tmpDir, NON_AGENT_ENV); 71 79 expect(result.exitCode).toBe(0); 72 80 expect(result.stdout).toContain('beacon: vit:github.com/octocat/hello-world'); 73 81 ··· 78 86 }, 30000); 79 87 80 88 test('verbose flag shows step details', () => { 81 - const result = run('adopt -v https://github.com/octocat/Hello-World', tmpDir); 89 + const result = run('adopt -v https://github.com/octocat/Hello-World', tmpDir, NON_AGENT_ENV); 82 90 expect(result.exitCode).toBe(0); 83 91 expect(result.stdout).toContain('[verbose]'); 84 92 expect(result.stdout).toContain('resolving beacon'); 85 93 }, 30000); 86 94 87 95 test('second adopt to same dir fails', () => { 88 - run('adopt https://github.com/octocat/Hello-World', tmpDir); 89 - const result = run('adopt https://github.com/octocat/Hello-World', tmpDir); 96 + run('adopt https://github.com/octocat/Hello-World', tmpDir, NON_AGENT_ENV); 97 + const result = run('adopt https://github.com/octocat/Hello-World', tmpDir, NON_AGENT_ENV); 90 98 expect(result.exitCode).not.toBe(0); 91 99 expect(result.stderr).toContain('already exists'); 92 100 }, 30000);
+84
test/agent.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 { detectCodingAgent, requireAgent, requireNotAgent } from '../src/lib/agent.js'; 6 + 7 + describe('agent', () => { 8 + let originalClaudeCode; 9 + let originalGeminiCli; 10 + let originalCodexCi; 11 + 12 + beforeEach(() => { 13 + originalClaudeCode = process.env.CLAUDECODE; 14 + originalGeminiCli = process.env.GEMINI_CLI; 15 + originalCodexCi = process.env.CODEX_CI; 16 + delete process.env.CLAUDECODE; 17 + delete process.env.GEMINI_CLI; 18 + delete process.env.CODEX_CI; 19 + }); 20 + 21 + afterEach(() => { 22 + if (originalClaudeCode === undefined) delete process.env.CLAUDECODE; 23 + else process.env.CLAUDECODE = originalClaudeCode; 24 + 25 + if (originalGeminiCli === undefined) delete process.env.GEMINI_CLI; 26 + else process.env.GEMINI_CLI = originalGeminiCli; 27 + 28 + if (originalCodexCi === undefined) delete process.env.CODEX_CI; 29 + else process.env.CODEX_CI = originalCodexCi; 30 + }); 31 + 32 + describe('detectCodingAgent', () => { 33 + test('returns null when no agent env vars are set', () => { 34 + expect(detectCodingAgent()).toBe(null); 35 + }); 36 + 37 + test('returns claude code when CLAUDECODE=1', () => { 38 + process.env.CLAUDECODE = '1'; 39 + expect(detectCodingAgent()).toEqual({ name: 'claude code', envVar: 'CLAUDECODE' }); 40 + }); 41 + 42 + test('returns gemini cli when GEMINI_CLI=1', () => { 43 + process.env.GEMINI_CLI = '1'; 44 + expect(detectCodingAgent()).toEqual({ name: 'gemini cli', envVar: 'GEMINI_CLI' }); 45 + }); 46 + 47 + test('returns codex when CODEX_CI=1', () => { 48 + process.env.CODEX_CI = '1'; 49 + expect(detectCodingAgent()).toEqual({ name: 'codex', envVar: 'CODEX_CI' }); 50 + }); 51 + 52 + test('returns null when env var is 0', () => { 53 + process.env.CLAUDECODE = '0'; 54 + expect(detectCodingAgent()).toBe(null); 55 + }); 56 + 57 + test('returns null when env var is empty', () => { 58 + process.env.CLAUDECODE = ''; 59 + expect(detectCodingAgent()).toBe(null); 60 + }); 61 + }); 62 + 63 + describe('requireAgent', () => { 64 + test('returns ok true when agent detected', () => { 65 + process.env.CLAUDECODE = '1'; 66 + expect(requireAgent()).toEqual({ ok: true, name: 'claude code', envVar: 'CLAUDECODE' }); 67 + }); 68 + 69 + test('returns ok false when no agent detected', () => { 70 + expect(requireAgent()).toEqual({ ok: false }); 71 + }); 72 + }); 73 + 74 + describe('requireNotAgent', () => { 75 + test('returns ok true when no agent detected', () => { 76 + expect(requireNotAgent()).toEqual({ ok: true }); 77 + }); 78 + 79 + test('returns ok false when agent detected', () => { 80 + process.env.CLAUDECODE = '1'; 81 + expect(requireNotAgent()).toEqual({ ok: false, name: 'claude code', envVar: 'CLAUDECODE' }); 82 + }); 83 + }); 84 + });
+8 -2
test/helpers.js
··· 6 6 7 7 const vitBin = join(import.meta.dir, '..', 'bin', 'vit.js'); 8 8 9 - export function run(args, cwd) { 9 + export function run(args, cwd, env) { 10 10 try { 11 11 return { 12 - stdout: execSync(`bun ${vitBin} ${args}`, { cwd, encoding: 'utf-8', timeout: 30000, stdio: ['pipe', 'pipe', 'pipe'] }).trim(), 12 + stdout: execSync(`bun ${vitBin} ${args}`, { 13 + cwd, 14 + encoding: 'utf-8', 15 + timeout: 30000, 16 + stdio: ['pipe', 'pipe', 'pipe'], 17 + env: env ? { ...process.env, ...env } : undefined, 18 + }).trim(), 13 19 exitCode: 0, 14 20 }; 15 21 } catch (err) {
+18 -12
test/init.test.js
··· 21 21 }); 22 22 23 23 test('writes beacon from HTTPS URL', () => { 24 - const result = run('init --beacon https://github.com/solpbc/vit.git', tmpDir); 24 + const result = run('init --beacon https://github.com/solpbc/vit.git', tmpDir, { CLAUDECODE: '1' }); 25 25 expect(result.exitCode).toBe(0); 26 26 expect(result.stdout).toBe('beacon: vit:github.com/solpbc/vit'); 27 27 ··· 30 30 }); 31 31 32 32 test('writes beacon from SSH URL', () => { 33 - const result = run('init --beacon git@github.com:solpbc/vit.git', tmpDir); 33 + const result = run('init --beacon git@github.com:solpbc/vit.git', tmpDir, { CLAUDECODE: '1' }); 34 34 expect(result.exitCode).toBe(0); 35 35 expect(result.stdout).toBe('beacon: vit:github.com/solpbc/vit'); 36 36 ··· 40 40 41 41 test('creates .vit directory if missing', () => { 42 42 expect(existsSync(join(tmpDir, '.vit'))).toBe(false); 43 - run('init --beacon https://github.com/solpbc/vit.git', tmpDir); 43 + run('init --beacon https://github.com/solpbc/vit.git', tmpDir, { CLAUDECODE: '1' }); 44 44 expect(existsSync(join(tmpDir, '.vit'))).toBe(true); 45 45 }); 46 46 47 47 test('overwrites existing beacon silently', () => { 48 - run('init --beacon https://github.com/old/repo.git', tmpDir); 49 - run('init --beacon https://github.com/solpbc/vit.git', tmpDir); 48 + run('init --beacon https://github.com/old/repo.git', tmpDir, { CLAUDECODE: '1' }); 49 + run('init --beacon https://github.com/solpbc/vit.git', tmpDir, { CLAUDECODE: '1' }); 50 50 51 51 const content = readFileSync(join(tmpDir, '.vit', 'config.json'), 'utf-8'); 52 52 expect(JSON.parse(content).beacon).toBe('vit:github.com/solpbc/vit'); ··· 57 57 execSync('git init', { cwd: tmpDir, stdio: 'pipe' }); 58 58 execSync('git remote add origin https://github.com/solpbc/vit.git', { cwd: tmpDir, stdio: 'pipe' }); 59 59 60 - const result = run('init --beacon .', tmpDir); 60 + const result = run('init --beacon .', tmpDir, { CLAUDECODE: '1' }); 61 61 expect(result.exitCode).toBe(0); 62 62 expect(result.stdout).toBe('beacon: vit:github.com/solpbc/vit'); 63 63 }); 64 64 65 65 test('errors when --beacon . has no git remote', () => { 66 66 // tmpDir is not a git repo 67 - const result = run('init --beacon .', tmpDir); 67 + const result = run('init --beacon .', tmpDir, { CLAUDECODE: '1' }); 68 68 expect(result.exitCode).not.toBe(0); 69 69 }); 70 70 71 71 test('reports beacon when no flag and beacon exists', () => { 72 - run('init --beacon https://github.com/solpbc/vit.git', tmpDir); 73 - const result = run('init', tmpDir); 72 + run('init --beacon https://github.com/solpbc/vit.git', tmpDir, { CLAUDECODE: '1' }); 73 + const result = run('init', tmpDir, { CLAUDECODE: '1' }); 74 74 expect(result.exitCode).toBe(0); 75 75 expect(result.stdout).toBe('beacon: vit:github.com/solpbc/vit'); 76 76 }); 77 77 78 78 test('reports not set when no flag and .vit exists but no beacon', () => { 79 79 mkdirSync(join(tmpDir, '.vit'), { recursive: true }); 80 - const result = run('init', tmpDir); 80 + const result = run('init', tmpDir, { CLAUDECODE: '1' }); 81 81 expect(result.exitCode).toBe(0); 82 82 expect(result.stdout).toBe('beacon: not set'); 83 83 }); 84 84 85 85 test('reports .vit not found when no flag and no .vit dir', () => { 86 - const result = run('init', tmpDir); 86 + const result = run('init', tmpDir, { CLAUDECODE: '1' }); 87 87 expect(result.exitCode).toBe(0); 88 88 expect(result.stdout).toBe('.vit directory not found'); 89 89 }); 90 90 91 91 test('errors on invalid git URL', () => { 92 - const result = run('init --beacon notaurl', tmpDir); 92 + const result = run('init --beacon notaurl', tmpDir, { CLAUDECODE: '1' }); 93 93 expect(result.exitCode).not.toBe(0); 94 + }); 95 + 96 + test('rejects when run outside a coding agent', () => { 97 + const result = run('init --beacon https://github.com/solpbc/vit.git', tmpDir, { CLAUDECODE: '', GEMINI_CLI: '', CODEX_CI: '' }); 98 + expect(result.exitCode).toBe(1); 99 + expect(result.stderr).toContain('should be run by a coding agent'); 94 100 }); 95 101 });
+13
test/setup.test.js
··· 1 + // SPDX-License-Identifier: AGPL-3.0-only 2 + // Copyright (c) 2026 sol pbc 3 + 4 + import { describe, test, expect } from 'bun:test'; 5 + import { run } from './helpers.js'; 6 + 7 + describe('vit setup', () => { 8 + test('rejects when run inside a coding agent', () => { 9 + const result = run('setup', undefined, { CLAUDECODE: '1' }); 10 + expect(result.exitCode).toBe(1); 11 + expect(result.stderr).toContain('cannot run inside claude code'); 12 + }); 13 + });
+8 -2
test/skim.test.js
··· 5 5 import { run } from './helpers.js'; 6 6 7 7 describe('vit skim', () => { 8 + test('rejects when run outside a coding agent', () => { 9 + const result = run('skim', '/tmp', { CLAUDECODE: '', GEMINI_CLI: '', CODEX_CI: '' }); 10 + expect(result.exitCode).not.toBe(0); 11 + expect(result.stderr).toContain('should be run by a coding agent'); 12 + }); 13 + 8 14 test('errors when DID is invalid', () => { 9 - const result = run('skim --did did:plc:abc'); 15 + const result = run('skim --did did:plc:abc', undefined, { CLAUDECODE: '1' }); 10 16 expect(result.exitCode).not.toBe(0); 11 17 expect(result.stderr).toBeTruthy(); 12 18 }); 13 19 14 20 test('errors when no beacon is set', () => { 15 - const result = run('skim', '/tmp'); 21 + const result = run('skim', '/tmp', { CLAUDECODE: '1' }); 16 22 expect(result.exitCode).not.toBe(0); 17 23 expect(result.stderr).toBeTruthy(); 18 24 });