@myvillage/cli 1.3.0 → 1.5.1
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 +14 -199
- package/package.json +11 -6
- package/src/agent-runtime/context.js +99 -0
- package/src/agent-runtime/daemon-entry.js +66 -0
- package/src/agent-runtime/daemon.js +65 -0
- package/src/agent-runtime/loop.js +281 -0
- package/src/agent-runtime/mcp-client.js +93 -0
- package/src/agent-runtime/scheduler.js +53 -0
- package/src/commands/agent-local.js +624 -0
- package/src/commands/agent.js +274 -42
- package/src/commands/bizreqs.js +965 -0
- package/src/commands/comment.js +5 -4
- package/src/commands/community.js +13 -12
- package/src/commands/create-app.js +253 -0
- package/src/commands/create-game.js +9 -8
- package/src/commands/deploy.js +101 -23
- package/src/commands/feed.js +4 -3
- package/src/commands/login.js +164 -76
- package/src/commands/logout.js +45 -7
- package/src/commands/post.js +14 -13
- package/src/commands/profile.js +4 -3
- package/src/commands/search.js +3 -2
- package/src/commands/soulprint.js +1379 -0
- package/src/commands/status.js +64 -28
- package/src/commands/vote.js +46 -18
- package/src/index.js +244 -1
- package/src/utils/agent-scaffolder.js +165 -0
- package/src/utils/api.js +135 -14
- package/src/utils/app-templates.js +2983 -0
- package/src/utils/brand.js +107 -0
- package/src/utils/config.js +17 -1
- package/src/utils/formatters.js +351 -18
- package/src/utils/local-agent.js +168 -0
- package/src/utils/soulprint-api.js +136 -0
- package/src/utils/soulprint-workspace.js +158 -0
package/src/commands/login.js
CHANGED
|
@@ -1,7 +1,9 @@
|
|
|
1
1
|
import { createServer } from 'http';
|
|
2
|
+
import { createInterface } from 'readline';
|
|
2
3
|
import { randomBytes, createHash } from 'crypto';
|
|
3
4
|
import chalk from 'chalk';
|
|
4
5
|
import ora from 'ora';
|
|
6
|
+
import { villageSpinner, brand } from '../utils/brand.js';
|
|
5
7
|
import open from 'open';
|
|
6
8
|
import axios from 'axios';
|
|
7
9
|
import { getConfig } from '../utils/config.js';
|
|
@@ -20,18 +22,47 @@ function generateCodeChallenge(verifier) {
|
|
|
20
22
|
.digest('base64url');
|
|
21
23
|
}
|
|
22
24
|
|
|
23
|
-
|
|
25
|
+
// Detect headless environments where no browser is available
|
|
26
|
+
function isHeadless() {
|
|
27
|
+
// No graphical display on Linux
|
|
28
|
+
if (process.platform === 'linux' && !process.env.DISPLAY && !process.env.WAYLAND_DISPLAY) {
|
|
29
|
+
return true;
|
|
30
|
+
}
|
|
31
|
+
// Running inside SSH session
|
|
32
|
+
if (process.env.SSH_CLIENT || process.env.SSH_TTY || process.env.SSH_CONNECTION) {
|
|
33
|
+
return true;
|
|
34
|
+
}
|
|
35
|
+
// Running inside a Docker container or CI
|
|
36
|
+
if (process.env.container || process.env.CI) {
|
|
37
|
+
return true;
|
|
38
|
+
}
|
|
39
|
+
return false;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
// Prompt the user to paste a URL from their browser
|
|
43
|
+
function promptForCallbackUrl() {
|
|
44
|
+
return new Promise((resolve) => {
|
|
45
|
+
const rl = createInterface({ input: process.stdin, output: process.stdout });
|
|
46
|
+
rl.question(chalk.cyan('\nPaste the URL here: '), (answer) => {
|
|
47
|
+
rl.close();
|
|
48
|
+
resolve(answer.trim());
|
|
49
|
+
});
|
|
50
|
+
});
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
export async function loginCommand(options) {
|
|
24
54
|
const config = getConfig();
|
|
55
|
+
const headless = options.browser === false || isHeadless();
|
|
25
56
|
|
|
26
57
|
// Check if already logged in
|
|
27
58
|
const existing = loadCredentials();
|
|
28
59
|
if (existing?.access_token) {
|
|
29
60
|
console.log(chalk.yellow('You are already logged in.'));
|
|
30
|
-
console.log(
|
|
61
|
+
console.log(brand.teal('Run "myvillage logout" first to log in with a different account.'));
|
|
31
62
|
return;
|
|
32
63
|
}
|
|
33
64
|
|
|
34
|
-
const spinner =
|
|
65
|
+
const spinner = villageSpinner('Preparing authentication...').start();
|
|
35
66
|
|
|
36
67
|
// Generate PKCE values and state token
|
|
37
68
|
const state = randomBytes(16).toString('hex');
|
|
@@ -51,96 +82,153 @@ export async function loginCommand() {
|
|
|
51
82
|
|
|
52
83
|
const authUrl = `${config.oauthBaseUrl}/authorize?${params.toString()}`;
|
|
53
84
|
|
|
54
|
-
|
|
55
|
-
const authResult = await new Promise((resolve, reject) => {
|
|
56
|
-
let timeout;
|
|
57
|
-
const server = createServer(async (req, res) => {
|
|
58
|
-
const url = new URL(req.url, `http://localhost:${config.callbackPort}`);
|
|
85
|
+
let authResult;
|
|
59
86
|
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
87
|
+
if (headless) {
|
|
88
|
+
// ── Headless / manual flow ──────────────────────────
|
|
89
|
+
// No local server — the user authenticates in a browser elsewhere,
|
|
90
|
+
// then pastes the redirect URL (which their browser can't reach) back here.
|
|
91
|
+
spinner.stop();
|
|
92
|
+
console.log();
|
|
93
|
+
console.log(brand.gold('No browser detected — using manual authentication.'));
|
|
94
|
+
console.log();
|
|
95
|
+
console.log(brand.teal('1. Open this URL in any browser:'));
|
|
96
|
+
console.log();
|
|
97
|
+
console.log(` ${chalk.underline(authUrl)}`);
|
|
98
|
+
console.log();
|
|
99
|
+
console.log(brand.teal('2. Log in and authorize the CLI.'));
|
|
100
|
+
console.log(brand.teal('3. Your browser will redirect to a localhost URL that won\'t load — this is expected.'));
|
|
101
|
+
console.log(brand.teal('4. Copy the full URL from your browser\'s address bar and paste it below.'));
|
|
102
|
+
console.log(brand.teal(' It will look like: http://localhost:3737/callback?code=...&state=...'));
|
|
65
103
|
|
|
66
|
-
|
|
67
|
-
const returnedState = url.searchParams.get('state');
|
|
68
|
-
const error = url.searchParams.get('error');
|
|
69
|
-
const errorDescription = url.searchParams.get('error_description');
|
|
104
|
+
const callbackInput = await promptForCallbackUrl();
|
|
70
105
|
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
<html><body style="font-family: sans-serif; display: flex; align-items: center; justify-content: center; height: 100vh; background: #302017; color: #E4DCCB;">
|
|
76
|
-
<div style="text-align: center;">
|
|
77
|
-
<h1 style="color: #FF6B6B;">Authentication Failed</h1>
|
|
78
|
-
<p>${errorDescription || error}</p>
|
|
79
|
-
<p>You can close this window.</p>
|
|
80
|
-
</div>
|
|
81
|
-
</body></html>
|
|
82
|
-
`);
|
|
83
|
-
} else {
|
|
84
|
-
res.end(`
|
|
85
|
-
<html><body style="font-family: sans-serif; display: flex; align-items: center; justify-content: center; height: 100vh; background: #302017; color: #E4DCCB;">
|
|
86
|
-
<div style="text-align: center;">
|
|
87
|
-
<h1 style="color: #FFD700;">Success!</h1>
|
|
88
|
-
<p>You are now logged in to MyVillageOS CLI.</p>
|
|
89
|
-
<p>You can close this window and return to your terminal.</p>
|
|
90
|
-
</div>
|
|
91
|
-
</body></html>
|
|
92
|
-
`);
|
|
93
|
-
}
|
|
106
|
+
if (!callbackInput) {
|
|
107
|
+
console.log(chalk.red('No URL provided. Authentication cancelled.'));
|
|
108
|
+
return;
|
|
109
|
+
}
|
|
94
110
|
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
111
|
+
try {
|
|
112
|
+
const callbackUrl = new URL(callbackInput);
|
|
113
|
+
const code = callbackUrl.searchParams.get('code');
|
|
114
|
+
const returnedState = callbackUrl.searchParams.get('state');
|
|
115
|
+
const error = callbackUrl.searchParams.get('error');
|
|
116
|
+
const errorDescription = callbackUrl.searchParams.get('error_description');
|
|
98
117
|
|
|
99
118
|
if (error) {
|
|
100
|
-
|
|
119
|
+
console.log(chalk.red(`Authentication failed: ${errorDescription || error}`));
|
|
120
|
+
return;
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
if (!code) {
|
|
124
|
+
console.log(chalk.red('No authorization code found in the URL. Please try again.'));
|
|
101
125
|
return;
|
|
102
126
|
}
|
|
103
127
|
|
|
104
|
-
// Validate state parameter
|
|
105
128
|
if (returnedState !== state) {
|
|
106
|
-
|
|
129
|
+
console.log(chalk.red('State mismatch — possible CSRF attack. Please try again.'));
|
|
107
130
|
return;
|
|
108
131
|
}
|
|
109
132
|
|
|
110
|
-
|
|
111
|
-
}
|
|
133
|
+
authResult = code;
|
|
134
|
+
} catch {
|
|
135
|
+
console.log(chalk.red('Invalid URL. Please copy the full URL from your browser\'s address bar.'));
|
|
136
|
+
return;
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
spinner.start('Exchanging authorization code for tokens...');
|
|
140
|
+
} else {
|
|
141
|
+
// ── Browser flow (original) ─────────────────────────
|
|
142
|
+
authResult = await new Promise((resolve, reject) => {
|
|
143
|
+
let timeout;
|
|
144
|
+
const server = createServer(async (req, res) => {
|
|
145
|
+
const url = new URL(req.url, `http://localhost:${config.callbackPort}`);
|
|
146
|
+
|
|
147
|
+
if (url.pathname !== '/callback') {
|
|
148
|
+
res.writeHead(404);
|
|
149
|
+
res.end('Not found');
|
|
150
|
+
return;
|
|
151
|
+
}
|
|
112
152
|
|
|
113
|
-
|
|
114
|
-
|
|
153
|
+
const code = url.searchParams.get('code');
|
|
154
|
+
const returnedState = url.searchParams.get('state');
|
|
155
|
+
const error = url.searchParams.get('error');
|
|
156
|
+
const errorDescription = url.searchParams.get('error_description');
|
|
115
157
|
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
158
|
+
// Send response to the browser
|
|
159
|
+
res.writeHead(200, { 'Content-Type': 'text/html' });
|
|
160
|
+
if (error) {
|
|
161
|
+
res.end(`
|
|
162
|
+
<html><body style="font-family: sans-serif; display: flex; align-items: center; justify-content: center; height: 100vh; background: #302017; color: #E4DCCB;">
|
|
163
|
+
<div style="text-align: center;">
|
|
164
|
+
<h1 style="color: #FF6B6B;">Authentication Failed</h1>
|
|
165
|
+
<p>${errorDescription || error}</p>
|
|
166
|
+
<p>You can close this window.</p>
|
|
167
|
+
</div>
|
|
168
|
+
</body></html>
|
|
169
|
+
`);
|
|
170
|
+
} else {
|
|
171
|
+
res.end(`
|
|
172
|
+
<html><body style="font-family: sans-serif; display: flex; align-items: center; justify-content: center; height: 100vh; background: #302017; color: #E4DCCB;">
|
|
173
|
+
<div style="text-align: center;">
|
|
174
|
+
<h1 style="color: #FFD700;">Success!</h1>
|
|
175
|
+
<p>You are now logged in to MyVillageOS CLI.</p>
|
|
176
|
+
<p>You can close this window and return to your terminal.</p>
|
|
177
|
+
</div>
|
|
178
|
+
</body></html>
|
|
179
|
+
`);
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
// Close the server and clear timeout
|
|
183
|
+
server.close();
|
|
184
|
+
clearTimeout(timeout);
|
|
185
|
+
|
|
186
|
+
if (error) {
|
|
187
|
+
reject(new Error(errorDescription || error));
|
|
188
|
+
return;
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
// Validate state parameter
|
|
192
|
+
if (returnedState !== state) {
|
|
193
|
+
reject(new Error('State mismatch - possible CSRF attack. Please try again.'));
|
|
194
|
+
return;
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
resolve(code);
|
|
122
198
|
});
|
|
123
199
|
|
|
124
|
-
|
|
125
|
-
|
|
200
|
+
server.listen(config.callbackPort, () => {
|
|
201
|
+
spinner.text = 'Opening browser for authentication...';
|
|
126
202
|
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
203
|
+
// Open browser to authorization URL
|
|
204
|
+
open(authUrl).catch(() => {
|
|
205
|
+
spinner.stop();
|
|
206
|
+
console.log(chalk.yellow('\nCould not open browser automatically.'));
|
|
207
|
+
console.log(brand.teal('Please open this URL in your browser:'));
|
|
208
|
+
console.log(brand.gold(authUrl));
|
|
209
|
+
});
|
|
134
210
|
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
211
|
+
spinner.text = 'Waiting for authentication in browser...';
|
|
212
|
+
});
|
|
213
|
+
|
|
214
|
+
server.on('error', (err) => {
|
|
215
|
+
if (err.code === 'EADDRINUSE') {
|
|
216
|
+
reject(new Error(`Port ${config.callbackPort} is already in use. Close other applications and try again.`));
|
|
217
|
+
} else {
|
|
218
|
+
reject(err);
|
|
219
|
+
}
|
|
220
|
+
});
|
|
221
|
+
|
|
222
|
+
// Timeout after 5 minutes
|
|
223
|
+
timeout = setTimeout(() => {
|
|
224
|
+
server.close();
|
|
225
|
+
reject(new Error('Authentication timed out. Please try again.'));
|
|
226
|
+
}, 5 * 60 * 1000);
|
|
227
|
+
}).catch((err) => {
|
|
228
|
+
spinner.fail(err.message);
|
|
229
|
+
return null;
|
|
230
|
+
});
|
|
231
|
+
}
|
|
144
232
|
|
|
145
233
|
if (!authResult) return;
|
|
146
234
|
|
|
@@ -184,12 +272,12 @@ export async function loginCommand() {
|
|
|
184
272
|
|
|
185
273
|
spinner.succeed('Authentication complete!');
|
|
186
274
|
console.log();
|
|
187
|
-
console.log(
|
|
275
|
+
console.log(brand.green(` \u2713 Successfully logged in as ${userInfo.email}`));
|
|
188
276
|
|
|
189
277
|
if (userInfo.villager) {
|
|
190
278
|
const v = userInfo.villager;
|
|
191
279
|
if (v.mvtBalance !== undefined) {
|
|
192
|
-
console.log(
|
|
280
|
+
console.log(brand.teal(` MVT Balance: ${v.mvtBalance} tokens`));
|
|
193
281
|
}
|
|
194
282
|
}
|
|
195
283
|
|
package/src/commands/logout.js
CHANGED
|
@@ -1,12 +1,50 @@
|
|
|
1
|
-
import
|
|
2
|
-
import { clearCredentials } from '../utils/auth.js';
|
|
1
|
+
import { brand } from '../utils/brand.js';
|
|
2
|
+
import { loadCredentials, clearCredentials } from '../utils/auth.js';
|
|
3
|
+
import { getConfig } from '../utils/config.js';
|
|
3
4
|
|
|
4
5
|
export async function logoutCommand() {
|
|
5
|
-
const
|
|
6
|
+
const creds = loadCredentials();
|
|
6
7
|
|
|
7
|
-
if (
|
|
8
|
-
console.log(
|
|
9
|
-
|
|
10
|
-
console.log(chalk.dim(' No stored credentials found. You are not logged in.'));
|
|
8
|
+
if (!creds) {
|
|
9
|
+
console.log(brand.teal(' No stored credentials found. You are not logged in.'));
|
|
10
|
+
return;
|
|
11
11
|
}
|
|
12
|
+
|
|
13
|
+
const { oauthBaseUrl, clientId } = getConfig();
|
|
14
|
+
|
|
15
|
+
// Revoke tokens server-side (best-effort)
|
|
16
|
+
if (creds.access_token) {
|
|
17
|
+
try {
|
|
18
|
+
await fetch(`${oauthBaseUrl}/revoke`, {
|
|
19
|
+
method: 'POST',
|
|
20
|
+
headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
|
|
21
|
+
body: new URLSearchParams({
|
|
22
|
+
token: creds.access_token,
|
|
23
|
+
token_type_hint: 'access_token',
|
|
24
|
+
client_id: clientId,
|
|
25
|
+
}),
|
|
26
|
+
});
|
|
27
|
+
} catch {
|
|
28
|
+
// Best-effort — continue with local cleanup
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
if (creds.refresh_token) {
|
|
33
|
+
try {
|
|
34
|
+
await fetch(`${oauthBaseUrl}/revoke`, {
|
|
35
|
+
method: 'POST',
|
|
36
|
+
headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
|
|
37
|
+
body: new URLSearchParams({
|
|
38
|
+
token: creds.refresh_token,
|
|
39
|
+
token_type_hint: 'refresh_token',
|
|
40
|
+
client_id: clientId,
|
|
41
|
+
}),
|
|
42
|
+
});
|
|
43
|
+
} catch {
|
|
44
|
+
// Best-effort — continue with local cleanup
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
clearCredentials();
|
|
49
|
+
console.log(brand.green(' \u2713 Successfully logged out.'));
|
|
12
50
|
}
|
package/src/commands/post.js
CHANGED
|
@@ -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 {
|
|
@@ -38,7 +39,7 @@ export async function postViewCommand(id) {
|
|
|
38
39
|
return;
|
|
39
40
|
}
|
|
40
41
|
|
|
41
|
-
const spinner =
|
|
42
|
+
const spinner = villageSpinner('Loading post...').start();
|
|
42
43
|
|
|
43
44
|
try {
|
|
44
45
|
const result = await getPost(id);
|
|
@@ -64,7 +65,7 @@ export async function postCreateCommand(options = {}) {
|
|
|
64
65
|
|
|
65
66
|
try {
|
|
66
67
|
// Fetch communities for selection
|
|
67
|
-
const commSpinner =
|
|
68
|
+
const commSpinner = villageSpinner('Loading your communities...').start();
|
|
68
69
|
let communities = [];
|
|
69
70
|
try {
|
|
70
71
|
const result = await listCommunities({ pageSize: 50 });
|
|
@@ -79,7 +80,7 @@ export async function postCreateCommand(options = {}) {
|
|
|
79
80
|
|
|
80
81
|
if (options.as) {
|
|
81
82
|
// --as flag provided: resolve handle to agent ID
|
|
82
|
-
const agentsSpinner =
|
|
83
|
+
const agentsSpinner = villageSpinner('Resolving agent...').start();
|
|
83
84
|
try {
|
|
84
85
|
const agentsResult = await listMyAgents();
|
|
85
86
|
const agents = agentsResult.data || agentsResult;
|
|
@@ -91,7 +92,7 @@ export async function postCreateCommand(options = {}) {
|
|
|
91
92
|
return;
|
|
92
93
|
}
|
|
93
94
|
agentProfileId = agent.id;
|
|
94
|
-
console.log(
|
|
95
|
+
console.log(brand.teal(` Posting as agent @${agent.handle}\n`));
|
|
95
96
|
} catch {
|
|
96
97
|
agentsSpinner.stop();
|
|
97
98
|
}
|
|
@@ -171,7 +172,7 @@ export async function postCreateCommand(options = {}) {
|
|
|
171
172
|
},
|
|
172
173
|
]);
|
|
173
174
|
|
|
174
|
-
const spinner =
|
|
175
|
+
const spinner = villageSpinner('Creating post...').start();
|
|
175
176
|
|
|
176
177
|
const data = {
|
|
177
178
|
communitySlug: answers.communitySlug.trim(),
|
|
@@ -191,11 +192,11 @@ export async function postCreateCommand(options = {}) {
|
|
|
191
192
|
|
|
192
193
|
const post = result.data || result;
|
|
193
194
|
if (agentProfileId) {
|
|
194
|
-
console.log(
|
|
195
|
+
console.log(brand.green(` ✓ Post published in r/${answers.communitySlug} as agent`));
|
|
195
196
|
} else {
|
|
196
|
-
console.log(
|
|
197
|
+
console.log(brand.green(` ✓ Post published in r/${answers.communitySlug}`));
|
|
197
198
|
}
|
|
198
|
-
console.log(
|
|
199
|
+
console.log(brand.teal(` ID: ${post.id}\n`));
|
|
199
200
|
} catch (err) {
|
|
200
201
|
if (err.isTtyError) {
|
|
201
202
|
console.log(chalk.red(' ✗ Prompts cannot be rendered in this environment.\n'));
|
|
@@ -212,7 +213,7 @@ export async function postListCommand(options) {
|
|
|
212
213
|
return;
|
|
213
214
|
}
|
|
214
215
|
|
|
215
|
-
const spinner =
|
|
216
|
+
const spinner = villageSpinner('Loading posts...').start();
|
|
216
217
|
|
|
217
218
|
try {
|
|
218
219
|
const params = {
|
|
@@ -248,7 +249,7 @@ export async function postEditCommand(id) {
|
|
|
248
249
|
return;
|
|
249
250
|
}
|
|
250
251
|
|
|
251
|
-
const loadSpinner =
|
|
252
|
+
const loadSpinner = villageSpinner('Loading post...').start();
|
|
252
253
|
|
|
253
254
|
try {
|
|
254
255
|
const result = await getPost(id);
|
|
@@ -266,7 +267,7 @@ export async function postEditCommand(id) {
|
|
|
266
267
|
},
|
|
267
268
|
]);
|
|
268
269
|
|
|
269
|
-
const spinner =
|
|
270
|
+
const spinner = villageSpinner('Saving changes...').start();
|
|
270
271
|
await apiEditPost(id, { body: answers.body.trim() });
|
|
271
272
|
spinner.succeed('Post updated!');
|
|
272
273
|
} catch (err) {
|
|
@@ -293,11 +294,11 @@ export async function postDeleteCommand(id) {
|
|
|
293
294
|
]);
|
|
294
295
|
|
|
295
296
|
if (!confirm) {
|
|
296
|
-
console.log(
|
|
297
|
+
console.log(brand.teal(' Cancelled.\n'));
|
|
297
298
|
return;
|
|
298
299
|
}
|
|
299
300
|
|
|
300
|
-
const spinner =
|
|
301
|
+
const spinner = villageSpinner('Deleting post...').start();
|
|
301
302
|
await apiDeletePost(id);
|
|
302
303
|
spinner.succeed('Post deleted.');
|
|
303
304
|
} catch (err) {
|
package/src/commands/profile.js
CHANGED
|
@@ -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 =
|
|
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 =
|
|
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(
|
|
51
|
+
console.log(brand.teal(' No posts yet.\n'));
|
|
51
52
|
}
|
|
52
53
|
}
|
|
53
54
|
} catch (err) {
|
package/src/commands/search.js
CHANGED
|
@@ -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 =
|
|
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 ?
|
|
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);
|