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
8 changes: 8 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,11 @@ jobs:
with:
bun-version: latest

- name: Setup Node
uses: actions/setup-node@v6
with:
node-version: 24

- name: Install
run: bun install --frozen-lockfile

Expand All @@ -38,3 +43,6 @@ jobs:

- name: Test with coverage
run: bun run test:coverage

- name: Test published package
run: bun run test:package
4 changes: 4 additions & 0 deletions .github/workflows/publish.yml
Original file line number Diff line number Diff line change
Expand Up @@ -79,6 +79,10 @@ jobs:
if: steps.check.outputs.already_published == 'false'
run: bun run test:coverage

- name: Test published package
if: steps.check.outputs.already_published == 'false'
run: bun run test:package

# Uses npm OIDC trusted publishing (no NPM_TOKEN).
# Requires npm >= 11.5.1. Node 24 ships with npm 11.x; if the default
# npm is too old we fall through to npx with a pinned version.
Expand Down
2 changes: 1 addition & 1 deletion .gitignore
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
node_modules/
.DS_Store
coverage/

dist/
1 change: 1 addition & 0 deletions biome.json
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@
"includes": [
"src/**/*.ts",
"test/**/*.ts",
"scripts/**/*.mjs",
"*.json",
"*.md",
"!**/node_modules",
Expand Down
5 changes: 5 additions & 0 deletions index.d.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
import type { Plugin } from "@opencode-ai/plugin"

export declare const OpencodeTranslate: Plugin

export default OpencodeTranslate
1 change: 1 addition & 0 deletions knip.json
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@
"@ai-sdk/openai-compatible"
],
"ignoreIssues": {
"index.d.ts": ["duplicates"],
"src/index.ts": ["duplicates"]
}
}
14 changes: 10 additions & 4 deletions package.json
Original file line number Diff line number Diff line change
@@ -1,9 +1,10 @@
{
"name": "opencode-translate",
"version": "1.0.6",
"version": "1.0.7",
"description": "OpenCode plugin that lets the user chat in a configured language while the main chat loop only sees English.",
"type": "module",
"main": "src/index.ts",
"main": "dist/index.js",
"types": "index.d.ts",
"license": "MIT",
"repository": {
"type": "git",
Expand All @@ -21,14 +22,16 @@
"i18n"
],
"files": [
"src",
"dist",
"index.d.ts",
"LICENSE",
"README.md"
],
"publishConfig": {
"access": "public"
},
"scripts": {
"build": "bun build src/index.ts --outfile dist/index.js --target node --format esm --packages external",
"typecheck": "tsgo --noEmit",
"format": "biome format --write .",
"format:check": "biome format .",
Expand All @@ -38,7 +41,10 @@
"check": "biome check --write .",
"check:ci": "biome check .",
"test": "bun test",
"test:coverage": "bun test --coverage"
"test:coverage": "bun test --coverage",
"test:package": "node scripts/package-smoke.mjs",
"prepare": "bun run build",
"prepack": "bun run build"
},
"peerDependencies": {
"@opencode-ai/plugin": ">=1.14.0"
Expand Down
53 changes: 53 additions & 0 deletions scripts/package-smoke.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
import assert from "node:assert/strict"
import { spawnSync } from "node:child_process"
import { mkdir, mkdtemp, rm, writeFile } from "node:fs/promises"
import { tmpdir } from "node:os"
import path from "node:path"
import { fileURLToPath } from "node:url"

const root = fileURLToPath(new URL("..", import.meta.url))
const temp = await mkdtemp(path.join(tmpdir(), "opencode-translate-package-"))

function run(command, args, cwd) {
const result = spawnSync(command, args, { cwd, encoding: "utf8" })
if (result.status !== 0) {
throw new Error(`${command} ${args.join(" ")} failed\n${result.stdout}\n${result.stderr}`)
}
return result.stdout
}

try {
const output = run("npm", ["pack", "--silent", "--json", "--pack-destination", temp], root)
const jsonStart = output.lastIndexOf("\n[")
const [packed] = JSON.parse(output.slice(jsonStart === -1 ? 0 : jsonStart + 1))
const paths = packed.files.map((file) => file.path)

assert(paths.includes("dist/index.js"), "package must contain the compiled entrypoint")
assert(paths.includes("index.d.ts"), "package must contain its public type declarations")
assert(!paths.some((file) => file.endsWith(".ts") && file !== "index.d.ts"), "package must not contain TS source")

const consumer = path.join(temp, "consumer")
await mkdir(consumer)
await writeFile(path.join(consumer, "package.json"), '{"private":true,"type":"module"}\n')

const tarball = path.join(temp, packed.filename)
run("npm", ["install", "--silent", "--ignore-scripts", "--no-audit", "--no-fund", tarball], consumer)
run(
"node",
[
"--input-type=module",
"--eval",
`
import assert from "node:assert/strict"
process.env.OPENCODE_TRANSLATE_DISABLE = "1"
const plugin = await import("opencode-translate")
assert.equal(typeof plugin.default, "function")
assert.equal(plugin.default, plugin.OpencodeTranslate)
assert.deepEqual(await plugin.default({ client: {}, directory: process.cwd() }, {}), {})
`,
],
consumer,
)
} finally {
await rm(temp, { recursive: true, force: true })
}
25 changes: 25 additions & 0 deletions test/types/public-api.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
/**
* Compile-time guard for the published type declaration.
*
* `index.d.ts` is hand-written and shipped to consumers instead of `main`
* (see `types` in package.json); nothing else ties it to the real
* implementation in `src/index.ts`. `bun run typecheck` includes this file
* (via the `test/**\/*.ts` glob in tsconfig.json), so a renamed, added,
* removed, or retyped export in either file fails the build here instead of
* shipping silently to consumers.
*
* This file has no `test()` calls — it is a compile-time-only check. Under
* `bun test` it is collected but contributes 0 assertions; the real
* enforcement happens in `bun run typecheck`.
*/
type SourceTypes = typeof import("../../src/index")
type PublishedTypes = typeof import("../../index")

type AssertExact<A, B> = [A] extends [B] ? ([B] extends [A] ? true : false) : false

type PublicApiMatchesSource = AssertExact<SourceTypes, PublishedTypes>

// If this line reports a type error, `index.d.ts` no longer matches the
// exports of `src/index.ts` — update whichever one is stale.
const publicApiMatchesSource: PublicApiMatchesSource = true
void publicApiMatchesSource