@dbx-tools/appkit-mastra 0.6.10 → 0.6.11

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.
@@ -1,19 +1,24 @@
1
1
  /**
2
2
  * Startup provisioning of remote Agent-Skill sources for a Mastra workspace.
3
3
  *
4
- * A {@link RemoteSkillSource} names WHERE a `SKILL.md` tree comes from - a
5
- * GitHub `owner/repo`, any git / GitLab URL, or a direct download URL - and
6
- * optional per-source policy. {@link provisionRemoteSkills} materializes every
7
- * source into a local `SKILL.md` tree at app boot and returns the directories
8
- * to hand Mastra as extra skill scan paths.
4
+ * A {@link RemoteSkillSource} names WHERE a `SKILL.md` tree comes from - the
5
+ * {@link AITOOLS_SOURCE} constant, a GitHub `owner/repo`, any git / GitLab URL,
6
+ * or a direct download URL - and optional per-source policy.
7
+ * {@link provisionRemoteSkills} materializes every source into a local
8
+ * `SKILL.md` tree at app boot and returns the directories to hand Mastra as
9
+ * extra skill scan paths.
9
10
  *
10
11
  * Resolution per source, in order:
11
12
  *
12
- * 1. the OPTIONAL `skills` npm CLI (peer dep): if installed, each source is
13
+ * 1. the {@link AITOOLS_SOURCE} constant (`"aitools"`): Databricks' own skill
14
+ * set, read straight from the public repo the `databricks aitools` CLI
15
+ * sources from. No CLI, no Databricks auth - which is what makes it usable
16
+ * inside a Databricks App container, where the CLI is not installed;
17
+ * 2. the OPTIONAL `skills` npm CLI (peer dep): if installed, each source is
13
18
  * copied into a staging dir with `skills add <source> --agent <dir> --copy`,
14
19
  * which understands every source format the ecosystem does (GitHub
15
20
  * shorthand, git URLs, archive/download URLs);
16
- * 2. otherwise a plain {@link fetch} of the source URL (built with
21
+ * 3. otherwise a plain {@link fetch} of the source URL (built with
17
22
  * {@link net.urlBuilder}), writing the downloaded `SKILL.md` to a staging
18
23
  * dir.
19
24
  *
@@ -35,7 +40,7 @@
35
40
 
36
41
  import { mkdir, readdir, readFile, rm, stat, writeFile } from "node:fs/promises";
37
42
  import { createRequire } from "node:module";
38
- import { join, posix } from "node:path";
43
+ import { dirname, join, posix } from "node:path";
39
44
  import type { WorkspaceClient } from "@databricks/sdk-experimental";
40
45
  import { appkit } from "@dbx-tools/appkit";
41
46
  import type { WorkspaceClientLike } from "@dbx-tools/appkit";
@@ -43,7 +48,8 @@ import { exec } from "@dbx-tools/core";
43
48
  import { DatabricksFileSystem } from "@dbx-tools/databricks";
44
49
  import { localFS, type LocalFileSystem } from "@dbx-tools/fs";
45
50
  import { find } from "@dbx-tools/path";
46
- import { error, hash, log, net, string } from "@dbx-tools/shared-core";
51
+ import { error, hash, json, log, net, object, string } from "@dbx-tools/shared-core";
52
+ import type { OneOrMany } from "@dbx-tools/shared-core";
47
53
  import type { FileSystem } from "@dbx-tools/shared-fs";
48
54
 
49
55
  const logger = log.logger("mastra/remote-skills");
@@ -68,20 +74,55 @@ const DEFAULT_MAX_DOWNLOAD_BYTES = 10 * 1024 * 1024;
68
74
  /** Stable temp directory holding one rebuilt skill tree per remote source. */
69
75
  const LOCAL_SKILLS_DIR = "mastra-local-skills";
70
76
 
77
+ /* ------------------------------ AI Tools ------------------------------ */
78
+
79
+ /**
80
+ * Source constant selecting Databricks' own AI Tools skill set.
81
+ *
82
+ * @example
83
+ * mastra({ remoteSkills: "aitools" });
84
+ */
85
+ export const AITOOLS_SOURCE = "aitools";
86
+
87
+ /** The {@link AITOOLS_SOURCE} literal, for callers spelling the union out. */
88
+ export type AiToolsSource = typeof AITOOLS_SOURCE;
89
+
90
+ /**
91
+ * The PUBLIC repo `databricks aitools install` sources from. Reading it
92
+ * directly is what lets an app get the same skills the CLI installs without
93
+ * the CLI - which a Databricks App container does not ship.
94
+ */
95
+ const AITOOLS_REPO = "databricks/databricks-agent-skills";
96
+
97
+ /** Repo ref the skills are read from. Override per source with `ref`. */
98
+ const AITOOLS_REF = "main";
99
+
100
+ /** Skill files downloaded at once; the set is ~30 skills of a few files each. */
101
+ const AITOOLS_CONCURRENCY = 8;
102
+
103
+ /** The subset of the repo's generated `manifest.json` this module reads. */
104
+ interface AiToolsManifest {
105
+ skills?: Record<string, { files?: string[]; repo_dir?: string }>;
106
+ }
107
+
71
108
  /* -------------------------------- types -------------------------------- */
72
109
 
73
110
  /**
74
111
  * One remote skill source and its per-source policy.
75
112
  *
76
- * `source` is anything the `skills` ecosystem understands: a GitHub
77
- * `owner/repo` shorthand, a full GitHub / GitLab / git URL, or a direct
78
- * download URL to a `SKILL.md` or archive.
113
+ * `source` is {@link AITOOLS_SOURCE} or anything the `skills` ecosystem
114
+ * understands: a GitHub `owner/repo` shorthand, a full GitHub / GitLab / git
115
+ * URL, or a direct download URL to a `SKILL.md` or archive.
79
116
  */
80
117
  export interface RemoteSkillSourceOptions {
81
- /** GitHub shorthand, git / GitLab URL, or a direct download URL. */
82
- source: string;
83
- /** Install only these skill names from the source (CLI path only). */
118
+ /** `"aitools"`, a GitHub shorthand, a git / GitLab URL, or a download URL. */
119
+ source: net.UrlLike | AiToolsSource;
120
+ /** Install only these skill names from the source. */
84
121
  skills?: string | string[];
122
+ /** Include experimental skills. `"aitools"` only. */
123
+ experimental?: boolean;
124
+ /** Pin the repo ref (tag / branch / sha). `"aitools"` only; defaults to `main`. */
125
+ ref?: string;
85
126
  /** Override the byte ceiling on a direct-fetch download for this source. */
86
127
  maxDownloadBytes?: number;
87
128
  /**
@@ -92,22 +133,26 @@ export interface RemoteSkillSourceOptions {
92
133
  }
93
134
 
94
135
  /**
95
- * A remote skill source: a bare source string (GitHub shorthand / URL) or a
96
- * {@link RemoteSkillSourceOptions} with per-source policy.
136
+ * A remote skill source: the {@link AITOOLS_SOURCE} constant, a URL-like
137
+ * (string / `URL` / `{ url }`), or a {@link RemoteSkillSourceOptions} bag with
138
+ * per-source policy.
97
139
  */
98
- export type RemoteSkillSource = string | RemoteSkillSourceOptions;
140
+ export type RemoteSkillSource = net.UrlLike | AiToolsSource | RemoteSkillSourceOptions;
99
141
 
100
142
  /**
101
- * The workspace `remoteSkills` option. A single source, a list, or a
102
- * {@link ProvisionRemoteSkillsOptions} bag when top-level policy is needed.
143
+ * The workspace `remoteSkills` option: one source, a non-empty list of them, or
144
+ * a {@link ProvisionRemoteSkillsOptions} bag when top-level policy is needed.
103
145
  */
104
146
  export type RemoteSkillsOption =
105
- RemoteSkillSource | RemoteSkillSource[] | ProvisionRemoteSkillsOptions;
147
+ RemoteSkillSource | OneOrMany<RemoteSkillSource> | ProvisionRemoteSkillsOptions;
148
+
149
+ /** A source entry with its `source` flattened to a plain string. */
150
+ type NormalizedSource = Omit<RemoteSkillSourceOptions, "source"> & { source: string };
106
151
 
107
152
  /** Top-level remote-skills provisioning options. */
108
153
  export interface ProvisionRemoteSkillsOptions {
109
154
  /** Sources to materialize at startup. */
110
- sources: RemoteSkillSource | RemoteSkillSource[];
155
+ sources: RemoteSkillSource | OneOrMany<RemoteSkillSource>;
111
156
  /**
112
157
  * Fail app startup when a source can't be resolved. Defaults to `true`. A
113
158
  * per-source `failOnError` wins over this.
@@ -147,26 +192,42 @@ export function normalizeRemoteSkillsOption(
147
192
  option: RemoteSkillsOption | undefined,
148
193
  ): ProvisionRemoteSkillsOptions | undefined {
149
194
  if (option === undefined) return undefined;
150
- if (typeof option === "string") return { sources: [option] };
151
- if (Array.isArray(option)) return option.length > 0 ? { sources: option } : undefined;
152
- if (isProvisionOptions(option)) {
153
- const sources = Array.isArray(option.sources) ? option.sources : [option.sources];
154
- return sources.length > 0 ? { ...option, sources } : undefined;
155
- }
156
- // A lone RemoteSkillSourceOptions object.
157
- return { sources: [option] };
195
+ const bag = isProvisionOptions(option) ? option : undefined;
196
+ const sources = toSourceList(bag ? bag.sources : (option as RemoteSkillSource));
197
+ // An empty list is the same as no configuration at all.
198
+ if (!object.isOneOrMany(sources)) return undefined;
199
+ return { ...bag, sources };
158
200
  }
159
201
 
160
202
  /** A `sources`-bearing bag is the top-level options shape, not a single source. */
161
- function isProvisionOptions(
162
- value: RemoteSkillSourceOptions | ProvisionRemoteSkillsOptions,
163
- ): value is ProvisionRemoteSkillsOptions {
164
- return "sources" in value;
203
+ function isProvisionOptions(value: unknown): value is ProvisionRemoteSkillsOptions {
204
+ return object.isRecord(value) && "sources" in value;
165
205
  }
166
206
 
167
- /** Coerce a source entry to its normalized {@link RemoteSkillSourceOptions}. */
168
- function toSourceOptions(source: RemoteSkillSource): RemoteSkillSourceOptions {
169
- return typeof source === "string" ? { source } : source;
207
+ /** One source or many, as a plain array. A source is never itself an array. */
208
+ function toSourceList(
209
+ input: RemoteSkillSource | OneOrMany<RemoteSkillSource>,
210
+ ): RemoteSkillSource[] {
211
+ return Array.isArray(input) ? [...input] : [input];
212
+ }
213
+
214
+ /** Flatten a source entry - string, `URL`, `{ url }`, or options bag - to a {@link NormalizedSource}. */
215
+ function toSourceOptions(source: RemoteSkillSource): NormalizedSource {
216
+ if (typeof source === "string") return { source };
217
+ if (source instanceof URL) return { source: source.toString() };
218
+ if ("source" in source) return { ...source, source: toSourceString(source.source) };
219
+ return { source: source.url };
220
+ }
221
+
222
+ /** A {@link net.UrlLike} as the plain string the staging paths work with. */
223
+ function toSourceString(source: net.UrlLike): string {
224
+ if (typeof source === "string") return source;
225
+ return source instanceof URL ? source.toString() : source.url;
226
+ }
227
+
228
+ /** True when a source selects the built-in Databricks AI Tools skill set. */
229
+ function isAiToolsSource(source: string): boolean {
230
+ return source.trim().toLowerCase() === AITOOLS_SOURCE;
170
231
  }
171
232
 
172
233
  /**
@@ -265,24 +326,92 @@ function resolveDatabricksBasePath(
265
326
  * `skills` CLI and falling back to a direct fetch.
266
327
  */
267
328
  async function stageSource(
268
- sourceOptions: RemoteSkillSourceOptions,
329
+ sourceOptions: NormalizedSource,
269
330
  stagingRoot: string,
270
331
  options: ProvisionRemoteSkillsOptions,
271
332
  ): Promise<string> {
272
333
  const target = join(stagingRoot, `src-${hash.id()}`);
273
334
  await mkdir(target, { recursive: true });
335
+ // AI Tools resolves from a known repo layout, so it never needs the CLI.
336
+ if (isAiToolsSource(sourceOptions.source)) return stageAiTools(sourceOptions, target, options);
274
337
  const viaCli = await stageViaSkillsCli(sourceOptions, target);
275
338
  if (viaCli) return viaCli;
276
339
  return stageViaFetch(sourceOptions, target, options);
277
340
  }
278
341
 
342
+ /**
343
+ * Materialize the Databricks AI Tools skills into `target`.
344
+ *
345
+ * Reads the repo's own generated `manifest.json` (which names each skill's
346
+ * files and whether it lives under `skills/` or `experimental/`) and downloads
347
+ * them - the same thing `databricks aitools install` does, minus the CLI and
348
+ * minus any Databricks auth, since the repo is public.
349
+ */
350
+ async function stageAiTools(
351
+ sourceOptions: NormalizedSource,
352
+ target: string,
353
+ options: ProvisionRemoteSkillsOptions,
354
+ ): Promise<string> {
355
+ const maxBytes = resolveMaxBytes(sourceOptions, options);
356
+ const ref = string.trimToNull(sourceOptions.ref) ?? AITOOLS_REF;
357
+ const manifest = json.parse(
358
+ (await download(aiToolsRawUrl(ref, "manifest.json"), maxBytes)).toString("utf8"),
359
+ undefined,
360
+ ) as AiToolsManifest | undefined;
361
+
362
+ const wanted = new Set(string.parseList(sourceOptions.skills));
363
+ const selected = Object.entries(manifest?.skills ?? {}).filter(([name, entry]) =>
364
+ wanted.size > 0
365
+ ? wanted.has(name)
366
+ : entry.repo_dir !== "experimental" || sourceOptions.experimental === true,
367
+ );
368
+ if (selected.length === 0) {
369
+ throw new Error(`no matching skills in ${AITOOLS_REPO}@${ref}`);
370
+ }
371
+
372
+ const files = selected.flatMap(([name, entry]) =>
373
+ (entry.files ?? []).map((file) => ({
374
+ url: aiToolsRawUrl(ref, `${entry.repo_dir ?? "skills"}/${name}/${file}`),
375
+ path: join(target, name, ...file.split("/")),
376
+ })),
377
+ );
378
+ await mapConcurrent(files, AITOOLS_CONCURRENCY, async (item) => {
379
+ const body = await download(item.url, maxBytes);
380
+ await mkdir(dirname(item.path), { recursive: true });
381
+ await writeFile(item.path, body);
382
+ });
383
+
384
+ logger.debug("aitools:staged", { ref, skills: selected.length, files: files.length });
385
+ return target;
386
+ }
387
+
388
+ /** Raw-content URL for a path in the AI Tools repo at `ref`. */
389
+ function aiToolsRawUrl(ref: string, path: string): string {
390
+ return `https://raw.githubusercontent.com/${AITOOLS_REPO}/${ref}/${path}`;
391
+ }
392
+
393
+ /** Run `worker` over `items`, at most `limit` in flight. */
394
+ async function mapConcurrent<T>(
395
+ items: readonly T[],
396
+ limit: number,
397
+ worker: (item: T) => Promise<void>,
398
+ ): Promise<void> {
399
+ let cursor = 0;
400
+ const runners = Array.from({ length: Math.min(limit, items.length) }, async () => {
401
+ while (cursor < items.length) {
402
+ await worker(items[cursor++]!);
403
+ }
404
+ });
405
+ await Promise.all(runners);
406
+ }
407
+
279
408
  /**
280
409
  * Copy a source into `target` with the `skills` CLI when it is installed.
281
410
  * Returns the dir holding the copied `SKILL.md` trees, or `undefined` when the
282
411
  * CLI is absent (so the caller can fall back to a fetch).
283
412
  */
284
413
  async function stageViaSkillsCli(
285
- sourceOptions: RemoteSkillSourceOptions,
414
+ sourceOptions: NormalizedSource,
286
415
  target: string,
287
416
  ): Promise<string | undefined> {
288
417
  const cli = await resolveSkillsCli();
@@ -338,7 +467,7 @@ async function resolveSkillsCli(): Promise<{ command: string; args: string[] } |
338
467
  * Used when the `skills` CLI isn't installed; supports a direct `SKILL.md` URL.
339
468
  */
340
469
  async function stageViaFetch(
341
- sourceOptions: RemoteSkillSourceOptions,
470
+ sourceOptions: NormalizedSource,
342
471
  target: string,
343
472
  options: ProvisionRemoteSkillsOptions,
344
473
  ): Promise<string> {
@@ -348,18 +477,7 @@ async function stageViaFetch(
348
477
  `source "${sourceOptions.source}" is not a URL and the optional "skills" package is not installed`,
349
478
  );
350
479
  }
351
- const maxBytes =
352
- sourceOptions.maxDownloadBytes ?? options.maxDownloadBytes ?? DEFAULT_MAX_DOWNLOAD_BYTES;
353
- const response = await fetch(url.toString());
354
- if (!response.ok) {
355
- throw new Error(
356
- `download failed: ${response.status} ${response.statusText} (${url.toString()})`,
357
- );
358
- }
359
- const buffer = Buffer.from(await response.arrayBuffer());
360
- if (buffer.byteLength > maxBytes) {
361
- throw new Error(`download exceeds ${maxBytes} bytes (${url.toString()})`);
362
- }
480
+ const buffer = await download(url.toString(), resolveMaxBytes(sourceOptions, options));
363
481
  const name = string.toSlug(deriveSkillName(url.toString())) || "remote-skill";
364
482
  const skillDir = join(target, name);
365
483
  await mkdir(skillDir, { recursive: true });
@@ -367,6 +485,27 @@ async function stageViaFetch(
367
485
  return target;
368
486
  }
369
487
 
488
+ /** The effective download ceiling: per-source, then top-level, then the default. */
489
+ function resolveMaxBytes(
490
+ sourceOptions: NormalizedSource,
491
+ options: ProvisionRemoteSkillsOptions,
492
+ ): number {
493
+ return sourceOptions.maxDownloadBytes ?? options.maxDownloadBytes ?? DEFAULT_MAX_DOWNLOAD_BYTES;
494
+ }
495
+
496
+ /** GET a URL, failing loudly on a non-2xx status or an oversized body. */
497
+ async function download(url: string, maxBytes: number): Promise<Buffer> {
498
+ const response = await fetch(url);
499
+ if (!response.ok) {
500
+ throw new Error(`download failed: ${response.status} ${response.statusText} (${url})`);
501
+ }
502
+ const buffer = Buffer.from(await response.arrayBuffer());
503
+ if (buffer.byteLength > maxBytes) {
504
+ throw new Error(`download exceeds ${maxBytes} bytes (${url})`);
505
+ }
506
+ return buffer;
507
+ }
508
+
370
509
  /** Derive a skill directory name from a download URL's last path segment. */
371
510
  function deriveSkillName(urlString: string): string {
372
511
  const url = net.urlBuilder(urlString);
@@ -1,73 +0,0 @@
1
- /**
2
- * Startup provisioning of Databricks AI Tools skills for a Mastra workspace.
3
- *
4
- * Databricks AI Tools (`databricks aitools`) are Databricks-owned Agent-Skill
5
- * (`SKILL.md`) trees installed and kept up to date through the Databricks CLI.
6
- * {@link provisionDatabricksAITools} shells out to the CLI
7
- * (`databricks aitools install --path <dir>`) to materialize a resolved,
8
- * agent-agnostic skill set into a directory, then hands it to Mastra as an
9
- * extra LOCAL skill scan path - so an agent gets first-class Databricks skills
10
- * without anyone hand-copying `SKILL.md` files. It never reimplements the CLI's
11
- * sourcing; the CLI is the source of truth.
12
- *
13
- * The option is `false | true | "auto" | DatabricksAIToolsOptions`:
14
- *
15
- * - `false` (default) - off.
16
- * - `"auto"` - run the CLI when it's installed; if the `databricks` CLI isn't
17
- * available, log and move on (never fails startup). The "on if the CLI is
18
- * around" default.
19
- * - `true` (`"require"`) - require the CLI: fail startup (subject to
20
- * `failOnError`) when it isn't available or produces nothing.
21
- * - an options bag for a specific `skills` subset, `experimental` skills, an
22
- * explicit output `path`, or a custom `cli` path.
23
- *
24
- * @module
25
- */
26
- /** How aggressively to enable Databricks AI Tools skills. */
27
- export type DatabricksAIToolsMode = "auto" | "require";
28
- /** Options for {@link provisionDatabricksAITools}. */
29
- export interface DatabricksAIToolsOptions {
30
- /**
31
- * `"auto"` (default) runs the CLI when installed and logs-and-continues when
32
- * it isn't; `"require"` fails startup when the CLI can't supply skills.
33
- */
34
- mode?: DatabricksAIToolsMode;
35
- /** Install only these skill names (comma/space list or array). */
36
- skills?: string | string[];
37
- /** Include experimental skills when the CLI has to fetch. */
38
- experimental?: boolean;
39
- /**
40
- * Explicit directory to materialize skills into with the CLI. Defaults to a
41
- * fresh {@link localFS.tmpFS} root.
42
- */
43
- path?: string;
44
- /** Absolute path to the `databricks` CLI. Defaults to `databricks` on PATH. */
45
- cli?: string;
46
- /**
47
- * Fail app startup when required skills can't be provisioned. Defaults to
48
- * `true` for `mode: "require"` and `false` for `"auto"`.
49
- */
50
- failOnError?: boolean;
51
- }
52
- /** The `databricksAITools` config option: a toggle, `"auto"`, or an options bag. */
53
- export type DatabricksAIToolsOption = boolean | "auto" | DatabricksAIToolsOptions;
54
- /** What {@link provisionDatabricksAITools} resolved. */
55
- export interface ProvisionedDatabricksAITools {
56
- /** Extra LOCAL skill scan paths to hand Mastra. Empty when disabled/unresolved. */
57
- localSkillPaths: string[];
58
- /** How the skills were sourced, for logging. */
59
- source?: "cli";
60
- }
61
- /** Normalize the `databricksAITools` option into a flat options bag, or `undefined` when off. */
62
- export declare function normalizeDatabricksAIToolsOption(option: DatabricksAIToolsOption | undefined): DatabricksAIToolsOptions | undefined;
63
- /**
64
- * Materialize Databricks AI Tools skills at app startup and return them as
65
- * extra local skill scan paths.
66
- *
67
- * Resolution: reuse the installed global tree when present (no CLI call) unless
68
- * `refresh`/`skills`/`path` ask for a fresh CLI materialization; otherwise run
69
- * `databricks aitools install --path <dir> --skills-only` when the CLI resolves.
70
- * A `"require"` provision that yields nothing fails startup unless
71
- * `failOnError: false`.
72
- */
73
- export declare function provisionDatabricksAITools(option: DatabricksAIToolsOption | undefined): Promise<ProvisionedDatabricksAITools>;
@@ -1,152 +0,0 @@
1
- /**
2
- * Startup provisioning of Databricks AI Tools skills for a Mastra workspace.
3
- *
4
- * Databricks AI Tools (`databricks aitools`) are Databricks-owned Agent-Skill
5
- * (`SKILL.md`) trees installed and kept up to date through the Databricks CLI.
6
- * {@link provisionDatabricksAITools} shells out to the CLI
7
- * (`databricks aitools install --path <dir>`) to materialize a resolved,
8
- * agent-agnostic skill set into a directory, then hands it to Mastra as an
9
- * extra LOCAL skill scan path - so an agent gets first-class Databricks skills
10
- * without anyone hand-copying `SKILL.md` files. It never reimplements the CLI's
11
- * sourcing; the CLI is the source of truth.
12
- *
13
- * The option is `false | true | "auto" | DatabricksAIToolsOptions`:
14
- *
15
- * - `false` (default) - off.
16
- * - `"auto"` - run the CLI when it's installed; if the `databricks` CLI isn't
17
- * available, log and move on (never fails startup). The "on if the CLI is
18
- * around" default.
19
- * - `true` (`"require"`) - require the CLI: fail startup (subject to
20
- * `failOnError`) when it isn't available or produces nothing.
21
- * - an options bag for a specific `skills` subset, `experimental` skills, an
22
- * explicit output `path`, or a custom `cli` path.
23
- *
24
- * @module
25
- */
26
- import { existsSync } from "node:fs";
27
- import { exec } from "@dbx-tools/core";
28
- import { localFS } from "@dbx-tools/fs";
29
- import { error, log, string } from "@dbx-tools/shared-core";
30
- const logger = log.logger("mastra/databricks-aitools");
31
- /** The Databricks CLI binary name; resolved on `PATH`. */
32
- const DATABRICKS_CLI = "databricks";
33
- /** Stable temp directory the CLI's skill tree is rebuilt into each boot. */
34
- const SKILLS_CACHE_DIR = "databricks-aitools";
35
- /* ------------------------------- helpers ------------------------------- */
36
- /** Normalize the `databricksAITools` option into a flat options bag, or `undefined` when off. */
37
- export function normalizeDatabricksAIToolsOption(option) {
38
- if (option === undefined || option === false)
39
- return undefined;
40
- if (option === true)
41
- return { mode: "require" };
42
- if (option === "auto")
43
- return { mode: "auto" };
44
- return { mode: option.mode ?? "auto", ...option };
45
- }
46
- /**
47
- * Materialize Databricks AI Tools skills at app startup and return them as
48
- * extra local skill scan paths.
49
- *
50
- * Resolution: reuse the installed global tree when present (no CLI call) unless
51
- * `refresh`/`skills`/`path` ask for a fresh CLI materialization; otherwise run
52
- * `databricks aitools install --path <dir> --skills-only` when the CLI resolves.
53
- * A `"require"` provision that yields nothing fails startup unless
54
- * `failOnError: false`.
55
- */
56
- export async function provisionDatabricksAITools(option) {
57
- const options = normalizeDatabricksAIToolsOption(option);
58
- const empty = { localSkillPaths: [] };
59
- if (!options)
60
- return empty;
61
- const mode = options.mode ?? "auto";
62
- const failOnError = options.failOnError ?? mode === "require";
63
- try {
64
- // The `databricks` CLI is the source of truth. When it's resolvable, run
65
- // `databricks aitools install --path <dir>` to materialize a fresh,
66
- // agent-agnostic skill set into a dir Mastra can scan.
67
- const cliPath = await resolveCli(options.cli);
68
- if (cliPath) {
69
- const dir = await materializeViaCli(cliPath, options);
70
- if (dir)
71
- return { localSkillPaths: [dir], source: "cli" };
72
- }
73
- // CLI not installed (or produced nothing). "auto" logs and moves on;
74
- // "require" fails startup unless `failOnError: false`.
75
- if (failOnError) {
76
- throw new Error(`Databricks AI Tools required but the "${DATABRICKS_CLI}" CLI is not available. Install the Databricks CLI (\`databricks aitools install\`), or set databricksAITools: "auto".`);
77
- }
78
- logger.info("databricks CLI not available; skipping AI Tools skills");
79
- return empty;
80
- }
81
- catch (err) {
82
- if (failOnError) {
83
- throw new Error(`failed to provision Databricks AI Tools skills: ${error.errorMessage(err)}`, { cause: error.toError(err) });
84
- }
85
- logger.warn("skipped", { error: error.errorMessage(err) });
86
- return empty;
87
- }
88
- }
89
- /** Resolve the `databricks` CLI, returning its invocation path or `undefined`. */
90
- async function resolveCli(cli) {
91
- const command = string.trimToNull(cli) ?? DATABRICKS_CLI;
92
- try {
93
- const result = await exec.spawn(command, ["aitools", "version"], {
94
- stdout: "capture",
95
- stderr: "capture",
96
- check: false,
97
- });
98
- if (result.exitCode === 0)
99
- return command;
100
- }
101
- catch {
102
- // CLI absent or `aitools` unsupported - caller falls back / fails.
103
- }
104
- return undefined;
105
- }
106
- /**
107
- * Run `databricks aitools install --path <dir>` to materialize a resolved,
108
- * agent-agnostic skill set into a directory (writes `<name>/SKILL.md` trees,
109
- * no agents, no state) Mastra can then scan.
110
- */
111
- async function materializeViaCli(cli, options) {
112
- const explicit = string.trimToNull(options.path);
113
- let dir;
114
- if (explicit) {
115
- // An explicit path is the caller's to own - install straight into it.
116
- await installSkills(cli, options, explicit);
117
- dir = explicit;
118
- }
119
- else {
120
- // The resolved skill set is the same on almost every boot, so rebuild ONE
121
- // stable temp tree rather than leaving a fresh scratch dir behind each
122
- // time. The CLI writes into a throwaway root that only replaces the stable
123
- // one on success, so a failed install keeps the previous skills usable.
124
- const stable = await localFS.rebuildFS(SKILLS_CACHE_DIR, (scratch) => installSkills(cli, options, scratch.root));
125
- dir = stable.root;
126
- }
127
- if (!existsSync(dir))
128
- return undefined;
129
- logger.debug("materialized aitools skills via CLI", {
130
- path: dir,
131
- skills: string.parseList(options.skills),
132
- });
133
- return dir;
134
- }
135
- /** Run `databricks aitools install --path <dir>`, failing loudly on a non-zero exit. */
136
- async function installSkills(cli, options, dir) {
137
- const args = ["aitools", "install", "--path", dir];
138
- const skills = string.parseList(options.skills);
139
- if (skills.length > 0)
140
- args.push("--skills", skills.join(","));
141
- if (options.experimental)
142
- args.push("--experimental");
143
- const result = await exec.spawn(cli, args, {
144
- stdout: "capture",
145
- stderr: "capture",
146
- check: false,
147
- });
148
- if (result.exitCode !== 0) {
149
- throw new Error(`databricks aitools install failed (exit ${result.exitCode}): ${result.stderr || result.stdout}`);
150
- }
151
- }
152
- //# sourceMappingURL=data:application/json;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoiZGF0YWJyaWNrcy1haXRvb2xzLmpzIiwic291cmNlUm9vdCI6IiIsInNvdXJjZXMiOlsiLi4vLi4vc3JjL2RhdGFicmlja3MtYWl0b29scy50cyJdLCJuYW1lcyI6W10sIm1hcHBpbmdzIjoiQUFBQTs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7O0dBd0JHO0FBRUgsT0FBTyxFQUFFLFVBQVUsRUFBRSxNQUFNLFNBQVMsQ0FBQztBQUNyQyxPQUFPLEVBQUUsSUFBSSxFQUFFLE1BQU0saUJBQWlCLENBQUM7QUFDdkMsT0FBTyxFQUFFLE9BQU8sRUFBRSxNQUFNLGVBQWUsQ0FBQztBQUN4QyxPQUFPLEVBQUUsS0FBSyxFQUFFLEdBQUcsRUFBRSxNQUFNLEVBQUUsTUFBTSx3QkFBd0IsQ0FBQztBQUU1RCxNQUFNLE1BQU0sR0FBRyxHQUFHLENBQUMsTUFBTSxDQUFDLDJCQUEyQixDQUFDLENBQUM7QUFFdkQsMERBQTBEO0FBQzFELE1BQU0sY0FBYyxHQUFHLFlBQVksQ0FBQztBQUVwQyw0RUFBNEU7QUFDNUUsTUFBTSxnQkFBZ0IsR0FBRyxvQkFBb0IsQ0FBQztBQTJDOUMsNkVBQTZFO0FBRTdFLGlHQUFpRztBQUNqRyxNQUFNLFVBQVUsZ0NBQWdDLENBQzlDLE1BQTJDO0lBRTNDLElBQUksTUFBTSxLQUFLLFNBQVMsSUFBSSxNQUFNLEtBQUssS0FBSztRQUFFLE9BQU8sU0FBUyxDQUFDO0lBQy9ELElBQUksTUFBTSxLQUFLLElBQUk7UUFBRSxPQUFPLEVBQUUsSUFBSSxFQUFFLFNBQVMsRUFBRSxDQUFDO0lBQ2hELElBQUksTUFBTSxLQUFLLE1BQU07UUFBRSxPQUFPLEVBQUUsSUFBSSxFQUFFLE1BQU0sRUFBRSxDQUFDO0lBQy9DLE9BQU8sRUFBRSxJQUFJLEVBQUUsTUFBTSxDQUFDLElBQUksSUFBSSxNQUFNLEVBQUUsR0FBRyxNQUFNLEVBQUUsQ0FBQztBQUNwRCxDQUFDO0FBRUQ7Ozs7Ozs7OztHQVNHO0FBQ0gsTUFBTSxDQUFDLEtBQUssVUFBVSwwQkFBMEIsQ0FD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ekMsTUFBTSxFQUFFLFNBQVM7UUFDakIsTUFBTSxFQUFFLFNBQVM7UUFDakIsS0FBSyxFQUFFLEtBQUs7S0FDYixDQUFDLENBQUM7SUFDSCxJQUFJLE1BQU0sQ0FBQyxRQUFRLEtBQUssQ0FBQyxFQUFFLENBQUM7UUFDMUIsTUFBTSxJQUFJLEtBQUssQ0FDYiwyQ0FBMkMsTUFBTSxDQUFDLFFBQVEsTUFBTSxNQUFNLENBQUMsTUFBTSxJQUFJLE1BQU0sQ0FBQyxNQUFNLEVBQUUsQ0FDakcsQ0FBQztJQUNKLENBQUM7QUFDSCxDQUFDIiwic291cmNlc0NvbnRlbnQiOlsiLyoqXG4gKiBTdGFydHVwIHByb3Zpc2lvbmluZyBvZiBEYXRhYnJpY2tzIEFJIFRvb2xzIHNraWxscyBmb3IgYSBNYXN0cmEgd29ya3NwYWNlLlxuICpcbiAqIERhdGFicmlja3MgQUkgVG9vbHMgKGBkYXRhYnJpY2tzIGFpdG9vbHNgKSBhcmUgRGF0YWJyaWNrcy1vd25lZCBBZ2VudC1Ta2lsbFxuICogKGBTS0lMTC5tZGApIHRyZWVzIGluc3RhbGxlZCBhbmQga2VwdCB1cCB0byBkYXRlIHRocm91Z2ggdGhlIERhdGFicmlja3MgQ0xJLlxuICoge0BsaW5rIHByb3Zpc2lvbkRhdGFicmlja3NBSVRvb2xzfSBzaGVsbHMgb3V0IHRvIHRoZSBDTElcbiAqIChgZGF0YWJyaWNrcyBhaXRvb2xzIGluc3RhbGwgLS1wYXRoIDxkaXI+YCkgdG8gbWF0ZXJpYWxpemUgYSByZXNvbHZlZCxcbiAqIGFnZW50LWFnbm9zdGljIHNraWxsIHNldCBpbnRvIGEgZGlyZWN0b3J5LCB0aGVuIGhhbmRzIGl0IHRvIE1hc3RyYSBhcyBhblxuICogZXh0cmEgTE9DQUwgc2tpbGwgc2NhbiBwYXRoIC0gc28gYW4gYWdlbnQgZ2V0cyBmaXJzdC1jbGFzcyBEYXRhYnJpY2tzIHNraWxsc1xuICogd2l0aG91dCBhbnlvbmUgaGFuZC1jb3B5aW5nIGBTS0lMTC5tZGAgZmlsZXMuIEl0IG5ldmVyIHJlaW1wbGVtZW50cyB0aGUgQ0xJJ3NcbiAqIHNvdXJjaW5nOyB0aGUgQ0xJIGlzIHRoZSBzb3VyY2Ugb2YgdHJ1dGguXG4gKlxuICogVGhlIG9wdGlvbiBpcyBgZmFsc2UgfCB0cnVlIHwgXCJhdXRvXCIgfCBEYXRhYnJpY2tzQUlUb29sc09wdGlvbnNgOlxuICpcbiAqIC0gYGZhbHNlYCAoZGVmYXVsdCkgLSBvZmYuXG4gKiAtIGBcImF1dG9cImAgLSBydW4gdGhlIENMSSB3aGVuIGl0J3MgaW5zdGFsbGVkOyBpZiB0aGUgYGRhdGFicmlja3NgIENMSSBpc24ndFxuICogICBhdmFpbGFibGUsIGxvZyBhbmQgbW92ZSBvbiAobmV2ZXIgZmFpbHMgc3RhcnR1cCkuIFRoZSBcIm9uIGlmIHRoZSBDTEkgaXNcbiAqICAgYXJvdW5kXCIgZGVmYXVsdC5cbiAqIC0gYHRydWVgIChgXCJyZXF1aXJlXCJgKSAtIHJlcXVpcmUgdGhlIENMSTogZmFpbCBzdGFydHVwIChzdWJqZWN0IHRvXG4gKiAgIGBmYWlsT25FcnJvcmApIHdoZW4gaXQgaXNuJ3QgYXZhaWxhYmxlIG9yIHByb2R1Y2VzIG5vdGhpbmcuXG4gKiAtIGFuIG9wdGlvbnMgYmFnIGZvciBhIHNwZWNpZmljIGBza2lsbHNgIHN1YnNldCwgYGV4cGVyaW1lbnRhbGAgc2tpbGxzLCBhblxuICogICBleHBsaWNpdCBvdXRwdXQgYHBhdGhgLCBvciBhIGN1c3RvbSBgY2xpYCBwYXRoLlxuICpcbiAqIEBtb2R1bGVcbiAqL1xuXG5pbXBvcnQgeyBleGlzdHNTeW5jIH0gZnJvbSBcIm5vZGU6ZnNcIjtcbmltcG9ydCB7IGV4ZWMgfSBmcm9tIFwiQGRieC10b29scy9jb3JlXCI7XG5pbXBvcnQgeyBsb2NhbEZTIH0gZnJvbSBcIkBkYngtdG9vbHMvZnNcIjtcbmltcG9ydCB7IGVycm9yLCBsb2csIHN0cmluZyB9IGZyb20gXCJAZGJ4LXRvb2xzL3NoYXJlZC1jb3JlXCI7XG5cbmNvbnN0IGxvZ2dlciA9IGxvZy5sb2dnZXIoXCJtYXN0cmEvZGF0YWJyaWNrcy1haXRvb2xzXCIpO1xuXG4vKiogVGhlIERhdGFicmlja3MgQ0xJIGJpbmFyeSBuYW1lOyByZXNvbHZlZCBvbiBgUEFUSGAuICovXG5jb25zdCBEQVRBQlJJQ0tTX0NMSSA9IFwiZGF0YWJyaWNrc1wiO1xuXG4vKiogU3RhYmxlIHRlbXAgZGlyZWN0b3J5IHRoZSBDTEkncyBza2lsbCB0cmVlIGlzIHJlYnVpbHQgaW50byBlYWNoIGJvb3QuICovXG5jb25zdCBTS0lMTFNfQ0FDSEVfRElSID0gXCJkYXRhYnJpY2tzLWFpdG9vbHNcIjtcblxuLyogLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0gdHlwZXMgLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0gKi9cblxuLyoqIEhvdyBhZ2dyZXNzaXZlbHkgdG8gZW5hYmxlIERhdGFicmlja3MgQUkgVG9vbHMgc2tpbGxzLiAqL1xuZXhwb3J0IHR5cGUgRGF0YWJyaWNrc0FJVG9vbHNNb2RlID0gXCJhdXRvXCIgfCBcInJlcXVpcmVcIjtcblxuLyoqIE9wdGlvbnMgZm9yIHtAbGluayBwcm92aXNpb25EYXRhYnJpY2tzQUlUb29sc30uICovXG5leHBvcnQgaW50ZXJmYWNlIERhdGFicmlja3NBSVRvb2xzT3B0aW9ucyB7XG4gIC8qKlxuICAgKiBgXCJhdXRvXCJgIChkZWZhdWx0KSBydW5zIHRoZSBDTEkgd2hlbiBpbnN0YWxsZWQgYW5kIGxvZ3MtYW5kLWNvbnRpbnVlcyB3aGVuXG4gICAqIGl0IGlzbid0OyBgXCJyZXF1aXJlXCJgIGZhaWxzIHN0YXJ0dXAgd2hlbiB0aGUgQ0xJIGNhbid0IHN1cHBseSBza2lsbHMuXG4gICAqL1xuICBtb2RlPzogRGF0YWJyaWNrc0FJVG9vbHNNb2RlO1xuICAvKiogSW5zdGFsbCBvbmx5IHRoZXNlIHNraWxsIG5hbWVzIChjb21tYS9zcGFjZSBsaXN0IG9yIGFycmF5KS4gKi9cbiAgc2tpbGxzPzogc3RyaW5nIHwgc3RyaW5nW107XG4gIC8qKiBJbmNsdWRlIGV4cGVyaW1lbnRhbCBza2lsbHMgd2hlbiB0aGUgQ0xJIGhhcyB0byBmZXRjaC4gKi9cbiAgZXhwZXJpbWVudGFsPzogYm9vbGVhbjtcbiAgLyoqXG4gICAqIEV4cGxpY2l0IGRpcmVjdG9yeSB0byBtYXRlcmlhbGl6ZSBza2lsbHMgaW50byB3aXRoIHRoZSBDTEkuIERlZmF1bHRzIHRvIGFcbiAgICogZnJlc2gge0BsaW5rIGxvY2FsRlMudG1wRlN9IHJvb3QuXG4gICAqL1xuICBwYXRoPzogc3RyaW5nO1xuICAvKiogQWJzb2x1dGUgcGF0aCB0byB0aGUgYGRhdGFicmlja3NgIENMSS4gRGVmYXVsdHMgdG8gYGRhdGFicmlja3NgIG9uIFBBVEguICovXG4gIGNsaT86IHN0cmluZztcbiAgLyoqXG4gICAqIEZhaWwgYXBwIHN0YXJ0dXAgd2hlbiByZXF1aXJlZCBza2lsbHMgY2FuJ3QgYmUgcHJvdmlzaW9uZWQuIERlZmF1bHRzIHRvXG4gICAqIGB0cnVlYCBmb3IgYG1vZGU6IFwicmVxdWlyZVwiYCBhbmQgYGZhbHNlYCBmb3IgYFwiYXV0b1wiYC5cbiAgICovXG4gIGZhaWxPbkVycm9yPzogYm9vbGVhbjtcbn1cblxuLyoqIFRoZSBgZGF0YWJyaWNrc0FJVG9vbHNgIGNvbmZpZyBvcHRpb246IGEgdG9nZ2xlLCBgXCJhdXRvXCJgLCBvciBhbiBvcHRpb25zIGJhZy4gKi9cbmV4cG9ydCB0eXBlIERhdGFicmlja3NBSVRvb2xzT3B0aW9uID0gYm9vbGVhbiB8IFwiYXV0b1wiIHwgRGF0YWJyaWNrc0FJVG9vbHNPcHRpb25zO1xuXG4vKiogV2hhdCB7QGxpbmsgcHJvdmlzaW9uRGF0YWJyaWNrc0FJVG9vbHN9IHJlc29sdmVkLiAqL1xuZXhwb3J0IGludGVyZmFjZSBQcm92aXNpb25lZERhdGFicmlja3NBSVRvb2xzIHtcbiAgLyoqIEV4dHJhIExPQ0FMIHNraWxsIHNjYW4gcGF0aHMgdG8gaGFuZCBNYXN0cmEuIEVtcHR5IHdoZW4gZGlzYWJsZWQvdW5yZXNvbHZlZC4gKi9cbiAgbG9jYWxTa2lsbFBhdGhzOiBzdHJpbmdbXTtcbiAgLyoqIEhvdyB0aGUgc2tpbGxzIHdlcmUgc291cmNlZCwgZm9yIGxvZ2dpbmcuICovXG4gIHNvdXJjZT86IFwiY2xpXCI7XG59XG5cbi8qIC0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0gaGVscGVycyAtLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tICovXG5cbi8qKiBOb3JtYWxpemUgdGhlIGBkYXRhYnJpY2tzQUlUb29sc2Agb3B0aW9uIGludG8gYSBmbGF0IG9wdGlvbnMgYmFnLCBvciBgdW5kZWZpbmVkYCB3aGVuIG9mZi4gKi9cbmV4cG9ydCBmdW5jdGlvbiBub3JtYWxpemVEYXRhYnJpY2tzQUlUb29sc09wdGlvbihcbiAgb3B0aW9uOiBEYXRhYnJpY2tzQUlUb29sc09wdGlvbiB8IHVuZGVmaW5lZCxcbik6IERhdGFicmlja3NBSVRvb2xzT3B0aW9ucyB8IHVuZGVmaW5lZCB7XG4gIGlmIChvcHRpb24gPT09IHVuZGVmaW5lZCB8fCBvcHRpb24gPT09IGZhbHNlKSByZXR1cm4gdW5kZWZpbmVkO1xuICBpZiAob3B0aW9uID09PSB0cnVlKSByZXR1cm4geyBtb2RlOiBcInJlcXVpcmVcIiB9O1xuICBpZiAob3B0aW9uID09PSBcImF1dG9cIikgcmV0dXJuIHsgbW9kZTogXCJhdXRvXCIgfTtcbiAgcmV0dXJuIHsgbW9kZTogb3B0aW9uLm1vZGUgPz8gXCJhdXRvXCIsIC4uLm9wdGlvbiB9O1xufVxuXG4vKipcbiAqIE1hdGVyaWFsaXplIERhdGFicmlja3MgQUkgVG9vbHMgc2tpbGxzIGF0IGFwcCBzdGFydHVwIGFuZCByZXR1cm4gdGhlbSBhc1xuICogZXh0cmEgbG9jYWwgc2tpbGwgc2NhbiBwYXRocy5cbiAqXG4gKiBSZXNvbHV0aW9uOiByZXVzZSB0aGUgaW5zdGFsbGVkIGdsb2JhbCB0cmVlIHdoZW4gcHJlc2VudCAobm8gQ0xJIGNhbGwpIHVubGVzc1xuICogYHJlZnJlc2hgL2Bza2lsbHNgL2BwYXRoYCBhc2sgZm9yIGEgZnJlc2ggQ0xJIG1hdGVyaWFsaXphdGlvbjsgb3RoZXJ3aXNlIHJ1blxuICogYGRhdGFicmlja3MgYWl0b29scyBpbnN0YWxsIC0tcGF0aCA8ZGlyPiAtLXNraWxscy1vbmx5YCB3aGVuIHRoZSBDTEkgcmVzb2x2ZXMuXG4gKiBBIGBcInJlcXVpcmVcImAgcHJvdmlzaW9uIHRoYXQgeWllbGRzIG5vdGhpbmcgZmFpbHMgc3RhcnR1cCB1bmxlc3NcbiAqIGBmYWlsT25FcnJvcjogZmFsc2VgLlxuICovXG5leHBvcnQgYXN5bmMgZnVuY3Rpb24gcHJvdmlzaW9uRGF0YWJyaWNrc0FJVG9vbHMoXG4gIG9wdGlvbjogRGF0YWJyaWNrc0FJVG9vbHNPcHRpb24gfCB1bmRlZmluZWQsXG4pOiBQcm9taXNlPFByb3Zpc2lvbmVkRGF0YWJyaWNrc0FJVG9vbHM+IHtcbiAgY29uc3Qgb3B0aW9ucyA9IG5vcm1hbGl6ZURhdGFicmlja3NBSVRvb2xzT3B0aW9uKG9wdGlvbik7XG4gIGNvbnN0IGVtcHR5OiBQcm92aXNpb25lZERhdGFicmlja3NBSVRvb2xzID0geyBsb2NhbFNraWxsUGF0aHM6IFtdIH07XG4gIGlmICghb3B0aW9ucykgcmV0dXJuIGVtcHR5O1xuXG4gIGNvbnN0IG1vZGUgPSBvcHRpb25zLm1vZGUgPz8gXCJhdXRvXCI7XG4gIGNvbnN0IGZhaWxPbkVycm9yID0gb3B0aW9ucy5mYWlsT25FcnJvciA/PyBtb2RlID09PSBcInJlcXVpcmVcIjtcblxuICB0cnkge1xuICAgIC8vIFRoZSBgZGF0YWJyaWNrc2AgQ0xJIGlzIHRoZSBzb3VyY2Ugb2YgdHJ1dGguIFdoZW4gaXQncyByZXNvbHZhYmxlLCBydW5cbiAgICAvLyBgZGF0YWJyaWNrcyBhaXRvb2xzIGluc3RhbGwgLS1wYXRoIDxkaXI+YCB0byBtYXRlcmlhbGl6ZSBhIGZyZXNoLFxuICAgIC8vIGFnZW50LWFnbm9zdGljIHNraWxsIHNldCBpbnRvIGEgZGlyIE1hc3RyYSBjYW4gc2Nhbi5cbiAgICBjb25zdCBjbGlQYXRoID0gYXdhaXQgcmVzb2x2ZUNsaShvcHRpb25zLmNsaSk7XG4gICAgaWYgKGNsaVBhdGgpIHtcbiAgICAgIGNvbnN0IGRpciA9IGF3YWl0IG1hdGVyaWFsaXplVmlhQ2xpKGNsaVBhdGgsIG9wdGlvbnMpO1xuICAgICAgaWYgKGRpcikgcmV0dXJuIHsgbG9jYWxTa2lsbFBhdGhzOiBbZGlyXSwgc291cmNlOiBcImNsaVwiIH07XG4gICAgfVxuXG4gICAgLy8gQ0xJIG5vdCBpbnN0YWxsZWQgKG9yIHByb2R1Y2VkIG5vdGhpbmcpLiBcImF1dG9cIiBsb2dzIGFuZCBtb3ZlcyBvbjtcbiAgICAvLyBcInJlcXVpcmVcIiBmYWlscyBzdGFydHVwIHVubGVzcyBgZmFpbE9uRXJyb3I6IGZhbHNlYC5cbiAgICBpZiAoZmFpbE9uRXJyb3IpIHtcbiAgICAgIHRocm93IG5ldyBFcnJvcihcbiAgICAgICAgYERhdGFicmlja3MgQUkgVG9vbHMgcmVxdWlyZWQgYnV0IHRoZSBcIiR7REFUQUJSSUNLU19DTEl9XCIgQ0xJIGlzIG5vdCBhdmFpbGFibGUuIEluc3RhbGwgdGhlIERhdGFicmlja3MgQ0xJIChcXGBkYXRhYnJpY2tzIGFpdG9vbHMgaW5zdGFsbFxcYCksIG9yIHNldCBkYXRhYnJpY2tzQUlUb29sczogXCJhdXRvXCIuYCxcbiAgICAgICk7XG4gICAgfVxuICAgIGxvZ2dlci5pbmZvKFwiZGF0YWJyaWNrcyBDTEkgbm90IGF2YWlsYWJsZTsgc2tpcHBpbmcgQUkgVG9vbHMgc2tpbGxzXCIpO1xuICAgIHJldHVybiBlbXB0eTtcbiAgfSBjYXRjaCAoZXJyKSB7XG4gICAgaWYgKGZhaWxPbkVycm9yKSB7XG4gICAgICB0aHJvdyBuZXcgRXJyb3IoXG4gICAgICAgIGBmYWlsZWQgdG8gcHJvdmlzaW9uIERhdGFicmlja3MgQUkgVG9vbHMgc2tpbGxzOiAke2Vycm9yLmVycm9yTWVzc2FnZShlcnIpfWAsXG4gICAgICAgIHsgY2F1c2U6IGVycm9yLnRvRXJyb3IoZXJyKSB9LFxuICAgICAgKTtcbiAgICB9XG4gICAgbG9nZ2VyLndhcm4oXCJza2lwcGVkXCIsIHsgZXJyb3I6IGVycm9yLmVycm9yTWVzc2FnZShlcnIpIH0pO1xuICAgIHJldHVybiBlbXB0eTtcbiAgfVxufVxuXG4vKiogUmVzb2x2ZSB0aGUgYGRhdGFicmlja3NgIENMSSwgcmV0dXJuaW5nIGl0cyBpbnZvY2F0aW9uIHBhdGggb3IgYHVuZGVmaW5lZGAuICovXG5hc3luYyBmdW5jdGlvbiByZXNvbHZlQ2xpKGNsaTogc3RyaW5nIHwgdW5kZWZpbmVkKTogUHJvbWlzZTxzdHJpbmcgfCB1bmRlZmluZWQ+IHtcbiAgY29uc3QgY29tbWFuZCA9IHN0cmluZy50cmltVG9OdWxsKGNsaSkgPz8gREFUQUJSSUNLU19DTEk7XG4gIHRyeSB7XG4gICAgY29uc3QgcmVzdWx0ID0gYXdhaXQgZXhlYy5zcGF3bihjb21tYW5kLCBbXCJhaXRvb2xzXCIsIFwidmVyc2lvblwiXSwge1xuICAgICAgc3Rkb3V0OiBcImNhcHR1cmVcIixcbiAgICAgIHN0ZGVycjogXCJjYXB0dXJlXCIsXG4gICAgICBjaGVjazogZmFsc2UsXG4gICAgfSk7XG4gICAgaWYgKHJlc3VsdC5leGl0Q29kZSA9PT0gMCkgcmV0dXJuIGNvbW1hbmQ7XG4gIH0gY2F0Y2gge1xuICAgIC8vIENMSSBhYnNlbnQgb3IgYGFpdG9vbHNgIHVuc3VwcG9ydGVkIC0gY2FsbGVyIGZhbGxzIGJhY2sgLyBmYWlscy5cbiAgfVxuICByZXR1cm4gdW5kZWZpbmVkO1xufVxuXG4vKipcbiAqIFJ1biBgZGF0YWJyaWNrcyBhaXRvb2xzIGluc3RhbGwgLS1wYXRoIDxkaXI+YCB0byBtYXRlcmlhbGl6ZSBhIHJlc29sdmVkLFxuICogYWdlbnQtYWdub3N0aWMgc2tpbGwgc2V0IGludG8gYSBkaXJlY3RvcnkgKHdyaXRlcyBgPG5hbWU+L1NLSUxMLm1kYCB0cmVlcyxcbiAqIG5vIGFnZW50cywgbm8gc3RhdGUpIE1hc3RyYSBjYW4gdGhlbiBzY2FuLlxuICovXG5hc3luYyBmdW5jdGlvbiBtYXRlcmlhbGl6ZVZpYUNsaShcbiAgY2xpOiBzdHJpbmcsXG4gIG9wdGlvbnM6IERhdGFicmlja3NBSVRvb2xzT3B0aW9ucyxcbik6IFByb21pc2U8c3RyaW5nIHwgdW5kZWZpbmVkPiB7XG4gIGNvbnN0IGV4cGxpY2l0ID0gc3RyaW5nLnRyaW1Ub051bGwob3B0aW9ucy5wYXRoKTtcbiAgbGV0IGRpcjogc3RyaW5nO1xuXG4gIGlmIChleHBsaWNpdCkge1xuICAgIC8vIEFuIGV4cGxpY2l0IHBhdGggaXMgdGhlIGNhbGxlcidzIHRvIG93biAtIGluc3RhbGwgc3RyYWlnaHQgaW50byBpdC5cbiAgICBhd2FpdCBpbnN0YWxsU2tpbGxzKGNsaSwgb3B0aW9ucywgZXhwbGljaXQpO1xuICAgIGRpciA9IGV4cGxpY2l0O1xuICB9IGVsc2Uge1xuICAgIC8vIFRoZSByZXNvbHZlZCBza2lsbCBzZXQgaXMgdGhlIHNhbWUgb24gYWxtb3N0IGV2ZXJ5IGJvb3QsIHNvIHJlYnVpbGQgT05FXG4gICAgLy8gc3RhYmxlIHRlbXAgdHJlZSByYXRoZXIgdGhhbiBsZWF2aW5nIGEgZnJlc2ggc2NyYXRjaCBkaXIgYmVoaW5kIGVhY2hcbiAgICAvLyB0aW1lLiBUaGUgQ0xJIHdyaXRlcyBpbnRvIGEgdGhyb3dhd2F5IHJvb3QgdGhhdCBvbmx5IHJlcGxhY2VzIHRoZSBzdGFibGVcbiAgICAvLyBvbmUgb24gc3VjY2Vzcywgc28gYSBmYWlsZWQgaW5zdGFsbCBrZWVwcyB0aGUgcHJldmlvdXMgc2tpbGxzIHVzYWJsZS5cbiAgICBjb25zdCBzdGFibGUgPSBhd2FpdCBsb2NhbEZTLnJlYnVpbGRGUyhTS0lMTFNfQ0FDSEVfRElSLCAoc2NyYXRjaCkgPT5cbiAgICAgIGluc3RhbGxTa2lsbHMoY2xpLCBvcHRpb25zLCBzY3JhdGNoLnJvb3QpLFxuICAgICk7XG4gICAgZGlyID0gc3RhYmxlLnJvb3Q7XG4gIH1cblxuICBpZiAoIWV4aXN0c1N5bmMoZGlyKSkgcmV0dXJuIHVuZGVmaW5lZDtcbiAgbG9nZ2VyLmRlYnVnKFwibWF0ZXJpYWxpemVkIGFpdG9vbHMgc2tpbGxzIHZpYSBDTElcIiwge1xuICAgIHBhdGg6IGRpcixcbiAgICBza2lsbHM6IHN0cmluZy5wYXJzZUxpc3Qob3B0aW9ucy5za2lsbHMpLFxuICB9KTtcbiAgcmV0dXJuIGRpcjtcbn1cblxuLyoqIFJ1biBgZGF0YWJyaWNrcyBhaXRvb2xzIGluc3RhbGwgLS1wYXRoIDxkaXI+YCwgZmFpbGluZyBsb3VkbHkgb24gYSBub24temVybyBleGl0LiAqL1xuYXN5bmMgZnVuY3Rpb24gaW5zdGFsbFNraWxscyhcbiAgY2xpOiBzdHJpbmcsXG4gIG9wdGlvbnM6IERhdGFicmlja3NBSVRvb2xzT3B0aW9ucyxcbiAgZGlyOiBzdHJpbmcsXG4pOiBQcm9taXNlPHZvaWQ+IHtcbiAgY29uc3QgYXJncyA9IFtcImFpdG9vbHNcIiwgXCJpbnN0YWxsXCIsIFwiLS1wYXRoXCIsIGRpcl07XG4gIGNvbnN0IHNraWxscyA9IHN0cmluZy5wYXJzZUxpc3Qob3B0aW9ucy5za2lsbHMpO1xuICBpZiAoc2tpbGxzLmxlbmd0aCA+IDApIGFyZ3MucHVzaChcIi0tc2tpbGxzXCIsIHNraWxscy5qb2luKFwiLFwiKSk7XG4gIGlmIChvcHRpb25zLmV4cGVyaW1lbnRhbCkgYXJncy5wdXNoKFwiLS1leHBlcmltZW50YWxcIik7XG5cbiAgY29uc3QgcmVzdWx0ID0gYXdhaXQgZXhlYy5zcGF3bihjbGksIGFyZ3MsIHtcbiAgICBzdGRvdXQ6IFwiY2FwdHVyZVwiLFxuICAgIHN0ZGVycjogXCJjYXB0dXJlXCIsXG4gICAgY2hlY2s6IGZhbHNlLFxuICB9KTtcbiAgaWYgKHJlc3VsdC5leGl0Q29kZSAhPT0gMCkge1xuICAgIHRocm93IG5ldyBFcnJvcihcbiAgICAgIGBkYXRhYnJpY2tzIGFpdG9vbHMgaW5zdGFsbCBmYWlsZWQgKGV4aXQgJHtyZXN1bHQuZXhpdENvZGV9KTogJHtyZXN1bHQuc3RkZXJyIHx8IHJlc3VsdC5zdGRvdXR9YCxcbiAgICApO1xuICB9XG59XG4iXX0=