@pagebridge/cli 0.0.1 → 0.0.2
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/.turbo/turbo-check-types.log +4 -0
- package/dist/commands/diagnose.d.ts.map +1 -1
- package/dist/commands/diagnose.js +43 -25
- package/dist/commands/list-sites.d.ts.map +1 -1
- package/dist/commands/list-sites.js +22 -17
- package/dist/commands/sync.d.ts.map +1 -1
- package/dist/commands/sync.js +128 -70
- package/dist/index.js +0 -6
- package/dist/logger.d.ts +7 -0
- package/dist/logger.d.ts.map +1 -0
- package/dist/logger.js +19 -0
- package/dist/migrate.d.ts +2 -0
- package/dist/migrate.d.ts.map +1 -0
- package/dist/migrate.js +13 -0
- package/dist/resolve-config.d.ts +17 -0
- package/dist/resolve-config.d.ts.map +1 -0
- package/dist/resolve-config.js +21 -0
- package/package.json +3 -5
- package/src/commands/diagnose.ts +45 -25
- package/src/commands/list-sites.ts +24 -20
- package/src/commands/sync.ts +137 -72
- package/src/index.ts +0 -8
- package/src/logger.ts +22 -0
- package/src/migrate.ts +13 -0
- package/src/resolve-config.ts +32 -0
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Resolves a config value from a CLI option first, then env var fallback.
|
|
3
|
+
*/
|
|
4
|
+
export function resolve(optionValue, envVarName) {
|
|
5
|
+
return optionValue ?? process.env[envVarName];
|
|
6
|
+
}
|
|
7
|
+
/**
|
|
8
|
+
* Validates that all required config entries have values.
|
|
9
|
+
* If any are missing, prints a clear error listing every missing entry and exits.
|
|
10
|
+
*/
|
|
11
|
+
export function requireConfig(entries) {
|
|
12
|
+
const missing = entries.filter((e) => !e.value);
|
|
13
|
+
if (missing.length === 0)
|
|
14
|
+
return;
|
|
15
|
+
console.error("Error: Missing required configuration.\n");
|
|
16
|
+
for (const entry of missing) {
|
|
17
|
+
console.error(` ${entry.name}`);
|
|
18
|
+
console.error(` ${entry.flag} or ${entry.envVar} env var\n`);
|
|
19
|
+
}
|
|
20
|
+
process.exit(1);
|
|
21
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@pagebridge/cli",
|
|
3
|
-
"version": "0.0.
|
|
3
|
+
"version": "0.0.2",
|
|
4
4
|
"private": false,
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"type": "module",
|
|
@@ -10,10 +10,8 @@
|
|
|
10
10
|
"dependencies": {
|
|
11
11
|
"@sanity/client": "^7.14.1",
|
|
12
12
|
"commander": "^14.0.3",
|
|
13
|
-
"
|
|
14
|
-
"
|
|
15
|
-
"@pagebridge/core": "^0.0.1",
|
|
16
|
-
"@pagebridge/db": "^0.0.1"
|
|
13
|
+
"@pagebridge/db": "^0.0.2",
|
|
14
|
+
"@pagebridge/core": "^0.0.1"
|
|
17
15
|
},
|
|
18
16
|
"devDependencies": {
|
|
19
17
|
"@types/node": "^22.15.3",
|
package/src/commands/diagnose.ts
CHANGED
|
@@ -1,11 +1,13 @@
|
|
|
1
1
|
import { Command } from "commander";
|
|
2
|
-
import postgres from "postgres";
|
|
3
2
|
import {
|
|
4
|
-
|
|
3
|
+
createDb,
|
|
5
4
|
unmatchDiagnostics,
|
|
6
5
|
eq,
|
|
7
6
|
desc,
|
|
8
7
|
} from "@pagebridge/db";
|
|
8
|
+
import { resolve, requireConfig } from "../resolve-config.js";
|
|
9
|
+
import { log } from "../logger.js";
|
|
10
|
+
import { migrateIfRequested } from "../migrate.js";
|
|
9
11
|
|
|
10
12
|
export const diagnoseCommand = new Command("diagnose")
|
|
11
13
|
.description("View diagnostics for unmatched URLs")
|
|
@@ -13,14 +15,28 @@ export const diagnoseCommand = new Command("diagnose")
|
|
|
13
15
|
.option("--reason <reason>", "Filter by unmatch reason")
|
|
14
16
|
.option("--limit <n>", "Limit number of results", "20")
|
|
15
17
|
.option("--json", "Output as JSON")
|
|
18
|
+
.option("--migrate", "Run database migrations before querying")
|
|
19
|
+
.option("--db-url <url>", "PostgreSQL connection string")
|
|
16
20
|
.action(async (options) => {
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
+
const dbUrl = resolve(options.dbUrl, "DATABASE_URL");
|
|
22
|
+
|
|
23
|
+
requireConfig([
|
|
24
|
+
{ name: "DATABASE_URL", flag: "--db-url <url>", envVar: "DATABASE_URL", value: dbUrl },
|
|
25
|
+
]);
|
|
26
|
+
|
|
27
|
+
// Run migrations if requested
|
|
28
|
+
await migrateIfRequested(!!options.migrate, dbUrl!);
|
|
29
|
+
|
|
30
|
+
const { db, close } = createDb(dbUrl!);
|
|
21
31
|
|
|
22
|
-
|
|
23
|
-
const
|
|
32
|
+
// Register shutdown handlers
|
|
33
|
+
const shutdown = async () => {
|
|
34
|
+
log.warn("Received shutdown signal, closing connections...");
|
|
35
|
+
await close();
|
|
36
|
+
process.exit(130);
|
|
37
|
+
};
|
|
38
|
+
process.on("SIGTERM", shutdown);
|
|
39
|
+
process.on("SIGINT", shutdown);
|
|
24
40
|
|
|
25
41
|
try {
|
|
26
42
|
const query = db
|
|
@@ -38,8 +54,8 @@ export const diagnoseCommand = new Command("diagnose")
|
|
|
38
54
|
}
|
|
39
55
|
|
|
40
56
|
if (results.length === 0) {
|
|
41
|
-
|
|
42
|
-
|
|
57
|
+
log.info(`No unmatched URLs found for ${options.site}`);
|
|
58
|
+
log.info(
|
|
43
59
|
`Run 'sync --site ${options.site}' first to generate diagnostics.`,
|
|
44
60
|
);
|
|
45
61
|
return;
|
|
@@ -53,48 +69,52 @@ export const diagnoseCommand = new Command("diagnose")
|
|
|
53
69
|
byReason.set(r.unmatchReason, existing);
|
|
54
70
|
}
|
|
55
71
|
|
|
56
|
-
|
|
57
|
-
|
|
72
|
+
log.info(`\nUnmatched URL Diagnostics for ${options.site}\n`);
|
|
73
|
+
log.info(`Total: ${results.length} unmatched URLs\n`);
|
|
58
74
|
|
|
59
75
|
for (const [reason, items] of byReason) {
|
|
60
|
-
|
|
76
|
+
log.info(
|
|
61
77
|
`${getReasonEmoji(reason)} ${getReasonDescription(reason)} (${items.length}):`,
|
|
62
78
|
);
|
|
63
|
-
console.log();
|
|
64
79
|
|
|
65
80
|
for (const item of items) {
|
|
66
|
-
|
|
81
|
+
log.info(` ${item.gscUrl}`);
|
|
67
82
|
if (item.extractedSlug) {
|
|
68
|
-
|
|
83
|
+
log.info(` Extracted slug: "${item.extractedSlug}"`);
|
|
69
84
|
}
|
|
70
85
|
if (item.similarSlugs) {
|
|
71
86
|
try {
|
|
72
87
|
const similar = JSON.parse(item.similarSlugs) as string[];
|
|
73
88
|
if (similar.length > 0) {
|
|
74
|
-
|
|
89
|
+
log.info(` Similar slugs in Sanity:`);
|
|
75
90
|
for (const s of similar) {
|
|
76
|
-
|
|
91
|
+
log.info(` - ${s}`);
|
|
77
92
|
}
|
|
78
93
|
}
|
|
79
94
|
} catch {
|
|
80
95
|
// Ignore parse errors
|
|
81
96
|
}
|
|
82
97
|
}
|
|
83
|
-
console.log();
|
|
84
98
|
}
|
|
85
99
|
}
|
|
86
100
|
|
|
87
|
-
|
|
88
|
-
|
|
101
|
+
log.info(`\nTo fix unmatched URLs:`);
|
|
102
|
+
log.info(
|
|
89
103
|
` 1. Check if the Sanity document exists with the correct slug`,
|
|
90
104
|
);
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
105
|
+
log.info(` 2. Verify the document type is in the contentTypes list`);
|
|
106
|
+
log.info(` 3. Ensure the slug field name matches your configuration`);
|
|
107
|
+
log.info(
|
|
94
108
|
` 4. If using a path prefix, verify it matches your URL structure`,
|
|
95
109
|
);
|
|
110
|
+
} catch (error) {
|
|
111
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
112
|
+
log.error(`Diagnose failed: ${message}`);
|
|
113
|
+
process.exitCode = 1;
|
|
96
114
|
} finally {
|
|
97
|
-
await
|
|
115
|
+
await close();
|
|
116
|
+
process.removeListener("SIGTERM", shutdown);
|
|
117
|
+
process.removeListener("SIGINT", shutdown);
|
|
98
118
|
}
|
|
99
119
|
});
|
|
100
120
|
|
|
@@ -1,23 +1,27 @@
|
|
|
1
1
|
import { Command } from "commander";
|
|
2
2
|
import { GSCClient } from "@pagebridge/core";
|
|
3
|
+
import { resolve, requireConfig } from "../resolve-config.js";
|
|
4
|
+
import { log } from "../logger.js";
|
|
3
5
|
|
|
4
6
|
export const listSitesCommand = new Command("list-sites")
|
|
5
7
|
.description("List all sites the service account has access to")
|
|
6
|
-
.
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
8
|
+
.option("--google-service-account <json>", "Google service account JSON")
|
|
9
|
+
.action(async (options) => {
|
|
10
|
+
const googleServiceAccount = resolve(options.googleServiceAccount, "GOOGLE_SERVICE_ACCOUNT");
|
|
11
|
+
|
|
12
|
+
requireConfig([
|
|
13
|
+
{ name: "GOOGLE_SERVICE_ACCOUNT", flag: "--google-service-account <json>", envVar: "GOOGLE_SERVICE_ACCOUNT", value: googleServiceAccount },
|
|
14
|
+
]);
|
|
11
15
|
|
|
12
16
|
let credentials;
|
|
13
17
|
try {
|
|
14
|
-
credentials = JSON.parse(
|
|
18
|
+
credentials = JSON.parse(googleServiceAccount!);
|
|
15
19
|
} catch {
|
|
16
|
-
|
|
20
|
+
log.error("Failed to parse GOOGLE_SERVICE_ACCOUNT as JSON");
|
|
17
21
|
process.exit(1);
|
|
18
22
|
}
|
|
19
23
|
|
|
20
|
-
|
|
24
|
+
log.info(`Using service account: ${credentials.client_email}`);
|
|
21
25
|
|
|
22
26
|
const gsc = new GSCClient({ credentials });
|
|
23
27
|
|
|
@@ -25,23 +29,23 @@ export const listSitesCommand = new Command("list-sites")
|
|
|
25
29
|
const sites = await gsc.listSites();
|
|
26
30
|
|
|
27
31
|
if (sites.length === 0) {
|
|
28
|
-
|
|
29
|
-
|
|
32
|
+
log.warn("No sites found. The service account has no access to any GSC properties.");
|
|
33
|
+
log.info("\nTo fix this:");
|
|
34
|
+
log.info(
|
|
35
|
+
"1. Go to Google Search Console > Settings > Users and permissions",
|
|
30
36
|
);
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
"1. Go to Google Search Console → Settings → Users and permissions",
|
|
34
|
-
);
|
|
35
|
-
console.log(`2. Add user: ${credentials.client_email}`);
|
|
36
|
-
console.log("3. Set permission level to 'Full'");
|
|
37
|
+
log.info(`2. Add user: ${credentials.client_email}`);
|
|
38
|
+
log.info("3. Set permission level to 'Full'");
|
|
37
39
|
} else {
|
|
38
|
-
|
|
39
|
-
sites.forEach((site) =>
|
|
40
|
-
|
|
40
|
+
log.info(`Found ${sites.length} site(s):\n`);
|
|
41
|
+
sites.forEach((site) => log.info(` ${site}`));
|
|
42
|
+
log.info(
|
|
41
43
|
'\nUse one of these exact values with: pnpm sync --site "<value>"',
|
|
42
44
|
);
|
|
43
45
|
}
|
|
44
46
|
} catch (error) {
|
|
45
|
-
|
|
47
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
48
|
+
log.error(`Failed to list sites: ${message}`);
|
|
49
|
+
process.exit(1);
|
|
46
50
|
}
|
|
47
51
|
});
|
package/src/commands/sync.ts
CHANGED
|
@@ -1,6 +1,5 @@
|
|
|
1
1
|
import { Command } from "commander";
|
|
2
2
|
import { createClient as createSanityClient } from "@sanity/client";
|
|
3
|
-
import postgres from "postgres";
|
|
4
3
|
import {
|
|
5
4
|
GSCClient,
|
|
6
5
|
SyncEngine,
|
|
@@ -10,7 +9,10 @@ import {
|
|
|
10
9
|
type MatchResult,
|
|
11
10
|
type UnmatchReason,
|
|
12
11
|
} from "@pagebridge/core";
|
|
13
|
-
import {
|
|
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";
|
|
14
16
|
|
|
15
17
|
function daysAgo(days: number): Date {
|
|
16
18
|
const date = new Date();
|
|
@@ -24,7 +26,7 @@ function createTimer(debug: boolean) {
|
|
|
24
26
|
end: (label: string, startTime: number) => {
|
|
25
27
|
if (debug) {
|
|
26
28
|
const elapsed = ((performance.now() - startTime) / 1000).toFixed(2);
|
|
27
|
-
|
|
29
|
+
log.debug(`${label} completed in ${elapsed}s`, true);
|
|
28
30
|
}
|
|
29
31
|
},
|
|
30
32
|
};
|
|
@@ -40,43 +42,104 @@ export const syncCommand = new Command("sync")
|
|
|
40
42
|
.option("--diagnose", "Show detailed diagnostics for unmatched URLs")
|
|
41
43
|
.option("--diagnose-url <url>", "Diagnose why a specific URL is not matching")
|
|
42
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")
|
|
43
52
|
.action(async (options) => {
|
|
44
53
|
const timer = createTimer(options.debug);
|
|
45
54
|
const syncStartTime = timer.start();
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
"
|
|
51
|
-
|
|
52
|
-
"SITE_URL",
|
|
53
|
-
];
|
|
54
|
-
|
|
55
|
-
for (const envVar of requiredEnvVars) {
|
|
56
|
-
if (!process.env[envVar]) {
|
|
57
|
-
console.error(`Missing required environment variable: ${envVar}`);
|
|
58
|
-
process.exit(1);
|
|
59
|
-
}
|
|
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);
|
|
60
61
|
}
|
|
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
|
+
|
|
62
82
|
let t = timer.start();
|
|
63
83
|
const sanity = createSanityClient({
|
|
64
|
-
projectId:
|
|
65
|
-
dataset:
|
|
66
|
-
token:
|
|
84
|
+
projectId: sanityProjectId!,
|
|
85
|
+
dataset: sanityDataset!,
|
|
86
|
+
token: sanityToken!,
|
|
67
87
|
apiVersion: "2024-01-01",
|
|
68
88
|
useCdn: false,
|
|
69
89
|
});
|
|
70
90
|
|
|
71
|
-
const
|
|
72
|
-
const db = createDbWithClient(sql);
|
|
91
|
+
const { db, close } = createDb(dbUrl!);
|
|
73
92
|
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
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 });
|
|
77
103
|
timer.end("Client initialization", t);
|
|
78
104
|
|
|
79
|
-
|
|
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}...`);
|
|
80
143
|
|
|
81
144
|
// Find or create the gscSite document in Sanity
|
|
82
145
|
t = timer.start();
|
|
@@ -96,7 +159,7 @@ export const syncCommand = new Command("sync")
|
|
|
96
159
|
);
|
|
97
160
|
|
|
98
161
|
if (!siteDoc) {
|
|
99
|
-
|
|
162
|
+
log.info(`Creating gscSite document for ${options.site}...`);
|
|
100
163
|
siteDoc = await sanity.create({
|
|
101
164
|
_type: "gscSite",
|
|
102
165
|
siteUrl: options.site,
|
|
@@ -114,16 +177,16 @@ export const syncCommand = new Command("sync")
|
|
|
114
177
|
const slugField = siteDoc.slugField ?? "slug";
|
|
115
178
|
const pathPrefix = siteDoc.pathPrefix ?? undefined;
|
|
116
179
|
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
180
|
+
log.info(`Configuration:`);
|
|
181
|
+
log.info(` Content types: ${contentTypes.join(", ")}`);
|
|
182
|
+
log.info(` Slug field: ${slugField}`);
|
|
183
|
+
log.info(` Path prefix: ${pathPrefix ?? "(none)"}`);
|
|
121
184
|
|
|
122
185
|
const syncEngine = new SyncEngine({ gsc, db, sanity });
|
|
123
186
|
const matcher = new URLMatcher(sanity, {
|
|
124
187
|
contentTypes,
|
|
125
188
|
slugField,
|
|
126
|
-
baseUrl:
|
|
189
|
+
baseUrl: siteUrl!,
|
|
127
190
|
pathPrefix,
|
|
128
191
|
});
|
|
129
192
|
|
|
@@ -136,7 +199,7 @@ export const syncCommand = new Command("sync")
|
|
|
136
199
|
});
|
|
137
200
|
timer.end("GSC data sync", t);
|
|
138
201
|
|
|
139
|
-
|
|
202
|
+
log.info(`Processed ${rowsProcessed} rows for ${pages.length} pages`);
|
|
140
203
|
|
|
141
204
|
t = timer.start();
|
|
142
205
|
const matches = await matcher.matchUrls(pages);
|
|
@@ -147,13 +210,13 @@ export const syncCommand = new Command("sync")
|
|
|
147
210
|
);
|
|
148
211
|
const unmatched = matches.filter((m) => !m.sanityId);
|
|
149
212
|
|
|
150
|
-
|
|
213
|
+
log.info(
|
|
151
214
|
`Matched ${matched.length}/${pages.length} URLs to Sanity documents`,
|
|
152
215
|
);
|
|
153
216
|
|
|
154
217
|
// Store diagnostics for unmatched URLs
|
|
155
218
|
if (unmatched.length > 0) {
|
|
156
|
-
|
|
219
|
+
log.info(`${unmatched.length} unmatched URLs`);
|
|
157
220
|
|
|
158
221
|
// Store diagnostics in database
|
|
159
222
|
t = timer.start();
|
|
@@ -205,7 +268,7 @@ export const syncCommand = new Command("sync")
|
|
|
205
268
|
|
|
206
269
|
// Show detailed diagnostics if --diagnose flag is set
|
|
207
270
|
if (options.diagnose) {
|
|
208
|
-
|
|
271
|
+
log.info(`\nUnmatched URL Diagnostics:\n`);
|
|
209
272
|
|
|
210
273
|
// Group by reason
|
|
211
274
|
const byReason = new Map<UnmatchReason, MatchResult[]>();
|
|
@@ -216,69 +279,68 @@ export const syncCommand = new Command("sync")
|
|
|
216
279
|
}
|
|
217
280
|
|
|
218
281
|
for (const [reason, urls] of byReason) {
|
|
219
|
-
|
|
282
|
+
log.info(
|
|
220
283
|
` ${getReasonEmoji(reason)} ${getReasonDescription(reason)} (${urls.length}):`,
|
|
221
284
|
);
|
|
222
285
|
const toShow = urls.slice(0, 5);
|
|
223
286
|
for (const u of toShow) {
|
|
224
|
-
|
|
287
|
+
log.info(` ${u.gscUrl}`);
|
|
225
288
|
if (u.extractedSlug) {
|
|
226
|
-
|
|
289
|
+
log.info(` Extracted slug: "${u.extractedSlug}"`);
|
|
227
290
|
}
|
|
228
291
|
if (u.diagnostics?.similarSlugs?.length) {
|
|
229
|
-
|
|
292
|
+
log.info(` Similar slugs in Sanity:`);
|
|
230
293
|
for (const similar of u.diagnostics.similarSlugs) {
|
|
231
|
-
|
|
294
|
+
log.info(` - ${similar}`);
|
|
232
295
|
}
|
|
233
296
|
}
|
|
234
297
|
}
|
|
235
298
|
if (urls.length > 5) {
|
|
236
|
-
|
|
299
|
+
log.info(` ... and ${urls.length - 5} more`);
|
|
237
300
|
}
|
|
238
|
-
console.log();
|
|
239
301
|
}
|
|
240
302
|
} else if (unmatched.length <= 10) {
|
|
241
|
-
unmatched.forEach((u) =>
|
|
242
|
-
|
|
303
|
+
unmatched.forEach((u) => log.info(` - ${u.gscUrl}`));
|
|
304
|
+
log.info(`\n Run with --diagnose for detailed diagnostics`);
|
|
243
305
|
} else {
|
|
244
|
-
|
|
306
|
+
log.info(` Run with --diagnose to see detailed diagnostics`);
|
|
245
307
|
}
|
|
246
308
|
}
|
|
247
309
|
|
|
248
310
|
// Handle --diagnose-url for a specific URL
|
|
249
311
|
if (options.diagnoseUrl) {
|
|
250
|
-
const targetUrl = options.diagnoseUrl;
|
|
312
|
+
const targetUrl = options.diagnoseUrl as string;
|
|
251
313
|
const allUrls = [targetUrl];
|
|
252
314
|
const [result] = await matcher.matchUrls(allUrls);
|
|
253
|
-
|
|
315
|
+
log.info(`\nDiagnostics for: ${targetUrl}\n`);
|
|
254
316
|
if (result) {
|
|
255
|
-
|
|
256
|
-
|
|
317
|
+
log.info(` Matched: ${result.sanityId ? "Yes" : "No"}`);
|
|
318
|
+
log.info(
|
|
257
319
|
` Reason: ${getReasonDescription(result.unmatchReason)}`,
|
|
258
320
|
);
|
|
259
321
|
if (result.extractedSlug) {
|
|
260
|
-
|
|
322
|
+
log.info(` Extracted slug: "${result.extractedSlug}"`);
|
|
261
323
|
}
|
|
262
324
|
if (result.matchedSlug) {
|
|
263
|
-
|
|
325
|
+
log.info(` Matched to Sanity slug: "${result.matchedSlug}"`);
|
|
264
326
|
}
|
|
265
327
|
if (result.diagnostics) {
|
|
266
|
-
|
|
328
|
+
log.info(
|
|
267
329
|
` Normalized URL: ${result.diagnostics.normalizedUrl}`,
|
|
268
330
|
);
|
|
269
|
-
|
|
331
|
+
log.info(
|
|
270
332
|
` Path after prefix: ${result.diagnostics.pathAfterPrefix}`,
|
|
271
333
|
);
|
|
272
|
-
|
|
334
|
+
log.info(
|
|
273
335
|
` Configured prefix: ${result.diagnostics.configuredPrefix ?? "(none)"}`,
|
|
274
336
|
);
|
|
275
|
-
|
|
337
|
+
log.info(
|
|
276
338
|
` Available Sanity slugs: ${result.diagnostics.availableSlugsCount}`,
|
|
277
339
|
);
|
|
278
340
|
if (result.diagnostics.similarSlugs?.length) {
|
|
279
|
-
|
|
341
|
+
log.info(` Similar slugs in Sanity:`);
|
|
280
342
|
for (const similar of result.diagnostics.similarSlugs) {
|
|
281
|
-
|
|
343
|
+
log.info(` - ${similar}`);
|
|
282
344
|
}
|
|
283
345
|
}
|
|
284
346
|
}
|
|
@@ -287,7 +349,7 @@ export const syncCommand = new Command("sync")
|
|
|
287
349
|
|
|
288
350
|
// Check index status if requested
|
|
289
351
|
if (options.checkIndex && matched.length > 0) {
|
|
290
|
-
|
|
352
|
+
log.info(`\nChecking index status for ${matched.length} pages...`);
|
|
291
353
|
t = timer.start();
|
|
292
354
|
const matchedUrls = matched.map((m) => m.gscUrl);
|
|
293
355
|
const indexResult = await syncEngine.syncIndexStatus(
|
|
@@ -295,7 +357,7 @@ export const syncCommand = new Command("sync")
|
|
|
295
357
|
matchedUrls,
|
|
296
358
|
);
|
|
297
359
|
timer.end("Index status check", t);
|
|
298
|
-
|
|
360
|
+
log.info(
|
|
299
361
|
` Indexed: ${indexResult.indexed}, Not indexed: ${indexResult.notIndexed}, Skipped: ${indexResult.skipped}`,
|
|
300
362
|
);
|
|
301
363
|
}
|
|
@@ -309,19 +371,19 @@ export const syncCommand = new Command("sync")
|
|
|
309
371
|
publishedDates,
|
|
310
372
|
{
|
|
311
373
|
enabled: true,
|
|
312
|
-
days:
|
|
374
|
+
days: quietPeriodDays,
|
|
313
375
|
},
|
|
314
376
|
);
|
|
315
377
|
timer.end("Decay detection", t);
|
|
316
378
|
|
|
317
|
-
|
|
379
|
+
log.info(`Detected ${signals.length} decay signals`);
|
|
318
380
|
|
|
319
381
|
if (options.dryRun) {
|
|
320
|
-
|
|
382
|
+
log.info("\nWould create the following tasks:");
|
|
321
383
|
signals.forEach((s) => {
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
384
|
+
log.info(` [${s.severity.toUpperCase()}] ${s.page}`);
|
|
385
|
+
log.info(` Reason: ${s.reason}`);
|
|
386
|
+
log.info(
|
|
325
387
|
` Position: ${s.metrics.positionBefore} -> ${s.metrics.positionNow}`,
|
|
326
388
|
);
|
|
327
389
|
});
|
|
@@ -334,7 +396,7 @@ export const syncCommand = new Command("sync")
|
|
|
334
396
|
matches,
|
|
335
397
|
);
|
|
336
398
|
timer.end("Task generation", t);
|
|
337
|
-
|
|
399
|
+
log.info(`Created ${created} new refresh tasks`);
|
|
338
400
|
}
|
|
339
401
|
}
|
|
340
402
|
|
|
@@ -342,16 +404,19 @@ export const syncCommand = new Command("sync")
|
|
|
342
404
|
t = timer.start();
|
|
343
405
|
await syncEngine.writeSnapshots(siteId, matched);
|
|
344
406
|
timer.end("Write Sanity snapshots", t);
|
|
345
|
-
|
|
407
|
+
log.info(`Updated Sanity snapshots`);
|
|
346
408
|
}
|
|
347
409
|
|
|
348
410
|
timer.end("Total sync", syncStartTime);
|
|
349
|
-
|
|
411
|
+
log.info(`\nSync complete!`);
|
|
350
412
|
} catch (error) {
|
|
351
|
-
|
|
352
|
-
|
|
413
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
414
|
+
log.error(`Sync failed: ${message}`);
|
|
415
|
+
process.exitCode = 1;
|
|
353
416
|
} finally {
|
|
354
|
-
await
|
|
417
|
+
await close();
|
|
418
|
+
process.removeListener("SIGTERM", shutdown);
|
|
419
|
+
process.removeListener("SIGINT", shutdown);
|
|
355
420
|
}
|
|
356
421
|
});
|
|
357
422
|
|
package/src/index.ts
CHANGED
|
@@ -1,12 +1,4 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
import { config } from "dotenv";
|
|
3
|
-
import { resolve } from "path";
|
|
4
|
-
import { fileURLToPath } from "url";
|
|
5
|
-
|
|
6
|
-
const __dirname = fileURLToPath(new URL(".", import.meta.url));
|
|
7
|
-
// Load .env from monorepo root
|
|
8
|
-
config({ path: resolve(__dirname, "../../../.env") });
|
|
9
|
-
|
|
10
2
|
import { program } from "commander";
|
|
11
3
|
import { syncCommand } from "./commands/sync.js";
|
|
12
4
|
import { listSitesCommand } from "./commands/list-sites.js";
|
package/src/logger.ts
ADDED
|
@@ -0,0 +1,22 @@
|
|
|
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
|
+
};
|