@edtools/cli 0.5.0 → 0.6.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/chunk-BI3UJPWA.js +690 -0
- package/dist/chunk-JCUQ7D56.js +688 -0
- package/dist/cli/commands/doctor.d.ts +6 -0
- package/dist/cli/commands/doctor.d.ts.map +1 -0
- package/dist/cli/commands/doctor.js +150 -0
- package/dist/cli/commands/doctor.js.map +1 -0
- package/dist/cli/commands/generate.d.ts +2 -0
- package/dist/cli/commands/generate.d.ts.map +1 -1
- package/dist/cli/commands/generate.js +148 -38
- 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 +11 -1
- package/dist/cli/commands/init.js.map +1 -1
- package/dist/cli/commands/validate.d.ts +9 -0
- package/dist/cli/commands/validate.d.ts.map +1 -0
- package/dist/cli/commands/validate.js +196 -0
- package/dist/cli/commands/validate.js.map +1 -0
- package/dist/cli/index.js +910 -87
- package/dist/cli/index.js.map +1 -1
- package/dist/core/generator.d.ts.map +1 -1
- package/dist/core/generator.js +36 -8
- package/dist/core/generator.js.map +1 -1
- package/dist/index.d.ts +64 -1
- package/dist/index.js +1 -1
- package/dist/types/content.d.ts +44 -0
- package/dist/types/content.d.ts.map +1 -1
- package/dist/types/hosting.d.ts +19 -0
- package/dist/types/hosting.d.ts.map +1 -0
- package/dist/types/hosting.js +2 -0
- package/dist/types/hosting.js.map +1 -0
- package/dist/types/index.d.ts +1 -0
- package/dist/types/index.d.ts.map +1 -1
- package/dist/types/index.js +1 -0
- package/dist/types/index.js.map +1 -1
- package/dist/ui/banner.d.ts.map +1 -1
- package/dist/ui/banner.js +9 -1
- package/dist/ui/banner.js.map +1 -1
- package/dist/utils/hosting-detection.d.ts +6 -0
- package/dist/utils/hosting-detection.d.ts.map +1 -0
- package/dist/utils/hosting-detection.js +173 -0
- package/dist/utils/hosting-detection.js.map +1 -0
- package/dist/utils/seo-validator.d.ts +35 -0
- package/dist/utils/seo-validator.d.ts.map +1 -0
- package/dist/utils/seo-validator.js +244 -0
- package/dist/utils/seo-validator.js.map +1 -0
- package/package.json +4 -3
package/dist/cli/index.js
CHANGED
|
@@ -1,16 +1,16 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
import {
|
|
3
3
|
ContentGenerator
|
|
4
|
-
} from "../chunk-
|
|
4
|
+
} from "../chunk-BI3UJPWA.js";
|
|
5
5
|
import "../chunk-INVECVSW.js";
|
|
6
6
|
|
|
7
7
|
// src/cli/index.ts
|
|
8
8
|
import { Command } from "commander";
|
|
9
|
-
import
|
|
9
|
+
import chalk7 from "chalk";
|
|
10
10
|
|
|
11
11
|
// src/cli/commands/init.ts
|
|
12
|
-
import
|
|
13
|
-
import
|
|
12
|
+
import fs2 from "fs-extra";
|
|
13
|
+
import path2 from "path";
|
|
14
14
|
import chalk2 from "chalk";
|
|
15
15
|
import ora from "ora";
|
|
16
16
|
import inquirer from "inquirer";
|
|
@@ -49,14 +49,22 @@ function showWelcome() {
|
|
|
49
49
|
console.log("");
|
|
50
50
|
console.log(` ${chalk.cyan("edtools init")} ${chalk.gray("Initialize your project")}`);
|
|
51
51
|
console.log(` ${chalk.cyan("edtools generate")} ${chalk.gray("Generate SEO-optimized posts")}`);
|
|
52
|
+
console.log(` ${chalk.cyan("edtools validate")} ${chalk.gray("Validate post quality")}`);
|
|
52
53
|
console.log(` ${chalk.cyan("edtools analyze")} ${chalk.gray("Analyze Google Search Console CSV")}`);
|
|
53
54
|
console.log("");
|
|
55
|
+
console.log(chalk.bold.cyan(" Advanced Usage:"));
|
|
56
|
+
console.log(chalk.gray(" \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500"));
|
|
57
|
+
console.log("");
|
|
58
|
+
console.log(` ${chalk.cyan("edtools generate --dry-run")} ${chalk.gray("Preview generation")}`);
|
|
59
|
+
console.log(` ${chalk.cyan("edtools generate --report json")} ${chalk.gray("JSON output")}`);
|
|
60
|
+
console.log(` ${chalk.cyan("edtools validate --threshold 85")} ${chalk.gray("Show posts < 85")}`);
|
|
61
|
+
console.log("");
|
|
54
62
|
console.log(chalk.gray(" Run ") + chalk.cyan("edtools --help") + chalk.gray(" for all commands"));
|
|
55
63
|
console.log("");
|
|
56
64
|
}
|
|
57
65
|
function getVersion() {
|
|
58
66
|
try {
|
|
59
|
-
return "0.
|
|
67
|
+
return "0.6.0";
|
|
60
68
|
} catch {
|
|
61
69
|
return "dev";
|
|
62
70
|
}
|
|
@@ -96,12 +104,160 @@ function warningBox(message) {
|
|
|
96
104
|
});
|
|
97
105
|
}
|
|
98
106
|
|
|
107
|
+
// src/utils/hosting-detection.ts
|
|
108
|
+
import fs from "fs";
|
|
109
|
+
import path from "path";
|
|
110
|
+
import yaml from "yaml";
|
|
111
|
+
var detectors = [
|
|
112
|
+
{
|
|
113
|
+
name: "Firebase",
|
|
114
|
+
file: "firebase.json",
|
|
115
|
+
parse: (content, projectPath) => {
|
|
116
|
+
try {
|
|
117
|
+
const config = JSON.parse(content);
|
|
118
|
+
const publicDir = config.hosting?.public || "public";
|
|
119
|
+
return {
|
|
120
|
+
platform: "Firebase",
|
|
121
|
+
publicDir,
|
|
122
|
+
configFile: "firebase.json"
|
|
123
|
+
};
|
|
124
|
+
} catch (error) {
|
|
125
|
+
return null;
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
},
|
|
129
|
+
{
|
|
130
|
+
name: "AWS Amplify",
|
|
131
|
+
file: "amplify.yml",
|
|
132
|
+
parse: (content, projectPath) => {
|
|
133
|
+
try {
|
|
134
|
+
const config = yaml.parse(content);
|
|
135
|
+
const baseDirectory = config?.frontend?.artifacts?.baseDirectory || "build";
|
|
136
|
+
return {
|
|
137
|
+
platform: "AWS Amplify",
|
|
138
|
+
publicDir: baseDirectory,
|
|
139
|
+
configFile: "amplify.yml"
|
|
140
|
+
};
|
|
141
|
+
} catch (error) {
|
|
142
|
+
return null;
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
},
|
|
146
|
+
{
|
|
147
|
+
name: "Vercel",
|
|
148
|
+
file: "vercel.json",
|
|
149
|
+
parse: (content, projectPath) => {
|
|
150
|
+
try {
|
|
151
|
+
const config = JSON.parse(content);
|
|
152
|
+
const outputDirectory = config.outputDirectory || config.buildCommand?.match(/out|dist|build/)?.[0] || "public";
|
|
153
|
+
return {
|
|
154
|
+
platform: "Vercel",
|
|
155
|
+
publicDir: outputDirectory,
|
|
156
|
+
configFile: "vercel.json"
|
|
157
|
+
};
|
|
158
|
+
} catch (error) {
|
|
159
|
+
return null;
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
},
|
|
163
|
+
{
|
|
164
|
+
name: "Netlify",
|
|
165
|
+
file: "netlify.toml",
|
|
166
|
+
parse: (content, projectPath) => {
|
|
167
|
+
try {
|
|
168
|
+
const match = content.match(/publish\s*=\s*"([^"]+)"/);
|
|
169
|
+
const publicDir = match?.[1] || "public";
|
|
170
|
+
return {
|
|
171
|
+
platform: "Netlify",
|
|
172
|
+
publicDir,
|
|
173
|
+
configFile: "netlify.toml"
|
|
174
|
+
};
|
|
175
|
+
} catch (error) {
|
|
176
|
+
return null;
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
},
|
|
180
|
+
{
|
|
181
|
+
name: "Next.js",
|
|
182
|
+
file: "next.config.js",
|
|
183
|
+
parse: (content, projectPath) => {
|
|
184
|
+
if (content.includes("output:") && content.includes("export")) {
|
|
185
|
+
const packageJsonPath = path.join(projectPath, "package.json");
|
|
186
|
+
if (fs.existsSync(packageJsonPath)) {
|
|
187
|
+
const packageJson = JSON.parse(fs.readFileSync(packageJsonPath, "utf-8"));
|
|
188
|
+
const buildScript = packageJson.scripts?.build || "";
|
|
189
|
+
if (buildScript.includes("out")) {
|
|
190
|
+
return {
|
|
191
|
+
platform: "Next.js (Static Export)",
|
|
192
|
+
publicDir: "out",
|
|
193
|
+
configFile: "next.config.js"
|
|
194
|
+
};
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
return {
|
|
198
|
+
platform: "Next.js (Static Export)",
|
|
199
|
+
publicDir: "out",
|
|
200
|
+
configFile: "next.config.js"
|
|
201
|
+
};
|
|
202
|
+
}
|
|
203
|
+
return null;
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
];
|
|
207
|
+
function detectHostingConfig(projectPath) {
|
|
208
|
+
for (const detector of detectors) {
|
|
209
|
+
const configPath = path.join(projectPath, detector.file);
|
|
210
|
+
if (fs.existsSync(configPath)) {
|
|
211
|
+
try {
|
|
212
|
+
const content = fs.readFileSync(configPath, "utf-8");
|
|
213
|
+
const result = detector.parse(content, projectPath);
|
|
214
|
+
if (result) {
|
|
215
|
+
return result;
|
|
216
|
+
}
|
|
217
|
+
} catch (error) {
|
|
218
|
+
console.error(`Error parsing ${detector.file}:`, error);
|
|
219
|
+
}
|
|
220
|
+
}
|
|
221
|
+
}
|
|
222
|
+
return null;
|
|
223
|
+
}
|
|
224
|
+
function validateOutputDir(outputDir, hostingConfig) {
|
|
225
|
+
if (!hostingConfig) {
|
|
226
|
+
return { valid: true };
|
|
227
|
+
}
|
|
228
|
+
const normalizedOutputDir = outputDir.replace(/^\.\//, "");
|
|
229
|
+
const normalizedPublicDir = hostingConfig.publicDir.replace(/^\.\//, "");
|
|
230
|
+
const isInPublicDir = normalizedOutputDir.startsWith(`${normalizedPublicDir}/`) || normalizedOutputDir === normalizedPublicDir;
|
|
231
|
+
const issues = [];
|
|
232
|
+
if (!isInPublicDir) {
|
|
233
|
+
issues.push(
|
|
234
|
+
`Output directory "${outputDir}" is outside the public directory "${hostingConfig.publicDir}"`
|
|
235
|
+
);
|
|
236
|
+
issues.push("Generated files will not be accessible in production");
|
|
237
|
+
}
|
|
238
|
+
return {
|
|
239
|
+
valid: isInPublicDir,
|
|
240
|
+
platform: hostingConfig.platform,
|
|
241
|
+
publicDir: hostingConfig.publicDir,
|
|
242
|
+
currentOutputDir: outputDir,
|
|
243
|
+
suggestion: `./${normalizedPublicDir}/blog`,
|
|
244
|
+
issues: issues.length > 0 ? issues : void 0
|
|
245
|
+
};
|
|
246
|
+
}
|
|
247
|
+
function getSuggestedOutputDir(projectPath) {
|
|
248
|
+
const hostingConfig = detectHostingConfig(projectPath);
|
|
249
|
+
if (hostingConfig) {
|
|
250
|
+
return `./${hostingConfig.publicDir}/blog`;
|
|
251
|
+
}
|
|
252
|
+
return "./blog";
|
|
253
|
+
}
|
|
254
|
+
|
|
99
255
|
// src/cli/commands/init.ts
|
|
100
256
|
async function initCommand(options) {
|
|
101
257
|
console.log(chalk2.cyan.bold("\n\u{1F680} Initializing Edtools...\n"));
|
|
102
|
-
const projectPath =
|
|
103
|
-
const configPath =
|
|
104
|
-
if (await
|
|
258
|
+
const projectPath = path2.resolve(options.path);
|
|
259
|
+
const configPath = path2.join(projectPath, "edtools.config.js");
|
|
260
|
+
if (await fs2.pathExists(configPath)) {
|
|
105
261
|
const { overwrite } = await inquirer.prompt([
|
|
106
262
|
{
|
|
107
263
|
type: "confirm",
|
|
@@ -118,9 +274,9 @@ async function initCommand(options) {
|
|
|
118
274
|
const spinner = ora("Analyzing your landing page...").start();
|
|
119
275
|
let productInfo = {};
|
|
120
276
|
try {
|
|
121
|
-
const indexPath =
|
|
122
|
-
if (await
|
|
123
|
-
const html = await
|
|
277
|
+
const indexPath = path2.join(projectPath, "index.html");
|
|
278
|
+
if (await fs2.pathExists(indexPath)) {
|
|
279
|
+
const html = await fs2.readFile(indexPath, "utf-8");
|
|
124
280
|
const $ = loadCheerio(html);
|
|
125
281
|
productInfo = {
|
|
126
282
|
name: $("title").text() || $("h1").first().text() || "My Product",
|
|
@@ -240,6 +396,15 @@ async function initCommand(options) {
|
|
|
240
396
|
...productInfo,
|
|
241
397
|
...answers
|
|
242
398
|
};
|
|
399
|
+
const suggestedOutputDir = getSuggestedOutputDir(projectPath);
|
|
400
|
+
const hostingConfig = detectHostingConfig(projectPath);
|
|
401
|
+
if (hostingConfig) {
|
|
402
|
+
console.log("");
|
|
403
|
+
console.log(chalk2.cyan("\u2713 Detected hosting platform: ") + chalk2.white(hostingConfig.platform));
|
|
404
|
+
console.log(chalk2.cyan(" Public directory: ") + chalk2.white(hostingConfig.publicDir));
|
|
405
|
+
console.log(chalk2.cyan(" Suggested blog directory: ") + chalk2.white(suggestedOutputDir));
|
|
406
|
+
console.log("");
|
|
407
|
+
}
|
|
243
408
|
const config = `module.exports = {
|
|
244
409
|
product: {
|
|
245
410
|
name: ${JSON.stringify(finalProductInfo.name)},
|
|
@@ -253,7 +418,7 @@ async function initCommand(options) {
|
|
|
253
418
|
},
|
|
254
419
|
|
|
255
420
|
content: {
|
|
256
|
-
outputDir:
|
|
421
|
+
outputDir: ${JSON.stringify(suggestedOutputDir)},
|
|
257
422
|
generateBlog: true,
|
|
258
423
|
},
|
|
259
424
|
|
|
@@ -264,17 +429,17 @@ async function initCommand(options) {
|
|
|
264
429
|
},
|
|
265
430
|
};
|
|
266
431
|
`;
|
|
267
|
-
await
|
|
268
|
-
const edtoolsDir =
|
|
269
|
-
await
|
|
432
|
+
await fs2.writeFile(configPath, config, "utf-8");
|
|
433
|
+
const edtoolsDir = path2.join(projectPath, ".edtools");
|
|
434
|
+
await fs2.ensureDir(edtoolsDir);
|
|
270
435
|
const edtoolsConfig = {
|
|
271
436
|
apiKey: apiKeyAnswer.apiKey,
|
|
272
437
|
provider: finalProductInfo.preferredProvider
|
|
273
438
|
};
|
|
274
|
-
const edtoolsConfigPath =
|
|
275
|
-
await
|
|
276
|
-
const gitignorePath =
|
|
277
|
-
await
|
|
439
|
+
const edtoolsConfigPath = path2.join(edtoolsDir, "config.json");
|
|
440
|
+
await fs2.writeFile(edtoolsConfigPath, JSON.stringify(edtoolsConfig, null, 2), "utf-8");
|
|
441
|
+
const gitignorePath = path2.join(edtoolsDir, ".gitignore");
|
|
442
|
+
await fs2.writeFile(gitignorePath, "*\n!.gitignore\n", "utf-8");
|
|
278
443
|
console.log("");
|
|
279
444
|
console.log(successBox("Configuration created successfully!"));
|
|
280
445
|
const filesCreated = `${chalk2.cyan("Files created:")}
|
|
@@ -294,17 +459,18 @@ ${chalk2.gray("API key stored securely (gitignored)")}`;
|
|
|
294
459
|
}
|
|
295
460
|
|
|
296
461
|
// src/cli/commands/generate.ts
|
|
297
|
-
import
|
|
298
|
-
import
|
|
462
|
+
import fs3 from "fs-extra";
|
|
463
|
+
import path3 from "path";
|
|
299
464
|
import { pathToFileURL } from "url";
|
|
300
465
|
import chalk3 from "chalk";
|
|
301
466
|
import ora2 from "ora";
|
|
302
467
|
import Table from "cli-table3";
|
|
468
|
+
import inquirer2 from "inquirer";
|
|
303
469
|
async function generateCommand(options) {
|
|
304
470
|
console.log(chalk3.cyan.bold("\n\u{1F4DD} Generating content...\n"));
|
|
305
471
|
const projectPath = process.cwd();
|
|
306
|
-
const configPath =
|
|
307
|
-
if (!await
|
|
472
|
+
const configPath = path3.join(projectPath, "edtools.config.js");
|
|
473
|
+
if (!await fs3.pathExists(configPath)) {
|
|
308
474
|
console.log(chalk3.red("\u2717 No edtools.config.js found"));
|
|
309
475
|
console.log(chalk3.yellow(' Run "edtools init" first\n'));
|
|
310
476
|
process.exit(1);
|
|
@@ -318,10 +484,10 @@ async function generateCommand(options) {
|
|
|
318
484
|
}
|
|
319
485
|
const provider = productInfo.preferredProvider || "anthropic";
|
|
320
486
|
let storedApiKey;
|
|
321
|
-
const edtoolsConfigPath =
|
|
322
|
-
if (await
|
|
487
|
+
const edtoolsConfigPath = path3.join(projectPath, ".edtools", "config.json");
|
|
488
|
+
if (await fs3.pathExists(edtoolsConfigPath)) {
|
|
323
489
|
try {
|
|
324
|
-
const edtoolsConfig = await
|
|
490
|
+
const edtoolsConfig = await fs3.readJson(edtoolsConfigPath);
|
|
325
491
|
storedApiKey = edtoolsConfig.apiKey;
|
|
326
492
|
} catch (error) {
|
|
327
493
|
}
|
|
@@ -348,14 +514,14 @@ async function generateCommand(options) {
|
|
|
348
514
|
}
|
|
349
515
|
let topics = options.topics;
|
|
350
516
|
if (options.fromCsv) {
|
|
351
|
-
const opportunitiesPath =
|
|
352
|
-
if (!await
|
|
517
|
+
const opportunitiesPath = path3.join(projectPath, ".edtools", "opportunities.json");
|
|
518
|
+
if (!await fs3.pathExists(opportunitiesPath)) {
|
|
353
519
|
console.log(chalk3.red("\u2717 No CSV analysis found"));
|
|
354
520
|
console.log(chalk3.yellow(' Run "edtools analyze" first to analyze your GSC data\n'));
|
|
355
521
|
process.exit(1);
|
|
356
522
|
}
|
|
357
523
|
try {
|
|
358
|
-
const oppData = await
|
|
524
|
+
const oppData = await fs3.readJson(opportunitiesPath);
|
|
359
525
|
topics = oppData.opportunities.slice(0, parseInt(options.posts, 10)).map((opp) => opp.suggestedTitle);
|
|
360
526
|
console.log(chalk3.cyan("\u{1F4CA} Using topics from CSV analysis:\n"));
|
|
361
527
|
topics.forEach((topic, i) => {
|
|
@@ -376,8 +542,40 @@ async function generateCommand(options) {
|
|
|
376
542
|
console.log(chalk3.yellow(`\u26A0\uFE0F Generating ${count} posts at once may trigger spam detection`));
|
|
377
543
|
console.log(chalk3.yellow(" Recommended: 3-5 posts per week\n"));
|
|
378
544
|
}
|
|
379
|
-
const outputDir =
|
|
380
|
-
await
|
|
545
|
+
const outputDir = path3.resolve(projectPath, options.output);
|
|
546
|
+
await fs3.ensureDir(outputDir);
|
|
547
|
+
const hostingConfig = detectHostingConfig(projectPath);
|
|
548
|
+
if (hostingConfig) {
|
|
549
|
+
const validation = validateOutputDir(options.output, hostingConfig);
|
|
550
|
+
if (!validation.valid) {
|
|
551
|
+
console.log(chalk3.yellow.bold("\n\u26A0\uFE0F HOSTING PLATFORM DETECTED: ") + chalk3.white(validation.platform));
|
|
552
|
+
console.log(chalk3.yellow(" - Public directory: ") + chalk3.white(validation.publicDir));
|
|
553
|
+
console.log(chalk3.yellow(" - Your outputDir: ") + chalk3.white(validation.currentOutputDir));
|
|
554
|
+
console.log(chalk3.yellow(" - Problem: Files outside ") + chalk3.white(validation.publicDir + "/") + chalk3.yellow(" won't be accessible"));
|
|
555
|
+
console.log("");
|
|
556
|
+
console.log(chalk3.red.bold("\u274C ISSUE: Output directory misconfiguration"));
|
|
557
|
+
console.log(chalk3.red(" Generated files will return 404 in production"));
|
|
558
|
+
console.log("");
|
|
559
|
+
console.log(chalk3.cyan.bold("\u{1F4DD} Suggested fix:"));
|
|
560
|
+
console.log(chalk3.cyan(" Update edtools.config.js:"));
|
|
561
|
+
console.log("");
|
|
562
|
+
console.log(chalk3.gray(" content: {"));
|
|
563
|
+
console.log(chalk3.green(` outputDir: '${validation.suggestion}'`) + chalk3.gray(" // \u2705 Correct path"));
|
|
564
|
+
console.log(chalk3.gray(" }"));
|
|
565
|
+
console.log("");
|
|
566
|
+
const answer = await inquirer2.prompt([{
|
|
567
|
+
type: "confirm",
|
|
568
|
+
name: "continue",
|
|
569
|
+
message: "Continue anyway?",
|
|
570
|
+
default: false
|
|
571
|
+
}]);
|
|
572
|
+
if (!answer.continue) {
|
|
573
|
+
console.log(chalk3.yellow("\n\u2717 Generation cancelled\n"));
|
|
574
|
+
process.exit(0);
|
|
575
|
+
}
|
|
576
|
+
console.log("");
|
|
577
|
+
}
|
|
578
|
+
}
|
|
381
579
|
console.log(chalk3.cyan("Configuration:"));
|
|
382
580
|
console.log(` Product: ${chalk3.white(productInfo.name)}`);
|
|
383
581
|
console.log(` Category: ${chalk3.white(productInfo.category)}`);
|
|
@@ -395,52 +593,120 @@ async function generateCommand(options) {
|
|
|
395
593
|
avoidDuplicates: true,
|
|
396
594
|
similarityThreshold: 0.85,
|
|
397
595
|
provider,
|
|
398
|
-
apiKey
|
|
596
|
+
apiKey,
|
|
597
|
+
dryRun: options.dryRun
|
|
399
598
|
};
|
|
400
599
|
const providerName = provider === "anthropic" ? "Claude" : "ChatGPT";
|
|
401
600
|
const spinner = ora2(`Generating content with ${providerName}...`).start();
|
|
402
601
|
try {
|
|
403
602
|
const result = await generator.generate(generateConfig);
|
|
404
603
|
if (result.success && result.posts.length > 0) {
|
|
405
|
-
|
|
604
|
+
const dryRunMode = result.dryRun || false;
|
|
605
|
+
spinner.succeed(chalk3.bold(dryRunMode ? "Preview generated successfully!" : "Content generated successfully!"));
|
|
406
606
|
console.log("");
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
chalk3.cyan.bold("#"),
|
|
410
|
-
chalk3.cyan.bold("Title"),
|
|
411
|
-
chalk3.cyan.bold("SEO Score"),
|
|
412
|
-
chalk3.cyan.bold("Path")
|
|
413
|
-
],
|
|
414
|
-
colWidths: [5, 40, 12, 50],
|
|
415
|
-
wordWrap: true,
|
|
416
|
-
style: {
|
|
417
|
-
head: [],
|
|
418
|
-
border: ["cyan"]
|
|
419
|
-
}
|
|
420
|
-
});
|
|
421
|
-
result.posts.forEach((post, i) => {
|
|
422
|
-
const scoreColor = getScoreColorFn(post.seoScore);
|
|
423
|
-
table.push([
|
|
424
|
-
chalk3.gray((i + 1).toString()),
|
|
425
|
-
chalk3.white(post.title),
|
|
426
|
-
scoreColor(`${post.seoScore}/100`),
|
|
427
|
-
chalk3.gray(post.path)
|
|
428
|
-
]);
|
|
429
|
-
});
|
|
430
|
-
console.log(table.toString());
|
|
431
|
-
console.log("");
|
|
432
|
-
console.log(successBox(`Generated ${result.posts.length} SEO-optimized ${result.posts.length === 1 ? "post" : "posts"}!`));
|
|
433
|
-
if (result.warnings && result.warnings.length > 0) {
|
|
434
|
-
const warningText = result.warnings.join("\n");
|
|
435
|
-
console.log(warningBox(warningText));
|
|
607
|
+
if (dryRunMode) {
|
|
608
|
+
console.log(chalk3.yellow.bold("\u{1F50D} DRY RUN MODE - No files were written\n"));
|
|
436
609
|
}
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
610
|
+
if (options.report === "json" || options.report === "pretty") {
|
|
611
|
+
const report = {
|
|
612
|
+
success: result.success,
|
|
613
|
+
generated: result.posts.length,
|
|
614
|
+
timestamp: (/* @__PURE__ */ new Date()).toISOString(),
|
|
615
|
+
files: result.posts.map((post) => ({
|
|
616
|
+
path: post.path,
|
|
617
|
+
slug: post.slug,
|
|
618
|
+
title: post.title,
|
|
619
|
+
url: post.url,
|
|
620
|
+
wordCount: post.wordCount,
|
|
621
|
+
readTime: post.readTime,
|
|
622
|
+
seoScore: post.seoScore,
|
|
623
|
+
issues: post.seoIssues || [],
|
|
624
|
+
metadata: {
|
|
625
|
+
id: post.id,
|
|
626
|
+
tags: post.tags,
|
|
627
|
+
keywords: post.keywords,
|
|
628
|
+
datePublished: post.datePublished
|
|
629
|
+
}
|
|
630
|
+
})),
|
|
631
|
+
manifestPath: result.manifestPath,
|
|
632
|
+
sitemapPath: result.sitemapPath,
|
|
633
|
+
stats: result.stats || {
|
|
634
|
+
avgSeoScore: result.posts.reduce((sum, p) => sum + p.seoScore, 0) / result.posts.length,
|
|
635
|
+
totalWords: result.posts.reduce((sum, p) => sum + p.wordCount, 0),
|
|
636
|
+
avgReadTime: result.posts[0]?.readTime || "0 min"
|
|
637
|
+
},
|
|
638
|
+
warnings: result.warnings || [],
|
|
639
|
+
errors: result.errors || [],
|
|
640
|
+
dryRun: dryRunMode
|
|
641
|
+
};
|
|
642
|
+
const indent = options.report === "pretty" ? 2 : 0;
|
|
643
|
+
console.log(JSON.stringify(report, null, indent));
|
|
644
|
+
} else {
|
|
645
|
+
const table = new Table({
|
|
646
|
+
head: [
|
|
647
|
+
chalk3.cyan.bold("#"),
|
|
648
|
+
chalk3.cyan.bold("Title"),
|
|
649
|
+
chalk3.cyan.bold("Words"),
|
|
650
|
+
chalk3.cyan.bold("SEO"),
|
|
651
|
+
dryRunMode ? chalk3.cyan.bold("Would Create") : chalk3.cyan.bold("Path")
|
|
652
|
+
],
|
|
653
|
+
colWidths: [5, 35, 10, 10, 45],
|
|
654
|
+
wordWrap: true,
|
|
655
|
+
style: {
|
|
656
|
+
head: [],
|
|
657
|
+
border: ["cyan"]
|
|
658
|
+
}
|
|
659
|
+
});
|
|
660
|
+
result.posts.forEach((post, i) => {
|
|
661
|
+
const scoreColor = getScoreColorFn(post.seoScore);
|
|
662
|
+
table.push([
|
|
663
|
+
chalk3.gray((i + 1).toString()),
|
|
664
|
+
chalk3.white(truncateTitle(post.title, 33)),
|
|
665
|
+
chalk3.white(post.wordCount.toString()),
|
|
666
|
+
scoreColor(`${post.seoScore}/100`),
|
|
667
|
+
chalk3.gray(post.path)
|
|
668
|
+
]);
|
|
669
|
+
});
|
|
670
|
+
console.log(table.toString());
|
|
671
|
+
console.log("");
|
|
672
|
+
if (result.stats) {
|
|
673
|
+
console.log(chalk3.cyan.bold("Statistics:\n"));
|
|
674
|
+
console.log(` Total words: ${chalk3.white(result.stats.totalWords)}`);
|
|
675
|
+
console.log(` Avg SEO score: ${getScoreColorFn(result.stats.avgSeoScore)(result.stats.avgSeoScore + "/100")}`);
|
|
676
|
+
console.log(` Avg read time: ${chalk3.white(result.stats.avgReadTime)}`);
|
|
677
|
+
console.log("");
|
|
678
|
+
}
|
|
679
|
+
if (dryRunMode) {
|
|
680
|
+
console.log(chalk3.yellow.bold("Would create:\n"));
|
|
681
|
+
if (result.manifestPath) {
|
|
682
|
+
console.log(` ${chalk3.yellow("\u2022")} ${chalk3.white(result.manifestPath)} (manifest with ${result.posts.length} posts)`);
|
|
683
|
+
}
|
|
684
|
+
if (result.sitemapPath) {
|
|
685
|
+
console.log(` ${chalk3.yellow("\u2022")} ${chalk3.white(result.sitemapPath)} (sitemap with ${result.posts.length} URLs)`);
|
|
686
|
+
}
|
|
687
|
+
console.log("");
|
|
688
|
+
}
|
|
689
|
+
if (!dryRunMode) {
|
|
690
|
+
console.log(successBox(`Generated ${result.posts.length} SEO-optimized ${result.posts.length === 1 ? "post" : "posts"}!`));
|
|
691
|
+
}
|
|
692
|
+
if (result.warnings && result.warnings.length > 0) {
|
|
693
|
+
const warningText = result.warnings.join("\n");
|
|
694
|
+
console.log(warningBox(warningText));
|
|
695
|
+
}
|
|
696
|
+
if (dryRunMode) {
|
|
697
|
+
console.log(chalk3.cyan.bold("Next step:"));
|
|
698
|
+
console.log(` ${chalk3.cyan("\u2022")} Run without ${chalk3.white("--dry-run")} to generate files
|
|
699
|
+
`);
|
|
700
|
+
} else {
|
|
701
|
+
console.log(chalk3.cyan.bold("Next steps:"));
|
|
702
|
+
console.log(` ${chalk3.cyan("1.")} Review generated content in ${chalk3.white(outputDir)}`);
|
|
703
|
+
console.log(` ${chalk3.cyan("2.")} Edit posts to add personal experience/expertise`);
|
|
704
|
+
console.log(` ${chalk3.cyan("3.")} Deploy to your website`);
|
|
705
|
+
console.log("");
|
|
706
|
+
console.log(chalk3.yellow(`\u{1F4A1} Tip: Wait 3-7 days before generating more posts to avoid spam detection
|
|
443
707
|
`));
|
|
708
|
+
}
|
|
709
|
+
}
|
|
444
710
|
} else {
|
|
445
711
|
spinner.fail(chalk3.bold("Failed to generate content"));
|
|
446
712
|
if (result.errors && result.errors.length > 0) {
|
|
@@ -456,23 +722,28 @@ async function generateCommand(options) {
|
|
|
456
722
|
}
|
|
457
723
|
}
|
|
458
724
|
function getScoreColorFn(score) {
|
|
459
|
-
if (score >=
|
|
460
|
-
if (score >=
|
|
725
|
+
if (score >= 90) return chalk3.green;
|
|
726
|
+
if (score >= 75) return chalk3.yellow;
|
|
727
|
+
if (score >= 60) return chalk3.hex("#FFA500");
|
|
461
728
|
return chalk3.red;
|
|
462
729
|
}
|
|
730
|
+
function truncateTitle(title, maxLen) {
|
|
731
|
+
if (title.length <= maxLen) return title;
|
|
732
|
+
return title.substring(0, maxLen - 3) + "...";
|
|
733
|
+
}
|
|
463
734
|
|
|
464
735
|
// src/cli/commands/analyze.ts
|
|
465
|
-
import
|
|
466
|
-
import
|
|
736
|
+
import fs5 from "fs-extra";
|
|
737
|
+
import path4 from "path";
|
|
467
738
|
import chalk4 from "chalk";
|
|
468
739
|
import ora3 from "ora";
|
|
469
740
|
import Table2 from "cli-table3";
|
|
470
741
|
|
|
471
742
|
// src/integrations/gsc-csv-analyzer.ts
|
|
472
|
-
import
|
|
743
|
+
import fs4 from "fs-extra";
|
|
473
744
|
import { parse } from "csv-parse/sync";
|
|
474
745
|
async function parseGSCCSV(filePath) {
|
|
475
|
-
const content = await
|
|
746
|
+
const content = await fs4.readFile(filePath, "utf-8");
|
|
476
747
|
const records = parse(content, {
|
|
477
748
|
columns: true,
|
|
478
749
|
skip_empty_lines: true,
|
|
@@ -568,7 +839,7 @@ async function saveOpportunities(opportunities, outputPath) {
|
|
|
568
839
|
count: opportunities.length,
|
|
569
840
|
opportunities
|
|
570
841
|
};
|
|
571
|
-
await
|
|
842
|
+
await fs4.writeJson(outputPath, data, { spaces: 2 });
|
|
572
843
|
}
|
|
573
844
|
|
|
574
845
|
// src/cli/commands/analyze.ts
|
|
@@ -577,9 +848,9 @@ async function analyzeCommand(options) {
|
|
|
577
848
|
const projectPath = process.cwd();
|
|
578
849
|
let csvPath;
|
|
579
850
|
if (options.file) {
|
|
580
|
-
csvPath =
|
|
851
|
+
csvPath = path4.resolve(options.file);
|
|
581
852
|
} else {
|
|
582
|
-
const files = await
|
|
853
|
+
const files = await fs5.readdir(projectPath);
|
|
583
854
|
const csvFiles = files.filter((f) => f.endsWith(".csv") && f.toLowerCase().includes("search"));
|
|
584
855
|
if (csvFiles.length === 0) {
|
|
585
856
|
console.log(chalk4.red("\u2717 No CSV file found"));
|
|
@@ -592,9 +863,9 @@ async function analyzeCommand(options) {
|
|
|
592
863
|
csvFiles.forEach((f) => console.log(` - ${f}`));
|
|
593
864
|
console.log(chalk4.yellow("\nUsing first file. Specify with --file to use a different one.\n"));
|
|
594
865
|
}
|
|
595
|
-
csvPath =
|
|
866
|
+
csvPath = path4.join(projectPath, csvFiles[0]);
|
|
596
867
|
}
|
|
597
|
-
if (!await
|
|
868
|
+
if (!await fs5.pathExists(csvPath)) {
|
|
598
869
|
console.log(chalk4.red(`\u2717 File not found: ${csvPath}
|
|
599
870
|
`));
|
|
600
871
|
process.exit(1);
|
|
@@ -603,7 +874,7 @@ async function analyzeCommand(options) {
|
|
|
603
874
|
let data;
|
|
604
875
|
try {
|
|
605
876
|
data = await parseGSCCSV(csvPath);
|
|
606
|
-
spinner.succeed(`Parsed ${chalk4.white(data.length)} keywords from ${chalk4.white(
|
|
877
|
+
spinner.succeed(`Parsed ${chalk4.white(data.length)} keywords from ${chalk4.white(path4.basename(csvPath))}`);
|
|
607
878
|
} catch (error) {
|
|
608
879
|
spinner.fail("Failed to parse CSV");
|
|
609
880
|
console.log(chalk4.red(`
|
|
@@ -708,9 +979,9 @@ Error: ${error.message}
|
|
|
708
979
|
console.log(topTable.toString());
|
|
709
980
|
console.log("");
|
|
710
981
|
}
|
|
711
|
-
const edtoolsDir =
|
|
712
|
-
await
|
|
713
|
-
const opportunitiesPath =
|
|
982
|
+
const edtoolsDir = path4.join(projectPath, ".edtools");
|
|
983
|
+
await fs5.ensureDir(edtoolsDir);
|
|
984
|
+
const opportunitiesPath = path4.join(edtoolsDir, "opportunities.json");
|
|
714
985
|
await saveOpportunities(analysis.opportunities, opportunitiesPath);
|
|
715
986
|
console.log(successBox(`Analysis saved to ${chalk4.white(".edtools/opportunities.json")}`));
|
|
716
987
|
console.log(chalk4.cyan.bold("Next steps:"));
|
|
@@ -720,17 +991,569 @@ Error: ${error.message}
|
|
|
720
991
|
console.log(chalk4.gray("\u{1F4A1} Use --from-csv to automatically generate posts for top opportunities\n"));
|
|
721
992
|
}
|
|
722
993
|
|
|
994
|
+
// src/cli/commands/validate.ts
|
|
995
|
+
import fs7 from "fs-extra";
|
|
996
|
+
import path5 from "path";
|
|
997
|
+
import chalk5 from "chalk";
|
|
998
|
+
import Table3 from "cli-table3";
|
|
999
|
+
|
|
1000
|
+
// src/utils/seo-validator.ts
|
|
1001
|
+
import * as cheerio from "cheerio";
|
|
1002
|
+
import fs6 from "fs-extra";
|
|
1003
|
+
async function validatePost(htmlPath) {
|
|
1004
|
+
const html = await fs6.readFile(htmlPath, "utf-8");
|
|
1005
|
+
const $ = cheerio.load(html);
|
|
1006
|
+
const issues = [];
|
|
1007
|
+
const passed = [];
|
|
1008
|
+
const metadata = {};
|
|
1009
|
+
const title = $("title").text().trim();
|
|
1010
|
+
metadata.titleLength = title.length;
|
|
1011
|
+
if (!title) {
|
|
1012
|
+
issues.push({
|
|
1013
|
+
severity: "error",
|
|
1014
|
+
category: "metadata",
|
|
1015
|
+
message: "Missing <title> tag",
|
|
1016
|
+
suggestion: "Add a descriptive title (50-60 characters)"
|
|
1017
|
+
});
|
|
1018
|
+
} else if (title.length < 30) {
|
|
1019
|
+
issues.push({
|
|
1020
|
+
severity: "warning",
|
|
1021
|
+
category: "metadata",
|
|
1022
|
+
message: `Title too short (${title.length} chars)`,
|
|
1023
|
+
suggestion: "Optimal length is 50-60 characters for better CTR"
|
|
1024
|
+
});
|
|
1025
|
+
} else if (title.length > 60) {
|
|
1026
|
+
issues.push({
|
|
1027
|
+
severity: "warning",
|
|
1028
|
+
category: "metadata",
|
|
1029
|
+
message: `Title too long (${title.length} chars)`,
|
|
1030
|
+
suggestion: "Shorten to 50-60 characters to avoid truncation in SERPs"
|
|
1031
|
+
});
|
|
1032
|
+
} else {
|
|
1033
|
+
passed.push(`Title length optimal (${title.length} chars)`);
|
|
1034
|
+
}
|
|
1035
|
+
const metaDesc = $('meta[name="description"]').attr("content")?.trim() || "";
|
|
1036
|
+
metadata.descriptionLength = metaDesc.length;
|
|
1037
|
+
if (!metaDesc) {
|
|
1038
|
+
issues.push({
|
|
1039
|
+
severity: "error",
|
|
1040
|
+
category: "metadata",
|
|
1041
|
+
message: "Missing meta description",
|
|
1042
|
+
suggestion: "Add a compelling description (150-160 characters)"
|
|
1043
|
+
});
|
|
1044
|
+
} else if (metaDesc.length < 120) {
|
|
1045
|
+
issues.push({
|
|
1046
|
+
severity: "warning",
|
|
1047
|
+
category: "metadata",
|
|
1048
|
+
message: `Meta description too short (${metaDesc.length}/160 chars)`,
|
|
1049
|
+
suggestion: `Add ${160 - metaDesc.length} more characters to optimize CTR`
|
|
1050
|
+
});
|
|
1051
|
+
} else if (metaDesc.length > 160) {
|
|
1052
|
+
issues.push({
|
|
1053
|
+
severity: "warning",
|
|
1054
|
+
category: "metadata",
|
|
1055
|
+
message: `Meta description too long (${metaDesc.length}/160 chars)`,
|
|
1056
|
+
suggestion: `Remove ${metaDesc.length - 160} characters to avoid truncation`
|
|
1057
|
+
});
|
|
1058
|
+
} else {
|
|
1059
|
+
passed.push(`Meta description optimal (${metaDesc.length} chars)`);
|
|
1060
|
+
}
|
|
1061
|
+
const h1Elements = $("h1");
|
|
1062
|
+
const h1Count = h1Elements.length;
|
|
1063
|
+
metadata.h1Count = h1Count;
|
|
1064
|
+
if (h1Count === 0) {
|
|
1065
|
+
issues.push({
|
|
1066
|
+
severity: "error",
|
|
1067
|
+
category: "headings",
|
|
1068
|
+
message: "Missing H1 heading",
|
|
1069
|
+
suggestion: "Add a single H1 with primary keyword"
|
|
1070
|
+
});
|
|
1071
|
+
} else if (h1Count > 1) {
|
|
1072
|
+
issues.push({
|
|
1073
|
+
severity: "warning",
|
|
1074
|
+
category: "headings",
|
|
1075
|
+
message: `Multiple H1 tags found (${h1Count})`,
|
|
1076
|
+
suggestion: "Use only one H1 per page for proper structure"
|
|
1077
|
+
});
|
|
1078
|
+
} else {
|
|
1079
|
+
passed.push("Single H1 heading");
|
|
1080
|
+
}
|
|
1081
|
+
const h2Count = $("h2").length;
|
|
1082
|
+
metadata.h2Count = h2Count;
|
|
1083
|
+
if (h2Count < 2) {
|
|
1084
|
+
issues.push({
|
|
1085
|
+
severity: "warning",
|
|
1086
|
+
category: "headings",
|
|
1087
|
+
message: `Few H2 headings for readability (${h2Count} found)`,
|
|
1088
|
+
suggestion: "Add 3-5 H2 headings for better structure and scannability"
|
|
1089
|
+
});
|
|
1090
|
+
} else if (h2Count > 10) {
|
|
1091
|
+
issues.push({
|
|
1092
|
+
severity: "info",
|
|
1093
|
+
category: "headings",
|
|
1094
|
+
message: `Many H2 headings (${h2Count})`,
|
|
1095
|
+
suggestion: "Consider if all sections need H2 or some should be H3"
|
|
1096
|
+
});
|
|
1097
|
+
} else {
|
|
1098
|
+
passed.push(`Good heading structure (${h2Count} H2s)`);
|
|
1099
|
+
}
|
|
1100
|
+
const images = $("img");
|
|
1101
|
+
metadata.imageCount = images.length;
|
|
1102
|
+
const imagesWithoutAlt = images.filter((i, el) => !$(el).attr("alt")).length;
|
|
1103
|
+
if (imagesWithoutAlt > 0) {
|
|
1104
|
+
issues.push({
|
|
1105
|
+
severity: "warning",
|
|
1106
|
+
category: "images",
|
|
1107
|
+
message: `${imagesWithoutAlt} image${imagesWithoutAlt > 1 ? "s" : ""} missing alt text`,
|
|
1108
|
+
suggestion: "Add descriptive alt text to all images for accessibility and SEO"
|
|
1109
|
+
});
|
|
1110
|
+
} else if (images.length > 0) {
|
|
1111
|
+
passed.push(`All images have alt text (${images.length} images)`);
|
|
1112
|
+
}
|
|
1113
|
+
const schemaScript = $('script[type="application/ld+json"]');
|
|
1114
|
+
if (schemaScript.length === 0) {
|
|
1115
|
+
issues.push({
|
|
1116
|
+
severity: "info",
|
|
1117
|
+
category: "structure",
|
|
1118
|
+
message: "Missing Schema.org markup",
|
|
1119
|
+
suggestion: "Add structured data for rich snippets in search results"
|
|
1120
|
+
});
|
|
1121
|
+
} else {
|
|
1122
|
+
passed.push("Has Schema.org markup");
|
|
1123
|
+
}
|
|
1124
|
+
const metaKeywords = $('meta[name="keywords"]');
|
|
1125
|
+
if (metaKeywords.length > 0) {
|
|
1126
|
+
issues.push({
|
|
1127
|
+
severity: "info",
|
|
1128
|
+
category: "metadata",
|
|
1129
|
+
message: "Using deprecated meta keywords tag",
|
|
1130
|
+
suggestion: "Remove meta keywords tag (not used by modern search engines)"
|
|
1131
|
+
});
|
|
1132
|
+
}
|
|
1133
|
+
const ogTitle = $('meta[property="og:title"]').attr("content");
|
|
1134
|
+
const ogDescription = $('meta[property="og:description"]').attr("content");
|
|
1135
|
+
const ogImage = $('meta[property="og:image"]').attr("content");
|
|
1136
|
+
if (!ogTitle || !ogDescription) {
|
|
1137
|
+
issues.push({
|
|
1138
|
+
severity: "info",
|
|
1139
|
+
category: "metadata",
|
|
1140
|
+
message: "Missing Open Graph tags",
|
|
1141
|
+
suggestion: "Add og:title, og:description, og:image for better social sharing"
|
|
1142
|
+
});
|
|
1143
|
+
} else {
|
|
1144
|
+
passed.push("Has Open Graph tags");
|
|
1145
|
+
}
|
|
1146
|
+
const bodyText = $("article, main, body").text();
|
|
1147
|
+
const wordCount = bodyText.replace(/\s+/g, " ").trim().split(/\s+/).filter((word) => word.length > 0).length;
|
|
1148
|
+
metadata.wordCount = wordCount;
|
|
1149
|
+
if (wordCount < 300) {
|
|
1150
|
+
issues.push({
|
|
1151
|
+
severity: "warning",
|
|
1152
|
+
category: "content",
|
|
1153
|
+
message: `Content too short (${wordCount} words)`,
|
|
1154
|
+
suggestion: "Aim for at least 1000 words for better SEO performance"
|
|
1155
|
+
});
|
|
1156
|
+
} else if (wordCount < 1e3) {
|
|
1157
|
+
issues.push({
|
|
1158
|
+
severity: "info",
|
|
1159
|
+
category: "content",
|
|
1160
|
+
message: `Content could be longer (${wordCount} words)`,
|
|
1161
|
+
suggestion: "Consider expanding to 1500+ words for comprehensive coverage"
|
|
1162
|
+
});
|
|
1163
|
+
} else {
|
|
1164
|
+
passed.push(`Good content length (${wordCount} words)`);
|
|
1165
|
+
}
|
|
1166
|
+
const links = $("a[href]");
|
|
1167
|
+
const internalLinks = links.filter((i, el) => {
|
|
1168
|
+
const href = $(el).attr("href") || "";
|
|
1169
|
+
return href.startsWith("/") || href.startsWith("#");
|
|
1170
|
+
}).length;
|
|
1171
|
+
const externalLinks = links.length - internalLinks;
|
|
1172
|
+
if (internalLinks === 0 && links.length > 0) {
|
|
1173
|
+
issues.push({
|
|
1174
|
+
severity: "info",
|
|
1175
|
+
category: "links",
|
|
1176
|
+
message: "No internal links found",
|
|
1177
|
+
suggestion: "Add 2-3 internal links to related content"
|
|
1178
|
+
});
|
|
1179
|
+
} else if (internalLinks > 0) {
|
|
1180
|
+
passed.push(`Has internal links (${internalLinks} links)`);
|
|
1181
|
+
}
|
|
1182
|
+
const errorCount = issues.filter((i) => i.severity === "error").length;
|
|
1183
|
+
const warningCount = issues.filter((i) => i.severity === "warning").length;
|
|
1184
|
+
const infoCount = issues.filter((i) => i.severity === "info").length;
|
|
1185
|
+
let score = 100;
|
|
1186
|
+
score -= errorCount * 20;
|
|
1187
|
+
score -= warningCount * 7;
|
|
1188
|
+
score -= infoCount * 2;
|
|
1189
|
+
score = Math.max(0, Math.min(100, score));
|
|
1190
|
+
return {
|
|
1191
|
+
path: htmlPath,
|
|
1192
|
+
title: title || "Untitled",
|
|
1193
|
+
seoScore: score,
|
|
1194
|
+
issues,
|
|
1195
|
+
passed,
|
|
1196
|
+
metadata
|
|
1197
|
+
};
|
|
1198
|
+
}
|
|
1199
|
+
function calculateValidationStats(results) {
|
|
1200
|
+
const totalPosts = results.length;
|
|
1201
|
+
const avgSeoScore = results.reduce((sum, r) => sum + r.seoScore, 0) / totalPosts;
|
|
1202
|
+
const totalIssues = results.reduce((sum, r) => sum + r.issues.length, 0);
|
|
1203
|
+
const postsWithIssues = results.filter((r) => r.issues.length > 0).length;
|
|
1204
|
+
const issuesBySeverity = {
|
|
1205
|
+
error: results.reduce((sum, r) => sum + r.issues.filter((i) => i.severity === "error").length, 0),
|
|
1206
|
+
warning: results.reduce((sum, r) => sum + r.issues.filter((i) => i.severity === "warning").length, 0),
|
|
1207
|
+
info: results.reduce((sum, r) => sum + r.issues.filter((i) => i.severity === "info").length, 0)
|
|
1208
|
+
};
|
|
1209
|
+
const issuesByCategory = results.reduce((acc, r) => {
|
|
1210
|
+
r.issues.forEach((issue) => {
|
|
1211
|
+
acc[issue.category] = (acc[issue.category] || 0) + 1;
|
|
1212
|
+
});
|
|
1213
|
+
return acc;
|
|
1214
|
+
}, {});
|
|
1215
|
+
return {
|
|
1216
|
+
totalPosts,
|
|
1217
|
+
avgSeoScore: Math.round(avgSeoScore * 10) / 10,
|
|
1218
|
+
totalIssues,
|
|
1219
|
+
postsWithIssues,
|
|
1220
|
+
issuesBySeverity,
|
|
1221
|
+
issuesByCategory
|
|
1222
|
+
};
|
|
1223
|
+
}
|
|
1224
|
+
|
|
1225
|
+
// src/cli/commands/validate.ts
|
|
1226
|
+
async function validateCommand(options) {
|
|
1227
|
+
console.log(chalk5.cyan.bold("\n\u{1F4CA} Validating SEO quality...\n"));
|
|
1228
|
+
const projectPath = process.cwd();
|
|
1229
|
+
let htmlFiles = [];
|
|
1230
|
+
if (options.post) {
|
|
1231
|
+
const postPath = path5.resolve(projectPath, options.post);
|
|
1232
|
+
if (!await fs7.pathExists(postPath)) {
|
|
1233
|
+
console.log(errorBox(`Post not found: ${postPath}`));
|
|
1234
|
+
process.exit(1);
|
|
1235
|
+
}
|
|
1236
|
+
htmlFiles = [postPath];
|
|
1237
|
+
} else if (options.posts) {
|
|
1238
|
+
const postsDir = path5.resolve(projectPath, options.posts);
|
|
1239
|
+
if (!await fs7.pathExists(postsDir)) {
|
|
1240
|
+
console.log(errorBox(`Directory not found: ${postsDir}`));
|
|
1241
|
+
process.exit(1);
|
|
1242
|
+
}
|
|
1243
|
+
htmlFiles = await findHtmlFiles(postsDir);
|
|
1244
|
+
if (htmlFiles.length === 0) {
|
|
1245
|
+
console.log(errorBox(`No HTML files found in: ${postsDir}`));
|
|
1246
|
+
process.exit(1);
|
|
1247
|
+
}
|
|
1248
|
+
} else {
|
|
1249
|
+
const defaultBlogDir = path5.join(projectPath, "blog");
|
|
1250
|
+
if (await fs7.pathExists(defaultBlogDir)) {
|
|
1251
|
+
htmlFiles = await findHtmlFiles(defaultBlogDir);
|
|
1252
|
+
if (htmlFiles.length === 0) {
|
|
1253
|
+
console.log(errorBox("No HTML files found in blog/ directory"));
|
|
1254
|
+
process.exit(1);
|
|
1255
|
+
}
|
|
1256
|
+
} else {
|
|
1257
|
+
console.log(errorBox("No posts directory specified"));
|
|
1258
|
+
console.log(chalk5.yellow("Usage: edtools validate --posts <dir> or --post <file>\n"));
|
|
1259
|
+
process.exit(1);
|
|
1260
|
+
}
|
|
1261
|
+
}
|
|
1262
|
+
console.log(chalk5.cyan(`Found ${htmlFiles.length} post${htmlFiles.length > 1 ? "s" : ""} to validate
|
|
1263
|
+
`));
|
|
1264
|
+
const results = [];
|
|
1265
|
+
for (const htmlFile of htmlFiles) {
|
|
1266
|
+
try {
|
|
1267
|
+
const result = await validatePost(htmlFile);
|
|
1268
|
+
results.push(result);
|
|
1269
|
+
} catch (error) {
|
|
1270
|
+
console.log(chalk5.yellow(`\u26A0\uFE0F Failed to validate ${htmlFile}: ${error.message}`));
|
|
1271
|
+
}
|
|
1272
|
+
}
|
|
1273
|
+
if (results.length === 0) {
|
|
1274
|
+
console.log(errorBox("No posts could be validated"));
|
|
1275
|
+
process.exit(1);
|
|
1276
|
+
}
|
|
1277
|
+
const threshold = options.threshold ? parseInt(options.threshold, 10) : 0;
|
|
1278
|
+
const filteredResults = threshold > 0 ? results.filter((r) => r.seoScore < threshold) : results;
|
|
1279
|
+
if (options.output) {
|
|
1280
|
+
const stats2 = calculateValidationStats(results);
|
|
1281
|
+
const report = {
|
|
1282
|
+
success: true,
|
|
1283
|
+
validated: results.length,
|
|
1284
|
+
timestamp: (/* @__PURE__ */ new Date()).toISOString(),
|
|
1285
|
+
posts: results.map((r) => ({
|
|
1286
|
+
path: r.path,
|
|
1287
|
+
title: r.title,
|
|
1288
|
+
seoScore: r.seoScore,
|
|
1289
|
+
issues: r.issues,
|
|
1290
|
+
passed: r.passed,
|
|
1291
|
+
metadata: r.metadata
|
|
1292
|
+
})),
|
|
1293
|
+
stats: stats2
|
|
1294
|
+
};
|
|
1295
|
+
const outputPath = path5.resolve(projectPath, options.output);
|
|
1296
|
+
await fs7.writeJson(outputPath, report, { spaces: 2 });
|
|
1297
|
+
console.log(successBox(`Validation report saved to ${outputPath}`));
|
|
1298
|
+
}
|
|
1299
|
+
if (filteredResults.length > 0) {
|
|
1300
|
+
const table = new Table3({
|
|
1301
|
+
head: [
|
|
1302
|
+
chalk5.cyan.bold("#"),
|
|
1303
|
+
chalk5.cyan.bold("Title"),
|
|
1304
|
+
chalk5.cyan.bold("Score"),
|
|
1305
|
+
chalk5.cyan.bold("Issues")
|
|
1306
|
+
],
|
|
1307
|
+
colWidths: [5, 40, 10, 50],
|
|
1308
|
+
wordWrap: true,
|
|
1309
|
+
style: {
|
|
1310
|
+
head: [],
|
|
1311
|
+
border: ["cyan"]
|
|
1312
|
+
}
|
|
1313
|
+
});
|
|
1314
|
+
filteredResults.forEach((result, i) => {
|
|
1315
|
+
const scoreColor = getScoreColor(result.seoScore);
|
|
1316
|
+
const issuesText = result.issues.length > 0 ? result.issues.map((issue) => {
|
|
1317
|
+
const icon = issue.severity === "error" ? "\u2717" : issue.severity === "warning" ? "\u26A0" : "\u2139";
|
|
1318
|
+
const color = issue.severity === "error" ? chalk5.red : issue.severity === "warning" ? chalk5.yellow : chalk5.blue;
|
|
1319
|
+
return color(`${icon} ${issue.message}`);
|
|
1320
|
+
}).join("\n") : chalk5.green("\u2713 No issues");
|
|
1321
|
+
table.push([
|
|
1322
|
+
chalk5.gray((i + 1).toString()),
|
|
1323
|
+
chalk5.white(truncate(result.title, 38)),
|
|
1324
|
+
scoreColor(`${result.seoScore}/100`),
|
|
1325
|
+
issuesText
|
|
1326
|
+
]);
|
|
1327
|
+
});
|
|
1328
|
+
console.log(table.toString());
|
|
1329
|
+
console.log("");
|
|
1330
|
+
} else if (threshold > 0) {
|
|
1331
|
+
console.log(successBox(`All posts have SEO score >= ${threshold}`));
|
|
1332
|
+
}
|
|
1333
|
+
const stats = calculateValidationStats(results);
|
|
1334
|
+
console.log(chalk5.cyan.bold("Overall Statistics:\n"));
|
|
1335
|
+
console.log(` Total posts validated: ${chalk5.white(stats.totalPosts)}`);
|
|
1336
|
+
console.log(` Average SEO score: ${getScoreColor(stats.avgSeoScore)(stats.avgSeoScore + "/100")}`);
|
|
1337
|
+
console.log(` Posts with issues: ${chalk5.white(stats.postsWithIssues)} (${Math.round(stats.postsWithIssues / stats.totalPosts * 100)}%)`);
|
|
1338
|
+
console.log(` Total issues found: ${chalk5.white(stats.totalIssues)}`);
|
|
1339
|
+
console.log("");
|
|
1340
|
+
if (stats.totalIssues > 0) {
|
|
1341
|
+
console.log(chalk5.cyan.bold("Issues by Severity:\n"));
|
|
1342
|
+
if (stats.issuesBySeverity.error > 0) {
|
|
1343
|
+
console.log(` ${chalk5.red("\u2717 Errors:")} ${chalk5.white(stats.issuesBySeverity.error)}`);
|
|
1344
|
+
}
|
|
1345
|
+
if (stats.issuesBySeverity.warning > 0) {
|
|
1346
|
+
console.log(` ${chalk5.yellow("\u26A0 Warnings:")} ${chalk5.white(stats.issuesBySeverity.warning)}`);
|
|
1347
|
+
}
|
|
1348
|
+
if (stats.issuesBySeverity.info > 0) {
|
|
1349
|
+
console.log(` ${chalk5.blue("\u2139 Info:")} ${chalk5.white(stats.issuesBySeverity.info)}`);
|
|
1350
|
+
}
|
|
1351
|
+
console.log("");
|
|
1352
|
+
}
|
|
1353
|
+
if (Object.keys(stats.issuesByCategory).length > 0) {
|
|
1354
|
+
console.log(chalk5.cyan.bold("Top Issue Categories:\n"));
|
|
1355
|
+
const sortedCategories = Object.entries(stats.issuesByCategory).sort((a, b) => b[1] - a[1]).slice(0, 5);
|
|
1356
|
+
sortedCategories.forEach(([category, count]) => {
|
|
1357
|
+
console.log(` ${chalk5.white(capitalize(category))}: ${chalk5.yellow(count)}`);
|
|
1358
|
+
});
|
|
1359
|
+
console.log("");
|
|
1360
|
+
}
|
|
1361
|
+
if (stats.avgSeoScore < 85) {
|
|
1362
|
+
console.log(chalk5.yellow.bold("\u{1F4A1} Recommendations:\n"));
|
|
1363
|
+
console.log(chalk5.yellow(" \u2022 Focus on fixing critical errors first"));
|
|
1364
|
+
console.log(chalk5.yellow(" \u2022 Optimize meta descriptions to 150-160 characters"));
|
|
1365
|
+
console.log(chalk5.yellow(" \u2022 Ensure all posts have proper heading structure (H1, H2s)"));
|
|
1366
|
+
console.log(chalk5.yellow(" \u2022 Add alt text to all images"));
|
|
1367
|
+
console.log("");
|
|
1368
|
+
} else {
|
|
1369
|
+
console.log(successBox(`Great job! Average SEO score is ${stats.avgSeoScore}/100`));
|
|
1370
|
+
}
|
|
1371
|
+
}
|
|
1372
|
+
async function findHtmlFiles(dir) {
|
|
1373
|
+
const files = [];
|
|
1374
|
+
const entries = await fs7.readdir(dir, { withFileTypes: true });
|
|
1375
|
+
for (const entry of entries) {
|
|
1376
|
+
const fullPath = path5.join(dir, entry.name);
|
|
1377
|
+
if (entry.isDirectory()) {
|
|
1378
|
+
const subFiles = await findHtmlFiles(fullPath);
|
|
1379
|
+
files.push(...subFiles);
|
|
1380
|
+
} else if (entry.isFile() && entry.name.endsWith(".html")) {
|
|
1381
|
+
files.push(fullPath);
|
|
1382
|
+
}
|
|
1383
|
+
}
|
|
1384
|
+
return files;
|
|
1385
|
+
}
|
|
1386
|
+
function getScoreColor(score) {
|
|
1387
|
+
if (score >= 90) return chalk5.green;
|
|
1388
|
+
if (score >= 75) return chalk5.yellow;
|
|
1389
|
+
if (score >= 60) return chalk5.hex("#FFA500");
|
|
1390
|
+
return chalk5.red;
|
|
1391
|
+
}
|
|
1392
|
+
function truncate(str, maxLen) {
|
|
1393
|
+
if (str.length <= maxLen) return str;
|
|
1394
|
+
return str.substring(0, maxLen - 3) + "...";
|
|
1395
|
+
}
|
|
1396
|
+
function capitalize(str) {
|
|
1397
|
+
return str.charAt(0).toUpperCase() + str.slice(1);
|
|
1398
|
+
}
|
|
1399
|
+
|
|
1400
|
+
// src/cli/commands/doctor.ts
|
|
1401
|
+
import fs8 from "fs-extra";
|
|
1402
|
+
import path6 from "path";
|
|
1403
|
+
import { pathToFileURL as pathToFileURL2 } from "url";
|
|
1404
|
+
import chalk6 from "chalk";
|
|
1405
|
+
async function doctorCommand(options) {
|
|
1406
|
+
console.log(chalk6.cyan.bold("\n\u{1F50D} Diagnosing project configuration...\n"));
|
|
1407
|
+
const projectPath = process.cwd();
|
|
1408
|
+
const configPath = path6.join(projectPath, "edtools.config.js");
|
|
1409
|
+
const issues = [];
|
|
1410
|
+
const warnings = [];
|
|
1411
|
+
const suggestions = [];
|
|
1412
|
+
if (await fs8.pathExists(configPath)) {
|
|
1413
|
+
console.log(chalk6.green("\u2713 Configuration file found: ") + chalk6.white("edtools.config.js"));
|
|
1414
|
+
} else {
|
|
1415
|
+
console.log(chalk6.red("\u2717 Configuration file not found"));
|
|
1416
|
+
console.log(chalk6.yellow(' Run "edtools init" to create configuration\n'));
|
|
1417
|
+
process.exit(1);
|
|
1418
|
+
}
|
|
1419
|
+
let productInfo;
|
|
1420
|
+
let outputDir;
|
|
1421
|
+
try {
|
|
1422
|
+
const configUrl = pathToFileURL2(configPath).href;
|
|
1423
|
+
const config = await import(configUrl);
|
|
1424
|
+
productInfo = config.default.product;
|
|
1425
|
+
outputDir = config.default.content?.outputDir || "./blog";
|
|
1426
|
+
if (!productInfo || !productInfo.name) {
|
|
1427
|
+
issues.push("Invalid product configuration in edtools.config.js");
|
|
1428
|
+
}
|
|
1429
|
+
} catch (error) {
|
|
1430
|
+
issues.push(`Failed to load edtools.config.js: ${error.message}`);
|
|
1431
|
+
console.log(chalk6.red("\u2717 Failed to load configuration file"));
|
|
1432
|
+
console.log(chalk6.red(` Error: ${error.message}
|
|
1433
|
+
`));
|
|
1434
|
+
process.exit(1);
|
|
1435
|
+
}
|
|
1436
|
+
const outputDirPath = path6.resolve(projectPath, outputDir);
|
|
1437
|
+
if (await fs8.pathExists(outputDirPath)) {
|
|
1438
|
+
console.log(chalk6.green("\u2713 Output directory exists: ") + chalk6.white(outputDir));
|
|
1439
|
+
} else {
|
|
1440
|
+
warnings.push(`Output directory does not exist: ${outputDir}`);
|
|
1441
|
+
console.log(chalk6.yellow("\u26A0\uFE0F Output directory does not exist: ") + chalk6.white(outputDir));
|
|
1442
|
+
console.log(chalk6.yellow(' It will be created when you run "edtools generate"'));
|
|
1443
|
+
}
|
|
1444
|
+
const edtoolsConfigPath = path6.join(projectPath, ".edtools", "config.json");
|
|
1445
|
+
let hasApiKey = false;
|
|
1446
|
+
if (await fs8.pathExists(edtoolsConfigPath)) {
|
|
1447
|
+
try {
|
|
1448
|
+
const edtoolsConfig = await fs8.readJson(edtoolsConfigPath);
|
|
1449
|
+
hasApiKey = !!edtoolsConfig.apiKey;
|
|
1450
|
+
} catch (error) {
|
|
1451
|
+
}
|
|
1452
|
+
}
|
|
1453
|
+
const hasEnvApiKey = !!(process.env.ANTHROPIC_API_KEY || process.env.OPENAI_API_KEY);
|
|
1454
|
+
if (hasApiKey || hasEnvApiKey) {
|
|
1455
|
+
console.log(chalk6.green("\u2713 API key configured"));
|
|
1456
|
+
} else {
|
|
1457
|
+
warnings.push('No API key found (set via environment variable or "edtools init")');
|
|
1458
|
+
console.log(chalk6.yellow("\u26A0\uFE0F No API key configured"));
|
|
1459
|
+
console.log(chalk6.yellow(" Set ANTHROPIC_API_KEY or OPENAI_API_KEY environment variable"));
|
|
1460
|
+
console.log(chalk6.yellow(' Or run "edtools init" to store API key'));
|
|
1461
|
+
}
|
|
1462
|
+
console.log("");
|
|
1463
|
+
const hostingConfig = detectHostingConfig(projectPath);
|
|
1464
|
+
if (hostingConfig) {
|
|
1465
|
+
console.log(chalk6.cyan("\u26A0\uFE0F HOSTING PLATFORM DETECTED: ") + chalk6.white(hostingConfig.platform));
|
|
1466
|
+
console.log(chalk6.cyan(" - Public directory: ") + chalk6.white(hostingConfig.publicDir));
|
|
1467
|
+
console.log(chalk6.cyan(" - Your outputDir: ") + chalk6.white(outputDir));
|
|
1468
|
+
const validation = validateOutputDir(outputDir, hostingConfig);
|
|
1469
|
+
if (!validation.valid) {
|
|
1470
|
+
issues.push("Output directory misconfiguration");
|
|
1471
|
+
console.log(chalk6.red(" - Problem: Files outside ") + chalk6.white(hostingConfig.publicDir + "/") + chalk6.red(" won't be accessible"));
|
|
1472
|
+
console.log("");
|
|
1473
|
+
console.log(chalk6.red.bold("\u274C ISSUE: Output directory misconfiguration"));
|
|
1474
|
+
console.log(chalk6.red(" Generated files will return 404 in production"));
|
|
1475
|
+
console.log("");
|
|
1476
|
+
console.log(chalk6.cyan.bold("\u{1F4DD} Suggested fix:"));
|
|
1477
|
+
console.log(chalk6.cyan(" Update edtools.config.js:"));
|
|
1478
|
+
console.log("");
|
|
1479
|
+
console.log(chalk6.gray(" content: {"));
|
|
1480
|
+
console.log(chalk6.green(` outputDir: '${validation.suggestion}'`) + chalk6.gray(" // \u2705 Correct path"));
|
|
1481
|
+
console.log(chalk6.gray(" }"));
|
|
1482
|
+
console.log("");
|
|
1483
|
+
suggestions.push(`Update outputDir to '${validation.suggestion}'`);
|
|
1484
|
+
if (options.fix) {
|
|
1485
|
+
console.log(chalk6.yellow.bold("\u{1F527} Auto-fixing configuration...\n"));
|
|
1486
|
+
try {
|
|
1487
|
+
const configContent = await fs8.readFile(configPath, "utf-8");
|
|
1488
|
+
const updatedConfig = configContent.replace(
|
|
1489
|
+
/outputDir:\s*['"]([^'"]+)['"]/,
|
|
1490
|
+
`outputDir: '${validation.suggestion}'`
|
|
1491
|
+
);
|
|
1492
|
+
await fs8.writeFile(configPath, updatedConfig, "utf-8");
|
|
1493
|
+
console.log(chalk6.green("\u2713 Updated edtools.config.js"));
|
|
1494
|
+
console.log(chalk6.green(` outputDir: '${validation.suggestion}'`));
|
|
1495
|
+
console.log("");
|
|
1496
|
+
console.log(chalk6.cyan('\u{1F389} Configuration fixed! Run "edtools generate" to create content.\n'));
|
|
1497
|
+
} catch (error) {
|
|
1498
|
+
console.log(chalk6.red("\u2717 Failed to auto-fix configuration"));
|
|
1499
|
+
console.log(chalk6.red(` Error: ${error.message}`));
|
|
1500
|
+
console.log(chalk6.yellow(" Please update edtools.config.js manually\n"));
|
|
1501
|
+
}
|
|
1502
|
+
} else {
|
|
1503
|
+
console.log(chalk6.cyan.bold("Run: ") + chalk6.white("edtools doctor --fix"));
|
|
1504
|
+
console.log(chalk6.cyan("To automatically apply suggested fixes\n"));
|
|
1505
|
+
}
|
|
1506
|
+
} else {
|
|
1507
|
+
console.log(chalk6.green(" - Status: \u2705 Output directory correctly configured"));
|
|
1508
|
+
console.log("");
|
|
1509
|
+
}
|
|
1510
|
+
} else {
|
|
1511
|
+
console.log(chalk6.gray("No hosting platform configuration detected"));
|
|
1512
|
+
console.log(chalk6.gray("(No firebase.json, vercel.json, netlify.toml, or amplify.yml found)"));
|
|
1513
|
+
console.log("");
|
|
1514
|
+
}
|
|
1515
|
+
console.log(chalk6.cyan.bold("Summary:\n"));
|
|
1516
|
+
if (issues.length === 0 && warnings.length === 0) {
|
|
1517
|
+
console.log(chalk6.green.bold("\u2705 All checks passed! Your project is ready to generate content.\n"));
|
|
1518
|
+
} else {
|
|
1519
|
+
if (issues.length > 0) {
|
|
1520
|
+
console.log(chalk6.red.bold(`\u274C ${issues.length} issue(s) found:`));
|
|
1521
|
+
issues.forEach((issue) => {
|
|
1522
|
+
console.log(chalk6.red(` - ${issue}`));
|
|
1523
|
+
});
|
|
1524
|
+
console.log("");
|
|
1525
|
+
}
|
|
1526
|
+
if (warnings.length > 0) {
|
|
1527
|
+
console.log(chalk6.yellow.bold(`\u26A0\uFE0F ${warnings.length} warning(s):`));
|
|
1528
|
+
warnings.forEach((warning) => {
|
|
1529
|
+
console.log(chalk6.yellow(` - ${warning}`));
|
|
1530
|
+
});
|
|
1531
|
+
console.log("");
|
|
1532
|
+
}
|
|
1533
|
+
if (suggestions.length > 0 && !options.fix) {
|
|
1534
|
+
console.log(chalk6.cyan.bold("\u{1F4A1} Suggestions:"));
|
|
1535
|
+
suggestions.forEach((suggestion) => {
|
|
1536
|
+
console.log(chalk6.cyan(` - ${suggestion}`));
|
|
1537
|
+
});
|
|
1538
|
+
console.log("");
|
|
1539
|
+
console.log(chalk6.cyan("Run ") + chalk6.white("edtools doctor --fix") + chalk6.cyan(" to automatically apply fixes\n"));
|
|
1540
|
+
}
|
|
1541
|
+
}
|
|
1542
|
+
}
|
|
1543
|
+
|
|
723
1544
|
// src/cli/index.ts
|
|
724
1545
|
var program = new Command();
|
|
725
|
-
program.name("edtools").description("AI-Powered Content Marketing CLI").version("0.
|
|
1546
|
+
program.name("edtools").description("AI-Powered Content Marketing CLI - Generate, validate, and optimize SEO content").version("0.6.1");
|
|
726
1547
|
program.command("init").description("Initialize edtools in your project").option("-p, --path <path>", "Project path", process.cwd()).action(initCommand);
|
|
727
|
-
program.command("generate").description("Generate blog posts").option("-n, --posts <number>", "Number of posts to generate", "3").option("-t, --topics <topics...>", "Specific topics to write about").option("-o, --output <dir>", "Output directory", "./blog").option("--api-key <key>", "
|
|
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", "./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);
|
|
728
1549
|
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
|
+
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
|
+
program.command("doctor").description("Diagnose project configuration and hosting setup").option("--fix", "Automatically fix issues").action(doctorCommand);
|
|
729
1552
|
program.command("config").description("View or set configuration").option("--set-api-key <key>", "Set Anthropic API key").action(async (options) => {
|
|
730
1553
|
if (options.setApiKey) {
|
|
731
|
-
console.log(
|
|
1554
|
+
console.log(chalk7.green("\u2713 API key saved"));
|
|
732
1555
|
} else {
|
|
733
|
-
console.log(
|
|
1556
|
+
console.log(chalk7.cyan("Configuration:"));
|
|
734
1557
|
console.log(` API Key: ${process.env.ANTHROPIC_API_KEY ? "[set]" : "[not set]"}`);
|
|
735
1558
|
}
|
|
736
1559
|
});
|