@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.
@@ -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.1",
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
- "dotenv": "^17.2.3",
14
- "postgres": "^3.4.8",
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",
@@ -1,11 +1,13 @@
1
1
  import { Command } from "commander";
2
- import postgres from "postgres";
3
2
  import {
4
- createDbWithClient,
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
- if (!process.env.DATABASE_URL) {
18
- console.error("Missing required environment variable: DATABASE_URL");
19
- process.exit(1);
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
- const sql = postgres(process.env.DATABASE_URL);
23
- const db = createDbWithClient(sql);
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
- console.log(`No unmatched URLs found for ${options.site}`);
42
- console.log(
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
- console.log(`\nUnmatched URL Diagnostics for ${options.site}\n`);
57
- console.log(`Total: ${results.length} unmatched URLs\n`);
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
- console.log(
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
- console.log(` ${item.gscUrl}`);
81
+ log.info(` ${item.gscUrl}`);
67
82
  if (item.extractedSlug) {
68
- console.log(` Extracted slug: "${item.extractedSlug}"`);
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
- console.log(` Similar slugs in Sanity:`);
89
+ log.info(` Similar slugs in Sanity:`);
75
90
  for (const s of similar) {
76
- console.log(` - ${s}`);
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
- console.log(`\nTo fix unmatched URLs:`);
88
- console.log(
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
- console.log(` 2. Verify the document type is in the contentTypes list`);
92
- console.log(` 3. Ensure the slug field name matches your configuration`);
93
- console.log(
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 sql.end();
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
- .action(async () => {
7
- if (!process.env.GOOGLE_SERVICE_ACCOUNT) {
8
- console.error("❌ Missing GOOGLE_SERVICE_ACCOUNT environment variable");
9
- process.exit(1);
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(process.env.GOOGLE_SERVICE_ACCOUNT);
18
+ credentials = JSON.parse(googleServiceAccount!);
15
19
  } catch {
16
- console.error("Failed to parse GOOGLE_SERVICE_ACCOUNT as JSON");
20
+ log.error("Failed to parse GOOGLE_SERVICE_ACCOUNT as JSON");
17
21
  process.exit(1);
18
22
  }
19
23
 
20
- console.log(`🔑 Using service account: ${credentials.client_email}`);
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
- console.log(
29
- "\n⚠️ No sites found. The service account has no access to any GSC properties.",
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
- console.log("\nTo fix this:");
32
- console.log(
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
- console.log(`\n✅ Found ${sites.length} site(s):\n`);
39
- sites.forEach((site) => console.log(` ${site}`));
40
- console.log(
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
- console.error("❌ Failed to list sites:", error);
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
  });
@@ -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 { createDbWithClient, unmatchDiagnostics } from "@pagebridge/db";
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
- console.log(`[DEBUG] ${label} completed in ${elapsed}s`);
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
- const requiredEnvVars = [
47
- "GOOGLE_SERVICE_ACCOUNT",
48
- "DATABASE_URL",
49
- "SANITY_PROJECT_ID",
50
- "SANITY_DATASET",
51
- "SANITY_TOKEN",
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: process.env.SANITY_PROJECT_ID!,
65
- dataset: process.env.SANITY_DATASET!,
66
- token: process.env.SANITY_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 sql = postgres(process.env.DATABASE_URL!);
72
- const db = createDbWithClient(sql);
91
+ const { db, close } = createDb(dbUrl!);
73
92
 
74
- const gsc = new GSCClient({
75
- credentials: JSON.parse(process.env.GOOGLE_SERVICE_ACCOUNT!),
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
- console.log(`Starting sync for ${options.site}...`);
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
- console.log(`Creating gscSite document for ${options.site}...`);
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
- console.log(`Configuration:`);
118
- console.log(` Content types: ${contentTypes.join(", ")}`);
119
- console.log(` Slug field: ${slugField}`);
120
- console.log(` Path prefix: ${pathPrefix ?? "(none)"}`);
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: process.env.SITE_URL!,
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
- console.log(`Processed ${rowsProcessed} rows for ${pages.length} pages`);
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
- console.log(
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
- console.log(`${unmatched.length} unmatched URLs`);
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
- console.log(`\nUnmatched URL Diagnostics:\n`);
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
- console.log(
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
- console.log(` ${u.gscUrl}`);
287
+ log.info(` ${u.gscUrl}`);
225
288
  if (u.extractedSlug) {
226
- console.log(` Extracted slug: "${u.extractedSlug}"`);
289
+ log.info(` Extracted slug: "${u.extractedSlug}"`);
227
290
  }
228
291
  if (u.diagnostics?.similarSlugs?.length) {
229
- console.log(` Similar slugs in Sanity:`);
292
+ log.info(` Similar slugs in Sanity:`);
230
293
  for (const similar of u.diagnostics.similarSlugs) {
231
- console.log(` - ${similar}`);
294
+ log.info(` - ${similar}`);
232
295
  }
233
296
  }
234
297
  }
235
298
  if (urls.length > 5) {
236
- console.log(` ... and ${urls.length - 5} more`);
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) => console.log(` - ${u.gscUrl}`));
242
- console.log(`\n Run with --diagnose for detailed diagnostics`);
303
+ unmatched.forEach((u) => log.info(` - ${u.gscUrl}`));
304
+ log.info(`\n Run with --diagnose for detailed diagnostics`);
243
305
  } else {
244
- console.log(` Run with --diagnose to see detailed diagnostics`);
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
- console.log(`\nDiagnostics for: ${targetUrl}\n`);
315
+ log.info(`\nDiagnostics for: ${targetUrl}\n`);
254
316
  if (result) {
255
- console.log(` Matched: ${result.sanityId ? "Yes" : "No"}`);
256
- console.log(
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
- console.log(` Extracted slug: "${result.extractedSlug}"`);
322
+ log.info(` Extracted slug: "${result.extractedSlug}"`);
261
323
  }
262
324
  if (result.matchedSlug) {
263
- console.log(` Matched to Sanity slug: "${result.matchedSlug}"`);
325
+ log.info(` Matched to Sanity slug: "${result.matchedSlug}"`);
264
326
  }
265
327
  if (result.diagnostics) {
266
- console.log(
328
+ log.info(
267
329
  ` Normalized URL: ${result.diagnostics.normalizedUrl}`,
268
330
  );
269
- console.log(
331
+ log.info(
270
332
  ` Path after prefix: ${result.diagnostics.pathAfterPrefix}`,
271
333
  );
272
- console.log(
334
+ log.info(
273
335
  ` Configured prefix: ${result.diagnostics.configuredPrefix ?? "(none)"}`,
274
336
  );
275
- console.log(
337
+ log.info(
276
338
  ` Available Sanity slugs: ${result.diagnostics.availableSlugsCount}`,
277
339
  );
278
340
  if (result.diagnostics.similarSlugs?.length) {
279
- console.log(` Similar slugs in Sanity:`);
341
+ log.info(` Similar slugs in Sanity:`);
280
342
  for (const similar of result.diagnostics.similarSlugs) {
281
- console.log(` - ${similar}`);
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
- console.log(`\nChecking index status for ${matched.length} pages...`);
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
- console.log(
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: parseInt(options.quietPeriod),
374
+ days: quietPeriodDays,
313
375
  },
314
376
  );
315
377
  timer.end("Decay detection", t);
316
378
 
317
- console.log(`Detected ${signals.length} decay signals`);
379
+ log.info(`Detected ${signals.length} decay signals`);
318
380
 
319
381
  if (options.dryRun) {
320
- console.log("\nWould create the following tasks:");
382
+ log.info("\nWould create the following tasks:");
321
383
  signals.forEach((s) => {
322
- console.log(` [${s.severity.toUpperCase()}] ${s.page}`);
323
- console.log(` Reason: ${s.reason}`);
324
- console.log(
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
- console.log(`Created ${created} new refresh tasks`);
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
- console.log(`Updated Sanity snapshots`);
407
+ log.info(`Updated Sanity snapshots`);
346
408
  }
347
409
 
348
410
  timer.end("Total sync", syncStartTime);
349
- console.log(`\nSync complete!`);
411
+ log.info(`\nSync complete!`);
350
412
  } catch (error) {
351
- console.error("Sync failed:", error);
352
- process.exit(1);
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 sql.end();
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
+ };