/** * island:publish * * Publish a discovered connected component as an org.latha.island record * on the user's PDS. Reads the SQLite cache from island:discover. * * The canonical island ID is the first 12 hex chars of the SHA-256 of the * lexmin vertex (lexicographically smallest URL in the component). This is * stable across runs — the same component always gets the same ID. * * Usage: * bun island:publish # list islands with their IDs * bun island:publish a1b2c3d4e5f6 # publish by canonical ID * bun island:publish a1b2c3d4e5f6 --dry-run # preview without writing * bun island:publish a1b2c3d4e5f6 --json # JSON output */ import { resolve, dirname } from 'node:path'; import { fileURLToPath } from 'node:url'; import { AtpAgent } from '@atproto/api'; import { loadDotEnv } from './cli-utils.js'; import { openDb, findComponents, findComponentById, fetchComponentEdges, resolveDidHandles, domainFromUrl, type Component, type EdgeRow } from './island-shared.js'; const __dirname = dirname(fileURLToPath(import.meta.url)); await loadDotEnv(resolve(__dirname, '..', '.env')); await loadDotEnv('/home/nandi/code/swarm/.env'); // fallback for shared credentials const BLUESKY_HANDLE = process.env.BLUESKY_IDENTIFIER ?? ''; const BLUESKY_APP_PASSWORD = process.env.BLUESKY_APP_PASSWORD ?? ''; const PDS_IDENTIFIER = process.env.PDS_IDENTIFIER ?? BLUESKY_HANDLE; const PDS_APP_PASSWORD = process.env.PDS_APP_PASSWORD ?? BLUESKY_APP_PASSWORD; const PDS_SERVICE = process.env.PDS_SERVICE ?? 'https://amanita.us-east.host.bsky.network'; // --- Build island record --- function buildIslandRecord(component: Component, edges: EdgeRow[], didHandles: Map) { const lexmin = component.lexmin; const domain = domainFromUrl(lexmin); const nodeCount = component.nodes.size; const edgeCount = edges.length; const contributorCount = component.dids.size; const connections = edges.map((e) => ({ uri: '', // no AT URI for discovered edges — they're aggregated from multiple DIDs source: e.source, target: e.target, ...(e.relation ? { connectionType: e.relation } : {}), note: `from ${didHandles.get(e.did) ?? e.did}`, })); const title = `Island: ${domain} (${nodeCount} nodes, ${edgeCount} edges)`; const themes = ['discovered-island']; const synthesis = [ `Discovered island centered on ${domain}.`, `${nodeCount} nodes connected by ${edgeCount} edges, contributed by ${contributorCount} DIDs.`, `Lexmin vertex: ${lexmin}`, `Contributors: ${[...component.dids].map((d) => didHandles.get(d) ?? d).join(', ')}`, ].join('\n\n'); return { $type: 'org.latha.island', source: { uri: lexmin, collection: 'network.cosmik.connection', }, connections, analysis: { title, themes, synthesis, }, createdAt: new Date().toISOString(), }; } // --- Main --- async function main(): Promise { const args = process.argv.slice(2); const wantJson = args.includes('--json'); const dryRun = args.includes('--dry-run'); const targetId = args.find((a) => !a.startsWith('--')); // Open DB let db; try { db = openDb(); const count = (db.query('SELECT COUNT(*) as c FROM edges').get() as { c: number } | null)?.c ?? 0; if (count === 0) { console.error('No edges found. Run `bun island:discover` first.'); process.exit(1); } } catch { console.error('Database not found. Run `bun island:discover` first.'); process.exit(1); } console.error('Finding components...'); const components = findComponents(db); // No positional arg: list all islands if (!targetId) { if (wantJson) { console.log(JSON.stringify(components.map((c) => ({ id: c.islandId, lexmin: c.lexmin, nodeCount: c.nodes.size, didCount: c.dids.size, })), null, 2)); } else { console.log(`\n${components.length} islands:\n`); for (const c of components.slice(0, 50)) { console.log(` ${c.islandId} ${c.nodes.size} nodes ${domainFromUrl(c.lexmin)}`); } if (components.length > 50) console.log(` ... and ${components.length - 50} more`); console.log(`\nUsage: bun island:publish [--dry-run] [--json]`); } db.close(); return; } const component = components.find((c) => c.islandId === targetId); if (!component) { console.error(`Island ${targetId} not found. Run without args to list available islands.`); process.exit(1); } console.error(`Island ${component.islandId}: ${component.nodes.size} nodes, ${component.dids.size} DIDs (lexmin: ${component.lexmin})`); const edges = fetchComponentEdges(db, component); const didHandles = resolveDidHandles(db, component.dids); const record = buildIslandRecord(component, edges, didHandles); if (dryRun) { console.error('[dry-run] Would publish:'); if (wantJson) { console.log(JSON.stringify({ islandId: component.islandId, record }, null, 2)); } else { console.log(`Title: ${record.analysis.title}`); console.log(`Source: ${record.source.uri}`); console.log(`Connections: ${record.connections.length}`); console.log(`Themes: ${record.analysis.themes.join(', ')}`); console.log(`\nSynthesis:\n${record.analysis.synthesis}`); console.log(`\nConnections:`); for (const c of record.connections) { console.log(` ${c.source} -> ${c.target} ${c.connectionType ?? ''} (${c.note})`); } } db.close(); return; } // Publish to PDS if (!PDS_IDENTIFIER || !PDS_APP_PASSWORD) { console.error('PDS_IDENTIFIER and PDS_APP_PASSWORD (or BLUESKY_IDENTIFIER/BLUESKY_APP_PASSWORD) must be set'); process.exit(1); } const agent = new AtpAgent({ service: PDS_SERVICE }); await agent.login({ identifier: PDS_IDENTIFIER, password: PDS_APP_PASSWORD }); const result = await agent.api.com.atproto.repo.createRecord({ repo: agent.session!.did, collection: 'org.latha.island', rkey: component.islandId, record, }); const atUri = result.data.uri; console.error(`Published: ${atUri}`); if (wantJson) { console.log(JSON.stringify({ islandId: component.islandId, atUri, record }, null, 2)); } else { console.log(atUri); } db.close(); } main().catch((err) => { console.error('Fatal:', err instanceof Error ? err.message : String(err)); process.exit(1); });