@visulima/task-runner 0.0.1 → 1.0.0-alpha.2

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 (49) hide show
  1. package/CHANGELOG.md +21 -0
  2. package/LICENSE.md +21 -0
  3. package/README.md +170 -36
  4. package/binding.js +204 -0
  5. package/dist/affected.d.ts +48 -0
  6. package/dist/cache.d.ts +103 -0
  7. package/dist/default-task-runner.d.ts +44 -0
  8. package/dist/file-access-tracker.d.ts +53 -0
  9. package/dist/fingerprint.d.ts +45 -0
  10. package/dist/framework-inference.d.ts +35 -0
  11. package/dist/graph-visualizer.d.ts +74 -0
  12. package/dist/incremental-hasher.d.ts +58 -0
  13. package/dist/index.d.ts +34 -0
  14. package/dist/index.js +20 -0
  15. package/dist/life-cycle.d.ts +36 -0
  16. package/dist/lockfile-hasher.d.ts +73 -0
  17. package/dist/native-binding.d.ts +64 -0
  18. package/dist/packem_shared/Cache-IYpTYVUC.js +298 -0
  19. package/dist/packem_shared/CompositeLifeCycle-7AtYw1dv.js +112 -0
  20. package/dist/packem_shared/FileAccessTracker-CrtBAt5D.js +239 -0
  21. package/dist/packem_shared/FingerprintManager-D6Y0erg-.js +227 -0
  22. package/dist/packem_shared/IncrementalFileHasher-Ds3J6dgb.js +151 -0
  23. package/dist/packem_shared/RemoteCache-BDqrnDEi.js +179 -0
  24. package/dist/packem_shared/TaskOrchestrator-BvYs3ONw.js +342 -0
  25. package/dist/packem_shared/TaskScheduler-CJilHDta.js +111 -0
  26. package/dist/packem_shared/TrackedTaskExecutor-BGUKFE-7.js +164 -0
  27. package/dist/packem_shared/collectFiles-ClXHnHhg.js +22 -0
  28. package/dist/packem_shared/computeTaskHash-BoCnnvIJ.js +356 -0
  29. package/dist/packem_shared/createTaskGraph-CcsFaSrz.js +164 -0
  30. package/dist/packem_shared/defaultTaskRunner-CrW4v5Ye.js +79 -0
  31. package/dist/packem_shared/detectFrameworks-CeFzKE6J.js +101 -0
  32. package/dist/packem_shared/extractPackageName-CbVNW-dr.js +189 -0
  33. package/dist/packem_shared/filterAffectedTasks-I-18zPg6.js +135 -0
  34. package/dist/packem_shared/findCycle-DF4_BRdO.js +212 -0
  35. package/dist/packem_shared/generateRunSummary-qn-_jKwt.js +134 -0
  36. package/dist/packem_shared/isNativeAvailable-BWhnZ4ES.js +19 -0
  37. package/dist/packem_shared/projectGraphToDot-VdTjHcVp.js +202 -0
  38. package/dist/packem_shared/utils-zO0ZRgtf.js +390 -0
  39. package/dist/remote-cache.d.ts +55 -0
  40. package/dist/run-summary.d.ts +89 -0
  41. package/dist/task-graph-utils.d.ts +39 -0
  42. package/dist/task-graph.d.ts +22 -0
  43. package/dist/task-hasher.d.ts +67 -0
  44. package/dist/task-orchestrator.d.ts +38 -0
  45. package/dist/task-scheduler.d.ts +18 -0
  46. package/dist/tracked-executor.d.ts +46 -0
  47. package/dist/types.d.ts +385 -0
  48. package/dist/utils.d.ts +39 -0
  49. package/package.json +72 -7
@@ -0,0 +1,227 @@
1
+ import { createRequire as __cjs_createRequire } from "node:module";
2
+
3
+ const __cjs_require = __cjs_createRequire(import.meta.url);
4
+
5
+ const __cjs_getProcess = typeof globalThis !== "undefined" && typeof globalThis.process !== "undefined" ? globalThis.process : process;
6
+
7
+ const __cjs_getBuiltinModule = (module) => {
8
+ // Check if we're in Node.js and version supports getBuiltinModule
9
+ if (typeof __cjs_getProcess !== "undefined" && __cjs_getProcess.versions && __cjs_getProcess.versions.node) {
10
+ const [major, minor] = __cjs_getProcess.versions.node.split(".").map(Number);
11
+ // Node.js 20.16.0+ and 22.3.0+
12
+ if (major > 22 || (major === 22 && minor >= 3) || (major === 20 && minor >= 16)) {
13
+ return __cjs_getProcess.getBuiltinModule(module);
14
+ }
15
+ }
16
+ // Fallback to createRequire
17
+ return __cjs_require(module);
18
+ };
19
+
20
+ const {
21
+ readdir,
22
+ stat
23
+ } = __cjs_getBuiltinModule("node:fs/promises");
24
+ import { resolve, relative } from '@visulima/path';
25
+ import { b as hashStrings, s as sortObjectKeys, h as hashFile } from './utils-zO0ZRgtf.js';
26
+
27
+ class FingerprintManager {
28
+ #workspaceRoot;
29
+ #fileHashCache = /* @__PURE__ */ new Map();
30
+ constructor(workspaceRoot) {
31
+ this.#workspaceRoot = resolve(workspaceRoot);
32
+ }
33
+ // eslint-disable-next-line sonarjs/cognitive-complexity
34
+ async createFingerprint(accesses, command, args, envVariables, envPatterns = [], untrackedEnvVariables = []) {
35
+ const fileHashes = {};
36
+ const missingPaths = /* @__PURE__ */ new Set();
37
+ const directoryListings = {};
38
+ for (const access of accesses) {
39
+ const relativePath = relative(this.#workspaceRoot, access.path);
40
+ switch (access.type) {
41
+ case "missing": {
42
+ missingPaths.add(relativePath);
43
+ break;
44
+ }
45
+ case "read":
46
+ case "stat": {
47
+ if (!fileHashes[relativePath]) {
48
+ const hash = await this.#hashFileWithCache(access.path);
49
+ if (hash) {
50
+ fileHashes[relativePath] = hash;
51
+ }
52
+ }
53
+ break;
54
+ }
55
+ case "readdir": {
56
+ if (!directoryListings[relativePath]) {
57
+ try {
58
+ const entries = await readdir(access.path);
59
+ directoryListings[relativePath] = entries.toSorted();
60
+ } catch {
61
+ directoryListings[relativePath] = [];
62
+ }
63
+ }
64
+ break;
65
+ }
66
+ }
67
+ }
68
+ const commandHash = hashStrings(`${command}:${JSON.stringify(sortObjectKeys(args))}`);
69
+ const envHashes = {};
70
+ const matchedEnvVariables = FingerprintManager.#resolveEnvPatterns(envPatterns, envVariables);
71
+ const untrackedSet = new Set(untrackedEnvVariables);
72
+ for (const [key, value] of Object.entries(matchedEnvVariables)) {
73
+ if (untrackedSet.has(key)) {
74
+ continue;
75
+ }
76
+ envHashes[key] = hashStrings(`${key}=${value ?? ""}`);
77
+ }
78
+ const missingFiles = [...missingPaths].toSorted();
79
+ return { commandHash, directoryListings, envHashes, fileHashes, missingFiles };
80
+ }
81
+ /**
82
+ * Validates a stored fingerprint against the current state.
83
+ * Returns null if valid (cache hit), or an array of reasons (cache miss).
84
+ *
85
+ * Does NOT use the file hash cache — validation must see current disk state.
86
+ */
87
+ async validate(fingerprint) {
88
+ const reasons = [];
89
+ const fileCheckPromises = Object.entries(fingerprint.fileHashes).map(async ([relativePath, previousHash]) => {
90
+ const absolutePath = resolve(this.#workspaceRoot, relativePath);
91
+ const currentHash = await hashFile(absolutePath);
92
+ if (!currentHash) {
93
+ return { detail: relativePath, previousHash, type: "file-deleted" };
94
+ }
95
+ if (currentHash !== previousHash) {
96
+ return { currentHash, detail: relativePath, previousHash, type: "file-changed" };
97
+ }
98
+ return void 0;
99
+ });
100
+ const missingCheckPromises = fingerprint.missingFiles.map(async (relativePath) => {
101
+ const absolutePath = resolve(this.#workspaceRoot, relativePath);
102
+ try {
103
+ await stat(absolutePath);
104
+ return { detail: relativePath, type: "file-created" };
105
+ } catch {
106
+ return void 0;
107
+ }
108
+ });
109
+ const directoryCheckPromises = Object.entries(fingerprint.directoryListings).map(async ([relativePath, previousEntries]) => {
110
+ const absolutePath = resolve(this.#workspaceRoot, relativePath);
111
+ try {
112
+ const readdirResult = await readdir(absolutePath);
113
+ const currentEntries = readdirResult.toSorted();
114
+ if (JSON.stringify(previousEntries) !== JSON.stringify(currentEntries)) {
115
+ return {
116
+ currentHash: JSON.stringify(currentEntries),
117
+ detail: relativePath,
118
+ previousHash: JSON.stringify(previousEntries),
119
+ type: "directory-changed"
120
+ };
121
+ }
122
+ } catch {
123
+ return { detail: relativePath, type: "directory-changed" };
124
+ }
125
+ return void 0;
126
+ });
127
+ const [fileResults, missingResults, directoryResults] = await Promise.all([
128
+ Promise.all(fileCheckPromises),
129
+ Promise.all(missingCheckPromises),
130
+ Promise.all(directoryCheckPromises)
131
+ ]);
132
+ for (const r of [...fileResults, ...missingResults, ...directoryResults]) {
133
+ if (r) {
134
+ reasons.push(r);
135
+ }
136
+ }
137
+ for (const [envName, previousHash] of Object.entries(fingerprint.envHashes)) {
138
+ const currentHash = hashStrings(`${envName}=${process.env[envName] ?? ""}`);
139
+ if (currentHash !== previousHash) {
140
+ reasons.push({
141
+ currentHash,
142
+ detail: envName,
143
+ previousHash,
144
+ type: "env-changed"
145
+ });
146
+ }
147
+ }
148
+ return reasons.length > 0 ? reasons : void 0;
149
+ }
150
+ // eslint-disable-next-line class-methods-use-this
151
+ validateCommand(fingerprint, command, args) {
152
+ const currentHash = hashStrings(`${command}:${JSON.stringify(sortObjectKeys(args))}`);
153
+ if (currentHash !== fingerprint.commandHash) {
154
+ return {
155
+ currentHash,
156
+ detail: "command arguments",
157
+ previousHash: fingerprint.commandHash,
158
+ type: "args-changed"
159
+ };
160
+ }
161
+ return void 0;
162
+ }
163
+ // eslint-disable-next-line class-methods-use-this
164
+ formatMissReasons(reasons) {
165
+ const lines = ["Cache miss reasons:"];
166
+ for (const reason of reasons) {
167
+ switch (reason.type) {
168
+ case "args-changed": {
169
+ lines.push(` - Command arguments changed`);
170
+ break;
171
+ }
172
+ case "directory-changed": {
173
+ lines.push(` - Directory contents changed: ${reason.detail}`);
174
+ break;
175
+ }
176
+ case "env-changed": {
177
+ lines.push(` - Environment variable changed: ${reason.detail}`);
178
+ break;
179
+ }
180
+ case "file-changed": {
181
+ lines.push(` - File modified: ${reason.detail}`);
182
+ break;
183
+ }
184
+ case "file-created": {
185
+ lines.push(` - File created (was missing): ${reason.detail}`);
186
+ break;
187
+ }
188
+ case "file-deleted": {
189
+ lines.push(` - File deleted: ${reason.detail}`);
190
+ break;
191
+ }
192
+ case "no-fingerprint": {
193
+ lines.push(` - No previous fingerprint found (first run)`);
194
+ break;
195
+ }
196
+ }
197
+ }
198
+ return lines.join("\n");
199
+ }
200
+ static #resolveEnvPatterns(patterns, envVariables) {
201
+ const result = {};
202
+ for (const pattern of patterns) {
203
+ if (pattern.includes("*")) {
204
+ const prefix = pattern.replace("*", "");
205
+ for (const [key, value] of Object.entries(envVariables)) {
206
+ if (key.startsWith(prefix)) {
207
+ result[key] = value;
208
+ }
209
+ }
210
+ } else {
211
+ result[pattern] = envVariables[pattern];
212
+ }
213
+ }
214
+ return result;
215
+ }
216
+ async #hashFileWithCache(filePath) {
217
+ const cached = this.#fileHashCache.get(filePath);
218
+ if (cached !== void 0) {
219
+ return cached;
220
+ }
221
+ const hash = await hashFile(filePath) ?? void 0;
222
+ this.#fileHashCache.set(filePath, hash);
223
+ return hash;
224
+ }
225
+ }
226
+
227
+ export { FingerprintManager };
@@ -0,0 +1,151 @@
1
+ import { createRequire as __cjs_createRequire } from "node:module";
2
+
3
+ const __cjs_require = __cjs_createRequire(import.meta.url);
4
+
5
+ const __cjs_getProcess = typeof globalThis !== "undefined" && typeof globalThis.process !== "undefined" ? globalThis.process : process;
6
+
7
+ const __cjs_getBuiltinModule = (module) => {
8
+ // Check if we're in Node.js and version supports getBuiltinModule
9
+ if (typeof __cjs_getProcess !== "undefined" && __cjs_getProcess.versions && __cjs_getProcess.versions.node) {
10
+ const [major, minor] = __cjs_getProcess.versions.node.split(".").map(Number);
11
+ // Node.js 20.16.0+ and 22.3.0+
12
+ if (major > 22 || (major === 22 && minor >= 3) || (major === 20 && minor >= 16)) {
13
+ return __cjs_getProcess.getBuiltinModule(module);
14
+ }
15
+ }
16
+ // Fallback to createRequire
17
+ return __cjs_require(module);
18
+ };
19
+
20
+ const {
21
+ readFile,
22
+ mkdir,
23
+ writeFile,
24
+ stat
25
+ } = __cjs_getBuiltinModule("node:fs/promises");
26
+ import { c as collectFiles, x as xxh3Hash } from './utils-zO0ZRgtf.js';
27
+ import { join, dirname, relative } from '@visulima/path';
28
+
29
+ const DEFAULT_IGNORED_DIRS = /* @__PURE__ */ new Set([".cache", ".git", ".task-runner-cache", "coverage", "dist", "node_modules"]);
30
+ class IncrementalFileHasher {
31
+ #workspaceRoot;
32
+ #ignoredDirs;
33
+ #snapshotPath;
34
+ #snapshot = /* @__PURE__ */ new Map();
35
+ #loaded = false;
36
+ constructor(options) {
37
+ this.#workspaceRoot = options.workspaceRoot;
38
+ this.#ignoredDirs = options.ignoredDirs ?? DEFAULT_IGNORED_DIRS;
39
+ this.#snapshotPath = options.snapshotPath ?? join(options.workspaceRoot, "node_modules", ".cache", "task-runner", "file-snapshot.json");
40
+ }
41
+ /**
42
+ * Loads the snapshot from disk if available.
43
+ * Called automatically on first use.
44
+ */
45
+ async load() {
46
+ if (this.#loaded) {
47
+ return;
48
+ }
49
+ this.#loaded = true;
50
+ if (!this.#snapshotPath) {
51
+ return;
52
+ }
53
+ try {
54
+ const content = await readFile(this.#snapshotPath, "utf8");
55
+ const data = JSON.parse(content);
56
+ for (const [path, snap] of Object.entries(data)) {
57
+ this.#snapshot.set(path, snap);
58
+ }
59
+ } catch {
60
+ }
61
+ }
62
+ /**
63
+ * Persists the current snapshot to disk for cross-run reuse.
64
+ */
65
+ async save() {
66
+ if (!this.#snapshotPath) {
67
+ return;
68
+ }
69
+ const directory = dirname(this.#snapshotPath);
70
+ await mkdir(directory, { recursive: true });
71
+ const data = {};
72
+ for (const [path, snap] of this.#snapshot) {
73
+ data[path] = snap;
74
+ }
75
+ await writeFile(this.#snapshotPath, JSON.stringify(data));
76
+ }
77
+ /**
78
+ * Incrementally hashes all files in a directory.
79
+ *
80
+ * Only files whose mtime or size changed since the last snapshot
81
+ * are re-read and re-hashed. Unchanged files reuse the cached hash.
82
+ *
83
+ * Returns a map of relative paths → hashes.
84
+ */
85
+ async hashDirectory(directoryPath) {
86
+ await this.load();
87
+ const result = {};
88
+ const filePaths = await collectFiles(directoryPath, this.#ignoredDirs);
89
+ const BATCH_SIZE = 64;
90
+ const batches = [];
91
+ for (let i = 0; i < filePaths.length; i += BATCH_SIZE) {
92
+ batches.push(filePaths.slice(i, i + BATCH_SIZE));
93
+ }
94
+ for (const batch of batches) {
95
+ const batchResults = await Promise.all(
96
+ batch.map(async (filePath) => {
97
+ const hash = await this.#hashFileIncremental(filePath);
98
+ const relativePath = relative(this.#workspaceRoot, filePath);
99
+ return { hash, relativePath };
100
+ })
101
+ );
102
+ for (const { hash, relativePath } of batchResults) {
103
+ if (hash) {
104
+ result[relativePath] = hash;
105
+ }
106
+ }
107
+ }
108
+ return result;
109
+ }
110
+ /**
111
+ * Hashes a single file incrementally.
112
+ * Returns the cached hash if mtime + size haven't changed.
113
+ */
114
+ async #hashFileIncremental(filePath) {
115
+ try {
116
+ const fileStat = await stat(filePath);
117
+ if (!fileStat.isFile()) {
118
+ return void 0;
119
+ }
120
+ const cached = this.#snapshot.get(filePath);
121
+ if (cached && cached.mtimeMs === fileStat.mtimeMs && cached.size === fileStat.size) {
122
+ return cached.hash;
123
+ }
124
+ const content = await readFile(filePath);
125
+ const hash = xxh3Hash(content);
126
+ this.#snapshot.set(filePath, {
127
+ hash,
128
+ mtimeMs: fileStat.mtimeMs,
129
+ size: fileStat.size
130
+ });
131
+ return hash;
132
+ } catch {
133
+ this.#snapshot.delete(filePath);
134
+ return void 0;
135
+ }
136
+ }
137
+ /**
138
+ * Returns how many files are in the snapshot (for diagnostics).
139
+ */
140
+ get snapshotSize() {
141
+ return this.#snapshot.size;
142
+ }
143
+ /**
144
+ * Clears the in-memory snapshot.
145
+ */
146
+ clear() {
147
+ this.#snapshot.clear();
148
+ }
149
+ }
150
+
151
+ export { IncrementalFileHasher };
@@ -0,0 +1,179 @@
1
+ import { createRequire as __cjs_createRequire } from "node:module";
2
+
3
+ const __cjs_require = __cjs_createRequire(import.meta.url);
4
+
5
+ const __cjs_getProcess = typeof globalThis !== "undefined" && typeof globalThis.process !== "undefined" ? globalThis.process : process;
6
+
7
+ const __cjs_getBuiltinModule = (module) => {
8
+ // Check if we're in Node.js and version supports getBuiltinModule
9
+ if (typeof __cjs_getProcess !== "undefined" && __cjs_getProcess.versions && __cjs_getProcess.versions.node) {
10
+ const [major, minor] = __cjs_getProcess.versions.node.split(".").map(Number);
11
+ // Node.js 20.16.0+ and 22.3.0+
12
+ if (major > 22 || (major === 22 && minor >= 3) || (major === 20 && minor >= 16)) {
13
+ return __cjs_getProcess.getBuiltinModule(module);
14
+ }
15
+ }
16
+ // Fallback to createRequire
17
+ return __cjs_require(module);
18
+ };
19
+
20
+ const {
21
+ execFile
22
+ } = __cjs_getBuiltinModule("node:child_process");
23
+ const {
24
+ createWriteStream
25
+ } = __cjs_getBuiltinModule("node:fs");
26
+ const {
27
+ mkdir,
28
+ rm,
29
+ stat,
30
+ readFile
31
+ } = __cjs_getBuiltinModule("node:fs/promises");
32
+ import { join } from '@visulima/path';
33
+ const {
34
+ pipeline
35
+ } = __cjs_getBuiltinModule("node:stream/promises");
36
+
37
+ const createTarGz = (sourceDirectory, outputPath) => new Promise((resolve, reject) => {
38
+ execFile("tar", ["-czf", outputPath, "-C", sourceDirectory, "."], (error) => {
39
+ if (error) {
40
+ reject(error);
41
+ } else {
42
+ resolve();
43
+ }
44
+ });
45
+ });
46
+ const extractTarGz = (archivePath, destinationDirectory) => new Promise((resolve, reject) => {
47
+ execFile("tar", ["-xzf", archivePath, "-C", destinationDirectory], (error) => {
48
+ if (error) {
49
+ reject(error);
50
+ } else {
51
+ resolve();
52
+ }
53
+ });
54
+ });
55
+ class RemoteCache {
56
+ #url;
57
+ #token;
58
+ #teamId;
59
+ #timeout;
60
+ #read;
61
+ #write;
62
+ #onUploadError;
63
+ constructor(options) {
64
+ this.#url = options.url.replace(/\/$/, "");
65
+ this.#token = options.token;
66
+ this.#teamId = options.teamId;
67
+ this.#timeout = options.timeout ?? 3e4;
68
+ this.#read = options.read ?? true;
69
+ this.#write = options.write ?? true;
70
+ this.#onUploadError = options.onUploadError;
71
+ }
72
+ /**
73
+ * Retrieves a cached artifact from the remote cache.
74
+ * Returns the local path to the extracted cache entry, or undefined if not found.
75
+ */
76
+ async retrieve(hash, localCacheDirectory) {
77
+ if (!this.#read) {
78
+ return false;
79
+ }
80
+ const entryDirectory = join(localCacheDirectory, hash);
81
+ const archivePath = join(localCacheDirectory, `.remote-${hash}.tar.gz`);
82
+ try {
83
+ const artifactUrl = this.#buildUrl(`/v8/artifacts/${hash}`);
84
+ const response = await fetch(artifactUrl, {
85
+ headers: this.#buildHeaders(),
86
+ method: "GET",
87
+ signal: AbortSignal.timeout(this.#timeout)
88
+ });
89
+ if (!response.ok) {
90
+ return false;
91
+ }
92
+ await mkdir(localCacheDirectory, { recursive: true });
93
+ const { body } = response;
94
+ if (!body) {
95
+ return false;
96
+ }
97
+ const fileStream = createWriteStream(archivePath);
98
+ await pipeline(body, fileStream);
99
+ await mkdir(entryDirectory, { recursive: true });
100
+ await extractTarGz(archivePath, entryDirectory);
101
+ await rm(archivePath, { force: true });
102
+ return true;
103
+ } catch {
104
+ await rm(archivePath, { force: true }).catch(() => {
105
+ });
106
+ await rm(entryDirectory, { force: true, recursive: true }).catch(() => {
107
+ });
108
+ return false;
109
+ }
110
+ }
111
+ /**
112
+ * Stores a cache entry in the remote cache.
113
+ */
114
+ async store(hash, localCacheDirectory) {
115
+ if (!this.#write) {
116
+ return false;
117
+ }
118
+ const entryDirectory = join(localCacheDirectory, hash);
119
+ const archivePath = join(localCacheDirectory, `.upload-${hash}.tar.gz`);
120
+ try {
121
+ await stat(join(entryDirectory, ".commit"));
122
+ await createTarGz(entryDirectory, archivePath);
123
+ const artifactUrl = this.#buildUrl(`/v8/artifacts/${hash}`);
124
+ const archiveContent = await readFile(archivePath);
125
+ const response = await fetch(artifactUrl, {
126
+ body: archiveContent,
127
+ headers: {
128
+ ...this.#buildHeaders(),
129
+ "Content-Length": String(archiveContent.length),
130
+ "Content-Type": "application/octet-stream"
131
+ },
132
+ method: "PUT",
133
+ signal: AbortSignal.timeout(this.#timeout)
134
+ });
135
+ await rm(archivePath, { force: true });
136
+ return response.ok;
137
+ } catch (error) {
138
+ await rm(archivePath, { force: true }).catch(() => {
139
+ });
140
+ this.#onUploadError?.(hash, error);
141
+ return false;
142
+ }
143
+ }
144
+ /**
145
+ * Checks if an artifact exists in the remote cache without downloading it.
146
+ */
147
+ async exists(hash) {
148
+ if (!this.#read) {
149
+ return false;
150
+ }
151
+ try {
152
+ const artifactUrl = this.#buildUrl(`/v8/artifacts/${hash}`);
153
+ const response = await fetch(artifactUrl, {
154
+ headers: this.#buildHeaders(),
155
+ method: "HEAD",
156
+ signal: AbortSignal.timeout(this.#timeout)
157
+ });
158
+ return response.ok;
159
+ } catch {
160
+ return false;
161
+ }
162
+ }
163
+ #buildUrl(path) {
164
+ const url = `${this.#url}${path}`;
165
+ if (this.#teamId) {
166
+ return `${url}?teamId=${encodeURIComponent(this.#teamId)}`;
167
+ }
168
+ return url;
169
+ }
170
+ #buildHeaders() {
171
+ const headers = {};
172
+ if (this.#token) {
173
+ headers["Authorization"] = `Bearer ${this.#token}`;
174
+ }
175
+ return headers;
176
+ }
177
+ }
178
+
179
+ export { RemoteCache };