globodai-mcp-payment-manager 1.0.1
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/.env.example +23 -0
- package/.github/workflows/ci.yml +26 -0
- package/.github/workflows/release.yml +82 -0
- package/LICENSE +21 -0
- package/README.md +362 -0
- package/dist/index.d.ts +31 -0
- package/dist/index.js +122 -0
- package/dist/lib/blockchain.d.ts +50 -0
- package/dist/lib/blockchain.js +287 -0
- package/dist/lib/cards.d.ts +83 -0
- package/dist/lib/cards.js +276 -0
- package/dist/lib/cli-runner.d.ts +31 -0
- package/dist/lib/cli-runner.js +77 -0
- package/dist/lib/crypto.d.ts +39 -0
- package/dist/lib/crypto.js +228 -0
- package/dist/lib/cvv-crypto.d.ts +23 -0
- package/dist/lib/cvv-crypto.js +67 -0
- package/dist/lib/mcp-core.d.ts +46 -0
- package/dist/lib/mcp-core.js +86 -0
- package/dist/lib/pin-manager.d.ts +69 -0
- package/dist/lib/pin-manager.js +199 -0
- package/dist/lib/wallets.d.ts +91 -0
- package/dist/lib/wallets.js +227 -0
- package/dist/tools/add-card.d.ts +65 -0
- package/dist/tools/add-card.js +97 -0
- package/dist/tools/add-wallet.d.ts +65 -0
- package/dist/tools/add-wallet.js +104 -0
- package/dist/tools/card-status.d.ts +20 -0
- package/dist/tools/card-status.js +26 -0
- package/dist/tools/confirm-payment.d.ts +44 -0
- package/dist/tools/confirm-payment.js +88 -0
- package/dist/tools/get-total-balance.d.ts +41 -0
- package/dist/tools/get-total-balance.js +98 -0
- package/dist/tools/get-transactions.d.ts +39 -0
- package/dist/tools/get-transactions.js +40 -0
- package/dist/tools/get-wallet-balance.d.ts +43 -0
- package/dist/tools/get-wallet-balance.js +69 -0
- package/dist/tools/list-cards.d.ts +36 -0
- package/dist/tools/list-cards.js +39 -0
- package/dist/tools/list-wallet-transactions.d.ts +63 -0
- package/dist/tools/list-wallet-transactions.js +76 -0
- package/dist/tools/list-wallets.d.ts +41 -0
- package/dist/tools/list-wallets.js +50 -0
- package/dist/tools/lock-cards.d.ts +16 -0
- package/dist/tools/lock-cards.js +23 -0
- package/dist/tools/prepare-crypto-tx.d.ts +69 -0
- package/dist/tools/prepare-crypto-tx.js +93 -0
- package/dist/tools/prepare-payment.d.ts +57 -0
- package/dist/tools/prepare-payment.js +93 -0
- package/dist/tools/remove-card.d.ts +25 -0
- package/dist/tools/remove-card.js +39 -0
- package/dist/tools/remove-wallet.d.ts +27 -0
- package/dist/tools/remove-wallet.js +40 -0
- package/dist/tools/setup-pin.d.ts +26 -0
- package/dist/tools/setup-pin.js +33 -0
- package/dist/tools/sign-crypto-tx.d.ts +42 -0
- package/dist/tools/sign-crypto-tx.js +75 -0
- package/dist/tools/unlock-cards.d.ts +35 -0
- package/dist/tools/unlock-cards.js +41 -0
- package/package.json +50 -0
- package/src/index.ts +139 -0
- package/src/lib/blockchain.ts +375 -0
- package/src/lib/cards.ts +372 -0
- package/src/lib/cli-runner.ts +113 -0
- package/src/lib/crypto.ts +284 -0
- package/src/lib/cvv-crypto.ts +81 -0
- package/src/lib/mcp-core.ts +127 -0
- package/src/lib/pin-manager.ts +252 -0
- package/src/lib/wallets.ts +331 -0
- package/src/tools/add-card.ts +108 -0
- package/src/tools/add-wallet.ts +114 -0
- package/src/tools/card-status.ts +32 -0
- package/src/tools/confirm-payment.ts +103 -0
- package/src/tools/get-total-balance.ts +123 -0
- package/src/tools/get-transactions.ts +49 -0
- package/src/tools/get-wallet-balance.ts +75 -0
- package/src/tools/list-cards.ts +52 -0
- package/src/tools/list-wallet-transactions.ts +83 -0
- package/src/tools/list-wallets.ts +63 -0
- package/src/tools/lock-cards.ts +31 -0
- package/src/tools/prepare-crypto-tx.ts +108 -0
- package/src/tools/prepare-payment.ts +108 -0
- package/src/tools/remove-card.ts +46 -0
- package/src/tools/remove-wallet.ts +47 -0
- package/src/tools/setup-pin.ts +39 -0
- package/src/tools/sign-crypto-tx.ts +90 -0
- package/src/tools/unlock-cards.ts +48 -0
- package/tsconfig.json +19 -0
package/src/lib/cards.ts
ADDED
|
@@ -0,0 +1,372 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Payment Card Management
|
|
3
|
+
*
|
|
4
|
+
* Secure storage for payment cards with encryption at rest.
|
|
5
|
+
* Uses the same crypto system as mail-manager for consistency.
|
|
6
|
+
*
|
|
7
|
+
* Security model:
|
|
8
|
+
* - Card numbers stored encrypted (only last 4 digits visible in plaintext)
|
|
9
|
+
* - CVV encrypted with PIN-derived key (requires unlock to use)
|
|
10
|
+
* - Expiration dates encrypted
|
|
11
|
+
* - All sensitive operations require explicit confirmation
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
import { existsSync, readFileSync, writeFileSync, mkdirSync } from "fs";
|
|
15
|
+
import { homedir } from "os";
|
|
16
|
+
import { join } from "path";
|
|
17
|
+
import { encrypt, decrypt, isEncrypted } from "./crypto";
|
|
18
|
+
import { encryptCvv, decryptCvv, isCvvEncrypted } from "./cvv-crypto";
|
|
19
|
+
import { isUnlocked } from "./pin-manager";
|
|
20
|
+
|
|
21
|
+
// Fields that must be encrypted with system key
|
|
22
|
+
const SENSITIVE_FIELDS = ["cardNumber", "expirationDate"];
|
|
23
|
+
// CVV is encrypted with PIN-derived key (separate)
|
|
24
|
+
|
|
25
|
+
export type CardType = "visa" | "mastercard" | "amex" | "discover" | "other";
|
|
26
|
+
export type CardUsage = "flight" | "train" | "hotel" | "general" | "all";
|
|
27
|
+
|
|
28
|
+
export interface PaymentCard {
|
|
29
|
+
id: string;
|
|
30
|
+
nickname: string; // e.g., "Visa perso", "Amex pro"
|
|
31
|
+
cardType: CardType;
|
|
32
|
+
lastFourDigits: string; // Always stored in plaintext for identification
|
|
33
|
+
cardNumber: string; // Encrypted - full card number
|
|
34
|
+
expirationDate: string; // Encrypted - MM/YY format
|
|
35
|
+
cvv?: string; // Encrypted with PIN-derived key
|
|
36
|
+
cardholderName: string;
|
|
37
|
+
billingAddress?: {
|
|
38
|
+
street: string;
|
|
39
|
+
city: string;
|
|
40
|
+
postalCode: string;
|
|
41
|
+
country: string;
|
|
42
|
+
};
|
|
43
|
+
// Usage restrictions
|
|
44
|
+
allowedUsage: CardUsage[];
|
|
45
|
+
enabled: boolean;
|
|
46
|
+
// Spending limits (optional)
|
|
47
|
+
limits?: {
|
|
48
|
+
perTransaction?: number;
|
|
49
|
+
daily?: number;
|
|
50
|
+
monthly?: number;
|
|
51
|
+
currency: string;
|
|
52
|
+
};
|
|
53
|
+
// Metadata
|
|
54
|
+
addedAt: string;
|
|
55
|
+
lastUsedAt?: string;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
// Transaction for audit log
|
|
59
|
+
export interface Transaction {
|
|
60
|
+
id: string;
|
|
61
|
+
cardId: string;
|
|
62
|
+
type: "flight" | "train" | "hotel" | "general";
|
|
63
|
+
amount: number;
|
|
64
|
+
currency: string;
|
|
65
|
+
description: string;
|
|
66
|
+
provider: string;
|
|
67
|
+
status: "pending" | "confirmed" | "completed" | "failed" | "refunded";
|
|
68
|
+
createdAt: string;
|
|
69
|
+
confirmedAt?: string;
|
|
70
|
+
completedAt?: string;
|
|
71
|
+
reference?: string; // Booking reference
|
|
72
|
+
details?: Record<string, unknown>;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
const CONFIG_DIR = join(homedir(), ".mcp-ecosystem");
|
|
76
|
+
const CARDS_FILE = join(CONFIG_DIR, "payment-cards.json");
|
|
77
|
+
const TRANSACTIONS_FILE = join(CONFIG_DIR, "payment-transactions.json");
|
|
78
|
+
|
|
79
|
+
/**
|
|
80
|
+
* Detect card type from number
|
|
81
|
+
*/
|
|
82
|
+
export function detectCardType(cardNumber: string): CardType {
|
|
83
|
+
const cleaned = cardNumber.replace(/\D/g, "");
|
|
84
|
+
|
|
85
|
+
if (/^4/.test(cleaned)) return "visa";
|
|
86
|
+
if (/^5[1-5]/.test(cleaned) || /^2[2-7]/.test(cleaned)) return "mastercard";
|
|
87
|
+
if (/^3[47]/.test(cleaned)) return "amex";
|
|
88
|
+
if (/^6(?:011|5)/.test(cleaned)) return "discover";
|
|
89
|
+
|
|
90
|
+
return "other";
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
/**
|
|
94
|
+
* Validate card number using Luhn algorithm
|
|
95
|
+
*/
|
|
96
|
+
export function validateCardNumber(cardNumber: string): boolean {
|
|
97
|
+
const cleaned = cardNumber.replace(/\D/g, "");
|
|
98
|
+
|
|
99
|
+
if (cleaned.length < 13 || cleaned.length > 19) return false;
|
|
100
|
+
|
|
101
|
+
let sum = 0;
|
|
102
|
+
let isEven = false;
|
|
103
|
+
|
|
104
|
+
for (let i = cleaned.length - 1; i >= 0; i--) {
|
|
105
|
+
let digit = parseInt(cleaned[i]!, 10);
|
|
106
|
+
|
|
107
|
+
if (isEven) {
|
|
108
|
+
digit *= 2;
|
|
109
|
+
if (digit > 9) digit -= 9;
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
sum += digit;
|
|
113
|
+
isEven = !isEven;
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
return sum % 10 === 0;
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
/**
|
|
120
|
+
* Validate expiration date
|
|
121
|
+
*/
|
|
122
|
+
export function validateExpiration(expiration: string): boolean {
|
|
123
|
+
const match = expiration.match(/^(\d{2})\/(\d{2})$/);
|
|
124
|
+
if (!match) return false;
|
|
125
|
+
|
|
126
|
+
const month = parseInt(match[1]!, 10);
|
|
127
|
+
const year = parseInt(match[2]!, 10) + 2000;
|
|
128
|
+
|
|
129
|
+
if (month < 1 || month > 12) return false;
|
|
130
|
+
|
|
131
|
+
const now = new Date();
|
|
132
|
+
const expDate = new Date(year, month, 0); // Last day of expiration month
|
|
133
|
+
|
|
134
|
+
return expDate > now;
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
function ensureConfigDir(): void {
|
|
138
|
+
if (!existsSync(CONFIG_DIR)) {
|
|
139
|
+
mkdirSync(CONFIG_DIR, { recursive: true });
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
/**
|
|
144
|
+
* Decrypt sensitive fields in a card
|
|
145
|
+
*/
|
|
146
|
+
async function decryptCard(card: PaymentCard): Promise<PaymentCard> {
|
|
147
|
+
const decrypted = { ...card };
|
|
148
|
+
|
|
149
|
+
for (const field of SENSITIVE_FIELDS) {
|
|
150
|
+
const value = (decrypted as any)[field];
|
|
151
|
+
if (value && typeof value === "string" && isEncrypted(value)) {
|
|
152
|
+
try {
|
|
153
|
+
(decrypted as any)[field] = await decrypt(value);
|
|
154
|
+
} catch (err) {
|
|
155
|
+
console.error(`[cards] Failed to decrypt ${field} for card ${card.nickname}`);
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
return decrypted;
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
/**
|
|
164
|
+
* Encrypt sensitive fields in a card
|
|
165
|
+
*/
|
|
166
|
+
async function encryptCard(card: PaymentCard): Promise<PaymentCard> {
|
|
167
|
+
const encrypted = { ...card };
|
|
168
|
+
|
|
169
|
+
for (const field of SENSITIVE_FIELDS) {
|
|
170
|
+
const value = (encrypted as any)[field];
|
|
171
|
+
if (value && typeof value === "string" && !isEncrypted(value)) {
|
|
172
|
+
(encrypted as any)[field] = await encrypt(value);
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
return encrypted;
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
// ============================================================================
|
|
180
|
+
// Card CRUD
|
|
181
|
+
// ============================================================================
|
|
182
|
+
|
|
183
|
+
export async function getCards(): Promise<PaymentCard[]> {
|
|
184
|
+
ensureConfigDir();
|
|
185
|
+
|
|
186
|
+
if (!existsSync(CARDS_FILE)) {
|
|
187
|
+
return [];
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
try {
|
|
191
|
+
const data = readFileSync(CARDS_FILE, "utf-8");
|
|
192
|
+
const cards: PaymentCard[] = JSON.parse(data);
|
|
193
|
+
return Promise.all(cards.map(decryptCard));
|
|
194
|
+
} catch {
|
|
195
|
+
return [];
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
export async function getCard(id: string): Promise<PaymentCard | null> {
|
|
200
|
+
const cards = await getCards();
|
|
201
|
+
return cards.find((c) => c.id === id) ?? null;
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
/**
|
|
205
|
+
* Get cards without sensitive data (for listing)
|
|
206
|
+
*/
|
|
207
|
+
export async function getCardsSafe(): Promise<Omit<PaymentCard, "cardNumber" | "expirationDate" | "cvv">[]> {
|
|
208
|
+
const cards = await getCards();
|
|
209
|
+
return cards.map(({ cardNumber, expirationDate, cvv, ...safe }) => safe);
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
/**
|
|
213
|
+
* Get decrypted CVV for a card (requires PIN unlock)
|
|
214
|
+
*/
|
|
215
|
+
export async function getCardCvv(cardId: string): Promise<string | null> {
|
|
216
|
+
if (!isUnlocked()) {
|
|
217
|
+
return null; // Must be unlocked
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
// Read raw card to get encrypted CVV
|
|
221
|
+
if (!existsSync(CARDS_FILE)) {
|
|
222
|
+
return null;
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
try {
|
|
226
|
+
const rawCards: PaymentCard[] = JSON.parse(readFileSync(CARDS_FILE, "utf-8"));
|
|
227
|
+
const card = rawCards.find((c) => c.id === cardId);
|
|
228
|
+
|
|
229
|
+
if (!card?.cvv) {
|
|
230
|
+
return null;
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
return decryptCvv(card.cvv);
|
|
234
|
+
} catch {
|
|
235
|
+
return null;
|
|
236
|
+
}
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
export async function addCard(card: PaymentCard): Promise<void> {
|
|
240
|
+
ensureConfigDir();
|
|
241
|
+
|
|
242
|
+
// Validate card
|
|
243
|
+
if (!validateCardNumber(card.cardNumber)) {
|
|
244
|
+
throw new Error("Invalid card number");
|
|
245
|
+
}
|
|
246
|
+
if (!validateExpiration(card.expirationDate)) {
|
|
247
|
+
throw new Error("Card is expired or invalid expiration date");
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
// Set derived fields
|
|
251
|
+
card.cardType = detectCardType(card.cardNumber);
|
|
252
|
+
card.lastFourDigits = card.cardNumber.replace(/\D/g, "").slice(-4);
|
|
253
|
+
card.addedAt = new Date().toISOString();
|
|
254
|
+
|
|
255
|
+
// Load existing (raw/encrypted)
|
|
256
|
+
let rawCards: PaymentCard[] = [];
|
|
257
|
+
if (existsSync(CARDS_FILE)) {
|
|
258
|
+
try {
|
|
259
|
+
rawCards = JSON.parse(readFileSync(CARDS_FILE, "utf-8"));
|
|
260
|
+
} catch {
|
|
261
|
+
rawCards = [];
|
|
262
|
+
}
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
// Encrypt and save
|
|
266
|
+
const encryptedCard = await encryptCard(card);
|
|
267
|
+
|
|
268
|
+
const existingIndex = rawCards.findIndex((c) => c.id === card.id);
|
|
269
|
+
if (existingIndex >= 0) {
|
|
270
|
+
rawCards[existingIndex] = encryptedCard;
|
|
271
|
+
} else {
|
|
272
|
+
rawCards.push(encryptedCard);
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
writeFileSync(CARDS_FILE, JSON.stringify(rawCards, null, 2), { mode: 0o600 });
|
|
276
|
+
console.error(`[cards] Saved card ${card.nickname} (****${card.lastFourDigits}) - encrypted`);
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
export async function removeCard(id: string): Promise<boolean> {
|
|
280
|
+
if (!existsSync(CARDS_FILE)) {
|
|
281
|
+
return false;
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
let rawCards: PaymentCard[] = [];
|
|
285
|
+
try {
|
|
286
|
+
rawCards = JSON.parse(readFileSync(CARDS_FILE, "utf-8"));
|
|
287
|
+
} catch {
|
|
288
|
+
return false;
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
const filtered = rawCards.filter((c) => c.id !== id);
|
|
292
|
+
|
|
293
|
+
if (filtered.length === rawCards.length) {
|
|
294
|
+
return false;
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
writeFileSync(CARDS_FILE, JSON.stringify(filtered, null, 2), { mode: 0o600 });
|
|
298
|
+
console.error(`[cards] Removed card ${id}`);
|
|
299
|
+
return true;
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
export async function updateCardUsage(id: string): Promise<void> {
|
|
303
|
+
const cards = await getCards();
|
|
304
|
+
const card = cards.find((c) => c.id === id);
|
|
305
|
+
|
|
306
|
+
if (card) {
|
|
307
|
+
card.lastUsedAt = new Date().toISOString();
|
|
308
|
+
|
|
309
|
+
// Re-encrypt and save all
|
|
310
|
+
const encrypted = await Promise.all(cards.map(encryptCard));
|
|
311
|
+
writeFileSync(CARDS_FILE, JSON.stringify(encrypted, null, 2), { mode: 0o600 });
|
|
312
|
+
}
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
// ============================================================================
|
|
316
|
+
// Transaction Log
|
|
317
|
+
// ============================================================================
|
|
318
|
+
|
|
319
|
+
export async function getTransactions(limit = 50): Promise<Transaction[]> {
|
|
320
|
+
ensureConfigDir();
|
|
321
|
+
|
|
322
|
+
if (!existsSync(TRANSACTIONS_FILE)) {
|
|
323
|
+
return [];
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
try {
|
|
327
|
+
const data = readFileSync(TRANSACTIONS_FILE, "utf-8");
|
|
328
|
+
const transactions: Transaction[] = JSON.parse(data);
|
|
329
|
+
return transactions.slice(-limit);
|
|
330
|
+
} catch {
|
|
331
|
+
return [];
|
|
332
|
+
}
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
export async function logTransaction(tx: Transaction): Promise<void> {
|
|
336
|
+
ensureConfigDir();
|
|
337
|
+
|
|
338
|
+
let transactions: Transaction[] = [];
|
|
339
|
+
if (existsSync(TRANSACTIONS_FILE)) {
|
|
340
|
+
try {
|
|
341
|
+
transactions = JSON.parse(readFileSync(TRANSACTIONS_FILE, "utf-8"));
|
|
342
|
+
} catch {
|
|
343
|
+
transactions = [];
|
|
344
|
+
}
|
|
345
|
+
}
|
|
346
|
+
|
|
347
|
+
transactions.push(tx);
|
|
348
|
+
|
|
349
|
+
// Keep last 1000 transactions
|
|
350
|
+
if (transactions.length > 1000) {
|
|
351
|
+
transactions = transactions.slice(-1000);
|
|
352
|
+
}
|
|
353
|
+
|
|
354
|
+
writeFileSync(TRANSACTIONS_FILE, JSON.stringify(transactions, null, 2), { mode: 0o600 });
|
|
355
|
+
}
|
|
356
|
+
|
|
357
|
+
export async function updateTransaction(id: string, updates: Partial<Transaction>): Promise<void> {
|
|
358
|
+
if (!existsSync(TRANSACTIONS_FILE)) return;
|
|
359
|
+
|
|
360
|
+
let transactions: Transaction[] = [];
|
|
361
|
+
try {
|
|
362
|
+
transactions = JSON.parse(readFileSync(TRANSACTIONS_FILE, "utf-8"));
|
|
363
|
+
} catch {
|
|
364
|
+
return;
|
|
365
|
+
}
|
|
366
|
+
|
|
367
|
+
const idx = transactions.findIndex((t) => t.id === id);
|
|
368
|
+
if (idx >= 0) {
|
|
369
|
+
transactions[idx] = { ...transactions[idx]!, ...updates };
|
|
370
|
+
writeFileSync(TRANSACTIONS_FILE, JSON.stringify(transactions, null, 2), { mode: 0o600 });
|
|
371
|
+
}
|
|
372
|
+
}
|
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* CLI Runner - Execute shell commands with secure credential injection
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import { spawn } from "child_process";
|
|
6
|
+
|
|
7
|
+
export interface ExecResult {
|
|
8
|
+
stdout: string;
|
|
9
|
+
stderr: string;
|
|
10
|
+
exitCode: number;
|
|
11
|
+
success: boolean;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export interface ExecOptions {
|
|
15
|
+
cwd?: string;
|
|
16
|
+
env?: Record<string, string>;
|
|
17
|
+
timeout?: number;
|
|
18
|
+
stdin?: string;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* Execute a CLI command and return the result
|
|
23
|
+
*/
|
|
24
|
+
export async function execCommand(
|
|
25
|
+
command: string,
|
|
26
|
+
args: string[],
|
|
27
|
+
options: ExecOptions = {}
|
|
28
|
+
): Promise<ExecResult> {
|
|
29
|
+
return new Promise((resolve) => {
|
|
30
|
+
const env = {
|
|
31
|
+
...process.env,
|
|
32
|
+
...options.env,
|
|
33
|
+
};
|
|
34
|
+
|
|
35
|
+
const proc = spawn(command, args, {
|
|
36
|
+
cwd: options.cwd,
|
|
37
|
+
env,
|
|
38
|
+
timeout: options.timeout ?? 60000,
|
|
39
|
+
});
|
|
40
|
+
|
|
41
|
+
let stdout = "";
|
|
42
|
+
let stderr = "";
|
|
43
|
+
|
|
44
|
+
proc.stdout.on("data", (data) => {
|
|
45
|
+
stdout += data.toString();
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
proc.stderr.on("data", (data) => {
|
|
49
|
+
stderr += data.toString();
|
|
50
|
+
});
|
|
51
|
+
|
|
52
|
+
if (options.stdin) {
|
|
53
|
+
proc.stdin.write(options.stdin);
|
|
54
|
+
proc.stdin.end();
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
proc.on("close", (code) => {
|
|
58
|
+
resolve({
|
|
59
|
+
stdout: stdout.trim(),
|
|
60
|
+
stderr: stderr.trim(),
|
|
61
|
+
exitCode: code ?? 0,
|
|
62
|
+
success: code === 0,
|
|
63
|
+
});
|
|
64
|
+
});
|
|
65
|
+
|
|
66
|
+
proc.on("error", (err) => {
|
|
67
|
+
resolve({
|
|
68
|
+
stdout: "",
|
|
69
|
+
stderr: err.message,
|
|
70
|
+
exitCode: 1,
|
|
71
|
+
success: false,
|
|
72
|
+
});
|
|
73
|
+
});
|
|
74
|
+
});
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
/**
|
|
78
|
+
* Execute a git command
|
|
79
|
+
*/
|
|
80
|
+
export async function execGit(
|
|
81
|
+
args: string[],
|
|
82
|
+
options: ExecOptions = {}
|
|
83
|
+
): Promise<ExecResult> {
|
|
84
|
+
return execCommand("git", args, options);
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
/**
|
|
88
|
+
* Execute a heroku command with API key
|
|
89
|
+
*/
|
|
90
|
+
export async function execHeroku(
|
|
91
|
+
args: string[],
|
|
92
|
+
apiKey: string,
|
|
93
|
+
options: ExecOptions = {}
|
|
94
|
+
): Promise<ExecResult> {
|
|
95
|
+
return execCommand("heroku", args, {
|
|
96
|
+
...options,
|
|
97
|
+
env: {
|
|
98
|
+
...options.env,
|
|
99
|
+
HEROKU_API_KEY: apiKey,
|
|
100
|
+
},
|
|
101
|
+
});
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
/**
|
|
105
|
+
* Parse JSON output safely
|
|
106
|
+
*/
|
|
107
|
+
export function parseJsonOutput<T>(output: string): T | null {
|
|
108
|
+
try {
|
|
109
|
+
return JSON.parse(output);
|
|
110
|
+
} catch {
|
|
111
|
+
return null;
|
|
112
|
+
}
|
|
113
|
+
}
|