@paytaca/opencode-plugin 0.1.6 → 0.1.7
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/dist/bundled/proxy.d.ts +1 -1
- package/dist/bundled/proxy.d.ts.map +1 -1
- package/dist/bundled/proxy.js +363 -47
- package/dist/bundled/proxy.js.map +1 -1
- package/dist/config.d.ts.map +1 -0
- package/dist/config.js.map +1 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +102 -16
- package/dist/index.js.map +1 -0
- package/dist/proxy.d.ts.map +1 -0
- package/dist/proxy.js.map +1 -0
- package/dist/types.d.ts.map +1 -0
- package/dist/types.js.map +1 -0
- package/dist/wallet.d.ts.map +1 -0
- package/dist/wallet.js.map +1 -0
- package/package.json +6 -1
- package/src/bundled/proxy.ts +0 -937
- package/src/bundled/wrapper.ts +0 -187
- package/src/config.ts +0 -105
- package/src/index.ts +0 -70
- package/src/proxy.ts +0 -283
- package/src/types.ts +0 -26
- package/src/wallet.ts +0 -296
- package/tsconfig.json +0 -19
package/src/bundled/wrapper.ts
DELETED
|
@@ -1,187 +0,0 @@
|
|
|
1
|
-
// This file contains the bundled payment wrapper script as a string
|
|
2
|
-
// It gets written to ~/.opencode-paytaca/paytaca-pay-wrapper.mjs at runtime
|
|
3
|
-
// Uses the same approach as the original wrapper but finds paytaca-cli dynamically
|
|
4
|
-
|
|
5
|
-
export const WRAPPER_SCRIPT_CONTENT = `#!/usr/bin/env node
|
|
6
|
-
/**
|
|
7
|
-
* Paytaca Pay Wrapper — handles large request bodies by reading from a file.
|
|
8
|
-
* Imports paytaca-cli modules directly (avoids CLI argument size limits).
|
|
9
|
-
*/
|
|
10
|
-
|
|
11
|
-
import { readFileSync } from 'fs';
|
|
12
|
-
import { execSync } from 'child_process';
|
|
13
|
-
import { fileURLToPath } from 'url';
|
|
14
|
-
import { dirname, join } from 'path';
|
|
15
|
-
|
|
16
|
-
// Find paytaca-cli installation
|
|
17
|
-
function findPaytacaCliPath() {
|
|
18
|
-
const possiblePaths = [];
|
|
19
|
-
|
|
20
|
-
// Try to get global npm root
|
|
21
|
-
try {
|
|
22
|
-
const globalPath = execSync('npm root -g', { encoding: 'utf8' }).trim();
|
|
23
|
-
possiblePaths.push(
|
|
24
|
-
join(globalPath, 'paytaca-cli'),
|
|
25
|
-
join(globalPath, 'opencode-plugin', 'node_modules', 'paytaca-cli'),
|
|
26
|
-
);
|
|
27
|
-
} catch {}
|
|
28
|
-
|
|
29
|
-
// Common global locations
|
|
30
|
-
possiblePaths.push(
|
|
31
|
-
'/usr/lib/node_modules/paytaca-cli',
|
|
32
|
-
'/usr/local/lib/node_modules/paytaca-cli',
|
|
33
|
-
'/opt/homebrew/lib/node_modules/paytaca-cli',
|
|
34
|
-
);
|
|
35
|
-
|
|
36
|
-
// Try current file's node_modules (for bundled installs)
|
|
37
|
-
try {
|
|
38
|
-
const currentFile = fileURLToPath(import.meta.url);
|
|
39
|
-
const currentDir = dirname(currentFile);
|
|
40
|
-
possiblePaths.push(
|
|
41
|
-
join(currentDir, '..', 'node_modules', 'paytaca-cli'),
|
|
42
|
-
join(currentDir, '..', '..', 'node_modules', 'paytaca-cli'),
|
|
43
|
-
);
|
|
44
|
-
} catch {}
|
|
45
|
-
|
|
46
|
-
// Find first valid path
|
|
47
|
-
for (const basePath of possiblePaths) {
|
|
48
|
-
try {
|
|
49
|
-
const walletPath = join(basePath, 'dist', 'wallet', 'index.js');
|
|
50
|
-
readFileSync(walletPath);
|
|
51
|
-
return basePath;
|
|
52
|
-
} catch {}
|
|
53
|
-
}
|
|
54
|
-
|
|
55
|
-
throw new Error('paytaca-cli not found. Try reinstalling opencode-plugin: npm install @paytaca/opencode-plugin');
|
|
56
|
-
}
|
|
57
|
-
|
|
58
|
-
// Load paytaca-cli modules
|
|
59
|
-
let loadMnemonic, loadWallet, LibauthHDWallet, X402Payer, parsePaymentRequiredJson, selectBchPaymentRequirements, BCH_DERIVATION_PATH;
|
|
60
|
-
|
|
61
|
-
try {
|
|
62
|
-
const basePath = findPaytacaCliPath();
|
|
63
|
-
|
|
64
|
-
({ loadMnemonic, loadWallet } = await import(join(basePath, 'dist', 'wallet', 'index.js')));
|
|
65
|
-
({ LibauthHDWallet } = await import(join(basePath, 'dist', 'wallet', 'keys.js')));
|
|
66
|
-
({ X402Payer } = await import(join(basePath, 'dist', 'wallet', 'x402.js')));
|
|
67
|
-
({ parsePaymentRequiredJson, selectBchPaymentRequirements } = await import(join(basePath, 'dist', 'utils', 'x402.js')));
|
|
68
|
-
({ BCH_DERIVATION_PATH } = await import(join(basePath, 'dist', 'utils', 'network.js')));
|
|
69
|
-
} catch (err) {
|
|
70
|
-
console.log(JSON.stringify({ success: false, error: 'Failed to load paytaca-cli: ' + err.message }));
|
|
71
|
-
process.exit(1);
|
|
72
|
-
}
|
|
73
|
-
|
|
74
|
-
async function main() {
|
|
75
|
-
const configPath = process.argv[2];
|
|
76
|
-
if (!configPath) {
|
|
77
|
-
console.log(JSON.stringify({ success: false, error: 'Usage: node paytaca-pay-wrapper.mjs <config.json>' }));
|
|
78
|
-
process.exit(1);
|
|
79
|
-
}
|
|
80
|
-
|
|
81
|
-
const config = JSON.parse(readFileSync(configPath, 'utf8'));
|
|
82
|
-
const { url, method, headers, bodyFile, chipnet, confirmed } = config;
|
|
83
|
-
|
|
84
|
-
const body = readFileSync(bodyFile, 'utf8');
|
|
85
|
-
|
|
86
|
-
const data = loadMnemonic();
|
|
87
|
-
if (!data) {
|
|
88
|
-
console.log(JSON.stringify({ success: false, error: 'No wallet found. Run paytaca wallet create first.' }));
|
|
89
|
-
process.exit(1);
|
|
90
|
-
}
|
|
91
|
-
|
|
92
|
-
const wallet = loadWallet();
|
|
93
|
-
const isChipnet = Boolean(chipnet);
|
|
94
|
-
const bchWallet = wallet.forNetwork(isChipnet);
|
|
95
|
-
const hdWallet = new LibauthHDWallet(data.mnemonic, BCH_DERIVATION_PATH, isChipnet ? 'chipnet' : 'mainnet');
|
|
96
|
-
const x402Payer = new X402Payer({ hdWallet, addressIndex: 0 });
|
|
97
|
-
|
|
98
|
-
try {
|
|
99
|
-
const result = await executePay(url, method, headers, body, bchWallet, x402Payer, isChipnet, confirmed);
|
|
100
|
-
console.log(JSON.stringify(result, null, 2));
|
|
101
|
-
} catch (err) {
|
|
102
|
-
console.log(JSON.stringify({ success: false, error: err.message || String(err) }, null, 2));
|
|
103
|
-
process.exit(1);
|
|
104
|
-
}
|
|
105
|
-
}
|
|
106
|
-
|
|
107
|
-
async function executePay(url, method, headers, body, bchWallet, x402Payer, isChipnet, confirmed) {
|
|
108
|
-
const response = await fetch(url, {
|
|
109
|
-
method,
|
|
110
|
-
headers,
|
|
111
|
-
body: ['POST', 'PUT', 'PATCH'].includes(method) ? body : undefined,
|
|
112
|
-
});
|
|
113
|
-
|
|
114
|
-
const responseHeaders = {};
|
|
115
|
-
response.headers.forEach((value, key) => { responseHeaders[key] = value; });
|
|
116
|
-
const responseText = await response.text();
|
|
117
|
-
let responseData;
|
|
118
|
-
try { responseData = JSON.parse(responseText); } catch { responseData = responseText; }
|
|
119
|
-
|
|
120
|
-
if (response.status === 402) {
|
|
121
|
-
const paymentRequired = parsePaymentRequiredJson(responseData);
|
|
122
|
-
if (!paymentRequired) {
|
|
123
|
-
return { success: false, status: 402, error: 'Could not parse PaymentRequired from 402 response body' };
|
|
124
|
-
}
|
|
125
|
-
const requirements = selectBchPaymentRequirements(paymentRequired, isChipnet ? 'chipnet' : 'mainnet');
|
|
126
|
-
if (!requirements) {
|
|
127
|
-
return {
|
|
128
|
-
success: false, status: 402, error: 'Server does not accept BCH payment',
|
|
129
|
-
data: { acceptedSchemes: paymentRequired.accepts.map(a => ({ scheme: a.scheme, network: a.network })) },
|
|
130
|
-
};
|
|
131
|
-
}
|
|
132
|
-
|
|
133
|
-
const payerAddress = x402Payer.getPayerAddress();
|
|
134
|
-
const address = requirements.payTo;
|
|
135
|
-
const amountBch = Number(requirements.amount) / 1e8;
|
|
136
|
-
const changeAddressSet = bchWallet.getAddressSetAt(0);
|
|
137
|
-
const changeAddress = changeAddressSet.change;
|
|
138
|
-
|
|
139
|
-
if (!confirmed) {
|
|
140
|
-
return {
|
|
141
|
-
success: false, status: 402, error: 'Payment not confirmed.',
|
|
142
|
-
payment: { required: true, amount: requirements.amount, payTo: address },
|
|
143
|
-
};
|
|
144
|
-
}
|
|
145
|
-
|
|
146
|
-
const sendResult = await bchWallet.sendBch(amountBch, address, changeAddress);
|
|
147
|
-
if (!sendResult.success) {
|
|
148
|
-
return { success: false, status: 402, payment: { required: true, error: sendResult.error }, error: sendResult.error };
|
|
149
|
-
}
|
|
150
|
-
|
|
151
|
-
const txid = sendResult.txid;
|
|
152
|
-
const paymentPayload = await x402Payer.createPaymentPayload(requirements, paymentRequired.resource.url, txid, 0, requirements.amount);
|
|
153
|
-
headers['PAYMENT-SIGNATURE'] = JSON.stringify(paymentPayload);
|
|
154
|
-
|
|
155
|
-
const retryResponse = await fetch(url, {
|
|
156
|
-
method,
|
|
157
|
-
headers,
|
|
158
|
-
body: ['POST', 'PUT', 'PATCH'].includes(method) ? body : undefined,
|
|
159
|
-
});
|
|
160
|
-
const retryResponseHeaders = {};
|
|
161
|
-
retryResponse.headers.forEach((value, key) => { retryResponseHeaders[key] = value; });
|
|
162
|
-
const retryResponseText = await retryResponse.text();
|
|
163
|
-
let retryResponseData;
|
|
164
|
-
try { retryResponseData = JSON.parse(retryResponseText); } catch { retryResponseData = retryResponseText; }
|
|
165
|
-
|
|
166
|
-
return {
|
|
167
|
-
success: retryResponse.ok,
|
|
168
|
-
status: retryResponse.status,
|
|
169
|
-
statusText: retryResponse.statusText,
|
|
170
|
-
headers: retryResponseHeaders,
|
|
171
|
-
data: retryResponseData,
|
|
172
|
-
payment: { required: true, txid, recipientAddress: address },
|
|
173
|
-
};
|
|
174
|
-
}
|
|
175
|
-
|
|
176
|
-
return {
|
|
177
|
-
success: response.ok,
|
|
178
|
-
status: response.status,
|
|
179
|
-
statusText: response.statusText,
|
|
180
|
-
headers: responseHeaders,
|
|
181
|
-
data: responseData,
|
|
182
|
-
payment: { required: false },
|
|
183
|
-
};
|
|
184
|
-
}
|
|
185
|
-
|
|
186
|
-
main();
|
|
187
|
-
`;
|
package/src/config.ts
DELETED
|
@@ -1,105 +0,0 @@
|
|
|
1
|
-
import * as fs from 'fs';
|
|
2
|
-
import * as path from 'path';
|
|
3
|
-
import { Config } from './types';
|
|
4
|
-
|
|
5
|
-
// Default production backend
|
|
6
|
-
export const DEFAULT_BACKEND_URL = 'https://api.paytaca.ai';
|
|
7
|
-
export const DEFAULT_PROXY_PORT = 8001;
|
|
8
|
-
|
|
9
|
-
// Config directory: ~/.opencode-paytaca/
|
|
10
|
-
export function getConfigDir(): string {
|
|
11
|
-
const home = process.env.HOME || process.env.USERPROFILE || '.';
|
|
12
|
-
return path.join(home, '.opencode-paytaca');
|
|
13
|
-
}
|
|
14
|
-
|
|
15
|
-
export function ensureConfigDir(configDir: string): void {
|
|
16
|
-
if (!fs.existsSync(configDir)) {
|
|
17
|
-
fs.mkdirSync(configDir, { recursive: true });
|
|
18
|
-
}
|
|
19
|
-
}
|
|
20
|
-
|
|
21
|
-
export function getConfigFile(configDir: string): string {
|
|
22
|
-
return path.join(configDir, 'config.json');
|
|
23
|
-
}
|
|
24
|
-
|
|
25
|
-
export function loadConfig(configDir: string): Config {
|
|
26
|
-
const configFile = getConfigFile(configDir);
|
|
27
|
-
|
|
28
|
-
// Priority 1: Environment variable (highest priority, for dev/testing)
|
|
29
|
-
const envBackendUrl = process.env.PAYTACA_BACKEND_URL;
|
|
30
|
-
if (envBackendUrl) {
|
|
31
|
-
return {
|
|
32
|
-
backendUrl: envBackendUrl,
|
|
33
|
-
proxyPort: DEFAULT_PROXY_PORT,
|
|
34
|
-
};
|
|
35
|
-
}
|
|
36
|
-
|
|
37
|
-
// Priority 2: Config file
|
|
38
|
-
if (fs.existsSync(configFile)) {
|
|
39
|
-
try {
|
|
40
|
-
const content = fs.readFileSync(configFile, 'utf8');
|
|
41
|
-
const parsed = JSON.parse(content);
|
|
42
|
-
return {
|
|
43
|
-
backendUrl: parsed.backendUrl || DEFAULT_BACKEND_URL,
|
|
44
|
-
proxyPort: parsed.proxyPort || DEFAULT_PROXY_PORT,
|
|
45
|
-
walletHash: parsed.walletHash,
|
|
46
|
-
proxyPid: parsed.proxyPid,
|
|
47
|
-
};
|
|
48
|
-
} catch (err) {
|
|
49
|
-
console.error('Failed to parse config:', err);
|
|
50
|
-
}
|
|
51
|
-
}
|
|
52
|
-
|
|
53
|
-
// Priority 3: Default production backend
|
|
54
|
-
return {
|
|
55
|
-
backendUrl: DEFAULT_BACKEND_URL,
|
|
56
|
-
proxyPort: DEFAULT_PROXY_PORT,
|
|
57
|
-
};
|
|
58
|
-
}
|
|
59
|
-
|
|
60
|
-
export function saveConfig(configDir: string, config: Config): void {
|
|
61
|
-
const configFile = getConfigFile(configDir);
|
|
62
|
-
ensureConfigDir(configDir);
|
|
63
|
-
fs.writeFileSync(configFile, JSON.stringify(config, null, 2));
|
|
64
|
-
}
|
|
65
|
-
|
|
66
|
-
export function getPidFile(configDir: string): string {
|
|
67
|
-
return path.join(configDir, 'proxy.pid');
|
|
68
|
-
}
|
|
69
|
-
|
|
70
|
-
export function getProxyScript(configDir: string): string {
|
|
71
|
-
return path.join(configDir, 'proxy.js');
|
|
72
|
-
}
|
|
73
|
-
|
|
74
|
-
export function getLogFile(configDir: string): string {
|
|
75
|
-
return path.join(configDir, 'proxy.log');
|
|
76
|
-
}
|
|
77
|
-
|
|
78
|
-
export function getWrapperScript(configDir: string): string {
|
|
79
|
-
return path.join(configDir, 'paytaca-pay-wrapper.mjs');
|
|
80
|
-
}
|
|
81
|
-
|
|
82
|
-
export function getHeartbeatFile(configDir: string): string {
|
|
83
|
-
return path.join(configDir, 'heartbeat');
|
|
84
|
-
}
|
|
85
|
-
|
|
86
|
-
// Touch the heartbeat file to signal the proxy we're still alive
|
|
87
|
-
export function updateHeartbeat(configDir: string): void {
|
|
88
|
-
const heartbeatFile = getHeartbeatFile(configDir);
|
|
89
|
-
try {
|
|
90
|
-
// Write current timestamp
|
|
91
|
-
fs.writeFileSync(heartbeatFile, Date.now().toString());
|
|
92
|
-
} catch (err) {
|
|
93
|
-
// Ignore errors
|
|
94
|
-
}
|
|
95
|
-
}
|
|
96
|
-
|
|
97
|
-
// Start heartbeat interval that updates every 5 seconds
|
|
98
|
-
export function startHeartbeat(configDir: string): NodeJS.Timeout {
|
|
99
|
-
// Update immediately
|
|
100
|
-
updateHeartbeat(configDir);
|
|
101
|
-
// Then every 5 seconds
|
|
102
|
-
return setInterval(() => {
|
|
103
|
-
updateHeartbeat(configDir);
|
|
104
|
-
}, 5000);
|
|
105
|
-
}
|
package/src/index.ts
DELETED
|
@@ -1,70 +0,0 @@
|
|
|
1
|
-
import { ensureConfigDir, getConfigDir, loadConfig, saveConfig } from './config';
|
|
2
|
-
import { checkWallet, ensureWallet, checkPaytacaCli, ensurePaytacaOnPath } from './wallet';
|
|
3
|
-
import { startProxy } from './proxy';
|
|
4
|
-
|
|
5
|
-
async function OpencodePlugin(_input?: any, _options?: any) {
|
|
6
|
-
const configDir = getConfigDir();
|
|
7
|
-
ensureConfigDir(configDir);
|
|
8
|
-
|
|
9
|
-
let config = loadConfig(configDir);
|
|
10
|
-
|
|
11
|
-
// Ensure paytaca binary is on PATH for internal use
|
|
12
|
-
ensurePaytacaOnPath();
|
|
13
|
-
|
|
14
|
-
// Check if paytaca-cli is installed
|
|
15
|
-
const hasPaytacaCli = await checkPaytacaCli();
|
|
16
|
-
if (!hasPaytacaCli) {
|
|
17
|
-
console.error('paytaca-cli not found. Install it with: npm install -g paytaca-cli');
|
|
18
|
-
return {};
|
|
19
|
-
}
|
|
20
|
-
|
|
21
|
-
// Ensure wallet exists (auto-create if needed)
|
|
22
|
-
try {
|
|
23
|
-
const wallet = await ensureWallet();
|
|
24
|
-
|
|
25
|
-
// Save wallet hash to config
|
|
26
|
-
if (wallet.hash) {
|
|
27
|
-
config.walletHash = wallet.hash;
|
|
28
|
-
saveConfig(configDir, config);
|
|
29
|
-
}
|
|
30
|
-
} catch (err: any) {
|
|
31
|
-
console.error('Wallet setup failed:', err.message);
|
|
32
|
-
return {};
|
|
33
|
-
}
|
|
34
|
-
|
|
35
|
-
// Start or reuse proxy
|
|
36
|
-
const proxy = await startProxy(configDir, config);
|
|
37
|
-
|
|
38
|
-
return {
|
|
39
|
-
config: async (cfg: any) => {
|
|
40
|
-
cfg.provider = cfg.provider || {};
|
|
41
|
-
cfg.provider['paytaca-ai'] = {
|
|
42
|
-
npm: '@ai-sdk/openai-compatible',
|
|
43
|
-
name: 'Paytaca AI',
|
|
44
|
-
options: {
|
|
45
|
-
baseURL: `http://localhost:${proxy.port}/v1`
|
|
46
|
-
},
|
|
47
|
-
models: {
|
|
48
|
-
'deepseek-ai/DeepSeek-V4-Flash': {
|
|
49
|
-
name: 'DeepSeek V4 Flash',
|
|
50
|
-
limit: {
|
|
51
|
-
context: 128000,
|
|
52
|
-
output: 8192
|
|
53
|
-
}
|
|
54
|
-
}
|
|
55
|
-
}
|
|
56
|
-
};
|
|
57
|
-
},
|
|
58
|
-
"chat.headers": async (_input: any, output: any) => {
|
|
59
|
-
let wallet = await checkWallet();
|
|
60
|
-
if (!wallet.exists) {
|
|
61
|
-
wallet = await ensureWallet();
|
|
62
|
-
}
|
|
63
|
-
output.headers = {
|
|
64
|
-
'X-Wallet-Hash': wallet.hash || ''
|
|
65
|
-
};
|
|
66
|
-
}
|
|
67
|
-
};
|
|
68
|
-
}
|
|
69
|
-
|
|
70
|
-
export = { id: '@paytaca/opencode-plugin', server: OpencodePlugin };
|
package/src/proxy.ts
DELETED
|
@@ -1,283 +0,0 @@
|
|
|
1
|
-
import * as fs from 'fs';
|
|
2
|
-
import * as path from 'path';
|
|
3
|
-
import { spawn, execSync } from 'child_process';
|
|
4
|
-
import { Config, ProxyInfo } from './types';
|
|
5
|
-
import {
|
|
6
|
-
getConfigDir,
|
|
7
|
-
getPidFile,
|
|
8
|
-
getProxyScript,
|
|
9
|
-
getLogFile,
|
|
10
|
-
getWrapperScript,
|
|
11
|
-
getHeartbeatFile,
|
|
12
|
-
saveConfig,
|
|
13
|
-
loadConfig,
|
|
14
|
-
ensureConfigDir
|
|
15
|
-
} from './config';
|
|
16
|
-
import { PROXY_SCRIPT_CONTENT } from './bundled/proxy';
|
|
17
|
-
import { WRAPPER_SCRIPT_CONTENT } from './bundled/wrapper';
|
|
18
|
-
|
|
19
|
-
// Store heartbeat interval reference
|
|
20
|
-
let heartbeatInterval: NodeJS.Timeout | null = null;
|
|
21
|
-
|
|
22
|
-
// Get path to paytaca binary (multi-strategy resolution)
|
|
23
|
-
function getPaytacaCommand(): string {
|
|
24
|
-
// Priority 1: Local node_modules (via require.resolve)
|
|
25
|
-
try {
|
|
26
|
-
const paytacaCliPkg = require.resolve('paytaca-cli/package.json');
|
|
27
|
-
return path.resolve(path.dirname(paytacaCliPkg), 'bin', 'paytaca.js');
|
|
28
|
-
} catch {}
|
|
29
|
-
|
|
30
|
-
// Priority 2: Local .bin symlink
|
|
31
|
-
const localPaytaca = path.join(__dirname, '..', 'node_modules', '.bin', 'paytaca');
|
|
32
|
-
if (fs.existsSync(localPaytaca)) {
|
|
33
|
-
return localPaytaca;
|
|
34
|
-
}
|
|
35
|
-
|
|
36
|
-
// Priority 3: Global npm root
|
|
37
|
-
try {
|
|
38
|
-
const globalRoot = execSync('npm root -g', { encoding: 'utf8' }).trim();
|
|
39
|
-
const pathsToCheck = [
|
|
40
|
-
path.join(globalRoot, 'paytaca-cli', 'bin', 'paytaca.js'),
|
|
41
|
-
path.join(globalRoot, '@paytaca', 'opencode-plugin', 'node_modules', 'paytaca-cli', 'bin', 'paytaca.js'),
|
|
42
|
-
];
|
|
43
|
-
for (const p of pathsToCheck) {
|
|
44
|
-
if (fs.existsSync(p)) {
|
|
45
|
-
return p;
|
|
46
|
-
}
|
|
47
|
-
}
|
|
48
|
-
} catch {}
|
|
49
|
-
|
|
50
|
-
// Priority 4: Common global installation paths
|
|
51
|
-
const commonPaths = [
|
|
52
|
-
'/usr/lib/node_modules/paytaca-cli/bin/paytaca.js',
|
|
53
|
-
'/usr/local/lib/node_modules/paytaca-cli/bin/paytaca.js',
|
|
54
|
-
'/opt/homebrew/lib/node_modules/paytaca-cli/bin/paytaca.js',
|
|
55
|
-
];
|
|
56
|
-
for (const p of commonPaths) {
|
|
57
|
-
if (fs.existsSync(p)) {
|
|
58
|
-
return p;
|
|
59
|
-
}
|
|
60
|
-
}
|
|
61
|
-
|
|
62
|
-
// Priority 5: which/where on PATH
|
|
63
|
-
try {
|
|
64
|
-
const which = process.platform === 'win32' ? 'where' : 'which';
|
|
65
|
-
const result = execSync(`${which} paytaca`, { encoding: 'utf8' }).trim().split('\n')[0];
|
|
66
|
-
if (result) {
|
|
67
|
-
return result;
|
|
68
|
-
}
|
|
69
|
-
} catch {}
|
|
70
|
-
|
|
71
|
-
// Priority 6: Bare command (rely on PATH at runtime)
|
|
72
|
-
return 'paytaca';
|
|
73
|
-
}
|
|
74
|
-
|
|
75
|
-
export async function isPortAvailable(port: number): Promise<boolean> {
|
|
76
|
-
return new Promise((resolve) => {
|
|
77
|
-
const server = require('net').createServer();
|
|
78
|
-
server.once('error', () => {
|
|
79
|
-
resolve(false);
|
|
80
|
-
});
|
|
81
|
-
server.once('listening', () => {
|
|
82
|
-
server.close();
|
|
83
|
-
resolve(true);
|
|
84
|
-
});
|
|
85
|
-
server.listen(port, '127.0.0.1');
|
|
86
|
-
});
|
|
87
|
-
}
|
|
88
|
-
|
|
89
|
-
export async function findAvailablePort(startPort: number = 8001, endPort: number = 8010): Promise<number> {
|
|
90
|
-
for (let port = startPort; port <= endPort; port++) {
|
|
91
|
-
if (await isPortAvailable(port)) {
|
|
92
|
-
return port;
|
|
93
|
-
}
|
|
94
|
-
}
|
|
95
|
-
throw new Error(`No available ports in range ${startPort}-${endPort}`);
|
|
96
|
-
}
|
|
97
|
-
|
|
98
|
-
export function isProcessRunning(pid: number): boolean {
|
|
99
|
-
try {
|
|
100
|
-
process.kill(pid, 0);
|
|
101
|
-
return true;
|
|
102
|
-
} catch {
|
|
103
|
-
return false;
|
|
104
|
-
}
|
|
105
|
-
}
|
|
106
|
-
|
|
107
|
-
export async function getProxyStatus(configDir: string): Promise<{ running: boolean; port?: number; pid?: number }> {
|
|
108
|
-
const pidFile = getPidFile(configDir);
|
|
109
|
-
|
|
110
|
-
if (!fs.existsSync(pidFile)) {
|
|
111
|
-
return { running: false };
|
|
112
|
-
}
|
|
113
|
-
|
|
114
|
-
try {
|
|
115
|
-
const pid = parseInt(fs.readFileSync(pidFile, 'utf8').trim());
|
|
116
|
-
if (isProcessRunning(pid)) {
|
|
117
|
-
const config = loadConfig(configDir);
|
|
118
|
-
return { running: true, port: config.proxyPort, pid };
|
|
119
|
-
}
|
|
120
|
-
} catch {
|
|
121
|
-
// Invalid PID file
|
|
122
|
-
}
|
|
123
|
-
|
|
124
|
-
return { running: false };
|
|
125
|
-
}
|
|
126
|
-
|
|
127
|
-
export async function startProxy(configDir: string, config: Config): Promise<ProxyInfo> {
|
|
128
|
-
const pidFile = getPidFile(configDir);
|
|
129
|
-
const proxyScript = getProxyScript(configDir);
|
|
130
|
-
const logFile = getLogFile(configDir);
|
|
131
|
-
const wrapperScript = getWrapperScript(configDir);
|
|
132
|
-
|
|
133
|
-
// Ensure config directory exists
|
|
134
|
-
ensureConfigDir(configDir);
|
|
135
|
-
|
|
136
|
-
// Check if proxy already running
|
|
137
|
-
const existingStatus = await getProxyStatus(configDir);
|
|
138
|
-
if (existingStatus.running && existingStatus.pid && existingStatus.port) {
|
|
139
|
-
// Just reuse existing proxy, update heartbeat
|
|
140
|
-
const heartbeatFile = getHeartbeatFile(configDir);
|
|
141
|
-
fs.writeFileSync(heartbeatFile, Date.now().toString());
|
|
142
|
-
|
|
143
|
-
// Start heartbeat updates
|
|
144
|
-
if (heartbeatInterval) {
|
|
145
|
-
clearInterval(heartbeatInterval);
|
|
146
|
-
}
|
|
147
|
-
heartbeatInterval = setInterval(() => {
|
|
148
|
-
try {
|
|
149
|
-
fs.writeFileSync(heartbeatFile, Date.now().toString());
|
|
150
|
-
} catch {}
|
|
151
|
-
}, 5000);
|
|
152
|
-
|
|
153
|
-
return {
|
|
154
|
-
port: existingStatus.port,
|
|
155
|
-
pid: existingStatus.pid
|
|
156
|
-
};
|
|
157
|
-
}
|
|
158
|
-
|
|
159
|
-
// Find available port
|
|
160
|
-
const port = await findAvailablePort(8001, 8010);
|
|
161
|
-
|
|
162
|
-
// Write bundled proxy script to config directory
|
|
163
|
-
fs.writeFileSync(proxyScript, PROXY_SCRIPT_CONTENT, 'utf8');
|
|
164
|
-
fs.chmodSync(proxyScript, '755');
|
|
165
|
-
|
|
166
|
-
// Write bundled wrapper script to config directory
|
|
167
|
-
fs.writeFileSync(wrapperScript, WRAPPER_SCRIPT_CONTENT, 'utf8');
|
|
168
|
-
fs.chmodSync(wrapperScript, '755');
|
|
169
|
-
|
|
170
|
-
// Start proxy (detached to avoid receiving SIGINT from terminal)
|
|
171
|
-
const paytacaCmd = getPaytacaCommand();
|
|
172
|
-
const proxy = spawn('node', [
|
|
173
|
-
proxyScript,
|
|
174
|
-
config.backendUrl,
|
|
175
|
-
port.toString()
|
|
176
|
-
], {
|
|
177
|
-
detached: true,
|
|
178
|
-
stdio: ['ignore', 'pipe', 'pipe'],
|
|
179
|
-
env: {
|
|
180
|
-
...process.env,
|
|
181
|
-
PAYTACA_CMD: paytacaCmd
|
|
182
|
-
}
|
|
183
|
-
});
|
|
184
|
-
|
|
185
|
-
proxy.unref();
|
|
186
|
-
|
|
187
|
-
if (!proxy.pid) {
|
|
188
|
-
throw new Error('Failed to start proxy: no PID available');
|
|
189
|
-
}
|
|
190
|
-
|
|
191
|
-
// Write PID file
|
|
192
|
-
fs.writeFileSync(pidFile, proxy.pid.toString());
|
|
193
|
-
|
|
194
|
-
// Update config - new proxy (preserve wallet hash from passed config)
|
|
195
|
-
config.proxyPort = port;
|
|
196
|
-
config.proxyPid = proxy.pid;
|
|
197
|
-
saveConfig(configDir, config);
|
|
198
|
-
|
|
199
|
-
// Initialize heartbeat file
|
|
200
|
-
const heartbeatFile = getHeartbeatFile(configDir);
|
|
201
|
-
fs.writeFileSync(heartbeatFile, Date.now().toString());
|
|
202
|
-
|
|
203
|
-
// Start heartbeat updates
|
|
204
|
-
if (heartbeatInterval) {
|
|
205
|
-
clearInterval(heartbeatInterval);
|
|
206
|
-
}
|
|
207
|
-
heartbeatInterval = setInterval(() => {
|
|
208
|
-
try {
|
|
209
|
-
fs.writeFileSync(heartbeatFile, Date.now().toString());
|
|
210
|
-
} catch {}
|
|
211
|
-
}, 5000);
|
|
212
|
-
|
|
213
|
-
// Handle proxy output
|
|
214
|
-
const logStream = fs.createWriteStream(logFile, { flags: 'a' });
|
|
215
|
-
proxy.stdout?.pipe(logStream);
|
|
216
|
-
proxy.stderr?.pipe(logStream);
|
|
217
|
-
|
|
218
|
-
// Wait for proxy to be ready
|
|
219
|
-
await waitForProxy(port);
|
|
220
|
-
|
|
221
|
-
return {
|
|
222
|
-
port,
|
|
223
|
-
pid: proxy.pid
|
|
224
|
-
};
|
|
225
|
-
}
|
|
226
|
-
|
|
227
|
-
export async function stopProxy(configDir: string): Promise<void> {
|
|
228
|
-
// Stop heartbeat updates
|
|
229
|
-
if (heartbeatInterval) {
|
|
230
|
-
clearInterval(heartbeatInterval);
|
|
231
|
-
heartbeatInterval = null;
|
|
232
|
-
}
|
|
233
|
-
|
|
234
|
-
// Note: We don't kill the proxy here anymore.
|
|
235
|
-
// The proxy monitors the heartbeat file and exits itself when stale.
|
|
236
|
-
// This handles multi-window scenarios correctly.
|
|
237
|
-
|
|
238
|
-
// Optional: Write a special "stopping" timestamp to speed up proxy shutdown
|
|
239
|
-
const heartbeatFile = getHeartbeatFile(configDir);
|
|
240
|
-
try {
|
|
241
|
-
fs.writeFileSync(heartbeatFile, '0'); // Special value: stopping
|
|
242
|
-
// Remove heartbeat file after a short delay
|
|
243
|
-
setTimeout(() => {
|
|
244
|
-
try {
|
|
245
|
-
if (fs.existsSync(heartbeatFile)) {
|
|
246
|
-
fs.unlinkSync(heartbeatFile);
|
|
247
|
-
}
|
|
248
|
-
} catch {}
|
|
249
|
-
}, 100);
|
|
250
|
-
} catch {}
|
|
251
|
-
}
|
|
252
|
-
|
|
253
|
-
async function waitForProxy(port: number, timeout: number = 10000): Promise<void> {
|
|
254
|
-
const start = Date.now();
|
|
255
|
-
|
|
256
|
-
while (Date.now() - start < timeout) {
|
|
257
|
-
try {
|
|
258
|
-
const response = await fetch(`http://localhost:${port}/v1/config`, {
|
|
259
|
-
signal: AbortSignal.timeout(1000)
|
|
260
|
-
});
|
|
261
|
-
if (response.ok) {
|
|
262
|
-
return;
|
|
263
|
-
}
|
|
264
|
-
} catch {
|
|
265
|
-
// Not ready yet
|
|
266
|
-
}
|
|
267
|
-
await new Promise(resolve => setTimeout(resolve, 500));
|
|
268
|
-
}
|
|
269
|
-
|
|
270
|
-
throw new Error(`Proxy failed to start on port ${port} within ${timeout}ms`);
|
|
271
|
-
}
|
|
272
|
-
|
|
273
|
-
export async function getProxyConfig(configDir: string): Promise<{ backendUrl: string; port: number } | null> {
|
|
274
|
-
const status = await getProxyStatus(configDir);
|
|
275
|
-
if (status.running && status.port) {
|
|
276
|
-
const config = loadConfig(configDir);
|
|
277
|
-
return {
|
|
278
|
-
backendUrl: config.backendUrl,
|
|
279
|
-
port: status.port
|
|
280
|
-
};
|
|
281
|
-
}
|
|
282
|
-
return null;
|
|
283
|
-
}
|
package/src/types.ts
DELETED
|
@@ -1,26 +0,0 @@
|
|
|
1
|
-
export interface Config {
|
|
2
|
-
backendUrl: string;
|
|
3
|
-
proxyPort: number;
|
|
4
|
-
walletHash?: string;
|
|
5
|
-
proxyPid?: number;
|
|
6
|
-
}
|
|
7
|
-
|
|
8
|
-
export interface WalletInfo {
|
|
9
|
-
exists: boolean;
|
|
10
|
-
hash?: string;
|
|
11
|
-
address?: string;
|
|
12
|
-
balance?: string;
|
|
13
|
-
mnemonic?: string;
|
|
14
|
-
}
|
|
15
|
-
|
|
16
|
-
export interface ProxyStatus {
|
|
17
|
-
running: boolean;
|
|
18
|
-
port?: number;
|
|
19
|
-
pid?: number;
|
|
20
|
-
url?: string;
|
|
21
|
-
}
|
|
22
|
-
|
|
23
|
-
export interface ProxyInfo {
|
|
24
|
-
port: number;
|
|
25
|
-
pid: number;
|
|
26
|
-
}
|