@dbx-tools/projen 0.9.79 → 0.9.95

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 CHANGED
@@ -308,6 +308,18 @@ Built-in tag mixins set runtime defaults for `shared`, `node`, `cli`, `server`,
308
308
  and `ui`. Repo-specific mixins layer package-specific dependencies,
309
309
  scripts, and generated files on top.
310
310
 
311
+ Every workspace root exposes a generic development watcher:
312
+
313
+ ```sh
314
+ bun run dev:watch [--debounce-ms 15000] [--restart-key r] <command> [args...]
315
+ ```
316
+
317
+ It restarts after watched writes have been quiet for 15 seconds. Command paths,
318
+ `--cwd`, and `--filter` identify the owning package; its transitive workspace
319
+ dependencies are watched with the shared generated/dependency/test/temp ignores.
320
+ Press the configured key for an immediate restart. The demo uses this task to
321
+ run its concrete server entrypoint; the `server` tag does not assume one.
322
+
311
323
  AppKit 0.81's `appkitServerConfig()` is the preferred tsdown preset for a
312
324
  consumer app using conventional compiled `server/agents/*/agent.ts` entries. It
313
325
  does not replace this engine's package discovery, Bun browser build, source-run
@@ -488,11 +500,12 @@ bun run release --no-release-notes
488
500
  bun run release --release-notes-instructions "Focus on operator-visible changes."
489
501
  ```
490
502
 
491
- `--demo-deploy` is off by default. After tagging and any local registry
492
- publication it runs `bun run demo:deploy`, which stages the AppKit demo
493
- from locally compiled Node packages and locally built Python wheels, resolves
494
- the configured/default workspace profile through `@dbx-tools/auth`, and deploys
495
- it. That step is local only and is not recorded in the annotated tag.
503
+ `--demo-deploy` is off by default and requires `--profile <name>`. After tagging
504
+ and any local registry publication it runs `bun run demo:deploy`, which stages
505
+ the AppKit demo from locally compiled Node packages and locally built Python
506
+ wheels, resolves only the selected workspace profile through `@dbx-tools/auth`,
507
+ force-refreshes its token with interactive login fallback, and deploys it. That
508
+ step is local only and is not recorded in the annotated tag.
496
509
 
497
510
  After bump and configured validation, release writes
498
511
  `docs/releases/vX.Y.Z.md` with `dbx genie exec -C "$PWD" --sandbox read-only
@@ -579,6 +592,7 @@ release [options]
579
592
  | `--no-release-notes` | skip writing docs/releases notes (Genie and git-log fallback) |
580
593
  | `--release-notes-instructions <text>` | append custom instructions to the Genie release-notes prompt |
581
594
  | `--demo-deploy` | after tagging, stage and deploy the AppKit demo app (off by default) |
595
+ | `--profile <name>` | Databricks CLI profile for --demo-deploy |
582
596
  | `--no-local-publish` | skip publishing to configured local registries |
583
597
  | `--local-registry <auto\|false\|url>` | local npm registry selection (default: "auto") |
584
598
  | `--local-pypi <auto\|false\|url>` | local devpi registry selection (default: "auto") |
package/index.ts CHANGED
@@ -3,13 +3,14 @@
3
3
  // Hand edits are overwritten on the next watch; this file is read-only.
4
4
 
5
5
  export const PACKAGE_IDENTIFIER = "@dbx-tools/projen";
6
- export const PACKAGE_VERSION = "0.9.79";
6
+ export const PACKAGE_VERSION = "0.9.95";
7
7
  export * as barrels from "./src/barrels.ts";
8
8
  export * as bunApp from "./src/bun-app.ts";
9
9
  export * as bunWorkflow from "./src/bun-workflow.ts";
10
10
  export * as clean from "./src/clean.ts";
11
11
  export * as codegen from "./src/codegen.ts";
12
12
  export * as dbxToolsConfig from "./src/dbx-tools-config.ts";
13
+ export * as devWatch from "./src/dev-watch.ts";
13
14
  export * as engineRoot from "./src/engine-root.ts";
14
15
  export * as generated from "./src/generated.ts";
15
16
  export * as mixin from "./src/mixin.ts";
@@ -40,6 +41,7 @@ export { BUN_DEV_OVERRIDE, BUN_BUILD_OVERRIDE, BUN_APP_OVERRIDES, RootBunfigFile
40
41
  export { BUN_VERSION } from "./src/bun-workflow.ts";
41
42
  export { DBXToolsConfigDataSchema, DBXToolsConfig } from "./src/dbx-tools-config.ts";
42
43
  export type { DBXToolsConfigOptions, DBXToolsConfigData } from "./src/dbx-tools-config.ts";
44
+ export { DEV_WATCH_TASK, DEV_WATCH_SCRIPT, DEV_RESTART_DEBOUNCE_MS, DEV_RESTART_KEY, SERVER_WATCH_DISABLED_ENV } from "./src/dev-watch.ts";
43
45
  export { resolvePkgRoot } from "./src/engine-root.ts";
44
46
  export { PYTHON_GENERATED_PACKAGE, PYTHON_SYNC_PACKAGE } from "./src/generated.ts";
45
47
  export type { HeaderOpts } from "./src/generated.ts";
package/package.json CHANGED
@@ -30,9 +30,9 @@
30
30
  },
31
31
  "dependencies": {
32
32
  "@clack/prompts": "^1.7.0",
33
- "@dbx-tools/core": "^0.9.79",
34
- "@dbx-tools/path": "^0.9.79",
35
- "@dbx-tools/shared-core": "^0.9.79",
33
+ "@dbx-tools/core": "^0.9.95",
34
+ "@dbx-tools/path": "^0.9.95",
35
+ "@dbx-tools/shared-core": "^0.9.95",
36
36
  "commander": "^15.0.0",
37
37
  "concurrently": "^10.0.3",
38
38
  "constructs": "^10.6.0",
@@ -54,7 +54,7 @@
54
54
  },
55
55
  "main": "index.ts",
56
56
  "license": "Apache-2.0",
57
- "version": "0.9.79",
57
+ "version": "0.9.95",
58
58
  "types": "index.ts",
59
59
  "type": "module",
60
60
  "exports": {
@@ -0,0 +1,15 @@
1
+ /** Root task and CLI defaults for the generic development command watcher. */
2
+
3
+ /** Projen task exposed as `bun run dev:watch`. */
4
+ export const DEV_WATCH_TASK = "dev:watch";
5
+ /** Shipped task entrypoint invoked by {@link DEV_WATCH_TASK}. */
6
+ export const DEV_WATCH_SCRIPT = "dev-watch.ts";
7
+ /** Quiet period after the last watched change before restarting. */
8
+ export const DEV_RESTART_DEBOUNCE_MS = 15_000;
9
+ /** Interactive key that requests an immediate restart. */
10
+ export const DEV_RESTART_KEY = "r";
11
+ /**
12
+ * Process environment that skips wrapping or running the file watcher.
13
+ * Parsed with `configUtils.boolean` (`1` / `true` / `yes` disable watch).
14
+ */
15
+ export const SERVER_WATCH_DISABLED_ENV = "SERVER_WATCH_DISABLED";
@@ -109,6 +109,7 @@ const DEFAULT_ALLOW_BUILDS: AllowBuilds = {
109
109
  const DEFAULT_OVERRIDES: Readonly<Record<string, string>> = {
110
110
  bun: BUN_VERSION,
111
111
  glob: "^13.0.0",
112
+ "p-map": "7.0.8",
112
113
  };
113
114
 
114
115
  /**
package/src/project-js.ts CHANGED
@@ -21,6 +21,7 @@ import { BUN_APP_OVERRIDES, RootBunfigFile } from "./bun-app.ts";
21
21
  import { BUN_VERSION } from "./bun-workflow.ts";
22
22
  import { codegenModulePaths, generateCodegen } from "./codegen.ts";
23
23
  import { DBXToolsConfig, type DBXToolsConfigOptions } from "./dbx-tools-config.ts";
24
+ import { DEV_WATCH_SCRIPT, DEV_WATCH_TASK } from "./dev-watch.ts";
24
25
  import { resolvePkgRoot } from "./engine-root.ts";
25
26
  import * as mixin from "./mixin.ts";
26
27
  import {
@@ -1070,6 +1071,11 @@ function resolveTags(p: DiscoveredPackage, tagPaths: Record<string, string[]>):
1070
1071
  function registerRootTasks(project: javascript.NodeProject): void {
1071
1072
  project.addTask("barrels", { execArgs: taskCommand("barrels.ts") });
1072
1073
  project.addTask("clean", { execArgs: taskCommand("clean.ts"), receiveArgs: true });
1074
+ project.addTask(DEV_WATCH_TASK, {
1075
+ description: "Restart a development command after watched changes settle",
1076
+ execArgs: taskCommand(DEV_WATCH_SCRIPT),
1077
+ receiveArgs: true,
1078
+ });
1073
1079
  project.addTask("sync", {
1074
1080
  execArgs: taskCommand("sync.ts"),
1075
1081
  receiveArgs: true,
package/src/tags.ts CHANGED
@@ -163,15 +163,11 @@ export const PACKAGE_TAG_MIXINS = {
163
163
  addPackageFiles(p, "bin");
164
164
  }),
165
165
  server: create(projectPredicate.hasTag("server"), (p) => {
166
- // A Node/Express service. `dev` and `start` run `src/server.ts` directly
167
- // through Bun, while package compilation remains native TypeScript.
166
+ // A Node/Express service. The root's generic `dev:watch` task owns watched
167
+ // execution because this tag cannot assume an entrypoint or command shape.
168
168
  p.addDeps("express@catalog:");
169
169
  p.addDevDeps("@types/node@catalog:", "@types/express@catalog:");
170
170
  applyCompilerOptions(p, NODE_COMPILER_OPTIONS);
171
- // bun runs the server `.ts` directly (native TS, no tsx). `--watch` restarts
172
- // on change - the tsx-watch replacement.
173
- (p.tasks.tryFind("dev") ?? p.addTask("dev")).reset("bun --watch src/server.ts");
174
- (p.tasks.tryFind("start") ?? p.addTask("start")).reset("bun src/server.ts");
175
171
  }),
176
172
  node: create(projectPredicate.hasTag("node"), (p) => {
177
173
  p.addDevDeps("@types/node@catalog:");
@@ -0,0 +1,339 @@
1
+ #!/usr/bin/env -S bun
2
+ /** Restart a development command after relevant workspace changes settle. */
3
+ import { isAbsolute, relative, resolve } from "node:path";
4
+ import { configUtils } from "@dbx-tools/core";
5
+ import * as exec from "@dbx-tools/core/exec";
6
+ import { watch as pathWatch } from "@dbx-tools/path";
7
+ import { log } from "@dbx-tools/shared-core";
8
+ import { Command, CommanderError, InvalidArgumentError, Option } from "commander";
9
+ import {
10
+ DEV_RESTART_DEBOUNCE_MS,
11
+ DEV_RESTART_KEY,
12
+ DEV_WATCH_TASK,
13
+ SERVER_WATCH_DISABLED_ENV,
14
+ } from "../src/dev-watch.ts";
15
+ import {
16
+ readPackageManifest,
17
+ recordedPackages,
18
+ resolveRepoRoot,
19
+ workspaceDependencyDirectories,
20
+ } from "../src/packages.ts";
21
+
22
+ const logger = log.logger("projen:dev-watch");
23
+ const SHUTDOWN_TIMEOUT_MS = 2_000;
24
+
25
+ export interface DevWatchOptions {
26
+ readonly command: readonly string[];
27
+ readonly debounceMs: number;
28
+ readonly restartKey: string;
29
+ /** When true, run the command once and skip file-change restarts. */
30
+ readonly serverWatchDisabled: boolean;
31
+ }
32
+
33
+ interface WorkspacePackage {
34
+ readonly dir: string;
35
+ readonly name?: string;
36
+ }
37
+
38
+ function debounceMilliseconds(value: string): number {
39
+ const parsed = configUtils.toNumber(value);
40
+ if (parsed === undefined || parsed < 0) {
41
+ throw new InvalidArgumentError("must be a non-negative number of milliseconds");
42
+ }
43
+ return parsed;
44
+ }
45
+
46
+ function singleKey(value: string): string {
47
+ if (Array.from(value).length !== 1) {
48
+ throw new InvalidArgumentError("must be exactly one character");
49
+ }
50
+ return value;
51
+ }
52
+
53
+ /** Parse watcher flags while passing every token after the command through unchanged. */
54
+ export function parseDevWatchOptions(args: string[]): DevWatchOptions {
55
+ const program = new Command()
56
+ .name(DEV_WATCH_TASK)
57
+ .description("Restart a development command after watched changes settle")
58
+ .exitOverride()
59
+ .configureOutput({
60
+ writeErr: (message) => {
61
+ if (import.meta.main) process.stderr.write(message);
62
+ },
63
+ })
64
+ .enablePositionalOptions()
65
+ .passThroughOptions()
66
+ .addOption(
67
+ new Option("--debounce-ms <milliseconds>", "quiet period before restart")
68
+ .default(DEV_RESTART_DEBOUNCE_MS)
69
+ .argParser(debounceMilliseconds),
70
+ )
71
+ .addOption(
72
+ new Option("--restart-key <key>", "interactive immediate-restart key")
73
+ .default(DEV_RESTART_KEY)
74
+ .argParser(singleKey),
75
+ )
76
+ .addOption(
77
+ new Option(
78
+ "--server-watch-disabled",
79
+ `run the command once without watching (${SERVER_WATCH_DISABLED_ENV})`,
80
+ ),
81
+ )
82
+ .argument("<command>")
83
+ .argument("[args...]")
84
+ .parse(["bun", DEV_WATCH_TASK, ...args]);
85
+ const options = program.opts<{
86
+ debounceMs: number;
87
+ restartKey: string;
88
+ serverWatchDisabled?: boolean;
89
+ }>();
90
+ const [command, commandArgs = []] = program.processedArgs as [string, string[]?];
91
+ const serverWatchDisabled =
92
+ options.serverWatchDisabled === true ||
93
+ configUtils.boolean(undefined, SERVER_WATCH_DISABLED_ENV, configUtils.ENV_ONLY) === true;
94
+ if (serverWatchDisabled) process.env[SERVER_WATCH_DISABLED_ENV] = "1";
95
+ return {
96
+ command: [command, ...commandArgs],
97
+ debounceMs: options.debounceMs,
98
+ restartKey: options.restartKey,
99
+ serverWatchDisabled,
100
+ };
101
+ }
102
+
103
+ function containsPath(parent: string, candidate: string): boolean {
104
+ const rel = relative(parent, candidate);
105
+ return rel === "" || (!rel.startsWith("..") && !isAbsolute(rel));
106
+ }
107
+
108
+ function workspacePackages(root: string): WorkspacePackage[] {
109
+ return recordedPackages(root)
110
+ .map(({ dir }) => {
111
+ const name = readPackageManifest(dir)?.name;
112
+ return { dir, name: typeof name === "string" ? name : undefined };
113
+ })
114
+ .sort((left, right) => right.dir.length - left.dir.length);
115
+ }
116
+
117
+ function optionArgument(
118
+ command: readonly string[],
119
+ longName: string,
120
+ shortName?: string,
121
+ ): string | undefined {
122
+ for (let index = 0; index < command.length; index++) {
123
+ const token = command[index]!;
124
+ if (token === longName || (shortName !== undefined && token === shortName)) {
125
+ return command[index + 1];
126
+ }
127
+ if (token.startsWith(`${longName}=`)) return token.slice(longName.length + 1);
128
+ if (shortName !== undefined && token.startsWith(`${shortName}=`)) {
129
+ return token.slice(shortName.length + 1);
130
+ }
131
+ }
132
+ return undefined;
133
+ }
134
+
135
+ function commandWorkingDirectory(command: readonly string[], cwd: string): string {
136
+ const configured = optionArgument(command, "--cwd");
137
+ return configured ? resolve(cwd, configured) : cwd;
138
+ }
139
+
140
+ function commandPackage(
141
+ command: readonly string[],
142
+ cwd: string,
143
+ packages: readonly WorkspacePackage[],
144
+ ): WorkspacePackage | undefined {
145
+ const selected = optionArgument(command, "--filter", "-F");
146
+ if (selected) {
147
+ const filtered = packages.find(({ name }) => name === selected);
148
+ if (filtered) return filtered;
149
+ }
150
+
151
+ const commandCwd = commandWorkingDirectory(command, cwd);
152
+ for (const token of command) {
153
+ if (token.startsWith("-")) continue;
154
+ const candidate = resolve(commandCwd, token);
155
+ const owner = packages.find(({ dir }) => containsPath(dir, candidate));
156
+ if (owner) return owner;
157
+ }
158
+ return packages.find(({ dir }) => containsPath(dir, commandCwd));
159
+ }
160
+
161
+ /** Source directories most likely to affect the command, using workspace dependency metadata. */
162
+ export function devWatchDirectories(
163
+ command: readonly string[],
164
+ cwd: string = process.cwd(),
165
+ ): string[] {
166
+ const root = resolveRepoRoot(cwd);
167
+ const packages = workspacePackages(root);
168
+ const owner = commandPackage(command, cwd, packages);
169
+ if (!owner) return [commandWorkingDirectory(command, cwd)];
170
+ const dependencies = owner.name ? workspaceDependencyDirectories(owner.name, root) : [];
171
+ return dependencies.length > 0 ? dependencies : [owner.dir];
172
+ }
173
+
174
+ /** Compare one raw terminal character with the configured restart key. */
175
+ export function isRestartKey(key: string, restartKey: string): boolean {
176
+ return key === restartKey || key.toLocaleLowerCase() === restartKey.toLocaleLowerCase();
177
+ }
178
+
179
+ /** Supervise one command until the user or host stops the task. */
180
+ export async function main(args: string[] = process.argv.slice(2)): Promise<void> {
181
+ let options: DevWatchOptions;
182
+ try {
183
+ options = parseDevWatchOptions(args);
184
+ } catch (err) {
185
+ if (!(err instanceof CommanderError)) throw err;
186
+ if (err.exitCode !== 0) process.exitCode = err.exitCode;
187
+ return;
188
+ }
189
+ const cwd = process.cwd();
190
+ if (options.serverWatchDisabled) {
191
+ logger.info(`${SERVER_WATCH_DISABLED_ENV} is set; running without file watching`);
192
+ const [command, ...commandArgs] = options.command;
193
+ const { exitCode } = await exec.spawn(command!, commandArgs, {
194
+ cwd,
195
+ stdin: "ignore",
196
+ stdout: "inherit",
197
+ stderr: "inherit",
198
+ check: false,
199
+ });
200
+ if (exitCode !== 0) process.exitCode = exitCode;
201
+ return;
202
+ }
203
+
204
+ const root = resolveRepoRoot(cwd);
205
+ const watchDirectories = devWatchDirectories(options.command, cwd);
206
+ const commandLabel = options.command.map((part) => JSON.stringify(part)).join(" ");
207
+ let child: exec.ChildProcessResult | undefined;
208
+ let timer: ReturnType<typeof setTimeout> | undefined;
209
+ let restartReason: string | undefined;
210
+ let restarting = false;
211
+ let stopping = false;
212
+
213
+ const watcher = pathWatch.watchFiles(watchDirectories, {
214
+ cwd: root,
215
+ ignoreInitial: true,
216
+ });
217
+
218
+ async function stopChild(): Promise<void> {
219
+ const current = child;
220
+ if (!current) return;
221
+ child = undefined;
222
+ try {
223
+ await exec.kill(current, {
224
+ gracefulTimeoutMs: SHUTDOWN_TIMEOUT_MS,
225
+ forceTimeoutMs: SHUTDOWN_TIMEOUT_MS,
226
+ });
227
+ } catch (err) {
228
+ logger.error("failed to stop command process tree:", err);
229
+ }
230
+ }
231
+
232
+ function startChild(reason: string): void {
233
+ logger.info(`${reason}; starting ${commandLabel}`);
234
+ const [command, ...commandArgs] = options.command;
235
+ const current = exec.spawn(command!, commandArgs, {
236
+ cwd,
237
+ stdin: "ignore",
238
+ stdout: "inherit",
239
+ stderr: "inherit",
240
+ check: false,
241
+ });
242
+ child = current;
243
+ void current
244
+ .then(({ exitCode }) => {
245
+ if (child !== current || stopping) return;
246
+ child = undefined;
247
+ logger.warn(
248
+ `command exited with code ${exitCode}; press ${JSON.stringify(options.restartKey)} or change a watched file to restart`,
249
+ );
250
+ })
251
+ .catch((err) => {
252
+ if (child !== current || stopping) return;
253
+ child = undefined;
254
+ logger.error("command failed:", err);
255
+ });
256
+ }
257
+
258
+ async function drainRestarts(): Promise<void> {
259
+ if (restarting) return;
260
+ restarting = true;
261
+ try {
262
+ while (restartReason !== undefined && !stopping) {
263
+ const reason = restartReason;
264
+ restartReason = undefined;
265
+ await stopChild();
266
+ if (!stopping) startChild(reason);
267
+ }
268
+ } finally {
269
+ restarting = false;
270
+ }
271
+ }
272
+
273
+ function requestRestart(reason: string): void {
274
+ restartReason = reason;
275
+ void drainRestarts();
276
+ }
277
+
278
+ function scheduleRestart(): void {
279
+ clearTimeout(timer);
280
+ timer = setTimeout(
281
+ () => requestRestart(`changes settled for ${options.debounceMs}ms`),
282
+ options.debounceMs,
283
+ );
284
+ }
285
+
286
+ watcher.on("all", scheduleRestart);
287
+ watcher.on("error", (err) => logger.error("watcher error:", err));
288
+ watcher.on("ready", () => {
289
+ const packages = watchDirectories.length;
290
+ logger.info(
291
+ `watching ${packages} workspace ${packages === 1 ? "directory" : "directories"} with ${options.debounceMs}ms restart debounce`,
292
+ );
293
+ logger.info(`press ${JSON.stringify(options.restartKey)} to restart; Ctrl-C to stop`);
294
+ });
295
+
296
+ const stdin = process.stdin;
297
+ const wasRaw = stdin.isTTY ? Boolean(stdin.isRaw) : false;
298
+ const onInput = (chunk: Buffer | string): void => {
299
+ for (const key of String(chunk)) {
300
+ if (key === "\u0003") {
301
+ void shutdown();
302
+ return;
303
+ }
304
+ if (isRestartKey(key, options.restartKey)) {
305
+ clearTimeout(timer);
306
+ requestRestart("manual restart requested");
307
+ }
308
+ }
309
+ };
310
+ if (stdin.isTTY) stdin.setRawMode(true);
311
+ stdin.resume();
312
+ stdin.on("data", onInput);
313
+
314
+ let finish!: () => void;
315
+ const complete = new Promise<void>((resolveComplete) => {
316
+ finish = resolveComplete;
317
+ });
318
+
319
+ async function shutdown(): Promise<void> {
320
+ if (stopping) return;
321
+ stopping = true;
322
+ clearTimeout(timer);
323
+ restartReason = undefined;
324
+ stdin.off("data", onInput);
325
+ if (stdin.isTTY) stdin.setRawMode(wasRaw);
326
+ stdin.pause();
327
+ await Promise.allSettled([watcher.close(), stopChild()]);
328
+ finish();
329
+ }
330
+
331
+ for (const signal of ["SIGINT", "SIGTERM", "SIGHUP"] as const) {
332
+ process.once(signal, () => void shutdown());
333
+ }
334
+
335
+ requestRestart("initial start");
336
+ await complete;
337
+ }
338
+
339
+ if (import.meta.main) await main();
@@ -72,6 +72,7 @@ function pythonProjects(root: string): PythonProjectFile[] {
72
72
  return allProjects;
73
73
  }
74
74
 
75
+ /** Build and upload every selected public Python workspace distribution. */
75
76
  export function publishPythonProjects(options: {
76
77
  readonly dryRun?: boolean;
77
78
  readonly indexUrl: string;
@@ -180,6 +181,7 @@ function buildPythonProjectsInternal(
180
181
  }
181
182
  }
182
183
 
184
+ /** Parse command-line publication options and run the Python release task. */
183
185
  export async function main(): Promise<void> {
184
186
  const program = new Command();
185
187
  program
package/tasks/release.ts CHANGED
@@ -71,7 +71,9 @@ function pushAnnotatedReleaseTag(options: {
71
71
  runTaskCommand(root, "git", ["tag", "--annotate", tag, "--message", annotation]);
72
72
  const githubAccount = projectUtils.resolveProjectGhAccount(root, { remote });
73
73
  if (!githubAccount) {
74
- logger.warn("no authenticated GitHub account can access the release repository; using git push");
74
+ logger.warn(
75
+ "no authenticated GitHub account can access the release repository; using git push",
76
+ );
75
77
  runTaskCommand(root, "git", ["push", remote, `refs/tags/${tag}`]);
76
78
  return;
77
79
  }
@@ -228,6 +230,8 @@ export async function runRelease(
228
230
  readonly validationTasks?: readonly string[];
229
231
  /** After tagging, stage and deploy the AppKit demo app. Off by default. */
230
232
  readonly demoDeploy?: boolean;
233
+ /** Explicit Databricks CLI profile used by the optional demo deployment. */
234
+ readonly demoProfile?: string;
231
235
  /** Write `docs/releases/vX.Y.Z.md` via Genie, with a git-log fallback. On by default. */
232
236
  readonly releaseNotes?: boolean;
233
237
  /** Additional instructions appended to the standard Genie release-notes prompt. */
@@ -238,6 +242,11 @@ export async function runRelease(
238
242
  const { branch, prefix, remote, root } = options;
239
243
  const selection = releaseStepSelection(options);
240
244
  const install = options.install ?? "auto";
245
+ const demoProfile = options.demoProfile?.trim();
246
+
247
+ if (options.demoDeploy && !demoProfile) {
248
+ throw new Error("--demo-deploy requires --profile <name>");
249
+ }
241
250
 
242
251
  if (install === "always") runTaskCommand(root, "bun", ["install"]);
243
252
  if (selection.validation && hasPackageScript(root, "eslint:fix")) {
@@ -331,8 +340,8 @@ export async function runRelease(
331
340
  });
332
341
  }
333
342
 
334
- if (options.demoDeploy) {
335
- runTaskCommand(root, "bun", ["run", "demo:deploy"]);
343
+ if (options.demoDeploy && demoProfile) {
344
+ runTaskCommand(root, "bun", ["run", "demo:deploy", "--profile", demoProfile]);
336
345
  }
337
346
 
338
347
  return tag;
@@ -379,6 +388,7 @@ export function createReleaseCommand(): Command {
379
388
  "append custom instructions to the Genie release-notes prompt",
380
389
  )
381
390
  .option("--demo-deploy", "after tagging, stage and deploy the AppKit demo app (off by default)")
391
+ .option("--profile <name>", "Databricks CLI profile for --demo-deploy")
382
392
  .option("--no-local-publish", "skip publishing to configured local registries")
383
393
  .option("--local-registry <auto|false|url>", "local npm registry selection", "auto")
384
394
  .option("--local-pypi <auto|false|url>", "local devpi registry selection", "auto")
@@ -397,6 +407,7 @@ export function createReleaseCommand(): Command {
397
407
  install: ReleaseInstallMode;
398
408
  validate: string[];
399
409
  demoDeploy?: boolean;
410
+ profile?: string;
400
411
  releaseNotes?: boolean;
401
412
  releaseNotesInstructions?: string;
402
413
  },
@@ -419,6 +430,7 @@ export function createReleaseCommand(): Command {
419
430
  validation: options.validation,
420
431
  validationTasks: options.validate,
421
432
  demoDeploy: options.demoDeploy,
433
+ demoProfile: options.profile,
422
434
  releaseNotes: options.releaseNotes,
423
435
  releaseNotesInstructions: options.releaseNotesInstructions,
424
436
  });