@dbx-tools/core 0.9.4 → 0.9.6

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.
@@ -30,11 +30,12 @@ import { spawnSync } from "node:child_process";
30
30
  import { readFileSync } from "node:fs";
31
31
  import { dirname, isAbsolute, relative, resolve, sep } from "node:path";
32
32
  import { parseEnv } from "node:util";
33
- import { json, log, object, string as sharedString } from "@dbx-tools/shared-core";
33
+ import { isDatabricksAppEnvironment } from "@dbx-tools/core-rs";
34
+ import { json, log, object, stringUtils } from "@dbx-tools/shared-core";
34
35
  import { parse as parseYaml } from "yaml";
35
36
  import { z } from "zod";
36
37
  import { cachedRecord, statSync } from "./file.ts";
37
- import { resolveWorkingDirectory, root as resolveProjectRoot } from "./project.ts";
38
+ import { resolveWorkingDirectory, root as resolveProjectRoot } from "./project-utils.ts";
38
39
 
39
40
  const logger = log.logger("config");
40
41
  const configFileCache = new Map<string, string | undefined>();
@@ -92,9 +93,6 @@ const NODE_ENV_ALTERNATIVES = {
92
93
  /** Highest valid TCP port number. */
93
94
  export const MAX_TCP_PORT = 65_535;
94
95
 
95
- /** Boolean environment override for {@link isDatabricksAppEnv}. */
96
- const DATABRICKS_APP_ENV_KEY = "DBX_TOOLS_DATABRICKS_APP_ENV";
97
-
98
96
  /** Boolean environment override for project `.env` reads. */
99
97
  const CONFIG_DOTENV_KEY = "DBX_TOOLS_CONFIG_DOTENV";
100
98
 
@@ -115,31 +113,15 @@ export const valueSchema = z
115
113
  message: "Interpolated values are not allowed",
116
114
  });
117
115
 
118
- const portValueSchema = z.preprocess(
119
- (input) => object.toNumber(input),
120
- z.number().int().min(1).max(MAX_TCP_PORT),
121
- );
116
+ const portValueSchema = z.preprocess((input) => {
117
+ if (typeof input === "number") return input;
118
+ if (typeof input !== "string" || !/^\d+$/.test(input.trim())) return Number.NaN;
119
+ return Number(input.trim());
120
+ }, z.number().int().min(1).max(MAX_TCP_PORT));
122
121
 
123
122
  const positiveNumberValue = z.preprocess((input) => object.toNumber(input), z.number().positive());
124
123
  const positiveIntValue = positiveNumberValue.transform(Math.floor);
125
124
 
126
- const workspaceHostSchema = valueSchema.refine(
127
- (host) => {
128
- try {
129
- return ["http:", "https:"].includes(new URL(host).protocol);
130
- } catch {
131
- return false;
132
- }
133
- },
134
- { message: "Must be an HTTP(S) URL" },
135
- );
136
-
137
- const databricksAppEnvSchema = z.object({
138
- DATABRICKS_APP_NAME: valueSchema,
139
- DATABRICKS_HOST: workspaceHostSchema,
140
- DATABRICKS_APP_PORT: portValueSchema,
141
- });
142
-
143
125
  /** A named bundle or App resource; concrete resource fields pass through. */
144
126
  export const bundleResourceSchema = z.object({ name: valueSchema.optional() }).passthrough();
145
127
 
@@ -269,10 +251,11 @@ export function getBundlePath(data: Record<string, unknown>, path: string): stri
269
251
  export function isDatabricksAppEnv(
270
252
  source: Record<string, string | undefined> = process.env,
271
253
  ): boolean {
272
- const override = object.toBoolean(source[DATABRICKS_APP_ENV_KEY]);
273
- if (override !== undefined) return override;
274
- const parsed = databricksAppEnvSchema.safeParse(source);
275
- return parsed.success;
254
+ return isDatabricksAppEnvironment(
255
+ new Map(
256
+ Object.entries(source).filter((entry): entry is [string, string] => entry[1] !== undefined),
257
+ ),
258
+ );
276
259
  }
277
260
 
278
261
  /** Exact, uppercase, and tokenized-uppercase names for a human-friendly key. */
@@ -283,7 +266,7 @@ export function environmentKeys(name: string): readonly string[] {
283
266
  .sequence(
284
267
  trimmed,
285
268
  trimmed.toUpperCase(),
286
- Array.from(sharedString.tokenize(trimmed)).join("_").toUpperCase(),
269
+ Array.from(stringUtils.tokenize(trimmed)).join("_").toUpperCase(),
287
270
  )
288
271
  .filter(Boolean)
289
272
  .distinct()
@@ -297,7 +280,7 @@ function keys(
297
280
  ): readonly string[] {
298
281
  const scopes = object
299
282
  .sequence(options.scope ?? DEFAULT_SCOPE)
300
- .map(sharedString.trimToEmpty)
283
+ .map(stringUtils.trimToEmpty)
301
284
  .filter(Boolean)
302
285
  .distinct()
303
286
  .toArray();
@@ -306,13 +289,13 @@ function keys(
306
289
  ? []
307
290
  : object
308
291
  .sequence(options.prefix)
309
- .map(sharedString.trimToEmpty)
292
+ .map(stringUtils.trimToEmpty)
310
293
  .filter(Boolean)
311
294
  .distinct()
312
295
  .toArray();
313
296
  return object
314
297
  .sequence(input)
315
- .map(sharedString.trimToEmpty)
298
+ .map(stringUtils.trimToEmpty)
316
299
  .filter(Boolean)
317
300
  .distinct()
318
301
  .flatMap((key) => {
@@ -364,12 +347,12 @@ function configSources(options: ConfigOptions): readonly ConfigSource[] {
364
347
  function configMapValue(value: ConfigMapValue | unknown): string | null {
365
348
  if (Array.isArray(value)) {
366
349
  for (const entry of value) {
367
- const resolved = sharedString.trimToNull(entry);
350
+ const resolved = stringUtils.trimToNull(entry);
368
351
  if (resolved !== null) return resolved;
369
352
  }
370
353
  return null;
371
354
  }
372
- return sharedString.trimToNull(value);
355
+ return stringUtils.trimToNull(value);
373
356
  }
374
357
 
375
358
  /**
@@ -407,7 +390,7 @@ export function name(
407
390
  * Known long and short names are interchangeable; unknown names pass through.
408
391
  */
409
392
  function nodeEnvNames(nodeEnv: unknown): string[] {
410
- const name = sharedString.trimToNull(nodeEnv)?.toLowerCase();
393
+ const name = stringUtils.trimToNull(nodeEnv)?.toLowerCase();
411
394
  if (!name || !/^[a-z0-9_-]+$/.test(name)) return [];
412
395
  for (const [canonical, alternatives] of Object.entries(NODE_ENV_ALTERNATIVES)) {
413
396
  const names: readonly string[] = alternatives;
@@ -432,7 +415,7 @@ export function string(
432
415
  input: ConfigKey,
433
416
  options?: ConfigOptions,
434
417
  ): string | undefined {
435
- return sharedString.trimToNull(configured) ?? text(input, options);
418
+ return stringUtils.trimToNull(configured) ?? text(input, options);
436
419
  }
437
420
 
438
421
  /**
@@ -498,7 +481,7 @@ export function port(
498
481
  }
499
482
 
500
483
  /**
501
- * Resolve a list through `string.parseList`, so an array from typed config and a
484
+ * Resolve a list through `stringUtils.parseList`, so an array from typed config and a
502
485
  * `"a, b c"` string normalize identically. `[]` when neither source has entries.
503
486
  */
504
487
  export function list(
@@ -507,10 +490,10 @@ export function list(
507
490
  transform?: (entry: string) => string,
508
491
  options?: ConfigOptions,
509
492
  ): string[] {
510
- const fromConfig = sharedString.parseList(configured, transform);
493
+ const fromConfig = stringUtils.parseList(configured, transform);
511
494
  return fromConfig.length > 0
512
495
  ? fromConfig
513
- : sharedString.parseList(text(input, options), transform);
496
+ : stringUtils.parseList(text(input, options), transform);
514
497
  }
515
498
 
516
499
  /**
@@ -534,7 +517,7 @@ export function bundleFile(cwd?: string | null): ConfigFile | undefined {
534
517
  return undefined;
535
518
  }
536
519
  const resolved = resolveWorkingDirectory(cwd);
537
- return loadBundleFile(resolved, sharedString.trimToNull(process.env.DATABRICKS_CONFIG_PROFILE));
520
+ return loadBundleFile(resolved, stringUtils.trimToNull(process.env.DATABRICKS_CONFIG_PROFILE));
538
521
  }
539
522
 
540
523
  /** The parsed `app.yaml` / `app.yml` for `cwd`, when local App config reads are enabled. */
@@ -680,8 +663,8 @@ function loadBundleFile(cwd: string, profile: string | null): ConfigFile | undef
680
663
  encoding: "utf8",
681
664
  stdio: ["ignore", "pipe", "pipe"],
682
665
  });
683
- const output = sharedString.trimToNull(result.stdout);
684
- const error = sharedString.trimToNull(result.stderr);
666
+ const output = stringUtils.trimToNull(result.stdout);
667
+ const error = stringUtils.trimToNull(result.stderr);
685
668
  if (output === null) {
686
669
  logger.debug("bundle validate produced no JSON", { path, status: result.status, error });
687
670
  return undefined;
package/src/file-lock.ts CHANGED
@@ -26,7 +26,7 @@ import { mkdir, open } from "node:fs/promises";
26
26
  import { tmpdir } from "node:os";
27
27
  import { dirname, join } from "node:path";
28
28
 
29
- import { async, error, functionModule, hash, log, object } from "@dbx-tools/shared-core";
29
+ import { asyncUtils, errorUtils, functionUtils, hash, log, object } from "@dbx-tools/shared-core";
30
30
  import lockfile from "proper-lockfile";
31
31
 
32
32
  const logger = log.logger("core:file-lock");
@@ -157,7 +157,7 @@ function lockId(key: unknown): string {
157
157
  * callers fall through to the next backend. A miss is cached too, so a process
158
158
  * that cannot flock never retries the load.
159
159
  */
160
- const resolveFlock = functionModule.memoize(async (): Promise<FlockFn | undefined> => {
160
+ const resolveFlock = functionUtils.memoize(async (): Promise<FlockFn | undefined> => {
161
161
  if (process.platform === "win32") return undefined;
162
162
  if (!process.versions.bun) return undefined;
163
163
 
@@ -172,7 +172,7 @@ const resolveFlock = functionModule.memoize(async (): Promise<FlockFn | undefine
172
172
  });
173
173
  return lib.symbols.flock;
174
174
  } catch (cause) {
175
- logger.debug("flock FFI unavailable", { error: error.errorMessage(cause) });
175
+ logger.debug("flock FFI unavailable", { error: errorUtils.errorMessage(cause) });
176
176
  return undefined;
177
177
  }
178
178
  });
@@ -207,7 +207,7 @@ async function waitForFlock(
207
207
  const rc = flock(fd, LOCK_EX | LOCK_NB);
208
208
  if (rc === 0) return;
209
209
  assertBeforeDeadline(lockPath, deadline);
210
- await async.sleep(POLL_MS);
210
+ await asyncUtils.sleep(POLL_MS);
211
211
  }
212
212
  }
213
213
 
@@ -242,9 +242,9 @@ async function acquireLockDirectory(
242
242
  });
243
243
  } catch (cause) {
244
244
  const err = cause as NodeJS.ErrnoException;
245
- if (err.code !== "ELOCKED") throw error.toError(cause);
245
+ if (err.code !== "ELOCKED") throw errorUtils.toError(cause);
246
246
  assertBeforeDeadline(lockPath, deadline);
247
- await async.sleep(POLL_MS);
247
+ await asyncUtils.sleep(POLL_MS);
248
248
  }
249
249
  }
250
250
  }
@@ -38,7 +38,7 @@ import {
38
38
  type Worker,
39
39
  type WorkerOptions,
40
40
  } from "node:worker_threads";
41
- import { error, hash, object } from "@dbx-tools/shared-core";
41
+ import { errorUtils, hash, object } from "@dbx-tools/shared-core";
42
42
 
43
43
  /**
44
44
  * `workerData` slot carrying the coordinator port into a worker, and the
@@ -251,7 +251,7 @@ class LockClient {
251
251
  this.port.postMessage({ type: "acquire", requestId, key } satisfies LockRequest);
252
252
  } catch (cause) {
253
253
  this.pending.delete(requestId);
254
- reject(error.toError(cause));
254
+ reject(errorUtils.toError(cause));
255
255
  }
256
256
  });
257
257
  } catch (cause) {
@@ -1,7 +1,7 @@
1
1
  import { spawnSync } from "node:child_process";
2
2
  import { Stats, readFileSync } from "node:fs";
3
3
  import { basename, dirname, join, resolve } from "node:path";
4
- import { json, net, string } from "@dbx-tools/shared-core";
4
+ import { json, net, stringUtils } from "@dbx-tools/shared-core";
5
5
  import { statSync as fileStatSync } from "./file.ts";
6
6
 
7
7
  const ROOT_MARKERS = [
@@ -14,7 +14,7 @@ const ROOT_MARKERS = [
14
14
 
15
15
  /** Resolve a blank, null, omitted, relative, or absolute cwd to an absolute path. */
16
16
  export function resolveWorkingDirectory(cwd?: string | null): string {
17
- return resolve(string.trimToNull(cwd) ?? process.cwd());
17
+ return resolve(stringUtils.trimToNull(cwd) ?? process.cwd());
18
18
  }
19
19
 
20
20
  /** A command's stdout, classified as a filesystem path and/or a URL. */
@@ -85,6 +85,7 @@ function gitRoot(cwd?: string): string | undefined {
85
85
  return commandRoot("git", ["rev-parse", "--show-toplevel"], resolved);
86
86
  }
87
87
 
88
+ /** Resolve the nearest project root without walking above its npm or Git boundary. */
88
89
  export function root(cwd?: string): string | undefined {
89
90
  const resolved = resolveWorkingDirectory(cwd);
90
91
  let current = resolved;
@@ -211,7 +212,7 @@ export function name(cwd?: string): string {
211
212
  */
212
213
  function repositoryUrlFromGh(cwd?: string): string | undefined {
213
214
  const out = projectContextCommand("gh", ["repo", "view", "--json", "url"], cwd).output;
214
- return string.trimToNull(json.parseRecord(out)?.url) ?? undefined;
215
+ return stringUtils.trimToNull(json.parseRecord(out)?.url) ?? undefined;
215
216
  }
216
217
 
217
218
  /** Resolve an ssh host alias (`~/.ssh/config`) to its effective `hostname` via `ssh -G`. */
@@ -327,7 +328,7 @@ function resolveNpmRegistry(
327
328
 
328
329
  function readPackageName(pkgPath: string): string | undefined {
329
330
  if (!fileStatSync(pkgPath)?.isFile()) return undefined;
330
- return string.trimToNull(json.parseRecord(readFileSync(pkgPath, "utf8"))?.name) ?? undefined;
331
+ return stringUtils.trimToNull(json.parseRecord(readFileSync(pkgPath, "utf8"))?.name) ?? undefined;
331
332
  }
332
333
 
333
334
  if (import.meta.main) {
package/lib/src/brand.js DELETED
@@ -1,66 +0,0 @@
1
- /** Node-only discovery and file loading for the shared brand context. */
2
- import { readFile } from "node:fs/promises";
3
- import { dirname, extname, isAbsolute, resolve } from "node:path";
4
- import { brand as sharedBrand } from "@dbx-tools/shared-core";
5
- import { statSync } from "./file.js";
6
- import { resolveProjectRoots } from "./project.js";
7
- const BRAND_CONTEXT_FILES = [
8
- "branding/brand.yaml",
9
- "branding/brand.yml",
10
- "branding/brand.json",
11
- "brand.yaml",
12
- "brand.yml",
13
- "brand.json",
14
- ];
15
- export const BrandContextSchema = sharedBrand.BrandContextSchema;
16
- export const defaultBrandContext = sharedBrand.defaultBrandContext;
17
- export const parseBrandContext = sharedBrand.parseBrandContext;
18
- export const brandContextJsonSchema = sharedBrand.brandContextJsonSchema;
19
- export const brandContextPrompt = sharedBrand.brandContextPrompt;
20
- /** Find a conventional YAML or JSON brand file from known project roots. */
21
- export function findBrandContextFile(cwd = process.cwd()) {
22
- for (const root of resolveProjectRoots(cwd)) {
23
- for (const candidate of BRAND_CONTEXT_FILES) {
24
- const path = resolve(root, candidate);
25
- if (statSync(path)?.isFile())
26
- return path;
27
- }
28
- }
29
- return undefined;
30
- }
31
- /** Read and validate one `.yaml`, `.yml`, or `.json` brand context file. */
32
- export async function loadBrandContextFile(path) {
33
- const source = await readFile(path, "utf8");
34
- const extension = extname(path).toLowerCase();
35
- let input;
36
- if (extension === ".json") {
37
- input = JSON.parse(source);
38
- }
39
- else if (extension === ".yaml" || extension === ".yml") {
40
- const { parse } = await import("yaml");
41
- input = parse(source);
42
- }
43
- else {
44
- throw new Error(`Unsupported brand context format: ${extension || "no extension"}`);
45
- }
46
- return sharedBrand.parseBrandContext(input);
47
- }
48
- /**
49
- * Discover and load a brand context. Missing files resolve to dbx tools defaults;
50
- * malformed files fail with their parser or Zod validation error.
51
- */
52
- export async function loadBrandContext(cwd = process.cwd()) {
53
- const path = findBrandContextFile(cwd);
54
- return path ? loadBrandContextFile(path) : sharedBrand.defaultBrandContext;
55
- }
56
- /** Resolve a relative asset reference against the brand file that declared it. */
57
- export function resolveBrandAssetPath(brandFile, asset) {
58
- if (isAbsolute(asset) ||
59
- asset.startsWith("@") ||
60
- asset.startsWith("//") ||
61
- /^[a-z][a-z\d+.-]*:/i.test(asset)) {
62
- return asset;
63
- }
64
- return resolve(dirname(brandFile), asset);
65
- }
66
- //# sourceMappingURL=data:application/json;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoiYnJhbmQuanMiLCJzb3VyY2VSb290IjoiIiwic291cmNlcyI6WyIuLi8uLi9zcmMvYnJhbmQudHMiXSwibmFtZXMiOltdLCJtYXBwaW5ncyI6IkFBQUEseUVBQXlFO0FBQ3pFLE9BQU8sRUFBRSxRQUFRLEVBQUUsTUFBTSxrQkFBa0IsQ0FBQztBQUM1QyxPQUFPLEVBQUUsT0FBTyxFQUFFLE9BQU8sRUFBRSxVQUFVLEVBQUUsT0FBTyxFQUFFLE1BQU0sV0FBVyxDQUFDO0FBQ2xFLE9BQU8sRUFBRSxLQUFLLElBQUksV0FBVyxFQUFFLE1BQU0sd0JBQXdCLENBQUM7QUFDOUQsT0FBTyxFQUFFLFFBQVEsRUFBRSxNQUFNLFdBQVcsQ0FBQztBQUNyQyxPQUFPLEVBQUUsbUJBQW1CLEVBQUUsTUFBTSxjQUFjLENBQUM7QUFFbkQsTUFBTSxtQkFBbUIsR0FBRztJQUMxQixxQkFBcUI7SUFDckIsb0JBQW9CO0lBQ3BCLHFCQUFxQjtJQUNyQixZQUFZO0lBQ1osV0FBVztJQUNYLFlBQVk7Q0FDSixDQUFDO0FBSVgsTUFBTSxDQUFDLE1BQU0sa0JBQWtCLEdBQUcsV0FBVyxDQUFDLGtCQUFrQixDQUFDO0FBQ2pFLE1BQU0sQ0FBQyxNQUFNLG1CQUFtQixHQUFHLFdBQVcsQ0FBQyxtQkFBbUIsQ0FBQztBQUNuRSxNQUFNLENBQUMsTUFBTSxpQkFBaUIsR0FBRyxXQUFXLENBQUMsaUJBQWlCLENBQUM7QUFDL0QsTUFBTSxDQUFDLE1BQU0sc0JBQXNCLEdBQUcsV0FBVyxDQUFDLHNCQUFzQixDQUFDO0FBQ3pFLE1BQU0sQ0FBQyxNQUFNLGtCQUFrQixHQUFHLFdBQVcsQ0FBQyxrQkFBa0IsQ0FBQztBQUVqRSw0RUFBNEU7QUFDNUUsTUFBTSxVQUFVLG9CQUFvQixDQUFDLE1BQWMsT0FBTyxDQUFDLEdBQUcsRUFBRTtJQUM5RCxLQUFLLE1BQU0sSUFBSSxJQUFJLG1CQUFtQixDQUFDLEdBQUcsQ0FBQyxFQUFFLENBQUM7UUFDNUMsS0FBSyxNQUFNLFNBQVMsSUFBSSxtQkFBbUIsRUFBRSxDQUFDO1lBQzVDLE1BQU0sSUFBSSxHQUFHLE9BQU8sQ0FBQyxJQUFJLEVBQUUsU0FBUyxDQUFDLENBQUM7WUFDdEMsSUFBSSxRQUFRLENBQUMsSUFBSSxDQUFDLEVBQUUsTUFBTSxFQUFFO2dCQUFFLE9BQU8sSUFBSSxDQUFDO1FBQzVDLENBQUM7SUFDSCxDQUFDO0lBQ0QsT0FBTyxTQUFTLENBQUM7QUFDbkIsQ0FBQztBQUVELDRFQUE0RTtBQUM1RSxNQUFNLENBQUMsS0FBSyxVQUFVLG9CQUFvQixDQUFDLElBQVk7SUFDckQsTUFBTSxNQUFNLEdBQUcsTUFBTSxRQUFRLENBQUMsSUFBSSxFQUFFLE1BQU0sQ0FBQyxDQUFDO0lBQzVDLE1BQU0sU0FBUyxHQUFHLE9BQU8sQ0FBQyxJQUFJLENBQUMsQ0FBQyxXQUFXLEVBQUUsQ0FBQztJQUM5QyxJQUFJLEtBQWMsQ0FBQztJQUVuQixJQUFJLFNBQVMsS0FBSyxPQUFPLEVBQUUsQ0FBQztRQUMxQixLQUFLLEdBQUcsSUFBSSxDQUFDLEtBQUssQ0FBQyxNQUFNLENBQVksQ0FBQztJQUN4QyxDQUFDO1NBQU0sSUFBSSxTQUFTLEtBQUssT0FBTyxJQUFJLFNBQVMsS0FBSyxNQUFNLEVBQUUsQ0FBQztRQUN6RCxNQUFNLEVBQUUsS0FBSyxFQUFFLEdBQUcsTUFBTSxNQUFNLENBQUMsTUFBTSxDQUFDLENBQUM7UUFDdkMsS0FBSyxHQUFHLEtBQUssQ0FBQyxNQUFNLENBQVksQ0FBQztJQUNuQyxDQUFDO1NBQU0sQ0FBQztRQUNOLE1BQU0sSUFBSSxLQUFLLENBQUMscUNBQXFDLFNBQVMsSUFBSSxjQUFjLEVBQUUsQ0FBQyxDQUFDO0lBQ3RGLENBQUM7SUFFRCxPQUFPLFdBQVcsQ0FBQyxpQkFBaUIsQ0FBQyxLQUFLLENBQUMsQ0FBQztBQUM5QyxDQUFDO0FBRUQ7OztHQUdHO0FBQ0gsTUFBTSxDQUFDLEtBQUssVUFBVSxnQkFBZ0IsQ0FBQyxNQUFjLE9BQU8sQ0FBQyxHQUFHLEVBQUU7SUFDaEUsTUFBTSxJQUFJLEdBQUcsb0JBQW9CLENBQUMsR0FBRyxDQUFDLENBQUM7SUFDdkMsT0FBTyxJQUFJLENBQUMsQ0FBQyxDQUFDLG9CQUFvQixDQUFDLElBQUksQ0FBQyxDQUFDLENBQUMsQ0FBQyxXQUFXLENBQUMsbUJBQW1CLENBQUM7QUFDN0UsQ0FBQztBQUVELGtGQUFrRjtBQUNsRixNQUFNLFVBQVUscUJBQXFCLENBQUMsU0FBaUIsRUFBRSxLQUFhO0lBQ3BFLElBQ0UsVUFBVSxDQUFDLEtBQUssQ0FBQztRQUNqQixLQUFLLENBQUMsVUFBVSxDQUFDLEdBQUcsQ0FBQztRQUNyQixLQUFLLENBQUMsVUFBVSxDQUFDLElBQUksQ0FBQztRQUN0QixxQkFBcUIsQ0FBQyxJQUFJLENBQUMsS0FBSyxDQUFDLEVBQ2pDLENBQUM7UUFDRCxPQUFPLEtBQUssQ0FBQztJQUNmLENBQUM7SUFDRCxPQUFPLE9BQU8sQ0FBQyxPQUFPLENBQUMsU0FBUyxDQUFDLEVBQUUsS0FBSyxDQUFDLENBQUM7QUFDNUMsQ0FBQyIsInNvdXJjZXNDb250ZW50IjpbIi8qKiBOb2RlLW9ubHkgZGlzY292ZXJ5IGFuZCBmaWxlIGxvYWRpbmcgZm9yIHRoZSBzaGFyZWQgYnJhbmQgY29udGV4dC4gKi9cbmltcG9ydCB7IHJlYWRGaWxlIH0gZnJvbSBcIm5vZGU6ZnMvcHJvbWlzZXNcIjtcbmltcG9ydCB7IGRpcm5hbWUsIGV4dG5hbWUsIGlzQWJzb2x1dGUsIHJlc29sdmUgfSBmcm9tIFwibm9kZTpwYXRoXCI7XG5pbXBvcnQgeyBicmFuZCBhcyBzaGFyZWRCcmFuZCB9IGZyb20gXCJAZGJ4LXRvb2xzL3NoYXJlZC1jb3JlXCI7XG5pbXBvcnQgeyBzdGF0U3luYyB9IGZyb20gXCIuL2ZpbGUudHNcIjtcbmltcG9ydCB7IHJlc29sdmVQcm9qZWN0Um9vdHMgfSBmcm9tIFwiLi9wcm9qZWN0LnRzXCI7XG5cbmNvbnN0IEJSQU5EX0NPTlRFWFRfRklMRVMgPSBbXG4gIFwiYnJhbmRpbmcvYnJhbmQueWFtbFwiLFxuICBcImJyYW5kaW5nL2JyYW5kLnltbFwiLFxuICBcImJyYW5kaW5nL2JyYW5kLmpzb25cIixcbiAgXCJicmFuZC55YW1sXCIsXG4gIFwiYnJhbmQueW1sXCIsXG4gIFwiYnJhbmQuanNvblwiLFxuXSBhcyBjb25zdDtcblxuZXhwb3J0IHR5cGUgQnJhbmRDb250ZXh0ID0gc2hhcmVkQnJhbmQuQnJhbmRDb250ZXh0O1xuZXhwb3J0IHR5cGUgQnJhbmRDb250ZXh0SW5wdXQgPSBzaGFyZWRCcmFuZC5CcmFuZENvbnRleHRJbnB1dDtcbmV4cG9ydCBjb25zdCBCcmFuZENvbnRleHRTY2hlbWEgPSBzaGFyZWRCcmFuZC5CcmFuZENvbnRleHRTY2hlbWE7XG5leHBvcnQgY29uc3QgZGVmYXVsdEJyYW5kQ29udGV4dCA9IHNoYXJlZEJyYW5kLmRlZmF1bHRCcmFuZENvbnRleHQ7XG5leHBvcnQgY29uc3QgcGFyc2VCcmFuZENvbnRleHQgPSBzaGFyZWRCcmFuZC5wYXJzZUJyYW5kQ29udGV4dDtcbmV4cG9ydCBjb25zdCBicmFuZENvbnRleHRKc29uU2NoZW1hID0gc2hhcmVkQnJhbmQuYnJhbmRDb250ZXh0SnNvblNjaGVtYTtcbmV4cG9ydCBjb25zdCBicmFuZENvbnRleHRQcm9tcHQgPSBzaGFyZWRCcmFuZC5icmFuZENvbnRleHRQcm9tcHQ7XG5cbi8qKiBGaW5kIGEgY29udmVudGlvbmFsIFlBTUwgb3IgSlNPTiBicmFuZCBmaWxlIGZyb20ga25vd24gcHJvamVjdCByb290cy4gKi9cbmV4cG9ydCBmdW5jdGlvbiBmaW5kQnJhbmRDb250ZXh0RmlsZShjd2Q6IHN0cmluZyA9IHByb2Nlc3MuY3dkKCkpOiBzdHJpbmcgfCB1bmRlZmluZWQge1xuICBmb3IgKGNvbnN0IHJvb3Qgb2YgcmVzb2x2ZVByb2plY3RSb290cyhjd2QpKSB7XG4gICAgZm9yIChjb25zdCBjYW5kaWRhdGUgb2YgQlJBTkRfQ09OVEVYVF9GSUxFUykge1xuICAgICAgY29uc3QgcGF0aCA9IHJlc29sdmUocm9vdCwgY2FuZGlkYXRlKTtcbiAgICAgIGlmIChzdGF0U3luYyhwYXRoKT8uaXNGaWxlKCkpIHJldHVybiBwYXRoO1xuICAgIH1cbiAgfVxuICByZXR1cm4gdW5kZWZpbmVkO1xufVxuXG4vKiogUmVhZCBhbmQgdmFsaWRhdGUgb25lIGAueWFtbGAsIGAueW1sYCwgb3IgYC5qc29uYCBicmFuZCBjb250ZXh0IGZpbGUuICovXG5leHBvcnQgYXN5bmMgZnVuY3Rpb24gbG9hZEJyYW5kQ29udGV4dEZpbGUocGF0aDogc3RyaW5nKTogUHJvbWlzZTxCcmFuZENvbnRleHQ+IHtcbiAgY29uc3Qgc291cmNlID0gYXdhaXQgcmVhZEZpbGUocGF0aCwgXCJ1dGY4XCIpO1xuICBjb25zdCBleHRlbnNpb24gPSBleHRuYW1lKHBhdGgpLnRvTG93ZXJDYXNlKCk7XG4gIGxldCBpbnB1dDogdW5rbm93bjtcblxuICBpZiAoZXh0ZW5zaW9uID09PSBcIi5qc29uXCIpIHtcbiAgICBpbnB1dCA9IEpTT04ucGFyc2Uoc291cmNlKSBhcyB1bmtub3duO1xuICB9IGVsc2UgaWYgKGV4dGVuc2lvbiA9PT0gXCIueWFtbFwiIHx8IGV4dGVuc2lvbiA9PT0gXCIueW1sXCIpIHtcbiAgICBjb25zdCB7IHBhcnNlIH0gPSBhd2FpdCBpbXBvcnQoXCJ5YW1sXCIpO1xuICAgIGlucHV0ID0gcGFyc2Uoc291cmNlKSBhcyB1bmtub3duO1xuICB9IGVsc2Uge1xuICAgIHRocm93IG5ldyBFcnJvcihgVW5zdXBwb3J0ZWQgYnJhbmQgY29udGV4dCBmb3JtYXQ6ICR7ZXh0ZW5zaW9uIHx8IFwibm8gZXh0ZW5zaW9uXCJ9YCk7XG4gIH1cblxuICByZXR1cm4gc2hhcmVkQnJhbmQucGFyc2VCcmFuZENvbnRleHQoaW5wdXQpO1xufVxuXG4vKipcbiAqIERpc2NvdmVyIGFuZCBsb2FkIGEgYnJhbmQgY29udGV4dC4gTWlzc2luZyBmaWxlcyByZXNvbHZlIHRvIGRieCB0b29scyBkZWZhdWx0cztcbiAqIG1hbGZvcm1lZCBmaWxlcyBmYWlsIHdpdGggdGhlaXIgcGFyc2VyIG9yIFpvZCB2YWxpZGF0aW9uIGVycm9yLlxuICovXG5leHBvcnQgYXN5bmMgZnVuY3Rpb24gbG9hZEJyYW5kQ29udGV4dChjd2Q6IHN0cmluZyA9IHByb2Nlc3MuY3dkKCkpOiBQcm9taXNlPEJyYW5kQ29udGV4dD4ge1xuICBjb25zdCBwYXRoID0gZmluZEJyYW5kQ29udGV4dEZpbGUoY3dkKTtcbiAgcmV0dXJuIHBhdGggPyBsb2FkQnJhbmRDb250ZXh0RmlsZShwYXRoKSA6IHNoYXJlZEJyYW5kLmRlZmF1bHRCcmFuZENvbnRleHQ7XG59XG5cbi8qKiBSZXNvbHZlIGEgcmVsYXRpdmUgYXNzZXQgcmVmZXJlbmNlIGFnYWluc3QgdGhlIGJyYW5kIGZpbGUgdGhhdCBkZWNsYXJlZCBpdC4gKi9cbmV4cG9ydCBmdW5jdGlvbiByZXNvbHZlQnJhbmRBc3NldFBhdGgoYnJhbmRGaWxlOiBzdHJpbmcsIGFzc2V0OiBzdHJpbmcpOiBzdHJpbmcge1xuICBpZiAoXG4gICAgaXNBYnNvbHV0ZShhc3NldCkgfHxcbiAgICBhc3NldC5zdGFydHNXaXRoKFwiQFwiKSB8fFxuICAgIGFzc2V0LnN0YXJ0c1dpdGgoXCIvL1wiKSB8fFxuICAgIC9eW2Etel1bYS16XFxkKy4tXSo6L2kudGVzdChhc3NldClcbiAgKSB7XG4gICAgcmV0dXJuIGFzc2V0O1xuICB9XG4gIHJldHVybiByZXNvbHZlKGRpcm5hbWUoYnJhbmRGaWxlKSwgYXNzZXQpO1xufVxuIl19