@paytaca/opencode-plugin 0.2.0 → 0.2.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@paytaca/opencode-plugin",
3
- "version": "0.2.0",
3
+ "version": "0.2.2",
4
4
  "description": "OpenCode plugin for Paytaca AI - AI inference provider powered by Bitcoin Cash micropayments",
5
5
  "main": "dist/index.js",
6
6
  "types": "dist/index.d.ts",
@@ -33,7 +33,7 @@
33
33
  "license": "MIT",
34
34
  "dependencies": {
35
35
  "@opencode-ai/plugin": "^1.17.8",
36
- "paytaca-cli": "^0.4.1"
36
+ "paytaca-cli": "^0.5.0"
37
37
  },
38
38
  "devDependencies": {
39
39
  "@types/node": "^20.0.0",
@@ -5,12 +5,14 @@ const os = require('os');
5
5
  const path = require('path');
6
6
 
7
7
  const log = (msg) => console.log(`[paytaca] ${msg}`);
8
+ const IS_WIN = process.platform === 'win32';
8
9
 
9
10
  function which(cmd) {
10
11
  try {
11
- const which = process.platform === 'win32' ? 'where' : 'which';
12
- const out = execSync(`${which} ${cmd}`, { encoding: 'utf8' }).trim().split('\n')[0];
13
- return out || null;
12
+ // 'where' on Windows may return CRLF-separated matches; take the first.
13
+ const w = IS_WIN ? 'where' : 'which';
14
+ const out = execSync(`${w} ${cmd}`, { encoding: 'utf8' }).trim();
15
+ return out ? out.split(/\r?\n/)[0].trim() : null;
14
16
  } catch {
15
17
  return null;
16
18
  }
@@ -37,9 +39,9 @@ function resolveLocalCliBin() {
37
39
  function globalBinDir() {
38
40
  try {
39
41
  const prefix = execSync('npm prefix -g', { encoding: 'utf8' }).trim();
40
- if (prefix) return path.join(prefix, process.platform === 'win32' ? '' : 'bin');
42
+ if (prefix) return IS_WIN ? prefix : path.join(prefix, 'bin');
41
43
  } catch {}
42
- if (process.platform === 'win32') return null;
44
+ if (IS_WIN) return null;
43
45
  const homeBin = path.join(os.homedir(), '.npm-global', 'bin');
44
46
  if (fs.existsSync(homeBin)) return homeBin;
45
47
  const nvmBin = process.env.NVM_BIN;
@@ -56,11 +58,130 @@ function asdfReshim() {
56
58
  } catch {}
57
59
  }
58
60
 
59
- async function linkGlobally(cliBin) {
60
- const binDir = globalBinDir();
61
+ const PACKAGE_NAME = '@paytaca/opencode-plugin';
62
+
63
+ const norm = (p) => path.resolve(p);
64
+ // `win` allows tests to exercise Windows path semantics without touching
65
+ // the real process.platform (which child_process.execSync reads to pick its
66
+ // shell). Defaults to the running platform.
67
+ function normKey(p, win) {
68
+ win = win === undefined ? IS_WIN : win;
69
+ const r = norm(p);
70
+ return win ? r.toLowerCase() : r;
71
+ }
72
+ // Is `child` the same as, or located beneath, `parent`?
73
+ function isInside(child, parent, win) {
74
+ const c = normKey(child, win);
75
+ const p = normKey(parent, win);
76
+ return c === p || c.startsWith(p + path.sep);
77
+ }
78
+
79
+ // Clean up version-inconsistency traps after a fresh install:
80
+ // 1. Remove opencode plugin-cache entries for other versions of this plugin.
81
+ // 2. Pin the dependency spec to this exact version in opencode package.jsons
82
+ // (^ ranges on 0.x exclude newer minors, which blocks upgrades).
83
+ // 3. Delete lockfiles that still pin an old version (they regenerate).
84
+ function selfPin() {
85
+ try {
86
+ const pkgRoot = path.resolve(__dirname, '..');
87
+ const own = JSON.parse(fs.readFileSync(path.join(pkgRoot, 'package.json'), 'utf8')).version;
88
+ if (!own) return;
89
+
90
+ // 1. Stale plugin caches (never touch the directory we were installed into)
91
+ const cacheBase = path.join(
92
+ process.env.XDG_CACHE_HOME || path.join(os.homedir(), '.cache'),
93
+ 'opencode', 'packages', '@paytaca'
94
+ );
95
+ try {
96
+ for (const entry of fs.readdirSync(cacheBase)) {
97
+ if (entry.indexOf('opencode-plugin') === -1) continue;
98
+ const dir = path.join(cacheBase, entry);
99
+ // Paths are case-insensitive on Windows; compare normalized keys.
100
+ if (isInside(pkgRoot, dir)) continue;
101
+ let version = null;
102
+ try {
103
+ version = JSON.parse(
104
+ fs.readFileSync(path.join(dir, 'node_modules', PACKAGE_NAME, 'package.json'), 'utf8')
105
+ ).version;
106
+ } catch {}
107
+ if (version !== own) {
108
+ try { fs.rmSync(dir, { recursive: true, force: true }); } catch {}
109
+ }
110
+ }
111
+ } catch {}
112
+
113
+ // 2. Exact-pin the spec in every opencode scope referencing the plugin.
114
+ // npm runs lifecycle scripts with cwd = the package install dir, so
115
+ // consider both `<cwd>/.opencode` and `<cwd>` itself as project scopes.
116
+ const cfgDir = path.join(
117
+ process.env.XDG_CONFIG_HOME || path.join(os.homedir(), '.config'),
118
+ 'opencode'
119
+ );
120
+ const cwd = process.cwd();
121
+ const seen = new Set();
122
+ const configDirs = [];
123
+ for (const d of [cfgDir, path.join(cwd, '.opencode'), cwd]) {
124
+ if (isInside(d, pkgRoot)) continue;
125
+ const key = normKey(d);
126
+ if (seen.has(key)) continue;
127
+ seen.add(key);
128
+ try {
129
+ if (fs.existsSync(path.join(d, 'package.json'))) configDirs.push(d);
130
+ } catch {}
131
+ }
132
+ for (const dir of configDirs) {
133
+ try {
134
+ const file = path.join(dir, 'package.json');
135
+ if (!fs.existsSync(file)) continue;
136
+ const json = JSON.parse(fs.readFileSync(file, 'utf8'));
137
+ const deps = json.dependencies || {};
138
+ if (typeof deps[PACKAGE_NAME] !== 'string' || deps[PACKAGE_NAME] === own) continue;
139
+ deps[PACKAGE_NAME] = own;
140
+ json.dependencies = deps;
141
+ fs.writeFileSync(file, JSON.stringify(json, null, 2) + '\n');
142
+ log(`Pinned ${PACKAGE_NAME} to ${own} in ${file}`);
143
+ } catch {}
144
+ }
145
+
146
+ // 3. Lockfiles pinning an old version would fail integrity on next install
147
+ for (const dir of configDirs) {
148
+ for (const lock of ['package-lock.json', 'bun.lock']) {
149
+ try {
150
+ const file = path.join(dir, lock);
151
+ if (!fs.existsSync(file)) continue;
152
+ if (fs.readFileSync(file, 'utf8').indexOf(PACKAGE_NAME) === -1) continue;
153
+ fs.rmSync(file, { force: true });
154
+ log(`Removed stale ${lock} in ${dir} (regenerates on next install)`);
155
+ } catch {}
156
+ }
157
+ }
158
+ } catch {}
159
+ }
160
+
161
+ // `opts.platform`/`opts.binDir` let tests drive the Windows/unix branches
162
+ // deterministically; in production they default to the running platform and a
163
+ // resolved global bin directory.
164
+ async function linkGlobally(cliBin, opts) {
165
+ opts = opts || {};
166
+ const win = opts.platform ? opts.platform === 'win32' : IS_WIN;
167
+ const binDir = opts.binDir || globalBinDir();
61
168
  if (!binDir) throw new Error('Could not determine global bin directory');
62
169
  fs.mkdirSync(binDir, { recursive: true });
63
170
 
171
+ if (win) {
172
+ // Windows can't run shebang'd .js files, and symlinking requires admin /
173
+ // Developer Mode. Ship a .cmd shim (used by cmd.exe and PowerShell) plus a
174
+ // POSIX-style wrapper so Git-Bash / MSYS users get a working `paytaca` too.
175
+ const cmdFile = path.join(binDir, 'paytaca.cmd');
176
+ fs.writeFileSync(cmdFile, '@echo off\r\nnode "' + cliBin + '" %*\r\n', 'utf8');
177
+ try {
178
+ const shFile = path.join(binDir, 'paytaca');
179
+ fs.writeFileSync(shFile, '#!/bin/sh\nexec node "' + cliBin + '" "$@"\n', 'utf8');
180
+ fs.chmodSync(shFile, '755');
181
+ } catch {}
182
+ return cmdFile;
183
+ }
184
+
64
185
  const link = path.join(binDir, 'paytaca');
65
186
  if (fs.existsSync(link) || fs.lstatSync(link, { throwIfNoEntry: false })) {
66
187
  const st = fs.lstatSync(link, { throwIfNoEntry: false });
@@ -74,6 +195,7 @@ async function linkGlobally(cliBin) {
74
195
 
75
196
  async function main() {
76
197
  if (process.env.PAYTACA_PLUGIN_SKIP_POSTINSTALL) return;
198
+ selfPin();
77
199
  if (which('paytaca') && runPaytacaVersion()) return;
78
200
 
79
201
  const cliBin = resolveLocalCliBin();
@@ -86,7 +208,7 @@ async function main() {
86
208
  const link = await linkGlobally(cliBin);
87
209
  log(`Linked paytaca -> ${link}`);
88
210
  } catch (err) {
89
- log(`Could not symlink paytaca globally (${err.message}).`);
211
+ log(`Could not link paytaca globally (${err.message}).`);
90
212
  log('Falling back to: npm install -g paytaca-cli');
91
213
  try {
92
214
  execSync('npm install -g paytaca-cli', { stdio: 'inherit' });
@@ -97,4 +219,21 @@ async function main() {
97
219
  }
98
220
  }
99
221
 
100
- main().catch(() => {});
222
+ // Export helpers for tests. When run as a script (npm postinstall), main()
223
+ // executes; when required by a test, only the functions are exposed.
224
+ if (require.main === module) {
225
+ main().catch(() => {});
226
+ }
227
+
228
+ module.exports = {
229
+ which,
230
+ runPaytacaVersion,
231
+ resolveLocalCliBin,
232
+ globalBinDir,
233
+ asdfReshim,
234
+ selfPin,
235
+ linkGlobally,
236
+ isInside,
237
+ normKey,
238
+ IS_WIN,
239
+ };