@opencode/util 2.0.21 → 2.0.22

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,4 @@
1
+ /** How the app shows a file, from its extension. */
2
+ export type ArtifactKind = "image" | "svg" | "audio" | "video" | "pdf" | "html" | "markdown" | "mermaid" | "table" | "font" | "text";
3
+ export declare function artifactMime(path: string): string | undefined;
4
+ export declare function artifactKind(path: string): ArtifactKind;
@@ -0,0 +1,77 @@
1
+ const mimes = new Map([
2
+ ["png", "image/png"],
3
+ ["jpg", "image/jpeg"],
4
+ ["jpeg", "image/jpeg"],
5
+ ["gif", "image/gif"],
6
+ ["webp", "image/webp"],
7
+ ["avif", "image/avif"],
8
+ ["bmp", "image/bmp"],
9
+ ["ico", "image/x-icon"],
10
+ ["tif", "image/tiff"],
11
+ ["tiff", "image/tiff"],
12
+ ["heic", "image/heic"],
13
+ ["svg", "image/svg+xml"],
14
+ ["mp3", "audio/mpeg"],
15
+ ["wav", "audio/wav"],
16
+ ["ogg", "audio/ogg"],
17
+ ["oga", "audio/ogg"],
18
+ ["m4a", "audio/mp4"],
19
+ ["aac", "audio/aac"],
20
+ ["flac", "audio/flac"],
21
+ ["opus", "audio/ogg"],
22
+ ["weba", "audio/webm"],
23
+ ["mp4", "video/mp4"],
24
+ ["m4v", "video/mp4"],
25
+ ["webm", "video/webm"],
26
+ ["mov", "video/quicktime"],
27
+ ["ogv", "video/ogg"],
28
+ ["mkv", "video/x-matroska"],
29
+ ["pdf", "application/pdf"],
30
+ ["html", "text/html"],
31
+ ["htm", "text/html"],
32
+ ["md", "text/markdown"],
33
+ ["markdown", "text/markdown"],
34
+ ["mdx", "text/markdown"],
35
+ ["mmd", "text/vnd.mermaid"],
36
+ ["mermaid", "text/vnd.mermaid"],
37
+ ["csv", "text/csv"],
38
+ ["tsv", "text/tab-separated-values"],
39
+ ["ttf", "font/ttf"],
40
+ ["otf", "font/otf"],
41
+ ["woff", "font/woff"],
42
+ ["woff2", "font/woff2"],
43
+ ]);
44
+ function artifactExtension(path) {
45
+ const name = path.split(/[\\/]/).pop() ?? "";
46
+ const index = name.lastIndexOf(".");
47
+ if (index <= 0)
48
+ return "";
49
+ return name.slice(index + 1).toLowerCase();
50
+ }
51
+ export function artifactMime(path) {
52
+ return mimes.get(artifactExtension(path));
53
+ }
54
+ export function artifactKind(path) {
55
+ const mime = artifactMime(path);
56
+ if (!mime)
57
+ return "text";
58
+ if (mime === "image/svg+xml")
59
+ return "svg";
60
+ if (mime === "application/pdf")
61
+ return "pdf";
62
+ if (mime === "text/html")
63
+ return "html";
64
+ if (mime === "text/markdown")
65
+ return "markdown";
66
+ if (mime === "text/vnd.mermaid")
67
+ return "mermaid";
68
+ if (mime === "text/csv" || mime === "text/tab-separated-values")
69
+ return "table";
70
+ if (mime.startsWith("image/"))
71
+ return "image";
72
+ if (mime.startsWith("audio/"))
73
+ return "audio";
74
+ if (mime.startsWith("font/"))
75
+ return "font";
76
+ return "video";
77
+ }
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,24 @@
1
+ import { describe, expect, test } from "bun:test";
2
+ import { artifactKind } from "./artifact.js";
3
+ describe("artifactKind", () => {
4
+ test.each([
5
+ ["shot.PNG", "image"],
6
+ ["logo.svg", "svg"],
7
+ ["song.mp3", "audio"],
8
+ ["demo.mp4", "video"],
9
+ ["clip.webm", "video"],
10
+ ["paper.pdf", "pdf"],
11
+ ["out/index.html", "html"],
12
+ ["README.md", "markdown"],
13
+ ["flow.mmd", "mermaid"],
14
+ ["data.csv", "table"],
15
+ ["data.tsv", "table"],
16
+ ["Inter.woff2", "font"],
17
+ ["src/app.ts", "text"],
18
+ ["Makefile", "text"],
19
+ [".env", "text"],
20
+ ["archive.tar.gz", "text"],
21
+ ])("classifies %s as %s", (path, kind) => {
22
+ expect(artifactKind(path)).toBe(kind);
23
+ });
24
+ });
package/dist/fs-util.d.ts CHANGED
@@ -19,6 +19,8 @@ export declare namespace FSUtil {
19
19
  readonly start: string;
20
20
  readonly stop?: string;
21
21
  readonly mode?: "all" | "first";
22
+ /** Only match regular files or directories (following symlinks). By default any existing path matches. */
23
+ readonly type?: "file" | "directory";
22
24
  }
23
25
  export interface Interface extends FileSystem.FileSystem {
24
26
  readonly isDir: (path: string) => Effect.Effect<boolean>;
package/dist/fs-util.js CHANGED
@@ -97,7 +97,12 @@ export var FSUtil;
97
97
  while (true) {
98
98
  for (const target of options.targets) {
99
99
  const search = join(current, target);
100
- if (yield* fs.exists(search)) {
100
+ const found = options.type === "file"
101
+ ? yield* isFile(search)
102
+ : options.type === "directory"
103
+ ? yield* isDir(search)
104
+ : yield* fs.exists(search);
105
+ if (found) {
101
106
  result.push(search);
102
107
  if (options.mode === "first")
103
108
  return result;
package/dist/path.d.ts CHANGED
@@ -2,3 +2,9 @@ export declare function getFilename(path: string | undefined): string;
2
2
  export declare function getDirectory(path: string | undefined): string;
3
3
  export declare function getFilenameTruncated(path: string | undefined, maxLength?: number): string;
4
4
  export declare function truncateMiddle(text: string, maxLength?: number): string;
5
+ /** A comparable form of a directory path: forward slashes for Windows paths, no trailing slash except on roots. */
6
+ export declare function comparablePath(path: string): string;
7
+ /** `child` is `parent` or inside it. Windows drive and UNC paths compare case-insensitively. */
8
+ export declare function containsDirectory(parent: string, child: string): boolean;
9
+ export declare function sameDirectory(a: string, b: string): boolean;
10
+ export declare function encodeFilePath(filepath: string): string;
package/dist/path.js CHANGED
@@ -31,3 +31,57 @@ export function truncateMiddle(text, maxLength = 20) {
31
31
  const end = Math.floor(available / 2);
32
32
  return text.slice(0, start) + "…" + text.slice(-end);
33
33
  }
34
+ const isDrive = (value) => {
35
+ if (value.length !== 2)
36
+ return false;
37
+ const code = value.charCodeAt(0);
38
+ return value[1] === ":" && ((code >= 65 && code <= 90) || (code >= 97 && code <= 122));
39
+ };
40
+ const trimTrailingSlashes = (value) => {
41
+ for (let i = value.length - 1; i >= 0; i--) {
42
+ if (value[i] !== "/")
43
+ return value.slice(0, i + 1);
44
+ }
45
+ return "";
46
+ };
47
+ const isWindowsPath = (value) => value[1] === ":" || value.startsWith("\\\\");
48
+ /** A comparable form of a directory path: forward slashes for Windows paths, no trailing slash except on roots. */
49
+ export function comparablePath(path) {
50
+ const value = isWindowsPath(path) ? path.replaceAll("\\", "/") : path;
51
+ const trimmed = trimTrailingSlashes(value);
52
+ if (!trimmed && value.startsWith("/"))
53
+ return "/";
54
+ if (isDrive(trimmed))
55
+ return `${trimmed}/`;
56
+ return trimmed;
57
+ }
58
+ /** `child` is `parent` or inside it. Windows drive and UNC paths compare case-insensitively. */
59
+ export function containsDirectory(parent, child) {
60
+ const normalize = (value) => {
61
+ const key = comparablePath(value);
62
+ return /^[a-z]:\//i.test(key) || key.startsWith("//") ? key.toLowerCase() : key;
63
+ };
64
+ const root = normalize(parent);
65
+ const target = normalize(child);
66
+ return target === root || target.startsWith(root.endsWith("/") ? root : `${root}/`);
67
+ }
68
+ export function sameDirectory(a, b) {
69
+ return containsDirectory(a, b) && containsDirectory(b, a);
70
+ }
71
+ export function encodeFilePath(filepath) {
72
+ // Normalize Windows paths: convert backslashes to forward slashes
73
+ const normalized = filepath.replace(/\\/g, "/");
74
+ // Handle Windows absolute paths (D:/path -> /D:/path for proper file:// URLs)
75
+ const rooted = /^[A-Za-z]:/.test(normalized) ? "/" + normalized : normalized;
76
+ // Encode each path segment (preserving forward slashes as path separators)
77
+ // Keep the colon in Windows drive letters (`/C:/...`) so downstream file URL parsers
78
+ // can reliably detect drives.
79
+ return rooted
80
+ .split("/")
81
+ .map((segment, index) => {
82
+ if (index === 1 && /^[A-Za-z]:$/.test(segment))
83
+ return segment;
84
+ return encodeURIComponent(segment);
85
+ })
86
+ .join("/");
87
+ }
package/dist/path.test.js CHANGED
@@ -1,5 +1,5 @@
1
1
  import { describe, expect, test } from "bun:test";
2
- import { getDirectory, getFilename, getFilenameTruncated, truncateMiddle } from "./path.js";
2
+ import { encodeFilePath, getDirectory, getFilename, getFilenameTruncated, truncateMiddle } from "./path.js";
3
3
  describe("client paths", () => {
4
4
  test("reads POSIX and Windows paths with the same rules", () => {
5
5
  expect(getFilename("/repo/src/index.ts")).toBe("index.ts");
@@ -34,3 +34,38 @@ describe("client paths", () => {
34
34
  expect(truncateMiddle("abcdefghijklmnop", 9)).toBe("abcd…mnop");
35
35
  });
36
36
  });
37
+ describe("encodeFilePath", () => {
38
+ const rows = [
39
+ ["/home/user/project/README.md", "/home/user/project/README.md"],
40
+ ["/home/user/file#name with spaces.txt", "/home/user/file%23name%20with%20spaces.txt"],
41
+ ["/path/to/file#with?special%chars&more.txt", "/path/to/file%23with%3Fspecial%25chars%26more.txt"],
42
+ ["/path/to/file?name.txt", "/path/to/file%3Fname.txt"],
43
+ ["/path/to/file%name.txt", "/path/to/file%25name.txt"],
44
+ ["/home/user/文档/README.md", "/home/user/%E6%96%87%E6%A1%A3/README.md"],
45
+ ["/", "/"],
46
+ ["", ""],
47
+ ["//path//to///file.txt", "//path//to///file.txt"],
48
+ ["src/components/App.tsx", "src/components/App.tsx"],
49
+ ["src\\components\\App.tsx", "src/components/App.tsx"],
50
+ ["D:\\dev\\projects\\opencode\\README.bs.md", "/D:/dev/projects/opencode/README.bs.md"],
51
+ ["D:\\dev\\projects\\opencode/README.bs.md", "/D:/dev/projects/opencode/README.bs.md"],
52
+ ["C:\\Program Files\\MyApp\\file with spaces.txt", "/C:/Program%20Files/MyApp/file%20with%20spaces.txt"],
53
+ ["D:\\projects\\file#name with ?marks.txt", "/D:/projects/file%23name%20with%20%3Fmarks.txt"],
54
+ ["C:\\", "/C:/"],
55
+ ["c:\\users\\test\\file.txt", "/c:/users/test/file.txt"],
56
+ ["D:", "/D:"],
57
+ ["C:\\Users\\test\\", "/C:/Users/test/"],
58
+ ["C:\\Users\\..\\test\\.\\file.txt", "/C:/Users/../test/./file.txt"],
59
+ ["/D:/path/file.txt", "/D:/path/file.txt"],
60
+ ];
61
+ test.each(rows)("encodes %p as %p", (input, expected) => {
62
+ expect(encodeFilePath(input)).toBe(expected);
63
+ });
64
+ test("every encoded path forms a file URL that keeps its query", () => {
65
+ rows.forEach(([input]) => {
66
+ const url = new URL(`file://${encodeFilePath(input)}?start=10`);
67
+ expect(url.protocol).toBe("file:");
68
+ expect(url.searchParams.get("start")).toBe("10");
69
+ });
70
+ });
71
+ });
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "$schema": "https://json.schemastore.org/package.json",
3
3
  "name": "@opencode/util",
4
- "version": "2.0.21",
4
+ "version": "2.0.22",
5
5
  "type": "module",
6
6
  "license": "MIT",
7
7
  "repository": {