@dubeyvishal/orbital-cli 1.0.4 → 1.0.6

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.
Files changed (55) hide show
  1. package/images/pic1.png +0 -0
  2. package/images/pic2.png +0 -0
  3. package/images/pic3.png +0 -0
  4. package/package.json +10 -11
  5. package/server/prisma/migrations/20260105143219_test_migration/migration.sql +7 -0
  6. package/server/prisma/migrations/20260105151026_authentication/migration.sql +78 -0
  7. package/server/prisma/migrations/20260114105919_add_devicecode_conversation_message/migration.sql +50 -0
  8. package/server/prisma/migrations/migration_lock.toml +3 -0
  9. package/server/prisma/schema.prisma +117 -0
  10. package/server/{ai → src/cli/ai}/googleService.js +4 -5
  11. package/server/{chat → src/cli/chat}/chat-with-ai-agent.js +43 -50
  12. package/server/{chat → src/cli/chat}/chat-with-ai-tools.js +70 -83
  13. package/server/{chat → src/cli/chat}/chat-with-ai.js +57 -69
  14. package/server/{commands → src/cli/commands}/General/openApp.js +13 -8
  15. package/server/{commands → src/cli/commands}/ai/wakeUp.js +18 -20
  16. package/server/{commands → src/cli/commands}/auth/aboutMe.js +3 -2
  17. package/server/{commands → src/cli/commands}/auth/login.js +35 -17
  18. package/server/{commands → src/cli/commands}/auth/logout.js +1 -1
  19. package/server/src/cli/commands/config/setkey.js +26 -0
  20. package/server/{main.js → src/cli/main.js} +1 -1
  21. package/server/src/cli/utils/apiClient.js +66 -0
  22. package/server/{config → src/config}/agentConfig.js +22 -17
  23. package/server/src/config/api.js +22 -0
  24. package/server/src/config/env.js +86 -0
  25. package/server/{config → src/config}/toolConfig.js +7 -0
  26. package/server/src/controllers/aiController.js +58 -0
  27. package/server/src/controllers/cliController.js +77 -0
  28. package/server/src/db/prisma.js +3 -0
  29. package/server/src/index.js +108 -0
  30. package/server/src/lib/auth.js +37 -0
  31. package/server/src/lib/credentialStore.js +47 -0
  32. package/server/src/lib/db.js +18 -0
  33. package/server/src/lib/dbHealth.js +106 -0
  34. package/server/src/lib/orbitalConfig.js +192 -0
  35. package/server/{utils → src/lib}/token.js +8 -14
  36. package/server/src/middleware/auth.js +39 -0
  37. package/server/src/middleware/errorHandler.js +11 -0
  38. package/server/src/prisma/migrations/20260107093841_device_flow/migration.sql +94 -0
  39. package/server/src/prisma/migrations/migration_lock.toml +3 -0
  40. package/server/src/prisma/schema.prisma +115 -0
  41. package/server/src/routes/authRoutes.js +14 -0
  42. package/server/src/routes/cliRoutes.js +18 -0
  43. package/server/src/service/chatService.js +156 -0
  44. package/server/src/services/aiService.js +10 -0
  45. package/server/src/services/chatService.js +1 -0
  46. package/server/src/types/express.d.ts +19 -0
  47. package/server/commands/config/setkey.js +0 -19
  48. package/server/config/env.js +0 -20
  49. package/server/utils/apiClient.js +0 -40
  50. package/server/utils/chatServiceClient.js +0 -73
  51. package/server/utils/orbitalConfig.js +0 -44
  52. /package/server/{commands → src/cli/commands}/General/playSong.js +0 -0
  53. /package/server/{commands → src/cli/commands}/General/searchYoutube.js +0 -0
  54. /package/server/{generalApp → src/cli/generalApp}/Apps.js +0 -0
  55. /package/server/{config → src/config}/googleConfig.js +0 -0
@@ -1,7 +1,7 @@
1
1
  import chalk from "chalk";
2
2
  import { cancel, confirm, intro, outro, isCancel } from "@clack/prompts";
3
3
  import { Command } from "commander";
4
- import { clearStoredToken, getStoredToken } from "../../utils/token.js";
4
+ import { clearStoredToken, getStoredToken } from "../../../lib/token.js";
5
5
 
6
6
  export const logoutAction = async()=>{
7
7
  intro(chalk.bold("Logout"));
@@ -0,0 +1,26 @@
1
+ import { Command } from "commander";
2
+ import chalk from "chalk";
3
+ import { setGeminiApiKey } from "../../../lib/orbitalConfig.js";
4
+ import { getCredentialServiceName } from "../../../lib/credentialStore.js";
5
+
6
+ const setKeyAction = async (apiKey) => {
7
+ try {
8
+ await setGeminiApiKey(apiKey);
9
+ console.log(chalk.green("Gemini API key saved."));
10
+ console.log(
11
+ chalk.gray(
12
+ `Stored securely in your OS credential manager (service: ${getCredentialServiceName()}).`
13
+ )
14
+ );
15
+ } catch (err) {
16
+ console.log(chalk.red("Failed to save key:"), err?.message || err);
17
+ process.exit(1);
18
+ }
19
+ };
20
+
21
+ export const setkey = new Command("set-key")
22
+ .description("Store your Gemini API key securely (keytar)")
23
+ .argument("<API_KEY>", "Your Gemini API key")
24
+ .alias("set")
25
+ .alias("setkey")
26
+ .action(setKeyAction);
@@ -1,6 +1,6 @@
1
1
  #!/usr/bin/env node
2
2
 
3
- import "./config/env.js";
3
+ import "../config/env.js";
4
4
  import chalk from "chalk";
5
5
  import figlet from "figlet";
6
6
  import {Command} from "commander";
@@ -0,0 +1,66 @@
1
+ import chalk from "chalk";
2
+ import { API_BASE } from "../../config/api.js";
3
+ import { getStoredToken, requireAuth } from "../../lib/token.js";
4
+
5
+ const buildUrl = (path) => {
6
+ const normalized = path.startsWith("/") ? path : `/${path}`;
7
+ return `${API_BASE}${normalized}`;
8
+ };
9
+
10
+ const getAuthHeader = async (isRequired) => {
11
+ const token = isRequired ? await requireAuth() : await getStoredToken();
12
+ if (!token?.access_token) return {};
13
+
14
+ const headerValue = token.token_type
15
+ ? `${token.token_type} ${token.access_token}`
16
+ : `Bearer ${token.access_token}`;
17
+
18
+ return { Authorization: headerValue };
19
+ };
20
+
21
+ export const apiRequest = async (
22
+ path,
23
+ { method = "GET", body, requireAuth: isRequired = true } = {}
24
+ ) => {
25
+ const headers = {
26
+ "content-type": "application/json",
27
+ ...(await getAuthHeader(isRequired)),
28
+ };
29
+
30
+ let response;
31
+ try {
32
+ response = await fetch(buildUrl(path), {
33
+ method,
34
+ headers,
35
+ body: body ? JSON.stringify(body) : undefined,
36
+ });
37
+ } catch (error) {
38
+ throw new Error(`Failed to reach server at ${API_BASE}`);
39
+ }
40
+
41
+ const text = await response.text();
42
+ let data = null;
43
+ if (text) {
44
+ try {
45
+ data = JSON.parse(text);
46
+ } catch {
47
+ data = null;
48
+ }
49
+ }
50
+
51
+ if (!response.ok) {
52
+ const errorMessage = data?.error || data?.message || response.statusText;
53
+ throw new Error(`${errorMessage} (HTTP ${response.status})`);
54
+ }
55
+
56
+ return data;
57
+ };
58
+
59
+ export const apiRequestSafe = async (...args) => {
60
+ try {
61
+ return await apiRequest(...args);
62
+ } catch (error) {
63
+ console.error(chalk.red(error?.message || "Request failed"));
64
+ throw error;
65
+ }
66
+ };
@@ -8,7 +8,7 @@ import { z } from "zod";
8
8
  const genenateObject = generateObject;
9
9
 
10
10
 
11
- const applicationSchema = z.object({
11
+ export const applicationSchema = z.object({
12
12
  folderName: z.string().describe("Kebab-Case folder name for the application"),
13
13
  description: z.string().describe("Brief description of what was created"),
14
14
  files: z.array(
@@ -35,7 +35,7 @@ const printSystem = (message) => {
35
35
  console.log(message);
36
36
  }
37
37
 
38
- const displayFileTree = (files, folderName) => {
38
+ export const displayFileTree = (files, folderName) => {
39
39
  printSystem(chalk.cyan("📂 Project Structure:"));
40
40
  printSystem(chalk.white(`${folderName}/`));
41
41
 
@@ -77,7 +77,7 @@ const displayFileTree = (files, folderName) => {
77
77
  printTree(tree, " ");
78
78
  }
79
79
 
80
- const createApplicationFiles = async(baseDir , folderName , files)=>{
80
+ export const createApplicationFiles = async(baseDir , folderName , files)=>{
81
81
  const appDir = path.join(baseDir , folderName)
82
82
 
83
83
  await fs.mkdir(appDir , {recursive : true});
@@ -97,18 +97,11 @@ const createApplicationFiles = async(baseDir , folderName , files)=>{
97
97
 
98
98
  }
99
99
 
100
-
101
- export const generateApplication = async (description, aiService, cwd = process.cwd()) => {
102
- try {
103
- printSystem(chalk.cyan(`\n Agent Mode: Generating your application...\n`));
104
- printSystem(chalk.gray(`Request: ${description}\n`));
105
-
106
- printSystem(chalk.magenta("Agent Response: \n"));
107
-
108
- const { object: application } = await generateObject({
109
- model: aiService.model,
110
- schema: applicationSchema,
111
- prompt: `Create a complete, production-ready application for: ${description}
100
+ export const generateApplicationPlan = async (description, aiService) => {
101
+ const { object: application } = await generateObject({
102
+ model: aiService.model,
103
+ schema: applicationSchema,
104
+ prompt: `Create a complete, production-ready application for: ${description}
112
105
 
113
106
  CRITICAL REQUIREMENTS:
114
107
  1. Generate ALL files needed for the application to run
@@ -125,9 +118,21 @@ Provide:
125
118
  - A meaningful kebab-case folder name
126
119
  - All necessary files with complete content
127
120
  - Setup commands (cd folder, npm install, npm run dev, etc.)
128
- - All dependencies with versions`
121
+ - All dependencies with versions`,
122
+ });
129
123
 
130
- });
124
+ return application;
125
+ };
126
+
127
+
128
+ export const generateApplication = async (description, aiService, cwd = process.cwd()) => {
129
+ try {
130
+ printSystem(chalk.cyan(`\n Agent Mode: Generating your application...\n`));
131
+ printSystem(chalk.gray(`Request: ${description}\n`));
132
+
133
+ printSystem(chalk.magenta("Agent Response: \n"));
134
+
135
+ const application = await generateApplicationPlan(description, aiService);
131
136
 
132
137
  printSystem(chalk.green(`\n Generated: ${application.folderName}`));
133
138
  printSystem(chalk.green(`\n Description: ${application.description}`));
@@ -0,0 +1,22 @@
1
+ const DEFAULT_API_BASE = "https://smart-cli-based-agent.onrender.com";
2
+ const DEFAULT_FRONTEND_URL = "https://smart-cli-based-agent-t7x4.vercel.app";
3
+
4
+ const stripTrailingSlash = (value) => {
5
+ if (typeof value !== "string") return value;
6
+ return value.replace(/\/+$/, "");
7
+ };
8
+
9
+ export const API_BASE = stripTrailingSlash(
10
+ process.env.ORBITAL_SERVER_URL ||
11
+ process.env.BACKEND_URL ||
12
+ process.env.SERVER_URL ||
13
+ DEFAULT_API_BASE
14
+ );
15
+
16
+ export const FRONTEND_URL = stripTrailingSlash(
17
+ process.env.FRONTEND_URL || process.env.CLIENT_ORIGIN || DEFAULT_FRONTEND_URL
18
+ );
19
+
20
+ export const AUTH_BASE_URL = stripTrailingSlash(
21
+ process.env.BETTER_AUTH_BASE_URL || API_BASE
22
+ );
@@ -0,0 +1,86 @@
1
+ import dotenv from "dotenv";
2
+ import path from "path";
3
+ import { fileURLToPath } from "url";
4
+
5
+ const __filename = fileURLToPath(import.meta.url);
6
+ const __dirname = path.dirname(__filename);
7
+
8
+ // Load env from the server package root (server/.env) regardless of where the process is started.
9
+ const serverEnvPath = path.resolve(__dirname, "../../.env");
10
+
11
+ dotenv.config({ path: serverEnvPath });
12
+
13
+ // Note: The Gemini API key is stored in the OS credential manager via keytar.
14
+ // CLI code will hydrate GOOGLE_GENERATIVE_AI_API_KEY from keytar when needed.
15
+
16
+ const stripWrappingQuotes = (value) => {
17
+ if (typeof value !== "string") return value;
18
+ return value.replace(/^\s*"|"\s*$/g, "").trim();
19
+ };
20
+
21
+ const normalizeNeonPostgresUrl = (raw) => {
22
+ const cleaned = stripWrappingQuotes(raw);
23
+ if (!cleaned) return cleaned;
24
+
25
+ try {
26
+ const url = new URL(cleaned);
27
+ const host = url.hostname || "";
28
+
29
+ const isNeon = host.endsWith("neon.tech");
30
+ if (!isNeon) return cleaned;
31
+
32
+ const isPooler = host.includes("-pooler.");
33
+
34
+ // Neon requires TLS.
35
+ if (!url.searchParams.has("sslmode")) url.searchParams.set("sslmode", "require");
36
+
37
+ // When using Neon pooler (PgBouncer), Prisma should run in PgBouncer mode
38
+ // and keep connection limits low.
39
+ if (isPooler && !url.searchParams.has("pgbouncer")) {
40
+ url.searchParams.set("pgbouncer", "true");
41
+ }
42
+ if (isPooler && !url.searchParams.has("connection_limit")) {
43
+ url.searchParams.set("connection_limit", "1");
44
+ }
45
+
46
+ // Avoid long hangs on cold/paused branches or blocked networks.
47
+ if (!url.searchParams.has("connect_timeout")) {
48
+ url.searchParams.set("connect_timeout", "10");
49
+ }
50
+
51
+ return url.toString();
52
+ } catch {
53
+ return cleaned;
54
+ }
55
+ };
56
+
57
+ if (process.env.DATABASE_URL) {
58
+ process.env.DATABASE_URL = normalizeNeonPostgresUrl(process.env.DATABASE_URL);
59
+ }
60
+
61
+ if (process.env.DIRECT_DATABASE_URL) {
62
+ process.env.DIRECT_DATABASE_URL = normalizeNeonPostgresUrl(
63
+ process.env.DIRECT_DATABASE_URL
64
+ );
65
+ }
66
+
67
+ // Helpful warnings for common Neon misconfigurations.
68
+ try {
69
+ if (process.env.DATABASE_URL) {
70
+ const url = new URL(process.env.DATABASE_URL);
71
+ if (url.hostname.includes("-pooler.") && url.searchParams.get("pgbouncer") !== "true") {
72
+ // eslint-disable-next-line no-console
73
+ console.warn(
74
+ "[env] DATABASE_URL points to a Neon pooler host but is missing `pgbouncer=true`. Add it to avoid Prisma connection issues."
75
+ );
76
+ }
77
+ if (url.hostname.endsWith("neon.tech") && url.searchParams.get("sslmode") !== "require") {
78
+ // eslint-disable-next-line no-console
79
+ console.warn(
80
+ "[env] Neon Postgres should use TLS. Ensure DATABASE_URL includes `sslmode=require`."
81
+ );
82
+ }
83
+ }
84
+ } catch {
85
+ // ignore
86
+ }
@@ -1,5 +1,6 @@
1
1
  import { google } from "@ai-sdk/google";
2
2
  import chalk from "chalk";
3
+ import { requireGeminiApiKeySync } from "../lib/orbitalConfig.js";
3
4
 
4
5
  export const availableTools = [
5
6
  {
@@ -32,6 +33,12 @@ export const getEnabledTools = () => {
32
33
  const tools = {};
33
34
 
34
35
  try {
36
+ const enabledToolCount = availableTools.filter((t) => t.enabled).length;
37
+ if (enabledToolCount > 0 && !process.env.GOOGLE_GENERATIVE_AI_API_KEY) {
38
+
39
+ requireGeminiApiKeySync();
40
+ }
41
+
35
42
  for (const toolConfig of availableTools) {
36
43
  if (toolConfig.enabled) {
37
44
  tools[toolConfig.id] = toolConfig.getTool();
@@ -0,0 +1,58 @@
1
+ import { ChatService } from "../services/chatService.js";
2
+ import { getAIService } from "../services/aiService.js";
3
+ import { enableTools, getEnabledTools, resetTools } from "../config/toolConfig.js";
4
+ import { generateApplicationPlan } from "../config/agentConfig.js";
5
+
6
+ const chatService = new ChatService();
7
+
8
+ export const respond = async (req, res, next) => {
9
+ try {
10
+ const { conversationId, mode, toolIds } = req.body || {};
11
+
12
+ if (!conversationId) {
13
+ return res.status(400).json({ error: "conversationId is required" });
14
+ }
15
+
16
+ const messages = await chatService.getMessages(conversationId);
17
+ const aiMessages = chatService.formatMessageForAI(messages);
18
+
19
+ let tools;
20
+ if (mode === "tool") {
21
+ resetTools();
22
+ if (Array.isArray(toolIds)) {
23
+ enableTools(toolIds);
24
+ }
25
+ tools = getEnabledTools();
26
+ }
27
+
28
+ const aiService = getAIService();
29
+ const result = await aiService.sendMessage(aiMessages, null, tools);
30
+
31
+ await chatService.addMessage(conversationId, "assistant", result.content);
32
+
33
+ return res.json({
34
+ content: result.content,
35
+ toolCalls: result.toolCalls || [],
36
+ toolResults: result.toolResults || [],
37
+ });
38
+ } catch (error) {
39
+ return next(error);
40
+ }
41
+ };
42
+
43
+ export const generateAgentPlan = async (req, res, next) => {
44
+ try {
45
+ const { description } = req.body || {};
46
+
47
+ if (!description || typeof description !== "string") {
48
+ return res.status(400).json({ error: "description is required" });
49
+ }
50
+
51
+ const aiService = getAIService();
52
+ const application = await generateApplicationPlan(description, aiService);
53
+
54
+ return res.json({ application });
55
+ } catch (error) {
56
+ return next(error);
57
+ }
58
+ };
@@ -0,0 +1,77 @@
1
+ import { ChatService } from "../services/chatService.js";
2
+
3
+ const chatService = new ChatService();
4
+
5
+ export const getMe = async (req, res) => {
6
+ return res.json({
7
+ user: req.user,
8
+ session: {
9
+ id: req.session?.id,
10
+ expiresAt: req.session?.expiresAt,
11
+ },
12
+ });
13
+ };
14
+
15
+ export const initConversation = async (req, res, next) => {
16
+ try {
17
+ const { conversationId, mode } = req.body || {};
18
+
19
+ const conversation = await chatService.getOrCreateConversation(
20
+ req.user.id,
21
+ conversationId || null,
22
+ mode || "chat"
23
+ );
24
+
25
+ const messages = await chatService.getMessages(conversation.id);
26
+
27
+ return res.json({ conversation, messages });
28
+ } catch (error) {
29
+ return next(error);
30
+ }
31
+ };
32
+
33
+ export const addMessage = async (req, res, next) => {
34
+ try {
35
+ const { conversationId, role, content } = req.body || {};
36
+
37
+ if (!conversationId || !role) {
38
+ return res.status(400).json({ error: "conversationId and role are required" });
39
+ }
40
+
41
+ const message = await chatService.addMessage(conversationId, role, content);
42
+ return res.json({ message });
43
+ } catch (error) {
44
+ return next(error);
45
+ }
46
+ };
47
+
48
+ export const getMessages = async (req, res, next) => {
49
+ try {
50
+ const { conversationId } = req.query || {};
51
+
52
+ if (!conversationId) {
53
+ return res.status(400).json({ error: "conversationId is required" });
54
+ }
55
+
56
+ const messages = await chatService.getMessages(conversationId);
57
+ return res.json({ messages });
58
+ } catch (error) {
59
+ return next(error);
60
+ }
61
+ };
62
+
63
+ export const updateTitle = async (req, res, next) => {
64
+ try {
65
+ const { id } = req.params || {};
66
+ const { title } = req.body || {};
67
+
68
+ if (!id || !title) {
69
+ return res.status(400).json({ error: "title is required" });
70
+ }
71
+
72
+ const conversation = await chatService.updateTitle(id, title);
73
+ return res.json({ conversation });
74
+ } catch (error) {
75
+ return next(error);
76
+ }
77
+ };
@@ -0,0 +1,3 @@
1
+ import prisma from "../lib/db.js";
2
+
3
+ export default prisma;
@@ -0,0 +1,108 @@
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
+ import { FRONTEND_URL } from "./config/api.js";
9
+ import cliRoutes from "./routes/cliRoutes.js";
10
+ import authRoutes from "./routes/authRoutes.js";
11
+ import { errorHandler } from "./middleware/errorHandler.js";
12
+
13
+ const app = express();
14
+ const PORT = process.env.PORT || 8080;
15
+ const CLIENT_ORIGIN = FRONTEND_URL;
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.use("/auth", authRoutes);
30
+
31
+ app.get("/api/me" , async (req, res)=>{
32
+ const session = await auth.api.getSession({
33
+ headers : fromNodeHeaders (req.headers),
34
+ });
35
+
36
+ if (session) {
37
+ return res.json(session);
38
+ }
39
+
40
+ const authHeader = req.headers.authorization;
41
+ if (!authHeader) {
42
+ return res.status(401).json(null);
43
+ }
44
+
45
+ const [scheme, credentials] = authHeader.split(" ");
46
+ if (!scheme || !credentials || scheme.toLowerCase() !== "bearer") {
47
+ return res.status(400).json({ error: "Invalid Authorization header" });
48
+ }
49
+
50
+ const dbSession = await prisma.session.findUnique({
51
+ where: { token: credentials },
52
+ include: { user: true },
53
+ });
54
+
55
+ if (!dbSession) {
56
+ return res.status(401).json(null);
57
+ }
58
+
59
+ if (dbSession.expiresAt && dbSession.expiresAt.getTime() <= Date.now()) {
60
+ return res.status(401).json({ error: "Session expired" });
61
+ }
62
+
63
+ return res.json({
64
+ user: dbSession.user,
65
+ session: {
66
+ id: dbSession.id,
67
+ expiresAt: dbSession.expiresAt,
68
+ },
69
+ });
70
+ });
71
+
72
+ app.get("/device" , async(req , res)=>{
73
+ const {user_code} = req.query;
74
+ res.redirect(`${CLIENT_ORIGIN}/device?user_code=${user_code}`)
75
+ });
76
+
77
+ app.use("/api/cli", cliRoutes);
78
+
79
+ app.use(errorHandler);
80
+
81
+ const start = async () => {
82
+ await ensureDbConnectionOrExit({
83
+ retries: Number(process.env.DB_CONNECT_RETRIES || 10),
84
+ initialDelayMs: Number(process.env.DB_CONNECT_INITIAL_DELAY_MS || 500),
85
+ maxDelayMs: Number(process.env.DB_CONNECT_MAX_DELAY_MS || 5000),
86
+ });
87
+
88
+ const server = app.listen(PORT, () => {
89
+ console.log(`Server is running on ${PORT}`);
90
+ });
91
+
92
+ const shutdown = async (signal) => {
93
+ try {
94
+ console.log(`\nReceived ${signal}. Shutting down...`);
95
+ server.close(() => {
96
+ process.exitCode = 0;
97
+ });
98
+ await prisma.$disconnect();
99
+ } catch {
100
+ process.exit(1);
101
+ }
102
+ };
103
+
104
+ process.on("SIGINT", () => shutdown("SIGINT"));
105
+ process.on("SIGTERM", () => shutdown("SIGTERM"));
106
+ };
107
+
108
+ start();
@@ -0,0 +1,37 @@
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
+ import { FRONTEND_URL } from "../config/api.js";
7
+
8
+ export const auth = betterAuth({
9
+ database: prismaAdapter(prisma, {
10
+ provider: "postgresql",
11
+ }),
12
+ // IMPORTANT: When the frontend proxies `/api/*` to the backend (Next.js rewrites),
13
+ // auth cookies are set on the frontend origin. The OAuth callback must therefore
14
+ // also land on the frontend origin to avoid `state_mismatch`.
15
+ baseURL:
16
+ process.env.FRONTEND_URL ||
17
+ process.env.CLIENT_ORIGIN ||
18
+ FRONTEND_URL,
19
+ basePath:"/api/auth" ,
20
+ trustedOrigins: [
21
+ process.env.CLIENT_ORIGIN ||
22
+ process.env.FRONTEND_URL ||
23
+ FRONTEND_URL,
24
+ FRONTEND_URL,
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
+ });
@@ -0,0 +1,47 @@
1
+ const ORBITAL_KEYTAR_SERVICE = "orbital-cli";
2
+ const ORBITAL_API_KEY_ACCOUNT = "api-key";
3
+
4
+ const loadKeytar = async () => {
5
+ try {
6
+ const mod = await import("keytar");
7
+ return mod?.default ?? mod;
8
+ } catch (err) {
9
+ const wrapped = new Error(
10
+ "keytar is not available. Install it and ensure your OS keychain is supported."
11
+ );
12
+ wrapped.cause = err;
13
+ wrapped.code = "ORBITAL_KEYTAR_NOT_AVAILABLE";
14
+ throw wrapped;
15
+ }
16
+ };
17
+
18
+ export const getCredentialServiceName = () => ORBITAL_KEYTAR_SERVICE;
19
+ export const getApiKeyAccountName = () => ORBITAL_API_KEY_ACCOUNT;
20
+
21
+ export const getStoredApiKey = async () => {
22
+ const keytar = await loadKeytar();
23
+ const value = await keytar.getPassword(
24
+ ORBITAL_KEYTAR_SERVICE,
25
+ ORBITAL_API_KEY_ACCOUNT
26
+ );
27
+ return typeof value === "string" ? value.trim() : "";
28
+ };
29
+
30
+ export const storeApiKey = async (apiKey) => {
31
+ const trimmed = typeof apiKey === "string" ? apiKey.trim() : "";
32
+ if (!trimmed) throw new Error("API key is required");
33
+
34
+ const keytar = await loadKeytar();
35
+ await keytar.setPassword(
36
+ ORBITAL_KEYTAR_SERVICE,
37
+ ORBITAL_API_KEY_ACCOUNT,
38
+ trimmed
39
+ );
40
+ return true;
41
+ };
42
+
43
+ export const deleteStoredApiKey = async () => {
44
+ const keytar = await loadKeytar();
45
+ await keytar.deletePassword(ORBITAL_KEYTAR_SERVICE, ORBITAL_API_KEY_ACCOUNT);
46
+ return true;
47
+ };
@@ -0,0 +1,18 @@
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;