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.
- package/dist/index.js +183 -4
- 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.
|
|
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) {
|