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 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) { return r.json(); }).then(function (d) {
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 || !ctl.content) {
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
- dependencies: { "@vercel/blob": "^0.23.4" }
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 { url: publicUrl, bookingUrl: publicUrl };
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 { ok: true, id, standalone: true, ...result2 };
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.2.2",
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
- "UPDATE leads SET booking_url = ?, owner_email = ?, booking_archetype = ?, booking_config = ? WHERE id = ?",
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({ id: t.id, name: t.name, ok: true, url: booked.bookingUrl });
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 if (r.currentBooking) say(c.dim(` They currently take bookings via ${r.currentBooking}.`));
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));
@@ -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
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "sproutkit-code",
3
- "version": "1.2.2",
3
+ "version": "1.3.0",
4
4
  "description": "Sproutkit in your terminal — find leads, build sites, run outreach, all via chat.",
5
5
  "bin": {
6
6
  "sproutkit": "./dist/index.js"