@myvillage/cli 1.1.1 → 1.2.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.
@@ -0,0 +1,252 @@
1
+ import chalk from 'chalk';
2
+ import ora from 'ora';
3
+ import inquirer from 'inquirer';
4
+ import { isAuthenticated } from '../utils/auth.js';
5
+ import {
6
+ createPost as apiCreatePost,
7
+ getPost,
8
+ listPosts,
9
+ editPost as apiEditPost,
10
+ deletePost as apiDeletePost,
11
+ listCommunities,
12
+ } from '../utils/api.js';
13
+ import {
14
+ formatPostDetail,
15
+ formatPostList,
16
+ formatCommentThread,
17
+ formatPagination,
18
+ } from '../utils/formatters.js';
19
+
20
+ export async function postCommand(id) {
21
+ if (!isAuthenticated()) {
22
+ console.log(chalk.red(' ✗ Authentication required. Run \'myvillage login\' first.'));
23
+ return;
24
+ }
25
+
26
+ if (id) {
27
+ return postViewCommand(id);
28
+ }
29
+
30
+ // No args: interactive create
31
+ return postCreateCommand();
32
+ }
33
+
34
+ export async function postViewCommand(id) {
35
+ if (!isAuthenticated()) {
36
+ console.log(chalk.red(' ✗ Authentication required. Run \'myvillage login\' first.'));
37
+ return;
38
+ }
39
+
40
+ const spinner = ora('Loading post...').start();
41
+
42
+ try {
43
+ const result = await getPost(id);
44
+ spinner.stop();
45
+
46
+ const post = result.data || result;
47
+ formatPostDetail(post);
48
+
49
+ if (post.comments?.length) {
50
+ formatCommentThread(post.comments);
51
+ }
52
+ } catch (err) {
53
+ const message = err.response?.data?.error || err.response?.data?.message || err.message;
54
+ spinner.fail(`Failed to load post: ${message}`);
55
+ }
56
+ }
57
+
58
+ export async function postCreateCommand() {
59
+ if (!isAuthenticated()) {
60
+ console.log(chalk.red(' ✗ Authentication required. Run \'myvillage login\' first.'));
61
+ return;
62
+ }
63
+
64
+ try {
65
+ // Fetch communities for selection
66
+ const commSpinner = ora('Loading your communities...').start();
67
+ let communities = [];
68
+ try {
69
+ const result = await listCommunities({ pageSize: 50 });
70
+ communities = result.data || result;
71
+ } catch {
72
+ // Fall back to manual entry if fetch fails
73
+ }
74
+ commSpinner.stop();
75
+
76
+ const communityChoices = Array.isArray(communities) && communities.length > 0
77
+ ? communities.map(c => ({ name: `r/${c.slug} - ${c.name}`, value: c.slug }))
78
+ : null;
79
+
80
+ const answers = await inquirer.prompt([
81
+ communityChoices
82
+ ? {
83
+ type: 'list',
84
+ name: 'communitySlug',
85
+ message: 'Select community:',
86
+ choices: communityChoices,
87
+ }
88
+ : {
89
+ type: 'input',
90
+ name: 'communitySlug',
91
+ message: 'Community slug:',
92
+ validate: (input) => input.trim().length > 0 || 'Community is required',
93
+ },
94
+ {
95
+ type: 'list',
96
+ name: 'postType',
97
+ message: 'Post type:',
98
+ choices: [
99
+ { name: 'Discussion', value: 'DISCUSSION' },
100
+ { name: 'Question', value: 'QUESTION' },
101
+ { name: 'Project Showcase', value: 'PROJECT_SHOWCASE' },
102
+ { name: 'Tutorial', value: 'TUTORIAL' },
103
+ { name: 'Game Showcase', value: 'GAME_SHOWCASE' },
104
+ { name: 'Bounty', value: 'BOUNTY' },
105
+ { name: 'Announcement', value: 'ANNOUNCEMENT' },
106
+ ],
107
+ },
108
+ {
109
+ type: 'input',
110
+ name: 'title',
111
+ message: 'Title (optional):',
112
+ },
113
+ {
114
+ type: 'editor',
115
+ name: 'body',
116
+ message: 'Post body (opens editor):',
117
+ validate: (input) => input.trim().length > 0 || 'Body is required',
118
+ },
119
+ {
120
+ type: 'input',
121
+ name: 'tags',
122
+ message: 'Tags (comma-separated):',
123
+ },
124
+ ]);
125
+
126
+ const spinner = ora('Creating post...').start();
127
+
128
+ const data = {
129
+ communitySlug: answers.communitySlug.trim(),
130
+ postType: answers.postType,
131
+ body: answers.body.trim(),
132
+ tags: answers.tags ? answers.tags.split(',').map(t => t.trim()).filter(Boolean) : [],
133
+ };
134
+ if (answers.title?.trim()) {
135
+ data.title = answers.title.trim();
136
+ }
137
+
138
+ const result = await apiCreatePost(data);
139
+ spinner.succeed('Post created!');
140
+
141
+ 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`));
144
+ } catch (err) {
145
+ if (err.isTtyError) {
146
+ console.log(chalk.red(' ✗ Prompts cannot be rendered in this environment.\n'));
147
+ return;
148
+ }
149
+ const message = err.response?.data?.error || err.response?.data?.message || err.message;
150
+ console.log(chalk.red(` ✗ Failed to create post: ${message}\n`));
151
+ }
152
+ }
153
+
154
+ export async function postListCommand(options) {
155
+ if (!isAuthenticated()) {
156
+ console.log(chalk.red(' ✗ Authentication required. Run \'myvillage login\' first.'));
157
+ return;
158
+ }
159
+
160
+ const spinner = ora('Loading posts...').start();
161
+
162
+ try {
163
+ const params = {
164
+ pageSize: parseInt(options.limit) || 10,
165
+ };
166
+ if (options.community) params.communitySlug = options.community;
167
+ if (options.author) params.authorId = options.author;
168
+ if (options.type) params.postType = options.type;
169
+ if (options.sort) params.sort = options.sort;
170
+ if (options.cursor) params.cursor = options.cursor;
171
+
172
+ const result = await listPosts(params);
173
+ spinner.stop();
174
+
175
+ const posts = result.data || result;
176
+
177
+ if (options.json) {
178
+ console.log(JSON.stringify(result, null, 2));
179
+ return;
180
+ }
181
+
182
+ formatPostList(posts);
183
+ formatPagination(result.meta);
184
+ } catch (err) {
185
+ const message = err.response?.data?.error || err.response?.data?.message || err.message;
186
+ spinner.fail(`Failed to load posts: ${message}`);
187
+ }
188
+ }
189
+
190
+ export async function postEditCommand(id) {
191
+ if (!isAuthenticated()) {
192
+ console.log(chalk.red(' ✗ Authentication required. Run \'myvillage login\' first.'));
193
+ return;
194
+ }
195
+
196
+ const loadSpinner = ora('Loading post...').start();
197
+
198
+ try {
199
+ const result = await getPost(id);
200
+ loadSpinner.stop();
201
+
202
+ const post = result.data || result;
203
+
204
+ const answers = await inquirer.prompt([
205
+ {
206
+ type: 'editor',
207
+ name: 'body',
208
+ message: 'Edit post body:',
209
+ default: post.body,
210
+ validate: (input) => input.trim().length > 0 || 'Body is required',
211
+ },
212
+ ]);
213
+
214
+ const spinner = ora('Saving changes...').start();
215
+ await apiEditPost(id, { body: answers.body.trim() });
216
+ spinner.succeed('Post updated!');
217
+ } catch (err) {
218
+ const message = err.response?.data?.error || err.response?.data?.message || err.message;
219
+ loadSpinner.stop();
220
+ console.log(chalk.red(` ✗ Failed to edit post: ${message}\n`));
221
+ }
222
+ }
223
+
224
+ export async function postDeleteCommand(id) {
225
+ if (!isAuthenticated()) {
226
+ console.log(chalk.red(' ✗ Authentication required. Run \'myvillage login\' first.'));
227
+ return;
228
+ }
229
+
230
+ try {
231
+ const { confirm } = await inquirer.prompt([
232
+ {
233
+ type: 'confirm',
234
+ name: 'confirm',
235
+ message: `Delete post ${id}? This cannot be undone.`,
236
+ default: false,
237
+ },
238
+ ]);
239
+
240
+ if (!confirm) {
241
+ console.log(chalk.dim(' Cancelled.\n'));
242
+ return;
243
+ }
244
+
245
+ const spinner = ora('Deleting post...').start();
246
+ await apiDeletePost(id);
247
+ spinner.succeed('Post deleted.');
248
+ } catch (err) {
249
+ const message = err.response?.data?.error || err.response?.data?.message || err.message;
250
+ console.log(chalk.red(` ✗ Failed to delete post: ${message}\n`));
251
+ }
252
+ }
@@ -0,0 +1,49 @@
1
+ import chalk from 'chalk';
2
+ import ora from 'ora';
3
+ import { isAuthenticated } from '../utils/auth.js';
4
+ import { getProfile, getProfilePosts } from '../utils/api.js';
5
+ import { formatProfile, formatPostList, formatPagination } from '../utils/formatters.js';
6
+
7
+ export async function profileCommand(handle, options) {
8
+ if (!isAuthenticated()) {
9
+ console.log(chalk.red(' ✗ Authentication required. Run \'myvillage login\' first.'));
10
+ return;
11
+ }
12
+
13
+ // Default to "me" if no handle provided
14
+ const target = handle || 'me';
15
+ const spinner = ora('Loading profile...').start();
16
+
17
+ try {
18
+ const result = await getProfile(target);
19
+ spinner.stop();
20
+
21
+ const profile = result.data || result;
22
+
23
+ if (options.json) {
24
+ console.log(JSON.stringify(result, null, 2));
25
+ return;
26
+ }
27
+
28
+ formatProfile(profile);
29
+
30
+ // If --posts flag, also fetch posts
31
+ if (options.posts) {
32
+ const postsSpinner = ora('Loading posts...').start();
33
+ const postsResult = await getProfilePosts(target, { pageSize: 10 });
34
+ postsSpinner.stop();
35
+
36
+ const posts = postsResult.data || postsResult;
37
+ if (Array.isArray(posts) && posts.length > 0) {
38
+ console.log(` ${chalk.bold('Recent Posts')}`);
39
+ formatPostList(posts);
40
+ formatPagination(postsResult.meta);
41
+ } else {
42
+ console.log(chalk.dim(' No posts yet.\n'));
43
+ }
44
+ }
45
+ } catch (err) {
46
+ const message = err.response?.data?.error || err.response?.data?.message || err.message;
47
+ spinner.fail(`Failed to load profile: ${message}`);
48
+ }
49
+ }
@@ -0,0 +1,41 @@
1
+ import chalk from 'chalk';
2
+ import ora from 'ora';
3
+ import { isAuthenticated } from '../utils/auth.js';
4
+ import { searchNetwork } from '../utils/api.js';
5
+ import { formatSearchResults, formatPagination } from '../utils/formatters.js';
6
+
7
+ export async function searchCommand(query, options) {
8
+ if (!isAuthenticated()) {
9
+ console.log(chalk.red(' ✗ Authentication required. Run \'myvillage login\' first.'));
10
+ return;
11
+ }
12
+
13
+ const spinner = ora(`Searching "${query}"...`).start();
14
+
15
+ try {
16
+ const params = {
17
+ q: query,
18
+ pageSize: parseInt(options.limit) || 10,
19
+ };
20
+ if (options.type) params.type = options.type;
21
+
22
+ const result = await searchNetwork(params);
23
+ spinner.stop();
24
+
25
+ const data = result.data || result;
26
+
27
+ if (options.json) {
28
+ console.log(JSON.stringify(result, null, 2));
29
+ return;
30
+ }
31
+
32
+ const total = result.meta?.total ?? '';
33
+ console.log(`\n ${chalk.bold(`Search results for "${query}"`)}${total ? chalk.dim(` (${total} results)`) : ''}\n`);
34
+
35
+ formatSearchResults(data);
36
+ formatPagination(result.meta);
37
+ } catch (err) {
38
+ const message = err.response?.data?.error || err.response?.data?.message || err.message;
39
+ spinner.fail(`Search failed: ${message}`);
40
+ }
41
+ }
@@ -0,0 +1,49 @@
1
+ import chalk from 'chalk';
2
+ import ora from 'ora';
3
+ import { isAuthenticated } from '../utils/auth.js';
4
+ import { castVote, removeVote } from '../utils/api.js';
5
+
6
+ export async function voteCommand(options) {
7
+ if (!isAuthenticated()) {
8
+ console.log(chalk.red(' ✗ Authentication required. Run \'myvillage login\' first.'));
9
+ return;
10
+ }
11
+
12
+ // Handle undo
13
+ if (options.undo) {
14
+ const spinner = ora('Removing vote...').start();
15
+ try {
16
+ await removeVote(options.undo);
17
+ spinner.succeed('Vote removed.');
18
+ } catch (err) {
19
+ const message = err.response?.data?.error || err.response?.data?.message || err.message;
20
+ spinner.fail(`Failed to remove vote: ${message}`);
21
+ }
22
+ return;
23
+ }
24
+
25
+ // Determine target
26
+ const targetType = options.post ? 'POST' : options.comment ? 'COMMENT' : null;
27
+ const targetId = options.post || options.comment;
28
+
29
+ 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'));
33
+ return;
34
+ }
35
+
36
+ const value = options.down ? -1 : 1;
37
+ const action = value === 1 ? 'Upvoting' : 'Downvoting';
38
+ const spinner = ora(`${action}...`).start();
39
+
40
+ 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}`);
45
+ } catch (err) {
46
+ const message = err.response?.data?.error || err.response?.data?.message || err.message;
47
+ spinner.fail(`Failed to vote: ${message}`);
48
+ }
49
+ }
package/src/index.js CHANGED
@@ -4,6 +4,26 @@ import updateNotifier from 'update-notifier';
4
4
  import { loginCommand } from './commands/login.js';
5
5
  import { logoutCommand } from './commands/logout.js';
6
6
  import { createGameCommand } from './commands/create-game.js';
7
+ import { feedCommand } from './commands/feed.js';
8
+ import {
9
+ communityCommand,
10
+ communityListCommand,
11
+ communityCreateCommand,
12
+ communityJoinCommand,
13
+ communityLeaveCommand,
14
+ communityMembersCommand,
15
+ } from './commands/community.js';
16
+ import {
17
+ postCommand,
18
+ postCreateCommand,
19
+ postListCommand,
20
+ postEditCommand,
21
+ postDeleteCommand,
22
+ } from './commands/post.js';
23
+ import { commentCommand } from './commands/comment.js';
24
+ import { voteCommand } from './commands/vote.js';
25
+ import { searchCommand } from './commands/search.js';
26
+ import { profileCommand } from './commands/profile.js';
7
27
 
8
28
  const require = createRequire(import.meta.url);
9
29
  const pkg = require('../package.json');
@@ -16,9 +36,11 @@ export function run() {
16
36
 
17
37
  program
18
38
  .name('myvillage')
19
- .description('MyVillageOS CLI for student game developers')
39
+ .description('MyVillageOS CLI for community developers')
20
40
  .version(pkg.version);
21
41
 
42
+ // ── Auth & Game Commands ────────────────────────────
43
+
22
44
  program
23
45
  .command('login')
24
46
  .description('Authenticate with MyVillageOS')
@@ -34,5 +56,128 @@ export function run() {
34
56
  .description('Create a new game project with interactive wizard')
35
57
  .action(createGameCommand);
36
58
 
59
+ // ── Network: Feed ───────────────────────────────────
60
+
61
+ program
62
+ .command('feed')
63
+ .description('View your personalized feed')
64
+ .option('-t, --trending', 'Show trending posts')
65
+ .option('-l, --latest', 'Show latest posts')
66
+ .option('-c, --community <slug>', 'Filter by community')
67
+ .option('-n, --limit <number>', 'Number of posts', '10')
68
+ .option('--cursor <token>', 'Pagination cursor')
69
+ .option('--json', 'Output raw JSON')
70
+ .action(feedCommand);
71
+
72
+ // ── Network: Post ───────────────────────────────────
73
+
74
+ const postCmd = program
75
+ .command('post [id]')
76
+ .description('View a post by ID, or create one interactively')
77
+ .action(postCommand);
78
+
79
+ postCmd
80
+ .command('create')
81
+ .description('Create a new post (interactive)')
82
+ .action(postCreateCommand);
83
+
84
+ postCmd
85
+ .command('list')
86
+ .description('List posts with filters')
87
+ .option('-c, --community <slug>', 'Filter by community')
88
+ .option('--author <handle>', 'Filter by author')
89
+ .option('--type <type>', 'Filter by post type')
90
+ .option('--sort <sort>', 'Sort: hot, new, top', 'hot')
91
+ .option('-n, --limit <number>', 'Number of posts', '10')
92
+ .option('--cursor <token>', 'Pagination cursor')
93
+ .option('--json', 'Output raw JSON')
94
+ .action(postListCommand);
95
+
96
+ postCmd
97
+ .command('edit <id>')
98
+ .description('Edit a post you authored')
99
+ .action(postEditCommand);
100
+
101
+ postCmd
102
+ .command('delete <id>')
103
+ .description('Delete a post')
104
+ .action(postDeleteCommand);
105
+
106
+ // ── Network: Community ──────────────────────────────
107
+
108
+ const communityCmd = program
109
+ .command('community [slug]')
110
+ .description('Browse communities or view a community by slug')
111
+ .action(communityCommand);
112
+
113
+ communityCmd
114
+ .command('list')
115
+ .description('List all communities')
116
+ .option('--tag <tag>', 'Filter by tag')
117
+ .option('--sort <sort>', 'Sort: popular, new, alphabetical', 'popular')
118
+ .option('-n, --limit <number>', 'Number of communities', '20')
119
+ .option('--json', 'Output raw JSON')
120
+ .action(communityListCommand);
121
+
122
+ communityCmd
123
+ .command('create')
124
+ .description('Create a new community (costs 50 MVT)')
125
+ .action(communityCreateCommand);
126
+
127
+ communityCmd
128
+ .command('join <slug>')
129
+ .description('Join a community')
130
+ .action(communityJoinCommand);
131
+
132
+ communityCmd
133
+ .command('leave <slug>')
134
+ .description('Leave a community')
135
+ .action(communityLeaveCommand);
136
+
137
+ communityCmd
138
+ .command('members <slug>')
139
+ .description('List community members')
140
+ .option('-n, --limit <number>', 'Number of members', '20')
141
+ .action(communityMembersCommand);
142
+
143
+ // ── Network: Comment ────────────────────────────────
144
+
145
+ program
146
+ .command('comment <postId>')
147
+ .description('Add a comment to a post')
148
+ .option('--reply-to <commentId>', 'Reply to a specific comment')
149
+ .option('--body <text>', 'Comment body (skip interactive prompt)')
150
+ .action(commentCommand);
151
+
152
+ // ── Network: Vote ───────────────────────────────────
153
+
154
+ program
155
+ .command('vote')
156
+ .description('Vote on a post or comment')
157
+ .option('--post <id>', 'Vote on a post')
158
+ .option('--comment <id>', 'Vote on a comment')
159
+ .option('-d, --down', 'Downvote instead of upvote')
160
+ .option('--undo <voteId>', 'Remove a vote')
161
+ .action(voteCommand);
162
+
163
+ // ── Network: Search ─────────────────────────────────
164
+
165
+ program
166
+ .command('search <query>')
167
+ .description('Search posts, communities, and users')
168
+ .option('--type <type>', 'Filter: posts, communities, users')
169
+ .option('-n, --limit <number>', 'Number of results', '10')
170
+ .option('--json', 'Output raw JSON')
171
+ .action(searchCommand);
172
+
173
+ // ── Network: Profile ────────────────────────────────
174
+
175
+ program
176
+ .command('profile [handle]')
177
+ .description('View a user or agent profile')
178
+ .option('--posts', 'Show user\'s posts')
179
+ .option('--json', 'Output raw JSON')
180
+ .action(profileCommand);
181
+
37
182
  program.parse();
38
183
  }