@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/chunk-BI3UJPWA.js +690 -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 +1 -1
- package/dist/cli/commands/generate.d.ts.map +1 -1
- package/dist/cli/commands/generate.js +50 -2
- 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/index.js +409 -62
- package/dist/cli/index.js.map +1 -1
- package/dist/core/generator.d.ts.map +1 -1
- package/dist/core/generator.js +6 -4
- package/dist/core/generator.js.map +1 -1
- package/dist/index.d.ts +20 -1
- package/dist/index.js +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/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/package.json +3 -2
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";
|
|
@@ -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 =
|
|
111
|
-
const configPath =
|
|
112
|
-
if (await
|
|
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 =
|
|
130
|
-
if (await
|
|
131
|
-
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");
|
|
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:
|
|
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
|
|
276
|
-
const edtoolsDir =
|
|
277
|
-
await
|
|
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 =
|
|
283
|
-
await
|
|
284
|
-
const gitignorePath =
|
|
285
|
-
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");
|
|
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
|
|
306
|
-
import
|
|
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 =
|
|
315
|
-
if (!await
|
|
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 =
|
|
330
|
-
if (await
|
|
487
|
+
const edtoolsConfigPath = path3.join(projectPath, ".edtools", "config.json");
|
|
488
|
+
if (await fs3.pathExists(edtoolsConfigPath)) {
|
|
331
489
|
try {
|
|
332
|
-
const edtoolsConfig = await
|
|
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 =
|
|
360
|
-
if (!await
|
|
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
|
|
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
|
-
|
|
388
|
-
|
|
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
|
|
547
|
-
import
|
|
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
|
|
755
|
+
import fs4 from "fs-extra";
|
|
554
756
|
import { parse } from "csv-parse/sync";
|
|
555
757
|
async function parseGSCCSV(filePath) {
|
|
556
|
-
const content = await
|
|
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
|
|
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 =
|
|
863
|
+
csvPath = path4.resolve(options.file);
|
|
662
864
|
} else {
|
|
663
|
-
const files = await
|
|
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 =
|
|
878
|
+
csvPath = path4.join(projectPath, csvFiles[0]);
|
|
677
879
|
}
|
|
678
|
-
if (!await
|
|
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(
|
|
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 =
|
|
793
|
-
await
|
|
794
|
-
const opportunitiesPath =
|
|
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
|
|
806
|
-
import
|
|
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
|
|
1014
|
+
import fs6 from "fs-extra";
|
|
813
1015
|
async function validatePost(htmlPath) {
|
|
814
|
-
const html = await
|
|
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 =
|
|
1042
|
-
if (!await
|
|
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 =
|
|
1049
|
-
if (!await
|
|
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 =
|
|
1060
|
-
if (await
|
|
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 =
|
|
1106
|
-
await
|
|
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
|
|
1386
|
+
const entries = await fs7.readdir(dir, { withFileTypes: true });
|
|
1185
1387
|
for (const entry of entries) {
|
|
1186
|
-
const fullPath =
|
|
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.
|
|
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
|
|
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(
|
|
1566
|
+
console.log(chalk7.green("\u2713 API key saved"));
|
|
1220
1567
|
} else {
|
|
1221
|
-
console.log(
|
|
1568
|
+
console.log(chalk7.cyan("Configuration:"));
|
|
1222
1569
|
console.log(` API Key: ${process.env.ANTHROPIC_API_KEY ? "[set]" : "[not set]"}`);
|
|
1223
1570
|
}
|
|
1224
1571
|
});
|