@helloleo/plugins 0.1.10 → 0.1.12

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
@@ -12332,10 +12332,10 @@ function tool(input) {
12332
12332
  tool.schema = exports_external;
12333
12333
  // src/tools/inspect-http.ts
12334
12334
  var z = tool.schema;
12335
- var ALLOWED_EXACT_HOSTS = new Set(["api.app.helloleo.dev"]);
12336
- var ALLOWED_HOST_SUFFIXES = [
12337
- ".augustin-d60.workers.dev",
12338
- ".live.helloleo.dev"
12335
+ var ALLOWED_BASE_HOSTS = [
12336
+ "helloleo.dev",
12337
+ "helloleo.app",
12338
+ "augustin-d60.workers.dev"
12339
12339
  ];
12340
12340
  var DEFAULT_TIMEOUT_MS = 15000;
12341
12341
  var MAX_TIMEOUT_MS = 30000;
@@ -12343,7 +12343,7 @@ var MAX_RESPONSE_BYTES = 1e6;
12343
12343
  var METHODS = ["GET", "POST", "PUT", "PATCH", "DELETE"];
12344
12344
  var METHODS_AUTO_JSON = new Set(["POST", "PUT", "PATCH"]);
12345
12345
  var argsShape = {
12346
- url: z.string().describe("Fully-qualified URL to request. Must be on the HelloLeo whitelist " + "(api.app.helloleo.dev, *.augustin-d60.workers.dev, *.live.helloleo.dev)."),
12346
+ url: z.string().describe("Fully-qualified URL to request. Must be on the HelloLeo whitelist " + "(**.helloleo.dev, **.helloleo.app, **.augustin-d60.workers.dev)."),
12347
12347
  method: z.enum(METHODS).default("GET").describe(`HTTP method (${METHODS.join(", ")}). Defaults to GET.`),
12348
12348
  body: z.string().optional().describe("Request body as a string. Not valid for GET. Sent with " + "Content-Type: application/json on POST/PUT/PATCH unless a " + "different Content-Type is supplied via headers."),
12349
12349
  headers: z.record(z.string(), z.string()).default({}).describe("Optional additional request headers as key/value pairs (e.g. Authorization)."),
@@ -12351,7 +12351,7 @@ var argsShape = {
12351
12351
  };
12352
12352
  var argsSchema = z.object(argsShape);
12353
12353
  var inspectHttp = tool({
12354
- description: "Send an HTTP request to a HelloLeo endpoint and return the status code, " + "response headers, and response body. Use this to inspect the live behaviour " + "of HelloLeo workers, the production API, or preview deployments so you can " + "self-debug instead of operating blind. " + "Only URLs on the HelloLeo whitelist are allowed: " + "api.app.helloleo.dev, *.augustin-d60.workers.dev, *.live.helloleo.dev. " + "Supports GET, POST, PUT, PATCH and DELETE (with an optional JSON body).",
12354
+ description: "Send an HTTP request to a HelloLeo endpoint and return the status code, " + "response headers, and response body. Use this to inspect the live behaviour " + "of HelloLeo workers, the production API, or preview deployments so you can " + "self-debug instead of operating blind. ",
12355
12355
  args: argsShape,
12356
12356
  async execute(rawArgs, context) {
12357
12357
  const args = argsSchema.parse(rawArgs);
@@ -12437,17 +12437,12 @@ function parseTargetUrl(raw) {
12437
12437
  return url2;
12438
12438
  }
12439
12439
  function allowedHostsHuman() {
12440
- return [
12441
- ...ALLOWED_EXACT_HOSTS,
12442
- ...ALLOWED_HOST_SUFFIXES.map((s) => `*${s}`)
12443
- ].join(", ");
12440
+ return ALLOWED_BASE_HOSTS.map((base) => `**.${base}`).join(", ");
12444
12441
  }
12445
12442
  function isAllowedHost(hostname3) {
12446
12443
  const host = hostname3.toLowerCase().replace(/\.$/, "");
12447
- if (ALLOWED_EXACT_HOSTS.has(host))
12448
- return true;
12449
- for (const suffix of ALLOWED_HOST_SUFFIXES) {
12450
- if (host.endsWith(suffix) && host.length > suffix.length)
12444
+ for (const base of ALLOWED_BASE_HOSTS) {
12445
+ if (host === base || host.endsWith(`.${base}`))
12451
12446
  return true;
12452
12447
  }
12453
12448
  return false;
@@ -12544,74 +12539,493 @@ var RemovePlugin = async () => {
12544
12539
  // src/tools/scaffold.ts
12545
12540
  import { mkdir } from "fs/promises";
12546
12541
  import path2 from "path";
12542
+ function packageJson(name) {
12543
+ return JSON.stringify({
12544
+ name,
12545
+ private: true,
12546
+ version: "0.0.0",
12547
+ type: "module",
12548
+ scripts: {
12549
+ dev: "vite",
12550
+ start: "vite",
12551
+ build: "vite build",
12552
+ preview: "vite preview"
12553
+ },
12554
+ dependencies: {
12555
+ react: "^18.3.1",
12556
+ "react-dom": "^18.3.1",
12557
+ clsx: "^2.1.1",
12558
+ "tailwind-merge": "^2.6.0",
12559
+ "lucide-react": "^1.14.0"
12560
+ },
12561
+ devDependencies: {
12562
+ "@types/react": "^18.3.12",
12563
+ "@types/react-dom": "^18.3.1",
12564
+ "@vitejs/plugin-react": "^4.3.4",
12565
+ autoprefixer: "^10.4.20",
12566
+ postcss: "^8.4.49",
12567
+ tailwindcss: "^3.4.17",
12568
+ typescript: "^5.6.3",
12569
+ vite: "^6.0.5"
12570
+ }
12571
+ }, null, 2);
12572
+ }
12573
+ function indexHtml(fontFamily) {
12574
+ const fontParam = fontFamily.replace(/\s+/g, "+");
12575
+ return `<!DOCTYPE html>
12576
+ <html lang="en">
12577
+ <head>
12578
+ <meta charset="UTF-8" />
12579
+ <meta name="viewport" content="width=device-width, initial-scale=1.0" />
12580
+ <link rel="preconnect" href="https://fonts.googleapis.com">
12581
+ <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
12582
+ <link href="https://fonts.googleapis.com/css2?family=${fontParam}:wght@400;500;600;700&display=swap" rel="stylesheet">
12583
+ <title>App</title>
12584
+ </head>
12585
+ <body>
12586
+ <div id="root"></div>
12587
+ <script type="module" src="/src/main.tsx"></script>
12588
+ </body>
12589
+ </html>`;
12590
+ }
12591
+ function viteConfig() {
12592
+ return `import { defineConfig } from 'vite'
12593
+ import react from '@vitejs/plugin-react'
12594
+ import path from 'path'
12547
12595
 
12548
- // src/tools/scaffold-netsuite.ts
12549
- function netsuiteTs() {
12596
+ export default defineConfig({
12597
+ plugins: [react()],
12598
+ server: {
12599
+ allowedHosts: true,
12600
+ },
12601
+ resolve: {
12602
+ alias: {
12603
+ '@': path.resolve(__dirname, './src'),
12604
+ },
12605
+ },
12606
+ })`;
12607
+ }
12608
+ function tailwindConfig() {
12609
+ return `export default {
12610
+ darkMode: ["class"],
12611
+ content: ["./index.html", "./src/**/*.{js,ts,jsx,tsx}"],
12612
+ theme: {
12613
+ extend: {
12614
+ colors: {
12615
+ background: "hsl(var(--background))",
12616
+ foreground: "hsl(var(--foreground))",
12617
+ card: { DEFAULT: "hsl(var(--card))", foreground: "hsl(var(--card-foreground))" },
12618
+ primary: { DEFAULT: "hsl(var(--primary))", foreground: "hsl(var(--primary-foreground))" },
12619
+ secondary: { DEFAULT: "hsl(var(--secondary))", foreground: "hsl(var(--secondary-foreground))" },
12620
+ muted: { DEFAULT: "hsl(var(--muted))", foreground: "hsl(var(--muted-foreground))" },
12621
+ accent: { DEFAULT: "hsl(var(--accent))", foreground: "hsl(var(--accent-foreground))" },
12622
+ destructive: { DEFAULT: "hsl(var(--destructive))", foreground: "hsl(var(--destructive-foreground))" },
12623
+ success: { DEFAULT: "hsl(var(--success))", foreground: "hsl(var(--success-foreground))" },
12624
+ warning: { DEFAULT: "hsl(var(--warning))", foreground: "hsl(var(--warning-foreground))" },
12625
+ border: "hsl(var(--border))",
12626
+ input: "hsl(var(--input))",
12627
+ ring: "hsl(var(--ring))",
12628
+ },
12629
+ fontFamily: {
12630
+ sans: "var(--font-sans)",
12631
+ heading: "var(--font-heading)",
12632
+ },
12633
+ borderRadius: {
12634
+ lg: "var(--radius)",
12635
+ md: "calc(var(--radius) - 2px)",
12636
+ sm: "calc(var(--radius) - 4px)",
12637
+ },
12638
+ borderWidth: {
12639
+ DEFAULT: "var(--border-width)",
12640
+ },
12641
+ boxShadow: {
12642
+ DEFAULT: "var(--shadow)",
12643
+ lg: "var(--shadow-lg)",
12644
+ },
12645
+ },
12646
+ },
12647
+ plugins: [],
12648
+ }`;
12649
+ }
12650
+ function postcssConfig() {
12651
+ return `export default {
12652
+ plugins: {
12653
+ tailwindcss: {},
12654
+ autoprefixer: {},
12655
+ },
12656
+ }`;
12657
+ }
12658
+ function indexCss(primaryColor, fontFamily, borderRadius) {
12659
+ const fontValue = `'${fontFamily}', ui-sans-serif, system-ui, sans-serif`;
12550
12660
  return `/*
12551
- * NetSuite OAuth client helper (bearer-token-in-fragment).
12661
+ * HelloLeo Design System -- READ BEFORE STYLING.
12552
12662
  *
12553
- * Two NetSuite Integration Records back this:
12554
- * 1. MCP (scope: mcp) -- used by HelloLeo at build time.
12555
- * 2. Runtime (scope: rest_webservices) -- used here at sign-in time.
12556
- * Runtime credentials live as HelloLeo Cloud secrets on the project's Worker
12557
- * (env.NETSUITE_ACCOUNT_ID / _CLIENT_ID / _CLIENT_SECRET). They never reach
12558
- * the browser. Cookies are NOT used -- frontend \u2194 Worker are cross-site
12559
- * (e.g. *.helloleo.dev <-> *.workers.dev) and Chrome partitions / drops
12560
- * third-party cookies in the HelloLeo preview iframe.
12663
+ * The CSS variables below are the single source of truth for the project's
12664
+ * design tokens. They power the no-code Design panel in the HelloLeo webapp:
12665
+ * the user edits colors / fonts / radius / shadows visually, and the panel
12666
+ * writes back to THIS file. Keeping the conventions intact is what unlocks
12667
+ * that round-trip.
12561
12668
  *
12562
- * Flow:
12563
- * 1. User clicks "Sign in with NetSuite".
12564
- * 2. Browser navigates to the Worker /auth/netsuite/start.
12565
- * 3. Worker redirects to NetSuite's OAuth UI with PKCE.
12566
- * 4. NetSuite redirects back to the Worker /auth/netsuite/callback.
12567
- * 5. Worker exchanges the code, mints a session id, then redirects to
12568
- * <return_to>#nssid=<sid>. URL fragments stay in the browser only.
12569
- * 6. This module reads the fragment on load, stashes the sid in
12570
- * sessionStorage, and sends it as Authorization: Bearer on every
12571
- * Worker call.
12669
+ * Conventions (do not break):
12670
+ * - All colors stored as raw HSL triplets (no hsl() wrapper, no #hex).
12671
+ * Tailwind classes wrap them via hsl(var(--token)). Never inline #hex
12672
+ * in components -- always use design tokens (bg-primary, text-foreground,
12673
+ * border-border, etc.).
12674
+ * - --primary / --secondary / --accent / --destructive / --success /
12675
+ * --warning each have a paired -foreground for text on that surface.
12676
+ * - --radius drives lg/md/sm via calc() in tailwind.config.js. Edit here,
12677
+ * not in component classes.
12678
+ * - --font-sans for body, --font-heading for headings. Both load from the
12679
+ * Google Fonts <link> in index.html.
12680
+ * - .dark variants mirror :root one-for-one. When adding a token, add it
12681
+ * to BOTH blocks or dark mode breaks.
12572
12682
  *
12573
- * NEVER use a hardcoded password gate. NEVER use sessionStorage as the
12574
- * authoritative auth state -- the only acceptable check is /api/me on the
12575
- * Worker. sessionStorage here is just a cache of the bearer token.
12683
+ * UI primitives (Button, Card, Input, Modal, Table, Badge, Alert, Spinner)
12684
+ * MUST live in src/components/ui/ and consume these tokens via Tailwind
12685
+ * classes -- never hardcoded colors. One responsibility per file, <50 lines
12686
+ * per component.
12576
12687
  */
12577
12688
 
12578
- export const WORKER_URL = import.meta.env.VITE_WORKER_URL ?? '';
12579
- const TOKEN_KEY = 'nssid';
12689
+ @tailwind base;
12690
+ @tailwind components;
12691
+ @tailwind utilities;
12580
12692
 
12581
- // Capture-and-strip the #nssid=... fragment as early as possible so URLs
12582
- // shown to the user stay clean. Runs once at module load.
12583
- (function bootstrapTokenFromHash() {
12584
- if (typeof window === 'undefined') return;
12585
- const hash = window.location.hash.replace(/^#/, '');
12586
- if (!hash) return;
12587
- const params = new URLSearchParams(hash);
12588
- const sid = params.get(TOKEN_KEY);
12589
- if (sid) {
12590
- try { sessionStorage.setItem(TOKEN_KEY, sid); } catch {}
12591
- params.delete(TOKEN_KEY);
12592
- const remaining = params.toString();
12593
- const newHash = remaining ? '#' + remaining : '';
12594
- window.history.replaceState(null, '', window.location.pathname + window.location.search + newHash);
12693
+ @layer base {
12694
+ :root {
12695
+ --background: 0 0% 100%;
12696
+ --foreground: 222.2 84% 4.9%;
12697
+ --card: 0 0% 100%;
12698
+ --card-foreground: 222.2 84% 4.9%;
12699
+ --primary: ${primaryColor};
12700
+ --primary-foreground: 210 40% 98%;
12701
+ --secondary: 210 40% 96.1%;
12702
+ --secondary-foreground: 222.2 47.4% 11.2%;
12703
+ --muted: 210 40% 96.1%;
12704
+ --muted-foreground: 215.4 16.3% 46.9%;
12705
+ --accent: 210 40% 96.1%;
12706
+ --accent-foreground: 222.2 47.4% 11.2%;
12707
+ --destructive: 0 84.2% 60.2%;
12708
+ --destructive-foreground: 210 40% 98%;
12709
+ --success: 142 76% 36%;
12710
+ --success-foreground: 210 40% 98%;
12711
+ --warning: 38 92% 50%;
12712
+ --warning-foreground: 222.2 47.4% 11.2%;
12713
+ --border: 214.3 31.8% 91.4%;
12714
+ --input: 214.3 31.8% 91.4%;
12715
+ --ring: 221.2 83.2% 53.3%;
12716
+ --radius: ${borderRadius};
12717
+ --border-width: 1px;
12718
+ --shadow: 0 1px 3px 0 rgb(0 0 0 / 0.1), 0 1px 2px -1px rgb(0 0 0 / 0.1);
12719
+ --shadow-lg: 0 10px 15px -3px rgb(0 0 0 / 0.1), 0 4px 6px -4px rgb(0 0 0 / 0.1);
12720
+ --font-sans: ${fontValue};
12721
+ --font-heading: ${fontValue};
12595
12722
  }
12596
- })();
12597
12723
 
12598
- export function getSessionToken(): string | null {
12599
- try { return sessionStorage.getItem(TOKEN_KEY); } catch { return null; }
12724
+ .dark {
12725
+ --background: 222.2 84% 4.9%;
12726
+ --foreground: 210 40% 98%;
12727
+ --card: 222.2 84% 4.9%;
12728
+ --card-foreground: 210 40% 98%;
12729
+ --primary: 217.2 91.2% 59.8%;
12730
+ --primary-foreground: 222.2 47.4% 11.2%;
12731
+ --secondary: 217.2 32.6% 17.5%;
12732
+ --secondary-foreground: 210 40% 98%;
12733
+ --muted: 217.2 32.6% 17.5%;
12734
+ --muted-foreground: 215 20.2% 65.1%;
12735
+ --accent: 217.2 32.6% 17.5%;
12736
+ --accent-foreground: 210 40% 98%;
12737
+ --destructive: 0 62.8% 30.6%;
12738
+ --destructive-foreground: 210 40% 98%;
12739
+ --success: 142 76% 36%;
12740
+ --success-foreground: 210 40% 98%;
12741
+ --warning: 38 92% 50%;
12742
+ --warning-foreground: 222.2 47.4% 11.2%;
12743
+ --border: 217.2 32.6% 17.5%;
12744
+ --input: 217.2 32.6% 17.5%;
12745
+ --ring: 224.3 76.3% 48%;
12746
+ --border-width: 1px;
12747
+ --shadow: 0 1px 3px 0 rgb(0 0 0 / 0.3), 0 1px 2px -1px rgb(0 0 0 / 0.3);
12748
+ --shadow-lg: 0 10px 15px -3px rgb(0 0 0 / 0.3), 0 4px 6px -4px rgb(0 0 0 / 0.3);
12749
+ --font-sans: ${fontValue};
12750
+ --font-heading: ${fontValue};
12751
+ }
12600
12752
  }
12601
12753
 
12602
- export function signInWithNetSuite(returnTo: string = window.location.href) {
12603
- if (!WORKER_URL) {
12604
- throw new Error(
12605
- 'VITE_WORKER_URL is not set. HelloLeo Cloud must be provisioned before sign-in works.',
12606
- );
12607
- }
12608
- const url = new URL(\`\${WORKER_URL.replace(/\\/$/, '')}/auth/netsuite/start\`);
12609
- url.searchParams.set('return_to', returnTo);
12610
- // The app usually renders inside the HelloLeo preview iframe. NetSuite's
12611
- // OAuth page sets X-Frame-Options: DENY, so navigating the iframe to the
12612
- // Worker (which 302s to NetSuite) ends up loading a page the browser
12613
- // refuses to render -- looks like "nothing happens" to the user. Navigate
12614
- // the top frame instead so the OAuth flow runs at the top level and the
12754
+ body {
12755
+ @apply bg-background text-foreground font-sans;
12756
+ }`;
12757
+ }
12758
+ function utilsTs() {
12759
+ return `import { clsx, type ClassValue } from 'clsx'
12760
+ import { twMerge } from 'tailwind-merge'
12761
+
12762
+ export function cn(...inputs: ClassValue[]) {
12763
+ return twMerge(clsx(inputs))
12764
+ }`;
12765
+ }
12766
+ function proxyTs() {
12767
+ return `/*
12768
+ * HelloLeo Integration Proxy -- runtime helper for connected integrations.
12769
+ *
12770
+ * All third-party APIs (Odoo, Airtable, HubSpot, Stripe, Notion, Gladia,
12771
+ * Microsoft 365, Klaviyo, Supabase, DimoMaint, etc.) are reachable through
12772
+ * this single proxy. The proxy handles auth and CORS using credentials the
12773
+ * user already configured in Project Settings -- you NEVER pass an API key
12774
+ * from frontend code, and you NEVER call third-party hosts directly.
12775
+ *
12776
+ * Two modes:
12777
+ * A) Frontend-only project -> import callIntegration() from this file.
12778
+ * B) HelloLeo Cloud project -> route via Worker fetch(env.INTEGRATION_PROXY).
12779
+ * Use Cloud whenever the project needs backend logic, automations,
12780
+ * webhooks, scheduled jobs, or AI agents. Keep secrets server-side.
12781
+ *
12782
+ * Integration slugs:
12783
+ * Use the exact slug from the Enabled MCP list with the "pipedream-"
12784
+ * prefix removed. e.g. "pipedream-gorgias_oauth" -> "gorgias_oauth".
12785
+ * Native integrations (odoo, airtable) keep their plain name.
12786
+ *
12787
+ * Response shape (always wrapped):
12788
+ * { success: boolean, status: number, data: <provider payload> }
12789
+ *
12790
+ * Unwrap "data" by provider:
12791
+ * odoo -> data.result
12792
+ * airtable -> data.records
12793
+ * notion -> data.results
12794
+ * microsoft 365 -> data.value
12795
+ * klaviyo -> data.data
12796
+ * supabase / dimomaint -> data
12797
+ *
12798
+ * Pagination: put query params directly in the endpoint string,
12799
+ * e.g. endpoint: '/api/tickets?cursor=xyz&per_page=100'.
12800
+ * Read pagination metadata from the response to build the next call.
12801
+ * Never .filter() large lists in the frontend -- paginate server-side.
12802
+ *
12803
+ * Secrets for non-integration APIs (e.g. raw OpenAI): store them in
12804
+ * HelloLeo Cloud -> Settings -> Cloud -> Secrets, and read via env.SECRET_NAME
12805
+ * from a Worker. Never embed secrets in frontend code.
12806
+ */
12807
+
12808
+ const PROJECT_UUID = import.meta.env.VITE_PROJECT_UUID ?? '';
12809
+ export const INTEGRATION_PROXY_URL =
12810
+ \`https://api.app.helloleo.dev/api/integration-proxy/\${PROJECT_UUID}\`;
12811
+
12812
+ export interface ProxyRequest {
12813
+ integration: string;
12814
+ endpoint: string;
12815
+ method?: 'GET' | 'POST' | 'PUT' | 'PATCH' | 'DELETE';
12816
+ body?: unknown;
12817
+ headers?: Record<string, string>;
12818
+ }
12819
+
12820
+ export interface ProxyResponse<T = unknown> {
12821
+ success: boolean;
12822
+ status: number;
12823
+ data: T;
12824
+ }
12825
+
12826
+ export async function callIntegration<T = unknown>(
12827
+ req: ProxyRequest,
12828
+ ): Promise<ProxyResponse<T>> {
12829
+ const res = await fetch(INTEGRATION_PROXY_URL, {
12830
+ method: 'POST',
12831
+ headers: { 'Content-Type': 'application/json' },
12832
+ body: JSON.stringify({ method: 'POST', ...req }),
12833
+ });
12834
+ return res.json();
12835
+ }
12836
+
12837
+ /* Example -- Odoo search_read:
12838
+ *
12839
+ * const r = await callIntegration({
12840
+ * integration: 'odoo',
12841
+ * endpoint: '/jsonrpc',
12842
+ * body: {
12843
+ * model: 'product.template',
12844
+ * method: 'search_read',
12845
+ * args: [[['type', '=', 'consu']]],
12846
+ * kwargs: { fields: ['name', 'list_price'], limit: 50 },
12847
+ * },
12848
+ * });
12849
+ * const products = r.data?.result ?? [];
12850
+ */
12851
+ `;
12852
+ }
12853
+ function hexToHslTriplet(hex3) {
12854
+ let h = hex3.trim().replace(/^#/, "");
12855
+ if (h.length === 3) {
12856
+ h = h.split("").map((c) => c + c).join("");
12857
+ }
12858
+ if (!/^[0-9a-fA-F]{6}$/.test(h))
12859
+ return "";
12860
+ const r = Number.parseInt(h.slice(0, 2), 16) / 255;
12861
+ const g = Number.parseInt(h.slice(2, 4), 16) / 255;
12862
+ const b = Number.parseInt(h.slice(4, 6), 16) / 255;
12863
+ const max = Math.max(r, g, b);
12864
+ const min = Math.min(r, g, b);
12865
+ const l = (max + min) / 2;
12866
+ let s = 0;
12867
+ let hue = 0;
12868
+ if (max !== min) {
12869
+ const d = max - min;
12870
+ s = l > 0.5 ? d / (2 - max - min) : d / (max + min);
12871
+ switch (max) {
12872
+ case r:
12873
+ hue = (g - b) / d + (g < b ? 6 : 0);
12874
+ break;
12875
+ case g:
12876
+ hue = (b - r) / d + 2;
12877
+ break;
12878
+ default:
12879
+ hue = (r - g) / d + 4;
12880
+ }
12881
+ hue *= 60;
12882
+ }
12883
+ return `${Math.round(hue)} ${Math.round(s * 100)}% ${Math.round(l * 100)}%`;
12884
+ }
12885
+ function normalizePrimaryColor(value) {
12886
+ const trimmed = value.trim();
12887
+ if (/^(#|[0-9a-fA-F]{3}$|[0-9a-fA-F]{6}$)/.test(trimmed)) {
12888
+ const hsl = hexToHslTriplet(trimmed);
12889
+ if (hsl)
12890
+ return hsl;
12891
+ }
12892
+ return trimmed;
12893
+ }
12894
+ function mainTsx() {
12895
+ return `import React from 'react'
12896
+ import ReactDOM from 'react-dom/client'
12897
+ import App from './App'
12898
+ import './index.css'
12899
+
12900
+ ReactDOM.createRoot(document.getElementById('root')!).render(
12901
+ <React.StrictMode>
12902
+ <App />
12903
+ </React.StrictMode>,
12904
+ )`;
12905
+ }
12906
+ async function writeBaseScaffold(opts, dir) {
12907
+ const name = opts.name ?? "leo-project";
12908
+ const primary = normalizePrimaryColor(opts.primaryColor ?? "221.2 83.2% 53.3%");
12909
+ const font = opts.fontFamily ?? "Inter";
12910
+ const radius = opts.borderRadius ?? "0.5rem";
12911
+ await mkdir(path2.join(dir, "src", "lib"), { recursive: true });
12912
+ const files = [
12913
+ ["package.json", packageJson(name)],
12914
+ ["index.html", indexHtml(font)],
12915
+ ["vite.config.js", viteConfig()],
12916
+ ["tailwind.config.js", tailwindConfig()],
12917
+ ["postcss.config.js", postcssConfig()],
12918
+ [path2.join("src", "index.css"), indexCss(primary, font, radius)],
12919
+ [path2.join("src", "lib", "utils.ts"), utilsTs()],
12920
+ [path2.join("src", "lib", "proxy.ts"), proxyTs()],
12921
+ [path2.join("src", "main.tsx"), mainTsx()]
12922
+ ];
12923
+ const created = [];
12924
+ for (const [filePath, content] of files) {
12925
+ await Bun.write(path2.join(dir, filePath), content);
12926
+ created.push(filePath);
12927
+ }
12928
+ return created;
12929
+ }
12930
+ var scaffold = tool({
12931
+ description: "Scaffold a new React + Vite + Tailwind CSS project with the HelloLeo design system. " + "Creates all mandatory boilerplate: package.json, index.html (with Google Fonts), " + "vite.config.js, tailwind.config.js, postcss.config.js, src/index.css (CSS variables " + "light+dark), src/lib/utils.ts (cn helper), src/lib/proxy.ts, and src/main.tsx. " + "Call this FIRST for any new project before writing App.tsx and components. " + "For NetSuite projects (NetSuite MCP in the live list), call `setup_netsuite` " + "INSTEAD -- it bundles base scaffold + OAuth wiring in one call. Do not call both.",
12932
+ args: {
12933
+ name: tool.schema.string().optional().describe("Project name for package.json (default: leo-project)"),
12934
+ primaryColor: tool.schema.string().optional().describe("Primary brand color. Prefer raw HSL triplet (no hsl() wrapper, no # prefix), " + 'e.g. "221.2 83.2% 53.3%". Hex like "#3b82f6" is also accepted and will ' + "be auto-converted. Default: blue."),
12935
+ fontFamily: tool.schema.string().optional().describe('Google Font family name, e.g. "Inter", "Poppins" (default: Inter)'),
12936
+ borderRadius: tool.schema.string().optional().describe('Base border radius, e.g. "0.5rem", "0.75rem" (default: 0.5rem)')
12937
+ },
12938
+ async execute(args, context) {
12939
+ const created = await writeBaseScaffold(args, context.directory);
12940
+ return [
12941
+ `Scaffolded ${created.length} files:`,
12942
+ ...created.map((f) => ` - ${f}`),
12943
+ "",
12944
+ "Next: create src/App.tsx with your application code.",
12945
+ "Design system reference: read the header comment in src/index.css",
12946
+ " before adding tokens or styling primitives.",
12947
+ "Integration calls: use callIntegration() from src/lib/proxy.ts.",
12948
+ " Header comment in that file documents slugs, response unwrapping,",
12949
+ " pagination, and frontend-only vs HelloLeo Cloud routing.",
12950
+ "Update package.json with any extra packages you need after scaffolding."
12951
+ ].join(`
12952
+ `);
12953
+ }
12954
+ });
12955
+ var ScaffoldPlugin = async () => {
12956
+ return {
12957
+ tool: { scaffold }
12958
+ };
12959
+ };
12960
+ // src/tools/setup-netsuite.ts
12961
+ import { mkdir as mkdir2 } from "fs/promises";
12962
+ import path3 from "path";
12963
+ function netsuiteTs() {
12964
+ return `/*
12965
+ * NetSuite OAuth client helper (bearer-token-in-fragment).
12966
+ *
12967
+ * Two NetSuite Integration Records back this:
12968
+ * 1. MCP (scope: mcp) -- used by HelloLeo at build time.
12969
+ * 2. Runtime (scope: rest_webservices) -- used here at sign-in time.
12970
+ * Runtime credentials live as HelloLeo Cloud secrets on the project's Worker
12971
+ * (env.NETSUITE_ACCOUNT_ID / _CLIENT_ID / _CLIENT_SECRET). They never reach
12972
+ * the browser. Cookies are NOT used -- frontend \u2194 Worker are cross-site
12973
+ * (e.g. *.helloleo.dev <-> *.workers.dev) and Chrome partitions / drops
12974
+ * third-party cookies in the HelloLeo preview iframe.
12975
+ *
12976
+ * Flow:
12977
+ * 1. User clicks "Sign in with NetSuite".
12978
+ * 2. Browser navigates to the Worker /auth/netsuite/start.
12979
+ * 3. Worker redirects to NetSuite's OAuth UI with PKCE.
12980
+ * 4. NetSuite redirects back to the Worker /auth/netsuite/callback.
12981
+ * 5. Worker exchanges the code, mints a session id, then redirects to
12982
+ * <return_to>#nssid=<sid>. URL fragments stay in the browser only.
12983
+ * 6. This module reads the fragment on load, stashes the sid in
12984
+ * sessionStorage, and sends it as Authorization: Bearer on every
12985
+ * Worker call.
12986
+ *
12987
+ * NEVER use a hardcoded password gate. NEVER use sessionStorage as the
12988
+ * authoritative auth state -- the only acceptable check is /api/me on the
12989
+ * Worker. sessionStorage here is just a cache of the bearer token.
12990
+ */
12991
+
12992
+ export const WORKER_URL = import.meta.env.VITE_WORKER_URL ?? '';
12993
+ const TOKEN_KEY = 'nssid';
12994
+
12995
+ // Capture-and-strip the #nssid=... fragment as early as possible so URLs
12996
+ // shown to the user stay clean. Runs once at module load.
12997
+ (function bootstrapTokenFromHash() {
12998
+ if (typeof window === 'undefined') return;
12999
+ const hash = window.location.hash.replace(/^#/, '');
13000
+ if (!hash) return;
13001
+ const params = new URLSearchParams(hash);
13002
+ const sid = params.get(TOKEN_KEY);
13003
+ if (sid) {
13004
+ try { sessionStorage.setItem(TOKEN_KEY, sid); } catch {}
13005
+ params.delete(TOKEN_KEY);
13006
+ const remaining = params.toString();
13007
+ const newHash = remaining ? '#' + remaining : '';
13008
+ window.history.replaceState(null, '', window.location.pathname + window.location.search + newHash);
13009
+ }
13010
+ })();
13011
+
13012
+ export function getSessionToken(): string | null {
13013
+ try { return sessionStorage.getItem(TOKEN_KEY); } catch { return null; }
13014
+ }
13015
+
13016
+ export function signInWithNetSuite(returnTo: string = window.location.href) {
13017
+ if (!WORKER_URL) {
13018
+ throw new Error(
13019
+ 'VITE_WORKER_URL is not set. HelloLeo Cloud must be provisioned before sign-in works.',
13020
+ );
13021
+ }
13022
+ const url = new URL(\`\${WORKER_URL.replace(/\\/$/, '')}/auth/netsuite/start\`);
13023
+ url.searchParams.set('return_to', returnTo);
13024
+ // The app usually renders inside the HelloLeo preview iframe. NetSuite's
13025
+ // OAuth page sets X-Frame-Options: DENY, so navigating the iframe to the
13026
+ // Worker (which 302s to NetSuite) ends up loading a page the browser
13027
+ // refuses to render -- looks like "nothing happens" to the user. Navigate
13028
+ // the top frame instead so the OAuth flow runs at the top level and the
12615
13029
  // callback redirect lands on the same top frame, which then reads the
12616
13030
  // #nssid= fragment. Fallback to same-frame navigation if the top window
12617
13031
  // is cross-origin and can't be reassigned.
@@ -12799,448 +13213,73 @@ export default function App() {
12799
13213
  }
12800
13214
  `;
12801
13215
  }
12802
-
12803
- // src/tools/scaffold.ts
12804
- function packageJson(name) {
12805
- return JSON.stringify({
12806
- name,
12807
- private: true,
12808
- version: "0.0.0",
12809
- type: "module",
12810
- scripts: {
12811
- dev: "vite",
12812
- start: "vite",
12813
- build: "vite build",
12814
- preview: "vite preview"
12815
- },
12816
- dependencies: {
12817
- react: "^18.3.1",
12818
- "react-dom": "^18.3.1",
12819
- clsx: "^2.1.1",
12820
- "tailwind-merge": "^2.6.0",
12821
- "lucide-react": "^1.14.0"
12822
- },
12823
- devDependencies: {
12824
- "@types/react": "^18.3.12",
12825
- "@types/react-dom": "^18.3.1",
12826
- "@vitejs/plugin-react": "^4.3.4",
12827
- autoprefixer: "^10.4.20",
12828
- postcss: "^8.4.49",
12829
- tailwindcss: "^3.4.17",
12830
- typescript: "^5.6.3",
12831
- vite: "^6.0.5"
12832
- }
12833
- }, null, 2);
12834
- }
12835
- function indexHtml(fontFamily) {
12836
- const fontParam = fontFamily.replace(/\s+/g, "+");
12837
- return `<!DOCTYPE html>
12838
- <html lang="en">
12839
- <head>
12840
- <meta charset="UTF-8" />
12841
- <meta name="viewport" content="width=device-width, initial-scale=1.0" />
12842
- <link rel="preconnect" href="https://fonts.googleapis.com">
12843
- <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
12844
- <link href="https://fonts.googleapis.com/css2?family=${fontParam}:wght@400;500;600;700&display=swap" rel="stylesheet">
12845
- <title>App</title>
12846
- </head>
12847
- <body>
12848
- <div id="root"></div>
12849
- <script type="module" src="/src/main.tsx"></script>
12850
- </body>
12851
- </html>`;
12852
- }
12853
- function viteConfig() {
12854
- return `import { defineConfig } from 'vite'
12855
- import react from '@vitejs/plugin-react'
12856
- import path from 'path'
12857
-
12858
- export default defineConfig({
12859
- plugins: [react()],
12860
- server: {
12861
- allowedHosts: true,
12862
- },
12863
- resolve: {
12864
- alias: {
12865
- '@': path.resolve(__dirname, './src'),
12866
- },
12867
- },
12868
- })`;
12869
- }
12870
- function tailwindConfig() {
12871
- return `export default {
12872
- darkMode: ["class"],
12873
- content: ["./index.html", "./src/**/*.{js,ts,jsx,tsx}"],
12874
- theme: {
12875
- extend: {
12876
- colors: {
12877
- background: "hsl(var(--background))",
12878
- foreground: "hsl(var(--foreground))",
12879
- card: { DEFAULT: "hsl(var(--card))", foreground: "hsl(var(--card-foreground))" },
12880
- primary: { DEFAULT: "hsl(var(--primary))", foreground: "hsl(var(--primary-foreground))" },
12881
- secondary: { DEFAULT: "hsl(var(--secondary))", foreground: "hsl(var(--secondary-foreground))" },
12882
- muted: { DEFAULT: "hsl(var(--muted))", foreground: "hsl(var(--muted-foreground))" },
12883
- accent: { DEFAULT: "hsl(var(--accent))", foreground: "hsl(var(--accent-foreground))" },
12884
- destructive: { DEFAULT: "hsl(var(--destructive))", foreground: "hsl(var(--destructive-foreground))" },
12885
- success: { DEFAULT: "hsl(var(--success))", foreground: "hsl(var(--success-foreground))" },
12886
- warning: { DEFAULT: "hsl(var(--warning))", foreground: "hsl(var(--warning-foreground))" },
12887
- border: "hsl(var(--border))",
12888
- input: "hsl(var(--input))",
12889
- ring: "hsl(var(--ring))",
12890
- },
12891
- fontFamily: {
12892
- sans: "var(--font-sans)",
12893
- heading: "var(--font-heading)",
12894
- },
12895
- borderRadius: {
12896
- lg: "var(--radius)",
12897
- md: "calc(var(--radius) - 2px)",
12898
- sm: "calc(var(--radius) - 4px)",
12899
- },
12900
- borderWidth: {
12901
- DEFAULT: "var(--border-width)",
12902
- },
12903
- boxShadow: {
12904
- DEFAULT: "var(--shadow)",
12905
- lg: "var(--shadow-lg)",
12906
- },
12907
- },
12908
- },
12909
- plugins: [],
12910
- }`;
12911
- }
12912
- function postcssConfig() {
12913
- return `export default {
12914
- plugins: {
12915
- tailwindcss: {},
12916
- autoprefixer: {},
12917
- },
12918
- }`;
12919
- }
12920
- function indexCss(primaryColor, fontFamily, borderRadius) {
12921
- const fontValue = `'${fontFamily}', ui-sans-serif, system-ui, sans-serif`;
12922
- return `/*
12923
- * HelloLeo Design System -- READ BEFORE STYLING.
12924
- *
12925
- * The CSS variables below are the single source of truth for the project's
12926
- * design tokens. They power the no-code Design panel in the HelloLeo webapp:
12927
- * the user edits colors / fonts / radius / shadows visually, and the panel
12928
- * writes back to THIS file. Keeping the conventions intact is what unlocks
12929
- * that round-trip.
12930
- *
12931
- * Conventions (do not break):
12932
- * - All colors stored as raw HSL triplets (no hsl() wrapper, no #hex).
12933
- * Tailwind classes wrap them via hsl(var(--token)). Never inline #hex
12934
- * in components -- always use design tokens (bg-primary, text-foreground,
12935
- * border-border, etc.).
12936
- * - --primary / --secondary / --accent / --destructive / --success /
12937
- * --warning each have a paired -foreground for text on that surface.
12938
- * - --radius drives lg/md/sm via calc() in tailwind.config.js. Edit here,
12939
- * not in component classes.
12940
- * - --font-sans for body, --font-heading for headings. Both load from the
12941
- * Google Fonts <link> in index.html.
12942
- * - .dark variants mirror :root one-for-one. When adding a token, add it
12943
- * to BOTH blocks or dark mode breaks.
12944
- *
12945
- * UI primitives (Button, Card, Input, Modal, Table, Badge, Alert, Spinner)
12946
- * MUST live in src/components/ui/ and consume these tokens via Tailwind
12947
- * classes -- never hardcoded colors. One responsibility per file, <50 lines
12948
- * per component.
12949
- */
12950
-
12951
- @tailwind base;
12952
- @tailwind components;
12953
- @tailwind utilities;
12954
-
12955
- @layer base {
12956
- :root {
12957
- --background: 0 0% 100%;
12958
- --foreground: 222.2 84% 4.9%;
12959
- --card: 0 0% 100%;
12960
- --card-foreground: 222.2 84% 4.9%;
12961
- --primary: ${primaryColor};
12962
- --primary-foreground: 210 40% 98%;
12963
- --secondary: 210 40% 96.1%;
12964
- --secondary-foreground: 222.2 47.4% 11.2%;
12965
- --muted: 210 40% 96.1%;
12966
- --muted-foreground: 215.4 16.3% 46.9%;
12967
- --accent: 210 40% 96.1%;
12968
- --accent-foreground: 222.2 47.4% 11.2%;
12969
- --destructive: 0 84.2% 60.2%;
12970
- --destructive-foreground: 210 40% 98%;
12971
- --success: 142 76% 36%;
12972
- --success-foreground: 210 40% 98%;
12973
- --warning: 38 92% 50%;
12974
- --warning-foreground: 222.2 47.4% 11.2%;
12975
- --border: 214.3 31.8% 91.4%;
12976
- --input: 214.3 31.8% 91.4%;
12977
- --ring: 221.2 83.2% 53.3%;
12978
- --radius: ${borderRadius};
12979
- --border-width: 1px;
12980
- --shadow: 0 1px 3px 0 rgb(0 0 0 / 0.1), 0 1px 2px -1px rgb(0 0 0 / 0.1);
12981
- --shadow-lg: 0 10px 15px -3px rgb(0 0 0 / 0.1), 0 4px 6px -4px rgb(0 0 0 / 0.1);
12982
- --font-sans: ${fontValue};
12983
- --font-heading: ${fontValue};
12984
- }
12985
-
12986
- .dark {
12987
- --background: 222.2 84% 4.9%;
12988
- --foreground: 210 40% 98%;
12989
- --card: 222.2 84% 4.9%;
12990
- --card-foreground: 210 40% 98%;
12991
- --primary: 217.2 91.2% 59.8%;
12992
- --primary-foreground: 222.2 47.4% 11.2%;
12993
- --secondary: 217.2 32.6% 17.5%;
12994
- --secondary-foreground: 210 40% 98%;
12995
- --muted: 217.2 32.6% 17.5%;
12996
- --muted-foreground: 215 20.2% 65.1%;
12997
- --accent: 217.2 32.6% 17.5%;
12998
- --accent-foreground: 210 40% 98%;
12999
- --destructive: 0 62.8% 30.6%;
13000
- --destructive-foreground: 210 40% 98%;
13001
- --success: 142 76% 36%;
13002
- --success-foreground: 210 40% 98%;
13003
- --warning: 38 92% 50%;
13004
- --warning-foreground: 222.2 47.4% 11.2%;
13005
- --border: 217.2 32.6% 17.5%;
13006
- --input: 217.2 32.6% 17.5%;
13007
- --ring: 224.3 76.3% 48%;
13008
- --border-width: 1px;
13009
- --shadow: 0 1px 3px 0 rgb(0 0 0 / 0.3), 0 1px 2px -1px rgb(0 0 0 / 0.3);
13010
- --shadow-lg: 0 10px 15px -3px rgb(0 0 0 / 0.3), 0 4px 6px -4px rgb(0 0 0 / 0.3);
13011
- --font-sans: ${fontValue};
13012
- --font-heading: ${fontValue};
13013
- }
13014
- }
13015
-
13016
- body {
13017
- @apply bg-background text-foreground font-sans;
13018
- }`;
13019
- }
13020
- function utilsTs() {
13021
- return `import { clsx, type ClassValue } from 'clsx'
13022
- import { twMerge } from 'tailwind-merge'
13023
-
13024
- export function cn(...inputs: ClassValue[]) {
13025
- return twMerge(clsx(inputs))
13026
- }`;
13027
- }
13028
- function proxyTs() {
13029
- return `/*
13030
- * HelloLeo Integration Proxy -- runtime helper for connected integrations.
13031
- *
13032
- * All third-party APIs (Odoo, Airtable, HubSpot, Stripe, Notion, Gladia,
13033
- * Microsoft 365, Klaviyo, Supabase, DimoMaint, etc.) are reachable through
13034
- * this single proxy. The proxy handles auth and CORS using credentials the
13035
- * user already configured in Project Settings -- you NEVER pass an API key
13036
- * from frontend code, and you NEVER call third-party hosts directly.
13037
- *
13038
- * Two modes:
13039
- * A) Frontend-only project -> import callIntegration() from this file.
13040
- * B) HelloLeo Cloud project -> route via Worker fetch(env.INTEGRATION_PROXY).
13041
- * Use Cloud whenever the project needs backend logic, automations,
13042
- * webhooks, scheduled jobs, or AI agents. Keep secrets server-side.
13043
- *
13044
- * Integration slugs:
13045
- * Use the exact slug from the Enabled MCP list with the "pipedream-"
13046
- * prefix removed. e.g. "pipedream-gorgias_oauth" -> "gorgias_oauth".
13047
- * Native integrations (odoo, airtable) keep their plain name.
13048
- *
13049
- * Response shape (always wrapped):
13050
- * { success: boolean, status: number, data: <provider payload> }
13051
- *
13052
- * Unwrap "data" by provider:
13053
- * odoo -> data.result
13054
- * airtable -> data.records
13055
- * notion -> data.results
13056
- * microsoft 365 -> data.value
13057
- * klaviyo -> data.data
13058
- * supabase / dimomaint -> data
13059
- *
13060
- * Pagination: put query params directly in the endpoint string,
13061
- * e.g. endpoint: '/api/tickets?cursor=xyz&per_page=100'.
13062
- * Read pagination metadata from the response to build the next call.
13063
- * Never .filter() large lists in the frontend -- paginate server-side.
13064
- *
13065
- * Secrets for non-integration APIs (e.g. raw OpenAI): store them in
13066
- * HelloLeo Cloud -> Settings -> Cloud -> Secrets, and read via env.SECRET_NAME
13067
- * from a Worker. Never embed secrets in frontend code.
13068
- */
13069
-
13070
- const PROJECT_UUID = import.meta.env.VITE_PROJECT_UUID ?? '';
13071
- export const INTEGRATION_PROXY_URL =
13072
- \`https://api.app.helloleo.dev/api/integration-proxy/\${PROJECT_UUID}\`;
13073
-
13074
- export interface ProxyRequest {
13075
- integration: string;
13076
- endpoint: string;
13077
- method?: 'GET' | 'POST' | 'PUT' | 'PATCH' | 'DELETE';
13078
- body?: unknown;
13079
- headers?: Record<string, string>;
13080
- }
13081
-
13082
- export interface ProxyResponse<T = unknown> {
13083
- success: boolean;
13084
- status: number;
13085
- data: T;
13086
- }
13087
-
13088
- export async function callIntegration<T = unknown>(
13089
- req: ProxyRequest,
13090
- ): Promise<ProxyResponse<T>> {
13091
- const res = await fetch(INTEGRATION_PROXY_URL, {
13092
- method: 'POST',
13093
- headers: { 'Content-Type': 'application/json' },
13094
- body: JSON.stringify({ method: 'POST', ...req }),
13095
- });
13096
- return res.json();
13097
- }
13098
-
13099
- /* Example -- Odoo search_read:
13100
- *
13101
- * const r = await callIntegration({
13102
- * integration: 'odoo',
13103
- * endpoint: '/jsonrpc',
13104
- * body: {
13105
- * model: 'product.template',
13106
- * method: 'search_read',
13107
- * args: [[['type', '=', 'consu']]],
13108
- * kwargs: { fields: ['name', 'list_price'], limit: 50 },
13109
- * },
13110
- * });
13111
- * const products = r.data?.result ?? [];
13112
- */
13113
- `;
13114
- }
13115
- function hexToHslTriplet(hex3) {
13116
- let h = hex3.trim().replace(/^#/, "");
13117
- if (h.length === 3) {
13118
- h = h.split("").map((c) => c + c).join("");
13119
- }
13120
- if (!/^[0-9a-fA-F]{6}$/.test(h))
13121
- return "";
13122
- const r = Number.parseInt(h.slice(0, 2), 16) / 255;
13123
- const g = Number.parseInt(h.slice(2, 4), 16) / 255;
13124
- const b = Number.parseInt(h.slice(4, 6), 16) / 255;
13125
- const max = Math.max(r, g, b);
13126
- const min = Math.min(r, g, b);
13127
- const l = (max + min) / 2;
13128
- let s = 0;
13129
- let hue = 0;
13130
- if (max !== min) {
13131
- const d = max - min;
13132
- s = l > 0.5 ? d / (2 - max - min) : d / (max + min);
13133
- switch (max) {
13134
- case r:
13135
- hue = (g - b) / d + (g < b ? 6 : 0);
13136
- break;
13137
- case g:
13138
- hue = (b - r) / d + 2;
13139
- break;
13140
- default:
13141
- hue = (r - g) / d + 4;
13142
- }
13143
- hue *= 60;
13144
- }
13145
- return `${Math.round(hue)} ${Math.round(s * 100)}% ${Math.round(l * 100)}%`;
13146
- }
13147
- function normalizePrimaryColor(value) {
13148
- const trimmed = value.trim();
13149
- if (/^(#|[0-9a-fA-F]{3}$|[0-9a-fA-F]{6}$)/.test(trimmed)) {
13150
- const hsl = hexToHslTriplet(trimmed);
13151
- if (hsl)
13152
- return hsl;
13153
- }
13154
- return trimmed;
13155
- }
13156
- function mainTsx() {
13157
- return `import React from 'react'
13158
- import ReactDOM from 'react-dom/client'
13159
- import App from './App'
13160
- import './index.css'
13161
-
13162
- ReactDOM.createRoot(document.getElementById('root')!).render(
13163
- <React.StrictMode>
13164
- <App />
13165
- </React.StrictMode>,
13166
- )`;
13167
- }
13168
- var scaffold = tool({
13169
- description: "Scaffold a new React + Vite + Tailwind CSS project with the HelloLeo design system. " + "Creates all mandatory boilerplate: package.json, index.html (with Google Fonts), " + "vite.config.js, tailwind.config.js, postcss.config.js, src/index.css (CSS variables " + "light+dark), src/lib/utils.ts (cn helper), and src/main.tsx. " + "Call this FIRST for any new project before writing App.tsx and components.",
13170
- args: {
13171
- name: tool.schema.string().optional().describe("Project name for package.json (default: leo-project)"),
13172
- primaryColor: tool.schema.string().optional().describe("Primary brand color. Prefer raw HSL triplet (no hsl() wrapper, no # prefix), " + 'e.g. "221.2 83.2% 53.3%". Hex like "#3b82f6" is also accepted and will ' + "be auto-converted. Default: blue."),
13173
- fontFamily: tool.schema.string().optional().describe('Google Font family name, e.g. "Inter", "Poppins" (default: Inter)'),
13174
- borderRadius: tool.schema.string().optional().describe('Base border radius, e.g. "0.5rem", "0.75rem" (default: 0.5rem)'),
13175
- integrations: tool.schema.array(tool.schema.string()).optional().describe("Connected integration slugs for this project (live MCP list). " + 'Pass ["netsuite"] when the project uses NetSuite -- the scaffold ' + "will add src/lib/netsuite.ts (OAuth client helper), " + "src/components/auth/{SignInWithNetSuite,LoginPage,AuthGate}.tsx " + "and a src/App.tsx wrapped in <AuthGate>. OAuth is wired from " + "day one. Forbids hardcoded password gates -- the gate IS the " + "real flow against the project's Worker.")
13176
- },
13177
- async execute(args, context) {
13216
+ var setupNetsuite = tool({
13217
+ description: "Bootstrap a NetSuite-backed React + Vite + Tailwind project in one call. " + "Writes the full HelloLeo base scaffold AND the NetSuite OAuth wiring " + "(src/lib/netsuite.ts, src/components/auth/{SignInWithNetSuite,LoginPage,AuthGate}.tsx, " + "src/App.tsx wrapped in <AuthGate>). " + "Use this INSTEAD of `scaffold` for any project that needs NetSuite. " + "Uses default theme (Inter font, blue primary, 0.5rem radius) -- edit " + "src/index.css afterwards to customise. Real OAuth is wired from day " + "one -- never replace the gate with a password placeholder.",
13218
+ args: {},
13219
+ async execute(_args, context) {
13178
13220
  const dir = context.directory;
13179
- const name = args.name ?? "leo-project";
13180
- const primary = normalizePrimaryColor(args.primaryColor ?? "221.2 83.2% 53.3%");
13181
- const font = args.fontFamily ?? "Inter";
13182
- const radius = args.borderRadius ?? "0.5rem";
13183
- const integrations = (args.integrations ?? []).map((s) => s.toLowerCase());
13184
- const hasNetsuite = integrations.includes("netsuite");
13185
- await mkdir(path2.join(dir, "src", "lib"), { recursive: true });
13186
- if (hasNetsuite) {
13187
- await mkdir(path2.join(dir, "src", "components", "auth"), {
13188
- recursive: true
13189
- });
13190
- }
13191
- const files = [
13192
- ["package.json", packageJson(name)],
13193
- ["index.html", indexHtml(font)],
13194
- ["vite.config.js", viteConfig()],
13195
- ["tailwind.config.js", tailwindConfig()],
13196
- ["postcss.config.js", postcssConfig()],
13197
- [path2.join("src", "index.css"), indexCss(primary, font, radius)],
13198
- [path2.join("src", "lib", "utils.ts"), utilsTs()],
13199
- [path2.join("src", "lib", "proxy.ts"), proxyTs()],
13200
- [path2.join("src", "main.tsx"), mainTsx()]
13201
- ];
13202
- if (hasNetsuite) {
13203
- files.push([path2.join("src", "lib", "netsuite.ts"), netsuiteTs()], [
13204
- path2.join("src", "components", "auth", "SignInWithNetSuite.tsx"),
13221
+ const base = await writeBaseScaffold({}, dir);
13222
+ await mkdir2(path3.join(dir, "src", "components", "auth"), {
13223
+ recursive: true
13224
+ });
13225
+ const nsFiles = [
13226
+ [path3.join("src", "lib", "netsuite.ts"), netsuiteTs()],
13227
+ [
13228
+ path3.join("src", "components", "auth", "SignInWithNetSuite.tsx"),
13205
13229
  signInButtonTsx()
13206
- ], [
13207
- path2.join("src", "components", "auth", "LoginPage.tsx"),
13230
+ ],
13231
+ [
13232
+ path3.join("src", "components", "auth", "LoginPage.tsx"),
13208
13233
  netsuiteLoginPageTsx()
13209
- ], [
13210
- path2.join("src", "components", "auth", "AuthGate.tsx"),
13234
+ ],
13235
+ [
13236
+ path3.join("src", "components", "auth", "AuthGate.tsx"),
13211
13237
  netsuiteAuthGateTsx()
13212
- ], [path2.join("src", "App.tsx"), netsuiteAppTsx()]);
13213
- }
13214
- const created = [];
13215
- for (const [filePath, content] of files) {
13216
- await Bun.write(path2.join(dir, filePath), content);
13217
- created.push(filePath);
13218
- }
13219
- const out = [
13220
- `Scaffolded ${created.length} files:`,
13221
- ...created.map((f) => ` - ${f}`),
13238
+ ],
13239
+ [path3.join("src", "App.tsx"), netsuiteAppTsx()]
13240
+ ];
13241
+ const nsCreated = [];
13242
+ for (const [filePath, content] of nsFiles) {
13243
+ await Bun.write(path3.join(dir, filePath), content);
13244
+ nsCreated.push(filePath);
13245
+ }
13246
+ const all = [...base, ...nsCreated];
13247
+ return [
13248
+ `Scaffolded ${all.length} files (base + NetSuite OAuth):`,
13249
+ ...all.map((f) => ` - ${f}`),
13222
13250
  "",
13223
- "Next: create src/App.tsx with your application code.",
13251
+ "Next: build your dashboard INSIDE <AuthGate> in src/App.tsx.",
13224
13252
  "Design system reference: read the header comment in src/index.css",
13225
13253
  " before adding tokens or styling primitives.",
13226
- "Integration calls: use callIntegration() from src/lib/proxy.ts.",
13227
- " Header comment in that file documents slugs, response unwrapping,",
13228
- " pagination, and frontend-only vs HelloLeo Cloud routing.",
13229
- "Update package.json with any extra packages you need after scaffolding."
13230
- ];
13231
- if (hasNetsuite) {
13232
- out.push("", "NetSuite project -- auth is already wired:", " - App.tsx wraps everything in <AuthGate>. Build your dashboard", " INSIDE the gate. NEVER remove the gate or build a password", " placeholder -- real OAuth is the only allowed login.", " - LoginPage.tsx + SignInWithNetSuite.tsx are the unauthenticated", " view. Customise the copy / imagery, keep <SignInWithNetSuite/>", " as the call-to-action.", " - signInWithNetSuite() in src/lib/netsuite.ts hits the Worker at", " /auth/netsuite/start. Deploy the Worker via cloudflare-backend", " MCP with routes:", " GET /auth/netsuite/start -- redirect to NetSuite OAuth", " GET /auth/netsuite/callback -- code -> token exchange", " GET /api/me -- current session", " POST /auth/netsuite/logout -- clear session", " Worker reads env.NETSUITE_ACCOUNT_ID, env.NETSUITE_CLIENT_ID,", " env.NETSUITE_CLIENT_SECRET (set by the runtime-setup wizard).", " - VITE_WORKER_URL must be set in the project env so the frontend", " knows where to send users. The HelloLeo Cloud provisioner", " seeds this automatically.");
13233
- }
13234
- return out.join(`
13254
+ "",
13255
+ "NetSuite auth -- already wired:",
13256
+ " - App.tsx wraps everything in <AuthGate>. NEVER remove the gate",
13257
+ " or build a password placeholder -- real OAuth is the only login.",
13258
+ " - LoginPage.tsx + SignInWithNetSuite.tsx are the unauthenticated",
13259
+ " view. Customise the copy / imagery, keep <SignInWithNetSuite/>",
13260
+ " as the call-to-action.",
13261
+ " - signInWithNetSuite() in src/lib/netsuite.ts hits the Worker at",
13262
+ " /auth/netsuite/start. Deploy the Worker via cloudflare-backend",
13263
+ " MCP with routes:",
13264
+ " GET /auth/netsuite/start -- redirect to NetSuite OAuth",
13265
+ " GET /auth/netsuite/callback -- code -> token exchange",
13266
+ " GET /api/me -- current session",
13267
+ " POST /auth/netsuite/logout -- clear session",
13268
+ " Worker reads env.NETSUITE_ACCOUNT_ID, env.NETSUITE_CLIENT_ID,",
13269
+ " env.NETSUITE_CLIENT_SECRET (set by the runtime-setup wizard).",
13270
+ " - VITE_WORKER_URL must be set so the frontend knows where to",
13271
+ " send users. The HelloLeo Cloud provisioner seeds this."
13272
+ ].join(`
13235
13273
  `);
13236
13274
  }
13237
13275
  });
13238
- var ScaffoldPlugin = async () => {
13276
+ var SetupNetsuitePlugin = async () => {
13239
13277
  return {
13240
- tool: { scaffold }
13278
+ tool: { setup_netsuite: setupNetsuite }
13241
13279
  };
13242
13280
  };
13243
13281
  export {
13282
+ SetupNetsuitePlugin,
13244
13283
  ScaffoldPlugin,
13245
13284
  RemovePlugin,
13246
13285
  InspectHttpPlugin