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.
Files changed (3) hide show
  1. package/README.md +113 -58
  2. package/bin/tm.js +419 -0
  3. 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 for developers.
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
+ ![Search and buy products](./docs/demo-search-buy.png)
12
+
13
+ ### Reverse Marketplace — stores compete for your order
14
+
15
+ ![Reverse marketplace](./docs/demo-reverse-marketplace.png)
16
+
17
+ ### Watch & Price Alerts via Telegram
18
+
19
+ ![Watch alerts](./docs/demo-watch-alerts.png)
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
- ## Usage
37
+ ## Quick Start
22
38
 
23
39
  ```bash
24
- tm <command> [options]
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 products --store 1 # Filter by store
59
- tm search "coffee berlin" # Search products
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 list # View 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
- # Shortcuts
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 for alias list
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 for reward list
165
+ tm rewards # Shortcut
115
166
  ```
116
167
 
117
- ### Categories & Offers
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
- ### Configuration
189
+ ### Merchant (Sellers)
136
190
 
137
191
  ```bash
138
- tm config get api # Show API endpoint
139
- tm config set api <url> # Set API endpoint
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
- ### Other
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 # Show 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
- ```bash
154
- # Browse coffee products in Berlin
155
- tm search "coffee" --city Berlin
210
+ TerminalMarket CLI supports Unix-style pipes:
156
211
 
157
- # Add product to cart and checkout
158
- tm add 123
159
- tm checkout
212
+ ```bash
213
+ # Find cheapest coffee
214
+ tm search coffee | sort price | head 1
160
215
 
161
- # Leave a 5-star review
162
- tm review 1 5 "Great coffee, fast delivery!"
216
+ # Count snacks under $10
217
+ tm search snacks | filter --max-price 10 | count
163
218
 
164
- # View your order history
165
- tm orders
219
+ # Monitor laptop prices via Telegram
220
+ tm watch create search laptop --sort price --name "Laptop tracker" --notify telegram
166
221
 
167
- # Use AI services
168
- tm ai topup 10
169
- tm ai run text-rewrite "Fix this text"
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")
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "terminalmarket",
3
- "version": "0.11.2",
3
+ "version": "0.12.0",
4
4
  "description": "TerminalMarket CLI — a curated marketplace for developers & founders (client for terminalmarket.app)",
5
5
  "bin": {
6
6
  "tm": "bin/tm.js"