sproutkit-code 1.2.2 → 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 +99 -14
- 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);
|
|
@@ -93928,7 +93970,11 @@ Interested?`;
|
|
|
93928
93970
|
website: biz.website,
|
|
93929
93971
|
currentBooking: check2.verdict === "messaging_only" ? check2.detail : check2.verdict,
|
|
93930
93972
|
standalone: true,
|
|
93931
|
-
...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
|
|
93932
93978
|
};
|
|
93933
93979
|
}
|
|
93934
93980
|
case "add_booking_site": {
|
|
@@ -93944,7 +93990,14 @@ Interested?`;
|
|
|
93944
93990
|
archetypeOverride: input.archetype,
|
|
93945
93991
|
ownerEmail: input.ownerEmail
|
|
93946
93992
|
});
|
|
93947
|
-
return {
|
|
93993
|
+
return {
|
|
93994
|
+
ok: true,
|
|
93995
|
+
id,
|
|
93996
|
+
standalone: true,
|
|
93997
|
+
...result2,
|
|
93998
|
+
adminUrl: result2.admin.url,
|
|
93999
|
+
adminPassword: result2.admin.password
|
|
94000
|
+
};
|
|
93948
94001
|
}
|
|
93949
94002
|
if (!lead.site_url) {
|
|
93950
94003
|
return {
|
|
@@ -94974,7 +95027,7 @@ var require_package2 = __commonJS({
|
|
|
94974
95027
|
"package.json"(exports2, module2) {
|
|
94975
95028
|
module2.exports = {
|
|
94976
95029
|
name: "sproutkit-code",
|
|
94977
|
-
version: "1.
|
|
95030
|
+
version: "1.3.0",
|
|
94978
95031
|
description: "Sproutkit in your terminal \u2014 find leads, build sites, run outreach, all via chat.",
|
|
94979
95032
|
bin: {
|
|
94980
95033
|
sproutkit: "./dist/index.js"
|
|
@@ -95895,15 +95948,21 @@ function makeInvokeIpc(emit) {
|
|
|
95895
95948
|
resendKey: resendKey || "",
|
|
95896
95949
|
ownerEmail: p.ownerEmail ?? "",
|
|
95897
95950
|
bookingConfig: config,
|
|
95951
|
+
reuseAdminPassword: lead.admin_password ?? null,
|
|
95898
95952
|
onProgress: (msg) => emit("bookings:progress", { leadId: p.leadId, msg })
|
|
95899
95953
|
});
|
|
95900
95954
|
db2.run(
|
|
95901
|
-
|
|
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 = ?`,
|
|
95902
95959
|
[
|
|
95903
95960
|
result.bookingUrl,
|
|
95904
95961
|
p.ownerEmail ?? "",
|
|
95905
95962
|
config.archetype,
|
|
95906
95963
|
JSON.stringify(config),
|
|
95964
|
+
result.admin.url,
|
|
95965
|
+
result.admin.password,
|
|
95907
95966
|
p.leadId
|
|
95908
95967
|
]
|
|
95909
95968
|
);
|
|
@@ -96165,7 +96224,14 @@ async function cmdPipeline(argv) {
|
|
|
96165
96224
|
setRow("building", "preparing outreach\u2026");
|
|
96166
96225
|
await runTool("prepare_outreach", { ids: [t.id] }, makeCtx({ json: true }));
|
|
96167
96226
|
setRow("built", booked.bookingUrl ?? "booking site live");
|
|
96168
|
-
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
|
+
});
|
|
96169
96235
|
} catch (e) {
|
|
96170
96236
|
setRow("failed", e.message.slice(0, 60));
|
|
96171
96237
|
results.push({ id: t.id, name: t.name, ok: false, error: e.message });
|
|
@@ -96232,6 +96298,13 @@ async function cmdPipeline(argv) {
|
|
|
96232
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}).`)
|
|
96233
96299
|
);
|
|
96234
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
|
+
}
|
|
96235
96308
|
say();
|
|
96236
96309
|
}
|
|
96237
96310
|
async function cmdRegenerate(argv) {
|
|
@@ -96297,6 +96370,14 @@ async function cmdCms(argv) {
|
|
|
96297
96370
|
process.exitCode = 1;
|
|
96298
96371
|
}
|
|
96299
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
|
+
}
|
|
96300
96381
|
async function cmdBook(argv) {
|
|
96301
96382
|
const { flags, rest } = parseArgs(argv);
|
|
96302
96383
|
const json = !!flags.json;
|
|
@@ -96326,7 +96407,10 @@ async function cmdBook(argv) {
|
|
|
96326
96407
|
if (status2) status2.done(true, `${r.business} \u2014 ${r.bookingUrl}`);
|
|
96327
96408
|
autoSync();
|
|
96328
96409
|
if (json) emitJson(r);
|
|
96329
|
-
else
|
|
96410
|
+
else {
|
|
96411
|
+
if (r.currentBooking) say(c.dim(` They currently take bookings via ${r.currentBooking}.`));
|
|
96412
|
+
sayAdminHandover(r.adminUrl, r.adminPassword);
|
|
96413
|
+
}
|
|
96330
96414
|
} catch (e) {
|
|
96331
96415
|
if (status2) status2.done(false, "Failed");
|
|
96332
96416
|
json ? emitJson({ ok: false, error: e.message }) : say(c.red(" " + e.message));
|
|
@@ -96366,6 +96450,7 @@ async function cmdBook(argv) {
|
|
|
96366
96450
|
if (status) status.done(true, result.bookingUrl ? `Booking live at ${result.bookingUrl}` : `Booking added to lead #${id}`);
|
|
96367
96451
|
autoSync();
|
|
96368
96452
|
if (json) emitJson(result);
|
|
96453
|
+
else sayAdminHandover(result.adminUrl, result.adminPassword);
|
|
96369
96454
|
} catch (e) {
|
|
96370
96455
|
if (status) status.done(false, "Booking failed");
|
|
96371
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
|