Skip to content

Commit 9374e76

Browse files
jdillonclaude
andauthored
fix: expand ${env:VAR} placeholders in beads.userId and beads.pathToBd settings (#72)
Settings like beads.userId were used verbatim without expanding VS Code-style ${env:VAR} placeholders. Adds a resolveEnvVariables utility that matches VS Code's variable syntax and applies it when reading string settings. Resolves: #60 Related: vsbeads-owi Co-authored-by: Jason Dillon <jdillon@users.noreply.github.com> Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
1 parent 797ef48 commit 9374e76

7 files changed

Lines changed: 77 additions & 4 deletions

File tree

CHANGELOG.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,10 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
77

88
## [Unreleased]
99

10+
### Fixed
11+
12+
- `beads.userId` and `beads.pathToBd` now expand `${env:VAR}` placeholders (#60)
13+
1014
## [0.13.0] - 2026-03-20
1115

1216
### Added

jest.config.js

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
/** @type {import('jest').Config} */
2+
module.exports = {
3+
preset: "ts-jest",
4+
testEnvironment: "node",
5+
testMatch: ["<rootDir>/src/**/__tests__/**/*.test.ts"],
6+
transform: {
7+
"^.+\\.ts$": ["ts-jest", { diagnostics: false }],
8+
},
9+
};

package.json

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -118,7 +118,7 @@
118118
"beads.pathToBd": {
119119
"type": "string",
120120
"default": "bd",
121-
"description": "Path to the bd CLI executable"
121+
"description": "Path to the bd CLI executable. Supports ${env:VAR} placeholders."
122122
},
123123
"beads.refreshInterval": {
124124
"type": "number",
@@ -133,7 +133,7 @@
133133
"beads.userId": {
134134
"type": "string",
135135
"default": "",
136-
"description": "Your user ID for 'Assign to me'. Defaults to $USER if not set."
136+
"description": "Your user ID for 'Assign to me'. Supports ${env:VAR} placeholders (e.g., ${env:USER}). Defaults to $USER if not set."
137137
},
138138
"beads.tooltipHoverDelay": {
139139
"type": "number",

src/backend/BeadsProjectManager.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ import * as path from "path";
55
import * as util from "util";
66
import * as vscode from "vscode";
77
import { Logger } from "../utils/logger";
8+
import { resolveEnvVariables } from "../utils/resolve-env-variables";
89
import { BeadsBackend } from "./BeadsBackend";
910
import { BeadsDoltBackend } from "./BeadsDoltBackend";
1011
import { BeadsProject } from "./types";
@@ -437,7 +438,7 @@ export class BeadsProjectManager implements vscode.Disposable {
437438
private getBdPath(): string {
438439
const config = vscode.workspace.getConfiguration("beads");
439440
const configuredBdPath = config.get<string>("pathToBd", "bd") ?? "bd";
440-
return this.resolveBdPath(configuredBdPath.trim());
441+
return this.resolveBdPath(resolveEnvVariables(configuredBdPath).trim());
441442
}
442443

443444
private isNotInitializedError(error: unknown): boolean {

src/providers/BaseViewProvider.ts

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ import {
1515
WebviewToExtensionMessage,
1616
} from "../backend/types";
1717
import { Logger } from "../utils/logger";
18+
import { resolveEnvVariables } from "../utils/resolve-env-variables";
1819

1920
export abstract class BaseViewProvider implements vscode.WebviewViewProvider {
2021
protected _view?: vscode.WebviewView;
@@ -89,7 +90,8 @@ export abstract class BaseViewProvider implements vscode.WebviewViewProvider {
8990
// Send settings
9091
const config = vscode.workspace.getConfiguration("beads");
9192
// User ID: prefer setting, fallback to $USER, then "unknown"
92-
const userId = config.get<string>("userId", "") || process.env.USER || process.env.USERNAME || "unknown";
93+
const rawUserId = config.get<string>("userId", "");
94+
const userId = resolveEnvVariables(rawUserId || "") || process.env.USER || process.env.USERNAME || "unknown";
9395
this.postMessage({
9496
type: "setSettings",
9597
settings: {
Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,47 @@
1+
import { resolveEnvVariables } from "../resolve-env-variables";
2+
3+
describe("resolveEnvVariables", () => {
4+
it("returns empty string unchanged", () => {
5+
expect(resolveEnvVariables("")).toBe("");
6+
});
7+
8+
it("returns string without placeholders unchanged", () => {
9+
expect(resolveEnvVariables("jdillon")).toBe("jdillon");
10+
});
11+
12+
it("expands a single ${env:VAR} placeholder", () => {
13+
process.env.TEST_BEADS_USER = "alice";
14+
expect(resolveEnvVariables("${env:TEST_BEADS_USER}")).toBe("alice");
15+
delete process.env.TEST_BEADS_USER;
16+
});
17+
18+
it("expands multiple placeholders", () => {
19+
process.env.TEST_FIRST = "hello";
20+
process.env.TEST_SECOND = "world";
21+
expect(resolveEnvVariables("${env:TEST_FIRST}-${env:TEST_SECOND}")).toBe("hello-world");
22+
delete process.env.TEST_FIRST;
23+
delete process.env.TEST_SECOND;
24+
});
25+
26+
it("replaces missing env var with empty string", () => {
27+
delete process.env.TEST_NONEXISTENT_VAR;
28+
expect(resolveEnvVariables("${env:TEST_NONEXISTENT_VAR}")).toBe("");
29+
});
30+
31+
it("preserves surrounding text around placeholder", () => {
32+
process.env.TEST_NAME = "bob";
33+
expect(resolveEnvVariables("user-${env:TEST_NAME}-admin")).toBe("user-bob-admin");
34+
delete process.env.TEST_NAME;
35+
});
36+
37+
it("does not expand ${VAR} without env: prefix", () => {
38+
process.env.TEST_RAW = "raw";
39+
expect(resolveEnvVariables("${TEST_RAW}")).toBe("${TEST_RAW}");
40+
delete process.env.TEST_RAW;
41+
});
42+
43+
it("does not expand malformed patterns", () => {
44+
expect(resolveEnvVariables("${env:}")).toBe("${env:}");
45+
expect(resolveEnvVariables("${env:UNCLOSED")).toBe("${env:UNCLOSED");
46+
});
47+
});

src/utils/resolve-env-variables.ts

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
/**
2+
* Expands ${env:VAR} placeholders in a string using process.env.
3+
* Matches VS Code's variable syntax used in launch.json and tasks.json.
4+
* Missing env vars are replaced with empty string.
5+
*/
6+
export function resolveEnvVariables(value: string): string {
7+
return value.replace(/\$\{env:([^}]+)\}/g, (_, varName) => {
8+
return process.env[varName] ?? "";
9+
});
10+
}

0 commit comments

Comments
 (0)