@pagebridge/cli 0.0.2 → 0.1.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -21
- package/README.md +88 -93
- package/dist/commands/doctor.d.ts +3 -0
- package/dist/commands/doctor.d.ts.map +1 -0
- package/dist/commands/doctor.js +353 -0
- package/dist/commands/init.d.ts +3 -0
- package/dist/commands/init.d.ts.map +1 -0
- package/dist/commands/init.js +182 -0
- package/dist/index.js +5 -1
- package/dist/resolve-config.d.ts +4 -1
- package/dist/resolve-config.d.ts.map +1 -1
- package/dist/resolve-config.js +8 -3
- package/package.json +23 -5
- package/.turbo/turbo-build.log +0 -2
- package/.turbo/turbo-check-types.log +0 -4
- package/eslint.config.js +0 -3
- package/src/commands/diagnose.ts +0 -149
- package/src/commands/list-sites.ts +0 -51
- package/src/commands/sync.ts +0 -471
- package/src/index.ts +0 -18
- package/src/logger.ts +0 -22
- package/src/migrate.ts +0 -13
- package/src/resolve-config.ts +0 -32
- package/tsconfig.json +0 -9
package/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
|
|
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
|
-
|
|
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
|
-
|
|
14
|
-
pnpm --filter @pagebridge/cli start <command>
|
|
19
|
+
### `pagebridge init`
|
|
15
20
|
|
|
16
|
-
|
|
17
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
#
|
|
44
|
-
|
|
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
|
-
|
|
77
|
+
pagebridge sync --site sc-domain:example.com --dry-run
|
|
48
78
|
|
|
49
79
|
# Sync data only, no refresh tasks
|
|
50
|
-
|
|
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
|
-
#
|
|
56
|
-
|
|
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
|
-
|
|
91
|
+
pagebridge list-sites
|
|
65
92
|
```
|
|
66
93
|
|
|
67
|
-
|
|
94
|
+
### `pagebridge diagnose`
|
|
68
95
|
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
-
|
|
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
|
|
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 (
|
|
82
|
-
|
|
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
|
|
86
|
-
|
|
110
|
+
# PostgreSQL connection string
|
|
111
|
+
PAGEBRIDGE_DATABASE_URL='postgresql://user:password@localhost:5432/pagebridge'
|
|
87
112
|
|
|
88
|
-
# Sanity
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
113
|
+
# Sanity configuration
|
|
114
|
+
PAGEBRIDGE_SANITY_PROJECT_ID='your-project-id'
|
|
115
|
+
PAGEBRIDGE_SANITY_DATASET='production'
|
|
116
|
+
PAGEBRIDGE_SANITY_TOKEN='sk...'
|
|
92
117
|
|
|
93
|
-
#
|
|
94
|
-
|
|
118
|
+
# Your website base URL (used for URL matching)
|
|
119
|
+
PAGEBRIDGE_SITE_URL='https://example.com'
|
|
95
120
|
```
|
|
96
121
|
|
|
97
|
-
|
|
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
|
|
102
|
-
2. **Find or create gscSite**
|
|
103
|
-
3. **Fetch GSC data**
|
|
104
|
-
4. **Store metrics**
|
|
105
|
-
5. **Match URLs**
|
|
106
|
-
6. **
|
|
107
|
-
7. **
|
|
108
|
-
8. **
|
|
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`
|
|
144
|
-
- `@pagebridge/db`
|
|
145
|
-
- `@sanity/client`
|
|
146
|
-
- `commander`
|
|
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
|
|
148
|
+
pnpm dev --filter=@pagebridge/cli
|
|
154
149
|
|
|
155
150
|
# Build
|
|
156
|
-
pnpm --filter
|
|
151
|
+
pnpm build --filter=@pagebridge/cli
|
|
157
152
|
|
|
158
153
|
# Type check
|
|
159
|
-
pnpm --filter
|
|
154
|
+
pnpm check-types --filter=@pagebridge/cli
|
|
160
155
|
```
|
|
161
156
|
|
|
162
157
|
## License
|
|
@@ -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"}
|
|
@@ -0,0 +1,353 @@
|
|
|
1
|
+
import { Command } from "commander";
|
|
2
|
+
import { createClient as createSanityClient } from "@sanity/client";
|
|
3
|
+
import { GSCClient } from "@pagebridge/core";
|
|
4
|
+
import { createDb, sql, searchAnalytics, queryAnalytics, syncLog, pageIndexStatus, unmatchDiagnostics, } from "@pagebridge/db";
|
|
5
|
+
import { resolve } from "../resolve-config.js";
|
|
6
|
+
import { log } from "../logger.js";
|
|
7
|
+
import { existsSync, readFileSync } from "fs";
|
|
8
|
+
import { resolve as resolvePath } from "path";
|
|
9
|
+
const ENV_FILES = [".env.local", ".env"];
|
|
10
|
+
function loadEnvFile(filePath) {
|
|
11
|
+
const content = readFileSync(filePath, "utf-8");
|
|
12
|
+
for (const line of content.split("\n")) {
|
|
13
|
+
const trimmed = line.trim();
|
|
14
|
+
if (!trimmed || trimmed.startsWith("#"))
|
|
15
|
+
continue;
|
|
16
|
+
const eqIndex = trimmed.indexOf("=");
|
|
17
|
+
if (eqIndex === -1)
|
|
18
|
+
continue;
|
|
19
|
+
const key = trimmed.slice(0, eqIndex).trim();
|
|
20
|
+
let value = trimmed.slice(eqIndex + 1).trim();
|
|
21
|
+
// Strip surrounding quotes
|
|
22
|
+
if ((value.startsWith('"') && value.endsWith('"')) ||
|
|
23
|
+
(value.startsWith("'") && value.endsWith("'"))) {
|
|
24
|
+
value = value.slice(1, -1);
|
|
25
|
+
}
|
|
26
|
+
// Don't overwrite existing env vars
|
|
27
|
+
if (process.env[key] === undefined) {
|
|
28
|
+
process.env[key] = value;
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
async function checkEnvFile() {
|
|
33
|
+
for (const name of ENV_FILES) {
|
|
34
|
+
const envPath = resolvePath(process.cwd(), name);
|
|
35
|
+
if (existsSync(envPath)) {
|
|
36
|
+
loadEnvFile(envPath);
|
|
37
|
+
return {
|
|
38
|
+
name: "Environment File",
|
|
39
|
+
status: "pass",
|
|
40
|
+
message: `${name} file found and loaded`,
|
|
41
|
+
};
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
return {
|
|
45
|
+
name: "Environment File",
|
|
46
|
+
status: "fail",
|
|
47
|
+
message: "No .env file found",
|
|
48
|
+
details: "Run 'pagebridge init' to create one (looks for .env.local, .env)",
|
|
49
|
+
};
|
|
50
|
+
}
|
|
51
|
+
async function checkEnvVars() {
|
|
52
|
+
const required = [
|
|
53
|
+
"GOOGLE_SERVICE_ACCOUNT",
|
|
54
|
+
"DATABASE_URL",
|
|
55
|
+
"SANITY_PROJECT_ID",
|
|
56
|
+
"SANITY_DATASET",
|
|
57
|
+
"SANITY_TOKEN",
|
|
58
|
+
"SITE_URL",
|
|
59
|
+
];
|
|
60
|
+
const missing = required.filter((key) => !process.env[`PAGEBRIDGE_${key}`] && !process.env[key]);
|
|
61
|
+
if (missing.length > 0) {
|
|
62
|
+
return {
|
|
63
|
+
name: "Environment Variables",
|
|
64
|
+
status: "fail",
|
|
65
|
+
message: `Missing ${missing.length} required variables`,
|
|
66
|
+
details: `Missing: ${missing.map((k) => `PAGEBRIDGE_${k}`).join(", ")}`,
|
|
67
|
+
};
|
|
68
|
+
}
|
|
69
|
+
return {
|
|
70
|
+
name: "Environment Variables",
|
|
71
|
+
status: "pass",
|
|
72
|
+
message: "All required variables are set",
|
|
73
|
+
};
|
|
74
|
+
}
|
|
75
|
+
async function checkDatabase() {
|
|
76
|
+
const dbUrl = resolve(undefined, "DATABASE_URL");
|
|
77
|
+
if (!dbUrl) {
|
|
78
|
+
return {
|
|
79
|
+
name: "Database Connection",
|
|
80
|
+
status: "skip",
|
|
81
|
+
message: "DATABASE_URL not set",
|
|
82
|
+
};
|
|
83
|
+
}
|
|
84
|
+
try {
|
|
85
|
+
const { db, close } = createDb(dbUrl);
|
|
86
|
+
await db.execute(sql `SELECT 1`);
|
|
87
|
+
await close();
|
|
88
|
+
return {
|
|
89
|
+
name: "Database Connection",
|
|
90
|
+
status: "pass",
|
|
91
|
+
message: "Connected successfully",
|
|
92
|
+
};
|
|
93
|
+
}
|
|
94
|
+
catch (error) {
|
|
95
|
+
return {
|
|
96
|
+
name: "Database Connection",
|
|
97
|
+
status: "fail",
|
|
98
|
+
message: "Connection failed",
|
|
99
|
+
details: error instanceof Error ? error.message : String(error),
|
|
100
|
+
};
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
async function checkDatabaseSchema() {
|
|
104
|
+
const dbUrl = resolve(undefined, "DATABASE_URL");
|
|
105
|
+
if (!dbUrl) {
|
|
106
|
+
return {
|
|
107
|
+
name: "Database Schema",
|
|
108
|
+
status: "skip",
|
|
109
|
+
message: "DATABASE_URL not set",
|
|
110
|
+
};
|
|
111
|
+
}
|
|
112
|
+
try {
|
|
113
|
+
const { db, close } = createDb(dbUrl);
|
|
114
|
+
// Check if tables exist by trying to query them
|
|
115
|
+
const tables = [
|
|
116
|
+
{ name: "search_analytics", table: searchAnalytics },
|
|
117
|
+
{ name: "query_analytics", table: queryAnalytics },
|
|
118
|
+
{ name: "sync_log", table: syncLog },
|
|
119
|
+
{ name: "page_index_status", table: pageIndexStatus },
|
|
120
|
+
{ name: "unmatch_diagnostics", table: unmatchDiagnostics },
|
|
121
|
+
];
|
|
122
|
+
const missingTables = [];
|
|
123
|
+
for (const { name, table } of tables) {
|
|
124
|
+
try {
|
|
125
|
+
await db.select().from(table).limit(1);
|
|
126
|
+
}
|
|
127
|
+
catch {
|
|
128
|
+
missingTables.push(name);
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
await close();
|
|
132
|
+
if (missingTables.length > 0) {
|
|
133
|
+
return {
|
|
134
|
+
name: "Database Schema",
|
|
135
|
+
status: "fail",
|
|
136
|
+
message: `Missing ${missingTables.length} tables`,
|
|
137
|
+
details: `Run 'pnpm db:push' or 'pagebridge sync --migrate'. Missing: ${missingTables.join(", ")}`,
|
|
138
|
+
};
|
|
139
|
+
}
|
|
140
|
+
return {
|
|
141
|
+
name: "Database Schema",
|
|
142
|
+
status: "pass",
|
|
143
|
+
message: "All tables exist",
|
|
144
|
+
};
|
|
145
|
+
}
|
|
146
|
+
catch (error) {
|
|
147
|
+
return {
|
|
148
|
+
name: "Database Schema",
|
|
149
|
+
status: "fail",
|
|
150
|
+
message: "Schema check failed",
|
|
151
|
+
details: error instanceof Error ? error.message : String(error),
|
|
152
|
+
};
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
async function checkSanity() {
|
|
156
|
+
const projectId = resolve(undefined, "SANITY_PROJECT_ID");
|
|
157
|
+
const dataset = resolve(undefined, "SANITY_DATASET");
|
|
158
|
+
const token = resolve(undefined, "SANITY_TOKEN");
|
|
159
|
+
if (!projectId || !dataset || !token) {
|
|
160
|
+
return {
|
|
161
|
+
name: "Sanity Connection",
|
|
162
|
+
status: "skip",
|
|
163
|
+
message: "Sanity credentials not set",
|
|
164
|
+
};
|
|
165
|
+
}
|
|
166
|
+
try {
|
|
167
|
+
const sanity = createSanityClient({
|
|
168
|
+
projectId,
|
|
169
|
+
dataset,
|
|
170
|
+
token,
|
|
171
|
+
apiVersion: "2024-01-01",
|
|
172
|
+
useCdn: false,
|
|
173
|
+
});
|
|
174
|
+
await sanity.fetch('*[_type == "gscSite"][0]{ _id }');
|
|
175
|
+
return {
|
|
176
|
+
name: "Sanity Connection",
|
|
177
|
+
status: "pass",
|
|
178
|
+
message: "Connected successfully",
|
|
179
|
+
};
|
|
180
|
+
}
|
|
181
|
+
catch (error) {
|
|
182
|
+
return {
|
|
183
|
+
name: "Sanity Connection",
|
|
184
|
+
status: "fail",
|
|
185
|
+
message: "Connection failed",
|
|
186
|
+
details: error instanceof Error ? error.message : String(error),
|
|
187
|
+
};
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
async function checkGoogleServiceAccount() {
|
|
191
|
+
const serviceAccount = resolve(undefined, "GOOGLE_SERVICE_ACCOUNT");
|
|
192
|
+
if (!serviceAccount) {
|
|
193
|
+
return {
|
|
194
|
+
name: "Google Service Account",
|
|
195
|
+
status: "skip",
|
|
196
|
+
message: "GOOGLE_SERVICE_ACCOUNT not set",
|
|
197
|
+
};
|
|
198
|
+
}
|
|
199
|
+
try {
|
|
200
|
+
const credentials = JSON.parse(serviceAccount);
|
|
201
|
+
if (!credentials.private_key || !credentials.client_email) {
|
|
202
|
+
return {
|
|
203
|
+
name: "Google Service Account",
|
|
204
|
+
status: "fail",
|
|
205
|
+
message: "Invalid service account format",
|
|
206
|
+
details: "Missing private_key or client_email",
|
|
207
|
+
};
|
|
208
|
+
}
|
|
209
|
+
return {
|
|
210
|
+
name: "Google Service Account",
|
|
211
|
+
status: "pass",
|
|
212
|
+
message: `Valid (${credentials.client_email})`,
|
|
213
|
+
};
|
|
214
|
+
}
|
|
215
|
+
catch {
|
|
216
|
+
return {
|
|
217
|
+
name: "Google Service Account",
|
|
218
|
+
status: "fail",
|
|
219
|
+
message: "Invalid JSON",
|
|
220
|
+
details: "Service account must be valid JSON",
|
|
221
|
+
};
|
|
222
|
+
}
|
|
223
|
+
}
|
|
224
|
+
async function checkGSCAccess() {
|
|
225
|
+
const serviceAccount = resolve(undefined, "GOOGLE_SERVICE_ACCOUNT");
|
|
226
|
+
if (!serviceAccount) {
|
|
227
|
+
return {
|
|
228
|
+
name: "GSC API Access",
|
|
229
|
+
status: "skip",
|
|
230
|
+
message: "Service account not configured",
|
|
231
|
+
};
|
|
232
|
+
}
|
|
233
|
+
try {
|
|
234
|
+
const credentials = JSON.parse(serviceAccount);
|
|
235
|
+
const gsc = new GSCClient({ credentials });
|
|
236
|
+
const sites = await gsc.listSites();
|
|
237
|
+
if (sites.length === 0) {
|
|
238
|
+
return {
|
|
239
|
+
name: "GSC API Access",
|
|
240
|
+
status: "warn",
|
|
241
|
+
message: "No sites found",
|
|
242
|
+
details: "Make sure the service account has access to your Search Console properties",
|
|
243
|
+
};
|
|
244
|
+
}
|
|
245
|
+
return {
|
|
246
|
+
name: "GSC API Access",
|
|
247
|
+
status: "pass",
|
|
248
|
+
message: `Found ${sites.length} site${sites.length > 1 ? "s" : ""}`,
|
|
249
|
+
};
|
|
250
|
+
}
|
|
251
|
+
catch (error) {
|
|
252
|
+
return {
|
|
253
|
+
name: "GSC API Access",
|
|
254
|
+
status: "fail",
|
|
255
|
+
message: "API access failed",
|
|
256
|
+
details: error instanceof Error ? error.message : String(error),
|
|
257
|
+
};
|
|
258
|
+
}
|
|
259
|
+
}
|
|
260
|
+
async function checkNodeVersion() {
|
|
261
|
+
const version = process.version;
|
|
262
|
+
const major = parseInt(version.slice(1).split(".")[0] ?? "0");
|
|
263
|
+
if (major < 18) {
|
|
264
|
+
return {
|
|
265
|
+
name: "Node.js Version",
|
|
266
|
+
status: "fail",
|
|
267
|
+
message: `Node.js ${version} is too old`,
|
|
268
|
+
details: "PageBridge requires Node.js 18 or higher",
|
|
269
|
+
};
|
|
270
|
+
}
|
|
271
|
+
return {
|
|
272
|
+
name: "Node.js Version",
|
|
273
|
+
status: "pass",
|
|
274
|
+
message: `Node.js ${version}`,
|
|
275
|
+
};
|
|
276
|
+
}
|
|
277
|
+
function getStatusIcon(status) {
|
|
278
|
+
switch (status) {
|
|
279
|
+
case "pass":
|
|
280
|
+
return "✅";
|
|
281
|
+
case "fail":
|
|
282
|
+
return "❌";
|
|
283
|
+
case "warn":
|
|
284
|
+
return "⚠️";
|
|
285
|
+
case "skip":
|
|
286
|
+
return "⏭️";
|
|
287
|
+
}
|
|
288
|
+
}
|
|
289
|
+
export const doctorCommand = new Command("doctor")
|
|
290
|
+
.description("Diagnose PageBridge setup and configuration issues")
|
|
291
|
+
.option("--verbose", "Show detailed information")
|
|
292
|
+
.action(async (options) => {
|
|
293
|
+
log.info("🏥 PageBridge Health Check\n");
|
|
294
|
+
const checks = [
|
|
295
|
+
checkNodeVersion,
|
|
296
|
+
checkEnvFile,
|
|
297
|
+
checkEnvVars,
|
|
298
|
+
checkDatabase,
|
|
299
|
+
checkDatabaseSchema,
|
|
300
|
+
checkSanity,
|
|
301
|
+
checkGoogleServiceAccount,
|
|
302
|
+
checkGSCAccess,
|
|
303
|
+
];
|
|
304
|
+
const results = [];
|
|
305
|
+
for (const check of checks) {
|
|
306
|
+
try {
|
|
307
|
+
const result = await check();
|
|
308
|
+
results.push(result);
|
|
309
|
+
const icon = getStatusIcon(result.status);
|
|
310
|
+
log.info(`${icon} ${result.name}: ${result.message}`);
|
|
311
|
+
if (options.verbose && result.details) {
|
|
312
|
+
log.info(` ${result.details}`);
|
|
313
|
+
}
|
|
314
|
+
else if (!options.verbose &&
|
|
315
|
+
result.status === "fail" &&
|
|
316
|
+
result.details) {
|
|
317
|
+
log.info(` ${result.details}`);
|
|
318
|
+
}
|
|
319
|
+
}
|
|
320
|
+
catch (error) {
|
|
321
|
+
results.push({
|
|
322
|
+
name: "Unknown Check",
|
|
323
|
+
status: "fail",
|
|
324
|
+
message: "Check failed",
|
|
325
|
+
details: error instanceof Error ? error.message : String(error),
|
|
326
|
+
});
|
|
327
|
+
}
|
|
328
|
+
}
|
|
329
|
+
// Summary
|
|
330
|
+
const passed = results.filter((r) => r.status === "pass").length;
|
|
331
|
+
const failed = results.filter((r) => r.status === "fail").length;
|
|
332
|
+
const warned = results.filter((r) => r.status === "warn").length;
|
|
333
|
+
const skipped = results.filter((r) => r.status === "skip").length;
|
|
334
|
+
log.info("\n📊 Summary:");
|
|
335
|
+
log.info(` ✅ Passed: ${passed}`);
|
|
336
|
+
if (failed > 0)
|
|
337
|
+
log.error(` ❌ Failed: ${failed}`);
|
|
338
|
+
if (warned > 0)
|
|
339
|
+
log.warn(` ⚠️ Warnings: ${warned}`);
|
|
340
|
+
if (skipped > 0)
|
|
341
|
+
log.info(` ⏭️ Skipped: ${skipped}`);
|
|
342
|
+
if (failed === 0 && warned === 0) {
|
|
343
|
+
log.info("\n🎉 All checks passed! PageBridge is ready to use.");
|
|
344
|
+
log.info("Run 'pagebridge sync --site <url> --migrate' to start syncing.");
|
|
345
|
+
}
|
|
346
|
+
else if (failed > 0) {
|
|
347
|
+
log.error("\n⚠️ Some checks failed. Fix the issues above and run 'pagebridge doctor' again.");
|
|
348
|
+
}
|
|
349
|
+
else {
|
|
350
|
+
log.warn("\n⚠️ Some warnings detected. PageBridge may still work, but review the warnings above.");
|
|
351
|
+
}
|
|
352
|
+
process.exit(failed > 0 ? 1 : 0);
|
|
353
|
+
});
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"init.d.ts","sourceRoot":"","sources":["../../src/commands/init.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,OAAO,EAAE,MAAM,WAAW,CAAC;AAmCpC,eAAO,MAAM,WAAW,SA8MpB,CAAC"}
|