@edtools/cli 0.6.2 → 0.7.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/dist/adapters/html/index.d.ts +2 -2
- package/dist/adapters/html/index.d.ts.map +1 -1
- package/dist/adapters/html/index.js +39 -5
- package/dist/adapters/html/index.js.map +1 -1
- package/dist/adapters/html/templates/base-layout.ejs +173 -0
- package/dist/adapters/html/templates/blog-index.html.ejs +170 -101
- package/dist/adapters/html/templates/blog-post-enhanced.ejs +655 -0
- package/dist/adapters/html/templates/blog-post-new.ejs +612 -0
- package/dist/adapters/html/templates/components/footer.ejs +330 -0
- package/dist/adapters/html/templates/components/header.ejs +208 -0
- package/dist/adapters/html/templates/components/social-share.ejs +202 -0
- package/dist/chunk-5N3D47CJ.js +823 -0
- package/dist/chunk-EPM74QNH.js +844 -0
- package/dist/chunk-OVFZRN7O.js +850 -0
- package/dist/chunk-TROAGFSZ.js +824 -0
- package/dist/chunk-U77FH5BI.js +823 -0
- package/dist/cli/commands/config.d.ts +17 -0
- package/dist/cli/commands/config.d.ts.map +1 -0
- package/dist/cli/commands/config.js +140 -0
- package/dist/cli/commands/config.js.map +1 -0
- package/dist/cli/commands/generate.d.ts.map +1 -1
- package/dist/cli/commands/generate.js +57 -14
- package/dist/cli/commands/generate.js.map +1 -1
- package/dist/cli/commands/init.d.ts.map +1 -1
- package/dist/cli/commands/init.js +114 -0
- package/dist/cli/commands/init.js.map +1 -1
- package/dist/cli/index.js +623 -288
- package/dist/cli/index.js.map +1 -1
- package/dist/core/generator.d.ts +4 -1
- package/dist/core/generator.d.ts.map +1 -1
- package/dist/core/generator.js +118 -3
- package/dist/core/generator.js.map +1 -1
- package/dist/index.d.ts +87 -3
- package/dist/index.js +1 -1
- package/dist/types/adapter.d.ts +17 -2
- package/dist/types/adapter.d.ts.map +1 -1
- package/dist/types/adapter.js.map +1 -1
- package/dist/types/content.d.ts +66 -0
- package/dist/types/content.d.ts.map +1 -1
- package/dist/ui/banner.d.ts.map +1 -1
- package/dist/ui/banner.js +18 -2
- package/dist/ui/banner.js.map +1 -1
- package/package.json +2 -2
package/dist/cli/index.js
CHANGED
|
@@ -1,16 +1,18 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
import {
|
|
3
3
|
ContentGenerator
|
|
4
|
-
} from "../chunk-
|
|
4
|
+
} from "../chunk-OVFZRN7O.js";
|
|
5
5
|
import "../chunk-INVECVSW.js";
|
|
6
6
|
|
|
7
7
|
// src/cli/index.ts
|
|
8
8
|
import { Command } from "commander";
|
|
9
|
-
import
|
|
9
|
+
import fs11 from "fs-extra";
|
|
10
|
+
import path9 from "path";
|
|
11
|
+
import { fileURLToPath as fileURLToPath2 } from "url";
|
|
10
12
|
|
|
11
13
|
// src/cli/commands/init.ts
|
|
12
|
-
import
|
|
13
|
-
import
|
|
14
|
+
import fs3 from "fs-extra";
|
|
15
|
+
import path3 from "path";
|
|
14
16
|
import chalk2 from "chalk";
|
|
15
17
|
import ora from "ora";
|
|
16
18
|
import inquirer from "inquirer";
|
|
@@ -21,6 +23,9 @@ import figlet from "figlet";
|
|
|
21
23
|
import gradient from "gradient-string";
|
|
22
24
|
import boxen from "boxen";
|
|
23
25
|
import chalk from "chalk";
|
|
26
|
+
import fs from "fs-extra";
|
|
27
|
+
import path from "path";
|
|
28
|
+
import { fileURLToPath } from "url";
|
|
24
29
|
function generateBanner() {
|
|
25
30
|
const asciiArt = figlet.textSync("edtools", {
|
|
26
31
|
font: "ANSI Shadow",
|
|
@@ -64,8 +69,24 @@ function showWelcome() {
|
|
|
64
69
|
}
|
|
65
70
|
function getVersion() {
|
|
66
71
|
try {
|
|
67
|
-
|
|
68
|
-
|
|
72
|
+
const __filename = fileURLToPath(import.meta.url);
|
|
73
|
+
const __dirname = path.dirname(__filename);
|
|
74
|
+
const possiblePaths = [
|
|
75
|
+
path.join(__dirname, "../../package.json"),
|
|
76
|
+
// From dist/ui/
|
|
77
|
+
path.join(__dirname, "../../../package.json"),
|
|
78
|
+
// From src/ui/
|
|
79
|
+
path.join(__dirname, "../../../../package.json")
|
|
80
|
+
// From npm global install
|
|
81
|
+
];
|
|
82
|
+
for (const pkgPath of possiblePaths) {
|
|
83
|
+
if (fs.existsSync(pkgPath)) {
|
|
84
|
+
const pkg = fs.readJsonSync(pkgPath);
|
|
85
|
+
return pkg.version || "unknown";
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
return "unknown";
|
|
89
|
+
} catch (error) {
|
|
69
90
|
return "dev";
|
|
70
91
|
}
|
|
71
92
|
}
|
|
@@ -105,8 +126,8 @@ function warningBox(message) {
|
|
|
105
126
|
}
|
|
106
127
|
|
|
107
128
|
// src/utils/hosting-detection.ts
|
|
108
|
-
import
|
|
109
|
-
import
|
|
129
|
+
import fs2 from "fs";
|
|
130
|
+
import path2 from "path";
|
|
110
131
|
import yaml from "yaml";
|
|
111
132
|
var detectors = [
|
|
112
133
|
{
|
|
@@ -182,9 +203,9 @@ var detectors = [
|
|
|
182
203
|
file: "next.config.js",
|
|
183
204
|
parse: (content, projectPath) => {
|
|
184
205
|
if (content.includes("output:") && content.includes("export")) {
|
|
185
|
-
const packageJsonPath =
|
|
186
|
-
if (
|
|
187
|
-
const packageJson = JSON.parse(
|
|
206
|
+
const packageJsonPath = path2.join(projectPath, "package.json");
|
|
207
|
+
if (fs2.existsSync(packageJsonPath)) {
|
|
208
|
+
const packageJson = JSON.parse(fs2.readFileSync(packageJsonPath, "utf-8"));
|
|
188
209
|
const buildScript = packageJson.scripts?.build || "";
|
|
189
210
|
if (buildScript.includes("out")) {
|
|
190
211
|
return {
|
|
@@ -206,10 +227,10 @@ var detectors = [
|
|
|
206
227
|
];
|
|
207
228
|
function detectHostingConfig(projectPath) {
|
|
208
229
|
for (const detector of detectors) {
|
|
209
|
-
const configPath =
|
|
210
|
-
if (
|
|
230
|
+
const configPath = path2.join(projectPath, detector.file);
|
|
231
|
+
if (fs2.existsSync(configPath)) {
|
|
211
232
|
try {
|
|
212
|
-
const content =
|
|
233
|
+
const content = fs2.readFileSync(configPath, "utf-8");
|
|
213
234
|
const result = detector.parse(content, projectPath);
|
|
214
235
|
if (result) {
|
|
215
236
|
return result;
|
|
@@ -255,9 +276,9 @@ function getSuggestedOutputDir(projectPath) {
|
|
|
255
276
|
// src/cli/commands/init.ts
|
|
256
277
|
async function initCommand(options) {
|
|
257
278
|
console.log(chalk2.cyan.bold("\n\u{1F680} Initializing Edtools...\n"));
|
|
258
|
-
const projectPath =
|
|
259
|
-
const configPath =
|
|
260
|
-
if (await
|
|
279
|
+
const projectPath = path3.resolve(options.path);
|
|
280
|
+
const configPath = path3.join(projectPath, "edtools.config.js");
|
|
281
|
+
if (await fs3.pathExists(configPath)) {
|
|
261
282
|
const { overwrite } = await inquirer.prompt([
|
|
262
283
|
{
|
|
263
284
|
type: "confirm",
|
|
@@ -274,9 +295,9 @@ async function initCommand(options) {
|
|
|
274
295
|
const spinner = ora("Analyzing your landing page...").start();
|
|
275
296
|
let productInfo = {};
|
|
276
297
|
try {
|
|
277
|
-
const indexPath =
|
|
278
|
-
if (await
|
|
279
|
-
const html = await
|
|
298
|
+
const indexPath = path3.join(projectPath, "index.html");
|
|
299
|
+
if (await fs3.pathExists(indexPath)) {
|
|
300
|
+
const html = await fs3.readFile(indexPath, "utf-8");
|
|
280
301
|
const $ = loadCheerio(html);
|
|
281
302
|
productInfo = {
|
|
282
303
|
name: $("title").text() || $("h1").first().text() || "My Product",
|
|
@@ -372,6 +393,111 @@ async function initCommand(options) {
|
|
|
372
393
|
default: "anthropic"
|
|
373
394
|
}
|
|
374
395
|
]);
|
|
396
|
+
console.log(chalk2.cyan("\n\u{1F3A8} Blog Branding (optional - press Enter to skip):\n"));
|
|
397
|
+
const blogBrandingAnswers = await inquirer.prompt([
|
|
398
|
+
{
|
|
399
|
+
type: "input",
|
|
400
|
+
name: "blogTitle",
|
|
401
|
+
message: "Blog title:",
|
|
402
|
+
default: `${answers.name} Blog`
|
|
403
|
+
},
|
|
404
|
+
{
|
|
405
|
+
type: "input",
|
|
406
|
+
name: "blogDescription",
|
|
407
|
+
message: "Blog description:",
|
|
408
|
+
default: `Learn about ${answers.name} and best practices`
|
|
409
|
+
},
|
|
410
|
+
{
|
|
411
|
+
type: "input",
|
|
412
|
+
name: "language",
|
|
413
|
+
message: "Blog language (en, es, fr, etc.):",
|
|
414
|
+
default: "en"
|
|
415
|
+
},
|
|
416
|
+
{
|
|
417
|
+
type: "input",
|
|
418
|
+
name: "logo",
|
|
419
|
+
message: "Logo URL (optional):",
|
|
420
|
+
default: ""
|
|
421
|
+
},
|
|
422
|
+
{
|
|
423
|
+
type: "input",
|
|
424
|
+
name: "primaryColor",
|
|
425
|
+
message: "Primary color (hex):",
|
|
426
|
+
default: "#2563eb",
|
|
427
|
+
validate: (input) => {
|
|
428
|
+
if (!input) return true;
|
|
429
|
+
return /^#[0-9A-F]{6}$/i.test(input) || "Please enter a valid hex color (e.g., #2563eb)";
|
|
430
|
+
}
|
|
431
|
+
}
|
|
432
|
+
]);
|
|
433
|
+
const { addNavigation } = await inquirer.prompt([
|
|
434
|
+
{
|
|
435
|
+
type: "confirm",
|
|
436
|
+
name: "addNavigation",
|
|
437
|
+
message: "Add navigation links?",
|
|
438
|
+
default: false
|
|
439
|
+
}
|
|
440
|
+
]);
|
|
441
|
+
let navigation = [];
|
|
442
|
+
if (addNavigation) {
|
|
443
|
+
let addMore = true;
|
|
444
|
+
while (addMore) {
|
|
445
|
+
const navItem = await inquirer.prompt([
|
|
446
|
+
{
|
|
447
|
+
type: "input",
|
|
448
|
+
name: "label",
|
|
449
|
+
message: "Navigation link label:",
|
|
450
|
+
validate: (input) => input.length > 0 || "Label is required"
|
|
451
|
+
},
|
|
452
|
+
{
|
|
453
|
+
type: "input",
|
|
454
|
+
name: "url",
|
|
455
|
+
message: "Navigation link URL:",
|
|
456
|
+
validate: (input) => input.length > 0 || "URL is required"
|
|
457
|
+
}
|
|
458
|
+
]);
|
|
459
|
+
navigation.push(navItem);
|
|
460
|
+
const { continueAdding } = await inquirer.prompt([
|
|
461
|
+
{
|
|
462
|
+
type: "confirm",
|
|
463
|
+
name: "continueAdding",
|
|
464
|
+
message: "Add another navigation link?",
|
|
465
|
+
default: false
|
|
466
|
+
}
|
|
467
|
+
]);
|
|
468
|
+
addMore = continueAdding;
|
|
469
|
+
}
|
|
470
|
+
}
|
|
471
|
+
console.log(chalk2.cyan("\n\u{1F4F1} Social Media (optional):\n"));
|
|
472
|
+
const socialAnswers = await inquirer.prompt([
|
|
473
|
+
{
|
|
474
|
+
type: "input",
|
|
475
|
+
name: "twitter",
|
|
476
|
+
message: "Twitter/X URL:",
|
|
477
|
+
default: ""
|
|
478
|
+
},
|
|
479
|
+
{
|
|
480
|
+
type: "input",
|
|
481
|
+
name: "linkedin",
|
|
482
|
+
message: "LinkedIn URL:",
|
|
483
|
+
default: ""
|
|
484
|
+
},
|
|
485
|
+
{
|
|
486
|
+
type: "input",
|
|
487
|
+
name: "github",
|
|
488
|
+
message: "GitHub URL:",
|
|
489
|
+
default: ""
|
|
490
|
+
},
|
|
491
|
+
{
|
|
492
|
+
type: "input",
|
|
493
|
+
name: "youtube",
|
|
494
|
+
message: "YouTube URL:",
|
|
495
|
+
default: ""
|
|
496
|
+
}
|
|
497
|
+
]);
|
|
498
|
+
const social = Object.fromEntries(
|
|
499
|
+
Object.entries(socialAnswers).filter(([_, value]) => value !== "")
|
|
500
|
+
);
|
|
375
501
|
console.log(chalk2.cyan("\n\u{1F511} API Key Setup:\n"));
|
|
376
502
|
const apiKeyAnswer = await inquirer.prompt([
|
|
377
503
|
{
|
|
@@ -417,6 +543,16 @@ async function initCommand(options) {
|
|
|
417
543
|
preferredProvider: ${JSON.stringify(finalProductInfo.preferredProvider)},
|
|
418
544
|
},
|
|
419
545
|
|
|
546
|
+
blog: {
|
|
547
|
+
title: ${JSON.stringify(blogBrandingAnswers.blogTitle)},
|
|
548
|
+
description: ${JSON.stringify(blogBrandingAnswers.blogDescription)},
|
|
549
|
+
language: ${JSON.stringify(blogBrandingAnswers.language)},
|
|
550
|
+
logo: ${JSON.stringify(blogBrandingAnswers.logo)},
|
|
551
|
+
primaryColor: ${JSON.stringify(blogBrandingAnswers.primaryColor)},
|
|
552
|
+
navigation: ${JSON.stringify(navigation, null, 2)},
|
|
553
|
+
social: ${JSON.stringify(social, null, 2)},
|
|
554
|
+
},
|
|
555
|
+
|
|
420
556
|
content: {
|
|
421
557
|
outputDir: ${JSON.stringify(suggestedOutputDir)},
|
|
422
558
|
generateBlog: true,
|
|
@@ -429,17 +565,17 @@ async function initCommand(options) {
|
|
|
429
565
|
},
|
|
430
566
|
};
|
|
431
567
|
`;
|
|
432
|
-
await
|
|
433
|
-
const edtoolsDir =
|
|
434
|
-
await
|
|
568
|
+
await fs3.writeFile(configPath, config, "utf-8");
|
|
569
|
+
const edtoolsDir = path3.join(projectPath, ".edtools");
|
|
570
|
+
await fs3.ensureDir(edtoolsDir);
|
|
435
571
|
const edtoolsConfig = {
|
|
436
572
|
apiKey: apiKeyAnswer.apiKey,
|
|
437
573
|
provider: finalProductInfo.preferredProvider
|
|
438
574
|
};
|
|
439
|
-
const edtoolsConfigPath =
|
|
440
|
-
await
|
|
441
|
-
const gitignorePath =
|
|
442
|
-
await
|
|
575
|
+
const edtoolsConfigPath = path3.join(edtoolsDir, "config.json");
|
|
576
|
+
await fs3.writeFile(edtoolsConfigPath, JSON.stringify(edtoolsConfig, null, 2), "utf-8");
|
|
577
|
+
const gitignorePath = path3.join(edtoolsDir, ".gitignore");
|
|
578
|
+
await fs3.writeFile(gitignorePath, "*\n!.gitignore\n", "utf-8");
|
|
443
579
|
console.log("");
|
|
444
580
|
console.log(successBox("Configuration created successfully!"));
|
|
445
581
|
const filesCreated = `${chalk2.cyan("Files created:")}
|
|
@@ -459,88 +595,267 @@ ${chalk2.gray("API key stored securely (gitignored)")}`;
|
|
|
459
595
|
}
|
|
460
596
|
|
|
461
597
|
// src/cli/commands/generate.ts
|
|
462
|
-
import
|
|
463
|
-
import
|
|
598
|
+
import fs5 from "fs-extra";
|
|
599
|
+
import path5 from "path";
|
|
464
600
|
import { pathToFileURL } from "url";
|
|
465
|
-
import
|
|
601
|
+
import chalk4 from "chalk";
|
|
466
602
|
import ora2 from "ora";
|
|
467
603
|
import Table from "cli-table3";
|
|
604
|
+
import inquirer3 from "inquirer";
|
|
605
|
+
|
|
606
|
+
// src/cli/commands/config.ts
|
|
607
|
+
import fs4 from "fs-extra";
|
|
608
|
+
import path4 from "path";
|
|
609
|
+
import chalk3 from "chalk";
|
|
468
610
|
import inquirer2 from "inquirer";
|
|
611
|
+
async function promptForApiKey(projectPath, providedKey, providedProvider) {
|
|
612
|
+
const edtoolsDir = path4.join(projectPath, ".edtools");
|
|
613
|
+
const configPath = path4.join(edtoolsDir, "config.json");
|
|
614
|
+
await fs4.ensureDir(edtoolsDir);
|
|
615
|
+
let existingConfig = {};
|
|
616
|
+
if (await fs4.pathExists(configPath)) {
|
|
617
|
+
try {
|
|
618
|
+
existingConfig = await fs4.readJson(configPath);
|
|
619
|
+
} catch (error) {
|
|
620
|
+
console.log(chalk3.yellow("\u26A0\uFE0F Existing config is corrupted, will create new one"));
|
|
621
|
+
}
|
|
622
|
+
}
|
|
623
|
+
let apiKey = providedKey;
|
|
624
|
+
let provider = providedProvider || existingConfig.provider || "anthropic";
|
|
625
|
+
if (!apiKey) {
|
|
626
|
+
console.log(chalk3.cyan("\n\u{1F511} API Key Setup:\n"));
|
|
627
|
+
const providerAnswer = await inquirer2.prompt([
|
|
628
|
+
{
|
|
629
|
+
type: "list",
|
|
630
|
+
name: "provider",
|
|
631
|
+
message: "AI provider:",
|
|
632
|
+
choices: [
|
|
633
|
+
{ name: "Claude (Anthropic) - Recommended", value: "anthropic" },
|
|
634
|
+
{ name: "ChatGPT (OpenAI)", value: "openai" }
|
|
635
|
+
],
|
|
636
|
+
default: provider
|
|
637
|
+
}
|
|
638
|
+
]);
|
|
639
|
+
provider = providerAnswer.provider;
|
|
640
|
+
const keyAnswer = await inquirer2.prompt([
|
|
641
|
+
{
|
|
642
|
+
type: "password",
|
|
643
|
+
name: "apiKey",
|
|
644
|
+
message: provider === "anthropic" ? "Anthropic API Key (from https://console.anthropic.com):" : "OpenAI API Key (from https://platform.openai.com/api-keys):",
|
|
645
|
+
validate: (input) => {
|
|
646
|
+
if (!input || input.trim().length === 0) {
|
|
647
|
+
return "API key is required";
|
|
648
|
+
}
|
|
649
|
+
if (provider === "anthropic" && !input.startsWith("sk-ant-")) {
|
|
650
|
+
return 'Anthropic API keys should start with "sk-ant-"';
|
|
651
|
+
}
|
|
652
|
+
if (provider === "openai" && !input.startsWith("sk-")) {
|
|
653
|
+
return 'OpenAI API keys should start with "sk-"';
|
|
654
|
+
}
|
|
655
|
+
return true;
|
|
656
|
+
}
|
|
657
|
+
}
|
|
658
|
+
]);
|
|
659
|
+
apiKey = keyAnswer.apiKey;
|
|
660
|
+
}
|
|
661
|
+
if (provider === "anthropic" && !apiKey.startsWith("sk-ant-")) {
|
|
662
|
+
console.log(errorBox('Invalid API Key: Anthropic API keys should start with "sk-ant-"'));
|
|
663
|
+
return null;
|
|
664
|
+
}
|
|
665
|
+
if (provider === "openai" && !apiKey.startsWith("sk-")) {
|
|
666
|
+
console.log(errorBox('Invalid API Key: OpenAI API keys should start with "sk-"'));
|
|
667
|
+
return null;
|
|
668
|
+
}
|
|
669
|
+
const config = {
|
|
670
|
+
...existingConfig,
|
|
671
|
+
apiKey,
|
|
672
|
+
provider
|
|
673
|
+
};
|
|
674
|
+
await fs4.writeFile(configPath, JSON.stringify(config, null, 2), "utf-8");
|
|
675
|
+
const gitignorePath = path4.join(edtoolsDir, ".gitignore");
|
|
676
|
+
if (!await fs4.pathExists(gitignorePath)) {
|
|
677
|
+
await fs4.writeFile(gitignorePath, "*\n!.gitignore\n", "utf-8");
|
|
678
|
+
}
|
|
679
|
+
console.log("");
|
|
680
|
+
console.log(successBox(`API Key configured successfully for ${provider}!`));
|
|
681
|
+
console.log(chalk3.gray(`Stored in: ${chalk3.white(".edtools/config.json")} (gitignored)
|
|
682
|
+
`));
|
|
683
|
+
return { apiKey, provider };
|
|
684
|
+
}
|
|
685
|
+
async function configSetApiKeyCommand(options) {
|
|
686
|
+
const projectPath = path4.resolve(options.path);
|
|
687
|
+
const result = await promptForApiKey(projectPath, options.key, options.provider);
|
|
688
|
+
if (!result) {
|
|
689
|
+
console.log(chalk3.red("\n\u2717 API key setup cancelled or failed\n"));
|
|
690
|
+
process.exit(1);
|
|
691
|
+
}
|
|
692
|
+
}
|
|
693
|
+
async function configShowCommand(options) {
|
|
694
|
+
const projectPath = path4.resolve(options.path);
|
|
695
|
+
const edtoolsConfigPath = path4.join(projectPath, ".edtools", "config.json");
|
|
696
|
+
const mainConfigPath = path4.join(projectPath, "edtools.config.js");
|
|
697
|
+
console.log(chalk3.cyan.bold("\n\u{1F4CB} Edtools Configuration:\n"));
|
|
698
|
+
if (await fs4.pathExists(mainConfigPath)) {
|
|
699
|
+
console.log(chalk3.green("\u2713") + " edtools.config.js found");
|
|
700
|
+
console.log(chalk3.gray(` Location: ${mainConfigPath}
|
|
701
|
+
`));
|
|
702
|
+
} else {
|
|
703
|
+
console.log(chalk3.red("\u2717") + " edtools.config.js not found");
|
|
704
|
+
console.log(chalk3.gray(` Run: ${chalk3.white("edtools init")} to create it
|
|
705
|
+
`));
|
|
706
|
+
}
|
|
707
|
+
if (await fs4.pathExists(edtoolsConfigPath)) {
|
|
708
|
+
try {
|
|
709
|
+
const config = await fs4.readJson(edtoolsConfigPath);
|
|
710
|
+
const hasApiKey = config.apiKey && config.apiKey.length > 0;
|
|
711
|
+
const provider = config.provider || "anthropic";
|
|
712
|
+
console.log(chalk3.green("\u2713") + " API key configured");
|
|
713
|
+
console.log(chalk3.gray(` Provider: ${chalk3.white(provider)}`));
|
|
714
|
+
console.log(chalk3.gray(` Key: ${chalk3.white(hasApiKey ? config.apiKey.substring(0, 10) + "..." : "Not set")}`));
|
|
715
|
+
console.log(chalk3.gray(` Location: ${edtoolsConfigPath}
|
|
716
|
+
`));
|
|
717
|
+
} catch (error) {
|
|
718
|
+
console.log(chalk3.red("\u2717") + " API key config is corrupted");
|
|
719
|
+
console.log(chalk3.gray(` Run: ${chalk3.white("edtools config set-api-key")} to fix it
|
|
720
|
+
`));
|
|
721
|
+
}
|
|
722
|
+
} else {
|
|
723
|
+
console.log(chalk3.red("\u2717") + " API key not configured");
|
|
724
|
+
console.log(chalk3.gray(` Run: ${chalk3.white("edtools config set-api-key")} or ${chalk3.white("edtools init")}
|
|
725
|
+
`));
|
|
726
|
+
}
|
|
727
|
+
const anthropicEnv = process.env.ANTHROPIC_API_KEY;
|
|
728
|
+
const openaiEnv = process.env.OPENAI_API_KEY;
|
|
729
|
+
if (anthropicEnv || openaiEnv) {
|
|
730
|
+
console.log(chalk3.cyan("Environment variables:"));
|
|
731
|
+
if (anthropicEnv) {
|
|
732
|
+
console.log(chalk3.gray(` ANTHROPIC_API_KEY: ${chalk3.white(anthropicEnv.substring(0, 10) + "...")}`));
|
|
733
|
+
}
|
|
734
|
+
if (openaiEnv) {
|
|
735
|
+
console.log(chalk3.gray(` OPENAI_API_KEY: ${chalk3.white(openaiEnv.substring(0, 10) + "...")}`));
|
|
736
|
+
}
|
|
737
|
+
console.log("");
|
|
738
|
+
}
|
|
739
|
+
console.log(chalk3.cyan("\u{1F4A1} Tip:"));
|
|
740
|
+
console.log(chalk3.gray(" API keys are searched in this order:"));
|
|
741
|
+
console.log(chalk3.gray(" 1. --api-key flag"));
|
|
742
|
+
console.log(chalk3.gray(" 2. .edtools/config.json"));
|
|
743
|
+
console.log(chalk3.gray(" 3. Environment variables\n"));
|
|
744
|
+
}
|
|
745
|
+
|
|
746
|
+
// src/cli/commands/generate.ts
|
|
469
747
|
async function generateCommand(options) {
|
|
470
|
-
console.log(
|
|
748
|
+
console.log(chalk4.cyan.bold("\n\u{1F4DD} Generating content...\n"));
|
|
471
749
|
const projectPath = process.cwd();
|
|
472
|
-
const configPath =
|
|
473
|
-
if (!await
|
|
474
|
-
console.log(
|
|
475
|
-
console.log(
|
|
750
|
+
const configPath = path5.join(projectPath, "edtools.config.js");
|
|
751
|
+
if (!await fs5.pathExists(configPath)) {
|
|
752
|
+
console.log(chalk4.red("\u2717 No edtools.config.js found"));
|
|
753
|
+
console.log(chalk4.yellow(' Run "edtools init" first\n'));
|
|
476
754
|
process.exit(1);
|
|
477
755
|
}
|
|
478
756
|
const configUrl = pathToFileURL(configPath).href;
|
|
479
757
|
const config = await import(configUrl);
|
|
480
758
|
const productInfo = config.default.product;
|
|
481
759
|
if (!productInfo || !productInfo.name) {
|
|
482
|
-
console.log(
|
|
760
|
+
console.log(chalk4.red("\u2717 Invalid configuration in edtools.config.js"));
|
|
483
761
|
process.exit(1);
|
|
484
762
|
}
|
|
485
763
|
const provider = productInfo.preferredProvider || "anthropic";
|
|
486
764
|
let storedApiKey;
|
|
487
|
-
const edtoolsConfigPath =
|
|
488
|
-
if (await
|
|
765
|
+
const edtoolsConfigPath = path5.join(projectPath, ".edtools", "config.json");
|
|
766
|
+
if (await fs5.pathExists(edtoolsConfigPath)) {
|
|
489
767
|
try {
|
|
490
|
-
const edtoolsConfig = await
|
|
768
|
+
const edtoolsConfig = await fs5.readJson(edtoolsConfigPath);
|
|
491
769
|
storedApiKey = edtoolsConfig.apiKey;
|
|
492
770
|
} catch (error) {
|
|
493
771
|
}
|
|
494
772
|
}
|
|
495
773
|
let apiKey;
|
|
774
|
+
let actualProvider = provider;
|
|
496
775
|
if (provider === "anthropic") {
|
|
497
776
|
apiKey = options.apiKey || process.env.ANTHROPIC_API_KEY || storedApiKey;
|
|
498
777
|
if (!apiKey) {
|
|
499
|
-
console.log(
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
778
|
+
console.log(chalk4.yellow("\n\u26A0\uFE0F No Anthropic API key configured\n"));
|
|
779
|
+
const { shouldConfigure } = await inquirer3.prompt([{
|
|
780
|
+
type: "confirm",
|
|
781
|
+
name: "shouldConfigure",
|
|
782
|
+
message: "Would you like to configure it now?",
|
|
783
|
+
default: true
|
|
784
|
+
}]);
|
|
785
|
+
if (shouldConfigure) {
|
|
786
|
+
const result = await promptForApiKey(projectPath);
|
|
787
|
+
if (result) {
|
|
788
|
+
apiKey = result.apiKey;
|
|
789
|
+
actualProvider = result.provider;
|
|
790
|
+
} else {
|
|
791
|
+
console.log(chalk4.red("\n\u2717 API key configuration failed\n"));
|
|
792
|
+
process.exit(1);
|
|
793
|
+
}
|
|
794
|
+
} else {
|
|
795
|
+
console.log(chalk4.cyan("\n\u{1F4A1} Quick fix options:\n"));
|
|
796
|
+
console.log(chalk4.gray(" 1. Run: ") + chalk4.white("edtools config set-api-key"));
|
|
797
|
+
console.log(chalk4.gray(" 2. Set env: ") + chalk4.white("export ANTHROPIC_API_KEY=sk-ant-..."));
|
|
798
|
+
console.log(chalk4.gray(" 3. Use flag: ") + chalk4.white("--api-key sk-ant-...\n"));
|
|
799
|
+
process.exit(1);
|
|
800
|
+
}
|
|
504
801
|
}
|
|
505
802
|
} else if (provider === "openai") {
|
|
506
803
|
apiKey = options.apiKey || process.env.OPENAI_API_KEY || storedApiKey;
|
|
507
804
|
if (!apiKey) {
|
|
508
|
-
console.log(
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
805
|
+
console.log(chalk4.yellow("\n\u26A0\uFE0F No OpenAI API key configured\n"));
|
|
806
|
+
const { shouldConfigure } = await inquirer3.prompt([{
|
|
807
|
+
type: "confirm",
|
|
808
|
+
name: "shouldConfigure",
|
|
809
|
+
message: "Would you like to configure it now?",
|
|
810
|
+
default: true
|
|
811
|
+
}]);
|
|
812
|
+
if (shouldConfigure) {
|
|
813
|
+
const result = await promptForApiKey(projectPath);
|
|
814
|
+
if (result) {
|
|
815
|
+
apiKey = result.apiKey;
|
|
816
|
+
actualProvider = result.provider;
|
|
817
|
+
} else {
|
|
818
|
+
console.log(chalk4.red("\n\u2717 API key configuration failed\n"));
|
|
819
|
+
process.exit(1);
|
|
820
|
+
}
|
|
821
|
+
} else {
|
|
822
|
+
console.log(chalk4.cyan("\n\u{1F4A1} Quick fix options:\n"));
|
|
823
|
+
console.log(chalk4.gray(" 1. Run: ") + chalk4.white("edtools config set-api-key"));
|
|
824
|
+
console.log(chalk4.gray(" 2. Set env: ") + chalk4.white("export OPENAI_API_KEY=sk-..."));
|
|
825
|
+
console.log(chalk4.gray(" 3. Use flag: ") + chalk4.white("--api-key sk-...\n"));
|
|
826
|
+
process.exit(1);
|
|
827
|
+
}
|
|
513
828
|
}
|
|
514
829
|
}
|
|
515
830
|
let topics = options.topics;
|
|
516
831
|
if (options.fromCsv) {
|
|
517
|
-
const opportunitiesPath =
|
|
518
|
-
if (!await
|
|
519
|
-
console.log(
|
|
520
|
-
console.log(
|
|
832
|
+
const opportunitiesPath = path5.join(projectPath, ".edtools", "opportunities.json");
|
|
833
|
+
if (!await fs5.pathExists(opportunitiesPath)) {
|
|
834
|
+
console.log(chalk4.red("\u2717 No CSV analysis found"));
|
|
835
|
+
console.log(chalk4.yellow(' Run "edtools analyze" first to analyze your GSC data\n'));
|
|
521
836
|
process.exit(1);
|
|
522
837
|
}
|
|
523
838
|
try {
|
|
524
|
-
const oppData = await
|
|
839
|
+
const oppData = await fs5.readJson(opportunitiesPath);
|
|
525
840
|
topics = oppData.opportunities.slice(0, parseInt(options.posts, 10)).map((opp) => opp.suggestedTitle);
|
|
526
|
-
console.log(
|
|
841
|
+
console.log(chalk4.cyan("\u{1F4CA} Using topics from CSV analysis:\n"));
|
|
527
842
|
topics.forEach((topic, i) => {
|
|
528
|
-
console.log(` ${
|
|
843
|
+
console.log(` ${chalk4.cyan((i + 1).toString() + ".")} ${chalk4.white(topic)}`);
|
|
529
844
|
});
|
|
530
845
|
console.log("");
|
|
531
846
|
} catch (error) {
|
|
532
|
-
console.log(
|
|
847
|
+
console.log(chalk4.red("\u2717 Failed to load CSV analysis"));
|
|
533
848
|
process.exit(1);
|
|
534
849
|
}
|
|
535
850
|
}
|
|
536
851
|
const count = parseInt(options.posts, 10);
|
|
537
852
|
if (isNaN(count) || count < 1 || count > 10) {
|
|
538
|
-
console.log(
|
|
853
|
+
console.log(chalk4.red("\u2717 Invalid number of posts (must be 1-10)"));
|
|
539
854
|
process.exit(1);
|
|
540
855
|
}
|
|
541
856
|
if (count > 5) {
|
|
542
|
-
console.log(
|
|
543
|
-
console.log(
|
|
857
|
+
console.log(chalk4.yellow(`\u26A0\uFE0F Generating ${count} posts at once may trigger spam detection`));
|
|
858
|
+
console.log(chalk4.yellow(" Recommended: 3-5 posts per week\n"));
|
|
544
859
|
}
|
|
545
860
|
let outputDirConfig;
|
|
546
861
|
let outputDirSource;
|
|
@@ -554,48 +869,48 @@ async function generateCommand(options) {
|
|
|
554
869
|
outputDirConfig = "./blog";
|
|
555
870
|
outputDirSource = "default";
|
|
556
871
|
}
|
|
557
|
-
const outputDir =
|
|
558
|
-
await
|
|
872
|
+
const outputDir = path5.resolve(projectPath, outputDirConfig);
|
|
873
|
+
await fs5.ensureDir(outputDir);
|
|
559
874
|
const hostingConfig = detectHostingConfig(projectPath);
|
|
560
875
|
if (hostingConfig) {
|
|
561
876
|
const validation = validateOutputDir(outputDirConfig, hostingConfig);
|
|
562
877
|
if (!validation.valid) {
|
|
563
|
-
console.log(
|
|
564
|
-
console.log(
|
|
565
|
-
console.log(
|
|
566
|
-
console.log(
|
|
878
|
+
console.log(chalk4.yellow.bold("\n\u26A0\uFE0F HOSTING PLATFORM DETECTED: ") + chalk4.white(validation.platform));
|
|
879
|
+
console.log(chalk4.yellow(" - Public directory: ") + chalk4.white(validation.publicDir));
|
|
880
|
+
console.log(chalk4.yellow(" - Your outputDir: ") + chalk4.white(validation.currentOutputDir));
|
|
881
|
+
console.log(chalk4.yellow(" - Problem: Files outside ") + chalk4.white(validation.publicDir + "/") + chalk4.yellow(" won't be accessible"));
|
|
567
882
|
console.log("");
|
|
568
|
-
console.log(
|
|
569
|
-
console.log(
|
|
883
|
+
console.log(chalk4.red.bold("\u274C ISSUE: Output directory misconfiguration"));
|
|
884
|
+
console.log(chalk4.red(" Generated files will return 404 in production"));
|
|
570
885
|
console.log("");
|
|
571
|
-
console.log(
|
|
572
|
-
console.log(
|
|
886
|
+
console.log(chalk4.cyan.bold("\u{1F4DD} Suggested fix:"));
|
|
887
|
+
console.log(chalk4.cyan(" Update edtools.config.js:"));
|
|
573
888
|
console.log("");
|
|
574
|
-
console.log(
|
|
575
|
-
console.log(
|
|
576
|
-
console.log(
|
|
889
|
+
console.log(chalk4.gray(" content: {"));
|
|
890
|
+
console.log(chalk4.green(` outputDir: '${validation.suggestion}'`) + chalk4.gray(" // \u2705 Correct path"));
|
|
891
|
+
console.log(chalk4.gray(" }"));
|
|
577
892
|
console.log("");
|
|
578
|
-
const answer = await
|
|
893
|
+
const answer = await inquirer3.prompt([{
|
|
579
894
|
type: "confirm",
|
|
580
895
|
name: "continue",
|
|
581
896
|
message: "Continue anyway?",
|
|
582
897
|
default: false
|
|
583
898
|
}]);
|
|
584
899
|
if (!answer.continue) {
|
|
585
|
-
console.log(
|
|
900
|
+
console.log(chalk4.yellow("\n\u2717 Generation cancelled\n"));
|
|
586
901
|
process.exit(0);
|
|
587
902
|
}
|
|
588
903
|
console.log("");
|
|
589
904
|
}
|
|
590
905
|
}
|
|
591
|
-
console.log(
|
|
592
|
-
console.log(` Product: ${
|
|
593
|
-
console.log(` Category: ${
|
|
594
|
-
console.log(` AI Provider: ${
|
|
595
|
-
console.log(` Posts to generate: ${
|
|
596
|
-
console.log(` Output directory: ${
|
|
906
|
+
console.log(chalk4.cyan("Configuration:"));
|
|
907
|
+
console.log(` Product: ${chalk4.white(productInfo.name)}`);
|
|
908
|
+
console.log(` Category: ${chalk4.white(productInfo.category)}`);
|
|
909
|
+
console.log(` AI Provider: ${chalk4.white(actualProvider === "anthropic" ? "Claude (Anthropic)" : "ChatGPT (OpenAI)")}`);
|
|
910
|
+
console.log(` Posts to generate: ${chalk4.white(count)}`);
|
|
911
|
+
console.log(` Output directory: ${chalk4.white(outputDir)} ${chalk4.gray("(" + outputDirSource + ")")}
|
|
597
912
|
`);
|
|
598
|
-
const generator = new ContentGenerator(apiKey,
|
|
913
|
+
const generator = new ContentGenerator(apiKey, actualProvider);
|
|
599
914
|
const generateConfig = {
|
|
600
915
|
productInfo,
|
|
601
916
|
topics,
|
|
@@ -604,20 +919,22 @@ async function generateCommand(options) {
|
|
|
604
919
|
projectPath,
|
|
605
920
|
avoidDuplicates: true,
|
|
606
921
|
similarityThreshold: 0.85,
|
|
607
|
-
provider,
|
|
922
|
+
provider: actualProvider,
|
|
608
923
|
apiKey,
|
|
609
|
-
dryRun: options.dryRun
|
|
924
|
+
dryRun: options.dryRun,
|
|
925
|
+
blogConfig: config.default.blog
|
|
926
|
+
// Pass blog config from edtools.config.js
|
|
610
927
|
};
|
|
611
|
-
const providerName =
|
|
928
|
+
const providerName = actualProvider === "anthropic" ? "Claude" : "ChatGPT";
|
|
612
929
|
const spinner = ora2(`Generating content with ${providerName}...`).start();
|
|
613
930
|
try {
|
|
614
931
|
const result = await generator.generate(generateConfig);
|
|
615
932
|
if (result.success && result.posts.length > 0) {
|
|
616
933
|
const dryRunMode = result.dryRun || false;
|
|
617
|
-
spinner.succeed(
|
|
934
|
+
spinner.succeed(chalk4.bold(dryRunMode ? "Preview generated successfully!" : "Content generated successfully!"));
|
|
618
935
|
console.log("");
|
|
619
936
|
if (dryRunMode) {
|
|
620
|
-
console.log(
|
|
937
|
+
console.log(chalk4.yellow.bold("\u{1F50D} DRY RUN MODE - No files were written\n"));
|
|
621
938
|
}
|
|
622
939
|
if (options.report === "json" || options.report === "pretty") {
|
|
623
940
|
const report = {
|
|
@@ -656,11 +973,11 @@ async function generateCommand(options) {
|
|
|
656
973
|
} else {
|
|
657
974
|
const table = new Table({
|
|
658
975
|
head: [
|
|
659
|
-
|
|
660
|
-
|
|
661
|
-
|
|
662
|
-
|
|
663
|
-
dryRunMode ?
|
|
976
|
+
chalk4.cyan.bold("#"),
|
|
977
|
+
chalk4.cyan.bold("Title"),
|
|
978
|
+
chalk4.cyan.bold("Words"),
|
|
979
|
+
chalk4.cyan.bold("SEO"),
|
|
980
|
+
dryRunMode ? chalk4.cyan.bold("Would Create") : chalk4.cyan.bold("Path")
|
|
664
981
|
],
|
|
665
982
|
colWidths: [5, 35, 10, 10, 45],
|
|
666
983
|
wordWrap: true,
|
|
@@ -672,29 +989,29 @@ async function generateCommand(options) {
|
|
|
672
989
|
result.posts.forEach((post, i) => {
|
|
673
990
|
const scoreColor = getScoreColorFn(post.seoScore);
|
|
674
991
|
table.push([
|
|
675
|
-
|
|
676
|
-
|
|
677
|
-
|
|
992
|
+
chalk4.gray((i + 1).toString()),
|
|
993
|
+
chalk4.white(truncateTitle(post.title, 33)),
|
|
994
|
+
chalk4.white(post.wordCount.toString()),
|
|
678
995
|
scoreColor(`${post.seoScore}/100`),
|
|
679
|
-
|
|
996
|
+
chalk4.gray(post.path)
|
|
680
997
|
]);
|
|
681
998
|
});
|
|
682
999
|
console.log(table.toString());
|
|
683
1000
|
console.log("");
|
|
684
1001
|
if (result.stats) {
|
|
685
|
-
console.log(
|
|
686
|
-
console.log(` Total words: ${
|
|
1002
|
+
console.log(chalk4.cyan.bold("Statistics:\n"));
|
|
1003
|
+
console.log(` Total words: ${chalk4.white(result.stats.totalWords)}`);
|
|
687
1004
|
console.log(` Avg SEO score: ${getScoreColorFn(result.stats.avgSeoScore)(result.stats.avgSeoScore + "/100")}`);
|
|
688
|
-
console.log(` Avg read time: ${
|
|
1005
|
+
console.log(` Avg read time: ${chalk4.white(result.stats.avgReadTime)}`);
|
|
689
1006
|
console.log("");
|
|
690
1007
|
}
|
|
691
1008
|
if (dryRunMode) {
|
|
692
|
-
console.log(
|
|
1009
|
+
console.log(chalk4.yellow.bold("Would create:\n"));
|
|
693
1010
|
if (result.manifestPath) {
|
|
694
|
-
console.log(` ${
|
|
1011
|
+
console.log(` ${chalk4.yellow("\u2022")} ${chalk4.white(result.manifestPath)} (manifest with ${result.posts.length} posts)`);
|
|
695
1012
|
}
|
|
696
1013
|
if (result.sitemapPath) {
|
|
697
|
-
console.log(` ${
|
|
1014
|
+
console.log(` ${chalk4.yellow("\u2022")} ${chalk4.white(result.sitemapPath)} (sitemap with ${result.posts.length} URLs)`);
|
|
698
1015
|
}
|
|
699
1016
|
console.log("");
|
|
700
1017
|
}
|
|
@@ -706,21 +1023,21 @@ async function generateCommand(options) {
|
|
|
706
1023
|
console.log(warningBox(warningText));
|
|
707
1024
|
}
|
|
708
1025
|
if (dryRunMode) {
|
|
709
|
-
console.log(
|
|
710
|
-
console.log(` ${
|
|
1026
|
+
console.log(chalk4.cyan.bold("Next step:"));
|
|
1027
|
+
console.log(` ${chalk4.cyan("\u2022")} Run without ${chalk4.white("--dry-run")} to generate files
|
|
711
1028
|
`);
|
|
712
1029
|
} else {
|
|
713
|
-
console.log(
|
|
714
|
-
console.log(` ${
|
|
715
|
-
console.log(` ${
|
|
716
|
-
console.log(` ${
|
|
1030
|
+
console.log(chalk4.cyan.bold("Next steps:"));
|
|
1031
|
+
console.log(` ${chalk4.cyan("1.")} Review generated content in ${chalk4.white(outputDir)}`);
|
|
1032
|
+
console.log(` ${chalk4.cyan("2.")} Edit posts to add personal experience/expertise`);
|
|
1033
|
+
console.log(` ${chalk4.cyan("3.")} Deploy to your website`);
|
|
717
1034
|
console.log("");
|
|
718
|
-
console.log(
|
|
1035
|
+
console.log(chalk4.yellow(`\u{1F4A1} Tip: Wait 3-7 days before generating more posts to avoid spam detection
|
|
719
1036
|
`));
|
|
720
1037
|
}
|
|
721
1038
|
}
|
|
722
1039
|
} else {
|
|
723
|
-
spinner.fail(
|
|
1040
|
+
spinner.fail(chalk4.bold("Failed to generate content"));
|
|
724
1041
|
if (result.errors && result.errors.length > 0) {
|
|
725
1042
|
const errorText = result.errors.join("\n");
|
|
726
1043
|
console.log(errorBox(errorText));
|
|
@@ -728,16 +1045,16 @@ async function generateCommand(options) {
|
|
|
728
1045
|
process.exit(1);
|
|
729
1046
|
}
|
|
730
1047
|
} catch (error) {
|
|
731
|
-
spinner.fail(
|
|
1048
|
+
spinner.fail(chalk4.bold("Error during generation"));
|
|
732
1049
|
console.log(errorBox(error.message));
|
|
733
1050
|
process.exit(1);
|
|
734
1051
|
}
|
|
735
1052
|
}
|
|
736
1053
|
function getScoreColorFn(score) {
|
|
737
|
-
if (score >= 90) return
|
|
738
|
-
if (score >= 75) return
|
|
739
|
-
if (score >= 60) return
|
|
740
|
-
return
|
|
1054
|
+
if (score >= 90) return chalk4.green;
|
|
1055
|
+
if (score >= 75) return chalk4.yellow;
|
|
1056
|
+
if (score >= 60) return chalk4.hex("#FFA500");
|
|
1057
|
+
return chalk4.red;
|
|
741
1058
|
}
|
|
742
1059
|
function truncateTitle(title, maxLen) {
|
|
743
1060
|
if (title.length <= maxLen) return title;
|
|
@@ -745,17 +1062,17 @@ function truncateTitle(title, maxLen) {
|
|
|
745
1062
|
}
|
|
746
1063
|
|
|
747
1064
|
// src/cli/commands/analyze.ts
|
|
748
|
-
import
|
|
749
|
-
import
|
|
750
|
-
import
|
|
1065
|
+
import fs7 from "fs-extra";
|
|
1066
|
+
import path6 from "path";
|
|
1067
|
+
import chalk5 from "chalk";
|
|
751
1068
|
import ora3 from "ora";
|
|
752
1069
|
import Table2 from "cli-table3";
|
|
753
1070
|
|
|
754
1071
|
// src/integrations/gsc-csv-analyzer.ts
|
|
755
|
-
import
|
|
1072
|
+
import fs6 from "fs-extra";
|
|
756
1073
|
import { parse } from "csv-parse/sync";
|
|
757
1074
|
async function parseGSCCSV(filePath) {
|
|
758
|
-
const content = await
|
|
1075
|
+
const content = await fs6.readFile(filePath, "utf-8");
|
|
759
1076
|
const records = parse(content, {
|
|
760
1077
|
columns: true,
|
|
761
1078
|
skip_empty_lines: true,
|
|
@@ -851,34 +1168,34 @@ async function saveOpportunities(opportunities, outputPath) {
|
|
|
851
1168
|
count: opportunities.length,
|
|
852
1169
|
opportunities
|
|
853
1170
|
};
|
|
854
|
-
await
|
|
1171
|
+
await fs6.writeJson(outputPath, data, { spaces: 2 });
|
|
855
1172
|
}
|
|
856
1173
|
|
|
857
1174
|
// src/cli/commands/analyze.ts
|
|
858
1175
|
async function analyzeCommand(options) {
|
|
859
|
-
console.log(
|
|
1176
|
+
console.log(chalk5.cyan.bold("\n\u{1F4CA} Analyzing Google Search Console Data...\n"));
|
|
860
1177
|
const projectPath = process.cwd();
|
|
861
1178
|
let csvPath;
|
|
862
1179
|
if (options.file) {
|
|
863
|
-
csvPath =
|
|
1180
|
+
csvPath = path6.resolve(options.file);
|
|
864
1181
|
} else {
|
|
865
|
-
const files = await
|
|
1182
|
+
const files = await fs7.readdir(projectPath);
|
|
866
1183
|
const csvFiles = files.filter((f) => f.endsWith(".csv") && f.toLowerCase().includes("search"));
|
|
867
1184
|
if (csvFiles.length === 0) {
|
|
868
|
-
console.log(
|
|
869
|
-
console.log(
|
|
870
|
-
console.log(
|
|
1185
|
+
console.log(chalk5.red("\u2717 No CSV file found"));
|
|
1186
|
+
console.log(chalk5.yellow(" Please provide a CSV file using --file option"));
|
|
1187
|
+
console.log(chalk5.yellow(" Example: edtools analyze --file search-console-data.csv\n"));
|
|
871
1188
|
process.exit(1);
|
|
872
1189
|
}
|
|
873
1190
|
if (csvFiles.length > 1) {
|
|
874
|
-
console.log(
|
|
1191
|
+
console.log(chalk5.yellow("\u26A0 Multiple CSV files found:"));
|
|
875
1192
|
csvFiles.forEach((f) => console.log(` - ${f}`));
|
|
876
|
-
console.log(
|
|
1193
|
+
console.log(chalk5.yellow("\nUsing first file. Specify with --file to use a different one.\n"));
|
|
877
1194
|
}
|
|
878
|
-
csvPath =
|
|
1195
|
+
csvPath = path6.join(projectPath, csvFiles[0]);
|
|
879
1196
|
}
|
|
880
|
-
if (!await
|
|
881
|
-
console.log(
|
|
1197
|
+
if (!await fs7.pathExists(csvPath)) {
|
|
1198
|
+
console.log(chalk5.red(`\u2717 File not found: ${csvPath}
|
|
882
1199
|
`));
|
|
883
1200
|
process.exit(1);
|
|
884
1201
|
}
|
|
@@ -886,10 +1203,10 @@ async function analyzeCommand(options) {
|
|
|
886
1203
|
let data;
|
|
887
1204
|
try {
|
|
888
1205
|
data = await parseGSCCSV(csvPath);
|
|
889
|
-
spinner.succeed(`Parsed ${
|
|
1206
|
+
spinner.succeed(`Parsed ${chalk5.white(data.length)} keywords from ${chalk5.white(path6.basename(csvPath))}`);
|
|
890
1207
|
} catch (error) {
|
|
891
1208
|
spinner.fail("Failed to parse CSV");
|
|
892
|
-
console.log(
|
|
1209
|
+
console.log(chalk5.red(`
|
|
893
1210
|
Error: ${error.message}
|
|
894
1211
|
`));
|
|
895
1212
|
process.exit(1);
|
|
@@ -904,7 +1221,7 @@ Error: ${error.message}
|
|
|
904
1221
|
analyzeSpinner.succeed("Analysis complete!");
|
|
905
1222
|
console.log("");
|
|
906
1223
|
const metricsTable = new Table2({
|
|
907
|
-
head: [
|
|
1224
|
+
head: [chalk5.cyan.bold("Metric"), chalk5.cyan.bold("Value")],
|
|
908
1225
|
colWidths: [30, 20],
|
|
909
1226
|
style: {
|
|
910
1227
|
head: [],
|
|
@@ -912,28 +1229,28 @@ Error: ${error.message}
|
|
|
912
1229
|
}
|
|
913
1230
|
});
|
|
914
1231
|
metricsTable.push(
|
|
915
|
-
["Total Keywords",
|
|
916
|
-
["Total Impressions",
|
|
917
|
-
["Total Clicks",
|
|
918
|
-
["Average CTR",
|
|
919
|
-
["Average Position",
|
|
1232
|
+
["Total Keywords", chalk5.white(analysis.totalQueries.toLocaleString())],
|
|
1233
|
+
["Total Impressions", chalk5.white(analysis.totalImpressions.toLocaleString())],
|
|
1234
|
+
["Total Clicks", chalk5.white(analysis.totalClicks.toLocaleString())],
|
|
1235
|
+
["Average CTR", chalk5.white((analysis.avgCTR * 100).toFixed(2) + "%")],
|
|
1236
|
+
["Average Position", chalk5.white(analysis.avgPosition.toFixed(1))]
|
|
920
1237
|
);
|
|
921
|
-
console.log(
|
|
1238
|
+
console.log(chalk5.bold.cyan("\u{1F4C8} Overall Metrics\n"));
|
|
922
1239
|
console.log(metricsTable.toString());
|
|
923
1240
|
console.log("");
|
|
924
1241
|
if (analysis.opportunities.length === 0) {
|
|
925
1242
|
console.log(warningBox("No opportunities found with current criteria.\nTry lowering --min-impressions or increasing --min-position."));
|
|
926
1243
|
} else {
|
|
927
|
-
console.log(
|
|
1244
|
+
console.log(chalk5.bold.cyan(`\u{1F3AF} Top ${analysis.opportunities.length} Content Opportunities
|
|
928
1245
|
`));
|
|
929
1246
|
const oppTable = new Table2({
|
|
930
1247
|
head: [
|
|
931
|
-
|
|
932
|
-
|
|
933
|
-
|
|
934
|
-
|
|
935
|
-
|
|
936
|
-
|
|
1248
|
+
chalk5.cyan.bold("#"),
|
|
1249
|
+
chalk5.cyan.bold("Keyword"),
|
|
1250
|
+
chalk5.cyan.bold("Impressions"),
|
|
1251
|
+
chalk5.cyan.bold("Position"),
|
|
1252
|
+
chalk5.cyan.bold("Potential"),
|
|
1253
|
+
chalk5.cyan.bold("Priority")
|
|
937
1254
|
],
|
|
938
1255
|
colWidths: [5, 40, 14, 12, 12, 12],
|
|
939
1256
|
wordWrap: true,
|
|
@@ -943,34 +1260,34 @@ Error: ${error.message}
|
|
|
943
1260
|
}
|
|
944
1261
|
});
|
|
945
1262
|
analysis.opportunities.forEach((opp, i) => {
|
|
946
|
-
const priorityColor = opp.priority === "high" ?
|
|
1263
|
+
const priorityColor = opp.priority === "high" ? chalk5.green : opp.priority === "medium" ? chalk5.yellow : chalk5.gray;
|
|
947
1264
|
oppTable.push([
|
|
948
|
-
|
|
949
|
-
|
|
950
|
-
|
|
951
|
-
|
|
952
|
-
|
|
1265
|
+
chalk5.gray((i + 1).toString()),
|
|
1266
|
+
chalk5.white(opp.keyword),
|
|
1267
|
+
chalk5.cyan(opp.impressions.toLocaleString()),
|
|
1268
|
+
chalk5.yellow(opp.position.toFixed(1)),
|
|
1269
|
+
chalk5.green(`+${opp.potentialClicks}`),
|
|
953
1270
|
priorityColor(opp.priority.toUpperCase())
|
|
954
1271
|
]);
|
|
955
1272
|
});
|
|
956
1273
|
console.log(oppTable.toString());
|
|
957
1274
|
console.log("");
|
|
958
|
-
console.log(
|
|
1275
|
+
console.log(chalk5.bold.cyan("\u{1F4A1} Suggested Blog Post Titles\n"));
|
|
959
1276
|
analysis.opportunities.slice(0, 5).forEach((opp, i) => {
|
|
960
|
-
console.log(` ${
|
|
961
|
-
console.log(` ${
|
|
1277
|
+
console.log(` ${chalk5.cyan((i + 1).toString() + ".")} ${chalk5.white(opp.suggestedTitle)}`);
|
|
1278
|
+
console.log(` ${chalk5.gray(opp.reason)}
|
|
962
1279
|
`);
|
|
963
1280
|
});
|
|
964
1281
|
}
|
|
965
1282
|
if (analysis.topKeywords.length > 0) {
|
|
966
|
-
console.log(
|
|
1283
|
+
console.log(chalk5.bold.cyan("\u2B50 Top Performing Keywords (by clicks)\n"));
|
|
967
1284
|
const topTable = new Table2({
|
|
968
1285
|
head: [
|
|
969
|
-
|
|
970
|
-
|
|
971
|
-
|
|
972
|
-
|
|
973
|
-
|
|
1286
|
+
chalk5.cyan.bold("#"),
|
|
1287
|
+
chalk5.cyan.bold("Keyword"),
|
|
1288
|
+
chalk5.cyan.bold("Clicks"),
|
|
1289
|
+
chalk5.cyan.bold("Impressions"),
|
|
1290
|
+
chalk5.cyan.bold("CTR")
|
|
974
1291
|
],
|
|
975
1292
|
colWidths: [5, 40, 12, 14, 12],
|
|
976
1293
|
wordWrap: true,
|
|
@@ -981,39 +1298,39 @@ Error: ${error.message}
|
|
|
981
1298
|
});
|
|
982
1299
|
analysis.topKeywords.slice(0, 5).forEach((kw, i) => {
|
|
983
1300
|
topTable.push([
|
|
984
|
-
|
|
985
|
-
|
|
986
|
-
|
|
987
|
-
|
|
988
|
-
|
|
1301
|
+
chalk5.gray((i + 1).toString()),
|
|
1302
|
+
chalk5.white(kw.query),
|
|
1303
|
+
chalk5.green(kw.clicks.toString()),
|
|
1304
|
+
chalk5.cyan(kw.impressions.toLocaleString()),
|
|
1305
|
+
chalk5.yellow((kw.ctr * 100).toFixed(2) + "%")
|
|
989
1306
|
]);
|
|
990
1307
|
});
|
|
991
1308
|
console.log(topTable.toString());
|
|
992
1309
|
console.log("");
|
|
993
1310
|
}
|
|
994
|
-
const edtoolsDir =
|
|
995
|
-
await
|
|
996
|
-
const opportunitiesPath =
|
|
1311
|
+
const edtoolsDir = path6.join(projectPath, ".edtools");
|
|
1312
|
+
await fs7.ensureDir(edtoolsDir);
|
|
1313
|
+
const opportunitiesPath = path6.join(edtoolsDir, "opportunities.json");
|
|
997
1314
|
await saveOpportunities(analysis.opportunities, opportunitiesPath);
|
|
998
|
-
console.log(successBox(`Analysis saved to ${
|
|
999
|
-
console.log(
|
|
1000
|
-
console.log(` ${
|
|
1001
|
-
console.log(` ${
|
|
1315
|
+
console.log(successBox(`Analysis saved to ${chalk5.white(".edtools/opportunities.json")}`));
|
|
1316
|
+
console.log(chalk5.cyan.bold("Next steps:"));
|
|
1317
|
+
console.log(` ${chalk5.cyan("1.")} Generate content: ${chalk5.white("edtools generate --from-csv")}`);
|
|
1318
|
+
console.log(` ${chalk5.cyan("2.")} Or specify topics: ${chalk5.white('edtools generate -t "topic 1" "topic 2"')}
|
|
1002
1319
|
`);
|
|
1003
|
-
console.log(
|
|
1320
|
+
console.log(chalk5.gray("\u{1F4A1} Use --from-csv to automatically generate posts for top opportunities\n"));
|
|
1004
1321
|
}
|
|
1005
1322
|
|
|
1006
1323
|
// src/cli/commands/validate.ts
|
|
1007
|
-
import
|
|
1008
|
-
import
|
|
1009
|
-
import
|
|
1324
|
+
import fs9 from "fs-extra";
|
|
1325
|
+
import path7 from "path";
|
|
1326
|
+
import chalk6 from "chalk";
|
|
1010
1327
|
import Table3 from "cli-table3";
|
|
1011
1328
|
|
|
1012
1329
|
// src/utils/seo-validator.ts
|
|
1013
1330
|
import * as cheerio from "cheerio";
|
|
1014
|
-
import
|
|
1331
|
+
import fs8 from "fs-extra";
|
|
1015
1332
|
async function validatePost(htmlPath) {
|
|
1016
|
-
const html = await
|
|
1333
|
+
const html = await fs8.readFile(htmlPath, "utf-8");
|
|
1017
1334
|
const $ = cheerio.load(html);
|
|
1018
1335
|
const issues = [];
|
|
1019
1336
|
const passed = [];
|
|
@@ -1236,19 +1553,19 @@ function calculateValidationStats(results) {
|
|
|
1236
1553
|
|
|
1237
1554
|
// src/cli/commands/validate.ts
|
|
1238
1555
|
async function validateCommand(options) {
|
|
1239
|
-
console.log(
|
|
1556
|
+
console.log(chalk6.cyan.bold("\n\u{1F4CA} Validating SEO quality...\n"));
|
|
1240
1557
|
const projectPath = process.cwd();
|
|
1241
1558
|
let htmlFiles = [];
|
|
1242
1559
|
if (options.post) {
|
|
1243
|
-
const postPath =
|
|
1244
|
-
if (!await
|
|
1560
|
+
const postPath = path7.resolve(projectPath, options.post);
|
|
1561
|
+
if (!await fs9.pathExists(postPath)) {
|
|
1245
1562
|
console.log(errorBox(`Post not found: ${postPath}`));
|
|
1246
1563
|
process.exit(1);
|
|
1247
1564
|
}
|
|
1248
1565
|
htmlFiles = [postPath];
|
|
1249
1566
|
} else if (options.posts) {
|
|
1250
|
-
const postsDir =
|
|
1251
|
-
if (!await
|
|
1567
|
+
const postsDir = path7.resolve(projectPath, options.posts);
|
|
1568
|
+
if (!await fs9.pathExists(postsDir)) {
|
|
1252
1569
|
console.log(errorBox(`Directory not found: ${postsDir}`));
|
|
1253
1570
|
process.exit(1);
|
|
1254
1571
|
}
|
|
@@ -1258,8 +1575,8 @@ async function validateCommand(options) {
|
|
|
1258
1575
|
process.exit(1);
|
|
1259
1576
|
}
|
|
1260
1577
|
} else {
|
|
1261
|
-
const defaultBlogDir =
|
|
1262
|
-
if (await
|
|
1578
|
+
const defaultBlogDir = path7.join(projectPath, "blog");
|
|
1579
|
+
if (await fs9.pathExists(defaultBlogDir)) {
|
|
1263
1580
|
htmlFiles = await findHtmlFiles(defaultBlogDir);
|
|
1264
1581
|
if (htmlFiles.length === 0) {
|
|
1265
1582
|
console.log(errorBox("No HTML files found in blog/ directory"));
|
|
@@ -1267,11 +1584,11 @@ async function validateCommand(options) {
|
|
|
1267
1584
|
}
|
|
1268
1585
|
} else {
|
|
1269
1586
|
console.log(errorBox("No posts directory specified"));
|
|
1270
|
-
console.log(
|
|
1587
|
+
console.log(chalk6.yellow("Usage: edtools validate --posts <dir> or --post <file>\n"));
|
|
1271
1588
|
process.exit(1);
|
|
1272
1589
|
}
|
|
1273
1590
|
}
|
|
1274
|
-
console.log(
|
|
1591
|
+
console.log(chalk6.cyan(`Found ${htmlFiles.length} post${htmlFiles.length > 1 ? "s" : ""} to validate
|
|
1275
1592
|
`));
|
|
1276
1593
|
const results = [];
|
|
1277
1594
|
for (const htmlFile of htmlFiles) {
|
|
@@ -1279,7 +1596,7 @@ async function validateCommand(options) {
|
|
|
1279
1596
|
const result = await validatePost(htmlFile);
|
|
1280
1597
|
results.push(result);
|
|
1281
1598
|
} catch (error) {
|
|
1282
|
-
console.log(
|
|
1599
|
+
console.log(chalk6.yellow(`\u26A0\uFE0F Failed to validate ${htmlFile}: ${error.message}`));
|
|
1283
1600
|
}
|
|
1284
1601
|
}
|
|
1285
1602
|
if (results.length === 0) {
|
|
@@ -1304,17 +1621,17 @@ async function validateCommand(options) {
|
|
|
1304
1621
|
})),
|
|
1305
1622
|
stats: stats2
|
|
1306
1623
|
};
|
|
1307
|
-
const outputPath =
|
|
1308
|
-
await
|
|
1624
|
+
const outputPath = path7.resolve(projectPath, options.output);
|
|
1625
|
+
await fs9.writeJson(outputPath, report, { spaces: 2 });
|
|
1309
1626
|
console.log(successBox(`Validation report saved to ${outputPath}`));
|
|
1310
1627
|
}
|
|
1311
1628
|
if (filteredResults.length > 0) {
|
|
1312
1629
|
const table = new Table3({
|
|
1313
1630
|
head: [
|
|
1314
|
-
|
|
1315
|
-
|
|
1316
|
-
|
|
1317
|
-
|
|
1631
|
+
chalk6.cyan.bold("#"),
|
|
1632
|
+
chalk6.cyan.bold("Title"),
|
|
1633
|
+
chalk6.cyan.bold("Score"),
|
|
1634
|
+
chalk6.cyan.bold("Issues")
|
|
1318
1635
|
],
|
|
1319
1636
|
colWidths: [5, 40, 10, 50],
|
|
1320
1637
|
wordWrap: true,
|
|
@@ -1327,12 +1644,12 @@ async function validateCommand(options) {
|
|
|
1327
1644
|
const scoreColor = getScoreColor(result.seoScore);
|
|
1328
1645
|
const issuesText = result.issues.length > 0 ? result.issues.map((issue) => {
|
|
1329
1646
|
const icon = issue.severity === "error" ? "\u2717" : issue.severity === "warning" ? "\u26A0" : "\u2139";
|
|
1330
|
-
const color = issue.severity === "error" ?
|
|
1647
|
+
const color = issue.severity === "error" ? chalk6.red : issue.severity === "warning" ? chalk6.yellow : chalk6.blue;
|
|
1331
1648
|
return color(`${icon} ${issue.message}`);
|
|
1332
|
-
}).join("\n") :
|
|
1649
|
+
}).join("\n") : chalk6.green("\u2713 No issues");
|
|
1333
1650
|
table.push([
|
|
1334
|
-
|
|
1335
|
-
|
|
1651
|
+
chalk6.gray((i + 1).toString()),
|
|
1652
|
+
chalk6.white(truncate(result.title, 38)),
|
|
1336
1653
|
scoreColor(`${result.seoScore}/100`),
|
|
1337
1654
|
issuesText
|
|
1338
1655
|
]);
|
|
@@ -1343,39 +1660,39 @@ async function validateCommand(options) {
|
|
|
1343
1660
|
console.log(successBox(`All posts have SEO score >= ${threshold}`));
|
|
1344
1661
|
}
|
|
1345
1662
|
const stats = calculateValidationStats(results);
|
|
1346
|
-
console.log(
|
|
1347
|
-
console.log(` Total posts validated: ${
|
|
1663
|
+
console.log(chalk6.cyan.bold("Overall Statistics:\n"));
|
|
1664
|
+
console.log(` Total posts validated: ${chalk6.white(stats.totalPosts)}`);
|
|
1348
1665
|
console.log(` Average SEO score: ${getScoreColor(stats.avgSeoScore)(stats.avgSeoScore + "/100")}`);
|
|
1349
|
-
console.log(` Posts with issues: ${
|
|
1350
|
-
console.log(` Total issues found: ${
|
|
1666
|
+
console.log(` Posts with issues: ${chalk6.white(stats.postsWithIssues)} (${Math.round(stats.postsWithIssues / stats.totalPosts * 100)}%)`);
|
|
1667
|
+
console.log(` Total issues found: ${chalk6.white(stats.totalIssues)}`);
|
|
1351
1668
|
console.log("");
|
|
1352
1669
|
if (stats.totalIssues > 0) {
|
|
1353
|
-
console.log(
|
|
1670
|
+
console.log(chalk6.cyan.bold("Issues by Severity:\n"));
|
|
1354
1671
|
if (stats.issuesBySeverity.error > 0) {
|
|
1355
|
-
console.log(` ${
|
|
1672
|
+
console.log(` ${chalk6.red("\u2717 Errors:")} ${chalk6.white(stats.issuesBySeverity.error)}`);
|
|
1356
1673
|
}
|
|
1357
1674
|
if (stats.issuesBySeverity.warning > 0) {
|
|
1358
|
-
console.log(` ${
|
|
1675
|
+
console.log(` ${chalk6.yellow("\u26A0 Warnings:")} ${chalk6.white(stats.issuesBySeverity.warning)}`);
|
|
1359
1676
|
}
|
|
1360
1677
|
if (stats.issuesBySeverity.info > 0) {
|
|
1361
|
-
console.log(` ${
|
|
1678
|
+
console.log(` ${chalk6.blue("\u2139 Info:")} ${chalk6.white(stats.issuesBySeverity.info)}`);
|
|
1362
1679
|
}
|
|
1363
1680
|
console.log("");
|
|
1364
1681
|
}
|
|
1365
1682
|
if (Object.keys(stats.issuesByCategory).length > 0) {
|
|
1366
|
-
console.log(
|
|
1683
|
+
console.log(chalk6.cyan.bold("Top Issue Categories:\n"));
|
|
1367
1684
|
const sortedCategories = Object.entries(stats.issuesByCategory).sort((a, b) => b[1] - a[1]).slice(0, 5);
|
|
1368
1685
|
sortedCategories.forEach(([category, count]) => {
|
|
1369
|
-
console.log(` ${
|
|
1686
|
+
console.log(` ${chalk6.white(capitalize(category))}: ${chalk6.yellow(count)}`);
|
|
1370
1687
|
});
|
|
1371
1688
|
console.log("");
|
|
1372
1689
|
}
|
|
1373
1690
|
if (stats.avgSeoScore < 85) {
|
|
1374
|
-
console.log(
|
|
1375
|
-
console.log(
|
|
1376
|
-
console.log(
|
|
1377
|
-
console.log(
|
|
1378
|
-
console.log(
|
|
1691
|
+
console.log(chalk6.yellow.bold("\u{1F4A1} Recommendations:\n"));
|
|
1692
|
+
console.log(chalk6.yellow(" \u2022 Focus on fixing critical errors first"));
|
|
1693
|
+
console.log(chalk6.yellow(" \u2022 Optimize meta descriptions to 150-160 characters"));
|
|
1694
|
+
console.log(chalk6.yellow(" \u2022 Ensure all posts have proper heading structure (H1, H2s)"));
|
|
1695
|
+
console.log(chalk6.yellow(" \u2022 Add alt text to all images"));
|
|
1379
1696
|
console.log("");
|
|
1380
1697
|
} else {
|
|
1381
1698
|
console.log(successBox(`Great job! Average SEO score is ${stats.avgSeoScore}/100`));
|
|
@@ -1383,9 +1700,9 @@ async function validateCommand(options) {
|
|
|
1383
1700
|
}
|
|
1384
1701
|
async function findHtmlFiles(dir) {
|
|
1385
1702
|
const files = [];
|
|
1386
|
-
const entries = await
|
|
1703
|
+
const entries = await fs9.readdir(dir, { withFileTypes: true });
|
|
1387
1704
|
for (const entry of entries) {
|
|
1388
|
-
const fullPath =
|
|
1705
|
+
const fullPath = path7.join(dir, entry.name);
|
|
1389
1706
|
if (entry.isDirectory()) {
|
|
1390
1707
|
const subFiles = await findHtmlFiles(fullPath);
|
|
1391
1708
|
files.push(...subFiles);
|
|
@@ -1396,10 +1713,10 @@ async function findHtmlFiles(dir) {
|
|
|
1396
1713
|
return files;
|
|
1397
1714
|
}
|
|
1398
1715
|
function getScoreColor(score) {
|
|
1399
|
-
if (score >= 90) return
|
|
1400
|
-
if (score >= 75) return
|
|
1401
|
-
if (score >= 60) return
|
|
1402
|
-
return
|
|
1716
|
+
if (score >= 90) return chalk6.green;
|
|
1717
|
+
if (score >= 75) return chalk6.yellow;
|
|
1718
|
+
if (score >= 60) return chalk6.hex("#FFA500");
|
|
1719
|
+
return chalk6.red;
|
|
1403
1720
|
}
|
|
1404
1721
|
function truncate(str, maxLen) {
|
|
1405
1722
|
if (str.length <= maxLen) return str;
|
|
@@ -1410,22 +1727,22 @@ function capitalize(str) {
|
|
|
1410
1727
|
}
|
|
1411
1728
|
|
|
1412
1729
|
// src/cli/commands/doctor.ts
|
|
1413
|
-
import
|
|
1414
|
-
import
|
|
1730
|
+
import fs10 from "fs-extra";
|
|
1731
|
+
import path8 from "path";
|
|
1415
1732
|
import { pathToFileURL as pathToFileURL2 } from "url";
|
|
1416
|
-
import
|
|
1733
|
+
import chalk7 from "chalk";
|
|
1417
1734
|
async function doctorCommand(options) {
|
|
1418
|
-
console.log(
|
|
1735
|
+
console.log(chalk7.cyan.bold("\n\u{1F50D} Diagnosing project configuration...\n"));
|
|
1419
1736
|
const projectPath = process.cwd();
|
|
1420
|
-
const configPath =
|
|
1737
|
+
const configPath = path8.join(projectPath, "edtools.config.js");
|
|
1421
1738
|
const issues = [];
|
|
1422
1739
|
const warnings = [];
|
|
1423
1740
|
const suggestions = [];
|
|
1424
|
-
if (await
|
|
1425
|
-
console.log(
|
|
1741
|
+
if (await fs10.pathExists(configPath)) {
|
|
1742
|
+
console.log(chalk7.green("\u2713 Configuration file found: ") + chalk7.white("edtools.config.js"));
|
|
1426
1743
|
} else {
|
|
1427
|
-
console.log(
|
|
1428
|
-
console.log(
|
|
1744
|
+
console.log(chalk7.red("\u2717 Configuration file not found"));
|
|
1745
|
+
console.log(chalk7.yellow(' Run "edtools init" to create configuration\n'));
|
|
1429
1746
|
process.exit(1);
|
|
1430
1747
|
}
|
|
1431
1748
|
let productInfo;
|
|
@@ -1440,135 +1757,153 @@ async function doctorCommand(options) {
|
|
|
1440
1757
|
}
|
|
1441
1758
|
} catch (error) {
|
|
1442
1759
|
issues.push(`Failed to load edtools.config.js: ${error.message}`);
|
|
1443
|
-
console.log(
|
|
1444
|
-
console.log(
|
|
1760
|
+
console.log(chalk7.red("\u2717 Failed to load configuration file"));
|
|
1761
|
+
console.log(chalk7.red(` Error: ${error.message}
|
|
1445
1762
|
`));
|
|
1446
1763
|
process.exit(1);
|
|
1447
1764
|
}
|
|
1448
|
-
const outputDirPath =
|
|
1449
|
-
if (await
|
|
1450
|
-
console.log(
|
|
1765
|
+
const outputDirPath = path8.resolve(projectPath, outputDir);
|
|
1766
|
+
if (await fs10.pathExists(outputDirPath)) {
|
|
1767
|
+
console.log(chalk7.green("\u2713 Output directory exists: ") + chalk7.white(outputDir));
|
|
1451
1768
|
} else {
|
|
1452
1769
|
warnings.push(`Output directory does not exist: ${outputDir}`);
|
|
1453
|
-
console.log(
|
|
1454
|
-
console.log(
|
|
1770
|
+
console.log(chalk7.yellow("\u26A0\uFE0F Output directory does not exist: ") + chalk7.white(outputDir));
|
|
1771
|
+
console.log(chalk7.yellow(' It will be created when you run "edtools generate"'));
|
|
1455
1772
|
}
|
|
1456
|
-
const edtoolsConfigPath =
|
|
1773
|
+
const edtoolsConfigPath = path8.join(projectPath, ".edtools", "config.json");
|
|
1457
1774
|
let hasApiKey = false;
|
|
1458
|
-
if (await
|
|
1775
|
+
if (await fs10.pathExists(edtoolsConfigPath)) {
|
|
1459
1776
|
try {
|
|
1460
|
-
const edtoolsConfig = await
|
|
1777
|
+
const edtoolsConfig = await fs10.readJson(edtoolsConfigPath);
|
|
1461
1778
|
hasApiKey = !!edtoolsConfig.apiKey;
|
|
1462
1779
|
} catch (error) {
|
|
1463
1780
|
}
|
|
1464
1781
|
}
|
|
1465
1782
|
const hasEnvApiKey = !!(process.env.ANTHROPIC_API_KEY || process.env.OPENAI_API_KEY);
|
|
1466
1783
|
if (hasApiKey || hasEnvApiKey) {
|
|
1467
|
-
console.log(
|
|
1784
|
+
console.log(chalk7.green("\u2713 API key configured"));
|
|
1468
1785
|
} else {
|
|
1469
1786
|
warnings.push('No API key found (set via environment variable or "edtools init")');
|
|
1470
|
-
console.log(
|
|
1471
|
-
console.log(
|
|
1472
|
-
console.log(
|
|
1787
|
+
console.log(chalk7.yellow("\u26A0\uFE0F No API key configured"));
|
|
1788
|
+
console.log(chalk7.yellow(" Set ANTHROPIC_API_KEY or OPENAI_API_KEY environment variable"));
|
|
1789
|
+
console.log(chalk7.yellow(' Or run "edtools init" to store API key'));
|
|
1473
1790
|
}
|
|
1474
1791
|
console.log("");
|
|
1475
1792
|
const hostingConfig = detectHostingConfig(projectPath);
|
|
1476
1793
|
if (hostingConfig) {
|
|
1477
|
-
console.log(
|
|
1478
|
-
console.log(
|
|
1479
|
-
console.log(
|
|
1794
|
+
console.log(chalk7.cyan("\u26A0\uFE0F HOSTING PLATFORM DETECTED: ") + chalk7.white(hostingConfig.platform));
|
|
1795
|
+
console.log(chalk7.cyan(" - Public directory: ") + chalk7.white(hostingConfig.publicDir));
|
|
1796
|
+
console.log(chalk7.cyan(" - Your outputDir: ") + chalk7.white(outputDir));
|
|
1480
1797
|
const validation = validateOutputDir(outputDir, hostingConfig);
|
|
1481
1798
|
if (!validation.valid) {
|
|
1482
1799
|
issues.push("Output directory misconfiguration");
|
|
1483
|
-
console.log(
|
|
1800
|
+
console.log(chalk7.red(" - Problem: Files outside ") + chalk7.white(hostingConfig.publicDir + "/") + chalk7.red(" won't be accessible"));
|
|
1484
1801
|
console.log("");
|
|
1485
|
-
console.log(
|
|
1486
|
-
console.log(
|
|
1802
|
+
console.log(chalk7.red.bold("\u274C ISSUE: Output directory misconfiguration"));
|
|
1803
|
+
console.log(chalk7.red(" Generated files will return 404 in production"));
|
|
1487
1804
|
console.log("");
|
|
1488
|
-
console.log(
|
|
1489
|
-
console.log(
|
|
1805
|
+
console.log(chalk7.cyan.bold("\u{1F4DD} Suggested fix:"));
|
|
1806
|
+
console.log(chalk7.cyan(" Update edtools.config.js:"));
|
|
1490
1807
|
console.log("");
|
|
1491
|
-
console.log(
|
|
1492
|
-
console.log(
|
|
1493
|
-
console.log(
|
|
1808
|
+
console.log(chalk7.gray(" content: {"));
|
|
1809
|
+
console.log(chalk7.green(` outputDir: '${validation.suggestion}'`) + chalk7.gray(" // \u2705 Correct path"));
|
|
1810
|
+
console.log(chalk7.gray(" }"));
|
|
1494
1811
|
console.log("");
|
|
1495
1812
|
suggestions.push(`Update outputDir to '${validation.suggestion}'`);
|
|
1496
1813
|
if (options.fix) {
|
|
1497
|
-
console.log(
|
|
1814
|
+
console.log(chalk7.yellow.bold("\u{1F527} Auto-fixing configuration...\n"));
|
|
1498
1815
|
try {
|
|
1499
|
-
const configContent = await
|
|
1816
|
+
const configContent = await fs10.readFile(configPath, "utf-8");
|
|
1500
1817
|
const updatedConfig = configContent.replace(
|
|
1501
1818
|
/outputDir:\s*['"]([^'"]+)['"]/,
|
|
1502
1819
|
`outputDir: '${validation.suggestion}'`
|
|
1503
1820
|
);
|
|
1504
|
-
await
|
|
1505
|
-
console.log(
|
|
1506
|
-
console.log(
|
|
1821
|
+
await fs10.writeFile(configPath, updatedConfig, "utf-8");
|
|
1822
|
+
console.log(chalk7.green("\u2713 Updated edtools.config.js"));
|
|
1823
|
+
console.log(chalk7.green(` outputDir: '${validation.suggestion}'`));
|
|
1507
1824
|
console.log("");
|
|
1508
|
-
console.log(
|
|
1825
|
+
console.log(chalk7.cyan('\u{1F389} Configuration fixed! Run "edtools generate" to create content.\n'));
|
|
1509
1826
|
} catch (error) {
|
|
1510
|
-
console.log(
|
|
1511
|
-
console.log(
|
|
1512
|
-
console.log(
|
|
1827
|
+
console.log(chalk7.red("\u2717 Failed to auto-fix configuration"));
|
|
1828
|
+
console.log(chalk7.red(` Error: ${error.message}`));
|
|
1829
|
+
console.log(chalk7.yellow(" Please update edtools.config.js manually\n"));
|
|
1513
1830
|
}
|
|
1514
1831
|
} else {
|
|
1515
|
-
console.log(
|
|
1516
|
-
console.log(
|
|
1832
|
+
console.log(chalk7.cyan.bold("Run: ") + chalk7.white("edtools doctor --fix"));
|
|
1833
|
+
console.log(chalk7.cyan("To automatically apply suggested fixes\n"));
|
|
1517
1834
|
}
|
|
1518
1835
|
} else {
|
|
1519
|
-
console.log(
|
|
1836
|
+
console.log(chalk7.green(" - Status: \u2705 Output directory correctly configured"));
|
|
1520
1837
|
console.log("");
|
|
1521
1838
|
}
|
|
1522
1839
|
} else {
|
|
1523
|
-
console.log(
|
|
1524
|
-
console.log(
|
|
1840
|
+
console.log(chalk7.gray("No hosting platform configuration detected"));
|
|
1841
|
+
console.log(chalk7.gray("(No firebase.json, vercel.json, netlify.toml, or amplify.yml found)"));
|
|
1525
1842
|
console.log("");
|
|
1526
1843
|
}
|
|
1527
|
-
console.log(
|
|
1844
|
+
console.log(chalk7.cyan.bold("Summary:\n"));
|
|
1528
1845
|
if (issues.length === 0 && warnings.length === 0) {
|
|
1529
|
-
console.log(
|
|
1846
|
+
console.log(chalk7.green.bold("\u2705 All checks passed! Your project is ready to generate content.\n"));
|
|
1530
1847
|
} else {
|
|
1531
1848
|
if (issues.length > 0) {
|
|
1532
|
-
console.log(
|
|
1849
|
+
console.log(chalk7.red.bold(`\u274C ${issues.length} issue(s) found:`));
|
|
1533
1850
|
issues.forEach((issue) => {
|
|
1534
|
-
console.log(
|
|
1851
|
+
console.log(chalk7.red(` - ${issue}`));
|
|
1535
1852
|
});
|
|
1536
1853
|
console.log("");
|
|
1537
1854
|
}
|
|
1538
1855
|
if (warnings.length > 0) {
|
|
1539
|
-
console.log(
|
|
1856
|
+
console.log(chalk7.yellow.bold(`\u26A0\uFE0F ${warnings.length} warning(s):`));
|
|
1540
1857
|
warnings.forEach((warning) => {
|
|
1541
|
-
console.log(
|
|
1858
|
+
console.log(chalk7.yellow(` - ${warning}`));
|
|
1542
1859
|
});
|
|
1543
1860
|
console.log("");
|
|
1544
1861
|
}
|
|
1545
1862
|
if (suggestions.length > 0 && !options.fix) {
|
|
1546
|
-
console.log(
|
|
1863
|
+
console.log(chalk7.cyan.bold("\u{1F4A1} Suggestions:"));
|
|
1547
1864
|
suggestions.forEach((suggestion) => {
|
|
1548
|
-
console.log(
|
|
1865
|
+
console.log(chalk7.cyan(` - ${suggestion}`));
|
|
1549
1866
|
});
|
|
1550
1867
|
console.log("");
|
|
1551
|
-
console.log(
|
|
1868
|
+
console.log(chalk7.cyan("Run ") + chalk7.white("edtools doctor --fix") + chalk7.cyan(" to automatically apply fixes\n"));
|
|
1552
1869
|
}
|
|
1553
1870
|
}
|
|
1554
1871
|
}
|
|
1555
1872
|
|
|
1556
1873
|
// src/cli/index.ts
|
|
1874
|
+
function getVersion2() {
|
|
1875
|
+
try {
|
|
1876
|
+
const __filename = fileURLToPath2(import.meta.url);
|
|
1877
|
+
const __dirname = path9.dirname(__filename);
|
|
1878
|
+
const possiblePaths = [
|
|
1879
|
+
path9.join(__dirname, "../../package.json"),
|
|
1880
|
+
// From dist/cli/
|
|
1881
|
+
path9.join(__dirname, "../../../package.json"),
|
|
1882
|
+
// From src/cli/
|
|
1883
|
+
path9.join(__dirname, "../../../../package.json")
|
|
1884
|
+
// From npm global install
|
|
1885
|
+
];
|
|
1886
|
+
for (const pkgPath of possiblePaths) {
|
|
1887
|
+
if (fs11.existsSync(pkgPath)) {
|
|
1888
|
+
const pkg = fs11.readJsonSync(pkgPath);
|
|
1889
|
+
return pkg.version || "unknown";
|
|
1890
|
+
}
|
|
1891
|
+
}
|
|
1892
|
+
return "unknown";
|
|
1893
|
+
} catch (error) {
|
|
1894
|
+
return "dev";
|
|
1895
|
+
}
|
|
1896
|
+
}
|
|
1557
1897
|
var program = new Command();
|
|
1558
|
-
program.name("edtools").description("AI-Powered Content Marketing CLI - Generate, validate, and optimize SEO content").version(
|
|
1898
|
+
program.name("edtools").description("AI-Powered Content Marketing CLI - Generate, validate, and optimize SEO content").version(getVersion2());
|
|
1559
1899
|
program.command("init").description("Initialize edtools in your project").option("-p, --path <path>", "Project path", process.cwd()).action(initCommand);
|
|
1560
1900
|
program.command("generate").description("Generate SEO-optimized blog posts").option("-n, --posts <number>", "Number of posts to generate (1-10)", "3").option("-t, --topics <topics...>", "Specific topics to write about").option("-o, --output <dir>", "Output directory (defaults to config or ./blog)").option("--api-key <key>", "API key (or use ANTHROPIC_API_KEY/OPENAI_API_KEY env var)").option("--from-csv", "Generate from CSV analysis opportunities").option("--dry-run", "Preview what would be generated without writing files").option("--report <format>", "Output format: json, table, pretty (default: table)", "table").action(generateCommand);
|
|
1561
1901
|
program.command("analyze").description("Analyze Google Search Console CSV data").option("-f, --file <path>", "Path to CSV file (auto-detects if not provided)").option("--min-impressions <number>", "Minimum impressions for opportunities", "50").option("--min-position <number>", "Minimum position for opportunities", "20").option("--limit <number>", "Number of opportunities to show", "10").action(analyzeCommand);
|
|
1562
1902
|
program.command("validate").description("Validate SEO quality of existing posts").option("--posts <dir>", "Posts directory to validate").option("--post <file>", "Single post file to validate").option("--output <file>", "Save validation report as JSON").option("--threshold <score>", "Only show posts below this score").action(validateCommand);
|
|
1563
1903
|
program.command("doctor").description("Diagnose project configuration and hosting setup").option("--fix", "Automatically fix issues").action(doctorCommand);
|
|
1564
|
-
program.command("config").description("
|
|
1565
|
-
|
|
1566
|
-
|
|
1567
|
-
} else {
|
|
1568
|
-
console.log(chalk7.cyan("Configuration:"));
|
|
1569
|
-
console.log(` API Key: ${process.env.ANTHROPIC_API_KEY ? "[set]" : "[not set]"}`);
|
|
1570
|
-
}
|
|
1571
|
-
});
|
|
1904
|
+
var configCmd = program.command("config").description("Manage edtools configuration");
|
|
1905
|
+
configCmd.command("show").description("Show current configuration status").option("-p, --path <path>", "Project path", process.cwd()).action(configShowCommand);
|
|
1906
|
+
configCmd.command("set-api-key").description("Set or update API key without overwriting edtools.config.js").option("-p, --path <path>", "Project path", process.cwd()).option("-k, --key <key>", "API key (if not provided, will prompt)").option("--provider <provider>", "Provider: anthropic or openai").action(configSetApiKeyCommand);
|
|
1572
1907
|
if (process.argv.length === 2) {
|
|
1573
1908
|
showWelcome();
|
|
1574
1909
|
process.exit(0);
|