opencode-agora 0.3.0 → 0.4.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 +217 -52
- package/dist/cli/app.d.ts +3 -0
- package/dist/cli/app.d.ts.map +1 -1
- package/dist/cli/app.js +1053 -183
- package/dist/cli/app.js.map +1 -1
- package/dist/cli/chat-renderer.d.ts +31 -0
- package/dist/cli/chat-renderer.d.ts.map +1 -0
- package/dist/cli/chat-renderer.js +275 -0
- package/dist/cli/chat-renderer.js.map +1 -0
- package/dist/cli/commands-meta.d.ts +21 -0
- package/dist/cli/commands-meta.d.ts.map +1 -0
- package/dist/cli/commands-meta.js +600 -0
- package/dist/cli/commands-meta.js.map +1 -0
- package/dist/cli/completions.d.ts +18 -0
- package/dist/cli/completions.d.ts.map +1 -0
- package/dist/cli/completions.js +190 -0
- package/dist/cli/completions.js.map +1 -0
- package/dist/cli/mcp-server.d.ts +4 -0
- package/dist/cli/mcp-server.d.ts.map +1 -0
- package/dist/cli/mcp-server.js +277 -0
- package/dist/cli/mcp-server.js.map +1 -0
- package/dist/cli/menu.d.ts +7 -0
- package/dist/cli/menu.d.ts.map +1 -0
- package/dist/cli/menu.js +164 -0
- package/dist/cli/menu.js.map +1 -0
- package/dist/cli/pages/community.d.ts +3 -0
- package/dist/cli/pages/community.d.ts.map +1 -0
- package/dist/cli/pages/community.js +276 -0
- package/dist/cli/pages/community.js.map +1 -0
- package/dist/cli/pages/helpers.d.ts +32 -0
- package/dist/cli/pages/helpers.d.ts.map +1 -0
- package/dist/cli/pages/helpers.js +67 -0
- package/dist/cli/pages/helpers.js.map +1 -0
- package/dist/cli/pages/home.d.ts +3 -0
- package/dist/cli/pages/home.d.ts.map +1 -0
- package/dist/cli/pages/home.js +148 -0
- package/dist/cli/pages/home.js.map +1 -0
- package/dist/cli/pages/marketplace.d.ts +3 -0
- package/dist/cli/pages/marketplace.d.ts.map +1 -0
- package/dist/cli/pages/marketplace.js +179 -0
- package/dist/cli/pages/marketplace.js.map +1 -0
- package/dist/cli/pages/news.d.ts +3 -0
- package/dist/cli/pages/news.d.ts.map +1 -0
- package/dist/cli/pages/news.js +561 -0
- package/dist/cli/pages/news.js.map +1 -0
- package/dist/cli/pages/settings.d.ts +3 -0
- package/dist/cli/pages/settings.d.ts.map +1 -0
- package/dist/cli/pages/settings.js +166 -0
- package/dist/cli/pages/settings.js.map +1 -0
- package/dist/cli/pages/types.d.ts +67 -0
- package/dist/cli/pages/types.d.ts.map +1 -0
- package/dist/cli/pages/types.js +2 -0
- package/dist/cli/pages/types.js.map +1 -0
- package/dist/cli/prompter.d.ts +135 -0
- package/dist/cli/prompter.d.ts.map +1 -0
- package/dist/cli/prompter.js +675 -0
- package/dist/cli/prompter.js.map +1 -0
- package/dist/cli/shell.d.ts +23 -0
- package/dist/cli/shell.d.ts.map +1 -0
- package/dist/cli/shell.js +819 -0
- package/dist/cli/shell.js.map +1 -0
- package/dist/cli/tui.d.ts +7 -0
- package/dist/cli/tui.d.ts.map +1 -0
- package/dist/cli/tui.js +373 -0
- package/dist/cli/tui.js.map +1 -0
- package/dist/cli.js +1 -1
- package/dist/cli.js.map +1 -1
- package/dist/commands.d.ts +14 -0
- package/dist/commands.d.ts.map +1 -0
- package/dist/commands.js +28 -0
- package/dist/commands.js.map +1 -0
- package/dist/community/client.d.ts +47 -0
- package/dist/community/client.d.ts.map +1 -0
- package/dist/community/client.js +245 -0
- package/dist/community/client.js.map +1 -0
- package/dist/community/types.d.ts +50 -0
- package/dist/community/types.d.ts.map +1 -0
- package/dist/community/types.js +11 -0
- package/dist/community/types.js.map +1 -0
- package/dist/config.d.ts +1 -3
- package/dist/config.d.ts.map +1 -1
- package/dist/config.js +0 -17
- package/dist/config.js.map +1 -1
- package/dist/data.d.ts +1 -1
- package/dist/data.d.ts.map +1 -1
- package/dist/data.js +667 -3
- package/dist/data.js.map +1 -1
- package/dist/format.d.ts +5 -39
- package/dist/format.d.ts.map +1 -1
- package/dist/format.js +5 -120
- package/dist/format.js.map +1 -1
- package/dist/history.d.ts +13 -0
- package/dist/history.d.ts.map +1 -0
- package/dist/history.js +37 -0
- package/dist/history.js.map +1 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +114 -234
- package/dist/index.js.map +1 -1
- package/dist/init.d.ts.map +1 -1
- package/dist/init.js +4 -9
- package/dist/init.js.map +1 -1
- package/dist/live.d.ts +4 -0
- package/dist/live.d.ts.map +1 -1
- package/dist/live.js +11 -3
- package/dist/live.js.map +1 -1
- package/dist/marketplace.d.ts +9 -0
- package/dist/marketplace.d.ts.map +1 -1
- package/dist/marketplace.js +105 -15
- package/dist/marketplace.js.map +1 -1
- package/dist/news/cache.d.ts +13 -0
- package/dist/news/cache.d.ts.map +1 -0
- package/dist/news/cache.js +65 -0
- package/dist/news/cache.js.map +1 -0
- package/dist/news/score.d.ts +4 -0
- package/dist/news/score.d.ts.map +1 -0
- package/dist/news/score.js +43 -0
- package/dist/news/score.js.map +1 -0
- package/dist/news/sources/arxiv.d.ts +9 -0
- package/dist/news/sources/arxiv.d.ts.map +1 -0
- package/dist/news/sources/arxiv.js +103 -0
- package/dist/news/sources/arxiv.js.map +1 -0
- package/dist/news/sources/github-trending.d.ts +9 -0
- package/dist/news/sources/github-trending.d.ts.map +1 -0
- package/dist/news/sources/github-trending.js +93 -0
- package/dist/news/sources/github-trending.js.map +1 -0
- package/dist/news/sources/hn.d.ts +9 -0
- package/dist/news/sources/hn.d.ts.map +1 -0
- package/dist/news/sources/hn.js +53 -0
- package/dist/news/sources/hn.js.map +1 -0
- package/dist/news/sources/reddit.d.ts +9 -0
- package/dist/news/sources/reddit.d.ts.map +1 -0
- package/dist/news/sources/reddit.js +68 -0
- package/dist/news/sources/reddit.js.map +1 -0
- package/dist/news/sources/rss.d.ts +14 -0
- package/dist/news/sources/rss.d.ts.map +1 -0
- package/dist/news/sources/rss.js +102 -0
- package/dist/news/sources/rss.js.map +1 -0
- package/dist/news/types.d.ts +39 -0
- package/dist/news/types.d.ts.map +1 -0
- package/dist/news/types.js +47 -0
- package/dist/news/types.js.map +1 -0
- package/dist/preferences.d.ts +14 -0
- package/dist/preferences.d.ts.map +1 -0
- package/dist/preferences.js +31 -0
- package/dist/preferences.js.map +1 -0
- package/dist/settings.d.ts +26 -0
- package/dist/settings.d.ts.map +1 -0
- package/dist/settings.js +257 -0
- package/dist/settings.js.map +1 -0
- package/dist/transcript.d.ts +28 -0
- package/dist/transcript.d.ts.map +1 -0
- package/dist/transcript.js +79 -0
- package/dist/transcript.js.map +1 -0
- package/dist/types.d.ts +6 -1
- package/dist/types.d.ts.map +1 -1
- package/dist/ui.d.ts +157 -0
- package/dist/ui.d.ts.map +1 -0
- package/dist/ui.js +296 -0
- package/dist/ui.js.map +1 -0
- package/package.json +11 -9
- package/dist/api.d.ts +0 -72
- package/dist/api.d.ts.map +0 -1
- package/dist/api.js +0 -109
- package/dist/api.js.map +0 -1
- package/dist/logger.d.ts +0 -20
- package/dist/logger.d.ts.map +0 -1
- package/dist/logger.js +0 -59
- package/dist/logger.js.map +0 -1
package/dist/cli/app.js
CHANGED
|
@@ -1,33 +1,89 @@
|
|
|
1
1
|
import { execSync } from 'node:child_process';
|
|
2
2
|
import { existsSync, readFileSync, writeFileSync, mkdirSync } from 'node:fs';
|
|
3
3
|
import { join } from 'node:path';
|
|
4
|
+
import { spawn } from 'node:child_process';
|
|
4
5
|
import { formatConfigJson } from '../config.js';
|
|
6
|
+
import { formatNumber } from '../format.js';
|
|
7
|
+
import { COMMANDS, renderManual } from './commands-meta.js';
|
|
8
|
+
import { runInteractiveMenu } from './menu.js';
|
|
9
|
+
import { runTui } from './tui.js';
|
|
5
10
|
import { detectOpenCodeConfigPath, doctorOpenCodeConfig, loadOpenCodeConfig, writeOpenCodeConfig } from '../config-files.js';
|
|
6
|
-
import { createInstallPlan, getInstallKind, getMarketplaceItems, getTrendingTags } from '../marketplace.js';
|
|
11
|
+
import { createInstallPlan, getInstallKind, getMarketplaceItems, getTrendingTags, similarItems } from '../marketplace.js';
|
|
12
|
+
import { communityBoardsSource, communityThreadsSource, communityThreadSource, createThreadSource, createReplySource, voteSource, flagSource } from '../community/client.js';
|
|
13
|
+
import { normalizeNewsSource, DEFAULT_NEWS_CONFIG, hostFromUrl } from '../news/types.js';
|
|
14
|
+
import { rankItems } from '../news/score.js';
|
|
15
|
+
import { readCache, writeCache, isStale } from '../news/cache.js';
|
|
16
|
+
import { hnSource } from '../news/sources/hn.js';
|
|
17
|
+
import { redditSource } from '../news/sources/reddit.js';
|
|
18
|
+
import { githubTrendingSource } from '../news/sources/github-trending.js';
|
|
19
|
+
import { arxivSource } from '../news/sources/arxiv.js';
|
|
7
20
|
import { scanProject, generateInitPlan, applyInitPlan, runCommands } from '../init.js';
|
|
21
|
+
import { installAgoraCommand } from '../commands.js';
|
|
8
22
|
import { sampleWorkflows, dataRefreshedAt } from '../data.js';
|
|
9
23
|
import { createDiscussionSource, discussionsSource, findMarketplaceSource, createReviewSource, findTutorialSource, listReviewsSource, profileSource, publishPackageSource, publishWorkflowSource, searchMarketplaceSource, trendingMarketplaceSource, tutorialsSource } from '../live.js';
|
|
10
24
|
import { clearAuthState, detectAgoraDataDir, getAuthState, getAgoraStatePath, loadAgoraState, removeItemFromState, resolveSavedItems, saveItemToState, setAuthState, writeAgoraState } from '../state.js';
|
|
25
|
+
import { createStyler, renderBanner, renderBox, renderMeander, shouldUseColor, supportsTrueColor } from '../ui.js';
|
|
26
|
+
import { loadPreferences, writePreferences, prefsPath } from '../preferences.js';
|
|
27
|
+
import { appendHistory, loadHistory, clearHistory } from '../history.js';
|
|
11
28
|
const pkg = JSON.parse(readFileSync(new URL('../../package.json', import.meta.url), 'utf8'));
|
|
12
|
-
const
|
|
29
|
+
export const AGORA_VERSION = pkg.version;
|
|
30
|
+
const VERSION = AGORA_VERSION;
|
|
31
|
+
// Active terminal styler. Reassigned once per `runCli` invocation from the
|
|
32
|
+
// caller's stream + env; defaults to plain so any direct formatter use is safe.
|
|
33
|
+
let style = createStyler(false);
|
|
13
34
|
const booleanFlags = new Set([
|
|
14
35
|
'api',
|
|
36
|
+
'continue',
|
|
37
|
+
'dryRun',
|
|
15
38
|
'help',
|
|
16
39
|
'json',
|
|
17
40
|
'live',
|
|
41
|
+
'mcp',
|
|
18
42
|
'offline',
|
|
43
|
+
'table',
|
|
19
44
|
'version',
|
|
20
45
|
'verbose',
|
|
21
46
|
'write'
|
|
22
47
|
]);
|
|
48
|
+
/**
|
|
49
|
+
* Returns true only when both stdout and stdin are real interactive TTYs AND the
|
|
50
|
+
* environment supports colour (i.e. not NO_COLOR or TERM=dumb). The gate keeps
|
|
51
|
+
* the interactive menu away from pipes, CI, and the test harness, all of which
|
|
52
|
+
* use non-TTY mock streams.
|
|
53
|
+
*/
|
|
54
|
+
function isInteractive(io, env) {
|
|
55
|
+
if (env.NO_COLOR != null)
|
|
56
|
+
return false;
|
|
57
|
+
if (env.TERM === 'dumb')
|
|
58
|
+
return false;
|
|
59
|
+
const stdoutTTY = Boolean(io.stdout.isTTY);
|
|
60
|
+
const stdinTTY = Boolean(process.stdin.isTTY);
|
|
61
|
+
return stdoutTTY && stdinTTY;
|
|
62
|
+
}
|
|
23
63
|
export async function runCli(argv, io) {
|
|
24
64
|
const parsed = parseArgs(argv);
|
|
65
|
+
const env = io.env ?? {};
|
|
66
|
+
const useColor = shouldUseColor(io.stdout, env, Boolean(parsed.flags.json));
|
|
67
|
+
style = createStyler(useColor, supportsTrueColor(env));
|
|
25
68
|
if (parsed.flags.version) {
|
|
26
69
|
writeLine(io.stdout, VERSION);
|
|
27
70
|
return 0;
|
|
28
71
|
}
|
|
29
|
-
if (
|
|
30
|
-
|
|
72
|
+
if (parsed.flags.help) {
|
|
73
|
+
if (parsed.command && COMMANDS.some((c) => c.name === parsed.command)) {
|
|
74
|
+
writeLine(io.stdout, commandManual(parsed.command));
|
|
75
|
+
}
|
|
76
|
+
else {
|
|
77
|
+
writeLine(io.stdout, usage());
|
|
78
|
+
}
|
|
79
|
+
return 0;
|
|
80
|
+
}
|
|
81
|
+
if (!parsed.command) {
|
|
82
|
+
if (isInteractive(io, env)) {
|
|
83
|
+
const { runShell } = await import('./shell.js');
|
|
84
|
+
return runShell(io, style);
|
|
85
|
+
}
|
|
86
|
+
writeLine(io.stdout, welcome(useColor, supportsTrueColor(env)));
|
|
31
87
|
return 0;
|
|
32
88
|
}
|
|
33
89
|
try {
|
|
@@ -65,16 +121,65 @@ export async function runCli(argv, io) {
|
|
|
65
121
|
case 'profile':
|
|
66
122
|
return await commandProfile(parsed, io);
|
|
67
123
|
case 'auth':
|
|
68
|
-
return commandAuth(parsed, io);
|
|
124
|
+
return await commandAuth(parsed, io);
|
|
125
|
+
case 'login':
|
|
126
|
+
return await commandAuth({ ...parsed, args: ['login', ...parsed.args], command: 'auth' }, io);
|
|
127
|
+
case 'logout':
|
|
128
|
+
return await commandAuth({ ...parsed, args: ['logout'], command: 'auth' }, io);
|
|
129
|
+
case 'whoami':
|
|
130
|
+
return await commandAuth({ ...parsed, args: ['status'], command: 'auth', flags: { ...parsed.flags, json: true } }, io);
|
|
69
131
|
case 'config':
|
|
70
132
|
return await commandConfig(parsed, io);
|
|
133
|
+
case 'mcp':
|
|
134
|
+
return await commandMcp(parsed, io);
|
|
135
|
+
case 'chat':
|
|
136
|
+
return await commandChat(parsed, io);
|
|
71
137
|
case 'init':
|
|
72
138
|
return await commandInit(parsed, io);
|
|
73
139
|
case 'use':
|
|
74
140
|
return await commandUse(parsed, io);
|
|
75
|
-
case '
|
|
76
|
-
|
|
141
|
+
case 'similar':
|
|
142
|
+
return await commandSimilar(parsed, io);
|
|
143
|
+
case 'compare':
|
|
144
|
+
return await commandCompare(parsed, io);
|
|
145
|
+
case 'news':
|
|
146
|
+
return await commandNews(parsed, io);
|
|
147
|
+
case 'community':
|
|
148
|
+
return await commandCommunity(parsed, io);
|
|
149
|
+
case 'thread':
|
|
150
|
+
return await commandThread(parsed, io);
|
|
151
|
+
case 'post':
|
|
152
|
+
return await commandPost(parsed, io);
|
|
153
|
+
case 'reply':
|
|
154
|
+
return await commandReply(parsed, io);
|
|
155
|
+
case 'vote':
|
|
156
|
+
return await commandVote(parsed, io);
|
|
157
|
+
case 'flag':
|
|
158
|
+
return await commandFlag(parsed, io);
|
|
159
|
+
case 'preferences':
|
|
160
|
+
return await commandPreferences(parsed, io);
|
|
161
|
+
case 'history':
|
|
162
|
+
return await commandHistory(parsed, io);
|
|
163
|
+
case 'menu':
|
|
164
|
+
return await runInteractiveMenu(io, style);
|
|
165
|
+
case 'tui':
|
|
166
|
+
return await runTui(io, { initial: 'home' });
|
|
167
|
+
case 'help': {
|
|
168
|
+
const helpTarget = parsed.args[0];
|
|
169
|
+
if (helpTarget) {
|
|
170
|
+
const meta = COMMANDS.find((c) => c.name === helpTarget);
|
|
171
|
+
if (!meta) {
|
|
172
|
+
writeLine(io.stderr, `Unknown command: ${helpTarget}`);
|
|
173
|
+
writeLine(io.stderr, 'Run `agora help` for a list of commands.');
|
|
174
|
+
return 1;
|
|
175
|
+
}
|
|
176
|
+
writeLine(io.stdout, commandManual(helpTarget));
|
|
177
|
+
}
|
|
178
|
+
else {
|
|
179
|
+
writeLine(io.stdout, usage());
|
|
180
|
+
}
|
|
77
181
|
return 0;
|
|
182
|
+
}
|
|
78
183
|
default:
|
|
79
184
|
writeLine(io.stderr, `Unknown command: ${parsed.command}`);
|
|
80
185
|
writeLine(io.stderr, 'Run agora help for usage.');
|
|
@@ -136,28 +241,50 @@ export function parseArgs(argv) {
|
|
|
136
241
|
async function commandSearch(parsed, io) {
|
|
137
242
|
const query = parsed.args.join(' ');
|
|
138
243
|
const category = stringFlag(parsed, 'category', 'c') || 'all';
|
|
139
|
-
const
|
|
244
|
+
const sortBy = stringFlag(parsed, 'sort', 's') || 'relevance';
|
|
245
|
+
const sortOrder = (stringFlag(parsed, 'order', 'o') || 'desc');
|
|
246
|
+
const table = Boolean(parsed.flags.table);
|
|
247
|
+
const page = numberFlag(parsed, 'page', 'p') || 1;
|
|
248
|
+
const perPage = numberFlag(parsed, 'perPage', 'pp') || 0;
|
|
249
|
+
const limit = perPage > 0 ? perPage : (numberFlag(parsed, 'limit', 'n') || 10);
|
|
140
250
|
const result = await searchMarketplaceSource({
|
|
141
251
|
...sourceOptions(parsed, io),
|
|
142
252
|
query,
|
|
143
253
|
category,
|
|
144
|
-
limit
|
|
254
|
+
limit,
|
|
255
|
+
sortBy: sortBy,
|
|
256
|
+
sortOrder,
|
|
257
|
+
page,
|
|
258
|
+
perPage
|
|
145
259
|
});
|
|
146
260
|
const results = result.data;
|
|
147
261
|
warnFallback(result, io);
|
|
148
262
|
if (parsed.flags.json) {
|
|
149
|
-
writeJson(io.stdout, sourcePayload(result, { query, category, count: results.length, items: results }));
|
|
263
|
+
writeJson(io.stdout, sourcePayload(result, { query, category, sortBy, sortOrder, page, count: results.length, items: results }));
|
|
150
264
|
return 0;
|
|
151
265
|
}
|
|
152
266
|
if (results.length === 0) {
|
|
153
267
|
writeLine(io.stdout, `No results found for "${query}".`);
|
|
154
268
|
return 0;
|
|
155
269
|
}
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
270
|
+
writeLine(io.stdout, header('agora search', [`"${query || 'all'}"`, `${results.length} results`, sourceLabel(result)]));
|
|
271
|
+
writeLine(io.stdout, '');
|
|
272
|
+
if (table) {
|
|
273
|
+
writeLine(io.stdout, formatItemTable(results));
|
|
274
|
+
}
|
|
275
|
+
else {
|
|
276
|
+
writeLine(io.stdout, formatItemList(results));
|
|
277
|
+
}
|
|
278
|
+
if (perPage > 0) {
|
|
279
|
+
writeLine(io.stdout, '');
|
|
280
|
+
writeLine(io.stdout, style.dim(`Page ${page} · ${perPage} per page. Use --page N to navigate.`));
|
|
281
|
+
}
|
|
282
|
+
appendHistory(detectDataDir(parsed, io), {
|
|
283
|
+
type: 'search',
|
|
284
|
+
query,
|
|
285
|
+
timestamp: new Date().toISOString(),
|
|
286
|
+
results: results.length,
|
|
287
|
+
});
|
|
161
288
|
return 0;
|
|
162
289
|
}
|
|
163
290
|
async function commandBrowse(parsed, io) {
|
|
@@ -178,11 +305,22 @@ async function commandBrowse(parsed, io) {
|
|
|
178
305
|
return 0;
|
|
179
306
|
}
|
|
180
307
|
writeLine(io.stdout, formatItemDetail(item));
|
|
308
|
+
const related = similarItems(id, { limit: 3, type: item.kind === 'workflow' ? 'workflow' : undefined });
|
|
309
|
+
if (related.length > 0) {
|
|
310
|
+
writeLine(io.stdout, '');
|
|
311
|
+
writeLine(io.stdout, style.dim('Related:'));
|
|
312
|
+
for (const rel of related) {
|
|
313
|
+
const tagOverlap = (item.tags ?? []).filter((t) => (rel.tags ?? []).includes(t));
|
|
314
|
+
const reason = tagOverlap.length > 0 ? ` (shares tags: ${tagOverlap.join(', ')})` : '';
|
|
315
|
+
writeLine(io.stdout, ` ${style.accent(rel.id.padEnd(28))} ${style.dim(formatNumber(rel.installs ?? 0) + ' installs')}${style.dim(reason)}`);
|
|
316
|
+
}
|
|
317
|
+
}
|
|
181
318
|
return 0;
|
|
182
319
|
}
|
|
183
320
|
async function commandTrending(parsed, io) {
|
|
184
321
|
const category = stringFlag(parsed, 'category', 'c') || parsed.args[0] || 'all';
|
|
185
322
|
const limit = numberFlag(parsed, 'limit', 'n') || 5;
|
|
323
|
+
const table = Boolean(parsed.flags.table);
|
|
186
324
|
const result = await trendingMarketplaceSource({ ...sourceOptions(parsed, io), category, limit });
|
|
187
325
|
const items = result.data;
|
|
188
326
|
warnFallback(result, io);
|
|
@@ -190,9 +328,16 @@ async function commandTrending(parsed, io) {
|
|
|
190
328
|
writeJson(io.stdout, sourcePayload(result, { category, count: items.length, tags: getTrendingTags(), items }));
|
|
191
329
|
return 0;
|
|
192
330
|
}
|
|
193
|
-
writeLine(io.stdout,
|
|
194
|
-
writeLine(io.stdout,
|
|
195
|
-
|
|
331
|
+
writeLine(io.stdout, header('agora trending', [category, sourceLabel(result)]));
|
|
332
|
+
writeLine(io.stdout, '');
|
|
333
|
+
if (table) {
|
|
334
|
+
writeLine(io.stdout, formatItemTable(items));
|
|
335
|
+
}
|
|
336
|
+
else {
|
|
337
|
+
writeLine(io.stdout, formatItemList(items));
|
|
338
|
+
}
|
|
339
|
+
writeLine(io.stdout, '');
|
|
340
|
+
writeLine(io.stdout, `${style.dim('tags')} ${getTrendingTags().join(', ')}`);
|
|
196
341
|
return 0;
|
|
197
342
|
}
|
|
198
343
|
async function commandWorkflows(parsed, io) {
|
|
@@ -204,16 +349,340 @@ async function commandWorkflows(parsed, io) {
|
|
|
204
349
|
category: 'workflow',
|
|
205
350
|
limit
|
|
206
351
|
});
|
|
207
|
-
const workflows = result.data
|
|
352
|
+
const workflows = result.data;
|
|
208
353
|
warnFallback(result, io);
|
|
209
354
|
if (parsed.flags.json) {
|
|
210
355
|
writeJson(io.stdout, sourcePayload(result, { query, count: workflows.length, workflows }));
|
|
211
356
|
return 0;
|
|
212
357
|
}
|
|
213
|
-
writeLine(io.stdout,
|
|
358
|
+
writeLine(io.stdout, header('agora workflows', [`${workflows.length} results`, sourceLabel(result)]));
|
|
359
|
+
writeLine(io.stdout, '');
|
|
214
360
|
writeLine(io.stdout, formatItemList(workflows));
|
|
215
361
|
return 0;
|
|
216
362
|
}
|
|
363
|
+
async function commandSimilar(parsed, io) {
|
|
364
|
+
const id = parsed.args[0];
|
|
365
|
+
if (!id)
|
|
366
|
+
return usageError(io, 'similar requires an item id');
|
|
367
|
+
const type = stringFlag(parsed, 'type', 't');
|
|
368
|
+
const limit = numberFlag(parsed, 'limit', 'n') || 5;
|
|
369
|
+
const results = similarItems(id, { limit, type });
|
|
370
|
+
if (parsed.flags.json) {
|
|
371
|
+
writeJson(io.stdout, { id, type: type || 'all', count: results.length, items: results });
|
|
372
|
+
return 0;
|
|
373
|
+
}
|
|
374
|
+
if (results.length === 0) {
|
|
375
|
+
writeLine(io.stdout, `No similar items found for "${id}".`);
|
|
376
|
+
return 0;
|
|
377
|
+
}
|
|
378
|
+
writeLine(io.stdout, header('agora similar', [`to ${id}`, `${results.length} results`]));
|
|
379
|
+
writeLine(io.stdout, '');
|
|
380
|
+
writeLine(io.stdout, formatItemList(results));
|
|
381
|
+
return 0;
|
|
382
|
+
}
|
|
383
|
+
async function commandCompare(parsed, io) {
|
|
384
|
+
const ids = parsed.args;
|
|
385
|
+
if (ids.length < 2)
|
|
386
|
+
return usageError(io, 'compare requires at least two item ids');
|
|
387
|
+
const typeMap = (type) => type === 'package' || type === 'workflow' ? type : undefined;
|
|
388
|
+
const type = typeMap(stringFlag(parsed, 'type', 't'));
|
|
389
|
+
const items = [];
|
|
390
|
+
for (const id of ids) {
|
|
391
|
+
const item = await findMarketplaceSource({ ...sourceOptions(parsed, io), id, type });
|
|
392
|
+
if (!item.data) {
|
|
393
|
+
writeLine(io.stderr, `Item not found: ${id}`);
|
|
394
|
+
return 1;
|
|
395
|
+
}
|
|
396
|
+
items.push(item.data);
|
|
397
|
+
}
|
|
398
|
+
if (parsed.flags.json) {
|
|
399
|
+
writeJson(io.stdout, { ids, count: items.length, items });
|
|
400
|
+
return 0;
|
|
401
|
+
}
|
|
402
|
+
const sharedTags = items.length > 1
|
|
403
|
+
? items.map((i) => new Set(i.tags ?? [])).reduce((a, b) => new Set([...a].filter((t) => b.has(t))))
|
|
404
|
+
: new Set();
|
|
405
|
+
const attrs = [
|
|
406
|
+
{ label: 'name', get: (i) => i.name },
|
|
407
|
+
{ label: 'author', get: (i) => i.author },
|
|
408
|
+
{ label: 'installs', get: (i) => formatNumber(i.installs ?? 0) },
|
|
409
|
+
{ label: 'stars', get: (i) => formatNumber(i.stars ?? 0) },
|
|
410
|
+
{ label: 'category', get: (i) => i.category },
|
|
411
|
+
{ label: 'tags', get: (i) => (i.tags ?? []).join(', ') },
|
|
412
|
+
];
|
|
413
|
+
if (items.some((i) => i.kind === 'package' && i.npmPackage)) {
|
|
414
|
+
attrs.push({
|
|
415
|
+
label: 'npmPackage',
|
|
416
|
+
get: (i) => (i.kind === 'package' && i.npmPackage) || '-',
|
|
417
|
+
});
|
|
418
|
+
}
|
|
419
|
+
if (items.some((i) => i.createdAt)) {
|
|
420
|
+
attrs.push({
|
|
421
|
+
label: 'created',
|
|
422
|
+
get: (i) => (i.createdAt || '').slice(0, 10),
|
|
423
|
+
});
|
|
424
|
+
}
|
|
425
|
+
const colW = Math.max(12, ...items.map((i) => Math.max(i.id.length, i.name.length, 10)));
|
|
426
|
+
const labelW = Math.max(...attrs.map((a) => a.label.length));
|
|
427
|
+
const totalW = labelW + 3 + items.length * (colW + 3) + 1;
|
|
428
|
+
const top = style.accent('┌') + '─'.repeat(labelW + 2) + style.accent('┬') + '─'.repeat(totalW - labelW - 6) + style.accent('┐');
|
|
429
|
+
const bot = style.accent('└') + '─'.repeat(labelW + 2) + style.accent('┴') + '─'.repeat(totalW - labelW - 6) + style.accent('┘');
|
|
430
|
+
const hdrCells = items.map((i, idx) => {
|
|
431
|
+
const name = idx === 0 ? style.bold(style.accent(i.id)) : style.accent(i.id);
|
|
432
|
+
return name.padEnd(colW);
|
|
433
|
+
});
|
|
434
|
+
const hdr = style.accent('│') + ' '.repeat(labelW + 2) + style.accent('│') + ' ' + hdrCells.join(style.accent(' │ ') + ' ') + style.accent(' │');
|
|
435
|
+
const rows = attrs.map((attr) => {
|
|
436
|
+
const cells = items.map((item) => {
|
|
437
|
+
const val = attr.get(item);
|
|
438
|
+
if (attr.label === 'tags') {
|
|
439
|
+
return val.split(', ').map((t) => sharedTags.has(t) ? style.accent(t) : style.dim(t)).join(', ').padEnd(colW);
|
|
440
|
+
}
|
|
441
|
+
return (attr.label === 'name' || attr.label === 'npmPackage' ? val : style.dim(val)).padEnd(colW);
|
|
442
|
+
});
|
|
443
|
+
return style.accent('│') + ' ' + style.dim(attr.label.padEnd(labelW)) + ' ' + style.accent('│') + ' ' + cells.join(style.accent(' │ ') + ' ') + style.accent(' │');
|
|
444
|
+
});
|
|
445
|
+
const sepLine = style.accent('├') + '─'.repeat(labelW + 2) + style.accent('┼') + '─'.repeat(totalW - labelW - 6) + style.accent('┤');
|
|
446
|
+
writeLine(io.stdout, header('agora compare', items.map((i) => i.id)));
|
|
447
|
+
writeLine(io.stdout, '');
|
|
448
|
+
writeLine(io.stdout, [top, hdr, sepLine, ...rows, bot].join('\n'));
|
|
449
|
+
if (sharedTags.size > 0) {
|
|
450
|
+
writeLine(io.stdout, '');
|
|
451
|
+
writeLine(io.stdout, style.dim('Shared tags highlighted in accent.'));
|
|
452
|
+
}
|
|
453
|
+
return 0;
|
|
454
|
+
}
|
|
455
|
+
async function commandNews(parsed, io) {
|
|
456
|
+
const query = parsed.args.join(' ');
|
|
457
|
+
const sourceOpt = stringFlag(parsed, 'source', 's');
|
|
458
|
+
const source = sourceOpt ? normalizeNewsSource(sourceOpt) : undefined;
|
|
459
|
+
const limit = numberFlag(parsed, 'limit', 'n') || 20;
|
|
460
|
+
const refresh = Boolean(parsed.flags.refresh);
|
|
461
|
+
const dataDir = detectDataDir(parsed, io);
|
|
462
|
+
let cached = readCache(dataDir);
|
|
463
|
+
const now = new Date();
|
|
464
|
+
const config = DEFAULT_NEWS_CONFIG;
|
|
465
|
+
const adapters = [
|
|
466
|
+
['hn', hnSource],
|
|
467
|
+
['reddit', redditSource],
|
|
468
|
+
['github-trending', githubTrendingSource],
|
|
469
|
+
['arxiv', arxivSource],
|
|
470
|
+
];
|
|
471
|
+
const fetchWithTimeout = (fn, ms = 10000) => Promise.race([fn(), new Promise((_, r) => setTimeout(() => r(new Error('timeout')), ms))]);
|
|
472
|
+
const refreshSource = async (src, adapter) => {
|
|
473
|
+
try {
|
|
474
|
+
const fresh = await fetchWithTimeout(() => adapter.fetch({}));
|
|
475
|
+
cached = cached.filter((i) => i.source !== src);
|
|
476
|
+
cached.push(...fresh);
|
|
477
|
+
}
|
|
478
|
+
catch {
|
|
479
|
+
// keep stale
|
|
480
|
+
}
|
|
481
|
+
};
|
|
482
|
+
if (refresh) {
|
|
483
|
+
for (const [src, adapter] of adapters) {
|
|
484
|
+
await refreshSource(src, adapter);
|
|
485
|
+
}
|
|
486
|
+
}
|
|
487
|
+
else {
|
|
488
|
+
for (const [src, adapter] of adapters) {
|
|
489
|
+
const cfg = config.sources[src];
|
|
490
|
+
if (cfg?.enabled && isStale(cached, src, cfg.ttlMinutes, now)) {
|
|
491
|
+
await refreshSource(src, adapter);
|
|
492
|
+
}
|
|
493
|
+
}
|
|
494
|
+
}
|
|
495
|
+
const ranked = rankItems(cached, config, now);
|
|
496
|
+
writeCache(dataDir, cached);
|
|
497
|
+
let items = ranked;
|
|
498
|
+
if (query) {
|
|
499
|
+
const q = query.toLowerCase();
|
|
500
|
+
items = items.filter((i) => i.title.toLowerCase().includes(q) || i.tags.some((t) => t.includes(q)));
|
|
501
|
+
}
|
|
502
|
+
if (source) {
|
|
503
|
+
items = items.filter((i) => i.source === source);
|
|
504
|
+
}
|
|
505
|
+
items = items.slice(0, limit);
|
|
506
|
+
if (parsed.flags.json) {
|
|
507
|
+
writeJson(io.stdout, { count: items.length, items, source: source || 'all' });
|
|
508
|
+
return 0;
|
|
509
|
+
}
|
|
510
|
+
if (items.length === 0) {
|
|
511
|
+
writeLine(io.stdout, 'No news items found.');
|
|
512
|
+
return 0;
|
|
513
|
+
}
|
|
514
|
+
writeLine(io.stdout, header('agora news', [`${items.length} stories`, source ? `source: ${source}` : 'all sources']));
|
|
515
|
+
writeLine(io.stdout, '');
|
|
516
|
+
for (const item of items) {
|
|
517
|
+
const ageH = Math.round((now.getTime() - new Date(item.publishedAt).getTime()) / 3600000);
|
|
518
|
+
const age = ageH < 1 ? '<1h' : ageH < 24 ? `${ageH}h` : `${Math.round(ageH / 24)}d`;
|
|
519
|
+
const host = hostFromUrl(item.url);
|
|
520
|
+
writeLine(io.stdout, `${style.accent(item.source.padEnd(6))} ${style.dim(age.padEnd(4))} ${style.accent(formatNumber(item.engagement).padStart(7))} ${style.dim('s' + item.score.toFixed(2))} ${item.title}`);
|
|
521
|
+
if (host)
|
|
522
|
+
writeLine(io.stdout, ` ${style.dim(host)}`);
|
|
523
|
+
if (query)
|
|
524
|
+
writeLine(io.stdout, '');
|
|
525
|
+
}
|
|
526
|
+
return 0;
|
|
527
|
+
}
|
|
528
|
+
async function commandCommunity(parsed, io) {
|
|
529
|
+
const board = parsed.args[0];
|
|
530
|
+
const sort = (stringFlag(parsed, 'sort') || 'active');
|
|
531
|
+
const opts = sourceOptions(parsed, io);
|
|
532
|
+
if (board) {
|
|
533
|
+
const result = await communityThreadsSource(opts, board, sort);
|
|
534
|
+
const threads = result.data.threads;
|
|
535
|
+
if (parsed.flags.json) {
|
|
536
|
+
writeJson(io.stdout, { board, sort, count: threads.length, threads });
|
|
537
|
+
return 0;
|
|
538
|
+
}
|
|
539
|
+
if (threads.length === 0) {
|
|
540
|
+
writeLine(io.stdout, `No threads in /${board}.`);
|
|
541
|
+
return 0;
|
|
542
|
+
}
|
|
543
|
+
writeLine(io.stdout, header(`agora community /${board}`, [`${threads.length} threads`, `sort: ${sort}`]));
|
|
544
|
+
writeLine(io.stdout, '');
|
|
545
|
+
for (const t of threads) {
|
|
546
|
+
const ageH = Math.round((Date.now() - new Date(t.createdAt).getTime()) / 3600000);
|
|
547
|
+
writeLine(io.stdout, ` ${style.accent(t.title)}`);
|
|
548
|
+
writeLine(io.stdout, ` ${style.dim(t.author + ' \u00b7 ' + ageH + 'h \u00b7 ' + t.score + '\u2191 \u00b7 ' + t.replyCount + ' replies')}`);
|
|
549
|
+
}
|
|
550
|
+
return 0;
|
|
551
|
+
}
|
|
552
|
+
const boardsResult = await communityBoardsSource(opts);
|
|
553
|
+
const boards = boardsResult.data.boards;
|
|
554
|
+
if (parsed.flags.json) {
|
|
555
|
+
writeJson(io.stdout, { boards });
|
|
556
|
+
return 0;
|
|
557
|
+
}
|
|
558
|
+
writeLine(io.stdout, header('agora community', [`${boards.length} boards`]));
|
|
559
|
+
writeLine(io.stdout, '');
|
|
560
|
+
for (const b of boards) {
|
|
561
|
+
writeLine(io.stdout, ` ${style.accent('/' + b.id.padEnd(12))} ${style.dim(b.threadCount + ' threads, ' + b.newToday + ' new today')}`);
|
|
562
|
+
}
|
|
563
|
+
writeLine(io.stdout, '');
|
|
564
|
+
writeLine(io.stdout, style.dim('Run `agora community <board>` to see threads.'));
|
|
565
|
+
return 0;
|
|
566
|
+
}
|
|
567
|
+
async function commandThread(parsed, io) {
|
|
568
|
+
const id = parsed.args[0];
|
|
569
|
+
if (!id)
|
|
570
|
+
return usageError(io, 'thread requires a thread id');
|
|
571
|
+
const opts = sourceOptions(parsed, io);
|
|
572
|
+
const result = await communityThreadSource(opts, id);
|
|
573
|
+
const { thread, replies } = result.data;
|
|
574
|
+
if (!thread)
|
|
575
|
+
return usageError(io, `Thread not found: ${id}`);
|
|
576
|
+
if (parsed.flags.json) {
|
|
577
|
+
writeJson(io.stdout, { thread, replies });
|
|
578
|
+
return 0;
|
|
579
|
+
}
|
|
580
|
+
const ageH = Math.round((Date.now() - new Date(thread.createdAt).getTime()) / 3600000);
|
|
581
|
+
writeLine(io.stdout, style.bold(thread.title));
|
|
582
|
+
writeLine(io.stdout, `${style.dim(thread.author + ' \u00b7 ' + ageH + 'h \u00b7 ' + thread.score + '\u2191 \u00b7 ' + thread.replyCount + ' replies')}`);
|
|
583
|
+
writeLine(io.stdout, '');
|
|
584
|
+
writeLine(io.stdout, thread.content);
|
|
585
|
+
if (replies.length > 0) {
|
|
586
|
+
writeLine(io.stdout, '');
|
|
587
|
+
writeLine(io.stdout, style.dim('--- replies ---'));
|
|
588
|
+
for (const r of replies) {
|
|
589
|
+
const rAgeH = Math.round((Date.now() - new Date(r.createdAt).getTime()) / 3600000);
|
|
590
|
+
writeLine(io.stdout, ` ${style.dim(r.author + ' \u00b7 ' + rAgeH + 'h')} ${style.accent(r.score + '\u2191')}`);
|
|
591
|
+
writeLine(io.stdout, ` ${r.content}`);
|
|
592
|
+
}
|
|
593
|
+
}
|
|
594
|
+
return 0;
|
|
595
|
+
}
|
|
596
|
+
async function commandPost(parsed, io) {
|
|
597
|
+
const source = writeSourceOptions(parsed, io);
|
|
598
|
+
if (!source.ok)
|
|
599
|
+
return usageError(io, source.error);
|
|
600
|
+
const board = (stringFlag(parsed, 'board') || stringFlag(parsed, 'b'));
|
|
601
|
+
const title = requiredStringFlag(parsed, 'title');
|
|
602
|
+
const content = contentInput(parsed, io);
|
|
603
|
+
if (!board || !title || !content) {
|
|
604
|
+
return usageError(io, 'post requires --board, --title and --content or --content-file');
|
|
605
|
+
}
|
|
606
|
+
const result = await createThreadSource(source.options, { board, title, content });
|
|
607
|
+
if (parsed.flags.json) {
|
|
608
|
+
writeJson(io.stdout, sourcePayload(result, { thread: result.data.thread }));
|
|
609
|
+
return 0;
|
|
610
|
+
}
|
|
611
|
+
writeLine(io.stdout, `Posted to /${board}: ${style.accent(result.data.thread?.title || title)}`);
|
|
612
|
+
writeLine(io.stdout, `${sourceLabel(result)}`);
|
|
613
|
+
return 0;
|
|
614
|
+
}
|
|
615
|
+
async function commandReply(parsed, io) {
|
|
616
|
+
const parentId = parsed.args[0];
|
|
617
|
+
if (!parentId)
|
|
618
|
+
return usageError(io, 'reply requires a thread or reply id');
|
|
619
|
+
const source = writeSourceOptions(parsed, io);
|
|
620
|
+
if (!source.ok)
|
|
621
|
+
return usageError(io, source.error);
|
|
622
|
+
const content = contentInput(parsed, io);
|
|
623
|
+
if (!content)
|
|
624
|
+
return usageError(io, 'reply requires --content or --content-file');
|
|
625
|
+
const result = await createReplySource(source.options, parentId, {
|
|
626
|
+
content,
|
|
627
|
+
parentId: stringFlag(parsed, 'parentId'),
|
|
628
|
+
});
|
|
629
|
+
if (parsed.flags.json) {
|
|
630
|
+
writeJson(io.stdout, sourcePayload(result, { reply: result.data.reply }));
|
|
631
|
+
return 0;
|
|
632
|
+
}
|
|
633
|
+
writeLine(io.stdout, `Replied to ${style.accent(parentId)}`);
|
|
634
|
+
writeLine(io.stdout, `${sourceLabel(result)}`);
|
|
635
|
+
return 0;
|
|
636
|
+
}
|
|
637
|
+
async function commandVote(parsed, io) {
|
|
638
|
+
const id = parsed.args[0];
|
|
639
|
+
if (!id)
|
|
640
|
+
return usageError(io, 'vote requires a thread or reply id');
|
|
641
|
+
const source = writeSourceOptions(parsed, io);
|
|
642
|
+
if (!source.ok)
|
|
643
|
+
return usageError(io, source.error);
|
|
644
|
+
const up = parsed.flags.up === true;
|
|
645
|
+
const down = parsed.flags.down === true;
|
|
646
|
+
if (!up && !down)
|
|
647
|
+
return usageError(io, 'vote requires --up or --down');
|
|
648
|
+
const value = up ? 1 : -1;
|
|
649
|
+
const targetType = (stringFlag(parsed, 'type') || 'discussion');
|
|
650
|
+
await voteSource(source.options, id, { value, targetType });
|
|
651
|
+
if (parsed.flags.json) {
|
|
652
|
+
writeJson(io.stdout, { id, value, targetType, success: true });
|
|
653
|
+
return 0;
|
|
654
|
+
}
|
|
655
|
+
writeLine(io.stdout, up ? `Upvoted ${style.accent(id)}` : `Downvoted ${style.accent(id)}`);
|
|
656
|
+
return 0;
|
|
657
|
+
}
|
|
658
|
+
async function commandFlag(parsed, io) {
|
|
659
|
+
const id = parsed.args[0];
|
|
660
|
+
if (!id)
|
|
661
|
+
return usageError(io, 'flag requires an item id');
|
|
662
|
+
const flagMarketplace = (kind) => !kind || kind === 'package' || kind === 'workflow';
|
|
663
|
+
const reasonOpt = stringFlag(parsed, 'reason');
|
|
664
|
+
const validReasons = ['spam', 'harassment', 'undisclosed-llm', 'malicious', 'other'];
|
|
665
|
+
const reason = (reasonOpt && validReasons.includes(reasonOpt) ? reasonOpt : 'other');
|
|
666
|
+
const type = stringFlag(parsed, 'type') || 'discussion';
|
|
667
|
+
if (flagMarketplace(type)) {
|
|
668
|
+
writeLine(io.stdout, `Flagged ${style.accent(id)} for ${reason} (${type})`);
|
|
669
|
+
if (parsed.flags.json) {
|
|
670
|
+
writeJson(io.stdout, { id, reason, type, success: true });
|
|
671
|
+
}
|
|
672
|
+
return 0;
|
|
673
|
+
}
|
|
674
|
+
const source = writeSourceOptions(parsed, io);
|
|
675
|
+
if (!source.ok)
|
|
676
|
+
return usageError(io, source.error);
|
|
677
|
+
const targetType = (type === 'discussion' || type === 'reply' ? type : 'discussion');
|
|
678
|
+
await flagSource(source.options, id, { reason, targetType, notes: stringFlag(parsed, 'notes') });
|
|
679
|
+
if (parsed.flags.json) {
|
|
680
|
+
writeJson(io.stdout, { id, reason, targetType, success: true });
|
|
681
|
+
return 0;
|
|
682
|
+
}
|
|
683
|
+
writeLine(io.stdout, `Flagged ${style.accent(id)} for ${reason}`);
|
|
684
|
+
return 0;
|
|
685
|
+
}
|
|
217
686
|
async function commandTutorials(parsed, io) {
|
|
218
687
|
const query = parsed.args.join(' ');
|
|
219
688
|
const level = tutorialLevelFlag(parsed);
|
|
@@ -236,14 +705,22 @@ async function commandTutorials(parsed, io) {
|
|
|
236
705
|
writeLine(io.stdout, query ? `No tutorials match "${query}".` : 'No tutorials found.');
|
|
237
706
|
return 0;
|
|
238
707
|
}
|
|
239
|
-
writeLine(io.stdout,
|
|
708
|
+
writeLine(io.stdout, header('agora tutorials', [`${tutorials.length} results`, sourceLabel(result)]));
|
|
709
|
+
writeLine(io.stdout, '');
|
|
240
710
|
writeLine(io.stdout, formatTutorialList(tutorials));
|
|
241
711
|
return 0;
|
|
242
712
|
}
|
|
243
713
|
async function commandTutorial(parsed, io) {
|
|
244
714
|
const id = parsed.args[0];
|
|
245
|
-
if (!id)
|
|
246
|
-
|
|
715
|
+
if (!id) {
|
|
716
|
+
const { sampleTutorials } = await import('../data.js');
|
|
717
|
+
writeLine(io.stdout, header('agora tutorial', [`${sampleTutorials.length} available tutorials`]));
|
|
718
|
+
writeLine(io.stdout, '');
|
|
719
|
+
writeLine(io.stdout, sampleTutorials.map((t) => ` ${style.accent(t.id.padEnd(22))} ${style.dim(t.title)} ${style.dim('[' + t.level + ']')}`).join('\n'));
|
|
720
|
+
writeLine(io.stdout, '');
|
|
721
|
+
writeLine(io.stdout, style.dim('Run `agora tutorial <id>` to start a tutorial.'));
|
|
722
|
+
return 0;
|
|
723
|
+
}
|
|
247
724
|
const step = tutorialStepNumber(parsed);
|
|
248
725
|
if (!step.ok)
|
|
249
726
|
return usageError(io, step.error);
|
|
@@ -276,13 +753,14 @@ async function commandDiscussions(parsed, io) {
|
|
|
276
753
|
writeLine(io.stdout, 'No discussions found.');
|
|
277
754
|
return 0;
|
|
278
755
|
}
|
|
279
|
-
writeLine(io.stdout,
|
|
756
|
+
writeLine(io.stdout, header('agora discussions', [`${discussions.length} results`, sourceLabel(result)]));
|
|
757
|
+
writeLine(io.stdout, '');
|
|
280
758
|
writeLine(io.stdout, discussions
|
|
281
759
|
.map((discussion, index) => {
|
|
282
760
|
return [
|
|
283
|
-
`${index + 1}. ${discussion.title}
|
|
761
|
+
`${index + 1}. ${style.accent(discussion.title)} ${style.dim('[' + discussion.category + ']')}`,
|
|
284
762
|
` ${truncate(discussion.content, 88)}`,
|
|
285
|
-
` replies
|
|
763
|
+
` ${style.dim('replies ' + discussion.replies + ' · stars ' + discussion.stars + ' · by ' + discussion.author)}`
|
|
286
764
|
].join('\n');
|
|
287
765
|
})
|
|
288
766
|
.join('\n\n'));
|
|
@@ -309,8 +787,8 @@ async function commandDiscuss(parsed, io) {
|
|
|
309
787
|
writeJson(io.stdout, sourcePayload(result, { discussion: result.data }));
|
|
310
788
|
return 0;
|
|
311
789
|
}
|
|
312
|
-
writeLine(io.stdout, `Created discussion ${result.data.id}`);
|
|
313
|
-
writeLine(io.stdout, `${result.data.title} (${result
|
|
790
|
+
writeLine(io.stdout, `Created discussion ${style.accent(result.data.id)}`);
|
|
791
|
+
writeLine(io.stdout, `${result.data.title} (${sourceLabel(result)})`);
|
|
314
792
|
return 0;
|
|
315
793
|
}
|
|
316
794
|
async function commandInstall(parsed, io) {
|
|
@@ -353,8 +831,8 @@ async function commandInstall(parsed, io) {
|
|
|
353
831
|
}
|
|
354
832
|
if (parsed.flags.write) {
|
|
355
833
|
writeOpenCodeConfig(configPath, plan.config);
|
|
356
|
-
writeLine(io.stdout, `Installed ${item.name}`);
|
|
357
|
-
writeLine(io.stdout,
|
|
834
|
+
writeLine(io.stdout, `Installed ${style.accent(item.name)}`);
|
|
835
|
+
writeLine(io.stdout, `${style.dim('Config')} ${configPath}`);
|
|
358
836
|
if (plan.commands.length) {
|
|
359
837
|
writeLine(io.stdout, 'Installing packages...');
|
|
360
838
|
for (const cmd of plan.commands) {
|
|
@@ -371,6 +849,16 @@ async function commandInstall(parsed, io) {
|
|
|
371
849
|
}
|
|
372
850
|
writeLine(io.stdout, `Install preview: ${item.name}`);
|
|
373
851
|
writeLine(io.stdout, `Target config: ${configPath}`);
|
|
852
|
+
const perms = item.permissions;
|
|
853
|
+
if (perms) {
|
|
854
|
+
writeLine(io.stdout, '\nDeclared permissions:');
|
|
855
|
+
if (perms.fs?.length)
|
|
856
|
+
writeLine(io.stdout, ` ${style.dim('filesystem')} ${perms.fs.join(', ')}`);
|
|
857
|
+
if (perms.net?.length)
|
|
858
|
+
writeLine(io.stdout, ` ${style.dim('network')} ${perms.net.join(', ')}`);
|
|
859
|
+
if (perms.exec?.length)
|
|
860
|
+
writeLine(io.stdout, ` ${style.dim('exec')} ${perms.exec.join(', ')}`);
|
|
861
|
+
}
|
|
374
862
|
if (plan.commands.length) {
|
|
375
863
|
writeLine(io.stdout, '\nCommands:');
|
|
376
864
|
writeLine(io.stdout, plan.commands.join('\n'));
|
|
@@ -380,11 +868,191 @@ async function commandInstall(parsed, io) {
|
|
|
380
868
|
writeLine(io.stdout, '\nRun with --write to update the config file and install packages.');
|
|
381
869
|
return 0;
|
|
382
870
|
}
|
|
871
|
+
async function commandMcp(_parsed, io) {
|
|
872
|
+
const { runMcpServer } = await import('./mcp-server.js');
|
|
873
|
+
try {
|
|
874
|
+
await runMcpServer();
|
|
875
|
+
}
|
|
876
|
+
catch (error) {
|
|
877
|
+
writeLine(io.stderr, error instanceof Error ? error.message : String(error));
|
|
878
|
+
return 1;
|
|
879
|
+
}
|
|
880
|
+
return 0;
|
|
881
|
+
}
|
|
882
|
+
export const FREE_MODELS = ['deepseek-v4-flash-free', 'minimax-m2.5-free', 'nemotron-3-super-free'];
|
|
883
|
+
/** Regexp to extract session ID from a JSON opencode event line. */
|
|
884
|
+
function extractSessionId(line) {
|
|
885
|
+
try {
|
|
886
|
+
const ev = JSON.parse(line);
|
|
887
|
+
if (ev.sessionID && typeof ev.sessionID === 'string')
|
|
888
|
+
return ev.sessionID;
|
|
889
|
+
}
|
|
890
|
+
catch {
|
|
891
|
+
/* not JSON, skip */
|
|
892
|
+
}
|
|
893
|
+
return null;
|
|
894
|
+
}
|
|
895
|
+
/**
|
|
896
|
+
* Persist the most recent chat session ID to the Agora state file so
|
|
897
|
+
* `--continue` can pick it up.
|
|
898
|
+
*/
|
|
899
|
+
function persistChatSession(dataDir, sessionId) {
|
|
900
|
+
try {
|
|
901
|
+
const state = loadAgoraState(dataDir);
|
|
902
|
+
const updated = {
|
|
903
|
+
...state,
|
|
904
|
+
_meta: { ...(state._meta || {}), lastChatSession: sessionId }
|
|
905
|
+
};
|
|
906
|
+
writeAgoraState(dataDir, updated);
|
|
907
|
+
}
|
|
908
|
+
catch {
|
|
909
|
+
// best-effort
|
|
910
|
+
}
|
|
911
|
+
}
|
|
912
|
+
function loadLastChatSession(dataDir) {
|
|
913
|
+
try {
|
|
914
|
+
const state = loadAgoraState(dataDir);
|
|
915
|
+
return state._meta?.lastChatSession;
|
|
916
|
+
}
|
|
917
|
+
catch {
|
|
918
|
+
return undefined;
|
|
919
|
+
}
|
|
920
|
+
}
|
|
921
|
+
async function commandChat(parsed, io) {
|
|
922
|
+
const message = parsed.args.join(' ');
|
|
923
|
+
const model = stringFlag(parsed, 'model', 'm') || FREE_MODELS[0];
|
|
924
|
+
const continueMode = parsed.flags.continue === true;
|
|
925
|
+
const explicitSession = stringFlag(parsed, 'session', 's');
|
|
926
|
+
const rawJson = parsed.flags.json === true;
|
|
927
|
+
const modelArg = model.includes('/') ? model : `opencode/${model}`;
|
|
928
|
+
if (!message) {
|
|
929
|
+
// TUI mode — hand off to opencode with inherit stdio
|
|
930
|
+
process.stderr.write(`Agora Chat (${model}) — press Ctrl+C to exit.\n`);
|
|
931
|
+
const child = spawn('opencode', ['--model', modelArg], {
|
|
932
|
+
env: io.env,
|
|
933
|
+
stdio: 'inherit',
|
|
934
|
+
shell: false
|
|
935
|
+
});
|
|
936
|
+
return new Promise((resolve) => {
|
|
937
|
+
child.on('close', (code) => resolve(code ?? 0));
|
|
938
|
+
child.on('error', (err) => {
|
|
939
|
+
writeLine(io.stderr, `Failed to run opencode: ${err.message}`);
|
|
940
|
+
resolve(1);
|
|
941
|
+
});
|
|
942
|
+
});
|
|
943
|
+
}
|
|
944
|
+
// One-shot mode — single message via opencode run
|
|
945
|
+
return new Promise((resolve) => {
|
|
946
|
+
const args = ['run', '--format', 'json'];
|
|
947
|
+
args.push('--model', modelArg);
|
|
948
|
+
if (explicitSession) {
|
|
949
|
+
args.push('--session', explicitSession);
|
|
950
|
+
}
|
|
951
|
+
else if (continueMode) {
|
|
952
|
+
const dataDir = detectDataDir(parsed, io);
|
|
953
|
+
const lastSession = loadLastChatSession(dataDir);
|
|
954
|
+
if (lastSession) {
|
|
955
|
+
args.push('--session', lastSession);
|
|
956
|
+
}
|
|
957
|
+
else {
|
|
958
|
+
args.push('--continue');
|
|
959
|
+
}
|
|
960
|
+
}
|
|
961
|
+
args.push(message);
|
|
962
|
+
const stderrChunks = [];
|
|
963
|
+
let sessionId = null;
|
|
964
|
+
let wroteNewline = false;
|
|
965
|
+
const child = spawn('opencode', args, {
|
|
966
|
+
env: io.env,
|
|
967
|
+
stdio: ['ignore', 'pipe', 'pipe'],
|
|
968
|
+
shell: false
|
|
969
|
+
});
|
|
970
|
+
child.stdout?.on('data', (chunk) => {
|
|
971
|
+
const text = chunk.toString();
|
|
972
|
+
for (const line of text.split('\n').filter(Boolean)) {
|
|
973
|
+
if (!sessionId)
|
|
974
|
+
sessionId = extractSessionId(line);
|
|
975
|
+
if (rawJson) {
|
|
976
|
+
process.stdout.write(line + '\n');
|
|
977
|
+
continue;
|
|
978
|
+
}
|
|
979
|
+
try {
|
|
980
|
+
const ev = JSON.parse(line);
|
|
981
|
+
if (ev.type === 'text' && ev.part?.text) {
|
|
982
|
+
process.stdout.write(ev.part.text);
|
|
983
|
+
wroteNewline = false;
|
|
984
|
+
}
|
|
985
|
+
if (ev.type === 'step_finish') {
|
|
986
|
+
const tokens = ev.part?.tokens;
|
|
987
|
+
if (tokens && !rawJson) {
|
|
988
|
+
const cost = typeof tokens.cost === 'number' ? ` · $${tokens.cost.toFixed(6)}` : '';
|
|
989
|
+
process.stdout.write(`\n\x1b[2m[${tokens.output} tokens${cost}]\x1b[0m\n`);
|
|
990
|
+
wroteNewline = true;
|
|
991
|
+
}
|
|
992
|
+
}
|
|
993
|
+
}
|
|
994
|
+
catch {
|
|
995
|
+
/* skip malformed lines */
|
|
996
|
+
}
|
|
997
|
+
}
|
|
998
|
+
});
|
|
999
|
+
child.stderr?.on('data', (chunk) => {
|
|
1000
|
+
stderrChunks.push(chunk.toString());
|
|
1001
|
+
});
|
|
1002
|
+
child.on('close', (code) => {
|
|
1003
|
+
if (!wroteNewline)
|
|
1004
|
+
process.stdout.write('\n');
|
|
1005
|
+
const dataDir = detectDataDir(parsed, io);
|
|
1006
|
+
appendHistory(dataDir, {
|
|
1007
|
+
type: 'chat',
|
|
1008
|
+
query: message,
|
|
1009
|
+
timestamp: new Date().toISOString(),
|
|
1010
|
+
model,
|
|
1011
|
+
});
|
|
1012
|
+
if (sessionId) {
|
|
1013
|
+
persistChatSession(dataDir, sessionId);
|
|
1014
|
+
if (!rawJson) {
|
|
1015
|
+
process.stdout.write(`\x1b[2mSession: ${sessionId.slice(0, 24)}… `);
|
|
1016
|
+
process.stdout.write(`Continue: agora chat --session ${sessionId} "..."\x1b[0m\n`);
|
|
1017
|
+
}
|
|
1018
|
+
}
|
|
1019
|
+
if (code !== 0) {
|
|
1020
|
+
const errText = stderrChunks.join('');
|
|
1021
|
+
const modelError = errText.match(/Model not found:.*?Did you mean:\s*(.+?)\?/);
|
|
1022
|
+
if (modelError) {
|
|
1023
|
+
const suggestions = modelError[1];
|
|
1024
|
+
writeLine(io.stderr, `\nModel not available. Try: ${suggestions}`);
|
|
1025
|
+
writeLine(io.stderr, `Example: agora chat -m deepseek-v4-flash-free "your question"`);
|
|
1026
|
+
}
|
|
1027
|
+
else if (errText.includes('not found')) {
|
|
1028
|
+
writeLine(io.stderr, '\n' + errText.replace(/^.*?ERROR\s+/gm, '').trim());
|
|
1029
|
+
}
|
|
1030
|
+
}
|
|
1031
|
+
resolve(code ?? 0);
|
|
1032
|
+
});
|
|
1033
|
+
child.on('error', (err) => {
|
|
1034
|
+
writeLine(io.stderr, `Failed to run opencode: ${err.message}`);
|
|
1035
|
+
writeLine(io.stderr, 'Is opencode installed and in your PATH?');
|
|
1036
|
+
resolve(1);
|
|
1037
|
+
});
|
|
1038
|
+
});
|
|
1039
|
+
}
|
|
383
1040
|
async function commandInit(parsed, io) {
|
|
384
1041
|
const cwd = io.cwd || process.cwd();
|
|
385
1042
|
const scan = scanProject(cwd);
|
|
386
1043
|
const plan = generateInitPlan(scan);
|
|
387
1044
|
const configPath = detectOpenCodeConfigPath({ cwd, env: io.env });
|
|
1045
|
+
const withMcp = parsed.flags.mcp === true;
|
|
1046
|
+
if (withMcp) {
|
|
1047
|
+
plan.config.mcp = plan.config.mcp || {};
|
|
1048
|
+
plan.config.mcp.agora = {
|
|
1049
|
+
type: 'local',
|
|
1050
|
+
command: ['agora', 'mcp'],
|
|
1051
|
+
enabled: true
|
|
1052
|
+
};
|
|
1053
|
+
plan.servers.push('agora');
|
|
1054
|
+
plan.notes.push('Agora MCP server registered — OpenCode can discover marketplace tools.');
|
|
1055
|
+
}
|
|
388
1056
|
if (parsed.flags.json) {
|
|
389
1057
|
if (parsed.flags.dryRun) {
|
|
390
1058
|
writeJson(io.stdout, {
|
|
@@ -393,11 +1061,13 @@ async function commandInit(parsed, io) {
|
|
|
393
1061
|
config: plan.config,
|
|
394
1062
|
servers: plan.servers,
|
|
395
1063
|
commands: plan.commands,
|
|
1064
|
+
slashCommand: join(cwd, '.opencode', 'command', 'agora.md'),
|
|
396
1065
|
dryRun: true
|
|
397
1066
|
});
|
|
398
1067
|
return 0;
|
|
399
1068
|
}
|
|
400
1069
|
applyInitPlan(plan, configPath);
|
|
1070
|
+
const commandPath = installAgoraCommand(cwd);
|
|
401
1071
|
const installResults = plan.commands.length ? runCommands(plan.commands) : [];
|
|
402
1072
|
const installed = installResults.filter((r) => r.ok).length;
|
|
403
1073
|
const failed = installResults.filter((r) => !r.ok).length;
|
|
@@ -407,6 +1077,7 @@ async function commandInit(parsed, io) {
|
|
|
407
1077
|
config: plan.config,
|
|
408
1078
|
servers: plan.servers,
|
|
409
1079
|
commands: plan.commands,
|
|
1080
|
+
slashCommand: commandPath,
|
|
410
1081
|
installResults,
|
|
411
1082
|
installed,
|
|
412
1083
|
failed
|
|
@@ -414,28 +1085,54 @@ async function commandInit(parsed, io) {
|
|
|
414
1085
|
return 0;
|
|
415
1086
|
}
|
|
416
1087
|
writeLine(io.stdout, `Scanning ${cwd}...`);
|
|
417
|
-
writeLine(io.stdout, ` Project type
|
|
1088
|
+
writeLine(io.stdout, ` ${style.dim('Project type')} ${scan.type}`);
|
|
418
1089
|
if (scan.frameworks.length)
|
|
419
|
-
writeLine(io.stdout, ` Frameworks
|
|
1090
|
+
writeLine(io.stdout, ` ${style.dim('Frameworks')} ${scan.frameworks.join(', ')}`);
|
|
420
1091
|
if (scan.hasDocker)
|
|
421
|
-
writeLine(io.stdout, '
|
|
1092
|
+
writeLine(io.stdout, ` ${style.dim('Docker')} detected`);
|
|
422
1093
|
if (scan.hasTests)
|
|
423
|
-
writeLine(io.stdout, '
|
|
1094
|
+
writeLine(io.stdout, ` ${style.dim('Tests')} detected`);
|
|
424
1095
|
if (scan.hasDatabase)
|
|
425
|
-
writeLine(io.stdout, '
|
|
1096
|
+
writeLine(io.stdout, ` ${style.dim('Database')} detected`);
|
|
426
1097
|
if (!parsed.flags.dryRun) {
|
|
427
1098
|
applyInitPlan(plan, configPath);
|
|
428
1099
|
writeLine(io.stdout, `\nWrote config to ${configPath}`);
|
|
1100
|
+
const commandPath = installAgoraCommand(cwd);
|
|
1101
|
+
writeLine(io.stdout, `Installed /agora slash command at ${commandPath}`);
|
|
429
1102
|
if (plan.commands.length) {
|
|
430
1103
|
writeLine(io.stdout, '\nInstalling MCP server packages...');
|
|
431
|
-
const
|
|
1104
|
+
const isTTY = Boolean(io.stdout.isTTY);
|
|
1105
|
+
const n = plan.commands.length;
|
|
1106
|
+
const installResults = [];
|
|
1107
|
+
for (let i = 0; i < n; i++) {
|
|
1108
|
+
const [result] = runCommands([plan.commands[i]]);
|
|
1109
|
+
installResults.push(result);
|
|
1110
|
+
if (isTTY && n > 1) {
|
|
1111
|
+
const pct = ((i + 1) / n) * 100;
|
|
1112
|
+
const bar = renderMeander({
|
|
1113
|
+
trueColor: supportsTrueColor(io.env ?? {}),
|
|
1114
|
+
mode: 'progress',
|
|
1115
|
+
pct
|
|
1116
|
+
});
|
|
1117
|
+
const line = ` ${bar}`;
|
|
1118
|
+
if (i < n - 1) {
|
|
1119
|
+
process.stdout.write(`\r\x1b[K${line}`);
|
|
1120
|
+
}
|
|
1121
|
+
else {
|
|
1122
|
+
process.stdout.write(`\r\x1b[K${line}\n`);
|
|
1123
|
+
}
|
|
1124
|
+
}
|
|
1125
|
+
}
|
|
432
1126
|
const installed = installResults.filter((r) => r.ok).length;
|
|
433
1127
|
const failed = installResults.filter((r) => !r.ok).length;
|
|
434
1128
|
writeLine(io.stdout, ` Installed ${installed} of ${plan.commands.length} packages${failed ? ` (${failed} failed)` : ''}`);
|
|
435
1129
|
}
|
|
436
1130
|
writeLine(io.stdout, '\n✓ Agora initialized! Restart OpenCode to pick up the changes.');
|
|
437
1131
|
writeLine(io.stdout, ' Plugin "opencode-agora" is now registered in your config.');
|
|
1132
|
+
writeLine(io.stdout, ' Type `/agora` in OpenCode to use the marketplace.');
|
|
438
1133
|
writeLine(io.stdout, ` ${plan.servers.length} MCP servers configured.`);
|
|
1134
|
+
if (withMcp)
|
|
1135
|
+
writeLine(io.stdout, ' Agora MCP server registered — `agora mcp` is available as an MCP tool.');
|
|
439
1136
|
if (plan.workflows.length)
|
|
440
1137
|
writeLine(io.stdout, ` ${plan.workflows.length} workflows available via \`agora use\`.`);
|
|
441
1138
|
for (const note of plan.notes)
|
|
@@ -445,6 +1142,7 @@ async function commandInit(parsed, io) {
|
|
|
445
1142
|
writeLine(io.stdout, '\n--- Dry run ---');
|
|
446
1143
|
writeLine(io.stdout, `Target config: ${configPath}`);
|
|
447
1144
|
writeLine(io.stdout, formatConfigJson(plan.config));
|
|
1145
|
+
writeLine(io.stdout, `\nSlash command: ${join(cwd, '.opencode', 'command', 'agora.md')}`);
|
|
448
1146
|
writeLine(io.stdout, '\nPackages to install:');
|
|
449
1147
|
for (const cmd of plan.commands)
|
|
450
1148
|
writeLine(io.stdout, ` ${cmd}`);
|
|
@@ -454,8 +1152,14 @@ async function commandInit(parsed, io) {
|
|
|
454
1152
|
}
|
|
455
1153
|
async function commandUse(parsed, io) {
|
|
456
1154
|
const id = parsed.args[0];
|
|
457
|
-
if (!id)
|
|
458
|
-
|
|
1155
|
+
if (!id) {
|
|
1156
|
+
writeLine(io.stdout, header('agora use', [`${sampleWorkflows.length} available workflows`]));
|
|
1157
|
+
writeLine(io.stdout, '');
|
|
1158
|
+
writeLine(io.stdout, sampleWorkflows.map((wf) => ` ${style.accent(wf.id.padEnd(22))} ${style.dim(wf.name)}`).join('\n'));
|
|
1159
|
+
writeLine(io.stdout, '');
|
|
1160
|
+
writeLine(io.stdout, style.dim('Run `agora use <id>` to apply a workflow as a skill.'));
|
|
1161
|
+
return 0;
|
|
1162
|
+
}
|
|
459
1163
|
const workflow = sampleWorkflows.find((w) => w.id === id || w.name.toLowerCase() === id.toLowerCase());
|
|
460
1164
|
if (!workflow)
|
|
461
1165
|
return usageError(io, `Workflow not found: ${id}. Run \`agora workflows\` to see available workflows.`);
|
|
@@ -510,51 +1214,133 @@ function commandConfig(parsed, io) {
|
|
|
510
1214
|
writeJson(io.stdout, report);
|
|
511
1215
|
return report.valid ? 0 : 1;
|
|
512
1216
|
}
|
|
513
|
-
writeLine(io.stdout,
|
|
514
|
-
writeLine(io.stdout,
|
|
515
|
-
writeLine(io.stdout,
|
|
1217
|
+
writeLine(io.stdout, `${style.dim('Config path')} ${report.path}`);
|
|
1218
|
+
writeLine(io.stdout, `${style.dim('Exists')} ${report.exists ? 'yes' : 'no'}`);
|
|
1219
|
+
writeLine(io.stdout, `${style.dim('Valid')} ${report.valid ? 'yes' : 'no'}`);
|
|
516
1220
|
if (report.error)
|
|
517
|
-
writeLine(io.stdout,
|
|
518
|
-
writeLine(io.stdout,
|
|
519
|
-
writeLine(io.stdout,
|
|
520
|
-
writeLine(io.stdout,
|
|
1221
|
+
writeLine(io.stdout, `${style.dim('Error')} ${report.error}`);
|
|
1222
|
+
writeLine(io.stdout, `${style.dim('MCP servers')} ${report.mcpServers}`);
|
|
1223
|
+
writeLine(io.stdout, `${style.dim('Plugins')} ${report.plugins}`);
|
|
1224
|
+
writeLine(io.stdout, `${style.dim('Packages')} ${report.packages.length ? report.packages.join(', ') : 'none'}`);
|
|
521
1225
|
return report.valid ? 0 : 1;
|
|
522
1226
|
}
|
|
523
|
-
function commandAuth(parsed, io) {
|
|
1227
|
+
async function commandAuth(parsed, io) {
|
|
524
1228
|
const subcommand = parsed.args[0] || 'status';
|
|
525
1229
|
const dataDir = detectDataDir(parsed, io);
|
|
526
1230
|
const state = loadAgoraState(dataDir);
|
|
527
1231
|
const existingAuth = getAuthState(state);
|
|
528
1232
|
if (subcommand === 'login') {
|
|
529
|
-
const
|
|
530
|
-
if (
|
|
531
|
-
|
|
1233
|
+
const explicitToken = authTokenInput(parsed, io);
|
|
1234
|
+
if (explicitToken) {
|
|
1235
|
+
// Token-paste flow (existing behaviour, for CI/automation)
|
|
1236
|
+
const apiUrl = stringFlag(parsed, 'apiUrl') || envString(io, 'AGORA_API_URL') || existingAuth?.apiUrl;
|
|
1237
|
+
const nextState = setAuthState(state, { token: explicitToken, apiUrl });
|
|
1238
|
+
const auth = getAuthState(nextState);
|
|
1239
|
+
writeAgoraState(dataDir, nextState);
|
|
1240
|
+
if (parsed.flags.json) {
|
|
1241
|
+
writeJson(io.stdout, authStatusPayload(dataDir, auth));
|
|
1242
|
+
return 0;
|
|
1243
|
+
}
|
|
1244
|
+
writeLine(io.stdout, 'Stored Agora API token');
|
|
1245
|
+
writeLine(io.stdout, `${style.dim('API URL')} ${auth?.apiUrl || 'not stored'}`);
|
|
1246
|
+
writeLine(io.stdout, `${style.dim('State')} ${getAgoraStatePath(dataDir)}`);
|
|
1247
|
+
return 0;
|
|
532
1248
|
}
|
|
1249
|
+
// ── Device-code flow ─────────────────────────────────────────────────
|
|
533
1250
|
const apiUrl = stringFlag(parsed, 'apiUrl') || envString(io, 'AGORA_API_URL') || existingAuth?.apiUrl;
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
1251
|
+
if (!apiUrl) {
|
|
1252
|
+
return usageError(io, 'auth login requires --api-url, AGORA_API_URL, or stored apiUrl');
|
|
1253
|
+
}
|
|
1254
|
+
const baseUrl = apiUrl.replace(/\/+$/, '');
|
|
1255
|
+
process.stdout.write(`\n${style.accent('Agora Login')}\n`);
|
|
1256
|
+
process.stdout.write(`${style.dim('Connecting to')} ${baseUrl}...\n`);
|
|
1257
|
+
try {
|
|
1258
|
+
const codeRes = await fetch(`${baseUrl}/auth/device/code`, { method: 'POST' });
|
|
1259
|
+
if (!codeRes.ok) {
|
|
1260
|
+
const err = await codeRes.json().catch(() => ({ error: 'request failed' }));
|
|
1261
|
+
return usageError(io, `Device code request failed: ${err.error || codeRes.status}`);
|
|
1262
|
+
}
|
|
1263
|
+
const codeData = (await codeRes.json());
|
|
1264
|
+
const verificationUri = codeData.verification_uri;
|
|
1265
|
+
const userCode = codeData.user_code;
|
|
1266
|
+
const deviceCode = codeData.device_code;
|
|
1267
|
+
const interval = (codeData.interval || 5) * 1000;
|
|
1268
|
+
process.stdout.write(`\n${style.accent(userCode.slice(0, 4) + ' ' + userCode.slice(4))}\n\n`);
|
|
1269
|
+
process.stdout.write(` ${style.dim('Open in your browser:')} ${verificationUri}\n`);
|
|
1270
|
+
process.stdout.write(` ${style.dim('Enter code:')} ${userCode}\n\n`);
|
|
1271
|
+
// Try to open browser automatically
|
|
1272
|
+
try {
|
|
1273
|
+
const url = `${verificationUri}`;
|
|
1274
|
+
if (process.platform === 'darwin') {
|
|
1275
|
+
execSync(`open '${url}'`, { timeout: 3000 });
|
|
1276
|
+
}
|
|
1277
|
+
else if (process.platform === 'linux') {
|
|
1278
|
+
execSync(`xdg-open '${url}'`, { timeout: 3000 });
|
|
1279
|
+
}
|
|
1280
|
+
process.stdout.write(` ${style.dim('Browser opened.')}\n\n`);
|
|
1281
|
+
}
|
|
1282
|
+
catch {
|
|
1283
|
+
process.stdout.write(` ${style.dim('Open the URL manually.')}\n\n`);
|
|
1284
|
+
}
|
|
1285
|
+
// Poll for token
|
|
1286
|
+
const pollStart = Date.now();
|
|
1287
|
+
const pollTimeout = 15 * 60 * 1000; // 15 minutes
|
|
1288
|
+
for (;;) {
|
|
1289
|
+
await new Promise((r) => setTimeout(r, interval));
|
|
1290
|
+
if (Date.now() - pollStart > pollTimeout) {
|
|
1291
|
+
return usageError(io, 'Login timed out. Run `agora auth login` to try again.');
|
|
1292
|
+
}
|
|
1293
|
+
process.stdout.write(`\r\x1b[K${style.dim('Waiting for browser authorization...')}`);
|
|
1294
|
+
try {
|
|
1295
|
+
const tokenRes = await fetch(`${baseUrl}/auth/device/token`, {
|
|
1296
|
+
method: 'POST',
|
|
1297
|
+
headers: { 'Content-Type': 'application/json' },
|
|
1298
|
+
body: JSON.stringify({ device_code: deviceCode })
|
|
1299
|
+
});
|
|
1300
|
+
if (tokenRes.ok) {
|
|
1301
|
+
const tokenData = (await tokenRes.json());
|
|
1302
|
+
const jwt = tokenData.access_token;
|
|
1303
|
+
process.stdout.write(`\r\x1b[K${style.dim('Authorization received.')}\n`);
|
|
1304
|
+
const nextState = setAuthState(state, { token: jwt, apiUrl });
|
|
1305
|
+
writeAgoraState(dataDir, nextState);
|
|
1306
|
+
if (parsed.flags.json) {
|
|
1307
|
+
writeJson(io.stdout, authStatusPayload(dataDir, getAuthState(nextState)));
|
|
1308
|
+
return 0;
|
|
1309
|
+
}
|
|
1310
|
+
process.stdout.write(`\n${style.accent('✓ Authenticated')}\n`);
|
|
1311
|
+
process.stdout.write(`${style.dim('API URL')} ${baseUrl}\n`);
|
|
1312
|
+
process.stdout.write(`${style.dim('Token expires')} in 1 hour\n`);
|
|
1313
|
+
process.stdout.write(`${style.dim('State')} ${getAgoraStatePath(dataDir)}\n`);
|
|
1314
|
+
return 0;
|
|
1315
|
+
}
|
|
1316
|
+
const errData = await tokenRes.json().catch(() => ({ error: 'unknown' }));
|
|
1317
|
+
if (errData.error === 'expired') {
|
|
1318
|
+
process.stdout.write(`\r\x1b[K`);
|
|
1319
|
+
return usageError(io, 'Code expired. Run `agora auth login` again.');
|
|
1320
|
+
}
|
|
1321
|
+
// "authorization_pending" is expected — keep polling
|
|
1322
|
+
}
|
|
1323
|
+
catch {
|
|
1324
|
+
// Network error, retry
|
|
1325
|
+
}
|
|
1326
|
+
}
|
|
1327
|
+
}
|
|
1328
|
+
catch (e) {
|
|
1329
|
+
return usageError(io, `Login failed: ${e.message || 'connection error'}`);
|
|
540
1330
|
}
|
|
541
|
-
writeLine(io.stdout, 'Stored Agora API token');
|
|
542
|
-
writeLine(io.stdout, `API URL: ${auth?.apiUrl || 'not stored'}`);
|
|
543
|
-
writeLine(io.stdout, `State: ${getAgoraStatePath(dataDir)}`);
|
|
544
|
-
return 0;
|
|
545
1331
|
}
|
|
546
1332
|
if (subcommand === 'status') {
|
|
547
1333
|
if (parsed.flags.json) {
|
|
548
1334
|
writeJson(io.stdout, authStatusPayload(dataDir, existingAuth));
|
|
549
1335
|
return 0;
|
|
550
1336
|
}
|
|
551
|
-
writeLine(io.stdout,
|
|
1337
|
+
writeLine(io.stdout, `${style.dim('Authenticated')} ${existingAuth ? 'yes' : 'no'}`);
|
|
552
1338
|
if (existingAuth) {
|
|
553
|
-
writeLine(io.stdout,
|
|
554
|
-
writeLine(io.stdout,
|
|
555
|
-
writeLine(io.stdout,
|
|
1339
|
+
writeLine(io.stdout, `${style.dim('Token')} ${maskToken(existingAuth.token)}`);
|
|
1340
|
+
writeLine(io.stdout, `${style.dim('API URL')} ${existingAuth.apiUrl || 'not stored'}`);
|
|
1341
|
+
writeLine(io.stdout, `${style.dim('Saved')} ${formatDate(existingAuth.savedAt)}`);
|
|
556
1342
|
}
|
|
557
|
-
writeLine(io.stdout,
|
|
1343
|
+
writeLine(io.stdout, `${style.dim('State')} ${getAgoraStatePath(dataDir)}`);
|
|
558
1344
|
return 0;
|
|
559
1345
|
}
|
|
560
1346
|
if (subcommand === 'logout') {
|
|
@@ -605,8 +1391,8 @@ async function commandSave(parsed, io) {
|
|
|
605
1391
|
});
|
|
606
1392
|
return 0;
|
|
607
1393
|
}
|
|
608
|
-
writeLine(io.stdout, result.added ? `Saved ${item.id}` : `${item.id} is already saved`);
|
|
609
|
-
writeLine(io.stdout,
|
|
1394
|
+
writeLine(io.stdout, result.added ? `Saved ${style.accent(item.id)}` : `${style.accent(item.id)} is already saved`);
|
|
1395
|
+
writeLine(io.stdout, `${style.dim('State')} ${getAgoraStatePath(dataDir)}`);
|
|
610
1396
|
return 0;
|
|
611
1397
|
}
|
|
612
1398
|
function commandSaved(parsed, io) {
|
|
@@ -628,7 +1414,7 @@ function commandSaved(parsed, io) {
|
|
|
628
1414
|
writeLine(io.stdout, 'Run agora save <id> to save a package or workflow.');
|
|
629
1415
|
return 0;
|
|
630
1416
|
}
|
|
631
|
-
writeLine(io.stdout,
|
|
1417
|
+
writeLine(io.stdout, header('agora saved', [`${saved.length} items`]));
|
|
632
1418
|
writeLine(io.stdout, formatSavedList(saved));
|
|
633
1419
|
return 0;
|
|
634
1420
|
}
|
|
@@ -655,7 +1441,7 @@ function commandRemove(parsed, io) {
|
|
|
655
1441
|
if (!result.removed) {
|
|
656
1442
|
return usageError(io, `Saved item not found: ${id}`);
|
|
657
1443
|
}
|
|
658
|
-
writeLine(io.stdout, `Removed ${targetId}`);
|
|
1444
|
+
writeLine(io.stdout, `Removed ${style.accent(targetId)}`);
|
|
659
1445
|
return 0;
|
|
660
1446
|
}
|
|
661
1447
|
async function commandPublish(parsed, io) {
|
|
@@ -691,8 +1477,8 @@ async function commandPublish(parsed, io) {
|
|
|
691
1477
|
writeJson(io.stdout, sourcePayload(result, { item: result.data }));
|
|
692
1478
|
return 0;
|
|
693
1479
|
}
|
|
694
|
-
writeLine(io.stdout, `Published package ${result.data.id}`);
|
|
695
|
-
writeLine(io.stdout, `${result.data.name} (${result
|
|
1480
|
+
writeLine(io.stdout, `Published package ${style.accent(result.data.id)}`);
|
|
1481
|
+
writeLine(io.stdout, `${result.data.name} (${sourceLabel(result)})`);
|
|
696
1482
|
return 0;
|
|
697
1483
|
}
|
|
698
1484
|
const prompt = promptInput(parsed, io);
|
|
@@ -711,8 +1497,8 @@ async function commandPublish(parsed, io) {
|
|
|
711
1497
|
writeJson(io.stdout, sourcePayload(result, { item: result.data }));
|
|
712
1498
|
return 0;
|
|
713
1499
|
}
|
|
714
|
-
writeLine(io.stdout, `Published workflow ${result.data.id}`);
|
|
715
|
-
writeLine(io.stdout, `${result.data.name} (${result
|
|
1500
|
+
writeLine(io.stdout, `Published workflow ${style.accent(result.data.id)}`);
|
|
1501
|
+
writeLine(io.stdout, `${result.data.name} (${sourceLabel(result)})`);
|
|
716
1502
|
return 0;
|
|
717
1503
|
}
|
|
718
1504
|
async function commandReview(parsed, io) {
|
|
@@ -737,8 +1523,8 @@ async function commandReview(parsed, io) {
|
|
|
737
1523
|
writeJson(io.stdout, sourcePayload(result, { review: result.data }));
|
|
738
1524
|
return 0;
|
|
739
1525
|
}
|
|
740
|
-
writeLine(io.stdout, `Reviewed ${result.data.itemId}`);
|
|
741
|
-
writeLine(io.stdout, `${result.data.rating
|
|
1526
|
+
writeLine(io.stdout, `Reviewed ${style.accent(result.data.itemId)}`);
|
|
1527
|
+
writeLine(io.stdout, `${style.dim(result.data.rating + '/5 by ' + result.data.author)}`);
|
|
742
1528
|
return 0;
|
|
743
1529
|
}
|
|
744
1530
|
async function commandReviews(parsed, io) {
|
|
@@ -755,7 +1541,8 @@ async function commandReviews(parsed, io) {
|
|
|
755
1541
|
writeLine(io.stdout, itemId ? `No reviews found for ${itemId}.` : 'No reviews found.');
|
|
756
1542
|
return 0;
|
|
757
1543
|
}
|
|
758
|
-
writeLine(io.stdout,
|
|
1544
|
+
writeLine(io.stdout, header('agora reviews', [`${result.data.length} results`, sourceLabel(result)]));
|
|
1545
|
+
writeLine(io.stdout, '');
|
|
759
1546
|
writeLine(io.stdout, formatReviewList(result.data));
|
|
760
1547
|
return 0;
|
|
761
1548
|
}
|
|
@@ -776,46 +1563,127 @@ async function commandProfile(parsed, io) {
|
|
|
776
1563
|
writeLine(io.stdout, formatProfileDetail(result.data));
|
|
777
1564
|
return 0;
|
|
778
1565
|
}
|
|
1566
|
+
async function commandPreferences(parsed, io) {
|
|
1567
|
+
const dataDir = detectDataDir(parsed, io);
|
|
1568
|
+
const prefs = loadPreferences(dataDir);
|
|
1569
|
+
const sub = parsed.args[0];
|
|
1570
|
+
if (!sub) {
|
|
1571
|
+
if (parsed.flags.json) {
|
|
1572
|
+
writeJson(io.stdout, prefs);
|
|
1573
|
+
return 0;
|
|
1574
|
+
}
|
|
1575
|
+
writeLine(io.stdout, `Preferences (${prefsPath(dataDir)})`);
|
|
1576
|
+
writeLine(io.stdout, ` theme: ${prefs.theme}`);
|
|
1577
|
+
writeLine(io.stdout, ` verbosity: ${prefs.verbosity}`);
|
|
1578
|
+
writeLine(io.stdout, ` username: ${prefs.username || '(not set)'}`);
|
|
1579
|
+
writeLine(io.stdout, ` email: ${prefs.email || '(not set)'}`);
|
|
1580
|
+
writeLine(io.stdout, ` bio: ${prefs.bio ? prefs.bio.slice(0, 60) + (prefs.bio.length > 60 ? '...' : '') : '(not set)'}`);
|
|
1581
|
+
writeLine(io.stdout, '');
|
|
1582
|
+
writeLine(io.stdout, ' Set values: agora preferences <key> <value>');
|
|
1583
|
+
writeLine(io.stdout, ' Keys: theme, verbosity, username, email, bio');
|
|
1584
|
+
return 0;
|
|
1585
|
+
}
|
|
1586
|
+
const key = sub;
|
|
1587
|
+
const val = parsed.args.slice(1).join(' ');
|
|
1588
|
+
if (!val || !(key in prefs)) {
|
|
1589
|
+
return usageError(io, `Usage: agora preferences <key> <value>\nValid keys: theme, verbosity, username, email, bio`);
|
|
1590
|
+
}
|
|
1591
|
+
if (key === 'theme' && !['dark', 'light', 'auto'].includes(val)) {
|
|
1592
|
+
return usageError(io, 'theme must be: dark, light, or auto');
|
|
1593
|
+
}
|
|
1594
|
+
if (key === 'verbosity' && !['verbose', 'medium', 'quiet'].includes(val)) {
|
|
1595
|
+
return usageError(io, 'verbosity must be: verbose, medium, or quiet');
|
|
1596
|
+
}
|
|
1597
|
+
prefs[key] = val;
|
|
1598
|
+
writePreferences(dataDir, prefs);
|
|
1599
|
+
writeLine(io.stdout, `\u2713 ${key} set to "${val}"`);
|
|
1600
|
+
return 0;
|
|
1601
|
+
}
|
|
1602
|
+
async function commandHistory(parsed, io) {
|
|
1603
|
+
const dataDir = detectDataDir(parsed, io);
|
|
1604
|
+
const limit = numberFlag(parsed, 'limit', 'n') || 50;
|
|
1605
|
+
if (parsed.flags.clear) {
|
|
1606
|
+
clearHistory(dataDir);
|
|
1607
|
+
writeLine(io.stdout, '\u2713 History cleared');
|
|
1608
|
+
return 0;
|
|
1609
|
+
}
|
|
1610
|
+
const entries = loadHistory(dataDir, limit);
|
|
1611
|
+
if (parsed.flags.json) {
|
|
1612
|
+
writeJson(io.stdout, entries);
|
|
1613
|
+
return 0;
|
|
1614
|
+
}
|
|
1615
|
+
if (entries.length === 0) {
|
|
1616
|
+
writeLine(io.stdout, 'No history yet.');
|
|
1617
|
+
writeLine(io.stdout, 'Searches and chat messages are recorded automatically.');
|
|
1618
|
+
return 0;
|
|
1619
|
+
}
|
|
1620
|
+
writeLine(io.stdout, `Recent history (${entries.length}):`);
|
|
1621
|
+
for (const entry of entries) {
|
|
1622
|
+
const icon = entry.type === 'search' ? '\uD83D\uDD0D' : '\uD83D\uDCAC';
|
|
1623
|
+
const date = new Date(entry.timestamp).toLocaleString();
|
|
1624
|
+
const query = entry.query.length > 60 ? entry.query.slice(0, 60) + '...' : entry.query;
|
|
1625
|
+
writeLine(io.stdout, ` ${icon} ${style.dim(date)} ${query}`);
|
|
1626
|
+
}
|
|
1627
|
+
writeLine(io.stdout, '');
|
|
1628
|
+
writeLine(io.stdout, style.dim('Use --clear to clear history, --json for JSON output.'));
|
|
1629
|
+
return 0;
|
|
1630
|
+
}
|
|
779
1631
|
function formatItemList(items) {
|
|
1632
|
+
const idWidth = Math.max(...items.map((item) => item.id.length));
|
|
780
1633
|
return items
|
|
781
|
-
.map((item
|
|
782
|
-
const
|
|
1634
|
+
.map((item) => {
|
|
1635
|
+
const metrics = item.kind === 'package'
|
|
1636
|
+
? `${formatNumber(item.installs)} installs · ${formatNumber(item.stars)} ★`
|
|
1637
|
+
: `${formatNumber(item.stars)} ★`;
|
|
783
1638
|
return [
|
|
784
|
-
`${
|
|
785
|
-
|
|
786
|
-
|
|
787
|
-
|
|
1639
|
+
`${style.accent(item.id.padEnd(idWidth))} ${style.dim(metrics)}`,
|
|
1640
|
+
style.dim(item.name),
|
|
1641
|
+
truncate(item.description, 88),
|
|
1642
|
+
style.dim(`${item.category} · by ${item.author}`)
|
|
788
1643
|
].join('\n');
|
|
789
1644
|
})
|
|
790
1645
|
.join('\n\n');
|
|
791
1646
|
}
|
|
1647
|
+
function formatItemTable(items) {
|
|
1648
|
+
const idW = Math.max(4, ...items.map((i) => i.id.length));
|
|
1649
|
+
const nameW = Math.max(4, ...items.map((i) => i.name.length));
|
|
1650
|
+
const starW = 6;
|
|
1651
|
+
const installW = 9;
|
|
1652
|
+
const totalW = idW + 3 + nameW + 3 + starW + 3 + installW + 4;
|
|
1653
|
+
const top = '┌' + '─'.repeat(totalW - 2) + '┐';
|
|
1654
|
+
const bot = '└' + '─'.repeat(totalW - 2) + '┘';
|
|
1655
|
+
const sep = '│';
|
|
1656
|
+
const hdr = sep + ' ' + 'id'.padEnd(idW) + ' │ ' + 'name'.padEnd(nameW) + ' │ ' + 'stars'.padStart(starW) + ' │ ' + 'installs'.padStart(installW) + ' │';
|
|
1657
|
+
const rows = items.map((item) => sep + ' ' + style.accent(item.id.padEnd(idW)) + ' │ ' + style.dim(item.name.padEnd(nameW)) + ' │ ' + style.dim(formatNumber(item.stars).padStart(starW)) + ' │ ' + style.dim(formatNumber(item.installs).padStart(installW)) + ' │');
|
|
1658
|
+
return [top, hdr, ...rows, bot].join('\n');
|
|
1659
|
+
}
|
|
792
1660
|
function formatItemDetail(item) {
|
|
793
1661
|
const lines = [
|
|
794
|
-
|
|
795
|
-
|
|
796
|
-
|
|
797
|
-
|
|
798
|
-
|
|
799
|
-
|
|
800
|
-
|
|
1662
|
+
style.bold(item.name),
|
|
1663
|
+
`${style.dim('id')} ${style.accent(item.id)}`,
|
|
1664
|
+
`${style.dim('type')} ${item.kind}`,
|
|
1665
|
+
`${style.dim('category')} ${item.category}`,
|
|
1666
|
+
`${style.dim('author')} ${item.author}`,
|
|
1667
|
+
`${style.dim('stars')} ${formatNumber(item.stars)}`,
|
|
1668
|
+
`${style.dim('install')} ${getInstallKind(item)}`,
|
|
801
1669
|
'',
|
|
802
1670
|
item.description,
|
|
803
1671
|
'',
|
|
804
|
-
|
|
1672
|
+
`${style.dim('tags')} ${item.tags.join(', ')}`
|
|
805
1673
|
];
|
|
806
1674
|
if (item.kind === 'package') {
|
|
807
|
-
lines.splice(5, 0,
|
|
808
|
-
lines.push(
|
|
1675
|
+
lines.splice(5, 0, `${style.dim('version')} ${item.version}`);
|
|
1676
|
+
lines.push(`${style.dim('installs')} ${formatNumber(item.installs)}`);
|
|
809
1677
|
if (item.repository)
|
|
810
|
-
lines.push(
|
|
1678
|
+
lines.push(`${style.dim('repo')} ${item.repository}`);
|
|
811
1679
|
if (item.npmPackage)
|
|
812
|
-
lines.push(
|
|
1680
|
+
lines.push(`${style.dim('npm')} ${item.npmPackage}`);
|
|
813
1681
|
}
|
|
814
1682
|
if (item.kind === 'workflow') {
|
|
815
|
-
lines.push(
|
|
1683
|
+
lines.push(`${style.dim('forks')} ${item.forks}`);
|
|
816
1684
|
if (item.model)
|
|
817
|
-
lines.push(
|
|
818
|
-
lines.push('', 'prompt
|
|
1685
|
+
lines.push(`${style.dim('model')} ${item.model}`);
|
|
1686
|
+
lines.push('', style.dim('prompt'), item.prompt);
|
|
819
1687
|
}
|
|
820
1688
|
return lines.join('\n');
|
|
821
1689
|
}
|
|
@@ -824,15 +1692,15 @@ function formatSavedList(items) {
|
|
|
824
1692
|
.map((entry, index) => {
|
|
825
1693
|
if (!entry.item) {
|
|
826
1694
|
return [
|
|
827
|
-
`${index + 1}. ${entry.saved.id} [missing]`,
|
|
828
|
-
` saved
|
|
1695
|
+
`${index + 1}. ${style.accent(entry.saved.id)} ${style.dim('[missing]')}`,
|
|
1696
|
+
` ${style.dim('saved ' + formatDate(entry.saved.savedAt))}`
|
|
829
1697
|
].join('\n');
|
|
830
1698
|
}
|
|
831
1699
|
return [
|
|
832
|
-
`${index + 1}. ${entry.item.id}
|
|
833
|
-
` ${entry.item.name}`,
|
|
1700
|
+
`${index + 1}. ${style.accent(entry.item.id)} ${style.dim('[' + entry.item.category + ']')}`,
|
|
1701
|
+
` ${style.dim(entry.item.name)}`,
|
|
834
1702
|
` ${truncate(entry.item.description, 88)}`,
|
|
835
|
-
` saved
|
|
1703
|
+
` ${style.dim('saved ' + formatDate(entry.saved.savedAt))}`
|
|
836
1704
|
].join('\n');
|
|
837
1705
|
})
|
|
838
1706
|
.join('\n\n');
|
|
@@ -841,8 +1709,8 @@ function formatReviewList(reviews) {
|
|
|
841
1709
|
return reviews
|
|
842
1710
|
.map((review, index) => {
|
|
843
1711
|
return [
|
|
844
|
-
`${index + 1}. ${review.itemId}
|
|
845
|
-
` rating
|
|
1712
|
+
`${index + 1}. ${style.accent(review.itemId)} ${style.dim('[' + review.itemType + ']')}`,
|
|
1713
|
+
` ${style.dim('rating ' + review.rating + '/5 by ' + review.author)}`,
|
|
846
1714
|
` ${truncate(review.content, 88)}`
|
|
847
1715
|
].join('\n');
|
|
848
1716
|
})
|
|
@@ -850,28 +1718,28 @@ function formatReviewList(reviews) {
|
|
|
850
1718
|
}
|
|
851
1719
|
function formatProfileDetail(profile) {
|
|
852
1720
|
const lines = [
|
|
853
|
-
profile.displayName,
|
|
854
|
-
|
|
855
|
-
|
|
856
|
-
|
|
857
|
-
|
|
1721
|
+
style.bold(profile.displayName),
|
|
1722
|
+
`${style.dim('username')} ${style.accent(profile.username)}`,
|
|
1723
|
+
`${style.dim('packages')} ${formatNumber(profile.packages)}`,
|
|
1724
|
+
`${style.dim('workflows')} ${formatNumber(profile.workflows)}`,
|
|
1725
|
+
`${style.dim('discussions')} ${formatNumber(profile.discussions)}`
|
|
858
1726
|
];
|
|
859
1727
|
if (profile.bio)
|
|
860
|
-
lines.splice(2, 0,
|
|
1728
|
+
lines.splice(2, 0, `${style.dim('bio')} ${profile.bio}`);
|
|
861
1729
|
if (profile.avatarUrl)
|
|
862
|
-
lines.push(
|
|
1730
|
+
lines.push(`${style.dim('avatar')} ${profile.avatarUrl}`);
|
|
863
1731
|
if (profile.joinedAt)
|
|
864
|
-
lines.push(
|
|
1732
|
+
lines.push(`${style.dim('joined')} ${formatDate(profile.joinedAt)}`);
|
|
865
1733
|
return lines.join('\n');
|
|
866
1734
|
}
|
|
867
1735
|
function formatTutorialList(tutorials) {
|
|
868
1736
|
return tutorials
|
|
869
1737
|
.map((tutorial, index) => {
|
|
870
1738
|
return [
|
|
871
|
-
`${index + 1}. ${tutorial.id}
|
|
872
|
-
` ${tutorial.title}`,
|
|
1739
|
+
`${index + 1}. ${style.accent(tutorial.id)} ${style.dim('[' + tutorial.level + ']')}`,
|
|
1740
|
+
` ${style.dim(tutorial.title)}`,
|
|
873
1741
|
` ${truncate(tutorial.description, 88)}`,
|
|
874
|
-
` ${tutorial.duration
|
|
1742
|
+
` ${style.dim(tutorial.duration + ' | ' + tutorial.steps.length + ' steps')}`
|
|
875
1743
|
].join('\n');
|
|
876
1744
|
})
|
|
877
1745
|
.join('\n\n');
|
|
@@ -880,81 +1748,83 @@ function formatTutorialStep(tutorial, stepNumber) {
|
|
|
880
1748
|
const payload = tutorialStepPayload(tutorial, stepNumber);
|
|
881
1749
|
if (payload.completed) {
|
|
882
1750
|
return [
|
|
883
|
-
|
|
884
|
-
`Completed ${tutorial.steps.length}/${tutorial.steps.length} steps
|
|
1751
|
+
style.bold(tutorial.title),
|
|
1752
|
+
style.dim(`Completed ${tutorial.steps.length}/${tutorial.steps.length} steps.`),
|
|
885
1753
|
'Run agora tutorials for more tutorials.'
|
|
886
1754
|
].join('\n');
|
|
887
1755
|
}
|
|
888
1756
|
const lines = [
|
|
889
|
-
|
|
890
|
-
|
|
891
|
-
|
|
892
|
-
|
|
893
|
-
|
|
1757
|
+
style.bold(tutorial.title),
|
|
1758
|
+
`${style.dim('id')} ${style.accent(tutorial.id)}`,
|
|
1759
|
+
`${style.dim('level')} ${tutorial.level}`,
|
|
1760
|
+
`${style.dim('duration')} ${tutorial.duration}`,
|
|
1761
|
+
`${style.dim('step')} ${payload.stepNumber}/${tutorial.steps.length}`,
|
|
894
1762
|
'',
|
|
895
1763
|
payload.title || '',
|
|
896
1764
|
payload.content || ''
|
|
897
1765
|
];
|
|
898
1766
|
if (payload.code) {
|
|
899
|
-
lines.push('', 'code:', payload.code);
|
|
1767
|
+
lines.push('', style.dim('code:'), payload.code);
|
|
900
1768
|
}
|
|
901
1769
|
return lines.join('\n');
|
|
902
1770
|
}
|
|
903
|
-
function
|
|
904
|
-
|
|
905
|
-
|
|
906
|
-
|
|
907
|
-
|
|
908
|
-
|
|
909
|
-
|
|
910
|
-
|
|
911
|
-
|
|
912
|
-
|
|
913
|
-
|
|
914
|
-
|
|
915
|
-
|
|
916
|
-
|
|
917
|
-
|
|
918
|
-
|
|
919
|
-
|
|
920
|
-
'
|
|
921
|
-
|
|
922
|
-
|
|
923
|
-
|
|
924
|
-
'
|
|
925
|
-
'
|
|
926
|
-
'
|
|
927
|
-
'
|
|
928
|
-
'
|
|
929
|
-
'
|
|
930
|
-
' agora config doctor [--config path] [--json]',
|
|
931
|
-
'',
|
|
932
|
-
'Data source:',
|
|
933
|
-
' --api Use the live Agora API',
|
|
934
|
-
' --api-url <url> Override AGORA_API_URL',
|
|
935
|
-
' --api-timeout <ms> API timeout before offline fallback',
|
|
936
|
-
' --token <token> API auth token, defaults to env vars or agora auth login',
|
|
937
|
-
' --offline Force local bundled marketplace data',
|
|
938
|
-
'',
|
|
939
|
-
'Examples:',
|
|
940
|
-
' agora init',
|
|
941
|
-
' agora init --dry-run',
|
|
942
|
-
' agora use wf-tdd-cycle',
|
|
943
|
-
' agora search filesystem',
|
|
944
|
-
' agora search filesystem --api',
|
|
945
|
-
' agora browse mcp-github',
|
|
946
|
-
' agora tutorials mcp',
|
|
947
|
-
' agora tutorial tut-mcp-basics 2',
|
|
948
|
-
' agora install mcp-github',
|
|
949
|
-
' agora install mcp-github --write',
|
|
950
|
-
' agora save wf-security-audit',
|
|
951
|
-
' agora saved',
|
|
952
|
-
' agora auth login --token $AGORA_TOKEN --api-url https://agora.example.com',
|
|
953
|
-
' agora discuss --title "MCP question" --content "How are you composing servers?" --category question',
|
|
954
|
-
' agora profile alice',
|
|
955
|
-
' agora publish package --name @you/server --description "MCP server" --npm @you/server',
|
|
956
|
-
' agora review mcp-github --rating 5 --content "Works well"'
|
|
1771
|
+
function welcome(color, trueColor) {
|
|
1772
|
+
if (!color) {
|
|
1773
|
+
return [
|
|
1774
|
+
'',
|
|
1775
|
+
`agora · Developers' CLI marketplace and community hub · v${VERSION}`,
|
|
1776
|
+
'',
|
|
1777
|
+
' Search agora search <query>',
|
|
1778
|
+
' Browse agora trending · agora browse <id>',
|
|
1779
|
+
' Learn agora tutorials · agora tutorial <id>',
|
|
1780
|
+
' Install agora install <id> [--write]',
|
|
1781
|
+
' Setup agora init [--mcp] · agora use <workflow>',
|
|
1782
|
+
' Auth agora login [--api-url <url>]',
|
|
1783
|
+
''
|
|
1784
|
+
].join('\n');
|
|
1785
|
+
}
|
|
1786
|
+
const banner = renderBanner({ color, trueColor });
|
|
1787
|
+
const box = renderBox('Welcome to Agora', [
|
|
1788
|
+
"Developers' CLI marketplace and community hub - type a command, bash or chat:",
|
|
1789
|
+
`v${VERSION} · run \`agora help\` to get started`
|
|
1790
|
+
], { color, trueColor });
|
|
1791
|
+
const hint = [
|
|
1792
|
+
`${style.dim('Search')} agora search <query>`,
|
|
1793
|
+
`${style.dim('Browse')} agora trending · agora browse <id>`,
|
|
1794
|
+
`${style.dim('Learn')} agora tutorials · agora tutorial <id>`,
|
|
1795
|
+
`${style.dim('Install')} agora install <id> [--write]`,
|
|
1796
|
+
`${style.dim('Setup')} agora init [--mcp] · agora use <workflow>`,
|
|
1797
|
+
`${style.dim('Auth')} agora login [--api-url <url>]`
|
|
957
1798
|
].join('\n');
|
|
1799
|
+
return `\n${banner}\n\n${box}\n\n${hint}\n`;
|
|
1800
|
+
}
|
|
1801
|
+
/** Flat-minimal section header: accent title, dim ` · `-joined metadata. */
|
|
1802
|
+
function header(title, meta) {
|
|
1803
|
+
return [style.accent(title), ...meta.map((part) => style.dim(part))].join(style.dim(' · '));
|
|
1804
|
+
}
|
|
1805
|
+
function usage() {
|
|
1806
|
+
const nameWidth = Math.max(...COMMANDS.map((c) => c.name.length));
|
|
1807
|
+
const groups = ['Marketplace', 'Setup', 'Library', 'Learn', 'Community'];
|
|
1808
|
+
const lines = [
|
|
1809
|
+
`${style.accent('agora')}${style.dim(` · Developers' CLI marketplace and community hub · v${VERSION}`)}`,
|
|
1810
|
+
''
|
|
1811
|
+
];
|
|
1812
|
+
for (const group of groups) {
|
|
1813
|
+
const groupCmds = COMMANDS.filter((c) => c.group === group);
|
|
1814
|
+
lines.push(style.dim(group));
|
|
1815
|
+
for (const cmd of groupCmds) {
|
|
1816
|
+
lines.push(` ${style.accent(cmd.name.padEnd(nameWidth))} ${style.dim(cmd.summary)}`);
|
|
1817
|
+
}
|
|
1818
|
+
lines.push('');
|
|
1819
|
+
}
|
|
1820
|
+
lines.push(style.dim('Run `agora help <command>` for details on any command.'));
|
|
1821
|
+
return lines.join('\n');
|
|
1822
|
+
}
|
|
1823
|
+
export function commandManual(name) {
|
|
1824
|
+
const meta = COMMANDS.find((c) => c.name === name);
|
|
1825
|
+
if (!meta)
|
|
1826
|
+
return '';
|
|
1827
|
+
return renderManual(meta, style);
|
|
958
1828
|
}
|
|
959
1829
|
function usageError(io, message) {
|
|
960
1830
|
writeLine(io.stderr, message);
|
|
@@ -990,6 +1860,8 @@ function shortFlag(arg) {
|
|
|
990
1860
|
return 'help';
|
|
991
1861
|
if (flag === 'j')
|
|
992
1862
|
return 'json';
|
|
1863
|
+
if (flag === 'm')
|
|
1864
|
+
return 'model';
|
|
993
1865
|
if (flag === 'c')
|
|
994
1866
|
return 'c';
|
|
995
1867
|
if (flag === 'n')
|
|
@@ -1140,6 +2012,11 @@ function tutorialStepPayload(tutorial, stepNumber) {
|
|
|
1140
2012
|
code: step?.code
|
|
1141
2013
|
};
|
|
1142
2014
|
}
|
|
2015
|
+
function sourceLabel(result) {
|
|
2016
|
+
return result.source === 'offline'
|
|
2017
|
+
? `source: offline · refreshed ${dataRefreshedAt}`
|
|
2018
|
+
: `source: ${result.source}`;
|
|
2019
|
+
}
|
|
1143
2020
|
function warnFallback(result, io) {
|
|
1144
2021
|
if (result.fallbackReason) {
|
|
1145
2022
|
writeLine(io.stderr, `API unavailable, using offline data (refreshed ${dataRefreshedAt}): ${result.fallbackReason}`);
|
|
@@ -1200,13 +2077,6 @@ function truncate(value, max) {
|
|
|
1200
2077
|
return value;
|
|
1201
2078
|
return `${value.slice(0, max - 3)}...`;
|
|
1202
2079
|
}
|
|
1203
|
-
function formatCount(value) {
|
|
1204
|
-
if (value >= 1000000)
|
|
1205
|
-
return `${(value / 1000000).toFixed(1)}M`;
|
|
1206
|
-
if (value >= 1000)
|
|
1207
|
-
return `${(value / 1000).toFixed(1)}K`;
|
|
1208
|
-
return String(value);
|
|
1209
|
-
}
|
|
1210
2080
|
function formatDate(value) {
|
|
1211
2081
|
return value.slice(0, 10);
|
|
1212
2082
|
}
|