@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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@helloleo/plugins",
3
- "version": "0.1.10",
3
+ "version": "0.1.12",
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";
@@ -3,10 +3,10 @@ import { tool } from "@opencode-ai/plugin";
3
3
 
4
4
  const z = tool.schema;
5
5
 
6
- const ALLOWED_EXACT_HOSTS = new Set(["api.app.helloleo.dev"]);
7
- const ALLOWED_HOST_SUFFIXES = [
8
- ".augustin-d60.workers.dev",
9
- ".live.helloleo.dev",
6
+ const ALLOWED_BASE_HOSTS = [
7
+ "helloleo.dev",
8
+ "helloleo.app",
9
+ "augustin-d60.workers.dev",
10
10
  ] as const;
11
11
 
12
12
  const DEFAULT_TIMEOUT_MS = 15_000;
@@ -22,7 +22,7 @@ const argsShape = {
22
22
  .string()
23
23
  .describe(
24
24
  "Fully-qualified URL to request. Must be on the HelloLeo whitelist " +
25
- "(api.app.helloleo.dev, *.augustin-d60.workers.dev, *.live.helloleo.dev).",
25
+ "(**.helloleo.dev, **.helloleo.app, **.augustin-d60.workers.dev).",
26
26
  ),
27
27
  method: z
28
28
  .enum(METHODS)
@@ -58,10 +58,7 @@ export const inspectHttp = tool({
58
58
  "Send an HTTP request to a HelloLeo endpoint and return the status code, " +
59
59
  "response headers, and response body. Use this to inspect the live behaviour " +
60
60
  "of HelloLeo workers, the production API, or preview deployments so you can " +
61
- "self-debug instead of operating blind. " +
62
- "Only URLs on the HelloLeo whitelist are allowed: " +
63
- "api.app.helloleo.dev, *.augustin-d60.workers.dev, *.live.helloleo.dev. " +
64
- "Supports GET, POST, PUT, PATCH and DELETE (with an optional JSON body).",
61
+ "self-debug instead of operating blind. ",
65
62
  args: argsShape,
66
63
  async execute(rawArgs, context) {
67
64
  const args = argsSchema.parse(rawArgs);
@@ -180,17 +177,13 @@ function parseTargetUrl(raw: string): URL {
180
177
  }
181
178
 
182
179
  function allowedHostsHuman(): string {
183
- return [
184
- ...ALLOWED_EXACT_HOSTS,
185
- ...ALLOWED_HOST_SUFFIXES.map((s) => `*${s}`),
186
- ].join(", ");
180
+ return ALLOWED_BASE_HOSTS.map((base) => `**.${base}`).join(", ");
187
181
  }
188
182
 
189
183
  function isAllowedHost(hostname: string): boolean {
190
184
  const host = hostname.toLowerCase().replace(/\.$/, "");
191
- if (ALLOWED_EXACT_HOSTS.has(host)) return true;
192
- for (const suffix of ALLOWED_HOST_SUFFIXES) {
193
- if (host.endsWith(suffix) && host.length > suffix.length) return true;
185
+ for (const base of ALLOWED_BASE_HOSTS) {
186
+ if (host === base || host.endsWith(`.${base}`)) return true;
194
187
  }
195
188
  return false;
196
189
  }
@@ -2,13 +2,6 @@ import { mkdir } from "node:fs/promises";
2
2
  import path from "node:path";
3
3
  import type { Plugin } from "@opencode-ai/plugin";
4
4
  import { tool } from "@opencode-ai/plugin";
5
- import {
6
- netsuiteAppTsx,
7
- netsuiteAuthGateTsx,
8
- netsuiteLoginPageTsx,
9
- netsuiteTs,
10
- signInButtonTsx,
11
- } from "./scaffold-netsuite";
12
5
 
13
6
  function packageJson(name: string) {
14
7
  return JSON.stringify(
@@ -394,13 +387,60 @@ ReactDOM.createRoot(document.getElementById('root')!).render(
394
387
  )`;
395
388
  }
396
389
 
390
+ export interface BaseScaffoldOptions {
391
+ name?: string;
392
+ primaryColor?: string;
393
+ fontFamily?: string;
394
+ borderRadius?: string;
395
+ }
396
+
397
+ /**
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.
401
+ */
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;
433
+ }
434
+
397
435
  export const scaffold = tool({
398
436
  description:
399
437
  "Scaffold a new React + Vite + Tailwind CSS project with the HelloLeo design system. " +
400
438
  "Creates all mandatory boilerplate: package.json, index.html (with Google Fonts), " +
401
439
  "vite.config.js, tailwind.config.js, postcss.config.js, src/index.css (CSS variables " +
402
- "light+dark), src/lib/utils.ts (cn helper), and src/main.tsx. " +
403
- "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.",
404
444
  args: {
405
445
  name: tool.schema
406
446
  .string()
@@ -426,75 +466,10 @@ export const scaffold = tool({
426
466
  .describe(
427
467
  'Base border radius, e.g. "0.5rem", "0.75rem" (default: 0.5rem)',
428
468
  ),
429
- integrations: tool.schema
430
- .array(tool.schema.string())
431
- .optional()
432
- .describe(
433
- "Connected integration slugs for this project (live MCP list). " +
434
- 'Pass ["netsuite"] when the project uses NetSuite -- the scaffold ' +
435
- "will add src/lib/netsuite.ts (OAuth client helper), " +
436
- "src/components/auth/{SignInWithNetSuite,LoginPage,AuthGate}.tsx " +
437
- "and a src/App.tsx wrapped in <AuthGate>. OAuth is wired from " +
438
- "day one. Forbids hardcoded password gates -- the gate IS the " +
439
- "real flow against the project's Worker.",
440
- ),
441
469
  },
442
470
  async execute(args, context) {
443
- const dir = context.directory;
444
- const name = args.name ?? "leo-project";
445
- const primary = normalizePrimaryColor(
446
- args.primaryColor ?? "221.2 83.2% 53.3%",
447
- );
448
- const font = args.fontFamily ?? "Inter";
449
- const radius = args.borderRadius ?? "0.5rem";
450
- const integrations = (args.integrations ?? []).map((s) => s.toLowerCase());
451
- const hasNetsuite = integrations.includes("netsuite");
452
-
453
- await mkdir(path.join(dir, "src", "lib"), { recursive: true });
454
- if (hasNetsuite) {
455
- await mkdir(path.join(dir, "src", "components", "auth"), {
456
- recursive: true,
457
- });
458
- }
459
-
460
- const files: [string, string][] = [
461
- ["package.json", packageJson(name)],
462
- ["index.html", indexHtml(font)],
463
- ["vite.config.js", viteConfig()],
464
- ["tailwind.config.js", tailwindConfig()],
465
- ["postcss.config.js", postcssConfig()],
466
- [path.join("src", "index.css"), indexCss(primary, font, radius)],
467
- [path.join("src", "lib", "utils.ts"), utilsTs()],
468
- [path.join("src", "lib", "proxy.ts"), proxyTs()],
469
- [path.join("src", "main.tsx"), mainTsx()],
470
- ];
471
-
472
- if (hasNetsuite) {
473
- files.push(
474
- [path.join("src", "lib", "netsuite.ts"), netsuiteTs()],
475
- [
476
- path.join("src", "components", "auth", "SignInWithNetSuite.tsx"),
477
- signInButtonTsx(),
478
- ],
479
- [
480
- path.join("src", "components", "auth", "LoginPage.tsx"),
481
- netsuiteLoginPageTsx(),
482
- ],
483
- [
484
- path.join("src", "components", "auth", "AuthGate.tsx"),
485
- netsuiteAuthGateTsx(),
486
- ],
487
- [path.join("src", "App.tsx"), netsuiteAppTsx()],
488
- );
489
- }
490
-
491
- const created: string[] = [];
492
- for (const [filePath, content] of files) {
493
- await Bun.write(path.join(dir, filePath), content);
494
- created.push(filePath);
495
- }
496
-
497
- const out: string[] = [
471
+ const created = await writeBaseScaffold(args, context.directory);
472
+ return [
498
473
  `Scaffolded ${created.length} files:`,
499
474
  ...created.map((f) => ` - ${f}`),
500
475
  "",
@@ -505,34 +480,7 @@ export const scaffold = tool({
505
480
  " Header comment in that file documents slugs, response unwrapping,",
506
481
  " pagination, and frontend-only vs HelloLeo Cloud routing.",
507
482
  "Update package.json with any extra packages you need after scaffolding.",
508
- ];
509
-
510
- if (hasNetsuite) {
511
- out.push(
512
- "",
513
- "NetSuite project -- auth is already wired:",
514
- " - App.tsx wraps everything in <AuthGate>. Build your dashboard",
515
- " INSIDE the gate. NEVER remove the gate or build a password",
516
- " placeholder -- real OAuth is the only allowed login.",
517
- " - LoginPage.tsx + SignInWithNetSuite.tsx are the unauthenticated",
518
- " view. Customise the copy / imagery, keep <SignInWithNetSuite/>",
519
- " as the call-to-action.",
520
- " - signInWithNetSuite() in src/lib/netsuite.ts hits the Worker at",
521
- " /auth/netsuite/start. Deploy the Worker via cloudflare-backend",
522
- " MCP with routes:",
523
- " GET /auth/netsuite/start -- redirect to NetSuite OAuth",
524
- " GET /auth/netsuite/callback -- code -> token exchange",
525
- " GET /api/me -- current session",
526
- " POST /auth/netsuite/logout -- clear session",
527
- " Worker reads env.NETSUITE_ACCOUNT_ID, env.NETSUITE_CLIENT_ID,",
528
- " env.NETSUITE_CLIENT_SECRET (set by the runtime-setup wizard).",
529
- " - VITE_WORKER_URL must be set in the project env so the frontend",
530
- " knows where to send users. The HelloLeo Cloud provisioner",
531
- " seeds this automatically.",
532
- );
533
- }
534
-
535
- return out.join("\n");
483
+ ].join("\n");
536
484
  },
537
485
  });
538
486
 
@@ -1,15 +1,22 @@
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
+
1
7
  /**
2
- * NetSuite-specific scaffold templates.
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.
3
13
  *
4
- * Extracted from scaffold.ts so the OAuth bits (client helper, sign-in
5
- * button, login page, auth gate, gated App.tsx entry point) live in one
6
- * file. Imported and emitted by scaffold.ts when the caller passes
7
- * `integrations: ["netsuite"]`. The string contents are byte-for-byte
8
- * identical to the previous inline versions — splitting is purely
9
- * organisational and does NOT change scaffold output.
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.
10
17
  */
11
18
 
12
- export function netsuiteTs(): string {
19
+ function netsuiteTs(): string {
13
20
  return `/*
14
21
  * NetSuite OAuth client helper (bearer-token-in-fragment).
15
22
  *
@@ -141,7 +148,7 @@ export async function workerFetch(pathOrUrl: string, init: RequestInit = {}): Pr
141
148
  `;
142
149
  }
143
150
 
144
- export function signInButtonTsx(): string {
151
+ function signInButtonTsx(): string {
145
152
  return `import { signInWithNetSuite } from '../../lib/netsuite';
146
153
 
147
154
  export function SignInWithNetSuite() {
@@ -157,7 +164,7 @@ export function SignInWithNetSuite() {
157
164
  `;
158
165
  }
159
166
 
160
- export function netsuiteLoginPageTsx(): string {
167
+ function netsuiteLoginPageTsx(): string {
161
168
  return `import { SignInWithNetSuite } from './SignInWithNetSuite';
162
169
 
163
170
  /**
@@ -202,7 +209,7 @@ export function LoginPage() {
202
209
  `;
203
210
  }
204
211
 
205
- export function netsuiteAuthGateTsx(): string {
212
+ function netsuiteAuthGateTsx(): string {
206
213
  return `import { useEffect, useState } from 'react';
207
214
  import { LoginPage } from './LoginPage';
208
215
  import { getNetSuiteSession, type NetSuiteSession } from '../../lib/netsuite';
@@ -244,7 +251,7 @@ export function AuthGate({ children }: { children: React.ReactNode }) {
244
251
  `;
245
252
  }
246
253
 
247
- export function netsuiteAppTsx(): string {
254
+ function netsuiteAppTsx(): string {
248
255
  return `import { AuthGate } from './components/auth/AuthGate';
249
256
 
250
257
  /**
@@ -266,3 +273,81 @@ export default function App() {
266
273
  }
267
274
  `;
268
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
+ };
@@ -1,22 +0,0 @@
1
- type Success<T> = {
2
- data: T;
3
- error: null;
4
- };
5
-
6
- type Failure<E> = {
7
- data: null;
8
- error: E;
9
- };
10
-
11
- type Result<T, E = Error> = Success<T> | Failure<E>;
12
-
13
- export async function tryCatch<T, E = Error>(
14
- promise: Promise<T>,
15
- ): Promise<Result<T, E>> {
16
- try {
17
- const data = await promise;
18
- return { data, error: null };
19
- } catch (error) {
20
- return { data: null, error: error as E };
21
- }
22
- }