Skip to content

Commit 1ff23a6

Browse files
authored
Merge pull request #7536 from continuedev/dallin/home-dir-warning
feat: warning when running CLI in home dir
2 parents 6b445a3 + 7b5e1ad commit 1ff23a6

File tree

3 files changed

+285
-2
lines changed

3 files changed

+285
-2
lines changed
Lines changed: 256 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,256 @@
1+
import * as fs from "node:fs";
2+
import * as path from "node:path";
3+
4+
import { afterEach, beforeEach, describe, expect, it } from "vitest";
5+
6+
import {
7+
cleanupTestContext,
8+
createTestConfig,
9+
createTestContext,
10+
runCLI,
11+
type CLITestContext,
12+
} from "../test-helpers/cli-helpers.js";
13+
import { HOME_DIRECTORY_WARNING } from "../ui/IntroMessage.js";
14+
15+
describe("E2E: Home Directory Warning", () => {
16+
let context: CLITestContext;
17+
18+
const testConfig = `name: Test Assistant
19+
version: 1.0.0
20+
schema: v1
21+
models:
22+
- model: gpt-4
23+
provider: openai
24+
apiKey: test-key
25+
roles:
26+
- chat`;
27+
28+
beforeEach(async () => {
29+
context = await createTestContext();
30+
});
31+
32+
afterEach(async () => {
33+
await cleanupTestContext(context);
34+
});
35+
36+
describe("home directory detection", () => {
37+
it("should NOT show warning in headless mode even when running from home directory", async () => {
38+
await createTestConfig(context, testConfig);
39+
40+
// Simulate running from home directory using the isolated test dir
41+
const result = await runCLI(context, {
42+
args: ["-p", "--config", context.configPath!, "test prompt"],
43+
env: {
44+
OPENAI_API_KEY: "test-key",
45+
// Ensure home vars point to the isolated test dir
46+
HOME: context.testDir,
47+
USERPROFILE: context.testDir,
48+
HOMEDRIVE: path.parse(context.testDir).root,
49+
HOMEPATH: path.relative(
50+
path.parse(context.testDir).root,
51+
context.testDir,
52+
),
53+
CONTINUE_GLOBAL_DIR: path.join(context.testDir, ".continue-global"),
54+
},
55+
timeout: 5000,
56+
expectError: true,
57+
});
58+
59+
// Should NOT contain the home directory warning in headless mode
60+
const output = result.stdout + result.stderr;
61+
expect(output).not.toContain(HOME_DIRECTORY_WARNING);
62+
});
63+
64+
it("should show warning when running from home directory in TUI mode", async () => {
65+
await createTestConfig(context, testConfig);
66+
67+
// Test TUI mode (without -p flag) from an isolated "home" directory
68+
const result = await runCLI(context, {
69+
args: ["--config", context.configPath!],
70+
env: {
71+
OPENAI_API_KEY: "test-key",
72+
HOME: context.testDir,
73+
USERPROFILE: context.testDir,
74+
HOMEDRIVE: path.parse(context.testDir).root,
75+
HOMEPATH: path.relative(
76+
path.parse(context.testDir).root,
77+
context.testDir,
78+
),
79+
CONTINUE_GLOBAL_DIR: path.join(context.testDir, ".continue-global"),
80+
},
81+
timeout: 3000,
82+
expectError: true,
83+
});
84+
85+
// In TUI mode, if we get any meaningful output, check for warning
86+
const output = result.stdout + result.stderr;
87+
if (
88+
output.includes("Continue") ||
89+
output.includes("Agent") ||
90+
output.includes("Model")
91+
) {
92+
expect(output).toContain(HOME_DIRECTORY_WARNING);
93+
} else {
94+
// TUI mode likely failed to start in test environment, which is expected
95+
// The important thing is that it attempted TUI mode, not headless mode
96+
expect(result.exitCode).toBeDefined();
97+
}
98+
});
99+
100+
it("should NOT show warning in headless mode regardless of platform", async () => {
101+
await createTestConfig(context, testConfig);
102+
103+
// Test with different platform-specific environment variables
104+
const platformEnvs: Record<string, string>[] = [
105+
// Unix-style
106+
{
107+
HOME: context.testDir,
108+
OPENAI_API_KEY: "test-key",
109+
},
110+
// Windows-style
111+
{
112+
USERPROFILE: context.testDir,
113+
HOMEDRIVE: path.parse(context.testDir).root,
114+
HOMEPATH: path.relative(
115+
path.parse(context.testDir).root,
116+
context.testDir,
117+
),
118+
OPENAI_API_KEY: "test-key",
119+
},
120+
];
121+
122+
for (const env of platformEnvs) {
123+
const result = await runCLI(context, {
124+
args: ["-p", "--config", context.configPath!, "test prompt"],
125+
env: {
126+
CONTINUE_GLOBAL_DIR: path.join(context.testDir, ".continue-global"),
127+
...env,
128+
},
129+
timeout: 5000,
130+
expectError: true,
131+
});
132+
133+
// Should NOT contain warning in headless mode regardless of platform
134+
const output = result.stdout + result.stderr;
135+
expect(output).not.toContain(HOME_DIRECTORY_WARNING);
136+
}
137+
});
138+
139+
it("should NOT show warning in headless mode with symlinked home directories", async () => {
140+
await createTestConfig(context, testConfig);
141+
142+
// Create a symlink to the isolated home directory to simulate symlinked home paths
143+
const realHome = fs.realpathSync(context.testDir);
144+
const linkPath = path.join(
145+
path.dirname(realHome),
146+
`${path.basename(realHome)}-link`,
147+
);
148+
try {
149+
if (!fs.existsSync(linkPath)) {
150+
fs.symlinkSync(realHome, linkPath, "dir");
151+
}
152+
} catch {
153+
// If symlink creation fails (e.g., on Windows without privileges), skip this specific scenario
154+
}
155+
156+
const result = await runCLI(context, {
157+
args: ["-p", "--config", context.configPath!, "test prompt"],
158+
env: {
159+
OPENAI_API_KEY: "test-key",
160+
// Point HOME to the symlink while cwd remains the real path
161+
HOME: fs.existsSync(linkPath) ? linkPath : realHome,
162+
USERPROFILE: fs.existsSync(linkPath) ? linkPath : realHome,
163+
HOMEDRIVE: path.parse(realHome).root,
164+
HOMEPATH: path.relative(path.parse(realHome).root, realHome),
165+
CONTINUE_GLOBAL_DIR: path.join(realHome, ".continue-global"),
166+
},
167+
timeout: 5000,
168+
expectError: true,
169+
});
170+
171+
// Should NOT detect home directory warning in headless mode even with resolved paths
172+
const output = result.stdout + result.stderr;
173+
expect(output).not.toContain(HOME_DIRECTORY_WARNING);
174+
});
175+
176+
it("should work in TUI mode (when available)", async () => {
177+
await createTestConfig(context, testConfig);
178+
179+
// Test TUI mode behavior (though it may fail in test environment)
180+
const result = await runCLI(context, {
181+
args: ["--config", context.configPath!],
182+
env: {
183+
OPENAI_API_KEY: "test-key",
184+
HOME: context.testDir,
185+
USERPROFILE: context.testDir,
186+
HOMEDRIVE: path.parse(context.testDir).root,
187+
HOMEPATH: path.relative(
188+
path.parse(context.testDir).root,
189+
context.testDir,
190+
),
191+
CONTINUE_GLOBAL_DIR: path.join(context.testDir, ".continue-global"),
192+
},
193+
timeout: 3000,
194+
expectError: true,
195+
});
196+
197+
// Even if TUI fails to start properly in test environment,
198+
// the warning should still be present in any output
199+
const output = result.stdout + result.stderr;
200+
if (output.includes("Continue")) {
201+
// If we got any meaningful output, check for warning
202+
expect(output).toContain(HOME_DIRECTORY_WARNING);
203+
}
204+
});
205+
});
206+
207+
describe("edge cases", () => {
208+
it("should handle case where home directory cannot be determined", async () => {
209+
await createTestConfig(context, testConfig);
210+
211+
const result = await runCLI(context, {
212+
args: ["-p", "--config", context.configPath!, "test prompt"],
213+
env: {
214+
OPENAI_API_KEY: "test-key",
215+
CONTINUE_GLOBAL_DIR: path.join(context.testDir, ".continue-global"),
216+
// Mask home directory environment variables so os.homedir() cannot derive a value
217+
HOME: "",
218+
USERPROFILE: "",
219+
HOMEDRIVE: "",
220+
HOMEPATH: "",
221+
},
222+
timeout: 5000,
223+
expectError: true,
224+
});
225+
226+
// Should not crash when home directory cannot be determined
227+
expect(result.exitCode).toBeDefined();
228+
const output = result.stdout + result.stderr;
229+
// Should not show warning if home cannot be determined (and we're in headless mode anyway)
230+
expect(output).not.toContain(HOME_DIRECTORY_WARNING);
231+
});
232+
233+
it("should handle relative vs absolute path comparisons", async () => {
234+
await createTestConfig(context, testConfig);
235+
236+
// Use a HOME that is equivalent but not identical (e.g., with './')
237+
const altHome = path.join(context.testDir, ".");
238+
const result = await runCLI(context, {
239+
args: ["-p", "--config", context.configPath!, "test prompt"],
240+
env: {
241+
OPENAI_API_KEY: "test-key",
242+
HOME: altHome,
243+
USERPROFILE: altHome,
244+
HOMEDRIVE: path.parse(altHome).root,
245+
HOMEPATH: path.relative(path.parse(altHome).root, altHome),
246+
CONTINUE_GLOBAL_DIR: path.join(context.testDir, ".continue-global"),
247+
},
248+
timeout: 5000,
249+
expectError: true,
250+
});
251+
252+
// Should handle path resolution correctly (no crash)
253+
expect(result.exitCode).toBeDefined();
254+
});
255+
});
256+
});

extensions/cli/src/ui/IntroMessage.tsx

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,6 @@
1+
import * as os from "node:os";
2+
import * as path from "node:path";
3+
14
import { AssistantUnrolled, ModelConfig } from "@continuedev/config-yaml";
25
import { Box, Text } from "ink";
36
import React, { useMemo } from "react";
@@ -9,6 +12,10 @@ import { isModelCapable } from "../utils/modelCapability.js";
912
import { ModelCapabilityWarning } from "./ModelCapabilityWarning.js";
1013
import { TipsDisplay, shouldShowTip } from "./TipsDisplay.js";
1114

15+
// Export the warning message for testing
16+
export const HOME_DIRECTORY_WARNING =
17+
"Run cn in a project directory for the best experience (currently in home directory)";
18+
1219
interface IntroMessageProps {
1320
config?: AssistantUnrolled;
1421
model?: ModelConfig;
@@ -33,6 +40,18 @@ const IntroMessage: React.FC<IntroMessageProps> = ({
3340
// Determine if we should show a tip (1 in 5 chance) - computed once on mount
3441
const showTip = useMemo(() => shouldShowTip(), []);
3542

43+
// Check if current working directory is the home directory
44+
const isInHomeDirectory = useMemo(() => {
45+
const cwd = process.cwd();
46+
const homedir = os.homedir();
47+
const resolvedCwd = path.resolve(cwd);
48+
const resolvedHome = path.resolve(homedir);
49+
if (process.platform === "win32") {
50+
return resolvedCwd.toLowerCase() === resolvedHome.toLowerCase();
51+
}
52+
return resolvedCwd === resolvedHome;
53+
}, []);
54+
3655
// Memoize expensive operations to avoid running on every resize
3756
const { allRules, modelCapable } = useMemo(() => {
3857
const allRules = extractRuleNames(config?.rules);
@@ -132,6 +151,14 @@ const IntroMessage: React.FC<IntroMessageProps> = ({
132151
{renderMcpPrompts()}
133152
{renderRules()}
134153
{renderMcpServers()}
154+
155+
{/* Home directory warning */}
156+
{isInHomeDirectory && (
157+
<>
158+
<Text color="yellow">{HOME_DIRECTORY_WARNING}</Text>
159+
<Text> </Text>
160+
</>
161+
)}
135162
</Box>
136163
);
137164
};

extensions/vscode/package-lock.json

Lines changed: 2 additions & 2 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

0 commit comments

Comments
 (0)