Skip to content

Commit add35d2

Browse files
authored
Merge pull request #1321 from QwenLM/mingholy/fix/sdk-cli-path
fix: cli path parsing issue in Windows
2 parents bc2a7ef + 4f970c9 commit add35d2

File tree

2 files changed

+132
-47
lines changed

2 files changed

+132
-47
lines changed

packages/sdk-typescript/src/utils/cliPath.ts

Lines changed: 38 additions & 37 deletions
Original file line numberDiff line numberDiff line change
@@ -150,48 +150,49 @@ export function parseExecutableSpec(executableSpec?: string): {
150150
}
151151

152152
// Check for runtime prefix (e.g., 'bun:/path/to/cli.js')
153+
// Use whitelist mechanism: only treat as runtime spec if prefix matches supported runtimes
154+
const supportedRuntimes = ['node', 'bun', 'tsx', 'deno'];
153155
const runtimeMatch = executableSpec.match(/^([^:]+):(.+)$/);
156+
154157
if (runtimeMatch) {
155158
const [, runtime, filePath] = runtimeMatch;
156-
if (!runtime || !filePath) {
157-
throw new Error(`Invalid runtime specification: '${executableSpec}'`);
158-
}
159-
160-
const supportedRuntimes = ['node', 'bun', 'tsx', 'deno'];
161-
if (!supportedRuntimes.includes(runtime)) {
162-
throw new Error(
163-
`Unsupported runtime '${runtime}'. Supported runtimes: ${supportedRuntimes.join(', ')}`,
164-
);
165-
}
166-
167-
if (!validateRuntimeAvailability(runtime)) {
168-
throw new Error(
169-
`Runtime '${runtime}' is not available on this system. Please install it first.`,
170-
);
171-
}
172159

173-
const resolvedPath = path.resolve(filePath);
174-
175-
if (!fs.existsSync(resolvedPath)) {
176-
throw new Error(
177-
`Executable file not found at '${resolvedPath}' for runtime '${runtime}'. ` +
178-
'Please check the file path and ensure the file exists.',
179-
);
180-
}
181-
182-
if (!validateFileExtensionForRuntime(resolvedPath, runtime)) {
183-
const ext = path.extname(resolvedPath);
184-
throw new Error(
185-
`File extension '${ext}' is not compatible with runtime '${runtime}'. ` +
186-
`Expected extensions for ${runtime}: ${getExpectedExtensions(runtime).join(', ')}`,
187-
);
160+
// Only process as runtime specification if it matches a supported runtime
161+
if (runtime && supportedRuntimes.includes(runtime)) {
162+
if (!filePath) {
163+
throw new Error(`Invalid runtime specification: '${executableSpec}'`);
164+
}
165+
166+
if (!validateRuntimeAvailability(runtime)) {
167+
throw new Error(
168+
`Runtime '${runtime}' is not available on this system. Please install it first.`,
169+
);
170+
}
171+
172+
const resolvedPath = path.resolve(filePath);
173+
174+
if (!fs.existsSync(resolvedPath)) {
175+
throw new Error(
176+
`Executable file not found at '${resolvedPath}' for runtime '${runtime}'. ` +
177+
'Please check the file path and ensure the file exists.',
178+
);
179+
}
180+
181+
if (!validateFileExtensionForRuntime(resolvedPath, runtime)) {
182+
const ext = path.extname(resolvedPath);
183+
throw new Error(
184+
`File extension '${ext}' is not compatible with runtime '${runtime}'. ` +
185+
`Expected extensions for ${runtime}: ${getExpectedExtensions(runtime).join(', ')}`,
186+
);
187+
}
188+
189+
return {
190+
runtime,
191+
executablePath: resolvedPath,
192+
isExplicitRuntime: true,
193+
};
188194
}
189-
190-
return {
191-
runtime,
192-
executablePath: resolvedPath,
193-
isExplicitRuntime: true,
194-
};
195+
// If not a supported runtime, fall through to treat as file path (e.g., Windows paths like 'D:\path\to\cli.js')
195196
}
196197

197198
// Check if it's a command name (no path separators) or a file path

packages/sdk-typescript/test/unit/cliPath.test.ts

Lines changed: 94 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -125,12 +125,43 @@ describe('CLI Path Utilities', () => {
125125
});
126126
});
127127

128-
it('should throw for invalid runtime prefix format', () => {
128+
it('should treat non-whitelisted runtime prefixes as command names', () => {
129+
// With whitelist approach, 'invalid:format' is not recognized as a runtime spec
130+
// so it's treated as a command name, which fails validation due to the colon
129131
expect(() => parseExecutableSpec('invalid:format')).toThrow(
130-
'Unsupported runtime',
132+
'Invalid command name',
131133
);
132134
});
133135

136+
it('should treat Windows drive letters as file paths, not runtime specs', () => {
137+
mockFs.existsSync.mockReturnValue(true);
138+
139+
// Test various Windows drive letters
140+
const windowsPaths = [
141+
'C:\\path\\to\\cli.js',
142+
'D:\\path\\to\\cli.js',
143+
'E:\\Users\\dev\\qwen\\cli.js',
144+
];
145+
146+
for (const winPath of windowsPaths) {
147+
const result = parseExecutableSpec(winPath);
148+
149+
expect(result.isExplicitRuntime).toBe(false);
150+
expect(result.runtime).toBeUndefined();
151+
expect(result.executablePath).toBe(path.resolve(winPath));
152+
}
153+
});
154+
155+
it('should handle Windows paths with forward slashes', () => {
156+
mockFs.existsSync.mockReturnValue(true);
157+
158+
const result = parseExecutableSpec('C:/path/to/cli.js');
159+
160+
expect(result.isExplicitRuntime).toBe(false);
161+
expect(result.runtime).toBeUndefined();
162+
expect(result.executablePath).toBe(path.resolve('C:/path/to/cli.js'));
163+
});
164+
134165
it('should throw when runtime-prefixed file does not exist', () => {
135166
mockFs.existsSync.mockReturnValue(false);
136167

@@ -453,6 +484,41 @@ describe('CLI Path Utilities', () => {
453484
originalInput: `bun:${bundlePath}`,
454485
});
455486
});
487+
488+
it('should handle Windows paths with drive letters', () => {
489+
const windowsPath = 'D:\\path\\to\\cli.js';
490+
const result = prepareSpawnInfo(windowsPath);
491+
492+
expect(result).toEqual({
493+
command: process.execPath,
494+
args: [path.resolve(windowsPath)],
495+
type: 'node',
496+
originalInput: windowsPath,
497+
});
498+
});
499+
500+
it('should handle Windows paths with TypeScript files', () => {
501+
const windowsPath = 'C:\\Users\\dev\\qwen\\index.ts';
502+
const result = prepareSpawnInfo(windowsPath);
503+
504+
expect(result).toEqual({
505+
command: 'tsx',
506+
args: [path.resolve(windowsPath)],
507+
type: 'tsx',
508+
originalInput: windowsPath,
509+
});
510+
});
511+
512+
it('should not confuse Windows drive letters with runtime prefixes', () => {
513+
// Ensure 'D:' is not treated as a runtime specification
514+
const windowsPath = 'D:\\workspace\\project\\cli.js';
515+
const result = prepareSpawnInfo(windowsPath);
516+
517+
// Should use node runtime based on .js extension, not treat 'D' as runtime
518+
expect(result.type).toBe('node');
519+
expect(result.command).toBe(process.execPath);
520+
expect(result.args).toEqual([path.resolve(windowsPath)]);
521+
});
456522
});
457523

458524
describe('error cases', () => {
@@ -472,21 +538,39 @@ describe('CLI Path Utilities', () => {
472538
);
473539
});
474540

475-
it('should provide helpful error for invalid runtime specification', () => {
541+
it('should treat non-whitelisted runtime prefixes as command names', () => {
542+
// With whitelist approach, 'invalid:spec' is not recognized as a runtime spec
543+
// so it's treated as a command name, which fails validation due to the colon
476544
expect(() => prepareSpawnInfo('invalid:spec')).toThrow(
477-
'Unsupported runtime',
545+
'Invalid command name',
546+
);
547+
});
548+
549+
it('should handle Windows paths correctly even when file is missing', () => {
550+
mockFs.existsSync.mockReturnValue(false);
551+
552+
expect(() => prepareSpawnInfo('D:\\missing\\cli.js')).toThrow(
553+
'Executable file not found at',
554+
);
555+
// Should not throw 'Invalid command name' error (which would happen if 'D:' was treated as invalid command)
556+
expect(() => prepareSpawnInfo('D:\\missing\\cli.js')).not.toThrow(
557+
'Invalid command name',
478558
);
479559
});
480560
});
481561

482562
describe('comprehensive validation', () => {
483563
describe('runtime validation', () => {
484-
it('should reject unsupported runtimes', () => {
485-
expect(() =>
486-
parseExecutableSpec('unsupported:/path/to/file.js'),
487-
).toThrow(
488-
"Unsupported runtime 'unsupported'. Supported runtimes: node, bun, tsx, deno",
489-
);
564+
it('should treat unsupported runtime prefixes as file paths', () => {
565+
mockFs.existsSync.mockReturnValue(true);
566+
567+
// With whitelist approach, 'unsupported:' is not recognized as a runtime spec
568+
// so 'unsupported:/path/to/file.js' is treated as a file path
569+
const result = parseExecutableSpec('unsupported:/path/to/file.js');
570+
571+
// Should be treated as a file path, not a runtime specification
572+
expect(result.isExplicitRuntime).toBe(false);
573+
expect(result.runtime).toBeUndefined();
490574
});
491575

492576
it('should validate runtime availability for explicit runtime specs', () => {

0 commit comments

Comments
 (0)