@helloleo/plugins 0.2.17 → 0.2.19
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 +940 -95
- package/dist/templates/tanstack-router-2026-06-15/src/lib/proxy.ts +39 -1
- package/dist/templates/tanstack-start-2026-06-18/src/lib/proxy.ts +38 -1
- package/dist/templates/tanstack-start-2026-07-30/src/lib/proxy.ts +30 -1
- package/package.json +1 -1
- package/src/templates/tanstack-router-2026-06-15/src/lib/proxy.ts +39 -1
- package/src/templates/tanstack-start-2026-06-18/src/lib/proxy.ts +38 -1
- package/src/templates/tanstack-start-2026-07-30/src/lib/proxy.ts +30 -1
- package/src/tools/scaffold.ts +43 -25
- package/src/tools/setup-netsuite.ts +1007 -98
|
@@ -7,6 +7,14 @@
|
|
|
7
7
|
* user already configured in Project Settings -- you NEVER pass an API key
|
|
8
8
|
* from frontend code, and you NEVER call third-party hosts directly.
|
|
9
9
|
*
|
|
10
|
+
* EXCEPTION -- NetSuite: never route it through this proxy. This proxy
|
|
11
|
+
* refreshes tokens per PROJECT (one shared credential for every end user).
|
|
12
|
+
* NetSuite runtime calls must stay per USER: the app signs each visitor in
|
|
13
|
+
* via its own /auth/netsuite/* + /api server routes and calls NetSuite with
|
|
14
|
+
* THAT visitor's session (see setup_netsuite / src/lib/netsuite-server.ts,
|
|
15
|
+
* netsuiteFetch(bearer(request), ...)). The proxy rejects "netsuite" with a
|
|
16
|
+
* 400 -- that's intentional, not a gap to fill in.
|
|
17
|
+
*
|
|
10
18
|
* Two modes:
|
|
11
19
|
* A) Frontend-only project -> import callIntegration() from this file.
|
|
12
20
|
* B) HelloLeo Cloud project -> route via Worker fetch(env.INTEGRATION_PROXY).
|
|
@@ -44,6 +52,29 @@ export const INTEGRATION_PROXY_URL = '{{LEO_INTEGRATION_PROXY_URL}}';
|
|
|
44
52
|
|
|
45
53
|
export const BACKEND_URL = (import.meta.env.VITE_WORKER_URL as string | undefined) ?? '';
|
|
46
54
|
|
|
55
|
+
// Multi-environment integrations: the published HelloLeo Cloud Worker has
|
|
56
|
+
// HELLOLEO_ENV=production injected at publish. Forwarding `?env=production`
|
|
57
|
+
// makes the proxy resolve credentials from the project's PRODUCTION integration
|
|
58
|
+
// connection (e.g. your production Odoo) instead of the development one, so a
|
|
59
|
+
// published app talks to production while the preview keeps talking to staging.
|
|
60
|
+
//
|
|
61
|
+
// Reads the flag from the server runtime (Cloudflare Worker / Node) when
|
|
62
|
+
// available; in the browser it's absent and we send no param — the proxy then
|
|
63
|
+
// derives the environment from the request origin, so client-side calls from
|
|
64
|
+
// the published app still resolve to production. Server-side calls (SSR / Cloud
|
|
65
|
+
// Worker) carry no browser origin, which is exactly why the explicit param
|
|
66
|
+
// matters there.
|
|
67
|
+
function proxyUrl(): string {
|
|
68
|
+
// Read via globalThis only (no bare `process` reference) so this compiles in
|
|
69
|
+
// any scaffolded project regardless of whether node types are present.
|
|
70
|
+
const g = globalThis as {
|
|
71
|
+
HELLOLEO_ENV?: string;
|
|
72
|
+
process?: { env?: Record<string, string | undefined> };
|
|
73
|
+
};
|
|
74
|
+
const env = g.HELLOLEO_ENV ?? g.process?.env?.HELLOLEO_ENV;
|
|
75
|
+
return env === 'production' ? `${INTEGRATION_PROXY_URL}?env=production` : INTEGRATION_PROXY_URL;
|
|
76
|
+
}
|
|
77
|
+
|
|
47
78
|
export interface ProxyRequest {
|
|
48
79
|
integration: string;
|
|
49
80
|
endpoint: string;
|
|
@@ -61,7 +92,14 @@ export interface ProxyResponse<T = unknown> {
|
|
|
61
92
|
export async function callIntegration<T = unknown>(
|
|
62
93
|
req: ProxyRequest,
|
|
63
94
|
): Promise<ProxyResponse<T>> {
|
|
64
|
-
|
|
95
|
+
if (req.integration.toLowerCase() === 'netsuite') {
|
|
96
|
+
throw new Error(
|
|
97
|
+
'NetSuite is not proxied -- use the setup_netsuite auth flow (its ' +
|
|
98
|
+
'/auth/netsuite + /api routes) with the signed-in user\'s session, ' +
|
|
99
|
+
'not callIntegration().',
|
|
100
|
+
);
|
|
101
|
+
}
|
|
102
|
+
const res = await fetch(proxyUrl(), {
|
|
65
103
|
method: 'POST',
|
|
66
104
|
headers: { 'Content-Type': 'application/json' },
|
|
67
105
|
body: JSON.stringify({ method: 'POST', ...req }),
|
|
@@ -7,6 +7,14 @@
|
|
|
7
7
|
* user already configured in Project Settings -- you NEVER pass an API key
|
|
8
8
|
* from frontend code, and you NEVER call third-party hosts directly.
|
|
9
9
|
*
|
|
10
|
+
* EXCEPTION -- NetSuite: never route it through this proxy. This proxy
|
|
11
|
+
* refreshes tokens per PROJECT (one shared credential for every end user).
|
|
12
|
+
* NetSuite runtime calls must stay per USER: the app signs each visitor in
|
|
13
|
+
* via its own /auth/netsuite/* + /api server routes and calls NetSuite with
|
|
14
|
+
* THAT visitor's session (see setup_netsuite / src/lib/netsuite-server.ts,
|
|
15
|
+
* netsuiteFetch(bearer(request), ...)). The proxy rejects "netsuite" with a
|
|
16
|
+
* 400 -- that's intentional, not a gap to fill in.
|
|
17
|
+
*
|
|
10
18
|
* Two modes:
|
|
11
19
|
* A) Frontend-only project -> import callIntegration() from this file.
|
|
12
20
|
* B) HelloLeo Cloud project -> route via Worker fetch(env.INTEGRATION_PROXY).
|
|
@@ -41,6 +49,29 @@
|
|
|
41
49
|
|
|
42
50
|
export const INTEGRATION_PROXY_URL = '{{LEO_INTEGRATION_PROXY_URL}}';
|
|
43
51
|
|
|
52
|
+
// Multi-environment integrations: the published HelloLeo Cloud Worker has
|
|
53
|
+
// HELLOLEO_ENV=production injected at publish. Forwarding `?env=production`
|
|
54
|
+
// makes the proxy resolve credentials from the project's PRODUCTION integration
|
|
55
|
+
// connection (e.g. your production Odoo) instead of the development one, so a
|
|
56
|
+
// published app talks to production while the preview keeps talking to staging.
|
|
57
|
+
//
|
|
58
|
+
// Reads the flag from the server runtime (Cloudflare Worker / Node) when
|
|
59
|
+
// available; in the browser it's absent and we send no param — the proxy then
|
|
60
|
+
// derives the environment from the request origin, so client-side calls from
|
|
61
|
+
// the published app still resolve to production. Server-side calls (SSR / Cloud
|
|
62
|
+
// Worker) carry no browser origin, which is exactly why the explicit param
|
|
63
|
+
// matters there.
|
|
64
|
+
function proxyUrl(): string {
|
|
65
|
+
// Read via globalThis only (no bare `process` reference) so this compiles in
|
|
66
|
+
// any scaffolded project regardless of whether node types are present.
|
|
67
|
+
const g = globalThis as {
|
|
68
|
+
HELLOLEO_ENV?: string;
|
|
69
|
+
process?: { env?: Record<string, string | undefined> };
|
|
70
|
+
};
|
|
71
|
+
const env = g.HELLOLEO_ENV ?? g.process?.env?.HELLOLEO_ENV;
|
|
72
|
+
return env === 'production' ? `${INTEGRATION_PROXY_URL}?env=production` : INTEGRATION_PROXY_URL;
|
|
73
|
+
}
|
|
74
|
+
|
|
44
75
|
export interface ProxyRequest {
|
|
45
76
|
integration: string;
|
|
46
77
|
endpoint: string;
|
|
@@ -58,7 +89,13 @@ export interface ProxyResponse<T = unknown> {
|
|
|
58
89
|
export async function callIntegration<T = unknown>(
|
|
59
90
|
req: ProxyRequest,
|
|
60
91
|
): Promise<ProxyResponse<T>> {
|
|
61
|
-
|
|
92
|
+
if (req.integration.toLowerCase() === 'netsuite') {
|
|
93
|
+
throw new Error(
|
|
94
|
+
'NetSuite is not proxied -- call it from this app\'s own /auth/netsuite + ' +
|
|
95
|
+
'/api server routes with the signed-in user\'s session, not callIntegration().',
|
|
96
|
+
);
|
|
97
|
+
}
|
|
98
|
+
const res = await fetch(proxyUrl(), {
|
|
62
99
|
method: 'POST',
|
|
63
100
|
headers: { 'Content-Type': 'application/json' },
|
|
64
101
|
body: JSON.stringify({ method: 'POST', ...req }),
|
|
@@ -41,6 +41,29 @@
|
|
|
41
41
|
|
|
42
42
|
export const INTEGRATION_PROXY_URL = '{{LEO_INTEGRATION_PROXY_URL}}';
|
|
43
43
|
|
|
44
|
+
// Multi-environment integrations: the published HelloLeo Cloud Worker has
|
|
45
|
+
// HELLOLEO_ENV=production injected at publish. Forwarding `?env=production`
|
|
46
|
+
// makes the proxy resolve credentials from the project's PRODUCTION integration
|
|
47
|
+
// connection (e.g. your production Odoo) instead of the development one, so a
|
|
48
|
+
// published app talks to production while the preview keeps talking to staging.
|
|
49
|
+
//
|
|
50
|
+
// Reads the flag from the server runtime (Cloudflare Worker / Node) when
|
|
51
|
+
// available; in the browser it's absent and we send no param — the proxy then
|
|
52
|
+
// derives the environment from the request origin, so client-side calls from
|
|
53
|
+
// the published app still resolve to production. Server-side calls (SSR / Cloud
|
|
54
|
+
// Worker) carry no browser origin, which is exactly why the explicit param
|
|
55
|
+
// matters there.
|
|
56
|
+
function proxyUrl(): string {
|
|
57
|
+
// Read via globalThis only (no bare `process` reference) so this compiles in
|
|
58
|
+
// any scaffolded project regardless of whether node types are present.
|
|
59
|
+
const g = globalThis as {
|
|
60
|
+
HELLOLEO_ENV?: string;
|
|
61
|
+
process?: { env?: Record<string, string | undefined> };
|
|
62
|
+
};
|
|
63
|
+
const env = g.HELLOLEO_ENV ?? g.process?.env?.HELLOLEO_ENV;
|
|
64
|
+
return env === 'production' ? `${INTEGRATION_PROXY_URL}?env=production` : INTEGRATION_PROXY_URL;
|
|
65
|
+
}
|
|
66
|
+
|
|
44
67
|
export interface ProxyRequest {
|
|
45
68
|
integration: string;
|
|
46
69
|
endpoint: string;
|
|
@@ -58,7 +81,13 @@ export interface ProxyResponse<T = unknown> {
|
|
|
58
81
|
export async function callIntegration<T = unknown>(
|
|
59
82
|
req: ProxyRequest,
|
|
60
83
|
): Promise<ProxyResponse<T>> {
|
|
61
|
-
|
|
84
|
+
if (req.integration.toLowerCase() === 'netsuite') {
|
|
85
|
+
throw new Error(
|
|
86
|
+
'NetSuite is not proxied -- call it from this app\'s own /auth/netsuite + ' +
|
|
87
|
+
'/api server routes with the signed-in user\'s session, not callIntegration().',
|
|
88
|
+
);
|
|
89
|
+
}
|
|
90
|
+
const res = await fetch(proxyUrl(), {
|
|
62
91
|
method: 'POST',
|
|
63
92
|
headers: { 'Content-Type': 'application/json' },
|
|
64
93
|
body: JSON.stringify({ method: 'POST', ...req }),
|
package/package.json
CHANGED
|
@@ -7,6 +7,14 @@
|
|
|
7
7
|
* user already configured in Project Settings -- you NEVER pass an API key
|
|
8
8
|
* from frontend code, and you NEVER call third-party hosts directly.
|
|
9
9
|
*
|
|
10
|
+
* EXCEPTION -- NetSuite: never route it through this proxy. This proxy
|
|
11
|
+
* refreshes tokens per PROJECT (one shared credential for every end user).
|
|
12
|
+
* NetSuite runtime calls must stay per USER: the app signs each visitor in
|
|
13
|
+
* via its own /auth/netsuite/* + /api server routes and calls NetSuite with
|
|
14
|
+
* THAT visitor's session (see setup_netsuite / src/lib/netsuite-server.ts,
|
|
15
|
+
* netsuiteFetch(bearer(request), ...)). The proxy rejects "netsuite" with a
|
|
16
|
+
* 400 -- that's intentional, not a gap to fill in.
|
|
17
|
+
*
|
|
10
18
|
* Two modes:
|
|
11
19
|
* A) Frontend-only project -> import callIntegration() from this file.
|
|
12
20
|
* B) HelloLeo Cloud project -> route via Worker fetch(env.INTEGRATION_PROXY).
|
|
@@ -44,6 +52,29 @@ export const INTEGRATION_PROXY_URL = '{{LEO_INTEGRATION_PROXY_URL}}';
|
|
|
44
52
|
|
|
45
53
|
export const BACKEND_URL = (import.meta.env.VITE_WORKER_URL as string | undefined) ?? '';
|
|
46
54
|
|
|
55
|
+
// Multi-environment integrations: the published HelloLeo Cloud Worker has
|
|
56
|
+
// HELLOLEO_ENV=production injected at publish. Forwarding `?env=production`
|
|
57
|
+
// makes the proxy resolve credentials from the project's PRODUCTION integration
|
|
58
|
+
// connection (e.g. your production Odoo) instead of the development one, so a
|
|
59
|
+
// published app talks to production while the preview keeps talking to staging.
|
|
60
|
+
//
|
|
61
|
+
// Reads the flag from the server runtime (Cloudflare Worker / Node) when
|
|
62
|
+
// available; in the browser it's absent and we send no param — the proxy then
|
|
63
|
+
// derives the environment from the request origin, so client-side calls from
|
|
64
|
+
// the published app still resolve to production. Server-side calls (SSR / Cloud
|
|
65
|
+
// Worker) carry no browser origin, which is exactly why the explicit param
|
|
66
|
+
// matters there.
|
|
67
|
+
function proxyUrl(): string {
|
|
68
|
+
// Read via globalThis only (no bare `process` reference) so this compiles in
|
|
69
|
+
// any scaffolded project regardless of whether node types are present.
|
|
70
|
+
const g = globalThis as {
|
|
71
|
+
HELLOLEO_ENV?: string;
|
|
72
|
+
process?: { env?: Record<string, string | undefined> };
|
|
73
|
+
};
|
|
74
|
+
const env = g.HELLOLEO_ENV ?? g.process?.env?.HELLOLEO_ENV;
|
|
75
|
+
return env === 'production' ? `${INTEGRATION_PROXY_URL}?env=production` : INTEGRATION_PROXY_URL;
|
|
76
|
+
}
|
|
77
|
+
|
|
47
78
|
export interface ProxyRequest {
|
|
48
79
|
integration: string;
|
|
49
80
|
endpoint: string;
|
|
@@ -61,7 +92,14 @@ export interface ProxyResponse<T = unknown> {
|
|
|
61
92
|
export async function callIntegration<T = unknown>(
|
|
62
93
|
req: ProxyRequest,
|
|
63
94
|
): Promise<ProxyResponse<T>> {
|
|
64
|
-
|
|
95
|
+
if (req.integration.toLowerCase() === 'netsuite') {
|
|
96
|
+
throw new Error(
|
|
97
|
+
'NetSuite is not proxied -- use the setup_netsuite auth flow (its ' +
|
|
98
|
+
'/auth/netsuite + /api routes) with the signed-in user\'s session, ' +
|
|
99
|
+
'not callIntegration().',
|
|
100
|
+
);
|
|
101
|
+
}
|
|
102
|
+
const res = await fetch(proxyUrl(), {
|
|
65
103
|
method: 'POST',
|
|
66
104
|
headers: { 'Content-Type': 'application/json' },
|
|
67
105
|
body: JSON.stringify({ method: 'POST', ...req }),
|
|
@@ -7,6 +7,14 @@
|
|
|
7
7
|
* user already configured in Project Settings -- you NEVER pass an API key
|
|
8
8
|
* from frontend code, and you NEVER call third-party hosts directly.
|
|
9
9
|
*
|
|
10
|
+
* EXCEPTION -- NetSuite: never route it through this proxy. This proxy
|
|
11
|
+
* refreshes tokens per PROJECT (one shared credential for every end user).
|
|
12
|
+
* NetSuite runtime calls must stay per USER: the app signs each visitor in
|
|
13
|
+
* via its own /auth/netsuite/* + /api server routes and calls NetSuite with
|
|
14
|
+
* THAT visitor's session (see setup_netsuite / src/lib/netsuite-server.ts,
|
|
15
|
+
* netsuiteFetch(bearer(request), ...)). The proxy rejects "netsuite" with a
|
|
16
|
+
* 400 -- that's intentional, not a gap to fill in.
|
|
17
|
+
*
|
|
10
18
|
* Two modes:
|
|
11
19
|
* A) Frontend-only project -> import callIntegration() from this file.
|
|
12
20
|
* B) HelloLeo Cloud project -> route via Worker fetch(env.INTEGRATION_PROXY).
|
|
@@ -41,6 +49,29 @@
|
|
|
41
49
|
|
|
42
50
|
export const INTEGRATION_PROXY_URL = '{{LEO_INTEGRATION_PROXY_URL}}';
|
|
43
51
|
|
|
52
|
+
// Multi-environment integrations: the published HelloLeo Cloud Worker has
|
|
53
|
+
// HELLOLEO_ENV=production injected at publish. Forwarding `?env=production`
|
|
54
|
+
// makes the proxy resolve credentials from the project's PRODUCTION integration
|
|
55
|
+
// connection (e.g. your production Odoo) instead of the development one, so a
|
|
56
|
+
// published app talks to production while the preview keeps talking to staging.
|
|
57
|
+
//
|
|
58
|
+
// Reads the flag from the server runtime (Cloudflare Worker / Node) when
|
|
59
|
+
// available; in the browser it's absent and we send no param — the proxy then
|
|
60
|
+
// derives the environment from the request origin, so client-side calls from
|
|
61
|
+
// the published app still resolve to production. Server-side calls (SSR / Cloud
|
|
62
|
+
// Worker) carry no browser origin, which is exactly why the explicit param
|
|
63
|
+
// matters there.
|
|
64
|
+
function proxyUrl(): string {
|
|
65
|
+
// Read via globalThis only (no bare `process` reference) so this compiles in
|
|
66
|
+
// any scaffolded project regardless of whether node types are present.
|
|
67
|
+
const g = globalThis as {
|
|
68
|
+
HELLOLEO_ENV?: string;
|
|
69
|
+
process?: { env?: Record<string, string | undefined> };
|
|
70
|
+
};
|
|
71
|
+
const env = g.HELLOLEO_ENV ?? g.process?.env?.HELLOLEO_ENV;
|
|
72
|
+
return env === 'production' ? `${INTEGRATION_PROXY_URL}?env=production` : INTEGRATION_PROXY_URL;
|
|
73
|
+
}
|
|
74
|
+
|
|
44
75
|
export interface ProxyRequest {
|
|
45
76
|
integration: string;
|
|
46
77
|
endpoint: string;
|
|
@@ -58,7 +89,13 @@ export interface ProxyResponse<T = unknown> {
|
|
|
58
89
|
export async function callIntegration<T = unknown>(
|
|
59
90
|
req: ProxyRequest,
|
|
60
91
|
): Promise<ProxyResponse<T>> {
|
|
61
|
-
|
|
92
|
+
if (req.integration.toLowerCase() === 'netsuite') {
|
|
93
|
+
throw new Error(
|
|
94
|
+
'NetSuite is not proxied -- call it from this app\'s own /auth/netsuite + ' +
|
|
95
|
+
'/api server routes with the signed-in user\'s session, not callIntegration().',
|
|
96
|
+
);
|
|
97
|
+
}
|
|
98
|
+
const res = await fetch(proxyUrl(), {
|
|
62
99
|
method: 'POST',
|
|
63
100
|
headers: { 'Content-Type': 'application/json' },
|
|
64
101
|
body: JSON.stringify({ method: 'POST', ...req }),
|
|
@@ -41,6 +41,29 @@
|
|
|
41
41
|
|
|
42
42
|
export const INTEGRATION_PROXY_URL = '{{LEO_INTEGRATION_PROXY_URL}}';
|
|
43
43
|
|
|
44
|
+
// Multi-environment integrations: the published HelloLeo Cloud Worker has
|
|
45
|
+
// HELLOLEO_ENV=production injected at publish. Forwarding `?env=production`
|
|
46
|
+
// makes the proxy resolve credentials from the project's PRODUCTION integration
|
|
47
|
+
// connection (e.g. your production Odoo) instead of the development one, so a
|
|
48
|
+
// published app talks to production while the preview keeps talking to staging.
|
|
49
|
+
//
|
|
50
|
+
// Reads the flag from the server runtime (Cloudflare Worker / Node) when
|
|
51
|
+
// available; in the browser it's absent and we send no param — the proxy then
|
|
52
|
+
// derives the environment from the request origin, so client-side calls from
|
|
53
|
+
// the published app still resolve to production. Server-side calls (SSR / Cloud
|
|
54
|
+
// Worker) carry no browser origin, which is exactly why the explicit param
|
|
55
|
+
// matters there.
|
|
56
|
+
function proxyUrl(): string {
|
|
57
|
+
// Read via globalThis only (no bare `process` reference) so this compiles in
|
|
58
|
+
// any scaffolded project regardless of whether node types are present.
|
|
59
|
+
const g = globalThis as {
|
|
60
|
+
HELLOLEO_ENV?: string;
|
|
61
|
+
process?: { env?: Record<string, string | undefined> };
|
|
62
|
+
};
|
|
63
|
+
const env = g.HELLOLEO_ENV ?? g.process?.env?.HELLOLEO_ENV;
|
|
64
|
+
return env === 'production' ? `${INTEGRATION_PROXY_URL}?env=production` : INTEGRATION_PROXY_URL;
|
|
65
|
+
}
|
|
66
|
+
|
|
44
67
|
export interface ProxyRequest {
|
|
45
68
|
integration: string;
|
|
46
69
|
endpoint: string;
|
|
@@ -58,7 +81,13 @@ export interface ProxyResponse<T = unknown> {
|
|
|
58
81
|
export async function callIntegration<T = unknown>(
|
|
59
82
|
req: ProxyRequest,
|
|
60
83
|
): Promise<ProxyResponse<T>> {
|
|
61
|
-
|
|
84
|
+
if (req.integration.toLowerCase() === 'netsuite') {
|
|
85
|
+
throw new Error(
|
|
86
|
+
'NetSuite is not proxied -- call it from this app\'s own /auth/netsuite + ' +
|
|
87
|
+
'/api server routes with the signed-in user\'s session, not callIntegration().',
|
|
88
|
+
);
|
|
89
|
+
}
|
|
90
|
+
const res = await fetch(proxyUrl(), {
|
|
62
91
|
method: 'POST',
|
|
63
92
|
headers: { 'Content-Type': 'application/json' },
|
|
64
93
|
body: JSON.stringify({ method: 'POST', ...req }),
|
package/src/tools/scaffold.ts
CHANGED
|
@@ -24,20 +24,36 @@ export const SCAFFOLD_TEMPLATES = {
|
|
|
24
24
|
|
|
25
25
|
export type ScaffoldType = keyof typeof SCAFFOLD_TEMPLATES;
|
|
26
26
|
|
|
27
|
+
/**
|
|
28
|
+
* Older template ids that still ship under src/templates/ and stay valid
|
|
29
|
+
* PLUGINS_SCAFFOLD_VERSION pins (a deployment may hold back a version).
|
|
30
|
+
*/
|
|
31
|
+
export const LEGACY_TEMPLATES = ["tanstack-start-2026-06-18"] as const;
|
|
32
|
+
|
|
27
33
|
/** Default template id, kept for callers that predate the `type` param. */
|
|
28
34
|
export const SCAFFOLD_VERSION = SCAFFOLD_TEMPLATES["tanstack-start"];
|
|
29
35
|
|
|
30
36
|
/**
|
|
37
|
+
* Resolve which template to render.
|
|
38
|
+
*
|
|
31
39
|
* The container can pin the template via PLUGINS_SCAFFOLD_VERSION (a versioned
|
|
32
|
-
* template id, e.g. "tanstack-start-2026-06-18")
|
|
33
|
-
*
|
|
34
|
-
*
|
|
40
|
+
* template id, e.g. "tanstack-start-2026-06-18"); unknown values are ignored
|
|
41
|
+
* so a stale env var can never reproduce the ENOENT-on-missing-template
|
|
42
|
+
* failure. An explicit `type` from the tool call wins the FAMILY (start vs
|
|
43
|
+
* router); the pin still supplies the version when it points inside that
|
|
44
|
+
* family. With neither, the pin decides, falling back to the router template.
|
|
35
45
|
*/
|
|
36
|
-
export function resolveTemplateId(): string {
|
|
46
|
+
export function resolveTemplateId(type?: ScaffoldType): string {
|
|
37
47
|
const version = process.env.PLUGINS_SCAFFOLD_VERSION;
|
|
38
|
-
const known: string[] =
|
|
39
|
-
|
|
40
|
-
|
|
48
|
+
const known: string[] = [
|
|
49
|
+
...Object.values(SCAFFOLD_TEMPLATES),
|
|
50
|
+
...LEGACY_TEMPLATES,
|
|
51
|
+
];
|
|
52
|
+
const pinned = version && known.includes(version) ? version : undefined;
|
|
53
|
+
if (type) {
|
|
54
|
+
return pinned?.startsWith(type) ? pinned : SCAFFOLD_TEMPLATES[type];
|
|
55
|
+
}
|
|
56
|
+
return pinned ?? SCAFFOLD_TEMPLATES["tanstack-router"];
|
|
41
57
|
}
|
|
42
58
|
|
|
43
59
|
/**
|
|
@@ -73,18 +89,18 @@ const scaffoldOptionsSchema = z.object({
|
|
|
73
89
|
description: z
|
|
74
90
|
.string()
|
|
75
91
|
.describe("One sentence describing the app. Used as the meta description."),
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
92
|
+
type: z
|
|
93
|
+
.enum(["tanstack-start", "tanstack-router"])
|
|
94
|
+
.optional()
|
|
95
|
+
.describe(
|
|
96
|
+
"Project type. 'tanstack-start': SSR app with API routes and a " +
|
|
97
|
+
"database, KV and bucket — use it whenever the app needs to " +
|
|
98
|
+
"persist data or run server-side code, and always for " +
|
|
99
|
+
"setup_authentication (it needs server routes). " +
|
|
100
|
+
"'tanstack-router': client-only SPA with no server and no database " +
|
|
101
|
+
"— use it only for pure frontends that talk to external APIs. " +
|
|
102
|
+
"Omit to use the deployment's default.",
|
|
103
|
+
),
|
|
88
104
|
});
|
|
89
105
|
|
|
90
106
|
/** Kebab-case slug of the product name, for package.json. */
|
|
@@ -103,10 +119,10 @@ function slugify(name: string): string {
|
|
|
103
119
|
* not part of the template tree). Returns the relative paths written.
|
|
104
120
|
*/
|
|
105
121
|
export async function writeBaseScaffold(
|
|
106
|
-
{ name, description }: z.infer<typeof scaffoldOptionsSchema>,
|
|
122
|
+
{ name, description, type }: z.infer<typeof scaffoldOptionsSchema>,
|
|
107
123
|
dir: string,
|
|
108
124
|
): Promise<string[]> {
|
|
109
|
-
const templateId = resolveTemplateId();
|
|
125
|
+
const templateId = resolveTemplateId(type);
|
|
110
126
|
const projectUuid = projectUuidFromDir(dir);
|
|
111
127
|
const created = await renderTemplate(
|
|
112
128
|
templateId,
|
|
@@ -133,15 +149,17 @@ export async function writeBaseScaffold(
|
|
|
133
149
|
export const scaffold = tool({
|
|
134
150
|
description:
|
|
135
151
|
"Scaffold a new project with React 19, file-based routing (TanStack), " +
|
|
136
|
-
"Tailwind v4, shadcn/ui, and react-query.
|
|
137
|
-
"
|
|
152
|
+
"Tailwind v4, shadcn/ui, and react-query. Two types: 'tanstack-start' " +
|
|
153
|
+
"(SSR, API routes, and a database, KV and bucket via @helloleo/runtime) " +
|
|
154
|
+
"or 'tanstack-router' (client-only SPA, no server, no database). Call " +
|
|
155
|
+
"this first for any new project, before writing routes or components.",
|
|
138
156
|
args: {
|
|
139
157
|
name: scaffoldOptionsSchema.shape.name,
|
|
140
158
|
description: scaffoldOptionsSchema.shape.description,
|
|
159
|
+
type: scaffoldOptionsSchema.shape.type,
|
|
141
160
|
},
|
|
142
161
|
async execute(args, context) {
|
|
143
|
-
const isStart =
|
|
144
|
-
resolveTemplateId() === SCAFFOLD_TEMPLATES["tanstack-start"];
|
|
162
|
+
const isStart = resolveTemplateId(args.type).startsWith("tanstack-start");
|
|
145
163
|
const created = await writeBaseScaffold(args, context.directory);
|
|
146
164
|
const uiComponents = created.filter((f) =>
|
|
147
165
|
f.startsWith("src/components/ui/"),
|