@helloleo/plugins 0.1.9 → 0.1.11

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
@@ -12908,6 +12908,63 @@ ReactDOM.createRoot(document.getElementById('root')!).render(
12908
12908
  </React.StrictMode>,
12909
12909
  )`;
12910
12910
  }
12911
+ async function writeBaseScaffold(opts, dir) {
12912
+ const name = opts.name ?? "leo-project";
12913
+ const primary = normalizePrimaryColor(opts.primaryColor ?? "221.2 83.2% 53.3%");
12914
+ const font = opts.fontFamily ?? "Inter";
12915
+ const radius = opts.borderRadius ?? "0.5rem";
12916
+ await mkdir(path2.join(dir, "src", "lib"), { recursive: true });
12917
+ const files = [
12918
+ ["package.json", packageJson(name)],
12919
+ ["index.html", indexHtml(font)],
12920
+ ["vite.config.js", viteConfig()],
12921
+ ["tailwind.config.js", tailwindConfig()],
12922
+ ["postcss.config.js", postcssConfig()],
12923
+ [path2.join("src", "index.css"), indexCss(primary, font, radius)],
12924
+ [path2.join("src", "lib", "utils.ts"), utilsTs()],
12925
+ [path2.join("src", "lib", "proxy.ts"), proxyTs()],
12926
+ [path2.join("src", "main.tsx"), mainTsx()]
12927
+ ];
12928
+ const created = [];
12929
+ for (const [filePath, content] of files) {
12930
+ await Bun.write(path2.join(dir, filePath), content);
12931
+ created.push(filePath);
12932
+ }
12933
+ return created;
12934
+ }
12935
+ var scaffold = tool({
12936
+ 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.",
12937
+ args: {
12938
+ name: tool.schema.string().optional().describe("Project name for package.json (default: leo-project)"),
12939
+ 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."),
12940
+ fontFamily: tool.schema.string().optional().describe('Google Font family name, e.g. "Inter", "Poppins" (default: Inter)'),
12941
+ borderRadius: tool.schema.string().optional().describe('Base border radius, e.g. "0.5rem", "0.75rem" (default: 0.5rem)')
12942
+ },
12943
+ async execute(args, context) {
12944
+ const created = await writeBaseScaffold(args, context.directory);
12945
+ return [
12946
+ `Scaffolded ${created.length} files:`,
12947
+ ...created.map((f) => ` - ${f}`),
12948
+ "",
12949
+ "Next: create src/App.tsx with your application code.",
12950
+ "Design system reference: read the header comment in src/index.css",
12951
+ " before adding tokens or styling primitives.",
12952
+ "Integration calls: use callIntegration() from src/lib/proxy.ts.",
12953
+ " Header comment in that file documents slugs, response unwrapping,",
12954
+ " pagination, and frontend-only vs HelloLeo Cloud routing.",
12955
+ "Update package.json with any extra packages you need after scaffolding."
12956
+ ].join(`
12957
+ `);
12958
+ }
12959
+ });
12960
+ var ScaffoldPlugin = async () => {
12961
+ return {
12962
+ tool: { scaffold }
12963
+ };
12964
+ };
12965
+ // src/tools/setup-netsuite.ts
12966
+ import { mkdir as mkdir2 } from "fs/promises";
12967
+ import path3 from "path";
12911
12968
  function netsuiteTs() {
12912
12969
  return `/*
12913
12970
  * NetSuite OAuth client helper (bearer-token-in-fragment).
@@ -12969,6 +13026,22 @@ export function signInWithNetSuite(returnTo: string = window.location.href) {
12969
13026
  }
12970
13027
  const url = new URL(\`\${WORKER_URL.replace(/\\/$/, '')}/auth/netsuite/start\`);
12971
13028
  url.searchParams.set('return_to', returnTo);
13029
+ // The app usually renders inside the HelloLeo preview iframe. NetSuite's
13030
+ // OAuth page sets X-Frame-Options: DENY, so navigating the iframe to the
13031
+ // Worker (which 302s to NetSuite) ends up loading a page the browser
13032
+ // refuses to render -- looks like "nothing happens" to the user. Navigate
13033
+ // the top frame instead so the OAuth flow runs at the top level and the
13034
+ // callback redirect lands on the same top frame, which then reads the
13035
+ // #nssid= fragment. Fallback to same-frame navigation if the top window
13036
+ // is cross-origin and can't be reassigned.
13037
+ try {
13038
+ if (window.top && window.top !== window) {
13039
+ window.top.location.assign(url.toString());
13040
+ return;
13041
+ }
13042
+ } catch {
13043
+ /* top window not accessible \u2014 fall through to same-frame navigation */
13044
+ }
12972
13045
  window.location.assign(url.toString());
12973
13046
  }
12974
13047
 
@@ -13099,7 +13172,7 @@ import { getNetSuiteSession, type NetSuiteSession } from '../../lib/netsuite';
13099
13172
  * export default function App() {
13100
13173
  * return (
13101
13174
  * <AuthGate>
13102
- * <Dashboard /> {/* your authenticated content */}
13175
+ * <Dashboard /> // your authenticated content
13103
13176
  * </AuthGate>
13104
13177
  * );
13105
13178
  * }
@@ -13145,82 +13218,73 @@ export default function App() {
13145
13218
  }
13146
13219
  `;
13147
13220
  }
13148
- var scaffold = tool({
13149
- 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.",
13150
- args: {
13151
- name: tool.schema.string().optional().describe("Project name for package.json (default: leo-project)"),
13152
- 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."),
13153
- fontFamily: tool.schema.string().optional().describe('Google Font family name, e.g. "Inter", "Poppins" (default: Inter)'),
13154
- borderRadius: tool.schema.string().optional().describe('Base border radius, e.g. "0.5rem", "0.75rem" (default: 0.5rem)'),
13155
- 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.")
13156
- },
13157
- async execute(args, context) {
13221
+ var setupNetsuite = tool({
13222
+ 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.",
13223
+ args: {},
13224
+ async execute(_args, context) {
13158
13225
  const dir = context.directory;
13159
- const name = args.name ?? "leo-project";
13160
- const primary = normalizePrimaryColor(args.primaryColor ?? "221.2 83.2% 53.3%");
13161
- const font = args.fontFamily ?? "Inter";
13162
- const radius = args.borderRadius ?? "0.5rem";
13163
- const integrations = (args.integrations ?? []).map((s) => s.toLowerCase());
13164
- const hasNetsuite = integrations.includes("netsuite");
13165
- await mkdir(path2.join(dir, "src", "lib"), { recursive: true });
13166
- if (hasNetsuite) {
13167
- await mkdir(path2.join(dir, "src", "components", "auth"), {
13168
- recursive: true
13169
- });
13170
- }
13171
- const files = [
13172
- ["package.json", packageJson(name)],
13173
- ["index.html", indexHtml(font)],
13174
- ["vite.config.js", viteConfig()],
13175
- ["tailwind.config.js", tailwindConfig()],
13176
- ["postcss.config.js", postcssConfig()],
13177
- [path2.join("src", "index.css"), indexCss(primary, font, radius)],
13178
- [path2.join("src", "lib", "utils.ts"), utilsTs()],
13179
- [path2.join("src", "lib", "proxy.ts"), proxyTs()],
13180
- [path2.join("src", "main.tsx"), mainTsx()]
13181
- ];
13182
- if (hasNetsuite) {
13183
- files.push([path2.join("src", "lib", "netsuite.ts"), netsuiteTs()], [
13184
- path2.join("src", "components", "auth", "SignInWithNetSuite.tsx"),
13226
+ const base = await writeBaseScaffold({}, dir);
13227
+ await mkdir2(path3.join(dir, "src", "components", "auth"), {
13228
+ recursive: true
13229
+ });
13230
+ const nsFiles = [
13231
+ [path3.join("src", "lib", "netsuite.ts"), netsuiteTs()],
13232
+ [
13233
+ path3.join("src", "components", "auth", "SignInWithNetSuite.tsx"),
13185
13234
  signInButtonTsx()
13186
- ], [
13187
- path2.join("src", "components", "auth", "LoginPage.tsx"),
13235
+ ],
13236
+ [
13237
+ path3.join("src", "components", "auth", "LoginPage.tsx"),
13188
13238
  netsuiteLoginPageTsx()
13189
- ], [
13190
- path2.join("src", "components", "auth", "AuthGate.tsx"),
13239
+ ],
13240
+ [
13241
+ path3.join("src", "components", "auth", "AuthGate.tsx"),
13191
13242
  netsuiteAuthGateTsx()
13192
- ], [path2.join("src", "App.tsx"), netsuiteAppTsx()]);
13193
- }
13194
- const created = [];
13195
- for (const [filePath, content] of files) {
13196
- await Bun.write(path2.join(dir, filePath), content);
13197
- created.push(filePath);
13198
- }
13199
- const out = [
13200
- `Scaffolded ${created.length} files:`,
13201
- ...created.map((f) => ` - ${f}`),
13243
+ ],
13244
+ [path3.join("src", "App.tsx"), netsuiteAppTsx()]
13245
+ ];
13246
+ const nsCreated = [];
13247
+ for (const [filePath, content] of nsFiles) {
13248
+ await Bun.write(path3.join(dir, filePath), content);
13249
+ nsCreated.push(filePath);
13250
+ }
13251
+ const all = [...base, ...nsCreated];
13252
+ return [
13253
+ `Scaffolded ${all.length} files (base + NetSuite OAuth):`,
13254
+ ...all.map((f) => ` - ${f}`),
13202
13255
  "",
13203
- "Next: create src/App.tsx with your application code.",
13256
+ "Next: build your dashboard INSIDE <AuthGate> in src/App.tsx.",
13204
13257
  "Design system reference: read the header comment in src/index.css",
13205
13258
  " before adding tokens or styling primitives.",
13206
- "Integration calls: use callIntegration() from src/lib/proxy.ts.",
13207
- " Header comment in that file documents slugs, response unwrapping,",
13208
- " pagination, and frontend-only vs HelloLeo Cloud routing.",
13209
- "Update package.json with any extra packages you need after scaffolding."
13210
- ];
13211
- if (hasNetsuite) {
13212
- 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.");
13213
- }
13214
- return out.join(`
13259
+ "",
13260
+ "NetSuite auth -- already wired:",
13261
+ " - App.tsx wraps everything in <AuthGate>. NEVER remove the gate",
13262
+ " or build a password placeholder -- real OAuth is the only login.",
13263
+ " - LoginPage.tsx + SignInWithNetSuite.tsx are the unauthenticated",
13264
+ " view. Customise the copy / imagery, keep <SignInWithNetSuite/>",
13265
+ " as the call-to-action.",
13266
+ " - signInWithNetSuite() in src/lib/netsuite.ts hits the Worker at",
13267
+ " /auth/netsuite/start. Deploy the Worker via cloudflare-backend",
13268
+ " MCP with routes:",
13269
+ " GET /auth/netsuite/start -- redirect to NetSuite OAuth",
13270
+ " GET /auth/netsuite/callback -- code -> token exchange",
13271
+ " GET /api/me -- current session",
13272
+ " POST /auth/netsuite/logout -- clear session",
13273
+ " Worker reads env.NETSUITE_ACCOUNT_ID, env.NETSUITE_CLIENT_ID,",
13274
+ " env.NETSUITE_CLIENT_SECRET (set by the runtime-setup wizard).",
13275
+ " - VITE_WORKER_URL must be set so the frontend knows where to",
13276
+ " send users. The HelloLeo Cloud provisioner seeds this."
13277
+ ].join(`
13215
13278
  `);
13216
13279
  }
13217
13280
  });
13218
- var ScaffoldPlugin = async () => {
13281
+ var SetupNetsuitePlugin = async () => {
13219
13282
  return {
13220
- tool: { scaffold }
13283
+ tool: { setup_netsuite: setupNetsuite }
13221
13284
  };
13222
13285
  };
13223
13286
  export {
13287
+ SetupNetsuitePlugin,
13224
13288
  ScaffoldPlugin,
13225
13289
  RemovePlugin,
13226
13290
  InspectHttpPlugin
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@helloleo/plugins",
3
- "version": "0.1.9",
3
+ "version": "0.1.11",
4
4
  "description": "OpenCode plugins for HelloLeo",
5
5
  "license": "UNLICENSED",
6
6
  "type": "module",
package/src/index.ts CHANGED
@@ -1,6 +1,7 @@
1
1
  export { InspectHttpPlugin } from "./tools/inspect-http.ts";
2
2
  export { RemovePlugin } from "./tools/remove.ts";
3
3
  export { ScaffoldPlugin } from "./tools/scaffold.ts";
4
+ export { SetupNetsuitePlugin } from "./tools/setup-netsuite.ts";
4
5
  // export { CreatePlanPlugin } from "./tools/create-plan.ts";
5
6
  // export { ListPlansPlugin } from "./tools/list-plans.ts";
6
7
  // export { ModifyPlanPlugin } from "./tools/modify-plan.ts";
@@ -387,246 +387,49 @@ ReactDOM.createRoot(document.getElementById('root')!).render(
387
387
  )`;
388
388
  }
389
389
 
390
- function netsuiteTs() {
391
- return `/*
392
- * NetSuite OAuth client helper (bearer-token-in-fragment).
393
- *
394
- * Two NetSuite Integration Records back this:
395
- * 1. MCP (scope: mcp) -- used by HelloLeo at build time.
396
- * 2. Runtime (scope: rest_webservices) -- used here at sign-in time.
397
- * Runtime credentials live as HelloLeo Cloud secrets on the project's Worker
398
- * (env.NETSUITE_ACCOUNT_ID / _CLIENT_ID / _CLIENT_SECRET). They never reach
399
- * the browser. Cookies are NOT used -- frontend ↔ Worker are cross-site
400
- * (e.g. *.helloleo.dev <-> *.workers.dev) and Chrome partitions / drops
401
- * third-party cookies in the HelloLeo preview iframe.
402
- *
403
- * Flow:
404
- * 1. User clicks "Sign in with NetSuite".
405
- * 2. Browser navigates to the Worker /auth/netsuite/start.
406
- * 3. Worker redirects to NetSuite's OAuth UI with PKCE.
407
- * 4. NetSuite redirects back to the Worker /auth/netsuite/callback.
408
- * 5. Worker exchanges the code, mints a session id, then redirects to
409
- * <return_to>#nssid=<sid>. URL fragments stay in the browser only.
410
- * 6. This module reads the fragment on load, stashes the sid in
411
- * sessionStorage, and sends it as Authorization: Bearer on every
412
- * Worker call.
413
- *
414
- * NEVER use a hardcoded password gate. NEVER use sessionStorage as the
415
- * authoritative auth state -- the only acceptable check is /api/me on the
416
- * Worker. sessionStorage here is just a cache of the bearer token.
417
- */
418
-
419
- export const WORKER_URL = import.meta.env.VITE_WORKER_URL ?? '';
420
- const TOKEN_KEY = 'nssid';
421
-
422
- // Capture-and-strip the #nssid=... fragment as early as possible so URLs
423
- // shown to the user stay clean. Runs once at module load.
424
- (function bootstrapTokenFromHash() {
425
- if (typeof window === 'undefined') return;
426
- const hash = window.location.hash.replace(/^#/, '');
427
- if (!hash) return;
428
- const params = new URLSearchParams(hash);
429
- const sid = params.get(TOKEN_KEY);
430
- if (sid) {
431
- try { sessionStorage.setItem(TOKEN_KEY, sid); } catch {}
432
- params.delete(TOKEN_KEY);
433
- const remaining = params.toString();
434
- const newHash = remaining ? '#' + remaining : '';
435
- window.history.replaceState(null, '', window.location.pathname + window.location.search + newHash);
436
- }
437
- })();
438
-
439
- export function getSessionToken(): string | null {
440
- try { return sessionStorage.getItem(TOKEN_KEY); } catch { return null; }
441
- }
442
-
443
- export function signInWithNetSuite(returnTo: string = window.location.href) {
444
- if (!WORKER_URL) {
445
- throw new Error(
446
- 'VITE_WORKER_URL is not set. HelloLeo Cloud must be provisioned before sign-in works.',
447
- );
448
- }
449
- const url = new URL(\`\${WORKER_URL.replace(/\\/$/, '')}/auth/netsuite/start\`);
450
- url.searchParams.set('return_to', returnTo);
451
- window.location.assign(url.toString());
452
- }
453
-
454
- export interface NetSuiteSession {
455
- authenticated: boolean;
456
- account_id?: string;
457
- user_id?: string;
458
- email?: string;
459
- }
460
-
461
- export async function getNetSuiteSession(): Promise<NetSuiteSession> {
462
- if (!WORKER_URL) return { authenticated: false };
463
- const sid = getSessionToken();
464
- if (!sid) return { authenticated: false };
465
- const res = await fetch(\`\${WORKER_URL.replace(/\\/$/, '')}/api/me\`, {
466
- headers: { Authorization: 'Bearer ' + sid },
467
- });
468
- if (!res.ok) return { authenticated: false };
469
- const data = await res.json();
470
- return { authenticated: Boolean(data?.account_id), ...data };
471
- }
472
-
473
- export async function signOutNetSuite(): Promise<void> {
474
- if (!WORKER_URL) return;
475
- const sid = getSessionToken();
476
- if (sid) {
477
- try {
478
- await fetch(\`\${WORKER_URL.replace(/\\/$/, '')}/auth/netsuite/logout\`, {
479
- method: 'POST',
480
- headers: { Authorization: 'Bearer ' + sid },
481
- });
482
- } catch {}
483
- }
484
- try { sessionStorage.removeItem(TOKEN_KEY); } catch {}
485
- window.location.reload();
486
- }
487
-
488
- /**
489
- * Helper for ALL other Worker calls (your dashboard endpoints, etc.).
490
- * Always go through this instead of bare fetch -- it injects the bearer
491
- * token and routes through the Worker so NetSuite tokens stay server-side.
492
- */
493
- export async function workerFetch(pathOrUrl: string, init: RequestInit = {}): Promise<Response> {
494
- if (!WORKER_URL) throw new Error('VITE_WORKER_URL is not set');
495
- const url = pathOrUrl.startsWith('http')
496
- ? pathOrUrl
497
- : \`\${WORKER_URL.replace(/\\/$/, '')}\${pathOrUrl.startsWith('/') ? '' : '/'}\${pathOrUrl}\`;
498
- const sid = getSessionToken();
499
- const headers = new Headers(init.headers ?? {});
500
- if (sid) headers.set('Authorization', 'Bearer ' + sid);
501
- return fetch(url, { ...init, headers });
502
- }
503
- `;
504
- }
505
-
506
- function signInButtonTsx() {
507
- return `import { signInWithNetSuite } from '../../lib/netsuite';
508
-
509
- export function SignInWithNetSuite() {
510
- return (
511
- <button
512
- onClick={() => signInWithNetSuite()}
513
- className="inline-flex items-center justify-center gap-2 rounded-md bg-primary px-4 py-2 text-sm font-medium text-primary-foreground shadow hover:bg-primary/90 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring"
514
- >
515
- Sign in with NetSuite
516
- </button>
517
- );
390
+ export interface BaseScaffoldOptions {
391
+ name?: string;
392
+ primaryColor?: string;
393
+ fontFamily?: string;
394
+ borderRadius?: string;
518
395
  }
519
- `;
520
- }
521
-
522
- function netsuiteLoginPageTsx() {
523
- return `import { SignInWithNetSuite } from './SignInWithNetSuite';
524
396
 
525
397
  /**
526
- * Default unauthenticated landing page. Replaces any password gate.
527
- * Customise copy / imagery freely but keep <SignInWithNetSuite /> as the
528
- * only call-to-action -- it points to the Worker's /auth/netsuite/start.
398
+ * Write the 9 base scaffold files (no integrations). Shared between
399
+ * `scaffold` (frontend-only) and `setup_netsuite` (base + OAuth files).
400
+ * Returns the list of relative file paths written.
529
401
  */
530
- export function LoginPage() {
531
- return (
532
- <div className="min-h-screen bg-background text-foreground flex">
533
- <div className="hidden lg:flex lg:w-1/2 bg-primary text-primary-foreground p-12 flex-col justify-between">
534
- <div className="text-xl font-semibold">NetSuite App</div>
535
- <div>
536
- <h1 className="text-4xl font-bold leading-tight">
537
- Connect your NetSuite account
538
- </h1>
539
- <p className="mt-4 text-primary-foreground/80 max-w-md">
540
- Sign in with your NetSuite credentials to access your data securely.
541
- Tokens stay server-side on HelloLeo Cloud -- they never reach the
542
- browser.
543
- </p>
544
- </div>
545
- <div className="text-xs text-primary-foreground/60">
546
- Powered by HelloLeo
547
- </div>
548
- </div>
549
- <div className="flex-1 flex items-center justify-center px-6 py-12">
550
- <div className="w-full max-w-sm space-y-6">
551
- <div>
552
- <h2 className="text-2xl font-semibold">Welcome</h2>
553
- <p className="mt-2 text-sm text-muted-foreground">
554
- Use the button below to sign in. You'll be redirected to NetSuite
555
- to authorise access, then back here.
556
- </p>
557
- </div>
558
- <SignInWithNetSuite />
559
- </div>
560
- </div>
561
- </div>
562
- );
563
- }
564
- `;
565
- }
566
-
567
- function netsuiteAuthGateTsx() {
568
- return `import { useEffect, useState } from 'react';
569
- import { LoginPage } from './LoginPage';
570
- import { getNetSuiteSession, type NetSuiteSession } from '../../lib/netsuite';
571
-
572
- /**
573
- * Mandatory auth gate for NetSuite projects. Wrap your real App content with
574
- * <AuthGate>...</AuthGate>. Never render NetSuite data without this gate -- it
575
- * is the ONLY supported authentication for a NetSuite project (no password
576
- * gates, no sessionStorage flags, no shared credential).
577
- *
578
- * Pattern in src/App.tsx:
579
- *
580
- * import { AuthGate } from './components/auth/AuthGate';
581
- * export default function App() {
582
- * return (
583
- * <AuthGate>
584
- * <Dashboard /> {/* your authenticated content */}
585
- * </AuthGate>
586
- * );
587
- * }
588
- */
589
- export function AuthGate({ children }: { children: React.ReactNode }) {
590
- const [session, setSession] = useState<NetSuiteSession | null>(null);
591
-
592
- useEffect(() => { getNetSuiteSession().then(setSession); }, []);
593
-
594
- if (session === null) {
595
- return (
596
- <div className="min-h-screen flex items-center justify-center bg-background">
597
- <div className="h-8 w-8 rounded-full border-2 border-primary border-t-transparent animate-spin" />
598
- </div>
599
- );
600
- }
601
-
602
- if (!session.authenticated) return <LoginPage />;
603
-
604
- return <>{children}</>;
605
- }
606
- `;
607
- }
608
-
609
- function netsuiteAppTsx() {
610
- return `import { AuthGate } from './components/auth/AuthGate';
611
-
612
- /**
613
- * NetSuite project entry point. KEEP <AuthGate> as the outermost element --
614
- * removing it disables real OAuth and exposes NetSuite data to anonymous
615
- * users. Build your dashboard / pages inside the gate.
616
- */
617
- export default function App() {
618
- return (
619
- <AuthGate>
620
- <div className="min-h-screen bg-background text-foreground p-8">
621
- <h1 className="text-2xl font-bold">You are signed in to NetSuite.</h1>
622
- <p className="text-muted-foreground mt-2">
623
- Replace this placeholder with your dashboard.
624
- </p>
625
- </div>
626
- </AuthGate>
627
- );
628
- }
629
- `;
402
+ export async function writeBaseScaffold(
403
+ opts: BaseScaffoldOptions,
404
+ dir: string,
405
+ ): Promise<string[]> {
406
+ const name = opts.name ?? "leo-project";
407
+ const primary = normalizePrimaryColor(
408
+ opts.primaryColor ?? "221.2 83.2% 53.3%",
409
+ );
410
+ const font = opts.fontFamily ?? "Inter";
411
+ const radius = opts.borderRadius ?? "0.5rem";
412
+
413
+ await mkdir(path.join(dir, "src", "lib"), { recursive: true });
414
+
415
+ const files: [string, string][] = [
416
+ ["package.json", packageJson(name)],
417
+ ["index.html", indexHtml(font)],
418
+ ["vite.config.js", viteConfig()],
419
+ ["tailwind.config.js", tailwindConfig()],
420
+ ["postcss.config.js", postcssConfig()],
421
+ [path.join("src", "index.css"), indexCss(primary, font, radius)],
422
+ [path.join("src", "lib", "utils.ts"), utilsTs()],
423
+ [path.join("src", "lib", "proxy.ts"), proxyTs()],
424
+ [path.join("src", "main.tsx"), mainTsx()],
425
+ ];
426
+
427
+ const created: string[] = [];
428
+ for (const [filePath, content] of files) {
429
+ await Bun.write(path.join(dir, filePath), content);
430
+ created.push(filePath);
431
+ }
432
+ return created;
630
433
  }
631
434
 
632
435
  export const scaffold = tool({
@@ -634,8 +437,10 @@ export const scaffold = tool({
634
437
  "Scaffold a new React + Vite + Tailwind CSS project with the HelloLeo design system. " +
635
438
  "Creates all mandatory boilerplate: package.json, index.html (with Google Fonts), " +
636
439
  "vite.config.js, tailwind.config.js, postcss.config.js, src/index.css (CSS variables " +
637
- "light+dark), src/lib/utils.ts (cn helper), and src/main.tsx. " +
638
- "Call this FIRST for any new project before writing App.tsx and components.",
440
+ "light+dark), src/lib/utils.ts (cn helper), src/lib/proxy.ts, and src/main.tsx. " +
441
+ "Call this FIRST for any new project before writing App.tsx and components. " +
442
+ "For NetSuite projects (NetSuite MCP in the live list), call `setup_netsuite` " +
443
+ "INSTEAD -- it bundles base scaffold + OAuth wiring in one call. Do not call both.",
639
444
  args: {
640
445
  name: tool.schema
641
446
  .string()
@@ -661,75 +466,10 @@ export const scaffold = tool({
661
466
  .describe(
662
467
  'Base border radius, e.g. "0.5rem", "0.75rem" (default: 0.5rem)',
663
468
  ),
664
- integrations: tool.schema
665
- .array(tool.schema.string())
666
- .optional()
667
- .describe(
668
- "Connected integration slugs for this project (live MCP list). " +
669
- 'Pass ["netsuite"] when the project uses NetSuite -- the scaffold ' +
670
- "will add src/lib/netsuite.ts (OAuth client helper), " +
671
- "src/components/auth/{SignInWithNetSuite,LoginPage,AuthGate}.tsx " +
672
- "and a src/App.tsx wrapped in <AuthGate>. OAuth is wired from " +
673
- "day one. Forbids hardcoded password gates -- the gate IS the " +
674
- "real flow against the project's Worker.",
675
- ),
676
469
  },
677
470
  async execute(args, context) {
678
- const dir = context.directory;
679
- const name = args.name ?? "leo-project";
680
- const primary = normalizePrimaryColor(
681
- args.primaryColor ?? "221.2 83.2% 53.3%",
682
- );
683
- const font = args.fontFamily ?? "Inter";
684
- const radius = args.borderRadius ?? "0.5rem";
685
- const integrations = (args.integrations ?? []).map((s) => s.toLowerCase());
686
- const hasNetsuite = integrations.includes("netsuite");
687
-
688
- await mkdir(path.join(dir, "src", "lib"), { recursive: true });
689
- if (hasNetsuite) {
690
- await mkdir(path.join(dir, "src", "components", "auth"), {
691
- recursive: true,
692
- });
693
- }
694
-
695
- const files: [string, string][] = [
696
- ["package.json", packageJson(name)],
697
- ["index.html", indexHtml(font)],
698
- ["vite.config.js", viteConfig()],
699
- ["tailwind.config.js", tailwindConfig()],
700
- ["postcss.config.js", postcssConfig()],
701
- [path.join("src", "index.css"), indexCss(primary, font, radius)],
702
- [path.join("src", "lib", "utils.ts"), utilsTs()],
703
- [path.join("src", "lib", "proxy.ts"), proxyTs()],
704
- [path.join("src", "main.tsx"), mainTsx()],
705
- ];
706
-
707
- if (hasNetsuite) {
708
- files.push(
709
- [path.join("src", "lib", "netsuite.ts"), netsuiteTs()],
710
- [
711
- path.join("src", "components", "auth", "SignInWithNetSuite.tsx"),
712
- signInButtonTsx(),
713
- ],
714
- [
715
- path.join("src", "components", "auth", "LoginPage.tsx"),
716
- netsuiteLoginPageTsx(),
717
- ],
718
- [
719
- path.join("src", "components", "auth", "AuthGate.tsx"),
720
- netsuiteAuthGateTsx(),
721
- ],
722
- [path.join("src", "App.tsx"), netsuiteAppTsx()],
723
- );
724
- }
725
-
726
- const created: string[] = [];
727
- for (const [filePath, content] of files) {
728
- await Bun.write(path.join(dir, filePath), content);
729
- created.push(filePath);
730
- }
731
-
732
- const out: string[] = [
471
+ const created = await writeBaseScaffold(args, context.directory);
472
+ return [
733
473
  `Scaffolded ${created.length} files:`,
734
474
  ...created.map((f) => ` - ${f}`),
735
475
  "",
@@ -740,34 +480,7 @@ export const scaffold = tool({
740
480
  " Header comment in that file documents slugs, response unwrapping,",
741
481
  " pagination, and frontend-only vs HelloLeo Cloud routing.",
742
482
  "Update package.json with any extra packages you need after scaffolding.",
743
- ];
744
-
745
- if (hasNetsuite) {
746
- out.push(
747
- "",
748
- "NetSuite project -- auth is already wired:",
749
- " - App.tsx wraps everything in <AuthGate>. Build your dashboard",
750
- " INSIDE the gate. NEVER remove the gate or build a password",
751
- " placeholder -- real OAuth is the only allowed login.",
752
- " - LoginPage.tsx + SignInWithNetSuite.tsx are the unauthenticated",
753
- " view. Customise the copy / imagery, keep <SignInWithNetSuite/>",
754
- " as the call-to-action.",
755
- " - signInWithNetSuite() in src/lib/netsuite.ts hits the Worker at",
756
- " /auth/netsuite/start. Deploy the Worker via cloudflare-backend",
757
- " MCP with routes:",
758
- " GET /auth/netsuite/start -- redirect to NetSuite OAuth",
759
- " GET /auth/netsuite/callback -- code -> token exchange",
760
- " GET /api/me -- current session",
761
- " POST /auth/netsuite/logout -- clear session",
762
- " Worker reads env.NETSUITE_ACCOUNT_ID, env.NETSUITE_CLIENT_ID,",
763
- " env.NETSUITE_CLIENT_SECRET (set by the runtime-setup wizard).",
764
- " - VITE_WORKER_URL must be set in the project env so the frontend",
765
- " knows where to send users. The HelloLeo Cloud provisioner",
766
- " seeds this automatically.",
767
- );
768
- }
769
-
770
- return out.join("\n");
483
+ ].join("\n");
771
484
  },
772
485
  });
773
486
 
@@ -0,0 +1,353 @@
1
+ import { mkdir } from "node:fs/promises";
2
+ import path from "node:path";
3
+ import type { Plugin } from "@opencode-ai/plugin";
4
+ import { tool } from "@opencode-ai/plugin";
5
+ import { writeBaseScaffold } from "./scaffold.ts";
6
+
7
+ /**
8
+ * One-shot bootstrap for a NetSuite project. Writes the base HelloLeo
9
+ * scaffold (via writeBaseScaffold) plus the NetSuite OAuth wiring
10
+ * (AuthGate, LoginPage, sign-in button, netsuite client, auth-gated
11
+ * App.tsx). All NetSuite-specific templates live in this file -- one
12
+ * place to read or change anything NetSuite-related.
13
+ *
14
+ * Use INSTEAD of `scaffold` when the project needs NetSuite. The tool
15
+ * name carries the intent so the LLM can't accidentally ship a project
16
+ * without the auth files.
17
+ */
18
+
19
+ function netsuiteTs(): string {
20
+ return `/*
21
+ * NetSuite OAuth client helper (bearer-token-in-fragment).
22
+ *
23
+ * Two NetSuite Integration Records back this:
24
+ * 1. MCP (scope: mcp) -- used by HelloLeo at build time.
25
+ * 2. Runtime (scope: rest_webservices) -- used here at sign-in time.
26
+ * Runtime credentials live as HelloLeo Cloud secrets on the project's Worker
27
+ * (env.NETSUITE_ACCOUNT_ID / _CLIENT_ID / _CLIENT_SECRET). They never reach
28
+ * the browser. Cookies are NOT used -- frontend ↔ Worker are cross-site
29
+ * (e.g. *.helloleo.dev <-> *.workers.dev) and Chrome partitions / drops
30
+ * third-party cookies in the HelloLeo preview iframe.
31
+ *
32
+ * Flow:
33
+ * 1. User clicks "Sign in with NetSuite".
34
+ * 2. Browser navigates to the Worker /auth/netsuite/start.
35
+ * 3. Worker redirects to NetSuite's OAuth UI with PKCE.
36
+ * 4. NetSuite redirects back to the Worker /auth/netsuite/callback.
37
+ * 5. Worker exchanges the code, mints a session id, then redirects to
38
+ * <return_to>#nssid=<sid>. URL fragments stay in the browser only.
39
+ * 6. This module reads the fragment on load, stashes the sid in
40
+ * sessionStorage, and sends it as Authorization: Bearer on every
41
+ * Worker call.
42
+ *
43
+ * NEVER use a hardcoded password gate. NEVER use sessionStorage as the
44
+ * authoritative auth state -- the only acceptable check is /api/me on the
45
+ * Worker. sessionStorage here is just a cache of the bearer token.
46
+ */
47
+
48
+ export const WORKER_URL = import.meta.env.VITE_WORKER_URL ?? '';
49
+ const TOKEN_KEY = 'nssid';
50
+
51
+ // Capture-and-strip the #nssid=... fragment as early as possible so URLs
52
+ // shown to the user stay clean. Runs once at module load.
53
+ (function bootstrapTokenFromHash() {
54
+ if (typeof window === 'undefined') return;
55
+ const hash = window.location.hash.replace(/^#/, '');
56
+ if (!hash) return;
57
+ const params = new URLSearchParams(hash);
58
+ const sid = params.get(TOKEN_KEY);
59
+ if (sid) {
60
+ try { sessionStorage.setItem(TOKEN_KEY, sid); } catch {}
61
+ params.delete(TOKEN_KEY);
62
+ const remaining = params.toString();
63
+ const newHash = remaining ? '#' + remaining : '';
64
+ window.history.replaceState(null, '', window.location.pathname + window.location.search + newHash);
65
+ }
66
+ })();
67
+
68
+ export function getSessionToken(): string | null {
69
+ try { return sessionStorage.getItem(TOKEN_KEY); } catch { return null; }
70
+ }
71
+
72
+ export function signInWithNetSuite(returnTo: string = window.location.href) {
73
+ if (!WORKER_URL) {
74
+ throw new Error(
75
+ 'VITE_WORKER_URL is not set. HelloLeo Cloud must be provisioned before sign-in works.',
76
+ );
77
+ }
78
+ const url = new URL(\`\${WORKER_URL.replace(/\\/$/, '')}/auth/netsuite/start\`);
79
+ url.searchParams.set('return_to', returnTo);
80
+ // The app usually renders inside the HelloLeo preview iframe. NetSuite's
81
+ // OAuth page sets X-Frame-Options: DENY, so navigating the iframe to the
82
+ // Worker (which 302s to NetSuite) ends up loading a page the browser
83
+ // refuses to render -- looks like "nothing happens" to the user. Navigate
84
+ // the top frame instead so the OAuth flow runs at the top level and the
85
+ // callback redirect lands on the same top frame, which then reads the
86
+ // #nssid= fragment. Fallback to same-frame navigation if the top window
87
+ // is cross-origin and can't be reassigned.
88
+ try {
89
+ if (window.top && window.top !== window) {
90
+ window.top.location.assign(url.toString());
91
+ return;
92
+ }
93
+ } catch {
94
+ /* top window not accessible — fall through to same-frame navigation */
95
+ }
96
+ window.location.assign(url.toString());
97
+ }
98
+
99
+ export interface NetSuiteSession {
100
+ authenticated: boolean;
101
+ account_id?: string;
102
+ user_id?: string;
103
+ email?: string;
104
+ }
105
+
106
+ export async function getNetSuiteSession(): Promise<NetSuiteSession> {
107
+ if (!WORKER_URL) return { authenticated: false };
108
+ const sid = getSessionToken();
109
+ if (!sid) return { authenticated: false };
110
+ const res = await fetch(\`\${WORKER_URL.replace(/\\/$/, '')}/api/me\`, {
111
+ headers: { Authorization: 'Bearer ' + sid },
112
+ });
113
+ if (!res.ok) return { authenticated: false };
114
+ const data = await res.json();
115
+ return { authenticated: Boolean(data?.account_id), ...data };
116
+ }
117
+
118
+ export async function signOutNetSuite(): Promise<void> {
119
+ if (!WORKER_URL) return;
120
+ const sid = getSessionToken();
121
+ if (sid) {
122
+ try {
123
+ await fetch(\`\${WORKER_URL.replace(/\\/$/, '')}/auth/netsuite/logout\`, {
124
+ method: 'POST',
125
+ headers: { Authorization: 'Bearer ' + sid },
126
+ });
127
+ } catch {}
128
+ }
129
+ try { sessionStorage.removeItem(TOKEN_KEY); } catch {}
130
+ window.location.reload();
131
+ }
132
+
133
+ /**
134
+ * Helper for ALL other Worker calls (your dashboard endpoints, etc.).
135
+ * Always go through this instead of bare fetch -- it injects the bearer
136
+ * token and routes through the Worker so NetSuite tokens stay server-side.
137
+ */
138
+ export async function workerFetch(pathOrUrl: string, init: RequestInit = {}): Promise<Response> {
139
+ if (!WORKER_URL) throw new Error('VITE_WORKER_URL is not set');
140
+ const url = pathOrUrl.startsWith('http')
141
+ ? pathOrUrl
142
+ : \`\${WORKER_URL.replace(/\\/$/, '')}\${pathOrUrl.startsWith('/') ? '' : '/'}\${pathOrUrl}\`;
143
+ const sid = getSessionToken();
144
+ const headers = new Headers(init.headers ?? {});
145
+ if (sid) headers.set('Authorization', 'Bearer ' + sid);
146
+ return fetch(url, { ...init, headers });
147
+ }
148
+ `;
149
+ }
150
+
151
+ function signInButtonTsx(): string {
152
+ return `import { signInWithNetSuite } from '../../lib/netsuite';
153
+
154
+ export function SignInWithNetSuite() {
155
+ return (
156
+ <button
157
+ onClick={() => signInWithNetSuite()}
158
+ className="inline-flex items-center justify-center gap-2 rounded-md bg-primary px-4 py-2 text-sm font-medium text-primary-foreground shadow hover:bg-primary/90 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring"
159
+ >
160
+ Sign in with NetSuite
161
+ </button>
162
+ );
163
+ }
164
+ `;
165
+ }
166
+
167
+ function netsuiteLoginPageTsx(): string {
168
+ return `import { SignInWithNetSuite } from './SignInWithNetSuite';
169
+
170
+ /**
171
+ * Default unauthenticated landing page. Replaces any password gate.
172
+ * Customise copy / imagery freely but keep <SignInWithNetSuite /> as the
173
+ * only call-to-action -- it points to the Worker's /auth/netsuite/start.
174
+ */
175
+ export function LoginPage() {
176
+ return (
177
+ <div className="min-h-screen bg-background text-foreground flex">
178
+ <div className="hidden lg:flex lg:w-1/2 bg-primary text-primary-foreground p-12 flex-col justify-between">
179
+ <div className="text-xl font-semibold">NetSuite App</div>
180
+ <div>
181
+ <h1 className="text-4xl font-bold leading-tight">
182
+ Connect your NetSuite account
183
+ </h1>
184
+ <p className="mt-4 text-primary-foreground/80 max-w-md">
185
+ Sign in with your NetSuite credentials to access your data securely.
186
+ Tokens stay server-side on HelloLeo Cloud -- they never reach the
187
+ browser.
188
+ </p>
189
+ </div>
190
+ <div className="text-xs text-primary-foreground/60">
191
+ Powered by HelloLeo
192
+ </div>
193
+ </div>
194
+ <div className="flex-1 flex items-center justify-center px-6 py-12">
195
+ <div className="w-full max-w-sm space-y-6">
196
+ <div>
197
+ <h2 className="text-2xl font-semibold">Welcome</h2>
198
+ <p className="mt-2 text-sm text-muted-foreground">
199
+ Use the button below to sign in. You'll be redirected to NetSuite
200
+ to authorise access, then back here.
201
+ </p>
202
+ </div>
203
+ <SignInWithNetSuite />
204
+ </div>
205
+ </div>
206
+ </div>
207
+ );
208
+ }
209
+ `;
210
+ }
211
+
212
+ function netsuiteAuthGateTsx(): string {
213
+ return `import { useEffect, useState } from 'react';
214
+ import { LoginPage } from './LoginPage';
215
+ import { getNetSuiteSession, type NetSuiteSession } from '../../lib/netsuite';
216
+
217
+ /**
218
+ * Mandatory auth gate for NetSuite projects. Wrap your real App content with
219
+ * <AuthGate>...</AuthGate>. Never render NetSuite data without this gate -- it
220
+ * is the ONLY supported authentication for a NetSuite project (no password
221
+ * gates, no sessionStorage flags, no shared credential).
222
+ *
223
+ * Pattern in src/App.tsx:
224
+ *
225
+ * import { AuthGate } from './components/auth/AuthGate';
226
+ * export default function App() {
227
+ * return (
228
+ * <AuthGate>
229
+ * <Dashboard /> // your authenticated content
230
+ * </AuthGate>
231
+ * );
232
+ * }
233
+ */
234
+ export function AuthGate({ children }: { children: React.ReactNode }) {
235
+ const [session, setSession] = useState<NetSuiteSession | null>(null);
236
+
237
+ useEffect(() => { getNetSuiteSession().then(setSession); }, []);
238
+
239
+ if (session === null) {
240
+ return (
241
+ <div className="min-h-screen flex items-center justify-center bg-background">
242
+ <div className="h-8 w-8 rounded-full border-2 border-primary border-t-transparent animate-spin" />
243
+ </div>
244
+ );
245
+ }
246
+
247
+ if (!session.authenticated) return <LoginPage />;
248
+
249
+ return <>{children}</>;
250
+ }
251
+ `;
252
+ }
253
+
254
+ function netsuiteAppTsx(): string {
255
+ return `import { AuthGate } from './components/auth/AuthGate';
256
+
257
+ /**
258
+ * NetSuite project entry point. KEEP <AuthGate> as the outermost element --
259
+ * removing it disables real OAuth and exposes NetSuite data to anonymous
260
+ * users. Build your dashboard / pages inside the gate.
261
+ */
262
+ export default function App() {
263
+ return (
264
+ <AuthGate>
265
+ <div className="min-h-screen bg-background text-foreground p-8">
266
+ <h1 className="text-2xl font-bold">You are signed in to NetSuite.</h1>
267
+ <p className="text-muted-foreground mt-2">
268
+ Replace this placeholder with your dashboard.
269
+ </p>
270
+ </div>
271
+ </AuthGate>
272
+ );
273
+ }
274
+ `;
275
+ }
276
+
277
+ export const setupNetsuite = tool({
278
+ description:
279
+ "Bootstrap a NetSuite-backed React + Vite + Tailwind project in one call. " +
280
+ "Writes the full HelloLeo base scaffold AND the NetSuite OAuth wiring " +
281
+ "(src/lib/netsuite.ts, src/components/auth/{SignInWithNetSuite,LoginPage,AuthGate}.tsx, " +
282
+ "src/App.tsx wrapped in <AuthGate>). " +
283
+ "Use this INSTEAD of `scaffold` for any project that needs NetSuite. " +
284
+ "Uses default theme (Inter font, blue primary, 0.5rem radius) -- edit " +
285
+ "src/index.css afterwards to customise. Real OAuth is wired from day " +
286
+ "one -- never replace the gate with a password placeholder.",
287
+ args: {},
288
+ async execute(_args, context) {
289
+ const dir = context.directory;
290
+ const base = await writeBaseScaffold({}, dir);
291
+
292
+ await mkdir(path.join(dir, "src", "components", "auth"), {
293
+ recursive: true,
294
+ });
295
+
296
+ const nsFiles: [string, string][] = [
297
+ [path.join("src", "lib", "netsuite.ts"), netsuiteTs()],
298
+ [
299
+ path.join("src", "components", "auth", "SignInWithNetSuite.tsx"),
300
+ signInButtonTsx(),
301
+ ],
302
+ [
303
+ path.join("src", "components", "auth", "LoginPage.tsx"),
304
+ netsuiteLoginPageTsx(),
305
+ ],
306
+ [
307
+ path.join("src", "components", "auth", "AuthGate.tsx"),
308
+ netsuiteAuthGateTsx(),
309
+ ],
310
+ [path.join("src", "App.tsx"), netsuiteAppTsx()],
311
+ ];
312
+
313
+ const nsCreated: string[] = [];
314
+ for (const [filePath, content] of nsFiles) {
315
+ await Bun.write(path.join(dir, filePath), content);
316
+ nsCreated.push(filePath);
317
+ }
318
+
319
+ const all = [...base, ...nsCreated];
320
+ return [
321
+ `Scaffolded ${all.length} files (base + NetSuite OAuth):`,
322
+ ...all.map((f) => ` - ${f}`),
323
+ "",
324
+ "Next: build your dashboard INSIDE <AuthGate> in src/App.tsx.",
325
+ "Design system reference: read the header comment in src/index.css",
326
+ " before adding tokens or styling primitives.",
327
+ "",
328
+ "NetSuite auth -- already wired:",
329
+ " - App.tsx wraps everything in <AuthGate>. NEVER remove the gate",
330
+ " or build a password placeholder -- real OAuth is the only login.",
331
+ " - LoginPage.tsx + SignInWithNetSuite.tsx are the unauthenticated",
332
+ " view. Customise the copy / imagery, keep <SignInWithNetSuite/>",
333
+ " as the call-to-action.",
334
+ " - signInWithNetSuite() in src/lib/netsuite.ts hits the Worker at",
335
+ " /auth/netsuite/start. Deploy the Worker via cloudflare-backend",
336
+ " MCP with routes:",
337
+ " GET /auth/netsuite/start -- redirect to NetSuite OAuth",
338
+ " GET /auth/netsuite/callback -- code -> token exchange",
339
+ " GET /api/me -- current session",
340
+ " POST /auth/netsuite/logout -- clear session",
341
+ " Worker reads env.NETSUITE_ACCOUNT_ID, env.NETSUITE_CLIENT_ID,",
342
+ " env.NETSUITE_CLIENT_SECRET (set by the runtime-setup wizard).",
343
+ " - VITE_WORKER_URL must be set so the frontend knows where to",
344
+ " send users. The HelloLeo Cloud provisioner seeds this.",
345
+ ].join("\n");
346
+ },
347
+ });
348
+
349
+ export const SetupNetsuitePlugin: Plugin = async () => {
350
+ return {
351
+ tool: { setup_netsuite: setupNetsuite },
352
+ };
353
+ };