sproutkit-code 0.16.0 → 0.17.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.
Files changed (2) hide show
  1. package/dist/index.js +183 -4
  2. package/package.json +1 -1
package/dist/index.js CHANGED
@@ -30557,6 +30557,147 @@ var require_llm = __commonJS({
30557
30557
  }
30558
30558
  });
30559
30559
 
30560
+ // ../desktop/dist/main/lib/qa.js
30561
+ var require_qa = __commonJS({
30562
+ "../desktop/dist/main/lib/qa.js"(exports2) {
30563
+ "use strict";
30564
+ Object.defineProperty(exports2, "__esModule", { value: true });
30565
+ exports2.staticChecks = staticChecks;
30566
+ exports2.reviewWithClaude = reviewWithClaude;
30567
+ exports2.fixIssues = fixIssues;
30568
+ var llm_1 = require_llm();
30569
+ function staticChecks(args) {
30570
+ const { code, photoCount, phone, businessName } = args;
30571
+ const issues = [];
30572
+ const deadHrefs = code.match(/href\s*=\s*["'`]\s*#\s*["'`]/g)?.length ?? 0;
30573
+ if (deadHrefs > 0) {
30574
+ issues.push({
30575
+ severity: "warn",
30576
+ code: "dead_href",
30577
+ detail: `${deadHrefs} placeholder href="#" link(s) \u2014 nav/CTAs should point somewhere real (tel:, /book, or a section id).`
30578
+ });
30579
+ }
30580
+ if (/lorem ipsum|todo:|placeholder text/i.test(code)) {
30581
+ issues.push({
30582
+ severity: "error",
30583
+ code: "placeholder_copy",
30584
+ detail: "Placeholder copy (lorem ipsum / TODO) left in the page."
30585
+ });
30586
+ }
30587
+ if (businessName && !code.includes(businessName.split(" ")[0])) {
30588
+ issues.push({
30589
+ severity: "error",
30590
+ code: "missing_business_name",
30591
+ detail: `The business name ("${businessName}") doesn't appear in the page.`
30592
+ });
30593
+ }
30594
+ if (phone && !code.includes("tel:")) {
30595
+ issues.push({
30596
+ severity: "warn",
30597
+ code: "missing_phone_cta",
30598
+ detail: "A phone number is on file but there's no tel: link to call it."
30599
+ });
30600
+ }
30601
+ const referenced = new Set((code.match(/\/photo(\d+)\.jpg/g) ?? []).map((m) => Number(m.match(/\d+/)[0])));
30602
+ const overRef = [...referenced].filter((n) => n > photoCount);
30603
+ if (overRef.length > 0) {
30604
+ issues.push({
30605
+ severity: "error",
30606
+ code: "broken_image_ref",
30607
+ detail: `References /photo${overRef.join(", /photo")}.jpg but only ${photoCount} photo(s) were shipped \u2014 those images will be broken.`
30608
+ });
30609
+ }
30610
+ if (photoCount > 0 && referenced.size === 0 && !code.includes("PHOTOS")) {
30611
+ issues.push({
30612
+ severity: "warn",
30613
+ code: "unused_photos",
30614
+ detail: `${photoCount} real photo(s) were shipped but the page uses none of them.`
30615
+ });
30616
+ }
30617
+ if (/>\s*(Opening Hours|Hours)\s*<\/[a-z0-9]+>\s*<\/(div|section)>/i.test(code)) {
30618
+ issues.push({
30619
+ severity: "warn",
30620
+ code: "empty_section",
30621
+ detail: "An Opening Hours heading appears with no content under it \u2014 omit the section instead of rendering it empty."
30622
+ });
30623
+ }
30624
+ return issues;
30625
+ }
30626
+ async function reviewWithClaude(args) {
30627
+ const prompt = `You are reviewing a generated single-page React site (App.jsx) for a real local business before it's shown to the owner. Find only genuine DEFECTS \u2014 things that would look broken, unfinished, or wrong to that owner.
30628
+
30629
+ Business: "${args.businessName}" (${args.businessType})
30630
+
30631
+ Look for: sections that render empty or with placeholder content; copy that doesn't match this business/industry; links that go nowhere; text that's cut off mid-sentence; duplicated sections; contact details that contradict each other; obviously broken layout (e.g. a grid with one item, overlapping fixed elements).
30632
+ Do NOT report style preferences, colour choices, or "could be improved" suggestions. Only defects.
30633
+
30634
+ Respond with ONLY compact JSON, no prose:
30635
+ {"issues":[{"severity":"error"|"warn","code":"short_slug","detail":"one sentence"}]}
30636
+ If there are no real defects, return {"issues":[]}.
30637
+
30638
+ App.jsx:
30639
+ \`\`\`jsx
30640
+ ${args.code.slice(0, 45e3)}
30641
+ \`\`\``;
30642
+ try {
30643
+ const res = await (0, llm_1.generateText)({
30644
+ apiKey: args.apiKey,
30645
+ prompt,
30646
+ maxTokens: 2048,
30647
+ timeoutMs: 18e4
30648
+ });
30649
+ const raw = res.text.replace(/^```(?:json)?\s*/i, "").replace(/\s*```\s*$/, "").trim();
30650
+ const match = raw.match(/\{[\s\S]*\}/);
30651
+ if (!match)
30652
+ return [];
30653
+ const parsed = JSON.parse(match[0]);
30654
+ if (!Array.isArray(parsed.issues))
30655
+ return [];
30656
+ return parsed.issues.filter((i) => !!i && typeof i.detail === "string").slice(0, 12).map((i) => ({
30657
+ severity: i.severity === "error" ? "error" : "warn",
30658
+ code: String(i.code || "issue").slice(0, 40),
30659
+ detail: String(i.detail).slice(0, 300)
30660
+ }));
30661
+ } catch {
30662
+ return [];
30663
+ }
30664
+ }
30665
+ async function fixIssues(args) {
30666
+ if (args.issues.length === 0)
30667
+ return null;
30668
+ const list = args.issues.map((i, n) => `${n + 1}. [${i.severity}] ${i.detail}`).join("\n");
30669
+ const prompt = `Fix these specific defects in the React component below. Change ONLY what's needed to fix them \u2014 do not redesign, restyle, rewrite copy that isn't faulty, or remove working sections.
30670
+
30671
+ Defects:
30672
+ ${list}
30673
+
30674
+ Rules: keep the same design language and structure; if a section can't be filled with real content, remove the section entirely rather than leaving it empty; never invent image URLs (only use the existing PHOTOS entries); the file must remain a single component ending in \`export default App;\`.
30675
+
30676
+ Return ONLY the complete corrected App.jsx \u2014 no markdown fences, no commentary.
30677
+
30678
+ \`\`\`jsx
30679
+ ${args.code.slice(0, 45e3)}
30680
+ \`\`\``;
30681
+ try {
30682
+ const res = await (0, llm_1.generateText)({
30683
+ apiKey: args.apiKey,
30684
+ prompt,
30685
+ maxTokens: 64e3,
30686
+ timeoutMs: 3e5
30687
+ });
30688
+ const cleaned = res.text.replace(/^```[a-z]*\s*/i, "").replace(/\s*```$/, "").trim();
30689
+ if (!cleaned.includes("export default App"))
30690
+ return null;
30691
+ if (cleaned.length < args.code.length * 0.5)
30692
+ return null;
30693
+ return cleaned;
30694
+ } catch {
30695
+ return null;
30696
+ }
30697
+ }
30698
+ }
30699
+ });
30700
+
30560
30701
  // ../desktop/dist/main/lib/models.js
30561
30702
  var require_models = __commonJS({
30562
30703
  "../desktop/dist/main/lib/models.js"(exports2) {
@@ -79518,6 +79659,7 @@ var require_builder = __commonJS({
79518
79659
  var runtime_1 = require_runtime();
79519
79660
  var db_1 = require_db();
79520
79661
  var llm_1 = require_llm();
79662
+ var qa_1 = require_qa();
79521
79663
  var fs3 = __importStar(require("fs"));
79522
79664
  var path3 = __importStar(require("path"));
79523
79665
  var models_1 = require_models();
@@ -79760,7 +79902,7 @@ var require_builder = __commonJS({
79760
79902
  }
79761
79903
  }
79762
79904
  async function buildAndDeploySite(args) {
79763
- const { lead, anthropicKey, vercelToken, placesKey, customInstructions, onProgress, includeCms, blobToken, reuseAdminPassword, skipContentExtraction, multipage, pages } = args;
79905
+ const { lead, anthropicKey, vercelToken, placesKey, customInstructions, onProgress, includeCms, blobToken, reuseAdminPassword, skipContentExtraction, multipage, pages, skipQa } = args;
79764
79906
  const pageList = multipage && (!pages || pages.length === 0) ? ["Home", "Services", "About", "Contact"] : pages ?? [];
79765
79907
  if (includeCms && !blobToken) {
79766
79908
  throw new Error("CMS is enabled but no Vercel Blob token is set. Add it in Settings \u2192 Vercel Blob token.");
@@ -80073,6 +80215,35 @@ ${prompt}` : prompt;
80073
80215
  }
80074
80216
  }
80075
80217
  appCode = appCode.replace("REPLACE_PHOTOS", JSON.stringify(photoPaths));
80218
+ if (!skipQa) {
80219
+ onProgress("Reviewing the site for defects\u2026");
80220
+ const staticIssues = (0, qa_1.staticChecks)({
80221
+ code: appCode,
80222
+ photoCount: photoFiles.length,
80223
+ phone: lead.phone ?? "",
80224
+ businessName: lead.name
80225
+ });
80226
+ const modelIssues = await (0, qa_1.reviewWithClaude)({
80227
+ code: appCode,
80228
+ businessName: lead.name,
80229
+ businessType: lead.type ?? "local business",
80230
+ apiKey: anthropicKey
80231
+ });
80232
+ const issues = [...staticIssues, ...modelIssues];
80233
+ if (issues.length > 0) {
80234
+ const errors = issues.filter((i) => i.severity === "error").length;
80235
+ onProgress(`QA found ${issues.length} issue${issues.length === 1 ? "" : "s"}${errors ? ` (${errors} serious)` : ""} \u2014 fixing\u2026`);
80236
+ const fixed = await (0, qa_1.fixIssues)({ code: appCode, issues, apiKey: anthropicKey });
80237
+ if (fixed) {
80238
+ appCode = fixed;
80239
+ onProgress("QA fixes applied.");
80240
+ } else {
80241
+ onProgress("QA fix didn't come back clean \u2014 deploying the original.");
80242
+ }
80243
+ } else {
80244
+ onProgress("QA passed.");
80245
+ }
80246
+ }
80076
80247
  onProgress("Deploying to Vercel...");
80077
80248
  const indexHtml = `<!DOCTYPE html>
80078
80249
  <html lang="en"><head><meta charset="UTF-8"><meta name="viewport" content="width=device-width,initial-scale=1"><title>${lead.name}</title></head>
@@ -92918,6 +93089,10 @@ var require_chat_tools = __commonJS({
92918
93089
  multipage: {
92919
93090
  type: "boolean",
92920
93091
  description: "When true, build a MULTI-PAGE site (Home/Services/About/Contact as real routed pages with a nav) instead of a single long-scroll page. Use when the user asks for a multipage site or a bigger site with separate pages."
93092
+ },
93093
+ skipQa: {
93094
+ type: "boolean",
93095
+ description: "Skip the post-build QA review (a review + fix pass that catches broken links, empty sections, broken image refs). Only set true when the user explicitly wants maximum speed/volume over quality."
92921
93096
  }
92922
93097
  },
92923
93098
  required: ["id"]
@@ -93047,6 +93222,7 @@ var require_chat_tools = __commonJS({
93047
93222
  customInstructions: args.instructions,
93048
93223
  includeCms: args.includeCms,
93049
93224
  multipage: args.multipage,
93225
+ skipQa: args.skipQa,
93050
93226
  blobToken,
93051
93227
  onProgress: (msg) => args.ctx.emitProgress?.({
93052
93228
  channel: "sites:progress",
@@ -93339,6 +93515,7 @@ Interested?`;
93339
93515
  instructions: input.instructions,
93340
93516
  includeCms: !!input.includeCms,
93341
93517
  multipage: !!input.multipage,
93518
+ skipQa: input.skipQa === true,
93342
93519
  ctx
93343
93520
  });
93344
93521
  return { ...result, id };
@@ -93356,6 +93533,7 @@ Interested?`;
93356
93533
  id,
93357
93534
  instructions,
93358
93535
  multipage: !!input.multipage,
93536
+ skipQa: input.skipQa === true,
93359
93537
  ctx
93360
93538
  });
93361
93539
  return { ...result, id };
@@ -94419,7 +94597,7 @@ var require_package2 = __commonJS({
94419
94597
  "package.json"(exports2, module2) {
94420
94598
  module2.exports = {
94421
94599
  name: "sproutkit-code",
94422
- version: "0.16.0",
94600
+ version: "0.17.0",
94423
94601
  description: "Sproutkit in your terminal \u2014 find leads, build sites, run outreach, all via chat.",
94424
94602
  bin: {
94425
94603
  sproutkit: "./dist/index.js"
@@ -95576,7 +95754,8 @@ async function cmdBuild(argv) {
95576
95754
  id,
95577
95755
  instructions: flags.instructions ? String(flags.instructions) : void 0,
95578
95756
  includeCms: !!flags.cms,
95579
- multipage: !!flags.multipage
95757
+ multipage: !!flags.multipage,
95758
+ skipQa: !!flags["skip-qa"]
95580
95759
  },
95581
95760
  makeCtx({ json, status })
95582
95761
  );
@@ -95661,7 +95840,7 @@ async function cmdRegenerate(argv) {
95661
95840
  try {
95662
95841
  const result = await runTool(
95663
95842
  "regenerate_site",
95664
- { id, instructions, multipage: !!flags.multipage },
95843
+ { id, instructions, multipage: !!flags.multipage, skipQa: !!flags["skip-qa"] },
95665
95844
  makeCtx({ json, status })
95666
95845
  );
95667
95846
  if (result && result.ok === false) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "sproutkit-code",
3
- "version": "0.16.0",
3
+ "version": "0.17.0",
4
4
  "description": "Sproutkit in your terminal — find leads, build sites, run outreach, all via chat.",
5
5
  "bin": {
6
6
  "sproutkit": "./dist/index.js"