@dbx-tools/appkit-mastra 0.6.9 → 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/src/workspaces.ts CHANGED
@@ -7,10 +7,15 @@
7
7
  * contributions merge extra filesystems and skill scan roots; built-in
8
8
  * Assistant skill trees are toggled with `assistantSkills` (on by default).
9
9
  *
10
+ * Databricks mounts use `@dbx-tools/databricks` {@link DatabricksFileSystem}
11
+ * wrapped by {@link filesystems}; missing roots fall back to
12
+ * {@link scratchFilesystem}.
13
+ *
10
14
  * @module
11
15
  */
12
16
 
13
17
  import type { WorkspaceClient } from "@databricks/sdk-experimental";
18
+ import { DatabricksFileSystem } from "@dbx-tools/databricks";
14
19
  import { log, string, token } from "@dbx-tools/shared-core";
15
20
  import type { RequestContext } from "@mastra/core/request-context";
16
21
  import {
@@ -22,7 +27,7 @@ import {
22
27
  } from "@mastra/core/workspace";
23
28
 
24
29
  import { MASTRA_SCOPES_KEY, MASTRA_USER_EMAIL_KEY, MASTRA_USER_KEY, type User } from "./config.ts";
25
- import { DatabricksWorkspaceFilesystem, emptyFilesystem } from "./filesystems.ts";
30
+ import { scratchFilesystem, filesystems } from "./filesystems.ts";
26
31
 
27
32
  /* ------------------------------ constants ------------------------------ */
28
33
 
@@ -175,8 +180,10 @@ function hasWorkspaceFileScope(requestContext: RequestContext | undefined): bool
175
180
  * set. Returns empty mounts when the OBO user or client is missing.
176
181
  * Mastra owns filesystem initialization.
177
182
  */
178
- function resolveAssistantSkillsMounts(context: WorkspaceMountContext): WorkspaceMountContribution {
179
- const mounts: Record<string, DatabricksWorkspaceFilesystem> = {};
183
+ async function resolveAssistantSkillsMounts(
184
+ context: WorkspaceMountContext,
185
+ ): Promise<WorkspaceMountContribution> {
186
+ const mounts: Record<string, WorkspaceFilesystem> = {};
180
187
  const requestContext = context.requestContext;
181
188
 
182
189
  if (!shouldMountAssistantSkills(requestContext)) {
@@ -189,7 +196,7 @@ function resolveAssistantSkillsMounts(context: WorkspaceMountContext): Workspace
189
196
  }
190
197
 
191
198
  const user = requestContext!.get(MASTRA_USER_KEY) as User | undefined;
192
- const client = user?.executionContext.client;
199
+ const client = user?.executionContext.client as WorkspaceClient | undefined;
193
200
  if (!client) {
194
201
  logger.debug("assistant-skills:skipped", {
195
202
  reason: "missing-obo-client",
@@ -198,14 +205,14 @@ function resolveAssistantSkillsMounts(context: WorkspaceMountContext): Workspace
198
205
  return { mounts, skillPaths: [] };
199
206
  }
200
207
 
201
- mounts[ASSISTANT_WORKSPACE_SKILLS_MOUNT] = databricksFilesystem(
208
+ mounts[ASSISTANT_WORKSPACE_SKILLS_MOUNT] = await databricksFilesystem(
202
209
  client,
203
210
  ASSISTANT_SHARED_SKILLS_PATH,
204
211
  );
205
212
 
206
213
  const email = resolveScopedEmail(requestContext);
207
214
  if (email) {
208
- mounts[ASSISTANT_USER_SKILLS_MOUNT] = databricksFilesystem(
215
+ mounts[ASSISTANT_USER_SKILLS_MOUNT] = await databricksFilesystem(
209
216
  client,
210
217
  userAssistantSkillsPath(email),
211
218
  false,
@@ -281,17 +288,35 @@ function resolveScopedEmail(requestContext: RequestContext | undefined): string
281
288
  return email?.trim() || undefined;
282
289
  }
283
290
 
284
- /** Construct a read-only {@link DatabricksWorkspaceFilesystem} for `basePath`. */
285
- function databricksFilesystem(
291
+ /**
292
+ * Wrap a {@link DatabricksFileSystem} as a Mastra filesystem. When the root is
293
+ * missing (and {@link readOnly} so we will not create it), fall back to
294
+ * {@link scratchFilesystem} so skill scans still have a writable local mount.
295
+ */
296
+ async function databricksFilesystem(
286
297
  client: WorkspaceClient,
287
- basePath: string,
298
+ root: string,
288
299
  readOnly: boolean = true,
289
- ): DatabricksWorkspaceFilesystem {
290
- return new DatabricksWorkspaceFilesystem({
300
+ ): Promise<WorkspaceFilesystem> {
301
+ const fs = new DatabricksFileSystem({
291
302
  client,
292
- basePath,
303
+ root,
293
304
  readOnly,
305
+ createRoot: !readOnly,
294
306
  });
307
+ try {
308
+ await fs.init();
309
+ if (await fs.exists(".")) {
310
+ return filesystems(fs, { readOnly });
311
+ }
312
+ } catch (err) {
313
+ logger.debug("databricks-mount:scratch-fallback", {
314
+ root,
315
+ readOnly,
316
+ error: err instanceof Error ? err.message : String(err),
317
+ });
318
+ }
319
+ return scratchFilesystem();
295
320
  }
296
321
 
297
322
  /**
@@ -329,8 +354,8 @@ async function resolveWorkspaceContribution(
329
354
  /**
330
355
  * Dynamic filesystem resolver passed to Mastra {@link Workspace}.
331
356
  *
332
- * Returns a {@link CompositeFilesystem} when any mount resolved; otherwise
333
- * {@link emptyFilesystem}.
357
+ * Returns a {@link CompositeFilesystem} when any mount resolved; otherwise a
358
+ * fresh {@link scratchFilesystem} so Mastra always has a writable local root.
334
359
  */
335
360
  async function resolveWorkspaceFilesystem(
336
361
  resolvers: WorkspaceMountResolver[],
@@ -339,10 +364,10 @@ async function resolveWorkspaceFilesystem(
339
364
  const { mounts } = await resolveWorkspaceContribution(resolvers, context);
340
365
  const mountKeys = Object.keys(mounts);
341
366
  if (mountKeys.length === 0) {
342
- logger.debug("filesystem:empty", {
367
+ logger.debug("filesystem:scratch", {
343
368
  hasRequestContext: Boolean(context.requestContext),
344
369
  });
345
- return emptyFilesystem();
370
+ return scratchFilesystem();
346
371
  }
347
372
  logger.debug("filesystem:composite", { mountKeys });
348
373
  return new CompositeFilesystem({ mounts });
@@ -1,83 +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
- * The CLI installs them globally under `~/.databricks/aitools/skills/<name>/`
7
- * (with a `.state.json` manifest) and can also write a resolved, agent-agnostic
8
- * copy to any directory with `databricks aitools install --path <dir>
9
- * --skills-only`.
10
- *
11
- * {@link provisionDatabricksAITools} folds those skills into a Mastra workspace
12
- * as extra LOCAL skill scan paths so an agent can use them without anyone
13
- * hand-copying `SKILL.md` files. It never reimplements the CLI's sourcing: it
14
- * either points Mastra at the already-installed global tree, or shells out to
15
- * the CLI to materialize a curated subset into a temp dir.
16
- *
17
- * The option is `false | true | "auto" | DatabricksAIToolsOptions`:
18
- *
19
- * - `false` (default) - off.
20
- * - `"auto"` - enable only when the global tree already exists OR the
21
- * `databricks` CLI is resolvable; otherwise silently no-op. The "on if the
22
- * CLI is around" default.
23
- * - `true` - required: fail startup (subject to `failOnError`) when neither the
24
- * installed tree nor the CLI can supply skills.
25
- * - an options bag for a specific `skills` subset, `experimental` skills, a
26
- * `refresh` that re-runs the CLI even when a tree exists, or an explicit
27
- * `path`.
28
- *
29
- * @module
30
- */
31
- /** How aggressively to enable Databricks AI Tools skills. */
32
- export type DatabricksAIToolsMode = "auto" | "require";
33
- /** Options for {@link provisionDatabricksAITools}. */
34
- export interface DatabricksAIToolsOptions {
35
- /**
36
- * `"auto"` (default) enables only when the installed tree exists or the CLI
37
- * is resolvable; `"require"` fails startup when neither can supply skills.
38
- */
39
- mode?: DatabricksAIToolsMode;
40
- /** Install only these skill names (comma/space list or array). */
41
- skills?: string | string[];
42
- /** Include experimental skills when the CLI has to fetch. */
43
- experimental?: boolean;
44
- /**
45
- * Re-run the CLI to (re)materialize skills into a fresh dir even when the
46
- * global tree already exists. Defaults to `false` (reuse the installed tree).
47
- */
48
- refresh?: boolean;
49
- /**
50
- * Explicit directory to materialize skills into with the CLI. Defaults to a
51
- * temp dir. Ignored when the installed global tree is used as-is.
52
- */
53
- path?: string;
54
- /** Absolute path to the `databricks` CLI. Defaults to `databricks` on PATH. */
55
- cli?: string;
56
- /**
57
- * Fail app startup when required skills can't be provisioned. Defaults to
58
- * `true` for `mode: "require"` and `false` for `"auto"`.
59
- */
60
- failOnError?: boolean;
61
- }
62
- /** The `databricksAITools` config option: a toggle, `"auto"`, or an options bag. */
63
- export type DatabricksAIToolsOption = boolean | "auto" | DatabricksAIToolsOptions;
64
- /** What {@link provisionDatabricksAITools} resolved. */
65
- export interface ProvisionedDatabricksAITools {
66
- /** Extra LOCAL skill scan paths to hand Mastra. Empty when disabled/unresolved. */
67
- localSkillPaths: string[];
68
- /** How the skills were sourced, for logging. */
69
- source?: "installed" | "cli";
70
- }
71
- /** Normalize the `databricksAITools` option into a flat options bag, or `undefined` when off. */
72
- export declare function normalizeDatabricksAIToolsOption(option: DatabricksAIToolsOption | undefined): DatabricksAIToolsOptions | undefined;
73
- /**
74
- * Materialize Databricks AI Tools skills at app startup and return them as
75
- * extra local skill scan paths.
76
- *
77
- * Resolution: reuse the installed global tree when present (no CLI call) unless
78
- * `refresh`/`skills`/`path` ask for a fresh CLI materialization; otherwise run
79
- * `databricks aitools install --path <dir> --skills-only` when the CLI resolves.
80
- * A `"require"` provision that yields nothing fails startup unless
81
- * `failOnError: false`.
82
- */
83
- export declare function provisionDatabricksAITools(option: DatabricksAIToolsOption | undefined): Promise<ProvisionedDatabricksAITools>;
@@ -1,150 +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
- * The CLI installs them globally under `~/.databricks/aitools/skills/<name>/`
7
- * (with a `.state.json` manifest) and can also write a resolved, agent-agnostic
8
- * copy to any directory with `databricks aitools install --path <dir>
9
- * --skills-only`.
10
- *
11
- * {@link provisionDatabricksAITools} folds those skills into a Mastra workspace
12
- * as extra LOCAL skill scan paths so an agent can use them without anyone
13
- * hand-copying `SKILL.md` files. It never reimplements the CLI's sourcing: it
14
- * either points Mastra at the already-installed global tree, or shells out to
15
- * the CLI to materialize a curated subset into a temp dir.
16
- *
17
- * The option is `false | true | "auto" | DatabricksAIToolsOptions`:
18
- *
19
- * - `false` (default) - off.
20
- * - `"auto"` - enable only when the global tree already exists OR the
21
- * `databricks` CLI is resolvable; otherwise silently no-op. The "on if the
22
- * CLI is around" default.
23
- * - `true` - required: fail startup (subject to `failOnError`) when neither the
24
- * installed tree nor the CLI can supply skills.
25
- * - an options bag for a specific `skills` subset, `experimental` skills, a
26
- * `refresh` that re-runs the CLI even when a tree exists, or an explicit
27
- * `path`.
28
- *
29
- * @module
30
- */
31
- import { mkdtemp } from "node:fs/promises";
32
- import { existsSync } from "node:fs";
33
- import { homedir, tmpdir } from "node:os";
34
- import { join } from "node:path";
35
- import { spawn } from "@dbx-tools/core";
36
- import { error, log, string } from "@dbx-tools/shared-core";
37
- const logger = log.logger("mastra/databricks-aitools");
38
- /** Global skills tree the `databricks aitools` CLI installs into. */
39
- const GLOBAL_AITOOLS_SKILLS_PATH = join(homedir(), ".databricks", "aitools", "skills");
40
- /** The Databricks CLI binary name; resolved on `PATH`. */
41
- const DATABRICKS_CLI = "databricks";
42
- /* ------------------------------- helpers ------------------------------- */
43
- /** Normalize the `databricksAITools` option into a flat options bag, or `undefined` when off. */
44
- export function normalizeDatabricksAIToolsOption(option) {
45
- if (option === undefined || option === false)
46
- return undefined;
47
- if (option === true)
48
- return { mode: "require" };
49
- if (option === "auto")
50
- return { mode: "auto" };
51
- return { mode: option.mode ?? "auto", ...option };
52
- }
53
- /**
54
- * Materialize Databricks AI Tools skills at app startup and return them as
55
- * extra local skill scan paths.
56
- *
57
- * Resolution: reuse the installed global tree when present (no CLI call) unless
58
- * `refresh`/`skills`/`path` ask for a fresh CLI materialization; otherwise run
59
- * `databricks aitools install --path <dir> --skills-only` when the CLI resolves.
60
- * A `"require"` provision that yields nothing fails startup unless
61
- * `failOnError: false`.
62
- */
63
- export async function provisionDatabricksAITools(option) {
64
- const options = normalizeDatabricksAIToolsOption(option);
65
- const empty = { localSkillPaths: [] };
66
- if (!options)
67
- return empty;
68
- const mode = options.mode ?? "auto";
69
- const failOnError = options.failOnError ?? mode === "require";
70
- const wantsCliFetch = options.refresh === true ||
71
- options.path !== undefined ||
72
- string.parseList(options.skills).length > 0 ||
73
- options.experimental === true;
74
- try {
75
- // Fast path: reuse the already-installed global tree, no CLI call.
76
- if (!wantsCliFetch && installedTreeExists()) {
77
- logger.debug("using installed aitools tree", { path: GLOBAL_AITOOLS_SKILLS_PATH });
78
- return { localSkillPaths: [GLOBAL_AITOOLS_SKILLS_PATH], source: "installed" };
79
- }
80
- const cliPath = await resolveCli(options.cli);
81
- if (cliPath) {
82
- const dir = await materializeViaCli(cliPath, options);
83
- if (dir)
84
- return { localSkillPaths: [dir], source: "cli" };
85
- }
86
- // Nothing fetched. Fall back to the installed tree if it's there.
87
- if (installedTreeExists()) {
88
- return { localSkillPaths: [GLOBAL_AITOOLS_SKILLS_PATH], source: "installed" };
89
- }
90
- if (failOnError) {
91
- throw new Error(`Databricks AI Tools requested but no installed skills tree at "${GLOBAL_AITOOLS_SKILLS_PATH}" and the "${DATABRICKS_CLI}" CLI is not resolvable. Install with \`databricks aitools install\`, or set databricksAITools: "auto".`);
92
- }
93
- logger.debug("databricks aitools unavailable; skipping");
94
- return empty;
95
- }
96
- catch (err) {
97
- if (failOnError) {
98
- throw new Error(`failed to provision Databricks AI Tools skills: ${error.errorMessage(err)}`, { cause: error.toError(err) });
99
- }
100
- logger.warn("skipped", { error: error.errorMessage(err) });
101
- return empty;
102
- }
103
- }
104
- /** Whether the CLI's global skills tree exists and holds at least one skill. */
105
- function installedTreeExists() {
106
- return existsSync(GLOBAL_AITOOLS_SKILLS_PATH);
107
- }
108
- /** Resolve the `databricks` CLI, returning its invocation path or `undefined`. */
109
- async function resolveCli(cli) {
110
- const command = string.trimToNull(cli) ?? DATABRICKS_CLI;
111
- try {
112
- const result = await spawn(command, ["aitools", "version"], {
113
- stdout: "capture",
114
- stderr: "capture",
115
- check: false,
116
- });
117
- if (result.exitCode === 0)
118
- return command;
119
- }
120
- catch {
121
- // CLI absent or `aitools` unsupported - caller falls back / fails.
122
- }
123
- return undefined;
124
- }
125
- /**
126
- * Run `databricks aitools install --path <dir> --skills-only` to materialize a
127
- * resolved, agent-agnostic skill set into a directory (no agents, no state).
128
- */
129
- async function materializeViaCli(cli, options) {
130
- const dir = string.trimToNull(options.path) ?? (await mkdtemp(join(tmpdir(), "databricks-aitools-")));
131
- const args = ["aitools", "install", "--path", dir, "--skills-only"];
132
- const skills = string.parseList(options.skills);
133
- if (skills.length > 0)
134
- args.push("--skills", skills.join(","));
135
- if (options.experimental)
136
- args.push("--experimental");
137
- const result = await spawn(cli, args, {
138
- stdout: "capture",
139
- stderr: "capture",
140
- check: false,
141
- });
142
- if (result.exitCode !== 0) {
143
- throw new Error(`databricks aitools install failed (exit ${result.exitCode}): ${result.stderr || result.stdout}`);
144
- }
145
- if (!existsSync(dir))
146
- return undefined;
147
- logger.debug("materialized aitools skills via CLI", { path: dir, skills });
148
- return dir;
149
- }
150
- //# sourceMappingURL=data:application/json;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoiZGF0YWJyaWNrcy1haXRvb2xzLmpzIiwic291cmNlUm9vdCI6IiIsInNvdXJjZXMiOlsiLi4vLi4vc3JjL2RhdGFicmlja3MtYWl0b29scy50cyJdLCJuYW1lcyI6W10sIm1hcHBpbmdzIjoiQUFBQTs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7R0E2Qkc7QUFFSCxPQUFPLEVBQUUsT0FBTyxFQUFFLE1BQU0sa0JBQWtCLENBQUM7QUFDM0MsT0FBTyxFQUFFLFVBQVUsRUFBRSxNQUFNLFNBQVMsQ0FBQztBQUNyQyxPQUFPLEVBQUUsT0FBTyxFQUFFLE1BQU0sRUFBRSxNQUFNLFNBQVMsQ0FBQztBQUMxQyxPQUFPLEVBQUUsSUFBSSxFQUFFLE1BQU0sV0FBVyxDQUFDO0FBQ2pDLE9BQU8sRUFBRSxLQUFLLEVBQUUsTUFBTSxpQkFBaUIsQ0FBQztBQUN4QyxPQUFPLEVBQUUsS0FBSyxFQUFFLEdBQUcsRUFBRSxNQUFNLEVBQUUsTUFBTSx3QkFBd0IsQ0FBQztBQUU1RCxNQUFNLE1BQU0sR0FBRyxHQUFHLENBQUMsTUFBTSxDQUFDLDJCQUEyQixDQUFDLENBQUM7QUFFdkQscUVBQXFFO0FBQ3JFLE1BQU0sMEJBQTBCLEdBQUcsSUFBSSxDQUFDLE9BQU8sRUFBRSxFQUFFLGFBQWEsRUFBRSxTQUFTLEVBQUUsUUFBUSxDQUFDLENBQUM7QUFFdkYsMERBQTBEO0FBQzFELE1BQU0sY0FBYyxHQUFHLFlBQVksQ0FBQztBQWdEcEMsNkVBQTZFO0FBRTdFLGlHQUFpRztBQUNqRyxNQUFNLFVBQVUsZ0NBQWdDLENBQzlDLE1BQTJDO0lBRTNDLElBQUksTUFBTSxLQUFLLFNBQVMsSUFBSSxNQUFNLEtBQUssS0FBSztRQUFFLE9BQU8sU0FBUyxDQUFDO0lBQy9ELElBQUksTUFBTSxLQUFLLElBQUk7UUFBRSxPQUFPLEVBQUUsSUFBSSxFQUFFLFNBQVMsRUFBRSxDQUFDO0lBQ2hELElBQUksTUFBTSxLQUFLLE1BQU07UUFBRSxPQUFPLEVBQUUsSUFBSSxFQUFFLE1BQU0sRUFB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QyxnQkFBZ0IsQ0FBQyxDQUFDO0lBRXRELE1BQU0sTUFBTSxHQUFHLE1BQU0sS0FBSyxDQUFDLEdBQUcsRUFBRSxJQUFJLEVBQUU7UUFDcEMsTUFBTSxFQUFFLFNBQVM7UUFDakIsTUFBTSxFQUFFLFNBQVM7UUFDakIsS0FBSyxFQUFFLEtBQUs7S0FDYixDQUFDLENBQUM7SUFDSCxJQUFJLE1BQU0sQ0FBQyxRQUFRLEtBQUssQ0FBQyxFQUFFLENBQUM7UUFDMUIsTUFBTSxJQUFJLEtBQUssQ0FDYiwyQ0FBMkMsTUFBTSxDQUFDLFFBQVEsTUFBTSxNQUFNLENBQUMsTUFBTSxJQUFJLE1BQU0sQ0FBQyxNQUFNLEVBQUUsQ0FDakcsQ0FBQztJQUNKLENBQUM7SUFDRCxJQUFJLENBQUMsVUFBVSxDQUFDLEdBQUcsQ0FBQztRQUFFLE9BQU8sU0FBUyxDQUFDO0lBQ3ZDLE1BQU0sQ0FBQyxLQUFLLENBQUMscUNBQXFDLEVBQUUsRUFBRSxJQUFJLEVBQUUsR0FBRyxFQUFFLE1BQU0sRUFBRSxDQUFDLENBQUM7SUFDM0UsT0FBTyxHQUFHLENBQUM7QUFDYixDQUFDIiwic291cmNlc0NvbnRlbnQiOlsiLyoqXG4gKiBTdGFydHVwIHByb3Zpc2lvbmluZyBvZiBEYXRhYnJpY2tzIEFJIFRvb2xzIHNraWxscyBmb3IgYSBNYXN0cmEgd29ya3NwYWNlLlxuICpcbiAqIERhdGFicmlja3MgQUkgVG9vbHMgKGBkYXRhYnJpY2tzIGFpdG9vbHNgKSBhcmUgRGF0YWJyaWNrcy1vd25lZCBBZ2VudC1Ta2lsbFxuICogKGBTS0lMTC5tZGApIHRyZWVzIGluc3RhbGxlZCBhbmQga2VwdCB1cCB0byBkYXRlIHRocm91Z2ggdGhlIERhdGFicmlja3MgQ0xJLlxuICogVGhlIENMSSBpbnN0YWxscyB0aGVtIGdsb2JhbGx5IHVuZGVyIGB+Ly5kYXRhYnJpY2tzL2FpdG9vbHMvc2tpbGxzLzxuYW1lPi9gXG4gKiAod2l0aCBhIGAuc3RhdGUuanNvbmAgbWFuaWZlc3QpIGFuZCBjYW4gYWxzbyB3cml0ZSBhIHJlc29sdmVkLCBhZ2VudC1hZ25vc3RpY1xuICogY29weSB0byBhbnkgZGlyZWN0b3J5IHdpdGggYGRhdGFicmlja3MgYWl0b29scyBpbnN0YWxsIC0tcGF0aCA8ZGlyPlxuICogLS1za2lsbHMtb25seWAuXG4gKlxuICoge0BsaW5rIHByb3Zpc2lvbkRhdGFicmlja3NBSVRvb2xzfSBmb2xkcyB0aG9zZSBza2lsbHMgaW50byBhIE1hc3RyYSB3b3Jrc3BhY2VcbiAqIGFzIGV4dHJhIExPQ0FMIHNraWxsIHNjYW4gcGF0aHMgc28gYW4gYWdlbnQgY2FuIHVzZSB0aGVtIHdpdGhvdXQgYW55b25lXG4gKiBoYW5kLWNvcHlpbmcgYFNLSUxMLm1kYCBmaWxlcy4gSXQgbmV2ZXIgcmVpbXBsZW1lbnRzIHRoZSBDTEkncyBzb3VyY2luZzogaXRcbiAqIGVpdGhlciBwb2ludHMgTWFzdHJhIGF0IHRoZSBhbHJlYWR5LWluc3RhbGxlZCBnbG9iYWwgdHJlZSwgb3Igc2hlbGxzIG91dCB0b1xuICogdGhlIENMSSB0byBtYXRlcmlhbGl6ZSBhIGN1cmF0ZWQgc3Vic2V0IGludG8gYSB0ZW1wIGRpci5cbiAqXG4gKiBUaGUgb3B0aW9uIGlzIGBmYWxzZSB8IHRydWUgfCBcImF1dG9cIiB8IERhdGFicmlja3NBSVRvb2xzT3B0aW9uc2A6XG4gKlxuICogLSBgZmFsc2VgIChkZWZhdWx0KSAtIG9mZi5cbiAqIC0gYFwiYXV0b1wiYCAtIGVuYWJsZSBvbmx5IHdoZW4gdGhlIGdsb2JhbCB0cmVlIGFscmVhZHkgZXhpc3RzIE9SIHRoZVxuICogICBgZGF0YWJyaWNrc2AgQ0xJIGlzIHJlc29sdmFibGU7IG90aGVyd2lzZSBzaWxlbnRseSBuby1vcC4gVGhlIFwib24gaWYgdGhlXG4gKiAgIENMSSBpcyBhcm91bmRcIiBkZWZhdWx0LlxuICogLSBgdHJ1ZWAgLSByZXF1aXJlZDogZmFpbCBzdGFydHVwIChzdWJqZWN0IHRvIGBmYWlsT25FcnJvcmApIHdoZW4gbmVpdGhlciB0aGVcbiAqICAgaW5zdGFsbGVkIHRyZWUgbm9yIHRoZSBDTEkgY2FuIHN1cHBseSBza2lsbHMuXG4gKiAtIGFuIG9wdGlvbnMgYmFnIGZvciBhIHNwZWNpZmljIGBza2lsbHNgIHN1YnNldCwgYGV4cGVyaW1lbnRhbGAgc2tpbGxzLCBhXG4gKiAgIGByZWZyZXNoYCB0aGF0IHJlLXJ1bnMgdGhlIENMSSBldmVuIHdoZW4gYSB0cmVlIGV4aXN0cywgb3IgYW4gZXhwbGljaXRcbiAqICAgYHBhdGhgLlxuICpcbiAqIEBtb2R1bGVcbiAqL1xuXG5pbXBvcnQgeyBta2R0ZW1wIH0gZnJvbSBcIm5vZGU6ZnMvcHJvbWlzZXNcIjtcbmltcG9ydCB7IGV4aXN0c1N5bmMgfSBmcm9tIFwibm9kZTpmc1wiO1xuaW1wb3J0IHsgaG9tZWRpciwgdG1wZGlyIH0gZnJvbSBcIm5vZGU6b3NcIjtcbmltcG9ydCB7IGpvaW4gfSBmcm9tIFwibm9kZTpwYXRoXCI7XG5pbXBvcnQgeyBzcGF3biB9IGZyb20gXCJAZGJ4LXRvb2xzL2NvcmVcIjtcbmltcG9ydCB7IGVycm9yLCBsb2csIHN0cmluZyB9IGZyb20gXCJAZGJ4LXRvb2xzL3NoYXJlZC1jb3JlXCI7XG5cbmNvbnN0IGxvZ2dlciA9IGxvZy5sb2dnZXIoXCJtYXN0cmEvZGF0YWJyaWNrcy1haXRvb2xzXCIpO1xuXG4vKiogR2xvYmFsIHNraWxscyB0cmVlIHRoZSBgZGF0YWJyaWNrcyBhaXRvb2xzYCBDTEkgaW5zdGFsbHMgaW50by4gKi9cbmNvbnN0IEdMT0JBTF9BSVRPT0xTX1NLSUxMU19QQVRIID0gam9pbihob21lZGlyKCksIFwiLmRhdGFicmlja3NcIiwgXCJhaXRvb2xzXCIsIFwic2tpbGxzXCIpO1xuXG4vKiogVGhlIERhdGFicmlja3MgQ0xJIGJpbmFyeSBuYW1lOyByZXNvbHZlZCBvbiBgUEFUSGAuICovXG5jb25zdCBEQVRBQlJJQ0tTX0NMSSA9IFwiZGF0YWJyaWNrc1wiO1xuXG4vKiAtLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLSB0eXBlcyAtLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLSAqL1xuXG4vKiogSG93IGFnZ3Jlc3NpdmVseSB0byBlbmFibGUgRGF0YWJyaWNrcyBBSSBUb29scyBza2lsbHMuICovXG5leHBvcnQgdHlwZSBEYXRhYnJpY2tzQUlUb29sc01vZGUgPSBcImF1dG9cIiB8IFwicmVxdWlyZVwiO1xuXG4vKiogT3B0aW9ucyBmb3Ige0BsaW5rIHByb3Zpc2lvbkRhdGFicmlja3NBSVRvb2xzfS4gKi9cbmV4cG9ydCBpbnRlcmZhY2UgRGF0YWJyaWNrc0FJVG9vbHNPcHRpb25zIHtcbiAgLyoqXG4gICAqIGBcImF1dG9cImAgKGRlZmF1bHQpIGVuYWJsZXMgb25seSB3aGVuIHRoZSBpbnN0YWxsZWQgdHJlZSBleGlzdHMgb3IgdGhlIENMSVxuICAgKiBpcyByZXNvbHZhYmxlOyBgXCJyZXF1aXJlXCJgIGZhaWxzIHN0YXJ0dXAgd2hlbiBuZWl0aGVyIGNhbiBzdXBwbHkgc2tpbGxzLlxuICAgKi9cbiAgbW9kZT86IERhdGFicmlja3NBSVRvb2xzTW9kZTtcbiAgLyoqIEluc3RhbGwgb25seSB0aGVzZSBza2lsbCBuYW1lcyAoY29tbWEvc3BhY2UgbGlzdCBvciBhcnJheSkuICovXG4gIHNraWxscz86IHN0cmluZyB8IHN0cmluZ1tdO1xuICAvKiogSW5jbHVkZSBleHBlcmltZW50YWwgc2tpbGxzIHdoZW4gdGhlIENMSSBoYXMgdG8gZmV0Y2guICovXG4gIGV4cGVyaW1lbnRhbD86IGJvb2xlYW47XG4gIC8qKlxuICAgKiBSZS1ydW4gdGhlIENMSSB0byAocmUpbWF0ZXJpYWxpemUgc2tpbGxzIGludG8gYSBmcmVzaCBkaXIgZXZlbiB3aGVuIHRoZVxuICAgKiBnbG9iYWwgdHJlZSBhbHJlYWR5IGV4aXN0cy4gRGVmYXVsdHMgdG8gYGZhbHNlYCAocmV1c2UgdGhlIGluc3RhbGxlZCB0cmVlKS5cbiAgICovXG4gIHJlZnJlc2g/OiBib29sZWFuO1xuICAvKipcbiAgICogRXhwbGljaXQgZGlyZWN0b3J5IHRvIG1hdGVyaWFsaXplIHNraWxscyBpbnRvIHdpdGggdGhlIENMSS4gRGVmYXVsdHMgdG8gYVxuICAgKiB0ZW1wIGRpci4gSWdub3JlZCB3aGVuIHRoZSBpbnN0YWxsZWQgZ2xvYmFsIHRyZWUgaXMgdXNlZCBhcy1pcy5cbiAgICovXG4gIHBhdGg/OiBzdHJpbmc7XG4gIC8qKiBBYnNvbHV0ZSBwYXRoIHRvIHRoZSBgZGF0YWJyaWNrc2AgQ0xJLiBEZWZhdWx0cyB0byBgZGF0YWJyaWNrc2Agb24gUEFUSC4gKi9cbiAgY2xpPzogc3RyaW5nO1xuICAvKipcbiAgICogRmFpbCBhcHAgc3RhcnR1cCB3aGVuIHJlcXVpcmVkIHNraWxscyBjYW4ndCBiZSBwcm92aXNpb25lZC4gRGVmYXVsdHMgdG9cbiAgICogYHRydWVgIGZvciBgbW9kZTogXCJyZXF1aXJlXCJgIGFuZCBgZmFsc2VgIGZvciBgXCJhdXRvXCJgLlxuICAgKi9cbiAgZmFpbE9uRXJyb3I/OiBib29sZWFuO1xufVxuXG4vKiogVGhlIGBkYXRhYnJpY2tzQUlUb29sc2AgY29uZmlnIG9wdGlvbjogYSB0b2dnbGUsIGBcImF1dG9cImAsIG9yIGFuIG9wdGlvbnMgYmFnLiAqL1xuZXhwb3J0IHR5cGUgRGF0YWJyaWNrc0FJVG9vbHNPcHRpb24gPSBib29sZWFuIHwgXCJhdXRvXCIgfCBEYXRhYnJpY2tzQUlUb29sc09wdGlvbnM7XG5cbi8qKiBXaGF0IHtAbGluayBwcm92aXNpb25EYXRhYnJpY2tzQUlUb29sc30gcmVzb2x2ZWQuICovXG5leHBvcnQgaW50ZXJmYWNlIFByb3Zpc2lvbmVkRGF0YWJyaWNrc0FJVG9vbHMge1xuICAvKiogRXh0cmEgTE9DQUwgc2tpbGwgc2NhbiBwYXRocyB0byBoYW5kIE1hc3RyYS4gRW1wdHkgd2hlbiBkaXNhYmxlZC91bnJlc29sdmVkLiAqL1xuICBsb2NhbFNraWxsUGF0aHM6IHN0cmluZ1tdO1xuICAvKiogSG93IHRoZSBza2lsbHMgd2VyZSBzb3VyY2VkLCBmb3IgbG9nZ2luZy4gKi9cbiAgc291cmNlPzogXCJpbnN0YWxsZWRcIiB8IFwiY2xpXCI7XG59XG5cbi8qIC0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0gaGVscGVycyAtLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tICovXG5cbi8qKiBOb3JtYWxpemUgdGhlIGBkYXRhYnJpY2tzQUlUb29sc2Agb3B0aW9uIGludG8gYSBmbGF0IG9wdGlvbnMgYmFnLCBvciBgdW5kZWZpbmVkYCB3aGVuIG9mZi4gKi9cbmV4cG9ydCBmdW5jdGlvbiBub3JtYWxpemVEYXRhYnJpY2tzQUlUb29sc09wdGlvbihcbiAgb3B0aW9uOiBEYXRhYnJpY2tzQUlUb29sc09wdGlvbiB8IHVuZGVmaW5lZCxcbik6IERhdGFicmlja3NBSVRvb2xzT3B0aW9ucyB8IHVuZGVmaW5lZCB7XG4gIGlmIChvcHRpb24gPT09IHVuZGVmaW5lZCB8fCBvcHRpb24gPT09IGZhbHNlKSByZXR1cm4gdW5kZWZpbmVkO1xuICBpZiAob3B0aW9uID09PSB0cnVlKSByZXR1cm4geyBtb2RlOiBcInJlcXVpcmVcIiB9O1xuICBpZiAob3B0aW9uID09PSBcImF1dG9cIikgcmV0dXJuIHsgbW9kZTogXCJhdXRvXCIgfTtcbiAgcmV0dXJuIHsgbW9kZTogb3B0aW9uLm1vZGUgPz8gXCJhdXRvXCIsIC4uLm9wdGlvbiB9O1xufVxuXG4vKipcbiAqIE1hdGVyaWFsaXplIERhdGFicmlja3MgQUkgVG9vbHMgc2tpbGxzIGF0IGFwcCBzdGFydHVwIGFuZCByZXR1cm4gdGhlbSBhc1xuICogZXh0cmEgbG9jYWwgc2tpbGwgc2NhbiBwYXRocy5cbiAqXG4gKiBSZXNvbHV0aW9uOiByZXVzZSB0aGUgaW5zdGFsbGVkIGdsb2JhbCB0cmVlIHdoZW4gcHJlc2VudCAobm8gQ0xJIGNhbGwpIHVubGVzc1xuICogYHJlZnJlc2hgL2Bza2lsbHNgL2BwYXRoYCBhc2sgZm9yIGEgZnJlc2ggQ0xJIG1hdGVyaWFsaXphdGlvbjsgb3RoZXJ3aXNlIHJ1blxuICogYGRhdGFicmlja3MgYWl0b29scyBpbnN0YWxsIC0tcGF0aCA8ZGlyPiAtLXNraWxscy1vbmx5YCB3aGVuIHRoZSBDTEkgcmVzb2x2ZXMuXG4gKiBBIGBcInJlcXVpcmVcImAgcHJvdmlzaW9uIHRoYXQgeWllbGRzIG5vdGhpbmcgZmFpbHMgc3RhcnR1cCB1bmxlc3NcbiAqIGBmYWlsT25FcnJvcjogZmFsc2VgLlxuICovXG5leHBvcnQgYXN5bmMgZnVuY3Rpb24gcHJvdmlzaW9uRGF0YWJyaWNrc0FJVG9vbHMoXG4gIG9wdGlvbjogRGF0YWJyaWNrc0FJVG9vbHNPcHRpb24gfCB1bmRlZmluZWQsXG4pOiBQcm9taXNlPFByb3Zpc2lvbmVkRGF0YWJyaWNrc0FJVG9vbHM+IHtcbiAgY29uc3Qgb3B0aW9ucyA9IG5vcm1hbGl6ZURhdGFicmlja3NBSVRvb2xzT3B0aW9uKG9wdGlvbik7XG4gIGNvbnN0IGVtcHR5OiBQcm92aXNpb25lZERhdGFicmlja3NBSVRvb2xzID0geyBsb2NhbFNraWxsUGF0aHM6IFtdIH07XG4gIGlmICghb3B0aW9ucykgcmV0dXJuIGVtcHR5O1xuXG4gIGNvbnN0IG1vZGUgPSBvcHRpb25zLm1vZGUgPz8gXCJhdXRvXCI7XG4gIGNvbnN0IGZhaWxPbkVycm9yID0gb3B0aW9ucy5mYWlsT25FcnJvciA/PyBtb2RlID09PSBcInJlcXVpcmVcIjtcbiAgY29uc3Qgd2FudHNDbGlGZXRjaCA9XG4gICAgb3B0aW9ucy5yZWZyZXNoID09PSB0cnVlIHx8XG4gICAgb3B0aW9ucy5wYXRoICE9PSB1bmRlZmluZWQgfHxcbiAgICBzdHJpbmcucGFyc2VMaXN0KG9wdGlvbnMuc2tpbGxzKS5sZW5ndGggPiAwIHx8XG4gICAgb3B0aW9ucy5leHBlcmltZW50YWwgPT09IHRydWU7XG5cbiAgdHJ5IHtcbiAgICAvLyBGYXN0IHBhdGg6IHJldXNlIHRoZSBhbHJlYWR5LWluc3RhbGxlZCBnbG9iYWwgdHJlZSwgbm8gQ0xJIGNhbGwuXG4gICAgaWYgKCF3YW50c0NsaUZldGNoICYmIGluc3RhbGxlZFRyZWVFeGlzdHMoKSkge1xuICAgICAgbG9nZ2VyLmRlYnVnKFwidXNpbmcgaW5zdGFsbGVkIGFpdG9vbHMgdHJlZVwiLCB7IHBhdGg6IEdMT0JBTF9BSVRPT0xTX1NLSUxMU19QQVRIIH0pO1xuICAgICAgcmV0dXJuIHsgbG9jYWxTa2lsbFBhdGhzOiBbR0xPQkFMX0FJVE9PTFNfU0tJTExTX1BBVEhdLCBzb3VyY2U6IFwiaW5zdGFsbGVkXCIgfTtcbiAgICB9XG5cbiAgICBjb25zdCBjbGlQYXRoID0gYXdhaXQgcmVzb2x2ZUNsaShvcHRpb25zLmNsaSk7XG4gICAgaWYgKGNsaVBhdGgpIHtcbiAgICAgIGNvbnN0IGRpciA9IGF3YWl0IG1hdGVyaWFsaXplVmlhQ2xpKGNsaVBhdGgsIG9wdGlvbnMpO1xuICAgICAgaWYgKGRpcikgcmV0dXJuIHsgbG9jYWxTa2lsbFBhdGhzOiBbZGlyXSwgc291cmNlOiBcImNsaVwiIH07XG4gICAgfVxuXG4gICAgLy8gTm90aGluZyBmZXRjaGVkLiBGYWxsIGJhY2sgdG8gdGhlIGluc3RhbGxlZCB0cmVlIGlmIGl0J3MgdGhlcmUuXG4gICAgaWYgKGluc3RhbGxlZFRyZWVFeGlzdHMoKSkge1xuICAgICAgcmV0dXJuIHsgbG9jYWxTa2lsbFBhdGhzOiBbR0xPQkFMX0FJVE9PTFNfU0tJTExTX1BBVEhdLCBzb3VyY2U6IFwiaW5zdGFsbGVkXCIgfTtcbiAgICB9XG5cbiAgICBpZiAoZmFpbE9uRXJyb3IpIHtcbiAgICAgIHRocm93IG5ldyBFcnJvcihcbiAgICAgICAgYERhdGFicmlja3MgQUkgVG9vbHMgcmVxdWVzdGVkIGJ1dCBubyBpbnN0YWxsZWQgc2tpbGxzIHRyZWUgYXQgXCIke0dMT0JBTF9BSVRPT0xTX1NLSUxMU19QQVRIfVwiIGFuZCB0aGUgXCIke0RBVEFCUklDS1NfQ0xJfVwiIENMSSBpcyBub3QgcmVzb2x2YWJsZS4gSW5zdGFsbCB3aXRoIFxcYGRhdGFicmlja3MgYWl0b29scyBpbnN0YWxsXFxgLCBvciBzZXQgZGF0YWJyaWNrc0FJVG9vbHM6IFwiYXV0b1wiLmAsXG4gICAgICApO1xuICAgIH1cbiAgICBsb2dnZXIuZGVidWcoXCJkYXRhYnJpY2tzIGFpdG9vbHMgdW5hdmFpbGFibGU7IHNraXBwaW5nXCIpO1xuICAgIHJldHVybiBlbXB0eTtcbiAgfSBjYXRjaCAoZXJyKSB7XG4gICAgaWYgKGZhaWxPbkVycm9yKSB7XG4gICAgICB0aHJvdyBuZXcgRXJyb3IoXG4gICAgICAgIGBmYWlsZWQgdG8gcHJvdmlzaW9uIERhdGFicmlja3MgQUkgVG9vbHMgc2tpbGxzOiAke2Vycm9yLmVycm9yTWVzc2FnZShlcnIpfWAsXG4gICAgICAgIHsgY2F1c2U6IGVycm9yLnRvRXJyb3IoZXJyKSB9LFxuICAgICAgKTtcbiAgICB9XG4gICAgbG9nZ2VyLndhcm4oXCJza2lwcGVkXCIsIHsgZXJyb3I6IGVycm9yLmVycm9yTWVzc2FnZShlcnIpIH0pO1xuICAgIHJldHVybiBlbXB0eTtcbiAgfVxufVxuXG4vKiogV2hldGhlciB0aGUgQ0xJJ3MgZ2xvYmFsIHNraWxscyB0cmVlIGV4aXN0cyBhbmQgaG9sZHMgYXQgbGVhc3Qgb25lIHNraWxsLiAqL1xuZnVuY3Rpb24gaW5zdGFsbGVkVHJlZUV4aXN0cygpOiBib29sZWFuIHtcbiAgcmV0dXJuIGV4aXN0c1N5bmMoR0xPQkFMX0FJVE9PTFNfU0tJTExTX1BBVEgpO1xufVxuXG4vKiogUmVzb2x2ZSB0aGUgYGRhdGFicmlja3NgIENMSSwgcmV0dXJuaW5nIGl0cyBpbnZvY2F0aW9uIHBhdGggb3IgYHVuZGVmaW5lZGAuICovXG5hc3luYyBmdW5jdGlvbiByZXNvbHZlQ2xpKGNsaTogc3RyaW5nIHwgdW5kZWZpbmVkKTogUHJvbWlzZTxzdHJpbmcgfCB1bmRlZmluZWQ+IHtcbiAgY29uc3QgY29tbWFuZCA9IHN0cmluZy50cmltVG9OdWxsKGNsaSkgPz8gREFUQUJSSUNLU19DTEk7XG4gIHRyeSB7XG4gICAgY29uc3QgcmVzdWx0ID0gYXdhaXQgc3Bhd24oY29tbWFuZCwgW1wiYWl0b29sc1wiLCBcInZlcnNpb25cIl0sIHtcbiAgICAgIHN0ZG91dDogXCJjYXB0dXJlXCIsXG4gICAgICBzdGRlcnI6IFwiY2FwdHVyZVwiLFxuICAgICAgY2hlY2s6IGZhbHNlLFxuICAgIH0pO1xuICAgIGlmIChyZXN1bHQuZXhpdENvZGUgPT09IDApIHJldHVybiBjb21tYW5kO1xuICB9IGNhdGNoIHtcbiAgICAvLyBDTEkgYWJzZW50IG9yIGBhaXRvb2xzYCB1bnN1cHBvcnRlZCAtIGNhbGxlciBmYWxscyBiYWNrIC8gZmFpbHMuXG4gIH1cbiAgcmV0dXJuIHVuZGVmaW5lZDtcbn1cblxuLyoqXG4gKiBSdW4gYGRhdGFicmlja3MgYWl0b29scyBpbnN0YWxsIC0tcGF0aCA8ZGlyPiAtLXNraWxscy1vbmx5YCB0byBtYXRlcmlhbGl6ZSBhXG4gKiByZXNvbHZlZCwgYWdlbnQtYWdub3N0aWMgc2tpbGwgc2V0IGludG8gYSBkaXJlY3RvcnkgKG5vIGFnZW50cywgbm8gc3RhdGUpLlxuICovXG5hc3luYyBmdW5jdGlvbiBtYXRlcmlhbGl6ZVZpYUNsaShcbiAgY2xpOiBzdHJpbmcsXG4gIG9wdGlvbnM6IERhdGFicmlja3NBSVRvb2xzT3B0aW9ucyxcbik6IFByb21pc2U8c3RyaW5nIHwgdW5kZWZpbmVkPiB7XG4gIGNvbnN0IGRpciA9XG4gICAgc3RyaW5nLnRyaW1Ub051bGwob3B0aW9ucy5wYXRoKSA/PyAoYXdhaXQgbWtkdGVtcChqb2luKHRtcGRpcigpLCBcImRhdGFicmlja3MtYWl0b29scy1cIikpKTtcbiAgY29uc3QgYXJncyA9IFtcImFpdG9vbHNcIiwgXCJpbnN0YWxsXCIsIFwiLS1wYXRoXCIsIGRpciwgXCItLXNraWxscy1vbmx5XCJdO1xuICBjb25zdCBza2lsbHMgPSBzdHJpbmcucGFyc2VMaXN0KG9wdGlvbnMuc2tpbGxzKTtcbiAgaWYgKHNraWxscy5sZW5ndGggPiAwKSBhcmdzLnB1c2goXCItLXNraWxsc1wiLCBza2lsbHMuam9pbihcIixcIikpO1xuICBpZiAob3B0aW9ucy5leHBlcmltZW50YWwpIGFyZ3MucHVzaChcIi0tZXhwZXJpbWVudGFsXCIpO1xuXG4gIGNvbnN0IHJlc3VsdCA9IGF3YWl0IHNwYXduKGNsaSwgYXJncywge1xuICAgIHN0ZG91dDogXCJjYXB0dXJlXCIsXG4gICAgc3RkZXJyOiBcImNhcHR1cmVcIixcbiAgICBjaGVjazogZmFsc2UsXG4gIH0pO1xuICBpZiAocmVzdWx0LmV4aXRDb2RlICE9PSAwKSB7XG4gICAgdGhyb3cgbmV3IEVycm9yKFxuICAgICAgYGRhdGFicmlja3MgYWl0b29scyBpbnN0YWxsIGZhaWxlZCAoZXhpdCAke3Jlc3VsdC5leGl0Q29kZX0pOiAke3Jlc3VsdC5zdGRlcnIgfHwgcmVzdWx0LnN0ZG91dH1gLFxuICAgICk7XG4gIH1cbiAgaWYgKCFleGlzdHNTeW5jKGRpcikpIHJldHVybiB1bmRlZmluZWQ7XG4gIGxvZ2dlci5kZWJ1ZyhcIm1hdGVyaWFsaXplZCBhaXRvb2xzIHNraWxscyB2aWEgQ0xJXCIsIHsgcGF0aDogZGlyLCBza2lsbHMgfSk7XG4gIHJldHVybiBkaXI7XG59XG4iXX0=
@@ -1,216 +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
- * The CLI installs them globally under `~/.databricks/aitools/skills/<name>/`
7
- * (with a `.state.json` manifest) and can also write a resolved, agent-agnostic
8
- * copy to any directory with `databricks aitools install --path <dir>
9
- * --skills-only`.
10
- *
11
- * {@link provisionDatabricksAITools} folds those skills into a Mastra workspace
12
- * as extra LOCAL skill scan paths so an agent can use them without anyone
13
- * hand-copying `SKILL.md` files. It never reimplements the CLI's sourcing: it
14
- * either points Mastra at the already-installed global tree, or shells out to
15
- * the CLI to materialize a curated subset into a temp dir.
16
- *
17
- * The option is `false | true | "auto" | DatabricksAIToolsOptions`:
18
- *
19
- * - `false` (default) - off.
20
- * - `"auto"` - enable only when the global tree already exists OR the
21
- * `databricks` CLI is resolvable; otherwise silently no-op. The "on if the
22
- * CLI is around" default.
23
- * - `true` - required: fail startup (subject to `failOnError`) when neither the
24
- * installed tree nor the CLI can supply skills.
25
- * - an options bag for a specific `skills` subset, `experimental` skills, a
26
- * `refresh` that re-runs the CLI even when a tree exists, or an explicit
27
- * `path`.
28
- *
29
- * @module
30
- */
31
-
32
- import { mkdtemp } from "node:fs/promises";
33
- import { existsSync } from "node:fs";
34
- import { homedir, tmpdir } from "node:os";
35
- import { join } from "node:path";
36
- import { spawn } from "@dbx-tools/core";
37
- import { error, log, string } from "@dbx-tools/shared-core";
38
-
39
- const logger = log.logger("mastra/databricks-aitools");
40
-
41
- /** Global skills tree the `databricks aitools` CLI installs into. */
42
- const GLOBAL_AITOOLS_SKILLS_PATH = join(homedir(), ".databricks", "aitools", "skills");
43
-
44
- /** The Databricks CLI binary name; resolved on `PATH`. */
45
- const DATABRICKS_CLI = "databricks";
46
-
47
- /* -------------------------------- types -------------------------------- */
48
-
49
- /** How aggressively to enable Databricks AI Tools skills. */
50
- export type DatabricksAIToolsMode = "auto" | "require";
51
-
52
- /** Options for {@link provisionDatabricksAITools}. */
53
- export interface DatabricksAIToolsOptions {
54
- /**
55
- * `"auto"` (default) enables only when the installed tree exists or the CLI
56
- * is resolvable; `"require"` fails startup when neither can supply skills.
57
- */
58
- mode?: DatabricksAIToolsMode;
59
- /** Install only these skill names (comma/space list or array). */
60
- skills?: string | string[];
61
- /** Include experimental skills when the CLI has to fetch. */
62
- experimental?: boolean;
63
- /**
64
- * Re-run the CLI to (re)materialize skills into a fresh dir even when the
65
- * global tree already exists. Defaults to `false` (reuse the installed tree).
66
- */
67
- refresh?: boolean;
68
- /**
69
- * Explicit directory to materialize skills into with the CLI. Defaults to a
70
- * temp dir. Ignored when the installed global tree is used as-is.
71
- */
72
- path?: string;
73
- /** Absolute path to the `databricks` CLI. Defaults to `databricks` on PATH. */
74
- cli?: string;
75
- /**
76
- * Fail app startup when required skills can't be provisioned. Defaults to
77
- * `true` for `mode: "require"` and `false` for `"auto"`.
78
- */
79
- failOnError?: boolean;
80
- }
81
-
82
- /** The `databricksAITools` config option: a toggle, `"auto"`, or an options bag. */
83
- export type DatabricksAIToolsOption = boolean | "auto" | DatabricksAIToolsOptions;
84
-
85
- /** What {@link provisionDatabricksAITools} resolved. */
86
- export interface ProvisionedDatabricksAITools {
87
- /** Extra LOCAL skill scan paths to hand Mastra. Empty when disabled/unresolved. */
88
- localSkillPaths: string[];
89
- /** How the skills were sourced, for logging. */
90
- source?: "installed" | "cli";
91
- }
92
-
93
- /* ------------------------------- helpers ------------------------------- */
94
-
95
- /** Normalize the `databricksAITools` option into a flat options bag, or `undefined` when off. */
96
- export function normalizeDatabricksAIToolsOption(
97
- option: DatabricksAIToolsOption | undefined,
98
- ): DatabricksAIToolsOptions | undefined {
99
- if (option === undefined || option === false) return undefined;
100
- if (option === true) return { mode: "require" };
101
- if (option === "auto") return { mode: "auto" };
102
- return { mode: option.mode ?? "auto", ...option };
103
- }
104
-
105
- /**
106
- * Materialize Databricks AI Tools skills at app startup and return them as
107
- * extra local skill scan paths.
108
- *
109
- * Resolution: reuse the installed global tree when present (no CLI call) unless
110
- * `refresh`/`skills`/`path` ask for a fresh CLI materialization; otherwise run
111
- * `databricks aitools install --path <dir> --skills-only` when the CLI resolves.
112
- * A `"require"` provision that yields nothing fails startup unless
113
- * `failOnError: false`.
114
- */
115
- export async function provisionDatabricksAITools(
116
- option: DatabricksAIToolsOption | undefined,
117
- ): Promise<ProvisionedDatabricksAITools> {
118
- const options = normalizeDatabricksAIToolsOption(option);
119
- const empty: ProvisionedDatabricksAITools = { localSkillPaths: [] };
120
- if (!options) return empty;
121
-
122
- const mode = options.mode ?? "auto";
123
- const failOnError = options.failOnError ?? mode === "require";
124
- const wantsCliFetch =
125
- options.refresh === true ||
126
- options.path !== undefined ||
127
- string.parseList(options.skills).length > 0 ||
128
- options.experimental === true;
129
-
130
- try {
131
- // Fast path: reuse the already-installed global tree, no CLI call.
132
- if (!wantsCliFetch && installedTreeExists()) {
133
- logger.debug("using installed aitools tree", { path: GLOBAL_AITOOLS_SKILLS_PATH });
134
- return { localSkillPaths: [GLOBAL_AITOOLS_SKILLS_PATH], source: "installed" };
135
- }
136
-
137
- const cliPath = await resolveCli(options.cli);
138
- if (cliPath) {
139
- const dir = await materializeViaCli(cliPath, options);
140
- if (dir) return { localSkillPaths: [dir], source: "cli" };
141
- }
142
-
143
- // Nothing fetched. Fall back to the installed tree if it's there.
144
- if (installedTreeExists()) {
145
- return { localSkillPaths: [GLOBAL_AITOOLS_SKILLS_PATH], source: "installed" };
146
- }
147
-
148
- if (failOnError) {
149
- throw new Error(
150
- `Databricks AI Tools requested but no installed skills tree at "${GLOBAL_AITOOLS_SKILLS_PATH}" and the "${DATABRICKS_CLI}" CLI is not resolvable. Install with \`databricks aitools install\`, or set databricksAITools: "auto".`,
151
- );
152
- }
153
- logger.debug("databricks aitools unavailable; skipping");
154
- return empty;
155
- } catch (err) {
156
- if (failOnError) {
157
- throw new Error(
158
- `failed to provision Databricks AI Tools skills: ${error.errorMessage(err)}`,
159
- { cause: error.toError(err) },
160
- );
161
- }
162
- logger.warn("skipped", { error: error.errorMessage(err) });
163
- return empty;
164
- }
165
- }
166
-
167
- /** Whether the CLI's global skills tree exists and holds at least one skill. */
168
- function installedTreeExists(): boolean {
169
- return existsSync(GLOBAL_AITOOLS_SKILLS_PATH);
170
- }
171
-
172
- /** Resolve the `databricks` CLI, returning its invocation path or `undefined`. */
173
- async function resolveCli(cli: string | undefined): Promise<string | undefined> {
174
- const command = string.trimToNull(cli) ?? DATABRICKS_CLI;
175
- try {
176
- const result = await spawn(command, ["aitools", "version"], {
177
- stdout: "capture",
178
- stderr: "capture",
179
- check: false,
180
- });
181
- if (result.exitCode === 0) return command;
182
- } catch {
183
- // CLI absent or `aitools` unsupported - caller falls back / fails.
184
- }
185
- return undefined;
186
- }
187
-
188
- /**
189
- * Run `databricks aitools install --path <dir> --skills-only` to materialize a
190
- * resolved, agent-agnostic skill set into a directory (no agents, no state).
191
- */
192
- async function materializeViaCli(
193
- cli: string,
194
- options: DatabricksAIToolsOptions,
195
- ): Promise<string | undefined> {
196
- const dir =
197
- string.trimToNull(options.path) ?? (await mkdtemp(join(tmpdir(), "databricks-aitools-")));
198
- const args = ["aitools", "install", "--path", dir, "--skills-only"];
199
- const skills = string.parseList(options.skills);
200
- if (skills.length > 0) args.push("--skills", skills.join(","));
201
- if (options.experimental) args.push("--experimental");
202
-
203
- const result = await spawn(cli, args, {
204
- stdout: "capture",
205
- stderr: "capture",
206
- check: false,
207
- });
208
- if (result.exitCode !== 0) {
209
- throw new Error(
210
- `databricks aitools install failed (exit ${result.exitCode}): ${result.stderr || result.stdout}`,
211
- );
212
- }
213
- if (!existsSync(dir)) return undefined;
214
- logger.debug("materialized aitools skills via CLI", { path: dir, skills });
215
- return dir;
216
- }