@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 CHANGED
@@ -7,8 +7,8 @@ const program = new Command();
7
7
 
8
8
  program
9
9
  .name("xyle")
10
- .description("SEO Intelligence Engine CLI")
11
- .version("0.5.0");
10
+ .description("SEO & AEO Intelligence Engine CLI")
11
+ .version("0.8.0");
12
12
 
13
13
  registerCommands(program);
14
14
 
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@xyleapp/cli",
3
- "version": "0.6.0",
4
- "description": "CLI for the Xyle SEO Intelligence Engine",
3
+ "version": "0.8.0",
4
+ "description": "CLI for the Xyle SEO & AEO Intelligence Engine",
5
5
  "type": "module",
6
6
  "bin": {
7
7
  "xyle": "bin/xyle.mjs"
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 score = data.score || 0;
137
- const color = score >= 0.7 ? "\x1b[32m" : "\x1b[33m";
138
- console.log(`${color}Score: ${Math.round(score * 100)}%\x1b[0m`);
139
- console.log(`Summary: ${data.summary || ""}`);
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
  }