@dbx-tools/projen 0.9.151 → 0.9.153

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/project-py.ts CHANGED
@@ -1,9 +1,11 @@
1
1
  /** Reusable uv workspace generation for Python packages hosted in a projen tree. */
2
+ import { existsSync, readdirSync } from "node:fs";
3
+ import { resolve } from "node:path";
2
4
  import * as projectUtils from "@dbx-tools/core/project-utils";
3
5
  import { stringUtils, type OneOrMany } from "@dbx-tools/shared-core";
4
6
  import { Component, License, TextFile, type Project, github, javascript, python } from "projen";
5
7
  import { JobPermission, type Job, type JobStep } from "projen/lib/github/workflows-model";
6
- import { PYTHON_GENERATED_PACKAGE } from "./generated.ts";
8
+ import { header, PYTHON_GENERATED_PACKAGE } from "./generated.ts";
7
9
  import { LICENSE, projectRepositoryUrl, taskCommand } from "./project-js.ts";
8
10
  import { isDBXToolsJavaScriptProject } from "./project-predicate.ts";
9
11
  import type { DBXToolsProject, DBXToolsProjectOptions } from "./project.ts";
@@ -107,7 +109,10 @@ interface PythonPublication {
107
109
 
108
110
  /** Options for {@link DBXToolsPythonWorkspace}. */
109
111
  export interface DBXToolsPythonWorkspaceOptions {
110
- readonly packages: readonly PythonPackageOptions[];
112
+ /** Package-specific options merged over packages discovered under the Python root. */
113
+ readonly packages?: readonly PythonPackageOptions[];
114
+ /** Discover direct child directories containing `src/`. Defaults to true. */
115
+ readonly discoverPackages?: boolean;
111
116
  readonly repository?: PythonRepositoryOptions;
112
117
  /** Repository-relative Python package root. Defaults to `packages/py`. */
113
118
  readonly root?: string;
@@ -214,6 +219,15 @@ export function pythonModuleName(scope: string, directory: string): string {
214
219
  return [scope, ...directory.split("/")].map((part) => part.replaceAll("-", "_")).join(".");
215
220
  }
216
221
 
222
+ /** Discover direct Python workspace members by their conventional source directory. */
223
+ function discoverPythonPackages(root: string): string[] {
224
+ if (!existsSync(root)) return [];
225
+ return readdirSync(root, { withFileTypes: true })
226
+ .filter((entry) => entry.isDirectory() && existsSync(resolve(root, entry.name, "src")))
227
+ .map((entry) => entry.name)
228
+ .sort();
229
+ }
230
+
217
231
  /** A Python package implemented with projen's `PythonProject` and uv backend. */
218
232
  export class DBXToolsPythonProject extends python.PythonProject implements DBXToolsProject {
219
233
  readonly language = "python" as const;
@@ -349,11 +363,30 @@ export class DBXToolsPythonWorkspace extends Component {
349
363
  ref: options.repository?.ref ?? "main",
350
364
  root: options.root ?? options.repository?.root ?? "packages/py",
351
365
  };
352
- const packageIdentities: ResolvedPythonPackageOptions[] = options.packages.map((pkg) => ({
353
- ...pkg,
354
- name: pkg.name ?? `${scope}-${stringUtils.toSlug(pkg.directory)}`,
355
- module: pkg.module ?? pythonModuleName(scope, pkg.directory),
356
- }));
366
+ const configuredPackages = new Map((options.packages ?? []).map((pkg) => [pkg.directory, pkg]));
367
+ const discoveredDirectories =
368
+ options.discoverPackages === false
369
+ ? []
370
+ : discoverPythonPackages(resolve(project.outdir, this.repository.root));
371
+ const directories = [
372
+ ...configuredPackages.keys(),
373
+ ...discoveredDirectories.filter((directory) => !configuredPackages.has(directory)),
374
+ ];
375
+ const packageIdentities: ResolvedPythonPackageOptions[] = directories.map((directory) => {
376
+ const pkg = configuredPackages.get(directory);
377
+ return {
378
+ directory,
379
+ description: `${scope} ${directory} Python package.`,
380
+ ...pkg,
381
+ name: pkg?.name ?? `${scope}-${stringUtils.toSlug(directory)}`,
382
+ module: pkg?.module ?? pythonModuleName(scope, directory),
383
+ };
384
+ });
385
+ if (packageIdentities.length === 0) {
386
+ throw new Error(
387
+ `No Python packages were configured or discovered under ${this.repository.root}`,
388
+ );
389
+ }
357
390
  const configuredNodeRuntimes = packageIdentities.filter(({ nodeRuntime }) => nodeRuntime);
358
391
  if (configuredNodeRuntimes.length > 1) {
359
392
  throw new Error("A Python workspace can configure only one shared Node runtime package");
@@ -760,26 +793,24 @@ export class DBXToolsPythonWorkspace extends Component {
760
793
  }),
761
794
  ];
762
795
  const helper = ".projen/pypi-trusted-publisher-instructions.mjs";
796
+ const helperHeader = header({ tool: "projen", source: ".projenrc.ts" })
797
+ .trimEnd()
798
+ .replaceAll("\n", "\n ");
763
799
  new TextFile(project.root, helper, {
800
+ marker: true,
801
+ readonly: true,
764
802
  // prettier-ignore
765
803
  lines: stringUtils.dedent(
766
804
  // ============================================================================
767
805
  /*js*/`
768
806
  #!/usr/bin/env bun
769
- import { parseArgs } from "node:util";
807
+ ${helperHeader}
808
+ import { printPypiTrustedPublisherInstructions } from "@dbx-tools/projen/pypi-trusted-publisher-instructions";
770
809
 
771
- const { values } = parseArgs({
772
- options: { secretFile: { type: "string" } },
810
+ await printPypiTrustedPublisherInstructions({
811
+ beforeAuthentication: ${JSON.stringify(linesBeforeAuthentication)},
812
+ afterAuthentication: ${JSON.stringify(linesAfterAuthentication)},
773
813
  });
774
- const beforeAuthentication = ${JSON.stringify(linesBeforeAuthentication)};
775
- const afterAuthentication = ${JSON.stringify(linesAfterAuthentication)};
776
- const authentication = values.secretFile
777
- ? [
778
- \`- If browser authentication is required, read credentials from \${values.secretFile} directly into the browser without printing, logging, or exposing secret values.\`,
779
- \`- If \${values.secretFile} is absent, invalid, or insufficient for authentication, pause and ask the user to complete authentication.\`,
780
- ]
781
- : ["- If browser authentication is required, pause and ask the user to complete authentication."];
782
- process.stdout.write(\`\${[...beforeAuthentication, ...authentication, ...afterAuthentication].join("\\n").trimEnd()}\\n\`);
783
814
  `
784
815
  // ============================================================================
785
816
  ).split("\n"),
package/src/tsconfig.ts CHANGED
@@ -3,6 +3,12 @@
3
3
  */
4
4
  import { Component, type Project, javascript, typescript } from "projen";
5
5
 
6
+ /** Consumer overrides for the repository-root TypeScript program. */
7
+ export interface DBXToolsRootTsconfigOptions {
8
+ readonly compilerOptions?: javascript.TypeScriptCompilerOptions;
9
+ readonly include?: readonly string[];
10
+ }
11
+
6
12
  /**
7
13
  * Compiler options for the ROOT program only (`.projenrc.ts` + the engine as
8
14
  * seen from the root). Each package owns its own projen-generated
@@ -40,7 +46,7 @@ export class DBXToolsRootTsconfig extends Component {
40
46
  /** Root program config inherited by the generated Projen config. */
41
47
  readonly config?: javascript.TypescriptConfig;
42
48
 
43
- constructor(scope: Project) {
49
+ constructor(scope: Project, options: DBXToolsRootTsconfigOptions = {}) {
44
50
  super(scope);
45
51
 
46
52
  if (scope instanceof typescript.TypeScriptProject) {
@@ -56,8 +62,10 @@ export class DBXToolsRootTsconfig extends Component {
56
62
  compilerOptions: {
57
63
  lib: ["ESNext"],
58
64
  types: ["node", "bun"],
65
+ jsx: javascript.TypeScriptJsxMode.REACT_JSX,
66
+ ...options.compilerOptions,
59
67
  },
60
- include: [".projenrc.ts"],
68
+ include: [".projenrc.ts", "test/**/*.ts", "test/**/*.tsx", ...(options.include ?? [])],
61
69
  exclude: ["node_modules", "**/dist", "**/node_modules"],
62
70
  });
63
71
  }
@@ -0,0 +1,182 @@
1
+ /**
2
+ * Workspace dependency protocol materialization for external npm consumers.
3
+ *
4
+ * @module
5
+ */
6
+ import {
7
+ chmodSync,
8
+ copyFileSync,
9
+ cpSync,
10
+ existsSync,
11
+ mkdirSync,
12
+ mkdtempSync,
13
+ readFileSync,
14
+ rmSync,
15
+ statSync,
16
+ writeFileSync,
17
+ } from "node:fs";
18
+ import { tmpdir } from "node:os";
19
+ import { dirname, join, resolve } from "node:path";
20
+ import * as exec from "@dbx-tools/core/exec";
21
+
22
+ const DEPENDENCY_FIELDS = [
23
+ "dependencies",
24
+ "devDependencies",
25
+ "optionalDependencies",
26
+ "peerDependencies",
27
+ ] as const;
28
+
29
+ interface StoredManifest {
30
+ readonly path: string;
31
+ readonly mode: number;
32
+ readonly text: string;
33
+ }
34
+
35
+ /** Replace workspace and catalogue protocols with concrete registry ranges. */
36
+ export function materializeWorkspaceManifest(
37
+ source: Readonly<Record<string, unknown>>,
38
+ workspace: Readonly<Record<string, unknown>>,
39
+ ): Readonly<Record<string, unknown>> {
40
+ const manifest = structuredClone(source) as Record<string, unknown>;
41
+ const version = workspace.version;
42
+ const catalog =
43
+ workspace.catalog && typeof workspace.catalog === "object"
44
+ ? (workspace.catalog as Readonly<Record<string, unknown>>)
45
+ : {};
46
+ const overrides =
47
+ workspace.overrides && typeof workspace.overrides === "object"
48
+ ? (workspace.overrides as Readonly<Record<string, unknown>>)
49
+ : {};
50
+ if (typeof version !== "string" || !version) {
51
+ throw new Error("Workspace manifest has no version");
52
+ }
53
+ for (const field of DEPENDENCY_FIELDS) {
54
+ const dependencies = manifest[field];
55
+ if (!dependencies || typeof dependencies !== "object") continue;
56
+ manifest[field] = Object.fromEntries(
57
+ Object.entries(dependencies).map(([name, value]) => [
58
+ name,
59
+ materializeDependency(name, value, version, catalog, overrides),
60
+ ]),
61
+ );
62
+ }
63
+ return manifest;
64
+ }
65
+
66
+ /**
67
+ * Materialize every generated workspace manifest for one external command and
68
+ * restore exact bytes and permissions afterward.
69
+ */
70
+ export async function withMaterializedWorkspaceManifests<T>(
71
+ root: string,
72
+ callback: () => Promise<T>,
73
+ ): Promise<T> {
74
+ const rootManifestPath = resolve(root, "package.json");
75
+ const rootManifest = readManifest(rootManifestPath);
76
+ const workspaces = Array.isArray(rootManifest.workspaces)
77
+ ? rootManifest.workspaces.filter((value): value is string => typeof value === "string")
78
+ : [];
79
+ const originals = [
80
+ rootManifestPath,
81
+ ...workspaces.map((workspace) => resolve(root, workspace, "package.json")),
82
+ ].map(storeManifest);
83
+
84
+ try {
85
+ for (const original of originals) {
86
+ const source = JSON.parse(original.text) as Record<string, unknown>;
87
+ writeManifest(original.path, materializeWorkspaceManifest(source, rootManifest));
88
+ }
89
+ return await callback();
90
+ } finally {
91
+ for (const original of originals) restoreManifest(original);
92
+ }
93
+ }
94
+
95
+ /**
96
+ * Copy tracked and non-ignored source into a temporary directory, materialize
97
+ * its manifests, and keep that snapshot stable for one external command.
98
+ */
99
+ export async function withStagedWorkspaceManifests<T>(
100
+ root: string,
101
+ callback: (stage: string) => Promise<T>,
102
+ ): Promise<T> {
103
+ const stage = mkdtempSync(join(tmpdir(), "dbx-tools-app-deploy-"));
104
+ try {
105
+ const listed = exec.spawnSync(
106
+ "git",
107
+ ["ls-files", "--cached", "--others", "--exclude-standard", "-z"],
108
+ {
109
+ cwd: root,
110
+ stdin: "ignore",
111
+ stdout: "capture",
112
+ stderr: "capture",
113
+ },
114
+ );
115
+ if (listed.exitCode !== 0) {
116
+ throw new Error(`Could not enumerate app source: ${listed.stderr}`);
117
+ }
118
+ for (const relativePath of listed.stdout.split("\0").filter(Boolean)) {
119
+ const source = resolve(root, relativePath);
120
+ if (!existsSync(source)) continue;
121
+ const destination = resolve(stage, relativePath);
122
+ mkdirSync(dirname(destination), { recursive: true });
123
+ copyFileSync(source, destination);
124
+ chmodSync(destination, statSync(source).mode & 0o777);
125
+ }
126
+ const bundleState = resolve(root, ".databricks");
127
+ if (existsSync(bundleState)) {
128
+ cpSync(bundleState, resolve(stage, ".databricks"), { recursive: true });
129
+ }
130
+ return await withMaterializedWorkspaceManifests(stage, () => callback(stage));
131
+ } finally {
132
+ rmSync(stage, { recursive: true, force: true });
133
+ }
134
+ }
135
+
136
+ function materializeDependency(
137
+ name: string,
138
+ value: unknown,
139
+ version: string,
140
+ catalog: Readonly<Record<string, unknown>>,
141
+ overrides: Readonly<Record<string, unknown>>,
142
+ ): unknown {
143
+ const override = overrides[name];
144
+ if (typeof override === "string" && override && !override.startsWith("$")) {
145
+ return override;
146
+ }
147
+ if (value === "catalog:") {
148
+ const resolved = catalog[name];
149
+ if (typeof resolved !== "string" || !resolved) {
150
+ throw new Error(`Workspace catalog has no version for ${name}`);
151
+ }
152
+ return resolved;
153
+ }
154
+ if (typeof value !== "string" || !value.startsWith("workspace:")) return value;
155
+ const selector = value.slice("workspace:".length);
156
+ if (selector === "*" || !selector) return version;
157
+ if (selector === "^" || selector === "~") return `${selector}${version}`;
158
+ return selector;
159
+ }
160
+
161
+ function readManifest(path: string): Record<string, unknown> {
162
+ return JSON.parse(readFileSync(path, "utf8")) as Record<string, unknown>;
163
+ }
164
+
165
+ function storeManifest(path: string): StoredManifest {
166
+ return {
167
+ path,
168
+ mode: statSync(path).mode & 0o777,
169
+ text: readFileSync(path, "utf8"),
170
+ };
171
+ }
172
+
173
+ function writeManifest(path: string, manifest: Readonly<Record<string, unknown>>): void {
174
+ chmodSync(path, 0o644);
175
+ writeFileSync(path, `${JSON.stringify(manifest, null, 2)}\n`);
176
+ }
177
+
178
+ function restoreManifest(manifest: StoredManifest): void {
179
+ chmodSync(manifest.path, 0o644);
180
+ writeFileSync(manifest.path, manifest.text);
181
+ chmodSync(manifest.path, manifest.mode);
182
+ }
@@ -0,0 +1,245 @@
1
+ #!/usr/bin/env -S bun
2
+ /** Deploy an AppKit workspace through npm-compatible temporary manifests. */
3
+ import { existsSync, rmSync } from "node:fs";
4
+ import { join } from "node:path";
5
+ import { profile } from "@dbx-tools/auth";
6
+ import { serializeArgs } from "@dbx-tools/cli-args";
7
+ import * as exec from "@dbx-tools/core/exec";
8
+ import { errorUtils, log } from "@dbx-tools/shared-core";
9
+ import { z } from "zod";
10
+ import { runTaskMain, taskCommand, taskOptions } from "./cli.ts";
11
+ import { withStagedWorkspaceManifests } from "../src/workspace-manifest.ts";
12
+
13
+ const logger = log.logger("projen:appkit-deploy");
14
+
15
+ export const AppKitDeployContextOptionsSchema = z.object({
16
+ target: z
17
+ .string()
18
+ .trim()
19
+ .min(1)
20
+ .optional()
21
+ .describe("Databricks bundle target")
22
+ .meta({ env: [], short: "t" }),
23
+ profile: z.string().trim().min(1).optional().describe("Databricks CLI profile").meta({
24
+ env: [],
25
+ helpDefault: false,
26
+ short: "p",
27
+ }),
28
+ var: z
29
+ .array(z.string().trim().min(1))
30
+ .default([])
31
+ .describe("Databricks bundle variable assignment")
32
+ .meta({ env: [] }),
33
+ });
34
+
35
+ export const AppKitDeployOptionsSchema = AppKitDeployContextOptionsSchema.extend({
36
+ autoApprove: z
37
+ .boolean()
38
+ .default(false)
39
+ .describe("Skip interactive deployment approval")
40
+ .meta({ env: [] }),
41
+ failOnActiveRuns: z
42
+ .boolean()
43
+ .default(true)
44
+ .describe("Fail when bundle jobs or pipelines are active")
45
+ .meta({ env: [] }),
46
+ skipTests: z
47
+ .boolean()
48
+ .default(true)
49
+ .describe("Skip Databricks CLI tests after the Bun validation")
50
+ .meta({ env: [] }),
51
+ skipValidation: z
52
+ .boolean()
53
+ .default(true)
54
+ .describe("Skip Databricks CLI npm validation after the Bun validation")
55
+ .meta({ env: [] }),
56
+ });
57
+
58
+ export type AppKitDeployOptions = z.output<typeof AppKitDeployOptionsSchema>;
59
+
60
+ interface DeployCommandResult {
61
+ readonly exitCode: number;
62
+ readonly stderr?: string;
63
+ readonly stdout?: string;
64
+ }
65
+
66
+ /** Effects accepted by the AppKit deploy task for focused tests. */
67
+ export interface AppKitDeployDependencies {
68
+ readonly environment?: NodeJS.ProcessEnv;
69
+ readonly logs?: (args: readonly string[], cwd: string) => Promise<DeployCommandResult>;
70
+ readonly plan?: (args: readonly string[], cwd: string) => Promise<DeployCommandResult>;
71
+ readonly root?: string;
72
+ readonly resolveProfile?: () => string | undefined;
73
+ readonly run?: (args: readonly string[], cwd: string) => Promise<DeployCommandResult>;
74
+ readonly validate?: (args: readonly string[], cwd: string) => Promise<DeployCommandResult>;
75
+ }
76
+
77
+ /** Run Databricks Apps deploy while generated workspace manifests are materialized. */
78
+ export async function deployApp(
79
+ args: readonly string[],
80
+ dependencies: AppKitDeployDependencies = {},
81
+ ): Promise<void> {
82
+ const root = dependencies.root ?? process.cwd();
83
+ const options = await resolveDeployOptions(args, dependencies);
84
+ const context = AppKitDeployContextOptionsSchema.parse({
85
+ target: options.target,
86
+ profile: options.profile,
87
+ var: options.var,
88
+ });
89
+ const contextArgs = serializeArgs(context);
90
+ const deployArgs = serializeArgs({
91
+ ...context,
92
+ ...(options.autoApprove ? { autoApprove: true } : {}),
93
+ ...(options.failOnActiveRuns ? { failOnActiveRuns: true } : {}),
94
+ ...(options.skipTests ? { skipTests: true } : {}),
95
+ ...(options.skipValidation ? { skipValidation: true } : {}),
96
+ });
97
+ const validate =
98
+ dependencies.validate ??
99
+ (dependencies.run
100
+ ? undefined
101
+ : (validateArgs: readonly string[], cwd: string) =>
102
+ exec.spawn(
103
+ "databricks",
104
+ ["bundle", "validate", ...serializeArgs({ strict: true }), ...validateArgs],
105
+ {
106
+ cwd,
107
+ env: dependencies.environment ?? process.env,
108
+ stdin: "ignore",
109
+ stdout: "inherit",
110
+ stderr: "inherit",
111
+ },
112
+ ));
113
+ if (validate) {
114
+ const validation = await validate(contextArgs, root);
115
+ if (validation.exitCode !== 0) {
116
+ throw new Error(`databricks bundle validate exited with code ${validation.exitCode}`);
117
+ }
118
+ }
119
+ const run =
120
+ dependencies.run ??
121
+ ((deployArgs: readonly string[], cwd: string) =>
122
+ exec.spawn("databricks", ["apps", "deploy", ...deployArgs], {
123
+ cwd,
124
+ env: dependencies.environment ?? process.env,
125
+ stdin: "inherit",
126
+ stdout: "inherit",
127
+ stderr: "inherit",
128
+ }));
129
+ await withStagedWorkspaceManifests(root, async (stage) => {
130
+ await reconcileStagedBundleState(stage, contextArgs, options.target, dependencies);
131
+ const result = await run(deployArgs, stage);
132
+ if (result.exitCode !== 0) {
133
+ await printAppLogs(contextArgs, stage, dependencies);
134
+ throw new Error(`databricks apps deploy exited with code ${result.exitCode}`);
135
+ }
136
+ });
137
+ }
138
+
139
+ async function reconcileStagedBundleState(
140
+ stage: string,
141
+ contextArgs: readonly string[],
142
+ target: string | undefined,
143
+ dependencies: AppKitDeployDependencies,
144
+ ): Promise<void> {
145
+ const plan =
146
+ dependencies.plan ??
147
+ (dependencies.run
148
+ ? undefined
149
+ : (args: readonly string[], cwd: string) =>
150
+ Promise.resolve(
151
+ exec.spawnSync(
152
+ "databricks",
153
+ ["bundle", "plan", ...serializeArgs({ quiet: true }), ...args],
154
+ {
155
+ cwd,
156
+ env: dependencies.environment ?? process.env,
157
+ stdin: "ignore",
158
+ stdout: "capture",
159
+ stderr: "capture",
160
+ },
161
+ ),
162
+ ));
163
+ if (!plan) return;
164
+ const result = await plan(contextArgs, stage);
165
+ if (result.exitCode === 0) return;
166
+ const output = `${result.stderr ?? ""}\n${result.stdout ?? ""}`.toLowerCase();
167
+ if (
168
+ !output.includes("lineage mismatch in state files") ||
169
+ !output.includes("remote direct state")
170
+ ) {
171
+ return;
172
+ }
173
+ if (!target) return;
174
+ const state = join(stage, ".databricks", "bundle", target, "resources.json");
175
+ if (!existsSync(state)) return;
176
+ rmSync(state);
177
+ logger.warn("Removed stale staged bundle state", { target });
178
+ }
179
+
180
+ async function printAppLogs(
181
+ contextArgs: readonly string[],
182
+ cwd: string,
183
+ dependencies: AppKitDeployDependencies,
184
+ ): Promise<void> {
185
+ logger.error("Databricks Apps deployment failed; recent app logs follow");
186
+ const showLogs =
187
+ dependencies.logs ??
188
+ ((args: readonly string[], logsCwd: string) =>
189
+ exec.spawn("databricks", ["apps", "logs", ...serializeArgs({ tailLines: 200 }), ...args], {
190
+ cwd: logsCwd,
191
+ env: dependencies.environment ?? process.env,
192
+ stdin: "ignore",
193
+ stdout: "inherit",
194
+ stderr: "inherit",
195
+ }));
196
+ try {
197
+ const result = await showLogs(contextArgs, cwd);
198
+ if (result.exitCode !== 0) {
199
+ logger.warn("Could not read Databricks App logs", { exitCode: result.exitCode });
200
+ }
201
+ } catch (error) {
202
+ logger.warn("Could not read Databricks App logs", {
203
+ message: errorUtils.toError(error).message,
204
+ });
205
+ }
206
+ }
207
+
208
+ async function resolveDeployOptions(
209
+ args: readonly string[],
210
+ dependencies: AppKitDeployDependencies,
211
+ ): Promise<AppKitDeployOptions> {
212
+ const parsed = await taskOptions(
213
+ taskCommand(
214
+ import.meta.url,
215
+ "Validate and deploy an AppKit workspace through Databricks Apps",
216
+ AppKitDeployOptionsSchema,
217
+ ),
218
+ AppKitDeployOptionsSchema,
219
+ args,
220
+ );
221
+ const environment = dependencies.environment ?? process.env;
222
+ const hasEnvironmentAuth =
223
+ Boolean(environment.DATABRICKS_HOST) &&
224
+ (Boolean(environment.DATABRICKS_TOKEN) ||
225
+ (Boolean(environment.DATABRICKS_CLIENT_ID) && Boolean(environment.DATABRICKS_CLIENT_SECRET)));
226
+ const configuredProfile = environment.DATABRICKS_CONFIG_PROFILE ?? environment.DATABRICKS_PROFILE;
227
+ const resolvedProfile =
228
+ parsed.profile ??
229
+ configuredProfile ??
230
+ (hasEnvironmentAuth
231
+ ? undefined
232
+ : (dependencies.resolveProfile ?? (() => profile.resolveProfile()?.name))());
233
+ if (!hasEnvironmentAuth && !resolvedProfile) {
234
+ throw new Error(
235
+ "Select a Databricks profile with --profile or configure Databricks environment credentials",
236
+ );
237
+ }
238
+ return AppKitDeployOptionsSchema.parse({
239
+ ...parsed,
240
+ target: parsed.target ?? environment.DATABRICKS_BUNDLE_TARGET,
241
+ profile: resolvedProfile,
242
+ });
243
+ }
244
+
245
+ await runTaskMain(import.meta, () => deployApp(process.argv.slice(2)));
@@ -50,7 +50,8 @@ function hasLifecycle(task: TaskSpec | undefined): boolean {
50
50
  /** Read the root's concrete workspace paths on every run, including newly added members. */
51
51
  export function compilePlan(root: string): CompilePlan {
52
52
  const workspace = readJson<PackageManifest>(join(root, "package.json"));
53
- const typescriptConfigs: string[] = [];
53
+ const rootConfig = join(root, "tsconfig.json");
54
+ const typescriptConfigs: string[] = existsSync(rootConfig) ? [rootConfig] : [];
54
55
  const customPackages: CompilePlan["customPackages"] = [];
55
56
  for (const member of workspace.workspaces ?? []) {
56
57
  const directory = resolve(root, member);
@@ -20,6 +20,9 @@ import { z } from "zod";
20
20
  import { runTaskMain, taskCommand, taskOptions } from "./cli.ts";
21
21
  import { TaskConcurrencyOptionSchema, TaskDryRunOptionSchema } from "./options.ts";
22
22
  import { runTaskCommandAsync } from "../src/_task-command.ts";
23
+ import { materializeWorkspaceManifest } from "../src/workspace-manifest.ts";
24
+
25
+ export { materializeWorkspaceManifest } from "../src/workspace-manifest.ts";
23
26
 
24
27
  const DEFAULT_REGISTRY = "https://registry.npmjs.org";
25
28
  const DEFAULT_PUBLISH_CONCURRENCY = 4;
@@ -52,60 +55,6 @@ export type NpmArchiveManifestTransform = (
52
55
 
53
56
  /** Entry-point fields projected from publishConfig into a packed manifest. */
54
57
  const PUBLISH_CONFIG_ENTRY_FIELDS = ["main", "types", "bin", "exports"] as const;
55
- const DEPENDENCY_FIELDS = [
56
- "dependencies",
57
- "devDependencies",
58
- "optionalDependencies",
59
- "peerDependencies",
60
- ] as const;
61
-
62
- /** Replace workspace and catalogue protocols with publishable registry ranges. */
63
- export function materializeWorkspaceManifest(
64
- source: Readonly<Record<string, unknown>>,
65
- workspace: Readonly<Record<string, unknown>>,
66
- ): Readonly<Record<string, unknown>> {
67
- const manifest = structuredClone(source) as Record<string, unknown>;
68
- const version = workspace.version;
69
- const catalog =
70
- workspace.catalog && typeof workspace.catalog === "object"
71
- ? (workspace.catalog as Readonly<Record<string, unknown>>)
72
- : {};
73
- if (typeof version !== "string" || !version) {
74
- throw new Error("Workspace manifest has no version");
75
- }
76
- for (const field of DEPENDENCY_FIELDS) {
77
- const dependencies = manifest[field];
78
- if (!dependencies || typeof dependencies !== "object") continue;
79
- manifest[field] = Object.fromEntries(
80
- Object.entries(dependencies).map(([name, value]) => [
81
- name,
82
- materializeDependency(name, value, version, catalog),
83
- ]),
84
- );
85
- }
86
- return manifest;
87
- }
88
-
89
- function materializeDependency(
90
- name: string,
91
- value: unknown,
92
- version: string,
93
- catalog: Readonly<Record<string, unknown>>,
94
- ): unknown {
95
- if (value === "catalog:") {
96
- const resolved = catalog[name];
97
- if (typeof resolved !== "string" || !resolved) {
98
- throw new Error(`Workspace catalog has no version for ${name}`);
99
- }
100
- return resolved;
101
- }
102
- if (typeof value !== "string" || !value.startsWith("workspace:")) return value;
103
- const selector = value.slice("workspace:".length);
104
- if (selector === "*" || !selector) return version;
105
- if (selector === "^" || selector === "~") return `${selector}${version}`;
106
- return selector;
107
- }
108
-
109
58
  /**
110
59
  * Fold compiled publish entry points onto an archive manifest without changing
111
60
  * the source checkout.