Merge remote-tracking branch 'origin/main'
Integrate 6 remote commits: platform health endpoint, web/intelligence modules, PR API, Redis compose, KiCad CI exports, README docs update. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -3,3 +3,7 @@ EDA_QUEUE_NAME=yiacad-eda
|
||||
KICANVAS_BUNDLE_URL=https://kicanvas.org/kicanvas/kicanvas.js
|
||||
KIBOT_CONFIG=/absolute/path/to/kibot.yaml
|
||||
KIAUTO_BIN=kiauto
|
||||
|
||||
# GitHub PR integration
|
||||
GITHUB_TOKEN=ghp_...
|
||||
GITHUB_REPO=electron-rare/Kill_LIFE
|
||||
|
||||
@@ -0,0 +1,60 @@
|
||||
import { NextResponse } from "next/server";
|
||||
import { getEdaQueue } from "@/lib/eda-queue";
|
||||
|
||||
export const runtime = "nodejs";
|
||||
|
||||
type ProbeResult = {
|
||||
status: "up" | "degraded" | "down";
|
||||
latency_ms?: number;
|
||||
detail?: string | number;
|
||||
};
|
||||
|
||||
async function probeUrl(url: string, timeoutMs = 1500): Promise<ProbeResult> {
|
||||
const t0 = Date.now();
|
||||
try {
|
||||
const res = await fetch(url, { signal: AbortSignal.timeout(timeoutMs) });
|
||||
return { status: res.ok ? "up" : "degraded", latency_ms: Date.now() - t0 };
|
||||
} catch {
|
||||
return { status: "down", latency_ms: Date.now() - t0 };
|
||||
}
|
||||
}
|
||||
|
||||
async function probeQueue(): Promise<ProbeResult> {
|
||||
try {
|
||||
const queue = getEdaQueue();
|
||||
const counts = await queue.getJobCounts("wait", "active", "failed");
|
||||
const depth = counts.wait ?? 0;
|
||||
const active = counts.active ?? 0;
|
||||
const failed = counts.failed ?? 0;
|
||||
const status = failed > 10 ? "degraded" : "up";
|
||||
return { status, detail: depth, ...(active ? { active } : {}), ...(failed ? { failed } : {}) } as ProbeResult & Record<string, unknown>;
|
||||
} catch (err) {
|
||||
return { status: "down", detail: String(err) };
|
||||
}
|
||||
}
|
||||
|
||||
export async function GET() {
|
||||
const yjsUrl =
|
||||
process.env.YJS_WS_HTTP_URL ??
|
||||
`http://localhost:${process.env.YJS_WS_PORT ?? "1234"}/`;
|
||||
const nextUrl = `http://localhost:${process.env.PORT ?? "3000"}/`;
|
||||
|
||||
const [nextProbe, yjsProbe, queueProbe] = await Promise.all([
|
||||
probeUrl(nextUrl),
|
||||
probeUrl(yjsUrl),
|
||||
probeQueue(),
|
||||
]);
|
||||
|
||||
const probes = {
|
||||
"next-js": nextProbe,
|
||||
"yjs-realtime": yjsProbe,
|
||||
"eda-queue": queueProbe,
|
||||
};
|
||||
|
||||
const upCount = Object.values(probes).filter((p) => p.status === "up").length;
|
||||
const total = Object.keys(probes).length;
|
||||
const status =
|
||||
upCount === total ? "up" : upCount === 0 ? "down" : "degraded";
|
||||
|
||||
return NextResponse.json({ status, up_count: upCount, total, probes });
|
||||
}
|
||||
@@ -3,7 +3,7 @@
|
||||
import dynamic from "next/dynamic";
|
||||
import { startTransition, useEffect, useRef, useState } from "react";
|
||||
import { useYjsExcalidraw } from "@/lib/use-yjs-excalidraw";
|
||||
import type { ExcalidrawImperativeAPI } from "@excalidraw/excalidraw/types/types";
|
||||
import type { ExcalidrawImperativeAPI } from "@excalidraw/excalidraw/types";
|
||||
|
||||
const Excalidraw = dynamic(
|
||||
async () => {
|
||||
|
||||
@@ -0,0 +1,12 @@
|
||||
services:
|
||||
redis:
|
||||
image: redis:7-alpine
|
||||
container_name: yiacad-redis-dev
|
||||
restart: unless-stopped
|
||||
ports:
|
||||
- "127.0.0.1:6379:6379"
|
||||
volumes:
|
||||
- yiacad-redis-data:/data
|
||||
|
||||
volumes:
|
||||
yiacad-redis-data:
|
||||
@@ -0,0 +1,106 @@
|
||||
/**
|
||||
* T-AI-323 — Intelligence health aggregator for Lot 23 (YiACAD Git-EDA Platform).
|
||||
*
|
||||
* Surfaces queue / worker / realtime status into a single structured payload
|
||||
* consumable by intelligence_tui and runtime_ai_gateway.
|
||||
*/
|
||||
|
||||
export type ServiceStatus = "ok" | "degraded" | "unavailable";
|
||||
|
||||
export interface QueueHealth {
|
||||
status: ServiceStatus;
|
||||
waiting: number;
|
||||
active: number;
|
||||
failed: number;
|
||||
latencyMs?: number;
|
||||
}
|
||||
|
||||
export interface WorkerHealth {
|
||||
status: ServiceStatus;
|
||||
running: boolean;
|
||||
lastJobId?: string;
|
||||
lastJobMs?: number;
|
||||
}
|
||||
|
||||
export interface RealtimeHealth {
|
||||
status: ServiceStatus;
|
||||
connected: boolean;
|
||||
clients: number;
|
||||
roomCount: number;
|
||||
}
|
||||
|
||||
export interface WebPlatformHealth {
|
||||
timestamp: string;
|
||||
overall: ServiceStatus;
|
||||
queue: QueueHealth;
|
||||
worker: WorkerHealth;
|
||||
realtime: RealtimeHealth;
|
||||
}
|
||||
|
||||
const QUEUE_API = process.env.EDA_QUEUE_URL ?? "http://localhost:3000/api/ops/queue";
|
||||
const WORKER_API = process.env.EDA_WORKER_URL ?? "http://localhost:3000/api/ops/worker";
|
||||
const REALTIME_API = process.env.YJS_HEALTH_URL ?? "http://localhost:1234/health";
|
||||
|
||||
async function fetchJson<T>(url: string, timeoutMs = 1500): Promise<T | null> {
|
||||
try {
|
||||
const res = await fetch(url, { signal: AbortSignal.timeout(timeoutMs) });
|
||||
if (!res.ok) return null;
|
||||
return (await res.json()) as T;
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
async function getQueueHealth(): Promise<QueueHealth> {
|
||||
const data = await fetchJson<Record<string, unknown>>(QUEUE_API);
|
||||
if (!data) return { status: "unavailable", waiting: 0, active: 0, failed: 0 };
|
||||
const waiting = typeof data.waiting === "number" ? data.waiting : 0;
|
||||
const active = typeof data.active === "number" ? data.active : 0;
|
||||
const failed = typeof data.failed === "number" ? data.failed : 0;
|
||||
const latencyMs = typeof data.latencyMs === "number" ? data.latencyMs : undefined;
|
||||
const status: ServiceStatus = failed > 10 ? "degraded" : "ok";
|
||||
return { status, waiting, active, failed, latencyMs };
|
||||
}
|
||||
|
||||
async function getWorkerHealth(): Promise<WorkerHealth> {
|
||||
const data = await fetchJson<Record<string, unknown>>(WORKER_API);
|
||||
if (!data) return { status: "unavailable", running: false };
|
||||
const running = data.running === true;
|
||||
const lastJobId = typeof data.lastJobId === "string" ? data.lastJobId : undefined;
|
||||
const lastJobMs = typeof data.lastJobMs === "number" ? data.lastJobMs : undefined;
|
||||
return { status: running ? "ok" : "degraded", running, lastJobId, lastJobMs };
|
||||
}
|
||||
|
||||
async function getRealtimeHealth(): Promise<RealtimeHealth> {
|
||||
const data = await fetchJson<Record<string, unknown>>(REALTIME_API);
|
||||
if (!data) return { status: "unavailable", connected: false, clients: 0, roomCount: 0 };
|
||||
const clients = typeof data.clients === "number" ? data.clients : 0;
|
||||
const roomCount = typeof data.rooms === "number" ? data.rooms : 0;
|
||||
return { status: "ok", connected: true, clients, roomCount };
|
||||
}
|
||||
|
||||
function roll(statuses: ServiceStatus[]): ServiceStatus {
|
||||
if (statuses.includes("unavailable")) return "degraded";
|
||||
if (statuses.includes("degraded")) return "degraded";
|
||||
return "ok";
|
||||
}
|
||||
|
||||
/**
|
||||
* Aggregate health from queue, worker, and realtime services.
|
||||
* Safe to call from Next.js API routes (server-side only).
|
||||
*/
|
||||
export async function getWebPlatformHealth(): Promise<WebPlatformHealth> {
|
||||
const [queue, worker, realtime] = await Promise.all([
|
||||
getQueueHealth(),
|
||||
getWorkerHealth(),
|
||||
getRealtimeHealth(),
|
||||
]);
|
||||
|
||||
return {
|
||||
timestamp: new Date().toISOString(),
|
||||
overall: roll([queue.status, worker.status, realtime.status]),
|
||||
queue,
|
||||
worker,
|
||||
realtime,
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,169 @@
|
||||
/**
|
||||
* T-AI-326 — MCP / service-first boundary for YiACAD review-assist surface.
|
||||
*
|
||||
* Defines tool contracts for:
|
||||
* - EDA worker invocation
|
||||
* - Parts search
|
||||
* - CI trigger
|
||||
* - Artifact fetch
|
||||
* - Review hints (read-only)
|
||||
*
|
||||
* All tools are READ-ONLY from the agent perspective. Write operations
|
||||
* (commit, merge, ERC fix) remain exclusively in the human operator path.
|
||||
*/
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Shared types
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
export interface McpToolResult<T = unknown> {
|
||||
ok: boolean;
|
||||
data?: T;
|
||||
error?: string;
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// EDA Worker tool
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
export type EdaJobKind = "erc" | "drc" | "bom" | "gerber" | "step";
|
||||
|
||||
export interface EdaJobRequest {
|
||||
kind: EdaJobKind;
|
||||
schematicPath: string;
|
||||
/** Optional ref (branch/commit) — defaults to HEAD */
|
||||
gitRef?: string;
|
||||
}
|
||||
|
||||
export interface EdaJobResult {
|
||||
jobId: string;
|
||||
status: "queued" | "running" | "done" | "failed";
|
||||
violationCount?: number;
|
||||
artifactUrl?: string;
|
||||
}
|
||||
|
||||
export async function enqueueEdaJob(
|
||||
req: EdaJobRequest,
|
||||
): Promise<McpToolResult<EdaJobResult>> {
|
||||
try {
|
||||
const res = await fetch("/api/eda/enqueue", {
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify(req),
|
||||
});
|
||||
if (!res.ok) return { ok: false, error: `HTTP ${res.status}` };
|
||||
return { ok: true, data: (await res.json()) as EdaJobResult };
|
||||
} catch (e) {
|
||||
return { ok: false, error: String(e) };
|
||||
}
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Parts search tool (read-only)
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
export interface PartsSearchQuery {
|
||||
query: string;
|
||||
category?: string;
|
||||
maxResults?: number;
|
||||
}
|
||||
|
||||
export interface PartResult {
|
||||
mpn: string;
|
||||
manufacturer: string;
|
||||
description: string;
|
||||
datasheet?: string;
|
||||
stock?: number;
|
||||
}
|
||||
|
||||
export async function searchParts(
|
||||
q: PartsSearchQuery,
|
||||
): Promise<McpToolResult<PartResult[]>> {
|
||||
const params = new URLSearchParams({ q: q.query });
|
||||
if (q.category) params.set("category", q.category);
|
||||
if (q.maxResults) params.set("limit", String(q.maxResults));
|
||||
try {
|
||||
const res = await fetch(`/api/parts/search?${params}`);
|
||||
if (!res.ok) return { ok: false, error: `HTTP ${res.status}` };
|
||||
return { ok: true, data: (await res.json()) as PartResult[] };
|
||||
} catch (e) {
|
||||
return { ok: false, error: String(e) };
|
||||
}
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// CI trigger tool
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
export interface CiTriggerRequest {
|
||||
workflow: "firmware" | "erc" | "drc" | "full";
|
||||
gitRef?: string;
|
||||
}
|
||||
|
||||
export interface CiRunRef {
|
||||
runId: string;
|
||||
url: string;
|
||||
status: "pending" | "running" | "success" | "failure";
|
||||
}
|
||||
|
||||
export async function triggerCi(
|
||||
req: CiTriggerRequest,
|
||||
): Promise<McpToolResult<CiRunRef>> {
|
||||
try {
|
||||
const res = await fetch("/api/ci/trigger", {
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify(req),
|
||||
});
|
||||
if (!res.ok) return { ok: false, error: `HTTP ${res.status}` };
|
||||
return { ok: true, data: (await res.json()) as CiRunRef };
|
||||
} catch (e) {
|
||||
return { ok: false, error: String(e) };
|
||||
}
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Artifact fetch tool (read-only)
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
export interface ArtifactRef {
|
||||
kind: "gerber" | "bom" | "step" | "report";
|
||||
jobId: string;
|
||||
}
|
||||
|
||||
export async function fetchArtifactUrl(
|
||||
ref: ArtifactRef,
|
||||
): Promise<McpToolResult<{ url: string; contentType: string }>> {
|
||||
try {
|
||||
const res = await fetch(`/api/artifacts/${ref.kind}/${ref.jobId}`);
|
||||
if (!res.ok) return { ok: false, error: `HTTP ${res.status}` };
|
||||
return { ok: true, data: (await res.json()) as { url: string; contentType: string } };
|
||||
} catch (e) {
|
||||
return { ok: false, error: String(e) };
|
||||
}
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Review hints (read-only, generated by intelligence overlay)
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
export interface ReviewHint {
|
||||
severity: "error" | "warning" | "info";
|
||||
component?: string;
|
||||
message: string;
|
||||
source: "erc" | "drc" | "bom" | "agent";
|
||||
}
|
||||
|
||||
export async function getReviewHints(
|
||||
schematicPath: string,
|
||||
): Promise<McpToolResult<ReviewHint[]>> {
|
||||
try {
|
||||
const res = await fetch(
|
||||
`/api/review/hints?path=${encodeURIComponent(schematicPath)}`,
|
||||
);
|
||||
if (!res.ok) return { ok: false, error: `HTTP ${res.status}` };
|
||||
return { ok: true, data: (await res.json()) as ReviewHint[] };
|
||||
} catch (e) {
|
||||
return { ok: false, error: String(e) };
|
||||
}
|
||||
}
|
||||
@@ -269,6 +269,60 @@ function buildReviewSummary(
|
||||
return `${branch ?? "detached-head"} · ${changeLabel} · latest CI ${latestRun} · ${artifacts.length} artifact(s)`;
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// GitHub PR API
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
const GITHUB_REPO =
|
||||
process.env.GITHUB_REPO ?? "electron-rare/Kill_LIFE";
|
||||
const GITHUB_API = "https://api.github.com";
|
||||
|
||||
async function fetchGitHubPRs(artifacts: ArtifactRecord[]): Promise<PullRequestRecord[] | null> {
|
||||
const token = process.env.GITHUB_TOKEN;
|
||||
if (!token) return null;
|
||||
|
||||
try {
|
||||
const resp = await fetch(`${GITHUB_API}/repos/${GITHUB_REPO}/pulls?state=open&per_page=20`, {
|
||||
headers: {
|
||||
Authorization: `Bearer ${token}`,
|
||||
Accept: "application/vnd.github+json",
|
||||
"X-GitHub-Api-Version": "2022-11-28",
|
||||
},
|
||||
signal: AbortSignal.timeout(5000),
|
||||
});
|
||||
if (!resp.ok) return null;
|
||||
|
||||
const prs = (await resp.json()) as Array<Record<string, unknown>>;
|
||||
const artifactIds = artifacts.map((a) => a.id);
|
||||
|
||||
return prs.map((pr) => {
|
||||
const files: string[] = [];
|
||||
const title = typeof pr.title === "string" ? pr.title : "";
|
||||
const sourceBranch = (pr.head as Record<string, unknown>)?.ref as string ?? "";
|
||||
const targetBranch = (pr.base as Record<string, unknown>)?.ref as string ?? "main";
|
||||
const author = ((pr.user as Record<string, unknown>)?.login as string) ?? "unknown";
|
||||
const state = typeof pr.state === "string" ? pr.state : "open";
|
||||
const number = typeof pr.number === "number" ? pr.number : 0;
|
||||
|
||||
return {
|
||||
id: String(number),
|
||||
title,
|
||||
status: state,
|
||||
author,
|
||||
hasPcbDiff: false,
|
||||
hasDiagramDiff: false,
|
||||
hasArtifactPreview: artifactIds.length > 0,
|
||||
sourceBranch,
|
||||
targetBranch,
|
||||
changedFiles: files,
|
||||
artifactIds,
|
||||
} satisfies PullRequestRecord;
|
||||
});
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
function derivePullRequests(
|
||||
branch: string | null,
|
||||
head: string | null,
|
||||
@@ -321,20 +375,22 @@ export async function getProjectSnapshot() {
|
||||
loadArtifacts(),
|
||||
getGitProjectState(REPO_ROOT, PROJECT_ROOT)
|
||||
]);
|
||||
const pullRequests = derivePullRequests(
|
||||
gitState.branch,
|
||||
gitState.head,
|
||||
gitState.author,
|
||||
gitState.changedFiles,
|
||||
ciRuns,
|
||||
artifacts
|
||||
);
|
||||
const pullRequests =
|
||||
(await fetchGitHubPRs(artifacts)) ??
|
||||
derivePullRequests(
|
||||
gitState.branch,
|
||||
gitState.head,
|
||||
gitState.author,
|
||||
gitState.changedFiles,
|
||||
ciRuns,
|
||||
artifacts
|
||||
);
|
||||
|
||||
return {
|
||||
id: `yiacad-${gitState.branch ?? "local"}`,
|
||||
name: "YiACAD Local Workspace",
|
||||
rootPath: toPosixPath(relative(REPO_ROOT, PROJECT_ROOT)),
|
||||
repoProvider: "local git read model (ready for gitea/gitlab adapter)",
|
||||
repoProvider: `github:${GITHUB_REPO}`,
|
||||
repoVisibility: "repo-backed working tree",
|
||||
repoBranch: gitState.branch,
|
||||
repoHead: gitState.head,
|
||||
|
||||
@@ -0,0 +1,75 @@
|
||||
/**
|
||||
* T-AI-325 — Git snapshot policy for Excalidraw-Yjs collaborative editing.
|
||||
*
|
||||
* Defines when Yjs realtime state is promoted to a Git commit, and how
|
||||
* conflicts between concurrent editors are resolved.
|
||||
*
|
||||
* Design decisions:
|
||||
* - Realtime sync (Yjs) is ephemeral: it keeps clients in sync but does NOT
|
||||
* automatically write to Git.
|
||||
* - Git commits are always explicit, triggered by a deliberate user action
|
||||
* ("Save to Git") or by a CI/agent event.
|
||||
* - Conflict resolution: last-write-wins within the Yjs CRDT (automatic),
|
||||
* Git-level conflicts are resolved by the snapshot author.
|
||||
*/
|
||||
|
||||
export type SnapshotTrigger =
|
||||
| "manual_save" // user clicked "Save to Git"
|
||||
| "ci_trigger" // CI pipeline requests a snapshot before running ERC/DRC
|
||||
| "agent_request"; // intelligence agent captures state for review
|
||||
|
||||
export interface GitSnapshotPolicy {
|
||||
/** Who may trigger a Git snapshot. */
|
||||
allowedTriggers: SnapshotTrigger[];
|
||||
|
||||
/**
|
||||
* Minimum time (ms) between automatic snapshots from the same session.
|
||||
* Manual saves are not rate-limited.
|
||||
*/
|
||||
minIntervalMs: number;
|
||||
|
||||
/**
|
||||
* If true, the snapshot includes the full Yjs document state as a
|
||||
* base64 blob in the commit message (for forensics).
|
||||
*/
|
||||
embedYjsState: boolean;
|
||||
|
||||
/**
|
||||
* Commit message template. Tokens: {trigger}, {author}, {timestamp}, {room}.
|
||||
*/
|
||||
commitMessageTemplate: string;
|
||||
}
|
||||
|
||||
/** Default policy for YiACAD collaborative sessions. */
|
||||
export const DEFAULT_POLICY: GitSnapshotPolicy = {
|
||||
allowedTriggers: ["manual_save", "ci_trigger", "agent_request"],
|
||||
minIntervalMs: 30_000, // 30s floor on non-manual snapshots
|
||||
embedYjsState: false,
|
||||
commitMessageTemplate: "chore(yiacad): snapshot [{trigger}] by {author} at {timestamp} — room {room}",
|
||||
};
|
||||
|
||||
export function buildCommitMessage(
|
||||
policy: GitSnapshotPolicy,
|
||||
ctx: { trigger: SnapshotTrigger; author: string; room: string },
|
||||
): string {
|
||||
return policy.commitMessageTemplate
|
||||
.replace("{trigger}", ctx.trigger)
|
||||
.replace("{author}", ctx.author)
|
||||
.replace("{timestamp}", new Date().toISOString())
|
||||
.replace("{room}", ctx.room);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns true if a snapshot may be taken given the last snapshot timestamp.
|
||||
* Manual saves always pass; other triggers are rate-limited.
|
||||
*/
|
||||
export function canSnapshot(
|
||||
trigger: SnapshotTrigger,
|
||||
policy: GitSnapshotPolicy,
|
||||
lastSnapshotAt: number | null,
|
||||
): boolean {
|
||||
if (!policy.allowedTriggers.includes(trigger)) return false;
|
||||
if (trigger === "manual_save") return true;
|
||||
if (lastSnapshotAt === null) return true;
|
||||
return Date.now() - lastSnapshotAt >= policy.minIntervalMs;
|
||||
}
|
||||
@@ -3,7 +3,7 @@
|
||||
import { useEffect, useRef, useState } from "react";
|
||||
import * as Y from "yjs";
|
||||
import { WebsocketProvider } from "y-websocket";
|
||||
import type { ExcalidrawElement } from "@excalidraw/excalidraw/types/element/types";
|
||||
import type { ExcalidrawElement } from "@excalidraw/excalidraw/element/types";
|
||||
|
||||
const WS_URL =
|
||||
typeof window !== "undefined"
|
||||
|
||||
Generated
+911
-14
File diff suppressed because it is too large
Load Diff
@@ -13,7 +13,12 @@
|
||||
},
|
||||
"dependencies": {
|
||||
"@excalidraw/excalidraw": "0.18.0",
|
||||
"@types/cors": "^2.8.19",
|
||||
"@types/express": "^5.0.6",
|
||||
"@types/ws": "^8.18.1",
|
||||
"bullmq": "5.71.0",
|
||||
"cors": "^2.8.6",
|
||||
"express": "^5.2.1",
|
||||
"graphql": "16.13.1",
|
||||
"graphql-yoga": "5.18.1",
|
||||
"next": "14.2.35",
|
||||
|
||||
+2
-1
@@ -36,6 +36,7 @@
|
||||
".next/types/**/*.ts"
|
||||
],
|
||||
"exclude": [
|
||||
"node_modules"
|
||||
"node_modules",
|
||||
"aperant"
|
||||
]
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user