@xyleapp/cli 0.6.0 → 0.8.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/bin/xyle.mjs +2 -2
- package/package.json +2 -2
- package/src/commands.mjs +196 -4
package/bin/xyle.mjs
CHANGED
package/package.json
CHANGED
package/src/commands.mjs
CHANGED
|
@@ -4,6 +4,9 @@
|
|
|
4
4
|
*/
|
|
5
5
|
|
|
6
6
|
import { createRequire } from "node:module";
|
|
7
|
+
import { existsSync } from "node:fs";
|
|
8
|
+
import { resolve } from "node:path";
|
|
9
|
+
import { execSync } from "node:child_process";
|
|
7
10
|
import { printJson, printTable } from "./formatting.mjs";
|
|
8
11
|
import {
|
|
9
12
|
checkHealth,
|
|
@@ -133,14 +136,63 @@ export function registerCommands(program) {
|
|
|
133
136
|
if (opts.json) {
|
|
134
137
|
console.log(printJson(data));
|
|
135
138
|
} else {
|
|
136
|
-
const
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
139
|
+
const scoreBar = (label, score) => {
|
|
140
|
+
const pct = Math.round((score || 0) * 100);
|
|
141
|
+
const filled = Math.round(pct / 5);
|
|
142
|
+
const bar = "\u2588".repeat(filled) + "\u2591".repeat(20 - filled);
|
|
143
|
+
const color = pct >= 70 ? "\x1b[32m" : pct >= 40 ? "\x1b[33m" : "\x1b[31m";
|
|
144
|
+
return ` ${label.padEnd(22)} ${color}${bar} ${pct}%\x1b[0m`;
|
|
145
|
+
};
|
|
146
|
+
|
|
147
|
+
// SEO Score Breakdown
|
|
148
|
+
const bd = data.seo_breakdown;
|
|
149
|
+
if (bd) {
|
|
150
|
+
console.log(`\n\x1b[1mSEO Score Breakdown\x1b[0m`);
|
|
151
|
+
console.log(scoreBar("Overall", bd.overall));
|
|
152
|
+
console.log(scoreBar("Technical (25%)", bd.technical));
|
|
153
|
+
console.log(scoreBar("On-Page (30%)", bd.on_page));
|
|
154
|
+
console.log(scoreBar("Content (25%)", bd.content));
|
|
155
|
+
console.log(scoreBar("Links (20%)", bd.links));
|
|
156
|
+
} else {
|
|
157
|
+
const score = data.score || 0;
|
|
158
|
+
const color = score >= 0.7 ? "\x1b[32m" : "\x1b[33m";
|
|
159
|
+
console.log(`${color}SEO Score: ${Math.round(score * 100)}%\x1b[0m`);
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
console.log(`\nSummary: ${data.summary || ""}`);
|
|
140
163
|
const missing = data.missing_topics || [];
|
|
141
164
|
if (missing.length) {
|
|
142
165
|
console.log(`Missing topics: ${missing.join(", ")}`);
|
|
143
166
|
}
|
|
167
|
+
|
|
168
|
+
// AEO Score
|
|
169
|
+
if (data.aeo_score != null) {
|
|
170
|
+
const aeoColor = data.aeo_score >= 0.7 ? "\x1b[32m" : "\x1b[33m";
|
|
171
|
+
console.log(`${aeoColor}AEO Score: ${Math.round(data.aeo_score * 100)}%\x1b[0m`);
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
// Structured Recommendations
|
|
175
|
+
const structured = data.recommendations || [];
|
|
176
|
+
if (structured.length) {
|
|
177
|
+
console.log(`\n\x1b[1mRecommendations\x1b[0m`);
|
|
178
|
+
const priorityColors = { critical: "\x1b[31m", important: "\x1b[33m", nice_to_have: "\x1b[2m" };
|
|
179
|
+
const priorityLabels = { critical: "CRITICAL", important: "IMPORTANT", nice_to_have: "NICE" };
|
|
180
|
+
for (const rec of structured) {
|
|
181
|
+
const pc = priorityColors[rec.priority] || "\x1b[0m";
|
|
182
|
+
const pl = priorityLabels[rec.priority] || rec.priority;
|
|
183
|
+
console.log(` ${pc}[${pl}]\x1b[0m \x1b[36m${rec.category}\x1b[0m ${rec.title}`);
|
|
184
|
+
console.log(` \x1b[2m${rec.description}\x1b[0m`);
|
|
185
|
+
}
|
|
186
|
+
} else {
|
|
187
|
+
// Fallback to old AEO recommendations
|
|
188
|
+
const recs = data.aeo_recommendations || [];
|
|
189
|
+
if (recs.length) {
|
|
190
|
+
console.log("\nAEO Recommendations:");
|
|
191
|
+
for (const rec of recs) {
|
|
192
|
+
console.log(` \x1b[36m\u2192\x1b[0m ${rec}`);
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
}
|
|
144
196
|
}
|
|
145
197
|
} catch (e) {
|
|
146
198
|
handleError(e);
|
|
@@ -184,6 +236,16 @@ export function registerCommands(program) {
|
|
|
184
236
|
if (opts.json) {
|
|
185
237
|
console.log(printJson(data));
|
|
186
238
|
} else {
|
|
239
|
+
const check = (v) => (v ? "\x1b[32m\u2713\x1b[0m" : "\x1b[31m\u2717\x1b[0m");
|
|
240
|
+
const scoreBar = (label, score) => {
|
|
241
|
+
const pct = Math.round((score || 0) * 100);
|
|
242
|
+
const filled = Math.round(pct / 5);
|
|
243
|
+
const bar = "\u2588".repeat(filled) + "\u2591".repeat(20 - filled);
|
|
244
|
+
const color = pct >= 70 ? "\x1b[32m" : pct >= 40 ? "\x1b[33m" : "\x1b[31m";
|
|
245
|
+
return ` ${label.padEnd(22)} ${color}${bar} ${pct}%\x1b[0m`;
|
|
246
|
+
};
|
|
247
|
+
|
|
248
|
+
console.log(`\n\x1b[1mCrawl Results\x1b[0m`);
|
|
187
249
|
console.log(`Title: ${data.title || "-"}`);
|
|
188
250
|
console.log(`Meta desc: ${data.meta_desc || "-"}`);
|
|
189
251
|
console.log(`Word count: ${data.word_count || 0}`);
|
|
@@ -194,6 +256,102 @@ export function registerCommands(program) {
|
|
|
194
256
|
console.log(` ${h}`);
|
|
195
257
|
}
|
|
196
258
|
}
|
|
259
|
+
|
|
260
|
+
// Technical SEO
|
|
261
|
+
const tech = data.technical_seo;
|
|
262
|
+
if (tech) {
|
|
263
|
+
console.log(`\n\x1b[1mTechnical SEO\x1b[0m`);
|
|
264
|
+
console.log(
|
|
265
|
+
` ${check(tech.is_https)} HTTPS ${check(tech.has_canonical)} Canonical ${check(tech.has_viewport)} Viewport`
|
|
266
|
+
);
|
|
267
|
+
console.log(
|
|
268
|
+
` ${check(tech.has_charset)} Charset ${check(tech.has_lang)} Lang (${tech.lang_value || "-"}) ${check(tech.has_favicon)} Favicon`
|
|
269
|
+
);
|
|
270
|
+
console.log(
|
|
271
|
+
` ${check(tech.has_og_title)} OG Title ${check(tech.has_og_desc)} OG Description ${check(tech.has_og_image)} OG Image`
|
|
272
|
+
);
|
|
273
|
+
console.log(
|
|
274
|
+
` ${check(tech.has_twitter_card)} Twitter Card ${check(tech.has_robots_meta)} Robots Meta ${check(tech.h1_count === 1)} H1 Count: ${tech.h1_count}`
|
|
275
|
+
);
|
|
276
|
+
console.log(` Title: ${tech.title_length} chars Meta desc: ${tech.meta_desc_length} chars Images without alt: ${tech.images_without_alt}/${tech.image_count}`);
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
// Rendering
|
|
280
|
+
const rendering = data.rendering;
|
|
281
|
+
if (rendering) {
|
|
282
|
+
console.log(`\n\x1b[1mRendering\x1b[0m`);
|
|
283
|
+
const fwLabel = rendering.detected_framework ? `\x1b[35m${rendering.detected_framework}\x1b[0m` : "none";
|
|
284
|
+
const rtColor = rendering.seo_impact === "good" ? "\x1b[32m" : rendering.seo_impact === "poor" ? "\x1b[31m" : "\x1b[33m";
|
|
285
|
+
console.log(` Framework: ${fwLabel} Type: ${rtColor}${rendering.rendering_type.toUpperCase()}\x1b[0m SEO Impact: ${rtColor}${rendering.seo_impact}\x1b[0m`);
|
|
286
|
+
console.log(` JS Bundles: ${rendering.js_bundle_count} DOM Content: ${rendering.initial_dom_has_content ? "rich" : "thin"} Client Router: ${rendering.uses_client_router ? "yes" : "no"}`);
|
|
287
|
+
if (rendering.rendering_notes) {
|
|
288
|
+
console.log(` \x1b[2m${rendering.rendering_notes}\x1b[0m`);
|
|
289
|
+
}
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
// Content Quality
|
|
293
|
+
const cq = data.content_quality;
|
|
294
|
+
if (cq) {
|
|
295
|
+
console.log(`\n\x1b[1mContent Quality\x1b[0m`);
|
|
296
|
+
console.log(` Words: ${cq.word_count} Sentences: ${cq.sentence_count} Paragraphs: ${cq.paragraph_count} Reading time: ${cq.reading_time_minutes} min`);
|
|
297
|
+
console.log(` Flesch Reading Ease: ${cq.flesch_reading_ease} Grade Level: ${cq.flesch_kincaid_grade} Vocabulary: ${Math.round(cq.vocabulary_richness * 100)}%`);
|
|
298
|
+
console.log(scoreBar("Humanness Score", cq.humanness_score));
|
|
299
|
+
console.log(scoreBar("Content Depth", cq.content_depth_score));
|
|
300
|
+
console.log(scoreBar("E-E-A-T Score", cq.eeat_score));
|
|
301
|
+
console.log(` ${check(cq.has_author)} Author ${check(cq.has_publish_date)} Publish Date ${check(cq.has_updated_date)} Updated Date ${check(cq.has_sources_section)} Sources`);
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
// Link Analysis
|
|
305
|
+
const links = data.link_analysis;
|
|
306
|
+
if (links) {
|
|
307
|
+
console.log(`\n\x1b[1mLink Analysis\x1b[0m`);
|
|
308
|
+
console.log(` Internal: ${links.internal_link_count} External: ${links.external_link_count} Nofollow: ${links.nofollow_link_count} In content: ${links.links_in_content}`);
|
|
309
|
+
const aqColor = links.anchor_quality === "good" ? "\x1b[32m" : links.anchor_quality === "poor" ? "\x1b[31m" : "\x1b[33m";
|
|
310
|
+
console.log(` Anchor quality: ${aqColor}${links.anchor_quality}\x1b[0m Generic anchors: ${links.generic_anchor_count} External ratio: ${Math.round(links.external_link_ratio * 100)}%`);
|
|
311
|
+
if (links.broken_link_sample && links.broken_link_sample.length) {
|
|
312
|
+
console.log(` \x1b[31mBroken links:\x1b[0m`);
|
|
313
|
+
for (const bl of links.broken_link_sample) {
|
|
314
|
+
console.log(` \x1b[31m\u2717\x1b[0m ${bl}`);
|
|
315
|
+
}
|
|
316
|
+
}
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
// Blog Signals
|
|
320
|
+
const blog = data.blog_signals;
|
|
321
|
+
if (blog && blog.is_blog_post) {
|
|
322
|
+
console.log(`\n\x1b[1mBlog Signals\x1b[0m`);
|
|
323
|
+
const freshColor = blog.content_freshness === "fresh" ? "\x1b[32m" : blog.content_freshness === "stale" ? "\x1b[31m" : "\x1b[33m";
|
|
324
|
+
console.log(` Author: ${blog.has_author_name || "-"} Published: ${blog.has_publish_date || "-"} Freshness: ${freshColor}${blog.content_freshness}\x1b[0m Reading time: ${blog.estimated_reading_time} min`);
|
|
325
|
+
console.log(` ${check(blog.has_social_sharing)} Social sharing`);
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
// AEO Signals
|
|
329
|
+
const aeo = data.aeo_signals;
|
|
330
|
+
if (aeo) {
|
|
331
|
+
console.log(`\n\x1b[1mAEO Signals\x1b[0m`);
|
|
332
|
+
console.log(
|
|
333
|
+
` ${check(aeo.has_article_schema)} Article Schema ${check(aeo.has_faq_schema)} FAQPage Schema ${check(aeo.has_howto_schema)} HowTo Schema`
|
|
334
|
+
);
|
|
335
|
+
console.log(
|
|
336
|
+
` ${check(aeo.heading_hierarchy_valid)} Heading Hierarchy ${check(aeo.has_faq_content)} FAQ Content ${check(aeo.has_date_modified)} Date Modified`
|
|
337
|
+
);
|
|
338
|
+
console.log(
|
|
339
|
+
` ${check(aeo.has_speakable_schema)} Speakable Schema ${check(aeo.has_breadcrumb_schema)} Breadcrumb Schema ${check(aeo.has_org_schema)} Org Schema`
|
|
340
|
+
);
|
|
341
|
+
console.log(
|
|
342
|
+
` ${check(aeo.has_video_schema)} Video Schema`
|
|
343
|
+
);
|
|
344
|
+
console.log(
|
|
345
|
+
` Lists: ${aeo.list_count} Tables: ${aeo.table_count} Concise answers: ${aeo.concise_answer_count} Definitions: ${aeo.definition_count} Citations: ${aeo.citation_count}`
|
|
346
|
+
);
|
|
347
|
+
console.log(
|
|
348
|
+
` Questions: ${aeo.question_count} Direct answer ratio: ${(aeo.direct_answer_ratio * 100).toFixed(0)}%`
|
|
349
|
+
);
|
|
350
|
+
if (aeo.avg_sentence_length > 0) {
|
|
351
|
+
console.log(` Avg sentence length: ${aeo.avg_sentence_length} words`);
|
|
352
|
+
}
|
|
353
|
+
}
|
|
354
|
+
|
|
197
355
|
const wc = data.word_count || 0;
|
|
198
356
|
if (wc > 0 && wc < 50) {
|
|
199
357
|
console.log(
|
|
@@ -435,4 +593,38 @@ export function registerCommands(program) {
|
|
|
435
593
|
);
|
|
436
594
|
}
|
|
437
595
|
});
|
|
596
|
+
|
|
597
|
+
// --- deploy ---
|
|
598
|
+
program
|
|
599
|
+
.command("deploy")
|
|
600
|
+
.description("Deploy Xyle services (API, frontend, trigger.dev)")
|
|
601
|
+
.option("--api", "Deploy API to Cloud Run")
|
|
602
|
+
.option("--frontend", "Deploy frontend to Vercel")
|
|
603
|
+
.option("--trigger", "Deploy Trigger.dev tasks")
|
|
604
|
+
.option("--dir <path>", "Project root directory", process.cwd())
|
|
605
|
+
.action(async (opts) => {
|
|
606
|
+
const scriptPath = resolve(opts.dir, "scripts", "deploy.sh");
|
|
607
|
+
if (!existsSync(scriptPath)) {
|
|
608
|
+
process.stderr.write(
|
|
609
|
+
`\x1b[31mDeploy script not found: ${scriptPath}\x1b[0m\n` +
|
|
610
|
+
`\x1b[2mRun this command from the project root or use --dir <path>\x1b[0m\n`
|
|
611
|
+
);
|
|
612
|
+
process.exit(1);
|
|
613
|
+
}
|
|
614
|
+
|
|
615
|
+
const flags = [];
|
|
616
|
+
if (opts.api) flags.push("--api");
|
|
617
|
+
if (opts.frontend) flags.push("--frontend");
|
|
618
|
+
if (opts.trigger) flags.push("--trigger");
|
|
619
|
+
// No flags = deploy all (script's default behavior)
|
|
620
|
+
|
|
621
|
+
const cmd = `bash "${scriptPath}" ${flags.join(" ")}`;
|
|
622
|
+
console.log(`\x1b[36mRunning:\x1b[0m ${cmd}\n`);
|
|
623
|
+
try {
|
|
624
|
+
execSync(cmd, { stdio: "inherit", cwd: opts.dir });
|
|
625
|
+
} catch (e) {
|
|
626
|
+
process.stderr.write(`\x1b[31mDeploy failed.\x1b[0m\n`);
|
|
627
|
+
process.exit(e.status || 1);
|
|
628
|
+
}
|
|
629
|
+
});
|
|
438
630
|
}
|