terminalmarket 0.11.2 → 0.12.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/README.md +113 -58
- package/bin/tm.js +419 -0
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -1,6 +1,22 @@
|
|
|
1
1
|
# TerminalMarket CLI
|
|
2
2
|
|
|
3
|
-
The official command-line interface for [TerminalMarket](https://terminalmarket.app) — a marketplace
|
|
3
|
+
The official command-line interface for [TerminalMarket](https://terminalmarket.app) — a developer marketplace that lives in your terminal.
|
|
4
|
+
|
|
5
|
+
Search, buy, and manage orders without leaving the command line. Unix pipes, price alerts, reverse marketplace — all from `tm`.
|
|
6
|
+
|
|
7
|
+
## Demo
|
|
8
|
+
|
|
9
|
+
### Search & Buy
|
|
10
|
+
|
|
11
|
+

|
|
12
|
+
|
|
13
|
+
### Reverse Marketplace — stores compete for your order
|
|
14
|
+
|
|
15
|
+

|
|
16
|
+
|
|
17
|
+
### Watch & Price Alerts via Telegram
|
|
18
|
+
|
|
19
|
+

|
|
4
20
|
|
|
5
21
|
## Installation
|
|
6
22
|
|
|
@@ -18,10 +34,15 @@ curl -fsSL https://terminalmarket.app/install.sh | sh
|
|
|
18
34
|
|
|
19
35
|
This installs `tm` into `~/.local/bin`.
|
|
20
36
|
|
|
21
|
-
##
|
|
37
|
+
## Quick Start
|
|
22
38
|
|
|
23
39
|
```bash
|
|
24
|
-
tm
|
|
40
|
+
tm register you@email.com # Create account
|
|
41
|
+
tm search coffee # Browse products
|
|
42
|
+
tm search coffee | sort price # Unix pipes work!
|
|
43
|
+
tm add coffee-03 # Add to cart
|
|
44
|
+
tm cart # View cart
|
|
45
|
+
tm checkout # Buy
|
|
25
46
|
```
|
|
26
47
|
|
|
27
48
|
## Commands
|
|
@@ -35,37 +56,59 @@ tm logout # Logout
|
|
|
35
56
|
tm whoami # Show current user info
|
|
36
57
|
tm me # Alias for whoami
|
|
37
58
|
tm auth github # Login with GitHub (opens browser)
|
|
38
|
-
tm github # Shortcut for GitHub auth
|
|
39
|
-
```
|
|
40
|
-
|
|
41
|
-
### Profile
|
|
42
|
-
|
|
43
|
-
```bash
|
|
44
|
-
tm profile # View your 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
|
|
51
59
|
```
|
|
52
60
|
|
|
53
|
-
### Shopping
|
|
61
|
+
### Shopping & Pipes
|
|
54
62
|
|
|
55
63
|
```bash
|
|
56
64
|
tm products # List all products
|
|
57
65
|
tm products --category coffee # Filter by category
|
|
58
|
-
tm
|
|
59
|
-
tm search
|
|
66
|
+
tm search "coffee" # Search products
|
|
67
|
+
tm search coffee | sort price # Sort by price (pipes!)
|
|
68
|
+
tm search coffee | head 3 # First 3 results
|
|
69
|
+
tm search coffee | count # Count results
|
|
70
|
+
tm search nut | filter --max-price 10 | sort price # Chain pipes
|
|
60
71
|
tm view <product-id> # View product details
|
|
61
72
|
tm add <product-id> # Add to cart
|
|
62
|
-
tm cart
|
|
73
|
+
tm cart # View cart
|
|
63
74
|
tm cart add <product-id> # Add to cart
|
|
64
75
|
tm cart remove <product-id> # Remove from cart
|
|
65
76
|
tm cart clear # Clear cart
|
|
66
77
|
tm checkout # Proceed to checkout
|
|
67
78
|
```
|
|
68
79
|
|
|
80
|
+
### Reverse Marketplace
|
|
81
|
+
|
|
82
|
+
Post what you need — sellers compete with offers.
|
|
83
|
+
|
|
84
|
+
```bash
|
|
85
|
+
tm request create Need a laptop --budget 700 --category hardware
|
|
86
|
+
tm request list # Your requests
|
|
87
|
+
tm request view <id> # View proposals from sellers
|
|
88
|
+
tm request accept <requestId> <proposalId> # Accept best offer
|
|
89
|
+
```
|
|
90
|
+
|
|
91
|
+
### Watch & Price Alerts
|
|
92
|
+
|
|
93
|
+
Set up persistent monitoring. Get notified via Telegram or in-app.
|
|
94
|
+
|
|
95
|
+
```bash
|
|
96
|
+
tm watch create search coffee --sort price --name "Coffee deals" --notify telegram
|
|
97
|
+
tm watch list # List watch rules
|
|
98
|
+
tm watch logs <id> # View match history
|
|
99
|
+
tm watch pause <id> # Pause a rule
|
|
100
|
+
tm watch resume <id> # Resume a rule
|
|
101
|
+
tm watch delete <id> # Delete a rule
|
|
102
|
+
```
|
|
103
|
+
|
|
104
|
+
### Telegram Integration
|
|
105
|
+
|
|
106
|
+
```bash
|
|
107
|
+
tm telegram link <code> # Link Telegram for notifications
|
|
108
|
+
tm telegram status # Check connection
|
|
109
|
+
tm telegram unlink # Disconnect
|
|
110
|
+
```
|
|
111
|
+
|
|
69
112
|
### Orders
|
|
70
113
|
|
|
71
114
|
```bash
|
|
@@ -73,6 +116,16 @@ tm orders # View order history
|
|
|
73
116
|
tm history # Alias for orders
|
|
74
117
|
```
|
|
75
118
|
|
|
119
|
+
### Jobs
|
|
120
|
+
|
|
121
|
+
```bash
|
|
122
|
+
tm jobs # Browse job listings
|
|
123
|
+
tm jobs list # List all vacancies
|
|
124
|
+
tm jobs view <id> # View job details
|
|
125
|
+
tm jobs apply <id> # Apply to a job
|
|
126
|
+
tm jobs my # Your applications
|
|
127
|
+
```
|
|
128
|
+
|
|
76
129
|
### Stores & Reviews
|
|
77
130
|
|
|
78
131
|
```bash
|
|
@@ -94,27 +147,36 @@ tm ai run <model> <input> # Run an AI model
|
|
|
94
147
|
tm ai credits # Check your credit balance
|
|
95
148
|
tm ai topup <amount> # Add credits ($5 minimum)
|
|
96
149
|
tm ai history # View usage history
|
|
97
|
-
|
|
98
|
-
#
|
|
99
|
-
tm credits # Check credits (shortcut)
|
|
100
|
-
tm topup <amount> # Add credits (shortcut)
|
|
150
|
+
tm credits # Shortcut
|
|
151
|
+
tm topup <amount> # Shortcut
|
|
101
152
|
```
|
|
102
153
|
|
|
103
154
|
### Aliases & Rewards
|
|
104
155
|
|
|
105
156
|
```bash
|
|
106
157
|
tm alias list # List your aliases
|
|
107
|
-
tm alias add <name> <command> # Create alias
|
|
158
|
+
tm alias add <name> <command> # Create alias (e.g. "morning-coffee" -> "add coffee-03")
|
|
108
159
|
tm alias remove <name> # Remove alias
|
|
109
|
-
tm aliases # Shortcut
|
|
160
|
+
tm aliases # Shortcut
|
|
110
161
|
|
|
111
162
|
tm reward list # List reward rules
|
|
112
|
-
tm reward add <product> <pushes> # Auto-order after N pushes
|
|
163
|
+
tm reward add <product> <pushes> # Auto-order after N git pushes
|
|
113
164
|
tm reward remove <id> # Remove reward rule
|
|
114
|
-
tm rewards # Shortcut
|
|
165
|
+
tm rewards # Shortcut
|
|
115
166
|
```
|
|
116
167
|
|
|
117
|
-
###
|
|
168
|
+
### Profile
|
|
169
|
+
|
|
170
|
+
```bash
|
|
171
|
+
tm profile # View your profile
|
|
172
|
+
tm profile set name "John Doe" # Update your name
|
|
173
|
+
tm profile set phone "+1234567890" # Update phone
|
|
174
|
+
tm profile set address "123 Main" # Update address
|
|
175
|
+
tm profile set city "Berlin" # Update city
|
|
176
|
+
tm profile set country "DE" # Update country
|
|
177
|
+
```
|
|
178
|
+
|
|
179
|
+
### Categories
|
|
118
180
|
|
|
119
181
|
```bash
|
|
120
182
|
tm categories # List all categories
|
|
@@ -122,51 +184,44 @@ tm category <slug> # List products in category
|
|
|
122
184
|
tm offers # List all offers
|
|
123
185
|
```
|
|
124
186
|
|
|
125
|
-
Available categories:
|
|
126
|
-
- `coffee` — Specialty coffee for developers
|
|
127
|
-
- `lunch` — Meal subscriptions & delivery
|
|
128
|
-
- `snacks` — Healthy snacks & energy packs
|
|
129
|
-
- `focus` — Deep work kits & nootropics
|
|
130
|
-
- `health` — Yoga, massage, developer health
|
|
131
|
-
- `coworking` — Coworking spaces & nomad services
|
|
132
|
-
- `digital` — Productivity tools & apps
|
|
133
|
-
- `services` — Taxi, booking, personal services
|
|
187
|
+
Available categories: `coffee`, `lunch`, `snacks`, `focus`, `health`, `coworking`, `digital`, `services`, `hardware`, `events`, `b2b`, `travel`, and more.
|
|
134
188
|
|
|
135
|
-
###
|
|
189
|
+
### Merchant (Sellers)
|
|
136
190
|
|
|
137
191
|
```bash
|
|
138
|
-
tm
|
|
139
|
-
tm
|
|
192
|
+
tm merchant dashboard # Seller dashboard
|
|
193
|
+
tm merchant product create --name "..." --price 10 --category coffee --description "..."
|
|
194
|
+
tm merchant orders # View store orders
|
|
195
|
+
tm merchant analytics # Sales analytics
|
|
140
196
|
```
|
|
141
197
|
|
|
142
|
-
###
|
|
198
|
+
### Configuration
|
|
143
199
|
|
|
144
200
|
```bash
|
|
201
|
+
tm config get api # Show API endpoint
|
|
202
|
+
tm config set api <url> # Set custom API endpoint
|
|
145
203
|
tm about # About TerminalMarket
|
|
146
|
-
tm help
|
|
147
|
-
tm help <command> # Help for specific command
|
|
204
|
+
tm --help # Show help
|
|
148
205
|
tm --version # Show version
|
|
149
206
|
```
|
|
150
207
|
|
|
151
|
-
## Examples
|
|
208
|
+
## Pipe Examples
|
|
152
209
|
|
|
153
|
-
|
|
154
|
-
# Browse coffee products in Berlin
|
|
155
|
-
tm search "coffee" --city Berlin
|
|
210
|
+
TerminalMarket CLI supports Unix-style pipes:
|
|
156
211
|
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
tm
|
|
212
|
+
```bash
|
|
213
|
+
# Find cheapest coffee
|
|
214
|
+
tm search coffee | sort price | head 1
|
|
160
215
|
|
|
161
|
-
#
|
|
162
|
-
tm
|
|
216
|
+
# Count snacks under $10
|
|
217
|
+
tm search snacks | filter --max-price 10 | count
|
|
163
218
|
|
|
164
|
-
#
|
|
165
|
-
tm
|
|
219
|
+
# Monitor laptop prices via Telegram
|
|
220
|
+
tm watch create search laptop --sort price --name "Laptop tracker" --notify telegram
|
|
166
221
|
|
|
167
|
-
#
|
|
168
|
-
tm
|
|
169
|
-
tm
|
|
222
|
+
# One-command morning routine (via alias)
|
|
223
|
+
tm alias add morning "add coffee-03"
|
|
224
|
+
tm morning
|
|
170
225
|
```
|
|
171
226
|
|
|
172
227
|
## Configuration
|
package/bin/tm.js
CHANGED
|
@@ -366,8 +366,24 @@ async function showProfile() {
|
|
|
366
366
|
console.log(` ${chalk.dim('Bio:')} ${user.bio}`);
|
|
367
367
|
}
|
|
368
368
|
console.log();
|
|
369
|
+
|
|
370
|
+
// Messengers
|
|
371
|
+
const hasMessenger = user.telegramUsername || user.whatsappNumber || user.viberNumber || user.discordUsername || user.teamsEmail;
|
|
372
|
+
console.log(chalk.cyan.bold(' Messengers'));
|
|
373
|
+
console.log(` ${chalk.dim('Telegram:')} ${user.telegramUsername || chalk.dim('(not set)')}`);
|
|
374
|
+
console.log(` ${chalk.dim('WhatsApp:')} ${user.whatsappNumber || chalk.dim('(not set)')}`);
|
|
375
|
+
console.log(` ${chalk.dim('Viber:')} ${user.viberNumber || chalk.dim('(not set)')}`);
|
|
376
|
+
console.log(` ${chalk.dim('Discord:')} ${user.discordUsername || chalk.dim('(not set)')}`);
|
|
377
|
+
console.log(` ${chalk.dim('Teams:')} ${user.teamsEmail || chalk.dim('(not set)')}`);
|
|
378
|
+
console.log(` ${chalk.dim('Preferred:')} ${user.preferredContact || chalk.dim('(not set)')}`);
|
|
379
|
+
if (!hasMessenger) {
|
|
380
|
+
console.log(chalk.yellow('\n ⚠ Add at least one messenger so posters can contact you directly!'));
|
|
381
|
+
console.log(chalk.dim(' tm profile set telegram @yourusername'));
|
|
382
|
+
}
|
|
383
|
+
console.log();
|
|
369
384
|
console.log(chalk.dim(' Use: tm profile set <field> <value>'));
|
|
370
385
|
console.log(chalk.dim(' Fields: name, phone, city, country, github, linkedin, skills, bio, available'));
|
|
386
|
+
console.log(chalk.dim(' telegram, whatsapp, viber, discord, teams, contact'));
|
|
371
387
|
console.log();
|
|
372
388
|
}
|
|
373
389
|
|
|
@@ -390,6 +406,12 @@ async function setProfileField(field, value) {
|
|
|
390
406
|
skills: "skills",
|
|
391
407
|
bio: "bio",
|
|
392
408
|
available: "availableForHire",
|
|
409
|
+
telegram: "telegramUsername",
|
|
410
|
+
whatsapp: "whatsappNumber",
|
|
411
|
+
viber: "viberNumber",
|
|
412
|
+
discord: "discordUsername",
|
|
413
|
+
teams: "teamsEmail",
|
|
414
|
+
contact: "preferredContact",
|
|
393
415
|
};
|
|
394
416
|
|
|
395
417
|
const apiField = fieldMapping[field];
|
|
@@ -2723,6 +2745,7 @@ const commandGroups = {
|
|
|
2723
2745
|
'Shopping': ['featured', 'deals', 'products', 'search', 'view', 'buy', 'book', 'open', 'categories'],
|
|
2724
2746
|
'Cart & Orders': ['cart', 'add', 'checkout', 'orders'],
|
|
2725
2747
|
'Reverse Marketplace': ['request'],
|
|
2748
|
+
'Dev Bounties': ['live', 'need', 'ping', 'bounty', 'reputation'],
|
|
2726
2749
|
'Automation': ['watch', 'telegram'],
|
|
2727
2750
|
'Developer Jobs': ['jobs', 'job', 'apply', 'applications'],
|
|
2728
2751
|
'Stores': ['sellers', 'store', 'reviews', 'where'],
|
|
@@ -2743,6 +2766,7 @@ const basicGroups = {
|
|
|
2743
2766
|
const advancedGroups = {
|
|
2744
2767
|
'Cart & Orders': ['cart', 'add', 'checkout', 'orders'],
|
|
2745
2768
|
'Reverse Marketplace': ['request'],
|
|
2769
|
+
'Dev Bounties': ['live', 'need', 'ping', 'bounty', 'reputation'],
|
|
2746
2770
|
'AI Services': ['ai', 'credits', 'topup'],
|
|
2747
2771
|
'Stores': ['sellers', 'store', 'reviews'],
|
|
2748
2772
|
'Automation': ['watch', 'telegram', 'alias', 'reward', 'subscribe', 'wishlist', 'webhook']
|
|
@@ -2854,6 +2878,7 @@ function showHelp(commandName = null, mode = 'basic') {
|
|
|
2854
2878
|
console.log();
|
|
2855
2879
|
|
|
2856
2880
|
printGroup('Shop', ['featured', 'deals', 'search', 'products'], '🛒', chalk.green);
|
|
2881
|
+
printGroup('Dev Bounties', ['live', 'need', 'ping', 'profile'], '🔥', chalk.red);
|
|
2857
2882
|
printGroup('Account', ['login', 'register', 'profile'], '👤', chalk.blue);
|
|
2858
2883
|
printGroup('Help', ['doctor', 'help'], '💡', chalk.gray);
|
|
2859
2884
|
|
|
@@ -2869,6 +2894,7 @@ function showHelp(commandName = null, mode = 'basic') {
|
|
|
2869
2894
|
|
|
2870
2895
|
printGroup('Cart & Orders', ['cart', 'add', 'checkout', 'orders'], '📦', chalk.yellow);
|
|
2871
2896
|
printGroup('Reverse Marketplace', ['request'], '📋', chalk.magenta);
|
|
2897
|
+
printGroup('Dev Bounties', ['live', 'need', 'ping', 'bounty', 'reputation'], '🔥', chalk.red);
|
|
2872
2898
|
printGroup('AI Services', ['ai', 'credits', 'topup'], '🤖', chalk.cyan);
|
|
2873
2899
|
printGroup('Stores', ['sellers', 'store', 'reviews'], '🏪', chalk.magenta);
|
|
2874
2900
|
printGroup('Automation', ['watch', 'telegram', 'alias', 'reward', 'subscribe', 'wishlist'], '👁', chalk.cyan);
|
|
@@ -2888,6 +2914,7 @@ function showHelp(commandName = null, mode = 'basic') {
|
|
|
2888
2914
|
'Shopping': chalk.green,
|
|
2889
2915
|
'Cart & Orders': chalk.yellow,
|
|
2890
2916
|
'Reverse Marketplace': chalk.magenta,
|
|
2917
|
+
'Dev Bounties': chalk.red,
|
|
2891
2918
|
'Automation': chalk.cyan,
|
|
2892
2919
|
'Developer Jobs': chalk.magenta,
|
|
2893
2920
|
'Stores': chalk.cyan,
|
|
@@ -2903,6 +2930,7 @@ function showHelp(commandName = null, mode = 'basic') {
|
|
|
2903
2930
|
'Shopping': '🛒',
|
|
2904
2931
|
'Cart & Orders': '📦',
|
|
2905
2932
|
'Reverse Marketplace': '📋',
|
|
2933
|
+
'Dev Bounties': '🔥',
|
|
2906
2934
|
'Automation': '👁',
|
|
2907
2935
|
'Developer Jobs': '💼',
|
|
2908
2936
|
'Stores': '🏪',
|
|
@@ -4066,6 +4094,397 @@ requestCmd.action(() => {
|
|
|
4066
4094
|
requestCmd.outputHelp();
|
|
4067
4095
|
});
|
|
4068
4096
|
|
|
4097
|
+
// ═══════════════════════════════════════════════════════════
|
|
4098
|
+
// Dev Bounties — Live Task Matching
|
|
4099
|
+
// ═══════════════════════════════════════════════════════════
|
|
4100
|
+
|
|
4101
|
+
const bountyCmd = program
|
|
4102
|
+
.command("bounty")
|
|
4103
|
+
.alias("bounties")
|
|
4104
|
+
.description("Dev Bounties: post micro-tasks with rewards, get instant dev help");
|
|
4105
|
+
|
|
4106
|
+
bountyCmd
|
|
4107
|
+
.command("create <title...>")
|
|
4108
|
+
.alias("new")
|
|
4109
|
+
.description("Create a new bounty (micro-task)")
|
|
4110
|
+
.option("--reward <reward>", "Reward amount (e.g. $40, negotiable)", "negotiable")
|
|
4111
|
+
.option("--category <cat>", "Category: dev, design, devops, data-science, ml, data-engineering, analytics, content", "dev")
|
|
4112
|
+
.option("--lang <techs>", "Required technologies (comma-separated, e.g. react,figma,css,pytorch,pandas)")
|
|
4113
|
+
.option("--urgency <urgency>", "Urgency: now, today, this_week, flexible", "today")
|
|
4114
|
+
.option("--expires <hours>", "Expires in N hours", "24")
|
|
4115
|
+
.option("--description <desc>", "Task description")
|
|
4116
|
+
.action(async (titleParts, opts) => {
|
|
4117
|
+
try {
|
|
4118
|
+
const title = titleParts.join(" ");
|
|
4119
|
+
const catAliases = { ds: "data-science", ml: "machine-learning", de: "data-engineering", ai: "machine-learning", datascience: "data-science", dataeng: "data-engineering" };
|
|
4120
|
+
const rawCat = (opts.category || "dev").toLowerCase();
|
|
4121
|
+
const body = {
|
|
4122
|
+
title,
|
|
4123
|
+
reward: opts.reward,
|
|
4124
|
+
category: catAliases[rawCat] || rawCat,
|
|
4125
|
+
urgency: opts.urgency,
|
|
4126
|
+
expiresInHours: parseInt(opts.expires, 10) || 24,
|
|
4127
|
+
};
|
|
4128
|
+
if (opts.description) body.description = opts.description;
|
|
4129
|
+
if (opts.lang) body.languages = opts.lang.split(",").map(l => l.trim()).filter(Boolean);
|
|
4130
|
+
|
|
4131
|
+
const spinner = createSpinner("Creating bounty...");
|
|
4132
|
+
const data = await apiPost("/bounties", body);
|
|
4133
|
+
stopSpinner(spinner);
|
|
4134
|
+
|
|
4135
|
+
showSuccess("Bounty created!");
|
|
4136
|
+
console.log(` ${chalk.dim("ID:")} #${data.id}`);
|
|
4137
|
+
console.log(` ${chalk.dim("Title:")} ${data.title}`);
|
|
4138
|
+
console.log(` ${chalk.dim("Reward:")} ${data.reward}`);
|
|
4139
|
+
console.log(` ${chalk.dim("Category:")} ${data.category}`);
|
|
4140
|
+
if (data.languages && data.languages.length) console.log(` ${chalk.dim("Tech:")} ${data.languages.map(l => chalk.magenta(l)).join(", ")}`);
|
|
4141
|
+
console.log(` ${chalk.dim("Urgency:")} ${data.urgency}`);
|
|
4142
|
+
console.log(` ${chalk.dim("Status:")} ${data.status}`);
|
|
4143
|
+
} catch (e) {
|
|
4144
|
+
showError(e?.message || "Failed to create bounty");
|
|
4145
|
+
process.exitCode = 1;
|
|
4146
|
+
}
|
|
4147
|
+
});
|
|
4148
|
+
|
|
4149
|
+
// tm need — shortcut for bounty create
|
|
4150
|
+
program
|
|
4151
|
+
.command("need <title...>")
|
|
4152
|
+
.description("Quick post: 'I need...' (creates a bounty)")
|
|
4153
|
+
.option("--reward <reward>", "Reward amount", "negotiable")
|
|
4154
|
+
.option("--category <cat>", "Category: dev, data-science, ml, analytics, design, devops...", "dev")
|
|
4155
|
+
.option("--lang <techs>", "Required technologies (comma-separated, e.g. pytorch,pandas,sql)")
|
|
4156
|
+
.action(async (titleParts, opts) => {
|
|
4157
|
+
try {
|
|
4158
|
+
const title = titleParts.join(" ");
|
|
4159
|
+
const catAliases = { ds: "data-science", ml: "machine-learning", de: "data-engineering", ai: "machine-learning", datascience: "data-science", dataeng: "data-engineering" };
|
|
4160
|
+
const rawCat = (opts.category || "dev").toLowerCase();
|
|
4161
|
+
const body = {
|
|
4162
|
+
title,
|
|
4163
|
+
reward: opts.reward,
|
|
4164
|
+
category: catAliases[rawCat] || rawCat,
|
|
4165
|
+
urgency: "today",
|
|
4166
|
+
expiresInHours: 24,
|
|
4167
|
+
};
|
|
4168
|
+
if (opts.lang) body.languages = opts.lang.split(",").map(l => l.trim()).filter(Boolean);
|
|
4169
|
+
|
|
4170
|
+
const spinner = createSpinner("Posting need...");
|
|
4171
|
+
const data = await apiPost("/bounties", body);
|
|
4172
|
+
stopSpinner(spinner);
|
|
4173
|
+
|
|
4174
|
+
showSuccess("Need posted!");
|
|
4175
|
+
console.log(` ${chalk.dim("ID:")} #${data.id}`);
|
|
4176
|
+
console.log(` ${chalk.dim("Title:")} ${data.title}`);
|
|
4177
|
+
console.log(` ${chalk.dim("Reward:")} ${data.reward}`);
|
|
4178
|
+
if (data.languages && data.languages.length) console.log(` ${chalk.dim("Tech:")} ${data.languages.map(l => chalk.magenta(l)).join(", ")}`);
|
|
4179
|
+
console.log(` ${chalk.cyan("Developers will see this in")} ${chalk.bold("tm live")}`);
|
|
4180
|
+
} catch (e) {
|
|
4181
|
+
showError(e?.message || "Failed to post need");
|
|
4182
|
+
process.exitCode = 1;
|
|
4183
|
+
}
|
|
4184
|
+
});
|
|
4185
|
+
|
|
4186
|
+
// tm live — view active bounties feed (auto-matched to your skills)
|
|
4187
|
+
program
|
|
4188
|
+
.command("live")
|
|
4189
|
+
.description("Live feed: active bounties matched to your skills")
|
|
4190
|
+
.option("--category <cat>", "Filter by category")
|
|
4191
|
+
.option("--urgency <urgency>", "Filter by urgency")
|
|
4192
|
+
.option("--lang <techs>", "Filter by technologies (comma-separated)")
|
|
4193
|
+
.option("--all", "Show ALL bounties (skip skill matching)")
|
|
4194
|
+
.option("--page <n>", "Page number (default 1, 10 per page)", "1")
|
|
4195
|
+
.action(async (opts) => {
|
|
4196
|
+
try {
|
|
4197
|
+
const page = Math.max(1, parseInt(opts.page) || 1);
|
|
4198
|
+
const limit = 10;
|
|
4199
|
+
const offset = (page - 1) * limit;
|
|
4200
|
+
const params = [`limit=${limit}`, `offset=${offset}`];
|
|
4201
|
+
if (opts.category) params.push(`category=${opts.category}`);
|
|
4202
|
+
if (opts.urgency) params.push(`urgency=${opts.urgency}`);
|
|
4203
|
+
if (opts.lang) params.push(`lang=${encodeURIComponent(opts.lang)}`);
|
|
4204
|
+
if (opts.all) {
|
|
4205
|
+
params.push("all=1");
|
|
4206
|
+
} else {
|
|
4207
|
+
params.push("match=auto");
|
|
4208
|
+
}
|
|
4209
|
+
const url = "/bounties?" + params.join("&");
|
|
4210
|
+
|
|
4211
|
+
const spinner = createSpinner("Loading live feed...");
|
|
4212
|
+
const raw = await apiGet(url);
|
|
4213
|
+
stopSpinner(spinner);
|
|
4214
|
+
|
|
4215
|
+
const bounties = Array.isArray(raw) ? raw : (raw.bounties || []);
|
|
4216
|
+
const total = Array.isArray(raw) ? bounties.length : (raw.total || bounties.length);
|
|
4217
|
+
const matchedBySkills = !Array.isArray(raw) && raw.matchedBySkills;
|
|
4218
|
+
const totalPages = Math.ceil(total / limit);
|
|
4219
|
+
|
|
4220
|
+
if (!bounties.length) {
|
|
4221
|
+
if (opts.all) {
|
|
4222
|
+
showInfo("No active bounties right now. Check back soon!");
|
|
4223
|
+
} else {
|
|
4224
|
+
showInfo("No bounties matching your skills. Try: tm live --all");
|
|
4225
|
+
}
|
|
4226
|
+
return;
|
|
4227
|
+
}
|
|
4228
|
+
|
|
4229
|
+
const matchLabel = matchedBySkills ? chalk.magenta(" matched to your skills") : opts.all ? chalk.dim(" all") : "";
|
|
4230
|
+
const pageLabel = totalPages > 1 ? chalk.dim(` page ${page}/${totalPages}`) : "";
|
|
4231
|
+
console.log(chalk.bold(`\n🔥 Live Bounties (${total})`) + matchLabel + pageLabel + "\n");
|
|
4232
|
+
for (const b of bounties) {
|
|
4233
|
+
const urgencyIcon = b.urgency === "now" ? "⚡" : b.urgency === "today" ? "🔥" : "📋";
|
|
4234
|
+
const responses = b.responseCount || 0;
|
|
4235
|
+
const respText = responses > 0 ? chalk.green(`${responses} response${responses > 1 ? "s" : ""}`) : chalk.dim("no responses");
|
|
4236
|
+
const langs = (b.languages && b.languages.length > 0) ? " " + b.languages.map(l => chalk.magenta(`[${l}]`)).join(" ") : "";
|
|
4237
|
+
console.log(`${urgencyIcon} ${chalk.bold(`#${b.id}`)} ${b.title} — ${chalk.yellow(b.reward)} · ${chalk.dim(b.category)}${langs} · ${respText}`);
|
|
4238
|
+
}
|
|
4239
|
+
console.log(`\n${chalk.dim("Respond with:")} ${chalk.cyan("tm ping <id>")} ${chalk.dim("or")} ${chalk.cyan('tm ping <id> --message "I can help"')}`);
|
|
4240
|
+
if (totalPages > 1 && page < totalPages) {
|
|
4241
|
+
console.log(`${chalk.dim("Next page:")} ${chalk.cyan(`tm live --page ${page + 1}`)}`);
|
|
4242
|
+
}
|
|
4243
|
+
if (matchedBySkills) {
|
|
4244
|
+
console.log(`${chalk.dim("See all:")} ${chalk.cyan("tm live --all")}`);
|
|
4245
|
+
}
|
|
4246
|
+
} catch (e) {
|
|
4247
|
+
showError(e?.message || "Failed to load live feed");
|
|
4248
|
+
process.exitCode = 1;
|
|
4249
|
+
}
|
|
4250
|
+
});
|
|
4251
|
+
|
|
4252
|
+
// tm ping <id> — respond to a bounty (checks profile first)
|
|
4253
|
+
program
|
|
4254
|
+
.command("ping <bountyId>")
|
|
4255
|
+
.description("Respond to a bounty (express interest)")
|
|
4256
|
+
.option("-m, --message <message>", "Your message to the poster")
|
|
4257
|
+
.action(async (bountyId, opts) => {
|
|
4258
|
+
try {
|
|
4259
|
+
// Check profile completeness before responding
|
|
4260
|
+
try {
|
|
4261
|
+
const profile = await apiGet("/profile");
|
|
4262
|
+
const missing = [];
|
|
4263
|
+
if (!profile.skills || profile.skills.length === 0) missing.push("skills");
|
|
4264
|
+
if (!profile.githubUsername) missing.push("github");
|
|
4265
|
+
if (!profile.bio) missing.push("bio");
|
|
4266
|
+
const hasMessenger = profile.telegramUsername || profile.whatsappNumber || profile.viberNumber || profile.discordUsername || profile.teamsEmail;
|
|
4267
|
+
if (!hasMessenger) missing.push("messenger");
|
|
4268
|
+
|
|
4269
|
+
if (missing.includes("skills")) {
|
|
4270
|
+
showError("Cannot respond — profile incomplete");
|
|
4271
|
+
console.log(`\n Posters need to see your skills to evaluate your fit.`);
|
|
4272
|
+
console.log(` Fill in your profile first:\n`);
|
|
4273
|
+
console.log(` ${chalk.cyan("tm profile set skills typescript,react,node")}`);
|
|
4274
|
+
console.log(` ${chalk.cyan("tm profile set github yourusername")}`);
|
|
4275
|
+
console.log(` ${chalk.cyan('tm profile set bio "Short description"')}`);
|
|
4276
|
+
console.log(` ${chalk.cyan('tm profile set telegram @yourusername')}`);
|
|
4277
|
+
console.log(`\n Then try ${chalk.cyan(`tm ping ${bountyId}`)} again.`);
|
|
4278
|
+
process.exitCode = 1;
|
|
4279
|
+
return;
|
|
4280
|
+
}
|
|
4281
|
+
if (missing.length > 0) {
|
|
4282
|
+
console.log(chalk.yellow(`\n⚠ Profile incomplete (${missing.join(", ")} missing). Posters check profiles before accepting.`));
|
|
4283
|
+
for (const f of missing) {
|
|
4284
|
+
if (f === "github") console.log(` ${chalk.cyan("tm profile set github yourusername")}`);
|
|
4285
|
+
if (f === "bio") console.log(` ${chalk.cyan('tm profile set bio "Your description"')}`);
|
|
4286
|
+
if (f === "messenger") console.log(` ${chalk.cyan('tm profile set telegram @yourusername')} ${chalk.dim("(or whatsapp, viber, discord, teams)")}`);
|
|
4287
|
+
}
|
|
4288
|
+
console.log();
|
|
4289
|
+
}
|
|
4290
|
+
} catch { /* profile check failed, proceed anyway */ }
|
|
4291
|
+
|
|
4292
|
+
const body = {};
|
|
4293
|
+
if (opts.message) body.message = opts.message;
|
|
4294
|
+
|
|
4295
|
+
const spinner = createSpinner("Sending response...");
|
|
4296
|
+
const data = await apiPost(`/bounties/${bountyId}/respond`, body);
|
|
4297
|
+
stopSpinner(spinner);
|
|
4298
|
+
|
|
4299
|
+
showSuccess("Response sent!");
|
|
4300
|
+
console.log(` ${chalk.dim("Bounty:")} #${bountyId}`);
|
|
4301
|
+
if (data.message) console.log(` ${chalk.dim("Message:")} ${data.message}`);
|
|
4302
|
+
console.log(`\n ${chalk.cyan("The poster will be notified and can accept your response.")}`);
|
|
4303
|
+
} catch (e) {
|
|
4304
|
+
showError(e?.message || "Failed to respond");
|
|
4305
|
+
process.exitCode = 1;
|
|
4306
|
+
}
|
|
4307
|
+
});
|
|
4308
|
+
|
|
4309
|
+
bountyCmd
|
|
4310
|
+
.command("list")
|
|
4311
|
+
.description("List your posted bounties")
|
|
4312
|
+
.action(async () => {
|
|
4313
|
+
try {
|
|
4314
|
+
const spinner = createSpinner("Loading your bounties...");
|
|
4315
|
+
const data = await apiGet("/my/bounties");
|
|
4316
|
+
stopSpinner(spinner);
|
|
4317
|
+
|
|
4318
|
+
if (!data.length) {
|
|
4319
|
+
showInfo("You haven't posted any bounties yet.");
|
|
4320
|
+
console.log(` ${chalk.dim("Create one with:")} ${chalk.cyan('tm need "Fix my Tailwind layout"')}`);
|
|
4321
|
+
return;
|
|
4322
|
+
}
|
|
4323
|
+
|
|
4324
|
+
console.log(chalk.bold(`\n📋 Your Bounties (${data.length})\n`));
|
|
4325
|
+
for (const b of data) {
|
|
4326
|
+
const statusIcon = b.status === "open" ? "●" : b.status === "in_progress" ? "◉" : b.status === "done" ? "✓" : "✗";
|
|
4327
|
+
const statusColor = b.status === "open" ? chalk.green : b.status === "in_progress" ? chalk.yellow : b.status === "done" ? chalk.blue : chalk.dim;
|
|
4328
|
+
console.log(`${statusColor(statusIcon)} ${chalk.bold(`#${b.id}`)} ${b.title} — ${chalk.yellow(b.reward)} · ${statusColor(b.status)} · ${b.responseCount || 0} responses`);
|
|
4329
|
+
}
|
|
4330
|
+
} catch (e) {
|
|
4331
|
+
showError(e?.message || "Failed to load bounties");
|
|
4332
|
+
process.exitCode = 1;
|
|
4333
|
+
}
|
|
4334
|
+
});
|
|
4335
|
+
|
|
4336
|
+
bountyCmd
|
|
4337
|
+
.command("view <id>")
|
|
4338
|
+
.description("View bounty details and responses")
|
|
4339
|
+
.action(async (id) => {
|
|
4340
|
+
try {
|
|
4341
|
+
const spinner = createSpinner("Loading bounty...");
|
|
4342
|
+
const data = await apiGet(`/bounties/${id}`);
|
|
4343
|
+
stopSpinner(spinner);
|
|
4344
|
+
|
|
4345
|
+
const b = data.bounty;
|
|
4346
|
+
console.log(chalk.bold(`\n📋 Bounty #${b.id}\n`));
|
|
4347
|
+
console.log(` ${chalk.dim("Title:")} ${b.title}`);
|
|
4348
|
+
if (b.description) console.log(` ${chalk.dim("Desc:")} ${b.description}`);
|
|
4349
|
+
console.log(` ${chalk.dim("Reward:")} ${chalk.yellow(b.reward)}`);
|
|
4350
|
+
console.log(` ${chalk.dim("Category:")} ${b.category}`);
|
|
4351
|
+
console.log(` ${chalk.dim("Urgency:")} ${b.urgency}`);
|
|
4352
|
+
console.log(` ${chalk.dim("Status:")} ${b.status}`);
|
|
4353
|
+
console.log(` ${chalk.dim("Posted by:")} ${b.posterUsername || 'anonymous'}`);
|
|
4354
|
+
console.log(` ${chalk.dim("Views:")} ${b.viewCount || 0}`);
|
|
4355
|
+
|
|
4356
|
+
if (data.responses?.length) {
|
|
4357
|
+
console.log(chalk.bold(`\n Responses (${data.responses.length}):`));
|
|
4358
|
+
for (const r of data.responses) {
|
|
4359
|
+
const statusIcon = r.status === "accepted" ? chalk.green("✓") : r.status === "rejected" ? chalk.red("✗") : chalk.dim("○");
|
|
4360
|
+
console.log(` ${statusIcon} ${chalk.bold(r.username || `User#${r.userId}`)} — ${r.message || '(no message)'}`);
|
|
4361
|
+
}
|
|
4362
|
+
}
|
|
4363
|
+
|
|
4364
|
+
if (data.reviews?.length) {
|
|
4365
|
+
console.log(chalk.bold(`\n Reviews:`));
|
|
4366
|
+
for (const r of data.reviews) {
|
|
4367
|
+
console.log(` ${"★".repeat(r.rating)}${"☆".repeat(5 - r.rating)} ${r.comment || ''}`);
|
|
4368
|
+
}
|
|
4369
|
+
}
|
|
4370
|
+
} catch (e) {
|
|
4371
|
+
showError(e?.message || "Failed to load bounty");
|
|
4372
|
+
process.exitCode = 1;
|
|
4373
|
+
}
|
|
4374
|
+
});
|
|
4375
|
+
|
|
4376
|
+
bountyCmd
|
|
4377
|
+
.command("accept <bountyId> <responseId>")
|
|
4378
|
+
.description("Accept a response (choose executor)")
|
|
4379
|
+
.action(async (bountyId, responseId) => {
|
|
4380
|
+
try {
|
|
4381
|
+
const spinner = createSpinner("Accepting response...");
|
|
4382
|
+
const data = await apiPost(`/bounties/${bountyId}/accept/${responseId}`);
|
|
4383
|
+
stopSpinner(spinner);
|
|
4384
|
+
|
|
4385
|
+
showSuccess("Response accepted!");
|
|
4386
|
+
console.log(` ${chalk.dim("Bounty:")} #${bountyId} → ${chalk.yellow("in_progress")}`);
|
|
4387
|
+
console.log(` ${chalk.dim("Executor:")} User #${data.bounty?.executorId}`);
|
|
4388
|
+
console.log(`\n ${chalk.cyan("Connect directly with your executor to start working.")}`);
|
|
4389
|
+
} catch (e) {
|
|
4390
|
+
showError(e?.message || "Failed to accept response");
|
|
4391
|
+
process.exitCode = 1;
|
|
4392
|
+
}
|
|
4393
|
+
});
|
|
4394
|
+
|
|
4395
|
+
bountyCmd
|
|
4396
|
+
.command("complete <bountyId>")
|
|
4397
|
+
.description("Mark bounty as done")
|
|
4398
|
+
.action(async (bountyId) => {
|
|
4399
|
+
try {
|
|
4400
|
+
const spinner = createSpinner("Completing bounty...");
|
|
4401
|
+
await apiPost(`/bounties/${bountyId}/complete`);
|
|
4402
|
+
stopSpinner(spinner);
|
|
4403
|
+
|
|
4404
|
+
showSuccess(`Bounty #${bountyId} completed!`);
|
|
4405
|
+
console.log(` ${chalk.dim("Don't forget to leave a review:")} ${chalk.cyan(`tm bounty review ${bountyId} 5 "Great work!"`)}`);
|
|
4406
|
+
} catch (e) {
|
|
4407
|
+
showError(e?.message || "Failed to complete bounty");
|
|
4408
|
+
process.exitCode = 1;
|
|
4409
|
+
}
|
|
4410
|
+
});
|
|
4411
|
+
|
|
4412
|
+
bountyCmd
|
|
4413
|
+
.command("review <bountyId> <rating> [comment...]")
|
|
4414
|
+
.description("Leave a review (1-5 stars)")
|
|
4415
|
+
.action(async (bountyId, rating, commentParts) => {
|
|
4416
|
+
try {
|
|
4417
|
+
const body = { rating: parseInt(rating, 10) };
|
|
4418
|
+
if (commentParts?.length) body.comment = commentParts.join(" ");
|
|
4419
|
+
|
|
4420
|
+
const spinner = createSpinner("Submitting review...");
|
|
4421
|
+
await apiPost(`/bounties/${bountyId}/review`, body);
|
|
4422
|
+
stopSpinner(spinner);
|
|
4423
|
+
|
|
4424
|
+
showSuccess("Review submitted!");
|
|
4425
|
+
console.log(` ${"★".repeat(body.rating)}${"☆".repeat(5 - body.rating)}`);
|
|
4426
|
+
} catch (e) {
|
|
4427
|
+
showError(e?.message || "Failed to submit review");
|
|
4428
|
+
process.exitCode = 1;
|
|
4429
|
+
}
|
|
4430
|
+
});
|
|
4431
|
+
|
|
4432
|
+
bountyCmd
|
|
4433
|
+
.command("cancel <bountyId>")
|
|
4434
|
+
.description("Cancel your bounty")
|
|
4435
|
+
.action(async (bountyId) => {
|
|
4436
|
+
try {
|
|
4437
|
+
const spinner = createSpinner("Cancelling bounty...");
|
|
4438
|
+
await apiDelete(`/bounties/${bountyId}`);
|
|
4439
|
+
stopSpinner(spinner);
|
|
4440
|
+
showSuccess(`Bounty #${bountyId} cancelled.`);
|
|
4441
|
+
} catch (e) {
|
|
4442
|
+
showError(e?.message || "Failed to cancel bounty");
|
|
4443
|
+
process.exitCode = 1;
|
|
4444
|
+
}
|
|
4445
|
+
});
|
|
4446
|
+
|
|
4447
|
+
// tm reputation [userId] — check bounty reputation
|
|
4448
|
+
program
|
|
4449
|
+
.command("reputation [userId]")
|
|
4450
|
+
.alias("rep")
|
|
4451
|
+
.description("Check bounty reputation (yours or another user)")
|
|
4452
|
+
.action(async (userId) => {
|
|
4453
|
+
try {
|
|
4454
|
+
let url;
|
|
4455
|
+
if (userId) {
|
|
4456
|
+
url = `/users/${userId}/bounty-reputation`;
|
|
4457
|
+
} else {
|
|
4458
|
+
const status = await apiGet("/auth/status");
|
|
4459
|
+
if (!status.isAuthenticated) {
|
|
4460
|
+
showError("Not authenticated. Use: tm reputation <userId>");
|
|
4461
|
+
return;
|
|
4462
|
+
}
|
|
4463
|
+
url = `/users/${status.user.id}/bounty-reputation`;
|
|
4464
|
+
}
|
|
4465
|
+
|
|
4466
|
+
const spinner = createSpinner("Loading reputation...");
|
|
4467
|
+
const data = await apiGet(url);
|
|
4468
|
+
stopSpinner(spinner);
|
|
4469
|
+
|
|
4470
|
+
console.log(chalk.bold("\n⭐ Bounty Reputation\n"));
|
|
4471
|
+
console.log(` ${chalk.dim("Overall:")} ${data.averageRating.toFixed(1)} / 5.0 (${data.reviewCount} reviews)`);
|
|
4472
|
+
if (data.asPoster) {
|
|
4473
|
+
console.log(` ${chalk.dim("As poster:")} ${data.asPoster.averageRating.toFixed(1)} / 5.0 (${data.asPoster.count} reviews)`);
|
|
4474
|
+
}
|
|
4475
|
+
if (data.asExecutor) {
|
|
4476
|
+
console.log(` ${chalk.dim("As executor:")} ${data.asExecutor.averageRating.toFixed(1)} / 5.0 (${data.asExecutor.count} reviews)`);
|
|
4477
|
+
}
|
|
4478
|
+
} catch (e) {
|
|
4479
|
+
showError(e?.message || "Failed to load reputation");
|
|
4480
|
+
process.exitCode = 1;
|
|
4481
|
+
}
|
|
4482
|
+
});
|
|
4483
|
+
|
|
4484
|
+
bountyCmd.action(() => {
|
|
4485
|
+
bountyCmd.outputHelp();
|
|
4486
|
+
});
|
|
4487
|
+
|
|
4069
4488
|
program
|
|
4070
4489
|
.command("help [command]")
|
|
4071
4490
|
.description("Show help for a command")
|