From 54020d41ce3f343555ad59ffabbdb6291dd88ec3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Miguel=20=C3=81ngel?= Date: Sun, 29 Mar 2026 08:15:01 +0200 Subject: [PATCH] fix(cli): use correct project name for symlinked directories (#117) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## 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) --- packages/cli/src/commands/dev.ts | 47 +++++++++++++++---------- packages/cli/src/server/studioServer.ts | 6 ++-- 2 files changed, 32 insertions(+), 21 deletions(-) diff --git a/packages/cli/src/commands/dev.ts b/packages/cli/src/commands/dev.ts index 02c95f117..d3be75aef 100644 --- a/packages/cli/src/commands/dev.ts +++ b/packages/cli/src/commands/dev.ts @@ -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 { +async function runDevMode(dir: string, projectName?: string): Promise { // 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 { 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 { +async function runLocalStudioMode(dir: string, projectName?: string): Promise { 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 { 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 { * 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 { +async function runEmbeddedMode( + dir: string, + startPort: number, + projectName?: string, +): Promise { 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 { 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 { 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. diff --git a/packages/cli/src/server/studioServer.ts b/packages/cli/src/server/studioServer.ts index db5245cc2..53296091d 100644 --- a/packages/cli/src/server/studioServer.ts +++ b/packages/cli/src/server/studioServer.ts @@ -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);