@dubeyvishal/orbital-cli 1.0.2 → 1.0.4
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/README.md +13 -0
- package/package.json +10 -7
- package/server/{src/cli/ai → ai}/googleService.js +1 -1
- package/server/{src/cli/chat → chat}/chat-with-ai-agent.js +21 -22
- package/server/{src/cli/chat → chat}/chat-with-ai-tools.js +22 -21
- package/server/{src/cli/chat → chat}/chat-with-ai.js +21 -18
- package/server/commands/General/openApp.js +71 -0
- package/server/commands/General/playSong.js +17 -0
- package/server/commands/General/searchYoutube.js +21 -0
- package/server/{src/cli/commands → commands}/ai/wakeUp.js +19 -28
- package/server/{src/cli/commands → commands}/auth/aboutMe.js +1 -1
- package/server/{src/cli/commands → commands}/auth/login.js +1 -1
- package/server/{src/cli/commands → commands}/auth/logout.js +1 -1
- package/server/{src/cli/commands → commands}/config/setkey.js +1 -1
- package/server/config/env.js +20 -0
- package/server/generalApp/Apps.js +21 -0
- package/server/{src/cli/main.js → main.js} +11 -3
- package/server/utils/apiClient.js +40 -0
- package/server/utils/chatServiceClient.js +73 -0
- package/server/prisma/migrations/20260105143219_test_migration/migration.sql +0 -7
- package/server/prisma/migrations/20260105151026_authentication/migration.sql +0 -78
- package/server/prisma/migrations/20260114105919_add_devicecode_conversation_message/migration.sql +0 -50
- package/server/prisma/migrations/migration_lock.toml +0 -3
- package/server/prisma/schema.prisma +0 -117
- package/server/src/config/env.js +0 -100
- package/server/src/index.js +0 -102
- package/server/src/lib/auth.js +0 -37
- package/server/src/lib/db.js +0 -18
- package/server/src/lib/dbHealth.js +0 -106
- package/server/src/prisma/migrations/20260107093841_device_flow/migration.sql +0 -94
- package/server/src/prisma/migrations/migration_lock.toml +0 -3
- package/server/src/prisma/schema.prisma +0 -115
- package/server/src/service/chatService.js +0 -156
- /package/server/{src/config → config}/agentConfig.js +0 -0
- /package/server/{src/config → config}/googleConfig.js +0 -0
- /package/server/{src/config → config}/toolConfig.js +0 -0
- /package/server/{src/lib → utils}/orbitalConfig.js +0 -0
- /package/server/{src/lib → utils}/token.js +0 -0
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
import { getStoredToken } from "./token.js";
|
|
2
|
+
|
|
3
|
+
const BASE_URL = "https://smart-cli-based-agent.onrender.com";
|
|
4
|
+
|
|
5
|
+
export class ChatServiceClient {
|
|
6
|
+
async getAuthHeaders() {
|
|
7
|
+
const token = await getStoredToken();
|
|
8
|
+
if (!token?.access_token) {
|
|
9
|
+
throw new Error("Not authenticated.");
|
|
10
|
+
}
|
|
11
|
+
return {
|
|
12
|
+
"Content-Type": "application/json",
|
|
13
|
+
"Authorization": `Bearer ${token.access_token}`
|
|
14
|
+
};
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
async getOrCreateConversation(userId, conversationId = null, mode = "chat") {
|
|
18
|
+
const headers = await this.getAuthHeaders();
|
|
19
|
+
const response = await fetch(`${BASE_URL}/api/chat/conversation`, {
|
|
20
|
+
method: "POST",
|
|
21
|
+
headers,
|
|
22
|
+
body: JSON.stringify({ conversationId, mode })
|
|
23
|
+
});
|
|
24
|
+
if (!response.ok) throw new Error("Failed to load conversation");
|
|
25
|
+
return await response.json();
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
async addMessage(conversationId, role, content) {
|
|
29
|
+
const headers = await this.getAuthHeaders();
|
|
30
|
+
const response = await fetch(`${BASE_URL}/api/chat/message`, {
|
|
31
|
+
method: "POST",
|
|
32
|
+
headers,
|
|
33
|
+
body: JSON.stringify({ conversationId, role, content })
|
|
34
|
+
});
|
|
35
|
+
if (!response.ok) throw new Error("Failed to add message");
|
|
36
|
+
return await response.json();
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
async getMessages(conversationId) {
|
|
40
|
+
const headers = await this.getAuthHeaders();
|
|
41
|
+
const response = await fetch(`${BASE_URL}/api/chat/message/${conversationId}`, {
|
|
42
|
+
headers
|
|
43
|
+
});
|
|
44
|
+
if (!response.ok) throw new Error("Failed to fetch messages");
|
|
45
|
+
return await response.json();
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
async updateTitle(conversationId, title) {
|
|
49
|
+
const headers = await this.getAuthHeaders();
|
|
50
|
+
const response = await fetch(`${BASE_URL}/api/chat/conversation/${conversationId}`, {
|
|
51
|
+
method: "PUT",
|
|
52
|
+
headers,
|
|
53
|
+
body: JSON.stringify({ title })
|
|
54
|
+
});
|
|
55
|
+
if (!response.ok) throw new Error("Failed to update title");
|
|
56
|
+
return await response.json();
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
parseContent(content) {
|
|
60
|
+
try {
|
|
61
|
+
return JSON.parse(content);
|
|
62
|
+
} catch {
|
|
63
|
+
return content;
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
formatMessageForAI(messages) {
|
|
68
|
+
return messages.map((msg) => ({
|
|
69
|
+
role: msg.role,
|
|
70
|
+
content: typeof msg.content === "string" ? msg.content : JSON.stringify(msg.content),
|
|
71
|
+
}));
|
|
72
|
+
}
|
|
73
|
+
}
|
|
@@ -1,78 +0,0 @@
|
|
|
1
|
-
-- CreateTable
|
|
2
|
-
CREATE TABLE "user" (
|
|
3
|
-
"id" TEXT NOT NULL,
|
|
4
|
-
"name" TEXT NOT NULL,
|
|
5
|
-
"email" TEXT NOT NULL,
|
|
6
|
-
"emailVerified" BOOLEAN NOT NULL DEFAULT false,
|
|
7
|
-
"image" TEXT,
|
|
8
|
-
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
|
9
|
-
"updatedAt" TIMESTAMP(3) NOT NULL,
|
|
10
|
-
|
|
11
|
-
CONSTRAINT "user_pkey" PRIMARY KEY ("id")
|
|
12
|
-
);
|
|
13
|
-
|
|
14
|
-
-- CreateTable
|
|
15
|
-
CREATE TABLE "session" (
|
|
16
|
-
"id" TEXT NOT NULL,
|
|
17
|
-
"expiresAt" TIMESTAMP(3) NOT NULL,
|
|
18
|
-
"token" TEXT NOT NULL,
|
|
19
|
-
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
|
20
|
-
"updatedAt" TIMESTAMP(3) NOT NULL,
|
|
21
|
-
"ipAddress" TEXT,
|
|
22
|
-
"userAgent" TEXT,
|
|
23
|
-
"userId" TEXT NOT NULL,
|
|
24
|
-
|
|
25
|
-
CONSTRAINT "session_pkey" PRIMARY KEY ("id")
|
|
26
|
-
);
|
|
27
|
-
|
|
28
|
-
-- CreateTable
|
|
29
|
-
CREATE TABLE "account" (
|
|
30
|
-
"id" TEXT NOT NULL,
|
|
31
|
-
"accountId" TEXT NOT NULL,
|
|
32
|
-
"providerId" TEXT NOT NULL,
|
|
33
|
-
"userId" TEXT NOT NULL,
|
|
34
|
-
"accessToken" TEXT,
|
|
35
|
-
"refreshToken" TEXT,
|
|
36
|
-
"idToken" TEXT,
|
|
37
|
-
"accessTokenExpiresAt" TIMESTAMP(3),
|
|
38
|
-
"refreshTokenExpiresAt" TIMESTAMP(3),
|
|
39
|
-
"scope" TEXT,
|
|
40
|
-
"password" TEXT,
|
|
41
|
-
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
|
42
|
-
"updatedAt" TIMESTAMP(3) NOT NULL,
|
|
43
|
-
|
|
44
|
-
CONSTRAINT "account_pkey" PRIMARY KEY ("id")
|
|
45
|
-
);
|
|
46
|
-
|
|
47
|
-
-- CreateTable
|
|
48
|
-
CREATE TABLE "verification" (
|
|
49
|
-
"id" TEXT NOT NULL,
|
|
50
|
-
"identifier" TEXT NOT NULL,
|
|
51
|
-
"value" TEXT NOT NULL,
|
|
52
|
-
"expiresAt" TIMESTAMP(3) NOT NULL,
|
|
53
|
-
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
|
54
|
-
"updatedAt" TIMESTAMP(3) NOT NULL,
|
|
55
|
-
|
|
56
|
-
CONSTRAINT "verification_pkey" PRIMARY KEY ("id")
|
|
57
|
-
);
|
|
58
|
-
|
|
59
|
-
-- CreateIndex
|
|
60
|
-
CREATE UNIQUE INDEX "user_email_key" ON "user"("email");
|
|
61
|
-
|
|
62
|
-
-- CreateIndex
|
|
63
|
-
CREATE INDEX "session_userId_idx" ON "session"("userId");
|
|
64
|
-
|
|
65
|
-
-- CreateIndex
|
|
66
|
-
CREATE UNIQUE INDEX "session_token_key" ON "session"("token");
|
|
67
|
-
|
|
68
|
-
-- CreateIndex
|
|
69
|
-
CREATE INDEX "account_userId_idx" ON "account"("userId");
|
|
70
|
-
|
|
71
|
-
-- CreateIndex
|
|
72
|
-
CREATE INDEX "verification_identifier_idx" ON "verification"("identifier");
|
|
73
|
-
|
|
74
|
-
-- AddForeignKey
|
|
75
|
-
ALTER TABLE "session" ADD CONSTRAINT "session_userId_fkey" FOREIGN KEY ("userId") REFERENCES "user"("id") ON DELETE CASCADE ON UPDATE CASCADE;
|
|
76
|
-
|
|
77
|
-
-- AddForeignKey
|
|
78
|
-
ALTER TABLE "account" ADD CONSTRAINT "account_userId_fkey" FOREIGN KEY ("userId") REFERENCES "user"("id") ON DELETE CASCADE ON UPDATE CASCADE;
|
package/server/prisma/migrations/20260114105919_add_devicecode_conversation_message/migration.sql
DELETED
|
@@ -1,50 +0,0 @@
|
|
|
1
|
-
-- CreateTable
|
|
2
|
-
CREATE TABLE "deviceCode" (
|
|
3
|
-
"id" TEXT NOT NULL,
|
|
4
|
-
"deviceCode" TEXT NOT NULL,
|
|
5
|
-
"userCode" TEXT NOT NULL,
|
|
6
|
-
"userId" TEXT,
|
|
7
|
-
"expiresAt" TIMESTAMP(3) NOT NULL,
|
|
8
|
-
"status" TEXT NOT NULL,
|
|
9
|
-
"lastPolledAt" TIMESTAMP(3),
|
|
10
|
-
"pollingInterval" INTEGER,
|
|
11
|
-
"clientId" TEXT,
|
|
12
|
-
"scope" TEXT,
|
|
13
|
-
|
|
14
|
-
CONSTRAINT "deviceCode_pkey" PRIMARY KEY ("id")
|
|
15
|
-
);
|
|
16
|
-
|
|
17
|
-
-- CreateTable
|
|
18
|
-
CREATE TABLE "conversation" (
|
|
19
|
-
"id" TEXT NOT NULL,
|
|
20
|
-
"userId" TEXT NOT NULL,
|
|
21
|
-
"title" TEXT NOT NULL,
|
|
22
|
-
"mode" TEXT NOT NULL DEFAULT 'chat',
|
|
23
|
-
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
|
24
|
-
"updatedAt" TIMESTAMP(3) NOT NULL,
|
|
25
|
-
|
|
26
|
-
CONSTRAINT "conversation_pkey" PRIMARY KEY ("id")
|
|
27
|
-
);
|
|
28
|
-
|
|
29
|
-
-- CreateTable
|
|
30
|
-
CREATE TABLE "message" (
|
|
31
|
-
"id" TEXT NOT NULL,
|
|
32
|
-
"conversationId" TEXT NOT NULL,
|
|
33
|
-
"role" TEXT NOT NULL,
|
|
34
|
-
"content" TEXT NOT NULL,
|
|
35
|
-
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
|
36
|
-
|
|
37
|
-
CONSTRAINT "message_pkey" PRIMARY KEY ("id")
|
|
38
|
-
);
|
|
39
|
-
|
|
40
|
-
-- CreateIndex
|
|
41
|
-
CREATE INDEX "conversation_userId_idx" ON "conversation"("userId");
|
|
42
|
-
|
|
43
|
-
-- CreateIndex
|
|
44
|
-
CREATE INDEX "message_conversationId_idx" ON "message"("conversationId");
|
|
45
|
-
|
|
46
|
-
-- AddForeignKey
|
|
47
|
-
ALTER TABLE "conversation" ADD CONSTRAINT "conversation_userId_fkey" FOREIGN KEY ("userId") REFERENCES "user"("id") ON DELETE CASCADE ON UPDATE CASCADE;
|
|
48
|
-
|
|
49
|
-
-- AddForeignKey
|
|
50
|
-
ALTER TABLE "message" ADD CONSTRAINT "message_conversationId_fkey" FOREIGN KEY ("conversationId") REFERENCES "conversation"("id") ON DELETE CASCADE ON UPDATE CASCADE;
|
|
@@ -1,117 +0,0 @@
|
|
|
1
|
-
generator client {
|
|
2
|
-
provider = "prisma-client-js"
|
|
3
|
-
}
|
|
4
|
-
|
|
5
|
-
datasource db {
|
|
6
|
-
provider = "postgresql"
|
|
7
|
-
url = env("DATABASE_URL")
|
|
8
|
-
directUrl = env("DIRECT_DATABASE_URL")
|
|
9
|
-
}
|
|
10
|
-
|
|
11
|
-
model Test {
|
|
12
|
-
id String @id @default(cuid())
|
|
13
|
-
name String
|
|
14
|
-
}
|
|
15
|
-
|
|
16
|
-
model User {
|
|
17
|
-
id String @id @default(cuid())
|
|
18
|
-
name String
|
|
19
|
-
email String @unique
|
|
20
|
-
emailVerified Boolean @default(false)
|
|
21
|
-
image String?
|
|
22
|
-
createdAt DateTime @default(now())
|
|
23
|
-
updatedAt DateTime @updatedAt
|
|
24
|
-
accounts Account[]
|
|
25
|
-
conversations Conversation[]
|
|
26
|
-
sessions Session[]
|
|
27
|
-
|
|
28
|
-
@@map("user")
|
|
29
|
-
}
|
|
30
|
-
|
|
31
|
-
model Session {
|
|
32
|
-
id String @id @default(cuid())
|
|
33
|
-
expiresAt DateTime
|
|
34
|
-
token String @unique
|
|
35
|
-
createdAt DateTime @default(now())
|
|
36
|
-
updatedAt DateTime @updatedAt
|
|
37
|
-
ipAddress String?
|
|
38
|
-
userAgent String?
|
|
39
|
-
userId String
|
|
40
|
-
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
|
|
41
|
-
|
|
42
|
-
@@index([userId])
|
|
43
|
-
@@map("session")
|
|
44
|
-
}
|
|
45
|
-
|
|
46
|
-
model Account {
|
|
47
|
-
id String @id @default(cuid())
|
|
48
|
-
accountId String
|
|
49
|
-
providerId String
|
|
50
|
-
userId String
|
|
51
|
-
accessToken String?
|
|
52
|
-
refreshToken String?
|
|
53
|
-
idToken String?
|
|
54
|
-
accessTokenExpiresAt DateTime?
|
|
55
|
-
refreshTokenExpiresAt DateTime?
|
|
56
|
-
scope String?
|
|
57
|
-
password String?
|
|
58
|
-
createdAt DateTime @default(now())
|
|
59
|
-
updatedAt DateTime @updatedAt
|
|
60
|
-
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
|
|
61
|
-
|
|
62
|
-
@@index([userId])
|
|
63
|
-
@@map("account")
|
|
64
|
-
}
|
|
65
|
-
|
|
66
|
-
model Verification {
|
|
67
|
-
id String @id @default(cuid())
|
|
68
|
-
identifier String
|
|
69
|
-
value String
|
|
70
|
-
expiresAt DateTime
|
|
71
|
-
createdAt DateTime @default(now())
|
|
72
|
-
updatedAt DateTime @updatedAt
|
|
73
|
-
|
|
74
|
-
@@index([identifier])
|
|
75
|
-
@@map("verification")
|
|
76
|
-
}
|
|
77
|
-
|
|
78
|
-
model DeviceCode {
|
|
79
|
-
id String @id @default(cuid())
|
|
80
|
-
deviceCode String
|
|
81
|
-
userCode String
|
|
82
|
-
userId String?
|
|
83
|
-
expiresAt DateTime
|
|
84
|
-
status String
|
|
85
|
-
lastPolledAt DateTime?
|
|
86
|
-
pollingInterval Int?
|
|
87
|
-
clientId String?
|
|
88
|
-
scope String?
|
|
89
|
-
|
|
90
|
-
@@map("deviceCode")
|
|
91
|
-
}
|
|
92
|
-
|
|
93
|
-
model Conversation {
|
|
94
|
-
id String @id @default(cuid())
|
|
95
|
-
userId String
|
|
96
|
-
title String
|
|
97
|
-
mode String @default("chat")
|
|
98
|
-
createdAt DateTime @default(now())
|
|
99
|
-
updatedAt DateTime @updatedAt
|
|
100
|
-
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
|
|
101
|
-
messages Message[]
|
|
102
|
-
|
|
103
|
-
@@index([userId])
|
|
104
|
-
@@map("conversation")
|
|
105
|
-
}
|
|
106
|
-
|
|
107
|
-
model Message {
|
|
108
|
-
id String @id @default(cuid())
|
|
109
|
-
conversationId String
|
|
110
|
-
role String
|
|
111
|
-
content String
|
|
112
|
-
createdAt DateTime @default(now())
|
|
113
|
-
conversation Conversation @relation(fields: [conversationId], references: [id], onDelete: Cascade)
|
|
114
|
-
|
|
115
|
-
@@index([conversationId])
|
|
116
|
-
@@map("message")
|
|
117
|
-
}
|
package/server/src/config/env.js
DELETED
|
@@ -1,100 +0,0 @@
|
|
|
1
|
-
import dotenv from "dotenv";
|
|
2
|
-
import path from "path";
|
|
3
|
-
import { fileURLToPath } from "url";
|
|
4
|
-
import fs from "fs";
|
|
5
|
-
import os from "os";
|
|
6
|
-
|
|
7
|
-
const __filename = fileURLToPath(import.meta.url);
|
|
8
|
-
const __dirname = path.dirname(__filename);
|
|
9
|
-
|
|
10
|
-
// Load env from the server package root (server/.env) regardless of where the process is started.
|
|
11
|
-
const serverEnvPath = path.resolve(__dirname, "../../.env");
|
|
12
|
-
|
|
13
|
-
dotenv.config({ path: serverEnvPath });
|
|
14
|
-
|
|
15
|
-
// Load Orbital user config (stored in the user's home directory) and
|
|
16
|
-
// hydrate env vars if they are not already set.
|
|
17
|
-
try {
|
|
18
|
-
const orbitalConfigPath = path.join(os.homedir(), ".orbital", "config.json");
|
|
19
|
-
if (!process.env.GOOGLE_GENERATIVE_AI_API_KEY && fs.existsSync(orbitalConfigPath)) {
|
|
20
|
-
const raw = fs.readFileSync(orbitalConfigPath, "utf-8");
|
|
21
|
-
const parsed = JSON.parse(raw);
|
|
22
|
-
if (parsed?.geminiApiKey && typeof parsed.geminiApiKey === "string") {
|
|
23
|
-
process.env.GOOGLE_GENERATIVE_AI_API_KEY = parsed.geminiApiKey.trim();
|
|
24
|
-
}
|
|
25
|
-
}
|
|
26
|
-
} catch {
|
|
27
|
-
// ignore
|
|
28
|
-
}
|
|
29
|
-
|
|
30
|
-
const stripWrappingQuotes = (value) => {
|
|
31
|
-
if (typeof value !== "string") return value;
|
|
32
|
-
return value.replace(/^\s*"|"\s*$/g, "").trim();
|
|
33
|
-
};
|
|
34
|
-
|
|
35
|
-
const normalizeNeonPostgresUrl = (raw) => {
|
|
36
|
-
const cleaned = stripWrappingQuotes(raw);
|
|
37
|
-
if (!cleaned) return cleaned;
|
|
38
|
-
|
|
39
|
-
try {
|
|
40
|
-
const url = new URL(cleaned);
|
|
41
|
-
const host = url.hostname || "";
|
|
42
|
-
|
|
43
|
-
const isNeon = host.endsWith("neon.tech");
|
|
44
|
-
if (!isNeon) return cleaned;
|
|
45
|
-
|
|
46
|
-
const isPooler = host.includes("-pooler.");
|
|
47
|
-
|
|
48
|
-
// Neon requires TLS.
|
|
49
|
-
if (!url.searchParams.has("sslmode")) url.searchParams.set("sslmode", "require");
|
|
50
|
-
|
|
51
|
-
// When using Neon pooler (PgBouncer), Prisma should run in PgBouncer mode
|
|
52
|
-
// and keep connection limits low.
|
|
53
|
-
if (isPooler && !url.searchParams.has("pgbouncer")) {
|
|
54
|
-
url.searchParams.set("pgbouncer", "true");
|
|
55
|
-
}
|
|
56
|
-
if (isPooler && !url.searchParams.has("connection_limit")) {
|
|
57
|
-
url.searchParams.set("connection_limit", "1");
|
|
58
|
-
}
|
|
59
|
-
|
|
60
|
-
// Avoid long hangs on cold/paused branches or blocked networks.
|
|
61
|
-
if (!url.searchParams.has("connect_timeout")) {
|
|
62
|
-
url.searchParams.set("connect_timeout", "10");
|
|
63
|
-
}
|
|
64
|
-
|
|
65
|
-
return url.toString();
|
|
66
|
-
} catch {
|
|
67
|
-
return cleaned;
|
|
68
|
-
}
|
|
69
|
-
};
|
|
70
|
-
|
|
71
|
-
if (process.env.DATABASE_URL) {
|
|
72
|
-
process.env.DATABASE_URL = normalizeNeonPostgresUrl(process.env.DATABASE_URL);
|
|
73
|
-
}
|
|
74
|
-
|
|
75
|
-
if (process.env.DIRECT_DATABASE_URL) {
|
|
76
|
-
process.env.DIRECT_DATABASE_URL = normalizeNeonPostgresUrl(
|
|
77
|
-
process.env.DIRECT_DATABASE_URL
|
|
78
|
-
);
|
|
79
|
-
}
|
|
80
|
-
|
|
81
|
-
// Helpful warnings for common Neon misconfigurations.
|
|
82
|
-
try {
|
|
83
|
-
if (process.env.DATABASE_URL) {
|
|
84
|
-
const url = new URL(process.env.DATABASE_URL);
|
|
85
|
-
if (url.hostname.includes("-pooler.") && url.searchParams.get("pgbouncer") !== "true") {
|
|
86
|
-
// eslint-disable-next-line no-console
|
|
87
|
-
console.warn(
|
|
88
|
-
"[env] DATABASE_URL points to a Neon pooler host but is missing `pgbouncer=true`. Add it to avoid Prisma connection issues."
|
|
89
|
-
);
|
|
90
|
-
}
|
|
91
|
-
if (url.hostname.endsWith("neon.tech") && url.searchParams.get("sslmode") !== "require") {
|
|
92
|
-
// eslint-disable-next-line no-console
|
|
93
|
-
console.warn(
|
|
94
|
-
"[env] Neon Postgres should use TLS. Ensure DATABASE_URL includes `sslmode=require`."
|
|
95
|
-
);
|
|
96
|
-
}
|
|
97
|
-
}
|
|
98
|
-
} catch {
|
|
99
|
-
// ignore
|
|
100
|
-
}
|
package/server/src/index.js
DELETED
|
@@ -1,102 +0,0 @@
|
|
|
1
|
-
import "./config/env.js";
|
|
2
|
-
import express from "express";
|
|
3
|
-
import { fromNodeHeaders, toNodeHandler } from "better-auth/node";
|
|
4
|
-
import cors from "cors";
|
|
5
|
-
import { auth } from "./lib/auth.js";
|
|
6
|
-
import prisma from "./lib/db.js";
|
|
7
|
-
import { ensureDbConnectionOrExit } from "./lib/dbHealth.js";
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
const app = express();
|
|
11
|
-
const PORT = process.env.PORT || 8080;
|
|
12
|
-
const CLIENT_ORIGIN =
|
|
13
|
-
process.env.CLIENT_ORIGIN ||
|
|
14
|
-
process.env.FRONTEND_URL ||
|
|
15
|
-
"https://smart-cli-based-agent-t7x4.vercel.app";
|
|
16
|
-
|
|
17
|
-
app.use(express.json());
|
|
18
|
-
|
|
19
|
-
app.use(
|
|
20
|
-
cors({
|
|
21
|
-
origin: CLIENT_ORIGIN,
|
|
22
|
-
methods: ["GET", "POST", "PUT", "DELETE"],
|
|
23
|
-
credentials: true,
|
|
24
|
-
})
|
|
25
|
-
);
|
|
26
|
-
|
|
27
|
-
app.all("/api/auth/*splat", toNodeHandler(auth));
|
|
28
|
-
|
|
29
|
-
app.get("/api/me" , async (req, res)=>{
|
|
30
|
-
const session = await auth.api.getSession({
|
|
31
|
-
headers : fromNodeHeaders (req.headers),
|
|
32
|
-
});
|
|
33
|
-
|
|
34
|
-
if (session) {
|
|
35
|
-
return res.json(session);
|
|
36
|
-
}
|
|
37
|
-
|
|
38
|
-
const authHeader = req.headers.authorization;
|
|
39
|
-
if (!authHeader) {
|
|
40
|
-
return res.status(401).json(null);
|
|
41
|
-
}
|
|
42
|
-
|
|
43
|
-
const [scheme, credentials] = authHeader.split(" ");
|
|
44
|
-
if (!scheme || !credentials || scheme.toLowerCase() !== "bearer") {
|
|
45
|
-
return res.status(400).json({ error: "Invalid Authorization header" });
|
|
46
|
-
}
|
|
47
|
-
|
|
48
|
-
const dbSession = await prisma.session.findUnique({
|
|
49
|
-
where: { token: credentials },
|
|
50
|
-
include: { user: true },
|
|
51
|
-
});
|
|
52
|
-
|
|
53
|
-
if (!dbSession) {
|
|
54
|
-
return res.status(401).json(null);
|
|
55
|
-
}
|
|
56
|
-
|
|
57
|
-
if (dbSession.expiresAt && dbSession.expiresAt.getTime() <= Date.now()) {
|
|
58
|
-
return res.status(401).json({ error: "Session expired" });
|
|
59
|
-
}
|
|
60
|
-
|
|
61
|
-
return res.json({
|
|
62
|
-
user: dbSession.user,
|
|
63
|
-
session: {
|
|
64
|
-
id: dbSession.id,
|
|
65
|
-
expiresAt: dbSession.expiresAt,
|
|
66
|
-
},
|
|
67
|
-
});
|
|
68
|
-
});
|
|
69
|
-
|
|
70
|
-
app.get("/device" , async(req , res)=>{
|
|
71
|
-
const {user_code} = req.query;
|
|
72
|
-
res.redirect(`${CLIENT_ORIGIN}/device?user_code=${user_code}`)
|
|
73
|
-
});
|
|
74
|
-
|
|
75
|
-
const start = async () => {
|
|
76
|
-
await ensureDbConnectionOrExit({
|
|
77
|
-
retries: Number(process.env.DB_CONNECT_RETRIES || 10),
|
|
78
|
-
initialDelayMs: Number(process.env.DB_CONNECT_INITIAL_DELAY_MS || 500),
|
|
79
|
-
maxDelayMs: Number(process.env.DB_CONNECT_MAX_DELAY_MS || 5000),
|
|
80
|
-
});
|
|
81
|
-
|
|
82
|
-
const server = app.listen(PORT, () => {
|
|
83
|
-
console.log(`Server is running on ${PORT}`);
|
|
84
|
-
});
|
|
85
|
-
|
|
86
|
-
const shutdown = async (signal) => {
|
|
87
|
-
try {
|
|
88
|
-
console.log(`\nReceived ${signal}. Shutting down...`);
|
|
89
|
-
server.close(() => {
|
|
90
|
-
process.exitCode = 0;
|
|
91
|
-
});
|
|
92
|
-
await prisma.$disconnect();
|
|
93
|
-
} catch {
|
|
94
|
-
process.exit(1);
|
|
95
|
-
}
|
|
96
|
-
};
|
|
97
|
-
|
|
98
|
-
process.on("SIGINT", () => shutdown("SIGINT"));
|
|
99
|
-
process.on("SIGTERM", () => shutdown("SIGTERM"));
|
|
100
|
-
};
|
|
101
|
-
|
|
102
|
-
start();
|
package/server/src/lib/auth.js
DELETED
|
@@ -1,37 +0,0 @@
|
|
|
1
|
-
import "../config/env.js";
|
|
2
|
-
import { betterAuth } from "better-auth";
|
|
3
|
-
import { prismaAdapter } from "better-auth/adapters/prisma";
|
|
4
|
-
import { deviceAuthorization } from "better-auth/plugins";
|
|
5
|
-
import prisma from "./db.js";
|
|
6
|
-
|
|
7
|
-
export const auth = betterAuth({
|
|
8
|
-
database: prismaAdapter(prisma, {
|
|
9
|
-
provider: "postgresql",
|
|
10
|
-
}),
|
|
11
|
-
// IMPORTANT: When the frontend proxies `/api/*` to the backend (Next.js rewrites),
|
|
12
|
-
// auth cookies are set on the frontend origin. The OAuth callback must therefore
|
|
13
|
-
// also land on the frontend origin to avoid `state_mismatch`.
|
|
14
|
-
baseURL:
|
|
15
|
-
process.env.BETTER_AUTH_BASE_URL ||
|
|
16
|
-
process.env.FRONTEND_URL ||
|
|
17
|
-
process.env.CLIENT_ORIGIN ||
|
|
18
|
-
"http://localhost:3000",
|
|
19
|
-
basePath:"/api/auth" ,
|
|
20
|
-
trustedOrigins: [
|
|
21
|
-
process.env.CLIENT_ORIGIN ||
|
|
22
|
-
process.env.FRONTEND_URL ||
|
|
23
|
-
"https://smart-cli-based-agent-t7x4.vercel.app",
|
|
24
|
-
"http://localhost:3000",
|
|
25
|
-
],
|
|
26
|
-
plugins: [
|
|
27
|
-
deviceAuthorization({
|
|
28
|
-
verificationUri: "/device",
|
|
29
|
-
}),
|
|
30
|
-
],
|
|
31
|
-
socialProviders :{
|
|
32
|
-
github : {
|
|
33
|
-
clientId : process.env.GITHUB_CLIENT_ID ,
|
|
34
|
-
clientSecret: process.env.GITHUB_CLIENT_SECRET
|
|
35
|
-
}
|
|
36
|
-
}
|
|
37
|
-
});
|
package/server/src/lib/db.js
DELETED
|
@@ -1,18 +0,0 @@
|
|
|
1
|
-
import "dotenv/config";
|
|
2
|
-
import { execSync } from "child_process";
|
|
3
|
-
import { PrismaClient } from "@prisma/client";
|
|
4
|
-
|
|
5
|
-
const globalForPrisma = globalThis;
|
|
6
|
-
|
|
7
|
-
// Safety net: generate client if missing
|
|
8
|
-
try {
|
|
9
|
-
execSync("npx prisma generate", { stdio: "ignore" });
|
|
10
|
-
} catch {}
|
|
11
|
-
|
|
12
|
-
const prisma = globalForPrisma.prisma ?? new PrismaClient();
|
|
13
|
-
|
|
14
|
-
if (process.env.NODE_ENV !== "production") {
|
|
15
|
-
globalForPrisma.prisma = prisma;
|
|
16
|
-
}
|
|
17
|
-
|
|
18
|
-
export default prisma;
|
|
@@ -1,106 +0,0 @@
|
|
|
1
|
-
import chalk from "chalk";
|
|
2
|
-
import prisma from "./db.js";
|
|
3
|
-
|
|
4
|
-
const stripWrappingQuotes = (value) => {
|
|
5
|
-
if (typeof value !== "string") return value;
|
|
6
|
-
return value.replace(/^\s*"|"\s*$/g, "").trim();
|
|
7
|
-
};
|
|
8
|
-
|
|
9
|
-
export const getDatabaseHostHint = () => {
|
|
10
|
-
const raw = stripWrappingQuotes(process.env.DATABASE_URL);
|
|
11
|
-
if (!raw) return undefined;
|
|
12
|
-
|
|
13
|
-
try {
|
|
14
|
-
const url = new URL(raw);
|
|
15
|
-
const port = url.port || "5432";
|
|
16
|
-
return `${url.hostname}:${port}`;
|
|
17
|
-
} catch {
|
|
18
|
-
return undefined;
|
|
19
|
-
}
|
|
20
|
-
};
|
|
21
|
-
|
|
22
|
-
export const isPrismaDbConnectionError = (error) => {
|
|
23
|
-
const message = String(error?.message || "");
|
|
24
|
-
return (
|
|
25
|
-
error?.name === "PrismaClientInitializationError" ||
|
|
26
|
-
message.includes("Can't reach database server") ||
|
|
27
|
-
message.includes("P1001")
|
|
28
|
-
);
|
|
29
|
-
};
|
|
30
|
-
|
|
31
|
-
export const formatDbConnectionTroubleshooting = () => {
|
|
32
|
-
const hostHint = getDatabaseHostHint();
|
|
33
|
-
const hostLine = hostHint ? ` at ${hostHint}` : "";
|
|
34
|
-
|
|
35
|
-
return [
|
|
36
|
-
chalk.red(`Database connection failed${hostLine}.`),
|
|
37
|
-
chalk.gray("\nFix checklist:"),
|
|
38
|
-
chalk.gray("- Verify `server/.env` has a valid `DATABASE_URL`"),
|
|
39
|
-
chalk.gray("- Ensure your Neon project/branch is running (not paused)"),
|
|
40
|
-
chalk.gray("- Check VPN/firewall/outbound access to port 5432"),
|
|
41
|
-
chalk.gray("- If you changed schema, run: `npm run prisma:migrate` in server/"),
|
|
42
|
-
].join("\n");
|
|
43
|
-
};
|
|
44
|
-
|
|
45
|
-
export const ensureDbConnection = async () => {
|
|
46
|
-
return ensureDbConnectionWithRetry();
|
|
47
|
-
};
|
|
48
|
-
|
|
49
|
-
const sleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
|
|
50
|
-
|
|
51
|
-
export const ensureDbConnectionWithRetry = async (options = {}) => {
|
|
52
|
-
const {
|
|
53
|
-
retries = Number(process.env.DB_CONNECT_RETRIES || 6),
|
|
54
|
-
initialDelayMs = 500,
|
|
55
|
-
maxDelayMs = 5000,
|
|
56
|
-
logAttempts = true,
|
|
57
|
-
} = options;
|
|
58
|
-
|
|
59
|
-
const attempts = Math.max(1, Number(retries) + 1);
|
|
60
|
-
let delayMs = initialDelayMs;
|
|
61
|
-
|
|
62
|
-
for (let attempt = 1; attempt <= attempts; attempt++) {
|
|
63
|
-
try {
|
|
64
|
-
await prisma.$connect();
|
|
65
|
-
await prisma.$queryRaw`SELECT 1`;
|
|
66
|
-
return true;
|
|
67
|
-
} catch (error) {
|
|
68
|
-
const isConnectionError = isPrismaDbConnectionError(error);
|
|
69
|
-
const isLastAttempt = attempt === attempts;
|
|
70
|
-
|
|
71
|
-
if (!isLastAttempt && isConnectionError) {
|
|
72
|
-
if (logAttempts) {
|
|
73
|
-
const hostHint = getDatabaseHostHint();
|
|
74
|
-
const hostLine = hostHint ? ` (${hostHint})` : "";
|
|
75
|
-
console.log(
|
|
76
|
-
chalk.yellow(
|
|
77
|
-
`Database not reachable${hostLine}. Retry ${attempt}/${attempts - 1} in ${delayMs}ms...`
|
|
78
|
-
)
|
|
79
|
-
);
|
|
80
|
-
}
|
|
81
|
-
await sleep(delayMs);
|
|
82
|
-
delayMs = Math.min(maxDelayMs, Math.floor(delayMs * 1.8));
|
|
83
|
-
continue;
|
|
84
|
-
}
|
|
85
|
-
|
|
86
|
-
if (isConnectionError) {
|
|
87
|
-
console.log(formatDbConnectionTroubleshooting());
|
|
88
|
-
return false;
|
|
89
|
-
}
|
|
90
|
-
|
|
91
|
-
console.log(chalk.red(`Database error: ${error?.message || error}`));
|
|
92
|
-
return false;
|
|
93
|
-
}
|
|
94
|
-
}
|
|
95
|
-
|
|
96
|
-
return false;
|
|
97
|
-
};
|
|
98
|
-
|
|
99
|
-
export const ensureDbConnectionOrExit = async (options = {}) => {
|
|
100
|
-
const ok = await ensureDbConnectionWithRetry(options);
|
|
101
|
-
if (ok) return true;
|
|
102
|
-
|
|
103
|
-
// eslint-disable-next-line no-console
|
|
104
|
-
console.error(chalk.red("\nServer cannot start without database connectivity. Exiting."));
|
|
105
|
-
process.exit(1);
|
|
106
|
-
};
|