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