@dbx-tools/core 0.1.21 → 0.1.23

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
@@ -15,6 +15,21 @@ Key features:
15
15
  - Workspace/project root discovery from package-manager files, git metadata, and
16
16
  the current working directory.
17
17
  - Safe filesystem stat and project naming helpers for CLIs and projen synth.
18
+ - YAML/JSON brand-context discovery and loading with shared Zod validation.
19
+
20
+ ## Load Brand Context
21
+
22
+ ```ts
23
+ import { brand } from "@dbx-tools/core";
24
+
25
+ const context = await brand.loadBrandContext();
26
+ ```
27
+
28
+ `loadBrandContext()` searches known npm/git project roots for
29
+ `branding/brand.yaml`, `.yml`, or `.json`, followed by equivalent root-level
30
+ files. Missing files return the complete dbx tools default context; malformed
31
+ files fail validation. Use `loadBrandContextFile(path)` for an explicit file and
32
+ `resolveBrandAssetPath(path, asset)` for relative asset references.
18
33
 
19
34
  ## Run Commands
20
35
 
@@ -80,3 +95,4 @@ basename. `project.stat()` returns `undefined` instead of throwing.
80
95
  - `exec` - async/sync process spawning, stdio handling, abort wiring, and shlex.
81
96
  - `project` - root discovery, project naming, git-remote parsing, and safe
82
97
  filesystem stat.
98
+ - `brand` - YAML/JSON discovery, parsing, validation, and asset path resolution.
package/index.ts CHANGED
@@ -2,8 +2,10 @@
2
2
  // Regenerated from the exporting modules in ./src.
3
3
  // Hand edits are overwritten on the next watch; this file is read-only.
4
4
 
5
+ export * as brand from "./src/brand";
5
6
  export * as exec from "./src/exec";
6
7
  export * as file from "./src/file";
7
8
  export * as project from "./src/project";
9
+ export type { BrandContext, BrandContextInput } from "./src/brand";
8
10
  export type { ExecStdio, LineHandler, StdioOption, ExecResult, ExecOptions, SyncExecStdio, SyncExecOptions, SpawnArgs } from "./src/exec";
9
11
  export type { ProjectContext } from "./src/project";
package/package.json CHANGED
@@ -11,14 +11,15 @@
11
11
  "typescript": "^5.9.3"
12
12
  },
13
13
  "dependencies": {
14
- "@dbx-tools/shared-core": "0.1.21"
14
+ "yaml": "^2.9.0",
15
+ "@dbx-tools/shared-core": "0.1.23"
15
16
  },
16
17
  "main": "index.ts",
17
18
  "license": "UNLICENSED",
18
19
  "publishConfig": {
19
20
  "access": "public"
20
21
  },
21
- "version": "0.1.21",
22
+ "version": "0.1.23",
22
23
  "types": "index.ts",
23
24
  "type": "module",
24
25
  "exports": {
package/src/brand.ts ADDED
@@ -0,0 +1,71 @@
1
+ /** Node-only discovery and file loading for the shared brand context. */
2
+ import { readFile } from "node:fs/promises";
3
+ import { dirname, extname, isAbsolute, resolve } from "node:path";
4
+ import { brand as sharedBrand } from "@dbx-tools/shared-core";
5
+ import { statSync } from "./file";
6
+ import { resolveProjectRoots } from "./project";
7
+
8
+ const BRAND_CONTEXT_FILES = [
9
+ "branding/brand.yaml",
10
+ "branding/brand.yml",
11
+ "branding/brand.json",
12
+ "brand.yaml",
13
+ "brand.yml",
14
+ "brand.json",
15
+ ] as const;
16
+
17
+ export type BrandContext = sharedBrand.BrandContext;
18
+ export type BrandContextInput = sharedBrand.BrandContextInput;
19
+ export const BrandContextSchema = sharedBrand.BrandContextSchema;
20
+ export const defaultBrandContext = sharedBrand.defaultBrandContext;
21
+ export const parseBrandContext = sharedBrand.parseBrandContext;
22
+ export const brandContextJsonSchema = sharedBrand.brandContextJsonSchema;
23
+ export const brandContextPrompt = sharedBrand.brandContextPrompt;
24
+
25
+ /** Find a conventional YAML or JSON brand file from known project roots. */
26
+ export function findBrandContextFile(cwd: string = process.cwd()): string | undefined {
27
+ for (const root of resolveProjectRoots(cwd)) {
28
+ for (const candidate of BRAND_CONTEXT_FILES) {
29
+ const path = resolve(root, candidate);
30
+ if (statSync(path)?.isFile()) return path;
31
+ }
32
+ }
33
+ return undefined;
34
+ }
35
+
36
+ /** Read and validate one `.yaml`, `.yml`, or `.json` brand context file. */
37
+ export async function loadBrandContextFile(path: string): Promise<BrandContext> {
38
+ const source = await readFile(path, "utf8");
39
+ const extension = extname(path).toLowerCase();
40
+ let input: unknown;
41
+
42
+ if (extension === ".json") input = JSON.parse(source) as unknown;
43
+ else if (extension === ".yaml" || extension === ".yml") {
44
+ const { parse } = await import("yaml");
45
+ input = parse(source) as unknown;
46
+ } else throw new Error(`Unsupported brand context format: ${extension || "no extension"}`);
47
+
48
+ return sharedBrand.parseBrandContext(input);
49
+ }
50
+
51
+ /**
52
+ * Discover and load a brand context. Missing files resolve to dbx tools defaults;
53
+ * malformed files fail with their parser or Zod validation error.
54
+ */
55
+ export async function loadBrandContext(cwd: string = process.cwd()): Promise<BrandContext> {
56
+ const path = findBrandContextFile(cwd);
57
+ return path ? loadBrandContextFile(path) : sharedBrand.defaultBrandContext;
58
+ }
59
+
60
+ /** Resolve a relative asset reference against the brand file that declared it. */
61
+ export function resolveBrandAssetPath(brandFile: string, asset: string): string {
62
+ if (
63
+ isAbsolute(asset) ||
64
+ asset.startsWith("@") ||
65
+ asset.startsWith("//") ||
66
+ /^[a-z][a-z\d+.-]*:/i.test(asset)
67
+ ) {
68
+ return asset;
69
+ }
70
+ return resolve(dirname(brandFile), asset);
71
+ }
@@ -0,0 +1,39 @@
1
+ import assert from "node:assert/strict";
2
+ import { mkdtemp, realpath, rm, writeFile, mkdir } from "node:fs/promises";
3
+ import { tmpdir } from "node:os";
4
+ import { join } from "node:path";
5
+ import { describe, it } from "node:test";
6
+ import { brand } from "../index";
7
+
8
+ describe("brand files", () => {
9
+ it("loads YAML and discovers the conventional branding path", async () => {
10
+ const root = await mkdtemp(join(tmpdir(), "dbx-tools-brand-"));
11
+ try {
12
+ await writeFile(join(root, "package.json"), '{"name":"fixture"}\n');
13
+ await mkdir(join(root, "branding"));
14
+ await writeFile(join(root, "branding", "brand.yaml"), "name: Fixture\ncolors:\n primary: '#123456'\n");
15
+
16
+ assert.equal(
17
+ brand.findBrandContextFile(root),
18
+ join(await realpath(root), "branding", "brand.yaml"),
19
+ );
20
+ const context = await brand.loadBrandContext(root);
21
+ assert.equal(context.name, "Fixture");
22
+ assert.equal(context.colors.primary, "#123456");
23
+ } finally {
24
+ await rm(root, { recursive: true, force: true });
25
+ }
26
+ });
27
+
28
+ it("loads JSON and resolves relative assets", async () => {
29
+ const root = await mkdtemp(join(tmpdir(), "dbx-tools-brand-"));
30
+ try {
31
+ const file = join(root, "brand.json");
32
+ await writeFile(file, '{"name":"JSON Fixture"}\n');
33
+ assert.equal((await brand.loadBrandContextFile(file)).name, "JSON Fixture");
34
+ assert.equal(brand.resolveBrandAssetPath(file, "assets/icon.svg"), join(root, "assets", "icon.svg"));
35
+ } finally {
36
+ await rm(root, { recursive: true, force: true });
37
+ }
38
+ });
39
+ });