This repository has no description
6.4 kB
183 lines
1/**
2 * island:publish
3 *
4 * Publish a discovered connected component as an org.latha.island record
5 * on the user's PDS. Reads the SQLite cache from island:discover.
6 *
7 * The canonical island ID is the first 12 hex chars of the SHA-256 of the
8 * lexmin vertex (lexicographically smallest URL in the component). This is
9 * stable across runs — the same component always gets the same ID.
10 *
11 * Usage:
12 * bun island:publish # list islands with their IDs
13 * bun island:publish a1b2c3d4e5f6 # publish by canonical ID
14 * bun island:publish a1b2c3d4e5f6 --dry-run # preview without writing
15 * bun island:publish a1b2c3d4e5f6 --json # JSON output
16 */
17
18import { resolve, dirname } from 'node:path';
19import { fileURLToPath } from 'node:url';
20import { AtpAgent } from '@atproto/api';
21import { loadDotEnv } from './cli-utils.js';
22import { openDb, findComponents, findComponentById, fetchComponentEdges, resolveDidHandles, domainFromUrl, type Component, type EdgeRow } from './island-shared.js';
23
24const __dirname = dirname(fileURLToPath(import.meta.url));
25await loadDotEnv(resolve(__dirname, '..', '.env'));
26await loadDotEnv('/home/nandi/code/swarm/.env'); // fallback for shared credentials
27
28const BLUESKY_HANDLE = process.env.BLUESKY_IDENTIFIER ?? '';
29const BLUESKY_APP_PASSWORD = process.env.BLUESKY_APP_PASSWORD ?? '';
30const PDS_IDENTIFIER = process.env.PDS_IDENTIFIER ?? BLUESKY_HANDLE;
31const PDS_APP_PASSWORD = process.env.PDS_APP_PASSWORD ?? BLUESKY_APP_PASSWORD;
32const PDS_SERVICE = process.env.PDS_SERVICE ?? 'https://amanita.us-east.host.bsky.network';
33
34// --- Build island record ---
35
36function buildIslandRecord(component: Component, edges: EdgeRow[], didHandles: Map<string, string | null>) {
37 const lexmin = component.lexmin;
38 const domain = domainFromUrl(lexmin);
39 const nodeCount = component.nodes.size;
40 const edgeCount = edges.length;
41 const contributorCount = component.dids.size;
42
43 const connections = edges.map((e) => ({
44 uri: '', // no AT URI for discovered edges — they're aggregated from multiple DIDs
45 source: e.source,
46 target: e.target,
47 ...(e.relation ? { connectionType: e.relation } : {}),
48 note: `from ${didHandles.get(e.did) ?? e.did}`,
49 }));
50
51 const title = `Island: ${domain} (${nodeCount} nodes, ${edgeCount} edges)`;
52 const themes = ['discovered-island'];
53 const synthesis = [
54 `Discovered island centered on ${domain}.`,
55 `${nodeCount} nodes connected by ${edgeCount} edges, contributed by ${contributorCount} DIDs.`,
56 `Lexmin vertex: ${lexmin}`,
57 `Contributors: ${[...component.dids].map((d) => didHandles.get(d) ?? d).join(', ')}`,
58 ].join('\n\n');
59
60 return {
61 $type: 'org.latha.island',
62 source: {
63 uri: lexmin,
64 collection: 'network.cosmik.connection',
65 },
66 connections,
67 analysis: {
68 title,
69 themes,
70 synthesis,
71 },
72 createdAt: new Date().toISOString(),
73 };
74}
75
76// --- Main ---
77
78async function main(): Promise<void> {
79 const args = process.argv.slice(2);
80 const wantJson = args.includes('--json');
81 const dryRun = args.includes('--dry-run');
82 const targetId = args.find((a) => !a.startsWith('--'));
83
84 // Open DB
85 let db;
86 try {
87 db = openDb();
88 const count = (db.query('SELECT COUNT(*) as c FROM edges').get() as { c: number } | null)?.c ?? 0;
89 if (count === 0) {
90 console.error('No edges found. Run `bun island:discover` first.');
91 process.exit(1);
92 }
93 } catch {
94 console.error('Database not found. Run `bun island:discover` first.');
95 process.exit(1);
96 }
97
98 console.error('Finding components...');
99 const components = findComponents(db);
100
101 // No positional arg: list all islands
102 if (!targetId) {
103 if (wantJson) {
104 console.log(JSON.stringify(components.map((c) => ({
105 id: c.islandId,
106 lexmin: c.lexmin,
107 nodeCount: c.nodes.size,
108 didCount: c.dids.size,
109 })), null, 2));
110 } else {
111 console.log(`\n${components.length} islands:\n`);
112 for (const c of components.slice(0, 50)) {
113 console.log(` ${c.islandId} ${c.nodes.size} nodes ${domainFromUrl(c.lexmin)}`);
114 }
115 if (components.length > 50) console.log(` ... and ${components.length - 50} more`);
116 console.log(`\nUsage: bun island:publish <island-id> [--dry-run] [--json]`);
117 }
118 db.close();
119 return;
120 }
121
122 const component = components.find((c) => c.islandId === targetId);
123 if (!component) {
124 console.error(`Island ${targetId} not found. Run without args to list available islands.`);
125 process.exit(1);
126 }
127
128 console.error(`Island ${component.islandId}: ${component.nodes.size} nodes, ${component.dids.size} DIDs (lexmin: ${component.lexmin})`);
129
130 const edges = fetchComponentEdges(db, component);
131 const didHandles = resolveDidHandles(db, component.dids);
132 const record = buildIslandRecord(component, edges, didHandles);
133
134 if (dryRun) {
135 console.error('[dry-run] Would publish:');
136 if (wantJson) {
137 console.log(JSON.stringify({ islandId: component.islandId, record }, null, 2));
138 } else {
139 console.log(`Title: ${record.analysis.title}`);
140 console.log(`Source: ${record.source.uri}`);
141 console.log(`Connections: ${record.connections.length}`);
142 console.log(`Themes: ${record.analysis.themes.join(', ')}`);
143 console.log(`\nSynthesis:\n${record.analysis.synthesis}`);
144 console.log(`\nConnections:`);
145 for (const c of record.connections) {
146 console.log(` ${c.source} -> ${c.target} ${c.connectionType ?? ''} (${c.note})`);
147 }
148 }
149 db.close();
150 return;
151 }
152
153 // Publish to PDS
154 if (!PDS_IDENTIFIER || !PDS_APP_PASSWORD) {
155 console.error('PDS_IDENTIFIER and PDS_APP_PASSWORD (or BLUESKY_IDENTIFIER/BLUESKY_APP_PASSWORD) must be set');
156 process.exit(1);
157 }
158
159 const agent = new AtpAgent({ service: PDS_SERVICE });
160 await agent.login({ identifier: PDS_IDENTIFIER, password: PDS_APP_PASSWORD });
161
162 const result = await agent.api.com.atproto.repo.createRecord({
163 repo: agent.session!.did,
164 collection: 'org.latha.island',
165 record,
166 });
167
168 const atUri = result.data.uri;
169 console.error(`Published: ${atUri}`);
170
171 if (wantJson) {
172 console.log(JSON.stringify({ islandId: component.islandId, atUri, record }, null, 2));
173 } else {
174 console.log(atUri);
175 }
176
177 db.close();
178}
179
180main().catch((err) => {
181 console.error('Fatal:', err instanceof Error ? err.message : String(err));
182 process.exit(1);
183});