@helloleo/plugins 0.1.20-beta.1 → 0.2.0-beta.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/index.js +12359 -61
- package/dist/templates/tanstack-start-2026-06-18/drizzle.config.ts +3 -8
- package/dist/templates/tanstack-start-2026-06-18/package.json +3 -3
- package/dist/templates/tanstack-start-2026-06-18/src/api/health.ts +66 -0
- package/dist/templates/tanstack-start-2026-06-18/src/routes/__root.tsx +1 -0
- package/dist/templates/tanstack-start-2026-06-18/src/server.ts +3 -2
- package/package.json +1 -1
- package/src/template-render.ts +5 -0
- package/src/templates/tanstack-start-2026-06-18/drizzle.config.ts +3 -8
- package/src/templates/tanstack-start-2026-06-18/package.json +3 -3
- package/src/templates/tanstack-start-2026-06-18/src/api/health.ts +66 -0
- package/src/templates/tanstack-start-2026-06-18/src/routes/__root.tsx +1 -0
- package/src/templates/tanstack-start-2026-06-18/src/server.ts +3 -2
- package/src/tools/scaffold.ts +38 -48
- package/src/tools/setup-netsuite.ts +28 -58
- package/dist/templates/tanstack-start-2026-06-18/wrangler.jsonc +0 -27
- package/src/templates/tanstack-start-2026-06-18/wrangler.jsonc +0 -27
|
@@ -2,14 +2,9 @@ import { defineConfig } from 'drizzle-kit'
|
|
|
2
2
|
|
|
3
3
|
// src/db/schema.ts is the single source of truth for the D1 / libsql schema.
|
|
4
4
|
//
|
|
5
|
-
//
|
|
6
|
-
//
|
|
7
|
-
|
|
8
|
-
//
|
|
9
|
-
// PROD: migrations are generated with `drizzle-kit generate` and applied to D1
|
|
10
|
-
// with `wrangler d1 migrations apply DB --remote` (reads migrations_dir in
|
|
11
|
-
// wrangler.jsonc) — so no d1-http driver or Cloudflare API env vars are needed
|
|
12
|
-
// here.
|
|
5
|
+
// In development, your database is always up-to-date. In production, we migrate
|
|
6
|
+
// automatically when you publish.
|
|
7
|
+
|
|
13
8
|
const devDbPath = process.env.DEV_DB_PATH ?? '.dev.db'
|
|
14
9
|
|
|
15
10
|
export default defineConfig({
|
|
@@ -9,10 +9,9 @@
|
|
|
9
9
|
"dev": "drizzle-kit push --force && vite dev",
|
|
10
10
|
"build": "oxlint --fix && vite build",
|
|
11
11
|
"preview": "vite preview",
|
|
12
|
-
"deploy": "bun run db:generate && vite build && wrangler deploy && wrangler d1 migrations apply DB --remote",
|
|
13
12
|
"db:generate": "drizzle-kit generate",
|
|
13
|
+
"generate-routes": "tsr generate",
|
|
14
14
|
"db:push": "drizzle-kit push --force",
|
|
15
|
-
"db:migrate": "wrangler d1 migrations apply DB --remote",
|
|
16
15
|
"typecheck": "tsc --noEmit",
|
|
17
16
|
"lint": "oxlint --fix",
|
|
18
17
|
"test": "vitest run"
|
|
@@ -75,10 +74,11 @@
|
|
|
75
74
|
"devDependencies": {
|
|
76
75
|
"@cloudflare/vite-plugin": "^1.13.0",
|
|
77
76
|
"@cloudflare/workers-types": "^4.20250906.0",
|
|
78
|
-
"@helloleo/vite-config": "^0.1.
|
|
77
|
+
"@helloleo/vite-config": "^0.1.4",
|
|
79
78
|
"@libsql/client": "^0.14.0",
|
|
80
79
|
"@tailwindcss/typography": "^0.5.16",
|
|
81
80
|
"drizzle-kit": "^0.31.10",
|
|
81
|
+
"@tanstack/router-cli": "^1.167.0",
|
|
82
82
|
"@tanstack/router-plugin": "latest",
|
|
83
83
|
"@testing-library/dom": "^10.4.1",
|
|
84
84
|
"@testing-library/react": "^16.3.0",
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
import { createFileRoute } from "@tanstack/react-router";
|
|
2
|
+
import { supabaseAdmin } from "@/integrations/supabase/client.server";
|
|
3
|
+
|
|
4
|
+
export const Route = createFileRoute("/api/health")({
|
|
5
|
+
server: {
|
|
6
|
+
handlers: {
|
|
7
|
+
GET: async () => {
|
|
8
|
+
const startedAt = Date.now();
|
|
9
|
+
|
|
10
|
+
// Server health
|
|
11
|
+
const server = {
|
|
12
|
+
status: "ok" as const,
|
|
13
|
+
uptimeSeconds: Math.round(
|
|
14
|
+
typeof process !== "undefined" &&
|
|
15
|
+
typeof process.uptime === "function"
|
|
16
|
+
? process.uptime()
|
|
17
|
+
: 0,
|
|
18
|
+
),
|
|
19
|
+
timestamp: new Date().toISOString(),
|
|
20
|
+
runtime: "cloudflare-workers",
|
|
21
|
+
};
|
|
22
|
+
|
|
23
|
+
// Database health — lightweight query against a known table
|
|
24
|
+
let database: {
|
|
25
|
+
status: "ok" | "error";
|
|
26
|
+
latencyMs: number;
|
|
27
|
+
error: string | null;
|
|
28
|
+
};
|
|
29
|
+
const dbStart = Date.now();
|
|
30
|
+
try {
|
|
31
|
+
const { error } = await supabaseAdmin
|
|
32
|
+
.from("notes")
|
|
33
|
+
.select("id", { count: "exact", head: true })
|
|
34
|
+
.limit(1);
|
|
35
|
+
if (error) throw error;
|
|
36
|
+
database = {
|
|
37
|
+
status: "ok",
|
|
38
|
+
latencyMs: Date.now() - dbStart,
|
|
39
|
+
error: null,
|
|
40
|
+
};
|
|
41
|
+
} catch (err) {
|
|
42
|
+
database = {
|
|
43
|
+
status: "error",
|
|
44
|
+
latencyMs: Date.now() - dbStart,
|
|
45
|
+
error: err instanceof Error ? err.message : String(err),
|
|
46
|
+
};
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
const overall =
|
|
50
|
+
server.status === "ok" && database.status === "ok"
|
|
51
|
+
? "ok"
|
|
52
|
+
: "degraded";
|
|
53
|
+
|
|
54
|
+
return Response.json(
|
|
55
|
+
{
|
|
56
|
+
status: overall,
|
|
57
|
+
server,
|
|
58
|
+
database,
|
|
59
|
+
checkedInMs: Date.now() - startedAt,
|
|
60
|
+
},
|
|
61
|
+
{ status: overall === "ok" ? 200 : 503 },
|
|
62
|
+
);
|
|
63
|
+
},
|
|
64
|
+
},
|
|
65
|
+
},
|
|
66
|
+
});
|
|
@@ -18,6 +18,7 @@ export const Route = createRootRouteWithContext<{ queryClient: QueryClient }>()(
|
|
|
18
18
|
{ charSet: 'utf-8' },
|
|
19
19
|
{ name: 'viewport', content: 'width=device-width, initial-scale=1.0' },
|
|
20
20
|
{ title: '{{LEO_NAME}}' },
|
|
21
|
+
{ name: 'description', content: '{{LEO_DESCRIPTION}}' },
|
|
21
22
|
],
|
|
22
23
|
links: [
|
|
23
24
|
{ rel: 'stylesheet', href: appCss },
|
|
@@ -1,5 +1,6 @@
|
|
|
1
|
-
// Cloudflare Worker entry (
|
|
2
|
-
// server entry so an SSR failure renders a clean 500
|
|
1
|
+
// Cloudflare Worker entry (the Worker `main`, configured by @helloleo/vite-config).
|
|
2
|
+
// Wraps the TanStack Start server entry so an SSR failure renders a clean 500
|
|
3
|
+
// instead of a raw stack.
|
|
3
4
|
type ServerEntry = {
|
|
4
5
|
fetch: (request: Request, env: unknown, ctx: unknown) => Promise<Response> | Response
|
|
5
6
|
}
|
package/package.json
CHANGED
package/src/template-render.ts
CHANGED
|
@@ -33,16 +33,21 @@ export async function renderTemplate(
|
|
|
33
33
|
const root = path.join(TEMPLATES_ROOT, templateId);
|
|
34
34
|
const glob = new Glob("**/*");
|
|
35
35
|
const created: string[] = [];
|
|
36
|
+
|
|
36
37
|
for await (const rel of glob.scan({
|
|
37
38
|
cwd: root,
|
|
38
39
|
dot: true,
|
|
39
40
|
onlyFiles: true,
|
|
40
41
|
})) {
|
|
41
42
|
if (SKIP.test(rel)) continue;
|
|
43
|
+
|
|
42
44
|
const raw = await Bun.file(path.join(root, rel)).text();
|
|
43
45
|
const content = raw.replace(TOKEN_RE, (_m, key: string) => vars[key] ?? "");
|
|
46
|
+
|
|
44
47
|
await Bun.write(path.join(destDir, rel), content);
|
|
48
|
+
|
|
45
49
|
created.push(rel);
|
|
46
50
|
}
|
|
51
|
+
|
|
47
52
|
return created.sort();
|
|
48
53
|
}
|
|
@@ -2,14 +2,9 @@ import { defineConfig } from 'drizzle-kit'
|
|
|
2
2
|
|
|
3
3
|
// src/db/schema.ts is the single source of truth for the D1 / libsql schema.
|
|
4
4
|
//
|
|
5
|
-
//
|
|
6
|
-
//
|
|
7
|
-
|
|
8
|
-
//
|
|
9
|
-
// PROD: migrations are generated with `drizzle-kit generate` and applied to D1
|
|
10
|
-
// with `wrangler d1 migrations apply DB --remote` (reads migrations_dir in
|
|
11
|
-
// wrangler.jsonc) — so no d1-http driver or Cloudflare API env vars are needed
|
|
12
|
-
// here.
|
|
5
|
+
// In development, your database is always up-to-date. In production, we migrate
|
|
6
|
+
// automatically when you publish.
|
|
7
|
+
|
|
13
8
|
const devDbPath = process.env.DEV_DB_PATH ?? '.dev.db'
|
|
14
9
|
|
|
15
10
|
export default defineConfig({
|
|
@@ -9,10 +9,9 @@
|
|
|
9
9
|
"dev": "drizzle-kit push --force && vite dev",
|
|
10
10
|
"build": "oxlint --fix && vite build",
|
|
11
11
|
"preview": "vite preview",
|
|
12
|
-
"deploy": "bun run db:generate && vite build && wrangler deploy && wrangler d1 migrations apply DB --remote",
|
|
13
12
|
"db:generate": "drizzle-kit generate",
|
|
13
|
+
"generate-routes": "tsr generate",
|
|
14
14
|
"db:push": "drizzle-kit push --force",
|
|
15
|
-
"db:migrate": "wrangler d1 migrations apply DB --remote",
|
|
16
15
|
"typecheck": "tsc --noEmit",
|
|
17
16
|
"lint": "oxlint --fix",
|
|
18
17
|
"test": "vitest run"
|
|
@@ -75,10 +74,11 @@
|
|
|
75
74
|
"devDependencies": {
|
|
76
75
|
"@cloudflare/vite-plugin": "^1.13.0",
|
|
77
76
|
"@cloudflare/workers-types": "^4.20250906.0",
|
|
78
|
-
"@helloleo/vite-config": "^0.1.
|
|
77
|
+
"@helloleo/vite-config": "^0.1.4",
|
|
79
78
|
"@libsql/client": "^0.14.0",
|
|
80
79
|
"@tailwindcss/typography": "^0.5.16",
|
|
81
80
|
"drizzle-kit": "^0.31.10",
|
|
81
|
+
"@tanstack/router-cli": "^1.167.0",
|
|
82
82
|
"@tanstack/router-plugin": "latest",
|
|
83
83
|
"@testing-library/dom": "^10.4.1",
|
|
84
84
|
"@testing-library/react": "^16.3.0",
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
import { createFileRoute } from "@tanstack/react-router";
|
|
2
|
+
import { supabaseAdmin } from "@/integrations/supabase/client.server";
|
|
3
|
+
|
|
4
|
+
export const Route = createFileRoute("/api/health")({
|
|
5
|
+
server: {
|
|
6
|
+
handlers: {
|
|
7
|
+
GET: async () => {
|
|
8
|
+
const startedAt = Date.now();
|
|
9
|
+
|
|
10
|
+
// Server health
|
|
11
|
+
const server = {
|
|
12
|
+
status: "ok" as const,
|
|
13
|
+
uptimeSeconds: Math.round(
|
|
14
|
+
typeof process !== "undefined" &&
|
|
15
|
+
typeof process.uptime === "function"
|
|
16
|
+
? process.uptime()
|
|
17
|
+
: 0,
|
|
18
|
+
),
|
|
19
|
+
timestamp: new Date().toISOString(),
|
|
20
|
+
runtime: "cloudflare-workers",
|
|
21
|
+
};
|
|
22
|
+
|
|
23
|
+
// Database health — lightweight query against a known table
|
|
24
|
+
let database: {
|
|
25
|
+
status: "ok" | "error";
|
|
26
|
+
latencyMs: number;
|
|
27
|
+
error: string | null;
|
|
28
|
+
};
|
|
29
|
+
const dbStart = Date.now();
|
|
30
|
+
try {
|
|
31
|
+
const { error } = await supabaseAdmin
|
|
32
|
+
.from("notes")
|
|
33
|
+
.select("id", { count: "exact", head: true })
|
|
34
|
+
.limit(1);
|
|
35
|
+
if (error) throw error;
|
|
36
|
+
database = {
|
|
37
|
+
status: "ok",
|
|
38
|
+
latencyMs: Date.now() - dbStart,
|
|
39
|
+
error: null,
|
|
40
|
+
};
|
|
41
|
+
} catch (err) {
|
|
42
|
+
database = {
|
|
43
|
+
status: "error",
|
|
44
|
+
latencyMs: Date.now() - dbStart,
|
|
45
|
+
error: err instanceof Error ? err.message : String(err),
|
|
46
|
+
};
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
const overall =
|
|
50
|
+
server.status === "ok" && database.status === "ok"
|
|
51
|
+
? "ok"
|
|
52
|
+
: "degraded";
|
|
53
|
+
|
|
54
|
+
return Response.json(
|
|
55
|
+
{
|
|
56
|
+
status: overall,
|
|
57
|
+
server,
|
|
58
|
+
database,
|
|
59
|
+
checkedInMs: Date.now() - startedAt,
|
|
60
|
+
},
|
|
61
|
+
{ status: overall === "ok" ? 200 : 503 },
|
|
62
|
+
);
|
|
63
|
+
},
|
|
64
|
+
},
|
|
65
|
+
},
|
|
66
|
+
});
|
|
@@ -18,6 +18,7 @@ export const Route = createRootRouteWithContext<{ queryClient: QueryClient }>()(
|
|
|
18
18
|
{ charSet: 'utf-8' },
|
|
19
19
|
{ name: 'viewport', content: 'width=device-width, initial-scale=1.0' },
|
|
20
20
|
{ title: '{{LEO_NAME}}' },
|
|
21
|
+
{ name: 'description', content: '{{LEO_DESCRIPTION}}' },
|
|
21
22
|
],
|
|
22
23
|
links: [
|
|
23
24
|
{ rel: 'stylesheet', href: appCss },
|
|
@@ -1,5 +1,6 @@
|
|
|
1
|
-
// Cloudflare Worker entry (
|
|
2
|
-
// server entry so an SSR failure renders a clean 500
|
|
1
|
+
// Cloudflare Worker entry (the Worker `main`, configured by @helloleo/vite-config).
|
|
2
|
+
// Wraps the TanStack Start server entry so an SSR failure renders a clean 500
|
|
3
|
+
// instead of a raw stack.
|
|
3
4
|
type ServerEntry = {
|
|
4
5
|
fetch: (request: Request, env: unknown, ctx: unknown) => Promise<Response> | Response
|
|
5
6
|
}
|
package/src/tools/scaffold.ts
CHANGED
|
@@ -2,6 +2,7 @@ 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 { z } from "zod/v4";
|
|
5
6
|
import { renderTemplate } from "../template-render.ts";
|
|
6
7
|
|
|
7
8
|
/**
|
|
@@ -25,25 +26,31 @@ export function projectUuidFromDir(dir: string): string {
|
|
|
25
26
|
return path.basename(dir);
|
|
26
27
|
}
|
|
27
28
|
|
|
28
|
-
|
|
29
|
-
name
|
|
30
|
-
|
|
29
|
+
const scaffoldOptionsSchema = z.object({
|
|
30
|
+
name: z
|
|
31
|
+
.string()
|
|
32
|
+
.describe("Short product name. Used as the page title and package name."),
|
|
33
|
+
description: z
|
|
34
|
+
.string()
|
|
35
|
+
.describe("One sentence describing the app. Used as the meta description."),
|
|
36
|
+
});
|
|
31
37
|
|
|
32
38
|
/**
|
|
33
39
|
* Write the base scaffold by rendering the TanStack Start template into dir,
|
|
34
40
|
* then adding .leo/project.json (template metadata the tool owns, not part of
|
|
35
|
-
* the template tree).
|
|
36
|
-
* `setup_netsuite` (base + OAuth files). Returns the relative paths written.
|
|
41
|
+
* the template tree). Returns the relative paths written.
|
|
37
42
|
*/
|
|
38
43
|
export async function writeBaseScaffold(
|
|
39
|
-
|
|
44
|
+
{ name, description }: z.infer<typeof scaffoldOptionsSchema>,
|
|
40
45
|
dir: string,
|
|
41
46
|
): Promise<string[]> {
|
|
42
|
-
const name = opts.name ?? "leo-project";
|
|
43
|
-
|
|
44
47
|
const created = await renderTemplate(
|
|
45
48
|
SCAFFOLD_VERSION,
|
|
46
|
-
{
|
|
49
|
+
{
|
|
50
|
+
LEO_NAME: name,
|
|
51
|
+
LEO_DESCRIPTION: description,
|
|
52
|
+
LEO_PROJECT_UUID: projectUuidFromDir(dir),
|
|
53
|
+
},
|
|
47
54
|
dir,
|
|
48
55
|
);
|
|
49
56
|
|
|
@@ -59,52 +66,35 @@ export async function writeBaseScaffold(
|
|
|
59
66
|
|
|
60
67
|
export const scaffold = tool({
|
|
61
68
|
description:
|
|
62
|
-
"Scaffold a new
|
|
63
|
-
"
|
|
64
|
-
"
|
|
65
|
-
"
|
|
66
|
-
"(src/server.ts) + wrangler.jsonc (DB/KV/BUCKET bindings), a Drizzle DB " +
|
|
67
|
-
"layer (src/db/schema.ts as the single source of truth, src/db/index.ts " +
|
|
68
|
-
"typed client, drizzle.config.ts), the complete shadcn/ui component set " +
|
|
69
|
-
"under src/components/ui, src/lib/utils.ts (cn helper), src/lib/proxy.ts " +
|
|
70
|
-
"(HelloLeo integration proxy), src/styles.css (shadcn design tokens, " +
|
|
71
|
-
"light+dark), plus package.json, vite.config.ts, tsconfig.json, " +
|
|
72
|
-
"components.json, .gitignore and the generated src/routeTree.gen.ts. Call " +
|
|
73
|
-
"this FIRST for any new project before adding routes and pages. For " +
|
|
74
|
-
"NetSuite projects (NetSuite MCP in the live list), call `setup_netsuite` " +
|
|
75
|
-
"INSTEAD -- it bundles base scaffold + OAuth wiring in one call. Do not " +
|
|
76
|
-
"call both.",
|
|
69
|
+
"Scaffold a new TanStack Start project with React 19, SSR, file-based " +
|
|
70
|
+
"routing, Tailwind v4, shadcn/ui, react-query, and a database, KV and " +
|
|
71
|
+
"bucket via @helloleo/runtime. Call this first for any new project, " +
|
|
72
|
+
"before writing routes or components.",
|
|
77
73
|
args: {
|
|
78
|
-
name:
|
|
79
|
-
|
|
80
|
-
.optional()
|
|
81
|
-
.describe("Project name for package.json (default: leo-project)"),
|
|
74
|
+
name: scaffoldOptionsSchema.shape.name,
|
|
75
|
+
description: scaffoldOptionsSchema.shape.description,
|
|
82
76
|
},
|
|
83
77
|
async execute(args, context) {
|
|
84
78
|
const created = await writeBaseScaffold(args, context.directory);
|
|
79
|
+
const uiComponents = created.filter((f) =>
|
|
80
|
+
f.startsWith("src/components/ui/"),
|
|
81
|
+
);
|
|
82
|
+
const rest = created.filter((f) => !f.startsWith("src/components/ui/"));
|
|
85
83
|
return [
|
|
86
84
|
`Scaffolded ${created.length} files:`,
|
|
87
|
-
...
|
|
85
|
+
...rest.map((f) => ` - ${f}`),
|
|
86
|
+
` - src/components/ui/ (${uiComponents.length} shadcn/ui components)`,
|
|
87
|
+
"",
|
|
88
|
+
"Notes:",
|
|
89
|
+
"- This is a TanStack Start project. The server entry is src/server.ts. There is no index.html or client entrypoint.",
|
|
90
|
+
"- Add routes as files in src/routes/. Do not default export route components.",
|
|
91
|
+
"- shadcn/ui components are already in src/components/ui.",
|
|
92
|
+
"- Design system: edit the tokens in src/styles.css (light and dark blocks).",
|
|
93
|
+
"- The database schema lives in src/db/schema.ts. Migrations run automatically.",
|
|
94
|
+
"- Always fetch data with react-query (useQuery and useMutation).",
|
|
88
95
|
"",
|
|
89
|
-
"Next:
|
|
90
|
-
"
|
|
91
|
-
"UI: shadcn/ui components are already in src/components/ui -- import and",
|
|
92
|
-
" use them; do not attempt to re-add via the shadcn CLI.",
|
|
93
|
-
"Design system: edit tokens in src/styles.css (light + dark blocks).",
|
|
94
|
-
"Database: edit src/db/schema.ts (the single source of truth) -- `bun run",
|
|
95
|
-
" db:push` syncs it to the dev DB; the `dev` script does this for you.",
|
|
96
|
-
" Query via `import { db } from '#/db'` inside loaders/server functions.",
|
|
97
|
-
" KV + R2: `import { kv, storage } from '@helloleo/runtime'`.",
|
|
98
|
-
"Data fetching: React Query is wired (QueryClient on the router context +",
|
|
99
|
-
" QueryClientProvider). ALWAYS fetch with @tanstack/react-query --",
|
|
100
|
-
" useQuery / useMutation in components, or a route loader calling",
|
|
101
|
-
" context.queryClient.ensureQueryData(). Do NOT hand-roll fetches with",
|
|
102
|
-
" useEffect + useState. Wrap the fetch call (usually callIntegration())",
|
|
103
|
-
" in the queryFn.",
|
|
104
|
-
"Integration calls: use callIntegration() from src/lib/proxy.ts -- as the",
|
|
105
|
-
" queryFn body, never on its own in useEffect. Its header comment",
|
|
106
|
-
" documents slugs, response unwrapping, pagination, and frontend-only vs",
|
|
107
|
-
" HelloLeo Cloud routing.",
|
|
96
|
+
"Next: read src/routes/index.tsx and continue with the user's request.",
|
|
97
|
+
"For NetSuite projects, call setup_netsuite.",
|
|
108
98
|
].join("\n");
|
|
109
99
|
},
|
|
110
100
|
});
|
|
@@ -2,21 +2,17 @@ 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 { writeBaseScaffold } from "./scaffold.ts";
|
|
6
5
|
|
|
7
6
|
/**
|
|
8
|
-
*
|
|
9
|
-
*
|
|
10
|
-
*
|
|
11
|
-
*
|
|
12
|
-
*
|
|
13
|
-
*
|
|
14
|
-
*
|
|
15
|
-
* to read or change anything NetSuite-related.
|
|
7
|
+
* Adds NetSuite OAuth wiring on top of an existing scaffold: the netsuite
|
|
8
|
+
* OAuth client, a NetSuite auth context (auth.tsx), and the AuthLayout /
|
|
9
|
+
* LoginPage / SignInWithNetSuite components. It never overwrites a
|
|
10
|
+
* scaffold file -- the one manual step is wrapping the root route's
|
|
11
|
+
* <Outlet /> with <AuthLayout> (the tool's output shows the snippet).
|
|
12
|
+
* All NetSuite-specific templates live in this file -- one place to read
|
|
13
|
+
* or change anything NetSuite-related.
|
|
16
14
|
*
|
|
17
|
-
*
|
|
18
|
-
* name carries the intent so the LLM can't accidentally ship a project
|
|
19
|
-
* without the auth files.
|
|
15
|
+
* Called AFTER `scaffold`, never instead of it.
|
|
20
16
|
*/
|
|
21
17
|
|
|
22
18
|
function netsuiteTs(): string {
|
|
@@ -342,21 +338,13 @@ function Gate({ children }: { children: ReactNode }) {
|
|
|
342
338
|
|
|
343
339
|
export const setupNetsuite = tool({
|
|
344
340
|
description:
|
|
345
|
-
"
|
|
346
|
-
"
|
|
347
|
-
"
|
|
348
|
-
"
|
|
349
|
-
"(NetSuiteAuthProvider + useNetSuiteAuth), and " +
|
|
350
|
-
"src/components/auth/{AuthLayout,LoginPage,SignInWithNetSuite}.tsx. " +
|
|
351
|
-
"Use this INSTEAD of `scaffold` for any project that needs NetSuite. " +
|
|
352
|
-
"After it runs there is exactly ONE manual step: wrap the root route's " +
|
|
353
|
-
"<Outlet /> with <AuthLayout> in src/routes/__root.tsx (the tool's output " +
|
|
354
|
-
"shows the snippet). Real OAuth is wired from day one -- never replace " +
|
|
355
|
-
"<AuthLayout> with a password placeholder.",
|
|
341
|
+
"Add NetSuite OAuth wiring to an existing project. Call scaffold first, " +
|
|
342
|
+
"then this. Writes the OAuth client, auth provider and login components. " +
|
|
343
|
+
"All files are additive, nothing is overwritten. Real OAuth is wired " +
|
|
344
|
+
"from day one, never replace <AuthLayout> with a password placeholder.",
|
|
356
345
|
args: {},
|
|
357
346
|
async execute(_args, context) {
|
|
358
347
|
const dir = context.directory;
|
|
359
|
-
const base = await writeBaseScaffold({}, dir);
|
|
360
348
|
|
|
361
349
|
await mkdir(path.join(dir, "src", "components", "auth"), {
|
|
362
350
|
recursive: true,
|
|
@@ -384,45 +372,27 @@ export const setupNetsuite = tool({
|
|
|
384
372
|
await Bun.write(path.join(dir, filePath), content);
|
|
385
373
|
}
|
|
386
374
|
|
|
387
|
-
const all = [...base, ...nsFiles.map(([f]) => f)];
|
|
388
375
|
return [
|
|
389
|
-
`
|
|
376
|
+
`Wrote ${nsFiles.length} NetSuite OAuth files (all additive, nothing overwritten):`,
|
|
377
|
+
" - src/lib/netsuite.ts (OAuth client: signIn, session, workerFetch)",
|
|
378
|
+
" - src/lib/auth.tsx (NetSuiteAuthProvider and useNetSuiteAuth)",
|
|
379
|
+
" - src/components/auth/AuthLayout.tsx (provider and auth gate)",
|
|
380
|
+
" - src/components/auth/LoginPage.tsx (unauthenticated view)",
|
|
381
|
+
" - src/components/auth/SignInWithNetSuite.tsx (sign-in button)",
|
|
390
382
|
"",
|
|
391
|
-
"
|
|
392
|
-
"
|
|
383
|
+
"Notes:",
|
|
384
|
+
"- AuthLayout shows LoginPage to signed-out users and renders the app's routes once they sign in with NetSuite.",
|
|
385
|
+
"- Read the session anywhere with useNetSuiteAuth() from src/lib/auth.tsx.",
|
|
386
|
+
"- Never replace AuthLayout with a password gate. Real OAuth is the only supported login.",
|
|
387
|
+
"- The auth worker (deployed via the cloudflare-backend MCP) serves /auth/netsuite/start, /auth/netsuite/callback, /api/me and /auth/netsuite/logout. It reads env.NETSUITE_ACCOUNT_ID, _CLIENT_ID and _CLIENT_SECRET. VITE_WORKER_URL must be set (HelloLeo Cloud seeds it).",
|
|
393
388
|
"",
|
|
394
|
-
"
|
|
395
|
-
"",
|
|
396
|
-
" function RootComponent() {",
|
|
397
|
-
" return (",
|
|
398
|
-
" <>",
|
|
399
|
-
" <AuthLayout>",
|
|
400
|
-
" <Outlet />",
|
|
401
|
-
" </AuthLayout>",
|
|
402
|
-
" {/* keep the existing <TanStackDevtools ... /> here */}",
|
|
403
|
-
" </>",
|
|
404
|
-
" )",
|
|
405
|
-
" }",
|
|
389
|
+
"Next: in src/routes/__root.tsx, import AuthLayout and wrap <Outlet /> with it. Keep any existing providers.",
|
|
406
390
|
"",
|
|
407
|
-
"
|
|
408
|
-
"in with NetSuite, your routes after. Read the session anywhere below it",
|
|
409
|
-
"with useNetSuiteAuth() from src/lib/auth.tsx. NEVER swap <AuthLayout>",
|
|
410
|
-
"for a password gate -- real OAuth is the only supported login.",
|
|
411
|
-
"",
|
|
412
|
-
"NetSuite OAuth files written:",
|
|
413
|
-
" - src/lib/netsuite.ts OAuth client (signIn / session / workerFetch)",
|
|
414
|
-
" - src/lib/auth.tsx NetSuiteAuthProvider + useNetSuiteAuth",
|
|
415
|
-
" - src/components/auth/AuthLayout.tsx provider + gate (wrap root with this)",
|
|
416
|
-
" - src/components/auth/LoginPage.tsx unauthenticated view",
|
|
417
|
-
" - src/components/auth/SignInWithNetSuite.tsx sign-in button",
|
|
391
|
+
" import { AuthLayout } from '../components/auth/AuthLayout'",
|
|
418
392
|
"",
|
|
419
|
-
"
|
|
420
|
-
"
|
|
421
|
-
"
|
|
422
|
-
" GET /api/me -- current session",
|
|
423
|
-
" POST /auth/netsuite/logout -- clear session",
|
|
424
|
-
" Reads env.NETSUITE_ACCOUNT_ID / _CLIENT_ID / _CLIENT_SECRET.",
|
|
425
|
-
" VITE_WORKER_URL must be set (HelloLeo Cloud provisioner seeds it).",
|
|
393
|
+
" <AuthLayout>",
|
|
394
|
+
" <Outlet />",
|
|
395
|
+
" </AuthLayout>",
|
|
426
396
|
].join("\n");
|
|
427
397
|
},
|
|
428
398
|
});
|
|
@@ -1,27 +0,0 @@
|
|
|
1
|
-
{
|
|
2
|
-
"$schema": "node_modules/wrangler/config-schema.json",
|
|
3
|
-
"name": "{{LEO_NAME}}",
|
|
4
|
-
"compatibility_date": "2025-09-02",
|
|
5
|
-
"compatibility_flags": ["nodejs_compat"],
|
|
6
|
-
// @cloudflare/vite-plugin builds the Worker from the TanStack Start server
|
|
7
|
-
// entry; src/server.ts wraps it. These three bindings are the PROD path that
|
|
8
|
-
// the dev cf-shim emulates locally — keep the names DB / KV / BUCKET, that is
|
|
9
|
-
// the contract @helloleo/runtime expects.
|
|
10
|
-
"main": "src/server.ts",
|
|
11
|
-
"observability": { "enabled": true },
|
|
12
|
-
// Provision real resources before deploy (`wrangler d1/kv/r2 create`) and swap
|
|
13
|
-
// the placeholder ids below. In dev none of this matters — the cf-shim backs
|
|
14
|
-
// DB with libsql (.dev.db), KV in libsql, and R2 on the local filesystem.
|
|
15
|
-
"d1_databases": [
|
|
16
|
-
{
|
|
17
|
-
"binding": "DB",
|
|
18
|
-
"database_name": "{{LEO_NAME}}",
|
|
19
|
-
"database_id": "REPLACE_WITH_D1_ID",
|
|
20
|
-
// drizzle-kit generate writes migrations here; `wrangler d1 migrations
|
|
21
|
-
// apply DB --remote` reads this dir to apply them to prod D1.
|
|
22
|
-
"migrations_dir": "drizzle"
|
|
23
|
-
}
|
|
24
|
-
],
|
|
25
|
-
"kv_namespaces": [{ "binding": "KV", "id": "REPLACE_WITH_KV_ID" }],
|
|
26
|
-
"r2_buckets": [{ "binding": "BUCKET", "bucket_name": "{{LEO_NAME}}" }]
|
|
27
|
-
}
|
|
@@ -1,27 +0,0 @@
|
|
|
1
|
-
{
|
|
2
|
-
"$schema": "node_modules/wrangler/config-schema.json",
|
|
3
|
-
"name": "{{LEO_NAME}}",
|
|
4
|
-
"compatibility_date": "2025-09-02",
|
|
5
|
-
"compatibility_flags": ["nodejs_compat"],
|
|
6
|
-
// @cloudflare/vite-plugin builds the Worker from the TanStack Start server
|
|
7
|
-
// entry; src/server.ts wraps it. These three bindings are the PROD path that
|
|
8
|
-
// the dev cf-shim emulates locally — keep the names DB / KV / BUCKET, that is
|
|
9
|
-
// the contract @helloleo/runtime expects.
|
|
10
|
-
"main": "src/server.ts",
|
|
11
|
-
"observability": { "enabled": true },
|
|
12
|
-
// Provision real resources before deploy (`wrangler d1/kv/r2 create`) and swap
|
|
13
|
-
// the placeholder ids below. In dev none of this matters — the cf-shim backs
|
|
14
|
-
// DB with libsql (.dev.db), KV in libsql, and R2 on the local filesystem.
|
|
15
|
-
"d1_databases": [
|
|
16
|
-
{
|
|
17
|
-
"binding": "DB",
|
|
18
|
-
"database_name": "{{LEO_NAME}}",
|
|
19
|
-
"database_id": "REPLACE_WITH_D1_ID",
|
|
20
|
-
// drizzle-kit generate writes migrations here; `wrangler d1 migrations
|
|
21
|
-
// apply DB --remote` reads this dir to apply them to prod D1.
|
|
22
|
-
"migrations_dir": "drizzle"
|
|
23
|
-
}
|
|
24
|
-
],
|
|
25
|
-
"kv_namespaces": [{ "binding": "KV", "id": "REPLACE_WITH_KV_ID" }],
|
|
26
|
-
"r2_buckets": [{ "binding": "BUCKET", "bucket_name": "{{LEO_NAME}}" }]
|
|
27
|
-
}
|