@myvillage/cli 1.2.2 → 1.5.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.
@@ -2,6 +2,7 @@ import { createServer } from 'http';
2
2
  import { randomBytes, createHash } from 'crypto';
3
3
  import chalk from 'chalk';
4
4
  import ora from 'ora';
5
+ import { villageSpinner, brand } from '../utils/brand.js';
5
6
  import open from 'open';
6
7
  import axios from 'axios';
7
8
  import { getConfig } from '../utils/config.js';
@@ -27,11 +28,11 @@ export async function loginCommand() {
27
28
  const existing = loadCredentials();
28
29
  if (existing?.access_token) {
29
30
  console.log(chalk.yellow('You are already logged in.'));
30
- console.log(chalk.dim('Run "myvillage logout" first to log in with a different account.'));
31
+ console.log(brand.teal('Run "myvillage logout" first to log in with a different account.'));
31
32
  return;
32
33
  }
33
34
 
34
- const spinner = ora('Preparing authentication...').start();
35
+ const spinner = villageSpinner('Preparing authentication...').start();
35
36
 
36
37
  // Generate PKCE values and state token
37
38
  const state = randomBytes(16).toString('hex');
@@ -117,8 +118,8 @@ export async function loginCommand() {
117
118
  open(authUrl).catch(() => {
118
119
  spinner.stop();
119
120
  console.log(chalk.yellow('\nCould not open browser automatically.'));
120
- console.log(chalk.dim('Please open this URL in your browser:'));
121
- console.log(chalk.cyan(authUrl));
121
+ console.log(brand.teal('Please open this URL in your browser:'));
122
+ console.log(brand.gold(authUrl));
122
123
  });
123
124
 
124
125
  spinner.text = 'Waiting for authentication in browser...';
@@ -184,12 +185,12 @@ export async function loginCommand() {
184
185
 
185
186
  spinner.succeed('Authentication complete!');
186
187
  console.log();
187
- console.log(chalk.green(` \u2713 Successfully logged in as ${userInfo.email}`));
188
+ console.log(brand.green(` \u2713 Successfully logged in as ${userInfo.email}`));
188
189
 
189
190
  if (userInfo.villager) {
190
191
  const v = userInfo.villager;
191
192
  if (v.mvtBalance !== undefined) {
192
- console.log(chalk.dim(` MVT Balance: ${v.mvtBalance} tokens`));
193
+ console.log(brand.teal(` MVT Balance: ${v.mvtBalance} tokens`));
193
194
  }
194
195
  }
195
196
 
@@ -1,12 +1,13 @@
1
1
  import chalk from 'chalk';
2
+ import { brand } from '../utils/brand.js';
2
3
  import { clearCredentials } from '../utils/auth.js';
3
4
 
4
5
  export async function logoutCommand() {
5
6
  const removed = clearCredentials();
6
7
 
7
8
  if (removed) {
8
- console.log(chalk.green(' \u2713 Successfully logged out.'));
9
+ console.log(brand.green(' \u2713 Successfully logged out.'));
9
10
  } else {
10
- console.log(chalk.dim(' No stored credentials found. You are not logged in.'));
11
+ console.log(brand.teal(' No stored credentials found. You are not logged in.'));
11
12
  }
12
13
  }
@@ -1,5 +1,6 @@
1
1
  import chalk from 'chalk';
2
2
  import ora from 'ora';
3
+ import { villageSpinner, brand } from '../utils/brand.js';
3
4
  import inquirer from 'inquirer';
4
5
  import { isAuthenticated } from '../utils/auth.js';
5
6
  import {
@@ -9,6 +10,7 @@ import {
9
10
  editPost as apiEditPost,
10
11
  deletePost as apiDeletePost,
11
12
  listCommunities,
13
+ listMyAgents,
12
14
  } from '../utils/api.js';
13
15
  import {
14
16
  formatPostDetail,
@@ -37,7 +39,7 @@ export async function postViewCommand(id) {
37
39
  return;
38
40
  }
39
41
 
40
- const spinner = ora('Loading post...').start();
42
+ const spinner = villageSpinner('Loading post...').start();
41
43
 
42
44
  try {
43
45
  const result = await getPost(id);
@@ -55,7 +57,7 @@ export async function postViewCommand(id) {
55
57
  }
56
58
  }
57
59
 
58
- export async function postCreateCommand() {
60
+ export async function postCreateCommand(options = {}) {
59
61
  if (!isAuthenticated()) {
60
62
  console.log(chalk.red(' ✗ Authentication required. Run \'myvillage login\' first.'));
61
63
  return;
@@ -63,7 +65,7 @@ export async function postCreateCommand() {
63
65
 
64
66
  try {
65
67
  // Fetch communities for selection
66
- const commSpinner = ora('Loading your communities...').start();
68
+ const commSpinner = villageSpinner('Loading your communities...').start();
67
69
  let communities = [];
68
70
  try {
69
71
  const result = await listCommunities({ pageSize: 50 });
@@ -73,6 +75,53 @@ export async function postCreateCommand() {
73
75
  }
74
76
  commSpinner.stop();
75
77
 
78
+ // Resolve agent identity
79
+ let agentProfileId = null;
80
+
81
+ if (options.as) {
82
+ // --as flag provided: resolve handle to agent ID
83
+ const agentsSpinner = villageSpinner('Resolving agent...').start();
84
+ try {
85
+ const agentsResult = await listMyAgents();
86
+ const agents = agentsResult.data || agentsResult;
87
+ const agent = Array.isArray(agents) ? agents.find(a => a.handle === options.as) : null;
88
+ agentsSpinner.stop();
89
+
90
+ if (!agent) {
91
+ console.log(chalk.red(` ✗ Agent @${options.as} not found. Run 'myvillage agent' to see your agents.`));
92
+ return;
93
+ }
94
+ agentProfileId = agent.id;
95
+ console.log(brand.teal(` Posting as agent @${agent.handle}\n`));
96
+ } catch {
97
+ agentsSpinner.stop();
98
+ }
99
+ } else {
100
+ // No --as flag: check if user has agents and prompt
101
+ try {
102
+ const agentsResult = await listMyAgents();
103
+ const agents = agentsResult.data || agentsResult;
104
+
105
+ if (Array.isArray(agents) && agents.length > 0) {
106
+ const { postAs } = await inquirer.prompt([
107
+ {
108
+ type: 'list',
109
+ name: 'postAs',
110
+ message: 'Post as:',
111
+ choices: [
112
+ { name: 'Yourself', value: null },
113
+ ...agents.map(a => ({ name: `@${a.handle} - ${a.displayName}`, value: a.id })),
114
+ ],
115
+ },
116
+ ]);
117
+
118
+ agentProfileId = postAs;
119
+ }
120
+ } catch {
121
+ // If fetching agents fails, just proceed as the user
122
+ }
123
+ }
124
+
76
125
  const communityChoices = Array.isArray(communities) && communities.length > 0
77
126
  ? communities.map(c => ({ name: `r/${c.slug} - ${c.name}`, value: c.slug }))
78
127
  : null;
@@ -123,7 +172,7 @@ export async function postCreateCommand() {
123
172
  },
124
173
  ]);
125
174
 
126
- const spinner = ora('Creating post...').start();
175
+ const spinner = villageSpinner('Creating post...').start();
127
176
 
128
177
  const data = {
129
178
  communitySlug: answers.communitySlug.trim(),
@@ -134,13 +183,20 @@ export async function postCreateCommand() {
134
183
  if (answers.title?.trim()) {
135
184
  data.title = answers.title.trim();
136
185
  }
186
+ if (agentProfileId) {
187
+ data.agentProfileId = agentProfileId;
188
+ }
137
189
 
138
190
  const result = await apiCreatePost(data);
139
191
  spinner.succeed('Post created!');
140
192
 
141
193
  const post = result.data || result;
142
- console.log(chalk.green(` ✓ Post published in r/${answers.communitySlug}`));
143
- console.log(chalk.dim(` ID: ${post.id}\n`));
194
+ if (agentProfileId) {
195
+ console.log(brand.green(` Post published in r/${answers.communitySlug} as agent`));
196
+ } else {
197
+ console.log(brand.green(` ✓ Post published in r/${answers.communitySlug}`));
198
+ }
199
+ console.log(brand.teal(` ID: ${post.id}\n`));
144
200
  } catch (err) {
145
201
  if (err.isTtyError) {
146
202
  console.log(chalk.red(' ✗ Prompts cannot be rendered in this environment.\n'));
@@ -157,7 +213,7 @@ export async function postListCommand(options) {
157
213
  return;
158
214
  }
159
215
 
160
- const spinner = ora('Loading posts...').start();
216
+ const spinner = villageSpinner('Loading posts...').start();
161
217
 
162
218
  try {
163
219
  const params = {
@@ -193,7 +249,7 @@ export async function postEditCommand(id) {
193
249
  return;
194
250
  }
195
251
 
196
- const loadSpinner = ora('Loading post...').start();
252
+ const loadSpinner = villageSpinner('Loading post...').start();
197
253
 
198
254
  try {
199
255
  const result = await getPost(id);
@@ -211,7 +267,7 @@ export async function postEditCommand(id) {
211
267
  },
212
268
  ]);
213
269
 
214
- const spinner = ora('Saving changes...').start();
270
+ const spinner = villageSpinner('Saving changes...').start();
215
271
  await apiEditPost(id, { body: answers.body.trim() });
216
272
  spinner.succeed('Post updated!');
217
273
  } catch (err) {
@@ -238,11 +294,11 @@ export async function postDeleteCommand(id) {
238
294
  ]);
239
295
 
240
296
  if (!confirm) {
241
- console.log(chalk.dim(' Cancelled.\n'));
297
+ console.log(brand.teal(' Cancelled.\n'));
242
298
  return;
243
299
  }
244
300
 
245
- const spinner = ora('Deleting post...').start();
301
+ const spinner = villageSpinner('Deleting post...').start();
246
302
  await apiDeletePost(id);
247
303
  spinner.succeed('Post deleted.');
248
304
  } catch (err) {
@@ -1,5 +1,6 @@
1
1
  import chalk from 'chalk';
2
2
  import ora from 'ora';
3
+ import { villageSpinner, brand } from '../utils/brand.js';
3
4
  import { isAuthenticated, loadCredentials } from '../utils/auth.js';
4
5
  import { getProfile, getProfilePosts } from '../utils/api.js';
5
6
  import { formatProfile, formatPostList, formatPagination } from '../utils/formatters.js';
@@ -20,7 +21,7 @@ export async function profileCommand(handle, options) {
20
21
  return;
21
22
  }
22
23
  }
23
- const spinner = ora('Loading profile...').start();
24
+ const spinner = villageSpinner('Loading profile...').start();
24
25
 
25
26
  try {
26
27
  const result = await getProfile(target);
@@ -37,7 +38,7 @@ export async function profileCommand(handle, options) {
37
38
 
38
39
  // If --posts flag, also fetch posts
39
40
  if (options.posts) {
40
- const postsSpinner = ora('Loading posts...').start();
41
+ const postsSpinner = villageSpinner('Loading posts...').start();
41
42
  const postsResult = await getProfilePosts(target, { pageSize: 10 });
42
43
  postsSpinner.stop();
43
44
 
@@ -47,7 +48,7 @@ export async function profileCommand(handle, options) {
47
48
  formatPostList(posts);
48
49
  formatPagination(postsResult.meta);
49
50
  } else {
50
- console.log(chalk.dim(' No posts yet.\n'));
51
+ console.log(brand.teal(' No posts yet.\n'));
51
52
  }
52
53
  }
53
54
  } catch (err) {
@@ -1,5 +1,6 @@
1
1
  import chalk from 'chalk';
2
2
  import ora from 'ora';
3
+ import { villageSpinner, brand } from '../utils/brand.js';
3
4
  import { isAuthenticated } from '../utils/auth.js';
4
5
  import { searchNetwork } from '../utils/api.js';
5
6
  import { formatSearchResults, formatPagination } from '../utils/formatters.js';
@@ -10,7 +11,7 @@ export async function searchCommand(query, options) {
10
11
  return;
11
12
  }
12
13
 
13
- const spinner = ora(`Searching "${query}"...`).start();
14
+ const spinner = villageSpinner(`Searching "${query}"...`).start();
14
15
 
15
16
  try {
16
17
  const params = {
@@ -30,7 +31,7 @@ export async function searchCommand(query, options) {
30
31
  }
31
32
 
32
33
  const total = result.meta?.total ?? '';
33
- console.log(`\n ${chalk.bold(`Search results for "${query}"`)}${total ? chalk.dim(` (${total} results)`) : ''}\n`);
34
+ console.log(`\n ${chalk.bold(`Search results for "${query}"`)}${total ? brand.teal(` (${total} results)`) : ''}\n`);
34
35
 
35
36
  formatSearchResults(data);
36
37
  formatPagination(result.meta);
@@ -2,8 +2,9 @@ import { existsSync, readFileSync } from 'fs';
2
2
  import { join, resolve } from 'path';
3
3
  import chalk from 'chalk';
4
4
  import ora from 'ora';
5
+ import { villageSpinner, brand } from '../utils/brand.js';
5
6
  import { isAuthenticated } from '../utils/auth.js';
6
- import { getMyGames, getGameStats } from '../utils/api.js';
7
+ import { getMyGames, getGameStatus } from '../utils/api.js';
7
8
 
8
9
  function readPackageJson(dir) {
9
10
  const pkgPath = join(dir, 'package.json');
@@ -48,33 +49,49 @@ export async function statusCommand() {
48
49
  }
49
50
 
50
51
  async function showGameStatus(gameId) {
51
- const spinner = ora('Fetching game status...').start();
52
+ const spinner = villageSpinner('Fetching game status...').start();
52
53
 
53
54
  try {
54
- const stats = await getGameStats(gameId);
55
+ const data = await getGameStatus(gameId);
56
+ const game = data.game || data;
55
57
 
56
58
  spinner.stop();
57
59
 
58
60
  console.log();
59
- console.log(chalk.bold(` ${stats.name || gameId}`));
61
+ console.log(chalk.bold(` ${game.title || game.name || gameId}`));
60
62
  console.log();
61
- console.log(` Status: ${formatStatus(stats.status)}`);
62
- console.log(` Play Count: ${chalk.cyan(stats.playCount ?? 0)}`);
63
- console.log(` MVT Earned: ${chalk.yellow(stats.mvtEarned ?? 0)} tokens`);
64
- console.log(` Last Updated: ${chalk.dim(formatDate(stats.lastUpdated))}`);
63
+ console.log(` Status: ${formatStatus(game.status)}`);
64
+ console.log(` Play Count: ${brand.gold(game.playCount ?? 0)}`);
65
+ console.log(` MVT Earned: ${chalk.yellow(game.mvtAwarded ?? 0)} tokens`);
66
+ console.log(` Last Updated: ${brand.teal(formatDate(game.updatedAt || game.lastUpdated))}`);
65
67
 
66
- if (stats.url) {
67
- console.log(` URL: ${chalk.cyan(stats.url)}`);
68
+ if (game.slug) {
69
+ console.log(` Slug: ${brand.teal(game.slug)}`);
68
70
  }
71
+
72
+ // Show review history if available
73
+ if (game.reviews?.length > 0) {
74
+ console.log();
75
+ console.log(chalk.bold(' Review History:'));
76
+ for (const review of game.reviews) {
77
+ const action = formatReviewAction(review.action);
78
+ const date = formatDate(review.createdAt);
79
+ console.log(` ${action} - ${brand.teal(date)}`);
80
+ if (review.notes) {
81
+ console.log(` ${brand.teal(review.notes)}`);
82
+ }
83
+ }
84
+ }
85
+
69
86
  console.log();
70
87
  } catch (err) {
71
- const message = err.response?.data?.message || err.message;
88
+ const message = err.response?.data?.error || err.response?.data?.message || err.message;
72
89
  spinner.fail(`Failed to fetch game status: ${message}`);
73
90
  }
74
91
  }
75
92
 
76
93
  async function showAllGames() {
77
- const spinner = ora('Fetching your games...').start();
94
+ const spinner = villageSpinner('Fetching your games...').start();
78
95
 
79
96
  try {
80
97
  const data = await getMyGames();
@@ -84,8 +101,8 @@ async function showAllGames() {
84
101
 
85
102
  if (!games.length) {
86
103
  console.log();
87
- console.log(chalk.dim(' No deployed games found.'));
88
- console.log(chalk.dim(' Create a game with "myvillage create-game" and deploy it!'));
104
+ console.log(brand.teal(' No deployed games found.'));
105
+ console.log(brand.teal(' Create a game with "myvillage create-game" and deploy it!'));
89
106
  console.log();
90
107
  return;
91
108
  }
@@ -95,32 +112,51 @@ async function showAllGames() {
95
112
  console.log();
96
113
 
97
114
  for (const game of games) {
98
- console.log(` ${chalk.yellow(game.name || game.gameId)}`);
99
- console.log(` Status: ${formatStatus(game.status)} | Plays: ${chalk.cyan(game.playCount ?? 0)} | MVT: ${chalk.yellow(game.mvtEarned ?? 0)}`);
115
+ console.log(` ${chalk.yellow(game.title || game.name || game.id)}`);
116
+ console.log(` Status: ${formatStatus(game.status)} | Plays: ${brand.gold(game.playCount ?? 0)} | MVT: ${chalk.yellow(game.mvtAwarded ?? 0)}`);
100
117
 
101
- if (game.url) {
102
- console.log(` ${chalk.dim(game.url)}`);
118
+ if (game.slug) {
119
+ console.log(` ${brand.teal(game.slug)}`);
103
120
  }
104
121
 
105
122
  console.log();
106
123
  }
107
124
  } catch (err) {
108
- const message = err.response?.data?.message || err.message;
125
+ const message = err.response?.data?.error || err.response?.data?.message || err.message;
109
126
  spinner.fail(`Failed to fetch games: ${message}`);
110
127
  }
111
128
  }
112
129
 
113
130
  function formatStatus(status) {
114
131
  switch (status) {
115
- case 'live':
116
- return chalk.green('Live');
117
- case 'building':
118
- return chalk.yellow('Building');
119
- case 'failed':
120
- return chalk.red('Failed');
121
- case 'pending':
122
- return chalk.dim('Pending');
132
+ case 'DRAFT':
133
+ return brand.teal('Draft');
134
+ case 'SUBMITTED':
135
+ return chalk.yellow('Submitted');
136
+ case 'UNDER_REVIEW':
137
+ return chalk.yellow('Under Review');
138
+ case 'APPROVED':
139
+ return chalk.green('Approved');
140
+ case 'REJECTED':
141
+ return chalk.red('Rejected');
142
+ case 'PUBLISHED':
143
+ return chalk.green('Published');
144
+ case 'ARCHIVED':
145
+ return brand.teal('Archived');
146
+ default:
147
+ return brand.teal(status || 'Unknown');
148
+ }
149
+ }
150
+
151
+ function formatReviewAction(action) {
152
+ switch (action) {
153
+ case 'APPROVE':
154
+ return chalk.green('Approved');
155
+ case 'REJECT':
156
+ return chalk.red('Rejected');
157
+ case 'REQUEST_CHANGES':
158
+ return chalk.yellow('Changes Requested');
123
159
  default:
124
- return chalk.dim(status || 'Unknown');
160
+ return brand.teal(action || 'Unknown');
125
161
  }
126
162
  }
@@ -1,25 +1,35 @@
1
1
  import chalk from 'chalk';
2
2
  import ora from 'ora';
3
+ import { villageSpinner, brand } from '../utils/brand.js';
3
4
  import { isAuthenticated } from '../utils/auth.js';
4
- import { castVote, removeVote } from '../utils/api.js';
5
+ import { castVote, listMyAgents } from '../utils/api.js';
5
6
 
6
7
  export async function voteCommand(options) {
7
8
  if (!isAuthenticated()) {
8
- console.log(chalk.red(' Authentication required. Run \'myvillage login\' first.'));
9
+ console.log(chalk.red(' \u2717 Authentication required. Run \'myvillage login\' first.'));
9
10
  return;
10
11
  }
11
12
 
12
- // Handle undo
13
- if (options.undo) {
14
- const spinner = ora('Removing vote...').start();
13
+ // Resolve agent identity if --as flag provided
14
+ let agentProfileId = null;
15
+
16
+ if (options.as) {
15
17
  try {
16
- await removeVote(options.undo);
17
- spinner.succeed('Vote removed.');
18
+ const agentsResult = await listMyAgents();
19
+ const agents = agentsResult.data || agentsResult;
20
+ const agent = Array.isArray(agents) ? agents.find(a => a.handle === options.as) : null;
21
+
22
+ if (!agent) {
23
+ console.log(chalk.red(` \u2717 Agent @${options.as} not found. Run 'myvillage agent' to see your agents.\n`));
24
+ return;
25
+ }
26
+ agentProfileId = agent.id;
27
+ console.log(brand.teal(` Voting as agent @${agent.handle}\n`));
18
28
  } catch (err) {
19
29
  const message = err.response?.data?.error || err.response?.data?.message || err.message;
20
- spinner.fail(`Failed to remove vote: ${message}`);
30
+ console.log(chalk.red(` \u2717 Failed to resolve agent: ${message}\n`));
31
+ return;
21
32
  }
22
- return;
23
33
  }
24
34
 
25
35
  // Determine target
@@ -27,21 +37,39 @@ export async function voteCommand(options) {
27
37
  const targetId = options.post || options.comment;
28
38
 
29
39
  if (!targetType || !targetId) {
30
- console.log(chalk.red(' Specify a target: --post <id> or --comment <id>'));
31
- console.log(chalk.dim(' Example: myvillage vote --post abc123'));
32
- console.log(chalk.dim(' Example: myvillage vote --comment xyz789 --down\n'));
40
+ console.log(chalk.red(' \u2717 Specify a target: --post <id> or --comment <id>'));
41
+ console.log(brand.teal(' Example: myvillage vote --post abc123'));
42
+ console.log(brand.teal(' Example: myvillage vote --comment xyz789 --down\n'));
33
43
  return;
34
44
  }
35
45
 
36
46
  const value = options.down ? -1 : 1;
37
- const action = value === 1 ? 'Upvoting' : 'Downvoting';
38
- const spinner = ora(`${action}...`).start();
47
+ const action = options.undo ? 'Removing vote from' : (value === 1 ? 'Upvoting' : 'Downvoting');
48
+ const spinner = villageSpinner(`${action} ${targetType.toLowerCase()}...`).start();
39
49
 
40
50
  try {
41
- await castVote({ targetType, targetId, value });
42
- const emoji = value === 1 ? chalk.green('▲') : chalk.red('▼');
43
- const word = value === 1 ? 'Upvoted' : 'Downvoted';
44
- spinner.succeed(`${word} ${targetType.toLowerCase()} ${targetId} ${emoji}`);
51
+ const data = { targetType, targetId, value };
52
+ if (agentProfileId) {
53
+ data.agentProfileId = agentProfileId;
54
+ }
55
+ const result = await castVote(data);
56
+ const resultAction = result?.data?.action;
57
+
58
+ if (options.undo) {
59
+ if (resultAction === 'removed') {
60
+ spinner.succeed(`Vote removed from ${targetType.toLowerCase()} ${targetId}`);
61
+ } else {
62
+ spinner.warn(`No existing vote to remove (${resultAction || 'created'}). A new vote was cast instead.`);
63
+ }
64
+ } else {
65
+ const emoji = value === 1 ? chalk.green('\u25b2') : chalk.red('\u25bc');
66
+ if (resultAction === 'removed') {
67
+ spinner.succeed(`Vote toggled off for ${targetType.toLowerCase()} ${targetId}`);
68
+ } else {
69
+ const word = value === 1 ? 'Upvoted' : 'Downvoted';
70
+ spinner.succeed(`${word} ${targetType.toLowerCase()} ${targetId} ${emoji}`);
71
+ }
72
+ }
45
73
  } catch (err) {
46
74
  const message = err.response?.data?.error || err.response?.data?.message || err.message;
47
75
  spinner.fail(`Failed to vote: ${message}`);