feat(mcp): apps.logs — compose-aware runtime logs
Adds apps.logs MCP tool + session REST endpoint for tailing runtime
container logs. Unblocks cold-start debugging for agent-deployed
compose apps (Twenty, Cal.com, Plane, etc.) where Coolify's own
/applications/{uuid}/logs endpoint returns empty.
Architecture:
- dockerfile / nixpacks / static apps → Coolify's REST logs API
- dockercompose apps → SSH into Coolify host,
`docker logs` per service
New SSH path uses a dedicated `vibn-logs` user (docker group, no
sudo, no pty, no port-forwarding, single ed25519 key). Private key
lives in COOLIFY_SSH_PRIVATE_KEY_B64 on the vibn-frontend Coolify
app; authorized_key is installed by scripts/setup-vibn-logs-user.sh
on the Coolify host.
Tool shape:
params: { uuid, service?, lines? (default 200, max 5000) }
returns: { uuid, buildPack, source: 'coolify_api'|'ssh_docker'|'empty',
services: { [name]: { container, lines, bytes, logs, status? } },
warnings: string[], truncated: boolean }
Made-with: Cursor
This commit is contained in:
@@ -26,6 +26,7 @@ import {
|
||||
getWorkspaceGcsHmacCredentials,
|
||||
} from '@/lib/workspace-gcs';
|
||||
import { VIBN_GCS_LOCATION } from '@/lib/gcp/storage';
|
||||
import { getApplicationRuntimeLogs } from '@/lib/coolify-logs';
|
||||
import {
|
||||
deployApplication,
|
||||
getApplicationInProject,
|
||||
@@ -97,6 +98,7 @@ export async function GET() {
|
||||
'apps.deployments',
|
||||
'apps.domains.list',
|
||||
'apps.domains.set',
|
||||
'apps.logs',
|
||||
'apps.envs.list',
|
||||
'apps.envs.upsert',
|
||||
'apps.envs.delete',
|
||||
@@ -189,6 +191,8 @@ export async function POST(request: Request) {
|
||||
return await toolAppsDomainsList(principal, params);
|
||||
case 'apps.domains.set':
|
||||
return await toolAppsDomainsSet(principal, params);
|
||||
case 'apps.logs':
|
||||
return await toolAppsLogs(principal, params);
|
||||
|
||||
case 'databases.list':
|
||||
return await toolDatabasesList(principal);
|
||||
@@ -400,6 +404,34 @@ async function toolAppsDeployments(principal: Principal, params: Record<string,
|
||||
return NextResponse.json({ result: deployments });
|
||||
}
|
||||
|
||||
/**
|
||||
* Runtime logs for a Coolify app. Compose-aware:
|
||||
* - Dockerfile/nixpacks apps → Coolify's `/applications/{uuid}/logs`
|
||||
* - Compose apps → SSH into Coolify host, `docker logs` per service
|
||||
*
|
||||
* Params:
|
||||
* uuid – app uuid (required)
|
||||
* service – compose service filter (optional)
|
||||
* lines – tail lines per container, default 200, max 5000
|
||||
*/
|
||||
async function toolAppsLogs(principal: Principal, params: Record<string, any>) {
|
||||
const projectUuid = requireCoolifyProject(principal);
|
||||
if (projectUuid instanceof NextResponse) return projectUuid;
|
||||
const appUuid = String(params.uuid ?? params.appUuid ?? '').trim();
|
||||
if (!appUuid) {
|
||||
return NextResponse.json({ error: 'Param "uuid" is required' }, { status: 400 });
|
||||
}
|
||||
await getApplicationInProject(appUuid, projectUuid);
|
||||
|
||||
const linesRaw = Number(params.lines ?? 200);
|
||||
const lines = Number.isFinite(linesRaw) ? linesRaw : 200;
|
||||
const serviceRaw = params.service;
|
||||
const service = typeof serviceRaw === 'string' && serviceRaw.trim() ? serviceRaw.trim() : undefined;
|
||||
|
||||
const result = await getApplicationRuntimeLogs(appUuid, { lines, service });
|
||||
return NextResponse.json({ result });
|
||||
}
|
||||
|
||||
async function toolAppsEnvsList(principal: Principal, params: Record<string, any>) {
|
||||
const projectUuid = requireCoolifyProject(principal);
|
||||
if (projectUuid instanceof NextResponse) return projectUuid;
|
||||
|
||||
49
app/api/workspaces/[slug]/apps/[uuid]/logs/route.ts
Normal file
49
app/api/workspaces/[slug]/apps/[uuid]/logs/route.ts
Normal file
@@ -0,0 +1,49 @@
|
||||
/**
|
||||
* GET /api/workspaces/[slug]/apps/[uuid]/logs
|
||||
*
|
||||
* Runtime logs for a Coolify app. Compose-aware: Coolify's REST API
|
||||
* for non-compose build packs, SSH into the Coolify host for per-
|
||||
* service `docker logs` on compose apps.
|
||||
*
|
||||
* Query params:
|
||||
* lines – tail lines per container (default 200, max 5000)
|
||||
* service – limit to one compose service (optional)
|
||||
*/
|
||||
|
||||
import { NextResponse } from 'next/server';
|
||||
import { requireWorkspacePrincipal } from '@/lib/auth/workspace-auth';
|
||||
import { getApplicationInProject, TenantError } from '@/lib/coolify';
|
||||
import { getApplicationRuntimeLogs } from '@/lib/coolify-logs';
|
||||
|
||||
export async function GET(
|
||||
request: Request,
|
||||
{ params }: { params: Promise<{ slug: string; uuid: string }> }
|
||||
) {
|
||||
const { slug, uuid } = await params;
|
||||
const principal = await requireWorkspacePrincipal(request, { targetSlug: slug });
|
||||
if (principal instanceof NextResponse) return principal;
|
||||
|
||||
const ws = principal.workspace;
|
||||
if (!ws.coolify_project_uuid) {
|
||||
return NextResponse.json({ error: 'Workspace has no Coolify project yet' }, { status: 503 });
|
||||
}
|
||||
|
||||
const url = new URL(request.url);
|
||||
const linesRaw = Number(url.searchParams.get('lines') ?? '200');
|
||||
const lines = Number.isFinite(linesRaw) ? linesRaw : 200;
|
||||
const service = url.searchParams.get('service') ?? undefined;
|
||||
|
||||
try {
|
||||
await getApplicationInProject(uuid, ws.coolify_project_uuid);
|
||||
const result = await getApplicationRuntimeLogs(uuid, { lines, service });
|
||||
return NextResponse.json(result);
|
||||
} catch (err) {
|
||||
if (err instanceof TenantError) {
|
||||
return NextResponse.json({ error: err.message }, { status: 403 });
|
||||
}
|
||||
return NextResponse.json(
|
||||
{ error: 'Failed to fetch runtime logs', details: err instanceof Error ? err.message : String(err) },
|
||||
{ status: 502 }
|
||||
);
|
||||
}
|
||||
}
|
||||
211
lib/coolify-logs.ts
Normal file
211
lib/coolify-logs.ts
Normal file
@@ -0,0 +1,211 @@
|
||||
/**
|
||||
* Unified runtime-logs fetcher for Coolify apps.
|
||||
*
|
||||
* Coolify's `/applications/{uuid}/logs` REST endpoint works well for
|
||||
* single-container build packs (dockerfile, nixpacks, static) but
|
||||
* returns empty for `dockercompose` — it has no way to pick which of
|
||||
* N services to tail.
|
||||
*
|
||||
* For compose apps we SSH into the Coolify host and call `docker logs`
|
||||
* directly against each compose-managed container (Coolify names them
|
||||
* `{service}-{appUuid}-{shortHash}`). The SSH user is dedicated to this
|
||||
* purpose — a member of the `docker` group, no sudo, single ed25519
|
||||
* key.
|
||||
*
|
||||
* The agent-facing shape is always `{ services: {...}, source, warnings }`
|
||||
* regardless of which path was taken. That way the MCP tool response is
|
||||
* stable whether Coolify's API grows per-service support later or not.
|
||||
*/
|
||||
|
||||
import { getApplication, getApplicationRuntimeLogsFromApi, type CoolifyApplication } from '@/lib/coolify';
|
||||
import { isCoolifySshConfigured, runOnCoolifyHost } from '@/lib/coolify-ssh';
|
||||
|
||||
export type LogsSource = 'coolify_api' | 'ssh_docker' | 'empty';
|
||||
|
||||
export interface ApplicationRuntimeLogs {
|
||||
uuid: string;
|
||||
buildPack: string;
|
||||
source: LogsSource;
|
||||
/**
|
||||
* Per-service logs. For non-compose apps there's a single entry keyed
|
||||
* by the application name (or "app"). For compose apps, one entry per
|
||||
* service (`server`, `db`, `redis`, etc.).
|
||||
*/
|
||||
services: Record<string, ServiceLogs>;
|
||||
warnings: string[];
|
||||
truncated: boolean;
|
||||
}
|
||||
|
||||
export interface ServiceLogs {
|
||||
container: string | null;
|
||||
lines: number;
|
||||
bytes: number;
|
||||
logs: string;
|
||||
/** Raw `docker ps` status string when we fetched over SSH. */
|
||||
status?: string;
|
||||
}
|
||||
|
||||
const DEFAULT_LINES = 200;
|
||||
const MAX_LINES = 5000;
|
||||
|
||||
export interface GetLogsOpts {
|
||||
/** Limit to a specific compose service (ignored for non-compose apps). */
|
||||
service?: string;
|
||||
/** Number of tail lines per container. Clamped to [1, 5000]. */
|
||||
lines?: number;
|
||||
}
|
||||
|
||||
/**
|
||||
* Shell-escape a single token for bash. We never pass user input through
|
||||
* here — values are derived from Coolify state (uuids, service names) or
|
||||
* our own integer line counts — but defense-in-depth is cheap.
|
||||
*/
|
||||
function sq(s: string): string {
|
||||
return `'${s.replace(/'/g, `'\\''`)}'`;
|
||||
}
|
||||
|
||||
export async function getApplicationRuntimeLogs(
|
||||
uuid: string,
|
||||
opts: GetLogsOpts = {},
|
||||
): Promise<ApplicationRuntimeLogs> {
|
||||
const lines = Math.max(1, Math.min(opts.lines ?? DEFAULT_LINES, MAX_LINES));
|
||||
const app = await getApplication(uuid);
|
||||
const buildPack = (app.build_pack ?? 'unknown') as string;
|
||||
const warnings: string[] = [];
|
||||
|
||||
// ── Compose path (needs SSH) ─────────────────────────────────────
|
||||
if (buildPack === 'dockercompose') {
|
||||
if (!isCoolifySshConfigured()) {
|
||||
warnings.push(
|
||||
'COOLIFY_SSH_* not configured — cannot tail per-service logs for compose apps. ' +
|
||||
'Set COOLIFY_SSH_HOST / COOLIFY_SSH_PRIVATE_KEY_B64 / COOLIFY_SSH_USER to enable.',
|
||||
);
|
||||
return {
|
||||
uuid,
|
||||
buildPack,
|
||||
source: 'empty',
|
||||
services: {},
|
||||
warnings,
|
||||
truncated: false,
|
||||
};
|
||||
}
|
||||
return await fetchComposeLogsViaSsh(app, lines, opts.service, warnings);
|
||||
}
|
||||
|
||||
// ── Single-container path (Coolify API) ─────────────────────────
|
||||
try {
|
||||
const res = await getApplicationRuntimeLogsFromApi(uuid, lines);
|
||||
const raw = (res?.logs ?? '').toString();
|
||||
const serviceName = (app.name as string) || 'app';
|
||||
return {
|
||||
uuid,
|
||||
buildPack,
|
||||
source: raw ? 'coolify_api' : 'empty',
|
||||
services: {
|
||||
[serviceName]: {
|
||||
container: null,
|
||||
lines: raw ? raw.split('\n').length : 0,
|
||||
bytes: Buffer.byteLength(raw, 'utf8'),
|
||||
logs: raw,
|
||||
},
|
||||
},
|
||||
warnings: raw
|
||||
? warnings
|
||||
: [...warnings, 'Coolify returned empty logs. App may not be running yet, or this is a known Coolify limitation for this runtime.'],
|
||||
truncated: false,
|
||||
};
|
||||
} catch (err) {
|
||||
warnings.push(`coolify_api error: ${err instanceof Error ? err.message : String(err)}`);
|
||||
// Fall through to SSH if we have it — handy for dockerfile apps too
|
||||
if (!isCoolifySshConfigured()) {
|
||||
return { uuid, buildPack, source: 'empty', services: {}, warnings, truncated: false };
|
||||
}
|
||||
return await fetchComposeLogsViaSsh(app, lines, opts.service, warnings);
|
||||
}
|
||||
}
|
||||
|
||||
async function fetchComposeLogsViaSsh(
|
||||
app: CoolifyApplication,
|
||||
lines: number,
|
||||
filterService: string | undefined,
|
||||
warnings: string[],
|
||||
): Promise<ApplicationRuntimeLogs> {
|
||||
const uuid = app.uuid as string;
|
||||
const buildPack = (app.build_pack ?? 'dockercompose') as string;
|
||||
|
||||
// 1) Enumerate containers that belong to this app. Coolify's naming
|
||||
// convention always includes the app uuid in the container name
|
||||
// (either as suffix for compose or as the name itself for single-
|
||||
// container apps).
|
||||
const ps = await runOnCoolifyHost(
|
||||
`docker ps -a --filter ${sq('name=' + uuid)} --format '{{.Names}}\t{{.Status}}'`,
|
||||
{ timeoutMs: 8_000 },
|
||||
);
|
||||
if (ps.code !== 0) {
|
||||
warnings.push(`docker ps exited ${ps.code}: ${ps.stderr.trim()}`);
|
||||
return { uuid, buildPack, source: 'empty', services: {}, warnings, truncated: ps.truncated };
|
||||
}
|
||||
|
||||
const containers = ps.stdout
|
||||
.split('\n')
|
||||
.map(l => l.trim())
|
||||
.filter(Boolean)
|
||||
.map(l => {
|
||||
const [name, ...rest] = l.split('\t');
|
||||
return { name, status: rest.join('\t') };
|
||||
});
|
||||
|
||||
if (containers.length === 0) {
|
||||
warnings.push('No containers found on Coolify host for this app uuid. Deployment may not have started yet.');
|
||||
return { uuid, buildPack, source: 'ssh_docker', services: {}, warnings, truncated: false };
|
||||
}
|
||||
|
||||
// 2) Derive a service name from each container.
|
||||
//
|
||||
// Coolify names compose containers like `{service}-{appUuid}-{short}`
|
||||
// For single-container apps it's just `{appSlug}-{appUuid}` or similar.
|
||||
// We strip the uuid to recover the service name.
|
||||
const services: Record<string, ServiceLogs> = {};
|
||||
let anyTruncated = false;
|
||||
|
||||
const targets = containers.filter(c => {
|
||||
if (!filterService) return true;
|
||||
return c.name.startsWith(filterService + '-') || c.name === filterService;
|
||||
});
|
||||
if (filterService && targets.length === 0) {
|
||||
warnings.push(`No container matched service=${filterService}. Available: ${containers.map(c => c.name).join(', ')}`);
|
||||
}
|
||||
|
||||
for (const c of targets) {
|
||||
// service name = name with -<uuid>-<hash> suffix stripped
|
||||
let service = c.name;
|
||||
const idx = c.name.indexOf(`-${uuid}`);
|
||||
if (idx > 0) service = c.name.slice(0, idx);
|
||||
// Guard against empty or duplicate service keys
|
||||
if (!service) service = c.name;
|
||||
if (services[service]) service = c.name;
|
||||
|
||||
const logsRes = await runOnCoolifyHost(
|
||||
`docker logs --tail ${Math.floor(lines)} --timestamps ${sq(c.name)} 2>&1`,
|
||||
{ timeoutMs: 10_000, maxBytes: 1_000_000 },
|
||||
);
|
||||
if (logsRes.truncated) anyTruncated = true;
|
||||
const text = logsRes.stdout; // stderr is merged via `2>&1`
|
||||
services[service] = {
|
||||
container: c.name,
|
||||
lines: text ? text.split('\n').length : 0,
|
||||
bytes: Buffer.byteLength(text, 'utf8'),
|
||||
logs: text,
|
||||
status: c.status,
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
uuid,
|
||||
buildPack,
|
||||
source: 'ssh_docker',
|
||||
services,
|
||||
warnings,
|
||||
truncated: anyTruncated,
|
||||
};
|
||||
}
|
||||
133
lib/coolify-ssh.ts
Normal file
133
lib/coolify-ssh.ts
Normal file
@@ -0,0 +1,133 @@
|
||||
/**
|
||||
* One-shot SSH-exec helper into the Coolify host.
|
||||
*
|
||||
* Used to pull docker runtime data that the Coolify REST API doesn't
|
||||
* surface cleanly (per-service logs for compose apps, container state,
|
||||
* etc.). Strictly command-in / string-out — we never hold a persistent
|
||||
* channel and we never expose an interactive shell to callers.
|
||||
*
|
||||
* Required env (injected into vibn-frontend at deploy time):
|
||||
* COOLIFY_SSH_HOST e.g. 34.19.250.135 or coolify.vibnai.com
|
||||
* COOLIFY_SSH_PORT default 22
|
||||
* COOLIFY_SSH_USER default "vibn-logs"
|
||||
* COOLIFY_SSH_PRIVATE_KEY_B64 base64-encoded PEM private key
|
||||
*
|
||||
* The authorised key lives in ~vibn-logs/.ssh/authorized_keys on the
|
||||
* Coolify host. That user must be a member of the `docker` group so
|
||||
* `docker logs`/`docker ps` work without sudo; it should NOT have
|
||||
* sudo rights.
|
||||
*/
|
||||
|
||||
import { Client, type ConnectConfig } from 'ssh2';
|
||||
|
||||
const DEFAULT_EXEC_TIMEOUT_MS = 10_000;
|
||||
const DEFAULT_MAX_BYTES = 1_000_000; // 1 MB – more than enough for `docker logs --tail 2000`
|
||||
|
||||
export interface CoolifySshResult {
|
||||
code: number | null;
|
||||
stdout: string;
|
||||
stderr: string;
|
||||
truncated: boolean;
|
||||
}
|
||||
|
||||
export interface CoolifySshOptions {
|
||||
/** Millis to wait for the command to finish before killing the channel. */
|
||||
timeoutMs?: number;
|
||||
/** Cap combined stdout+stderr. Extra bytes are dropped and `truncated=true`. */
|
||||
maxBytes?: number;
|
||||
}
|
||||
|
||||
function loadConnectConfig(): ConnectConfig {
|
||||
const host = process.env.COOLIFY_SSH_HOST;
|
||||
const keyB64 = process.env.COOLIFY_SSH_PRIVATE_KEY_B64;
|
||||
if (!host) throw new Error('COOLIFY_SSH_HOST is not set');
|
||||
if (!keyB64) throw new Error('COOLIFY_SSH_PRIVATE_KEY_B64 is not set');
|
||||
const privateKey = Buffer.from(keyB64, 'base64').toString('utf8');
|
||||
const port = Number(process.env.COOLIFY_SSH_PORT ?? 22);
|
||||
const username = process.env.COOLIFY_SSH_USER ?? 'vibn-logs';
|
||||
return {
|
||||
host,
|
||||
port,
|
||||
username,
|
||||
privateKey,
|
||||
readyTimeout: 8_000,
|
||||
// We accept the host key on first connect; we're always connecting
|
||||
// to infra we own, but a future hardening pass should pin the host
|
||||
// key fingerprint via `hostVerifier`.
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Run a single command on the Coolify host and collect its output.
|
||||
*
|
||||
* The helper enforces:
|
||||
* - a wall-clock timeout (default 10s)
|
||||
* - a combined-output byte cap (default 1 MB)
|
||||
* - always-close semantics (connection is destroyed on return / throw)
|
||||
*/
|
||||
export function runOnCoolifyHost(
|
||||
command: string,
|
||||
opts: CoolifySshOptions = {},
|
||||
): Promise<CoolifySshResult> {
|
||||
const cfg = loadConnectConfig();
|
||||
const timeoutMs = opts.timeoutMs ?? DEFAULT_EXEC_TIMEOUT_MS;
|
||||
const maxBytes = opts.maxBytes ?? DEFAULT_MAX_BYTES;
|
||||
|
||||
return new Promise((resolve, reject) => {
|
||||
const conn = new Client();
|
||||
let settled = false;
|
||||
let stdout = '';
|
||||
let stderr = '';
|
||||
let truncated = false;
|
||||
const settle = (fn: () => void) => {
|
||||
if (settled) return;
|
||||
settled = true;
|
||||
clearTimeout(timer);
|
||||
try { conn.end(); } catch { /* noop */ }
|
||||
fn();
|
||||
};
|
||||
const timer = setTimeout(() => {
|
||||
settle(() => reject(new Error(`[coolify-ssh] timeout after ${timeoutMs}ms running: ${command}`)));
|
||||
}, timeoutMs);
|
||||
|
||||
conn
|
||||
.on('ready', () => {
|
||||
conn.exec(command, (err, stream) => {
|
||||
if (err) return settle(() => reject(err));
|
||||
stream
|
||||
.on('close', (code: number | null) => {
|
||||
settle(() => resolve({ code, stdout, stderr, truncated }));
|
||||
})
|
||||
.on('data', (chunk: Buffer) => {
|
||||
if (stdout.length + stderr.length + chunk.length > maxBytes) {
|
||||
truncated = true;
|
||||
const room = Math.max(0, maxBytes - stdout.length - stderr.length);
|
||||
stdout += chunk.slice(0, room).toString('utf8');
|
||||
} else {
|
||||
stdout += chunk.toString('utf8');
|
||||
}
|
||||
})
|
||||
.stderr.on('data', (chunk: Buffer) => {
|
||||
if (stdout.length + stderr.length + chunk.length > maxBytes) {
|
||||
truncated = true;
|
||||
const room = Math.max(0, maxBytes - stdout.length - stderr.length);
|
||||
stderr += chunk.slice(0, room).toString('utf8');
|
||||
} else {
|
||||
stderr += chunk.toString('utf8');
|
||||
}
|
||||
});
|
||||
});
|
||||
})
|
||||
.on('error', (err) => settle(() => reject(err)))
|
||||
.connect(cfg);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* True if the env is configured to talk to the Coolify host.
|
||||
* Callers use this to gracefully degrade when SSH isn't available
|
||||
* (e.g. local dev without the key pair mounted).
|
||||
*/
|
||||
export function isCoolifySshConfigured(): boolean {
|
||||
return !!(process.env.COOLIFY_SSH_HOST && process.env.COOLIFY_SSH_PRIVATE_KEY_B64);
|
||||
}
|
||||
@@ -59,6 +59,7 @@ export interface CoolifyApplication {
|
||||
environment_id?: number;
|
||||
environment_name?: string;
|
||||
environment?: { id?: number; project_uuid?: string; project?: { uuid?: string } };
|
||||
build_pack?: string;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -548,6 +549,19 @@ export async function getDeploymentLogs(deploymentUuid: string): Promise<{ logs:
|
||||
return coolifyFetch(`/deployments/${deploymentUuid}/logs`);
|
||||
}
|
||||
|
||||
/**
|
||||
* Coolify's "runtime logs" endpoint. Returns `{ logs: "…" }` for simple
|
||||
* Dockerfile/nixpacks apps; returns an empty string for `dockercompose`
|
||||
* apps (Coolify v4 doesn't know which of the compose services to tail).
|
||||
* Use coolify-logs.getApplicationRuntimeLogs for the compose-aware path.
|
||||
*/
|
||||
export async function getApplicationRuntimeLogsFromApi(
|
||||
uuid: string,
|
||||
lines = 200,
|
||||
): Promise<{ logs: string }> {
|
||||
return coolifyFetch(`/applications/${uuid}/logs?lines=${Math.max(1, Math.min(lines, 5000))}`);
|
||||
}
|
||||
|
||||
export async function listApplicationDeployments(uuid: string): Promise<CoolifyDeployment[]> {
|
||||
// Coolify v4 nests this under /deployments/applications/{uuid}
|
||||
// and returns { count, deployments }. Normalize to a flat array.
|
||||
|
||||
100
package-lock.json
generated
100
package-lock.json
generated
@@ -44,6 +44,7 @@
|
||||
"react-dom": "^19.2.4",
|
||||
"remark-gfm": "^4.0.1",
|
||||
"sonner": "^2.0.7",
|
||||
"ssh2": "^1.17.0",
|
||||
"tailwind-merge": "^3.4.0",
|
||||
"tsx": "^4.20.6",
|
||||
"uuid": "^13.0.0",
|
||||
@@ -56,6 +57,7 @@
|
||||
"@types/node": "^20",
|
||||
"@types/react": "^19",
|
||||
"@types/react-dom": "^19",
|
||||
"@types/ssh2": "^1.15.5",
|
||||
"eslint": "^9",
|
||||
"eslint-config-next": "16.0.1",
|
||||
"firebase-admin": "^13.6.0",
|
||||
@@ -5586,6 +5588,33 @@
|
||||
"@types/node": "*"
|
||||
}
|
||||
},
|
||||
"node_modules/@types/ssh2": {
|
||||
"version": "1.15.5",
|
||||
"resolved": "https://registry.npmjs.org/@types/ssh2/-/ssh2-1.15.5.tgz",
|
||||
"integrity": "sha512-N1ASjp/nXH3ovBHddRJpli4ozpk6UdDYIX4RJWFa9L1YKnzdhTlVmiGHm4DZnj/jLbqZpes4aeR30EFGQtvhQQ==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@types/node": "^18.11.18"
|
||||
}
|
||||
},
|
||||
"node_modules/@types/ssh2/node_modules/@types/node": {
|
||||
"version": "18.19.130",
|
||||
"resolved": "https://registry.npmjs.org/@types/node/-/node-18.19.130.tgz",
|
||||
"integrity": "sha512-GRaXQx6jGfL8sKfaIDD6OupbIHBr9jv7Jnaml9tB7l4v068PAOXqfcujMMo5PhbIs6ggR1XODELqahT2R8v0fg==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"undici-types": "~5.26.4"
|
||||
}
|
||||
},
|
||||
"node_modules/@types/ssh2/node_modules/undici-types": {
|
||||
"version": "5.26.5",
|
||||
"resolved": "https://registry.npmjs.org/undici-types/-/undici-types-5.26.5.tgz",
|
||||
"integrity": "sha512-JlCMO+ehdEIKqlFxk6IfVoAUVmgz7cU7zD/h9XZ0qzeosSHmUJVOzSQvvYSYWXkFXC+IfLKSIffhv0sVZup6pA==",
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@types/tough-cookie": {
|
||||
"version": "4.0.5",
|
||||
"resolved": "https://registry.npmjs.org/@types/tough-cookie/-/tough-cookie-4.0.5.tgz",
|
||||
@@ -6498,6 +6527,15 @@
|
||||
"node": ">=8"
|
||||
}
|
||||
},
|
||||
"node_modules/asn1": {
|
||||
"version": "0.2.6",
|
||||
"resolved": "https://registry.npmjs.org/asn1/-/asn1-0.2.6.tgz",
|
||||
"integrity": "sha512-ix/FxPn0MDjeyJ7i/yoHGFt/EX6LyNbxSEhPPXODPL+KB0VPk86UYfL0lMdy+KCnv+fmvIzySwaK5COwqVbWTQ==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"safer-buffer": "~2.1.0"
|
||||
}
|
||||
},
|
||||
"node_modules/assistant-cloud": {
|
||||
"version": "0.1.20",
|
||||
"resolved": "https://registry.npmjs.org/assistant-cloud/-/assistant-cloud-0.1.20.tgz",
|
||||
@@ -6654,6 +6692,15 @@
|
||||
"baseline-browser-mapping": "dist/cli.js"
|
||||
}
|
||||
},
|
||||
"node_modules/bcrypt-pbkdf": {
|
||||
"version": "1.0.2",
|
||||
"resolved": "https://registry.npmjs.org/bcrypt-pbkdf/-/bcrypt-pbkdf-1.0.2.tgz",
|
||||
"integrity": "sha512-qeFIXtP4MSoi6NLqO12WfqARWWuCKi2Rn/9hJLEmtB5yTNr9DqFWkJRCf2qShWzPeAMRnOgCrq0sg/KLv5ES9w==",
|
||||
"license": "BSD-3-Clause",
|
||||
"dependencies": {
|
||||
"tweetnacl": "^0.14.3"
|
||||
}
|
||||
},
|
||||
"node_modules/bignumber.js": {
|
||||
"version": "9.3.1",
|
||||
"resolved": "https://registry.npmjs.org/bignumber.js/-/bignumber.js-9.3.1.tgz",
|
||||
@@ -6751,6 +6798,15 @@
|
||||
"integrity": "sha512-zRpUiDwd/xk6ADqPMATG8vc9VPrkck7T07OIx0gnjmJAnHnTVXNQG3vfvWNuiZIkwu9KrKdA1iJKfsfTVxE6NA==",
|
||||
"license": "BSD-3-Clause"
|
||||
},
|
||||
"node_modules/buildcheck": {
|
||||
"version": "0.0.7",
|
||||
"resolved": "https://registry.npmjs.org/buildcheck/-/buildcheck-0.0.7.tgz",
|
||||
"integrity": "sha512-lHblz4ahamxpTmnsk+MNTRWsjYKv965MwOrSJyeD588rR3Jcu7swE+0wN5F+PbL5cjgu/9ObkhfzEPuofEMwLA==",
|
||||
"optional": true,
|
||||
"engines": {
|
||||
"node": ">=10.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/bytes": {
|
||||
"version": "3.1.2",
|
||||
"resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.2.tgz",
|
||||
@@ -7065,6 +7121,20 @@
|
||||
"url": "https://opencollective.com/express"
|
||||
}
|
||||
},
|
||||
"node_modules/cpu-features": {
|
||||
"version": "0.0.10",
|
||||
"resolved": "https://registry.npmjs.org/cpu-features/-/cpu-features-0.0.10.tgz",
|
||||
"integrity": "sha512-9IkYqtX3YHPCzoVg1Py+o9057a3i0fp7S530UWokCSaFVTc7CwXPRiOjRjBQQ18ZCNafx78YfnG+HALxtVmOGA==",
|
||||
"hasInstallScript": true,
|
||||
"optional": true,
|
||||
"dependencies": {
|
||||
"buildcheck": "~0.0.6",
|
||||
"nan": "^2.19.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=10.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/cross-spawn": {
|
||||
"version": "7.0.6",
|
||||
"resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz",
|
||||
@@ -12287,6 +12357,13 @@
|
||||
"integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/nan": {
|
||||
"version": "2.26.2",
|
||||
"resolved": "https://registry.npmjs.org/nan/-/nan-2.26.2.tgz",
|
||||
"integrity": "sha512-0tTvBTYkt3tdGw22nrAy50x7gpbGCCFH3AFcyS5WiUu7Eu4vWlri1woE6qHBSfy11vksDqkiwjOnlR7WV8G1Hw==",
|
||||
"license": "MIT",
|
||||
"optional": true
|
||||
},
|
||||
"node_modules/nanoid": {
|
||||
"version": "3.3.11",
|
||||
"resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.11.tgz",
|
||||
@@ -14326,6 +14403,23 @@
|
||||
"node": ">= 10.x"
|
||||
}
|
||||
},
|
||||
"node_modules/ssh2": {
|
||||
"version": "1.17.0",
|
||||
"resolved": "https://registry.npmjs.org/ssh2/-/ssh2-1.17.0.tgz",
|
||||
"integrity": "sha512-wPldCk3asibAjQ/kziWQQt1Wh3PgDFpC0XpwclzKcdT1vql6KeYxf5LIt4nlFkUeR8WuphYMKqUA56X4rjbfgQ==",
|
||||
"hasInstallScript": true,
|
||||
"dependencies": {
|
||||
"asn1": "^0.2.6",
|
||||
"bcrypt-pbkdf": "^1.0.2"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=10.16.0"
|
||||
},
|
||||
"optionalDependencies": {
|
||||
"cpu-features": "~0.0.10",
|
||||
"nan": "^2.23.0"
|
||||
}
|
||||
},
|
||||
"node_modules/stable-hash": {
|
||||
"version": "0.0.5",
|
||||
"resolved": "https://registry.npmjs.org/stable-hash/-/stable-hash-0.0.5.tgz",
|
||||
@@ -14936,6 +15030,12 @@
|
||||
"url": "https://github.com/sponsors/Wombosvideo"
|
||||
}
|
||||
},
|
||||
"node_modules/tweetnacl": {
|
||||
"version": "0.14.5",
|
||||
"resolved": "https://registry.npmjs.org/tweetnacl/-/tweetnacl-0.14.5.tgz",
|
||||
"integrity": "sha512-KXXFFdAbFXY4geFIwoyNK+f5Z1b7swfXABfL7HXCmoIWMKU3dmS26672A4EeQtDzLKy7SXmfBu51JolvEKwtGA==",
|
||||
"license": "Unlicense"
|
||||
},
|
||||
"node_modules/type-check": {
|
||||
"version": "0.4.0",
|
||||
"resolved": "https://registry.npmjs.org/type-check/-/type-check-0.4.0.tgz",
|
||||
|
||||
@@ -60,6 +60,7 @@
|
||||
"react-dom": "^19.2.4",
|
||||
"remark-gfm": "^4.0.1",
|
||||
"sonner": "^2.0.7",
|
||||
"ssh2": "^1.17.0",
|
||||
"tailwind-merge": "^3.4.0",
|
||||
"tsx": "^4.20.6",
|
||||
"uuid": "^13.0.0",
|
||||
@@ -72,6 +73,7 @@
|
||||
"@types/node": "^20",
|
||||
"@types/react": "^19",
|
||||
"@types/react-dom": "^19",
|
||||
"@types/ssh2": "^1.15.5",
|
||||
"eslint": "^9",
|
||||
"eslint-config-next": "16.0.1",
|
||||
"firebase-admin": "^13.6.0",
|
||||
|
||||
Reference in New Issue
Block a user