@dbx-tools/cli-service 0.9.40 → 0.9.46

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/service.ts CHANGED
@@ -3,7 +3,8 @@
3
3
  *
4
4
  * This module owns current-user service compilation and lifecycle across
5
5
  * consuming CLIs. Reuse {@link CliService} rather than adding product-specific
6
- * installers, startup registration, runtime directories, or control sockets.
6
+ * installers, uv Python environments, startup registration, runtime
7
+ * directories, or control sockets.
7
8
  *
8
9
  * @module
9
10
  */
@@ -26,7 +27,11 @@ import {
26
27
  resolveServicePaths,
27
28
  type ServiceRuntimeContext,
28
29
  } from "./_paths.ts";
29
- import { externalRuntimePackages, installServiceRuntime } from "./_runtime.ts";
30
+ import {
31
+ externalRuntimePackages,
32
+ installServicePythonPackage,
33
+ installServiceRuntime,
34
+ } from "./_runtime.ts";
30
35
  import { installStartup, removeStartup, type ServiceLaunch } from "./_startup.ts";
31
36
  import {
32
37
  CliServiceDefinitionSchema,
@@ -54,6 +59,15 @@ export type CliServiceRuntimeInstaller = (
54
59
  dependencies: Readonly<Record<string, string>>,
55
60
  ) => Promise<void>;
56
61
 
62
+ /** Install a Python package into a service-owned uv environment. */
63
+ export type CliServicePythonInstaller = (
64
+ uvExecutable: string,
65
+ directory: string,
66
+ packageSpecifier: string,
67
+ python: string,
68
+ platform: NodeJS.Platform,
69
+ ) => Promise<string>;
70
+
57
71
  /** Host runtime overrides for tests, embedded distributions, and nonstandard homes. */
58
72
  export interface CliServiceRuntimeOptions {
59
73
  /** Operating system used for configuration and startup registration. */
@@ -68,10 +82,14 @@ export interface CliServiceRuntimeOptions {
68
82
  readonly globalHomeDirectory?: string;
69
83
  /** Package-provided Bun executable override. */
70
84
  readonly bunExecutable?: string;
85
+ /** uv executable used for managed Python package environments. */
86
+ readonly uvExecutable?: string;
71
87
  /** Standalone compiler override. */
72
88
  readonly compile?: CliServiceCompiler;
73
89
  /** External runtime dependency installer override. */
74
90
  readonly installRuntime?: CliServiceRuntimeInstaller;
91
+ /** Python package installer override. */
92
+ readonly installPython?: CliServicePythonInstaller;
75
93
  /** System-tray host source entrypoint override. */
76
94
  readonly hostEntrypoint?: string;
77
95
  /** systray2 native executable override. */
@@ -119,8 +137,10 @@ export class CliService implements CliServiceLifecycle {
119
137
  private readonly runtime: ServiceRuntimeContext;
120
138
  private readonly globalHomeDirectory: string;
121
139
  private readonly bunExecutable: string;
140
+ private readonly uvExecutable: string;
122
141
  private readonly compiler: CliServiceCompiler;
123
142
  private readonly runtimeInstaller: CliServiceRuntimeInstaller;
143
+ private readonly pythonInstaller: CliServicePythonInstaller;
124
144
  private readonly hostEntrypoint: string;
125
145
  private readonly trayExecutable: string;
126
146
 
@@ -142,11 +162,13 @@ export class CliService implements CliServiceLifecycle {
142
162
  this.globalHomeDirectory =
143
163
  options.globalHomeDirectory ?? join(this.runtime.homeDirectory, ".dbx-tools");
144
164
  this.bunExecutable = options.bunExecutable ?? resolveBunExecutable();
165
+ this.uvExecutable = options.uvExecutable ?? "uv";
145
166
  this.compiler =
146
167
  options.compile ??
147
168
  ((entrypoint, output, workingDirectory, external) =>
148
169
  compileWithBun(this.bunExecutable, entrypoint, output, workingDirectory, external));
149
170
  this.runtimeInstaller = options.installRuntime ?? installServiceRuntime;
171
+ this.pythonInstaller = options.installPython ?? installServicePythonPackage;
150
172
  this.hostEntrypoint = options.hostEntrypoint ?? resolveHostEntrypoint();
151
173
  this.trayExecutable = options.trayExecutable ?? resolveTrayExecutable(this.runtime.platform);
152
174
  }
@@ -162,7 +184,7 @@ export class CliService implements CliServiceLifecycle {
162
184
  Object.fromEntries(Object.entries(dependencies).filter(([name]) => external.has(name))),
163
185
  );
164
186
  const paths = resolveServicePaths(this.definition, this.runtime);
165
- const installedDefinition = await this.installDefinition(owner);
187
+ const installedDefinition = await this.installDefinition(owner, paths.directory);
166
188
  const host = await this.ensureCompiledBinary(
167
189
  this.binaryName(installedDefinition, "service"),
168
190
  this.hostEntrypoint,
@@ -270,13 +292,39 @@ export class CliService implements CliServiceLifecycle {
270
292
  };
271
293
  }
272
294
 
273
- private async installDefinition(owner: ServicePackage): Promise<CliServiceDefinition> {
295
+ private async installDefinition(
296
+ owner: ServicePackage,
297
+ directory: string,
298
+ ): Promise<CliServiceDefinition> {
274
299
  const definition = CliServiceDefinitionSchema.parse({
275
300
  ...this.definition,
276
301
  version: this.definition.version ?? owner.version,
277
302
  });
303
+ if (!definition.version) throw new Error(`${definition.name} has no installable version`);
304
+ const pythonPackage = definition.pythonPackage
305
+ ? {
306
+ ...definition.pythonPackage,
307
+ version: definition.pythonPackage.version ?? definition.version,
308
+ }
309
+ : undefined;
310
+ const pythonEnvironment: Record<string, string> = {};
311
+ if (pythonPackage) {
312
+ pythonEnvironment.PYTHON = await this.pythonInstaller(
313
+ this.uvExecutable,
314
+ join(directory, "python"),
315
+ `${pythonPackage.name}==${pythonPackage.version}`,
316
+ pythonPackage.python,
317
+ this.runtime.platform,
318
+ );
319
+ }
278
320
  const command = definition.command
279
- ? await this.installCommand(definition, owner, definition.command, "command")
321
+ ? await this.installCommand(
322
+ definition,
323
+ owner,
324
+ definition.command,
325
+ "command",
326
+ pythonEnvironment,
327
+ )
280
328
  : undefined;
281
329
  const menu: CliServiceMenuItem[] = [];
282
330
  for (const [index, item] of (definition.menu ?? []).entries()) {
@@ -289,6 +337,7 @@ export class CliService implements CliServiceLifecycle {
289
337
  owner,
290
338
  item.command,
291
339
  `menu-${index + 1}`,
340
+ pythonEnvironment,
292
341
  ),
293
342
  }
294
343
  : item,
@@ -296,6 +345,7 @@ export class CliService implements CliServiceLifecycle {
296
345
  }
297
346
  return CliServiceDefinitionSchema.parse({
298
347
  ...definition,
348
+ ...(pythonPackage ? { pythonPackage } : {}),
299
349
  ...(command ? { command } : {}),
300
350
  ...(menu.length > 0 ? { menu } : {}),
301
351
  });
@@ -306,8 +356,15 @@ export class CliService implements CliServiceLifecycle {
306
356
  owner: ServicePackage,
307
357
  command: CliServiceCommand,
308
358
  purpose: string,
359
+ runtimeEnvironment: Readonly<Record<string, string>>,
309
360
  ): Promise<CliServiceCommand> {
310
- if (command.executable) return command;
361
+ const environment = { ...command.environment, ...runtimeEnvironment };
362
+ if (command.executable) {
363
+ return {
364
+ ...command,
365
+ ...(Object.keys(environment).length > 0 ? { environment } : {}),
366
+ };
367
+ }
311
368
  const entrypoint = command.entrypoint ?? owner.bin(command.binName);
312
369
  const installed = await this.ensureCompiledBinary(
313
370
  this.binaryName(definition, purpose),
@@ -318,7 +375,7 @@ export class CliService implements CliServiceLifecycle {
318
375
  return {
319
376
  executable: installed.path,
320
377
  ...(command.arguments ? { arguments: command.arguments } : {}),
321
- ...(command.environment ? { environment: command.environment } : {}),
378
+ ...(Object.keys(environment).length > 0 ? { environment } : {}),
322
379
  cwd: command.cwd ?? this.globalHomeDirectory,
323
380
  };
324
381
  }