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.
- package/bin/tm.js +547 -1
- 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 () => {
|