sproutkit-code 1.3.0 → 1.4.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/commands/sproutkit-book.md +18 -0
- package/dist/commands/sproutkit-build.md +18 -0
- package/dist/commands/sproutkit-find.md +15 -0
- package/dist/commands/sproutkit-handover.md +22 -0
- package/dist/commands/sproutkit-leads.md +16 -0
- package/dist/commands/sproutkit-outreach.md +17 -0
- package/dist/commands/sproutkit-pipeline.md +20 -0
- package/dist/commands/sproutkit.md +15 -0
- package/dist/index.js +524 -14
- package/dist/skill/SKILL.md +14 -0
- package/package.json +1 -1
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
---
|
|
2
|
+
description: Build a booking site (for a lead, or any business by name)
|
|
3
|
+
argument-hint: "<leadId> | \"Business Name, Town\" [--standalone]"
|
|
4
|
+
allowed-tools: Bash(sproutkit *)
|
|
5
|
+
---
|
|
6
|
+
Build a booking site. The operator asked: $ARGUMENTS
|
|
7
|
+
|
|
8
|
+
Use `sproutkit book`. Notes:
|
|
9
|
+
- A lead id adds booking onto a site Sproutkit already built (same domain, /book).
|
|
10
|
+
- `--standalone` gives a booking site on its OWN url — for a client who already
|
|
11
|
+
has their own website and just needs bookings.
|
|
12
|
+
- A business NAME instead of an id works even if they aren't a lead yet: it looks
|
|
13
|
+
them up, reads their site, and builds a booking page in one shot.
|
|
14
|
+
- `--archetype slots|reservation|enquiry`, `--instructions "…"`, `--email owner@x`.
|
|
15
|
+
|
|
16
|
+
Booking sites include a client `/admin` panel. The result carries `adminUrl` and
|
|
17
|
+
`adminPassword` — ALWAYS pass both on to the operator. The password is shown only
|
|
18
|
+
at build time, so if you swallow it the client can't log in.
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
---
|
|
2
|
+
description: Build a demo website for a lead
|
|
3
|
+
argument-hint: "<leadId> [--multipage] [instructions]"
|
|
4
|
+
allowed-tools: Bash(sproutkit *)
|
|
5
|
+
---
|
|
6
|
+
Build a site. The operator asked: $ARGUMENTS
|
|
7
|
+
|
|
8
|
+
Use `sproutkit build <leadId>`. Notes:
|
|
9
|
+
- `--multipage` gives real separate pages instead of one long scroll.
|
|
10
|
+
- Put any styling or content steer in `--instructions "…"`.
|
|
11
|
+
- `--skip-qa` only if they want speed over polish.
|
|
12
|
+
- No lead id given? Show `sproutkit list --status discovered` and ask which one.
|
|
13
|
+
|
|
14
|
+
Every generated site carries a DEMO banner by design — that's what stops clients
|
|
15
|
+
using the free preview as their real site. Don't try to remove it here; that's
|
|
16
|
+
what `/sproutkit-handover` does once they've paid.
|
|
17
|
+
|
|
18
|
+
When it's done, give the operator the live URL.
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
---
|
|
2
|
+
description: Find local businesses with no website (or no online booking)
|
|
3
|
+
argument-hint: "[area] e.g. Leeds LS1, or \"Swansea --no-booking\""
|
|
4
|
+
allowed-tools: Bash(sproutkit *)
|
|
5
|
+
---
|
|
6
|
+
Find new leads. The operator asked for: $ARGUMENTS
|
|
7
|
+
|
|
8
|
+
Run `sproutkit find` with that area. Notes:
|
|
9
|
+
- Default finds businesses with NO website. If they mention booking, WhatsApp
|
|
10
|
+
bookings, or "has a site already", add `--mode no_booking` instead.
|
|
11
|
+
- Pass `--categories` only if they named specific trades.
|
|
12
|
+
- Results stream in as each category returns, so let it finish before summarising.
|
|
13
|
+
|
|
14
|
+
Then report how many were found and the most promising few, with a one-line
|
|
15
|
+
reason each. Don't build anything yet.
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
---
|
|
2
|
+
description: Hand a finished site to a paying client (removes the DEMO banner)
|
|
3
|
+
argument-hint: "<leadId> [--domain thesalon.co.uk]"
|
|
4
|
+
allowed-tools: Bash(sproutkit *)
|
|
5
|
+
---
|
|
6
|
+
Hand over a site. The operator asked: $ARGUMENTS
|
|
7
|
+
|
|
8
|
+
This is the PAID-DELIVERABLE step. It removes the DEMO banner from a live site,
|
|
9
|
+
attaches the client's domain, and marks the lead won. The banner is what stops a
|
|
10
|
+
client using a free preview as their real site, so:
|
|
11
|
+
|
|
12
|
+
1. Confirm the operator is telling you this client has PAID or signed off. If
|
|
13
|
+
that isn't explicit, ask — don't infer it from the site looking finished.
|
|
14
|
+
2. Then run `sproutkit handover <leadId> --yes` (add `--domain x.co.uk` if given).
|
|
15
|
+
The command refuses without `--yes` by design.
|
|
16
|
+
|
|
17
|
+
Afterwards, relay the whole credentials sheet verbatim: site URL, /admin panel
|
|
18
|
+
and password, booking page and its password, and any DNS records the client has
|
|
19
|
+
to set at their registrar. That output is the client handover — don't summarise
|
|
20
|
+
the passwords away.
|
|
21
|
+
|
|
22
|
+
To re-check whether a domain's DNS has propagated, run the same command again.
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
---
|
|
2
|
+
description: Show current leads and their stage
|
|
3
|
+
argument-hint: "[filter] e.g. site_built, or Leeds"
|
|
4
|
+
allowed-tools: Bash(sproutkit *)
|
|
5
|
+
---
|
|
6
|
+
Leads:
|
|
7
|
+
|
|
8
|
+
!`sproutkit list --compact 2>/dev/null`
|
|
9
|
+
|
|
10
|
+
The operator asked to see: $ARGUMENTS
|
|
11
|
+
|
|
12
|
+
Every lead is listed above (one JSON object per line, nothing truncated).
|
|
13
|
+
|
|
14
|
+
Show the matching leads as a compact table — id, name, town, stage, and whether
|
|
15
|
+
they have a site / booking page. If they didn't narrow it down, group by stage.
|
|
16
|
+
Read-only: don't build, contact, or change anything.
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
---
|
|
2
|
+
description: Prepare outreach messages for leads with a built site
|
|
3
|
+
argument-hint: "[--ids 3,5] [--channel sms|whatsapp|email]"
|
|
4
|
+
allowed-tools: Bash(sproutkit *)
|
|
5
|
+
---
|
|
6
|
+
Prepare outreach. The operator asked: $ARGUMENTS
|
|
7
|
+
|
|
8
|
+
This CONTACTS REAL BUSINESSES and moves leads to Contacted, so:
|
|
9
|
+
|
|
10
|
+
1. First run `sproutkit list --status site_built --json` and show exactly who is
|
|
11
|
+
about to be contacted, with a count.
|
|
12
|
+
2. Ask the operator to confirm before running `sproutkit outreach`.
|
|
13
|
+
3. Only then run it. Never add `--ids` you invented — use the ones they gave.
|
|
14
|
+
|
|
15
|
+
Routing is per-lead automatically: mobile number → SMS/WhatsApp, otherwise email
|
|
16
|
+
(harvesting the address if needed). Only pass `--channel` if they asked for a
|
|
17
|
+
specific one.
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
---
|
|
2
|
+
description: Run the full pipeline — build then prepare outreach, in parallel
|
|
3
|
+
argument-hint: "[x3] [--book] [--limit 10]"
|
|
4
|
+
allowed-tools: Bash(sproutkit *)
|
|
5
|
+
---
|
|
6
|
+
Run the pipeline. The operator asked: $ARGUMENTS
|
|
7
|
+
|
|
8
|
+
This builds sites AND prepares outreach to real businesses, for many leads at
|
|
9
|
+
once. Before running:
|
|
10
|
+
|
|
11
|
+
1. Show which leads are queued (`sproutkit list --status discovered --json`) and
|
|
12
|
+
the count.
|
|
13
|
+
2. Confirm with the operator.
|
|
14
|
+
3. Run `sproutkit pipeline x<N>` where N is the concurrency they asked for
|
|
15
|
+
(default 3). Add `--book` to include a booking page, `--limit`/`--ids` to
|
|
16
|
+
scope it.
|
|
17
|
+
|
|
18
|
+
It prints a live board and can be stopped with the stop key. When it finishes,
|
|
19
|
+
report how many succeeded, and pass on any client admin panel credentials it
|
|
20
|
+
printed.
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
---
|
|
2
|
+
description: Sproutkit — show the pipeline and what to do next
|
|
3
|
+
allowed-tools: Bash(sproutkit *)
|
|
4
|
+
---
|
|
5
|
+
Current pipeline:
|
|
6
|
+
|
|
7
|
+
!`sproutkit list --compact 2>/dev/null`
|
|
8
|
+
|
|
9
|
+
Setup check:
|
|
10
|
+
|
|
11
|
+
!`sproutkit doctor 2>&1 | tail -20`
|
|
12
|
+
|
|
13
|
+
Summarise the pipeline for the operator in a few lines: how many leads at each
|
|
14
|
+
stage, and the single most useful next action. Mention any setup problem the
|
|
15
|
+
doctor output reveals. Don't run anything else unless asked.
|
package/dist/index.js
CHANGED
|
@@ -183,6 +183,17 @@ var require_db = __commonJS({
|
|
|
183
183
|
} catch {
|
|
184
184
|
}
|
|
185
185
|
}
|
|
186
|
+
for (const col of [
|
|
187
|
+
"booking_admin_url TEXT",
|
|
188
|
+
"booking_admin_password TEXT",
|
|
189
|
+
"handed_over_at TEXT",
|
|
190
|
+
"custom_domain TEXT"
|
|
191
|
+
]) {
|
|
192
|
+
try {
|
|
193
|
+
db2.exec(`ALTER TABLE leads ADD COLUMN ${col}`);
|
|
194
|
+
} catch {
|
|
195
|
+
}
|
|
196
|
+
}
|
|
186
197
|
try {
|
|
187
198
|
db2.exec("ALTER TABLE leads ADD COLUMN dead_at TEXT");
|
|
188
199
|
} catch {
|
|
@@ -31050,6 +31061,121 @@ export default function App() {
|
|
|
31050
31061
|
}
|
|
31051
31062
|
});
|
|
31052
31063
|
|
|
31064
|
+
// ../desktop/dist/main/lib/vercel-domains.js
|
|
31065
|
+
var require_vercel_domains = __commonJS({
|
|
31066
|
+
"../desktop/dist/main/lib/vercel-domains.js"(exports2) {
|
|
31067
|
+
"use strict";
|
|
31068
|
+
var __importDefault = exports2 && exports2.__importDefault || function(mod) {
|
|
31069
|
+
return mod && mod.__esModule ? mod : { "default": mod };
|
|
31070
|
+
};
|
|
31071
|
+
Object.defineProperty(exports2, "__esModule", { value: true });
|
|
31072
|
+
exports2.attachDomain = attachDomain;
|
|
31073
|
+
exports2.domainStatus = domainStatus;
|
|
31074
|
+
var axios_1 = __importDefault(require_axios());
|
|
31075
|
+
function qs(teamId) {
|
|
31076
|
+
return teamId ? `?teamId=${encodeURIComponent(teamId)}` : "";
|
|
31077
|
+
}
|
|
31078
|
+
function isApex(domain) {
|
|
31079
|
+
const parts = domain.split(".");
|
|
31080
|
+
if (parts.length <= 2)
|
|
31081
|
+
return true;
|
|
31082
|
+
const twoPartSuffixes = ["co.uk", "org.uk", "me.uk", "ac.uk", "com.au", "co.nz", "co.za", "com.br"];
|
|
31083
|
+
return parts.length === 3 && twoPartSuffixes.includes(parts.slice(-2).join("."));
|
|
31084
|
+
}
|
|
31085
|
+
async function fetchConfig(domain, projectId, vercelToken, teamId) {
|
|
31086
|
+
try {
|
|
31087
|
+
const params = new URLSearchParams({ projectIdOrName: projectId });
|
|
31088
|
+
if (teamId)
|
|
31089
|
+
params.set("teamId", teamId);
|
|
31090
|
+
const res = await axios_1.default.get(`https://api.vercel.com/v6/domains/${encodeURIComponent(domain)}/config?${params.toString()}`, { headers: { Authorization: `Bearer ${vercelToken}` }, timeout: 2e4 });
|
|
31091
|
+
return res.data;
|
|
31092
|
+
} catch {
|
|
31093
|
+
return null;
|
|
31094
|
+
}
|
|
31095
|
+
}
|
|
31096
|
+
function recordsFrom(domain, cfg) {
|
|
31097
|
+
const apex = isApex(domain);
|
|
31098
|
+
if (!cfg)
|
|
31099
|
+
return [];
|
|
31100
|
+
if (apex) {
|
|
31101
|
+
const ip = cfg.recommendedIPv4?.find((r) => r.rank === 1)?.value?.[0] ?? cfg.recommendedIPv4?.[0]?.value?.[0];
|
|
31102
|
+
if (!ip)
|
|
31103
|
+
return [];
|
|
31104
|
+
return [{ type: "A", name: "@", value: ip, note: "root domain" }];
|
|
31105
|
+
}
|
|
31106
|
+
const target = cfg.recommendedCNAME?.find((r) => r.rank === 1)?.value ?? cfg.recommendedCNAME?.[0]?.value;
|
|
31107
|
+
if (!target)
|
|
31108
|
+
return [];
|
|
31109
|
+
const sub = domain.split(".")[0];
|
|
31110
|
+
return [{ type: "CNAME", name: sub, value: target }];
|
|
31111
|
+
}
|
|
31112
|
+
async function attachDomain(args) {
|
|
31113
|
+
const { vercelToken, projectId, teamId } = args;
|
|
31114
|
+
const domain = args.domain.trim().toLowerCase().replace(/^https?:\/\//, "").replace(/\/.*$/, "");
|
|
31115
|
+
if (!/^[a-z0-9]([a-z0-9-]*[a-z0-9])?(\.[a-z0-9]([a-z0-9-]*[a-z0-9])?)+$/.test(domain)) {
|
|
31116
|
+
throw new Error(`"${args.domain}" doesn't look like a domain name (expected e.g. thesalon.co.uk).`);
|
|
31117
|
+
}
|
|
31118
|
+
let verified = false;
|
|
31119
|
+
let alreadyAttached = false;
|
|
31120
|
+
let apexName = domain;
|
|
31121
|
+
let ownershipChallenges = [];
|
|
31122
|
+
let warning;
|
|
31123
|
+
try {
|
|
31124
|
+
const res = await axios_1.default.post(`https://api.vercel.com/v10/projects/${encodeURIComponent(projectId)}/domains${qs(teamId)}`, { name: domain }, { headers: { Authorization: `Bearer ${vercelToken}` }, timeout: 2e4 });
|
|
31125
|
+
verified = !!res.data.verified;
|
|
31126
|
+
apexName = res.data.apexName ?? domain;
|
|
31127
|
+
ownershipChallenges = (res.data.verification ?? []).filter((v) => v.type?.toUpperCase() === "TXT").map((v) => ({ type: "TXT", name: v.domain, value: v.value, note: v.reason }));
|
|
31128
|
+
} catch (e) {
|
|
31129
|
+
const err = e;
|
|
31130
|
+
const status = err.response?.status;
|
|
31131
|
+
const msg = err.response?.data?.error?.message ?? e.message;
|
|
31132
|
+
if (status === 400) {
|
|
31133
|
+
alreadyAttached = true;
|
|
31134
|
+
} else if (status === 409) {
|
|
31135
|
+
throw new Error(`${domain} is already attached to a different Vercel project or account. Remove it there first, then retry. (${msg})`);
|
|
31136
|
+
} else if (status === 402) {
|
|
31137
|
+
throw new Error(`Vercel needs a payment method on the account before adding a custom domain. (${msg})`);
|
|
31138
|
+
} else if (status === 403) {
|
|
31139
|
+
throw new Error(`Your Vercel token doesn't have permission to add ${domain}. (${msg})`);
|
|
31140
|
+
} else {
|
|
31141
|
+
throw new Error(`Couldn't attach ${domain}: ${msg}`);
|
|
31142
|
+
}
|
|
31143
|
+
}
|
|
31144
|
+
const cfg = await fetchConfig(domain, projectId, vercelToken, teamId);
|
|
31145
|
+
const records = recordsFrom(domain, cfg);
|
|
31146
|
+
if (!records.length) {
|
|
31147
|
+
warning = "Couldn't read the DNS records from Vercel. Open the project's Domains tab in the Vercel dashboard to see the exact A/CNAME values.";
|
|
31148
|
+
}
|
|
31149
|
+
return {
|
|
31150
|
+
domain,
|
|
31151
|
+
apexDomain: apexName,
|
|
31152
|
+
verified,
|
|
31153
|
+
dnsConfigured: cfg ? cfg.misconfigured === false : false,
|
|
31154
|
+
alreadyAttached,
|
|
31155
|
+
records,
|
|
31156
|
+
ownershipChallenges,
|
|
31157
|
+
warning
|
|
31158
|
+
};
|
|
31159
|
+
}
|
|
31160
|
+
async function domainStatus(args) {
|
|
31161
|
+
const { domain, projectId, vercelToken, teamId } = args;
|
|
31162
|
+
let verified = false;
|
|
31163
|
+
try {
|
|
31164
|
+
const res = await axios_1.default.get(`https://api.vercel.com/v9/projects/${encodeURIComponent(projectId)}/domains/${encodeURIComponent(domain)}${qs(teamId)}`, { headers: { Authorization: `Bearer ${vercelToken}` }, timeout: 2e4 });
|
|
31165
|
+
verified = !!res.data.verified;
|
|
31166
|
+
} catch {
|
|
31167
|
+
}
|
|
31168
|
+
const cfg = await fetchConfig(domain, projectId, vercelToken, teamId);
|
|
31169
|
+
return {
|
|
31170
|
+
domain,
|
|
31171
|
+
verified,
|
|
31172
|
+
dnsConfigured: cfg ? cfg.misconfigured === false : false,
|
|
31173
|
+
records: recordsFrom(domain, cfg)
|
|
31174
|
+
};
|
|
31175
|
+
}
|
|
31176
|
+
}
|
|
31177
|
+
});
|
|
31178
|
+
|
|
31053
31179
|
// ../../node_modules/bcryptjs/umd/index.js
|
|
31054
31180
|
var require_umd = __commonJS({
|
|
31055
31181
|
"../../node_modules/bcryptjs/umd/index.js"(exports2, module2) {
|
|
@@ -34354,6 +34480,7 @@ export default async function handler(req, res) {
|
|
|
34354
34480
|
</style>
|
|
34355
34481
|
</head>
|
|
34356
34482
|
<body>
|
|
34483
|
+
<!--SPROUTKIT_BANNER-->
|
|
34357
34484
|
<div id="root"></div>
|
|
34358
34485
|
<script src="https://unpkg.com/react@18/umd/react.production.min.js" crossorigin></script>
|
|
34359
34486
|
<script src="https://unpkg.com/react-dom@18/umd/react-dom.production.min.js" crossorigin></script>
|
|
@@ -79674,6 +79801,7 @@ var require_builder = __commonJS({
|
|
|
79674
79801
|
exports2.redeployCmsOnly = redeployCmsOnly;
|
|
79675
79802
|
exports2.deployBookingOnly = deployBookingOnly2;
|
|
79676
79803
|
exports2.deployStandaloneBooking = deployStandaloneBooking2;
|
|
79804
|
+
exports2.handoverSite = handoverSite2;
|
|
79677
79805
|
var sdk_1 = __importDefault(require_sdk());
|
|
79678
79806
|
var axios_1 = __importDefault(require_axios());
|
|
79679
79807
|
var crypto_1 = require("crypto");
|
|
@@ -79685,6 +79813,7 @@ var require_builder = __commonJS({
|
|
|
79685
79813
|
var path3 = __importStar(require("path"));
|
|
79686
79814
|
var models_1 = require_models();
|
|
79687
79815
|
var cms_template_1 = require_cms_template();
|
|
79816
|
+
var vercel_domains_1 = require_vercel_domains();
|
|
79688
79817
|
var cms_runtime_1 = require_cms_runtime();
|
|
79689
79818
|
var booking_runtime_1 = require_booking_runtime();
|
|
79690
79819
|
function siteUidFor(leadId) {
|
|
@@ -79759,7 +79888,18 @@ var require_builder = __commonJS({
|
|
|
79759
79888
|
const pattern = new RegExp(`https?:\\/\\/[^\\s"'\`)]*(${STOCK_HOSTS.map((h) => h.replace(/\./g, "\\.")).join("|")})[^\\s"'\`)]*`, "gi");
|
|
79760
79889
|
return code.replace(pattern, "");
|
|
79761
79890
|
}
|
|
79762
|
-
function
|
|
79891
|
+
function bannerFor(lead) {
|
|
79892
|
+
let handedOver = false;
|
|
79893
|
+
try {
|
|
79894
|
+
const row = (0, db_1.getDb)().get("SELECT handed_over_at FROM leads WHERE id = ?", [lead.id]);
|
|
79895
|
+
handedOver = !!row?.handed_over_at;
|
|
79896
|
+
} catch {
|
|
79897
|
+
}
|
|
79898
|
+
return demoBanner(lead.name, handedOver);
|
|
79899
|
+
}
|
|
79900
|
+
function demoBanner(businessName, handedOver) {
|
|
79901
|
+
if (handedOver)
|
|
79902
|
+
return "";
|
|
79763
79903
|
const esc = (v) => v.replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">").replace(/"/g, """);
|
|
79764
79904
|
const label = `Preview site for ${esc(businessName)}`;
|
|
79765
79905
|
return `<div id="sproutkit-demo-banner" role="note" style="all:initial;display:block;box-sizing:border-box;width:100%;background:#1f2937;color:#f9fafb;font-family:ui-sans-serif,system-ui,-apple-system,'Segoe UI',Roboto,sans-serif;font-size:13px;line-height:1.4;padding:10px 16px;text-align:center;letter-spacing:.01em">
|
|
@@ -80339,7 +80479,7 @@ ${prompt}` : prompt;
|
|
|
80339
80479
|
onProgress("Deploying to Vercel...");
|
|
80340
80480
|
const indexHtml = `<!DOCTYPE html>
|
|
80341
80481
|
<html lang="en"><head><meta charset="UTF-8"><meta name="viewport" content="width=device-width,initial-scale=1"><title>${lead.name}</title></head>
|
|
80342
|
-
<body style="margin:0">${
|
|
80482
|
+
<body style="margin:0">${bannerFor(lead)}<div id="root"></div><script type="module" src="/src/main.jsx"></script></body></html>`;
|
|
80343
80483
|
const mainJsx = `import { StrictMode } from 'react';
|
|
80344
80484
|
import { createRoot } from 'react-dom/client';
|
|
80345
80485
|
import App from './App.jsx';
|
|
@@ -80534,7 +80674,7 @@ export default defineConfig({ plugins: [react()] });`;
|
|
|
80534
80674
|
{ source: "/book/", destination: "/book/index.html" }
|
|
80535
80675
|
]
|
|
80536
80676
|
});
|
|
80537
|
-
const indexHtmlCms = `<!DOCTYPE html><html lang="en"><head><meta charset="UTF-8"><meta name="viewport" content="width=device-width,initial-scale=1"><title>${lead.name}</title></head><body style="margin:0">${
|
|
80677
|
+
const indexHtmlCms = `<!DOCTYPE html><html lang="en"><head><meta charset="UTF-8"><meta name="viewport" content="width=device-width,initial-scale=1"><title>${lead.name}</title></head><body style="margin:0">${bannerFor(lead)}<div id="root"></div><script type="module" src="/src/main.jsx"></script></body></html>`;
|
|
80538
80678
|
const mainJsxCms = `import { StrictMode } from 'react';
|
|
80539
80679
|
import { createRoot } from 'react-dom/client';
|
|
80540
80680
|
import App from './App.jsx';
|
|
@@ -80611,7 +80751,7 @@ export default defineConfig({ plugins: [react()] });`;
|
|
|
80611
80751
|
snapshot[appJsxIndex] = { ...snapshot[appJsxIndex], data: modifiedApp };
|
|
80612
80752
|
saveSnapshot(lead.id, snapshot);
|
|
80613
80753
|
const files = snapshot.map((f) => ({ file: f.file, data: f.data, encoding: f.encoding }));
|
|
80614
|
-
const indexHtml = `<!DOCTYPE html><html lang="en"><head><meta charset="UTF-8"><meta name="viewport" content="width=device-width,initial-scale=1"><title>${lead.name}</title></head><body style="margin:0">${
|
|
80754
|
+
const indexHtml = `<!DOCTYPE html><html lang="en"><head><meta charset="UTF-8"><meta name="viewport" content="width=device-width,initial-scale=1"><title>${lead.name}</title></head><body style="margin:0">${bannerFor(lead)}<div id="root"></div><script type="module" src="/src/main.jsx"></script></body></html>`;
|
|
80615
80755
|
const mainJsx = `import { StrictMode } from 'react';
|
|
80616
80756
|
import { createRoot } from 'react-dom/client';
|
|
80617
80757
|
import App from './App.jsx';
|
|
@@ -80683,7 +80823,13 @@ export default defineConfig({ plugins: [react()] });`;
|
|
|
80683
80823
|
{ file: "vercel.json", data: vercelJson, encoding: "utf-8" },
|
|
80684
80824
|
{ file: "api/bookings.js", data: booking_runtime_1.API_BOOKINGS_JS, encoding: "utf-8" },
|
|
80685
80825
|
{ file: "api/booking-config.js", data: booking_runtime_1.API_BOOKING_CONFIG_JS, encoding: "utf-8" },
|
|
80686
|
-
{
|
|
80826
|
+
{
|
|
80827
|
+
file: "public/book/index.html",
|
|
80828
|
+
// The parent site's index.html already carries the banner; this page
|
|
80829
|
+
// just needs the placeholder removed.
|
|
80830
|
+
data: booking_runtime_1.BOOK_INDEX_HTML.replace("<!--SPROUTKIT_BANNER-->", ""),
|
|
80831
|
+
encoding: "utf-8"
|
|
80832
|
+
},
|
|
80687
80833
|
{ file: "public/book/app.js", data: booking_runtime_1.BOOK_APP_JS, encoding: "utf-8" },
|
|
80688
80834
|
// Refresh the CMS admin app (contains the Bookings tab now)
|
|
80689
80835
|
{ file: "public/admin/index.html", data: cms_runtime_1.ADMIN_INDEX_HTML, encoding: "utf-8" },
|
|
@@ -80711,8 +80857,7 @@ export default defineConfig({ plugins: [react()] });`;
|
|
|
80711
80857
|
async function deployStandaloneBooking2(args) {
|
|
80712
80858
|
const { lead, vercelToken, blobToken, resendKey, ownerEmail, bookingConfig, onProgress } = args;
|
|
80713
80859
|
const adminPassword = args.reuseAdminPassword ?? (0, cms_runtime_1.generateFriendlyPassword)();
|
|
80714
|
-
const
|
|
80715
|
-
const projectName = `sprout-book-${lead.id}-${slug}`;
|
|
80860
|
+
const projectName = standaloneBookingProjectName(lead);
|
|
80716
80861
|
onProgress("Seeding booking config\u2026");
|
|
80717
80862
|
const standaloneSiteUid = siteUidFor(lead.id);
|
|
80718
80863
|
try {
|
|
@@ -80776,7 +80921,7 @@ export default defineConfig({ plugins: [react()] });`;
|
|
|
80776
80921
|
];
|
|
80777
80922
|
files[0] = {
|
|
80778
80923
|
file: "index.html",
|
|
80779
|
-
data: booking_runtime_1.BOOK_INDEX_HTML.replace(/\/book\/app\.js/g, "/app.js"),
|
|
80924
|
+
data: booking_runtime_1.BOOK_INDEX_HTML.replace(/\/book\/app\.js/g, "/app.js").replace("<!--SPROUTKIT_BANNER-->", bannerFor(lead)),
|
|
80780
80925
|
encoding: "utf-8"
|
|
80781
80926
|
};
|
|
80782
80927
|
onProgress("Deploying booking site\u2026");
|
|
@@ -80809,6 +80954,149 @@ export default defineConfig({ plugins: [react()] });`;
|
|
|
80809
80954
|
admin: { url: `${publicUrl}/admin`, password: adminPassword }
|
|
80810
80955
|
};
|
|
80811
80956
|
}
|
|
80957
|
+
async function handoverSite2(args) {
|
|
80958
|
+
const { lead, vercelToken, blobToken, domain, teamId, onProgress } = args;
|
|
80959
|
+
const db2 = (0, db_1.getDb)();
|
|
80960
|
+
const row = db2.get(`SELECT site_url, booking_url, admin_url, admin_password,
|
|
80961
|
+
booking_admin_url, booking_admin_password, handed_over_at, site_uid
|
|
80962
|
+
FROM leads WHERE id = ?`, [lead.id]);
|
|
80963
|
+
if (!row)
|
|
80964
|
+
throw new Error("Lead not found");
|
|
80965
|
+
if (!row.site_url && !row.booking_url) {
|
|
80966
|
+
throw new Error("Nothing to hand over \u2014 this lead has no site and no booking page yet. Build one first.");
|
|
80967
|
+
}
|
|
80968
|
+
const alreadyHandedOver = !!row.handed_over_at;
|
|
80969
|
+
db2.run("UPDATE leads SET handed_over_at = COALESCE(handed_over_at, datetime('now')) WHERE id = ?", [
|
|
80970
|
+
lead.id
|
|
80971
|
+
]);
|
|
80972
|
+
let url = row.site_url ?? row.booking_url ?? "";
|
|
80973
|
+
let bannerRemoved = false;
|
|
80974
|
+
if (row.site_url) {
|
|
80975
|
+
try {
|
|
80976
|
+
onProgress("Rebuilding site without the demo banner\u2026");
|
|
80977
|
+
const result = await redeployWithoutBanner({
|
|
80978
|
+
lead,
|
|
80979
|
+
vercelToken,
|
|
80980
|
+
blobToken: blobToken ?? null,
|
|
80981
|
+
hasCms: !!row.admin_url,
|
|
80982
|
+
adminPassword: row.admin_password,
|
|
80983
|
+
siteUid: row.site_uid,
|
|
80984
|
+
onProgress
|
|
80985
|
+
});
|
|
80986
|
+
url = result.url;
|
|
80987
|
+
bannerRemoved = true;
|
|
80988
|
+
} catch (e) {
|
|
80989
|
+
db2.run("UPDATE leads SET handed_over_at = ? WHERE id = ?", [row.handed_over_at, lead.id]);
|
|
80990
|
+
throw new Error(`Couldn't rebuild the site without the banner: ${e.message}`);
|
|
80991
|
+
}
|
|
80992
|
+
}
|
|
80993
|
+
let domainInfo;
|
|
80994
|
+
if (domain) {
|
|
80995
|
+
onProgress(`Attaching ${domain}\u2026`);
|
|
80996
|
+
const projectId = row.site_url ? projectNameFor(lead) : standaloneBookingProjectName(lead);
|
|
80997
|
+
const attached = await (0, vercel_domains_1.attachDomain)({ domain, projectId, vercelToken, teamId });
|
|
80998
|
+
domainInfo = {
|
|
80999
|
+
domain: attached.domain,
|
|
81000
|
+
verified: attached.verified,
|
|
81001
|
+
dnsConfigured: attached.dnsConfigured,
|
|
81002
|
+
records: attached.records,
|
|
81003
|
+
ownershipChallenges: attached.ownershipChallenges,
|
|
81004
|
+
warning: attached.warning
|
|
81005
|
+
};
|
|
81006
|
+
db2.run("UPDATE leads SET custom_domain = ? WHERE id = ?", [attached.domain, lead.id]);
|
|
81007
|
+
}
|
|
81008
|
+
return {
|
|
81009
|
+
url,
|
|
81010
|
+
admin: row.admin_url && row.admin_password ? { url: row.admin_url, password: row.admin_password } : void 0,
|
|
81011
|
+
bookingUrl: row.booking_url ?? void 0,
|
|
81012
|
+
bookingAdmin: row.booking_admin_url && row.booking_admin_password ? { url: row.booking_admin_url, password: row.booking_admin_password } : void 0,
|
|
81013
|
+
domain: domainInfo,
|
|
81014
|
+
bannerRemoved,
|
|
81015
|
+
alreadyHandedOver
|
|
81016
|
+
};
|
|
81017
|
+
}
|
|
81018
|
+
async function redeployWithoutBanner(args) {
|
|
81019
|
+
const { lead, vercelToken, blobToken, hasCms, adminPassword, siteUid, onProgress } = args;
|
|
81020
|
+
const projectName = projectNameFor(lead);
|
|
81021
|
+
onProgress("Loading site source\u2026");
|
|
81022
|
+
const snapshot = loadSnapshot(lead.id);
|
|
81023
|
+
if (!snapshot || !snapshot.some((f) => f.file === "src/App.jsx")) {
|
|
81024
|
+
throw new Error("No local snapshot of this site. Run `sproutkit regenerate` once to save one, then hand over again.");
|
|
81025
|
+
}
|
|
81026
|
+
const files = snapshot.map((f) => ({
|
|
81027
|
+
file: f.file,
|
|
81028
|
+
data: f.data,
|
|
81029
|
+
encoding: f.encoding
|
|
81030
|
+
}));
|
|
81031
|
+
const multipage = /react-router-dom/.test(snapshot.find((f) => f.file === "src/App.jsx")?.data ?? "");
|
|
81032
|
+
const deps = { react: "^18.3.1", "react-dom": "^18.3.1" };
|
|
81033
|
+
if (multipage)
|
|
81034
|
+
deps["react-router-dom"] = "^6.26.2";
|
|
81035
|
+
if (hasCms) {
|
|
81036
|
+
deps["@vercel/blob"] = "^0.23.4";
|
|
81037
|
+
deps.bcryptjs = "^2.4.3";
|
|
81038
|
+
}
|
|
81039
|
+
const indexHtml = `<!DOCTYPE html><html lang="en"><head><meta charset="UTF-8"><meta name="viewport" content="width=device-width,initial-scale=1"><title>${lead.name}</title></head><body style="margin:0">${bannerFor(lead)}<div id="root"></div><script type="module" src="/src/main.jsx"></script></body></html>`;
|
|
81040
|
+
files.push({ file: "index.html", data: indexHtml, encoding: "utf-8" }, {
|
|
81041
|
+
file: "src/main.jsx",
|
|
81042
|
+
data: `import { StrictMode } from 'react';
|
|
81043
|
+
import { createRoot } from 'react-dom/client';
|
|
81044
|
+
import App from './App.jsx';
|
|
81045
|
+
createRoot(document.getElementById('root')).render(<StrictMode><App /></StrictMode>);`,
|
|
81046
|
+
encoding: "utf-8"
|
|
81047
|
+
}, {
|
|
81048
|
+
file: "vite.config.js",
|
|
81049
|
+
data: `import { defineConfig } from 'vite';
|
|
81050
|
+
import react from '@vitejs/plugin-react';
|
|
81051
|
+
export default defineConfig({ plugins: [react()] });`,
|
|
81052
|
+
encoding: "utf-8"
|
|
81053
|
+
}, {
|
|
81054
|
+
file: "package.json",
|
|
81055
|
+
data: JSON.stringify({
|
|
81056
|
+
name: "sprout-site",
|
|
81057
|
+
private: true,
|
|
81058
|
+
version: "0.1.0",
|
|
81059
|
+
type: "module",
|
|
81060
|
+
scripts: { build: "vite build", dev: "vite" },
|
|
81061
|
+
dependencies: deps,
|
|
81062
|
+
devDependencies: { "@vitejs/plugin-react": "^4.3.1", vite: "^5.4.2" }
|
|
81063
|
+
}),
|
|
81064
|
+
encoding: "utf-8"
|
|
81065
|
+
});
|
|
81066
|
+
if (hasCms && adminPassword && blobToken) {
|
|
81067
|
+
onProgress("Preserving the admin panel\u2026");
|
|
81068
|
+
files.push({
|
|
81069
|
+
file: "vercel.json",
|
|
81070
|
+
data: JSON.stringify({
|
|
81071
|
+
rewrites: [
|
|
81072
|
+
{ source: "/admin", destination: "/admin/index.html" },
|
|
81073
|
+
{ source: "/admin/", destination: "/admin/index.html" },
|
|
81074
|
+
{ source: "/book", destination: "/book/index.html" },
|
|
81075
|
+
{ source: "/book/", destination: "/book/index.html" }
|
|
81076
|
+
]
|
|
81077
|
+
}),
|
|
81078
|
+
encoding: "utf-8"
|
|
81079
|
+
}, { file: "api/content.js", data: cms_runtime_1.API_CONTENT_JS, encoding: "utf-8" }, { file: "api/admin/login.js", data: cms_runtime_1.API_ADMIN_LOGIN_JS, encoding: "utf-8" }, { file: "api/admin/whoami.js", data: cms_runtime_1.API_ADMIN_WHOAMI_JS, encoding: "utf-8" }, { file: "api/admin/upload.js", data: cms_runtime_1.API_ADMIN_UPLOAD_JS, encoding: "utf-8" }, { file: "public/admin/index.html", data: cms_runtime_1.ADMIN_INDEX_HTML, encoding: "utf-8" }, { file: "public/admin/app.js", data: cms_runtime_1.ADMIN_APP_JS, encoding: "utf-8" });
|
|
81080
|
+
const env = {
|
|
81081
|
+
BLOB_READ_WRITE_TOKEN: blobToken,
|
|
81082
|
+
ADMIN_PASSWORD_HASH: await (0, cms_runtime_1.hashPassword)(adminPassword),
|
|
81083
|
+
SESSION_SECRET: (0, cms_runtime_1.generateSessionSecret)(),
|
|
81084
|
+
BUSINESS_NAME: lead.name
|
|
81085
|
+
};
|
|
81086
|
+
if (siteUid)
|
|
81087
|
+
env.SITE_ID = siteUid;
|
|
81088
|
+
await setProjectEnv(projectName, vercelToken, env);
|
|
81089
|
+
}
|
|
81090
|
+
onProgress("Deploying\u2026");
|
|
81091
|
+
const res = await postVercelDeployment({ name: projectName, files, projectSettings: { framework: "vite" }, target: "production" }, vercelToken, onProgress);
|
|
81092
|
+
const url = await stableProjectUrl({
|
|
81093
|
+
projectName,
|
|
81094
|
+
vercelToken,
|
|
81095
|
+
fallbackUrl: `https://${res.data.url}`
|
|
81096
|
+
});
|
|
81097
|
+
onProgress(`Live at ${url}`);
|
|
81098
|
+
return { url };
|
|
81099
|
+
}
|
|
80812
81100
|
function hasBookLink(code) {
|
|
80813
81101
|
return /(href\s*=\s*["'`{]*\s*["'`]?\/book\/?["'`}]?)|(location(?:\.href)?\s*=\s*["'`]\/book["'`])/i.test(code);
|
|
80814
81102
|
}
|
|
@@ -80950,6 +81238,10 @@ Return this exact shape:
|
|
|
80950
81238
|
return null;
|
|
80951
81239
|
}
|
|
80952
81240
|
}
|
|
81241
|
+
function standaloneBookingProjectName(lead) {
|
|
81242
|
+
const slug = lead.name.toLowerCase().replace(/[^a-z0-9]/g, "-").replace(/-+/g, "-").slice(0, 26);
|
|
81243
|
+
return `sprout-book-${lead.id}-${slug}`;
|
|
81244
|
+
}
|
|
80953
81245
|
function projectNameFor(lead) {
|
|
80954
81246
|
const slug = lead.name.toLowerCase().replace(/[^a-z0-9]/g, "-").replace(/-+/g, "-").slice(0, 30);
|
|
80955
81247
|
return `sprout-${lead.id}-${slug}`;
|
|
@@ -93359,6 +93651,24 @@ var require_chat_tools = __commonJS({
|
|
|
93359
93651
|
},
|
|
93360
93652
|
requiresConfirmation: true
|
|
93361
93653
|
},
|
|
93654
|
+
{
|
|
93655
|
+
name: "hand_over_site",
|
|
93656
|
+
description: "HAND OVER a finished site to the paying client: removes the DEMO/preview banner, optionally attaches the client's own domain, and returns every credential in one place (site URL, /admin panel + password, booking page + its password, and the DNS records the client must set at their registrar). This is the paid-deliverable step \u2014 the banner exists to stop clients using a free demo as their real site, so only run this when the user says the client has paid / signed off / is going live. Requires the lead to already have a built site or booking page.",
|
|
93657
|
+
input_schema: {
|
|
93658
|
+
type: "object",
|
|
93659
|
+
properties: {
|
|
93660
|
+
id: { type: "number", description: "Lead id to hand over." },
|
|
93661
|
+
domain: {
|
|
93662
|
+
type: "string",
|
|
93663
|
+
description: "Optional: the client's own domain, e.g. 'thesalon.co.uk' or 'www.thesalon.co.uk'. Attaches it to the project and returns the exact DNS records to configure. Omit to keep the site on its .vercel.app URL."
|
|
93664
|
+
}
|
|
93665
|
+
},
|
|
93666
|
+
required: ["id"]
|
|
93667
|
+
},
|
|
93668
|
+
// Commercial + hard to walk back: it unbrands a live client site and marks
|
|
93669
|
+
// the lead won. Always ask first.
|
|
93670
|
+
requiresConfirmation: true
|
|
93671
|
+
},
|
|
93362
93672
|
{
|
|
93363
93673
|
name: "booking_site_for_business",
|
|
93364
93674
|
description: "ONE-SHOT: build a standalone booking site for a business the user NAMES, even if it isn't a lead yet. Give it the business name (include the town/area if known, e.g. 'Kings Barbers, Leeds'). It looks the business up on Google Places, reads their existing website to understand the business and check whether they already take bookings online, saves them as a lead, and deploys a booking page on its own URL for them to link from their site. Use this for requests like 'build a booking site for The Oak Barbers' when that business isn't in the pipeline. Costs a Places lookup + a Vercel deploy.",
|
|
@@ -94013,6 +94323,25 @@ Interested?`;
|
|
|
94013
94323
|
});
|
|
94014
94324
|
return { ok: true, id, ...result };
|
|
94015
94325
|
}
|
|
94326
|
+
case "hand_over_site": {
|
|
94327
|
+
const id = Number(input.id);
|
|
94328
|
+
const lead = db2.get("SELECT id, name, site_url, booking_url FROM leads WHERE id = ?", [id]);
|
|
94329
|
+
if (!lead)
|
|
94330
|
+
throw new Error(`Lead #${id} not found`);
|
|
94331
|
+
if (!lead.site_url && !lead.booking_url) {
|
|
94332
|
+
return {
|
|
94333
|
+
ok: false,
|
|
94334
|
+
error: `#${id} ${lead.name} has no site or booking page yet \u2014 build one before handing it over.`
|
|
94335
|
+
};
|
|
94336
|
+
}
|
|
94337
|
+
const result = await ctx.invokeIpc("site:handover", {
|
|
94338
|
+
leadId: id,
|
|
94339
|
+
domain: input.domain
|
|
94340
|
+
});
|
|
94341
|
+
db2.run("UPDATE leads SET status = 'won' WHERE id = ?", [id]);
|
|
94342
|
+
ctx.emitProgress?.({ channel: "leads:changed", payload: { reason: "handover" } });
|
|
94343
|
+
return { ok: true, id, business: lead.name, ...result };
|
|
94344
|
+
}
|
|
94016
94345
|
case "provision_cms": {
|
|
94017
94346
|
const id = Number(input.id);
|
|
94018
94347
|
const result = await ctx.invokeIpc("cms:provision", id);
|
|
@@ -95027,7 +95356,7 @@ var require_package2 = __commonJS({
|
|
|
95027
95356
|
"package.json"(exports2, module2) {
|
|
95028
95357
|
module2.exports = {
|
|
95029
95358
|
name: "sproutkit-code",
|
|
95030
|
-
version: "1.
|
|
95359
|
+
version: "1.4.0",
|
|
95031
95360
|
description: "Sproutkit in your terminal \u2014 find leads, build sites, run outreach, all via chat.",
|
|
95032
95361
|
bin: {
|
|
95033
95362
|
sproutkit: "./dist/index.js"
|
|
@@ -95087,6 +95416,7 @@ var provisionCmsForLead = cms.provisionCmsForLead;
|
|
|
95087
95416
|
var generatePreset = bookingPreset.generatePreset;
|
|
95088
95417
|
var deployBookingOnly = builder.deployBookingOnly;
|
|
95089
95418
|
var deployStandaloneBooking = builder.deployStandaloneBooking;
|
|
95419
|
+
var handoverSite = builder.handoverSite;
|
|
95090
95420
|
var createBlobStore = vercelStore.createBlobStore;
|
|
95091
95421
|
var refreshModels = models.refreshModels;
|
|
95092
95422
|
var refreshIfStale = models.refreshIfStale;
|
|
@@ -95405,6 +95735,43 @@ var StatusBoard = class {
|
|
|
95405
95735
|
var import_fs = __toESM(require("fs"));
|
|
95406
95736
|
var import_path = __toESM(require("path"));
|
|
95407
95737
|
var import_os2 = __toESM(require("os"));
|
|
95738
|
+
function bundledCommandsDir() {
|
|
95739
|
+
const candidates = [
|
|
95740
|
+
import_path.default.join(__dirname, "commands"),
|
|
95741
|
+
import_path.default.join(__dirname, "..", "dist", "commands"),
|
|
95742
|
+
import_path.default.join(__dirname, "..", "commands")
|
|
95743
|
+
];
|
|
95744
|
+
for (const c2 of candidates) {
|
|
95745
|
+
if (import_fs.default.existsSync(import_path.default.join(c2, "sproutkit.md"))) return c2;
|
|
95746
|
+
}
|
|
95747
|
+
return candidates[0];
|
|
95748
|
+
}
|
|
95749
|
+
function commandsTargetDir() {
|
|
95750
|
+
return import_path.default.join(import_os2.default.homedir(), ".claude", "commands");
|
|
95751
|
+
}
|
|
95752
|
+
function installCommands() {
|
|
95753
|
+
try {
|
|
95754
|
+
const src = bundledCommandsDir();
|
|
95755
|
+
if (!import_fs.default.existsSync(src)) {
|
|
95756
|
+
return { ok: false, error: `Command source not found (looked in ${src}).` };
|
|
95757
|
+
}
|
|
95758
|
+
const dest = commandsTargetDir();
|
|
95759
|
+
import_fs.default.mkdirSync(dest, { recursive: true });
|
|
95760
|
+
let count = 0;
|
|
95761
|
+
for (const f of import_fs.default.readdirSync(src)) {
|
|
95762
|
+
if (!f.endsWith(".md")) continue;
|
|
95763
|
+
if (f !== "sproutkit.md" && !f.startsWith("sproutkit-")) continue;
|
|
95764
|
+
import_fs.default.copyFileSync(import_path.default.join(src, f), import_path.default.join(dest, f));
|
|
95765
|
+
count++;
|
|
95766
|
+
}
|
|
95767
|
+
return { ok: true, count, path: dest };
|
|
95768
|
+
} catch (e) {
|
|
95769
|
+
return { ok: false, error: e.message };
|
|
95770
|
+
}
|
|
95771
|
+
}
|
|
95772
|
+
function commandsInstalled() {
|
|
95773
|
+
return import_fs.default.existsSync(import_path.default.join(commandsTargetDir(), "sproutkit.md"));
|
|
95774
|
+
}
|
|
95408
95775
|
function bundledSkillDir() {
|
|
95409
95776
|
const candidates = [
|
|
95410
95777
|
import_path.default.join(__dirname, "skill"),
|
|
@@ -95514,10 +95881,16 @@ async function offerSkillInstall(force) {
|
|
|
95514
95881
|
const res = installSkill();
|
|
95515
95882
|
if (res.ok) {
|
|
95516
95883
|
say(c.green(` \u2713 Skill installed to ${res.path}`));
|
|
95517
|
-
say(c.dim(" Now run `claude` in any terminal and ask it to find or build leads."));
|
|
95518
95884
|
} else {
|
|
95519
95885
|
say(c.amber(` Couldn't install the skill: ${res.error}`));
|
|
95520
95886
|
}
|
|
95887
|
+
const cmds = installCommands();
|
|
95888
|
+
if (cmds.ok) {
|
|
95889
|
+
say(c.green(` \u2713 ${cmds.count} slash commands installed \u2014 type / in Claude Code`));
|
|
95890
|
+
say(c.dim(" /sproutkit, /sproutkit-find, /sproutkit-build, /sproutkit-handover \u2026"));
|
|
95891
|
+
} else {
|
|
95892
|
+
say(c.amber(` Couldn't install the slash commands: ${cmds.error}`));
|
|
95893
|
+
}
|
|
95521
95894
|
say();
|
|
95522
95895
|
}
|
|
95523
95896
|
async function onboardLicense(force) {
|
|
@@ -95948,12 +96321,12 @@ function makeInvokeIpc(emit) {
|
|
|
95948
96321
|
resendKey: resendKey || "",
|
|
95949
96322
|
ownerEmail: p.ownerEmail ?? "",
|
|
95950
96323
|
bookingConfig: config,
|
|
95951
|
-
reuseAdminPassword: lead.
|
|
96324
|
+
reuseAdminPassword: lead.booking_admin_password ?? null,
|
|
95952
96325
|
onProgress: (msg) => emit("bookings:progress", { leadId: p.leadId, msg })
|
|
95953
96326
|
});
|
|
95954
96327
|
db2.run(
|
|
95955
96328
|
`UPDATE leads SET booking_url = ?, owner_email = ?, booking_archetype = ?, booking_config = ?,
|
|
95956
|
-
|
|
96329
|
+
booking_admin_url = ?, booking_admin_password = ?,
|
|
95957
96330
|
status = CASE WHEN status = 'discovered' THEN 'site_built' ELSE status END
|
|
95958
96331
|
WHERE id = ?`,
|
|
95959
96332
|
[
|
|
@@ -95969,6 +96342,21 @@ function makeInvokeIpc(emit) {
|
|
|
95969
96342
|
emit("bookings:done", { leadId: p.leadId, ...result });
|
|
95970
96343
|
return result;
|
|
95971
96344
|
}
|
|
96345
|
+
if (channel === "site:handover") {
|
|
96346
|
+
const p = payload;
|
|
96347
|
+
const db2 = getDb();
|
|
96348
|
+
const lead = db2.get("SELECT * FROM leads WHERE id = ?", [p.leadId]);
|
|
96349
|
+
if (!lead) throw new Error("Lead not found");
|
|
96350
|
+
const vercelToken = getSetting("vercel_token");
|
|
96351
|
+
if (!vercelToken) throw new Error("Vercel token not set");
|
|
96352
|
+
return await handoverSite({
|
|
96353
|
+
lead,
|
|
96354
|
+
vercelToken,
|
|
96355
|
+
blobToken: getSetting("blob_token"),
|
|
96356
|
+
domain: p.domain ?? null,
|
|
96357
|
+
onProgress: (msg) => emit("sites:progress", { leadId: p.leadId, msg })
|
|
96358
|
+
});
|
|
96359
|
+
}
|
|
95972
96360
|
throw new Error(`channel not supported in CLI: ${channel}`);
|
|
95973
96361
|
};
|
|
95974
96362
|
}
|
|
@@ -96076,6 +96464,24 @@ async function cmdList(argv) {
|
|
|
96076
96464
|
emitJson(rows);
|
|
96077
96465
|
return;
|
|
96078
96466
|
}
|
|
96467
|
+
if (flags.compact) {
|
|
96468
|
+
for (const r of rows) {
|
|
96469
|
+
process.stdout.write(
|
|
96470
|
+
JSON.stringify({
|
|
96471
|
+
id: r.id,
|
|
96472
|
+
name: r.name,
|
|
96473
|
+
city: r.city,
|
|
96474
|
+
type: r.type,
|
|
96475
|
+
status: r.status,
|
|
96476
|
+
phone: r.phone || void 0,
|
|
96477
|
+
site_url: r.site_url || void 0,
|
|
96478
|
+
booking_url: r.booking_url || void 0,
|
|
96479
|
+
lead_kind: r.lead_kind || void 0
|
|
96480
|
+
}) + "\n"
|
|
96481
|
+
);
|
|
96482
|
+
}
|
|
96483
|
+
return;
|
|
96484
|
+
}
|
|
96079
96485
|
if (rows.length === 0) {
|
|
96080
96486
|
say(c.dim(' No leads yet. Try: sproutkit find "barbers in <area>"'));
|
|
96081
96487
|
return;
|
|
@@ -96307,6 +96713,89 @@ async function cmdPipeline(argv) {
|
|
|
96307
96713
|
}
|
|
96308
96714
|
say();
|
|
96309
96715
|
}
|
|
96716
|
+
async function cmdHandover(argv) {
|
|
96717
|
+
const { flags, rest } = parseArgs(argv);
|
|
96718
|
+
const json = !!flags.json;
|
|
96719
|
+
const id = Number(flags.id ?? rest[0]);
|
|
96720
|
+
if (!id || Number.isNaN(id)) {
|
|
96721
|
+
const err = "Usage: sproutkit handover <leadId> [--domain thesalon.co.uk]";
|
|
96722
|
+
json ? emitJson({ ok: false, error: err }) : say(c.red(" " + err));
|
|
96723
|
+
process.exitCode = 1;
|
|
96724
|
+
return;
|
|
96725
|
+
}
|
|
96726
|
+
const domain = flags.domain ? String(flags.domain) : void 0;
|
|
96727
|
+
if (!flags.yes && !flags.y) {
|
|
96728
|
+
const lead = await runTool("get_lead", { id }, makeCtx({ json: true }));
|
|
96729
|
+
const label = lead?.name ? `${lead.name} (#${id})` : `lead #${id}`;
|
|
96730
|
+
const msg = `This removes the DEMO banner from ${label} and marks them won \u2014 only do it once the client has paid. Re-run with --yes to confirm: sproutkit handover ${id}${domain ? ` --domain ${domain}` : ""} --yes`;
|
|
96731
|
+
json ? emitJson({ ok: false, error: msg, needsConfirmation: true }) : say(c.amber(" " + msg));
|
|
96732
|
+
process.exitCode = 1;
|
|
96733
|
+
return;
|
|
96734
|
+
}
|
|
96735
|
+
const status = json ? void 0 : new Status();
|
|
96736
|
+
if (status) status.start(`Handing over lead #${id}`);
|
|
96737
|
+
try {
|
|
96738
|
+
const r = await runTool("hand_over_site", { id, domain }, makeCtx({ json, status }));
|
|
96739
|
+
if (r && r.ok === false) {
|
|
96740
|
+
if (status) status.done(false, r.error ?? "Handover failed");
|
|
96741
|
+
if (json) emitJson(r);
|
|
96742
|
+
process.exitCode = 1;
|
|
96743
|
+
return;
|
|
96744
|
+
}
|
|
96745
|
+
if (status) status.done(true, r.bannerRemoved ? "Demo banner removed" : "Handed over");
|
|
96746
|
+
autoSync();
|
|
96747
|
+
if (json) {
|
|
96748
|
+
emitJson(r);
|
|
96749
|
+
return;
|
|
96750
|
+
}
|
|
96751
|
+
say();
|
|
96752
|
+
say(c.bold(` Handover \u2014 ${r.business ?? `lead #${id}`}`));
|
|
96753
|
+
if (r.alreadyHandedOver) say(c.dim(" (already handed over previously \u2014 re-running is safe)"));
|
|
96754
|
+
say();
|
|
96755
|
+
if (r.url) say(` Website: ${r.url}`);
|
|
96756
|
+
if (r.admin) {
|
|
96757
|
+
say(` Admin panel: ${r.admin.url}`);
|
|
96758
|
+
say(` Password: ${c.bold(r.admin.password)}`);
|
|
96759
|
+
}
|
|
96760
|
+
if (r.bookingUrl) say(` Booking page: ${r.bookingUrl}`);
|
|
96761
|
+
if (r.bookingAdmin) {
|
|
96762
|
+
say(` Booking admin: ${r.bookingAdmin.url}`);
|
|
96763
|
+
say(` Password: ${c.bold(r.bookingAdmin.password)}`);
|
|
96764
|
+
}
|
|
96765
|
+
if (r.domain) {
|
|
96766
|
+
const d = r.domain;
|
|
96767
|
+
say();
|
|
96768
|
+
say(c.bold(` Domain \u2014 ${d.domain}`));
|
|
96769
|
+
if (d.dnsConfigured) {
|
|
96770
|
+
say(c.green(" DNS is already pointing at us. Nothing to do."));
|
|
96771
|
+
} else {
|
|
96772
|
+
say(c.dim(" Ask the client to add these records at their domain registrar:"));
|
|
96773
|
+
if (d.records.length) {
|
|
96774
|
+
for (const rec of d.records) {
|
|
96775
|
+
say(` ${rec.type.padEnd(6)} ${rec.name.padEnd(6)} ${rec.value}${rec.note ? c.dim(` (${rec.note})`) : ""}`);
|
|
96776
|
+
}
|
|
96777
|
+
}
|
|
96778
|
+
if (d.ownershipChallenges.length) {
|
|
96779
|
+
say(c.dim(" Plus this record to prove ownership:"));
|
|
96780
|
+
for (const rec of d.ownershipChallenges) {
|
|
96781
|
+
say(` ${rec.type.padEnd(6)} ${rec.name} ${rec.value}`);
|
|
96782
|
+
}
|
|
96783
|
+
}
|
|
96784
|
+
if (d.warning) say(c.amber(" " + d.warning));
|
|
96785
|
+
say(c.dim(" DNS can take minutes to 48h. Re-check with: sproutkit handover " + id + " --domain " + d.domain));
|
|
96786
|
+
}
|
|
96787
|
+
}
|
|
96788
|
+
if (!r.bannerRemoved && r.bookingUrl && !r.url) {
|
|
96789
|
+
say();
|
|
96790
|
+
say(c.dim(" Booking-only client: the demo banner comes off next time the booking site deploys."));
|
|
96791
|
+
}
|
|
96792
|
+
say();
|
|
96793
|
+
} catch (e) {
|
|
96794
|
+
if (status) status.done(false, "Handover failed");
|
|
96795
|
+
json ? emitJson({ ok: false, error: e.message }) : say(c.red(" " + e.message));
|
|
96796
|
+
process.exitCode = 1;
|
|
96797
|
+
}
|
|
96798
|
+
}
|
|
96310
96799
|
async function cmdRegenerate(argv) {
|
|
96311
96800
|
const { flags, rest } = parseArgs(argv);
|
|
96312
96801
|
const json = !!flags.json;
|
|
@@ -96364,6 +96853,7 @@ async function cmdCms(argv) {
|
|
|
96364
96853
|
if (status) status.done(true, result.publicUrl ? `CMS live at ${result.publicUrl}` : `CMS set up for #${id}`);
|
|
96365
96854
|
autoSync();
|
|
96366
96855
|
if (json) emitJson(result);
|
|
96856
|
+
else sayAdminHandover(result.adminUrl, result.adminPassword);
|
|
96367
96857
|
} catch (e) {
|
|
96368
96858
|
if (status) status.done(false, "CMS setup failed");
|
|
96369
96859
|
json ? emitJson({ ok: false, error: e.message }) : say(c.red(" " + e.message));
|
|
@@ -96676,6 +97166,14 @@ async function runDoctor() {
|
|
|
96676
97166
|
fix: "Run 'sproutkit setup' and say yes to installing the skill, so `claude` can drive Sproutkit."
|
|
96677
97167
|
}
|
|
96678
97168
|
);
|
|
97169
|
+
rows.push(
|
|
97170
|
+
commandsInstalled() ? { level: "ok", label: "Slash commands", detail: commandsTargetDir() } : {
|
|
97171
|
+
level: "warn",
|
|
97172
|
+
label: "Slash commands",
|
|
97173
|
+
detail: "not installed",
|
|
97174
|
+
fix: "Run 'sproutkit setup' so /sproutkit-find, /sproutkit-build etc. appear when you type / in Claude Code."
|
|
97175
|
+
}
|
|
97176
|
+
);
|
|
96679
97177
|
say();
|
|
96680
97178
|
say(c.greenBold(" Sproutkit doctor"));
|
|
96681
97179
|
say(c.dim(` Backend: ${backend === "claude_code" ? "Claude subscription" : "Anthropic API key"}`));
|
|
@@ -96741,7 +97239,7 @@ async function main() {
|
|
|
96741
97239
|
closeTty();
|
|
96742
97240
|
return;
|
|
96743
97241
|
}
|
|
96744
|
-
if (arg === "find" || arg === "list" || arg === "build" || arg === "build-all" || arg === "regenerate" || arg === "cms" || arg === "book" || arg === "outreach" || arg === "pipeline") {
|
|
97242
|
+
if (arg === "find" || arg === "list" || arg === "build" || arg === "build-all" || arg === "regenerate" || arg === "cms" || arg === "book" || arg === "handover" || arg === "outreach" || arg === "pipeline") {
|
|
96745
97243
|
const subArgs = process.argv.slice(3);
|
|
96746
97244
|
if (subArgs.includes("--help") || subArgs.includes("-h")) {
|
|
96747
97245
|
const USAGE = {
|
|
@@ -96753,7 +97251,7 @@ async function main() {
|
|
|
96753
97251
|
" --json machine-readable output"
|
|
96754
97252
|
],
|
|
96755
97253
|
list: [
|
|
96756
|
-
"sproutkit list [--status <s>] [--city <c>] [--limit <n>] [--json]",
|
|
97254
|
+
"sproutkit list [--status <s>] [--city <c>] [--limit <n>] [--json|--compact]",
|
|
96757
97255
|
" statuses: discovered, site_built, contacted, replied, won, dead"
|
|
96758
97256
|
],
|
|
96759
97257
|
build: [
|
|
@@ -96781,6 +97279,13 @@ async function main() {
|
|
|
96781
97279
|
' --instructions "\u2026" e.g. "45-min slots, add beard trim \xA312"',
|
|
96782
97280
|
" --email <addr> owner email for booking notifications"
|
|
96783
97281
|
],
|
|
97282
|
+
handover: [
|
|
97283
|
+
"sproutkit handover <leadId> [--domain thesalon.co.uk]",
|
|
97284
|
+
" the paid-deliverable step: removes the DEMO banner, attaches the",
|
|
97285
|
+
" client's own domain, and prints every credential in one place",
|
|
97286
|
+
" --domain <domain> client's domain; prints the DNS records to set",
|
|
97287
|
+
" --yes required \u2014 confirms the client has paid"
|
|
97288
|
+
],
|
|
96784
97289
|
outreach: [
|
|
96785
97290
|
"sproutkit outreach [--ids 3,5] [--channel sms|whatsapp] [--json]",
|
|
96786
97291
|
" Per lead: phone -> a messaging link you open; no phone -> emailed (needs SMTP).",
|
|
@@ -96815,6 +97320,7 @@ async function main() {
|
|
|
96815
97320
|
else if (arg === "regenerate") await cmdRegenerate(subArgs);
|
|
96816
97321
|
else if (arg === "cms") await cmdCms(subArgs);
|
|
96817
97322
|
else if (arg === "book") await cmdBook(subArgs);
|
|
97323
|
+
else if (arg === "handover") await cmdHandover(subArgs);
|
|
96818
97324
|
else if (arg === "pipeline") await cmdPipeline(subArgs);
|
|
96819
97325
|
else await cmdOutreach(subArgs);
|
|
96820
97326
|
closeTty();
|
|
@@ -96880,6 +97386,10 @@ async function main() {
|
|
|
96880
97386
|
if (res.ok) say(c.dim(" Installed the Sproutkit skill for Claude Code."));
|
|
96881
97387
|
else say(c.amber(` Couldn't install the Sproutkit skill: ${res.error}`));
|
|
96882
97388
|
}
|
|
97389
|
+
if (!commandsInstalled()) {
|
|
97390
|
+
const res = installCommands();
|
|
97391
|
+
if (res.ok) say(c.dim(` Installed ${res.count} /sproutkit slash commands.`));
|
|
97392
|
+
}
|
|
96883
97393
|
await notifyIfOutdated();
|
|
96884
97394
|
say();
|
|
96885
97395
|
say(c.greenBold(" Sproutkit") + c.dim(" \u2014 starting Claude Code"));
|
package/dist/skill/SKILL.md
CHANGED
|
@@ -74,6 +74,20 @@ you can parse results reliably, then summarise them for the user in prose.
|
|
|
74
74
|
`sproutkit book "Kings Barbers, Leeds" --json` looks them up, reads their
|
|
75
75
|
site, and builds a booking page in one shot.
|
|
76
76
|
|
|
77
|
+
- **Hand a finished site to a paying client** — `sproutkit handover <leadId> --yes --json`
|
|
78
|
+
- THE PAID-DELIVERABLE STEP. Removes the DEMO banner, optionally attaches the
|
|
79
|
+
client's own domain, marks the lead won, and prints one sheet with the site
|
|
80
|
+
URL, /admin panel + password, booking page + password, and the DNS records
|
|
81
|
+
the client must set at their registrar.
|
|
82
|
+
- `--domain thesalon.co.uk` attaches their domain and returns the exact A or
|
|
83
|
+
CNAME record. DNS is out of our control, so re-run the same command later to
|
|
84
|
+
re-check whether it has propagated.
|
|
85
|
+
- `--yes` is REQUIRED. Without it the command refuses and explains why. Only
|
|
86
|
+
pass it when the user has clearly said the client has paid / signed off /
|
|
87
|
+
is going live — never infer it.
|
|
88
|
+
- The banner exists to stop clients using a free demo as their real site, so
|
|
89
|
+
do NOT run this just because a site looks finished.
|
|
90
|
+
|
|
77
91
|
- **Run the full pipeline for many clients** — `sproutkit pipeline x3 --json` (optional `--book`, `--limit 10`, `--ids 3,5`)
|
|
78
92
|
- Builds → (optionally adds booking) → prepares outreach for several clients
|
|
79
93
|
CONCURRENTLY (capped at 4), refilling as each finishes. Use for "run the
|