@pagebridge/cli 0.0.2 → 0.1.1
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.
- package/LICENSE +21 -21
- package/README.md +88 -93
- package/dist/commands/doctor.d.ts +3 -0
- package/dist/commands/doctor.d.ts.map +1 -0
- package/dist/commands/doctor.js +353 -0
- package/dist/commands/init.d.ts +3 -0
- package/dist/commands/init.d.ts.map +1 -0
- package/dist/commands/init.js +182 -0
- package/dist/index.js +5 -1
- package/dist/resolve-config.d.ts +4 -1
- package/dist/resolve-config.d.ts.map +1 -1
- package/dist/resolve-config.js +8 -3
- package/package.json +23 -5
- package/.turbo/turbo-build.log +0 -2
- package/.turbo/turbo-check-types.log +0 -4
- package/eslint.config.js +0 -3
- package/src/commands/diagnose.ts +0 -149
- package/src/commands/list-sites.ts +0 -51
- package/src/commands/sync.ts +0 -471
- package/src/index.ts +0 -18
- package/src/logger.ts +0 -22
- package/src/migrate.ts +0 -13
- package/src/resolve-config.ts +0 -32
- package/tsconfig.json +0 -9
package/src/commands/sync.ts
DELETED
|
@@ -1,471 +0,0 @@
|
|
|
1
|
-
import { Command } from "commander";
|
|
2
|
-
import { createClient as createSanityClient } from "@sanity/client";
|
|
3
|
-
import {
|
|
4
|
-
GSCClient,
|
|
5
|
-
SyncEngine,
|
|
6
|
-
DecayDetector,
|
|
7
|
-
URLMatcher,
|
|
8
|
-
TaskGenerator,
|
|
9
|
-
type MatchResult,
|
|
10
|
-
type UnmatchReason,
|
|
11
|
-
} from "@pagebridge/core";
|
|
12
|
-
import { createDb, sql, unmatchDiagnostics } from "@pagebridge/db";
|
|
13
|
-
import { resolve, requireConfig } from "../resolve-config.js";
|
|
14
|
-
import { log } from "../logger.js";
|
|
15
|
-
import { migrateIfRequested } from "../migrate.js";
|
|
16
|
-
|
|
17
|
-
function daysAgo(days: number): Date {
|
|
18
|
-
const date = new Date();
|
|
19
|
-
date.setDate(date.getDate() - days);
|
|
20
|
-
return date;
|
|
21
|
-
}
|
|
22
|
-
|
|
23
|
-
function createTimer(debug: boolean) {
|
|
24
|
-
return {
|
|
25
|
-
start: () => performance.now(),
|
|
26
|
-
end: (label: string, startTime: number) => {
|
|
27
|
-
if (debug) {
|
|
28
|
-
const elapsed = ((performance.now() - startTime) / 1000).toFixed(2);
|
|
29
|
-
log.debug(`${label} completed in ${elapsed}s`, true);
|
|
30
|
-
}
|
|
31
|
-
},
|
|
32
|
-
};
|
|
33
|
-
}
|
|
34
|
-
|
|
35
|
-
export const syncCommand = new Command("sync")
|
|
36
|
-
.description("Sync GSC data and generate refresh tasks")
|
|
37
|
-
.requiredOption("--site <url>", "GSC site URL (e.g., sc-domain:example.com)")
|
|
38
|
-
.option("--dry-run", "Preview changes without writing to Sanity")
|
|
39
|
-
.option("--skip-tasks", "Only sync data, do not generate tasks")
|
|
40
|
-
.option("--check-index", "Check Google index status for matched pages")
|
|
41
|
-
.option("--quiet-period <days>", "Ignore pages published within N days", "45")
|
|
42
|
-
.option("--diagnose", "Show detailed diagnostics for unmatched URLs")
|
|
43
|
-
.option("--diagnose-url <url>", "Diagnose why a specific URL is not matching")
|
|
44
|
-
.option("--debug", "Enable debug logging with timing information")
|
|
45
|
-
.option("--migrate", "Run database migrations before syncing")
|
|
46
|
-
.option("--google-service-account <json>", "Google service account JSON")
|
|
47
|
-
.option("--db-url <url>", "PostgreSQL connection string")
|
|
48
|
-
.option("--sanity-project-id <id>", "Sanity project ID")
|
|
49
|
-
.option("--sanity-dataset <name>", "Sanity dataset name")
|
|
50
|
-
.option("--sanity-token <token>", "Sanity API token")
|
|
51
|
-
.option("--site-url <url>", "Your website base URL for URL matching")
|
|
52
|
-
.action(async (options) => {
|
|
53
|
-
const timer = createTimer(options.debug);
|
|
54
|
-
const syncStartTime = timer.start();
|
|
55
|
-
|
|
56
|
-
// Validate --quiet-period
|
|
57
|
-
const quietPeriodDays = parseInt(options.quietPeriod as string);
|
|
58
|
-
if (isNaN(quietPeriodDays)) {
|
|
59
|
-
log.error(`Invalid --quiet-period value: "${options.quietPeriod}". Must be a number.`);
|
|
60
|
-
process.exit(1);
|
|
61
|
-
}
|
|
62
|
-
|
|
63
|
-
const googleServiceAccount = resolve(options.googleServiceAccount, "GOOGLE_SERVICE_ACCOUNT");
|
|
64
|
-
const dbUrl = resolve(options.dbUrl, "DATABASE_URL");
|
|
65
|
-
const sanityProjectId = resolve(options.sanityProjectId, "SANITY_PROJECT_ID");
|
|
66
|
-
const sanityDataset = resolve(options.sanityDataset, "SANITY_DATASET");
|
|
67
|
-
const sanityToken = resolve(options.sanityToken, "SANITY_TOKEN");
|
|
68
|
-
const siteUrl = resolve(options.siteUrl, "SITE_URL");
|
|
69
|
-
|
|
70
|
-
requireConfig([
|
|
71
|
-
{ name: "GOOGLE_SERVICE_ACCOUNT", flag: "--google-service-account <json>", envVar: "GOOGLE_SERVICE_ACCOUNT", value: googleServiceAccount },
|
|
72
|
-
{ name: "DATABASE_URL", flag: "--db-url <url>", envVar: "DATABASE_URL", value: dbUrl },
|
|
73
|
-
{ name: "SANITY_PROJECT_ID", flag: "--sanity-project-id <id>", envVar: "SANITY_PROJECT_ID", value: sanityProjectId },
|
|
74
|
-
{ name: "SANITY_DATASET", flag: "--sanity-dataset <name>", envVar: "SANITY_DATASET", value: sanityDataset },
|
|
75
|
-
{ name: "SANITY_TOKEN", flag: "--sanity-token <token>", envVar: "SANITY_TOKEN", value: sanityToken },
|
|
76
|
-
{ name: "SITE_URL", flag: "--site-url <url>", envVar: "SITE_URL", value: siteUrl },
|
|
77
|
-
]);
|
|
78
|
-
|
|
79
|
-
// Run migrations if requested
|
|
80
|
-
await migrateIfRequested(!!options.migrate, dbUrl!);
|
|
81
|
-
|
|
82
|
-
let t = timer.start();
|
|
83
|
-
const sanity = createSanityClient({
|
|
84
|
-
projectId: sanityProjectId!,
|
|
85
|
-
dataset: sanityDataset!,
|
|
86
|
-
token: sanityToken!,
|
|
87
|
-
apiVersion: "2024-01-01",
|
|
88
|
-
useCdn: false,
|
|
89
|
-
});
|
|
90
|
-
|
|
91
|
-
const { db, close } = createDb(dbUrl!);
|
|
92
|
-
|
|
93
|
-
let credentials: { client_email: string; private_key: string };
|
|
94
|
-
try {
|
|
95
|
-
credentials = JSON.parse(googleServiceAccount!) as typeof credentials;
|
|
96
|
-
} catch {
|
|
97
|
-
log.error("Failed to parse GOOGLE_SERVICE_ACCOUNT as JSON");
|
|
98
|
-
await close();
|
|
99
|
-
process.exit(1);
|
|
100
|
-
}
|
|
101
|
-
|
|
102
|
-
const gsc = new GSCClient({ credentials });
|
|
103
|
-
timer.end("Client initialization", t);
|
|
104
|
-
|
|
105
|
-
// Register shutdown handlers
|
|
106
|
-
const shutdown = async () => {
|
|
107
|
-
log.warn("Received shutdown signal, closing connections...");
|
|
108
|
-
await close();
|
|
109
|
-
process.exit(130);
|
|
110
|
-
};
|
|
111
|
-
process.on("SIGTERM", shutdown);
|
|
112
|
-
process.on("SIGINT", shutdown);
|
|
113
|
-
|
|
114
|
-
// Pre-flight connection validation
|
|
115
|
-
try {
|
|
116
|
-
log.info("Validating connections...");
|
|
117
|
-
|
|
118
|
-
t = timer.start();
|
|
119
|
-
await db.execute(sql`SELECT 1`);
|
|
120
|
-
timer.end("DB connection check", t);
|
|
121
|
-
|
|
122
|
-
t = timer.start();
|
|
123
|
-
await sanity.fetch('*[_type == "gscSite"][0]{ _id }');
|
|
124
|
-
timer.end("Sanity connection check", t);
|
|
125
|
-
|
|
126
|
-
t = timer.start();
|
|
127
|
-
const sites = await gsc.listSites();
|
|
128
|
-
timer.end("GSC connection check", t);
|
|
129
|
-
if (!sites.includes(options.site as string)) {
|
|
130
|
-
log.warn(`Site "${options.site}" not found in GSC site list. It may be new or the service account may lack access.`);
|
|
131
|
-
}
|
|
132
|
-
} catch (error) {
|
|
133
|
-
const message = error instanceof Error ? error.message : String(error);
|
|
134
|
-
log.error(`Pre-flight connection check failed: ${message}`);
|
|
135
|
-
await close();
|
|
136
|
-
process.exitCode = 1;
|
|
137
|
-
process.removeListener("SIGTERM", shutdown);
|
|
138
|
-
process.removeListener("SIGINT", shutdown);
|
|
139
|
-
return;
|
|
140
|
-
}
|
|
141
|
-
|
|
142
|
-
log.info(`Starting sync for ${options.site}...`);
|
|
143
|
-
|
|
144
|
-
// Find or create the gscSite document in Sanity
|
|
145
|
-
t = timer.start();
|
|
146
|
-
let siteDoc = await sanity.fetch<{
|
|
147
|
-
_id: string;
|
|
148
|
-
pathPrefix?: string;
|
|
149
|
-
contentTypes?: string[];
|
|
150
|
-
slugField?: string;
|
|
151
|
-
} | null>(
|
|
152
|
-
`*[_type == "gscSite" && siteUrl == $siteUrl][0]{
|
|
153
|
-
_id,
|
|
154
|
-
pathPrefix,
|
|
155
|
-
contentTypes,
|
|
156
|
-
slugField
|
|
157
|
-
}`,
|
|
158
|
-
{ siteUrl: options.site },
|
|
159
|
-
);
|
|
160
|
-
|
|
161
|
-
if (!siteDoc) {
|
|
162
|
-
log.info(`Creating gscSite document for ${options.site}...`);
|
|
163
|
-
siteDoc = await sanity.create({
|
|
164
|
-
_type: "gscSite",
|
|
165
|
-
siteUrl: options.site,
|
|
166
|
-
enabled: true,
|
|
167
|
-
contentTypes: ["post", "page"],
|
|
168
|
-
slugField: "slug",
|
|
169
|
-
});
|
|
170
|
-
}
|
|
171
|
-
timer.end("Fetch gscSite document", t);
|
|
172
|
-
|
|
173
|
-
const siteId = siteDoc._id;
|
|
174
|
-
|
|
175
|
-
// Use configuration from gscSite document
|
|
176
|
-
const contentTypes = siteDoc.contentTypes ?? ["post", "page"];
|
|
177
|
-
const slugField = siteDoc.slugField ?? "slug";
|
|
178
|
-
const pathPrefix = siteDoc.pathPrefix ?? undefined;
|
|
179
|
-
|
|
180
|
-
log.info(`Configuration:`);
|
|
181
|
-
log.info(` Content types: ${contentTypes.join(", ")}`);
|
|
182
|
-
log.info(` Slug field: ${slugField}`);
|
|
183
|
-
log.info(` Path prefix: ${pathPrefix ?? "(none)"}`);
|
|
184
|
-
|
|
185
|
-
const syncEngine = new SyncEngine({ gsc, db, sanity });
|
|
186
|
-
const matcher = new URLMatcher(sanity, {
|
|
187
|
-
contentTypes,
|
|
188
|
-
slugField,
|
|
189
|
-
baseUrl: siteUrl!,
|
|
190
|
-
pathPrefix,
|
|
191
|
-
});
|
|
192
|
-
|
|
193
|
-
try {
|
|
194
|
-
t = timer.start();
|
|
195
|
-
const { pages, rowsProcessed } = await syncEngine.sync({
|
|
196
|
-
siteUrl: options.site,
|
|
197
|
-
startDate: daysAgo(90),
|
|
198
|
-
endDate: daysAgo(3),
|
|
199
|
-
});
|
|
200
|
-
timer.end("GSC data sync", t);
|
|
201
|
-
|
|
202
|
-
log.info(`Processed ${rowsProcessed} rows for ${pages.length} pages`);
|
|
203
|
-
|
|
204
|
-
t = timer.start();
|
|
205
|
-
const matches = await matcher.matchUrls(pages);
|
|
206
|
-
timer.end("URL matching", t);
|
|
207
|
-
|
|
208
|
-
const matched = matches.filter(
|
|
209
|
-
(m): m is MatchResult & { sanityId: string } => !!m.sanityId,
|
|
210
|
-
);
|
|
211
|
-
const unmatched = matches.filter((m) => !m.sanityId);
|
|
212
|
-
|
|
213
|
-
log.info(
|
|
214
|
-
`Matched ${matched.length}/${pages.length} URLs to Sanity documents`,
|
|
215
|
-
);
|
|
216
|
-
|
|
217
|
-
// Store diagnostics for unmatched URLs
|
|
218
|
-
if (unmatched.length > 0) {
|
|
219
|
-
log.info(`${unmatched.length} unmatched URLs`);
|
|
220
|
-
|
|
221
|
-
// Store diagnostics in database
|
|
222
|
-
t = timer.start();
|
|
223
|
-
for (const u of unmatched) {
|
|
224
|
-
const diagId = `${options.site}:${u.gscUrl}`;
|
|
225
|
-
await db
|
|
226
|
-
.insert(unmatchDiagnostics)
|
|
227
|
-
.values({
|
|
228
|
-
id: diagId,
|
|
229
|
-
siteId: options.site,
|
|
230
|
-
gscUrl: u.gscUrl,
|
|
231
|
-
extractedSlug: u.extractedSlug ?? null,
|
|
232
|
-
unmatchReason: u.unmatchReason,
|
|
233
|
-
normalizedUrl: u.diagnostics?.normalizedUrl ?? null,
|
|
234
|
-
pathAfterPrefix: u.diagnostics?.pathAfterPrefix ?? null,
|
|
235
|
-
configuredPrefix: u.diagnostics?.configuredPrefix ?? null,
|
|
236
|
-
similarSlugs: u.diagnostics?.similarSlugs
|
|
237
|
-
? JSON.stringify(u.diagnostics.similarSlugs)
|
|
238
|
-
: null,
|
|
239
|
-
availableSlugsCount: u.diagnostics?.availableSlugsCount ?? null,
|
|
240
|
-
lastSeenAt: new Date(),
|
|
241
|
-
})
|
|
242
|
-
.onConflictDoUpdate({
|
|
243
|
-
target: unmatchDiagnostics.id,
|
|
244
|
-
set: {
|
|
245
|
-
extractedSlug: u.extractedSlug ?? null,
|
|
246
|
-
unmatchReason: u.unmatchReason,
|
|
247
|
-
normalizedUrl: u.diagnostics?.normalizedUrl ?? null,
|
|
248
|
-
pathAfterPrefix: u.diagnostics?.pathAfterPrefix ?? null,
|
|
249
|
-
configuredPrefix: u.diagnostics?.configuredPrefix ?? null,
|
|
250
|
-
similarSlugs: u.diagnostics?.similarSlugs
|
|
251
|
-
? JSON.stringify(u.diagnostics.similarSlugs)
|
|
252
|
-
: null,
|
|
253
|
-
availableSlugsCount: u.diagnostics?.availableSlugsCount ?? null,
|
|
254
|
-
lastSeenAt: new Date(),
|
|
255
|
-
},
|
|
256
|
-
});
|
|
257
|
-
}
|
|
258
|
-
|
|
259
|
-
// Update gscSite with unmatched count
|
|
260
|
-
await sanity
|
|
261
|
-
.patch(siteId)
|
|
262
|
-
.set({
|
|
263
|
-
unmatchedCount: unmatched.length,
|
|
264
|
-
lastDiagnosticsAt: new Date().toISOString(),
|
|
265
|
-
})
|
|
266
|
-
.commit();
|
|
267
|
-
timer.end("Store unmatched diagnostics", t);
|
|
268
|
-
|
|
269
|
-
// Show detailed diagnostics if --diagnose flag is set
|
|
270
|
-
if (options.diagnose) {
|
|
271
|
-
log.info(`\nUnmatched URL Diagnostics:\n`);
|
|
272
|
-
|
|
273
|
-
// Group by reason
|
|
274
|
-
const byReason = new Map<UnmatchReason, MatchResult[]>();
|
|
275
|
-
for (const u of unmatched) {
|
|
276
|
-
const existing = byReason.get(u.unmatchReason) ?? [];
|
|
277
|
-
existing.push(u);
|
|
278
|
-
byReason.set(u.unmatchReason, existing);
|
|
279
|
-
}
|
|
280
|
-
|
|
281
|
-
for (const [reason, urls] of byReason) {
|
|
282
|
-
log.info(
|
|
283
|
-
` ${getReasonEmoji(reason)} ${getReasonDescription(reason)} (${urls.length}):`,
|
|
284
|
-
);
|
|
285
|
-
const toShow = urls.slice(0, 5);
|
|
286
|
-
for (const u of toShow) {
|
|
287
|
-
log.info(` ${u.gscUrl}`);
|
|
288
|
-
if (u.extractedSlug) {
|
|
289
|
-
log.info(` Extracted slug: "${u.extractedSlug}"`);
|
|
290
|
-
}
|
|
291
|
-
if (u.diagnostics?.similarSlugs?.length) {
|
|
292
|
-
log.info(` Similar slugs in Sanity:`);
|
|
293
|
-
for (const similar of u.diagnostics.similarSlugs) {
|
|
294
|
-
log.info(` - ${similar}`);
|
|
295
|
-
}
|
|
296
|
-
}
|
|
297
|
-
}
|
|
298
|
-
if (urls.length > 5) {
|
|
299
|
-
log.info(` ... and ${urls.length - 5} more`);
|
|
300
|
-
}
|
|
301
|
-
}
|
|
302
|
-
} else if (unmatched.length <= 10) {
|
|
303
|
-
unmatched.forEach((u) => log.info(` - ${u.gscUrl}`));
|
|
304
|
-
log.info(`\n Run with --diagnose for detailed diagnostics`);
|
|
305
|
-
} else {
|
|
306
|
-
log.info(` Run with --diagnose to see detailed diagnostics`);
|
|
307
|
-
}
|
|
308
|
-
}
|
|
309
|
-
|
|
310
|
-
// Handle --diagnose-url for a specific URL
|
|
311
|
-
if (options.diagnoseUrl) {
|
|
312
|
-
const targetUrl = options.diagnoseUrl as string;
|
|
313
|
-
const allUrls = [targetUrl];
|
|
314
|
-
const [result] = await matcher.matchUrls(allUrls);
|
|
315
|
-
log.info(`\nDiagnostics for: ${targetUrl}\n`);
|
|
316
|
-
if (result) {
|
|
317
|
-
log.info(` Matched: ${result.sanityId ? "Yes" : "No"}`);
|
|
318
|
-
log.info(
|
|
319
|
-
` Reason: ${getReasonDescription(result.unmatchReason)}`,
|
|
320
|
-
);
|
|
321
|
-
if (result.extractedSlug) {
|
|
322
|
-
log.info(` Extracted slug: "${result.extractedSlug}"`);
|
|
323
|
-
}
|
|
324
|
-
if (result.matchedSlug) {
|
|
325
|
-
log.info(` Matched to Sanity slug: "${result.matchedSlug}"`);
|
|
326
|
-
}
|
|
327
|
-
if (result.diagnostics) {
|
|
328
|
-
log.info(
|
|
329
|
-
` Normalized URL: ${result.diagnostics.normalizedUrl}`,
|
|
330
|
-
);
|
|
331
|
-
log.info(
|
|
332
|
-
` Path after prefix: ${result.diagnostics.pathAfterPrefix}`,
|
|
333
|
-
);
|
|
334
|
-
log.info(
|
|
335
|
-
` Configured prefix: ${result.diagnostics.configuredPrefix ?? "(none)"}`,
|
|
336
|
-
);
|
|
337
|
-
log.info(
|
|
338
|
-
` Available Sanity slugs: ${result.diagnostics.availableSlugsCount}`,
|
|
339
|
-
);
|
|
340
|
-
if (result.diagnostics.similarSlugs?.length) {
|
|
341
|
-
log.info(` Similar slugs in Sanity:`);
|
|
342
|
-
for (const similar of result.diagnostics.similarSlugs) {
|
|
343
|
-
log.info(` - ${similar}`);
|
|
344
|
-
}
|
|
345
|
-
}
|
|
346
|
-
}
|
|
347
|
-
}
|
|
348
|
-
}
|
|
349
|
-
|
|
350
|
-
// Check index status if requested
|
|
351
|
-
if (options.checkIndex && matched.length > 0) {
|
|
352
|
-
log.info(`\nChecking index status for ${matched.length} pages...`);
|
|
353
|
-
t = timer.start();
|
|
354
|
-
const matchedUrls = matched.map((m) => m.gscUrl);
|
|
355
|
-
const indexResult = await syncEngine.syncIndexStatus(
|
|
356
|
-
options.site,
|
|
357
|
-
matchedUrls,
|
|
358
|
-
);
|
|
359
|
-
timer.end("Index status check", t);
|
|
360
|
-
log.info(
|
|
361
|
-
` Indexed: ${indexResult.indexed}, Not indexed: ${indexResult.notIndexed}, Skipped: ${indexResult.skipped}`,
|
|
362
|
-
);
|
|
363
|
-
}
|
|
364
|
-
|
|
365
|
-
if (!options.skipTasks) {
|
|
366
|
-
t = timer.start();
|
|
367
|
-
const publishedDates = await getPublishedDates(sanity, matched);
|
|
368
|
-
const detector = new DecayDetector(db);
|
|
369
|
-
const signals = await detector.detectDecay(
|
|
370
|
-
options.site,
|
|
371
|
-
publishedDates,
|
|
372
|
-
{
|
|
373
|
-
enabled: true,
|
|
374
|
-
days: quietPeriodDays,
|
|
375
|
-
},
|
|
376
|
-
);
|
|
377
|
-
timer.end("Decay detection", t);
|
|
378
|
-
|
|
379
|
-
log.info(`Detected ${signals.length} decay signals`);
|
|
380
|
-
|
|
381
|
-
if (options.dryRun) {
|
|
382
|
-
log.info("\nWould create the following tasks:");
|
|
383
|
-
signals.forEach((s) => {
|
|
384
|
-
log.info(` [${s.severity.toUpperCase()}] ${s.page}`);
|
|
385
|
-
log.info(` Reason: ${s.reason}`);
|
|
386
|
-
log.info(
|
|
387
|
-
` Position: ${s.metrics.positionBefore} -> ${s.metrics.positionNow}`,
|
|
388
|
-
);
|
|
389
|
-
});
|
|
390
|
-
} else {
|
|
391
|
-
t = timer.start();
|
|
392
|
-
const taskGenerator = new TaskGenerator(sanity);
|
|
393
|
-
const created = await taskGenerator.createTasks(
|
|
394
|
-
siteId,
|
|
395
|
-
signals,
|
|
396
|
-
matches,
|
|
397
|
-
);
|
|
398
|
-
timer.end("Task generation", t);
|
|
399
|
-
log.info(`Created ${created} new refresh tasks`);
|
|
400
|
-
}
|
|
401
|
-
}
|
|
402
|
-
|
|
403
|
-
if (!options.dryRun) {
|
|
404
|
-
t = timer.start();
|
|
405
|
-
await syncEngine.writeSnapshots(siteId, matched);
|
|
406
|
-
timer.end("Write Sanity snapshots", t);
|
|
407
|
-
log.info(`Updated Sanity snapshots`);
|
|
408
|
-
}
|
|
409
|
-
|
|
410
|
-
timer.end("Total sync", syncStartTime);
|
|
411
|
-
log.info(`\nSync complete!`);
|
|
412
|
-
} catch (error) {
|
|
413
|
-
const message = error instanceof Error ? error.message : String(error);
|
|
414
|
-
log.error(`Sync failed: ${message}`);
|
|
415
|
-
process.exitCode = 1;
|
|
416
|
-
} finally {
|
|
417
|
-
await close();
|
|
418
|
-
process.removeListener("SIGTERM", shutdown);
|
|
419
|
-
process.removeListener("SIGINT", shutdown);
|
|
420
|
-
}
|
|
421
|
-
});
|
|
422
|
-
|
|
423
|
-
async function getPublishedDates(
|
|
424
|
-
sanity: ReturnType<typeof createSanityClient>,
|
|
425
|
-
matches: MatchResult[],
|
|
426
|
-
): Promise<Map<string, Date>> {
|
|
427
|
-
const ids = matches.map((m) => m.sanityId).filter(Boolean);
|
|
428
|
-
const docs = await sanity.fetch<
|
|
429
|
-
{ _id: string; _createdAt: string; publishedAt?: string }[]
|
|
430
|
-
>(`*[_id in $ids]{ _id, _createdAt, publishedAt }`, { ids });
|
|
431
|
-
|
|
432
|
-
const map = new Map<string, Date>();
|
|
433
|
-
for (const doc of docs) {
|
|
434
|
-
const match = matches.find((m) => m.sanityId === doc._id);
|
|
435
|
-
if (match) {
|
|
436
|
-
const dateStr = doc.publishedAt ?? doc._createdAt;
|
|
437
|
-
map.set(match.gscUrl, new Date(dateStr));
|
|
438
|
-
}
|
|
439
|
-
}
|
|
440
|
-
return map;
|
|
441
|
-
}
|
|
442
|
-
|
|
443
|
-
function getReasonEmoji(reason: UnmatchReason): string {
|
|
444
|
-
switch (reason) {
|
|
445
|
-
case "matched":
|
|
446
|
-
return "[OK]";
|
|
447
|
-
case "no_slug_extracted":
|
|
448
|
-
return "[SLUG]";
|
|
449
|
-
case "no_matching_document":
|
|
450
|
-
return "[DOC]";
|
|
451
|
-
case "outside_path_prefix":
|
|
452
|
-
return "[PREFIX]";
|
|
453
|
-
default:
|
|
454
|
-
return "[?]";
|
|
455
|
-
}
|
|
456
|
-
}
|
|
457
|
-
|
|
458
|
-
function getReasonDescription(reason: UnmatchReason): string {
|
|
459
|
-
switch (reason) {
|
|
460
|
-
case "matched":
|
|
461
|
-
return "Successfully matched";
|
|
462
|
-
case "no_slug_extracted":
|
|
463
|
-
return "Could not extract slug from URL";
|
|
464
|
-
case "no_matching_document":
|
|
465
|
-
return "No Sanity document with matching slug";
|
|
466
|
-
case "outside_path_prefix":
|
|
467
|
-
return "URL outside configured path prefix";
|
|
468
|
-
default:
|
|
469
|
-
return "Unknown reason";
|
|
470
|
-
}
|
|
471
|
-
}
|
package/src/index.ts
DELETED
|
@@ -1,18 +0,0 @@
|
|
|
1
|
-
#!/usr/bin/env node
|
|
2
|
-
import { program } from "commander";
|
|
3
|
-
import { syncCommand } from "./commands/sync.js";
|
|
4
|
-
import { listSitesCommand } from "./commands/list-sites.js";
|
|
5
|
-
import { diagnoseCommand } from "./commands/diagnose.js";
|
|
6
|
-
|
|
7
|
-
program
|
|
8
|
-
.name("pagebridge")
|
|
9
|
-
.description(
|
|
10
|
-
"PageBridge - Connect Google Search Console to Sanity CMS",
|
|
11
|
-
)
|
|
12
|
-
.version("0.0.1");
|
|
13
|
-
|
|
14
|
-
program.addCommand(syncCommand);
|
|
15
|
-
program.addCommand(listSitesCommand);
|
|
16
|
-
program.addCommand(diagnoseCommand);
|
|
17
|
-
|
|
18
|
-
program.parse();
|
package/src/logger.ts
DELETED
|
@@ -1,22 +0,0 @@
|
|
|
1
|
-
type Level = "INFO" | "WARN" | "ERROR" | "DEBUG";
|
|
2
|
-
|
|
3
|
-
function format(level: Level, msg: string): string {
|
|
4
|
-
return `[${new Date().toISOString()}] [${level}] ${msg}`;
|
|
5
|
-
}
|
|
6
|
-
|
|
7
|
-
export const log = {
|
|
8
|
-
info(msg: string) {
|
|
9
|
-
console.log(format("INFO", msg));
|
|
10
|
-
},
|
|
11
|
-
warn(msg: string) {
|
|
12
|
-
console.warn(format("WARN", msg));
|
|
13
|
-
},
|
|
14
|
-
error(msg: string) {
|
|
15
|
-
console.error(format("ERROR", msg));
|
|
16
|
-
},
|
|
17
|
-
debug(msg: string, enabled: boolean) {
|
|
18
|
-
if (enabled) {
|
|
19
|
-
console.log(format("DEBUG", msg));
|
|
20
|
-
}
|
|
21
|
-
},
|
|
22
|
-
};
|
package/src/migrate.ts
DELETED
|
@@ -1,13 +0,0 @@
|
|
|
1
|
-
import { resolve } from "node:path";
|
|
2
|
-
import { fileURLToPath } from "node:url";
|
|
3
|
-
import { runMigrations } from "@pagebridge/db";
|
|
4
|
-
import { log } from "./logger.js";
|
|
5
|
-
|
|
6
|
-
export async function migrateIfRequested(shouldMigrate: boolean, dbUrl: string) {
|
|
7
|
-
if (!shouldMigrate) return;
|
|
8
|
-
log.info("Running database migrations...");
|
|
9
|
-
const pkgPath = import.meta.resolve("@pagebridge/db");
|
|
10
|
-
const migrationsFolder = resolve(fileURLToPath(pkgPath), "../../drizzle");
|
|
11
|
-
await runMigrations(dbUrl, migrationsFolder);
|
|
12
|
-
log.info("Migrations complete.");
|
|
13
|
-
}
|
package/src/resolve-config.ts
DELETED
|
@@ -1,32 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Resolves a config value from a CLI option first, then env var fallback.
|
|
3
|
-
*/
|
|
4
|
-
export function resolve(
|
|
5
|
-
optionValue: string | undefined,
|
|
6
|
-
envVarName: string,
|
|
7
|
-
): string | undefined {
|
|
8
|
-
return optionValue ?? process.env[envVarName];
|
|
9
|
-
}
|
|
10
|
-
|
|
11
|
-
interface ConfigEntry {
|
|
12
|
-
name: string;
|
|
13
|
-
flag: string;
|
|
14
|
-
envVar: string;
|
|
15
|
-
value: string | undefined;
|
|
16
|
-
}
|
|
17
|
-
|
|
18
|
-
/**
|
|
19
|
-
* Validates that all required config entries have values.
|
|
20
|
-
* If any are missing, prints a clear error listing every missing entry and exits.
|
|
21
|
-
*/
|
|
22
|
-
export function requireConfig(entries: ConfigEntry[]): void {
|
|
23
|
-
const missing = entries.filter((e) => !e.value);
|
|
24
|
-
if (missing.length === 0) return;
|
|
25
|
-
|
|
26
|
-
console.error("Error: Missing required configuration.\n");
|
|
27
|
-
for (const entry of missing) {
|
|
28
|
-
console.error(` ${entry.name}`);
|
|
29
|
-
console.error(` ${entry.flag} or ${entry.envVar} env var\n`);
|
|
30
|
-
}
|
|
31
|
-
process.exit(1);
|
|
32
|
-
}
|