packages/cli/src/commands/sync.ts 3.7 K raw
1
import { command, flag } from "cmd-ts";
2
import { select, spinner, log } from "@clack/prompts";
3
import * as path from "node:path";
4
import { loadConfig, findConfig } from "../lib/config";
5
import {
6
	loadCredentials,
7
	listAllCredentials,
8
	getCredentials,
9
} from "../lib/credentials";
10
import { getOAuthHandle, getOAuthSession } from "../lib/oauth-store";
11
import { createAgent } from "../lib/atproto";
12
import { syncStateFromPDS } from "../lib/sync";
13
import { exitOnCancel } from "../lib/prompts";
14
15
export const syncCommand = command({
16
	name: "sync",
17
	description: "Sync state from ATProto to restore .sequoia-state.json",
18
	args: {
19
		updateFrontmatter: flag({
20
			long: "update-frontmatter",
21
			short: "u",
22
			description: "Update frontmatter atUri fields in local markdown files",
23
		}),
24
		dryRun: flag({
25
			long: "dry-run",
26
			short: "n",
27
			description: "Preview what would be synced without making changes",
28
		}),
29
	},
30
	handler: async ({ updateFrontmatter, dryRun }) => {
31
		// Load config
32
		const configPath = await findConfig();
33
		if (!configPath) {
34
			log.error("No sequoia.json found. Run 'sequoia init' first.");
35
			process.exit(1);
36
		}
37
38
		const config = await loadConfig(configPath);
39
		const configDir = path.dirname(configPath);
40
41
		log.info(`Site: ${config.siteUrl}`);
42
		log.info(`Publication: ${config.publicationUri}`);
43
44
		// Load credentials
45
		let credentials = await loadCredentials(config.identity);
46
47
		if (!credentials) {
48
			const identities = await listAllCredentials();
49
			if (identities.length === 0) {
50
				log.error(
51
					"No credentials found. Run 'sequoia login' or 'sequoia auth' first.",
52
				);
53
				process.exit(1);
54
			}
55
56
			// Build labels with handles for OAuth sessions
57
			const options = await Promise.all(
58
				identities.map(async (cred) => {
59
					if (cred.type === "oauth") {
60
						const handle = await getOAuthHandle(cred.id);
61
						return {
62
							value: cred.id,
63
							label: `${handle || cred.id} (OAuth)`,
64
						};
65
					}
66
					return {
67
						value: cred.id,
68
						label: `${cred.id} (App Password)`,
69
					};
70
				}),
71
			);
72
73
			log.info("Multiple identities found. Select one to use:");
74
			const selected = exitOnCancel(
75
				await select({
76
					message: "Identity:",
77
					options,
78
				}),
79
			);
80
81
			// Load the selected credentials
82
			const selectedCred = identities.find((c) => c.id === selected);
83
			if (selectedCred?.type === "oauth") {
84
				const session = await getOAuthSession(selected);
85
				if (session) {
86
					const handle = await getOAuthHandle(selected);
87
					credentials = {
88
						type: "oauth",
89
						did: selected,
90
						handle: handle || selected,
91
					};
92
				}
93
			} else {
94
				credentials = await getCredentials(selected);
95
			}
96
97
			if (!credentials) {
98
				log.error("Failed to load selected credentials.");
99
				process.exit(1);
100
			}
101
		}
102
103
		// Create agent
104
		const s = spinner();
105
		const connectingTo =
106
			credentials.type === "oauth" ? credentials.handle : credentials.pdsUrl;
107
		s.start(`Connecting as ${connectingTo}...`);
108
		let agent: Awaited<ReturnType<typeof createAgent>> | undefined;
109
		try {
110
			agent = await createAgent(credentials);
111
			s.stop(`Logged in as ${agent.did}`);
112
		} catch (error) {
113
			s.stop("Failed to login");
114
			log.error(`Failed to login: ${error}`);
115
			process.exit(1);
116
		}
117
118
		// Sync state from PDS
119
		s.start("Fetching documents from PDS...");
120
		const result = await syncStateFromPDS(agent, config, configDir, {
121
			updateFrontmatter,
122
			dryRun,
123
			quiet: false,
124
		});
125
		s.stop(`Found documents on PDS`);
126
127
		if (!dryRun) {
128
			const stateCount = Object.keys(result.state.posts).length;
129
			log.success(`\nSaved .sequoia-state.json (${stateCount} entries)`);
130
131
			if (result.frontmatterUpdatesApplied > 0) {
132
				log.success(
133
					`Updated frontmatter in ${result.frontmatterUpdatesApplied} files`,
134
				);
135
			}
136
		}
137
138
		log.success("\nSync complete!");
139
	},
140
});