fix(cli): use correct project name for symlinked directories (#117)

## Summary
- When running `hyperframes dev .` inside a symlinked directory, the project name showed the resolved target name instead of the visible directory name
- Now uses `$PWD` to preserve the user-facing name
- Added `projectName` option to `StudioServerOptions` for explicit override

## Test plan
- [ ] `ln -s /path/to/project my-project && cd my-project && hyperframes dev .` → should show "my-project"
- [ ] `hyperframes dev /path/to/project` → should show "project" (basename of path)
This commit is contained in:
Miguel Ángel
2026-03-29 08:15:01 +02:00
committed by GitHub
parent 6d105a3e64
commit 54020d41ce
2 changed files with 32 additions and 21 deletions
+28 -19
View File
@@ -63,34 +63,39 @@ export default defineCommand({
port: { type: "string", description: "Port to run the dev server on", default: "3002" },
},
async run({ args }) {
const dir = resolve(args.dir ?? ".");
const rawArg = args.dir;
const dir = resolve(rawArg ?? ".");
const startPort = parseInt(args.port ?? "3002", 10);
// Compute display name: preserve symlink/CWD name when user runs "hyperframes dev ."
const isImplicitCwd = !rawArg || rawArg === "." || rawArg === "./";
const projectName = isImplicitCwd ? basename(process.env.PWD ?? dir) : basename(dir);
if (isDevMode()) {
return runDevMode(dir);
return runDevMode(dir, projectName);
}
// If @hyperframes/studio is installed locally, use Vite for full HMR
if (hasLocalStudio(dir)) {
return runLocalStudioMode(dir);
return runLocalStudioMode(dir, projectName);
}
return runEmbeddedMode(dir, startPort);
return runEmbeddedMode(dir, startPort, projectName);
},
});
/**
* Dev mode: spawn pnpm studio from the monorepo (existing behavior).
*/
async function runDevMode(dir: string): Promise<void> {
async function runDevMode(dir: string, projectName?: string): Promise<void> {
// Find monorepo root by navigating from packages/cli/src/commands/
const thisFile = fileURLToPath(import.meta.url);
const repoRoot = resolve(dirname(thisFile), "..", "..", "..", "..");
// Symlink project into the studio's data directory
const projectsDir = join(repoRoot, "packages", "studio", "data", "projects");
const projectName = basename(dir);
const symlinkPath = join(projectsDir, projectName);
const pName = projectName ?? basename(dir);
const symlinkPath = join(projectsDir, pName);
mkdirSync(projectsDir, { recursive: true });
@@ -140,13 +145,13 @@ async function runDevMode(dir: string): Promise<void> {
frontendUrl = localMatch[1] ?? "";
s.stop(c.success("Studio running"));
console.log();
console.log(` ${c.dim("Project")} ${c.accent(projectName)}`);
console.log(` ${c.dim("Project")} ${c.accent(pName)}`);
console.log(` ${c.dim("Studio")} ${c.accent(frontendUrl)}`);
console.log();
console.log(` ${c.dim("Press Ctrl+C to stop")}`);
console.log();
const urlToOpen = `${frontendUrl}#/project/${projectName}`;
const urlToOpen = `${frontendUrl}#/project/${pName}`;
import("open").then((mod) => mod.default(urlToOpen)).catch(() => {});
child.stdout?.removeListener("data", handleOutput);
@@ -197,14 +202,14 @@ function hasLocalStudio(dir: string): boolean {
* Local studio mode: spawn Vite using a locally installed @hyperframes/studio.
* Provides full Vite HMR and the complete studio experience.
*/
async function runLocalStudioMode(dir: string): Promise<void> {
async function runLocalStudioMode(dir: string, projectName?: string): Promise<void> {
const req = createRequire(join(dir, "package.json"));
const studioPkgPath = dirname(req.resolve("@hyperframes/studio/package.json"));
const projectName = basename(dir);
const pName = projectName ?? basename(dir);
// Symlink project into studio's data directory
const projectsDir = join(studioPkgPath, "data", "projects");
const symlinkPath = join(projectsDir, projectName);
const symlinkPath = join(projectsDir, pName);
mkdirSync(projectsDir, { recursive: true });
let createdSymlink = false;
@@ -239,12 +244,12 @@ async function runLocalStudioMode(dir: string): Promise<void> {
const url = localMatch[1] ?? "";
s.stop(c.success("Studio running"));
console.log();
console.log(` ${c.dim("Project")} ${c.accent(projectName)}`);
console.log(` ${c.dim("Project")} ${c.accent(pName)}`);
console.log(` ${c.dim("Studio")} ${c.accent(url)}`);
console.log();
console.log(` ${c.dim("Press Ctrl+C to stop")}`);
console.log();
import("open").then((mod) => mod.default(`${url}#project/${projectName}`)).catch(() => {});
import("open").then((mod) => mod.default(`${url}#project/${pName}`)).catch(() => {});
}
}
@@ -274,11 +279,15 @@ async function runLocalStudioMode(dir: string): Promise<void> {
* Embedded mode: serve the pre-built studio SPA with a standalone Hono server.
* Works without any additional dependencies — the studio is bundled in dist/.
*/
async function runEmbeddedMode(dir: string, startPort: number): Promise<void> {
async function runEmbeddedMode(
dir: string,
startPort: number,
projectName?: string,
): Promise<void> {
const { createStudioServer } = await import("../server/studioServer.js");
const projectName = basename(dir);
const { app } = createStudioServer({ projectDir: dir });
const pName = projectName ?? basename(dir);
const { app } = createStudioServer({ projectDir: dir, projectName: pName });
clack.intro(c.bold("hyperframes dev"));
const s = clack.spinner();
@@ -303,7 +312,7 @@ async function runEmbeddedMode(dir: string, startPort: number): Promise<void> {
console.log(` ${c.warn(`Port ${startPort} is in use, using ${actualPort} instead`)}`);
console.log();
}
console.log(` ${c.dim("Project")} ${c.accent(projectName)}`);
console.log(` ${c.dim("Project")} ${c.accent(pName)}`);
console.log(` ${c.dim("Studio")} ${c.accent(url)}`);
console.log();
console.log(` ${c.dim("Edit with your AI agent — it has HyperFrames skills installed.")}`);
@@ -311,7 +320,7 @@ async function runEmbeddedMode(dir: string, startPort: number): Promise<void> {
console.log();
console.log(` ${c.dim("Press Ctrl+C to stop")}`);
console.log();
import("open").then((mod) => mod.default(`${url}#project/${projectName}`)).catch(() => {});
import("open").then((mod) => mod.default(`${url}#project/${pName}`)).catch(() => {});
// Block until the process is killed. Ctrl+C (SIGINT) uses Node's default
// behavior — exit immediately. The OS reclaims the port and file handles.
+4 -2
View File
@@ -48,6 +48,8 @@ function resolveRuntimePath(): string {
export interface StudioServerOptions {
projectDir: string;
/** Display name for the project. Defaults to basename of projectDir. */
projectName?: string;
}
export interface StudioServer {
@@ -56,8 +58,8 @@ export interface StudioServer {
}
export function createStudioServer(options: StudioServerOptions): StudioServer {
const { projectDir } = options;
const projectId = basename(projectDir);
const { projectDir, projectName } = options;
const projectId = projectName || basename(projectDir);
const studioDir = resolveDistDir();
const runtimePath = resolveRuntimePath();
const watcher = createProjectWatcher(projectDir);