Skip to content

Commit debaa49

Browse files
authored
fix: Windows genlayer up, localhost access, cross-platform CI (#288)
* fix(ci): fix bash syntax error in docs workflow Swap quote nesting in node -p command — outer single quotes, inner double quotes — so bash doesn't consume the escapes. * fix: Windows genlayer up, enable localhost access, cross-platform CI - Fix Windows `genlayer up` failing with ECONNREFUSED (#1548): remove detached `start cmd.exe` wrapper, quote paths for spaces, use -d flag - Enable localhost access by default: generate docker-compose.override.yml and genvm-module-web.yaml so GenVM can reach host services (Anvil, etc.) - Add docker-compose.yml to npm package files (was missing) - Add cross-platform CI matrix (ubuntu, macos, windows) - Add unit tests for platform commands and localhost access setup * fix: use path.resolve in configFileManager tests for Windows compat Tests hardcoded Unix-style paths (/mocked/home/.genlayer) but path.resolve produces backslash paths on Windows. Use path.resolve for expected values so tests pass on all platforms.
1 parent f022b3b commit debaa49

12 files changed

Lines changed: 238 additions & 11 deletions

File tree

.github/workflows/validate-code.yml

Lines changed: 8 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -12,14 +12,18 @@ on:
1212

1313
jobs:
1414
build-and-test:
15-
name: Build and Test
16-
runs-on: ubuntu-latest
15+
name: Build and Test (${{ matrix.os }})
16+
runs-on: ${{ matrix.os }}
17+
strategy:
18+
matrix:
19+
os: [ubuntu-latest, macos-latest, windows-latest]
1720

1821
steps:
1922
- name: Checkout code
2023
uses: actions/checkout@v4
2124

2225
- name: Install libsecret runtime
26+
if: runner.os == 'Linux'
2327
run: sudo apt-get update && sudo apt-get install -y libsecret-1-0
2428

2529
- name: Set up Node.js
@@ -38,10 +42,10 @@ jobs:
3842
run: npm run test:coverage
3943

4044
- name: Upload coverage report
41-
if: success()
45+
if: success() && matrix.os == 'ubuntu-latest'
4246
uses: codecov/codecov-action@v5.4.3
4347
with:
4448
verbose: true
4549
token: ${{ secrets.CODECOV_TOKEN }}
4650
fail_ci_if_error: true
47-
directory: coverage
51+
directory: coverage

package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@
1212
"scripts",
1313
"templates",
1414
".env.example",
15+
"docker-compose.yml",
1516
"README.md",
1617
"LICENSE"
1718
],

src/commands/general/init.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -127,6 +127,7 @@ export class InitAction extends BaseAction {
127127
this.simulatorService.addConfigToEnvFile({LOCALNETVERSION: localnetVersion});
128128

129129
this.setSpinnerText("Running GenLayer Localnet...");
130+
this.simulatorService.setupLocalhostAccess();
130131
await this.simulatorService.runSimulator();
131132

132133
this.setSpinnerText("Waiting for localnet to be ready...");

src/commands/general/start.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,7 @@ export class StartAction extends BaseAction {
4141
this.setSpinnerText(`Starting GenLayer Localnet (${restartValidatorsHintText})...`);
4242

4343
try {
44+
this.simulatorService.setupLocalhostAccess();
4445
await this.simulatorService.runSimulator();
4546
} catch (error) {
4647
this.failSpinner("Error starting the simulator", error);

src/lib/config/simulator.ts

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -3,9 +3,9 @@ export const DEFAULT_JSON_RPC_URL = "http://localhost:4000/api";
33
export const CONTAINERS_NAME_PREFIX = "/genlayer-";
44
export const IMAGES_NAME_PREFIX = "yeagerai";
55
export const DEFAULT_RUN_SIMULATOR_COMMAND = (location: string, profiles: string) => ({
6-
darwin: `osascript -e 'tell application "Terminal" to do script "cd ${location} && docker compose build && docker compose -p genlayer ${profiles} up"'`,
7-
win32: `start cmd.exe /c "cd /d ${location} && docker compose build && docker compose -p genlayer ${profiles} up && pause"`,
8-
linux: `nohup bash -c 'cd ${location} && docker compose build && docker compose -p genlayer ${profiles} up -d '`,
6+
darwin: `osascript -e 'tell application "Terminal" to do script "cd \\"${location}\\" && docker compose build && docker compose -p genlayer ${profiles} up"'`,
7+
win32: `cd /d "${location}" && docker compose build && docker compose -p genlayer ${profiles} up -d`,
8+
linux: `nohup bash -c 'cd "${location}" && docker compose build && docker compose -p genlayer ${profiles} up -d'`,
99
});
1010
export const DEFAULT_RUN_DOCKER_COMMAND = {
1111
darwin: "open -a Docker",

src/lib/interfaces/ISimulatorService.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@ export interface ISimulatorService {
2222
normalizeLocalnetVersion(version: string): string;
2323
compareVersions(version1: string, version2: string): number;
2424
isLocalnetRunning(): Promise<boolean>;
25+
setupLocalhostAccess(): void;
2526
}
2627

2728

src/lib/services/simulator.ts

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -335,6 +335,37 @@ export class SimulatorService implements ISimulatorService {
335335
return 0;
336336
}
337337

338+
public setupLocalhostAccess(): void {
339+
const overridePath = path.join(this.location, 'docker-compose.override.yml');
340+
const overrideContent = `# Auto-generated by genlayer CLI — enables localhost access for local development
341+
services:
342+
jsonrpc:
343+
volumes:
344+
- ./config-overrides/genvm-module-web.yaml:/genvm/config/genvm-module-web.yaml:ro
345+
extra_hosts:
346+
- "host.docker.internal:host-gateway"
347+
- "anvil-local:host-gateway"
348+
`;
349+
fs.writeFileSync(overridePath, overrideContent);
350+
351+
const configDir = path.join(this.location, 'config-overrides');
352+
if (!fs.existsSync(configDir)) {
353+
fs.mkdirSync(configDir, { recursive: true });
354+
}
355+
356+
const genvmConfigPath = path.join(configDir, 'genvm-module-web.yaml');
357+
const genvmConfigContent = `# Auto-generated by genlayer CLI — allows GenVM to reach localhost services
358+
always_allow_hosts:
359+
- "localhost"
360+
- "127.0.0.1"
361+
- "host.docker.internal"
362+
- "anvil-local"
363+
`;
364+
fs.writeFileSync(genvmConfigPath, genvmConfigContent);
365+
366+
console.warn('\x1b[33m⚠ Localhost access enabled — GenVM can reach host services (Anvil, local APIs). Not for production use.\x1b[0m');
367+
}
368+
338369
public async isLocalnetRunning(): Promise<boolean> {
339370
const genlayerContainers = await this.getGenlayerContainers();
340371
const runningContainers = genlayerContainers.filter(container => container.State === "running");

tests/actions/init.test.ts

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@ describe("InitAction", () => {
2323
let openFrontendSpy: ReturnType<typeof vi.spyOn>;
2424
let getFrontendUrlSpy: ReturnType<typeof vi.spyOn>;
2525
let normalizeLocalnetVersionSpy: ReturnType<typeof vi.spyOn>;
26+
let setupLocalhostAccessSpy: ReturnType<typeof vi.spyOn>;
2627

2728
const defaultConfig = {defaultOllamaModel: "llama3"};
2829

@@ -82,6 +83,9 @@ describe("InitAction", () => {
8283
normalizeLocalnetVersionSpy = vi
8384
.spyOn(SimulatorService.prototype, "normalizeLocalnetVersion")
8485
.mockImplementation((v: string) => v) as any;
86+
setupLocalhostAccessSpy = vi
87+
.spyOn(SimulatorService.prototype, "setupLocalhostAccess")
88+
.mockImplementation(() => {});
8589
vi.spyOn(SimulatorService.prototype, "isLocalnetRunning").mockResolvedValue(false);
8690
});
8791

@@ -90,6 +94,18 @@ describe("InitAction", () => {
9094
});
9195

9296
describe("Successful Execution", () => {
97+
test("should call setupLocalhostAccess before running simulator", async () => {
98+
inquirerPromptSpy
99+
.mockResolvedValueOnce({confirmAction: true})
100+
.mockResolvedValueOnce({selectedLlmProviders: ["openai"]})
101+
.mockResolvedValueOnce({openai: "API_KEY_OPENAI"});
102+
103+
await initAction.execute(defaultOptions);
104+
105+
expect(setupLocalhostAccessSpy).toHaveBeenCalled();
106+
expect(runSimulatorSpy).toHaveBeenCalled();
107+
});
108+
93109
test("should show combined confirmation message when localnet is running", async () => {
94110
vi.spyOn(SimulatorService.prototype, "isLocalnetRunning").mockResolvedValue(true);
95111

tests/actions/start.test.ts

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,15 @@ describe("StartAction", () => {
4141
ollama: false
4242
};
4343

44+
test("should call setupLocalhostAccess before running simulator", async () => {
45+
mockSimulatorService.isLocalnetRunning = vi.fn().mockResolvedValue(false);
46+
47+
await startAction.execute(defaultOptions);
48+
49+
expect(mockSimulatorService.setupLocalhostAccess).toHaveBeenCalled();
50+
expect(mockSimulatorService.runSimulator).toHaveBeenCalled();
51+
});
52+
4453
test("should check if localnet is running and proceed without confirmation when not running", async () => {
4554
mockSimulatorService.isLocalnetRunning = vi.fn().mockResolvedValue(false);
4655

tests/libs/configFileManager.test.ts

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -9,15 +9,16 @@ vi.mock("fs");
99
vi.mock("os")
1010

1111
describe("ConfigFileManager", () => {
12-
const mockFolderPath = "/mocked/home/.genlayer";
13-
const mockKeystoresPath = `${mockFolderPath}/keystores`;
14-
const mockConfigFilePath = `${mockFolderPath}/genlayer-config.json`;
12+
const mockHome = path.resolve("/mocked/home");
13+
const mockFolderPath = path.resolve(mockHome, ".genlayer");
14+
const mockKeystoresPath = path.resolve(mockFolderPath, "keystores");
15+
const mockConfigFilePath = path.resolve(mockFolderPath, "genlayer-config.json");
1516

1617
let configFileManager: ConfigFileManager;
1718

1819
beforeEach(() => {
1920
vi.clearAllMocks();
20-
vi.mocked(os.homedir).mockReturnValue("/mocked/home");
21+
vi.mocked(os.homedir).mockReturnValue(mockHome);
2122
vi.mocked(fs.existsSync).mockReturnValue(true);
2223
vi.mocked(fs.readFileSync).mockReturnValue(JSON.stringify({}));
2324
configFileManager = new ConfigFileManager();

0 commit comments

Comments
 (0)