Skip to content

Commit 9c6bc86

Browse files
committed
refactor: extract path normalization in FileSyncer to separate util
1 parent 2310455 commit 9c6bc86

2 files changed

Lines changed: 66 additions & 42 deletions

File tree

src/pathUtils.ts

Lines changed: 64 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,64 @@
1+
import * as path from "path";
2+
3+
/**
4+
* Normalize a path to the standard Humanloop API format.
5+
*
6+
* This function is primarily used when interacting with the Humanloop API to ensure paths
7+
* follow the standard format: 'path/to/resource' without leading/trailing slashes.
8+
* It's used when pulling files from Humanloop to local filesystem (see FileSyncer.pull)
9+
*
10+
* The function:
11+
* - Converts Windows backslashes to forward slashes
12+
* - Normalizes consecutive slashes
13+
* - Optionally strips file extensions (e.g. .prompt, .agent)
14+
* - Removes leading/trailing slashes to match API conventions
15+
*
16+
* Leading/trailing slashes are stripped because the Humanloop API expects paths in the
17+
* format 'path/to/resource' without them. This is consistent with how the API stores
18+
* and references files, and ensures paths work correctly in both API calls and local
19+
* filesystem operations.
20+
*
21+
* @param pathStr - The path to normalize. Can be a Windows or Unix-style path.
22+
* @param stripExtension - If true, removes the file extension (e.g. .prompt, .agent)
23+
* @returns Normalized path string in the format 'path/to/resource'
24+
*
25+
* @example
26+
* normalizePath("path/to/file.prompt")
27+
* // => 'path/to/file.prompt'
28+
*
29+
* @example
30+
* normalizePath("path/to/file.prompt", true)
31+
* // => 'path/to/file'
32+
*
33+
* @example
34+
* normalizePath("\\windows\\style\\path.prompt")
35+
* // => 'windows/style/path.prompt'
36+
*
37+
* @example
38+
* normalizePath("/leading/slash/path/")
39+
* // => 'leading/slash/path'
40+
*
41+
* @example
42+
* normalizePath("multiple//slashes//path")
43+
* // => 'multiple/slashes/path'
44+
*/
45+
export function normalizePath(
46+
pathStr: string,
47+
stripExtension: boolean = false,
48+
): string {
49+
// Convert Windows backslashes to forward slashes
50+
const normalizedSeparators = pathStr.replace(/\\/g, "/");
51+
52+
// Use path.posix to handle path normalization (handles consecutive slashes)
53+
// We use posix to ensure forward slashes are used consistently
54+
let normalizedPath = path.posix.normalize(normalizedSeparators);
55+
56+
// Strip extension if requested
57+
if (stripExtension) {
58+
const ext = path.posix.extname(normalizedPath);
59+
normalizedPath = normalizedPath.slice(0, -ext.length);
60+
}
61+
62+
// Remove leading/trailing slashes
63+
return normalizedPath.replace(/^\/+|\/+$/g, "");
64+
}

src/sync/FileSyncer.ts

Lines changed: 2 additions & 42 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ import path from "path";
55
import LRUCache from "../cache/LRUCache";
66
import { HumanloopRuntimeError } from "../error";
77
import { HumanloopClient } from "../humanloop.client";
8+
import * as pathUtils from "../pathUtils";
89

910
// Default cache size for file content caching
1011
const DEFAULT_CACHE_SIZE = 100;
@@ -380,7 +381,7 @@ export default class FileSyncer {
380381
isFilePath = this.isFile(filePath);
381382

382383
// For API communication, we need path without extension
383-
apiPath = this._normalizePath(filePath, true);
384+
apiPath = pathUtils.normalizePath(filePath, true);
384385
}
385386

386387
try {
@@ -424,45 +425,4 @@ export default class FileSyncer {
424425
throw new HumanloopRuntimeError(`Pull operation failed: ${error}`);
425426
}
426427
}
427-
428-
/**
429-
* Normalize the path by removing extensions, etc.
430-
*/
431-
private _normalizePath(filePath: string, stripExtension: boolean = false): string {
432-
if (!filePath) return "";
433-
434-
// Remove any file extensions if requested
435-
let normalizedPath =
436-
stripExtension && filePath.includes(".")
437-
? filePath.substring(0, filePath.lastIndexOf("."))
438-
: filePath;
439-
440-
// Convert backslashes to forward slashes
441-
normalizedPath = normalizedPath.replace(/\\/g, "/");
442-
443-
// Remove leading/trailing whitespace and slashes
444-
normalizedPath = normalizedPath.trim().replace(/^\/+|\/+$/g, "");
445-
446-
// Normalize multiple consecutive slashes into a single forward slash
447-
while (normalizedPath.includes("//")) {
448-
normalizedPath = normalizedPath.replace(/\/\//g, "/");
449-
}
450-
451-
return normalizedPath;
452-
}
453-
454-
private _parseErrorResponse(response: any): string {
455-
try {
456-
if (response?.error?.message) {
457-
return response.error.message;
458-
}
459-
if (typeof response === "string") {
460-
return response;
461-
}
462-
return JSON.stringify(response);
463-
} catch (e) {
464-
log(`Failed to parse error message: ${e}`, "DEBUG", this.verbose);
465-
return String(response);
466-
}
467-
}
468428
}

0 commit comments

Comments
 (0)