@pagebridge/cli 0.0.1 → 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.
@@ -1,8 +1,10 @@
1
1
  import { Command } from "commander";
2
2
  import { createClient as createSanityClient } from "@sanity/client";
3
- import postgres from "postgres";
4
3
  import { GSCClient, SyncEngine, DecayDetector, URLMatcher, TaskGenerator, } from "@pagebridge/core";
5
- import { createDbWithClient, unmatchDiagnostics } from "@pagebridge/db";
4
+ import { createDb, sql, unmatchDiagnostics } from "@pagebridge/db";
5
+ import { resolve, requireConfig } from "../resolve-config.js";
6
+ import { log } from "../logger.js";
7
+ import { migrateIfRequested } from "../migrate.js";
6
8
  function daysAgo(days) {
7
9
  const date = new Date();
8
10
  date.setDate(date.getDate() - days);
@@ -14,7 +16,7 @@ function createTimer(debug) {
14
16
  end: (label, startTime) => {
15
17
  if (debug) {
16
18
  const elapsed = ((performance.now() - startTime) / 1000).toFixed(2);
17
- console.log(`[DEBUG] ${label} completed in ${elapsed}s`);
19
+ log.debug(`${label} completed in ${elapsed}s`, true);
18
20
  }
19
21
  },
20
22
  };
@@ -29,38 +31,92 @@ export const syncCommand = new Command("sync")
29
31
  .option("--diagnose", "Show detailed diagnostics for unmatched URLs")
30
32
  .option("--diagnose-url <url>", "Diagnose why a specific URL is not matching")
31
33
  .option("--debug", "Enable debug logging with timing information")
34
+ .option("--migrate", "Run database migrations before syncing")
35
+ .option("--google-service-account <json>", "Google service account JSON")
36
+ .option("--db-url <url>", "PostgreSQL connection string")
37
+ .option("--sanity-project-id <id>", "Sanity project ID")
38
+ .option("--sanity-dataset <name>", "Sanity dataset name")
39
+ .option("--sanity-token <token>", "Sanity API token")
40
+ .option("--site-url <url>", "Your website base URL for URL matching")
32
41
  .action(async (options) => {
33
42
  const timer = createTimer(options.debug);
34
43
  const syncStartTime = timer.start();
35
- const requiredEnvVars = [
36
- "GOOGLE_SERVICE_ACCOUNT",
37
- "DATABASE_URL",
38
- "SANITY_PROJECT_ID",
39
- "SANITY_DATASET",
40
- "SANITY_TOKEN",
41
- "SITE_URL",
42
- ];
43
- for (const envVar of requiredEnvVars) {
44
- if (!process.env[envVar]) {
45
- console.error(`Missing required environment variable: ${envVar}`);
46
- process.exit(1);
47
- }
44
+ // Validate --quiet-period
45
+ const quietPeriodDays = parseInt(options.quietPeriod);
46
+ if (isNaN(quietPeriodDays)) {
47
+ log.error(`Invalid --quiet-period value: "${options.quietPeriod}". Must be a number.`);
48
+ process.exit(1);
48
49
  }
50
+ const googleServiceAccount = resolve(options.googleServiceAccount, "GOOGLE_SERVICE_ACCOUNT");
51
+ const dbUrl = resolve(options.dbUrl, "DATABASE_URL");
52
+ const sanityProjectId = resolve(options.sanityProjectId, "SANITY_PROJECT_ID");
53
+ const sanityDataset = resolve(options.sanityDataset, "SANITY_DATASET");
54
+ const sanityToken = resolve(options.sanityToken, "SANITY_TOKEN");
55
+ const siteUrl = resolve(options.siteUrl, "SITE_URL");
56
+ requireConfig([
57
+ { name: "GOOGLE_SERVICE_ACCOUNT", flag: "--google-service-account <json>", envVar: "GOOGLE_SERVICE_ACCOUNT", value: googleServiceAccount },
58
+ { name: "DATABASE_URL", flag: "--db-url <url>", envVar: "DATABASE_URL", value: dbUrl },
59
+ { name: "SANITY_PROJECT_ID", flag: "--sanity-project-id <id>", envVar: "SANITY_PROJECT_ID", value: sanityProjectId },
60
+ { name: "SANITY_DATASET", flag: "--sanity-dataset <name>", envVar: "SANITY_DATASET", value: sanityDataset },
61
+ { name: "SANITY_TOKEN", flag: "--sanity-token <token>", envVar: "SANITY_TOKEN", value: sanityToken },
62
+ { name: "SITE_URL", flag: "--site-url <url>", envVar: "SITE_URL", value: siteUrl },
63
+ ]);
64
+ // Run migrations if requested
65
+ await migrateIfRequested(!!options.migrate, dbUrl);
49
66
  let t = timer.start();
50
67
  const sanity = createSanityClient({
51
- projectId: process.env.SANITY_PROJECT_ID,
52
- dataset: process.env.SANITY_DATASET,
53
- token: process.env.SANITY_TOKEN,
68
+ projectId: sanityProjectId,
69
+ dataset: sanityDataset,
70
+ token: sanityToken,
54
71
  apiVersion: "2024-01-01",
55
72
  useCdn: false,
56
73
  });
57
- const sql = postgres(process.env.DATABASE_URL);
58
- const db = createDbWithClient(sql);
59
- const gsc = new GSCClient({
60
- credentials: JSON.parse(process.env.GOOGLE_SERVICE_ACCOUNT),
61
- });
74
+ const { db, close } = createDb(dbUrl);
75
+ let credentials;
76
+ try {
77
+ credentials = JSON.parse(googleServiceAccount);
78
+ }
79
+ catch {
80
+ log.error("Failed to parse GOOGLE_SERVICE_ACCOUNT as JSON");
81
+ await close();
82
+ process.exit(1);
83
+ }
84
+ const gsc = new GSCClient({ credentials });
62
85
  timer.end("Client initialization", t);
63
- console.log(`Starting sync for ${options.site}...`);
86
+ // Register shutdown handlers
87
+ const shutdown = async () => {
88
+ log.warn("Received shutdown signal, closing connections...");
89
+ await close();
90
+ process.exit(130);
91
+ };
92
+ process.on("SIGTERM", shutdown);
93
+ process.on("SIGINT", shutdown);
94
+ // Pre-flight connection validation
95
+ try {
96
+ log.info("Validating connections...");
97
+ t = timer.start();
98
+ await db.execute(sql `SELECT 1`);
99
+ timer.end("DB connection check", t);
100
+ t = timer.start();
101
+ await sanity.fetch('*[_type == "gscSite"][0]{ _id }');
102
+ timer.end("Sanity connection check", t);
103
+ t = timer.start();
104
+ const sites = await gsc.listSites();
105
+ timer.end("GSC connection check", t);
106
+ if (!sites.includes(options.site)) {
107
+ log.warn(`Site "${options.site}" not found in GSC site list. It may be new or the service account may lack access.`);
108
+ }
109
+ }
110
+ catch (error) {
111
+ const message = error instanceof Error ? error.message : String(error);
112
+ log.error(`Pre-flight connection check failed: ${message}`);
113
+ await close();
114
+ process.exitCode = 1;
115
+ process.removeListener("SIGTERM", shutdown);
116
+ process.removeListener("SIGINT", shutdown);
117
+ return;
118
+ }
119
+ log.info(`Starting sync for ${options.site}...`);
64
120
  // Find or create the gscSite document in Sanity
65
121
  t = timer.start();
66
122
  let siteDoc = await sanity.fetch(`*[_type == "gscSite" && siteUrl == $siteUrl][0]{
@@ -70,7 +126,7 @@ export const syncCommand = new Command("sync")
70
126
  slugField
71
127
  }`, { siteUrl: options.site });
72
128
  if (!siteDoc) {
73
- console.log(`Creating gscSite document for ${options.site}...`);
129
+ log.info(`Creating gscSite document for ${options.site}...`);
74
130
  siteDoc = await sanity.create({
75
131
  _type: "gscSite",
76
132
  siteUrl: options.site,
@@ -85,15 +141,15 @@ export const syncCommand = new Command("sync")
85
141
  const contentTypes = siteDoc.contentTypes ?? ["post", "page"];
86
142
  const slugField = siteDoc.slugField ?? "slug";
87
143
  const pathPrefix = siteDoc.pathPrefix ?? undefined;
88
- console.log(`Configuration:`);
89
- console.log(` Content types: ${contentTypes.join(", ")}`);
90
- console.log(` Slug field: ${slugField}`);
91
- console.log(` Path prefix: ${pathPrefix ?? "(none)"}`);
144
+ log.info(`Configuration:`);
145
+ log.info(` Content types: ${contentTypes.join(", ")}`);
146
+ log.info(` Slug field: ${slugField}`);
147
+ log.info(` Path prefix: ${pathPrefix ?? "(none)"}`);
92
148
  const syncEngine = new SyncEngine({ gsc, db, sanity });
93
149
  const matcher = new URLMatcher(sanity, {
94
150
  contentTypes,
95
151
  slugField,
96
- baseUrl: process.env.SITE_URL,
152
+ baseUrl: siteUrl,
97
153
  pathPrefix,
98
154
  });
99
155
  try {
@@ -104,16 +160,16 @@ export const syncCommand = new Command("sync")
104
160
  endDate: daysAgo(3),
105
161
  });
106
162
  timer.end("GSC data sync", t);
107
- console.log(`Processed ${rowsProcessed} rows for ${pages.length} pages`);
163
+ log.info(`Processed ${rowsProcessed} rows for ${pages.length} pages`);
108
164
  t = timer.start();
109
165
  const matches = await matcher.matchUrls(pages);
110
166
  timer.end("URL matching", t);
111
167
  const matched = matches.filter((m) => !!m.sanityId);
112
168
  const unmatched = matches.filter((m) => !m.sanityId);
113
- console.log(`Matched ${matched.length}/${pages.length} URLs to Sanity documents`);
169
+ log.info(`Matched ${matched.length}/${pages.length} URLs to Sanity documents`);
114
170
  // Store diagnostics for unmatched URLs
115
171
  if (unmatched.length > 0) {
116
- console.log(`${unmatched.length} unmatched URLs`);
172
+ log.info(`${unmatched.length} unmatched URLs`);
117
173
  // Store diagnostics in database
118
174
  t = timer.start();
119
175
  for (const u of unmatched) {
@@ -162,7 +218,7 @@ export const syncCommand = new Command("sync")
162
218
  timer.end("Store unmatched diagnostics", t);
163
219
  // Show detailed diagnostics if --diagnose flag is set
164
220
  if (options.diagnose) {
165
- console.log(`\nUnmatched URL Diagnostics:\n`);
221
+ log.info(`\nUnmatched URL Diagnostics:\n`);
166
222
  // Group by reason
167
223
  const byReason = new Map();
168
224
  for (const u of unmatched) {
@@ -171,32 +227,31 @@ export const syncCommand = new Command("sync")
171
227
  byReason.set(u.unmatchReason, existing);
172
228
  }
173
229
  for (const [reason, urls] of byReason) {
174
- console.log(` ${getReasonEmoji(reason)} ${getReasonDescription(reason)} (${urls.length}):`);
230
+ log.info(` ${getReasonEmoji(reason)} ${getReasonDescription(reason)} (${urls.length}):`);
175
231
  const toShow = urls.slice(0, 5);
176
232
  for (const u of toShow) {
177
- console.log(` ${u.gscUrl}`);
233
+ log.info(` ${u.gscUrl}`);
178
234
  if (u.extractedSlug) {
179
- console.log(` Extracted slug: "${u.extractedSlug}"`);
235
+ log.info(` Extracted slug: "${u.extractedSlug}"`);
180
236
  }
181
237
  if (u.diagnostics?.similarSlugs?.length) {
182
- console.log(` Similar slugs in Sanity:`);
238
+ log.info(` Similar slugs in Sanity:`);
183
239
  for (const similar of u.diagnostics.similarSlugs) {
184
- console.log(` - ${similar}`);
240
+ log.info(` - ${similar}`);
185
241
  }
186
242
  }
187
243
  }
188
244
  if (urls.length > 5) {
189
- console.log(` ... and ${urls.length - 5} more`);
245
+ log.info(` ... and ${urls.length - 5} more`);
190
246
  }
191
- console.log();
192
247
  }
193
248
  }
194
249
  else if (unmatched.length <= 10) {
195
- unmatched.forEach((u) => console.log(` - ${u.gscUrl}`));
196
- console.log(`\n Run with --diagnose for detailed diagnostics`);
250
+ unmatched.forEach((u) => log.info(` - ${u.gscUrl}`));
251
+ log.info(`\n Run with --diagnose for detailed diagnostics`);
197
252
  }
198
253
  else {
199
- console.log(` Run with --diagnose to see detailed diagnostics`);
254
+ log.info(` Run with --diagnose to see detailed diagnostics`);
200
255
  }
201
256
  }
202
257
  // Handle --diagnose-url for a specific URL
@@ -204,25 +259,25 @@ export const syncCommand = new Command("sync")
204
259
  const targetUrl = options.diagnoseUrl;
205
260
  const allUrls = [targetUrl];
206
261
  const [result] = await matcher.matchUrls(allUrls);
207
- console.log(`\nDiagnostics for: ${targetUrl}\n`);
262
+ log.info(`\nDiagnostics for: ${targetUrl}\n`);
208
263
  if (result) {
209
- console.log(` Matched: ${result.sanityId ? "Yes" : "No"}`);
210
- console.log(` Reason: ${getReasonDescription(result.unmatchReason)}`);
264
+ log.info(` Matched: ${result.sanityId ? "Yes" : "No"}`);
265
+ log.info(` Reason: ${getReasonDescription(result.unmatchReason)}`);
211
266
  if (result.extractedSlug) {
212
- console.log(` Extracted slug: "${result.extractedSlug}"`);
267
+ log.info(` Extracted slug: "${result.extractedSlug}"`);
213
268
  }
214
269
  if (result.matchedSlug) {
215
- console.log(` Matched to Sanity slug: "${result.matchedSlug}"`);
270
+ log.info(` Matched to Sanity slug: "${result.matchedSlug}"`);
216
271
  }
217
272
  if (result.diagnostics) {
218
- console.log(` Normalized URL: ${result.diagnostics.normalizedUrl}`);
219
- console.log(` Path after prefix: ${result.diagnostics.pathAfterPrefix}`);
220
- console.log(` Configured prefix: ${result.diagnostics.configuredPrefix ?? "(none)"}`);
221
- console.log(` Available Sanity slugs: ${result.diagnostics.availableSlugsCount}`);
273
+ log.info(` Normalized URL: ${result.diagnostics.normalizedUrl}`);
274
+ log.info(` Path after prefix: ${result.diagnostics.pathAfterPrefix}`);
275
+ log.info(` Configured prefix: ${result.diagnostics.configuredPrefix ?? "(none)"}`);
276
+ log.info(` Available Sanity slugs: ${result.diagnostics.availableSlugsCount}`);
222
277
  if (result.diagnostics.similarSlugs?.length) {
223
- console.log(` Similar slugs in Sanity:`);
278
+ log.info(` Similar slugs in Sanity:`);
224
279
  for (const similar of result.diagnostics.similarSlugs) {
225
- console.log(` - ${similar}`);
280
+ log.info(` - ${similar}`);
226
281
  }
227
282
  }
228
283
  }
@@ -230,12 +285,12 @@ export const syncCommand = new Command("sync")
230
285
  }
231
286
  // Check index status if requested
232
287
  if (options.checkIndex && matched.length > 0) {
233
- console.log(`\nChecking index status for ${matched.length} pages...`);
288
+ log.info(`\nChecking index status for ${matched.length} pages...`);
234
289
  t = timer.start();
235
290
  const matchedUrls = matched.map((m) => m.gscUrl);
236
291
  const indexResult = await syncEngine.syncIndexStatus(options.site, matchedUrls);
237
292
  timer.end("Index status check", t);
238
- console.log(` Indexed: ${indexResult.indexed}, Not indexed: ${indexResult.notIndexed}, Skipped: ${indexResult.skipped}`);
293
+ log.info(` Indexed: ${indexResult.indexed}, Not indexed: ${indexResult.notIndexed}, Skipped: ${indexResult.skipped}`);
239
294
  }
240
295
  if (!options.skipTasks) {
241
296
  t = timer.start();
@@ -243,16 +298,16 @@ export const syncCommand = new Command("sync")
243
298
  const detector = new DecayDetector(db);
244
299
  const signals = await detector.detectDecay(options.site, publishedDates, {
245
300
  enabled: true,
246
- days: parseInt(options.quietPeriod),
301
+ days: quietPeriodDays,
247
302
  });
248
303
  timer.end("Decay detection", t);
249
- console.log(`Detected ${signals.length} decay signals`);
304
+ log.info(`Detected ${signals.length} decay signals`);
250
305
  if (options.dryRun) {
251
- console.log("\nWould create the following tasks:");
306
+ log.info("\nWould create the following tasks:");
252
307
  signals.forEach((s) => {
253
- console.log(` [${s.severity.toUpperCase()}] ${s.page}`);
254
- console.log(` Reason: ${s.reason}`);
255
- console.log(` Position: ${s.metrics.positionBefore} -> ${s.metrics.positionNow}`);
308
+ log.info(` [${s.severity.toUpperCase()}] ${s.page}`);
309
+ log.info(` Reason: ${s.reason}`);
310
+ log.info(` Position: ${s.metrics.positionBefore} -> ${s.metrics.positionNow}`);
256
311
  });
257
312
  }
258
313
  else {
@@ -260,24 +315,27 @@ export const syncCommand = new Command("sync")
260
315
  const taskGenerator = new TaskGenerator(sanity);
261
316
  const created = await taskGenerator.createTasks(siteId, signals, matches);
262
317
  timer.end("Task generation", t);
263
- console.log(`Created ${created} new refresh tasks`);
318
+ log.info(`Created ${created} new refresh tasks`);
264
319
  }
265
320
  }
266
321
  if (!options.dryRun) {
267
322
  t = timer.start();
268
323
  await syncEngine.writeSnapshots(siteId, matched);
269
324
  timer.end("Write Sanity snapshots", t);
270
- console.log(`Updated Sanity snapshots`);
325
+ log.info(`Updated Sanity snapshots`);
271
326
  }
272
327
  timer.end("Total sync", syncStartTime);
273
- console.log(`\nSync complete!`);
328
+ log.info(`\nSync complete!`);
274
329
  }
275
330
  catch (error) {
276
- console.error("Sync failed:", error);
277
- process.exit(1);
331
+ const message = error instanceof Error ? error.message : String(error);
332
+ log.error(`Sync failed: ${message}`);
333
+ process.exitCode = 1;
278
334
  }
279
335
  finally {
280
- await sql.end();
336
+ await close();
337
+ process.removeListener("SIGTERM", shutdown);
338
+ process.removeListener("SIGINT", shutdown);
281
339
  }
282
340
  });
283
341
  async function getPublishedDates(sanity, matches) {
package/dist/index.js CHANGED
@@ -1,18 +1,16 @@
1
1
  #!/usr/bin/env node
2
- import { config } from "dotenv";
3
- import { resolve } from "path";
4
- import { fileURLToPath } from "url";
5
- const __dirname = fileURLToPath(new URL(".", import.meta.url));
6
- // Load .env from monorepo root
7
- config({ path: resolve(__dirname, "../../../.env") });
8
2
  import { program } from "commander";
3
+ import { initCommand } from "./commands/init.js";
4
+ import { doctorCommand } from "./commands/doctor.js";
9
5
  import { syncCommand } from "./commands/sync.js";
10
6
  import { listSitesCommand } from "./commands/list-sites.js";
11
7
  import { diagnoseCommand } from "./commands/diagnose.js";
12
8
  program
13
9
  .name("pagebridge")
14
10
  .description("PageBridge - Connect Google Search Console to Sanity CMS")
15
- .version("0.0.1");
11
+ .version("0.0.2");
12
+ program.addCommand(initCommand);
13
+ program.addCommand(doctorCommand);
16
14
  program.addCommand(syncCommand);
17
15
  program.addCommand(listSitesCommand);
18
16
  program.addCommand(diagnoseCommand);
@@ -0,0 +1,7 @@
1
+ export declare const log: {
2
+ info(msg: string): void;
3
+ warn(msg: string): void;
4
+ error(msg: string): void;
5
+ debug(msg: string, enabled: boolean): void;
6
+ };
7
+ //# sourceMappingURL=logger.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"logger.d.ts","sourceRoot":"","sources":["../src/logger.ts"],"names":[],"mappings":"AAMA,eAAO,MAAM,GAAG;cACJ,MAAM;cAGN,MAAM;eAGL,MAAM;eAGN,MAAM,WAAW,OAAO;CAKpC,CAAC"}
package/dist/logger.js ADDED
@@ -0,0 +1,19 @@
1
+ function format(level, msg) {
2
+ return `[${new Date().toISOString()}] [${level}] ${msg}`;
3
+ }
4
+ export const log = {
5
+ info(msg) {
6
+ console.log(format("INFO", msg));
7
+ },
8
+ warn(msg) {
9
+ console.warn(format("WARN", msg));
10
+ },
11
+ error(msg) {
12
+ console.error(format("ERROR", msg));
13
+ },
14
+ debug(msg, enabled) {
15
+ if (enabled) {
16
+ console.log(format("DEBUG", msg));
17
+ }
18
+ },
19
+ };
@@ -0,0 +1,2 @@
1
+ export declare function migrateIfRequested(shouldMigrate: boolean, dbUrl: string): Promise<void>;
2
+ //# sourceMappingURL=migrate.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"migrate.d.ts","sourceRoot":"","sources":["../src/migrate.ts"],"names":[],"mappings":"AAKA,wBAAsB,kBAAkB,CAAC,aAAa,EAAE,OAAO,EAAE,KAAK,EAAE,MAAM,iBAO7E"}
@@ -0,0 +1,13 @@
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
+ export async function migrateIfRequested(shouldMigrate, dbUrl) {
6
+ if (!shouldMigrate)
7
+ 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
+ }
@@ -0,0 +1,20 @@
1
+ /**
2
+ * Resolves a config value from:
3
+ * 1. CLI option (highest priority)
4
+ * 2. PAGEBRIDGE_ prefixed env var (e.g. PAGEBRIDGE_DATABASE_URL)
5
+ * 3. Unprefixed env var fallback (e.g. DATABASE_URL)
6
+ */
7
+ export declare function resolve(optionValue: string | undefined, envVarName: string): string | undefined;
8
+ interface ConfigEntry {
9
+ name: string;
10
+ flag: string;
11
+ envVar: string;
12
+ value: string | undefined;
13
+ }
14
+ /**
15
+ * Validates that all required config entries have values.
16
+ * If any are missing, prints a clear error listing every missing entry and exits.
17
+ */
18
+ export declare function requireConfig(entries: ConfigEntry[]): void;
19
+ export {};
20
+ //# sourceMappingURL=resolve-config.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"resolve-config.d.ts","sourceRoot":"","sources":["../src/resolve-config.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AACH,wBAAgB,OAAO,CACrB,WAAW,EAAE,MAAM,GAAG,SAAS,EAC/B,UAAU,EAAE,MAAM,GACjB,MAAM,GAAG,SAAS,CAMpB;AAED,UAAU,WAAW;IACnB,IAAI,EAAE,MAAM,CAAC;IACb,IAAI,EAAE,MAAM,CAAC;IACb,MAAM,EAAE,MAAM,CAAC;IACf,KAAK,EAAE,MAAM,GAAG,SAAS,CAAC;CAC3B;AAED;;;GAGG;AACH,wBAAgB,aAAa,CAAC,OAAO,EAAE,WAAW,EAAE,GAAG,IAAI,CAY1D"}
@@ -0,0 +1,26 @@
1
+ /**
2
+ * Resolves a config value from:
3
+ * 1. CLI option (highest priority)
4
+ * 2. PAGEBRIDGE_ prefixed env var (e.g. PAGEBRIDGE_DATABASE_URL)
5
+ * 3. Unprefixed env var fallback (e.g. DATABASE_URL)
6
+ */
7
+ export function resolve(optionValue, envVarName) {
8
+ return (optionValue ??
9
+ process.env[`PAGEBRIDGE_${envVarName}`] ??
10
+ process.env[envVarName]);
11
+ }
12
+ /**
13
+ * Validates that all required config entries have values.
14
+ * If any are missing, prints a clear error listing every missing entry and exits.
15
+ */
16
+ export function requireConfig(entries) {
17
+ const missing = entries.filter((e) => !e.value);
18
+ if (missing.length === 0)
19
+ return;
20
+ console.error("Error: Missing required configuration.\n");
21
+ for (const entry of missing) {
22
+ console.error(` ${entry.name}`);
23
+ console.error(` ${entry.flag} or PAGEBRIDGE_${entry.envVar} / ${entry.envVar} env var\n`);
24
+ }
25
+ process.exit(1);
26
+ }
package/package.json CHANGED
@@ -1,26 +1,42 @@
1
1
  {
2
2
  "name": "@pagebridge/cli",
3
- "version": "0.0.1",
3
+ "version": "0.1.1",
4
+ "description": "CLI for PageBridge — sync Google Search Console data, detect content decay, and generate refresh tasks",
4
5
  "private": false,
5
6
  "license": "MIT",
6
7
  "type": "module",
8
+ "keywords": [
9
+ "pagebridge",
10
+ "cli",
11
+ "google-search-console",
12
+ "gsc",
13
+ "seo",
14
+ "content-decay",
15
+ "sanity",
16
+ "search-analytics",
17
+ "content-refresh",
18
+ "decay-detection"
19
+ ],
7
20
  "bin": {
8
21
  "pagebridge": "./dist/index.js"
9
22
  },
23
+ "files": [
24
+ "dist",
25
+ "LICENSE",
26
+ "README.md"
27
+ ],
10
28
  "dependencies": {
11
29
  "@sanity/client": "^7.14.1",
12
30
  "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"
31
+ "@pagebridge/core": "^0.0.3",
32
+ "@pagebridge/db": "^0.0.3"
17
33
  },
18
34
  "devDependencies": {
19
35
  "@types/node": "^22.15.3",
20
36
  "eslint": "^9.39.1",
21
37
  "typescript": "^5.9.3",
22
- "@pagebridge/typescript-config": "^0.0.0",
23
- "@pagebridge/eslint-config": "^0.0.0"
38
+ "@pagebridge/eslint-config": "^0.0.0",
39
+ "@pagebridge/typescript-config": "^0.0.0"
24
40
  },
25
41
  "scripts": {
26
42
  "build": "tsc",
@@ -1,2 +0,0 @@
1
- [?9001h[?1004h[?25l> @pagebridge/cli@0.0.1 build F:\Code\pagebridge\oss\apps\cli
2
- > tsc]0;C:\WINDOWS\system32\cmd.exe[?25h[?9001l[?1004l
package/eslint.config.js DELETED
@@ -1,3 +0,0 @@
1
- import { config } from "@pagebridge/eslint-config/base";
2
-
3
- export default [...config];