From d33c2cebe493b254e7988938ab4f8b54922232cf Mon Sep 17 00:00:00 2001 From: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com> Date: Tue, 14 Jul 2026 01:56:48 +0000 Subject: [PATCH] fix(cli): resolve SDK config imports from CLI's bundled copy when project has no SDK installed Co-Authored-By: mantra --- packages/cli/src/commands/config-file.test.ts | 25 +++++++++++ packages/cli/src/commands/config-file.ts | 4 +- packages/cli/src/commands/doctor.ts | 4 +- packages/cli/src/lib/config-jiti.ts | 43 +++++++++++++++++++ 4 files changed, 72 insertions(+), 4 deletions(-) create mode 100644 packages/cli/src/lib/config-jiti.ts diff --git a/packages/cli/src/commands/config-file.test.ts b/packages/cli/src/commands/config-file.test.ts index 69c2673b2..e62bd2888 100644 --- a/packages/cli/src/commands/config-file.test.ts +++ b/packages/cli/src/commands/config-file.test.ts @@ -3,6 +3,31 @@ import * as os from "os"; import * as path from "path"; import { afterEach, beforeEach, describe, expect, it } from "vitest"; import { assertConfigPullTarget, buildConfigPushSource, resolveConfigFilePathForPull } from "./config-file.js"; +import { createConfigFileJiti } from "../lib/config-jiti.js"; + +describe("createConfigFileJiti", () => { + let tmpDir: string; + + beforeEach(() => { + tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), "hexclave-cli-config-jiti-")); + }); + + afterEach(() => { + fs.rmSync(tmpDir, { recursive: true, force: true }); + }); + + it("loads config authoring imports when the project has no SDK installed", async () => { + const configPath = path.join(tmpDir, "hexclave.config.ts"); + fs.writeFileSync( + configPath, + 'import { defineHexclaveConfig } from "@hexclave/js/config";\nexport const config = defineHexclaveConfig({ auth: { allowSignUp: true } });\n', + ); + + const configModule = await createConfigFileJiti(configPath).import<{ config: { auth: { allowSignUp: boolean } } }>(configPath); + + expect(configModule.config).toEqual({ auth: { allowSignUp: true } }); + }); +}); describe("resolveConfigFilePathForPull", () => { let tmpDir: string; diff --git a/packages/cli/src/commands/config-file.ts b/packages/cli/src/commands/config-file.ts index 5a57a7c72..5bdbd50de 100644 --- a/packages/cli/src/commands/config-file.ts +++ b/packages/cli/src/commands/config-file.ts @@ -287,8 +287,8 @@ export function registerConfigCommand(program: Command) { throw new CliError("Config file must have a .js or .ts extension."); } - const { createJiti } = await import("jiti"); - const jiti = createJiti(import.meta.url); + const { createConfigFileJiti } = await import("../lib/config-jiti.js"); + const jiti = createConfigFileJiti(filePath); const configModule: { config?: unknown } = await jiti.import(filePath); const config = parseConfigOverride(configModule.config); diff --git a/packages/cli/src/commands/doctor.ts b/packages/cli/src/commands/doctor.ts index 3b609ba80..cc8aec397 100644 --- a/packages/cli/src/commands/doctor.ts +++ b/packages/cli/src/commands/doctor.ts @@ -453,8 +453,8 @@ function configFileCheck(): CheckSpec { if (!foundPath || !foundRel) return null; // skip — config file is optional try { - const { createJiti } = await import("jiti"); - const jiti = createJiti(import.meta.url); + const { createConfigFileJiti } = await import("../lib/config-jiti.js"); + const jiti = createConfigFileJiti(foundPath); const mod = await jiti.import<{ config?: unknown }>(foundPath); const config = mod.config; if (config === undefined) { diff --git a/packages/cli/src/lib/config-jiti.ts b/packages/cli/src/lib/config-jiti.ts new file mode 100644 index 000000000..c5bacddf1 --- /dev/null +++ b/packages/cli/src/lib/config-jiti.ts @@ -0,0 +1,43 @@ +import { createRequire } from "node:module"; +import { createJiti, type Jiti } from "jiti"; + +const CONFIG_IMPORT_SPECIFIERS = [ + "@hexclave/js/config", + "@hexclave/next/config", + "@hexclave/react/config", + "@hexclave/tanstack-start/config", + "@hexclave/template/config", + "@stackframe/js", + "@stackframe/next", + "@stackframe/react", + "@stackframe/stack", + "@stackframe/template", +] as const; + +/** + * Loads config authoring imports from the project's SDK when available, while + * allowing bare CI checkouts to use the CLI's bundled SDK copy. Jiti resolves + * imports relative to the config file, so those projects may otherwise fail. + */ +export function createConfigFileJiti(configFilePath: string): Jiti { + const configRequire = createRequire(configFilePath); + const unresolvedSpecifiers: string[] = []; + + for (const specifier of CONFIG_IMPORT_SPECIFIERS) { + try { + configRequire.resolve(specifier); + } catch { + unresolvedSpecifiers.push(specifier); + } + } + + const configAliases: Record = {}; + if (unresolvedSpecifiers.length > 0) { + const cliConfigEntrypoint = createRequire(import.meta.url).resolve("@hexclave/js/config"); + for (const specifier of unresolvedSpecifiers) { + configAliases[specifier] = cliConfigEntrypoint; + } + } + + return createJiti(import.meta.url, { alias: configAliases }); +}