@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.
package/LICENSE CHANGED
@@ -1,21 +1,21 @@
1
- MIT License
2
-
3
- Copyright (c) 2026 Soma Somorjai
4
-
5
- Permission is hereby granted, free of charge, to any person obtaining a copy
6
- of this software and associated documentation files (the "Software"), to deal
7
- in the Software without restriction, including without limitation the rights
8
- to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
- copies of the Software, and to permit persons to whom the Software is
10
- furnished to do so, subject to the following conditions:
11
-
12
- The above copyright notice and this permission notice shall be included in all
13
- copies or substantial portions of the Software.
14
-
15
- THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
- IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
- FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
- AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
- LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
- OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
- SOFTWARE.
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Soma Somorjai
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md CHANGED
@@ -1,162 +1,157 @@
1
1
  # @pagebridge/cli
2
2
 
3
- Command-line interface for PageBridge. Syncs Google Search Console data to Sanity CMS, detects content decay, and generates refresh tasks.
3
+ Command-line interface for PageBridge. Syncs Google Search Console data to your PostgreSQL database, matches URLs to Sanity documents, detects content decay, and generates refresh tasks.
4
4
 
5
5
  ## Installation
6
6
 
7
- The CLI is a private workspace package. Build and run it from the monorepo root:
7
+ ```bash
8
+ pnpm add -D @pagebridge/cli
9
+ ```
10
+
11
+ Or run from the monorepo:
8
12
 
9
13
  ```bash
10
- # Build the CLI
11
14
  pnpm build --filter=@pagebridge/cli
15
+ ```
16
+
17
+ ## Commands
12
18
 
13
- # Run commands
14
- pnpm --filter @pagebridge/cli start <command>
19
+ ### `pagebridge init`
15
20
 
16
- # Or use the binary name directly after building
17
- ./apps/cli/dist/index.js <command>
21
+ Interactive setup wizard. Walks you through configuring credentials, tests each connection, and writes a `.env` file with `PAGEBRIDGE_`-prefixed variables.
22
+
23
+ ```bash
24
+ pagebridge init
18
25
  ```
19
26
 
20
- ## Commands
27
+ Options:
28
+
29
+ | Option | Description |
30
+ |--------|-------------|
31
+ | `--skip-db-check` | Skip database connection test |
32
+ | `--skip-sanity-check` | Skip Sanity API test |
33
+ | `--skip-gsc-check` | Skip Google Search Console API test |
34
+
35
+ ### `pagebridge doctor`
21
36
 
22
- ### sync
37
+ Diagnose configuration issues. Checks your env file, credentials, database connection, schema, Sanity access, and GSC API access.
38
+
39
+ ```bash
40
+ pagebridge doctor
41
+ pagebridge doctor --verbose
42
+ ```
43
+
44
+ Looks for `.env.local` first, then `.env`. Loads the file and validates all required variables.
45
+
46
+ ### `pagebridge sync`
23
47
 
24
48
  Sync Google Search Console data and optionally generate refresh tasks for decaying content.
25
49
 
26
50
  ```bash
27
- pnpm --filter @pagebridge/cli start sync --site sc-domain:example.com
51
+ pagebridge sync --site sc-domain:example.com
28
52
  ```
29
53
 
30
54
  Options:
31
55
 
32
56
  | Option | Description | Default |
33
57
  |--------|-------------|---------|
34
- | `--site <url>` | GSC site URL (required) | - |
35
- | `--dry-run` | Preview changes without writing to Sanity | false |
36
- | `--skip-tasks` | Only sync data, skip task generation | false |
37
- | `--check-index` | Check Google index status for pages | false |
38
- | `--quiet-period <days>` | Days to ignore recently published content | 45 |
58
+ | `--site <url>` | GSC site URL **(required)** | |
59
+ | `--dry-run` | Preview changes without writing to Sanity | `false` |
60
+ | `--skip-tasks` | Only sync data, skip task generation | `false` |
61
+ | `--check-index` | Check Google index status for pages | `false` |
62
+ | `--quiet-period <days>` | Days to ignore recently published content | `45` |
63
+ | `--diagnose` | Show detailed diagnostics for unmatched URLs | `false` |
64
+ | `--diagnose-url <url>` | Diagnose why a specific URL is not matching | — |
65
+ | `--migrate` | Run database migrations before syncing | `false` |
66
+ | `--debug` | Enable debug logging with timing information | `false` |
67
+
68
+ All credentials can also be passed as flags (`--db-url`, `--sanity-project-id`, etc.) to override env vars.
39
69
 
40
70
  Examples:
41
71
 
42
72
  ```bash
43
- # Basic sync
44
- pnpm --filter @pagebridge/cli start sync --site sc-domain:example.com
73
+ # First sync — creates tables automatically
74
+ pagebridge sync --site sc-domain:example.com --migrate
45
75
 
46
76
  # Preview what would be synced
47
- pnpm --filter @pagebridge/cli start sync --site sc-domain:example.com --dry-run
77
+ pagebridge sync --site sc-domain:example.com --dry-run
48
78
 
49
79
  # Sync data only, no refresh tasks
50
- pnpm --filter @pagebridge/cli start sync --site sc-domain:example.com --skip-tasks
51
-
52
- # Include index status checks
53
- pnpm --filter @pagebridge/cli start sync --site sc-domain:example.com --check-index
80
+ pagebridge sync --site sc-domain:example.com --skip-tasks
54
81
 
55
- # Use a shorter quiet period (30 days)
56
- pnpm --filter @pagebridge/cli start sync --site sc-domain:example.com --quiet-period 30
82
+ # Debug why a URL isn't matching
83
+ pagebridge sync --site sc-domain:example.com --diagnose-url https://example.com/my-page
57
84
  ```
58
85
 
59
- ### list-sites
86
+ ### `pagebridge list-sites`
60
87
 
61
- List all Google Search Console properties accessible by the service account.
88
+ List all Google Search Console properties accessible by the configured service account.
62
89
 
63
90
  ```bash
64
- pnpm --filter @pagebridge/cli start list-sites
91
+ pagebridge list-sites
65
92
  ```
66
93
 
67
- Output:
94
+ ### `pagebridge diagnose`
68
95
 
69
- ```
70
- Available GSC Sites:
71
- - sc-domain:example.com
72
- - https://www.example.com/
73
- - sc-domain:another-site.com
96
+ Show stored diagnostics for unmatched URLs from previous sync runs.
97
+
98
+ ```bash
99
+ pagebridge diagnose --site sc-domain:example.com
74
100
  ```
75
101
 
76
102
  ## Environment Variables
77
103
 
78
- Create a `.env` file in the repository root with:
104
+ Create a `.env.local` or `.env` file. PageBridge uses a `PAGEBRIDGE_` prefix to avoid conflicts with your project's existing env vars:
79
105
 
80
106
  ```bash
81
- # Google Service Account (required)
82
- # JSON stringified credentials from Google Cloud Console
83
- GOOGLE_SERVICE_ACCOUNT='{"type":"service_account","project_id":"...","private_key":"..."}'
107
+ # Google Service Account JSON (stringified)
108
+ PAGEBRIDGE_GOOGLE_SERVICE_ACCOUNT='{"type":"service_account","project_id":"..."}'
84
109
 
85
- # PostgreSQL Database (required)
86
- DATABASE_URL=postgresql://user:password@localhost:5432/content_keep
110
+ # PostgreSQL connection string
111
+ PAGEBRIDGE_DATABASE_URL='postgresql://user:password@localhost:5432/pagebridge'
87
112
 
88
- # Sanity Configuration (required)
89
- SANITY_PROJECT_ID=your-project-id
90
- SANITY_DATASET=production
91
- SANITY_TOKEN=your-write-token
113
+ # Sanity configuration
114
+ PAGEBRIDGE_SANITY_PROJECT_ID='your-project-id'
115
+ PAGEBRIDGE_SANITY_DATASET='production'
116
+ PAGEBRIDGE_SANITY_TOKEN='sk...'
92
117
 
93
- # Site URL for URL matching (required)
94
- SITE_URL=https://example.com
118
+ # Your website base URL (used for URL matching)
119
+ PAGEBRIDGE_SITE_URL='https://example.com'
95
120
  ```
96
121
 
97
- ## Workflow
122
+ Unprefixed names (`DATABASE_URL`, `SANITY_TOKEN`, etc.) are also supported as a fallback. The `PAGEBRIDGE_`-prefixed version always takes priority.
123
+
124
+ ## Sync Workflow
98
125
 
99
126
  The `sync` command performs these steps:
100
127
 
101
- 1. **Validate environment** - Checks all required variables are set
102
- 2. **Find or create gscSite** - Ensures a Sanity document exists for the site
103
- 3. **Fetch GSC data** - Retrieves 90 days of search analytics (skipping last 3 days for data stability)
104
- 4. **Store metrics** - Writes page and query metrics to PostgreSQL
105
- 5. **Match URLs** - Maps GSC pages to Sanity documents by slug
106
- 6. **Write snapshots** - Creates gscSnapshot documents in Sanity with metrics and top queries
107
- 7. **Check index status** (optional) - Queries Google URL Inspection API
108
- 8. **Detect decay** - Analyzes metrics for decay patterns
109
- 9. **Generate tasks** - Creates gscRefreshTask documents for pages showing decay
110
-
111
- ## Programmatic Usage
112
-
113
- The CLI uses `@pagebridge/core` under the hood. For programmatic access:
114
-
115
- ```typescript
116
- import { GSCClient, SyncEngine, DecayDetector, TaskGenerator } from '@pagebridge/core';
117
- import { createDb } from '@pagebridge/db';
118
- import { createClient } from '@sanity/client';
119
-
120
- const gscClient = new GSCClient({
121
- serviceAccountJson: process.env.GOOGLE_SERVICE_ACCOUNT,
122
- });
123
-
124
- const db = createDb(process.env.DATABASE_URL);
125
-
126
- const sanityClient = createClient({
127
- projectId: process.env.SANITY_PROJECT_ID,
128
- dataset: process.env.SANITY_DATASET,
129
- token: process.env.SANITY_TOKEN,
130
- apiVersion: '2024-01-01',
131
- useCdn: false,
132
- });
133
-
134
- const engine = new SyncEngine({ gscClient, db, sanityClient });
135
- const result = await engine.sync({
136
- siteUrl: 'sc-domain:example.com',
137
- siteId: 'sanity-site-id',
138
- });
139
- ```
128
+ 1. **Validate connections** Tests database, Sanity, and GSC access
129
+ 2. **Find or create gscSite** Ensures a Sanity document exists for the site
130
+ 3. **Fetch GSC data** Retrieves 90 days of search analytics (skipping last 3 days for data stability)
131
+ 4. **Store metrics** Writes page and query metrics to PostgreSQL
132
+ 5. **Match URLs** Maps GSC pages to Sanity documents by slug
133
+ 6. **Detect decay** Analyzes metrics for position drops, CTR issues, and traffic decline
134
+ 7. **Generate tasks** Creates `gscRefreshTask` documents for pages showing decay
135
+ 8. **Write snapshots** Creates `gscSnapshot` documents in Sanity with metrics and top queries
140
136
 
141
137
  ## Dependencies
142
138
 
143
- - `@pagebridge/core` - Business logic
144
- - `@pagebridge/db` - Database operations
145
- - `@sanity/client` - Sanity API
146
- - `commander` - CLI framework
147
- - `dotenv` - Environment variable loading
139
+ - `@pagebridge/core` Sync engine, decay detection, URL matching
140
+ - `@pagebridge/db` PostgreSQL schema and queries
141
+ - `@sanity/client` Sanity API client
142
+ - `commander` CLI framework
148
143
 
149
144
  ## Development
150
145
 
151
146
  ```bash
152
147
  # Watch mode
153
- pnpm --filter @pagebridge/cli dev
148
+ pnpm dev --filter=@pagebridge/cli
154
149
 
155
150
  # Build
156
- pnpm --filter @pagebridge/cli build
151
+ pnpm build --filter=@pagebridge/cli
157
152
 
158
153
  # Type check
159
- pnpm --filter @pagebridge/cli check-types
154
+ pnpm check-types --filter=@pagebridge/cli
160
155
  ```
161
156
 
162
157
  ## License
@@ -1 +1 @@
1
- {"version":3,"file":"diagnose.d.ts","sourceRoot":"","sources":["../../src/commands/diagnose.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,OAAO,EAAE,MAAM,WAAW,CAAC;AASpC,eAAO,MAAM,eAAe,SAyFxB,CAAC"}
1
+ {"version":3,"file":"diagnose.d.ts","sourceRoot":"","sources":["../../src/commands/diagnose.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,OAAO,EAAE,MAAM,WAAW,CAAC;AAWpC,eAAO,MAAM,eAAe,SA2GxB,CAAC"}
@@ -1,19 +1,32 @@
1
1
  import { Command } from "commander";
2
- import postgres from "postgres";
3
- import { createDbWithClient, unmatchDiagnostics, eq, desc, } from "@pagebridge/db";
2
+ import { createDb, unmatchDiagnostics, eq, desc, } from "@pagebridge/db";
3
+ import { resolve, requireConfig } from "../resolve-config.js";
4
+ import { log } from "../logger.js";
5
+ import { migrateIfRequested } from "../migrate.js";
4
6
  export const diagnoseCommand = new Command("diagnose")
5
7
  .description("View diagnostics for unmatched URLs")
6
8
  .requiredOption("--site <url>", "GSC site URL (e.g., sc-domain:example.com)")
7
9
  .option("--reason <reason>", "Filter by unmatch reason")
8
10
  .option("--limit <n>", "Limit number of results", "20")
9
11
  .option("--json", "Output as JSON")
12
+ .option("--migrate", "Run database migrations before querying")
13
+ .option("--db-url <url>", "PostgreSQL connection string")
10
14
  .action(async (options) => {
11
- if (!process.env.DATABASE_URL) {
12
- console.error("Missing required environment variable: DATABASE_URL");
13
- process.exit(1);
14
- }
15
- const sql = postgres(process.env.DATABASE_URL);
16
- const db = createDbWithClient(sql);
15
+ const dbUrl = resolve(options.dbUrl, "DATABASE_URL");
16
+ requireConfig([
17
+ { name: "DATABASE_URL", flag: "--db-url <url>", envVar: "DATABASE_URL", value: dbUrl },
18
+ ]);
19
+ // Run migrations if requested
20
+ await migrateIfRequested(!!options.migrate, dbUrl);
21
+ const { db, close } = createDb(dbUrl);
22
+ // Register shutdown handlers
23
+ const shutdown = async () => {
24
+ log.warn("Received shutdown signal, closing connections...");
25
+ await close();
26
+ process.exit(130);
27
+ };
28
+ process.on("SIGTERM", shutdown);
29
+ process.on("SIGINT", shutdown);
17
30
  try {
18
31
  const query = db
19
32
  .select()
@@ -27,8 +40,8 @@ export const diagnoseCommand = new Command("diagnose")
27
40
  return;
28
41
  }
29
42
  if (results.length === 0) {
30
- console.log(`No unmatched URLs found for ${options.site}`);
31
- console.log(`Run 'sync --site ${options.site}' first to generate diagnostics.`);
43
+ log.info(`No unmatched URLs found for ${options.site}`);
44
+ log.info(`Run 'sync --site ${options.site}' first to generate diagnostics.`);
32
45
  return;
33
46
  }
34
47
  // Group by reason
@@ -38,23 +51,22 @@ export const diagnoseCommand = new Command("diagnose")
38
51
  existing.push(r);
39
52
  byReason.set(r.unmatchReason, existing);
40
53
  }
41
- console.log(`\nUnmatched URL Diagnostics for ${options.site}\n`);
42
- console.log(`Total: ${results.length} unmatched URLs\n`);
54
+ log.info(`\nUnmatched URL Diagnostics for ${options.site}\n`);
55
+ log.info(`Total: ${results.length} unmatched URLs\n`);
43
56
  for (const [reason, items] of byReason) {
44
- console.log(`${getReasonEmoji(reason)} ${getReasonDescription(reason)} (${items.length}):`);
45
- console.log();
57
+ log.info(`${getReasonEmoji(reason)} ${getReasonDescription(reason)} (${items.length}):`);
46
58
  for (const item of items) {
47
- console.log(` ${item.gscUrl}`);
59
+ log.info(` ${item.gscUrl}`);
48
60
  if (item.extractedSlug) {
49
- console.log(` Extracted slug: "${item.extractedSlug}"`);
61
+ log.info(` Extracted slug: "${item.extractedSlug}"`);
50
62
  }
51
63
  if (item.similarSlugs) {
52
64
  try {
53
65
  const similar = JSON.parse(item.similarSlugs);
54
66
  if (similar.length > 0) {
55
- console.log(` Similar slugs in Sanity:`);
67
+ log.info(` Similar slugs in Sanity:`);
56
68
  for (const s of similar) {
57
- console.log(` - ${s}`);
69
+ log.info(` - ${s}`);
58
70
  }
59
71
  }
60
72
  }
@@ -62,17 +74,23 @@ export const diagnoseCommand = new Command("diagnose")
62
74
  // Ignore parse errors
63
75
  }
64
76
  }
65
- console.log();
66
77
  }
67
78
  }
68
- console.log(`\nTo fix unmatched URLs:`);
69
- console.log(` 1. Check if the Sanity document exists with the correct slug`);
70
- console.log(` 2. Verify the document type is in the contentTypes list`);
71
- console.log(` 3. Ensure the slug field name matches your configuration`);
72
- console.log(` 4. If using a path prefix, verify it matches your URL structure`);
79
+ log.info(`\nTo fix unmatched URLs:`);
80
+ log.info(` 1. Check if the Sanity document exists with the correct slug`);
81
+ log.info(` 2. Verify the document type is in the contentTypes list`);
82
+ log.info(` 3. Ensure the slug field name matches your configuration`);
83
+ log.info(` 4. If using a path prefix, verify it matches your URL structure`);
84
+ }
85
+ catch (error) {
86
+ const message = error instanceof Error ? error.message : String(error);
87
+ log.error(`Diagnose failed: ${message}`);
88
+ process.exitCode = 1;
73
89
  }
74
90
  finally {
75
- await sql.end();
91
+ await close();
92
+ process.removeListener("SIGTERM", shutdown);
93
+ process.removeListener("SIGINT", shutdown);
76
94
  }
77
95
  });
78
96
  function getReasonEmoji(reason) {
@@ -0,0 +1,3 @@
1
+ import { Command } from "commander";
2
+ export declare const doctorCommand: Command;
3
+ //# sourceMappingURL=doctor.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"doctor.d.ts","sourceRoot":"","sources":["../../src/commands/doctor.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,OAAO,EAAE,MAAM,WAAW,CAAC;AA+UpC,eAAO,MAAM,aAAa,SA0EtB,CAAC"}