@dbx-tools/cli-service 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/src/icon.ts CHANGED
@@ -1,7 +1,7 @@
1
1
  import { deflateSync } from "node:zlib";
2
2
 
3
3
  const SIZE = 32;
4
- const RECTANGLES = [
4
+ const MODEL_PROXY_RECTANGLES = [
5
5
  [4, 13, 6, 6],
6
6
  [24, 4, 6, 6],
7
7
  [24, 13, 6, 6],
@@ -12,35 +12,93 @@ const RECTANGLES = [
12
12
  [18, 15, 6, 2],
13
13
  [18, 24, 6, 2],
14
14
  ] as const;
15
+ const GRAPHITI_BOLT_RECTANGLES = [
16
+ [10, 5, 10, 2],
17
+ [17, 7, 2, 2],
18
+ [15, 9, 7, 2],
19
+ ] as const;
20
+
21
+ /** Plain glyphs available to identify dbx-tools system-tray services. */
22
+ export type ServiceTrayGlyph = "model-proxy" | "graphiti" | "lakebase";
15
23
 
16
- /** Return the dbx-tools service glyph as a systray2 PNG or Windows ICO payload. */
17
- export function serviceTrayIcon(platform: NodeJS.Platform = process.platform): string {
24
+ /** Return a service glyph as a systray2 PNG or Windows ICO payload. */
25
+ export function serviceTrayIcon(
26
+ glyph: ServiceTrayGlyph = "model-proxy",
27
+ platform: NodeJS.Platform = process.platform,
28
+ ): string {
18
29
  const color =
19
30
  platform === "darwin"
20
31
  ? ([0x00, 0x00, 0x00, 0xff] as const)
21
32
  : ([0xff, 0x36, 0x21, 0xff] as const);
22
- const png = encodePng(renderGlyph(color));
33
+ const png = encodePng(renderGlyph(glyph, color));
23
34
  return (platform === "win32" ? encodeIco(png) : png).toString("base64");
24
35
  }
25
36
 
26
- function renderGlyph(color: readonly [number, number, number, number]): Buffer {
37
+ function renderGlyph(
38
+ glyph: ServiceTrayGlyph,
39
+ color: readonly [number, number, number, number],
40
+ ): Buffer {
27
41
  const rgba = Buffer.alloc(SIZE * SIZE * 4);
28
42
  for (let y = 0; y < SIZE; y += 1) {
29
43
  for (let x = 0; x < SIZE; x += 1) {
30
- if (
31
- !RECTANGLES.some(
32
- ([left, top, width, height]) =>
33
- x >= left && x < left + width && y >= top && y < top + height,
34
- )
35
- ) {
36
- continue;
37
- }
38
- rgba.set(color, (y * SIZE + x) * 4);
44
+ const opacity = glyphOpacity(glyph, x, y);
45
+ if (opacity === 0) continue;
46
+ rgba.set([color[0], color[1], color[2], Math.round(color[3] * opacity)], (y * SIZE + x) * 4);
39
47
  }
40
48
  }
41
49
  return rgba;
42
50
  }
43
51
 
52
+ function glyphOpacity(glyph: ServiceTrayGlyph, x: number, y: number): number {
53
+ switch (glyph) {
54
+ case "model-proxy":
55
+ return modelProxyGlyphContains(x, y) ? 1 : 0;
56
+ case "graphiti":
57
+ return graphitiGlyphContains(x, y) ? 1 : 0;
58
+ case "lakebase":
59
+ return lakebaseGlyphOpacity(x, y);
60
+ }
61
+ }
62
+
63
+ function modelProxyGlyphContains(x: number, y: number): boolean {
64
+ return MODEL_PROXY_RECTANGLES.some(
65
+ ([left, top, width, height]) => x >= left && x < left + width && y >= top && y < top + height,
66
+ );
67
+ }
68
+
69
+ function graphitiGlyphContains(x: number, y: number): boolean {
70
+ // Preserve the Graphiti bot mark through its bolt, antennae, outlined head, and ringed eyes.
71
+ const bolt = GRAPHITI_BOLT_RECTANGLES.some(
72
+ ([left, top, width, height]) => x >= left && x < left + width && y >= top && y < top + height,
73
+ );
74
+ const head =
75
+ (y >= 10 && y < 13 && x >= 6 && x < 26) ||
76
+ (y >= 13 && y < 25 && x >= 4 && x < 7) ||
77
+ (y >= 13 && y < 25 && x >= 25 && x < 28) ||
78
+ (y >= 24 && y < 27 && x >= 6 && x < 26);
79
+ const antenna =
80
+ y >= 3 &&
81
+ y < 11 &&
82
+ ((x >= 7 && x < 9) || (x >= 23 && x < 25));
83
+ return bolt || head || antenna || graphitiEyeContains(x, y, 11) || graphitiEyeContains(x, y, 21);
84
+ }
85
+
86
+ function graphitiEyeContains(x: number, y: number, centerX: number): boolean {
87
+ const distance = (x - centerX) ** 2 + (y - 18) ** 2;
88
+ return distance >= 10 && distance <= 25;
89
+ }
90
+
91
+ function lakebaseGlyphOpacity(x: number, y: number): number {
92
+ if (x < 5 || x >= 27) return 0;
93
+ if (y >= 5 && y < 11) return 0.45;
94
+
95
+ // Keep the official mark's light middle band, dark wave channel, and solid lower band.
96
+ const middleBoundary = 19 - Math.round(Math.sin(((x - 5) / 21) * Math.PI * 2));
97
+ if (y >= 13 && y < middleBoundary) return 0.45;
98
+ if (y >= middleBoundary + 3 && y < 27) return 1;
99
+ return 0;
100
+ }
101
+
44
102
  function encodePng(rgba: Buffer): Buffer {
45
103
  const header = Buffer.alloc(13);
46
104
  header.writeUInt32BE(SIZE, 0);
package/src/service.ts CHANGED
@@ -66,6 +66,7 @@ export type CliServicePythonInstaller = (
66
66
  packageSpecifiers: readonly string[],
67
67
  python: string,
68
68
  platform: NodeJS.Platform,
69
+ offline: boolean,
69
70
  ) => Promise<string>;
70
71
 
71
72
  /** Host runtime overrides for tests, embedded distributions, and nonstandard homes. */
@@ -100,6 +101,10 @@ export interface CliServiceRuntimeOptions {
100
101
  export interface CliServiceInstallOptions {
101
102
  /** Start the service immediately after writing its configuration and login entry. */
102
103
  readonly start?: boolean;
104
+ /** Install the primary Python package from this local project instead of the registry. */
105
+ readonly pythonProject?: string;
106
+ /** Resolve and install Python packages only from the uv cache. */
107
+ readonly offline?: boolean;
103
108
  }
104
109
 
105
110
  /** Current installation and process state for a system-tray service. */
@@ -124,6 +129,8 @@ export interface CliServiceLifecycle {
124
129
  stop(): Promise<void>;
125
130
  /** Stop and start the installed service. */
126
131
  restart(): Promise<void>;
132
+ /** Return the managed service process log path. */
133
+ logPath(): string;
127
134
  /** Stop the service and remove its login entry and package-owned state. */
128
135
  uninstall(): Promise<void>;
129
136
  /** Read the current installation and process state. */
@@ -175,6 +182,9 @@ export class CliService implements CliServiceLifecycle {
175
182
 
176
183
  /** Install the login entry and optionally start the service. */
177
184
  async install(options: CliServiceInstallOptions = {}): Promise<void> {
185
+ if (options.pythonProject && !this.definition.pythonPackage) {
186
+ throw new Error(`${this.definition.name} does not define a Python package`);
187
+ }
178
188
  const owner = resolveServicePackage(this.definition.packageName);
179
189
  const dependencies = owner.dependencies();
180
190
  const external = new Set(externalRuntimePackages(dependencies));
@@ -184,7 +194,7 @@ export class CliService implements CliServiceLifecycle {
184
194
  Object.fromEntries(Object.entries(dependencies).filter(([name]) => external.has(name))),
185
195
  );
186
196
  const paths = resolveServicePaths(this.definition, this.runtime);
187
- const installedDefinition = await this.installDefinition(owner, paths.directory);
197
+ const installedDefinition = await this.installDefinition(owner, paths.directory, options);
188
198
  const host = await this.ensureCompiledBinary(
189
199
  this.binaryName(installedDefinition, "service"),
190
200
  this.hostEntrypoint,
@@ -260,6 +270,11 @@ export class CliService implements CliServiceLifecycle {
260
270
  await this.start();
261
271
  }
262
272
 
273
+ /** Return the managed service process log path. */
274
+ logPath(): string {
275
+ return resolveServicePaths(this.definition, this.runtime).processLog;
276
+ }
277
+
263
278
  /** Stop the service and remove its login entry and package-owned state. */
264
279
  async uninstall(): Promise<void> {
265
280
  const paths = resolveServicePaths(this.definition, this.runtime);
@@ -295,6 +310,7 @@ export class CliService implements CliServiceLifecycle {
295
310
  private async installDefinition(
296
311
  owner: ServicePackage,
297
312
  directory: string,
313
+ options: CliServiceInstallOptions,
298
314
  ): Promise<CliServiceDefinition> {
299
315
  const definition = CliServiceDefinitionSchema.parse({
300
316
  ...this.definition,
@@ -309,12 +325,16 @@ export class CliService implements CliServiceLifecycle {
309
325
  : undefined;
310
326
  const pythonEnvironment: Record<string, string> = {};
311
327
  if (pythonPackage) {
328
+ const primarySpecifier = options.pythonProject
329
+ ? _localPythonSpecifier(options.pythonProject, pythonPackage.name)
330
+ : `${pythonPackage.name}==${pythonPackage.version}`;
312
331
  pythonEnvironment.PYTHON = await this.pythonInstaller(
313
332
  this.uvExecutable,
314
333
  join(directory, "python"),
315
- [`${pythonPackage.name}==${pythonPackage.version}`, ...pythonPackage.dependencies],
334
+ [primarySpecifier, ...pythonPackage.dependencies],
316
335
  pythonPackage.python,
317
336
  this.runtime.platform,
337
+ options.offline ?? false,
318
338
  );
319
339
  }
320
340
  const command = definition.command
@@ -429,6 +449,16 @@ export class CliService implements CliServiceLifecycle {
429
449
  }
430
450
  }
431
451
 
452
+ /** Preserve a package's extras while replacing its registry name with a local project path. */
453
+ function _localPythonSpecifier(project: string, packageName: string): string {
454
+ const directory = resolve(project);
455
+ if (!existsSync(join(directory, "pyproject.toml"))) {
456
+ throw new Error(`Local Python project has no pyproject.toml: ${directory}`);
457
+ }
458
+ const extras = /(\[[^\]]+\])$/.exec(packageName)?.[1] ?? "";
459
+ return `${directory}${extras}`;
460
+ }
461
+
432
462
  function resolveHostEntrypoint(): string {
433
463
  const current = fileURLToPath(import.meta.url);
434
464
  const source =