Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -97,6 +97,12 @@
`lastDurableSetupAt` while keeping legacy `lastSuccessfulSetupAt` readable as
a backward-compatible migration path

### Bug Fixes

- make the published bin wrapper follow symlinked `.bin` entrypoints so
`npx @gonkagate/opencode-setup` and `node_modules/.bin/opencode-setup`
actually execute on Unix-like systems instead of exiting silently

### Added

- public curated model picker UI in the CLI, currently backed by one validated
Expand Down
6 changes: 2 additions & 4 deletions bin/gonkagate-opencode.js
Original file line number Diff line number Diff line change
@@ -1,14 +1,12 @@
#!/usr/bin/env node

import process from "node:process";
import { pathToFileURL } from "node:url";
import { main, renderCliEntrypointError } from "../dist/cli.js";
import { isEntrypointInvocation } from "../dist/entrypoint.js";

export { renderCliEntrypointError };

const isEntrypoint =
process.argv[1] !== undefined &&
import.meta.url === pathToFileURL(process.argv[1]).href;
const isEntrypoint = isEntrypointInvocation(import.meta.url);

function handleCliError(error) {
const renderedError = renderCliEntrypointError(error);
Expand Down
6 changes: 2 additions & 4 deletions src/cli.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,9 @@
import process from "node:process";
import { pathToFileURL } from "node:url";
import { run as runCli } from "./cli/execute.js";
import { parseCliOptions } from "./cli/parse.js";
import { renderCliEntrypointError } from "./cli/render.js";
import type { CliRunOptions, CliRunResult } from "./cli/contracts.js";
import { isEntrypointInvocation } from "./entrypoint.js";

export { parseCliOptions };
export { renderCliEntrypointError } from "./cli/render.js";
Expand Down Expand Up @@ -45,9 +45,7 @@ function handleCliError(error: unknown): void {
process.exitCode = renderedError.exitCode;
}

const isEntrypoint =
process.argv[1] !== undefined &&
import.meta.url === pathToFileURL(process.argv[1]).href;
const isEntrypoint = isEntrypointInvocation(import.meta.url);

if (isEntrypoint) {
main().catch(handleCliError);
Expand Down
29 changes: 29 additions & 0 deletions src/entrypoint.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
import { realpathSync } from "node:fs";
import { fileURLToPath, pathToFileURL } from "node:url";

function tryResolveRealPath(path: string): string | undefined {
try {
return realpathSync(path);
} catch {
return undefined;
}
}

export function isEntrypointInvocation(
importMetaUrl: string,
argv1 = process.argv[1],
): boolean {
if (argv1 === undefined) {
return false;
}

const importMetaPath = fileURLToPath(importMetaUrl);
const argv1RealPath = tryResolveRealPath(argv1);
const importMetaRealPath = tryResolveRealPath(importMetaPath);

if (argv1RealPath !== undefined && importMetaRealPath !== undefined) {
return argv1RealPath === importMetaRealPath;
}

return importMetaUrl === pathToFileURL(argv1).href;
}
38 changes: 37 additions & 1 deletion test/cli.test.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
import assert from "node:assert/strict";
import { spawnSync } from "node:child_process";
import { resolve } from "node:path";
import { mkdtempSync, rmSync, symlinkSync } from "node:fs";
import { join, resolve } from "node:path";
import { tmpdir } from "node:os";
import { pathToFileURL } from "node:url";
import test from "node:test";
import { formatOpencodeModelRef } from "../src/constants/models.js";
Expand Down Expand Up @@ -161,6 +163,40 @@ test("CLI wrapper exposes the shipped help surface", () => {
assert.match(helpResult.stdout, new RegExp(escapeRegExp(GONKAGATE_BASE_URL)));
});

test("CLI wrapper still runs when invoked through a symlinked bin path", (t) => {
const tempDir = mkdtempSync(join(tmpdir(), "gonkagate-opencode-bin-"));
const binPath = resolve(repoRoot, CONTRACT_METADATA.binPath);
const linkedBinPath = resolve(tempDir, CONTRACT_METADATA.binName);

t.after(() => {
rmSync(tempDir, { force: true, recursive: true });
});

try {
symlinkSync(binPath, linkedBinPath, "file");
} catch (error) {
if (
error instanceof Error &&
"code" in error &&
(error as NodeJS.ErrnoException).code === "EPERM"
) {
t.skip("Symlinks are unavailable in this environment.");
return;
}

throw error;
}

const helpResult = spawnSync(process.execPath, [linkedBinPath, "--help"], {
cwd: repoRoot,
encoding: "utf8",
});

assert.equal(helpResult.status, 0);
assert.match(helpResult.stdout, /Usage: opencode-setup/i);
assert.match(helpResult.stdout, /Configure OpenCode to use GonkaGate/i);
});

test("interactive runs show the public model picker even when one validated model is available", async () => {
const promptMessages: string[] = [];
const promptChoiceSnapshots: string[][] = [];
Expand Down
Loading