sproutkit-code 1.2.2 → 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 +620 -25
- package/dist/skill/SKILL.md +22 -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) {
|
|
@@ -33287,13 +33413,23 @@ export default async function handler(req, res) {
|
|
|
33287
33413
|
var s = useState(null);
|
|
33288
33414
|
var v = s[0], set = s[1];
|
|
33289
33415
|
var loading = useState(true);
|
|
33416
|
+
// Standalone booking sites ship /api/bookings but NO /api/content. Treat a
|
|
33417
|
+
// missing content API as "this is a bookings-only site" instead of hanging
|
|
33418
|
+
// the whole panel on a permanent Loading\u2026 state.
|
|
33419
|
+
var available = useState(true);
|
|
33290
33420
|
useEffect(function () {
|
|
33291
|
-
fetch("/api/content").then(function (r) {
|
|
33421
|
+
fetch("/api/content").then(function (r) {
|
|
33422
|
+
if (!r.ok) throw new Error("no content api");
|
|
33423
|
+
return r.json();
|
|
33424
|
+
}).then(function (d) {
|
|
33292
33425
|
set(d);
|
|
33293
33426
|
loading[1](false);
|
|
33427
|
+
}).catch(function () {
|
|
33428
|
+
available[1](false);
|
|
33429
|
+
loading[1](false);
|
|
33294
33430
|
});
|
|
33295
33431
|
}, []);
|
|
33296
|
-
return { content: v, setContent: set, loading: loading[0] };
|
|
33432
|
+
return { content: v, setContent: set, loading: loading[0], available: available[0] };
|
|
33297
33433
|
}
|
|
33298
33434
|
|
|
33299
33435
|
function Login(props) {
|
|
@@ -33823,7 +33959,7 @@ export default async function handler(req, res) {
|
|
|
33823
33959
|
);
|
|
33824
33960
|
}
|
|
33825
33961
|
|
|
33826
|
-
function BookingsPanel() {
|
|
33962
|
+
function BookingsPanel(props) {
|
|
33827
33963
|
var sub = useState("incoming");
|
|
33828
33964
|
var b = useBookings();
|
|
33829
33965
|
var c = useBookingConfig();
|
|
@@ -33832,7 +33968,7 @@ export default async function handler(req, res) {
|
|
|
33832
33968
|
h("div", null,
|
|
33833
33969
|
h("div", { style: { fontSize: 15, fontWeight: 600 } }, "Bookings"),
|
|
33834
33970
|
h("div", { style: { fontSize: 12, color: "var(--muted)" } },
|
|
33835
|
-
h("a", { href: "/book", target: "_blank" }, "Public booking page \u2192")
|
|
33971
|
+
h("a", { href: props && props.bookingPath ? props.bookingPath : "/book", target: "_blank" }, "Public booking page \u2192")
|
|
33836
33972
|
)
|
|
33837
33973
|
),
|
|
33838
33974
|
h("div", { style: { display: "flex", gap: 4 } },
|
|
@@ -33861,9 +33997,14 @@ export default async function handler(req, res) {
|
|
|
33861
33997
|
if (!authed[0]) {
|
|
33862
33998
|
return h(Login, { onLogin: function () { authed[1](true); } });
|
|
33863
33999
|
}
|
|
33864
|
-
if (ctl.loading
|
|
34000
|
+
if (ctl.loading) {
|
|
33865
34001
|
return h("div", { className: "app" }, "Loading\u2026");
|
|
33866
34002
|
}
|
|
34003
|
+
// Bookings-only site: no Site content tab to switch to, and the public
|
|
34004
|
+
// booking page is the site root rather than /book.
|
|
34005
|
+
if (!ctl.available || !ctl.content) {
|
|
34006
|
+
return h("div", { className: "app" }, h(BookingsPanel, { bookingPath: "/" }));
|
|
34007
|
+
}
|
|
33867
34008
|
|
|
33868
34009
|
// Top nav lets the owner flip between Site content and Bookings.
|
|
33869
34010
|
return h("div", { className: "app" },
|
|
@@ -34339,6 +34480,7 @@ export default async function handler(req, res) {
|
|
|
34339
34480
|
</style>
|
|
34340
34481
|
</head>
|
|
34341
34482
|
<body>
|
|
34483
|
+
<!--SPROUTKIT_BANNER-->
|
|
34342
34484
|
<div id="root"></div>
|
|
34343
34485
|
<script src="https://unpkg.com/react@18/umd/react.production.min.js" crossorigin></script>
|
|
34344
34486
|
<script src="https://unpkg.com/react-dom@18/umd/react-dom.production.min.js" crossorigin></script>
|
|
@@ -79659,6 +79801,7 @@ var require_builder = __commonJS({
|
|
|
79659
79801
|
exports2.redeployCmsOnly = redeployCmsOnly;
|
|
79660
79802
|
exports2.deployBookingOnly = deployBookingOnly2;
|
|
79661
79803
|
exports2.deployStandaloneBooking = deployStandaloneBooking2;
|
|
79804
|
+
exports2.handoverSite = handoverSite2;
|
|
79662
79805
|
var sdk_1 = __importDefault(require_sdk());
|
|
79663
79806
|
var axios_1 = __importDefault(require_axios());
|
|
79664
79807
|
var crypto_1 = require("crypto");
|
|
@@ -79670,6 +79813,7 @@ var require_builder = __commonJS({
|
|
|
79670
79813
|
var path3 = __importStar(require("path"));
|
|
79671
79814
|
var models_1 = require_models();
|
|
79672
79815
|
var cms_template_1 = require_cms_template();
|
|
79816
|
+
var vercel_domains_1 = require_vercel_domains();
|
|
79673
79817
|
var cms_runtime_1 = require_cms_runtime();
|
|
79674
79818
|
var booking_runtime_1 = require_booking_runtime();
|
|
79675
79819
|
function siteUidFor(leadId) {
|
|
@@ -79744,7 +79888,18 @@ var require_builder = __commonJS({
|
|
|
79744
79888
|
const pattern = new RegExp(`https?:\\/\\/[^\\s"'\`)]*(${STOCK_HOSTS.map((h) => h.replace(/\./g, "\\.")).join("|")})[^\\s"'\`)]*`, "gi");
|
|
79745
79889
|
return code.replace(pattern, "");
|
|
79746
79890
|
}
|
|
79747
|
-
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 "";
|
|
79748
79903
|
const esc = (v) => v.replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">").replace(/"/g, """);
|
|
79749
79904
|
const label = `Preview site for ${esc(businessName)}`;
|
|
79750
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">
|
|
@@ -80324,7 +80479,7 @@ ${prompt}` : prompt;
|
|
|
80324
80479
|
onProgress("Deploying to Vercel...");
|
|
80325
80480
|
const indexHtml = `<!DOCTYPE html>
|
|
80326
80481
|
<html lang="en"><head><meta charset="UTF-8"><meta name="viewport" content="width=device-width,initial-scale=1"><title>${lead.name}</title></head>
|
|
80327
|
-
<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>`;
|
|
80328
80483
|
const mainJsx = `import { StrictMode } from 'react';
|
|
80329
80484
|
import { createRoot } from 'react-dom/client';
|
|
80330
80485
|
import App from './App.jsx';
|
|
@@ -80519,7 +80674,7 @@ export default defineConfig({ plugins: [react()] });`;
|
|
|
80519
80674
|
{ source: "/book/", destination: "/book/index.html" }
|
|
80520
80675
|
]
|
|
80521
80676
|
});
|
|
80522
|
-
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>`;
|
|
80523
80678
|
const mainJsxCms = `import { StrictMode } from 'react';
|
|
80524
80679
|
import { createRoot } from 'react-dom/client';
|
|
80525
80680
|
import App from './App.jsx';
|
|
@@ -80596,7 +80751,7 @@ export default defineConfig({ plugins: [react()] });`;
|
|
|
80596
80751
|
snapshot[appJsxIndex] = { ...snapshot[appJsxIndex], data: modifiedApp };
|
|
80597
80752
|
saveSnapshot(lead.id, snapshot);
|
|
80598
80753
|
const files = snapshot.map((f) => ({ file: f.file, data: f.data, encoding: f.encoding }));
|
|
80599
|
-
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>`;
|
|
80600
80755
|
const mainJsx = `import { StrictMode } from 'react';
|
|
80601
80756
|
import { createRoot } from 'react-dom/client';
|
|
80602
80757
|
import App from './App.jsx';
|
|
@@ -80668,7 +80823,13 @@ export default defineConfig({ plugins: [react()] });`;
|
|
|
80668
80823
|
{ file: "vercel.json", data: vercelJson, encoding: "utf-8" },
|
|
80669
80824
|
{ file: "api/bookings.js", data: booking_runtime_1.API_BOOKINGS_JS, encoding: "utf-8" },
|
|
80670
80825
|
{ file: "api/booking-config.js", data: booking_runtime_1.API_BOOKING_CONFIG_JS, encoding: "utf-8" },
|
|
80671
|
-
{
|
|
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
|
+
},
|
|
80672
80833
|
{ file: "public/book/app.js", data: booking_runtime_1.BOOK_APP_JS, encoding: "utf-8" },
|
|
80673
80834
|
// Refresh the CMS admin app (contains the Bookings tab now)
|
|
80674
80835
|
{ file: "public/admin/index.html", data: cms_runtime_1.ADMIN_INDEX_HTML, encoding: "utf-8" },
|
|
@@ -80695,8 +80856,8 @@ export default defineConfig({ plugins: [react()] });`;
|
|
|
80695
80856
|
}
|
|
80696
80857
|
async function deployStandaloneBooking2(args) {
|
|
80697
80858
|
const { lead, vercelToken, blobToken, resendKey, ownerEmail, bookingConfig, onProgress } = args;
|
|
80698
|
-
const
|
|
80699
|
-
const projectName =
|
|
80859
|
+
const adminPassword = args.reuseAdminPassword ?? (0, cms_runtime_1.generateFriendlyPassword)();
|
|
80860
|
+
const projectName = standaloneBookingProjectName(lead);
|
|
80700
80861
|
onProgress("Seeding booking config\u2026");
|
|
80701
80862
|
const standaloneSiteUid = siteUidFor(lead.id);
|
|
80702
80863
|
try {
|
|
@@ -80712,18 +80873,39 @@ export default defineConfig({ plugins: [react()] });`;
|
|
|
80712
80873
|
throw new Error(`Failed to seed booking config: ${e.message}`);
|
|
80713
80874
|
}
|
|
80714
80875
|
onProgress("Setting booking env vars\u2026");
|
|
80876
|
+
const adminHash = await (0, cms_runtime_1.hashPassword)(adminPassword);
|
|
80715
80877
|
await setProjectEnv(projectName, vercelToken, {
|
|
80716
80878
|
RESEND_API_KEY: resendKey,
|
|
80717
80879
|
OWNER_EMAIL: ownerEmail,
|
|
80718
80880
|
BUSINESS_NAME: lead.name,
|
|
80719
80881
|
BLOB_READ_WRITE_TOKEN: blobToken,
|
|
80720
|
-
SITE_ID: standaloneSiteUid
|
|
80882
|
+
SITE_ID: standaloneSiteUid,
|
|
80883
|
+
// Admin panel auth.
|
|
80884
|
+
ADMIN_PASSWORD_HASH: adminHash,
|
|
80885
|
+
SESSION_SECRET: (0, cms_runtime_1.generateSessionSecret)()
|
|
80721
80886
|
});
|
|
80722
80887
|
const files = [
|
|
80723
80888
|
{ file: "index.html", data: booking_runtime_1.BOOK_INDEX_HTML, encoding: "utf-8" },
|
|
80724
80889
|
{ file: "app.js", data: booking_runtime_1.BOOK_APP_JS, encoding: "utf-8" },
|
|
80725
80890
|
{ file: "api/bookings.js", data: booking_runtime_1.API_BOOKINGS_JS, encoding: "utf-8" },
|
|
80726
80891
|
{ file: "api/booking-config.js", data: booking_runtime_1.API_BOOKING_CONFIG_JS, encoding: "utf-8" },
|
|
80892
|
+
// The client's own dashboard: view bookings, edit services/hours. Same
|
|
80893
|
+
// admin the CMS sites use — it already has a Bookings tab.
|
|
80894
|
+
{ file: "public/admin/index.html", data: cms_runtime_1.ADMIN_INDEX_HTML, encoding: "utf-8" },
|
|
80895
|
+
{ file: "public/admin/app.js", data: cms_runtime_1.ADMIN_APP_JS, encoding: "utf-8" },
|
|
80896
|
+
{ file: "api/admin/login.js", data: cms_runtime_1.API_ADMIN_LOGIN_JS, encoding: "utf-8" },
|
|
80897
|
+
{ file: "api/admin/whoami.js", data: cms_runtime_1.API_ADMIN_WHOAMI_JS, encoding: "utf-8" },
|
|
80898
|
+
// Serve /admin without a trailing-slash redirect dance.
|
|
80899
|
+
{
|
|
80900
|
+
file: "vercel.json",
|
|
80901
|
+
data: JSON.stringify({
|
|
80902
|
+
rewrites: [
|
|
80903
|
+
{ source: "/admin", destination: "/admin/index.html" },
|
|
80904
|
+
{ source: "/admin/", destination: "/admin/index.html" }
|
|
80905
|
+
]
|
|
80906
|
+
}),
|
|
80907
|
+
encoding: "utf-8"
|
|
80908
|
+
},
|
|
80727
80909
|
{
|
|
80728
80910
|
file: "package.json",
|
|
80729
80911
|
data: JSON.stringify({
|
|
@@ -80731,14 +80913,15 @@ export default defineConfig({ plugins: [react()] });`;
|
|
|
80731
80913
|
private: true,
|
|
80732
80914
|
version: "0.1.0",
|
|
80733
80915
|
type: "module",
|
|
80734
|
-
|
|
80916
|
+
// bcryptjs is required by api/admin/login.js.
|
|
80917
|
+
dependencies: { "@vercel/blob": "^0.23.4", bcryptjs: "^2.4.3" }
|
|
80735
80918
|
}),
|
|
80736
80919
|
encoding: "utf-8"
|
|
80737
80920
|
}
|
|
80738
80921
|
];
|
|
80739
80922
|
files[0] = {
|
|
80740
80923
|
file: "index.html",
|
|
80741
|
-
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)),
|
|
80742
80925
|
encoding: "utf-8"
|
|
80743
80926
|
};
|
|
80744
80927
|
onProgress("Deploying booking site\u2026");
|
|
@@ -80765,7 +80948,154 @@ export default defineConfig({ plugins: [react()] });`;
|
|
|
80765
80948
|
fallbackUrl: `https://${res.data.url}`
|
|
80766
80949
|
});
|
|
80767
80950
|
onProgress(`Live at ${publicUrl}`);
|
|
80768
|
-
return {
|
|
80951
|
+
return {
|
|
80952
|
+
url: publicUrl,
|
|
80953
|
+
bookingUrl: publicUrl,
|
|
80954
|
+
admin: { url: `${publicUrl}/admin`, password: adminPassword }
|
|
80955
|
+
};
|
|
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 };
|
|
80769
81099
|
}
|
|
80770
81100
|
function hasBookLink(code) {
|
|
80771
81101
|
return /(href\s*=\s*["'`{]*\s*["'`]?\/book\/?["'`}]?)|(location(?:\.href)?\s*=\s*["'`]\/book["'`])/i.test(code);
|
|
@@ -80908,6 +81238,10 @@ Return this exact shape:
|
|
|
80908
81238
|
return null;
|
|
80909
81239
|
}
|
|
80910
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
|
+
}
|
|
80911
81245
|
function projectNameFor(lead) {
|
|
80912
81246
|
const slug = lead.name.toLowerCase().replace(/[^a-z0-9]/g, "-").replace(/-+/g, "-").slice(0, 30);
|
|
80913
81247
|
return `sprout-${lead.id}-${slug}`;
|
|
@@ -93317,6 +93651,24 @@ var require_chat_tools = __commonJS({
|
|
|
93317
93651
|
},
|
|
93318
93652
|
requiresConfirmation: true
|
|
93319
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
|
+
},
|
|
93320
93672
|
{
|
|
93321
93673
|
name: "booking_site_for_business",
|
|
93322
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.",
|
|
@@ -93928,7 +94280,11 @@ Interested?`;
|
|
|
93928
94280
|
website: biz.website,
|
|
93929
94281
|
currentBooking: check2.verdict === "messaging_only" ? check2.detail : check2.verdict,
|
|
93930
94282
|
standalone: true,
|
|
93931
|
-
...result
|
|
94283
|
+
...result,
|
|
94284
|
+
// Surfaced flat as well as nested so these always reach the operator —
|
|
94285
|
+
// they are the only copy of the client's password.
|
|
94286
|
+
adminUrl: result.admin.url,
|
|
94287
|
+
adminPassword: result.admin.password
|
|
93932
94288
|
};
|
|
93933
94289
|
}
|
|
93934
94290
|
case "add_booking_site": {
|
|
@@ -93944,7 +94300,14 @@ Interested?`;
|
|
|
93944
94300
|
archetypeOverride: input.archetype,
|
|
93945
94301
|
ownerEmail: input.ownerEmail
|
|
93946
94302
|
});
|
|
93947
|
-
return {
|
|
94303
|
+
return {
|
|
94304
|
+
ok: true,
|
|
94305
|
+
id,
|
|
94306
|
+
standalone: true,
|
|
94307
|
+
...result2,
|
|
94308
|
+
adminUrl: result2.admin.url,
|
|
94309
|
+
adminPassword: result2.admin.password
|
|
94310
|
+
};
|
|
93948
94311
|
}
|
|
93949
94312
|
if (!lead.site_url) {
|
|
93950
94313
|
return {
|
|
@@ -93960,6 +94323,25 @@ Interested?`;
|
|
|
93960
94323
|
});
|
|
93961
94324
|
return { ok: true, id, ...result };
|
|
93962
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
|
+
}
|
|
93963
94345
|
case "provision_cms": {
|
|
93964
94346
|
const id = Number(input.id);
|
|
93965
94347
|
const result = await ctx.invokeIpc("cms:provision", id);
|
|
@@ -94974,7 +95356,7 @@ var require_package2 = __commonJS({
|
|
|
94974
95356
|
"package.json"(exports2, module2) {
|
|
94975
95357
|
module2.exports = {
|
|
94976
95358
|
name: "sproutkit-code",
|
|
94977
|
-
version: "1.
|
|
95359
|
+
version: "1.4.0",
|
|
94978
95360
|
description: "Sproutkit in your terminal \u2014 find leads, build sites, run outreach, all via chat.",
|
|
94979
95361
|
bin: {
|
|
94980
95362
|
sproutkit: "./dist/index.js"
|
|
@@ -95034,6 +95416,7 @@ var provisionCmsForLead = cms.provisionCmsForLead;
|
|
|
95034
95416
|
var generatePreset = bookingPreset.generatePreset;
|
|
95035
95417
|
var deployBookingOnly = builder.deployBookingOnly;
|
|
95036
95418
|
var deployStandaloneBooking = builder.deployStandaloneBooking;
|
|
95419
|
+
var handoverSite = builder.handoverSite;
|
|
95037
95420
|
var createBlobStore = vercelStore.createBlobStore;
|
|
95038
95421
|
var refreshModels = models.refreshModels;
|
|
95039
95422
|
var refreshIfStale = models.refreshIfStale;
|
|
@@ -95352,6 +95735,43 @@ var StatusBoard = class {
|
|
|
95352
95735
|
var import_fs = __toESM(require("fs"));
|
|
95353
95736
|
var import_path = __toESM(require("path"));
|
|
95354
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
|
+
}
|
|
95355
95775
|
function bundledSkillDir() {
|
|
95356
95776
|
const candidates = [
|
|
95357
95777
|
import_path.default.join(__dirname, "skill"),
|
|
@@ -95461,10 +95881,16 @@ async function offerSkillInstall(force) {
|
|
|
95461
95881
|
const res = installSkill();
|
|
95462
95882
|
if (res.ok) {
|
|
95463
95883
|
say(c.green(` \u2713 Skill installed to ${res.path}`));
|
|
95464
|
-
say(c.dim(" Now run `claude` in any terminal and ask it to find or build leads."));
|
|
95465
95884
|
} else {
|
|
95466
95885
|
say(c.amber(` Couldn't install the skill: ${res.error}`));
|
|
95467
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
|
+
}
|
|
95468
95894
|
say();
|
|
95469
95895
|
}
|
|
95470
95896
|
async function onboardLicense(force) {
|
|
@@ -95895,21 +96321,42 @@ function makeInvokeIpc(emit) {
|
|
|
95895
96321
|
resendKey: resendKey || "",
|
|
95896
96322
|
ownerEmail: p.ownerEmail ?? "",
|
|
95897
96323
|
bookingConfig: config,
|
|
96324
|
+
reuseAdminPassword: lead.booking_admin_password ?? null,
|
|
95898
96325
|
onProgress: (msg) => emit("bookings:progress", { leadId: p.leadId, msg })
|
|
95899
96326
|
});
|
|
95900
96327
|
db2.run(
|
|
95901
|
-
|
|
96328
|
+
`UPDATE leads SET booking_url = ?, owner_email = ?, booking_archetype = ?, booking_config = ?,
|
|
96329
|
+
booking_admin_url = ?, booking_admin_password = ?,
|
|
96330
|
+
status = CASE WHEN status = 'discovered' THEN 'site_built' ELSE status END
|
|
96331
|
+
WHERE id = ?`,
|
|
95902
96332
|
[
|
|
95903
96333
|
result.bookingUrl,
|
|
95904
96334
|
p.ownerEmail ?? "",
|
|
95905
96335
|
config.archetype,
|
|
95906
96336
|
JSON.stringify(config),
|
|
96337
|
+
result.admin.url,
|
|
96338
|
+
result.admin.password,
|
|
95907
96339
|
p.leadId
|
|
95908
96340
|
]
|
|
95909
96341
|
);
|
|
95910
96342
|
emit("bookings:done", { leadId: p.leadId, ...result });
|
|
95911
96343
|
return result;
|
|
95912
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
|
+
}
|
|
95913
96360
|
throw new Error(`channel not supported in CLI: ${channel}`);
|
|
95914
96361
|
};
|
|
95915
96362
|
}
|
|
@@ -96017,6 +96464,24 @@ async function cmdList(argv) {
|
|
|
96017
96464
|
emitJson(rows);
|
|
96018
96465
|
return;
|
|
96019
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
|
+
}
|
|
96020
96485
|
if (rows.length === 0) {
|
|
96021
96486
|
say(c.dim(' No leads yet. Try: sproutkit find "barbers in <area>"'));
|
|
96022
96487
|
return;
|
|
@@ -96165,7 +96630,14 @@ async function cmdPipeline(argv) {
|
|
|
96165
96630
|
setRow("building", "preparing outreach\u2026");
|
|
96166
96631
|
await runTool("prepare_outreach", { ids: [t.id] }, makeCtx({ json: true }));
|
|
96167
96632
|
setRow("built", booked.bookingUrl ?? "booking site live");
|
|
96168
|
-
results.push({
|
|
96633
|
+
results.push({
|
|
96634
|
+
id: t.id,
|
|
96635
|
+
name: t.name,
|
|
96636
|
+
ok: true,
|
|
96637
|
+
url: booked.bookingUrl,
|
|
96638
|
+
adminUrl: booked.adminUrl,
|
|
96639
|
+
adminPassword: booked.adminPassword
|
|
96640
|
+
});
|
|
96169
96641
|
} catch (e) {
|
|
96170
96642
|
setRow("failed", e.message.slice(0, 60));
|
|
96171
96643
|
results.push({ id: t.id, name: t.name, ok: false, error: e.message });
|
|
@@ -96232,8 +96704,98 @@ async function cmdPipeline(argv) {
|
|
|
96232
96704
|
c.green(` Pipeline ${cancelled ? "stopped" : "done"} \u2014 ${ok} client${ok === 1 ? "" : "s"} built + outreach prepared`) + (failed ? c.red(`, ${failed} failed`) : "") + (cancelled ? c.amber(`, ${cancelled} not started`) : "") + c.dim(` (concurrency ${concurrency}).`)
|
|
96233
96705
|
);
|
|
96234
96706
|
say(c.dim(" Open the message links above, or check emailed leads in your Contacted column."));
|
|
96707
|
+
const withAdmin = results.filter((r) => r.ok && r.adminUrl && r.adminPassword);
|
|
96708
|
+
if (withAdmin.length) {
|
|
96709
|
+
say();
|
|
96710
|
+
say(c.bold(" Client admin panels"));
|
|
96711
|
+
for (const r of withAdmin) say(` ${r.name} \u2014 ${r.adminUrl} ${c.bold(String(r.adminPassword))}`);
|
|
96712
|
+
say(c.dim(" Also stored per lead: sproutkit list --json"));
|
|
96713
|
+
}
|
|
96235
96714
|
say();
|
|
96236
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
|
+
}
|
|
96237
96799
|
async function cmdRegenerate(argv) {
|
|
96238
96800
|
const { flags, rest } = parseArgs(argv);
|
|
96239
96801
|
const json = !!flags.json;
|
|
@@ -96291,12 +96853,21 @@ async function cmdCms(argv) {
|
|
|
96291
96853
|
if (status) status.done(true, result.publicUrl ? `CMS live at ${result.publicUrl}` : `CMS set up for #${id}`);
|
|
96292
96854
|
autoSync();
|
|
96293
96855
|
if (json) emitJson(result);
|
|
96856
|
+
else sayAdminHandover(result.adminUrl, result.adminPassword);
|
|
96294
96857
|
} catch (e) {
|
|
96295
96858
|
if (status) status.done(false, "CMS setup failed");
|
|
96296
96859
|
json ? emitJson({ ok: false, error: e.message }) : say(c.red(" " + e.message));
|
|
96297
96860
|
process.exitCode = 1;
|
|
96298
96861
|
}
|
|
96299
96862
|
}
|
|
96863
|
+
function sayAdminHandover(adminUrl, adminPassword) {
|
|
96864
|
+
if (!adminUrl || !adminPassword) return;
|
|
96865
|
+
say("");
|
|
96866
|
+
say(c.bold(" Client handover"));
|
|
96867
|
+
say(` Admin panel: ${adminUrl}`);
|
|
96868
|
+
say(` Password: ${adminPassword}`);
|
|
96869
|
+
say(c.dim(" Send these to the client \u2014 they manage bookings and content there."));
|
|
96870
|
+
}
|
|
96300
96871
|
async function cmdBook(argv) {
|
|
96301
96872
|
const { flags, rest } = parseArgs(argv);
|
|
96302
96873
|
const json = !!flags.json;
|
|
@@ -96326,7 +96897,10 @@ async function cmdBook(argv) {
|
|
|
96326
96897
|
if (status2) status2.done(true, `${r.business} \u2014 ${r.bookingUrl}`);
|
|
96327
96898
|
autoSync();
|
|
96328
96899
|
if (json) emitJson(r);
|
|
96329
|
-
else
|
|
96900
|
+
else {
|
|
96901
|
+
if (r.currentBooking) say(c.dim(` They currently take bookings via ${r.currentBooking}.`));
|
|
96902
|
+
sayAdminHandover(r.adminUrl, r.adminPassword);
|
|
96903
|
+
}
|
|
96330
96904
|
} catch (e) {
|
|
96331
96905
|
if (status2) status2.done(false, "Failed");
|
|
96332
96906
|
json ? emitJson({ ok: false, error: e.message }) : say(c.red(" " + e.message));
|
|
@@ -96366,6 +96940,7 @@ async function cmdBook(argv) {
|
|
|
96366
96940
|
if (status) status.done(true, result.bookingUrl ? `Booking live at ${result.bookingUrl}` : `Booking added to lead #${id}`);
|
|
96367
96941
|
autoSync();
|
|
96368
96942
|
if (json) emitJson(result);
|
|
96943
|
+
else sayAdminHandover(result.adminUrl, result.adminPassword);
|
|
96369
96944
|
} catch (e) {
|
|
96370
96945
|
if (status) status.done(false, "Booking failed");
|
|
96371
96946
|
json ? emitJson({ ok: false, error: e.message }) : say(c.red(" " + e.message));
|
|
@@ -96591,6 +97166,14 @@ async function runDoctor() {
|
|
|
96591
97166
|
fix: "Run 'sproutkit setup' and say yes to installing the skill, so `claude` can drive Sproutkit."
|
|
96592
97167
|
}
|
|
96593
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
|
+
);
|
|
96594
97177
|
say();
|
|
96595
97178
|
say(c.greenBold(" Sproutkit doctor"));
|
|
96596
97179
|
say(c.dim(` Backend: ${backend === "claude_code" ? "Claude subscription" : "Anthropic API key"}`));
|
|
@@ -96656,7 +97239,7 @@ async function main() {
|
|
|
96656
97239
|
closeTty();
|
|
96657
97240
|
return;
|
|
96658
97241
|
}
|
|
96659
|
-
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") {
|
|
96660
97243
|
const subArgs = process.argv.slice(3);
|
|
96661
97244
|
if (subArgs.includes("--help") || subArgs.includes("-h")) {
|
|
96662
97245
|
const USAGE = {
|
|
@@ -96668,7 +97251,7 @@ async function main() {
|
|
|
96668
97251
|
" --json machine-readable output"
|
|
96669
97252
|
],
|
|
96670
97253
|
list: [
|
|
96671
|
-
"sproutkit list [--status <s>] [--city <c>] [--limit <n>] [--json]",
|
|
97254
|
+
"sproutkit list [--status <s>] [--city <c>] [--limit <n>] [--json|--compact]",
|
|
96672
97255
|
" statuses: discovered, site_built, contacted, replied, won, dead"
|
|
96673
97256
|
],
|
|
96674
97257
|
build: [
|
|
@@ -96696,6 +97279,13 @@ async function main() {
|
|
|
96696
97279
|
' --instructions "\u2026" e.g. "45-min slots, add beard trim \xA312"',
|
|
96697
97280
|
" --email <addr> owner email for booking notifications"
|
|
96698
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
|
+
],
|
|
96699
97289
|
outreach: [
|
|
96700
97290
|
"sproutkit outreach [--ids 3,5] [--channel sms|whatsapp] [--json]",
|
|
96701
97291
|
" Per lead: phone -> a messaging link you open; no phone -> emailed (needs SMTP).",
|
|
@@ -96730,6 +97320,7 @@ async function main() {
|
|
|
96730
97320
|
else if (arg === "regenerate") await cmdRegenerate(subArgs);
|
|
96731
97321
|
else if (arg === "cms") await cmdCms(subArgs);
|
|
96732
97322
|
else if (arg === "book") await cmdBook(subArgs);
|
|
97323
|
+
else if (arg === "handover") await cmdHandover(subArgs);
|
|
96733
97324
|
else if (arg === "pipeline") await cmdPipeline(subArgs);
|
|
96734
97325
|
else await cmdOutreach(subArgs);
|
|
96735
97326
|
closeTty();
|
|
@@ -96795,6 +97386,10 @@ async function main() {
|
|
|
96795
97386
|
if (res.ok) say(c.dim(" Installed the Sproutkit skill for Claude Code."));
|
|
96796
97387
|
else say(c.amber(` Couldn't install the Sproutkit skill: ${res.error}`));
|
|
96797
97388
|
}
|
|
97389
|
+
if (!commandsInstalled()) {
|
|
97390
|
+
const res = installCommands();
|
|
97391
|
+
if (res.ok) say(c.dim(` Installed ${res.count} /sproutkit slash commands.`));
|
|
97392
|
+
}
|
|
96798
97393
|
await notifyIfOutdated();
|
|
96799
97394
|
say();
|
|
96800
97395
|
say(c.greenBold(" Sproutkit") + c.dim(" \u2014 starting Claude Code"));
|
package/dist/skill/SKILL.md
CHANGED
|
@@ -65,6 +65,28 @@ you can parse results reliably, then summarise them for the user in prose.
|
|
|
65
65
|
`--archetype slots|reservation|enquiry`, `--email owner@x.com`.
|
|
66
66
|
- Use `book --standalone` when the user says the client "already has a
|
|
67
67
|
website" or "just needs a booking page".
|
|
68
|
+
- Booking sites include a client `/admin` panel where the owner sees incoming
|
|
69
|
+
bookings and edits their services and hours. The JSON result carries
|
|
70
|
+
`adminUrl` and `adminPassword` — ALWAYS relay both to the user, they are the
|
|
71
|
+
client handover and the password is shown only at build time. It is stored
|
|
72
|
+
on the lead afterwards (`sproutkit list --json`) if they lose it.
|
|
73
|
+
- You can also name a business that isn't a lead yet:
|
|
74
|
+
`sproutkit book "Kings Barbers, Leeds" --json` looks them up, reads their
|
|
75
|
+
site, and builds a booking page in one shot.
|
|
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.
|
|
68
90
|
|
|
69
91
|
- **Run the full pipeline for many clients** — `sproutkit pipeline x3 --json` (optional `--book`, `--limit 10`, `--ids 3,5`)
|
|
70
92
|
- Builds → (optionally adds booking) → prepares outreach for several clients
|