Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions apps/mesh/.gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,9 @@ coverage/
# Temporary files
/tmp/

# Dev/test local data directory
.mesh-dev/



# Authentication tokens
Expand Down
3 changes: 2 additions & 1 deletion apps/mesh/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,8 @@
"dist/**/*"
],
"scripts": {
"dev": "bun run migrate && concurrently \"bun run dev:client\" \"bun run dev:server\"",
"dev": "bun run scripts/dev.ts",
"dev:saas": "bun run migrate && concurrently \"bun run dev:client\" \"bun run dev:server\"",
"dev:client": "bun --bun vite dev",
"dev:server": "NODE_ENV=development bun --env-file=.env --hot run src/index.ts",
"build:client": "bun --bun vite build",
Expand Down
183 changes: 183 additions & 0 deletions apps/mesh/scripts/dev.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,183 @@
#!/usr/bin/env bun
/**
* Development environment setup script.
*
* Mirrors the CLI (src/cli.ts) behaviour so that `bun dev` and
* `bunx @decocms/mesh` share the same ~/deco data directory, secrets,
* and local-mode defaults.
*
* After setting up the environment it spawns the regular dev pipeline:
* bun run migrate && concurrently "bun run dev:client" "bun run dev:server"
*/

import { existsSync } from "fs";
import { mkdir } from "fs/promises";
import { createInterface } from "readline";
import { homedir } from "os";
import { join } from "path";
import { randomBytes } from "crypto";
import { spawn } from "child_process";

// ============================================================================
// Resolve MESH_HOME
// ============================================================================

// When MESH_HOME is explicitly set, respect it (CI, tests, custom setups).
// Otherwise default to ~/deco for interactive dev.
const meshAppDir = import.meta.dir.replace("/scripts", "");
const explicitHome = process.env.MESH_HOME;
const userHome = join(homedir(), "deco");
// In CI / non-TTY without explicit MESH_HOME, use a repo-local directory
// so tests never touch the developer's real ~/deco data.
const ciHome = join(meshAppDir, ".mesh-dev");

const dim = "\x1b[2m";
const reset = "\x1b[0m";
const bold = "\x1b[1m";
const cyan = "\x1b[36m";
const yellow = "\x1b[33m";
const green = "\x1b[32m";

function prompt(question: string): Promise<string> {
const rl = createInterface({ input: process.stdin, output: process.stdout });
return new Promise((resolve) => {
rl.question(question, (answer) => {
rl.close();
resolve(answer.trim());
});
});
}

let meshHome: string;

if (explicitHome) {
// Explicit MESH_HOME takes priority (CI, tests, custom setups)
meshHome = explicitHome;
} else if (!process.stdin.isTTY) {
// Non-interactive (CI) — use repo-local directory to avoid touching ~/deco
meshHome = ciHome;
} else if (existsSync(userHome)) {
// Interactive with existing ~/deco — use it
meshHome = userHome;
} else {
// Interactive, first run — prompt for location
const displayDefault = userHome.replace(homedir(), "~");
console.log("");
console.log(`${bold}${cyan}MCP Mesh${reset} ${dim}(dev)${reset}`);
console.log("");
const answer = await prompt(
` Where should Mesh store its data? ${dim}(${displayDefault})${reset} `,
);
if (answer === "") {
meshHome = userHome;
} else {
meshHome = answer.startsWith("~")
? join(homedir(), answer.slice(1))
Copy link
Contributor

@cubic-dev-ai cubic-dev-ai bot Feb 28, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2: ~/ expansion is incorrect: answer.slice(1) starts with /, so join(homedir(), ...) resolves to the filesystem root (e.g. /deco) instead of the user’s home directory. Strip the leading ~/ before joining to avoid writing data in the wrong location.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At apps/mesh/scripts/dev.ts, line 60:

<comment>`~/` expansion is incorrect: `answer.slice(1)` starts with `/`, so `join(homedir(), ...)` resolves to the filesystem root (e.g. `/deco`) instead of the user’s home directory. Strip the leading `~/` before joining to avoid writing data in the wrong location.</comment>

<file context>
@@ -0,0 +1,168 @@
+    meshHome = defaultHome;
+  } else {
+    meshHome = answer.startsWith("~")
+      ? join(homedir(), answer.slice(1))
+      : answer;
+  }
</file context>
Fix with Cubic

: answer;
}
}

// ============================================================================
// Secrets management (same logic as src/cli.ts)
// ============================================================================

await mkdir(meshHome, { recursive: true, mode: 0o700 });

const secretsFilePath = join(meshHome, "secrets.json");

interface SecretsFile {
BETTER_AUTH_SECRET?: string;
ENCRYPTION_KEY?: string;
}

let savedSecrets: SecretsFile = {};
try {
const file = Bun.file(secretsFilePath);
if (await file.exists()) {
savedSecrets = await file.json();
}
} catch {
// File doesn't exist or is invalid — will create new secrets
}

let secretsModified = false;

if (!process.env.BETTER_AUTH_SECRET) {
if (savedSecrets.BETTER_AUTH_SECRET) {
process.env.BETTER_AUTH_SECRET = savedSecrets.BETTER_AUTH_SECRET;
} else {
savedSecrets.BETTER_AUTH_SECRET = randomBytes(32).toString("base64");
process.env.BETTER_AUTH_SECRET = savedSecrets.BETTER_AUTH_SECRET;
secretsModified = true;
}
}

if (!process.env.ENCRYPTION_KEY) {
if (savedSecrets.ENCRYPTION_KEY) {
process.env.ENCRYPTION_KEY = savedSecrets.ENCRYPTION_KEY;
} else {
savedSecrets.ENCRYPTION_KEY = "";
process.env.ENCRYPTION_KEY = savedSecrets.ENCRYPTION_KEY;
secretsModified = true;
}
}

if (secretsModified) {
try {
await Bun.write(secretsFilePath, JSON.stringify(savedSecrets, null, 2));
} catch (error) {
console.warn(
`${yellow}Warning: Could not save secrets file: ${error}${reset}`,
);
}
}

// ============================================================================
// Set environment variables
// ============================================================================

process.env.MESH_HOME = meshHome;
process.env.DATABASE_URL = `file:${join(meshHome, "mesh.db")}`;
process.env.MESH_LOCAL_MODE = "true";

// ============================================================================
// Banner
// ============================================================================

const displayHome = meshHome.replace(homedir(), "~");

console.log("");
console.log(`${bold}${cyan}MCP Mesh${reset} ${dim}(dev)${reset}`);
console.log("");
console.log(
`${bold} Mode: ${green}Local${reset}${bold} (auto-login enabled)${reset}`,
);
console.log(`${bold} Home: ${dim}${displayHome}/${reset}`);
console.log(`${bold} Database: ${dim}${displayHome}/mesh.db${reset}`);
console.log("");

// ============================================================================
// Spawn the dev pipeline
// ============================================================================

const child = spawn(
"bun",
[
"run",
"migrate",
"&&",
"concurrently",
'"bun run dev:client"',
'"bun run dev:server"',
],
{
stdio: "inherit",
shell: true,
env: process.env,
cwd: meshAppDir,
},
);

child.on("exit", (code) => {
process.exit(code ?? 0);
});
8 changes: 6 additions & 2 deletions apps/mesh/src/api/app.ts
Original file line number Diff line number Diff line change
Expand Up @@ -673,8 +673,12 @@ export async function createApp(options: CreateAppOptions = {}) {
app.use("/mcp/virtual-mcp/:virtualMcpId?", mcpAuth);
app.use("/mcp/self", mcpAuth);

// Dev-only routes (local file storage MCP for testing object-storage plugin)
if (process.env.NODE_ENV !== "production") {
// Local file storage MCP routes
// Available in dev mode (NODE_ENV !== production) OR when local mode is active
if (
process.env.NODE_ENV !== "production" ||
process.env.MESH_LOCAL_MODE === "true"
) {
// Using require() for synchronous loading to ensure routes are registered
// before any requests come in. Static imports in dev-only.ts allow knip tracking.
// eslint-disable-next-line @typescript-eslint/no-require-imports
Expand Down
52 changes: 52 additions & 0 deletions apps/mesh/src/api/routes/auth.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,11 @@
import { Hono } from "hono";
import { authConfig, resetPasswordEnabled } from "../../auth";
import { KNOWN_OAUTH_PROVIDERS, OAuthProvider } from "@/auth/oauth-providers";
import {
getLocalAdminUser,
isLocalMode,
LOCAL_ADMIN_PASSWORD,
} from "@/auth/local-mode";

const app = new Hono();

Expand Down Expand Up @@ -41,6 +46,11 @@ export type AuthConfig = {
* Disabled by default in production unless UNSAFE_ALLOW_STDIO_TRANSPORT=true
*/
stdioEnabled: boolean;
/**
* Whether local mode is active (zero-ceremony developer experience).
* When true, the frontend should auto-login and skip org selection.
*/
localMode: boolean;
};

/**
Expand Down Expand Up @@ -87,6 +97,7 @@ app.get("/config", async (c) => {
enabled: false,
},
stdioEnabled,
localMode: isLocalMode(),
};

return c.json({ success: true, config });
Expand All @@ -104,4 +115,45 @@ app.get("/config", async (c) => {
}
});

/**
* Local Mode Auto-Session Endpoint
*
* When local mode is active, this endpoint signs in the admin user
* and returns the session. The frontend calls this to skip the login form.
*
* Route: POST /api/auth/custom/local-session
*/
app.post("/local-session", async (c) => {
if (!isLocalMode()) {
return c.json({ success: false, error: "Local mode is not active" }, 403);
}

try {
const { auth } = await import("../../auth");
const adminUser = await getLocalAdminUser();
if (!adminUser) {
return c.json(
{ success: false, error: "Local admin user not found" },
500,
);
}

// Sign in as the local admin user
const result = await auth.api.signInEmail({
body: {
email: adminUser.email,
password: LOCAL_ADMIN_PASSWORD,
},
asResponse: true,
});

// Forward the response (includes Set-Cookie headers)
return result;
} catch (error) {
const errorMessage =
error instanceof Error ? error.message : "Failed to create local session";
return c.json({ success: false, error: errorMessage }, 500);
}
});

export default app;
7 changes: 5 additions & 2 deletions apps/mesh/src/api/routes/dev-assets-mcp.ts
Original file line number Diff line number Diff line change
Expand Up @@ -47,8 +47,11 @@ interface ToolDefinition {
};
}

// Base directory for dev assets (relative to cwd)
const DEV_ASSETS_BASE_DIR = "./data/assets";
// Base directory for assets.
// Uses MESH_HOME/assets when available (local mode), falls back to ./data/assets
const DEV_ASSETS_BASE_DIR = process.env.MESH_HOME
? `${process.env.MESH_HOME}/assets`
: "./data/assets";

// Default URL expiration time in seconds (1 hour)
const DEFAULT_EXPIRES_IN = 3600;
Expand Down
Loading
Loading