@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.
- package/README.md +29 -32
- package/index.ts +2 -3
- package/lib/index.d.ts +2 -3
- package/lib/index.js +2 -2
- package/lib/src/config.d.ts +0 -21
- package/lib/src/config.js +2 -6
- package/lib/src/plugin.js +2 -14
- package/lib/src/remote-skills.d.ts +41 -20
- package/lib/src/remote-skills.js +127 -32
- package/lib/tsconfig.tsbuildinfo +1 -1
- package/package.json +13 -13
- package/src/config.ts +1 -28
- package/src/plugin.ts +1 -14
- package/src/remote-skills.ts +191 -52
- package/lib/src/databricks-aitools.d.ts +0 -73
- package/lib/src/databricks-aitools.js +0 -152
- package/src/databricks-aitools.ts +0 -216
package/src/remote-skills.ts
CHANGED
|
@@ -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 -
|
|
5
|
-
* GitHub `owner/repo`, any git / GitLab URL,
|
|
6
|
-
* optional per-source policy.
|
|
7
|
-
* source into a local
|
|
8
|
-
* to hand Mastra as
|
|
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
|
|
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
|
-
*
|
|
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
|
|
77
|
-
* `owner/repo` shorthand, a full GitHub / GitLab / git
|
|
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
|
|
82
|
-
source:
|
|
83
|
-
/** Install only these skill names from the source
|
|
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:
|
|
96
|
-
* {@link RemoteSkillSourceOptions} with
|
|
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 =
|
|
140
|
+
export type RemoteSkillSource = net.UrlLike | AiToolsSource | RemoteSkillSourceOptions;
|
|
99
141
|
|
|
100
142
|
/**
|
|
101
|
-
* The workspace `remoteSkills` option
|
|
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
|
|
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
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
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
|
|
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
|
-
/**
|
|
168
|
-
function
|
|
169
|
-
|
|
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:
|
|
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:
|
|
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:
|
|
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
|
|
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=
|