@helloleo/plugins 0.1.14 → 0.1.16
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 +197 -469
- package/dist/templates/tanstack-router-2026-06-15/components.json +22 -0
- package/dist/templates/tanstack-router-2026-06-15/index.html +13 -0
- package/dist/templates/tanstack-router-2026-06-15/package.json +94 -0
- package/dist/templates/tanstack-router-2026-06-15/src/components/ui/accordion.tsx +51 -0
- package/dist/templates/tanstack-router-2026-06-15/src/components/ui/alert-dialog.tsx +115 -0
- package/dist/templates/tanstack-router-2026-06-15/src/components/ui/alert.tsx +49 -0
- package/dist/templates/tanstack-router-2026-06-15/src/components/ui/aspect-ratio.tsx +5 -0
- package/dist/templates/tanstack-router-2026-06-15/src/components/ui/avatar.tsx +47 -0
- package/dist/templates/tanstack-router-2026-06-15/src/components/ui/badge.tsx +32 -0
- package/dist/templates/tanstack-router-2026-06-15/src/components/ui/breadcrumb.tsx +101 -0
- package/dist/templates/tanstack-router-2026-06-15/src/components/ui/button.tsx +49 -0
- package/dist/templates/tanstack-router-2026-06-15/src/components/ui/calendar.tsx +177 -0
- package/dist/templates/tanstack-router-2026-06-15/src/components/ui/card.tsx +55 -0
- package/dist/templates/tanstack-router-2026-06-15/src/components/ui/carousel.tsx +240 -0
- package/dist/templates/tanstack-router-2026-06-15/src/components/ui/chart.tsx +331 -0
- package/dist/templates/tanstack-router-2026-06-15/src/components/ui/checkbox.tsx +26 -0
- package/dist/templates/tanstack-router-2026-06-15/src/components/ui/collapsible.tsx +11 -0
- package/dist/templates/tanstack-router-2026-06-15/src/components/ui/command.tsx +143 -0
- package/dist/templates/tanstack-router-2026-06-15/src/components/ui/context-menu.tsx +187 -0
- package/dist/templates/tanstack-router-2026-06-15/src/components/ui/dialog.tsx +104 -0
- package/dist/templates/tanstack-router-2026-06-15/src/components/ui/drawer.tsx +98 -0
- package/dist/templates/tanstack-router-2026-06-15/src/components/ui/dropdown-menu.tsx +188 -0
- package/dist/templates/tanstack-router-2026-06-15/src/components/ui/form.tsx +171 -0
- package/dist/templates/tanstack-router-2026-06-15/src/components/ui/hover-card.tsx +27 -0
- package/dist/templates/tanstack-router-2026-06-15/src/components/ui/input-otp.tsx +69 -0
- package/dist/templates/tanstack-router-2026-06-15/src/components/ui/input.tsx +22 -0
- package/dist/templates/tanstack-router-2026-06-15/src/components/ui/label.tsx +21 -0
- package/dist/templates/tanstack-router-2026-06-15/src/components/ui/menubar.tsx +229 -0
- package/dist/templates/tanstack-router-2026-06-15/src/components/ui/navigation-menu.tsx +120 -0
- package/dist/templates/tanstack-router-2026-06-15/src/components/ui/pagination.tsx +98 -0
- package/dist/templates/tanstack-router-2026-06-15/src/components/ui/popover.tsx +31 -0
- package/dist/templates/tanstack-router-2026-06-15/src/components/ui/progress.tsx +25 -0
- package/dist/templates/tanstack-router-2026-06-15/src/components/ui/radio-group.tsx +36 -0
- package/dist/templates/tanstack-router-2026-06-15/src/components/ui/resizable.tsx +37 -0
- package/dist/templates/tanstack-router-2026-06-15/src/components/ui/scroll-area.tsx +44 -0
- package/dist/templates/tanstack-router-2026-06-15/src/components/ui/select.tsx +152 -0
- package/dist/templates/tanstack-router-2026-06-15/src/components/ui/separator.tsx +24 -0
- package/dist/templates/tanstack-router-2026-06-15/src/components/ui/sheet.tsx +122 -0
- package/dist/templates/tanstack-router-2026-06-15/src/components/ui/sidebar.tsx +744 -0
- package/dist/templates/tanstack-router-2026-06-15/src/components/ui/skeleton.tsx +7 -0
- package/dist/templates/tanstack-router-2026-06-15/src/components/ui/slider.tsx +23 -0
- package/dist/templates/tanstack-router-2026-06-15/src/components/ui/sonner.tsx +23 -0
- package/dist/templates/tanstack-router-2026-06-15/src/components/ui/switch.tsx +27 -0
- package/dist/templates/tanstack-router-2026-06-15/src/components/ui/table.tsx +94 -0
- package/dist/templates/tanstack-router-2026-06-15/src/components/ui/tabs.tsx +53 -0
- package/dist/templates/tanstack-router-2026-06-15/src/components/ui/textarea.tsx +21 -0
- package/dist/templates/tanstack-router-2026-06-15/src/components/ui/toggle-group.tsx +57 -0
- package/dist/templates/tanstack-router-2026-06-15/src/components/ui/toggle.tsx +42 -0
- package/dist/templates/tanstack-router-2026-06-15/src/components/ui/tooltip.tsx +32 -0
- package/dist/templates/tanstack-router-2026-06-15/src/hooks/use-mobile.tsx +19 -0
- package/dist/templates/tanstack-router-2026-06-15/src/lib/proxy.ts +83 -0
- package/dist/templates/tanstack-router-2026-06-15/src/lib/utils.ts +6 -0
- package/dist/templates/tanstack-router-2026-06-15/src/main.tsx +33 -0
- package/dist/templates/tanstack-router-2026-06-15/src/routeTree.gen.ts +59 -0
- package/dist/templates/tanstack-router-2026-06-15/src/router.tsx +23 -0
- package/dist/templates/tanstack-router-2026-06-15/src/routes/__root.tsx +16 -0
- package/dist/templates/tanstack-router-2026-06-15/src/routes/index.tsx +14 -0
- package/dist/templates/tanstack-router-2026-06-15/src/styles.css +214 -0
- package/dist/templates/tanstack-router-2026-06-15/tsconfig.json +27 -0
- package/dist/templates/tanstack-router-2026-06-15/vite.config.ts +24 -0
- package/package.json +3 -2
- package/src/template-render.ts +48 -0
- package/src/templates/tanstack-router-2026-06-15/components.json +22 -0
- package/src/templates/tanstack-router-2026-06-15/index.html +13 -0
- package/src/templates/tanstack-router-2026-06-15/package.json +94 -0
- package/src/templates/tanstack-router-2026-06-15/src/components/ui/accordion.tsx +51 -0
- package/src/templates/tanstack-router-2026-06-15/src/components/ui/alert-dialog.tsx +115 -0
- package/src/templates/tanstack-router-2026-06-15/src/components/ui/alert.tsx +49 -0
- package/src/templates/tanstack-router-2026-06-15/src/components/ui/aspect-ratio.tsx +5 -0
- package/src/templates/tanstack-router-2026-06-15/src/components/ui/avatar.tsx +47 -0
- package/src/templates/tanstack-router-2026-06-15/src/components/ui/badge.tsx +32 -0
- package/src/templates/tanstack-router-2026-06-15/src/components/ui/breadcrumb.tsx +101 -0
- package/src/templates/tanstack-router-2026-06-15/src/components/ui/button.tsx +49 -0
- package/src/templates/tanstack-router-2026-06-15/src/components/ui/calendar.tsx +177 -0
- package/src/templates/tanstack-router-2026-06-15/src/components/ui/card.tsx +55 -0
- package/src/templates/tanstack-router-2026-06-15/src/components/ui/carousel.tsx +240 -0
- package/src/templates/tanstack-router-2026-06-15/src/components/ui/chart.tsx +331 -0
- package/src/templates/tanstack-router-2026-06-15/src/components/ui/checkbox.tsx +26 -0
- package/src/templates/tanstack-router-2026-06-15/src/components/ui/collapsible.tsx +11 -0
- package/src/templates/tanstack-router-2026-06-15/src/components/ui/command.tsx +143 -0
- package/src/templates/tanstack-router-2026-06-15/src/components/ui/context-menu.tsx +187 -0
- package/src/templates/tanstack-router-2026-06-15/src/components/ui/dialog.tsx +104 -0
- package/src/templates/tanstack-router-2026-06-15/src/components/ui/drawer.tsx +98 -0
- package/src/templates/tanstack-router-2026-06-15/src/components/ui/dropdown-menu.tsx +188 -0
- package/src/templates/tanstack-router-2026-06-15/src/components/ui/form.tsx +171 -0
- package/src/templates/tanstack-router-2026-06-15/src/components/ui/hover-card.tsx +27 -0
- package/src/templates/tanstack-router-2026-06-15/src/components/ui/input-otp.tsx +69 -0
- package/src/templates/tanstack-router-2026-06-15/src/components/ui/input.tsx +22 -0
- package/src/templates/tanstack-router-2026-06-15/src/components/ui/label.tsx +21 -0
- package/src/templates/tanstack-router-2026-06-15/src/components/ui/menubar.tsx +229 -0
- package/src/templates/tanstack-router-2026-06-15/src/components/ui/navigation-menu.tsx +120 -0
- package/src/templates/tanstack-router-2026-06-15/src/components/ui/pagination.tsx +98 -0
- package/src/templates/tanstack-router-2026-06-15/src/components/ui/popover.tsx +31 -0
- package/src/templates/tanstack-router-2026-06-15/src/components/ui/progress.tsx +25 -0
- package/src/templates/tanstack-router-2026-06-15/src/components/ui/radio-group.tsx +36 -0
- package/src/templates/tanstack-router-2026-06-15/src/components/ui/resizable.tsx +37 -0
- package/src/templates/tanstack-router-2026-06-15/src/components/ui/scroll-area.tsx +44 -0
- package/src/templates/tanstack-router-2026-06-15/src/components/ui/select.tsx +152 -0
- package/src/templates/tanstack-router-2026-06-15/src/components/ui/separator.tsx +24 -0
- package/src/templates/tanstack-router-2026-06-15/src/components/ui/sheet.tsx +122 -0
- package/src/templates/tanstack-router-2026-06-15/src/components/ui/sidebar.tsx +744 -0
- package/src/templates/tanstack-router-2026-06-15/src/components/ui/skeleton.tsx +7 -0
- package/src/templates/tanstack-router-2026-06-15/src/components/ui/slider.tsx +23 -0
- package/src/templates/tanstack-router-2026-06-15/src/components/ui/sonner.tsx +23 -0
- package/src/templates/tanstack-router-2026-06-15/src/components/ui/switch.tsx +27 -0
- package/src/templates/tanstack-router-2026-06-15/src/components/ui/table.tsx +94 -0
- package/src/templates/tanstack-router-2026-06-15/src/components/ui/tabs.tsx +53 -0
- package/src/templates/tanstack-router-2026-06-15/src/components/ui/textarea.tsx +21 -0
- package/src/templates/tanstack-router-2026-06-15/src/components/ui/toggle-group.tsx +57 -0
- package/src/templates/tanstack-router-2026-06-15/src/components/ui/toggle.tsx +42 -0
- package/src/templates/tanstack-router-2026-06-15/src/components/ui/tooltip.tsx +32 -0
- package/src/templates/tanstack-router-2026-06-15/src/hooks/use-mobile.tsx +19 -0
- package/src/templates/tanstack-router-2026-06-15/src/lib/proxy.ts +83 -0
- package/src/templates/tanstack-router-2026-06-15/src/lib/utils.ts +6 -0
- package/src/templates/tanstack-router-2026-06-15/src/main.tsx +33 -0
- package/src/templates/tanstack-router-2026-06-15/src/routeTree.gen.ts +59 -0
- package/src/templates/tanstack-router-2026-06-15/src/router.tsx +23 -0
- package/src/templates/tanstack-router-2026-06-15/src/routes/__root.tsx +16 -0
- package/src/templates/tanstack-router-2026-06-15/src/routes/index.tsx +14 -0
- package/src/templates/tanstack-router-2026-06-15/src/styles.css +214 -0
- package/src/templates/tanstack-router-2026-06-15/tsconfig.json +27 -0
- package/src/templates/tanstack-router-2026-06-15/vite.config.ts +24 -0
- package/src/tools/scaffold.ts +51 -444
- package/src/tools/setup-netsuite.ts +161 -80
package/dist/index.js
CHANGED
|
@@ -12538,404 +12538,50 @@ var RemovePlugin = async () => {
|
|
|
12538
12538
|
};
|
|
12539
12539
|
// src/tools/scaffold.ts
|
|
12540
12540
|
import { mkdir } from "fs/promises";
|
|
12541
|
-
import
|
|
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'
|
|
12595
|
-
|
|
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`;
|
|
12660
|
-
return `/*
|
|
12661
|
-
* HelloLeo Design System -- READ BEFORE STYLING.
|
|
12662
|
-
*
|
|
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.
|
|
12668
|
-
*
|
|
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.
|
|
12682
|
-
*
|
|
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.
|
|
12687
|
-
*/
|
|
12688
|
-
|
|
12689
|
-
@tailwind base;
|
|
12690
|
-
@tailwind components;
|
|
12691
|
-
@tailwind utilities;
|
|
12692
|
-
|
|
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};
|
|
12722
|
-
}
|
|
12541
|
+
import path3 from "path";
|
|
12723
12542
|
|
|
12724
|
-
|
|
12725
|
-
|
|
12726
|
-
|
|
12727
|
-
|
|
12728
|
-
|
|
12729
|
-
|
|
12730
|
-
|
|
12731
|
-
|
|
12732
|
-
|
|
12733
|
-
|
|
12734
|
-
|
|
12735
|
-
|
|
12736
|
-
|
|
12737
|
-
|
|
12738
|
-
|
|
12739
|
-
|
|
12740
|
-
|
|
12741
|
-
|
|
12742
|
-
|
|
12743
|
-
|
|
12744
|
-
|
|
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};
|
|
12543
|
+
// src/template-render.ts
|
|
12544
|
+
import path2 from "path";
|
|
12545
|
+
var {Glob } = globalThis.Bun;
|
|
12546
|
+
var TEMPLATES_ROOT = path2.join(import.meta.dir, "templates");
|
|
12547
|
+
var TOKEN_RE = /\{\{(LEO_[A-Z0-9_]+)\}\}/g;
|
|
12548
|
+
var SKIP = /(^|\/)(node_modules|dist|dist-ssr|\.tanstack|\.output|bun\.lock|bun\.lockb|package-lock\.json|yarn\.lock|pnpm-lock\.yaml)(\/|$)/;
|
|
12549
|
+
async function renderTemplate(templateId, vars, destDir) {
|
|
12550
|
+
const root = path2.join(TEMPLATES_ROOT, templateId);
|
|
12551
|
+
const glob = new Glob("**/*");
|
|
12552
|
+
const created = [];
|
|
12553
|
+
for await (const rel of glob.scan({
|
|
12554
|
+
cwd: root,
|
|
12555
|
+
dot: true,
|
|
12556
|
+
onlyFiles: true
|
|
12557
|
+
})) {
|
|
12558
|
+
if (SKIP.test(rel))
|
|
12559
|
+
continue;
|
|
12560
|
+
const raw = await Bun.file(path2.join(root, rel)).text();
|
|
12561
|
+
const content = raw.replace(TOKEN_RE, (_m, key) => vars[key] ?? "");
|
|
12562
|
+
await Bun.write(path2.join(destDir, rel), content);
|
|
12563
|
+
created.push(rel);
|
|
12751
12564
|
}
|
|
12565
|
+
return created.sort();
|
|
12752
12566
|
}
|
|
12753
12567
|
|
|
12754
|
-
|
|
12755
|
-
|
|
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
|
-
}
|
|
12568
|
+
// src/tools/scaffold.ts
|
|
12569
|
+
var SCAFFOLD_VERSION = "tanstack-router-2026-06-15";
|
|
12766
12570
|
function projectUuidFromDir(dir) {
|
|
12767
|
-
return
|
|
12768
|
-
}
|
|
12769
|
-
function proxyTs(projectUuid) {
|
|
12770
|
-
return `/*
|
|
12771
|
-
* HelloLeo Integration Proxy -- runtime helper for connected integrations.
|
|
12772
|
-
*
|
|
12773
|
-
* All third-party APIs (Odoo, Airtable, HubSpot, Stripe, Notion, Gladia,
|
|
12774
|
-
* Microsoft 365, Klaviyo, Supabase, DimoMaint, etc.) are reachable through
|
|
12775
|
-
* this single proxy. The proxy handles auth and CORS using credentials the
|
|
12776
|
-
* user already configured in Project Settings -- you NEVER pass an API key
|
|
12777
|
-
* from frontend code, and you NEVER call third-party hosts directly.
|
|
12778
|
-
*
|
|
12779
|
-
* Two modes:
|
|
12780
|
-
* A) Frontend-only project -> import callIntegration() from this file.
|
|
12781
|
-
* B) HelloLeo Cloud project -> route via Worker fetch(env.INTEGRATION_PROXY).
|
|
12782
|
-
* Use Cloud whenever the project needs backend logic, automations,
|
|
12783
|
-
* webhooks, scheduled jobs, or AI agents. Keep secrets server-side.
|
|
12784
|
-
*
|
|
12785
|
-
* Integration slugs:
|
|
12786
|
-
* Use the exact slug from the Enabled MCP list with the "pipedream-"
|
|
12787
|
-
* prefix removed. e.g. "pipedream-gorgias_oauth" -> "gorgias_oauth".
|
|
12788
|
-
* Native integrations (odoo, airtable) keep their plain name.
|
|
12789
|
-
*
|
|
12790
|
-
* Response shape (always wrapped):
|
|
12791
|
-
* { success: boolean, status: number, data: <provider payload> }
|
|
12792
|
-
*
|
|
12793
|
-
* Unwrap "data" by provider:
|
|
12794
|
-
* odoo -> data.result
|
|
12795
|
-
* airtable -> data.records
|
|
12796
|
-
* notion -> data.results
|
|
12797
|
-
* microsoft 365 -> data.value
|
|
12798
|
-
* klaviyo -> data.data
|
|
12799
|
-
* supabase / dimomaint -> data
|
|
12800
|
-
*
|
|
12801
|
-
* Pagination: put query params directly in the endpoint string,
|
|
12802
|
-
* e.g. endpoint: '/api/tickets?cursor=xyz&per_page=100'.
|
|
12803
|
-
* Read pagination metadata from the response to build the next call.
|
|
12804
|
-
* Never .filter() large lists in the frontend -- paginate server-side.
|
|
12805
|
-
*
|
|
12806
|
-
* Secrets for non-integration APIs (e.g. raw OpenAI): store them in
|
|
12807
|
-
* HelloLeo Cloud -> Settings -> Cloud -> Secrets, and read via env.SECRET_NAME
|
|
12808
|
-
* from a Worker. Never embed secrets in frontend code.
|
|
12809
|
-
*/
|
|
12810
|
-
|
|
12811
|
-
export const INTEGRATION_PROXY_URL =
|
|
12812
|
-
'https://api.app.helloleo.dev/api/integration-proxy/${projectUuid}';
|
|
12813
|
-
|
|
12814
|
-
export interface ProxyRequest {
|
|
12815
|
-
integration: string;
|
|
12816
|
-
endpoint: string;
|
|
12817
|
-
method?: 'GET' | 'POST' | 'PUT' | 'PATCH' | 'DELETE';
|
|
12818
|
-
body?: unknown;
|
|
12819
|
-
headers?: Record<string, string>;
|
|
12820
|
-
}
|
|
12821
|
-
|
|
12822
|
-
export interface ProxyResponse<T = unknown> {
|
|
12823
|
-
success: boolean;
|
|
12824
|
-
status: number;
|
|
12825
|
-
data: T;
|
|
12826
|
-
}
|
|
12827
|
-
|
|
12828
|
-
export async function callIntegration<T = unknown>(
|
|
12829
|
-
req: ProxyRequest,
|
|
12830
|
-
): Promise<ProxyResponse<T>> {
|
|
12831
|
-
const res = await fetch(INTEGRATION_PROXY_URL, {
|
|
12832
|
-
method: 'POST',
|
|
12833
|
-
headers: { 'Content-Type': 'application/json' },
|
|
12834
|
-
body: JSON.stringify({ method: 'POST', ...req }),
|
|
12835
|
-
});
|
|
12836
|
-
return res.json();
|
|
12837
|
-
}
|
|
12838
|
-
|
|
12839
|
-
/* Example -- Odoo search_read:
|
|
12840
|
-
*
|
|
12841
|
-
* const r = await callIntegration({
|
|
12842
|
-
* integration: 'odoo',
|
|
12843
|
-
* endpoint: '/jsonrpc',
|
|
12844
|
-
* body: {
|
|
12845
|
-
* model: 'product.template',
|
|
12846
|
-
* method: 'search_read',
|
|
12847
|
-
* args: [[['type', '=', 'consu']]],
|
|
12848
|
-
* kwargs: { fields: ['name', 'list_price'], limit: 50 },
|
|
12849
|
-
* },
|
|
12850
|
-
* });
|
|
12851
|
-
* const products = r.data?.result ?? [];
|
|
12852
|
-
*/
|
|
12853
|
-
`;
|
|
12854
|
-
}
|
|
12855
|
-
function hexToHslTriplet(hex3) {
|
|
12856
|
-
let h = hex3.trim().replace(/^#/, "");
|
|
12857
|
-
if (h.length === 3) {
|
|
12858
|
-
h = h.split("").map((c) => c + c).join("");
|
|
12859
|
-
}
|
|
12860
|
-
if (!/^[0-9a-fA-F]{6}$/.test(h))
|
|
12861
|
-
return "";
|
|
12862
|
-
const r = Number.parseInt(h.slice(0, 2), 16) / 255;
|
|
12863
|
-
const g = Number.parseInt(h.slice(2, 4), 16) / 255;
|
|
12864
|
-
const b = Number.parseInt(h.slice(4, 6), 16) / 255;
|
|
12865
|
-
const max = Math.max(r, g, b);
|
|
12866
|
-
const min = Math.min(r, g, b);
|
|
12867
|
-
const l = (max + min) / 2;
|
|
12868
|
-
let s = 0;
|
|
12869
|
-
let hue = 0;
|
|
12870
|
-
if (max !== min) {
|
|
12871
|
-
const d = max - min;
|
|
12872
|
-
s = l > 0.5 ? d / (2 - max - min) : d / (max + min);
|
|
12873
|
-
switch (max) {
|
|
12874
|
-
case r:
|
|
12875
|
-
hue = (g - b) / d + (g < b ? 6 : 0);
|
|
12876
|
-
break;
|
|
12877
|
-
case g:
|
|
12878
|
-
hue = (b - r) / d + 2;
|
|
12879
|
-
break;
|
|
12880
|
-
default:
|
|
12881
|
-
hue = (r - g) / d + 4;
|
|
12882
|
-
}
|
|
12883
|
-
hue *= 60;
|
|
12884
|
-
}
|
|
12885
|
-
return `${Math.round(hue)} ${Math.round(s * 100)}% ${Math.round(l * 100)}%`;
|
|
12886
|
-
}
|
|
12887
|
-
function normalizePrimaryColor(value) {
|
|
12888
|
-
const trimmed = value.trim();
|
|
12889
|
-
if (/^(#|[0-9a-fA-F]{3}$|[0-9a-fA-F]{6}$)/.test(trimmed)) {
|
|
12890
|
-
const hsl = hexToHslTriplet(trimmed);
|
|
12891
|
-
if (hsl)
|
|
12892
|
-
return hsl;
|
|
12893
|
-
}
|
|
12894
|
-
return trimmed;
|
|
12895
|
-
}
|
|
12896
|
-
function mainTsx() {
|
|
12897
|
-
return `import React from 'react'
|
|
12898
|
-
import ReactDOM from 'react-dom/client'
|
|
12899
|
-
import App from './App'
|
|
12900
|
-
import './index.css'
|
|
12901
|
-
|
|
12902
|
-
ReactDOM.createRoot(document.getElementById('root')!).render(
|
|
12903
|
-
<React.StrictMode>
|
|
12904
|
-
<App />
|
|
12905
|
-
</React.StrictMode>,
|
|
12906
|
-
)`;
|
|
12571
|
+
return path3.basename(dir);
|
|
12907
12572
|
}
|
|
12908
12573
|
async function writeBaseScaffold(opts, dir) {
|
|
12909
12574
|
const name = opts.name ?? "leo-project";
|
|
12910
|
-
const
|
|
12911
|
-
|
|
12912
|
-
const
|
|
12913
|
-
await
|
|
12914
|
-
|
|
12915
|
-
["package.json", packageJson(name)],
|
|
12916
|
-
["index.html", indexHtml(font)],
|
|
12917
|
-
["vite.config.js", viteConfig()],
|
|
12918
|
-
["tailwind.config.js", tailwindConfig()],
|
|
12919
|
-
["postcss.config.js", postcssConfig()],
|
|
12920
|
-
[path2.join("src", "index.css"), indexCss(primary, font, radius)],
|
|
12921
|
-
[path2.join("src", "lib", "utils.ts"), utilsTs()],
|
|
12922
|
-
[path2.join("src", "lib", "proxy.ts"), proxyTs(projectUuidFromDir(dir))],
|
|
12923
|
-
[path2.join("src", "main.tsx"), mainTsx()]
|
|
12924
|
-
];
|
|
12925
|
-
const created = [];
|
|
12926
|
-
for (const [filePath, content] of files) {
|
|
12927
|
-
await Bun.write(path2.join(dir, filePath), content);
|
|
12928
|
-
created.push(filePath);
|
|
12929
|
-
}
|
|
12930
|
-
return created;
|
|
12575
|
+
const created = await renderTemplate(SCAFFOLD_VERSION, { LEO_NAME: name, LEO_PROJECT_UUID: projectUuidFromDir(dir) }, dir);
|
|
12576
|
+
await mkdir(path3.join(dir, ".leo"), { recursive: true });
|
|
12577
|
+
const leoPath = path3.join(".leo", "project.json");
|
|
12578
|
+
await Bun.write(path3.join(dir, leoPath), JSON.stringify({ $schema: 1, scaffold: SCAFFOLD_VERSION }, null, 2));
|
|
12579
|
+
return [...created, leoPath];
|
|
12931
12580
|
}
|
|
12932
12581
|
var scaffold = tool({
|
|
12933
|
-
description: "Scaffold a new React + Vite + Tailwind CSS
|
|
12582
|
+
description: "Scaffold a new React 19 + Vite + Tailwind CSS v4 + TanStack Router project " + "with shadcn/ui baked in. Renders the full template: TanStack file-based " + "routing (src/routes/), the complete shadcn/ui component set under " + "src/components/ui, src/lib/utils.ts (cn helper), src/lib/proxy.ts " + "(HelloLeo integration proxy), src/styles.css (shadcn design tokens, " + "light+dark), plus package.json, vite.config.ts, tsconfig.json, " + "tsr.config.json, components.json, .gitignore and the generated " + "src/routeTree.gen.ts. Call this FIRST for any new project before adding " + "routes and pages. 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.",
|
|
12934
12583
|
args: {
|
|
12935
|
-
name: tool.schema.string().optional().describe("Project name for package.json (default: leo-project)")
|
|
12936
|
-
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."),
|
|
12937
|
-
fontFamily: tool.schema.string().optional().describe('Google Font family name, e.g. "Inter", "Poppins" (default: Inter)'),
|
|
12938
|
-
borderRadius: tool.schema.string().optional().describe('Base border radius, e.g. "0.5rem", "0.75rem" (default: 0.5rem)')
|
|
12584
|
+
name: tool.schema.string().optional().describe("Project name for package.json (default: leo-project)")
|
|
12939
12585
|
},
|
|
12940
12586
|
async execute(args, context) {
|
|
12941
12587
|
const created = await writeBaseScaffold(args, context.directory);
|
|
@@ -12943,13 +12589,21 @@ var scaffold = tool({
|
|
|
12943
12589
|
`Scaffolded ${created.length} files:`,
|
|
12944
12590
|
...created.map((f) => ` - ${f}`),
|
|
12945
12591
|
"",
|
|
12946
|
-
"Next:
|
|
12947
|
-
"
|
|
12948
|
-
"
|
|
12949
|
-
"
|
|
12950
|
-
"
|
|
12951
|
-
"
|
|
12952
|
-
"
|
|
12592
|
+
"Next: add routes as files under src/routes/ (TanStack file-based",
|
|
12593
|
+
" routing). Edit src/routes/index.tsx for the home page.",
|
|
12594
|
+
"UI: shadcn/ui components are already in src/components/ui -- import and",
|
|
12595
|
+
" use them; do not attempt to re-add via the shadcn CLI.",
|
|
12596
|
+
"Design system: edit tokens in src/styles.css (light + dark blocks).",
|
|
12597
|
+
"Data fetching: React Query is wired (QueryClient on the router context +",
|
|
12598
|
+
" QueryClientProvider). ALWAYS fetch with @tanstack/react-query --",
|
|
12599
|
+
" useQuery / useMutation in components, or a route loader calling",
|
|
12600
|
+
" context.queryClient.ensureQueryData(). Do NOT hand-roll fetches with",
|
|
12601
|
+
" useEffect + useState. Wrap the fetch call (usually callIntegration())",
|
|
12602
|
+
" in the queryFn.",
|
|
12603
|
+
"Integration calls: use callIntegration() from src/lib/proxy.ts -- as the",
|
|
12604
|
+
" queryFn body, never on its own in useEffect. Its header comment",
|
|
12605
|
+
" documents slugs, response unwrapping, pagination, and frontend-only vs",
|
|
12606
|
+
" HelloLeo Cloud routing."
|
|
12953
12607
|
].join(`
|
|
12954
12608
|
`);
|
|
12955
12609
|
}
|
|
@@ -12961,7 +12615,7 @@ var ScaffoldPlugin = async () => {
|
|
|
12961
12615
|
};
|
|
12962
12616
|
// src/tools/setup-netsuite.ts
|
|
12963
12617
|
import { mkdir as mkdir2 } from "fs/promises";
|
|
12964
|
-
import
|
|
12618
|
+
import path4 from "path";
|
|
12965
12619
|
function netsuiteTs() {
|
|
12966
12620
|
return `/*
|
|
12967
12621
|
* NetSuite OAuth client helper (bearer-token-in-fragment).
|
|
@@ -13152,34 +12806,119 @@ export function LoginPage() {
|
|
|
13152
12806
|
}
|
|
13153
12807
|
`;
|
|
13154
12808
|
}
|
|
13155
|
-
function
|
|
13156
|
-
return `import
|
|
12809
|
+
function authTsx() {
|
|
12810
|
+
return `import * as React from 'react';
|
|
12811
|
+
import {
|
|
12812
|
+
getNetSuiteSession,
|
|
12813
|
+
signInWithNetSuite,
|
|
12814
|
+
signOutNetSuite,
|
|
12815
|
+
type NetSuiteSession,
|
|
12816
|
+
} from './netsuite';
|
|
12817
|
+
|
|
12818
|
+
/**
|
|
12819
|
+
* NetSuite auth context. Holds the current session (resolved from the Worker's
|
|
12820
|
+
* /api/me via getNetSuiteSession) and exposes sign-in / sign-out. The only
|
|
12821
|
+
* authoritative auth check is /api/me -- never trust sessionStorage directly.
|
|
12822
|
+
*/
|
|
12823
|
+
export interface NetSuiteAuthContextValue {
|
|
12824
|
+
pending: boolean;
|
|
12825
|
+
isAuthenticated: boolean;
|
|
12826
|
+
session: NetSuiteSession | null;
|
|
12827
|
+
signIn: () => void;
|
|
12828
|
+
signOut: () => Promise<void>;
|
|
12829
|
+
refresh: () => Promise<void>;
|
|
12830
|
+
}
|
|
12831
|
+
|
|
12832
|
+
const NetSuiteAuthContext =
|
|
12833
|
+
React.createContext<NetSuiteAuthContextValue | null>(null);
|
|
12834
|
+
|
|
12835
|
+
export function NetSuiteAuthProvider({
|
|
12836
|
+
children,
|
|
12837
|
+
}: {
|
|
12838
|
+
children: React.ReactNode;
|
|
12839
|
+
}) {
|
|
12840
|
+
const [session, setSession] = React.useState<NetSuiteSession | null>(null);
|
|
12841
|
+
const [pending, setPending] = React.useState(true);
|
|
12842
|
+
|
|
12843
|
+
const refresh = React.useCallback(async () => {
|
|
12844
|
+
setPending(true);
|
|
12845
|
+
try {
|
|
12846
|
+
setSession(await getNetSuiteSession());
|
|
12847
|
+
} finally {
|
|
12848
|
+
setPending(false);
|
|
12849
|
+
}
|
|
12850
|
+
}, []);
|
|
12851
|
+
|
|
12852
|
+
React.useEffect(() => {
|
|
12853
|
+
refresh();
|
|
12854
|
+
}, [refresh]);
|
|
12855
|
+
|
|
12856
|
+
const value: NetSuiteAuthContextValue = {
|
|
12857
|
+
pending,
|
|
12858
|
+
isAuthenticated: Boolean(session?.authenticated),
|
|
12859
|
+
session,
|
|
12860
|
+
signIn: () => signInWithNetSuite(),
|
|
12861
|
+
signOut: signOutNetSuite,
|
|
12862
|
+
refresh,
|
|
12863
|
+
};
|
|
12864
|
+
|
|
12865
|
+
return (
|
|
12866
|
+
<NetSuiteAuthContext.Provider value={value}>
|
|
12867
|
+
{children}
|
|
12868
|
+
</NetSuiteAuthContext.Provider>
|
|
12869
|
+
);
|
|
12870
|
+
}
|
|
12871
|
+
|
|
12872
|
+
export function useNetSuiteAuth(): NetSuiteAuthContextValue {
|
|
12873
|
+
const ctx = React.useContext(NetSuiteAuthContext);
|
|
12874
|
+
if (!ctx) {
|
|
12875
|
+
throw new Error('useNetSuiteAuth must be used within <NetSuiteAuthProvider>');
|
|
12876
|
+
}
|
|
12877
|
+
return ctx;
|
|
12878
|
+
}
|
|
12879
|
+
`;
|
|
12880
|
+
}
|
|
12881
|
+
function authLayoutTsx() {
|
|
12882
|
+
return `import type { ReactNode } from 'react';
|
|
12883
|
+
import { NetSuiteAuthProvider, useNetSuiteAuth } from '../../lib/auth';
|
|
13157
12884
|
import { LoginPage } from './LoginPage';
|
|
13158
|
-
import { getNetSuiteSession, type NetSuiteSession } from '../../lib/netsuite';
|
|
13159
12885
|
|
|
13160
12886
|
/**
|
|
13161
|
-
*
|
|
13162
|
-
*
|
|
13163
|
-
* is the ONLY supported authentication for a NetSuite project (no password
|
|
13164
|
-
* gates, no sessionStorage flags, no shared credential).
|
|
13165
|
-
*
|
|
13166
|
-
* Pattern in src/App.tsx:
|
|
12887
|
+
* NetSuite auth layout. This is the ONLY wiring you need to add by hand --
|
|
12888
|
+
* wrap the root route's <Outlet /> with it:
|
|
13167
12889
|
*
|
|
13168
|
-
*
|
|
13169
|
-
*
|
|
12890
|
+
* // src/routes/__root.tsx
|
|
12891
|
+
* import { AuthLayout } from '../components/auth/AuthLayout';
|
|
12892
|
+
* ...
|
|
12893
|
+
* function RootComponent() {
|
|
13170
12894
|
* return (
|
|
13171
|
-
*
|
|
13172
|
-
* <
|
|
13173
|
-
*
|
|
12895
|
+
* <>
|
|
12896
|
+
* <AuthLayout>
|
|
12897
|
+
* <Outlet />
|
|
12898
|
+
* </AuthLayout>
|
|
12899
|
+
* { ...devtools... }
|
|
12900
|
+
* </>
|
|
13174
12901
|
* );
|
|
13175
12902
|
* }
|
|
12903
|
+
*
|
|
12904
|
+
* It provides NetSuite auth context to the whole tree and gates rendering:
|
|
12905
|
+
* while the session resolves it shows a spinner, unauthenticated users see
|
|
12906
|
+
* <LoginPage />, authenticated users see your routes. Use useNetSuiteAuth()
|
|
12907
|
+
* anywhere below it to read the session or sign out. NEVER swap this for a
|
|
12908
|
+
* password gate -- real OAuth is the only supported login.
|
|
13176
12909
|
*/
|
|
13177
|
-
export function
|
|
13178
|
-
|
|
12910
|
+
export function AuthLayout({ children }: { children: ReactNode }) {
|
|
12911
|
+
return (
|
|
12912
|
+
<NetSuiteAuthProvider>
|
|
12913
|
+
<Gate>{children}</Gate>
|
|
12914
|
+
</NetSuiteAuthProvider>
|
|
12915
|
+
);
|
|
12916
|
+
}
|
|
13179
12917
|
|
|
13180
|
-
|
|
12918
|
+
function Gate({ children }: { children: ReactNode }) {
|
|
12919
|
+
const { pending, isAuthenticated } = useNetSuiteAuth();
|
|
13181
12920
|
|
|
13182
|
-
if (
|
|
12921
|
+
if (pending) {
|
|
13183
12922
|
return (
|
|
13184
12923
|
<div className="min-h-screen flex items-center justify-center bg-background">
|
|
13185
12924
|
<div className="h-8 w-8 rounded-full border-2 border-primary border-t-transparent animate-spin" />
|
|
@@ -13187,90 +12926,79 @@ export function AuthGate({ children }: { children: React.ReactNode }) {
|
|
|
13187
12926
|
);
|
|
13188
12927
|
}
|
|
13189
12928
|
|
|
13190
|
-
if (!
|
|
12929
|
+
if (!isAuthenticated) return <LoginPage />;
|
|
13191
12930
|
|
|
13192
12931
|
return <>{children}</>;
|
|
13193
12932
|
}
|
|
13194
12933
|
`;
|
|
13195
12934
|
}
|
|
13196
|
-
function netsuiteAppTsx() {
|
|
13197
|
-
return `import { AuthGate } from './components/auth/AuthGate';
|
|
13198
|
-
|
|
13199
|
-
/**
|
|
13200
|
-
* NetSuite project entry point. KEEP <AuthGate> as the outermost element --
|
|
13201
|
-
* removing it disables real OAuth and exposes NetSuite data to anonymous
|
|
13202
|
-
* users. Build your dashboard / pages inside the gate.
|
|
13203
|
-
*/
|
|
13204
|
-
export default function App() {
|
|
13205
|
-
return (
|
|
13206
|
-
<AuthGate>
|
|
13207
|
-
<div className="min-h-screen bg-background text-foreground p-8">
|
|
13208
|
-
<h1 className="text-2xl font-bold">You are signed in to NetSuite.</h1>
|
|
13209
|
-
<p className="text-muted-foreground mt-2">
|
|
13210
|
-
Replace this placeholder with your dashboard.
|
|
13211
|
-
</p>
|
|
13212
|
-
</div>
|
|
13213
|
-
</AuthGate>
|
|
13214
|
-
);
|
|
13215
|
-
}
|
|
13216
|
-
`;
|
|
13217
|
-
}
|
|
13218
12935
|
var setupNetsuite = tool({
|
|
13219
|
-
description: "Bootstrap a NetSuite-backed React + Vite + Tailwind project in one call.
|
|
12936
|
+
description: "Bootstrap a NetSuite-backed React + Vite + Tailwind v4 + TanStack Router " + "project in one call. Writes the full HelloLeo base scaffold PLUS the " + "NetSuite OAuth wiring, all ADDITIVE -- it never overwrites a scaffold " + "file. Adds src/lib/netsuite.ts (OAuth client), src/lib/auth.tsx " + "(NetSuiteAuthProvider + useNetSuiteAuth), and " + "src/components/auth/{AuthLayout,LoginPage,SignInWithNetSuite}.tsx. " + "Use this INSTEAD of `scaffold` for any project that needs NetSuite. " + "After it runs there is exactly ONE manual step: wrap the root route's " + "<Outlet /> with <AuthLayout> in src/routes/__root.tsx (the tool's output " + "shows the snippet). Real OAuth is wired from day one -- never replace " + "<AuthLayout> with a password placeholder.",
|
|
13220
12937
|
args: {},
|
|
13221
12938
|
async execute(_args, context) {
|
|
13222
12939
|
const dir = context.directory;
|
|
13223
12940
|
const base = await writeBaseScaffold({}, dir);
|
|
13224
|
-
await mkdir2(
|
|
12941
|
+
await mkdir2(path4.join(dir, "src", "components", "auth"), {
|
|
13225
12942
|
recursive: true
|
|
13226
12943
|
});
|
|
13227
12944
|
const nsFiles = [
|
|
13228
|
-
[
|
|
12945
|
+
[path4.join("src", "lib", "netsuite.ts"), netsuiteTs()],
|
|
12946
|
+
[path4.join("src", "lib", "auth.tsx"), authTsx()],
|
|
13229
12947
|
[
|
|
13230
|
-
|
|
13231
|
-
|
|
12948
|
+
path4.join("src", "components", "auth", "AuthLayout.tsx"),
|
|
12949
|
+
authLayoutTsx()
|
|
13232
12950
|
],
|
|
13233
12951
|
[
|
|
13234
|
-
|
|
12952
|
+
path4.join("src", "components", "auth", "LoginPage.tsx"),
|
|
13235
12953
|
netsuiteLoginPageTsx()
|
|
13236
12954
|
],
|
|
13237
12955
|
[
|
|
13238
|
-
|
|
13239
|
-
|
|
13240
|
-
]
|
|
13241
|
-
[path3.join("src", "App.tsx"), netsuiteAppTsx()]
|
|
12956
|
+
path4.join("src", "components", "auth", "SignInWithNetSuite.tsx"),
|
|
12957
|
+
signInButtonTsx()
|
|
12958
|
+
]
|
|
13242
12959
|
];
|
|
13243
|
-
const nsCreated = [];
|
|
13244
12960
|
for (const [filePath, content] of nsFiles) {
|
|
13245
|
-
await Bun.write(
|
|
13246
|
-
nsCreated.push(filePath);
|
|
12961
|
+
await Bun.write(path4.join(dir, filePath), content);
|
|
13247
12962
|
}
|
|
13248
|
-
const all = [...base, ...
|
|
12963
|
+
const all = [...base, ...nsFiles.map(([f]) => f)];
|
|
13249
12964
|
return [
|
|
13250
|
-
`
|
|
13251
|
-
|
|
12965
|
+
`Generated ${all.length} files (base scaffold + NetSuite OAuth, all additive -- nothing overwritten).`,
|
|
12966
|
+
"",
|
|
12967
|
+
"All you need to do is wrap the root layout with the auth layout.",
|
|
12968
|
+
"In src/routes/__root.tsx, import AuthLayout and wrap <Outlet />:",
|
|
12969
|
+
"",
|
|
12970
|
+
" import { AuthLayout } from '../components/auth/AuthLayout'",
|
|
12971
|
+
"",
|
|
12972
|
+
" function RootComponent() {",
|
|
12973
|
+
" return (",
|
|
12974
|
+
" <>",
|
|
12975
|
+
" <AuthLayout>",
|
|
12976
|
+
" <Outlet />",
|
|
12977
|
+
" </AuthLayout>",
|
|
12978
|
+
" {/* keep the existing <TanStackDevtools ... /> here */}",
|
|
12979
|
+
" </>",
|
|
12980
|
+
" )",
|
|
12981
|
+
" }",
|
|
12982
|
+
"",
|
|
12983
|
+
"That single wrap gates every route: <LoginPage /> until the user signs",
|
|
12984
|
+
"in with NetSuite, your routes after. Read the session anywhere below it",
|
|
12985
|
+
"with useNetSuiteAuth() from src/lib/auth.tsx. NEVER swap <AuthLayout>",
|
|
12986
|
+
"for a password gate -- real OAuth is the only supported login.",
|
|
13252
12987
|
"",
|
|
13253
|
-
"
|
|
13254
|
-
"
|
|
13255
|
-
"
|
|
12988
|
+
"NetSuite OAuth files written:",
|
|
12989
|
+
" - src/lib/netsuite.ts OAuth client (signIn / session / workerFetch)",
|
|
12990
|
+
" - src/lib/auth.tsx NetSuiteAuthProvider + useNetSuiteAuth",
|
|
12991
|
+
" - src/components/auth/AuthLayout.tsx provider + gate (wrap root with this)",
|
|
12992
|
+
" - src/components/auth/LoginPage.tsx unauthenticated view",
|
|
12993
|
+
" - src/components/auth/SignInWithNetSuite.tsx sign-in button",
|
|
13256
12994
|
"",
|
|
13257
|
-
"
|
|
13258
|
-
" - App.tsx wraps everything in <AuthGate>. NEVER remove the gate",
|
|
13259
|
-
" or build a password placeholder -- real OAuth is the only login.",
|
|
13260
|
-
" - LoginPage.tsx + SignInWithNetSuite.tsx are the unauthenticated",
|
|
13261
|
-
" view. Customise the copy / imagery, keep <SignInWithNetSuite/>",
|
|
13262
|
-
" as the call-to-action.",
|
|
13263
|
-
" - signInWithNetSuite() in src/lib/netsuite.ts hits the Worker at",
|
|
13264
|
-
" /auth/netsuite/start. Deploy the Worker via cloudflare-backend",
|
|
13265
|
-
" MCP with routes:",
|
|
12995
|
+
"Worker (deploy via cloudflare-backend MCP) must serve:",
|
|
13266
12996
|
" GET /auth/netsuite/start -- redirect to NetSuite OAuth",
|
|
13267
12997
|
" GET /auth/netsuite/callback -- code -> token exchange",
|
|
13268
|
-
" GET /api/me
|
|
12998
|
+
" GET /api/me -- current session",
|
|
13269
12999
|
" POST /auth/netsuite/logout -- clear session",
|
|
13270
|
-
"
|
|
13271
|
-
"
|
|
13272
|
-
" - VITE_WORKER_URL must be set so the frontend knows where to",
|
|
13273
|
-
" send users. The HelloLeo Cloud provisioner seeds this."
|
|
13000
|
+
" Reads env.NETSUITE_ACCOUNT_ID / _CLIENT_ID / _CLIENT_SECRET.",
|
|
13001
|
+
" VITE_WORKER_URL must be set (HelloLeo Cloud provisioner seeds it)."
|
|
13274
13002
|
].join(`
|
|
13275
13003
|
`);
|
|
13276
13004
|
}
|