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
33 changes: 33 additions & 0 deletions .mise/tasks/local-pack-test
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
#!/usr/bin/env bash
#MISE description="Install packed plugin into OpenCode cache (production-like test)"
set -euo pipefail

PLUGIN_LOCAL_FILE="${HOME}/.config/opencode/plugin/opencode-synced-local.ts"
CACHE_DIR="${HOME}/.cache/opencode"
PACKAGE_DIR="$(pwd)"
VERSION="$(node -p "require('./package.json').version")"

if [ -f "${PLUGIN_LOCAL_FILE}" ]; then
rm -f "${PLUGIN_LOCAL_FILE}"
fi

TARBALL="$(npm pack | tail -n 1)"

rm -rf "${CACHE_DIR}/node_modules/opencode-synced"

if [ -f "${CACHE_DIR}/package.json" ]; then
python - <<PY
import json, re
from pathlib import Path
version = "${VERSION}"
path = Path("${CACHE_DIR}") / "package.json"
text = path.read_text()
deps = dict(re.findall(r'\"([^\\\"]+)\"\\s*:\\s*\"([^\\\"]+)\"', text))
deps["opencode-synced"] = version
path.write_text(json.dumps({"dependencies": deps}, indent=2) + "\\n")
Comment on lines +20 to +27
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

Using regular expressions to parse and modify a JSON file is brittle and can lead to unexpected behavior. This implementation overwrites the entire package.json with only the dependencies field, which would remove other important fields like name, version, or scripts. A much safer and more robust approach is to use Python's built-in json module to load, modify, and then write back the JSON data. This preserves the original structure of the file while updating the necessary dependency.

import json
from pathlib import Path
version = "${VERSION}"
path = Path("${CACHE_DIR}") / "package.json"
try:
    data = json.loads(path.read_text())
except json.JSONDecodeError:
    data = {}

if "dependencies" not in data:
    data["dependencies"] = {}

data["dependencies"]["opencode-synced"] = version
path.write_text(json.dumps(data, indent=2) + "\n")

PY
fi

(cd "${CACHE_DIR}" && npm install --no-save "${PACKAGE_DIR}/${TARBALL}")

echo "Installed ${TARBALL} (${VERSION}) into ${CACHE_DIR}"
2 changes: 1 addition & 1 deletion .release-please-manifest.json
Original file line number Diff line number Diff line change
@@ -1,3 +1,3 @@
{
".": "0.4.1"
".": "0.4.2"
}
8 changes: 8 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,14 @@

All notable changes to this project will be documented here by Release Please.

## [0.4.2](https://github.com/iHildy/opencode-synced/compare/v0.3.0...v0.4.2) (2025-12-31)


### Bug Fixes

* harden plugin load when command assets are missing and broaden module exports
* add production-like local pack test script

## [0.4.1](https://github.com/iHildy/opencode-synced/compare/v0.4.0...v0.4.1) (2025-12-31)


Expand Down
19 changes: 19 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -247,6 +247,25 @@ bun -e '
- `bun run test`
- `bun run lint`

### Local testing (production-like)

To test the same artifact that would be published, install from a packed tarball
into OpenCode's cache:

```bash
mise run local-pack-test
```

Then set `~/.config/opencode/opencode.json` to use:

```jsonc
{
"plugin": ["opencode-synced"]
}
```

Restart OpenCode to pick up the cached install.


## Prefer a CLI version?

Expand Down
14 changes: 7 additions & 7 deletions package.json
Original file line number Diff line number Diff line change
@@ -1,13 +1,16 @@
{
"name": "opencode-synced",
"version": "0.4.1",
"version": "0.4.2",
"description": "Sync global OpenCode config across machines via GitHub.",
"author": {
"name": "Ian Hildebrand"
},
"type": "module",
"main": "./dist/index.js",
"types": "./dist/index.d.ts",
"exports": {
".": {
"import": "./dist/index.js",
"types": "./dist/index.d.ts",
"default": "./dist/index.js"
}
Expand All @@ -19,9 +22,7 @@
"publishConfig": {
"access": "public"
},
"files": [
"dist"
],
"files": ["dist"],
"dependencies": {
"@opencode-ai/plugin": "1.0.85"
},
Expand All @@ -38,6 +39,7 @@
},
"scripts": {
"build": "rm -rf dist && tsc -p tsconfig.build.json && cp -r src/command dist/command",
"prepack": "bun run build",
"test": "vitest run",
"test:watch": "vitest",
"lint": "biome lint .",
Expand All @@ -47,8 +49,6 @@
"prepare": "husky"
},
"lint-staged": {
"*.{js,ts,json}": [
"biome check --write --no-errors-on-unmatched"
]
"*.{js,ts,json}": ["biome check --write --no-errors-on-unmatched"]
}
}
34 changes: 24 additions & 10 deletions src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -81,18 +81,29 @@ async function loadCommands(): Promise<ParsedCommand[]> {
const commands: ParsedCommand[] = [];
const commandDir = path.join(getModuleDir(), 'command');

try {
const stats = await fs.stat(commandDir);
if (!stats.isDirectory()) {
return commands;
}
} catch {
return commands;
}

const files = await scanMdFiles(commandDir);
for (const file of files) {
const content = await fs.readFile(file, 'utf-8');
const { frontmatter, body } = parseFrontmatter(content);
const relativePath = path.relative(commandDir, file);
const name = relativePath.replace(/\.md$/, '').replace(/\//g, '-');

commands.push({
name,
frontmatter,
template: body,
});
try {
const content = await fs.readFile(file, 'utf-8');
const { frontmatter, body } = parseFrontmatter(content);
const relativePath = path.relative(commandDir, file);
const name = relativePath.replace(/\.md$/, '').replace(/\//g, '-');

commands.push({
name,
frontmatter,
template: body,
});
} catch {}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

Swallowing errors with an empty catch block can make debugging very difficult. If a command file fails to load for any reason (e.g., malformed content, read permissions), the error will be silently ignored, and there will be no indication of what went wrong. It is a best practice to at least log the error to provide visibility into such issues.

    } catch (error) {
      console.error(`[opencode-synced] Failed to load command from ${file}:`, error);
    }

}

return commands;
Expand Down Expand Up @@ -210,6 +221,9 @@ export const OpencodeConfigSync: Plugin = async (ctx) => {
};
};

export const OpencodeSynced = OpencodeConfigSync;
export default OpencodeConfigSync;

function formatError(error: unknown): string {
if (error instanceof Error) return error.message;
return String(error);
Expand Down
Loading