@neetru/cli 2.24.1 → 2.26.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/commands/ai.js +8 -8
- package/dist/commands/bootstrap.js +18 -1
- package/dist/commands/bootstrap.js.map +1 -1
- package/dist/commands/build.js +12 -17
- package/dist/commands/build.js.map +1 -1
- package/dist/commands/dev-up.d.ts +10 -26
- package/dist/commands/dev-up.js +78 -140
- package/dist/commands/dev-up.js.map +1 -1
- package/dist/commands/doctor.js +10 -4
- package/dist/commands/doctor.js.map +1 -1
- package/dist/commands/init.js +147 -147
- package/dist/commands/logs.d.ts +29 -0
- package/dist/commands/logs.js +161 -0
- package/dist/commands/logs.js.map +1 -1
- package/dist/commands/webhooks.d.ts +34 -0
- package/dist/commands/webhooks.js +162 -0
- package/dist/commands/webhooks.js.map +1 -0
- package/dist/index.js +109 -27
- package/dist/index.js.map +1 -1
- package/dist/lib/ai/context.js +91 -91
- package/dist/lib/compose-generator.d.ts +36 -0
- package/dist/lib/compose-generator.js +152 -0
- package/dist/lib/compose-generator.js.map +1 -0
- package/dist/lib/config-schema.d.ts +18 -0
- package/dist/lib/config-schema.js +4 -0
- package/dist/lib/config-schema.js.map +1 -1
- package/dist/utils/safe-exit.d.ts +23 -0
- package/dist/utils/safe-exit.js +53 -1
- package/dist/utils/safe-exit.js.map +1 -1
- package/package.json +1 -1
- package/templates/auth/callback.ts +22 -22
- package/templates/auth/sign-in.tsx +41 -41
- package/templates/billing/checkout.ts +38 -22
- package/templates/billing/page.tsx +84 -43
- package/templates/billing/webhooks.ts +83 -0
- package/templates/dev/Dockerfile.agent +39 -0
- package/templates/dev/agent-entrypoint.sh +41 -0
- package/templates/dev/docker-compose.dev.yml +63 -0
- package/templates/support/ticket-form.tsx +68 -68
- package/templates/usage/track.ts +30 -30
- package/templates/users/profile.tsx +43 -43
- package/dist/commands/fn.d.ts +0 -6
- package/dist/commands/fn.js +0 -88
- package/dist/commands/fn.js.map +0 -1
- package/dist/commands/marketplace.d.ts +0 -36
- package/dist/commands/marketplace.js +0 -585
- package/dist/commands/marketplace.js.map +0 -1
- package/dist/lib/code-agent-neetru-tools.d.ts +0 -15
- package/dist/lib/code-agent-neetru-tools.js +0 -571
- package/dist/lib/code-agent-neetru-tools.js.map +0 -1
- package/dist/lib/code-agent-report.d.ts +0 -22
- package/dist/lib/code-agent-report.js +0 -43
- package/dist/lib/code-agent-report.js.map +0 -1
|
@@ -1,43 +1,84 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Template — `src/
|
|
3
|
-
*
|
|
4
|
-
* Página
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
}
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
1
|
+
/**
|
|
2
|
+
* Template — `src/app/(marketing)/billing/page.tsx` (gerado por `neetru add billing`).
|
|
3
|
+
*
|
|
4
|
+
* Página de planos: lista os planos do produto e usa <CheckoutLink> do SDK
|
|
5
|
+
* pra redirecionar ao Stripe. O <CheckoutLink> usa OIDC — o usuário precisa
|
|
6
|
+
* estar logado (veja `neetru add auth`).
|
|
7
|
+
*
|
|
8
|
+
* Pré-requisito: NEXT_PUBLIC_NEETRU_API_KEY e NEXT_PUBLIC_NEETRU_PRODUCT_ID
|
|
9
|
+
* no .env.local.
|
|
10
|
+
*/
|
|
11
|
+
'use client';
|
|
12
|
+
|
|
13
|
+
import { createNeetruClient } from '@neetru/sdk';
|
|
14
|
+
import { CheckoutLink } from '@neetru/sdk/react';
|
|
15
|
+
import { useEffect, useState } from 'react';
|
|
16
|
+
|
|
17
|
+
const client = createNeetruClient({
|
|
18
|
+
apiKey: process.env.NEXT_PUBLIC_NEETRU_API_KEY!,
|
|
19
|
+
productId: process.env.NEXT_PUBLIC_NEETRU_PRODUCT_ID!,
|
|
20
|
+
});
|
|
21
|
+
|
|
22
|
+
interface Plan {
|
|
23
|
+
id: string;
|
|
24
|
+
name: string;
|
|
25
|
+
description: string;
|
|
26
|
+
price: string;
|
|
27
|
+
slug: string;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export default function BillingPage() {
|
|
31
|
+
const [plans, setPlans] = useState<Plan[]>([]);
|
|
32
|
+
const [loading, setLoading] = useState(true);
|
|
33
|
+
|
|
34
|
+
useEffect(() => {
|
|
35
|
+
client.catalog
|
|
36
|
+
.list()
|
|
37
|
+
.then(({ products }) => {
|
|
38
|
+
const product = products.find(
|
|
39
|
+
(p) => p.id === process.env.NEXT_PUBLIC_NEETRU_PRODUCT_ID,
|
|
40
|
+
);
|
|
41
|
+
setPlans(
|
|
42
|
+
product?.plans?.map((plan: { id: string; name: string; description?: string; amount?: number; interval?: string; slug?: string }) => ({
|
|
43
|
+
id: plan.id,
|
|
44
|
+
name: plan.name,
|
|
45
|
+
description: plan.description ?? '',
|
|
46
|
+
price: plan.amount ? `R$ ${(plan.amount / 100).toFixed(2)}/${plan.interval ?? 'mês'}` : 'Sob consulta',
|
|
47
|
+
slug: plan.slug ?? plan.id,
|
|
48
|
+
})) ?? [],
|
|
49
|
+
);
|
|
50
|
+
})
|
|
51
|
+
.catch(console.error)
|
|
52
|
+
.finally(() => setLoading(false));
|
|
53
|
+
}, []);
|
|
54
|
+
|
|
55
|
+
if (loading) return <main className="p-8"><p>Carregando planos...</p></main>;
|
|
56
|
+
|
|
57
|
+
return (
|
|
58
|
+
<main className="p-8">
|
|
59
|
+
<h1 className="text-3xl font-bold mb-6">Planos</h1>
|
|
60
|
+
<div className="grid gap-4 md:grid-cols-3">
|
|
61
|
+
{plans.map((plan) => (
|
|
62
|
+
<article key={plan.id} className="border p-6 flex flex-col gap-4">
|
|
63
|
+
<div>
|
|
64
|
+
<h2 className="text-xl font-semibold">{plan.name}</h2>
|
|
65
|
+
<p className="text-2xl font-bold mt-2">{plan.price}</p>
|
|
66
|
+
{plan.description && (
|
|
67
|
+
<p className="text-sm text-zinc-600 mt-1">{plan.description}</p>
|
|
68
|
+
)}
|
|
69
|
+
</div>
|
|
70
|
+
<CheckoutLink
|
|
71
|
+
client={client}
|
|
72
|
+
productId={process.env.NEXT_PUBLIC_NEETRU_PRODUCT_ID!}
|
|
73
|
+
planId={plan.id}
|
|
74
|
+
callbackUrl={`${typeof window !== 'undefined' ? window.location.origin : ''}/billing/success`}
|
|
75
|
+
className="inline-block mt-auto px-4 py-2 bg-zinc-900 text-white text-center"
|
|
76
|
+
>
|
|
77
|
+
Assinar
|
|
78
|
+
</CheckoutLink>
|
|
79
|
+
</article>
|
|
80
|
+
))}
|
|
81
|
+
</div>
|
|
82
|
+
</main>
|
|
83
|
+
);
|
|
84
|
+
}
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Template — `src/app/api/webhooks/neetru/route.ts` (gerado por `neetru add billing`).
|
|
3
|
+
*
|
|
4
|
+
* Recebe webhooks do Neetru Core: subscription.activated, .cancelled,
|
|
5
|
+
* .payment_failed, .trial_ending e outros.
|
|
6
|
+
*
|
|
7
|
+
* O secret HMAC deve bater com o registrado via `neetru webhooks register`
|
|
8
|
+
* ou com NEETRU_WEBHOOK_SECRET no .env.local (dev).
|
|
9
|
+
*
|
|
10
|
+
* Pré-requisito: registrar o endpoint via:
|
|
11
|
+
* neetru webhooks register --product-id <id> --url <URL> --events subscription.activated,subscription.cancelled,subscription.payment_failed
|
|
12
|
+
*/
|
|
13
|
+
import { NextRequest, NextResponse } from 'next/server';
|
|
14
|
+
import { verifyWebhookRequest } from '@neetru/sdk/webhooks';
|
|
15
|
+
|
|
16
|
+
export const runtime = 'nodejs';
|
|
17
|
+
|
|
18
|
+
const WEBHOOK_SECRET = process.env.NEETRU_WEBHOOK_SECRET;
|
|
19
|
+
|
|
20
|
+
interface WebhookPayload {
|
|
21
|
+
type: string;
|
|
22
|
+
data: Record<string, unknown>;
|
|
23
|
+
timestamp?: string;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
async function handleSubscriptionActivated(data: Record<string, unknown>) {
|
|
27
|
+
const { subscriptionId, productId, tenantId } = data as Record<string, string>;
|
|
28
|
+
console.log('Assinatura ativada:', { subscriptionId, productId, tenantId });
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
async function handleSubscriptionCancelled(data: Record<string, unknown>) {
|
|
32
|
+
const { subscriptionId, productId, tenantId } = data as Record<string, string>;
|
|
33
|
+
console.log('Assinatura cancelada:', { subscriptionId, productId, tenantId });
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
async function handleSubscriptionPaymentFailed(data: Record<string, unknown>) {
|
|
37
|
+
const { subscriptionId, productId, tenantId } = data as Record<string, string>;
|
|
38
|
+
console.log('Pagamento falhou:', { subscriptionId, productId, tenantId });
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
async function handleTrialEnding(data: Record<string, unknown>) {
|
|
42
|
+
const { subscriptionId, productId, tenantId } = data as Record<string, string>;
|
|
43
|
+
console.log('Trial terminando:', { subscriptionId, productId, tenantId });
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
const HANDLERS: Record<string, (data: Record<string, unknown>) => Promise<void>> = {
|
|
47
|
+
'subscription.activated': handleSubscriptionActivated,
|
|
48
|
+
'subscription.cancelled': handleSubscriptionCancelled,
|
|
49
|
+
'subscription.payment_failed': handleSubscriptionPaymentFailed,
|
|
50
|
+
'subscription.trial_ending': handleTrialEnding,
|
|
51
|
+
};
|
|
52
|
+
|
|
53
|
+
export async function POST(request: NextRequest) {
|
|
54
|
+
if (!WEBHOOK_SECRET) {
|
|
55
|
+
return NextResponse.json(
|
|
56
|
+
{ error: 'NEETRU_WEBHOOK_SECRET não configurado' },
|
|
57
|
+
{ status: 500 },
|
|
58
|
+
);
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
const rawBody = await request.text();
|
|
62
|
+
const signature = request.headers.get('x-neetru-signature') ?? '';
|
|
63
|
+
const timestamp = request.headers.get('x-neetru-timestamp') ?? '';
|
|
64
|
+
|
|
65
|
+
const isValid = verifyWebhookRequest(rawBody, signature, WEBHOOK_SECRET, { timestamp });
|
|
66
|
+
if (!isValid) {
|
|
67
|
+
return NextResponse.json({ error: 'invalid_signature' }, { status: 401 });
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
let payload: WebhookPayload;
|
|
71
|
+
try {
|
|
72
|
+
payload = JSON.parse(rawBody);
|
|
73
|
+
} catch {
|
|
74
|
+
return NextResponse.json({ error: 'invalid_json' }, { status: 400 });
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
const handler = HANDLERS[payload.type];
|
|
78
|
+
if (handler) {
|
|
79
|
+
await handler(payload.data);
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
return NextResponse.json({ ok: true });
|
|
83
|
+
}
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
# Neetru Agent — modo dev (Docker container, sem systemd)
|
|
2
|
+
#
|
|
3
|
+
# Em produção, o agente roda como systemd service na VM.
|
|
4
|
+
# Em dev-local, roda como processo Node direto no container.
|
|
5
|
+
# O deploy do app SaaS é feito como child process (não systemd unit).
|
|
6
|
+
#
|
|
7
|
+
# Build:
|
|
8
|
+
# docker build -f Dockerfile.agent -t neetru-agent-dev .
|
|
9
|
+
|
|
10
|
+
FROM node:22-slim
|
|
11
|
+
|
|
12
|
+
# Dependências do agente em produção (apt, certbot, etc.)
|
|
13
|
+
RUN apt-get update && apt-get install -y --no-install-recommends \
|
|
14
|
+
curl ca-certificates systemctl \
|
|
15
|
+
&& rm -rf /var/lib/apt/lists/*
|
|
16
|
+
|
|
17
|
+
# Diretório do agente
|
|
18
|
+
WORKDIR /opt/neetru
|
|
19
|
+
|
|
20
|
+
# Baixa o agente mais recente do Core
|
|
21
|
+
ARG CORE_URL=https://core.neetru.com
|
|
22
|
+
ENV NEETRU_CORE_URL=${CORE_URL}
|
|
23
|
+
|
|
24
|
+
# Download do binário (fallback: usa curl se GCS não disponível)
|
|
25
|
+
RUN ARCH=$(uname -m | sed 's/x86_64/x64/;s/aarch64/arm64/') && \
|
|
26
|
+
curl -fsSL -o /usr/local/bin/neetru-agent \
|
|
27
|
+
"${NEETRU_CORE_URL}/releases/latest/neetru-agent-linux-${ARCH}" || \
|
|
28
|
+
echo "Agente não encontrado no Core — será montado via volume" && \
|
|
29
|
+
chmod +x /usr/local/bin/neetru-agent || true
|
|
30
|
+
|
|
31
|
+
# Entrypoint: agente em modo dev
|
|
32
|
+
# - NEETRU_DEV_MODE=true desabilita systemd, usa child_process direto
|
|
33
|
+
# - NEETRU_APP_PORT define a porta onde o app SaaS vai rodar
|
|
34
|
+
COPY templates/dev/agent-entrypoint.sh /usr/local/bin/neetru-agent-entrypoint
|
|
35
|
+
RUN chmod +x /usr/local/bin/neetru-agent-entrypoint
|
|
36
|
+
|
|
37
|
+
EXPOSE 3000
|
|
38
|
+
|
|
39
|
+
ENTRYPOINT ["/usr/local/bin/neetru-agent-entrypoint"]
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
#!/bin/bash
|
|
2
|
+
# Neetru Agent — entrypoint modo dev (Docker container)
|
|
3
|
+
#
|
|
4
|
+
# Em modo dev, o agente NAO usa systemd. Em vez disso:
|
|
5
|
+
# - Deploy = child process (npm install + node server)
|
|
6
|
+
# - Funções = child process (neetru-functions)
|
|
7
|
+
# - Healthcheck = /health endpoint no próprio agente
|
|
8
|
+
#
|
|
9
|
+
# Env vars obrigatórias:
|
|
10
|
+
# NEETRU_SERVER_ID — id do servidor (ex: dev-local-meuproduto)
|
|
11
|
+
# NEETRU_CORE_URL — URL do Core (ex: https://core.neetru.com)
|
|
12
|
+
# NEETRU_FIREBASE_PROJECT — projeto Firebase
|
|
13
|
+
# NEETRU_DEV_MODE — "true" ativa modo dev
|
|
14
|
+
|
|
15
|
+
set -e
|
|
16
|
+
|
|
17
|
+
echo "═══════════════════════════════════════════"
|
|
18
|
+
echo " Neetru Agent — Dev Mode"
|
|
19
|
+
echo " Server: ${NEETRU_SERVER_ID}"
|
|
20
|
+
echo " Core: ${NEETRU_CORE_URL}"
|
|
21
|
+
echo "═══════════════════════════════════════════"
|
|
22
|
+
|
|
23
|
+
# Verifica binário
|
|
24
|
+
if [ ! -f /usr/local/bin/neetru-agent ]; then
|
|
25
|
+
echo "[AGENT] Binário não encontrado. Montando via volume..."
|
|
26
|
+
echo "[AGENT] Certifique-se de que o agente está em /opt/neetru/neetru-agent"
|
|
27
|
+
ls -la /opt/neetru/ || true
|
|
28
|
+
fi
|
|
29
|
+
|
|
30
|
+
# Cria diretórios necessários
|
|
31
|
+
mkdir -p /var/cache/neetru /opt/neetru/deploys
|
|
32
|
+
|
|
33
|
+
# Inicia o agente
|
|
34
|
+
echo "[AGENT] Iniciando agente Neetru..."
|
|
35
|
+
exec node /usr/local/bin/neetru-agent 2>&1 || exec node /opt/neetru/neetru-agent 2>&1 || {
|
|
36
|
+
echo "[AGENT] ERRO: Não foi possível iniciar o agente."
|
|
37
|
+
echo "[AGENT] Verifique se o binário existe em /usr/local/bin/neetru-agent"
|
|
38
|
+
echo "[AGENT] ou monte o binário via volume em /opt/neetru/neetru-agent"
|
|
39
|
+
sleep 10
|
|
40
|
+
exit 1
|
|
41
|
+
}
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
# Neetru Dev Local — emulador de VM ({{ENGINE}})
|
|
2
|
+
# Gerado por `neetru dev up`. Modifique o neetru.config.json e rode novamente.
|
|
3
|
+
#
|
|
4
|
+
# Este compose simula uma VM de produção Neetru:
|
|
5
|
+
# 1. Banco de dados ({{ENGINE}}) = mesmo engine que vai pra produção
|
|
6
|
+
# 2. Agente Neetru = recebe comandos do Core, faz deploy, npm install, healthcheck
|
|
7
|
+
# 3. App SaaS = deployado pelo agente, acessível em http://localhost:{{APP_PORT}}
|
|
8
|
+
#
|
|
9
|
+
# Uso:
|
|
10
|
+
# neetru dev up # gera este compose + sobe tudo
|
|
11
|
+
# docker compose -f neetru-compose.dev.yml up -d # manual
|
|
12
|
+
# docker compose -f neetru-compose.dev.yml down # desliga
|
|
13
|
+
|
|
14
|
+
services:
|
|
15
|
+
# ── Banco de dados ({{ENGINE}}) ────────────────────────────────────────────
|
|
16
|
+
database:
|
|
17
|
+
image: {{DB_IMAGE}}
|
|
18
|
+
container_name: neetru-dev-db
|
|
19
|
+
environment:
|
|
20
|
+
{{DB_ENV}}
|
|
21
|
+
ports:
|
|
22
|
+
- "{{DB_PORT}}:{{DB_INTERNAL_PORT}}"
|
|
23
|
+
volumes:
|
|
24
|
+
- neetru-db-data:{{DB_DATA_DIR}}
|
|
25
|
+
healthcheck:
|
|
26
|
+
{{DB_HEALTHCHECK}}
|
|
27
|
+
restart: unless-stopped
|
|
28
|
+
networks:
|
|
29
|
+
- neetru-dev-net
|
|
30
|
+
|
|
31
|
+
# ── Agente Neetru (modo dev — sem systemd) ────────────────────────────────
|
|
32
|
+
agent:
|
|
33
|
+
build:
|
|
34
|
+
context: .
|
|
35
|
+
dockerfile: Dockerfile.agent
|
|
36
|
+
container_name: neetru-dev-agent
|
|
37
|
+
environment:
|
|
38
|
+
- NEETRU_SERVER_ID=dev-local-{{PRODUCT_SLUG}}
|
|
39
|
+
- NEETRU_CORE_URL={{CORE_URL}}
|
|
40
|
+
- NEETRU_FIREBASE_PROJECT={{FIREBASE_PROJECT}}
|
|
41
|
+
- NEETRU_FIREBASE_API_KEY={{FIREBASE_API_KEY}}
|
|
42
|
+
- NEETRU_DEV_MODE=true
|
|
43
|
+
- NEETRU_APP_PORT={{APP_PORT}}
|
|
44
|
+
ports:
|
|
45
|
+
- "{{APP_PORT}}:{{APP_PORT}}"
|
|
46
|
+
volumes:
|
|
47
|
+
- /var/run/docker.sock:/var/run/docker.sock
|
|
48
|
+
- neetru-agent-data:/var/cache/neetru
|
|
49
|
+
- .:/app/product:cached
|
|
50
|
+
depends_on:
|
|
51
|
+
database:
|
|
52
|
+
condition: service_healthy
|
|
53
|
+
restart: unless-stopped
|
|
54
|
+
networks:
|
|
55
|
+
- neetru-dev-net
|
|
56
|
+
|
|
57
|
+
volumes:
|
|
58
|
+
neetru-db-data:
|
|
59
|
+
neetru-agent-data:
|
|
60
|
+
|
|
61
|
+
networks:
|
|
62
|
+
neetru-dev-net:
|
|
63
|
+
driver: bridge
|
|
@@ -1,68 +1,68 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Template — `src/lib/neetru/support/ticket-form.tsx` (gerado por `neetru add support`).
|
|
3
|
-
*
|
|
4
|
-
* Form de suporte: cria ticket via SDK.
|
|
5
|
-
*/
|
|
6
|
-
'use client';
|
|
7
|
-
|
|
8
|
-
import { useState } from 'react';
|
|
9
|
-
import { createNeetruClient } from '@neetru/sdk';
|
|
10
|
-
|
|
11
|
-
const client = createNeetruClient({
|
|
12
|
-
apiKey: process.env.NEXT_PUBLIC_NEETRU_API_KEY,
|
|
13
|
-
});
|
|
14
|
-
|
|
15
|
-
export function SupportTicketForm() {
|
|
16
|
-
const [subject, setSubject] = useState('');
|
|
17
|
-
const [message, setMessage] = useState('');
|
|
18
|
-
const [submitting, setSubmitting] = useState(false);
|
|
19
|
-
const [result, setResult] = useState<string | null>(null);
|
|
20
|
-
|
|
21
|
-
async function handleSubmit(e: React.FormEvent) {
|
|
22
|
-
e.preventDefault();
|
|
23
|
-
setSubmitting(true);
|
|
24
|
-
try {
|
|
25
|
-
const ticket = await client.support.createTicket({ subject, message });
|
|
26
|
-
setResult(`Ticket criado: ${ticket.id}`);
|
|
27
|
-
setSubject('');
|
|
28
|
-
setMessage('');
|
|
29
|
-
} catch (err) {
|
|
30
|
-
setResult('Falha ao criar ticket.');
|
|
31
|
-
console.error(err);
|
|
32
|
-
} finally {
|
|
33
|
-
setSubmitting(false);
|
|
34
|
-
}
|
|
35
|
-
}
|
|
36
|
-
|
|
37
|
-
return (
|
|
38
|
-
<form onSubmit={handleSubmit} className="grid gap-3 max-w-md">
|
|
39
|
-
<label className="grid gap-1">
|
|
40
|
-
<span className="text-sm font-medium">Assunto</span>
|
|
41
|
-
<input
|
|
42
|
-
required
|
|
43
|
-
value={subject}
|
|
44
|
-
onChange={(e) => setSubject(e.target.value)}
|
|
45
|
-
className="border px-3 py-2"
|
|
46
|
-
/>
|
|
47
|
-
</label>
|
|
48
|
-
<label className="grid gap-1">
|
|
49
|
-
<span className="text-sm font-medium">Mensagem</span>
|
|
50
|
-
<textarea
|
|
51
|
-
required
|
|
52
|
-
rows={5}
|
|
53
|
-
value={message}
|
|
54
|
-
onChange={(e) => setMessage(e.target.value)}
|
|
55
|
-
className="border px-3 py-2"
|
|
56
|
-
/>
|
|
57
|
-
</label>
|
|
58
|
-
<button
|
|
59
|
-
type="submit"
|
|
60
|
-
disabled={submitting}
|
|
61
|
-
className="px-4 py-2 bg-zinc-900 text-white"
|
|
62
|
-
>
|
|
63
|
-
{submitting ? 'Enviando...' : 'Enviar'}
|
|
64
|
-
</button>
|
|
65
|
-
{result && <p className="text-sm">{result}</p>}
|
|
66
|
-
</form>
|
|
67
|
-
);
|
|
68
|
-
}
|
|
1
|
+
/**
|
|
2
|
+
* Template — `src/lib/neetru/support/ticket-form.tsx` (gerado por `neetru add support`).
|
|
3
|
+
*
|
|
4
|
+
* Form de suporte: cria ticket via SDK.
|
|
5
|
+
*/
|
|
6
|
+
'use client';
|
|
7
|
+
|
|
8
|
+
import { useState } from 'react';
|
|
9
|
+
import { createNeetruClient } from '@neetru/sdk';
|
|
10
|
+
|
|
11
|
+
const client = createNeetruClient({
|
|
12
|
+
apiKey: process.env.NEXT_PUBLIC_NEETRU_API_KEY,
|
|
13
|
+
});
|
|
14
|
+
|
|
15
|
+
export function SupportTicketForm() {
|
|
16
|
+
const [subject, setSubject] = useState('');
|
|
17
|
+
const [message, setMessage] = useState('');
|
|
18
|
+
const [submitting, setSubmitting] = useState(false);
|
|
19
|
+
const [result, setResult] = useState<string | null>(null);
|
|
20
|
+
|
|
21
|
+
async function handleSubmit(e: React.FormEvent) {
|
|
22
|
+
e.preventDefault();
|
|
23
|
+
setSubmitting(true);
|
|
24
|
+
try {
|
|
25
|
+
const ticket = await client.support.createTicket({ subject, message });
|
|
26
|
+
setResult(`Ticket criado: ${ticket.id}`);
|
|
27
|
+
setSubject('');
|
|
28
|
+
setMessage('');
|
|
29
|
+
} catch (err) {
|
|
30
|
+
setResult('Falha ao criar ticket.');
|
|
31
|
+
console.error(err);
|
|
32
|
+
} finally {
|
|
33
|
+
setSubmitting(false);
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
return (
|
|
38
|
+
<form onSubmit={handleSubmit} className="grid gap-3 max-w-md">
|
|
39
|
+
<label className="grid gap-1">
|
|
40
|
+
<span className="text-sm font-medium">Assunto</span>
|
|
41
|
+
<input
|
|
42
|
+
required
|
|
43
|
+
value={subject}
|
|
44
|
+
onChange={(e) => setSubject(e.target.value)}
|
|
45
|
+
className="border px-3 py-2"
|
|
46
|
+
/>
|
|
47
|
+
</label>
|
|
48
|
+
<label className="grid gap-1">
|
|
49
|
+
<span className="text-sm font-medium">Mensagem</span>
|
|
50
|
+
<textarea
|
|
51
|
+
required
|
|
52
|
+
rows={5}
|
|
53
|
+
value={message}
|
|
54
|
+
onChange={(e) => setMessage(e.target.value)}
|
|
55
|
+
className="border px-3 py-2"
|
|
56
|
+
/>
|
|
57
|
+
</label>
|
|
58
|
+
<button
|
|
59
|
+
type="submit"
|
|
60
|
+
disabled={submitting}
|
|
61
|
+
className="px-4 py-2 bg-zinc-900 text-white"
|
|
62
|
+
>
|
|
63
|
+
{submitting ? 'Enviando...' : 'Enviar'}
|
|
64
|
+
</button>
|
|
65
|
+
{result && <p className="text-sm">{result}</p>}
|
|
66
|
+
</form>
|
|
67
|
+
);
|
|
68
|
+
}
|
package/templates/usage/track.ts
CHANGED
|
@@ -1,30 +1,30 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Template — `src/lib/neetru/usage/track.ts` (gerado por `neetru add usage`).
|
|
3
|
-
*
|
|
4
|
-
* Helper para reportar uso de resources metered ao Core.
|
|
5
|
-
*
|
|
6
|
-
* @example
|
|
7
|
-
* ```ts
|
|
8
|
-
* import { reportApiCall } from '@/lib/neetru/usage/track';
|
|
9
|
-
* await reportApiCall();
|
|
10
|
-
* ```
|
|
11
|
-
*/
|
|
12
|
-
import { createNeetruClient } from '@neetru/sdk';
|
|
13
|
-
|
|
14
|
-
const client = createNeetruClient({
|
|
15
|
-
apiKey: process.env.NEETRU_API_KEY,
|
|
16
|
-
productId: process.env.NEETRU_PRODUCT_ID,
|
|
17
|
-
tenantId: process.env.NEETRU_TENANT_ID,
|
|
18
|
-
});
|
|
19
|
-
|
|
20
|
-
export async function reportApiCall(qty = 1): Promise<void> {
|
|
21
|
-
try {
|
|
22
|
-
await client.usage.report('api_calls', qty);
|
|
23
|
-
} catch (err) {
|
|
24
|
-
console.warn('usage.report failed', err);
|
|
25
|
-
}
|
|
26
|
-
}
|
|
27
|
-
|
|
28
|
-
export async function checkApiCallLimit() {
|
|
29
|
-
return client.usage.check('api_calls');
|
|
30
|
-
}
|
|
1
|
+
/**
|
|
2
|
+
* Template — `src/lib/neetru/usage/track.ts` (gerado por `neetru add usage`).
|
|
3
|
+
*
|
|
4
|
+
* Helper para reportar uso de resources metered ao Core.
|
|
5
|
+
*
|
|
6
|
+
* @example
|
|
7
|
+
* ```ts
|
|
8
|
+
* import { reportApiCall } from '@/lib/neetru/usage/track';
|
|
9
|
+
* await reportApiCall();
|
|
10
|
+
* ```
|
|
11
|
+
*/
|
|
12
|
+
import { createNeetruClient } from '@neetru/sdk';
|
|
13
|
+
|
|
14
|
+
const client = createNeetruClient({
|
|
15
|
+
apiKey: process.env.NEETRU_API_KEY,
|
|
16
|
+
productId: process.env.NEETRU_PRODUCT_ID,
|
|
17
|
+
tenantId: process.env.NEETRU_TENANT_ID,
|
|
18
|
+
});
|
|
19
|
+
|
|
20
|
+
export async function reportApiCall(qty = 1): Promise<void> {
|
|
21
|
+
try {
|
|
22
|
+
await client.usage.report('api_calls', qty);
|
|
23
|
+
} catch (err) {
|
|
24
|
+
console.warn('usage.report failed', err);
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export async function checkApiCallLimit() {
|
|
29
|
+
return client.usage.check('api_calls');
|
|
30
|
+
}
|
|
@@ -1,43 +1,43 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Template — `src/lib/neetru/users/profile.tsx` (gerado por `neetru add users`).
|
|
3
|
-
*
|
|
4
|
-
* Componente de perfil mostra dados do user via SDK.
|
|
5
|
-
*/
|
|
6
|
-
'use client';
|
|
7
|
-
|
|
8
|
-
import { useEffect, useState } from 'react';
|
|
9
|
-
import { createNeetruClient, type NeetruUser } from '@neetru/sdk';
|
|
10
|
-
|
|
11
|
-
const client = createNeetruClient({
|
|
12
|
-
apiKey: process.env.NEXT_PUBLIC_NEETRU_API_KEY,
|
|
13
|
-
});
|
|
14
|
-
|
|
15
|
-
export function UserProfile() {
|
|
16
|
-
const [user, setUser] = useState<NeetruUser | null>(null);
|
|
17
|
-
useEffect(() => {
|
|
18
|
-
return client.auth.onAuthStateChanged(setUser);
|
|
19
|
-
}, []);
|
|
20
|
-
|
|
21
|
-
if (!user) return <p>Não autenticado.</p>;
|
|
22
|
-
|
|
23
|
-
return (
|
|
24
|
-
<div className="border p-4">
|
|
25
|
-
<p>
|
|
26
|
-
<strong>Nome:</strong> {user.displayName ?? '—'}
|
|
27
|
-
</p>
|
|
28
|
-
<p>
|
|
29
|
-
<strong>Email:</strong> {user.email}
|
|
30
|
-
</p>
|
|
31
|
-
<p>
|
|
32
|
-
<strong>UID:</strong> {user.uid}
|
|
33
|
-
</p>
|
|
34
|
-
<button
|
|
35
|
-
type="button"
|
|
36
|
-
onClick={() => client.auth.signOut()}
|
|
37
|
-
className="mt-4 px-3 py-1 border"
|
|
38
|
-
>
|
|
39
|
-
Sair
|
|
40
|
-
</button>
|
|
41
|
-
</div>
|
|
42
|
-
);
|
|
43
|
-
}
|
|
1
|
+
/**
|
|
2
|
+
* Template — `src/lib/neetru/users/profile.tsx` (gerado por `neetru add users`).
|
|
3
|
+
*
|
|
4
|
+
* Componente de perfil mostra dados do user via SDK.
|
|
5
|
+
*/
|
|
6
|
+
'use client';
|
|
7
|
+
|
|
8
|
+
import { useEffect, useState } from 'react';
|
|
9
|
+
import { createNeetruClient, type NeetruUser } from '@neetru/sdk';
|
|
10
|
+
|
|
11
|
+
const client = createNeetruClient({
|
|
12
|
+
apiKey: process.env.NEXT_PUBLIC_NEETRU_API_KEY,
|
|
13
|
+
});
|
|
14
|
+
|
|
15
|
+
export function UserProfile() {
|
|
16
|
+
const [user, setUser] = useState<NeetruUser | null>(null);
|
|
17
|
+
useEffect(() => {
|
|
18
|
+
return client.auth.onAuthStateChanged(setUser);
|
|
19
|
+
}, []);
|
|
20
|
+
|
|
21
|
+
if (!user) return <p>Não autenticado.</p>;
|
|
22
|
+
|
|
23
|
+
return (
|
|
24
|
+
<div className="border p-4">
|
|
25
|
+
<p>
|
|
26
|
+
<strong>Nome:</strong> {user.displayName ?? '—'}
|
|
27
|
+
</p>
|
|
28
|
+
<p>
|
|
29
|
+
<strong>Email:</strong> {user.email}
|
|
30
|
+
</p>
|
|
31
|
+
<p>
|
|
32
|
+
<strong>UID:</strong> {user.uid}
|
|
33
|
+
</p>
|
|
34
|
+
<button
|
|
35
|
+
type="button"
|
|
36
|
+
onClick={() => client.auth.signOut()}
|
|
37
|
+
className="mt-4 px-3 py-1 border"
|
|
38
|
+
>
|
|
39
|
+
Sair
|
|
40
|
+
</button>
|
|
41
|
+
</div>
|
|
42
|
+
);
|
|
43
|
+
}
|