@hyperbrowser/sdk 0.92.7 → 1.0.0

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.
Files changed (68) hide show
  1. package/README.md +190 -0
  2. package/dist/client.d.ts +4 -20
  3. package/dist/client.js +7 -17
  4. package/dist/error.d.ts +27 -0
  5. package/dist/error.js +32 -0
  6. package/dist/image-builds.d.ts +6 -0
  7. package/dist/image-builds.js +12 -0
  8. package/dist/index.d.ts +3 -0
  9. package/dist/index.js +12 -1
  10. package/dist/retry.d.ts +14 -0
  11. package/dist/retry.js +49 -0
  12. package/dist/sandbox/base.d.ts +27 -0
  13. package/dist/sandbox/base.js +298 -99
  14. package/dist/sandbox/files.d.ts +24 -25
  15. package/dist/sandbox/files.js +146 -37
  16. package/dist/sandbox/image-build/artifacts.d.ts +16 -0
  17. package/dist/sandbox/image-build/artifacts.js +16 -0
  18. package/dist/sandbox/image-build/blake2b.d.ts +2 -0
  19. package/dist/sandbox/image-build/blake2b.js +89 -0
  20. package/dist/sandbox/image-build/common.d.ts +19 -0
  21. package/dist/sandbox/image-build/common.js +109 -0
  22. package/dist/sandbox/image-build/context.d.ts +43 -0
  23. package/dist/sandbox/image-build/context.js +599 -0
  24. package/dist/sandbox/image-build/docker-image.d.ts +34 -0
  25. package/dist/sandbox/image-build/docker-image.js +472 -0
  26. package/dist/sandbox/image-build/dockerfile-analysis.d.ts +17 -0
  27. package/dist/sandbox/image-build/dockerfile-analysis.js +409 -0
  28. package/dist/sandbox/image-build/dockerignore.d.ts +18 -0
  29. package/dist/sandbox/image-build/dockerignore.js +275 -0
  30. package/dist/sandbox/image-build/gzip.d.ts +11 -0
  31. package/dist/sandbox/image-build/gzip.js +91 -0
  32. package/dist/sandbox/image-build/image-init.d.ts +5 -0
  33. package/dist/sandbox/image-build/image-init.js +97 -0
  34. package/dist/sandbox/image-build/index.d.ts +13 -0
  35. package/dist/sandbox/image-build/index.js +36 -0
  36. package/dist/sandbox/image-build/local-docker.d.ts +11 -0
  37. package/dist/sandbox/image-build/local-docker.js +72 -0
  38. package/dist/sandbox/image-build/resolution.d.ts +27 -0
  39. package/dist/sandbox/image-build/resolution.js +137 -0
  40. package/dist/sandbox/image-build/tar.d.ts +41 -0
  41. package/dist/sandbox/image-build/tar.js +378 -0
  42. package/dist/sandbox/image-build/upload.d.ts +10 -0
  43. package/dist/sandbox/image-build/upload.js +143 -0
  44. package/dist/sandbox/index.d.ts +2 -1
  45. package/dist/sandbox/index.js +4 -1
  46. package/dist/sandbox/process-output.d.ts +36 -0
  47. package/dist/sandbox/process-output.js +117 -0
  48. package/dist/sandbox/process.d.ts +25 -1
  49. package/dist/sandbox/process.js +197 -29
  50. package/dist/sandbox/terminal.js +9 -5
  51. package/dist/sandbox/ws.d.ts +4 -1
  52. package/dist/sandbox/ws.js +33 -7
  53. package/dist/services/agents/jev-computer-use.d.ts +30 -0
  54. package/dist/services/agents/jev-computer-use.js +103 -0
  55. package/dist/services/base.d.ts +2 -0
  56. package/dist/services/base.js +46 -30
  57. package/dist/services/normalize.d.ts +3 -0
  58. package/dist/services/normalize.js +21 -0
  59. package/dist/services/sandboxes.d.ts +40 -3
  60. package/dist/services/sandboxes.js +569 -53
  61. package/dist/services/volumes.js +29 -14
  62. package/dist/types/agents/jev-computer-use.d.ts +43 -0
  63. package/dist/types/agents/jev-computer-use.js +2 -0
  64. package/dist/types/constants.d.ts +5 -2
  65. package/dist/types/index.d.ts +4 -2
  66. package/dist/types/sandbox.d.ts +246 -30
  67. package/dist/types/volume.d.ts +7 -7
  68. package/package.json +24 -5
@@ -0,0 +1,409 @@
1
+ "use strict";
2
+ /**
3
+ * Conservative Dockerfile analysis for remote build context selection.
4
+ *
5
+ * This module intentionally does not try to execute or fully reproduce the
6
+ * BuildKit Dockerfile frontend. It recognizes the context-consuming syntax the
7
+ * SDK can analyze safely and requests a full context for anything ambiguous.
8
+ * That keeps sparse uploads an optimization rather than a correctness
9
+ * requirement.
10
+ */
11
+ Object.defineProperty(exports, "__esModule", { value: true });
12
+ exports.analyzeDockerfileSources = exports.shlexSplit = void 0;
13
+ const KNOWN_INSTRUCTIONS = new Set([
14
+ "ADD",
15
+ "ARG",
16
+ "CMD",
17
+ "COPY",
18
+ "ENTRYPOINT",
19
+ "ENV",
20
+ "EXPOSE",
21
+ "FROM",
22
+ "HEALTHCHECK",
23
+ "LABEL",
24
+ "MAINTAINER",
25
+ "ONBUILD",
26
+ "RUN",
27
+ "SHELL",
28
+ "STOPSIGNAL",
29
+ "USER",
30
+ "VOLUME",
31
+ "WORKDIR",
32
+ ]);
33
+ const KNOWN_RUN_FLAGS = new Set(["device", "mount", "network", "security"]);
34
+ const INSTRUCTION_PATTERN = /^([A-Za-z]+)(?:[ \t]+(.*))?$/s;
35
+ const DIRECTIVE_PATTERN = /^\s*#\s*(escape|syntax)\s*=\s*(\S+)/gim;
36
+ const FLAG_NAME_PATTERN = /^[a-z][a-z0-9-]*$/;
37
+ const SCP_GIT_SOURCE_PATTERN = /^git@[^:/\s]+:.+/;
38
+ // Python splitlines includes these control separators.
39
+ // eslint-disable-next-line no-control-regex
40
+ const LINE_SEPARATOR_PATTERN = /\r\n|[\n\r\v\f\x1c\x1d\x1e\x85\u2028\u2029]/;
41
+ class AnalysisFallback extends Error {
42
+ constructor(reason) {
43
+ super(reason);
44
+ this.reason = reason;
45
+ }
46
+ }
47
+ class ParseError extends Error {
48
+ }
49
+ /** Python `str.strip()` semantics: a UTF-8 BOM is not whitespace and must not be trimmed. */
50
+ const stripWhitespace = (value) => value.replace(/^(?:(?!\ufeff)\s)+|(?:(?!\ufeff)\s)+$/g, "");
51
+ /** POSIX `shlex.split` word splitting. */
52
+ const shlexSplit = (value) => {
53
+ const words = [];
54
+ let current = "";
55
+ let inWord = false;
56
+ let quote = null;
57
+ let index = 0;
58
+ while (index < value.length) {
59
+ const character = value[index];
60
+ index += 1;
61
+ if (quote === "'") {
62
+ if (character === "'") {
63
+ quote = null;
64
+ }
65
+ else {
66
+ current += character;
67
+ }
68
+ continue;
69
+ }
70
+ if (quote === '"') {
71
+ if (character === '"') {
72
+ quote = null;
73
+ }
74
+ else if (character === "\\") {
75
+ if (index >= value.length) {
76
+ throw new ParseError("No escaped character");
77
+ }
78
+ const escaped = value[index];
79
+ index += 1;
80
+ if (escaped === '"' || escaped === "\\") {
81
+ current += escaped;
82
+ }
83
+ else {
84
+ current += "\\" + escaped;
85
+ }
86
+ }
87
+ else {
88
+ current += character;
89
+ }
90
+ continue;
91
+ }
92
+ if (character === "\\") {
93
+ if (index >= value.length) {
94
+ throw new ParseError("No escaped character");
95
+ }
96
+ current += value[index];
97
+ index += 1;
98
+ inWord = true;
99
+ }
100
+ else if (character === '"' || character === "'") {
101
+ quote = character;
102
+ inWord = true;
103
+ }
104
+ else if (/\s/.test(character)) {
105
+ if (inWord) {
106
+ words.push(current);
107
+ current = "";
108
+ inWord = false;
109
+ }
110
+ }
111
+ else {
112
+ current += character;
113
+ inWord = true;
114
+ }
115
+ }
116
+ if (quote !== null) {
117
+ throw new ParseError("No closing quotation");
118
+ }
119
+ if (inWord) {
120
+ words.push(current);
121
+ }
122
+ return words;
123
+ };
124
+ exports.shlexSplit = shlexSplit;
125
+ const isOfficialDockerfileFrontend = (reference) => {
126
+ let normalized = reference.trim();
127
+ if (normalized.startsWith("docker-image://")) {
128
+ normalized = normalized.slice("docker-image://".length);
129
+ }
130
+ normalized = normalized.split("@", 1)[0];
131
+ const lastSlash = normalized.lastIndexOf("/");
132
+ const lastColon = normalized.lastIndexOf(":");
133
+ if (lastColon > lastSlash) {
134
+ normalized = normalized.slice(0, lastColon);
135
+ }
136
+ return new Set([
137
+ "docker/dockerfile",
138
+ "docker.io/docker/dockerfile",
139
+ "index.docker.io/docker/dockerfile",
140
+ "docker/dockerfile-upstream",
141
+ "docker.io/docker/dockerfile-upstream",
142
+ "index.docker.io/docker/dockerfile-upstream",
143
+ ]).has(normalized);
144
+ };
145
+ const isRemoteAddSource = (source) => {
146
+ if (SCP_GIT_SOURCE_PATTERN.test(source)) {
147
+ return true;
148
+ }
149
+ return ["http://", "https://", "git://", "ssh://"].some((prefix) => source.startsWith(prefix));
150
+ };
151
+ const sourceHasPattern = (source) => source.includes("*") || source.includes("?") || source.includes("[");
152
+ const takeWord = (value) => {
153
+ let quote = null;
154
+ let escaped = false;
155
+ for (let index = 0; index < value.length; index += 1) {
156
+ const character = value[index];
157
+ if (escaped) {
158
+ escaped = false;
159
+ continue;
160
+ }
161
+ if (character === "\\" && quote !== "'") {
162
+ escaped = true;
163
+ continue;
164
+ }
165
+ if (quote !== null) {
166
+ if (character === quote) {
167
+ quote = null;
168
+ }
169
+ continue;
170
+ }
171
+ if (character === '"' || character === "'") {
172
+ quote = character;
173
+ }
174
+ else if (/\s/.test(character)) {
175
+ return [value.slice(0, index), value.slice(index)];
176
+ }
177
+ }
178
+ if (quote !== null || escaped) {
179
+ throw new ParseError("unterminated Dockerfile instruction word");
180
+ }
181
+ return [value, ""];
182
+ };
183
+ const splitLeadingFlags = (body) => {
184
+ const flags = [];
185
+ let remaining = body.trimStart();
186
+ while (remaining.startsWith("--")) {
187
+ const [rawToken, rest] = takeWord(remaining);
188
+ remaining = rest;
189
+ const decoded = (0, exports.shlexSplit)(rawToken);
190
+ if (decoded.length !== 1 || !decoded[0].startsWith("--")) {
191
+ throw new ParseError("invalid Dockerfile instruction flag");
192
+ }
193
+ const flag = decoded[0].slice(2);
194
+ const separator = flag.indexOf("=");
195
+ const key = (separator === -1 ? flag : flag.slice(0, separator)).toLowerCase();
196
+ const value = separator === -1 ? "" : flag.slice(separator + 1);
197
+ if (!FLAG_NAME_PATTERN.test(key)) {
198
+ throw new ParseError("invalid Dockerfile instruction flag");
199
+ }
200
+ if (key === "mount" && (rawToken.includes('"') || rawToken.includes("'"))) {
201
+ // BuildKit parses mount values as CSV after Dockerfile word processing.
202
+ // Quoted CSV values are deliberately outside the subset implemented here.
203
+ throw new ParseError("quoted RUN mount requires full context");
204
+ }
205
+ flags.push([key, value]);
206
+ remaining = remaining.trimStart();
207
+ }
208
+ return [flags, remaining];
209
+ };
210
+ const parseCopyAddValues = (body) => {
211
+ const [parsedFlags, remaining] = splitLeadingFlags(body);
212
+ const flags = new Map(parsedFlags);
213
+ let values;
214
+ if (remaining.startsWith("[")) {
215
+ const parsed = JSON.parse(remaining);
216
+ if (!Array.isArray(parsed) || !parsed.every((value) => typeof value === "string")) {
217
+ throw new ParseError("invalid JSON COPY/ADD");
218
+ }
219
+ values = parsed;
220
+ }
221
+ else {
222
+ if (remaining.includes("\\")) {
223
+ // BuildKit's shell-form COPY/ADD word splitting is not POSIX shlex.
224
+ // Backslash-containing forms use the full context rather than risk
225
+ // selecting a different set of sources.
226
+ throw new ParseError("escaped shell COPY/ADD requires full context");
227
+ }
228
+ values = (0, exports.shlexSplit)(remaining);
229
+ }
230
+ if (values.length < 2) {
231
+ throw new ParseError("COPY/ADD is missing source or destination");
232
+ }
233
+ return [flags, values];
234
+ };
235
+ const parseMountOptions = (value) => {
236
+ const options = new Map();
237
+ for (const field of value.split(",")) {
238
+ const separator = field.indexOf("=");
239
+ const key = (separator === -1 ? field : field.slice(0, separator)).trim().toLowerCase();
240
+ if (!key) {
241
+ throw new AnalysisFallback("run_mount_parse_failed");
242
+ }
243
+ options.set(key, separator === -1 ? "" : field.slice(separator + 1).trim());
244
+ }
245
+ return options;
246
+ };
247
+ const parseInstruction = (line) => {
248
+ const match = INSTRUCTION_PATTERN.exec(line);
249
+ if (match === null) {
250
+ throw new AnalysisFallback("dockerfile_parse_failed");
251
+ }
252
+ return [match[1].toUpperCase(), match[2] || ""];
253
+ };
254
+ const validateParserDirectives = (text) => {
255
+ for (const match of text.matchAll(DIRECTIVE_PATTERN)) {
256
+ const name = match[1].toLowerCase();
257
+ const value = match[2];
258
+ if (name === "escape" && value !== "\\") {
259
+ throw new AnalysisFallback("dockerfile_parse_failed");
260
+ }
261
+ if (name === "syntax" && !isOfficialDockerfileFrontend(value)) {
262
+ throw new AnalysisFallback("custom_dockerfile_frontend");
263
+ }
264
+ }
265
+ };
266
+ const logicalLines = (text) => {
267
+ const lines = [];
268
+ let parts = [];
269
+ const rawLines = text.split(LINE_SEPARATOR_PATTERN);
270
+ if (rawLines.length > 0 && rawLines[rawLines.length - 1] === "") {
271
+ rawLines.pop();
272
+ }
273
+ for (const rawLine of rawLines) {
274
+ const stripped = stripWhitespace(rawLine);
275
+ if (parts.length === 0 && (!stripped || stripped.startsWith("#"))) {
276
+ continue;
277
+ }
278
+ if (parts.length > 0 && (!stripped || stripped.startsWith("#"))) {
279
+ // BuildKit has nuanced rules for comments and empty lines in a
280
+ // continuation. Full context is the safe answer here.
281
+ throw new AnalysisFallback("dockerfile_parse_failed");
282
+ }
283
+ if (stripped.endsWith("\\")) {
284
+ parts.push(stripped.slice(0, -1).trimEnd());
285
+ continue;
286
+ }
287
+ parts.push(stripped);
288
+ lines.push(parts.join(" "));
289
+ parts = [];
290
+ }
291
+ if (parts.length > 0) {
292
+ throw new AnalysisFallback("dockerfile_parse_failed");
293
+ }
294
+ return lines;
295
+ };
296
+ const analyzeCopyOrAdd = (instruction, body) => {
297
+ let flags;
298
+ let values;
299
+ try {
300
+ [flags, values] = parseCopyAddValues(body);
301
+ }
302
+ catch (error) {
303
+ if (error instanceof ParseError || error instanceof SyntaxError) {
304
+ throw new AnalysisFallback("dockerfile_instruction_parse_failed");
305
+ }
306
+ throw error;
307
+ }
308
+ if (instruction === "COPY" && flags.has("from")) {
309
+ return [];
310
+ }
311
+ const localSources = [];
312
+ for (const source of values.slice(0, -1)) {
313
+ if (source.includes("$") || source.includes("\x00")) {
314
+ throw new AnalysisFallback(instruction === "COPY" ? "copy_source_requires_expansion" : "add_source_requires_expansion");
315
+ }
316
+ if (source.includes("\\")) {
317
+ // A backslash is a path character on Unix but a separator on Windows.
318
+ // Full context keeps selection platform-independent.
319
+ throw new AnalysisFallback("dockerfile_source_path");
320
+ }
321
+ if (instruction === "ADD" && isRemoteAddSource(source)) {
322
+ continue;
323
+ }
324
+ if (sourceHasPattern(source)) {
325
+ throw new AnalysisFallback("dockerfile_source_pattern");
326
+ }
327
+ localSources.push(source);
328
+ }
329
+ return localSources;
330
+ };
331
+ const analyzeRun = (body) => {
332
+ let flags;
333
+ try {
334
+ [flags] = splitLeadingFlags(body);
335
+ }
336
+ catch (error) {
337
+ if (error instanceof ParseError) {
338
+ throw new AnalysisFallback("run_mount_parse_failed");
339
+ }
340
+ throw error;
341
+ }
342
+ const groups = [];
343
+ for (const [name, value] of flags) {
344
+ if (!KNOWN_RUN_FLAGS.has(name)) {
345
+ throw new AnalysisFallback("dockerfile_instruction_parse_failed");
346
+ }
347
+ if (name !== "mount") {
348
+ continue;
349
+ }
350
+ if (!value || value.includes('"') || value.includes("'")) {
351
+ throw new AnalysisFallback("run_mount_parse_failed");
352
+ }
353
+ const options = parseMountOptions(value);
354
+ if ((options.get("type") ?? "bind") !== "bind" || options.get("from")) {
355
+ continue;
356
+ }
357
+ const source = options.get("source") || options.get("src") || ".";
358
+ if (source.includes("$") || source.includes("\x00")) {
359
+ throw new AnalysisFallback("run_bind_source_requires_expansion");
360
+ }
361
+ groups.push([source]);
362
+ }
363
+ return groups;
364
+ };
365
+ const analyzeLines = (lines) => {
366
+ const groups = [];
367
+ for (const line of lines) {
368
+ const [instruction, body] = parseInstruction(line);
369
+ if (!KNOWN_INSTRUCTIONS.has(instruction)) {
370
+ throw new AnalysisFallback("dockerfile_instruction_parse_failed");
371
+ }
372
+ if (instruction === "COPY" || instruction === "ADD") {
373
+ const group = analyzeCopyOrAdd(instruction, body);
374
+ if (group.length > 0) {
375
+ groups.push(group);
376
+ }
377
+ }
378
+ else if (instruction === "RUN") {
379
+ groups.push(...analyzeRun(body));
380
+ }
381
+ }
382
+ return groups;
383
+ };
384
+ /** Return local source groups or a reason to upload the full context. */
385
+ const analyzeDockerfileSources = (data) => {
386
+ let text;
387
+ try {
388
+ text = new TextDecoder("utf-8", { fatal: true, ignoreBOM: true }).decode(data);
389
+ }
390
+ catch {
391
+ return { groups: [], fallbackReason: "dockerfile_parse_failed" };
392
+ }
393
+ try {
394
+ validateParserDirectives(text);
395
+ // Heredocs are valid BuildKit syntax, but treating the entire Dockerfile
396
+ // as full context is safer than partially parsing them.
397
+ if (text.includes("<<")) {
398
+ throw new AnalysisFallback("dockerfile_instruction_parse_failed");
399
+ }
400
+ return { groups: analyzeLines(logicalLines(text)), fallbackReason: "" };
401
+ }
402
+ catch (error) {
403
+ if (error instanceof AnalysisFallback) {
404
+ return { groups: [], fallbackReason: error.reason };
405
+ }
406
+ throw error;
407
+ }
408
+ };
409
+ exports.analyzeDockerfileSources = analyzeDockerfileSources;
@@ -0,0 +1,18 @@
1
+ /**
2
+ * Docker-compatible `.dockerignore` parsing and matching.
3
+ *
4
+ * The matching contract intentionally follows Moby `patternmatcher` v0.6.1,
5
+ * which is also what the Hyperbrowser CLI reaches through BuildKit's
6
+ * filesystem utilities.
7
+ */
8
+ /** Parse ignore-file lines like Moby's `ignorefile.ReadAll`. */
9
+ export declare const readIgnorePatterns: (text: string) => string[];
10
+ /** Ordered Docker ignore matcher with parent-directory semantics. */
11
+ export declare class DockerIgnoreMatcher {
12
+ private readonly patterns;
13
+ readonly hasNegations: boolean;
14
+ constructor(patterns: Iterable<string>);
15
+ static fromFile(path: string): DockerIgnoreMatcher;
16
+ static fromText(text: string): DockerIgnoreMatcher;
17
+ matches(relativePath: string): boolean;
18
+ }
@@ -0,0 +1,275 @@
1
+ "use strict";
2
+ /**
3
+ * Docker-compatible `.dockerignore` parsing and matching.
4
+ *
5
+ * The matching contract intentionally follows Moby `patternmatcher` v0.6.1,
6
+ * which is also what the Hyperbrowser CLI reaches through BuildKit's
7
+ * filesystem utilities.
8
+ */
9
+ Object.defineProperty(exports, "__esModule", { value: true });
10
+ exports.DockerIgnoreMatcher = exports.readIgnorePatterns = void 0;
11
+ const fs_1 = require("fs");
12
+ const common_1 = require("./common");
13
+ const REGEX_META_WITHOUT_GLOB_MEANING = new Set(".+()|{}$");
14
+ /** Apply the Unix `filepath.Clean` behavior used by the CLI. */
15
+ const cleanPath = (value) => {
16
+ if (value.startsWith("//")) {
17
+ value = "/" + value.replace(/^\/+/, "");
18
+ }
19
+ return (0, common_1.posixNormpath)(value);
20
+ };
21
+ /** Parse ignore-file lines like Moby's `ignorefile.ReadAll`. */
22
+ const readIgnorePatterns = (text) => {
23
+ const patterns = [];
24
+ text.split("\n").forEach((rawLine, index) => {
25
+ if (rawLine.endsWith("\r")) {
26
+ rawLine = rawLine.slice(0, -1);
27
+ }
28
+ if (index === 0 && rawLine.startsWith("\ufeff")) {
29
+ rawLine = rawLine.slice(1);
30
+ }
31
+ if (rawLine.startsWith("#")) {
32
+ return;
33
+ }
34
+ let value = rawLine.trim();
35
+ if (!value) {
36
+ return;
37
+ }
38
+ const exclusion = value.startsWith("!");
39
+ if (exclusion) {
40
+ value = value.slice(1).trim();
41
+ }
42
+ if (value) {
43
+ value = cleanPath(value);
44
+ if (value.length > 1 && value.startsWith("/")) {
45
+ value = value.slice(1);
46
+ }
47
+ }
48
+ patterns.push((exclusion ? "!" : "") + value);
49
+ });
50
+ return patterns;
51
+ };
52
+ exports.readIgnorePatterns = readIgnorePatterns;
53
+ const validateCharacterClass = (pattern, cursor) => {
54
+ const consumeValue = (position) => {
55
+ if (position >= pattern.length || pattern[position] === "-" || pattern[position] === "]") {
56
+ throw new Error(`invalid Docker ignore pattern "${pattern}"`);
57
+ }
58
+ if (pattern[position] === "\\") {
59
+ position += 1;
60
+ if (position >= pattern.length) {
61
+ throw new Error(`invalid Docker ignore pattern "${pattern}"`);
62
+ }
63
+ }
64
+ position += 1;
65
+ if (position >= pattern.length) {
66
+ throw new Error(`invalid Docker ignore pattern "${pattern}"`);
67
+ }
68
+ return position;
69
+ };
70
+ if (cursor < pattern.length && pattern[cursor] === "^") {
71
+ cursor += 1;
72
+ }
73
+ let ranges = 0;
74
+ while (true) {
75
+ if (cursor < pattern.length && pattern[cursor] === "]" && ranges) {
76
+ return cursor + 1;
77
+ }
78
+ cursor = consumeValue(cursor);
79
+ if (cursor < pattern.length && pattern[cursor] === "-") {
80
+ cursor = consumeValue(cursor + 1);
81
+ }
82
+ ranges += 1;
83
+ }
84
+ };
85
+ /** Reject malformed patterns using Go filepath.Match's Unix grammar. */
86
+ const validateFilepathPattern = (pattern) => {
87
+ let cursor = 0;
88
+ while (cursor < pattern.length) {
89
+ const character = pattern[cursor];
90
+ if (character === "\\") {
91
+ cursor += 1;
92
+ if (cursor === pattern.length) {
93
+ throw new Error(`invalid Docker ignore pattern "${pattern}": trailing escape`);
94
+ }
95
+ }
96
+ else if (character === "[") {
97
+ cursor = validateCharacterClass(pattern, cursor + 1);
98
+ continue;
99
+ }
100
+ cursor += 1;
101
+ }
102
+ };
103
+ /** Compile one cleaned Moby pattern into its optimized match form. */
104
+ const compilePattern = (pattern) => {
105
+ const parts = ["^"];
106
+ let matchType = "exact";
107
+ let cursor = 0;
108
+ let tokenIndex = 0;
109
+ let inClass = false;
110
+ while (cursor < pattern.length) {
111
+ const character = pattern[cursor];
112
+ cursor += 1;
113
+ if (character === "*") {
114
+ if (cursor < pattern.length && pattern[cursor] === "*") {
115
+ cursor += 1;
116
+ if (cursor < pattern.length && pattern[cursor] === "/") {
117
+ cursor += 1;
118
+ }
119
+ if (cursor === pattern.length) {
120
+ if (matchType === "exact") {
121
+ matchType = "prefix";
122
+ }
123
+ else {
124
+ parts.push(".*");
125
+ matchType = "regexp";
126
+ }
127
+ }
128
+ else {
129
+ parts.push("(?:.*/)?");
130
+ matchType = "regexp";
131
+ }
132
+ if (tokenIndex === 0) {
133
+ matchType = "suffix";
134
+ }
135
+ }
136
+ else {
137
+ parts.push("[^/]*");
138
+ matchType = "regexp";
139
+ }
140
+ }
141
+ else if (character === "?") {
142
+ parts.push("[^/]");
143
+ matchType = "regexp";
144
+ }
145
+ else if (REGEX_META_WITHOUT_GLOB_MEANING.has(character)) {
146
+ parts.push("\\" + character);
147
+ }
148
+ else if (character === "\\") {
149
+ if (cursor < pattern.length) {
150
+ const codepoint = pattern.codePointAt(cursor);
151
+ parts.push(`\\u{${codepoint.toString(16)}}`);
152
+ cursor += codepoint > 0xffff ? 2 : 1;
153
+ matchType = "regexp";
154
+ }
155
+ else {
156
+ throw new Error(`invalid Docker ignore pattern "${pattern}": trailing escape`);
157
+ }
158
+ }
159
+ else {
160
+ // Brackets remain regex syntax because they are also filepath glob
161
+ // syntax. All other characters are literal in the Moby compiler.
162
+ parts.push(character === "]" && !inClass ? "\\]" : character);
163
+ if (character === "[")
164
+ inClass = true;
165
+ else if (character === "]")
166
+ inClass = false;
167
+ if (character === "[" || character === "]") {
168
+ matchType = "regexp";
169
+ }
170
+ }
171
+ tokenIndex += 1;
172
+ }
173
+ if (matchType !== "regexp") {
174
+ return { matchType, regexp: null };
175
+ }
176
+ parts.push("$");
177
+ try {
178
+ // Python and Go wildcards consume Unicode codepoints, not UTF-16 units.
179
+ return { matchType, regexp: new RegExp(parts.join(""), "u") };
180
+ }
181
+ catch (error) {
182
+ throw new Error(`invalid Docker ignore pattern "${pattern}": ${error instanceof Error ? error.message : error}`);
183
+ }
184
+ };
185
+ class DockerIgnorePattern {
186
+ constructor(rawPattern) {
187
+ let value = cleanPath(rawPattern.trim());
188
+ const exclusion = value.startsWith("!");
189
+ if (exclusion) {
190
+ if (value === "!") {
191
+ throw new Error('illegal exclusion pattern: "!"');
192
+ }
193
+ value = value.slice(1);
194
+ }
195
+ validateFilepathPattern(value);
196
+ const compiled = compilePattern(value);
197
+ this.value = value;
198
+ this.exclusion = exclusion;
199
+ this.matchType = compiled.matchType;
200
+ this.regexp = compiled.regexp;
201
+ }
202
+ matchesPath(path) {
203
+ if (this.matchType === "exact") {
204
+ return path === this.value;
205
+ }
206
+ if (this.matchType === "prefix") {
207
+ return path.startsWith(this.value.slice(0, -2));
208
+ }
209
+ if (this.matchType === "suffix") {
210
+ const suffix = this.value.slice(2);
211
+ return path.endsWith(suffix) || (suffix.startsWith("/") && path === suffix.slice(1));
212
+ }
213
+ if (this.regexp === null) {
214
+ throw new Error(`invalid Docker ignore pattern: "${this.value}"`);
215
+ }
216
+ return this.regexp.test(path);
217
+ }
218
+ }
219
+ /** Ordered Docker ignore matcher with parent-directory semantics. */
220
+ class DockerIgnoreMatcher {
221
+ constructor(patterns) {
222
+ const compiled = [];
223
+ for (const pattern of patterns) {
224
+ const cleaned = pattern.trim();
225
+ if (!cleaned) {
226
+ continue;
227
+ }
228
+ compiled.push(new DockerIgnorePattern(cleaned));
229
+ }
230
+ this.patterns = compiled;
231
+ this.hasNegations = compiled.some((pattern) => pattern.exclusion);
232
+ }
233
+ static fromFile(path) {
234
+ let text = (0, fs_1.readFileSync)(path).toString("utf8");
235
+ if (text.startsWith("\ufeff")) {
236
+ text = text.slice(1);
237
+ }
238
+ return new DockerIgnoreMatcher((0, exports.readIgnorePatterns)(text));
239
+ }
240
+ static fromText(text) {
241
+ return new DockerIgnoreMatcher((0, exports.readIgnorePatterns)(text));
242
+ }
243
+ matches(relativePath) {
244
+ // Traversal supplies slash-delimited paths. On Unix, a backslash can be
245
+ // part of a filename and must remain available for glob escaping.
246
+ const path = cleanPath(relativePath);
247
+ if (path === ".") {
248
+ return false;
249
+ }
250
+ const parent = (0, common_1.posixDirname)(path);
251
+ const parentParts = parent && parent !== "." ? parent.split("/") : [];
252
+ let matched = false;
253
+ for (const pattern of this.patterns) {
254
+ // An exclusion can only re-include an ignored path, and a normal pattern
255
+ // only needs checking while the path is included.
256
+ if (pattern.exclusion !== matched) {
257
+ continue;
258
+ }
259
+ let patternMatches = pattern.matchesPath(path);
260
+ if (!patternMatches) {
261
+ for (let length = 1; length <= parentParts.length; length += 1) {
262
+ if (pattern.matchesPath(parentParts.slice(0, length).join("/"))) {
263
+ patternMatches = true;
264
+ break;
265
+ }
266
+ }
267
+ }
268
+ if (patternMatches) {
269
+ matched = !pattern.exclusion;
270
+ }
271
+ }
272
+ return matched;
273
+ }
274
+ }
275
+ exports.DockerIgnoreMatcher = DockerIgnoreMatcher;