@meltstudio/meltctl 4.18.0 → 4.20.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 CHANGED
@@ -31,6 +31,13 @@ meltctl project templates
31
31
  # Submit daily standup report
32
32
  meltctl standup
33
33
 
34
+ # Send feedback and coins to a teammate
35
+ meltctl feedback
36
+
37
+ # Check your coin balance or team leaderboard
38
+ meltctl coins
39
+ meltctl coins --leaderboard
40
+
34
41
  # Other commands
35
42
  meltctl logout
36
43
  meltctl version --check
@@ -0,0 +1,5 @@
1
+ interface CoinsOptions {
2
+ leaderboard?: boolean;
3
+ }
4
+ export declare function coinsCommand(options: CoinsOptions): Promise<void>;
5
+ export {};
@@ -0,0 +1,49 @@
1
+ import chalk from 'chalk';
2
+ import { isAuthenticated, authenticatedFetch } from '../utils/auth.js';
3
+ export async function coinsCommand(options) {
4
+ if (!(await isAuthenticated())) {
5
+ console.error(chalk.red('Not authenticated. Run `npx @meltstudio/meltctl@latest login` first.'));
6
+ process.exit(1);
7
+ }
8
+ if (options.leaderboard) {
9
+ await showLeaderboard();
10
+ }
11
+ else {
12
+ await showBalance();
13
+ }
14
+ }
15
+ async function showBalance() {
16
+ const res = await authenticatedFetch('/coins');
17
+ if (!res.ok) {
18
+ const body = (await res.json());
19
+ console.error(chalk.red(`Failed to fetch coins: ${body.error ?? res.statusText}`));
20
+ process.exit(1);
21
+ }
22
+ const data = (await res.json());
23
+ console.log(chalk.bold.cyan('\n Your Coins (last 28 days)'));
24
+ console.log(` ${chalk.bold(String(data.coins))} coin${data.coins !== 1 ? 's' : ''} received\n`);
25
+ }
26
+ async function showLeaderboard() {
27
+ const res = await authenticatedFetch('/coins/leaderboard');
28
+ if (!res.ok) {
29
+ const body = (await res.json());
30
+ console.error(chalk.red(`Failed to fetch leaderboard: ${body.error ?? res.statusText}`));
31
+ process.exit(1);
32
+ }
33
+ const entries = (await res.json());
34
+ if (entries.length === 0) {
35
+ console.log(chalk.yellow('\n No coins have been sent in the last 28 days.\n'));
36
+ return;
37
+ }
38
+ console.log(chalk.bold.cyan('\n Leaderboard (last 28 days)\n'));
39
+ // Calculate column widths
40
+ const maxNameLen = Math.max(...entries.map(e => e.name.length), 4);
41
+ console.log(chalk.dim(` ${'#'.padEnd(4)} ${'Name'.padEnd(maxNameLen)} Coins`));
42
+ entries.forEach((entry, i) => {
43
+ const rank = String(i + 1).padEnd(4);
44
+ const name = entry.name.padEnd(maxNameLen);
45
+ const coins = String(entry.coins);
46
+ console.log(` ${rank} ${name} ${coins}`);
47
+ });
48
+ console.log();
49
+ }
@@ -0,0 +1,7 @@
1
+ interface FeedbackOptions {
2
+ to?: string;
3
+ coins?: string;
4
+ description?: string;
5
+ }
6
+ export declare function feedbackCommand(options: FeedbackOptions): Promise<void>;
7
+ export {};
@@ -0,0 +1,99 @@
1
+ import chalk from 'chalk';
2
+ import { input, select, editor } from '@inquirer/prompts';
3
+ import { isAuthenticated, authenticatedFetch } from '../utils/auth.js';
4
+ const EDITOR_HINT = chalk.dim('(type \\\\e to open your editor)');
5
+ async function promptDescription() {
6
+ const value = await input({
7
+ message: `Write your feedback (50–500 chars): ${EDITOR_HINT}`,
8
+ });
9
+ if (value === '\\e') {
10
+ try {
11
+ const editorValue = await editor({ message: 'Write your feedback:' });
12
+ return validateDescription(editorValue);
13
+ }
14
+ catch {
15
+ console.log(chalk.yellow('Editor failed to open. Falling back to inline input.'));
16
+ return promptDescription();
17
+ }
18
+ }
19
+ return validateDescription(value);
20
+ }
21
+ function validateDescription(value) {
22
+ const trimmed = value.trim();
23
+ if (trimmed.length < 50) {
24
+ console.log(chalk.yellow(`Too short (${trimmed.length}/50 min). Please write more.`));
25
+ return promptDescription();
26
+ }
27
+ if (trimmed.length > 500) {
28
+ console.log(chalk.yellow(`Too long (${trimmed.length}/500 max). Please shorten it.`));
29
+ return promptDescription();
30
+ }
31
+ return trimmed;
32
+ }
33
+ export async function feedbackCommand(options) {
34
+ if (!(await isAuthenticated())) {
35
+ console.error(chalk.red('Not authenticated. Run `npx @meltstudio/meltctl@latest login` first.'));
36
+ process.exit(1);
37
+ }
38
+ let toUserId;
39
+ let coins;
40
+ let description;
41
+ if (options.to && options.coins && options.description) {
42
+ // Hidden flags for E2E testing
43
+ toUserId = parseInt(options.to, 10);
44
+ coins = parseInt(options.coins, 10);
45
+ description = options.description;
46
+ }
47
+ else {
48
+ console.log(chalk.bold.cyan('\n Send Feedback\n'));
49
+ // Fetch recipients
50
+ let recipients;
51
+ try {
52
+ const res = await authenticatedFetch('/feedback/recipients');
53
+ if (!res.ok) {
54
+ const body = (await res.json());
55
+ console.error(chalk.red(`Failed to load recipients: ${body.error ?? res.statusText}`));
56
+ process.exit(1);
57
+ }
58
+ recipients = (await res.json());
59
+ }
60
+ catch {
61
+ console.error(chalk.red('Failed to connect to the server.'));
62
+ process.exit(1);
63
+ }
64
+ if (recipients.length === 0) {
65
+ console.log(chalk.yellow('No recipients available.'));
66
+ return;
67
+ }
68
+ const selectedRecipient = await select({
69
+ message: 'Who would you like to recognize?',
70
+ choices: recipients.map(r => ({
71
+ name: `${r.firstName} ${r.lastName} (${r.email})`,
72
+ value: r.id,
73
+ })),
74
+ });
75
+ toUserId = selectedRecipient;
76
+ coins = await select({
77
+ message: 'How many coins?',
78
+ choices: [
79
+ { name: '1 coin', value: 1 },
80
+ { name: '2 coins', value: 2 },
81
+ { name: '3 coins', value: 3 },
82
+ ],
83
+ });
84
+ description = await promptDescription();
85
+ }
86
+ const res = await authenticatedFetch('/feedback', {
87
+ method: 'POST',
88
+ headers: { 'Content-Type': 'application/json' },
89
+ body: JSON.stringify({ toUserId, coins, description }),
90
+ });
91
+ if (res.ok) {
92
+ console.log(chalk.green(`\n ✓ Feedback sent! You gave ${coins} coin${coins > 1 ? 's' : ''}.\n`));
93
+ }
94
+ else {
95
+ const body = (await res.json());
96
+ console.error(chalk.red(`\nFailed to send feedback: ${body.error ?? res.statusText}`));
97
+ process.exit(1);
98
+ }
99
+ }
package/dist/index.js CHANGED
@@ -12,6 +12,8 @@ import { printBanner } from './utils/banner.js';
12
12
  import { checkAndEnforceUpdate } from './utils/version-check.js';
13
13
  import { versionCheckCommand } from './commands/version.js';
14
14
  import { standupCommand } from './commands/standup.js';
15
+ import { feedbackCommand } from './commands/feedback.js';
16
+ import { coinsCommand } from './commands/coins.js';
15
17
  // Read version from package.json
16
18
  const __filename = fileURLToPath(import.meta.url);
17
19
  const __dirname = dirname(__filename);
@@ -65,6 +67,22 @@ program
65
67
  .action(async (options) => {
66
68
  await standupCommand(options);
67
69
  });
70
+ program
71
+ .command('feedback')
72
+ .description('send feedback and coins to a teammate')
73
+ .addOption(new Option('--to <userId>').hideHelp())
74
+ .addOption(new Option('--coins <amount>').hideHelp())
75
+ .addOption(new Option('--description <text>').hideHelp())
76
+ .action(async (options) => {
77
+ await feedbackCommand(options);
78
+ });
79
+ program
80
+ .command('coins')
81
+ .description('check your coin balance or the team leaderboard')
82
+ .option('--leaderboard', 'show the team leaderboard')
83
+ .action(async (options) => {
84
+ await coinsCommand(options);
85
+ });
68
86
  program
69
87
  .command('version')
70
88
  .description('show current version')
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@meltstudio/meltctl",
3
- "version": "4.18.0",
3
+ "version": "4.20.0",
4
4
  "description": "AI-first development tools for teams - set up AGENTS.md, Claude Code, Cursor, and Copilot standards",
5
5
  "main": "dist/index.js",
6
6
  "type": "module",