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 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);
@@ -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("SELECT id, name, phone, email, address, city, site_url, lead_kind, booking_platform, booking_url FROM leads WHERE status = 'site_built' AND site_url IS NOT NULL AND site_url != '' ORDER BY score DESC NULLS LAST, id DESC", []);
93698
- const eligible = rows.filter((r) => r.site_url);
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: "No leads with a built site to contact. Build sites first, or check the leads already moved past 'ready'."
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 = ? WHERE id = ?", [result.bookingUrl, leadId]);
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 { 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
+ };
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.2.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
- "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 = ?`,
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({ 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
+ });
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 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
+ }
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));
@@ -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.1",
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"