seo-intel 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.env.example +41 -0
- package/LICENSE +75 -0
- package/README.md +243 -0
- package/Start SEO Intel.bat +9 -0
- package/Start SEO Intel.command +8 -0
- package/cli.js +3727 -0
- package/config/example.json +29 -0
- package/config/setup-wizard.js +522 -0
- package/crawler/index.js +566 -0
- package/crawler/robots.js +103 -0
- package/crawler/sanitize.js +124 -0
- package/crawler/schema-parser.js +168 -0
- package/crawler/sitemap.js +103 -0
- package/crawler/stealth.js +393 -0
- package/crawler/subdomain-discovery.js +341 -0
- package/db/db.js +213 -0
- package/db/schema.sql +120 -0
- package/exports/competitive.js +186 -0
- package/exports/heuristics.js +67 -0
- package/exports/queries.js +197 -0
- package/exports/suggestive.js +230 -0
- package/exports/technical.js +180 -0
- package/exports/templates.js +77 -0
- package/lib/gate.js +204 -0
- package/lib/license.js +369 -0
- package/lib/oauth.js +432 -0
- package/lib/updater.js +324 -0
- package/package.json +68 -0
- package/reports/generate-html.js +6194 -0
- package/reports/generate-site-graph.js +949 -0
- package/reports/gsc-loader.js +190 -0
- package/scheduler.js +142 -0
- package/seo-audit.js +619 -0
- package/seo-intel.png +0 -0
- package/server.js +602 -0
- package/setup/ROADMAP.md +109 -0
- package/setup/checks.js +483 -0
- package/setup/config-builder.js +227 -0
- package/setup/engine.js +65 -0
- package/setup/installers.js +197 -0
- package/setup/models.js +328 -0
- package/setup/openclaw-bridge.js +329 -0
- package/setup/validator.js +395 -0
- package/setup/web-routes.js +688 -0
- package/setup/wizard.html +2920 -0
- package/start-seo-intel.sh +8 -0
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
{
|
|
2
|
+
"project": "myproject",
|
|
3
|
+
"context": {
|
|
4
|
+
"siteName": "My Project",
|
|
5
|
+
"url": "https://example.com",
|
|
6
|
+
"industry": "Describe your industry and niche here",
|
|
7
|
+
"audience": "Who are your target users/customers?",
|
|
8
|
+
"goal": "What's your SEO objective? (e.g. outrank competitor X, grow organic traffic)",
|
|
9
|
+
"maturity": "early stage | growing | established"
|
|
10
|
+
},
|
|
11
|
+
"target": {
|
|
12
|
+
"domain": "example.com",
|
|
13
|
+
"maxPages": 200,
|
|
14
|
+
"crawlMode": "standard"
|
|
15
|
+
},
|
|
16
|
+
"competitors": [
|
|
17
|
+
{
|
|
18
|
+
"domain": "competitor1.com",
|
|
19
|
+
"maxPages": 100,
|
|
20
|
+
"crawlMode": "standard"
|
|
21
|
+
},
|
|
22
|
+
{
|
|
23
|
+
"domain": "competitor2.com",
|
|
24
|
+
"maxPages": 100,
|
|
25
|
+
"crawlMode": "standard"
|
|
26
|
+
}
|
|
27
|
+
],
|
|
28
|
+
"owned": []
|
|
29
|
+
}
|
|
@@ -0,0 +1,522 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* SEO Intel — Interactive Setup Wizard (CLI)
|
|
4
|
+
*
|
|
5
|
+
* Guides a new user through full setup:
|
|
6
|
+
* 1. System check (Node, npm, Ollama, Playwright)
|
|
7
|
+
* 2. Auto-install missing dependencies
|
|
8
|
+
* 3. Model selection (extraction + analysis tiers)
|
|
9
|
+
* 4. API key setup
|
|
10
|
+
* 5. Project configuration (target + competitors)
|
|
11
|
+
* 6. Pipeline validation (real crawl + extraction test)
|
|
12
|
+
* 7. Summary + next steps
|
|
13
|
+
*
|
|
14
|
+
* Uses the shared setup engine (setup/engine.js) for all logic.
|
|
15
|
+
*
|
|
16
|
+
* Usage:
|
|
17
|
+
* node config/setup-wizard.js
|
|
18
|
+
* node config/setup-wizard.js --project myproject
|
|
19
|
+
*/
|
|
20
|
+
|
|
21
|
+
import { createInterface } from 'readline';
|
|
22
|
+
import {
|
|
23
|
+
fullSystemCheck,
|
|
24
|
+
getModelRecommendations,
|
|
25
|
+
EXTRACTION_MODELS,
|
|
26
|
+
ANALYSIS_MODELS,
|
|
27
|
+
installNpmDeps,
|
|
28
|
+
installPlaywright,
|
|
29
|
+
pullOllamaModel,
|
|
30
|
+
createEnvFile,
|
|
31
|
+
runFullValidation,
|
|
32
|
+
buildProjectConfig,
|
|
33
|
+
writeProjectConfig,
|
|
34
|
+
updateEnvForSetup,
|
|
35
|
+
slugify,
|
|
36
|
+
domainFromUrl,
|
|
37
|
+
testApiKey,
|
|
38
|
+
} from '../setup/engine.js';
|
|
39
|
+
|
|
40
|
+
// ─── Chalk-lite (avoid import complexity) ──────────────────────────────────
|
|
41
|
+
const c = {
|
|
42
|
+
bold: s => `\x1b[1m${s}\x1b[0m`,
|
|
43
|
+
dim: s => `\x1b[2m${s}\x1b[0m`,
|
|
44
|
+
green: s => `\x1b[32m${s}\x1b[0m`,
|
|
45
|
+
yellow: s => `\x1b[33m${s}\x1b[0m`,
|
|
46
|
+
cyan: s => `\x1b[36m${s}\x1b[0m`,
|
|
47
|
+
red: s => `\x1b[31m${s}\x1b[0m`,
|
|
48
|
+
gold: s => `\x1b[38;5;214m${s}\x1b[0m`,
|
|
49
|
+
magenta:s => `\x1b[35m${s}\x1b[0m`,
|
|
50
|
+
};
|
|
51
|
+
|
|
52
|
+
const rl = createInterface({ input: process.stdin, output: process.stdout });
|
|
53
|
+
const ask = (q) => new Promise(res => rl.question(q, res));
|
|
54
|
+
|
|
55
|
+
function hr() { console.log(c.dim('─'.repeat(60))); }
|
|
56
|
+
function section(num, title) {
|
|
57
|
+
console.log('');
|
|
58
|
+
hr();
|
|
59
|
+
console.log(c.gold(c.bold(` Chapter ${num} — ${title}`)));
|
|
60
|
+
hr();
|
|
61
|
+
}
|
|
62
|
+
function ok(msg) { console.log(c.green(` ✓ ${msg}`)); }
|
|
63
|
+
function warn(msg) { console.log(c.yellow(` ⚠ ${msg}`)); }
|
|
64
|
+
function fail(msg) { console.log(c.red(` ✗ ${msg}`)); }
|
|
65
|
+
function info(msg) { console.log(c.dim(` ${msg}`)); }
|
|
66
|
+
function next(msg) { console.log(''); console.log(c.cyan(` → ${msg}`)); console.log(''); }
|
|
67
|
+
|
|
68
|
+
// ─── Main wizard ─────────────────────────────────────────────────────────────
|
|
69
|
+
|
|
70
|
+
async function run() {
|
|
71
|
+
console.log('');
|
|
72
|
+
console.log(c.gold(c.bold(' 🔶 SEO Intel — Setup Wizard')));
|
|
73
|
+
console.log(c.dim(' Point it at 5 domains. Get the gap report in 10 minutes.'));
|
|
74
|
+
console.log('');
|
|
75
|
+
|
|
76
|
+
const args = process.argv.slice(2);
|
|
77
|
+
const projectArg = args.includes('--project') ? args[args.indexOf('--project') + 1] : null;
|
|
78
|
+
|
|
79
|
+
// Track choices for later
|
|
80
|
+
let selectedOllamaHost = null;
|
|
81
|
+
let selectedExtractionModel = null;
|
|
82
|
+
let selectedAnalysisProvider = null;
|
|
83
|
+
let selectedApiKey = null;
|
|
84
|
+
|
|
85
|
+
// ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
|
86
|
+
// Chapter 1: System Check
|
|
87
|
+
// ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
|
88
|
+
section(1, 'System Check');
|
|
89
|
+
info('Scanning your system...');
|
|
90
|
+
console.log('');
|
|
91
|
+
|
|
92
|
+
const status = await fullSystemCheck();
|
|
93
|
+
|
|
94
|
+
// Node.js
|
|
95
|
+
if (status.node.meetsMinimum) {
|
|
96
|
+
ok(`Node.js ${status.node.version}`);
|
|
97
|
+
} else if (status.node.installed) {
|
|
98
|
+
fail(`Node.js ${status.node.version} — need v18+. Please upgrade.`);
|
|
99
|
+
} else {
|
|
100
|
+
fail('Node.js not found. Install from https://nodejs.org');
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
// npm
|
|
104
|
+
if (status.npm.installed) {
|
|
105
|
+
ok(`npm ${status.npm.version}`);
|
|
106
|
+
} else {
|
|
107
|
+
fail('npm not found');
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
// npm dependencies
|
|
111
|
+
if (status.npmDeps.installed) {
|
|
112
|
+
ok('npm dependencies installed');
|
|
113
|
+
} else {
|
|
114
|
+
warn(`Missing npm packages: ${status.npmDeps.missing.slice(0, 5).join(', ')}`);
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
// Playwright
|
|
118
|
+
if (status.playwright.installed && status.playwright.chromiumReady) {
|
|
119
|
+
ok('Playwright + Chromium ready');
|
|
120
|
+
} else if (status.playwright.installed) {
|
|
121
|
+
warn('Playwright installed but Chromium browser missing');
|
|
122
|
+
} else {
|
|
123
|
+
warn('Playwright not installed');
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
// Ollama
|
|
127
|
+
if (status.ollama.available) {
|
|
128
|
+
ok(`Ollama available (${status.ollama.mode}) — ${status.ollama.models.length} models at ${status.ollama.host}`);
|
|
129
|
+
} else if (status.ollama.installed) {
|
|
130
|
+
warn('Ollama installed but not running or no models. Start Ollama and pull a model.');
|
|
131
|
+
} else {
|
|
132
|
+
warn('Ollama not found — extraction will use degraded mode (regex only)');
|
|
133
|
+
info('Install from https://ollama.com for local AI extraction');
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
// VRAM / GPU
|
|
137
|
+
if (status.vram.available) {
|
|
138
|
+
ok(`GPU: ${status.vram.gpuName} — ${Math.round(status.vram.vramMB / 1024)}GB ${status.vram.source === 'apple-silicon-unified' ? '(unified memory, ~75% available for GPU)' : 'VRAM'}`);
|
|
139
|
+
} else {
|
|
140
|
+
info('No GPU detected — CPU mode available but slower');
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
// .env
|
|
144
|
+
if (status.env.exists) {
|
|
145
|
+
const keys = Object.entries(status.env.keys).filter(([_, v]) => v).map(([k]) => k);
|
|
146
|
+
if (keys.length > 0) {
|
|
147
|
+
ok(`.env found — keys: ${keys.join(', ')}`);
|
|
148
|
+
} else {
|
|
149
|
+
ok('.env found (no API keys configured yet)');
|
|
150
|
+
}
|
|
151
|
+
} else {
|
|
152
|
+
info('No .env file yet — will create one');
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
// Existing configs
|
|
156
|
+
if (status.configs.configs.length > 0) {
|
|
157
|
+
ok(`Existing projects: ${status.configs.configs.map(c => c.project).join(', ')}`);
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
if (!status.node.meetsMinimum) {
|
|
161
|
+
console.log('');
|
|
162
|
+
fail('Node.js 18+ is required. Please install it and re-run this wizard.');
|
|
163
|
+
rl.close();
|
|
164
|
+
process.exit(1);
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
// ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
|
168
|
+
// Chapter 2: Auto-Install
|
|
169
|
+
// ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
|
170
|
+
const needsInstall = !status.npmDeps.installed || !status.playwright.installed || !status.playwright.chromiumReady;
|
|
171
|
+
|
|
172
|
+
if (needsInstall) {
|
|
173
|
+
section(2, 'Install Dependencies');
|
|
174
|
+
|
|
175
|
+
// npm install
|
|
176
|
+
if (!status.npmDeps.installed) {
|
|
177
|
+
const answer = await ask(' Install npm dependencies? [Y/n]: ');
|
|
178
|
+
if (answer.toLowerCase() !== 'n') {
|
|
179
|
+
for await (const ev of installNpmDeps()) {
|
|
180
|
+
if (ev.status === 'start') info(ev.message);
|
|
181
|
+
else if (ev.status === 'done') ok(ev.message);
|
|
182
|
+
else if (ev.status === 'error') fail(ev.message);
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
// Playwright
|
|
188
|
+
if (!status.playwright.installed || !status.playwright.chromiumReady) {
|
|
189
|
+
const answer = await ask(' Install Playwright Chromium browser? (~150MB) [Y/n]: ');
|
|
190
|
+
if (answer.toLowerCase() !== 'n') {
|
|
191
|
+
for await (const ev of installPlaywright()) {
|
|
192
|
+
if (ev.status === 'start') info(ev.message);
|
|
193
|
+
else if (ev.status === 'done') ok(ev.message);
|
|
194
|
+
else if (ev.status === 'error') fail(ev.message);
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
// .env
|
|
200
|
+
if (!status.env.exists) {
|
|
201
|
+
for (const ev of createEnvFile()) {
|
|
202
|
+
if (ev.status === 'done') ok(ev.message);
|
|
203
|
+
}
|
|
204
|
+
}
|
|
205
|
+
} else {
|
|
206
|
+
section(2, 'Dependencies');
|
|
207
|
+
ok('All dependencies already installed.');
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
// ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
|
211
|
+
// Chapter 3: Model Selection
|
|
212
|
+
// ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
|
213
|
+
section(3, 'Choose Your Models');
|
|
214
|
+
|
|
215
|
+
const models = getModelRecommendations(
|
|
216
|
+
status.ollama.models,
|
|
217
|
+
status.env.keys,
|
|
218
|
+
status.vram.vramMB
|
|
219
|
+
);
|
|
220
|
+
|
|
221
|
+
// ── Extraction tier ──
|
|
222
|
+
console.log('');
|
|
223
|
+
console.log(c.bold(' 📦 Extraction Model (local, runs during crawl)'));
|
|
224
|
+
console.log(c.dim(' Extracts structured SEO data from each crawled page using a local AI model.'));
|
|
225
|
+
console.log(c.dim(' Minimum 4B parameters for reliable JSON extraction.'));
|
|
226
|
+
console.log('');
|
|
227
|
+
|
|
228
|
+
if (status.ollama.available) {
|
|
229
|
+
// Show available options
|
|
230
|
+
const fittingModels = models.allExtraction.filter(m => !m.legacy && m.fitsVram);
|
|
231
|
+
fittingModels.forEach((m, i) => {
|
|
232
|
+
const marker = m.installed ? c.green('✓ installed') : c.dim(' not pulled');
|
|
233
|
+
const rec = models.extraction?.model?.id === m.id ? c.gold(' ★ recommended') : '';
|
|
234
|
+
console.log(` ${i + 1}. ${c.bold(m.name)} (${m.vram}) — ${m.quality}${rec}`);
|
|
235
|
+
console.log(` ${c.dim(m.description)} ${marker}`);
|
|
236
|
+
});
|
|
237
|
+
|
|
238
|
+
console.log(` ${fittingModels.length + 1}. ${c.dim('Skip — use degraded mode (regex only, no AI)')}`);
|
|
239
|
+
console.log('');
|
|
240
|
+
|
|
241
|
+
const choice = await ask(` Choose extraction model [1-${fittingModels.length + 1}] (default: 1): `);
|
|
242
|
+
const idx = parseInt(choice.trim()) - 1;
|
|
243
|
+
|
|
244
|
+
if (idx >= 0 && idx < fittingModels.length) {
|
|
245
|
+
const chosen = fittingModels[idx];
|
|
246
|
+
selectedExtractionModel = chosen.id;
|
|
247
|
+
selectedOllamaHost = status.ollama.host;
|
|
248
|
+
ok(`Selected: ${chosen.name}`);
|
|
249
|
+
|
|
250
|
+
// Offer to pull if not installed
|
|
251
|
+
if (!chosen.installed && status.ollama.available) {
|
|
252
|
+
const pullAnswer = await ask(` Pull ${chosen.id} now? (may take a few minutes) [Y/n]: `);
|
|
253
|
+
if (pullAnswer.toLowerCase() !== 'n') {
|
|
254
|
+
for await (const ev of pullOllamaModel(chosen.id, status.ollama.host)) {
|
|
255
|
+
if (ev.status === 'start') info(ev.message);
|
|
256
|
+
else if (ev.status === 'progress') process.stdout.write(`\r ${c.dim(ev.message)} `);
|
|
257
|
+
else if (ev.status === 'done') { process.stdout.write('\r'); ok(ev.message); }
|
|
258
|
+
else if (ev.status === 'error') { process.stdout.write('\r'); fail(ev.message); }
|
|
259
|
+
}
|
|
260
|
+
}
|
|
261
|
+
}
|
|
262
|
+
} else {
|
|
263
|
+
info('Skipping extraction model — will use regex fallback.');
|
|
264
|
+
}
|
|
265
|
+
} else {
|
|
266
|
+
warn('No Ollama available. Extraction will use degraded mode (regex only).');
|
|
267
|
+
info('Install Ollama (https://ollama.com) and pull a model for better results.');
|
|
268
|
+
info('Recommended: ollama pull qwen3.5:9b');
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
// ── Analysis tier ──
|
|
272
|
+
console.log('');
|
|
273
|
+
console.log(c.bold(' 🧠 Analysis Model (cloud, runs during analysis)'));
|
|
274
|
+
console.log(c.dim(' A powerful model analyzes your crawl data to find keyword gaps,'));
|
|
275
|
+
console.log(c.dim(' competitive opportunities, and strategic recommendations.'));
|
|
276
|
+
console.log(c.dim(' Cloud models recommended — they have larger context windows.'));
|
|
277
|
+
console.log('');
|
|
278
|
+
|
|
279
|
+
const cloudModels = ANALYSIS_MODELS.filter(m => m.type === 'cloud');
|
|
280
|
+
cloudModels.forEach((m, i) => {
|
|
281
|
+
const configured = models.allAnalysis.find(am => am.id === m.id)?.configured;
|
|
282
|
+
const marker = configured ? c.green('✓ key found') : c.dim(' needs key');
|
|
283
|
+
const rec = m.recommended ? c.gold(' ★') : '';
|
|
284
|
+
console.log(` ${i + 1}. ${c.bold(m.name)} — ${m.context} ctx, ${m.costNote}${rec}`);
|
|
285
|
+
console.log(` ${c.dim(m.description)} ${marker}`);
|
|
286
|
+
});
|
|
287
|
+
console.log(` ${cloudModels.length + 1}. ${c.dim('Skip — no cloud analysis (local only)')}`);
|
|
288
|
+
console.log('');
|
|
289
|
+
|
|
290
|
+
const analysisChoice = await ask(` Choose analysis model [1-${cloudModels.length + 1}] (default: 1): `);
|
|
291
|
+
const analysisIdx = parseInt(analysisChoice.trim()) - 1;
|
|
292
|
+
|
|
293
|
+
if (analysisIdx >= 0 && analysisIdx < cloudModels.length) {
|
|
294
|
+
const chosen = cloudModels[analysisIdx];
|
|
295
|
+
selectedAnalysisProvider = chosen.id;
|
|
296
|
+
|
|
297
|
+
// Check if key already configured
|
|
298
|
+
const existing = models.allAnalysis.find(am => am.id === chosen.id);
|
|
299
|
+
if (existing?.configured) {
|
|
300
|
+
ok(`${chosen.name} — API key already configured.`);
|
|
301
|
+
// Read the actual key for validation
|
|
302
|
+
const envKeys = status.env.raw || {};
|
|
303
|
+
selectedApiKey = envKeys[chosen.envKey] || null;
|
|
304
|
+
} else {
|
|
305
|
+
console.log('');
|
|
306
|
+
info(`Get your ${chosen.name} API key: ${chosen.setupUrl}`);
|
|
307
|
+
info(chosen.setupNote);
|
|
308
|
+
const key = await ask(` Paste ${chosen.name} API key (or press Enter to skip): `);
|
|
309
|
+
if (key.trim()) {
|
|
310
|
+
selectedApiKey = key.trim();
|
|
311
|
+
// Validate immediately
|
|
312
|
+
info('Validating key...');
|
|
313
|
+
const testResult = await testApiKey(chosen.id, selectedApiKey);
|
|
314
|
+
if (testResult.valid) {
|
|
315
|
+
ok(`${chosen.name} API key valid (${testResult.latencyMs}ms)`);
|
|
316
|
+
updateEnvForSetup({ [chosen.id === 'gemini' ? 'geminiKey' : chosen.id === 'claude' ? 'anthropicKey' : chosen.id === 'openai' ? 'openaiKey' : 'deepseekKey']: selectedApiKey });
|
|
317
|
+
ok('Key saved to .env');
|
|
318
|
+
} else {
|
|
319
|
+
warn(`Key validation failed: ${testResult.error}`);
|
|
320
|
+
info('You can fix this later by editing .env');
|
|
321
|
+
}
|
|
322
|
+
} else {
|
|
323
|
+
info('Skipping — you can add the key later in .env');
|
|
324
|
+
}
|
|
325
|
+
}
|
|
326
|
+
} else {
|
|
327
|
+
info('Skipping cloud analysis — local mode only.');
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
// Save Ollama config to .env if changed
|
|
331
|
+
if (selectedOllamaHost && selectedExtractionModel) {
|
|
332
|
+
updateEnvForSetup({
|
|
333
|
+
ollamaUrl: selectedOllamaHost,
|
|
334
|
+
ollamaModel: selectedExtractionModel,
|
|
335
|
+
});
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
// ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
|
339
|
+
// Chapter 4: Project Setup
|
|
340
|
+
// ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
|
341
|
+
section(4, 'Your Project');
|
|
342
|
+
|
|
343
|
+
let projectName = projectArg;
|
|
344
|
+
if (!projectName) {
|
|
345
|
+
const input = await ask(' Project name (e.g. "myclient" or "mysite"): ');
|
|
346
|
+
projectName = slugify(input.trim()) || 'myproject';
|
|
347
|
+
}
|
|
348
|
+
|
|
349
|
+
// Check for existing config
|
|
350
|
+
if (status.configs.configs.find(c => c.project === projectName)) {
|
|
351
|
+
const overwrite = await ask(c.yellow(` Config for "${projectName}" already exists. Overwrite? [y/N]: `));
|
|
352
|
+
if (overwrite.toLowerCase() !== 'y') {
|
|
353
|
+
info(`Keeping existing config. Skipping to validation.`);
|
|
354
|
+
// Jump ahead to validation
|
|
355
|
+
await runValidationChapter(projectName, selectedOllamaHost, selectedExtractionModel, selectedAnalysisProvider, selectedApiKey);
|
|
356
|
+
rl.close();
|
|
357
|
+
return;
|
|
358
|
+
}
|
|
359
|
+
}
|
|
360
|
+
|
|
361
|
+
const siteUrl = await ask(' Your site URL (e.g. https://mysite.io): ');
|
|
362
|
+
const siteName = await ask(' Site name (e.g. Carbium): ');
|
|
363
|
+
const industry = await ask(' Industry / niche: ');
|
|
364
|
+
const audience = await ask(' Target audience: ');
|
|
365
|
+
const goal = await ask(' SEO goal in one sentence: ');
|
|
366
|
+
|
|
367
|
+
ok(`Project: ${projectName} → ${siteUrl}`);
|
|
368
|
+
|
|
369
|
+
// Owned subdomains
|
|
370
|
+
console.log('');
|
|
371
|
+
info('Do you have subdomains? (e.g. blog.example.com, docs.example.com)');
|
|
372
|
+
const owned = [];
|
|
373
|
+
const hasOwned = await ask(' Add owned subdomains? [y/N]: ');
|
|
374
|
+
if (hasOwned.toLowerCase() === 'y') {
|
|
375
|
+
let i = 1;
|
|
376
|
+
while (true) {
|
|
377
|
+
const sub = await ask(` Subdomain ${i} URL (or press Enter to finish): `);
|
|
378
|
+
if (!sub.trim()) break;
|
|
379
|
+
owned.push({ url: sub.trim() });
|
|
380
|
+
ok(`Added: ${domainFromUrl(sub.trim())}`);
|
|
381
|
+
i++;
|
|
382
|
+
}
|
|
383
|
+
}
|
|
384
|
+
|
|
385
|
+
// Competitors
|
|
386
|
+
console.log('');
|
|
387
|
+
info('Now add competitors (enter one domain per line, blank line when done):');
|
|
388
|
+
const competitors = [];
|
|
389
|
+
let compIdx = 1;
|
|
390
|
+
while (true) {
|
|
391
|
+
const comp = await ask(` Competitor ${compIdx} URL (or press Enter to finish): `);
|
|
392
|
+
if (!comp.trim()) break;
|
|
393
|
+
competitors.push({ url: comp.trim() });
|
|
394
|
+
ok(`Added: ${domainFromUrl(comp.trim())}`);
|
|
395
|
+
compIdx++;
|
|
396
|
+
}
|
|
397
|
+
|
|
398
|
+
if (competitors.length === 0) {
|
|
399
|
+
warn('No competitors added. You can edit the config file later.');
|
|
400
|
+
}
|
|
401
|
+
|
|
402
|
+
// Crawl settings
|
|
403
|
+
console.log('');
|
|
404
|
+
console.log(c.dim(' Crawl mode:'));
|
|
405
|
+
console.log(c.dim(' 1. Standard — fast, good for most sites'));
|
|
406
|
+
console.log(c.dim(' 2. Stealth — Playwright browser, bypasses bot detection (slower)'));
|
|
407
|
+
console.log(c.dim(' 3. Manual — you pass --stealth flag each time'));
|
|
408
|
+
|
|
409
|
+
const modeChoice = await ask(' Mode [1/2/3] (default: 1): ');
|
|
410
|
+
const crawlMode = modeChoice.trim() === '2' ? 'stealth' : modeChoice.trim() === '3' ? 'manual' : 'standard';
|
|
411
|
+
|
|
412
|
+
const pagesInput = await ask(' Pages per domain? [default: 50]: ');
|
|
413
|
+
const pagesPerDomain = parseInt(pagesInput.trim()) || 50;
|
|
414
|
+
|
|
415
|
+
// ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
|
416
|
+
// Chapter 5: Save Config
|
|
417
|
+
// ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
|
418
|
+
section(5, 'Saving Configuration');
|
|
419
|
+
|
|
420
|
+
const config = buildProjectConfig({
|
|
421
|
+
projectName,
|
|
422
|
+
targetUrl: siteUrl.trim(),
|
|
423
|
+
siteName: siteName.trim() || projectName,
|
|
424
|
+
industry: industry.trim(),
|
|
425
|
+
audience: audience.trim(),
|
|
426
|
+
goal: goal.trim(),
|
|
427
|
+
competitors,
|
|
428
|
+
owned,
|
|
429
|
+
crawlMode,
|
|
430
|
+
pagesPerDomain,
|
|
431
|
+
ollamaHost: selectedOllamaHost,
|
|
432
|
+
extractionModel: selectedExtractionModel,
|
|
433
|
+
});
|
|
434
|
+
|
|
435
|
+
const result = writeProjectConfig(config);
|
|
436
|
+
ok(`Config saved: ${result.path}`);
|
|
437
|
+
|
|
438
|
+
// ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
|
439
|
+
// Chapter 6: Pipeline Validation
|
|
440
|
+
// ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
|
441
|
+
await runValidationChapter(projectName, selectedOllamaHost, selectedExtractionModel, selectedAnalysisProvider, selectedApiKey, siteUrl.trim());
|
|
442
|
+
|
|
443
|
+
// ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
|
444
|
+
// Summary
|
|
445
|
+
// ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
|
446
|
+
section('✅', 'Setup Complete');
|
|
447
|
+
|
|
448
|
+
console.log(` ${c.bold('Project:')} ${projectName}`);
|
|
449
|
+
console.log(` ${c.bold('Target:')} ${siteUrl.trim()}`);
|
|
450
|
+
if (owned.length > 0) {
|
|
451
|
+
console.log(` ${c.bold('Owned:')} ${owned.map(o => domainFromUrl(o.url)).join(', ')}`);
|
|
452
|
+
}
|
|
453
|
+
console.log(` ${c.bold('Competitors:')} ${competitors.map(co => domainFromUrl(co.url)).join(', ') || 'none'}`);
|
|
454
|
+
console.log(` ${c.bold('Extraction:')} ${selectedExtractionModel ? `${selectedExtractionModel} (${selectedOllamaHost})` : 'degraded (regex)'}`);
|
|
455
|
+
console.log(` ${c.bold('Analysis:')} ${selectedAnalysisProvider || 'none'}`);
|
|
456
|
+
console.log(` ${c.bold('Config:')} config/${projectName}.json`);
|
|
457
|
+
|
|
458
|
+
console.log('');
|
|
459
|
+
console.log(c.cyan(' → Next steps:'));
|
|
460
|
+
console.log('');
|
|
461
|
+
console.log(c.bold(` node cli.js crawl ${projectName}${crawlMode === 'stealth' ? ' --stealth' : ''}`));
|
|
462
|
+
console.log(c.dim(' ↑ Crawl your site + all competitors'));
|
|
463
|
+
console.log('');
|
|
464
|
+
console.log(c.bold(` node cli.js html ${projectName}`));
|
|
465
|
+
console.log(c.dim(' ↑ Generate your SEO dashboard'));
|
|
466
|
+
console.log('');
|
|
467
|
+
console.log(c.bold(` node cli.js serve`));
|
|
468
|
+
console.log(c.dim(' ↑ Open the live dashboard in your browser'));
|
|
469
|
+
console.log('');
|
|
470
|
+
console.log(c.dim(' Any time you need help: node cli.js --help'));
|
|
471
|
+
console.log('');
|
|
472
|
+
|
|
473
|
+
rl.close();
|
|
474
|
+
}
|
|
475
|
+
|
|
476
|
+
// ── Validation Chapter ──────────────────────────────────────────────────────
|
|
477
|
+
|
|
478
|
+
async function runValidationChapter(projectName, ollamaHost, ollamaModel, apiProvider, apiKey, targetUrl) {
|
|
479
|
+
section(6, 'Pipeline Validation');
|
|
480
|
+
|
|
481
|
+
const runTest = await ask(' Run end-to-end validation? (crawl 1 page + extract) [Y/n]: ');
|
|
482
|
+
if (runTest.toLowerCase() === 'n') {
|
|
483
|
+
info('Skipping validation. You can run it later: node cli.js setup --project ' + projectName);
|
|
484
|
+
return;
|
|
485
|
+
}
|
|
486
|
+
|
|
487
|
+
console.log('');
|
|
488
|
+
|
|
489
|
+
const validationConfig = {
|
|
490
|
+
ollamaHost: ollamaHost || null,
|
|
491
|
+
ollamaModel: ollamaModel || null,
|
|
492
|
+
apiProvider: apiProvider || null,
|
|
493
|
+
apiKey: apiKey || null,
|
|
494
|
+
targetUrl: targetUrl || null,
|
|
495
|
+
};
|
|
496
|
+
|
|
497
|
+
for await (const step of runFullValidation(validationConfig)) {
|
|
498
|
+
if (step.step === 'summary') {
|
|
499
|
+
console.log('');
|
|
500
|
+
if (step.status === 'pass') {
|
|
501
|
+
ok(c.bold(`All tests passed! ${step.detail}`));
|
|
502
|
+
} else {
|
|
503
|
+
warn(`${step.detail} — some features may be limited.`);
|
|
504
|
+
}
|
|
505
|
+
continue;
|
|
506
|
+
}
|
|
507
|
+
|
|
508
|
+
const icon = step.status === 'pass' ? c.green('✓')
|
|
509
|
+
: step.status === 'fail' ? c.red('✗')
|
|
510
|
+
: step.status === 'skip' ? c.dim('○')
|
|
511
|
+
: c.yellow('…');
|
|
512
|
+
|
|
513
|
+
const label = step.step.padEnd(12);
|
|
514
|
+
console.log(` ${icon} ${label} ${step.detail}`);
|
|
515
|
+
}
|
|
516
|
+
}
|
|
517
|
+
|
|
518
|
+
run().catch(err => {
|
|
519
|
+
console.error('\n❌ Wizard error:', err.message);
|
|
520
|
+
if (process.env.DEBUG) console.error(err.stack);
|
|
521
|
+
process.exit(1);
|
|
522
|
+
});
|