Skip to content

Commit 656dbb4

Browse files
authored
Merge branch 'main' into fix/752-embed-absolute-paths
2 parents 1847de5 + 818947e commit 656dbb4

2 files changed

Lines changed: 58 additions & 6 deletions

File tree

src/domain/search/models.ts

Lines changed: 17 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -96,13 +96,26 @@ export function getModelConfig(modelKey?: string): ModelConfig {
9696
}
9797

9898
/**
99-
* Prompt the user to install a missing package interactively.
99+
* Attempt to install a missing package.
100+
* In TTY environments, prompts the user for confirmation first.
101+
* In non-TTY environments (CI, piped stdin), installs automatically with a log message.
100102
* Returns true if the package was installed, false otherwise.
101-
* Skips the prompt entirely in non-TTY environments (CI, piped stdin).
102103
* @internal Not part of the public barrel.
103104
*/
104105
export function promptInstall(packageName: string): Promise<boolean> {
105-
if (!process.stdin.isTTY) return Promise.resolve(false);
106+
if (!process.stdin.isTTY) {
107+
info(`Installing ${packageName} (optional dependency for semantic search)…`);
108+
try {
109+
execFileSync('npm', ['install', '--no-save', packageName], {
110+
stdio: 'inherit',
111+
timeout: 300_000,
112+
});
113+
return Promise.resolve(true);
114+
} catch (err) {
115+
info(`Auto-install failed: ${err instanceof Error ? err.message : String(err)}`);
116+
return Promise.resolve(false);
117+
}
118+
}
106119

107120
return new Promise((resolve) => {
108121
const rl = createInterface({ input: process.stdin, output: process.stderr });
@@ -128,7 +141,7 @@ export function promptInstall(packageName: string): Promise<boolean> {
128141
/**
129142
* Lazy-load @huggingface/transformers.
130143
* If the package is missing, prompts the user to install it interactively.
131-
* In non-TTY environments, prints an error and exits.
144+
* In non-TTY environments, attempts automatic installation.
132145
* @internal Not part of the public barrel.
133146
*/
134147
export async function loadTransformers(): Promise<unknown> {

tests/unit/prompt-install.test.ts

Lines changed: 41 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -34,12 +34,49 @@ describe('loadTransformers install prompt', () => {
3434
vi.restoreAllMocks();
3535
});
3636

37-
test('non-TTY: throws EngineError without prompting', async () => {
37+
test('non-TTY: auto-installs without prompting', async () => {
3838
process.stdin.isTTY = undefined;
3939

40+
let importCount = 0;
4041
const rlFactory = vi.fn();
42+
const execMock = vi.fn();
4143
vi.doMock('node:readline', () => ({ createInterface: rlFactory }));
42-
vi.doMock('node:child_process', () => ({ execFileSync: vi.fn() }));
44+
vi.doMock('node:child_process', () => ({ execFileSync: execMock }));
45+
vi.doMock('@huggingface/transformers', () => {
46+
importCount++;
47+
if (importCount <= 1) throw new Error('Cannot find package');
48+
return {
49+
pipeline: async () => async (batch: string[]) => ({
50+
data: new Float32Array(384 * batch.length),
51+
}),
52+
cos_sim: () => 0,
53+
};
54+
});
55+
56+
const { embed } = await import('../../src/domain/search/index.js');
57+
58+
const result = await embed(['test text'], 'minilm');
59+
expect(result.vectors).toHaveLength(1);
60+
expect(result.dim).toBe(384);
61+
// readline should NOT have been called — no prompt in non-TTY
62+
expect(rlFactory).not.toHaveBeenCalled();
63+
// npm install should have been called automatically
64+
expect(execMock).toHaveBeenCalledWith(
65+
'npm',
66+
['install', '--no-save', '@huggingface/transformers'],
67+
expect.objectContaining({ stdio: 'inherit', timeout: 300_000 }),
68+
);
69+
});
70+
71+
test('non-TTY: throws EngineError when auto-install fails', async () => {
72+
process.stdin.isTTY = undefined;
73+
74+
const rlFactory = vi.fn();
75+
const execMock = vi.fn(() => {
76+
throw new Error('npm ERR!');
77+
});
78+
vi.doMock('node:readline', () => ({ createInterface: rlFactory }));
79+
vi.doMock('node:child_process', () => ({ execFileSync: execMock }));
4380
vi.doMock('@huggingface/transformers', () => {
4481
throw new Error('Cannot find package');
4582
});
@@ -55,6 +92,8 @@ describe('loadTransformers install prompt', () => {
5592
});
5693
// readline should NOT have been called — no prompt in non-TTY
5794
expect(rlFactory).not.toHaveBeenCalled();
95+
// npm install was attempted
96+
expect(execMock).toHaveBeenCalled();
5897
});
5998

6099
test('TTY + user declines: throws EngineError', async () => {

0 commit comments

Comments
 (0)