@edtools/cli 0.6.0 → 0.6.2

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/cli/index.js CHANGED
@@ -1,16 +1,16 @@
1
1
  #!/usr/bin/env node
2
2
  import {
3
3
  ContentGenerator
4
- } from "../chunk-JCUQ7D56.js";
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 chalk6 from "chalk";
9
+ import chalk7 from "chalk";
10
10
 
11
11
  // src/cli/commands/init.ts
12
- import fs from "fs-extra";
13
- import path from "path";
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";
@@ -104,12 +104,160 @@ function warningBox(message) {
104
104
  });
105
105
  }
106
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
+
107
255
  // src/cli/commands/init.ts
108
256
  async function initCommand(options) {
109
257
  console.log(chalk2.cyan.bold("\n\u{1F680} Initializing Edtools...\n"));
110
- const projectPath = path.resolve(options.path);
111
- const configPath = path.join(projectPath, "edtools.config.js");
112
- if (await fs.pathExists(configPath)) {
258
+ const projectPath = path2.resolve(options.path);
259
+ const configPath = path2.join(projectPath, "edtools.config.js");
260
+ if (await fs2.pathExists(configPath)) {
113
261
  const { overwrite } = await inquirer.prompt([
114
262
  {
115
263
  type: "confirm",
@@ -126,9 +274,9 @@ async function initCommand(options) {
126
274
  const spinner = ora("Analyzing your landing page...").start();
127
275
  let productInfo = {};
128
276
  try {
129
- const indexPath = path.join(projectPath, "index.html");
130
- if (await fs.pathExists(indexPath)) {
131
- const html = await fs.readFile(indexPath, "utf-8");
277
+ const indexPath = path2.join(projectPath, "index.html");
278
+ if (await fs2.pathExists(indexPath)) {
279
+ const html = await fs2.readFile(indexPath, "utf-8");
132
280
  const $ = loadCheerio(html);
133
281
  productInfo = {
134
282
  name: $("title").text() || $("h1").first().text() || "My Product",
@@ -248,6 +396,15 @@ async function initCommand(options) {
248
396
  ...productInfo,
249
397
  ...answers
250
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
+ }
251
408
  const config = `module.exports = {
252
409
  product: {
253
410
  name: ${JSON.stringify(finalProductInfo.name)},
@@ -261,7 +418,7 @@ async function initCommand(options) {
261
418
  },
262
419
 
263
420
  content: {
264
- outputDir: './blog',
421
+ outputDir: ${JSON.stringify(suggestedOutputDir)},
265
422
  generateBlog: true,
266
423
  },
267
424
 
@@ -272,17 +429,17 @@ async function initCommand(options) {
272
429
  },
273
430
  };
274
431
  `;
275
- await fs.writeFile(configPath, config, "utf-8");
276
- const edtoolsDir = path.join(projectPath, ".edtools");
277
- await fs.ensureDir(edtoolsDir);
432
+ await fs2.writeFile(configPath, config, "utf-8");
433
+ const edtoolsDir = path2.join(projectPath, ".edtools");
434
+ await fs2.ensureDir(edtoolsDir);
278
435
  const edtoolsConfig = {
279
436
  apiKey: apiKeyAnswer.apiKey,
280
437
  provider: finalProductInfo.preferredProvider
281
438
  };
282
- const edtoolsConfigPath = path.join(edtoolsDir, "config.json");
283
- await fs.writeFile(edtoolsConfigPath, JSON.stringify(edtoolsConfig, null, 2), "utf-8");
284
- const gitignorePath = path.join(edtoolsDir, ".gitignore");
285
- await fs.writeFile(gitignorePath, "*\n!.gitignore\n", "utf-8");
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");
286
443
  console.log("");
287
444
  console.log(successBox("Configuration created successfully!"));
288
445
  const filesCreated = `${chalk2.cyan("Files created:")}
@@ -302,17 +459,18 @@ ${chalk2.gray("API key stored securely (gitignored)")}`;
302
459
  }
303
460
 
304
461
  // src/cli/commands/generate.ts
305
- import fs2 from "fs-extra";
306
- import path2 from "path";
462
+ import fs3 from "fs-extra";
463
+ import path3 from "path";
307
464
  import { pathToFileURL } from "url";
308
465
  import chalk3 from "chalk";
309
466
  import ora2 from "ora";
310
467
  import Table from "cli-table3";
468
+ import inquirer2 from "inquirer";
311
469
  async function generateCommand(options) {
312
470
  console.log(chalk3.cyan.bold("\n\u{1F4DD} Generating content...\n"));
313
471
  const projectPath = process.cwd();
314
- const configPath = path2.join(projectPath, "edtools.config.js");
315
- if (!await fs2.pathExists(configPath)) {
472
+ const configPath = path3.join(projectPath, "edtools.config.js");
473
+ if (!await fs3.pathExists(configPath)) {
316
474
  console.log(chalk3.red("\u2717 No edtools.config.js found"));
317
475
  console.log(chalk3.yellow(' Run "edtools init" first\n'));
318
476
  process.exit(1);
@@ -326,10 +484,10 @@ async function generateCommand(options) {
326
484
  }
327
485
  const provider = productInfo.preferredProvider || "anthropic";
328
486
  let storedApiKey;
329
- const edtoolsConfigPath = path2.join(projectPath, ".edtools", "config.json");
330
- if (await fs2.pathExists(edtoolsConfigPath)) {
487
+ const edtoolsConfigPath = path3.join(projectPath, ".edtools", "config.json");
488
+ if (await fs3.pathExists(edtoolsConfigPath)) {
331
489
  try {
332
- const edtoolsConfig = await fs2.readJson(edtoolsConfigPath);
490
+ const edtoolsConfig = await fs3.readJson(edtoolsConfigPath);
333
491
  storedApiKey = edtoolsConfig.apiKey;
334
492
  } catch (error) {
335
493
  }
@@ -356,14 +514,14 @@ async function generateCommand(options) {
356
514
  }
357
515
  let topics = options.topics;
358
516
  if (options.fromCsv) {
359
- const opportunitiesPath = path2.join(projectPath, ".edtools", "opportunities.json");
360
- if (!await fs2.pathExists(opportunitiesPath)) {
517
+ const opportunitiesPath = path3.join(projectPath, ".edtools", "opportunities.json");
518
+ if (!await fs3.pathExists(opportunitiesPath)) {
361
519
  console.log(chalk3.red("\u2717 No CSV analysis found"));
362
520
  console.log(chalk3.yellow(' Run "edtools analyze" first to analyze your GSC data\n'));
363
521
  process.exit(1);
364
522
  }
365
523
  try {
366
- const oppData = await fs2.readJson(opportunitiesPath);
524
+ const oppData = await fs3.readJson(opportunitiesPath);
367
525
  topics = oppData.opportunities.slice(0, parseInt(options.posts, 10)).map((opp) => opp.suggestedTitle);
368
526
  console.log(chalk3.cyan("\u{1F4CA} Using topics from CSV analysis:\n"));
369
527
  topics.forEach((topic, i) => {
@@ -384,14 +542,58 @@ async function generateCommand(options) {
384
542
  console.log(chalk3.yellow(`\u26A0\uFE0F Generating ${count} posts at once may trigger spam detection`));
385
543
  console.log(chalk3.yellow(" Recommended: 3-5 posts per week\n"));
386
544
  }
387
- const outputDir = path2.resolve(projectPath, options.output);
388
- await fs2.ensureDir(outputDir);
545
+ let outputDirConfig;
546
+ let outputDirSource;
547
+ if (options.output) {
548
+ outputDirConfig = options.output;
549
+ outputDirSource = "CLI flag (--output)";
550
+ } else if (config.default.content?.outputDir) {
551
+ outputDirConfig = config.default.content.outputDir;
552
+ outputDirSource = "edtools.config.js";
553
+ } else {
554
+ outputDirConfig = "./blog";
555
+ outputDirSource = "default";
556
+ }
557
+ const outputDir = path3.resolve(projectPath, outputDirConfig);
558
+ await fs3.ensureDir(outputDir);
559
+ const hostingConfig = detectHostingConfig(projectPath);
560
+ if (hostingConfig) {
561
+ const validation = validateOutputDir(outputDirConfig, hostingConfig);
562
+ if (!validation.valid) {
563
+ console.log(chalk3.yellow.bold("\n\u26A0\uFE0F HOSTING PLATFORM DETECTED: ") + chalk3.white(validation.platform));
564
+ console.log(chalk3.yellow(" - Public directory: ") + chalk3.white(validation.publicDir));
565
+ console.log(chalk3.yellow(" - Your outputDir: ") + chalk3.white(validation.currentOutputDir));
566
+ console.log(chalk3.yellow(" - Problem: Files outside ") + chalk3.white(validation.publicDir + "/") + chalk3.yellow(" won't be accessible"));
567
+ console.log("");
568
+ console.log(chalk3.red.bold("\u274C ISSUE: Output directory misconfiguration"));
569
+ console.log(chalk3.red(" Generated files will return 404 in production"));
570
+ console.log("");
571
+ console.log(chalk3.cyan.bold("\u{1F4DD} Suggested fix:"));
572
+ console.log(chalk3.cyan(" Update edtools.config.js:"));
573
+ console.log("");
574
+ console.log(chalk3.gray(" content: {"));
575
+ console.log(chalk3.green(` outputDir: '${validation.suggestion}'`) + chalk3.gray(" // \u2705 Correct path"));
576
+ console.log(chalk3.gray(" }"));
577
+ console.log("");
578
+ const answer = await inquirer2.prompt([{
579
+ type: "confirm",
580
+ name: "continue",
581
+ message: "Continue anyway?",
582
+ default: false
583
+ }]);
584
+ if (!answer.continue) {
585
+ console.log(chalk3.yellow("\n\u2717 Generation cancelled\n"));
586
+ process.exit(0);
587
+ }
588
+ console.log("");
589
+ }
590
+ }
389
591
  console.log(chalk3.cyan("Configuration:"));
390
592
  console.log(` Product: ${chalk3.white(productInfo.name)}`);
391
593
  console.log(` Category: ${chalk3.white(productInfo.category)}`);
392
594
  console.log(` AI Provider: ${chalk3.white(provider === "anthropic" ? "Claude (Anthropic)" : "ChatGPT (OpenAI)")}`);
393
595
  console.log(` Posts to generate: ${chalk3.white(count)}`);
394
- console.log(` Output directory: ${chalk3.white(outputDir)}
596
+ console.log(` Output directory: ${chalk3.white(outputDir)} ${chalk3.gray("(" + outputDirSource + ")")}
395
597
  `);
396
598
  const generator = new ContentGenerator(apiKey, provider);
397
599
  const generateConfig = {
@@ -543,17 +745,17 @@ function truncateTitle(title, maxLen) {
543
745
  }
544
746
 
545
747
  // src/cli/commands/analyze.ts
546
- import fs4 from "fs-extra";
547
- import path3 from "path";
748
+ import fs5 from "fs-extra";
749
+ import path4 from "path";
548
750
  import chalk4 from "chalk";
549
751
  import ora3 from "ora";
550
752
  import Table2 from "cli-table3";
551
753
 
552
754
  // src/integrations/gsc-csv-analyzer.ts
553
- import fs3 from "fs-extra";
755
+ import fs4 from "fs-extra";
554
756
  import { parse } from "csv-parse/sync";
555
757
  async function parseGSCCSV(filePath) {
556
- const content = await fs3.readFile(filePath, "utf-8");
758
+ const content = await fs4.readFile(filePath, "utf-8");
557
759
  const records = parse(content, {
558
760
  columns: true,
559
761
  skip_empty_lines: true,
@@ -649,7 +851,7 @@ async function saveOpportunities(opportunities, outputPath) {
649
851
  count: opportunities.length,
650
852
  opportunities
651
853
  };
652
- await fs3.writeJson(outputPath, data, { spaces: 2 });
854
+ await fs4.writeJson(outputPath, data, { spaces: 2 });
653
855
  }
654
856
 
655
857
  // src/cli/commands/analyze.ts
@@ -658,9 +860,9 @@ async function analyzeCommand(options) {
658
860
  const projectPath = process.cwd();
659
861
  let csvPath;
660
862
  if (options.file) {
661
- csvPath = path3.resolve(options.file);
863
+ csvPath = path4.resolve(options.file);
662
864
  } else {
663
- const files = await fs4.readdir(projectPath);
865
+ const files = await fs5.readdir(projectPath);
664
866
  const csvFiles = files.filter((f) => f.endsWith(".csv") && f.toLowerCase().includes("search"));
665
867
  if (csvFiles.length === 0) {
666
868
  console.log(chalk4.red("\u2717 No CSV file found"));
@@ -673,9 +875,9 @@ async function analyzeCommand(options) {
673
875
  csvFiles.forEach((f) => console.log(` - ${f}`));
674
876
  console.log(chalk4.yellow("\nUsing first file. Specify with --file to use a different one.\n"));
675
877
  }
676
- csvPath = path3.join(projectPath, csvFiles[0]);
878
+ csvPath = path4.join(projectPath, csvFiles[0]);
677
879
  }
678
- if (!await fs4.pathExists(csvPath)) {
880
+ if (!await fs5.pathExists(csvPath)) {
679
881
  console.log(chalk4.red(`\u2717 File not found: ${csvPath}
680
882
  `));
681
883
  process.exit(1);
@@ -684,7 +886,7 @@ async function analyzeCommand(options) {
684
886
  let data;
685
887
  try {
686
888
  data = await parseGSCCSV(csvPath);
687
- spinner.succeed(`Parsed ${chalk4.white(data.length)} keywords from ${chalk4.white(path3.basename(csvPath))}`);
889
+ spinner.succeed(`Parsed ${chalk4.white(data.length)} keywords from ${chalk4.white(path4.basename(csvPath))}`);
688
890
  } catch (error) {
689
891
  spinner.fail("Failed to parse CSV");
690
892
  console.log(chalk4.red(`
@@ -789,9 +991,9 @@ Error: ${error.message}
789
991
  console.log(topTable.toString());
790
992
  console.log("");
791
993
  }
792
- const edtoolsDir = path3.join(projectPath, ".edtools");
793
- await fs4.ensureDir(edtoolsDir);
794
- const opportunitiesPath = path3.join(edtoolsDir, "opportunities.json");
994
+ const edtoolsDir = path4.join(projectPath, ".edtools");
995
+ await fs5.ensureDir(edtoolsDir);
996
+ const opportunitiesPath = path4.join(edtoolsDir, "opportunities.json");
795
997
  await saveOpportunities(analysis.opportunities, opportunitiesPath);
796
998
  console.log(successBox(`Analysis saved to ${chalk4.white(".edtools/opportunities.json")}`));
797
999
  console.log(chalk4.cyan.bold("Next steps:"));
@@ -802,16 +1004,16 @@ Error: ${error.message}
802
1004
  }
803
1005
 
804
1006
  // src/cli/commands/validate.ts
805
- import fs6 from "fs-extra";
806
- import path4 from "path";
1007
+ import fs7 from "fs-extra";
1008
+ import path5 from "path";
807
1009
  import chalk5 from "chalk";
808
1010
  import Table3 from "cli-table3";
809
1011
 
810
1012
  // src/utils/seo-validator.ts
811
1013
  import * as cheerio from "cheerio";
812
- import fs5 from "fs-extra";
1014
+ import fs6 from "fs-extra";
813
1015
  async function validatePost(htmlPath) {
814
- const html = await fs5.readFile(htmlPath, "utf-8");
1016
+ const html = await fs6.readFile(htmlPath, "utf-8");
815
1017
  const $ = cheerio.load(html);
816
1018
  const issues = [];
817
1019
  const passed = [];
@@ -1038,15 +1240,15 @@ async function validateCommand(options) {
1038
1240
  const projectPath = process.cwd();
1039
1241
  let htmlFiles = [];
1040
1242
  if (options.post) {
1041
- const postPath = path4.resolve(projectPath, options.post);
1042
- if (!await fs6.pathExists(postPath)) {
1243
+ const postPath = path5.resolve(projectPath, options.post);
1244
+ if (!await fs7.pathExists(postPath)) {
1043
1245
  console.log(errorBox(`Post not found: ${postPath}`));
1044
1246
  process.exit(1);
1045
1247
  }
1046
1248
  htmlFiles = [postPath];
1047
1249
  } else if (options.posts) {
1048
- const postsDir = path4.resolve(projectPath, options.posts);
1049
- if (!await fs6.pathExists(postsDir)) {
1250
+ const postsDir = path5.resolve(projectPath, options.posts);
1251
+ if (!await fs7.pathExists(postsDir)) {
1050
1252
  console.log(errorBox(`Directory not found: ${postsDir}`));
1051
1253
  process.exit(1);
1052
1254
  }
@@ -1056,8 +1258,8 @@ async function validateCommand(options) {
1056
1258
  process.exit(1);
1057
1259
  }
1058
1260
  } else {
1059
- const defaultBlogDir = path4.join(projectPath, "blog");
1060
- if (await fs6.pathExists(defaultBlogDir)) {
1261
+ const defaultBlogDir = path5.join(projectPath, "blog");
1262
+ if (await fs7.pathExists(defaultBlogDir)) {
1061
1263
  htmlFiles = await findHtmlFiles(defaultBlogDir);
1062
1264
  if (htmlFiles.length === 0) {
1063
1265
  console.log(errorBox("No HTML files found in blog/ directory"));
@@ -1102,8 +1304,8 @@ async function validateCommand(options) {
1102
1304
  })),
1103
1305
  stats: stats2
1104
1306
  };
1105
- const outputPath = path4.resolve(projectPath, options.output);
1106
- await fs6.writeJson(outputPath, report, { spaces: 2 });
1307
+ const outputPath = path5.resolve(projectPath, options.output);
1308
+ await fs7.writeJson(outputPath, report, { spaces: 2 });
1107
1309
  console.log(successBox(`Validation report saved to ${outputPath}`));
1108
1310
  }
1109
1311
  if (filteredResults.length > 0) {
@@ -1181,9 +1383,9 @@ async function validateCommand(options) {
1181
1383
  }
1182
1384
  async function findHtmlFiles(dir) {
1183
1385
  const files = [];
1184
- const entries = await fs6.readdir(dir, { withFileTypes: true });
1386
+ const entries = await fs7.readdir(dir, { withFileTypes: true });
1185
1387
  for (const entry of entries) {
1186
- const fullPath = path4.join(dir, entry.name);
1388
+ const fullPath = path5.join(dir, entry.name);
1187
1389
  if (entry.isDirectory()) {
1188
1390
  const subFiles = await findHtmlFiles(fullPath);
1189
1391
  files.push(...subFiles);
@@ -1207,18 +1409,163 @@ function capitalize(str) {
1207
1409
  return str.charAt(0).toUpperCase() + str.slice(1);
1208
1410
  }
1209
1411
 
1412
+ // src/cli/commands/doctor.ts
1413
+ import fs8 from "fs-extra";
1414
+ import path6 from "path";
1415
+ import { pathToFileURL as pathToFileURL2 } from "url";
1416
+ import chalk6 from "chalk";
1417
+ async function doctorCommand(options) {
1418
+ console.log(chalk6.cyan.bold("\n\u{1F50D} Diagnosing project configuration...\n"));
1419
+ const projectPath = process.cwd();
1420
+ const configPath = path6.join(projectPath, "edtools.config.js");
1421
+ const issues = [];
1422
+ const warnings = [];
1423
+ const suggestions = [];
1424
+ if (await fs8.pathExists(configPath)) {
1425
+ console.log(chalk6.green("\u2713 Configuration file found: ") + chalk6.white("edtools.config.js"));
1426
+ } else {
1427
+ console.log(chalk6.red("\u2717 Configuration file not found"));
1428
+ console.log(chalk6.yellow(' Run "edtools init" to create configuration\n'));
1429
+ process.exit(1);
1430
+ }
1431
+ let productInfo;
1432
+ let outputDir;
1433
+ try {
1434
+ const configUrl = pathToFileURL2(configPath).href;
1435
+ const config = await import(configUrl);
1436
+ productInfo = config.default.product;
1437
+ outputDir = config.default.content?.outputDir || "./blog";
1438
+ if (!productInfo || !productInfo.name) {
1439
+ issues.push("Invalid product configuration in edtools.config.js");
1440
+ }
1441
+ } catch (error) {
1442
+ issues.push(`Failed to load edtools.config.js: ${error.message}`);
1443
+ console.log(chalk6.red("\u2717 Failed to load configuration file"));
1444
+ console.log(chalk6.red(` Error: ${error.message}
1445
+ `));
1446
+ process.exit(1);
1447
+ }
1448
+ const outputDirPath = path6.resolve(projectPath, outputDir);
1449
+ if (await fs8.pathExists(outputDirPath)) {
1450
+ console.log(chalk6.green("\u2713 Output directory exists: ") + chalk6.white(outputDir));
1451
+ } else {
1452
+ warnings.push(`Output directory does not exist: ${outputDir}`);
1453
+ console.log(chalk6.yellow("\u26A0\uFE0F Output directory does not exist: ") + chalk6.white(outputDir));
1454
+ console.log(chalk6.yellow(' It will be created when you run "edtools generate"'));
1455
+ }
1456
+ const edtoolsConfigPath = path6.join(projectPath, ".edtools", "config.json");
1457
+ let hasApiKey = false;
1458
+ if (await fs8.pathExists(edtoolsConfigPath)) {
1459
+ try {
1460
+ const edtoolsConfig = await fs8.readJson(edtoolsConfigPath);
1461
+ hasApiKey = !!edtoolsConfig.apiKey;
1462
+ } catch (error) {
1463
+ }
1464
+ }
1465
+ const hasEnvApiKey = !!(process.env.ANTHROPIC_API_KEY || process.env.OPENAI_API_KEY);
1466
+ if (hasApiKey || hasEnvApiKey) {
1467
+ console.log(chalk6.green("\u2713 API key configured"));
1468
+ } else {
1469
+ warnings.push('No API key found (set via environment variable or "edtools init")');
1470
+ console.log(chalk6.yellow("\u26A0\uFE0F No API key configured"));
1471
+ console.log(chalk6.yellow(" Set ANTHROPIC_API_KEY or OPENAI_API_KEY environment variable"));
1472
+ console.log(chalk6.yellow(' Or run "edtools init" to store API key'));
1473
+ }
1474
+ console.log("");
1475
+ const hostingConfig = detectHostingConfig(projectPath);
1476
+ if (hostingConfig) {
1477
+ console.log(chalk6.cyan("\u26A0\uFE0F HOSTING PLATFORM DETECTED: ") + chalk6.white(hostingConfig.platform));
1478
+ console.log(chalk6.cyan(" - Public directory: ") + chalk6.white(hostingConfig.publicDir));
1479
+ console.log(chalk6.cyan(" - Your outputDir: ") + chalk6.white(outputDir));
1480
+ const validation = validateOutputDir(outputDir, hostingConfig);
1481
+ if (!validation.valid) {
1482
+ issues.push("Output directory misconfiguration");
1483
+ console.log(chalk6.red(" - Problem: Files outside ") + chalk6.white(hostingConfig.publicDir + "/") + chalk6.red(" won't be accessible"));
1484
+ console.log("");
1485
+ console.log(chalk6.red.bold("\u274C ISSUE: Output directory misconfiguration"));
1486
+ console.log(chalk6.red(" Generated files will return 404 in production"));
1487
+ console.log("");
1488
+ console.log(chalk6.cyan.bold("\u{1F4DD} Suggested fix:"));
1489
+ console.log(chalk6.cyan(" Update edtools.config.js:"));
1490
+ console.log("");
1491
+ console.log(chalk6.gray(" content: {"));
1492
+ console.log(chalk6.green(` outputDir: '${validation.suggestion}'`) + chalk6.gray(" // \u2705 Correct path"));
1493
+ console.log(chalk6.gray(" }"));
1494
+ console.log("");
1495
+ suggestions.push(`Update outputDir to '${validation.suggestion}'`);
1496
+ if (options.fix) {
1497
+ console.log(chalk6.yellow.bold("\u{1F527} Auto-fixing configuration...\n"));
1498
+ try {
1499
+ const configContent = await fs8.readFile(configPath, "utf-8");
1500
+ const updatedConfig = configContent.replace(
1501
+ /outputDir:\s*['"]([^'"]+)['"]/,
1502
+ `outputDir: '${validation.suggestion}'`
1503
+ );
1504
+ await fs8.writeFile(configPath, updatedConfig, "utf-8");
1505
+ console.log(chalk6.green("\u2713 Updated edtools.config.js"));
1506
+ console.log(chalk6.green(` outputDir: '${validation.suggestion}'`));
1507
+ console.log("");
1508
+ console.log(chalk6.cyan('\u{1F389} Configuration fixed! Run "edtools generate" to create content.\n'));
1509
+ } catch (error) {
1510
+ console.log(chalk6.red("\u2717 Failed to auto-fix configuration"));
1511
+ console.log(chalk6.red(` Error: ${error.message}`));
1512
+ console.log(chalk6.yellow(" Please update edtools.config.js manually\n"));
1513
+ }
1514
+ } else {
1515
+ console.log(chalk6.cyan.bold("Run: ") + chalk6.white("edtools doctor --fix"));
1516
+ console.log(chalk6.cyan("To automatically apply suggested fixes\n"));
1517
+ }
1518
+ } else {
1519
+ console.log(chalk6.green(" - Status: \u2705 Output directory correctly configured"));
1520
+ console.log("");
1521
+ }
1522
+ } else {
1523
+ console.log(chalk6.gray("No hosting platform configuration detected"));
1524
+ console.log(chalk6.gray("(No firebase.json, vercel.json, netlify.toml, or amplify.yml found)"));
1525
+ console.log("");
1526
+ }
1527
+ console.log(chalk6.cyan.bold("Summary:\n"));
1528
+ if (issues.length === 0 && warnings.length === 0) {
1529
+ console.log(chalk6.green.bold("\u2705 All checks passed! Your project is ready to generate content.\n"));
1530
+ } else {
1531
+ if (issues.length > 0) {
1532
+ console.log(chalk6.red.bold(`\u274C ${issues.length} issue(s) found:`));
1533
+ issues.forEach((issue) => {
1534
+ console.log(chalk6.red(` - ${issue}`));
1535
+ });
1536
+ console.log("");
1537
+ }
1538
+ if (warnings.length > 0) {
1539
+ console.log(chalk6.yellow.bold(`\u26A0\uFE0F ${warnings.length} warning(s):`));
1540
+ warnings.forEach((warning) => {
1541
+ console.log(chalk6.yellow(` - ${warning}`));
1542
+ });
1543
+ console.log("");
1544
+ }
1545
+ if (suggestions.length > 0 && !options.fix) {
1546
+ console.log(chalk6.cyan.bold("\u{1F4A1} Suggestions:"));
1547
+ suggestions.forEach((suggestion) => {
1548
+ console.log(chalk6.cyan(` - ${suggestion}`));
1549
+ });
1550
+ console.log("");
1551
+ console.log(chalk6.cyan("Run ") + chalk6.white("edtools doctor --fix") + chalk6.cyan(" to automatically apply fixes\n"));
1552
+ }
1553
+ }
1554
+ }
1555
+
1210
1556
  // src/cli/index.ts
1211
1557
  var program = new Command();
1212
- program.name("edtools").description("AI-Powered Content Marketing CLI - Generate, validate, and optimize SEO content").version("0.6.0");
1558
+ program.name("edtools").description("AI-Powered Content Marketing CLI - Generate, validate, and optimize SEO content").version("0.6.2");
1213
1559
  program.command("init").description("Initialize edtools in your project").option("-p, --path <path>", "Project path", process.cwd()).action(initCommand);
1214
- 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);
1560
+ 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);
1215
1561
  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);
1216
1562
  program.command("validate").description("Validate SEO quality of existing posts").option("--posts <dir>", "Posts directory to validate").option("--post <file>", "Single post file to validate").option("--output <file>", "Save validation report as JSON").option("--threshold <score>", "Only show posts below this score").action(validateCommand);
1563
+ program.command("doctor").description("Diagnose project configuration and hosting setup").option("--fix", "Automatically fix issues").action(doctorCommand);
1217
1564
  program.command("config").description("View or set configuration").option("--set-api-key <key>", "Set Anthropic API key").action(async (options) => {
1218
1565
  if (options.setApiKey) {
1219
- console.log(chalk6.green("\u2713 API key saved"));
1566
+ console.log(chalk7.green("\u2713 API key saved"));
1220
1567
  } else {
1221
- console.log(chalk6.cyan("Configuration:"));
1568
+ console.log(chalk7.cyan("Configuration:"));
1222
1569
  console.log(` API Key: ${process.env.ANTHROPIC_API_KEY ? "[set]" : "[not set]"}`);
1223
1570
  }
1224
1571
  });