@dbx-tools/core 0.9.5 → 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.
- package/README.md +13 -13
- package/index.ts +9 -9
- package/lib/index.d.ts +9 -9
- package/lib/index.js +7 -7
- package/lib/src/bin.js +3 -3
- package/lib/src/{brand.d.ts → brand-files.d.ts} +6 -6
- package/lib/src/brand-files.js +66 -0
- package/lib/src/bundle.js +6 -6
- package/lib/src/{config.d.ts → config-utils.d.ts} +1 -1
- package/lib/src/config-utils.js +583 -0
- package/lib/src/file-lock.js +7 -7
- package/lib/src/process-lock.js +3 -3
- package/lib/src/{project.d.ts → project-utils.d.ts} +1 -0
- package/lib/src/project-utils.js +296 -0
- package/lib/tsconfig.tsbuildinfo +1 -1
- package/package.json +27 -2
- package/src/bin.ts +2 -2
- package/src/{brand.ts → brand-files.ts} +11 -11
- package/src/bundle.ts +5 -5
- package/src/{config.ts → config-utils.ts} +27 -44
- package/src/file-lock.ts +6 -6
- package/src/process-lock.ts +2 -2
- package/src/{project.ts → project-utils.ts} +5 -4
- package/lib/src/brand.js +0 -66
- package/lib/src/config.js +0 -595
- package/lib/src/project.js +0 -295
|
@@ -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 {
|
|
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)
|
|
120
|
-
|
|
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
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
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(
|
|
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(
|
|
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(
|
|
292
|
+
.map(stringUtils.trimToEmpty)
|
|
310
293
|
.filter(Boolean)
|
|
311
294
|
.distinct()
|
|
312
295
|
.toArray();
|
|
313
296
|
return object
|
|
314
297
|
.sequence(input)
|
|
315
|
-
.map(
|
|
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 =
|
|
350
|
+
const resolved = stringUtils.trimToNull(entry);
|
|
368
351
|
if (resolved !== null) return resolved;
|
|
369
352
|
}
|
|
370
353
|
return null;
|
|
371
354
|
}
|
|
372
|
-
return
|
|
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 =
|
|
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
|
|
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 `
|
|
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 =
|
|
493
|
+
const fromConfig = stringUtils.parseList(configured, transform);
|
|
511
494
|
return fromConfig.length > 0
|
|
512
495
|
? fromConfig
|
|
513
|
-
:
|
|
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,
|
|
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 =
|
|
684
|
-
const error =
|
|
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 {
|
|
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 =
|
|
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:
|
|
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
|
|
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
|
|
245
|
+
if (err.code !== "ELOCKED") throw errorUtils.toError(cause);
|
|
246
246
|
assertBeforeDeadline(lockPath, deadline);
|
|
247
|
-
await
|
|
247
|
+
await asyncUtils.sleep(POLL_MS);
|
|
248
248
|
}
|
|
249
249
|
}
|
|
250
250
|
}
|
package/src/process-lock.ts
CHANGED
|
@@ -38,7 +38,7 @@ import {
|
|
|
38
38
|
type Worker,
|
|
39
39
|
type WorkerOptions,
|
|
40
40
|
} from "node:worker_threads";
|
|
41
|
-
import {
|
|
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(
|
|
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,
|
|
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(
|
|
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
|
|
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
|
|
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
|