@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 +21 -21
- package/README.md +88 -93
- package/dist/commands/diagnose.d.ts.map +1 -1
- package/dist/commands/diagnose.js +43 -25
- 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/commands/list-sites.d.ts.map +1 -1
- package/dist/commands/list-sites.js +22 -17
- package/dist/commands/sync.d.ts.map +1 -1
- package/dist/commands/sync.js +128 -70
- package/dist/index.js +5 -7
- package/dist/logger.d.ts +7 -0
- package/dist/logger.d.ts.map +1 -0
- package/dist/logger.js +19 -0
- package/dist/migrate.d.ts +2 -0
- package/dist/migrate.d.ts.map +1 -0
- package/dist/migrate.js +13 -0
- package/dist/resolve-config.d.ts +20 -0
- package/dist/resolve-config.d.ts.map +1 -0
- package/dist/resolve-config.js +26 -0
- package/package.json +23 -7
- package/.turbo/turbo-build.log +0 -2
- package/eslint.config.js +0 -3
- package/src/commands/diagnose.ts +0 -129
- package/src/commands/list-sites.ts +0 -47
- package/src/commands/sync.ts +0 -406
- package/src/index.ts +0 -26
- package/tsconfig.json +0 -9
|
@@ -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"}
|
|
@@ -0,0 +1,182 @@
|
|
|
1
|
+
import { Command } from "commander";
|
|
2
|
+
import { createClient as createSanityClient } from "@sanity/client";
|
|
3
|
+
import { GSCClient } from "@pagebridge/core";
|
|
4
|
+
import { createDb, sql } from "@pagebridge/db";
|
|
5
|
+
import { log } from "../logger.js";
|
|
6
|
+
import { existsSync, writeFileSync } from "fs";
|
|
7
|
+
import { resolve as resolvePath } from "path";
|
|
8
|
+
import * as readline from "readline/promises";
|
|
9
|
+
import { stdin as input, stdout as output } from "process";
|
|
10
|
+
const rl = readline.createInterface({ input, output });
|
|
11
|
+
async function prompt(question, defaultValue) {
|
|
12
|
+
const displayQuestion = defaultValue
|
|
13
|
+
? `${question} (${defaultValue}): `
|
|
14
|
+
: `${question}: `;
|
|
15
|
+
const answer = await rl.question(displayQuestion);
|
|
16
|
+
return answer.trim() || defaultValue || "";
|
|
17
|
+
}
|
|
18
|
+
async function confirmPrompt(question, defaultYes = true) {
|
|
19
|
+
const suffix = defaultYes ? " (Y/n): " : " (y/N): ";
|
|
20
|
+
const answer = await rl.question(question + suffix);
|
|
21
|
+
const normalized = answer.trim().toLowerCase();
|
|
22
|
+
if (!normalized)
|
|
23
|
+
return defaultYes;
|
|
24
|
+
return normalized === "y" || normalized === "yes";
|
|
25
|
+
}
|
|
26
|
+
export const initCommand = new Command("init")
|
|
27
|
+
.description("Interactive setup wizard for PageBridge")
|
|
28
|
+
.option("--skip-db-check", "Skip database connection test")
|
|
29
|
+
.option("--skip-sanity-check", "Skip Sanity API test")
|
|
30
|
+
.option("--skip-gsc-check", "Skip Google Search Console API test")
|
|
31
|
+
.action(async (options) => {
|
|
32
|
+
log.info("š PageBridge Interactive Setup\n");
|
|
33
|
+
const envPath = resolvePath(process.cwd(), ".env");
|
|
34
|
+
// Check if .env already exists
|
|
35
|
+
if (existsSync(envPath)) {
|
|
36
|
+
const overwrite = await confirmPrompt(".env file already exists. Do you want to overwrite it?", false);
|
|
37
|
+
if (!overwrite) {
|
|
38
|
+
log.info("Skipping .env creation. Exiting...");
|
|
39
|
+
rl.close();
|
|
40
|
+
return;
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
log.info("Let's configure your environment variables.\n");
|
|
44
|
+
// Database URL
|
|
45
|
+
log.info("š¦ Database Configuration");
|
|
46
|
+
const dbUrl = await prompt("PostgreSQL connection string", "postgresql://postgres:postgres@localhost:5432/gsc_sanity");
|
|
47
|
+
if (!options.skipDbCheck && dbUrl) {
|
|
48
|
+
try {
|
|
49
|
+
log.info("Testing database connection...");
|
|
50
|
+
const { db, close } = createDb(dbUrl);
|
|
51
|
+
await db.execute(sql `SELECT 1`);
|
|
52
|
+
await close();
|
|
53
|
+
log.info("ā
Database connection successful\n");
|
|
54
|
+
}
|
|
55
|
+
catch (error) {
|
|
56
|
+
log.error(`ā Database connection failed: ${error instanceof Error ? error.message : String(error)}`);
|
|
57
|
+
log.warn("You can continue anyway, but you'll need to fix this before syncing.\n");
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
// Sanity Configuration
|
|
61
|
+
log.info("šØ Sanity Configuration");
|
|
62
|
+
const sanityProjectId = await prompt("Sanity Project ID");
|
|
63
|
+
const sanityDataset = await prompt("Sanity Dataset", "production");
|
|
64
|
+
const sanityToken = await prompt("Sanity API Token (with Editor permissions)");
|
|
65
|
+
if (!options.skipSanityCheck && sanityProjectId && sanityToken) {
|
|
66
|
+
try {
|
|
67
|
+
log.info("Testing Sanity API connection...");
|
|
68
|
+
const sanity = createSanityClient({
|
|
69
|
+
projectId: sanityProjectId,
|
|
70
|
+
dataset: sanityDataset,
|
|
71
|
+
token: sanityToken,
|
|
72
|
+
apiVersion: "2024-01-01",
|
|
73
|
+
useCdn: false,
|
|
74
|
+
});
|
|
75
|
+
await sanity.fetch('*[_type == "gscSite"][0]{ _id }');
|
|
76
|
+
log.info("ā
Sanity connection successful\n");
|
|
77
|
+
}
|
|
78
|
+
catch (error) {
|
|
79
|
+
log.error(`ā Sanity connection failed: ${error instanceof Error ? error.message : String(error)}`);
|
|
80
|
+
log.warn("Check your project ID, dataset, and token permissions.\n");
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
// Google Service Account
|
|
84
|
+
log.info("š Google Search Console Configuration");
|
|
85
|
+
log.info("You need a Google Cloud service account with Search Console API access.");
|
|
86
|
+
log.info('Paste the entire service account JSON (it should start with {"type":"service_account"...):\n');
|
|
87
|
+
const googleServiceAccount = await prompt("Google Service Account JSON");
|
|
88
|
+
// Validate JSON
|
|
89
|
+
let credentials = null;
|
|
90
|
+
try {
|
|
91
|
+
credentials = JSON.parse(googleServiceAccount);
|
|
92
|
+
if (!credentials) {
|
|
93
|
+
throw new Error("Parsed credentials is null");
|
|
94
|
+
}
|
|
95
|
+
log.info(`ā
Valid JSON for service account: ${credentials.client_email}\n`);
|
|
96
|
+
}
|
|
97
|
+
catch {
|
|
98
|
+
log.error("ā Invalid JSON format for service account");
|
|
99
|
+
log.warn("You'll need to fix this in .env before syncing.\n");
|
|
100
|
+
}
|
|
101
|
+
// Test GSC API
|
|
102
|
+
if (!options.skipGscCheck && credentials) {
|
|
103
|
+
try {
|
|
104
|
+
log.info("Testing Google Search Console API access...");
|
|
105
|
+
const gsc = new GSCClient({ credentials });
|
|
106
|
+
const sites = await gsc.listSites();
|
|
107
|
+
log.info(`ā
GSC API access successful. Found ${sites.length} sites:`);
|
|
108
|
+
sites.forEach((site) => log.info(` - ${site}`));
|
|
109
|
+
log.info("");
|
|
110
|
+
}
|
|
111
|
+
catch (error) {
|
|
112
|
+
log.error(`ā GSC API access failed: ${error instanceof Error ? error.message : String(error)}`);
|
|
113
|
+
log.warn("Make sure the service account has access to your Search Console properties.\n");
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
// Site URL
|
|
117
|
+
log.info("š Site Configuration");
|
|
118
|
+
const siteUrl = await prompt("Your website base URL (e.g., https://example.com)");
|
|
119
|
+
// Write .env file
|
|
120
|
+
const envContent = `# PageBridge Environment Variables
|
|
121
|
+
# Generated by 'pagebridge init' on ${new Date().toISOString()}
|
|
122
|
+
# Uses PAGEBRIDGE_ prefix to avoid conflicts with your project's env vars.
|
|
123
|
+
|
|
124
|
+
# Google Service Account JSON (stringified)
|
|
125
|
+
PAGEBRIDGE_GOOGLE_SERVICE_ACCOUNT='${googleServiceAccount}'
|
|
126
|
+
|
|
127
|
+
# PostgreSQL connection string
|
|
128
|
+
PAGEBRIDGE_DATABASE_URL='${dbUrl}'
|
|
129
|
+
|
|
130
|
+
# Sanity Studio configuration
|
|
131
|
+
PAGEBRIDGE_SANITY_PROJECT_ID='${sanityProjectId}'
|
|
132
|
+
PAGEBRIDGE_SANITY_DATASET='${sanityDataset}'
|
|
133
|
+
PAGEBRIDGE_SANITY_TOKEN='${sanityToken}'
|
|
134
|
+
|
|
135
|
+
# Your site URL
|
|
136
|
+
PAGEBRIDGE_SITE_URL='${siteUrl}'
|
|
137
|
+
`;
|
|
138
|
+
writeFileSync(envPath, envContent, "utf-8");
|
|
139
|
+
log.info("ā
.env file created successfully!\n");
|
|
140
|
+
// Offer to run migrations
|
|
141
|
+
const runMigrations = await confirmPrompt("Would you like to run database migrations now?", true);
|
|
142
|
+
if (runMigrations && dbUrl) {
|
|
143
|
+
log.info("Running database migrations...");
|
|
144
|
+
try {
|
|
145
|
+
// Import migrate function
|
|
146
|
+
const { migrateIfRequested } = await import("../migrate.js");
|
|
147
|
+
await migrateIfRequested(true, dbUrl);
|
|
148
|
+
log.info("ā
Migrations completed\n");
|
|
149
|
+
}
|
|
150
|
+
catch (error) {
|
|
151
|
+
log.error(`ā Migration failed: ${error instanceof Error ? error.message : String(error)}\n`);
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
// Offer to run first sync
|
|
155
|
+
if (credentials) {
|
|
156
|
+
const runSync = await confirmPrompt("Would you like to run your first sync now?", false);
|
|
157
|
+
if (runSync) {
|
|
158
|
+
const sites = await new GSCClient({ credentials }).listSites();
|
|
159
|
+
if (sites.length === 0) {
|
|
160
|
+
log.warn("No sites found in your Search Console account.");
|
|
161
|
+
}
|
|
162
|
+
else if (sites.length === 1) {
|
|
163
|
+
log.info(`\nUsing site: ${sites[0]}`);
|
|
164
|
+
// Note: You'd import and call the sync command here
|
|
165
|
+
log.info(`Run: pagebridge sync --site "${sites[0]}" --migrate`);
|
|
166
|
+
}
|
|
167
|
+
else {
|
|
168
|
+
log.info("\nAvailable sites:");
|
|
169
|
+
sites.forEach((site, i) => log.info(` ${i + 1}. ${site}`));
|
|
170
|
+
const siteChoice = await prompt("\nEnter site number or full URL");
|
|
171
|
+
const selectedSite = sites[parseInt(siteChoice) - 1] || siteChoice;
|
|
172
|
+
log.info(`\nRun: pagebridge sync --site "${selectedSite}" --migrate`);
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
log.info("\nš Setup complete!");
|
|
177
|
+
log.info("\nNext steps:");
|
|
178
|
+
log.info(" 1. Review your .env file");
|
|
179
|
+
log.info(" 2. Run: pagebridge sync --site <your-site-url> --migrate");
|
|
180
|
+
log.info(" 3. Open Sanity Studio to see your performance data\n");
|
|
181
|
+
rl.close();
|
|
182
|
+
});
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"list-sites.d.ts","sourceRoot":"","sources":["../../src/commands/list-sites.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,OAAO,EAAE,MAAM,WAAW,CAAC;
|
|
1
|
+
{"version":3,"file":"list-sites.d.ts","sourceRoot":"","sources":["../../src/commands/list-sites.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,OAAO,EAAE,MAAM,WAAW,CAAC;AAKpC,eAAO,MAAM,gBAAgB,SA6CzB,CAAC"}
|
|
@@ -1,38 +1,43 @@
|
|
|
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
|
export const listSitesCommand = new Command("list-sites")
|
|
4
6
|
.description("List all sites the service account has access to")
|
|
5
|
-
.
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
7
|
+
.option("--google-service-account <json>", "Google service account JSON")
|
|
8
|
+
.action(async (options) => {
|
|
9
|
+
const googleServiceAccount = resolve(options.googleServiceAccount, "GOOGLE_SERVICE_ACCOUNT");
|
|
10
|
+
requireConfig([
|
|
11
|
+
{ name: "GOOGLE_SERVICE_ACCOUNT", flag: "--google-service-account <json>", envVar: "GOOGLE_SERVICE_ACCOUNT", value: googleServiceAccount },
|
|
12
|
+
]);
|
|
10
13
|
let credentials;
|
|
11
14
|
try {
|
|
12
|
-
credentials = JSON.parse(
|
|
15
|
+
credentials = JSON.parse(googleServiceAccount);
|
|
13
16
|
}
|
|
14
17
|
catch {
|
|
15
|
-
|
|
18
|
+
log.error("Failed to parse GOOGLE_SERVICE_ACCOUNT as JSON");
|
|
16
19
|
process.exit(1);
|
|
17
20
|
}
|
|
18
|
-
|
|
21
|
+
log.info(`Using service account: ${credentials.client_email}`);
|
|
19
22
|
const gsc = new GSCClient({ credentials });
|
|
20
23
|
try {
|
|
21
24
|
const sites = await gsc.listSites();
|
|
22
25
|
if (sites.length === 0) {
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
26
|
+
log.warn("No sites found. The service account has no access to any GSC properties.");
|
|
27
|
+
log.info("\nTo fix this:");
|
|
28
|
+
log.info("1. Go to Google Search Console > Settings > Users and permissions");
|
|
29
|
+
log.info(`2. Add user: ${credentials.client_email}`);
|
|
30
|
+
log.info("3. Set permission level to 'Full'");
|
|
28
31
|
}
|
|
29
32
|
else {
|
|
30
|
-
|
|
31
|
-
sites.forEach((site) =>
|
|
32
|
-
|
|
33
|
+
log.info(`Found ${sites.length} site(s):\n`);
|
|
34
|
+
sites.forEach((site) => log.info(` ${site}`));
|
|
35
|
+
log.info('\nUse one of these exact values with: pnpm sync --site "<value>"');
|
|
33
36
|
}
|
|
34
37
|
}
|
|
35
38
|
catch (error) {
|
|
36
|
-
|
|
39
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
40
|
+
log.error(`Failed to list sites: ${message}`);
|
|
41
|
+
process.exit(1);
|
|
37
42
|
}
|
|
38
43
|
});
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"sync.d.ts","sourceRoot":"","sources":["../../src/commands/sync.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,OAAO,EAAE,MAAM,WAAW,CAAC;
|
|
1
|
+
{"version":3,"file":"sync.d.ts","sourceRoot":"","sources":["../../src/commands/sync.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,OAAO,EAAE,MAAM,WAAW,CAAC;AAkCpC,eAAO,MAAM,WAAW,SAkYpB,CAAC"}
|