@edtools/cli 0.6.1 → 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 +1 -1
- package/dist/cli/commands/generate.d.ts.map +1 -1
- package/dist/cli/commands/generate.js +74 -17
- 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 +567 -265
- 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,131 +573,322 @@ ${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"));
|
|
837
|
+
}
|
|
838
|
+
let outputDirConfig;
|
|
839
|
+
let outputDirSource;
|
|
840
|
+
if (options.output) {
|
|
841
|
+
outputDirConfig = options.output;
|
|
842
|
+
outputDirSource = "CLI flag (--output)";
|
|
843
|
+
} else if (config.default.content?.outputDir) {
|
|
844
|
+
outputDirConfig = config.default.content.outputDir;
|
|
845
|
+
outputDirSource = "edtools.config.js";
|
|
846
|
+
} else {
|
|
847
|
+
outputDirConfig = "./blog";
|
|
848
|
+
outputDirSource = "default";
|
|
544
849
|
}
|
|
545
|
-
const outputDir =
|
|
546
|
-
await
|
|
850
|
+
const outputDir = path4.resolve(projectPath, outputDirConfig);
|
|
851
|
+
await fs4.ensureDir(outputDir);
|
|
547
852
|
const hostingConfig = detectHostingConfig(projectPath);
|
|
548
853
|
if (hostingConfig) {
|
|
549
|
-
const validation = validateOutputDir(
|
|
854
|
+
const validation = validateOutputDir(outputDirConfig, hostingConfig);
|
|
550
855
|
if (!validation.valid) {
|
|
551
|
-
console.log(
|
|
552
|
-
console.log(
|
|
553
|
-
console.log(
|
|
554
|
-
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"));
|
|
555
860
|
console.log("");
|
|
556
|
-
console.log(
|
|
557
|
-
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"));
|
|
558
863
|
console.log("");
|
|
559
|
-
console.log(
|
|
560
|
-
console.log(
|
|
864
|
+
console.log(chalk4.cyan.bold("\u{1F4DD} Suggested fix:"));
|
|
865
|
+
console.log(chalk4.cyan(" Update edtools.config.js:"));
|
|
561
866
|
console.log("");
|
|
562
|
-
console.log(
|
|
563
|
-
console.log(
|
|
564
|
-
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(" }"));
|
|
565
870
|
console.log("");
|
|
566
|
-
const answer = await
|
|
871
|
+
const answer = await inquirer3.prompt([{
|
|
567
872
|
type: "confirm",
|
|
568
873
|
name: "continue",
|
|
569
874
|
message: "Continue anyway?",
|
|
570
875
|
default: false
|
|
571
876
|
}]);
|
|
572
877
|
if (!answer.continue) {
|
|
573
|
-
console.log(
|
|
878
|
+
console.log(chalk4.yellow("\n\u2717 Generation cancelled\n"));
|
|
574
879
|
process.exit(0);
|
|
575
880
|
}
|
|
576
881
|
console.log("");
|
|
577
882
|
}
|
|
578
883
|
}
|
|
579
|
-
console.log(
|
|
580
|
-
console.log(` Product: ${
|
|
581
|
-
console.log(` Category: ${
|
|
582
|
-
console.log(` AI Provider: ${
|
|
583
|
-
console.log(` Posts to generate: ${
|
|
584
|
-
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 + ")")}
|
|
585
890
|
`);
|
|
586
|
-
const generator = new ContentGenerator(apiKey,
|
|
891
|
+
const generator = new ContentGenerator(apiKey, actualProvider);
|
|
587
892
|
const generateConfig = {
|
|
588
893
|
productInfo,
|
|
589
894
|
topics,
|
|
@@ -592,20 +897,22 @@ async function generateCommand(options) {
|
|
|
592
897
|
projectPath,
|
|
593
898
|
avoidDuplicates: true,
|
|
594
899
|
similarityThreshold: 0.85,
|
|
595
|
-
provider,
|
|
900
|
+
provider: actualProvider,
|
|
596
901
|
apiKey,
|
|
597
|
-
dryRun: options.dryRun
|
|
902
|
+
dryRun: options.dryRun,
|
|
903
|
+
blogConfig: config.default.blog
|
|
904
|
+
// Pass blog config from edtools.config.js
|
|
598
905
|
};
|
|
599
|
-
const providerName =
|
|
906
|
+
const providerName = actualProvider === "anthropic" ? "Claude" : "ChatGPT";
|
|
600
907
|
const spinner = ora2(`Generating content with ${providerName}...`).start();
|
|
601
908
|
try {
|
|
602
909
|
const result = await generator.generate(generateConfig);
|
|
603
910
|
if (result.success && result.posts.length > 0) {
|
|
604
911
|
const dryRunMode = result.dryRun || false;
|
|
605
|
-
spinner.succeed(
|
|
912
|
+
spinner.succeed(chalk4.bold(dryRunMode ? "Preview generated successfully!" : "Content generated successfully!"));
|
|
606
913
|
console.log("");
|
|
607
914
|
if (dryRunMode) {
|
|
608
|
-
console.log(
|
|
915
|
+
console.log(chalk4.yellow.bold("\u{1F50D} DRY RUN MODE - No files were written\n"));
|
|
609
916
|
}
|
|
610
917
|
if (options.report === "json" || options.report === "pretty") {
|
|
611
918
|
const report = {
|
|
@@ -644,11 +951,11 @@ async function generateCommand(options) {
|
|
|
644
951
|
} else {
|
|
645
952
|
const table = new Table({
|
|
646
953
|
head: [
|
|
647
|
-
|
|
648
|
-
|
|
649
|
-
|
|
650
|
-
|
|
651
|
-
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")
|
|
652
959
|
],
|
|
653
960
|
colWidths: [5, 35, 10, 10, 45],
|
|
654
961
|
wordWrap: true,
|
|
@@ -660,29 +967,29 @@ async function generateCommand(options) {
|
|
|
660
967
|
result.posts.forEach((post, i) => {
|
|
661
968
|
const scoreColor = getScoreColorFn(post.seoScore);
|
|
662
969
|
table.push([
|
|
663
|
-
|
|
664
|
-
|
|
665
|
-
|
|
970
|
+
chalk4.gray((i + 1).toString()),
|
|
971
|
+
chalk4.white(truncateTitle(post.title, 33)),
|
|
972
|
+
chalk4.white(post.wordCount.toString()),
|
|
666
973
|
scoreColor(`${post.seoScore}/100`),
|
|
667
|
-
|
|
974
|
+
chalk4.gray(post.path)
|
|
668
975
|
]);
|
|
669
976
|
});
|
|
670
977
|
console.log(table.toString());
|
|
671
978
|
console.log("");
|
|
672
979
|
if (result.stats) {
|
|
673
|
-
console.log(
|
|
674
|
-
console.log(` Total words: ${
|
|
980
|
+
console.log(chalk4.cyan.bold("Statistics:\n"));
|
|
981
|
+
console.log(` Total words: ${chalk4.white(result.stats.totalWords)}`);
|
|
675
982
|
console.log(` Avg SEO score: ${getScoreColorFn(result.stats.avgSeoScore)(result.stats.avgSeoScore + "/100")}`);
|
|
676
|
-
console.log(` Avg read time: ${
|
|
983
|
+
console.log(` Avg read time: ${chalk4.white(result.stats.avgReadTime)}`);
|
|
677
984
|
console.log("");
|
|
678
985
|
}
|
|
679
986
|
if (dryRunMode) {
|
|
680
|
-
console.log(
|
|
987
|
+
console.log(chalk4.yellow.bold("Would create:\n"));
|
|
681
988
|
if (result.manifestPath) {
|
|
682
|
-
console.log(` ${
|
|
989
|
+
console.log(` ${chalk4.yellow("\u2022")} ${chalk4.white(result.manifestPath)} (manifest with ${result.posts.length} posts)`);
|
|
683
990
|
}
|
|
684
991
|
if (result.sitemapPath) {
|
|
685
|
-
console.log(` ${
|
|
992
|
+
console.log(` ${chalk4.yellow("\u2022")} ${chalk4.white(result.sitemapPath)} (sitemap with ${result.posts.length} URLs)`);
|
|
686
993
|
}
|
|
687
994
|
console.log("");
|
|
688
995
|
}
|
|
@@ -694,21 +1001,21 @@ async function generateCommand(options) {
|
|
|
694
1001
|
console.log(warningBox(warningText));
|
|
695
1002
|
}
|
|
696
1003
|
if (dryRunMode) {
|
|
697
|
-
console.log(
|
|
698
|
-
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
|
|
699
1006
|
`);
|
|
700
1007
|
} else {
|
|
701
|
-
console.log(
|
|
702
|
-
console.log(` ${
|
|
703
|
-
console.log(` ${
|
|
704
|
-
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`);
|
|
705
1012
|
console.log("");
|
|
706
|
-
console.log(
|
|
1013
|
+
console.log(chalk4.yellow(`\u{1F4A1} Tip: Wait 3-7 days before generating more posts to avoid spam detection
|
|
707
1014
|
`));
|
|
708
1015
|
}
|
|
709
1016
|
}
|
|
710
1017
|
} else {
|
|
711
|
-
spinner.fail(
|
|
1018
|
+
spinner.fail(chalk4.bold("Failed to generate content"));
|
|
712
1019
|
if (result.errors && result.errors.length > 0) {
|
|
713
1020
|
const errorText = result.errors.join("\n");
|
|
714
1021
|
console.log(errorBox(errorText));
|
|
@@ -716,16 +1023,16 @@ async function generateCommand(options) {
|
|
|
716
1023
|
process.exit(1);
|
|
717
1024
|
}
|
|
718
1025
|
} catch (error) {
|
|
719
|
-
spinner.fail(
|
|
1026
|
+
spinner.fail(chalk4.bold("Error during generation"));
|
|
720
1027
|
console.log(errorBox(error.message));
|
|
721
1028
|
process.exit(1);
|
|
722
1029
|
}
|
|
723
1030
|
}
|
|
724
1031
|
function getScoreColorFn(score) {
|
|
725
|
-
if (score >= 90) return
|
|
726
|
-
if (score >= 75) return
|
|
727
|
-
if (score >= 60) return
|
|
728
|
-
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;
|
|
729
1036
|
}
|
|
730
1037
|
function truncateTitle(title, maxLen) {
|
|
731
1038
|
if (title.length <= maxLen) return title;
|
|
@@ -733,17 +1040,17 @@ function truncateTitle(title, maxLen) {
|
|
|
733
1040
|
}
|
|
734
1041
|
|
|
735
1042
|
// src/cli/commands/analyze.ts
|
|
736
|
-
import
|
|
737
|
-
import
|
|
738
|
-
import
|
|
1043
|
+
import fs6 from "fs-extra";
|
|
1044
|
+
import path5 from "path";
|
|
1045
|
+
import chalk5 from "chalk";
|
|
739
1046
|
import ora3 from "ora";
|
|
740
1047
|
import Table2 from "cli-table3";
|
|
741
1048
|
|
|
742
1049
|
// src/integrations/gsc-csv-analyzer.ts
|
|
743
|
-
import
|
|
1050
|
+
import fs5 from "fs-extra";
|
|
744
1051
|
import { parse } from "csv-parse/sync";
|
|
745
1052
|
async function parseGSCCSV(filePath) {
|
|
746
|
-
const content = await
|
|
1053
|
+
const content = await fs5.readFile(filePath, "utf-8");
|
|
747
1054
|
const records = parse(content, {
|
|
748
1055
|
columns: true,
|
|
749
1056
|
skip_empty_lines: true,
|
|
@@ -839,34 +1146,34 @@ async function saveOpportunities(opportunities, outputPath) {
|
|
|
839
1146
|
count: opportunities.length,
|
|
840
1147
|
opportunities
|
|
841
1148
|
};
|
|
842
|
-
await
|
|
1149
|
+
await fs5.writeJson(outputPath, data, { spaces: 2 });
|
|
843
1150
|
}
|
|
844
1151
|
|
|
845
1152
|
// src/cli/commands/analyze.ts
|
|
846
1153
|
async function analyzeCommand(options) {
|
|
847
|
-
console.log(
|
|
1154
|
+
console.log(chalk5.cyan.bold("\n\u{1F4CA} Analyzing Google Search Console Data...\n"));
|
|
848
1155
|
const projectPath = process.cwd();
|
|
849
1156
|
let csvPath;
|
|
850
1157
|
if (options.file) {
|
|
851
|
-
csvPath =
|
|
1158
|
+
csvPath = path5.resolve(options.file);
|
|
852
1159
|
} else {
|
|
853
|
-
const files = await
|
|
1160
|
+
const files = await fs6.readdir(projectPath);
|
|
854
1161
|
const csvFiles = files.filter((f) => f.endsWith(".csv") && f.toLowerCase().includes("search"));
|
|
855
1162
|
if (csvFiles.length === 0) {
|
|
856
|
-
console.log(
|
|
857
|
-
console.log(
|
|
858
|
-
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"));
|
|
859
1166
|
process.exit(1);
|
|
860
1167
|
}
|
|
861
1168
|
if (csvFiles.length > 1) {
|
|
862
|
-
console.log(
|
|
1169
|
+
console.log(chalk5.yellow("\u26A0 Multiple CSV files found:"));
|
|
863
1170
|
csvFiles.forEach((f) => console.log(` - ${f}`));
|
|
864
|
-
console.log(
|
|
1171
|
+
console.log(chalk5.yellow("\nUsing first file. Specify with --file to use a different one.\n"));
|
|
865
1172
|
}
|
|
866
|
-
csvPath =
|
|
1173
|
+
csvPath = path5.join(projectPath, csvFiles[0]);
|
|
867
1174
|
}
|
|
868
|
-
if (!await
|
|
869
|
-
console.log(
|
|
1175
|
+
if (!await fs6.pathExists(csvPath)) {
|
|
1176
|
+
console.log(chalk5.red(`\u2717 File not found: ${csvPath}
|
|
870
1177
|
`));
|
|
871
1178
|
process.exit(1);
|
|
872
1179
|
}
|
|
@@ -874,10 +1181,10 @@ async function analyzeCommand(options) {
|
|
|
874
1181
|
let data;
|
|
875
1182
|
try {
|
|
876
1183
|
data = await parseGSCCSV(csvPath);
|
|
877
|
-
spinner.succeed(`Parsed ${
|
|
1184
|
+
spinner.succeed(`Parsed ${chalk5.white(data.length)} keywords from ${chalk5.white(path5.basename(csvPath))}`);
|
|
878
1185
|
} catch (error) {
|
|
879
1186
|
spinner.fail("Failed to parse CSV");
|
|
880
|
-
console.log(
|
|
1187
|
+
console.log(chalk5.red(`
|
|
881
1188
|
Error: ${error.message}
|
|
882
1189
|
`));
|
|
883
1190
|
process.exit(1);
|
|
@@ -892,7 +1199,7 @@ Error: ${error.message}
|
|
|
892
1199
|
analyzeSpinner.succeed("Analysis complete!");
|
|
893
1200
|
console.log("");
|
|
894
1201
|
const metricsTable = new Table2({
|
|
895
|
-
head: [
|
|
1202
|
+
head: [chalk5.cyan.bold("Metric"), chalk5.cyan.bold("Value")],
|
|
896
1203
|
colWidths: [30, 20],
|
|
897
1204
|
style: {
|
|
898
1205
|
head: [],
|
|
@@ -900,28 +1207,28 @@ Error: ${error.message}
|
|
|
900
1207
|
}
|
|
901
1208
|
});
|
|
902
1209
|
metricsTable.push(
|
|
903
|
-
["Total Keywords",
|
|
904
|
-
["Total Impressions",
|
|
905
|
-
["Total Clicks",
|
|
906
|
-
["Average CTR",
|
|
907
|
-
["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))]
|
|
908
1215
|
);
|
|
909
|
-
console.log(
|
|
1216
|
+
console.log(chalk5.bold.cyan("\u{1F4C8} Overall Metrics\n"));
|
|
910
1217
|
console.log(metricsTable.toString());
|
|
911
1218
|
console.log("");
|
|
912
1219
|
if (analysis.opportunities.length === 0) {
|
|
913
1220
|
console.log(warningBox("No opportunities found with current criteria.\nTry lowering --min-impressions or increasing --min-position."));
|
|
914
1221
|
} else {
|
|
915
|
-
console.log(
|
|
1222
|
+
console.log(chalk5.bold.cyan(`\u{1F3AF} Top ${analysis.opportunities.length} Content Opportunities
|
|
916
1223
|
`));
|
|
917
1224
|
const oppTable = new Table2({
|
|
918
1225
|
head: [
|
|
919
|
-
|
|
920
|
-
|
|
921
|
-
|
|
922
|
-
|
|
923
|
-
|
|
924
|
-
|
|
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")
|
|
925
1232
|
],
|
|
926
1233
|
colWidths: [5, 40, 14, 12, 12, 12],
|
|
927
1234
|
wordWrap: true,
|
|
@@ -931,34 +1238,34 @@ Error: ${error.message}
|
|
|
931
1238
|
}
|
|
932
1239
|
});
|
|
933
1240
|
analysis.opportunities.forEach((opp, i) => {
|
|
934
|
-
const priorityColor = opp.priority === "high" ?
|
|
1241
|
+
const priorityColor = opp.priority === "high" ? chalk5.green : opp.priority === "medium" ? chalk5.yellow : chalk5.gray;
|
|
935
1242
|
oppTable.push([
|
|
936
|
-
|
|
937
|
-
|
|
938
|
-
|
|
939
|
-
|
|
940
|
-
|
|
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}`),
|
|
941
1248
|
priorityColor(opp.priority.toUpperCase())
|
|
942
1249
|
]);
|
|
943
1250
|
});
|
|
944
1251
|
console.log(oppTable.toString());
|
|
945
1252
|
console.log("");
|
|
946
|
-
console.log(
|
|
1253
|
+
console.log(chalk5.bold.cyan("\u{1F4A1} Suggested Blog Post Titles\n"));
|
|
947
1254
|
analysis.opportunities.slice(0, 5).forEach((opp, i) => {
|
|
948
|
-
console.log(` ${
|
|
949
|
-
console.log(` ${
|
|
1255
|
+
console.log(` ${chalk5.cyan((i + 1).toString() + ".")} ${chalk5.white(opp.suggestedTitle)}`);
|
|
1256
|
+
console.log(` ${chalk5.gray(opp.reason)}
|
|
950
1257
|
`);
|
|
951
1258
|
});
|
|
952
1259
|
}
|
|
953
1260
|
if (analysis.topKeywords.length > 0) {
|
|
954
|
-
console.log(
|
|
1261
|
+
console.log(chalk5.bold.cyan("\u2B50 Top Performing Keywords (by clicks)\n"));
|
|
955
1262
|
const topTable = new Table2({
|
|
956
1263
|
head: [
|
|
957
|
-
|
|
958
|
-
|
|
959
|
-
|
|
960
|
-
|
|
961
|
-
|
|
1264
|
+
chalk5.cyan.bold("#"),
|
|
1265
|
+
chalk5.cyan.bold("Keyword"),
|
|
1266
|
+
chalk5.cyan.bold("Clicks"),
|
|
1267
|
+
chalk5.cyan.bold("Impressions"),
|
|
1268
|
+
chalk5.cyan.bold("CTR")
|
|
962
1269
|
],
|
|
963
1270
|
colWidths: [5, 40, 12, 14, 12],
|
|
964
1271
|
wordWrap: true,
|
|
@@ -969,39 +1276,39 @@ Error: ${error.message}
|
|
|
969
1276
|
});
|
|
970
1277
|
analysis.topKeywords.slice(0, 5).forEach((kw, i) => {
|
|
971
1278
|
topTable.push([
|
|
972
|
-
|
|
973
|
-
|
|
974
|
-
|
|
975
|
-
|
|
976
|
-
|
|
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) + "%")
|
|
977
1284
|
]);
|
|
978
1285
|
});
|
|
979
1286
|
console.log(topTable.toString());
|
|
980
1287
|
console.log("");
|
|
981
1288
|
}
|
|
982
|
-
const edtoolsDir =
|
|
983
|
-
await
|
|
984
|
-
const opportunitiesPath =
|
|
1289
|
+
const edtoolsDir = path5.join(projectPath, ".edtools");
|
|
1290
|
+
await fs6.ensureDir(edtoolsDir);
|
|
1291
|
+
const opportunitiesPath = path5.join(edtoolsDir, "opportunities.json");
|
|
985
1292
|
await saveOpportunities(analysis.opportunities, opportunitiesPath);
|
|
986
|
-
console.log(successBox(`Analysis saved to ${
|
|
987
|
-
console.log(
|
|
988
|
-
console.log(` ${
|
|
989
|
-
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"')}
|
|
990
1297
|
`);
|
|
991
|
-
console.log(
|
|
1298
|
+
console.log(chalk5.gray("\u{1F4A1} Use --from-csv to automatically generate posts for top opportunities\n"));
|
|
992
1299
|
}
|
|
993
1300
|
|
|
994
1301
|
// src/cli/commands/validate.ts
|
|
995
|
-
import
|
|
996
|
-
import
|
|
997
|
-
import
|
|
1302
|
+
import fs8 from "fs-extra";
|
|
1303
|
+
import path6 from "path";
|
|
1304
|
+
import chalk6 from "chalk";
|
|
998
1305
|
import Table3 from "cli-table3";
|
|
999
1306
|
|
|
1000
1307
|
// src/utils/seo-validator.ts
|
|
1001
1308
|
import * as cheerio from "cheerio";
|
|
1002
|
-
import
|
|
1309
|
+
import fs7 from "fs-extra";
|
|
1003
1310
|
async function validatePost(htmlPath) {
|
|
1004
|
-
const html = await
|
|
1311
|
+
const html = await fs7.readFile(htmlPath, "utf-8");
|
|
1005
1312
|
const $ = cheerio.load(html);
|
|
1006
1313
|
const issues = [];
|
|
1007
1314
|
const passed = [];
|
|
@@ -1224,19 +1531,19 @@ function calculateValidationStats(results) {
|
|
|
1224
1531
|
|
|
1225
1532
|
// src/cli/commands/validate.ts
|
|
1226
1533
|
async function validateCommand(options) {
|
|
1227
|
-
console.log(
|
|
1534
|
+
console.log(chalk6.cyan.bold("\n\u{1F4CA} Validating SEO quality...\n"));
|
|
1228
1535
|
const projectPath = process.cwd();
|
|
1229
1536
|
let htmlFiles = [];
|
|
1230
1537
|
if (options.post) {
|
|
1231
|
-
const postPath =
|
|
1232
|
-
if (!await
|
|
1538
|
+
const postPath = path6.resolve(projectPath, options.post);
|
|
1539
|
+
if (!await fs8.pathExists(postPath)) {
|
|
1233
1540
|
console.log(errorBox(`Post not found: ${postPath}`));
|
|
1234
1541
|
process.exit(1);
|
|
1235
1542
|
}
|
|
1236
1543
|
htmlFiles = [postPath];
|
|
1237
1544
|
} else if (options.posts) {
|
|
1238
|
-
const postsDir =
|
|
1239
|
-
if (!await
|
|
1545
|
+
const postsDir = path6.resolve(projectPath, options.posts);
|
|
1546
|
+
if (!await fs8.pathExists(postsDir)) {
|
|
1240
1547
|
console.log(errorBox(`Directory not found: ${postsDir}`));
|
|
1241
1548
|
process.exit(1);
|
|
1242
1549
|
}
|
|
@@ -1246,8 +1553,8 @@ async function validateCommand(options) {
|
|
|
1246
1553
|
process.exit(1);
|
|
1247
1554
|
}
|
|
1248
1555
|
} else {
|
|
1249
|
-
const defaultBlogDir =
|
|
1250
|
-
if (await
|
|
1556
|
+
const defaultBlogDir = path6.join(projectPath, "blog");
|
|
1557
|
+
if (await fs8.pathExists(defaultBlogDir)) {
|
|
1251
1558
|
htmlFiles = await findHtmlFiles(defaultBlogDir);
|
|
1252
1559
|
if (htmlFiles.length === 0) {
|
|
1253
1560
|
console.log(errorBox("No HTML files found in blog/ directory"));
|
|
@@ -1255,11 +1562,11 @@ async function validateCommand(options) {
|
|
|
1255
1562
|
}
|
|
1256
1563
|
} else {
|
|
1257
1564
|
console.log(errorBox("No posts directory specified"));
|
|
1258
|
-
console.log(
|
|
1565
|
+
console.log(chalk6.yellow("Usage: edtools validate --posts <dir> or --post <file>\n"));
|
|
1259
1566
|
process.exit(1);
|
|
1260
1567
|
}
|
|
1261
1568
|
}
|
|
1262
|
-
console.log(
|
|
1569
|
+
console.log(chalk6.cyan(`Found ${htmlFiles.length} post${htmlFiles.length > 1 ? "s" : ""} to validate
|
|
1263
1570
|
`));
|
|
1264
1571
|
const results = [];
|
|
1265
1572
|
for (const htmlFile of htmlFiles) {
|
|
@@ -1267,7 +1574,7 @@ async function validateCommand(options) {
|
|
|
1267
1574
|
const result = await validatePost(htmlFile);
|
|
1268
1575
|
results.push(result);
|
|
1269
1576
|
} catch (error) {
|
|
1270
|
-
console.log(
|
|
1577
|
+
console.log(chalk6.yellow(`\u26A0\uFE0F Failed to validate ${htmlFile}: ${error.message}`));
|
|
1271
1578
|
}
|
|
1272
1579
|
}
|
|
1273
1580
|
if (results.length === 0) {
|
|
@@ -1292,17 +1599,17 @@ async function validateCommand(options) {
|
|
|
1292
1599
|
})),
|
|
1293
1600
|
stats: stats2
|
|
1294
1601
|
};
|
|
1295
|
-
const outputPath =
|
|
1296
|
-
await
|
|
1602
|
+
const outputPath = path6.resolve(projectPath, options.output);
|
|
1603
|
+
await fs8.writeJson(outputPath, report, { spaces: 2 });
|
|
1297
1604
|
console.log(successBox(`Validation report saved to ${outputPath}`));
|
|
1298
1605
|
}
|
|
1299
1606
|
if (filteredResults.length > 0) {
|
|
1300
1607
|
const table = new Table3({
|
|
1301
1608
|
head: [
|
|
1302
|
-
|
|
1303
|
-
|
|
1304
|
-
|
|
1305
|
-
|
|
1609
|
+
chalk6.cyan.bold("#"),
|
|
1610
|
+
chalk6.cyan.bold("Title"),
|
|
1611
|
+
chalk6.cyan.bold("Score"),
|
|
1612
|
+
chalk6.cyan.bold("Issues")
|
|
1306
1613
|
],
|
|
1307
1614
|
colWidths: [5, 40, 10, 50],
|
|
1308
1615
|
wordWrap: true,
|
|
@@ -1315,12 +1622,12 @@ async function validateCommand(options) {
|
|
|
1315
1622
|
const scoreColor = getScoreColor(result.seoScore);
|
|
1316
1623
|
const issuesText = result.issues.length > 0 ? result.issues.map((issue) => {
|
|
1317
1624
|
const icon = issue.severity === "error" ? "\u2717" : issue.severity === "warning" ? "\u26A0" : "\u2139";
|
|
1318
|
-
const color = issue.severity === "error" ?
|
|
1625
|
+
const color = issue.severity === "error" ? chalk6.red : issue.severity === "warning" ? chalk6.yellow : chalk6.blue;
|
|
1319
1626
|
return color(`${icon} ${issue.message}`);
|
|
1320
|
-
}).join("\n") :
|
|
1627
|
+
}).join("\n") : chalk6.green("\u2713 No issues");
|
|
1321
1628
|
table.push([
|
|
1322
|
-
|
|
1323
|
-
|
|
1629
|
+
chalk6.gray((i + 1).toString()),
|
|
1630
|
+
chalk6.white(truncate(result.title, 38)),
|
|
1324
1631
|
scoreColor(`${result.seoScore}/100`),
|
|
1325
1632
|
issuesText
|
|
1326
1633
|
]);
|
|
@@ -1331,39 +1638,39 @@ async function validateCommand(options) {
|
|
|
1331
1638
|
console.log(successBox(`All posts have SEO score >= ${threshold}`));
|
|
1332
1639
|
}
|
|
1333
1640
|
const stats = calculateValidationStats(results);
|
|
1334
|
-
console.log(
|
|
1335
|
-
console.log(` Total posts validated: ${
|
|
1641
|
+
console.log(chalk6.cyan.bold("Overall Statistics:\n"));
|
|
1642
|
+
console.log(` Total posts validated: ${chalk6.white(stats.totalPosts)}`);
|
|
1336
1643
|
console.log(` Average SEO score: ${getScoreColor(stats.avgSeoScore)(stats.avgSeoScore + "/100")}`);
|
|
1337
|
-
console.log(` Posts with issues: ${
|
|
1338
|
-
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)}`);
|
|
1339
1646
|
console.log("");
|
|
1340
1647
|
if (stats.totalIssues > 0) {
|
|
1341
|
-
console.log(
|
|
1648
|
+
console.log(chalk6.cyan.bold("Issues by Severity:\n"));
|
|
1342
1649
|
if (stats.issuesBySeverity.error > 0) {
|
|
1343
|
-
console.log(` ${
|
|
1650
|
+
console.log(` ${chalk6.red("\u2717 Errors:")} ${chalk6.white(stats.issuesBySeverity.error)}`);
|
|
1344
1651
|
}
|
|
1345
1652
|
if (stats.issuesBySeverity.warning > 0) {
|
|
1346
|
-
console.log(` ${
|
|
1653
|
+
console.log(` ${chalk6.yellow("\u26A0 Warnings:")} ${chalk6.white(stats.issuesBySeverity.warning)}`);
|
|
1347
1654
|
}
|
|
1348
1655
|
if (stats.issuesBySeverity.info > 0) {
|
|
1349
|
-
console.log(` ${
|
|
1656
|
+
console.log(` ${chalk6.blue("\u2139 Info:")} ${chalk6.white(stats.issuesBySeverity.info)}`);
|
|
1350
1657
|
}
|
|
1351
1658
|
console.log("");
|
|
1352
1659
|
}
|
|
1353
1660
|
if (Object.keys(stats.issuesByCategory).length > 0) {
|
|
1354
|
-
console.log(
|
|
1661
|
+
console.log(chalk6.cyan.bold("Top Issue Categories:\n"));
|
|
1355
1662
|
const sortedCategories = Object.entries(stats.issuesByCategory).sort((a, b) => b[1] - a[1]).slice(0, 5);
|
|
1356
1663
|
sortedCategories.forEach(([category, count]) => {
|
|
1357
|
-
console.log(` ${
|
|
1664
|
+
console.log(` ${chalk6.white(capitalize(category))}: ${chalk6.yellow(count)}`);
|
|
1358
1665
|
});
|
|
1359
1666
|
console.log("");
|
|
1360
1667
|
}
|
|
1361
1668
|
if (stats.avgSeoScore < 85) {
|
|
1362
|
-
console.log(
|
|
1363
|
-
console.log(
|
|
1364
|
-
console.log(
|
|
1365
|
-
console.log(
|
|
1366
|
-
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"));
|
|
1367
1674
|
console.log("");
|
|
1368
1675
|
} else {
|
|
1369
1676
|
console.log(successBox(`Great job! Average SEO score is ${stats.avgSeoScore}/100`));
|
|
@@ -1371,9 +1678,9 @@ async function validateCommand(options) {
|
|
|
1371
1678
|
}
|
|
1372
1679
|
async function findHtmlFiles(dir) {
|
|
1373
1680
|
const files = [];
|
|
1374
|
-
const entries = await
|
|
1681
|
+
const entries = await fs8.readdir(dir, { withFileTypes: true });
|
|
1375
1682
|
for (const entry of entries) {
|
|
1376
|
-
const fullPath =
|
|
1683
|
+
const fullPath = path6.join(dir, entry.name);
|
|
1377
1684
|
if (entry.isDirectory()) {
|
|
1378
1685
|
const subFiles = await findHtmlFiles(fullPath);
|
|
1379
1686
|
files.push(...subFiles);
|
|
@@ -1384,10 +1691,10 @@ async function findHtmlFiles(dir) {
|
|
|
1384
1691
|
return files;
|
|
1385
1692
|
}
|
|
1386
1693
|
function getScoreColor(score) {
|
|
1387
|
-
if (score >= 90) return
|
|
1388
|
-
if (score >= 75) return
|
|
1389
|
-
if (score >= 60) return
|
|
1390
|
-
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;
|
|
1391
1698
|
}
|
|
1392
1699
|
function truncate(str, maxLen) {
|
|
1393
1700
|
if (str.length <= maxLen) return str;
|
|
@@ -1398,22 +1705,22 @@ function capitalize(str) {
|
|
|
1398
1705
|
}
|
|
1399
1706
|
|
|
1400
1707
|
// src/cli/commands/doctor.ts
|
|
1401
|
-
import
|
|
1402
|
-
import
|
|
1708
|
+
import fs9 from "fs-extra";
|
|
1709
|
+
import path7 from "path";
|
|
1403
1710
|
import { pathToFileURL as pathToFileURL2 } from "url";
|
|
1404
|
-
import
|
|
1711
|
+
import chalk7 from "chalk";
|
|
1405
1712
|
async function doctorCommand(options) {
|
|
1406
|
-
console.log(
|
|
1713
|
+
console.log(chalk7.cyan.bold("\n\u{1F50D} Diagnosing project configuration...\n"));
|
|
1407
1714
|
const projectPath = process.cwd();
|
|
1408
|
-
const configPath =
|
|
1715
|
+
const configPath = path7.join(projectPath, "edtools.config.js");
|
|
1409
1716
|
const issues = [];
|
|
1410
1717
|
const warnings = [];
|
|
1411
1718
|
const suggestions = [];
|
|
1412
|
-
if (await
|
|
1413
|
-
console.log(
|
|
1719
|
+
if (await fs9.pathExists(configPath)) {
|
|
1720
|
+
console.log(chalk7.green("\u2713 Configuration file found: ") + chalk7.white("edtools.config.js"));
|
|
1414
1721
|
} else {
|
|
1415
|
-
console.log(
|
|
1416
|
-
console.log(
|
|
1722
|
+
console.log(chalk7.red("\u2717 Configuration file not found"));
|
|
1723
|
+
console.log(chalk7.yellow(' Run "edtools init" to create configuration\n'));
|
|
1417
1724
|
process.exit(1);
|
|
1418
1725
|
}
|
|
1419
1726
|
let productInfo;
|
|
@@ -1428,135 +1735,130 @@ async function doctorCommand(options) {
|
|
|
1428
1735
|
}
|
|
1429
1736
|
} catch (error) {
|
|
1430
1737
|
issues.push(`Failed to load edtools.config.js: ${error.message}`);
|
|
1431
|
-
console.log(
|
|
1432
|
-
console.log(
|
|
1738
|
+
console.log(chalk7.red("\u2717 Failed to load configuration file"));
|
|
1739
|
+
console.log(chalk7.red(` Error: ${error.message}
|
|
1433
1740
|
`));
|
|
1434
1741
|
process.exit(1);
|
|
1435
1742
|
}
|
|
1436
|
-
const outputDirPath =
|
|
1437
|
-
if (await
|
|
1438
|
-
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));
|
|
1439
1746
|
} else {
|
|
1440
1747
|
warnings.push(`Output directory does not exist: ${outputDir}`);
|
|
1441
|
-
console.log(
|
|
1442
|
-
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"'));
|
|
1443
1750
|
}
|
|
1444
|
-
const edtoolsConfigPath =
|
|
1751
|
+
const edtoolsConfigPath = path7.join(projectPath, ".edtools", "config.json");
|
|
1445
1752
|
let hasApiKey = false;
|
|
1446
|
-
if (await
|
|
1753
|
+
if (await fs9.pathExists(edtoolsConfigPath)) {
|
|
1447
1754
|
try {
|
|
1448
|
-
const edtoolsConfig = await
|
|
1755
|
+
const edtoolsConfig = await fs9.readJson(edtoolsConfigPath);
|
|
1449
1756
|
hasApiKey = !!edtoolsConfig.apiKey;
|
|
1450
1757
|
} catch (error) {
|
|
1451
1758
|
}
|
|
1452
1759
|
}
|
|
1453
1760
|
const hasEnvApiKey = !!(process.env.ANTHROPIC_API_KEY || process.env.OPENAI_API_KEY);
|
|
1454
1761
|
if (hasApiKey || hasEnvApiKey) {
|
|
1455
|
-
console.log(
|
|
1762
|
+
console.log(chalk7.green("\u2713 API key configured"));
|
|
1456
1763
|
} else {
|
|
1457
1764
|
warnings.push('No API key found (set via environment variable or "edtools init")');
|
|
1458
|
-
console.log(
|
|
1459
|
-
console.log(
|
|
1460
|
-
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'));
|
|
1461
1768
|
}
|
|
1462
1769
|
console.log("");
|
|
1463
1770
|
const hostingConfig = detectHostingConfig(projectPath);
|
|
1464
1771
|
if (hostingConfig) {
|
|
1465
|
-
console.log(
|
|
1466
|
-
console.log(
|
|
1467
|
-
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));
|
|
1468
1775
|
const validation = validateOutputDir(outputDir, hostingConfig);
|
|
1469
1776
|
if (!validation.valid) {
|
|
1470
1777
|
issues.push("Output directory misconfiguration");
|
|
1471
|
-
console.log(
|
|
1778
|
+
console.log(chalk7.red(" - Problem: Files outside ") + chalk7.white(hostingConfig.publicDir + "/") + chalk7.red(" won't be accessible"));
|
|
1472
1779
|
console.log("");
|
|
1473
|
-
console.log(
|
|
1474
|
-
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"));
|
|
1475
1782
|
console.log("");
|
|
1476
|
-
console.log(
|
|
1477
|
-
console.log(
|
|
1783
|
+
console.log(chalk7.cyan.bold("\u{1F4DD} Suggested fix:"));
|
|
1784
|
+
console.log(chalk7.cyan(" Update edtools.config.js:"));
|
|
1478
1785
|
console.log("");
|
|
1479
|
-
console.log(
|
|
1480
|
-
console.log(
|
|
1481
|
-
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(" }"));
|
|
1482
1789
|
console.log("");
|
|
1483
1790
|
suggestions.push(`Update outputDir to '${validation.suggestion}'`);
|
|
1484
1791
|
if (options.fix) {
|
|
1485
|
-
console.log(
|
|
1792
|
+
console.log(chalk7.yellow.bold("\u{1F527} Auto-fixing configuration...\n"));
|
|
1486
1793
|
try {
|
|
1487
|
-
const configContent = await
|
|
1794
|
+
const configContent = await fs9.readFile(configPath, "utf-8");
|
|
1488
1795
|
const updatedConfig = configContent.replace(
|
|
1489
1796
|
/outputDir:\s*['"]([^'"]+)['"]/,
|
|
1490
1797
|
`outputDir: '${validation.suggestion}'`
|
|
1491
1798
|
);
|
|
1492
|
-
await
|
|
1493
|
-
console.log(
|
|
1494
|
-
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}'`));
|
|
1495
1802
|
console.log("");
|
|
1496
|
-
console.log(
|
|
1803
|
+
console.log(chalk7.cyan('\u{1F389} Configuration fixed! Run "edtools generate" to create content.\n'));
|
|
1497
1804
|
} catch (error) {
|
|
1498
|
-
console.log(
|
|
1499
|
-
console.log(
|
|
1500
|
-
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"));
|
|
1501
1808
|
}
|
|
1502
1809
|
} else {
|
|
1503
|
-
console.log(
|
|
1504
|
-
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"));
|
|
1505
1812
|
}
|
|
1506
1813
|
} else {
|
|
1507
|
-
console.log(
|
|
1814
|
+
console.log(chalk7.green(" - Status: \u2705 Output directory correctly configured"));
|
|
1508
1815
|
console.log("");
|
|
1509
1816
|
}
|
|
1510
1817
|
} else {
|
|
1511
|
-
console.log(
|
|
1512
|
-
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)"));
|
|
1513
1820
|
console.log("");
|
|
1514
1821
|
}
|
|
1515
|
-
console.log(
|
|
1822
|
+
console.log(chalk7.cyan.bold("Summary:\n"));
|
|
1516
1823
|
if (issues.length === 0 && warnings.length === 0) {
|
|
1517
|
-
console.log(
|
|
1824
|
+
console.log(chalk7.green.bold("\u2705 All checks passed! Your project is ready to generate content.\n"));
|
|
1518
1825
|
} else {
|
|
1519
1826
|
if (issues.length > 0) {
|
|
1520
|
-
console.log(
|
|
1827
|
+
console.log(chalk7.red.bold(`\u274C ${issues.length} issue(s) found:`));
|
|
1521
1828
|
issues.forEach((issue) => {
|
|
1522
|
-
console.log(
|
|
1829
|
+
console.log(chalk7.red(` - ${issue}`));
|
|
1523
1830
|
});
|
|
1524
1831
|
console.log("");
|
|
1525
1832
|
}
|
|
1526
1833
|
if (warnings.length > 0) {
|
|
1527
|
-
console.log(
|
|
1834
|
+
console.log(chalk7.yellow.bold(`\u26A0\uFE0F ${warnings.length} warning(s):`));
|
|
1528
1835
|
warnings.forEach((warning) => {
|
|
1529
|
-
console.log(
|
|
1836
|
+
console.log(chalk7.yellow(` - ${warning}`));
|
|
1530
1837
|
});
|
|
1531
1838
|
console.log("");
|
|
1532
1839
|
}
|
|
1533
1840
|
if (suggestions.length > 0 && !options.fix) {
|
|
1534
|
-
console.log(
|
|
1841
|
+
console.log(chalk7.cyan.bold("\u{1F4A1} Suggestions:"));
|
|
1535
1842
|
suggestions.forEach((suggestion) => {
|
|
1536
|
-
console.log(
|
|
1843
|
+
console.log(chalk7.cyan(` - ${suggestion}`));
|
|
1537
1844
|
});
|
|
1538
1845
|
console.log("");
|
|
1539
|
-
console.log(
|
|
1846
|
+
console.log(chalk7.cyan("Run ") + chalk7.white("edtools doctor --fix") + chalk7.cyan(" to automatically apply fixes\n"));
|
|
1540
1847
|
}
|
|
1541
1848
|
}
|
|
1542
1849
|
}
|
|
1543
1850
|
|
|
1544
1851
|
// src/cli/index.ts
|
|
1545
1852
|
var program = new Command();
|
|
1546
|
-
program.name("edtools").description("AI-Powered Content Marketing CLI - Generate, validate, and optimize SEO content").version("0.6.
|
|
1853
|
+
program.name("edtools").description("AI-Powered Content Marketing CLI - Generate, validate, and optimize SEO content").version("0.6.2");
|
|
1547
1854
|
program.command("init").description("Initialize edtools in your project").option("-p, --path <path>", "Project path", process.cwd()).action(initCommand);
|
|
1548
|
-
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
|
|
1855
|
+
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);
|
|
1549
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);
|
|
1550
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);
|
|
1551
1858
|
program.command("doctor").description("Diagnose project configuration and hosting setup").option("--fix", "Automatically fix issues").action(doctorCommand);
|
|
1552
|
-
program.command("config").description("
|
|
1553
|
-
|
|
1554
|
-
|
|
1555
|
-
} else {
|
|
1556
|
-
console.log(chalk7.cyan("Configuration:"));
|
|
1557
|
-
console.log(` API Key: ${process.env.ANTHROPIC_API_KEY ? "[set]" : "[not set]"}`);
|
|
1558
|
-
}
|
|
1559
|
-
});
|
|
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);
|
|
1560
1862
|
if (process.argv.length === 2) {
|
|
1561
1863
|
showWelcome();
|
|
1562
1864
|
process.exit(0);
|