terminalmarket 0.12.0 β†’ 0.13.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 (2) hide show
  1. package/bin/tm.js +547 -1
  2. package/package.json +1 -1
package/bin/tm.js CHANGED
@@ -299,6 +299,80 @@ program
299
299
  }
300
300
  });
301
301
 
302
+ // Share a bounty link
303
+ program
304
+ .command("share <bountyId>")
305
+ .description("Get a shareable link for a bounty")
306
+ .action(async (bountyId) => {
307
+ try {
308
+ const id = parseInt(bountyId, 10);
309
+ if (isNaN(id)) {
310
+ console.log(chalk.red("Invalid bounty ID"));
311
+ return;
312
+ }
313
+ const bounty = await apiGet(`/bounties/${id}`);
314
+ if (!bounty || !bounty.id) {
315
+ console.log(chalk.red("Bounty not found"));
316
+ return;
317
+ }
318
+ const base = getApiBase().replace("/api", "");
319
+ const link = `${base}/bounty/${id}`;
320
+ console.log();
321
+ console.log(chalk.bold(` πŸ“‹ ${bounty.title}`));
322
+ console.log(` πŸ’° ${bounty.reward || "TBD"} Β· ${bounty.category || "dev"}`);
323
+ console.log();
324
+ console.log(chalk.cyan(` ${link}`));
325
+ console.log();
326
+ console.log(chalk.dim(" Send this link to a colleague, or they can respond:"));
327
+ console.log(chalk.green(` $ tm ping ${id} --message "I can help"`));
328
+ console.log();
329
+ } catch (e) {
330
+ console.error(chalk.red(e?.message || String(e)));
331
+ process.exitCode = 1;
332
+ }
333
+ });
334
+
335
+ // Developer leaderboard
336
+ program
337
+ .command("top")
338
+ .description("Show top developers by completed bounties")
339
+ .option("-p, --period <period>", "Time period: all, month, week", "all")
340
+ .option("-l, --limit <n>", "Number of entries", "15")
341
+ .action(async (opts) => {
342
+ try {
343
+ const limit = parseInt(opts.limit, 10) || 15;
344
+ const result = await apiGet(`/leaderboard?period=${opts.period}&limit=${limit}`);
345
+ const leaders = result?.leaders || [];
346
+
347
+ if (leaders.length === 0) {
348
+ console.log(chalk.yellow("\n No leaderboard data yet. Complete bounties to appear here!\n"));
349
+ console.log(chalk.dim(" Browse bounties: tm live"));
350
+ return;
351
+ }
352
+
353
+ console.log();
354
+ console.log(chalk.bold(" πŸ† Developer Leaderboard") + chalk.dim(` (${opts.period === "all" ? "all time" : opts.period})`));
355
+ console.log();
356
+
357
+ for (const leader of leaders) {
358
+ const medal = leader.rank === 1 ? "πŸ₯‡" : leader.rank === 2 ? "πŸ₯ˆ" : leader.rank === 3 ? "πŸ₯‰" : chalk.dim(`#${String(leader.rank).padStart(2)}`);
359
+ const name = chalk.bold(leader.username);
360
+ const earned = chalk.green(leader.totalEarned);
361
+ const bounties = chalk.dim(`${leader.completedBounties} bounties`);
362
+ const rating = leader.avgRating ? chalk.yellow(`β˜…${leader.avgRating}`) : "";
363
+ const skills = leader.topSkills?.length > 0 ? chalk.dim(` [${leader.topSkills.slice(0, 3).join(", ")}]`) : "";
364
+
365
+ console.log(` ${medal} ${name.padEnd(24)} ${earned.padEnd(14)} ${bounties.padEnd(14)} ${rating}${skills}`);
366
+ }
367
+ console.log();
368
+ console.log(chalk.dim(" Full leaderboard: https://terminalmarket.app/leaderboard"));
369
+ console.log();
370
+ } catch (e) {
371
+ console.error(chalk.red(e?.message || String(e)));
372
+ process.exitCode = 1;
373
+ }
374
+ });
375
+
302
376
  program
303
377
  .command("whoami")
304
378
  .alias("me")
@@ -381,6 +455,25 @@ async function showProfile() {
381
455
  console.log(chalk.dim(' tm profile set telegram @yourusername'));
382
456
  }
383
457
  console.log();
458
+
459
+ // Bounty earnings badge
460
+ try {
461
+ const bountyStats = await apiGet("/my/bounty-stats");
462
+ if (bountyStats && (bountyStats.completedBounties > 0 || bountyStats.totalEarned)) {
463
+ console.log(chalk.cyan.bold(' Bounty Earnings'));
464
+ console.log(` ${chalk.dim('Completed:')} ${chalk.green.bold(bountyStats.completedBounties || 0)} bounties`);
465
+ if (bountyStats.totalEarned) {
466
+ console.log(` ${chalk.dim('Earned:')} ${chalk.green.bold('$' + bountyStats.totalEarned)}`);
467
+ }
468
+ if (bountyStats.avgRating) {
469
+ console.log(` ${chalk.dim('Rating:')} ${chalk.yellow('β˜…' + bountyStats.avgRating)}`);
470
+ }
471
+ console.log();
472
+ }
473
+ } catch (_) {
474
+ // silently skip if not available
475
+ }
476
+
384
477
  console.log(chalk.dim(' Use: tm profile set <field> <value>'));
385
478
  console.log(chalk.dim(' Fields: name, phone, city, country, github, linkedin, skills, bio, available'));
386
479
  console.log(chalk.dim(' telegram, whatsapp, viber, discord, teams, contact'));
@@ -2752,6 +2845,7 @@ const commandGroups = {
2752
2845
  'AI Services': ['ai', 'credits', 'topup'],
2753
2846
  'On-Demand Tasks': ['tasks', 'task'],
2754
2847
  'Personalization': ['alias', 'reward'],
2848
+ 'Integrations': ['apps'],
2755
2849
  'Info': ['about', 'stats', 'policy', 'privacy', 'faq', 'contact'],
2756
2850
  'System': ['start', 'doctor', 'config', 'help']
2757
2851
  };
@@ -2880,6 +2974,7 @@ function showHelp(commandName = null, mode = 'basic') {
2880
2974
  printGroup('Shop', ['featured', 'deals', 'search', 'products'], 'πŸ›’', chalk.green);
2881
2975
  printGroup('Dev Bounties', ['live', 'need', 'ping', 'profile'], 'πŸ”₯', chalk.red);
2882
2976
  printGroup('Account', ['login', 'register', 'profile'], 'πŸ‘€', chalk.blue);
2977
+ printGroup('Integrations', ['apps'], '🧩', chalk.magenta);
2883
2978
  printGroup('Help', ['doctor', 'help'], 'πŸ’‘', chalk.gray);
2884
2979
 
2885
2980
  console.log(chalk.dim('─'.repeat(45)));
@@ -2920,6 +3015,7 @@ function showHelp(commandName = null, mode = 'basic') {
2920
3015
  'Stores': chalk.cyan,
2921
3016
  'AI Services': chalk.cyan,
2922
3017
  'On-Demand Tasks': chalk.yellow,
3018
+ 'Integrations': chalk.magenta,
2923
3019
  'Personalization': chalk.white,
2924
3020
  'Info': chalk.dim,
2925
3021
  'System': chalk.gray
@@ -2936,6 +3032,7 @@ function showHelp(commandName = null, mode = 'basic') {
2936
3032
  'Stores': 'πŸͺ',
2937
3033
  'AI Services': 'πŸ€–',
2938
3034
  'On-Demand Tasks': '⚑',
3035
+ 'Integrations': '🧩',
2939
3036
  'Personalization': 'βš™οΈ',
2940
3037
  'Info': 'ℹ️',
2941
3038
  'System': 'πŸ’»'
@@ -3659,7 +3756,6 @@ program
3659
3756
 
3660
3757
  program
3661
3758
  .command("applications")
3662
- .alias("apps")
3663
3759
  .description("View your job applications")
3664
3760
  .action(async () => {
3665
3761
  try {
@@ -4300,6 +4396,17 @@ program
4300
4396
  console.log(` ${chalk.dim("Bounty:")} #${bountyId}`);
4301
4397
  if (data.message) console.log(` ${chalk.dim("Message:")} ${data.message}`);
4302
4398
  console.log(`\n ${chalk.cyan("The poster will be notified and can accept your response.")}`);
4399
+
4400
+ // Check if user has completed bounties and suggest invite
4401
+ try {
4402
+ const stats = await apiGet("/my/bounty-stats");
4403
+ if (stats && stats.completedBounties > 0 && stats.completedBounties <= 3) {
4404
+ console.log();
4405
+ console.log(chalk.dim(" ─────────────────────────────────────"));
4406
+ console.log(` ${chalk.yellow("πŸ’‘")} Know someone who'd like to earn on bounties?`);
4407
+ console.log(` ${chalk.cyan("tm invite --email colleague@company.com")}`);
4408
+ }
4409
+ } catch (_) {}
4303
4410
  } catch (e) {
4304
4411
  showError(e?.message || "Failed to respond");
4305
4412
  process.exitCode = 1;
@@ -4485,6 +4592,417 @@ bountyCmd.action(() => {
4485
4592
  bountyCmd.outputHelp();
4486
4593
  });
4487
4594
 
4595
+ // -----------------
4596
+ // apps β€” SaaS integrations catalog
4597
+ // -----------------
4598
+ const appsCmd = program
4599
+ .command("apps")
4600
+ .description("Browse and manage SaaS integrations");
4601
+
4602
+ appsCmd
4603
+ .command("list")
4604
+ .description("List available integrations")
4605
+ .action(async () => {
4606
+ const spinner = createSpinner("Loading integrations...");
4607
+ try {
4608
+ const apps = await apiGet("/apps");
4609
+ stopSpinner(spinner);
4610
+ if (!apps || apps.length === 0) {
4611
+ showInfo("No integrations available yet.");
4612
+ return;
4613
+ }
4614
+ console.log();
4615
+ showSection("🧩 Available Integrations");
4616
+ for (const app of apps) {
4617
+ const icon = app.icon || "πŸ“¦";
4618
+ const cmds = (app.commands || []).map(c => c.name).join(", ");
4619
+ console.log(` ${icon} ${chalk.bold(app.name)} ${chalk.dim(`(tm ${app.slug})`)}`);
4620
+ if (app.description) console.log(` ${chalk.dim(app.description)}`);
4621
+ console.log(` ${chalk.dim(`${app.commands?.length || 0} commands Β· ${app.totalUsers || 0} users`)}`);
4622
+ if (cmds) console.log(` ${chalk.dim("Commands:")} ${cmds}`);
4623
+ console.log();
4624
+ }
4625
+ } catch (e) {
4626
+ stopSpinner(spinner);
4627
+ showError(e?.message || "Failed to load integrations");
4628
+ }
4629
+ });
4630
+
4631
+ appsCmd
4632
+ .command("info <slug>")
4633
+ .description("View app details and commands")
4634
+ .action(async (slug) => {
4635
+ const spinner = createSpinner(`Loading ${slug}...`);
4636
+ try {
4637
+ const app = await apiGet(`/apps/${slug}`);
4638
+ stopSpinner(spinner);
4639
+ console.log();
4640
+ const icon = app.icon || "πŸ“¦";
4641
+ showSection(`${icon} ${app.name}`);
4642
+ if (app.description) console.log(` ${app.description}`);
4643
+ if (app.seller) console.log(` ${chalk.dim(`by ${app.seller.name}${app.seller.verified ? " βœ“" : ""}`)}`);
4644
+ console.log(` ${chalk.dim(`${app.totalUsers} users Β· ${app.totalCalls} API calls Β· Auth: ${app.authType}`)}`);
4645
+
4646
+ const commands = app.manifest?.commands || [];
4647
+ if (commands.length > 0) {
4648
+ console.log();
4649
+ console.log(` ${chalk.bold("Commands:")}`);
4650
+ for (const cmd of commands) {
4651
+ const args = (cmd.args || cmd.params || [])
4652
+ .map(a => a.required ? `<${a.name}>` : `[${a.name}]`).join(" ");
4653
+ const urlTag = cmd.open_url ? chalk.cyan(" [opens browser]") : "";
4654
+ console.log(` ${chalk.green(`tm ${slug} ${cmd.name}`)} ${chalk.dim(args)}${urlTag}`);
4655
+ console.log(` ${chalk.dim(cmd.description)}`);
4656
+ }
4657
+ }
4658
+
4659
+ const plans = app.manifest?.plans;
4660
+ if (plans && plans.length > 0) {
4661
+ console.log();
4662
+ console.log(` ${chalk.bold("Plans:")}`);
4663
+ for (const p of plans) {
4664
+ const price = p.price?.monthly === 0 ? "Free" : `$${p.price?.monthly}/mo`;
4665
+ console.log(` ${chalk.yellow(p.key)} β€” ${price}${p.sites ? ` Β· ${p.sites} sites` : ""}${p.pagesPerSite ? ` Β· ${p.pagesPerSite} pages` : ""}`);
4666
+ }
4667
+ }
4668
+
4669
+ console.log();
4670
+ const hasAuthConnect = !!app.manifest?.auth?.connect;
4671
+ if (hasAuthConnect) {
4672
+ showInfo(`Connect: ${chalk.bold(`tm ${slug} connect`)}`);
4673
+ } else {
4674
+ showInfo(`Connect: ${chalk.bold(`tm ${slug} connect <your-api-key>`)}`);
4675
+ }
4676
+ } catch (e) {
4677
+ stopSpinner(spinner);
4678
+ showError(e?.message || "App not found");
4679
+ }
4680
+ });
4681
+
4682
+ appsCmd
4683
+ .command("my")
4684
+ .description("List your connected apps")
4685
+ .action(async () => {
4686
+ try {
4687
+ const apps = await apiGet("/my/apps");
4688
+ if (!apps || apps.length === 0) {
4689
+ showInfo("No connected apps. Browse with: tm apps list");
4690
+ return;
4691
+ }
4692
+ console.log();
4693
+ showSection(`πŸ”— Connected Apps (${apps.length})`);
4694
+ for (const app of apps) {
4695
+ console.log(` ${app.icon || "πŸ“¦"} ${chalk.bold(app.name)} ${chalk.dim(`(${app.slug})`)}`);
4696
+ }
4697
+ } catch (e) {
4698
+ showError(e?.message || "Failed to load connected apps");
4699
+ }
4700
+ });
4701
+
4702
+ appsCmd.action(() => {
4703
+ appsCmd.commands.find(c => c.name() === "list")?.parseAsync(["list"], { from: "user" });
4704
+ });
4705
+
4706
+ // -----------------
4707
+ // Dynamic namespace handler β€” intercepts unknown commands and checks if they're app namespaces
4708
+ // -----------------
4709
+ async function handleDynamicNamespace(namespace, args) {
4710
+ const subCommand = args[0] || "help";
4711
+ const restArgs = args.slice(1);
4712
+
4713
+ // "help" or no args β€” show app info
4714
+ if (subCommand === "help" || args.length === 0) {
4715
+ await appsCmd.commands.find(c => c.name() === "info")?.parseAsync(["info", namespace], { from: "user" });
4716
+ return;
4717
+ }
4718
+
4719
+ // "connect" flow
4720
+ if (subCommand === "connect") {
4721
+ await handleNamespaceConnect(namespace, restArgs);
4722
+ return;
4723
+ }
4724
+
4725
+ // "disconnect"
4726
+ if (subCommand === "disconnect") {
4727
+ const spinner = createSpinner("Disconnecting...");
4728
+ try {
4729
+ await apiDelete(`/apps/${namespace}/connect`);
4730
+ stopSpinner(spinner);
4731
+ showSuccess(`Disconnected from ${namespace}.`);
4732
+ } catch (e) {
4733
+ stopSpinner(spinner);
4734
+ showError(e?.message || "Failed to disconnect");
4735
+ }
4736
+ return;
4737
+ }
4738
+
4739
+ // Execute a command
4740
+ await handleNamespaceExecute(namespace, subCommand, restArgs);
4741
+ }
4742
+
4743
+ async function handleNamespaceConnect(namespace, args) {
4744
+ const spinner = createSpinner(`Connecting to ${namespace}...`);
4745
+ try {
4746
+ // First, fetch app detail to check auth type
4747
+ let app;
4748
+ try {
4749
+ app = await apiGet(`/apps/${namespace}`);
4750
+ } catch {
4751
+ stopSpinner(spinner);
4752
+ showError(`App "${namespace}" not found.`);
4753
+ return;
4754
+ }
4755
+ stopSpinner(spinner);
4756
+
4757
+ const hasAuthConnect = !!app.manifest?.auth?.connect;
4758
+ let body;
4759
+
4760
+ if (hasAuthConnect) {
4761
+ // Email-based connect
4762
+ let email = args[0];
4763
+ if (!email) {
4764
+ // Ask for email interactively
4765
+ const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
4766
+ email = await new Promise(resolve => {
4767
+ rl.question(chalk.cyan("Enter your email for ") + chalk.bold(app.name) + ": ", answer => {
4768
+ rl.close();
4769
+ resolve(answer.trim());
4770
+ });
4771
+ });
4772
+ }
4773
+ if (!email) {
4774
+ showError("Email is required to connect.");
4775
+ return;
4776
+ }
4777
+ body = { email };
4778
+ } else {
4779
+ // Raw API key
4780
+ const token = args[0];
4781
+ if (!token) {
4782
+ showError(`Usage: tm ${namespace} connect <your-api-key>`);
4783
+ return;
4784
+ }
4785
+ body = { token };
4786
+ }
4787
+
4788
+ const spinner2 = createSpinner("Authenticating...");
4789
+ try {
4790
+ const result = await apiPost(`/apps/${namespace}/connect`, body);
4791
+ stopSpinner(spinner2);
4792
+ showSuccess(`Connected to ${app.name}!`);
4793
+ if (result.plan) console.log(` ${chalk.dim("Plan:")} ${result.plan}`);
4794
+ if (result.isNew) console.log(` ${chalk.dim("New account created.")}`);
4795
+
4796
+ const commands = app.manifest?.commands || [];
4797
+ if (commands.length > 0) {
4798
+ const first = commands[0];
4799
+ const firstArgs = (first.args || first.params || [])
4800
+ .filter(a => a.required).map(a => `<${a.name}>`).join(" ");
4801
+ console.log();
4802
+ showInfo(`Try: ${chalk.bold(`tm ${namespace} ${first.name} ${firstArgs}`)}`);
4803
+ }
4804
+ } catch (e) {
4805
+ stopSpinner(spinner2);
4806
+ showError(e?.message || "Failed to connect");
4807
+ }
4808
+ } catch (e) {
4809
+ showError(e?.message || "Failed to connect");
4810
+ }
4811
+ }
4812
+
4813
+ async function handleNamespaceExecute(namespace, command, args) {
4814
+ // Parse args: positional + named (--key value)
4815
+ const positional = [];
4816
+ const named = {};
4817
+ for (let i = 0; i < args.length; i++) {
4818
+ const arg = args[i];
4819
+ if (arg.startsWith("--")) {
4820
+ const eqIdx = arg.indexOf("=");
4821
+ if (eqIdx > 0) {
4822
+ named[arg.slice(2, eqIdx)] = arg.slice(eqIdx + 1);
4823
+ } else {
4824
+ const key = arg.slice(2);
4825
+ if (i + 1 < args.length && !args[i + 1].startsWith("--")) {
4826
+ named[key] = args[++i];
4827
+ } else {
4828
+ named[key] = "true";
4829
+ }
4830
+ }
4831
+ } else {
4832
+ positional.push(arg);
4833
+ }
4834
+ }
4835
+
4836
+ // Fetch app info to resolve positional args
4837
+ let appDetail;
4838
+ try {
4839
+ appDetail = await apiGet(`/apps/${namespace}`);
4840
+ } catch {}
4841
+
4842
+ const cmdDef = appDetail?.manifest?.commands?.find(c => c.name === command);
4843
+ if (!cmdDef && appDetail) {
4844
+ const available = (appDetail.manifest?.commands || []).map(c => c.name).join(", ");
4845
+ showError(`Unknown command: ${command}. Available: ${available || "none"}`);
4846
+ return;
4847
+ }
4848
+
4849
+ // Resolve positional args using command definition
4850
+ const params = { ...named };
4851
+ if (cmdDef) {
4852
+ const argDefs = cmdDef.args || cmdDef.params || [];
4853
+ let posIdx = 0;
4854
+ for (const def of argDefs) {
4855
+ if (params[def.name] !== undefined) continue;
4856
+ if (posIdx < positional.length) {
4857
+ params[def.name] = positional[posIdx++];
4858
+ } else if (def.default !== undefined) {
4859
+ params[def.name] = String(def.default);
4860
+ }
4861
+ }
4862
+ }
4863
+
4864
+ const spinner = createSpinner(`Running ${namespace} ${command}...`);
4865
+ try {
4866
+ const result = await apiPost(`/apps/${namespace}/execute`, { command, params });
4867
+ stopSpinner(spinner);
4868
+
4869
+ // Handle open_url
4870
+ if (result.open_url) {
4871
+ showSuccess(`Opening in browser...`);
4872
+ if (shouldOpenExternal()) {
4873
+ try { await open(result.open_url); } catch {}
4874
+ }
4875
+ console.log(` ${chalk.dim(result.open_url)}`);
4876
+ return;
4877
+ }
4878
+
4879
+ const data = result.data;
4880
+
4881
+ // Error from upstream
4882
+ if (data?.error) {
4883
+ showError(data.error);
4884
+ if (data.upgradeRequired) {
4885
+ showInfo(`Upgrade: ${chalk.bold(`tm ${namespace} upgrade pro`)}`);
4886
+ }
4887
+ return;
4888
+ }
4889
+
4890
+ // Pretty-format known patterns
4891
+ formatNamespaceResponse(namespace, command, data);
4892
+ } catch (e) {
4893
+ stopSpinner(spinner);
4894
+ const msg = e?.message || "Command failed";
4895
+ if (msg.includes("Not connected") || msg.includes("needsConnect")) {
4896
+ showError(`Not connected to ${namespace}. Run: ${chalk.bold(`tm ${namespace} connect`)}`);
4897
+ } else {
4898
+ showError(msg);
4899
+ }
4900
+ }
4901
+ }
4902
+
4903
+ function formatNamespaceResponse(namespace, command, data) {
4904
+ if (typeof data === "string") {
4905
+ console.log(data);
4906
+ return;
4907
+ }
4908
+ if (!data || typeof data !== "object") {
4909
+ showSuccess("OK");
4910
+ return;
4911
+ }
4912
+
4913
+ // Sites list
4914
+ if (data.sites && Array.isArray(data.sites)) {
4915
+ if (data.sites.length === 0) {
4916
+ showInfo(`No sites yet. Add one: ${chalk.bold(`tm ${namespace} add <domain>`)}`);
4917
+ return;
4918
+ }
4919
+ showSection(`Your sites (${data.sites.length})`);
4920
+ for (const s of data.sites) {
4921
+ const statusIcon = s.status === "ready" ? "βœ…" : s.status === "scanning" ? "⏳" : s.status === "error" ? "❌" : "⏸️";
4922
+ console.log(` ${statusIcon} ${chalk.bold(s.domain)} ${chalk.dim(`[${s.status}]`)} ${chalk.dim(`ID: ${(s.id || "").slice(0, 8)}...`)}`);
4923
+ }
4924
+ return;
4925
+ }
4926
+
4927
+ // Site status with coverage
4928
+ if (data.coveragePct !== undefined) {
4929
+ const pct = data.coveragePct;
4930
+ const filled = Math.round(pct / 5);
4931
+ const bar = chalk.green("β–ˆ".repeat(filled)) + chalk.dim("β–‘".repeat(20 - filled));
4932
+ console.log();
4933
+ showSection(`${data.domain || "Site"} β€” ${data.status}`);
4934
+ console.log(` [${bar}] ${pct}% coverage`);
4935
+ console.log(` ${data.pagesFound || 0} pages found Β· ${data.pagesReady || 0} ready`);
4936
+
4937
+ if (data.pageTypes && typeof data.pageTypes === "object") {
4938
+ console.log();
4939
+ console.log(` ${chalk.bold("Page Types:")}`);
4940
+ for (const [type, count] of Object.entries(data.pageTypes)) {
4941
+ console.log(` ${chalk.dim(type)}: ${count}`);
4942
+ }
4943
+ }
4944
+
4945
+ if (data.installScript) {
4946
+ console.log();
4947
+ console.log(` ${chalk.bold("Install Script:")}`);
4948
+ console.log(` ${chalk.cyan(data.installScript)}`);
4949
+ }
4950
+
4951
+ if (data.upgradeRequired) {
4952
+ console.log();
4953
+ showWarning(`Upgrade needed: ${chalk.bold(`tm ${namespace} upgrade pro`)}`);
4954
+ }
4955
+ return;
4956
+ }
4957
+
4958
+ // Account info
4959
+ if (data.email && data.plan !== undefined) {
4960
+ showSection("Account");
4961
+ console.log(` ${chalk.dim("Email:")} ${data.email}`);
4962
+ console.log(` ${chalk.dim("Plan:")} ${chalk.bold(data.plan)}`);
4963
+ if (data.usage) {
4964
+ console.log(` ${chalk.dim("Sites:")} ${data.usage.sites}/${data.limits?.sites ?? "∞"}`);
4965
+ console.log(` ${chalk.dim("Pages:")} ${data.usage.pages}/${data.limits?.pagesPerSite ?? "∞"}`);
4966
+ }
4967
+ if (data.dashboardUrl) {
4968
+ console.log(` ${chalk.dim("Dashboard:")} ${chalk.bold(`tm ${namespace} dashboard`)}`);
4969
+ }
4970
+ return;
4971
+ }
4972
+
4973
+ // Billing status
4974
+ if (data.active !== undefined && data.plan) {
4975
+ console.log(` Plan: ${chalk.bold(data.plan)} Β· ${data.active ? chalk.green("Active") : chalk.dim("Inactive")}${data.nextBilling ? ` Β· Next: ${data.nextBilling}` : ""}`);
4976
+ return;
4977
+ }
4978
+
4979
+ // Site added
4980
+ if (data.siteId && data.domain) {
4981
+ showSuccess(`Site added: ${data.domain}`);
4982
+ console.log(` ${chalk.dim("ID:")} ${data.siteId}`);
4983
+ if (data.apiKey) console.log(` ${chalk.dim("API Key:")} ${data.apiKey}`);
4984
+ showInfo(`Next: ${chalk.bold(`tm ${namespace} scan ${data.siteId}`)}`);
4985
+ return;
4986
+ }
4987
+
4988
+ // Scan started
4989
+ if (data.ok && data.status === "scanning") {
4990
+ showSuccess(data.message || "Scan started!");
4991
+ if (data.scanLogId) console.log(` ${chalk.dim("Scan log:")} ${data.scanLogId}`);
4992
+ showInfo(`Check progress: ${chalk.bold(`tm ${namespace} status <site_id>`)}`);
4993
+ return;
4994
+ }
4995
+
4996
+ // Generic OK
4997
+ if (data.ok === true) {
4998
+ showSuccess(data.message || "OK");
4999
+ return;
5000
+ }
5001
+
5002
+ // Fallback: JSON output
5003
+ console.log(JSON.stringify(data, null, 2));
5004
+ }
5005
+
4488
5006
  program
4489
5007
  .command("help [command]")
4490
5008
  .description("Show help for a command")
@@ -4500,6 +5018,34 @@ program
4500
5018
  }
4501
5019
  });
4502
5020
 
5021
+ // Cache for resolved namespaces
5022
+ const _namespaceCache = new Map();
5023
+
5024
+ async function isAppNamespace(name) {
5025
+ if (!name || typeof name !== "string") return false;
5026
+ if (_namespaceCache.has(name)) return _namespaceCache.get(name);
5027
+ try {
5028
+ await apiGet(`/apps/${name}`);
5029
+ _namespaceCache.set(name, true);
5030
+ return true;
5031
+ } catch {
5032
+ _namespaceCache.set(name, false);
5033
+ return false;
5034
+ }
5035
+ }
5036
+
5037
+ // Intercept unknown commands to check dynamic namespaces
5038
+ program.on("command:*", async (operands) => {
5039
+ const unknownCmd = operands[0];
5040
+ if (await isAppNamespace(unknownCmd)) {
5041
+ const extra = process.argv.slice(process.argv.indexOf(unknownCmd) + 1);
5042
+ await handleDynamicNamespace(unknownCmd, extra);
5043
+ } else {
5044
+ showError(`Unknown command: ${unknownCmd}. Use 'tm help' for usage.`);
5045
+ process.exitCode = 1;
5046
+ }
5047
+ });
5048
+
4503
5049
  // Handle no args - show beautiful welcome screen
4504
5050
  if (process.argv.length <= 2) {
4505
5051
  (async () => {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "terminalmarket",
3
- "version": "0.12.0",
3
+ "version": "0.13.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"