botfork 0.1.0
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/index.js +147 -0
- package/package.json +27 -0
package/index.js
ADDED
|
@@ -0,0 +1,147 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* botfork — terminal client for an Anexus store backend.
|
|
4
|
+
* Pure display + fetch: only chalk/ora/figlet. All network calls go to
|
|
5
|
+
* ANEXUS_SERVER_URL (default http://localhost:4000). No file writes/exec.
|
|
6
|
+
* Browse → GET /api/store/products
|
|
7
|
+
* Buy → POST /api/checkout (guest, no login)
|
|
8
|
+
* Support → GET /api/orders/mine | GET /api/orders/:id?key= (IP + key)
|
|
9
|
+
*/
|
|
10
|
+
import chalk from 'chalk';
|
|
11
|
+
import ora from 'ora';
|
|
12
|
+
import figlet from 'figlet';
|
|
13
|
+
import readline from 'node:readline';
|
|
14
|
+
|
|
15
|
+
const API = process.env.ANEXUS_SERVER_URL || 'http://localhost:4000';
|
|
16
|
+
|
|
17
|
+
// Lines are buffered from startup. With piped input a fast EOF can never race
|
|
18
|
+
// a prompt; interactively rl reads as usual. Empty string means input closed.
|
|
19
|
+
const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
|
|
20
|
+
const inputLines = [];
|
|
21
|
+
let ioClosed = false;
|
|
22
|
+
rl.on('line', l => inputLines.push(l));
|
|
23
|
+
rl.on('close', () => { ioClosed = true; });
|
|
24
|
+
const ask = q => new Promise(resolve => {
|
|
25
|
+
rl.output.write(q);
|
|
26
|
+
const done = v => { rl.off('close', onClosed); resolve(v); };
|
|
27
|
+
const onClosed = () => done('');
|
|
28
|
+
rl.on('close', onClosed);
|
|
29
|
+
if (inputLines.length) return done(inputLines.shift());
|
|
30
|
+
if (ioClosed) return done('');
|
|
31
|
+
rl.once('line', l => done(l));
|
|
32
|
+
});
|
|
33
|
+
|
|
34
|
+
// Every visible delay is a real network call under an ora spinner.
|
|
35
|
+
async function api(path, body) {
|
|
36
|
+
const spinner = ora(` calling ${path}…`).start();
|
|
37
|
+
try {
|
|
38
|
+
const res = await fetch(API + path, {
|
|
39
|
+
method: body ? 'POST' : 'GET',
|
|
40
|
+
headers: { 'content-type': 'application/json' },
|
|
41
|
+
body: body ? JSON.stringify(body) : undefined,
|
|
42
|
+
});
|
|
43
|
+
const data = await res.json().catch(() => ({}));
|
|
44
|
+
if (!res.ok) throw new Error(data.error || `HTTP ${res.status}`);
|
|
45
|
+
return data;
|
|
46
|
+
} catch (err) {
|
|
47
|
+
throw new Error(`cannot reach ${API} — is the server running? (${err.message})`);
|
|
48
|
+
} finally {
|
|
49
|
+
spinner.stop();
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
const statusColor = s => (s === 'paid' ? chalk.green(s) : chalk.yellow(s));
|
|
54
|
+
|
|
55
|
+
async function browse() {
|
|
56
|
+
const { items } = await api('/api/store/products');
|
|
57
|
+
if (!items || items.length === 0) return console.log(chalk.yellow('\n The store is empty.'));
|
|
58
|
+
console.log(chalk.bold('\n Storefront'));
|
|
59
|
+
for (const p of items) {
|
|
60
|
+
const price = p.discountPercent > 0
|
|
61
|
+
? `${chalk.red(p.price)} → ${chalk.green(p.effectivePrice)} (-${p.discountPercent}%)`
|
|
62
|
+
: chalk.green(p.price);
|
|
63
|
+
console.log(` ${chalk.cyan(p.id)} ${p.name} ${price} [${chalk.dim(p.categoryName)}]`);
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
async function viewBuy() {
|
|
68
|
+
const id = (await ask('\n product id (enter to cancel): ')).trim();
|
|
69
|
+
if (!id) return;
|
|
70
|
+
const p = await api(`/api/store/products/${id}`);
|
|
71
|
+
console.log(`\n ${chalk.bold(p.name)} — ${chalk.green(p.effectivePrice)}`);
|
|
72
|
+
if (p.description) console.log(` ${p.description}`);
|
|
73
|
+
console.log(` type: ${p.type}${p.hasFile ? `, size: ${(p.size / 1024).toFixed(1)} KB` : ''}`);
|
|
74
|
+
if ((await ask(' buy it? (y/n): ')).toLowerCase() !== 'y') return;
|
|
75
|
+
|
|
76
|
+
const order = await api('/api/checkout', { items: [{ productId: id }] });
|
|
77
|
+
console.log(`\n ${chalk.bold('Order')} ${order.orderId} — ${statusColor(order.status)} — ${order.total} ${order.currency}`);
|
|
78
|
+
if (order.paymentUrl) {
|
|
79
|
+
console.log(chalk.cyan(` ${'─'.repeat(60)}`));
|
|
80
|
+
console.log(chalk.bold(' PAY HERE:')); // Real OxaPay URL — copy to a browser.
|
|
81
|
+
console.log(chalk.cyan.bold(` ${order.paymentUrl}`));
|
|
82
|
+
console.log(chalk.cyan(` ${'─'.repeat(60)}`));
|
|
83
|
+
} else if (order.demo) {
|
|
84
|
+
console.log(chalk.cyan(' (demo mode: no OxaPay integration)'));
|
|
85
|
+
console.log(chalk.dim(` guest key: ${order.guestKey}`));
|
|
86
|
+
if (order.demoUrl) console.log(chalk.dim(` demo page: ${order.demoUrl}`));
|
|
87
|
+
}
|
|
88
|
+
console.log(chalk.dim(' keep the order id + guest key — Support can recall it offline.'));
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
async function support() {
|
|
92
|
+
const { orders } = await api('/api/orders/mine');
|
|
93
|
+
if (!orders || orders.length === 0) console.log(chalk.yellow('\n No orders found from this IP.'));
|
|
94
|
+
for (const o of orders) {
|
|
95
|
+
console.log(`\n ${chalk.cyan(o.orderId)} ${statusColor(o.status)} ${o.total} (${chalk.dim(o.createdAt)})`);
|
|
96
|
+
for (const l of o.items) console.log(` • ${l.name} × ${l.quantity}`);
|
|
97
|
+
for (const d of o.downloads) console.log(chalk.dim(` ↳ download ${d.downloadUrl} (${d.windowMinutes} min)`));
|
|
98
|
+
}
|
|
99
|
+
// IP tooling only; if the buyer's IP changed they can still prove ownership.
|
|
100
|
+
const oid = (await ask('\n paste an order id to look up manually (enter to go back): ')).trim();
|
|
101
|
+
if (!oid) return;
|
|
102
|
+
const key = (await ask(' and its guest key: ')).trim();
|
|
103
|
+
try {
|
|
104
|
+
const o = await api(`/api/orders/${oid}?key=${encodeURIComponent(key)}`);
|
|
105
|
+
console.log('\n ' + chalk.cyan(o.orderId) + ' ' + statusColor(o.status) + ' ' + o.total);
|
|
106
|
+
} catch (err) {
|
|
107
|
+
console.log(chalk.red(`\n ${err.message}`));
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
async function main() {
|
|
112
|
+
console.clear();
|
|
113
|
+
try {
|
|
114
|
+
const logo = await new Promise((res, rej) =>
|
|
115
|
+
figlet.text('Anexus', { font: 'Standard' }, (e, t) => (e ? rej(e) : res(t))));
|
|
116
|
+
console.log(chalk.cyan.bold(logo));
|
|
117
|
+
} catch { /* font rendered via the optional figlet dep; ignore failure */ }
|
|
118
|
+
console.log(chalk.dim(` terminal client · server: ${API}`));
|
|
119
|
+
console.log(chalk.dim(` orders are tied to your IP; keep your guest key if it changes\n`));
|
|
120
|
+
|
|
121
|
+
for (;;) {
|
|
122
|
+
for (const [n, label] of [['1', 'Browse products'], ['2', 'View product / buy'], ['3', 'Support'], ['4', 'Exit']])
|
|
123
|
+
console.log(chalk.bold(n === '4' ? chalk.red(` ${n}) ${label}`) : ` ${n}) ${label}`));
|
|
124
|
+
const choice = (await ask('\n choice: ')).trim();
|
|
125
|
+
try {
|
|
126
|
+
if (choice === '1') await browse();
|
|
127
|
+
else if (choice === '2') await viewBuy();
|
|
128
|
+
else if (choice === '3') await support();
|
|
129
|
+
else if (choice === '4') break;
|
|
130
|
+
else if (ioClosed) break; // piped input exhausted — exit instead of looping
|
|
131
|
+
else console.log(chalk.yellow(' unknown option — pick 1–4.'));
|
|
132
|
+
} catch (err) {
|
|
133
|
+
console.log(chalk.red(`\n ${err.message}`));
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
goodbye();
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
function goodbye() {
|
|
140
|
+
rl.close();
|
|
141
|
+
console.log(chalk.magenta('\n 👋 Thanks for using anexus-cli. Goodbye!'));
|
|
142
|
+
process.exit(0);
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
rl.on('SIGINT', goodbye); // graceful Ctrl+C
|
|
146
|
+
|
|
147
|
+
main().catch(err => { console.error(chalk.red(`\n ${err.message}`)); goodbye(); });
|
package/package.json
ADDED
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "botfork",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "Terminal client for an Anexus store backend — browse, buy and track your orders with zero login.",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"bin": {
|
|
7
|
+
"botfork": "index.js"
|
|
8
|
+
},
|
|
9
|
+
"files": [
|
|
10
|
+
"index.js"
|
|
11
|
+
],
|
|
12
|
+
"engines": {
|
|
13
|
+
"node": ">=18.17.0"
|
|
14
|
+
},
|
|
15
|
+
"keywords": [
|
|
16
|
+
"anexus",
|
|
17
|
+
"store",
|
|
18
|
+
"cli",
|
|
19
|
+
"checkout"
|
|
20
|
+
],
|
|
21
|
+
"license": "MIT",
|
|
22
|
+
"dependencies": {
|
|
23
|
+
"chalk": "^5.4.1",
|
|
24
|
+
"figlet": "^1.8.0",
|
|
25
|
+
"ora": "^8.1.0"
|
|
26
|
+
}
|
|
27
|
+
}
|