diff --git a/.devcontainer/devcontainer-lock.json b/.devcontainer/devcontainer-lock.json new file mode 100644 index 00000000..b6f196a7 --- /dev/null +++ b/.devcontainer/devcontainer-lock.json @@ -0,0 +1,9 @@ +{ + "features": { + "ghcr.io/devcontainers/features/common-utils:2": { + "version": "2.5.9", + "resolved": "ghcr.io/devcontainers/features/common-utils@sha256:cb0c4d3c276f157eed17935747e364178d75fee17f55c4e129966f64633deb3a", + "integrity": "sha256:cb0c4d3c276f157eed17935747e364178d75fee17f55c4e129966f64633deb3a" + } + } +} diff --git a/.devcontainer/devcontainer.json b/.devcontainer/devcontainer.json new file mode 100644 index 00000000..7fa8e0d4 --- /dev/null +++ b/.devcontainer/devcontainer.json @@ -0,0 +1,8 @@ +{ + "name": "Yasa", + "image": "oven/bun:1.3", + "mounts": [ + "source=opencode-node-modules,target=${containerWorkspaceFolder}/node_modules,type=volume" + ], + "postCreateCommand": "bun install" +} \ No newline at end of file diff --git a/bun.lock b/bun.lock index 1e3a6425..7c08efb3 100644 --- a/bun.lock +++ b/bun.lock @@ -10,6 +10,7 @@ "@opencode-ai/script": "workspace:*", "@opencode-ai/sdk": "workspace:*", "heap-snapshot-toolkit": "1.1.3", + "openai": "^6.16.0", "typescript": "catalog:", }, "devDependencies": { @@ -4614,7 +4615,7 @@ "open": ["open@10.1.2", "", { "dependencies": { "default-browser": "^5.2.1", "define-lazy-prop": "^3.0.0", "is-inside-container": "^1.0.0", "is-wsl": "^3.1.0" } }, "sha512-cxN6aIDPz6rm8hbebcP7vrQNhvRcveZoJU72Y7vskh4oIm+BZwBECnx5nTmrlres1Qapvx27Qo1Auukpf8PKXw=="], - "openai": ["openai@5.11.0", "", { "peerDependencies": { "ws": "^8.18.0", "zod": "^3.23.8" }, "optionalPeers": ["ws", "zod"], "bin": { "openai": "bin/cli" } }, "sha512-+AuTc5pVjlnTuA9zvn8rA/k+1RluPIx9AD4eDcnutv6JNwHHZxIhkFy+tmMKCvmMFDQzfA/r1ujvPWB19DQkYg=="], + "openai": ["openai@6.39.1", "", { "peerDependencies": { "ws": "^8.18.0", "zod": "^3.25 || ^4.0" }, "optionalPeers": ["ws", "zod"], "bin": { "openai": "bin/cli" } }, "sha512-z3dO9fEWOXBzlXynVb/xZ/tujzUjFWQWn3C0n0mw6Vo0zJTbEkaN4b2cLWjhJ6haJQx8LlREoafHRl+Gu/Hl+A=="], "openapi-types": ["openapi-types@12.1.3", "", {}, "sha512-N4YtSYJqghVu4iek2ZUvcN/0aqH1kRDuNqzcycDxhOUpg7GdvLa2F3DgS6yBNhInhv2r/6I0Flkn7CqL8+nIcw=="], @@ -6030,6 +6031,8 @@ "@opencode-ai/app/@opencode-ai/client": ["@opencode-ai/client@vendor/opencode-ai-client-1.17.13-v2.tgz", {}, "sha512-332kgNifvpQOF9e3UA+pIa5xPrMhLaQkUiNiO+meS0Ba9HjSE6hfsWnEojMkD0DPSLqPP6rCF1dDoF7U0Y0OCQ=="], + "@opencode-ai/console-function/openai": ["openai@5.11.0", "", { "peerDependencies": { "ws": "^8.18.0", "zod": "^3.23.8" }, "optionalPeers": ["ws", "zod"], "bin": { "openai": "bin/cli" } }, "sha512-+AuTc5pVjlnTuA9zvn8rA/k+1RluPIx9AD4eDcnutv6JNwHHZxIhkFy+tmMKCvmMFDQzfA/r1ujvPWB19DQkYg=="], + "@opencode-ai/core/@ai-sdk/openai": ["@ai-sdk/openai@3.0.84", "", { "dependencies": { "@ai-sdk/provider": "3.0.14", "@ai-sdk/provider-utils": "4.0.38" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-cmgbeJL0bbY0yTJH4/AdmP5E7MjWRL9G8UdhIi0JlV/So03o82ORJofW8OzwCZPTORVQblFbpZXYGDcUd9NdUQ=="], "@opencode-ai/core/@ai-sdk/openai-compatible": ["@ai-sdk/openai-compatible@2.0.41", "", { "dependencies": { "@ai-sdk/provider": "3.0.8", "@ai-sdk/provider-utils": "4.0.23" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-kNAGINk71AlOXx10Dq/PXw4t/9XjdK8uxfpVElRwtSFMdeSiLVt58p9TPx4/FJD+hxZuVhvxYj9r42osxWq79g=="], @@ -6334,8 +6337,6 @@ "gaxios/node-fetch": ["node-fetch@3.3.2", "", { "dependencies": { "data-uri-to-buffer": "^4.0.0", "fetch-blob": "^3.1.4", "formdata-polyfill": "^4.0.10" } }, "sha512-dRB78srN/l6gqWulah9SrxeYnxeddIG30+GOqK/9OlLVyLg3HPnr6SqOWTWOXKRwC2eGYCkZ59NNuSgvSrpgOA=="], - "gitlab-ai-provider/openai": ["openai@6.39.1", "", { "peerDependencies": { "ws": "^8.18.0", "zod": "^3.25 || ^4.0" }, "optionalPeers": ["ws", "zod"], "bin": { "openai": "bin/cli" } }, "sha512-z3dO9fEWOXBzlXynVb/xZ/tujzUjFWQWn3C0n0mw6Vo0zJTbEkaN4b2cLWjhJ6haJQx8LlREoafHRl+Gu/Hl+A=="], - "gitlab-ai-provider/zod": ["zod@3.25.76", "", {}, "sha512-gzUt/qt81nXsFGKIFcC3YnfEAx5NkunCfnDlvuBSSFS02bcXu4Lmea0AFIUwbLWxWPx3d9p8S5QoaujKcNQxcQ=="], "globby/ignore": ["ignore@5.3.2", "", {}, "sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g=="], diff --git a/package.json b/package.json index 58712547..dc29a90c 100644 --- a/package.json +++ b/package.json @@ -115,6 +115,7 @@ "@opencode-ai/script": "workspace:*", "@opencode-ai/sdk": "workspace:*", "heap-snapshot-toolkit": "1.1.3", + "openai": "^6.16.0", "typescript": "catalog:" }, "repository": { diff --git a/packages/core/package.json b/packages/core/package.json index 0aa257a8..e2942e0f 100644 --- a/packages/core/package.json +++ b/packages/core/package.json @@ -85,22 +85,22 @@ "@effect/opentelemetry": "catalog:", "@effect/platform-node": "catalog:", "@effect/sql-sqlite-bun": "catalog:", - "@lydell/node-pty": "catalog:", "@ff-labs/fff-bun": "0.9.4", + "@lydell/node-pty": "catalog:", "@npmcli/arborist": "9.4.0", "@npmcli/config": "10.8.1", "@opencode-ai/effect-drizzle-sqlite": "workspace:*", "@opencode-ai/effect-sqlite-node": "workspace:*", "@opencode-ai/llm": "workspace:*", - "@opencode-ai/schema": "workspace:*", "@opencode-ai/plugin": "workspace:*", + "@opencode-ai/schema": "workspace:*", + "@openrouter/ai-sdk-provider": "2.9.0", "@opentelemetry/api": "1.9.0", "@opentelemetry/context-async-hooks": "2.6.1", "@opentelemetry/exporter-trace-otlp-http": "0.214.0", "@opentelemetry/sdk-trace-base": "2.6.1", "@parcel/watcher": "2.5.1", "@silvia-odwyer/photon-node": "0.3.4", - "@openrouter/ai-sdk-provider": "2.9.0", "ai-gateway-provider": "3.1.2", "bun-pty": "0.4.8", "cross-spawn": "catalog:", @@ -113,8 +113,8 @@ "google-auth-library": "10.5.0", "gray-matter": "4.0.3", "htmlparser2": "8.0.2", - "immer": "11.1.4", "ignore": "7.0.5", + "immer": "11.1.4", "jsonc-parser": "3.3.1", "mime-types": "3.0.2", "minimatch": "10.2.5", diff --git a/packages/opencode/src/util/repository.ts b/packages/opencode/src/util/repository.ts index dfeee432..8405b86a 100644 --- a/packages/opencode/src/util/repository.ts +++ b/packages/opencode/src/util/repository.ts @@ -136,29 +136,38 @@ function buildFileReference(input: { url: URL; remote: string }) { } satisfies FileReference } -export function parseRepositoryReference(input: string) { - const cleaned = normalizeRepositoryInput(input) - if (!cleaned) return null +function tryGithubPrefixedReference(cleaned: string): Reference | null | undefined { const githubPrefixed = cleaned.match(/^github:([^/\s]+)\/([^/\s]+)$/) - if (githubPrefixed) { - return buildRemoteReference({ host: "github.com", segments: [githubPrefixed[1], githubPrefixed[2]] }) - } - - if (!cleaned.includes("://")) { - const scp = cleaned.match(/^(?:[^@/\s]+@)?([^:/\s]+):(.+)$/) - if (scp) return buildRemoteReference({ host: scp[1], segments: parts(scp[2]), remote: cleaned }) + if (!githubPrefixed) return undefined + return buildRemoteReference({ host: "github.com", segments: [githubPrefixed[1], githubPrefixed[2]] }) +} - const direct = parts(cleaned) - if (direct.length >= 2 && hostLike(direct[0])) { - return buildRemoteReference({ host: direct[0], segments: direct.slice(1) }) - } +function tryScpReference(cleaned: string): Reference | null | undefined { + const scp = cleaned.match(/^(?:[^@/\s]+@)?([^:/\s]+):(.+)$/) + if (!scp) return undefined + return buildRemoteReference({ host: scp[1], segments: parts(scp[2]), remote: cleaned }) +} - if (direct.length === 2) { - return buildRemoteReference({ host: "github.com", segments: direct }) - } +function tryDirectReference(cleaned: string): Reference | null | undefined { + const direct = parts(cleaned) + if (direct.length >= 2 && hostLike(direct[0])) { + return buildRemoteReference({ host: direct[0], segments: direct.slice(1) }) } + if (direct.length === 2) { + return buildRemoteReference({ host: "github.com", segments: direct }) + } + return undefined +} +function tryShorthandReference(cleaned: string): Reference | null | undefined { + if (cleaned.includes("://")) return undefined + const scpResult = tryScpReference(cleaned) + if (scpResult !== undefined) return scpResult + return tryDirectReference(cleaned) +} + +function tryUrlReference(cleaned: string): Reference | null { try { const url = new URL(cleaned) if (url.protocol === "file:") return buildFileReference({ url, remote: cleaned }) @@ -174,7 +183,18 @@ export function parseRepositoryReference(input: string) { return null } } +export function parseRepositoryReference(input: string): Reference | null { + const cleaned = normalizeRepositoryInput(input) + if (!cleaned) return null + const githubPrefixed = tryGithubPrefixedReference(cleaned) + if (githubPrefixed !== undefined) return githubPrefixed + + const shorthand = tryShorthandReference(cleaned) + if (shorthand !== undefined) return shorthand + + return tryUrlReference(cleaned) +} export function isFileRepositoryReference(reference: Reference): reference is FileReference { return reference.protocol === "file:" } diff --git a/packages/opencode/test/util/repository.test.ts b/packages/opencode/test/util/repository.test.ts index 5c619f2a..a5d8bc33 100644 --- a/packages/opencode/test/util/repository.test.ts +++ b/packages/opencode/test/util/repository.test.ts @@ -8,6 +8,7 @@ import { UnsupportedLocalRepositoryError, isFileRepositoryReference, isRemoteRepositoryReference, + parseGitHubRemote, parseRemoteRepositoryReference, parseRepositoryReference, repositoryCacheIdentity, @@ -90,4 +91,37 @@ describe("util.repository", () => { expect(() => validateRepositoryBranch("bad branch")).toThrow("Branch must contain only alphanumeric characters") expect(() => validateRepositoryBranch("bad branch")).toThrow(InvalidRepositoryBranchError) }) -}) + + test("parses github: prefixed shorthand", () => { + const reference = parseRemoteRepositoryReference("github:owner/repo") + + expect(reference).toMatchObject({ + host: "github.com", + owner: "owner", + repo: "repo", + label: "owner/repo", + }) + }) + + test("parses github remote shorthand into owner and repo", () => { + expect(parseGitHubRemote("git@github.com:owner/repo.git")).toEqual({ owner: "owner", repo: "repo" }) + expect(parseGitHubRemote("https://github.com/owner/repo.git")).toEqual({ owner: "owner", repo: "repo" }) + + expect(parseGitHubRemote("owner/repo")).toBeNull() + expect(parseGitHubRemote("https://gitlab.com/owner/repo.git")).toBeNull() + expect(parseGitHubRemote("not a valid remote")).toBeNull() + }) + + test("uses custom github base url override when set", () => { + const original = process.env.OPENCODE_REPO_CLONE_GITHUB_BASE_URL + process.env.OPENCODE_REPO_CLONE_GITHUB_BASE_URL = "https://github.internal.example.com" + + try { + const reference = parseRemoteRepositoryReference("owner/repo") + expect(reference.remote).toBe("https://github.internal.example.com/owner/repo.git") + } finally { + if (original === undefined) delete process.env.OPENCODE_REPO_CLONE_GITHUB_BASE_URL + else process.env.OPENCODE_REPO_CLONE_GITHUB_BASE_URL = original + } + }) +}) \ No newline at end of file diff --git a/tmp/opencode-provider-dynamic-o82f8c/index.html b/tmp/opencode-provider-dynamic-o82f8c/index.html new file mode 100644 index 00000000..57206a92 --- /dev/null +++ b/tmp/opencode-provider-dynamic-o82f8c/index.html @@ -0,0 +1,98 @@ + + + + + + + LCOV - lcov.info - ../../../../tmp/opencode-provider-dynamic-o82f8c + + + + + + + + + + + + + + +
LCOV - code coverage report
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
Current view:top level - ../../../../tmp/opencode-provider-dynamic-o82f8cCoverageTotalHit
Test:lcov.infoLines:100.0 %11
Test Date:2026-08-28 11:41:16Functions:-00
+
+ +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + +

File Sort by file nameLine Coverage Sort by line coverage
Rate Total Hit
provider.mjs +
100.0%
+
100.0 %11
Note: 'Function Coverage' columns elided as function owner is not identified.
+
+
+ + + + +
Generated by: LCOV version 2.0-1
+
+ + + diff --git a/tmp/opencode-provider-dynamic-o82f8c/provider.mjs.gcov.html b/tmp/opencode-provider-dynamic-o82f8c/provider.mjs.gcov.html new file mode 100644 index 00000000..755bbe05 --- /dev/null +++ b/tmp/opencode-provider-dynamic-o82f8c/provider.mjs.gcov.html @@ -0,0 +1,77 @@ + + + + + + + LCOV - lcov.info - ../../../../tmp/opencode-provider-dynamic-o82f8c/provider.mjs + + + + + + + + + + + + + + +
LCOV - code coverage report
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
Current view:top level - ../../../../tmp/opencode-provider-dynamic-o82f8c - provider.mjsCoverageTotalHit
Test:lcov.infoLines:100.0 %11
Test Date:2026-08-28 11:41:16Functions:-00
+
+ + + + + + + + +

+
            Line data    Source code
+
+       1           39 : /* ../../../../tmp/opencode-provider-dynamic-o82f8c/provider.mjs empty or unreadable */
+        
+
+
+ + + + +
Generated by: LCOV version 2.0-1
+
+ + +