bitgit 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,125 @@
1
+ /**
2
+ * bit init — scaffold .bit.yaml + .well-known/path402.json
3
+ */
4
+
5
+ import { writeFileSync, mkdirSync, existsSync, readFileSync } from 'fs';
6
+ import { execSync } from 'child_process';
7
+ import { resolve, basename } from 'path';
8
+ import { stringify as toYaml } from 'yaml';
9
+ import { configPath, type BitConfig } from '../config.js';
10
+ import * as readline from 'readline';
11
+
12
+ function ask(prompt: string, defaultValue?: string): Promise<string> {
13
+ const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
14
+ const suffix = defaultValue ? ` [${defaultValue}]` : '';
15
+ return new Promise((resolve) => {
16
+ rl.question(`${prompt}${suffix}: `, (answer) => {
17
+ rl.close();
18
+ resolve(answer.trim() || defaultValue || '');
19
+ });
20
+ });
21
+ }
22
+
23
+ function detectProjectName(): string {
24
+ const cwd = process.cwd();
25
+
26
+ // Try package.json
27
+ const pkgPath = resolve(cwd, 'package.json');
28
+ if (existsSync(pkgPath)) {
29
+ try {
30
+ const pkg = JSON.parse(readFileSync(pkgPath, 'utf8'));
31
+ if (pkg.name) return pkg.name.replace(/^@[^/]+\//, '');
32
+ } catch {}
33
+ }
34
+
35
+ // Try git remote
36
+ try {
37
+ const remote = execSync('git remote get-url origin', { encoding: 'utf8', stdio: ['pipe', 'pipe', 'pipe'] }).trim();
38
+ const match = remote.match(/\/([^/]+?)(?:\.git)?$/);
39
+ if (match) return match[1];
40
+ } catch {}
41
+
42
+ // Fall back to directory name
43
+ return basename(cwd);
44
+ }
45
+
46
+ export async function init(): Promise<void> {
47
+ const cfgPath = configPath();
48
+ if (existsSync(cfgPath)) {
49
+ console.log('.bit.yaml already exists. Delete it first to re-initialize.');
50
+ return;
51
+ }
52
+
53
+ console.log('bit init — setting up Bitcoin config for this project\n');
54
+
55
+ const detectedName = detectProjectName();
56
+ const name = await ask('Project name', detectedName);
57
+ const domain = await ask('Domain (optional)');
58
+ const token = await ask('Token symbol (optional)');
59
+ const contentType = await ask('Content type (blog/repo/domain/custom)', 'blog') as BitConfig['content']['type'];
60
+ const source = await ask(
61
+ 'Content source directory',
62
+ contentType === 'blog' ? 'content/blog/' : contentType === 'repo' ? '.' : '',
63
+ );
64
+ const format = await ask('Inscription format (bitcoin_schema/op_return)', 'bitcoin_schema') as BitConfig['content']['format'];
65
+
66
+ const config: BitConfig = {
67
+ project: {
68
+ name,
69
+ ...(domain ? { domain } : {}),
70
+ ...(token ? { token } : {}),
71
+ },
72
+ wallet: {
73
+ key_env: 'BOASE_TREASURY_PRIVATE_KEY',
74
+ },
75
+ content: {
76
+ type: contentType,
77
+ source,
78
+ format,
79
+ protocol: `${name}-${contentType}`,
80
+ },
81
+ db: {
82
+ supabase_url_env: 'NEXT_PUBLIC_SUPABASE_URL',
83
+ supabase_key_env: 'SUPABASE_SERVICE_ROLE_KEY',
84
+ version_table: 'blog_post_versions',
85
+ },
86
+ ...(domain
87
+ ? {
88
+ dns_dex: {
89
+ token_symbol: token ? `$${token}` : `$${domain}`,
90
+ },
91
+ }
92
+ : {}),
93
+ };
94
+
95
+ writeFileSync(cfgPath, toYaml(config), 'utf8');
96
+ console.log(`\nCreated ${cfgPath}`);
97
+
98
+ // Generate .well-known/path402.json
99
+ if (domain) {
100
+ const wellKnownDir = resolve(process.cwd(), 'public', '.well-known');
101
+ mkdirSync(wellKnownDir, { recursive: true });
102
+
103
+ const path402 = {
104
+ name,
105
+ domain,
106
+ token: token || undefined,
107
+ protocol: '$402',
108
+ endpoints: {
109
+ press: `https://${domain}/api/path402/press`,
110
+ discover: `https://${domain}/api/path402/discover`,
111
+ },
112
+ };
113
+
114
+ const wellKnownPath = resolve(wellKnownDir, 'path402.json');
115
+ writeFileSync(wellKnownPath, JSON.stringify(path402, null, 2), 'utf8');
116
+ console.log(`Created ${wellKnownPath}`);
117
+
118
+ // Print DNS records
119
+ console.log(`\nAdd these DNS TXT records for ${domain}:`);
120
+ console.log(` _path402.${domain} → v=path402; endpoint=https://${domain}/api/path402`);
121
+ console.log(` _dnsdex.${domain} → dnsdex-verify=pending`);
122
+ }
123
+
124
+ console.log('\nDone. Run `bit status` to verify, `bit push` to inscribe.');
125
+ }
@@ -0,0 +1,267 @@
1
+ /**
2
+ * bit push — git push + inscribe changed content on BSV
3
+ *
4
+ * 1. git push to remote (if commits to push)
5
+ * 2. Detect changes since last inscription (git diff)
6
+ * 3. Inscribe changed content on BSV
7
+ * 4. Update version chain in DB
8
+ */
9
+
10
+ import { execSync } from 'child_process';
11
+ import { readFileSync, existsSync } from 'fs';
12
+ import { resolve, relative } from 'path';
13
+ import { createHash } from 'crypto';
14
+ import { PrivateKey } from '@bsv/sdk';
15
+ import { loadConfig, resolvePrivateKey } from '../config.js';
16
+ import { fetchUtxos, type UTXO } from '../bsv/utxo.js';
17
+ import { buildInscriptionTx } from '../bsv/tx.js';
18
+ import { buildOpReturn, buildBitcoinSchema } from '../bsv/script.js';
19
+ import { broadcast } from '../bsv/broadcast.js';
20
+ import { getDb } from '../db.js';
21
+
22
+ export async function push(args: string[]): Promise<void> {
23
+ const dryRun = args.includes('--dry-run');
24
+ const skipGit = args.includes('--skip-git');
25
+ const config = loadConfig();
26
+ const cwd = process.cwd();
27
+
28
+ console.log(`bit push — ${config.project.name}`);
29
+ if (dryRun) console.log(' (dry run — no transactions will be broadcast)\n');
30
+
31
+ // 1. Git push
32
+ if (!skipGit) {
33
+ try {
34
+ const status = execSync('git status --porcelain', { cwd, encoding: 'utf8' }).trim();
35
+ if (status) {
36
+ console.log('Warning: uncommitted changes exist. Pushing what is committed.\n');
37
+ }
38
+
39
+ // Check if there are commits to push
40
+ try {
41
+ const ahead = execSync('git rev-list --count @{upstream}..HEAD', {
42
+ cwd,
43
+ encoding: 'utf8',
44
+ stdio: ['pipe', 'pipe', 'pipe'],
45
+ }).trim();
46
+
47
+ if (parseInt(ahead) > 0) {
48
+ console.log(`Pushing ${ahead} commit(s) to remote...`);
49
+ if (!dryRun) {
50
+ execSync('git push', { cwd, stdio: 'inherit' });
51
+ }
52
+ console.log();
53
+ } else {
54
+ console.log('Git: up to date with remote.\n');
55
+ }
56
+ } catch {
57
+ // No upstream tracking — skip git push
58
+ console.log('Git: no upstream branch. Skipping git push.\n');
59
+ }
60
+ } catch (err: any) {
61
+ console.log(`Git push failed: ${err.message}. Continuing with inscription.\n`);
62
+ }
63
+ }
64
+
65
+ // 2. Find changed content
66
+ const sourceDir = resolve(cwd, config.content.source);
67
+ if (!existsSync(sourceDir)) {
68
+ console.error(`Content source not found: ${config.content.source}`);
69
+ process.exit(1);
70
+ }
71
+
72
+ // Get last inscribed commit from DB (if configured)
73
+ let lastInscribedCommit: string | null = null;
74
+ let db;
75
+ try {
76
+ db = getDb(config);
77
+ const versionTable = config.db?.version_table || 'blog_post_versions';
78
+ const { data } = await db
79
+ .from(versionTable)
80
+ .select('content_hash, inscription_txid')
81
+ .eq('project', config.project.name)
82
+ .order('created_at', { ascending: false })
83
+ .limit(1)
84
+ .maybeSingle();
85
+
86
+ if (data?.inscription_txid) {
87
+ console.log(`Last inscription: ${data.inscription_txid}`);
88
+ }
89
+ } catch {
90
+ // DB not available — inscribe everything in source
91
+ }
92
+
93
+ // Find files to inscribe
94
+ let filesToInscribe: string[] = [];
95
+
96
+ try {
97
+ // Try git diff to find changes
98
+ const lastTag = execSync('git describe --tags --abbrev=0 2>/dev/null || echo ""', {
99
+ cwd,
100
+ encoding: 'utf8',
101
+ }).trim();
102
+
103
+ const diffBase = lastTag || 'HEAD~1';
104
+ const diff = execSync(`git diff --name-only ${diffBase} -- "${config.content.source}"`, {
105
+ cwd,
106
+ encoding: 'utf8',
107
+ }).trim();
108
+
109
+ if (diff) {
110
+ filesToInscribe = diff.split('\n').filter((f) => existsSync(resolve(cwd, f)));
111
+ }
112
+ } catch {
113
+ // No git history — fall through
114
+ }
115
+
116
+ // If no git diff results, use all files in source directory
117
+ if (filesToInscribe.length === 0) {
118
+ try {
119
+ const allFiles = execSync(`find "${sourceDir}" -type f -name "*.md" -o -name "*.json" | sort`, {
120
+ cwd,
121
+ encoding: 'utf8',
122
+ }).trim();
123
+ if (allFiles) {
124
+ filesToInscribe = allFiles.split('\n').map((f) => relative(cwd, f));
125
+ }
126
+ } catch {}
127
+ }
128
+
129
+ if (filesToInscribe.length === 0) {
130
+ console.log('No content files found to inscribe.');
131
+ return;
132
+ }
133
+
134
+ console.log(`Found ${filesToInscribe.length} file(s) to inscribe:`);
135
+ filesToInscribe.forEach((f) => console.log(` ${f}`));
136
+ console.log();
137
+
138
+ if (dryRun) {
139
+ console.log('Dry run — skipping inscription.');
140
+ return;
141
+ }
142
+
143
+ // 3. Inscribe on BSV
144
+ const wif = resolvePrivateKey(config);
145
+ const privateKey = PrivateKey.fromWif(wif);
146
+ const address = privateKey.toPublicKey().toAddress();
147
+
148
+ console.log(`Treasury: ${address}`);
149
+
150
+ const utxos = await fetchUtxos(address);
151
+ if (utxos.length === 0) {
152
+ console.error('No UTXOs found. Fund the treasury address first.');
153
+ process.exit(1);
154
+ }
155
+
156
+ let currentUtxo = utxos[0];
157
+ let totalFees = 0;
158
+ const results: { file: string; txid: string }[] = [];
159
+
160
+ for (let i = 0; i < filesToInscribe.length; i++) {
161
+ const filePath = resolve(cwd, filesToInscribe[i]);
162
+ const content = readFileSync(filePath, 'utf8');
163
+ const contentHash = createHash('sha256').update(content).digest('hex');
164
+
165
+ console.log(`[${i + 1}/${filesToInscribe.length}] ${filesToInscribe[i]} (${content.length} bytes)`);
166
+
167
+ // Build OP_RETURN script based on format
168
+ let opReturnScript;
169
+ const payloadSize = content.length;
170
+
171
+ if (config.content.format === 'bitcoin_schema') {
172
+ const mapData: Record<string, string> = {
173
+ app: config.project.domain || config.project.name,
174
+ type: config.content.type,
175
+ file: filesToInscribe[i],
176
+ hash: contentHash,
177
+ };
178
+
179
+ // Extract slug from filename for blog posts
180
+ if (config.content.type === 'blog') {
181
+ const slug = filesToInscribe[i]
182
+ .replace(/^.*\//, '')
183
+ .replace(/\.md$/, '');
184
+ mapData.slug = slug;
185
+ }
186
+
187
+ opReturnScript = buildBitcoinSchema(
188
+ content,
189
+ content.endsWith('.json') ? 'application/json' : 'text/markdown',
190
+ mapData,
191
+ privateKey,
192
+ );
193
+ } else {
194
+ const payload = JSON.stringify({
195
+ protocol: config.content.protocol,
196
+ file: filesToInscribe[i],
197
+ hash: contentHash,
198
+ content,
199
+ timestamp: new Date().toISOString(),
200
+ });
201
+ opReturnScript = buildOpReturn(
202
+ config.content.protocol,
203
+ 'application/json',
204
+ payload,
205
+ );
206
+ }
207
+
208
+ try {
209
+ const { tx, fee, changeSats } = await buildInscriptionTx({
210
+ privateKey,
211
+ utxo: currentUtxo,
212
+ opReturnScript,
213
+ payloadSize,
214
+ });
215
+
216
+ const rawTx = tx.toHex();
217
+ const txid = await broadcast(rawTx);
218
+
219
+ totalFees += fee;
220
+ results.push({ file: filesToInscribe[i], txid });
221
+
222
+ console.log(` TXID: ${txid}`);
223
+ console.log(` Fee: ${fee} sats | Remaining: ${changeSats} sats`);
224
+ console.log(` https://whatsonchain.com/tx/${txid}`);
225
+
226
+ // 4. Update version chain in DB
227
+ if (db && config.db?.version_table) {
228
+ try {
229
+ await db.from(config.db.version_table).insert({
230
+ project: config.project.name,
231
+ file_path: filesToInscribe[i],
232
+ content_hash: contentHash,
233
+ inscription_txid: txid,
234
+ created_at: new Date().toISOString(),
235
+ });
236
+ } catch (dbErr: any) {
237
+ console.log(` DB update failed: ${dbErr.message}`);
238
+ }
239
+ }
240
+
241
+ // Chain: use change output as next input
242
+ currentUtxo = {
243
+ txid,
244
+ vout: 1, // change is output index 1 (after OP_RETURN at 0)
245
+ satoshis: changeSats,
246
+ script: currentUtxo.script,
247
+ };
248
+
249
+ // Small delay for mempool propagation
250
+ if (i < filesToInscribe.length - 1) {
251
+ await new Promise((r) => setTimeout(r, 500));
252
+ }
253
+ } catch (err: any) {
254
+ console.log(` FAILED: ${err.message}`);
255
+ }
256
+ }
257
+
258
+ // Summary
259
+ console.log('\n' + '='.repeat(50));
260
+ console.log(`Inscribed: ${results.length}/${filesToInscribe.length} files`);
261
+ console.log(`Total fees: ${totalFees} sats`);
262
+
263
+ if (results.length > 0) {
264
+ console.log('\nInscriptions:');
265
+ results.forEach((r) => console.log(` ${r.file} → ${r.txid}`));
266
+ }
267
+ }
@@ -0,0 +1,151 @@
1
+ /**
2
+ * bit register <domain> — inscribe a domain on DNS-DEX
3
+ */
4
+
5
+ import { createHash } from 'crypto';
6
+ import { PrivateKey } from '@bsv/sdk';
7
+ import { loadConfig, resolvePrivateKey } from '../config.js';
8
+ import { selectUtxo } from '../bsv/utxo.js';
9
+ import { buildInscriptionTx } from '../bsv/tx.js';
10
+ import { buildOpReturn } from '../bsv/script.js';
11
+ import { broadcast } from '../bsv/broadcast.js';
12
+ import { getDb } from '../db.js';
13
+
14
+ export async function register(domain: string, args: string[]): Promise<void> {
15
+ const dryRun = args.includes('--dry-run');
16
+ const supply = parseInt(args.find((a) => a.startsWith('--supply='))?.split('=')[1] || '1000000000');
17
+ const category = args.find((a) => a.startsWith('--category='))?.split('=')[1] || 'other';
18
+
19
+ let config;
20
+ try {
21
+ config = loadConfig();
22
+ } catch {
23
+ // No config — use env vars directly
24
+ config = null;
25
+ }
26
+
27
+ console.log(`bit register — inscribing ${domain} on DNS-DEX`);
28
+ if (dryRun) console.log(' (dry run)\n');
29
+
30
+ const ts = new Date().toISOString();
31
+ const tick = `$${domain}`;
32
+
33
+ // In dry-run mode, skip key resolution if no key available
34
+ let wif: string | undefined;
35
+ let address = '(no key configured)';
36
+ try {
37
+ wif = config ? resolvePrivateKey(config) : (
38
+ process.env.BOASE_TREASURY_PRIVATE_KEY ||
39
+ process.env.BSV_PRIVATE_KEY ||
40
+ undefined
41
+ );
42
+ if (wif) {
43
+ const privateKey = PrivateKey.fromWif(wif);
44
+ address = privateKey.toPublicKey().toAddress();
45
+ }
46
+ } catch {}
47
+
48
+ if (!wif && !dryRun) {
49
+ console.error('No private key found. Set BOASE_TREASURY_PRIVATE_KEY.');
50
+ process.exit(1);
51
+ }
52
+
53
+ // Build DNS-DEX inscription document
54
+ const doc = {
55
+ p: 'dnsdex-domain',
56
+ v: '1.0',
57
+ op: 'mint',
58
+ tick,
59
+ domain,
60
+ supply,
61
+ owner: {
62
+ wallet: config?.dns_dex?.token_symbol || tick,
63
+ type: 'treasury_key',
64
+ },
65
+ x402: {
66
+ fee_cents: 1,
67
+ initial_price: 0,
68
+ },
69
+ meta: {
70
+ category,
71
+ description: `https://${domain}`,
72
+ timestamp: ts,
73
+ },
74
+ verification: {
75
+ method: 'treasury_key',
76
+ proof: createHash('sha256')
77
+ .update(`${domain}:${address}:${ts}`)
78
+ .digest('hex')
79
+ .slice(0, 32),
80
+ verified_at: ts,
81
+ },
82
+ platform: 'dns-dex.com',
83
+ };
84
+
85
+ const payload = JSON.stringify(doc);
86
+ const contentHash = createHash('sha256').update(payload).digest('hex');
87
+
88
+ console.log(`Treasury: ${address}`);
89
+ console.log(`Tick: ${tick} | Supply: ${supply.toLocaleString()} | Category: ${category}`);
90
+ console.log(`Payload: ${payload.length} bytes\n`);
91
+
92
+ if (dryRun) {
93
+ console.log('Dry run — would inscribe:');
94
+ console.log(JSON.stringify(doc, null, 2));
95
+ return;
96
+ }
97
+
98
+ const privateKey = PrivateKey.fromWif(wif!);
99
+
100
+ const utxo = await selectUtxo(address);
101
+ const opReturnScript = buildOpReturn('dnsdex-domain', 'application/json', payload);
102
+
103
+ const { tx, fee, changeSats } = await buildInscriptionTx({
104
+ privateKey,
105
+ utxo,
106
+ opReturnScript,
107
+ payloadSize: payload.length,
108
+ });
109
+
110
+ const txid = await broadcast(tx.toHex());
111
+ const origin = `${txid}_0`;
112
+ const inscriptionId = `${txid}i0`;
113
+
114
+ console.log('Domain inscribed on BSV!');
115
+ console.log(` TXID: ${txid}`);
116
+ console.log(` Origin: ${origin}`);
117
+ console.log(` Fee: ${fee} sats`);
118
+ console.log(` Explorer: https://whatsonchain.com/tx/${txid}`);
119
+ console.log(` Ordinal: https://1satordinals.com/inscription/${inscriptionId}`);
120
+
121
+ // Update DB
122
+ try {
123
+ const db = getDb(config || undefined);
124
+ const { error } = await db
125
+ .from('dnsdex_domains')
126
+ .upsert({
127
+ domain,
128
+ status: 'active',
129
+ bsv_txid: txid,
130
+ bsv_origin: origin,
131
+ inscription_id: inscriptionId,
132
+ content_hash: contentHash,
133
+ verification_method: 'treasury_key',
134
+ verified_at: ts,
135
+ updated_at: ts,
136
+ }, { onConflict: 'domain' });
137
+
138
+ if (error) {
139
+ console.log(`\nDB update failed: ${error.message}`);
140
+ } else {
141
+ console.log(`\nDB: ${domain} → active`);
142
+ }
143
+ } catch (err: any) {
144
+ console.log(`\nDB not available: ${err.message}`);
145
+ }
146
+
147
+ // Print DNS records to add
148
+ console.log(`\nAdd these DNS TXT records for ${domain}:`);
149
+ console.log(` _dnsdex.${domain} → dnsdex-verify=${txid}`);
150
+ console.log(` _dnsdex-verification.${domain} → ${doc.verification.proof}`);
151
+ }
@@ -0,0 +1,148 @@
1
+ /**
2
+ * bit status — show Bitcoin state for this project
3
+ */
4
+
5
+ import { PrivateKey } from '@bsv/sdk';
6
+ import { loadConfig, resolvePrivateKey } from '../config.js';
7
+ import { fetchUtxos } from '../bsv/utxo.js';
8
+ import { getDb } from '../db.js';
9
+
10
+ export async function status(): Promise<void> {
11
+ const config = loadConfig();
12
+
13
+ console.log(`bit status — ${config.project.name}\n`);
14
+
15
+ // Wallet info
16
+ let address: string;
17
+ try {
18
+ const wif = resolvePrivateKey(config);
19
+ const privateKey = PrivateKey.fromWif(wif);
20
+ address = privateKey.toPublicKey().toAddress();
21
+
22
+ const utxos = await fetchUtxos(address);
23
+ const totalSats = utxos.reduce((sum, u) => sum + u.satoshis, 0);
24
+
25
+ console.log('Wallet:');
26
+ console.log(` Address: ${address}`);
27
+ console.log(` UTXOs: ${utxos.length}`);
28
+ console.log(` Balance: ${totalSats.toLocaleString()} sats`);
29
+ console.log();
30
+ } catch (err: any) {
31
+ console.log(`Wallet: not configured (${err.message})\n`);
32
+ address = '';
33
+ }
34
+
35
+ // Domain info
36
+ if (config.project.domain) {
37
+ console.log('Domain:');
38
+ console.log(` Name: ${config.project.domain}`);
39
+
40
+ // Check DNS-DEX registration
41
+ try {
42
+ const db = getDb(config);
43
+ const { data } = await db
44
+ .from('dnsdex_domains')
45
+ .select('status, bsv_txid, verified_at')
46
+ .eq('domain', config.project.domain)
47
+ .maybeSingle();
48
+
49
+ if (data) {
50
+ console.log(` DNS-DEX: ${data.status}`);
51
+ if (data.bsv_txid) {
52
+ console.log(` TXID: ${data.bsv_txid}`);
53
+ console.log(` Explorer: https://whatsonchain.com/tx/${data.bsv_txid}`);
54
+ }
55
+ } else {
56
+ console.log(' DNS-DEX: not registered');
57
+ }
58
+ } catch {
59
+ console.log(' DNS-DEX: (DB not available)');
60
+ }
61
+
62
+ // Check DNS TXT records
63
+ try {
64
+ const { execSync } = await import('child_process');
65
+ const txt = execSync(`dig +short TXT _dnsdex.${config.project.domain} 2>/dev/null`, {
66
+ encoding: 'utf8',
67
+ }).trim();
68
+ console.log(` DNS TXT: ${txt || '(not set)'}`);
69
+ } catch {
70
+ console.log(' DNS TXT: (could not resolve)');
71
+ }
72
+
73
+ console.log();
74
+ }
75
+
76
+ // Token info
77
+ if (config.project.token) {
78
+ console.log('Token:');
79
+ console.log(` Symbol: ${config.project.token}`);
80
+
81
+ try {
82
+ const db = getDb(config);
83
+ const { data } = await db
84
+ .from('path402_tokens')
85
+ .select('id, name, total_supply, current_supply, base_price_sats, deploy_txid')
86
+ .eq('symbol', config.project.token)
87
+ .maybeSingle();
88
+
89
+ if (data) {
90
+ console.log(` Name: ${data.name}`);
91
+ console.log(` Supply: ${data.current_supply}/${data.total_supply}`);
92
+ console.log(` Price: ${data.base_price_sats} sats`);
93
+ if (data.deploy_txid) {
94
+ console.log(` On-chain: https://whatsonchain.com/tx/${data.deploy_txid}`);
95
+ }
96
+
97
+ // Holder count
98
+ const { count } = await db
99
+ .from('path402_holdings')
100
+ .select('*', { count: 'exact', head: true })
101
+ .eq('token_id', data.id)
102
+ .gt('balance', 0);
103
+
104
+ console.log(` Holders: ${count || 0}`);
105
+ } else {
106
+ console.log(' (not found in path402_tokens)');
107
+ }
108
+ } catch {
109
+ console.log(' (DB not available)');
110
+ }
111
+
112
+ console.log();
113
+ }
114
+
115
+ // Version chain
116
+ console.log('Version chain:');
117
+ try {
118
+ const db = getDb(config);
119
+ const versionTable = config.db?.version_table || 'blog_post_versions';
120
+ const { data, error } = await db
121
+ .from(versionTable)
122
+ .select('file_path, content_hash, inscription_txid, created_at')
123
+ .eq('project', config.project.name)
124
+ .order('created_at', { ascending: false })
125
+ .limit(5);
126
+
127
+ if (error) {
128
+ console.log(` (query failed: ${error.message})`);
129
+ } else if (!data || data.length === 0) {
130
+ console.log(' No inscriptions yet. Run `bit push` to inscribe content.');
131
+ } else {
132
+ console.log(` Latest ${data.length} inscription(s):`);
133
+ for (const row of data) {
134
+ const date = new Date(row.created_at).toISOString().split('T')[0];
135
+ console.log(` ${date} ${row.file_path || '(unknown)'}`);
136
+ console.log(` ${row.inscription_txid}`);
137
+ }
138
+ }
139
+ } catch {
140
+ console.log(' (DB not available)');
141
+ }
142
+
143
+ // Content source
144
+ console.log(`\nConfig:`);
145
+ console.log(` Source: ${config.content.source}`);
146
+ console.log(` Format: ${config.content.format}`);
147
+ console.log(` Protocol: ${config.content.protocol}`);
148
+ }