terminalmarket 0.7.3 → 0.8.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/README.md +51 -64
- package/bin/tm.js +151 -60
- package/package.json +39 -7
- package/src/config.js +16 -0
- package/src/ui.js +186 -0
package/README.md
CHANGED
|
@@ -4,10 +4,20 @@ The official command-line interface for [TerminalMarket](https://terminalmarket.
|
|
|
4
4
|
|
|
5
5
|
## Installation
|
|
6
6
|
|
|
7
|
+
### npm (requires Node.js)
|
|
8
|
+
|
|
7
9
|
```bash
|
|
8
10
|
npm install -g terminalmarket
|
|
9
11
|
```
|
|
10
12
|
|
|
13
|
+
### Standalone binary (no Node.js needed)
|
|
14
|
+
|
|
15
|
+
```bash
|
|
16
|
+
curl -fsSL https://terminalmarket.app/install.sh | sh
|
|
17
|
+
```
|
|
18
|
+
|
|
19
|
+
This installs `tm` into `~/.local/bin`.
|
|
20
|
+
|
|
11
21
|
## Usage
|
|
12
22
|
|
|
13
23
|
```bash
|
|
@@ -19,22 +29,25 @@ tm <command> [options]
|
|
|
19
29
|
### Authentication
|
|
20
30
|
|
|
21
31
|
```bash
|
|
22
|
-
tm register <email>
|
|
23
|
-
tm login <email>
|
|
32
|
+
tm register <email> [password] # Create a new account
|
|
33
|
+
tm login <email> [password] # Login to your account
|
|
24
34
|
tm logout # Logout
|
|
25
35
|
tm whoami # Show current user info
|
|
26
36
|
tm me # Alias for whoami
|
|
37
|
+
tm auth github # Login with GitHub (opens browser)
|
|
38
|
+
tm github # Shortcut for GitHub auth
|
|
27
39
|
```
|
|
28
40
|
|
|
29
41
|
### Profile
|
|
30
42
|
|
|
31
43
|
```bash
|
|
32
44
|
tm profile # View your profile
|
|
33
|
-
tm profile
|
|
34
|
-
tm profile
|
|
35
|
-
tm profile
|
|
36
|
-
tm profile
|
|
37
|
-
tm profile
|
|
45
|
+
tm profile view # View your profile
|
|
46
|
+
tm profile set name "John Doe" # Update your name
|
|
47
|
+
tm profile set phone "+1234567890" # Update phone
|
|
48
|
+
tm profile set address "123 Main" # Update address
|
|
49
|
+
tm profile set city "Berlin" # Update city
|
|
50
|
+
tm profile set country "DE" # Update country
|
|
38
51
|
```
|
|
39
52
|
|
|
40
53
|
### Shopping
|
|
@@ -87,32 +100,29 @@ tm credits # Check credits (shortcut)
|
|
|
87
100
|
tm topup <amount> # Add credits (shortcut)
|
|
88
101
|
```
|
|
89
102
|
|
|
90
|
-
|
|
103
|
+
### Aliases & Rewards
|
|
104
|
+
|
|
91
105
|
```bash
|
|
92
|
-
tm
|
|
93
|
-
tm
|
|
94
|
-
tm
|
|
95
|
-
tm
|
|
106
|
+
tm alias list # List your aliases
|
|
107
|
+
tm alias add <name> <command> # Create alias
|
|
108
|
+
tm alias remove <name> # Remove alias
|
|
109
|
+
tm aliases # Shortcut for alias list
|
|
110
|
+
|
|
111
|
+
tm reward list # List reward rules
|
|
112
|
+
tm reward add <product> <pushes> # Auto-order after N pushes
|
|
113
|
+
tm reward remove <id> # Remove reward rule
|
|
114
|
+
tm rewards # Shortcut for reward list
|
|
96
115
|
```
|
|
97
116
|
|
|
98
|
-
### Service Types
|
|
99
|
-
|
|
100
|
-
Products have different service types:
|
|
101
|
-
- Global — SaaS, digital products, worldwide delivery
|
|
102
|
-
- National — Country-wide delivery/services
|
|
103
|
-
- Local — City-specific services (food delivery, coworking, etc.)
|
|
104
|
-
|
|
105
117
|
### Categories & Offers
|
|
106
118
|
|
|
107
119
|
```bash
|
|
108
120
|
tm categories # List all categories
|
|
109
121
|
tm category <slug> # List products in category
|
|
110
122
|
tm offers # List all offers
|
|
111
|
-
tm offers --product <id> # Filter by product
|
|
112
|
-
tm offers --seller <id> # Filter by seller
|
|
113
123
|
```
|
|
114
124
|
|
|
115
|
-
Available categories
|
|
125
|
+
Available categories:
|
|
116
126
|
- `coffee` — Specialty coffee for developers
|
|
117
127
|
- `lunch` — Meal subscriptions & delivery
|
|
118
128
|
- `snacks` — Healthy snacks & energy packs
|
|
@@ -134,6 +144,7 @@ tm config set api <url> # Set API endpoint
|
|
|
134
144
|
```bash
|
|
135
145
|
tm about # About TerminalMarket
|
|
136
146
|
tm help # Show help
|
|
147
|
+
tm help <command> # Help for specific command
|
|
137
148
|
tm --version # Show version
|
|
138
149
|
```
|
|
139
150
|
|
|
@@ -152,49 +163,11 @@ tm review 1 5 "Great coffee, fast delivery!"
|
|
|
152
163
|
|
|
153
164
|
# View your order history
|
|
154
165
|
tm orders
|
|
155
|
-
```
|
|
156
166
|
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
| Free | $0/mo | 5 | 5% | Basic analytics |
|
|
162
|
-
| Basic | $29/mo | 50 | 4% | Priority support |
|
|
163
|
-
| Premium | $99/mo | 1000 | 2.5% | Stripe Connect, Terminal Checkout |
|
|
164
|
-
|
|
165
|
-
## API Endpoints Used
|
|
166
|
-
|
|
167
|
-
### Public
|
|
168
|
-
- `GET /api/products` — List products
|
|
169
|
-
- `GET /api/products/:id` — Get product details
|
|
170
|
-
- `GET /api/products/slug/:slug` — Get product by slug
|
|
171
|
-
- `GET /api/products/category/:category` — Products by category
|
|
172
|
-
- `GET /api/products/search` — Search products
|
|
173
|
-
- `GET /api/categories` — List categories
|
|
174
|
-
- `GET /api/sellers` — List sellers
|
|
175
|
-
- `GET /api/sellers/:slug` — Get seller details
|
|
176
|
-
- `GET /api/offers` — List offers
|
|
177
|
-
- `GET /api/stores/:id/reviews` — Get store reviews
|
|
178
|
-
- `GET /api/stores/:id/rating` — Get store rating
|
|
179
|
-
|
|
180
|
-
### Authenticated
|
|
181
|
-
- `POST /api/auth/register` — Create account
|
|
182
|
-
- `POST /api/auth/login` — Login
|
|
183
|
-
- `POST /api/auth/logout` — Logout
|
|
184
|
-
- `GET /api/auth/status` — Check auth status
|
|
185
|
-
- `PATCH /api/profile` — Update profile
|
|
186
|
-
- `GET /api/cart` — Get cart
|
|
187
|
-
- `POST /api/cart/add` — Add to cart
|
|
188
|
-
- `POST /api/cart/remove` — Remove from cart
|
|
189
|
-
- `POST /api/cart/clear` — Clear cart
|
|
190
|
-
- `GET /api/orders` — Get orders
|
|
191
|
-
- `POST /api/stores/:id/reviews` — Leave review
|
|
192
|
-
- `GET /api/credits` — Get AI credits balance
|
|
193
|
-
- `POST /api/credits/topup` — Create Stripe checkout for credits
|
|
194
|
-
- `POST /api/ai/run/:model` — Run AI model
|
|
195
|
-
- `GET /api/ai/history` — Get AI usage history
|
|
196
|
-
- `POST /api/clicks` — Track clicks
|
|
197
|
-
- `POST /api/intents` — Create purchase intent
|
|
167
|
+
# Use AI services
|
|
168
|
+
tm ai topup 10
|
|
169
|
+
tm ai run text-rewrite "Fix this text"
|
|
170
|
+
```
|
|
198
171
|
|
|
199
172
|
## Configuration
|
|
200
173
|
|
|
@@ -204,6 +177,20 @@ The CLI stores configuration in `~/.config/terminalmarket/config.json`:
|
|
|
204
177
|
- `sessionCookie`: Session cookie for authentication
|
|
205
178
|
- `user`: Cached user info
|
|
206
179
|
|
|
180
|
+
## Building Binaries
|
|
181
|
+
|
|
182
|
+
See [INSTALL_BINARIES.md](./INSTALL_BINARIES.md) for instructions on building standalone binaries.
|
|
183
|
+
|
|
184
|
+
```bash
|
|
185
|
+
npm ci
|
|
186
|
+
npm run build:bin
|
|
187
|
+
```
|
|
188
|
+
|
|
189
|
+
This produces binaries in `dist/`:
|
|
190
|
+
- `tm-linux-x64`
|
|
191
|
+
- `tm-macos-x64`
|
|
192
|
+
- `tm-macos-arm64`
|
|
193
|
+
|
|
207
194
|
## License
|
|
208
195
|
|
|
209
196
|
MIT
|
package/bin/tm.js
CHANGED
|
@@ -4,15 +4,29 @@ import { Command } from "commander";
|
|
|
4
4
|
import chalk from "chalk";
|
|
5
5
|
import open from "open";
|
|
6
6
|
import readline from "readline";
|
|
7
|
+
import { readFileSync } from "fs";
|
|
8
|
+
import { fileURLToPath } from "url";
|
|
9
|
+
import { dirname, join } from "path";
|
|
7
10
|
|
|
8
11
|
import { apiGet, apiPost, apiDelete, apiPatch } from "../src/api.js";
|
|
9
|
-
import { getApiBase, setApiBase, getUser, setUser, clearUser, clearSession } from "../src/config.js";
|
|
12
|
+
import { getApiBase, setApiBase, getUser, setUser, clearUser, clearSession, isFirstRun, markFirstRunComplete, setLocation, getLocation } from "../src/config.js";
|
|
10
13
|
import {
|
|
11
14
|
printTable, pickProductFields, pickSellerFields, pickOfferFields, containsQuery, formatStars,
|
|
12
15
|
printHeader, printDivider, printSuccess, printError, printWarning, printInfo, printField, printEmpty,
|
|
13
16
|
printProductCard, printCart, printOrders, printStoreCard, printSellers, printReviews, printAIModels, printCredits
|
|
14
17
|
} from "../src/format.js";
|
|
18
|
+
import { showWelcome, showBox, showError, showSuccess, showStatusBar, showNextSteps, createSpinner, stopSpinner } from "../src/ui.js";
|
|
15
19
|
|
|
20
|
+
const __filename = fileURLToPath(import.meta.url);
|
|
21
|
+
const __dirname = dirname(__filename);
|
|
22
|
+
const pkg = JSON.parse(readFileSync(join(__dirname, "../package.json"), "utf-8"));
|
|
23
|
+
const VERSION = pkg.version;
|
|
24
|
+
|
|
25
|
+
if (isFirstRun() && process.argv.length <= 2) {
|
|
26
|
+
showWelcome(VERSION);
|
|
27
|
+
markFirstRunComplete();
|
|
28
|
+
process.exit(0);
|
|
29
|
+
}
|
|
16
30
|
// Helper for hidden password input
|
|
17
31
|
function askPassword(prompt = "Password: ") {
|
|
18
32
|
return new Promise((resolve) => {
|
|
@@ -63,7 +77,7 @@ const program = new Command();
|
|
|
63
77
|
program
|
|
64
78
|
.name("tm")
|
|
65
79
|
.description("TerminalMarket CLI — marketplace for developers")
|
|
66
|
-
.version(
|
|
80
|
+
.version(VERSION);
|
|
67
81
|
|
|
68
82
|
// -----------------
|
|
69
83
|
// config
|
|
@@ -903,61 +917,6 @@ program
|
|
|
903
917
|
}
|
|
904
918
|
});
|
|
905
919
|
|
|
906
|
-
// -----------------
|
|
907
|
-
// where command (location search)
|
|
908
|
-
// -----------------
|
|
909
|
-
program
|
|
910
|
-
.command("where <city>")
|
|
911
|
-
.description("Find products and sellers in a city")
|
|
912
|
-
.option("-c, --country <country>", "Filter by country")
|
|
913
|
-
.action(async (city, opts) => {
|
|
914
|
-
try {
|
|
915
|
-
console.log(chalk.bold(`Services in ${city}`));
|
|
916
|
-
console.log("");
|
|
917
|
-
|
|
918
|
-
// Search products by city
|
|
919
|
-
const params = new URLSearchParams();
|
|
920
|
-
params.set("city", city);
|
|
921
|
-
if (opts.country) params.set("country", opts.country);
|
|
922
|
-
|
|
923
|
-
const products = await apiGet(`/products?${params.toString()}`);
|
|
924
|
-
const localProducts = (products || []).filter(p =>
|
|
925
|
-
p.serviceType === "local" &&
|
|
926
|
-
p.serviceCity?.toLowerCase() === city.toLowerCase()
|
|
927
|
-
);
|
|
928
|
-
|
|
929
|
-
if (localProducts.length > 0) {
|
|
930
|
-
console.log(chalk.cyan("Products:"));
|
|
931
|
-
localProducts.forEach(p => {
|
|
932
|
-
console.log(` ${p.name} - $${p.price} (${p.category})`);
|
|
933
|
-
});
|
|
934
|
-
console.log("");
|
|
935
|
-
}
|
|
936
|
-
|
|
937
|
-
// Search sellers by city
|
|
938
|
-
const sellers = await apiGet("/sellers");
|
|
939
|
-
const localSellers = (sellers || []).filter(s =>
|
|
940
|
-
s.serviceType === "local" &&
|
|
941
|
-
s.baseCity?.toLowerCase() === city.toLowerCase()
|
|
942
|
-
);
|
|
943
|
-
|
|
944
|
-
if (localSellers.length > 0) {
|
|
945
|
-
console.log(chalk.cyan("Sellers:"));
|
|
946
|
-
localSellers.forEach(s => {
|
|
947
|
-
console.log(` ${s.name} (${s.slug})`);
|
|
948
|
-
});
|
|
949
|
-
}
|
|
950
|
-
|
|
951
|
-
if (localProducts.length === 0 && localSellers.length === 0) {
|
|
952
|
-
console.log(chalk.yellow(`No local services found in ${city}.`));
|
|
953
|
-
console.log(chalk.dim("Try: tm products --city <city>"));
|
|
954
|
-
}
|
|
955
|
-
} catch (e) {
|
|
956
|
-
console.error(chalk.red(e?.message || String(e)));
|
|
957
|
-
process.exitCode = 1;
|
|
958
|
-
}
|
|
959
|
-
});
|
|
960
|
-
|
|
961
920
|
// -----------------
|
|
962
921
|
// categories
|
|
963
922
|
// -----------------
|
|
@@ -995,6 +954,7 @@ program
|
|
|
995
954
|
.option("--city <city>", "Filter by city (for local services)")
|
|
996
955
|
.option("--country <country>", "Filter by country")
|
|
997
956
|
.action(async (opts) => {
|
|
957
|
+
const spinner = createSpinner("Fetching products...");
|
|
998
958
|
try {
|
|
999
959
|
const limit = Math.max(1, Math.min(200, Number.parseInt(opts.limit, 10) || 20));
|
|
1000
960
|
|
|
@@ -1015,6 +975,8 @@ program
|
|
|
1015
975
|
}
|
|
1016
976
|
|
|
1017
977
|
const products = await apiGet(url);
|
|
978
|
+
stopSpinner(true, `Found ${products.length} products`);
|
|
979
|
+
|
|
1018
980
|
const rows = (products || []).slice(0, limit).map(pickProductFields);
|
|
1019
981
|
printTable(rows, [
|
|
1020
982
|
{ key: "id", title: "id" },
|
|
@@ -1024,7 +986,13 @@ program
|
|
|
1024
986
|
{ key: "category", title: "category" },
|
|
1025
987
|
{ key: "serviceType", title: "type" },
|
|
1026
988
|
]);
|
|
989
|
+
|
|
990
|
+
showNextSteps([
|
|
991
|
+
{ cmd: "tm view <id>", desc: "view product details" },
|
|
992
|
+
{ cmd: "tm add <id>", desc: "add to cart" }
|
|
993
|
+
]);
|
|
1027
994
|
} catch (e) {
|
|
995
|
+
stopSpinner(false, "Failed to load products");
|
|
1028
996
|
console.error(chalk.red(e?.message || String(e)));
|
|
1029
997
|
process.exitCode = 1;
|
|
1030
998
|
}
|
|
@@ -1438,8 +1406,14 @@ program
|
|
|
1438
1406
|
console.log();
|
|
1439
1407
|
console.log(chalk.dim(' ─────────────────────────────────────────────'));
|
|
1440
1408
|
console.log();
|
|
1409
|
+
console.log(chalk.white(' Install:'));
|
|
1410
|
+
console.log();
|
|
1411
|
+
console.log(` ${chalk.dim('npm:')} ${chalk.green('npm i -g terminalmarket')}`);
|
|
1412
|
+
console.log(` ${chalk.dim('curl:')} ${chalk.cyan('curl -fsSL https://terminalmarket.app/install.sh | sh')}`);
|
|
1413
|
+
console.log();
|
|
1414
|
+
console.log(chalk.dim(' ─────────────────────────────────────────────'));
|
|
1415
|
+
console.log();
|
|
1441
1416
|
console.log(` ${chalk.dim('Website:')} ${chalk.cyan('https://terminalmarket.app')}`);
|
|
1442
|
-
console.log(` ${chalk.dim('Install:')} ${chalk.green('npm i -g terminalmarket')}`);
|
|
1443
1417
|
console.log(` ${chalk.dim('Version:')} ${chalk.white('0.7.2')}`);
|
|
1444
1418
|
console.log();
|
|
1445
1419
|
});
|
|
@@ -1456,7 +1430,7 @@ const commandGroups = {
|
|
|
1456
1430
|
'Stores': ['sellers', 'seller', 'store', 'reviews', 'review', 'where'],
|
|
1457
1431
|
'AI Services': ['ai', 'credits', 'topup'],
|
|
1458
1432
|
'Personalization': ['alias', 'aliases', 'reward', 'rewards'],
|
|
1459
|
-
'System': ['config', 'help', 'about', 'offers']
|
|
1433
|
+
'System': ['start', 'config', 'help', 'about', 'offers']
|
|
1460
1434
|
};
|
|
1461
1435
|
|
|
1462
1436
|
// Custom help formatter
|
|
@@ -1527,7 +1501,7 @@ function showHelp(commandName = null) {
|
|
|
1527
1501
|
|
|
1528
1502
|
console.log();
|
|
1529
1503
|
console.log(chalk.green.bold(' ╔' + line + '╗'));
|
|
1530
|
-
console.log(chalk.green.bold(' ║') + chalk.white.bold(pad(' TerminalMarket CLI', W - 8)) + chalk.dim(
|
|
1504
|
+
console.log(chalk.green.bold(' ║') + chalk.white.bold(pad(' TerminalMarket CLI', W - 8)) + chalk.dim(` v${VERSION} `) + chalk.green.bold('║'));
|
|
1531
1505
|
console.log(chalk.green.bold(' ║') + chalk.dim(pad(' Marketplace for developers', W)) + chalk.green.bold('║'));
|
|
1532
1506
|
console.log(chalk.green.bold(' ╚' + line + '╝'));
|
|
1533
1507
|
console.log();
|
|
@@ -1598,6 +1572,122 @@ function showHelp(commandName = null) {
|
|
|
1598
1572
|
console.log();
|
|
1599
1573
|
}
|
|
1600
1574
|
|
|
1575
|
+
program
|
|
1576
|
+
.command("where [city]")
|
|
1577
|
+
.description("Set or view your location (for local services)")
|
|
1578
|
+
.action(async (city) => {
|
|
1579
|
+
if (city) {
|
|
1580
|
+
setLocation(city);
|
|
1581
|
+
showSuccess(`Location set to ${city}`);
|
|
1582
|
+
showNextSteps([
|
|
1583
|
+
{ cmd: "tm products", desc: "browse products in " + city },
|
|
1584
|
+
{ cmd: "tm search lunch", desc: "find lunch options" }
|
|
1585
|
+
]);
|
|
1586
|
+
} else {
|
|
1587
|
+
const location = getLocation();
|
|
1588
|
+
if (location?.city) {
|
|
1589
|
+
console.log();
|
|
1590
|
+
console.log(chalk.green(" 📍 Location: ") + chalk.white.bold(location.city));
|
|
1591
|
+
console.log();
|
|
1592
|
+
console.log(chalk.dim(" 💡 tm where <city> — change location"));
|
|
1593
|
+
console.log();
|
|
1594
|
+
} else {
|
|
1595
|
+
console.log();
|
|
1596
|
+
console.log(chalk.dim(" 📍 Location not set"));
|
|
1597
|
+
console.log();
|
|
1598
|
+
console.log(chalk.dim(" 💡 Set location for local services:"));
|
|
1599
|
+
console.log(chalk.cyan(" tm where berlin"));
|
|
1600
|
+
console.log(chalk.cyan(" tm where prague"));
|
|
1601
|
+
console.log();
|
|
1602
|
+
}
|
|
1603
|
+
}
|
|
1604
|
+
});
|
|
1605
|
+
|
|
1606
|
+
program
|
|
1607
|
+
.command("start")
|
|
1608
|
+
.alias("tour")
|
|
1609
|
+
.description("Interactive onboarding tour")
|
|
1610
|
+
.action(async () => {
|
|
1611
|
+
const inquirer = await import("inquirer").then(m => m.default);
|
|
1612
|
+
|
|
1613
|
+
console.log();
|
|
1614
|
+
console.log(chalk.green.bold(" Welcome to TerminalMarket! 🚀"));
|
|
1615
|
+
console.log(chalk.dim(" Let's get you started with a quick tour."));
|
|
1616
|
+
console.log();
|
|
1617
|
+
|
|
1618
|
+
const { city } = await inquirer.prompt([
|
|
1619
|
+
{
|
|
1620
|
+
type: "input",
|
|
1621
|
+
name: "city",
|
|
1622
|
+
message: "What city are you in?",
|
|
1623
|
+
default: "Berlin"
|
|
1624
|
+
}
|
|
1625
|
+
]);
|
|
1626
|
+
|
|
1627
|
+
setLocation(city);
|
|
1628
|
+
console.log(chalk.green(` ✓ Location set to ${city}`));
|
|
1629
|
+
console.log();
|
|
1630
|
+
|
|
1631
|
+
const { action } = await inquirer.prompt([
|
|
1632
|
+
{
|
|
1633
|
+
type: "list",
|
|
1634
|
+
name: "action",
|
|
1635
|
+
message: "What would you like to explore?",
|
|
1636
|
+
choices: [
|
|
1637
|
+
{ name: "🍽 Food & Drinks", value: "food" },
|
|
1638
|
+
{ name: "🤖 AI Services", value: "ai" },
|
|
1639
|
+
{ name: "🏢 Coworking Spaces", value: "coworking" },
|
|
1640
|
+
{ name: "🛠 Developer Tools", value: "digital" },
|
|
1641
|
+
{ name: "📦 Browse all products", value: "all" }
|
|
1642
|
+
]
|
|
1643
|
+
}
|
|
1644
|
+
]);
|
|
1645
|
+
|
|
1646
|
+
console.log();
|
|
1647
|
+
|
|
1648
|
+
const spinner = createSpinner("Fetching products...");
|
|
1649
|
+
|
|
1650
|
+
try {
|
|
1651
|
+
let products;
|
|
1652
|
+
if (action === "all") {
|
|
1653
|
+
products = await apiGet("/products");
|
|
1654
|
+
} else if (action === "ai") {
|
|
1655
|
+
stopSpinner(true, "AI models");
|
|
1656
|
+
const models = await apiGet("/ai/models");
|
|
1657
|
+
printAIModels(models);
|
|
1658
|
+
showNextSteps([
|
|
1659
|
+
{ cmd: "tm ai topup 10", desc: "add $10 credits" },
|
|
1660
|
+
{ cmd: "tm ai run <model> <prompt>", desc: "run an AI model" }
|
|
1661
|
+
]);
|
|
1662
|
+
return;
|
|
1663
|
+
} else {
|
|
1664
|
+
products = await apiGet(`/products/category/${action}`);
|
|
1665
|
+
}
|
|
1666
|
+
|
|
1667
|
+
stopSpinner(true, `Found ${products.length} products`);
|
|
1668
|
+
|
|
1669
|
+
if (products.length > 0) {
|
|
1670
|
+
const rows = products.slice(0, 5).map(pickProductFields);
|
|
1671
|
+
printTable(rows, [
|
|
1672
|
+
{ key: "id", title: "ID" },
|
|
1673
|
+
{ key: "name", title: "Name" },
|
|
1674
|
+
{ key: "price", title: "Price" },
|
|
1675
|
+
{ key: "category", title: "Category" }
|
|
1676
|
+
]);
|
|
1677
|
+
}
|
|
1678
|
+
|
|
1679
|
+
showNextSteps([
|
|
1680
|
+
{ cmd: "tm view <id>", desc: "view product details" },
|
|
1681
|
+
{ cmd: "tm add <id>", desc: "add to cart" },
|
|
1682
|
+
{ cmd: "tm search <query>", desc: "search products" }
|
|
1683
|
+
]);
|
|
1684
|
+
|
|
1685
|
+
} catch (e) {
|
|
1686
|
+
stopSpinner(false, "Failed to load");
|
|
1687
|
+
printError(e?.message || String(e));
|
|
1688
|
+
}
|
|
1689
|
+
});
|
|
1690
|
+
|
|
1601
1691
|
program
|
|
1602
1692
|
.command("help [command]")
|
|
1603
1693
|
.description("Show help for a command")
|
|
@@ -1608,5 +1698,6 @@ program
|
|
|
1608
1698
|
program.parse(process.argv);
|
|
1609
1699
|
|
|
1610
1700
|
if (!process.argv.slice(2).length) {
|
|
1701
|
+
showStatusBar();
|
|
1611
1702
|
showHelp();
|
|
1612
1703
|
}
|
package/package.json
CHANGED
|
@@ -1,34 +1,66 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "terminalmarket",
|
|
3
|
-
"version": "0.
|
|
4
|
-
"description": "TerminalMarket CLI — marketplace for developers (client for terminalmarket.app)",
|
|
3
|
+
"version": "0.8.1",
|
|
4
|
+
"description": "TerminalMarket CLI — a curated marketplace for developers & founders (client for terminalmarket.app)",
|
|
5
5
|
"bin": {
|
|
6
|
-
"tm": "
|
|
6
|
+
"tm": "bin/tm.js"
|
|
7
7
|
},
|
|
8
8
|
"type": "module",
|
|
9
9
|
"keywords": [
|
|
10
10
|
"cli",
|
|
11
|
-
"marketplace",
|
|
12
11
|
"terminal",
|
|
12
|
+
"marketplace",
|
|
13
13
|
"developers",
|
|
14
|
+
"founders",
|
|
14
15
|
"food",
|
|
15
16
|
"subscription",
|
|
16
17
|
"saas",
|
|
17
|
-
"coworking"
|
|
18
|
+
"coworking",
|
|
19
|
+
"tools",
|
|
20
|
+
"telegram",
|
|
21
|
+
"discord"
|
|
18
22
|
],
|
|
19
23
|
"author": "TerminalMarket",
|
|
20
24
|
"license": "MIT",
|
|
21
25
|
"repository": {
|
|
22
26
|
"type": "git",
|
|
23
|
-
"url": "https://github.com/terminalmarket/cli"
|
|
27
|
+
"url": "git+https://github.com/terminalmarket/cli.git"
|
|
28
|
+
},
|
|
29
|
+
"bugs": {
|
|
30
|
+
"url": "https://github.com/terminalmarket/cli/issues"
|
|
24
31
|
},
|
|
25
32
|
"homepage": "https://terminalmarket.app",
|
|
33
|
+
"files": [
|
|
34
|
+
"bin/",
|
|
35
|
+
"src/",
|
|
36
|
+
"dist/",
|
|
37
|
+
"README.md",
|
|
38
|
+
"LICENSE"
|
|
39
|
+
],
|
|
40
|
+
"scripts": {
|
|
41
|
+
"bundle:cli": "esbuild bin/tm.js --bundle --platform=node --format=cjs --outfile=dist/tm.cjs",
|
|
42
|
+
"build:bin": "npm run bundle:cli && pkg dist/tm.cjs --targets node18-linux-x64,node18-macos-x64,node18-macos-arm64,node18-win-x64 --output dist/tm",
|
|
43
|
+
"build:bin:linux": "npm run bundle:cli && pkg dist/tm.cjs --targets node18-linux-x64 --output dist/tm",
|
|
44
|
+
"build:bin:mac": "npm run bundle:cli && pkg dist/tm.cjs --targets node18-macos-x64,node18-macos-arm64 --output dist/tm",
|
|
45
|
+
"build:bin:win": "npm run bundle:cli && pkg dist/tm.cjs --targets node18-win-x64 --output dist/tm",
|
|
46
|
+
"clean": "rm -rf dist",
|
|
47
|
+
"prepack": "npm run clean",
|
|
48
|
+
"test:smoke": "node bin/tm.js --help"
|
|
49
|
+
},
|
|
50
|
+
"devDependencies": {
|
|
51
|
+
"esbuild": "^0.25.0",
|
|
52
|
+
"pkg": "^5.8.1"
|
|
53
|
+
},
|
|
26
54
|
"dependencies": {
|
|
55
|
+
"boxen": "^7.1.1",
|
|
27
56
|
"chalk": "^5.3.0",
|
|
57
|
+
"cli-table3": "^0.6.5",
|
|
28
58
|
"commander": "^12.1.0",
|
|
29
59
|
"conf": "^12.0.0",
|
|
60
|
+
"inquirer": "^9.2.15",
|
|
30
61
|
"node-fetch": "^3.3.2",
|
|
31
|
-
"open": "^9.1.0"
|
|
62
|
+
"open": "^9.1.0",
|
|
63
|
+
"ora": "^8.0.1"
|
|
32
64
|
},
|
|
33
65
|
"engines": {
|
|
34
66
|
"node": ">=18.0.0"
|
package/src/config.js
CHANGED
|
@@ -36,3 +36,19 @@ export function setUser(user) {
|
|
|
36
36
|
export function clearUser() {
|
|
37
37
|
conf.delete("user");
|
|
38
38
|
}
|
|
39
|
+
|
|
40
|
+
export function isFirstRun() {
|
|
41
|
+
return !conf.get("firstRunComplete", false);
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
export function markFirstRunComplete() {
|
|
45
|
+
conf.set("firstRunComplete", true);
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
export function getLocation() {
|
|
49
|
+
return conf.get("location", null);
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
export function setLocation(city, country = null) {
|
|
53
|
+
conf.set("location", { city, country });
|
|
54
|
+
}
|
package/src/ui.js
ADDED
|
@@ -0,0 +1,186 @@
|
|
|
1
|
+
import chalk from "chalk";
|
|
2
|
+
import boxen from "boxen";
|
|
3
|
+
import ora from "ora";
|
|
4
|
+
import Table from "cli-table3";
|
|
5
|
+
import { getUser, getLocation } from "./config.js";
|
|
6
|
+
|
|
7
|
+
let currentSpinner = null;
|
|
8
|
+
|
|
9
|
+
export function createSpinner(text) {
|
|
10
|
+
if (currentSpinner) {
|
|
11
|
+
currentSpinner.stop();
|
|
12
|
+
}
|
|
13
|
+
currentSpinner = ora({
|
|
14
|
+
text,
|
|
15
|
+
color: "green",
|
|
16
|
+
spinner: "dots"
|
|
17
|
+
}).start();
|
|
18
|
+
return currentSpinner;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export function stopSpinner(success = true, text = null) {
|
|
22
|
+
if (currentSpinner) {
|
|
23
|
+
if (success) {
|
|
24
|
+
currentSpinner.succeed(text);
|
|
25
|
+
} else {
|
|
26
|
+
currentSpinner.fail(text);
|
|
27
|
+
}
|
|
28
|
+
currentSpinner = null;
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
export function showWelcome(version) {
|
|
33
|
+
const content = `${chalk.green.bold("TerminalMarket CLI")} ${chalk.dim(`v${version}`)}
|
|
34
|
+
${chalk.dim("A curated marketplace for developers & founders")}
|
|
35
|
+
|
|
36
|
+
${chalk.white("Try one of these:")}
|
|
37
|
+
${chalk.cyan("tm products")} ${chalk.dim("— browse products")}
|
|
38
|
+
${chalk.cyan("tm search lunch")} ${chalk.dim("— search for lunch deals")}
|
|
39
|
+
${chalk.cyan("tm categories")} ${chalk.dim("— explore categories")}
|
|
40
|
+
${chalk.cyan("tm ai list")} ${chalk.dim("— browse AI models")}
|
|
41
|
+
${chalk.cyan("tm start")} ${chalk.dim("— interactive tour")}
|
|
42
|
+
|
|
43
|
+
${chalk.dim("Tip: this is a real marketplace — products open real checkout pages.")}`;
|
|
44
|
+
|
|
45
|
+
console.log();
|
|
46
|
+
console.log(boxen(content, {
|
|
47
|
+
padding: 1,
|
|
48
|
+
margin: 0,
|
|
49
|
+
borderStyle: "round",
|
|
50
|
+
borderColor: "green"
|
|
51
|
+
}));
|
|
52
|
+
console.log();
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
export function showBox(title, content, options = {}) {
|
|
56
|
+
const { borderColor = "green", padding = 1 } = options;
|
|
57
|
+
|
|
58
|
+
const text = title
|
|
59
|
+
? `${chalk.bold(title)}\n\n${content}`
|
|
60
|
+
: content;
|
|
61
|
+
|
|
62
|
+
console.log();
|
|
63
|
+
console.log(boxen(text, {
|
|
64
|
+
padding,
|
|
65
|
+
borderStyle: "round",
|
|
66
|
+
borderColor
|
|
67
|
+
}));
|
|
68
|
+
console.log();
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
export function showError(message, hint = null) {
|
|
72
|
+
let content = chalk.red.bold("Error: ") + chalk.white(message);
|
|
73
|
+
if (hint) {
|
|
74
|
+
content += "\n\n" + chalk.dim("💡 " + hint);
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
console.log();
|
|
78
|
+
console.log(boxen(content, {
|
|
79
|
+
padding: 1,
|
|
80
|
+
borderStyle: "round",
|
|
81
|
+
borderColor: "red"
|
|
82
|
+
}));
|
|
83
|
+
console.log();
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
export function showSuccess(message) {
|
|
87
|
+
console.log();
|
|
88
|
+
console.log(boxen(chalk.green("✓ ") + chalk.white(message), {
|
|
89
|
+
padding: 1,
|
|
90
|
+
borderStyle: "round",
|
|
91
|
+
borderColor: "green"
|
|
92
|
+
}));
|
|
93
|
+
console.log();
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
export function showStatusBar() {
|
|
97
|
+
const user = getUser();
|
|
98
|
+
const location = getLocation();
|
|
99
|
+
|
|
100
|
+
const parts = [];
|
|
101
|
+
|
|
102
|
+
if (location?.city) {
|
|
103
|
+
parts.push(chalk.cyan("📍 " + location.city));
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
if (user) {
|
|
107
|
+
parts.push(chalk.magenta("👤 " + (user.username || user.email?.split("@")[0] || "User")));
|
|
108
|
+
} else {
|
|
109
|
+
parts.push(chalk.dim("👤 Guest"));
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
if (parts.length > 0) {
|
|
113
|
+
console.log(chalk.dim(" " + parts.join(" │ ")));
|
|
114
|
+
console.log();
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
export function showNextSteps(steps) {
|
|
119
|
+
console.log();
|
|
120
|
+
console.log(chalk.dim(" Next:"));
|
|
121
|
+
steps.forEach(step => {
|
|
122
|
+
console.log(chalk.dim(" → ") + chalk.cyan(step.cmd) + chalk.dim(" — " + step.desc));
|
|
123
|
+
});
|
|
124
|
+
console.log();
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
export function createTable(headers, options = {}) {
|
|
128
|
+
const { compact = false } = options;
|
|
129
|
+
|
|
130
|
+
return new Table({
|
|
131
|
+
head: headers.map(h => chalk.cyan.bold(h)),
|
|
132
|
+
style: {
|
|
133
|
+
head: [],
|
|
134
|
+
border: ["dim"],
|
|
135
|
+
compact
|
|
136
|
+
},
|
|
137
|
+
chars: compact ? {
|
|
138
|
+
"top": "", "top-mid": "", "top-left": "", "top-right": "",
|
|
139
|
+
"bottom": "", "bottom-mid": "", "bottom-left": "", "bottom-right": "",
|
|
140
|
+
"left": " ", "left-mid": "", "mid": "", "mid-mid": "",
|
|
141
|
+
"right": "", "right-mid": "", "middle": " │ "
|
|
142
|
+
} : undefined
|
|
143
|
+
});
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
export function printTableData(data, columns) {
|
|
147
|
+
if (!data?.length) {
|
|
148
|
+
console.log(chalk.dim(" No results found."));
|
|
149
|
+
return;
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
const headers = columns.map(c => c.title);
|
|
153
|
+
const table = createTable(headers, { compact: true });
|
|
154
|
+
|
|
155
|
+
data.forEach(row => {
|
|
156
|
+
const cells = columns.map(col => {
|
|
157
|
+
let value = row[col.key] ?? "";
|
|
158
|
+
|
|
159
|
+
if (col.key === "price" || col.key === "total") {
|
|
160
|
+
value = chalk.green(value);
|
|
161
|
+
} else if (col.key === "name" || col.key === "title") {
|
|
162
|
+
value = chalk.white.bold(value);
|
|
163
|
+
} else if (col.key === "id") {
|
|
164
|
+
value = chalk.dim(value);
|
|
165
|
+
} else if (col.key === "status") {
|
|
166
|
+
const status = String(value).toLowerCase();
|
|
167
|
+
if (status === "delivered" || status === "active") {
|
|
168
|
+
value = chalk.green(value);
|
|
169
|
+
} else if (status === "pending") {
|
|
170
|
+
value = chalk.yellow(value);
|
|
171
|
+
} else if (status === "cancelled") {
|
|
172
|
+
value = chalk.red(value);
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
return value;
|
|
177
|
+
});
|
|
178
|
+
|
|
179
|
+
table.push(cells);
|
|
180
|
+
});
|
|
181
|
+
|
|
182
|
+
console.log();
|
|
183
|
+
console.log(table.toString());
|
|
184
|
+
console.log();
|
|
185
|
+
console.log(chalk.dim(` Showing ${data.length} result${data.length !== 1 ? "s" : ""}`));
|
|
186
|
+
}
|