sproutkit-code 1.2.1 → 1.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/index.js +113 -18
- package/dist/skill/SKILL.md +8 -0
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -33287,13 +33287,23 @@ export default async function handler(req, res) {
|
|
|
33287
33287
|
var s = useState(null);
|
|
33288
33288
|
var v = s[0], set = s[1];
|
|
33289
33289
|
var loading = useState(true);
|
|
33290
|
+
// Standalone booking sites ship /api/bookings but NO /api/content. Treat a
|
|
33291
|
+
// missing content API as "this is a bookings-only site" instead of hanging
|
|
33292
|
+
// the whole panel on a permanent Loading\u2026 state.
|
|
33293
|
+
var available = useState(true);
|
|
33290
33294
|
useEffect(function () {
|
|
33291
|
-
fetch("/api/content").then(function (r) {
|
|
33295
|
+
fetch("/api/content").then(function (r) {
|
|
33296
|
+
if (!r.ok) throw new Error("no content api");
|
|
33297
|
+
return r.json();
|
|
33298
|
+
}).then(function (d) {
|
|
33292
33299
|
set(d);
|
|
33293
33300
|
loading[1](false);
|
|
33301
|
+
}).catch(function () {
|
|
33302
|
+
available[1](false);
|
|
33303
|
+
loading[1](false);
|
|
33294
33304
|
});
|
|
33295
33305
|
}, []);
|
|
33296
|
-
return { content: v, setContent: set, loading: loading[0] };
|
|
33306
|
+
return { content: v, setContent: set, loading: loading[0], available: available[0] };
|
|
33297
33307
|
}
|
|
33298
33308
|
|
|
33299
33309
|
function Login(props) {
|
|
@@ -33823,7 +33833,7 @@ export default async function handler(req, res) {
|
|
|
33823
33833
|
);
|
|
33824
33834
|
}
|
|
33825
33835
|
|
|
33826
|
-
function BookingsPanel() {
|
|
33836
|
+
function BookingsPanel(props) {
|
|
33827
33837
|
var sub = useState("incoming");
|
|
33828
33838
|
var b = useBookings();
|
|
33829
33839
|
var c = useBookingConfig();
|
|
@@ -33832,7 +33842,7 @@ export default async function handler(req, res) {
|
|
|
33832
33842
|
h("div", null,
|
|
33833
33843
|
h("div", { style: { fontSize: 15, fontWeight: 600 } }, "Bookings"),
|
|
33834
33844
|
h("div", { style: { fontSize: 12, color: "var(--muted)" } },
|
|
33835
|
-
h("a", { href: "/book", target: "_blank" }, "Public booking page \u2192")
|
|
33845
|
+
h("a", { href: props && props.bookingPath ? props.bookingPath : "/book", target: "_blank" }, "Public booking page \u2192")
|
|
33836
33846
|
)
|
|
33837
33847
|
),
|
|
33838
33848
|
h("div", { style: { display: "flex", gap: 4 } },
|
|
@@ -33861,9 +33871,14 @@ export default async function handler(req, res) {
|
|
|
33861
33871
|
if (!authed[0]) {
|
|
33862
33872
|
return h(Login, { onLogin: function () { authed[1](true); } });
|
|
33863
33873
|
}
|
|
33864
|
-
if (ctl.loading
|
|
33874
|
+
if (ctl.loading) {
|
|
33865
33875
|
return h("div", { className: "app" }, "Loading\u2026");
|
|
33866
33876
|
}
|
|
33877
|
+
// Bookings-only site: no Site content tab to switch to, and the public
|
|
33878
|
+
// booking page is the site root rather than /book.
|
|
33879
|
+
if (!ctl.available || !ctl.content) {
|
|
33880
|
+
return h("div", { className: "app" }, h(BookingsPanel, { bookingPath: "/" }));
|
|
33881
|
+
}
|
|
33867
33882
|
|
|
33868
33883
|
// Top nav lets the owner flip between Site content and Bookings.
|
|
33869
33884
|
return h("div", { className: "app" },
|
|
@@ -80695,6 +80710,7 @@ export default defineConfig({ plugins: [react()] });`;
|
|
|
80695
80710
|
}
|
|
80696
80711
|
async function deployStandaloneBooking2(args) {
|
|
80697
80712
|
const { lead, vercelToken, blobToken, resendKey, ownerEmail, bookingConfig, onProgress } = args;
|
|
80713
|
+
const adminPassword = args.reuseAdminPassword ?? (0, cms_runtime_1.generateFriendlyPassword)();
|
|
80698
80714
|
const slug = lead.name.toLowerCase().replace(/[^a-z0-9]/g, "-").replace(/-+/g, "-").slice(0, 26);
|
|
80699
80715
|
const projectName = `sprout-book-${lead.id}-${slug}`;
|
|
80700
80716
|
onProgress("Seeding booking config\u2026");
|
|
@@ -80712,18 +80728,39 @@ export default defineConfig({ plugins: [react()] });`;
|
|
|
80712
80728
|
throw new Error(`Failed to seed booking config: ${e.message}`);
|
|
80713
80729
|
}
|
|
80714
80730
|
onProgress("Setting booking env vars\u2026");
|
|
80731
|
+
const adminHash = await (0, cms_runtime_1.hashPassword)(adminPassword);
|
|
80715
80732
|
await setProjectEnv(projectName, vercelToken, {
|
|
80716
80733
|
RESEND_API_KEY: resendKey,
|
|
80717
80734
|
OWNER_EMAIL: ownerEmail,
|
|
80718
80735
|
BUSINESS_NAME: lead.name,
|
|
80719
80736
|
BLOB_READ_WRITE_TOKEN: blobToken,
|
|
80720
|
-
SITE_ID: standaloneSiteUid
|
|
80737
|
+
SITE_ID: standaloneSiteUid,
|
|
80738
|
+
// Admin panel auth.
|
|
80739
|
+
ADMIN_PASSWORD_HASH: adminHash,
|
|
80740
|
+
SESSION_SECRET: (0, cms_runtime_1.generateSessionSecret)()
|
|
80721
80741
|
});
|
|
80722
80742
|
const files = [
|
|
80723
80743
|
{ file: "index.html", data: booking_runtime_1.BOOK_INDEX_HTML, encoding: "utf-8" },
|
|
80724
80744
|
{ file: "app.js", data: booking_runtime_1.BOOK_APP_JS, encoding: "utf-8" },
|
|
80725
80745
|
{ file: "api/bookings.js", data: booking_runtime_1.API_BOOKINGS_JS, encoding: "utf-8" },
|
|
80726
80746
|
{ file: "api/booking-config.js", data: booking_runtime_1.API_BOOKING_CONFIG_JS, encoding: "utf-8" },
|
|
80747
|
+
// The client's own dashboard: view bookings, edit services/hours. Same
|
|
80748
|
+
// admin the CMS sites use — it already has a Bookings tab.
|
|
80749
|
+
{ file: "public/admin/index.html", data: cms_runtime_1.ADMIN_INDEX_HTML, encoding: "utf-8" },
|
|
80750
|
+
{ file: "public/admin/app.js", data: cms_runtime_1.ADMIN_APP_JS, encoding: "utf-8" },
|
|
80751
|
+
{ file: "api/admin/login.js", data: cms_runtime_1.API_ADMIN_LOGIN_JS, encoding: "utf-8" },
|
|
80752
|
+
{ file: "api/admin/whoami.js", data: cms_runtime_1.API_ADMIN_WHOAMI_JS, encoding: "utf-8" },
|
|
80753
|
+
// Serve /admin without a trailing-slash redirect dance.
|
|
80754
|
+
{
|
|
80755
|
+
file: "vercel.json",
|
|
80756
|
+
data: JSON.stringify({
|
|
80757
|
+
rewrites: [
|
|
80758
|
+
{ source: "/admin", destination: "/admin/index.html" },
|
|
80759
|
+
{ source: "/admin/", destination: "/admin/index.html" }
|
|
80760
|
+
]
|
|
80761
|
+
}),
|
|
80762
|
+
encoding: "utf-8"
|
|
80763
|
+
},
|
|
80727
80764
|
{
|
|
80728
80765
|
file: "package.json",
|
|
80729
80766
|
data: JSON.stringify({
|
|
@@ -80731,7 +80768,8 @@ export default defineConfig({ plugins: [react()] });`;
|
|
|
80731
80768
|
private: true,
|
|
80732
80769
|
version: "0.1.0",
|
|
80733
80770
|
type: "module",
|
|
80734
|
-
|
|
80771
|
+
// bcryptjs is required by api/admin/login.js.
|
|
80772
|
+
dependencies: { "@vercel/blob": "^0.23.4", bcryptjs: "^2.4.3" }
|
|
80735
80773
|
}),
|
|
80736
80774
|
encoding: "utf-8"
|
|
80737
80775
|
}
|
|
@@ -80765,7 +80803,11 @@ export default defineConfig({ plugins: [react()] });`;
|
|
|
80765
80803
|
fallbackUrl: `https://${res.data.url}`
|
|
80766
80804
|
});
|
|
80767
80805
|
onProgress(`Live at ${publicUrl}`);
|
|
80768
|
-
return {
|
|
80806
|
+
return {
|
|
80807
|
+
url: publicUrl,
|
|
80808
|
+
bookingUrl: publicUrl,
|
|
80809
|
+
admin: { url: `${publicUrl}/admin`, password: adminPassword }
|
|
80810
|
+
};
|
|
80769
80811
|
}
|
|
80770
80812
|
function hasBookLink(code) {
|
|
80771
80813
|
return /(href\s*=\s*["'`{]*\s*["'`]?\/book\/?["'`}]?)|(location(?:\.href)?\s*=\s*["'`]\/book["'`])/i.test(code);
|
|
@@ -93694,12 +93736,19 @@ Interested?`;
|
|
|
93694
93736
|
}
|
|
93695
93737
|
case "prepare_outreach": {
|
|
93696
93738
|
const explicit = input.ids;
|
|
93697
|
-
const rows = explicit && explicit.length > 0 ? db2.all(`SELECT id, name, phone, email, address, city, site_url, lead_kind, booking_platform, booking_url FROM leads WHERE id IN (${explicit.map(() => "?").join(",")})`, explicit) : db2.all(
|
|
93698
|
-
|
|
93739
|
+
const rows = explicit && explicit.length > 0 ? db2.all(`SELECT id, name, phone, email, address, city, site_url, lead_kind, booking_platform, booking_url FROM leads WHERE id IN (${explicit.map(() => "?").join(",")})`, explicit) : db2.all(
|
|
93740
|
+
// Eligible = we have something to show them: either a demo site
|
|
93741
|
+
// we built (site_url) OR a booking page we built (booking_url).
|
|
93742
|
+
// Booking leads never get a site_url, so requiring it silently
|
|
93743
|
+
// excluded every one-shot booking customer.
|
|
93744
|
+
"SELECT id, name, phone, email, address, city, site_url, lead_kind, booking_platform, booking_url FROM leads WHERE status IN ('site_built','discovered') AND ((site_url IS NOT NULL AND site_url != '') OR (booking_url IS NOT NULL AND booking_url != '')) ORDER BY score DESC NULLS LAST, id DESC",
|
|
93745
|
+
[]
|
|
93746
|
+
);
|
|
93747
|
+
const eligible = rows.filter((r) => r.site_url || r.booking_url);
|
|
93699
93748
|
if (eligible.length === 0) {
|
|
93700
93749
|
return {
|
|
93701
93750
|
ok: false,
|
|
93702
|
-
error: "
|
|
93751
|
+
error: "Nothing built to contact anyone about yet. Build a demo site (sproutkit build <id>) or a booking page (sproutkit book <id>) first, or check whether these leads have already moved past 'ready'."
|
|
93703
93752
|
};
|
|
93704
93753
|
}
|
|
93705
93754
|
const channelRow = db2.get("SELECT value FROM settings WHERE key = ?", ["outreach_channel"]);
|
|
@@ -93910,7 +93959,10 @@ Interested?`;
|
|
|
93910
93959
|
archetypeOverride: input.archetype,
|
|
93911
93960
|
ownerEmail: input.ownerEmail
|
|
93912
93961
|
});
|
|
93913
|
-
db2.run("UPDATE leads SET booking_url =
|
|
93962
|
+
db2.run("UPDATE leads SET booking_url = ?, status = 'site_built' WHERE id = ?", [
|
|
93963
|
+
result.bookingUrl,
|
|
93964
|
+
leadId
|
|
93965
|
+
]);
|
|
93914
93966
|
return {
|
|
93915
93967
|
ok: true,
|
|
93916
93968
|
id: leadId,
|
|
@@ -93918,7 +93970,11 @@ Interested?`;
|
|
|
93918
93970
|
website: biz.website,
|
|
93919
93971
|
currentBooking: check2.verdict === "messaging_only" ? check2.detail : check2.verdict,
|
|
93920
93972
|
standalone: true,
|
|
93921
|
-
...result
|
|
93973
|
+
...result,
|
|
93974
|
+
// Surfaced flat as well as nested so these always reach the operator —
|
|
93975
|
+
// they are the only copy of the client's password.
|
|
93976
|
+
adminUrl: result.admin.url,
|
|
93977
|
+
adminPassword: result.admin.password
|
|
93922
93978
|
};
|
|
93923
93979
|
}
|
|
93924
93980
|
case "add_booking_site": {
|
|
@@ -93934,7 +93990,14 @@ Interested?`;
|
|
|
93934
93990
|
archetypeOverride: input.archetype,
|
|
93935
93991
|
ownerEmail: input.ownerEmail
|
|
93936
93992
|
});
|
|
93937
|
-
return {
|
|
93993
|
+
return {
|
|
93994
|
+
ok: true,
|
|
93995
|
+
id,
|
|
93996
|
+
standalone: true,
|
|
93997
|
+
...result2,
|
|
93998
|
+
adminUrl: result2.admin.url,
|
|
93999
|
+
adminPassword: result2.admin.password
|
|
94000
|
+
};
|
|
93938
94001
|
}
|
|
93939
94002
|
if (!lead.site_url) {
|
|
93940
94003
|
return {
|
|
@@ -94964,7 +95027,7 @@ var require_package2 = __commonJS({
|
|
|
94964
95027
|
"package.json"(exports2, module2) {
|
|
94965
95028
|
module2.exports = {
|
|
94966
95029
|
name: "sproutkit-code",
|
|
94967
|
-
version: "1.
|
|
95030
|
+
version: "1.3.0",
|
|
94968
95031
|
description: "Sproutkit in your terminal \u2014 find leads, build sites, run outreach, all via chat.",
|
|
94969
95032
|
bin: {
|
|
94970
95033
|
sproutkit: "./dist/index.js"
|
|
@@ -95885,15 +95948,21 @@ function makeInvokeIpc(emit) {
|
|
|
95885
95948
|
resendKey: resendKey || "",
|
|
95886
95949
|
ownerEmail: p.ownerEmail ?? "",
|
|
95887
95950
|
bookingConfig: config,
|
|
95951
|
+
reuseAdminPassword: lead.admin_password ?? null,
|
|
95888
95952
|
onProgress: (msg) => emit("bookings:progress", { leadId: p.leadId, msg })
|
|
95889
95953
|
});
|
|
95890
95954
|
db2.run(
|
|
95891
|
-
|
|
95955
|
+
`UPDATE leads SET booking_url = ?, owner_email = ?, booking_archetype = ?, booking_config = ?,
|
|
95956
|
+
admin_url = ?, admin_password = ?,
|
|
95957
|
+
status = CASE WHEN status = 'discovered' THEN 'site_built' ELSE status END
|
|
95958
|
+
WHERE id = ?`,
|
|
95892
95959
|
[
|
|
95893
95960
|
result.bookingUrl,
|
|
95894
95961
|
p.ownerEmail ?? "",
|
|
95895
95962
|
config.archetype,
|
|
95896
95963
|
JSON.stringify(config),
|
|
95964
|
+
result.admin.url,
|
|
95965
|
+
result.admin.password,
|
|
95897
95966
|
p.leadId
|
|
95898
95967
|
]
|
|
95899
95968
|
);
|
|
@@ -96155,7 +96224,14 @@ async function cmdPipeline(argv) {
|
|
|
96155
96224
|
setRow("building", "preparing outreach\u2026");
|
|
96156
96225
|
await runTool("prepare_outreach", { ids: [t.id] }, makeCtx({ json: true }));
|
|
96157
96226
|
setRow("built", booked.bookingUrl ?? "booking site live");
|
|
96158
|
-
results.push({
|
|
96227
|
+
results.push({
|
|
96228
|
+
id: t.id,
|
|
96229
|
+
name: t.name,
|
|
96230
|
+
ok: true,
|
|
96231
|
+
url: booked.bookingUrl,
|
|
96232
|
+
adminUrl: booked.adminUrl,
|
|
96233
|
+
adminPassword: booked.adminPassword
|
|
96234
|
+
});
|
|
96159
96235
|
} catch (e) {
|
|
96160
96236
|
setRow("failed", e.message.slice(0, 60));
|
|
96161
96237
|
results.push({ id: t.id, name: t.name, ok: false, error: e.message });
|
|
@@ -96222,6 +96298,13 @@ async function cmdPipeline(argv) {
|
|
|
96222
96298
|
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}).`)
|
|
96223
96299
|
);
|
|
96224
96300
|
say(c.dim(" Open the message links above, or check emailed leads in your Contacted column."));
|
|
96301
|
+
const withAdmin = results.filter((r) => r.ok && r.adminUrl && r.adminPassword);
|
|
96302
|
+
if (withAdmin.length) {
|
|
96303
|
+
say();
|
|
96304
|
+
say(c.bold(" Client admin panels"));
|
|
96305
|
+
for (const r of withAdmin) say(` ${r.name} \u2014 ${r.adminUrl} ${c.bold(String(r.adminPassword))}`);
|
|
96306
|
+
say(c.dim(" Also stored per lead: sproutkit list --json"));
|
|
96307
|
+
}
|
|
96225
96308
|
say();
|
|
96226
96309
|
}
|
|
96227
96310
|
async function cmdRegenerate(argv) {
|
|
@@ -96287,6 +96370,14 @@ async function cmdCms(argv) {
|
|
|
96287
96370
|
process.exitCode = 1;
|
|
96288
96371
|
}
|
|
96289
96372
|
}
|
|
96373
|
+
function sayAdminHandover(adminUrl, adminPassword) {
|
|
96374
|
+
if (!adminUrl || !adminPassword) return;
|
|
96375
|
+
say("");
|
|
96376
|
+
say(c.bold(" Client handover"));
|
|
96377
|
+
say(` Admin panel: ${adminUrl}`);
|
|
96378
|
+
say(` Password: ${adminPassword}`);
|
|
96379
|
+
say(c.dim(" Send these to the client \u2014 they manage bookings and content there."));
|
|
96380
|
+
}
|
|
96290
96381
|
async function cmdBook(argv) {
|
|
96291
96382
|
const { flags, rest } = parseArgs(argv);
|
|
96292
96383
|
const json = !!flags.json;
|
|
@@ -96316,7 +96407,10 @@ async function cmdBook(argv) {
|
|
|
96316
96407
|
if (status2) status2.done(true, `${r.business} \u2014 ${r.bookingUrl}`);
|
|
96317
96408
|
autoSync();
|
|
96318
96409
|
if (json) emitJson(r);
|
|
96319
|
-
else
|
|
96410
|
+
else {
|
|
96411
|
+
if (r.currentBooking) say(c.dim(` They currently take bookings via ${r.currentBooking}.`));
|
|
96412
|
+
sayAdminHandover(r.adminUrl, r.adminPassword);
|
|
96413
|
+
}
|
|
96320
96414
|
} catch (e) {
|
|
96321
96415
|
if (status2) status2.done(false, "Failed");
|
|
96322
96416
|
json ? emitJson({ ok: false, error: e.message }) : say(c.red(" " + e.message));
|
|
@@ -96356,6 +96450,7 @@ async function cmdBook(argv) {
|
|
|
96356
96450
|
if (status) status.done(true, result.bookingUrl ? `Booking live at ${result.bookingUrl}` : `Booking added to lead #${id}`);
|
|
96357
96451
|
autoSync();
|
|
96358
96452
|
if (json) emitJson(result);
|
|
96453
|
+
else sayAdminHandover(result.adminUrl, result.adminPassword);
|
|
96359
96454
|
} catch (e) {
|
|
96360
96455
|
if (status) status.done(false, "Booking failed");
|
|
96361
96456
|
json ? emitJson({ ok: false, error: e.message }) : say(c.red(" " + e.message));
|
package/dist/skill/SKILL.md
CHANGED
|
@@ -65,6 +65,14 @@ 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.
|
|
68
76
|
|
|
69
77
|
- **Run the full pipeline for many clients** — `sproutkit pipeline x3 --json` (optional `--book`, `--limit 10`, `--ids 3,5`)
|
|
70
78
|
- Builds → (optionally adds booking) → prepares outreach for several clients
|