opencode-agora 0.2.2 → 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 +1202 -227
- 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-files.d.ts.map +1 -1
- package/dist/config-files.js +11 -8
- package/dist/config-files.js.map +1 -1
- package/dist/config.d.ts +8 -8
- package/dist/config.d.ts.map +1 -1
- package/dist/config.js +16 -27
- 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 +1013 -545
- 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 -118
- 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 +151 -236
- package/dist/index.js.map +1 -1
- package/dist/init.d.ts +4 -1
- package/dist/init.d.ts.map +1 -1
- package/dist/init.js +103 -51
- 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 +87 -19
- 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 +128 -33
- 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/state.d.ts.map +1 -1
- package/dist/state.js +7 -6
- package/dist/state.js.map +1 -1
- 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 +21 -10
- package/dist/api.d.ts +0 -69
- 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,24 +1,89 @@
|
|
|
1
1
|
import { execSync } from 'node:child_process';
|
|
2
|
-
import { readFileSync, writeFileSync, mkdirSync } from 'node:fs';
|
|
2
|
+
import { existsSync, readFileSync, writeFileSync, mkdirSync } from 'node:fs';
|
|
3
3
|
import { join } from 'node:path';
|
|
4
|
-
import
|
|
4
|
+
import { spawn } from 'node:child_process';
|
|
5
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';
|
|
6
10
|
import { detectOpenCodeConfigPath, doctorOpenCodeConfig, loadOpenCodeConfig, writeOpenCodeConfig } from '../config-files.js';
|
|
7
|
-
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';
|
|
8
20
|
import { scanProject, generateInitPlan, applyInitPlan, runCommands } from '../init.js';
|
|
21
|
+
import { installAgoraCommand } from '../commands.js';
|
|
9
22
|
import { sampleWorkflows, dataRefreshedAt } from '../data.js';
|
|
10
23
|
import { createDiscussionSource, discussionsSource, findMarketplaceSource, createReviewSource, findTutorialSource, listReviewsSource, profileSource, publishPackageSource, publishWorkflowSource, searchMarketplaceSource, trendingMarketplaceSource, tutorialsSource } from '../live.js';
|
|
11
24
|
import { clearAuthState, detectAgoraDataDir, getAuthState, getAgoraStatePath, loadAgoraState, removeItemFromState, resolveSavedItems, saveItemToState, setAuthState, writeAgoraState } from '../state.js';
|
|
12
|
-
|
|
13
|
-
|
|
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';
|
|
28
|
+
const pkg = JSON.parse(readFileSync(new URL('../../package.json', import.meta.url), 'utf8'));
|
|
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);
|
|
34
|
+
const booleanFlags = new Set([
|
|
35
|
+
'api',
|
|
36
|
+
'continue',
|
|
37
|
+
'dryRun',
|
|
38
|
+
'help',
|
|
39
|
+
'json',
|
|
40
|
+
'live',
|
|
41
|
+
'mcp',
|
|
42
|
+
'offline',
|
|
43
|
+
'table',
|
|
44
|
+
'version',
|
|
45
|
+
'verbose',
|
|
46
|
+
'write'
|
|
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
|
+
}
|
|
14
63
|
export async function runCli(argv, io) {
|
|
15
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));
|
|
16
68
|
if (parsed.flags.version) {
|
|
17
69
|
writeLine(io.stdout, VERSION);
|
|
18
70
|
return 0;
|
|
19
71
|
}
|
|
20
|
-
if (
|
|
21
|
-
|
|
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)));
|
|
22
87
|
return 0;
|
|
23
88
|
}
|
|
24
89
|
try {
|
|
@@ -56,16 +121,65 @@ export async function runCli(argv, io) {
|
|
|
56
121
|
case 'profile':
|
|
57
122
|
return await commandProfile(parsed, io);
|
|
58
123
|
case 'auth':
|
|
59
|
-
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);
|
|
60
131
|
case 'config':
|
|
61
132
|
return await commandConfig(parsed, io);
|
|
133
|
+
case 'mcp':
|
|
134
|
+
return await commandMcp(parsed, io);
|
|
135
|
+
case 'chat':
|
|
136
|
+
return await commandChat(parsed, io);
|
|
62
137
|
case 'init':
|
|
63
138
|
return await commandInit(parsed, io);
|
|
64
139
|
case 'use':
|
|
65
140
|
return await commandUse(parsed, io);
|
|
66
|
-
case '
|
|
67
|
-
|
|
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
|
+
}
|
|
68
181
|
return 0;
|
|
182
|
+
}
|
|
69
183
|
default:
|
|
70
184
|
writeLine(io.stderr, `Unknown command: ${parsed.command}`);
|
|
71
185
|
writeLine(io.stderr, 'Run agora help for usage.');
|
|
@@ -92,7 +206,9 @@ export function parseArgs(argv) {
|
|
|
92
206
|
if (inlineValue !== undefined) {
|
|
93
207
|
flags[key] = inlineValue;
|
|
94
208
|
}
|
|
95
|
-
else if (!booleanFlags.has(key) &&
|
|
209
|
+
else if (!booleanFlags.has(key) &&
|
|
210
|
+
argv[index + 1] &&
|
|
211
|
+
(!argv[index + 1].startsWith('-') || /^-\d/.test(argv[index + 1]))) {
|
|
96
212
|
flags[key] = argv[index + 1];
|
|
97
213
|
index += 1;
|
|
98
214
|
}
|
|
@@ -103,7 +219,9 @@ export function parseArgs(argv) {
|
|
|
103
219
|
}
|
|
104
220
|
if (arg.startsWith('-') && arg.length > 1) {
|
|
105
221
|
const key = shortFlag(arg);
|
|
106
|
-
if (!booleanFlags.has(key) &&
|
|
222
|
+
if (!booleanFlags.has(key) &&
|
|
223
|
+
argv[index + 1] &&
|
|
224
|
+
(!argv[index + 1].startsWith('-') || /^-\d/.test(argv[index + 1]))) {
|
|
107
225
|
flags[key] = argv[index + 1];
|
|
108
226
|
index += 1;
|
|
109
227
|
}
|
|
@@ -123,30 +241,61 @@ export function parseArgs(argv) {
|
|
|
123
241
|
async function commandSearch(parsed, io) {
|
|
124
242
|
const query = parsed.args.join(' ');
|
|
125
243
|
const category = stringFlag(parsed, 'category', 'c') || 'all';
|
|
126
|
-
const
|
|
127
|
-
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);
|
|
250
|
+
const result = await searchMarketplaceSource({
|
|
251
|
+
...sourceOptions(parsed, io),
|
|
252
|
+
query,
|
|
253
|
+
category,
|
|
254
|
+
limit,
|
|
255
|
+
sortBy: sortBy,
|
|
256
|
+
sortOrder,
|
|
257
|
+
page,
|
|
258
|
+
perPage
|
|
259
|
+
});
|
|
128
260
|
const results = result.data;
|
|
129
261
|
warnFallback(result, io);
|
|
130
262
|
if (parsed.flags.json) {
|
|
131
|
-
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 }));
|
|
132
264
|
return 0;
|
|
133
265
|
}
|
|
134
266
|
if (results.length === 0) {
|
|
135
267
|
writeLine(io.stdout, `No results found for "${query}".`);
|
|
136
268
|
return 0;
|
|
137
269
|
}
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
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
|
+
});
|
|
143
288
|
return 0;
|
|
144
289
|
}
|
|
145
290
|
async function commandBrowse(parsed, io) {
|
|
146
291
|
const id = parsed.args[0];
|
|
147
292
|
if (!id)
|
|
148
293
|
return usageError(io, 'browse requires an item id');
|
|
149
|
-
const result = await findMarketplaceSource({
|
|
294
|
+
const result = await findMarketplaceSource({
|
|
295
|
+
...sourceOptions(parsed, io),
|
|
296
|
+
id,
|
|
297
|
+
type: stringFlag(parsed, 'type', 't')
|
|
298
|
+
});
|
|
150
299
|
const item = result.data;
|
|
151
300
|
warnFallback(result, io);
|
|
152
301
|
if (!item)
|
|
@@ -156,11 +305,22 @@ async function commandBrowse(parsed, io) {
|
|
|
156
305
|
return 0;
|
|
157
306
|
}
|
|
158
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
|
+
}
|
|
159
318
|
return 0;
|
|
160
319
|
}
|
|
161
320
|
async function commandTrending(parsed, io) {
|
|
162
321
|
const category = stringFlag(parsed, 'category', 'c') || parsed.args[0] || 'all';
|
|
163
322
|
const limit = numberFlag(parsed, 'limit', 'n') || 5;
|
|
323
|
+
const table = Boolean(parsed.flags.table);
|
|
164
324
|
const result = await trendingMarketplaceSource({ ...sourceOptions(parsed, io), category, limit });
|
|
165
325
|
const items = result.data;
|
|
166
326
|
warnFallback(result, io);
|
|
@@ -168,32 +328,373 @@ async function commandTrending(parsed, io) {
|
|
|
168
328
|
writeJson(io.stdout, sourcePayload(result, { category, count: items.length, tags: getTrendingTags(), items }));
|
|
169
329
|
return 0;
|
|
170
330
|
}
|
|
171
|
-
writeLine(io.stdout,
|
|
172
|
-
writeLine(io.stdout,
|
|
173
|
-
|
|
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(', ')}`);
|
|
174
341
|
return 0;
|
|
175
342
|
}
|
|
176
343
|
async function commandWorkflows(parsed, io) {
|
|
177
344
|
const query = parsed.args.join(' ');
|
|
178
345
|
const limit = numberFlag(parsed, 'limit', 'n') || 10;
|
|
179
|
-
const result = await searchMarketplaceSource({
|
|
180
|
-
|
|
346
|
+
const result = await searchMarketplaceSource({
|
|
347
|
+
...sourceOptions(parsed, io),
|
|
348
|
+
query,
|
|
349
|
+
category: 'workflow',
|
|
350
|
+
limit
|
|
351
|
+
});
|
|
352
|
+
const workflows = result.data;
|
|
181
353
|
warnFallback(result, io);
|
|
182
354
|
if (parsed.flags.json) {
|
|
183
355
|
writeJson(io.stdout, sourcePayload(result, { query, count: workflows.length, workflows }));
|
|
184
356
|
return 0;
|
|
185
357
|
}
|
|
186
|
-
writeLine(io.stdout,
|
|
358
|
+
writeLine(io.stdout, header('agora workflows', [`${workflows.length} results`, sourceLabel(result)]));
|
|
359
|
+
writeLine(io.stdout, '');
|
|
187
360
|
writeLine(io.stdout, formatItemList(workflows));
|
|
188
361
|
return 0;
|
|
189
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
|
+
}
|
|
190
686
|
async function commandTutorials(parsed, io) {
|
|
191
687
|
const query = parsed.args.join(' ');
|
|
192
688
|
const level = tutorialLevelFlag(parsed);
|
|
193
689
|
if (!level.ok)
|
|
194
690
|
return usageError(io, level.error);
|
|
195
691
|
const limit = numberFlag(parsed, 'limit', 'n') || 20;
|
|
196
|
-
const result = await tutorialsSource({
|
|
692
|
+
const result = await tutorialsSource({
|
|
693
|
+
...sourceOptions(parsed, io),
|
|
694
|
+
query,
|
|
695
|
+
level: level.value,
|
|
696
|
+
limit
|
|
697
|
+
});
|
|
197
698
|
const tutorials = result.data;
|
|
198
699
|
warnFallback(result, io);
|
|
199
700
|
if (parsed.flags.json) {
|
|
@@ -204,14 +705,22 @@ async function commandTutorials(parsed, io) {
|
|
|
204
705
|
writeLine(io.stdout, query ? `No tutorials match "${query}".` : 'No tutorials found.');
|
|
205
706
|
return 0;
|
|
206
707
|
}
|
|
207
|
-
writeLine(io.stdout,
|
|
708
|
+
writeLine(io.stdout, header('agora tutorials', [`${tutorials.length} results`, sourceLabel(result)]));
|
|
709
|
+
writeLine(io.stdout, '');
|
|
208
710
|
writeLine(io.stdout, formatTutorialList(tutorials));
|
|
209
711
|
return 0;
|
|
210
712
|
}
|
|
211
713
|
async function commandTutorial(parsed, io) {
|
|
212
714
|
const id = parsed.args[0];
|
|
213
|
-
if (!id)
|
|
214
|
-
|
|
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
|
+
}
|
|
215
724
|
const step = tutorialStepNumber(parsed);
|
|
216
725
|
if (!step.ok)
|
|
217
726
|
return usageError(io, step.error);
|
|
@@ -244,14 +753,17 @@ async function commandDiscussions(parsed, io) {
|
|
|
244
753
|
writeLine(io.stdout, 'No discussions found.');
|
|
245
754
|
return 0;
|
|
246
755
|
}
|
|
247
|
-
writeLine(io.stdout,
|
|
248
|
-
writeLine(io.stdout,
|
|
756
|
+
writeLine(io.stdout, header('agora discussions', [`${discussions.length} results`, sourceLabel(result)]));
|
|
757
|
+
writeLine(io.stdout, '');
|
|
758
|
+
writeLine(io.stdout, discussions
|
|
759
|
+
.map((discussion, index) => {
|
|
249
760
|
return [
|
|
250
|
-
`${index + 1}. ${discussion.title}
|
|
761
|
+
`${index + 1}. ${style.accent(discussion.title)} ${style.dim('[' + discussion.category + ']')}`,
|
|
251
762
|
` ${truncate(discussion.content, 88)}`,
|
|
252
|
-
` replies
|
|
763
|
+
` ${style.dim('replies ' + discussion.replies + ' · stars ' + discussion.stars + ' · by ' + discussion.author)}`
|
|
253
764
|
].join('\n');
|
|
254
|
-
})
|
|
765
|
+
})
|
|
766
|
+
.join('\n\n'));
|
|
255
767
|
return 0;
|
|
256
768
|
}
|
|
257
769
|
async function commandDiscuss(parsed, io) {
|
|
@@ -259,7 +771,7 @@ async function commandDiscuss(parsed, io) {
|
|
|
259
771
|
if (!source.ok)
|
|
260
772
|
return usageError(io, source.error);
|
|
261
773
|
const title = requiredStringFlag(parsed, 'title');
|
|
262
|
-
const content = contentInput(parsed);
|
|
774
|
+
const content = contentInput(parsed, io);
|
|
263
775
|
if (!title || !content) {
|
|
264
776
|
return usageError(io, 'discuss requires --title and --content or --content-file');
|
|
265
777
|
}
|
|
@@ -275,15 +787,19 @@ async function commandDiscuss(parsed, io) {
|
|
|
275
787
|
writeJson(io.stdout, sourcePayload(result, { discussion: result.data }));
|
|
276
788
|
return 0;
|
|
277
789
|
}
|
|
278
|
-
writeLine(io.stdout, `Created discussion ${result.data.id}`);
|
|
279
|
-
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)})`);
|
|
280
792
|
return 0;
|
|
281
793
|
}
|
|
282
794
|
async function commandInstall(parsed, io) {
|
|
283
795
|
const id = parsed.args[0];
|
|
284
796
|
if (!id)
|
|
285
797
|
return usageError(io, 'install requires an item id');
|
|
286
|
-
const source = await findMarketplaceSource({
|
|
798
|
+
const source = await findMarketplaceSource({
|
|
799
|
+
...sourceOptions(parsed, io),
|
|
800
|
+
id,
|
|
801
|
+
type: stringFlag(parsed, 'type', 't')
|
|
802
|
+
});
|
|
287
803
|
const item = source.data;
|
|
288
804
|
warnFallback(source, io);
|
|
289
805
|
if (!item)
|
|
@@ -315,8 +831,8 @@ async function commandInstall(parsed, io) {
|
|
|
315
831
|
}
|
|
316
832
|
if (parsed.flags.write) {
|
|
317
833
|
writeOpenCodeConfig(configPath, plan.config);
|
|
318
|
-
writeLine(io.stdout, `Installed ${item.name}`);
|
|
319
|
-
writeLine(io.stdout,
|
|
834
|
+
writeLine(io.stdout, `Installed ${style.accent(item.name)}`);
|
|
835
|
+
writeLine(io.stdout, `${style.dim('Config')} ${configPath}`);
|
|
320
836
|
if (plan.commands.length) {
|
|
321
837
|
writeLine(io.stdout, 'Installing packages...');
|
|
322
838
|
for (const cmd of plan.commands) {
|
|
@@ -333,6 +849,16 @@ async function commandInstall(parsed, io) {
|
|
|
333
849
|
}
|
|
334
850
|
writeLine(io.stdout, `Install preview: ${item.name}`);
|
|
335
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
|
+
}
|
|
336
862
|
if (plan.commands.length) {
|
|
337
863
|
writeLine(io.stdout, '\nCommands:');
|
|
338
864
|
writeLine(io.stdout, plan.commands.join('\n'));
|
|
@@ -342,36 +868,271 @@ async function commandInstall(parsed, io) {
|
|
|
342
868
|
writeLine(io.stdout, '\nRun with --write to update the config file and install packages.');
|
|
343
869
|
return 0;
|
|
344
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
|
+
}
|
|
345
1040
|
async function commandInit(parsed, io) {
|
|
346
1041
|
const cwd = io.cwd || process.cwd();
|
|
347
1042
|
const scan = scanProject(cwd);
|
|
1043
|
+
const plan = generateInitPlan(scan);
|
|
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
|
+
}
|
|
348
1056
|
if (parsed.flags.json) {
|
|
349
|
-
|
|
1057
|
+
if (parsed.flags.dryRun) {
|
|
1058
|
+
writeJson(io.stdout, {
|
|
1059
|
+
projectType: scan.type,
|
|
1060
|
+
frameworks: scan.frameworks,
|
|
1061
|
+
config: plan.config,
|
|
1062
|
+
servers: plan.servers,
|
|
1063
|
+
commands: plan.commands,
|
|
1064
|
+
slashCommand: join(cwd, '.opencode', 'command', 'agora.md'),
|
|
1065
|
+
dryRun: true
|
|
1066
|
+
});
|
|
1067
|
+
return 0;
|
|
1068
|
+
}
|
|
1069
|
+
applyInitPlan(plan, configPath);
|
|
1070
|
+
const commandPath = installAgoraCommand(cwd);
|
|
1071
|
+
const installResults = plan.commands.length ? runCommands(plan.commands) : [];
|
|
1072
|
+
const installed = installResults.filter((r) => r.ok).length;
|
|
1073
|
+
const failed = installResults.filter((r) => !r.ok).length;
|
|
1074
|
+
writeJson(io.stdout, {
|
|
1075
|
+
projectType: scan.type,
|
|
1076
|
+
frameworks: scan.frameworks,
|
|
1077
|
+
config: plan.config,
|
|
1078
|
+
servers: plan.servers,
|
|
1079
|
+
commands: plan.commands,
|
|
1080
|
+
slashCommand: commandPath,
|
|
1081
|
+
installResults,
|
|
1082
|
+
installed,
|
|
1083
|
+
failed
|
|
1084
|
+
});
|
|
350
1085
|
return 0;
|
|
351
1086
|
}
|
|
352
1087
|
writeLine(io.stdout, `Scanning ${cwd}...`);
|
|
353
|
-
writeLine(io.stdout, ` Project type
|
|
1088
|
+
writeLine(io.stdout, ` ${style.dim('Project type')} ${scan.type}`);
|
|
354
1089
|
if (scan.frameworks.length)
|
|
355
|
-
writeLine(io.stdout, ` Frameworks
|
|
1090
|
+
writeLine(io.stdout, ` ${style.dim('Frameworks')} ${scan.frameworks.join(', ')}`);
|
|
356
1091
|
if (scan.hasDocker)
|
|
357
|
-
writeLine(io.stdout, '
|
|
1092
|
+
writeLine(io.stdout, ` ${style.dim('Docker')} detected`);
|
|
358
1093
|
if (scan.hasTests)
|
|
359
|
-
writeLine(io.stdout, '
|
|
1094
|
+
writeLine(io.stdout, ` ${style.dim('Tests')} detected`);
|
|
360
1095
|
if (scan.hasDatabase)
|
|
361
|
-
writeLine(io.stdout, '
|
|
362
|
-
const plan = generateInitPlan(scan);
|
|
363
|
-
const configPath = detectOpenCodeConfigPath({ cwd, env: io.env });
|
|
1096
|
+
writeLine(io.stdout, ` ${style.dim('Database')} detected`);
|
|
364
1097
|
if (!parsed.flags.dryRun) {
|
|
365
1098
|
applyInitPlan(plan, configPath);
|
|
366
1099
|
writeLine(io.stdout, `\nWrote config to ${configPath}`);
|
|
1100
|
+
const commandPath = installAgoraCommand(cwd);
|
|
1101
|
+
writeLine(io.stdout, `Installed /agora slash command at ${commandPath}`);
|
|
367
1102
|
if (plan.commands.length) {
|
|
368
1103
|
writeLine(io.stdout, '\nInstalling MCP server packages...');
|
|
369
|
-
|
|
370
|
-
|
|
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
|
+
}
|
|
1126
|
+
const installed = installResults.filter((r) => r.ok).length;
|
|
1127
|
+
const failed = installResults.filter((r) => !r.ok).length;
|
|
1128
|
+
writeLine(io.stdout, ` Installed ${installed} of ${plan.commands.length} packages${failed ? ` (${failed} failed)` : ''}`);
|
|
371
1129
|
}
|
|
372
1130
|
writeLine(io.stdout, '\n✓ Agora initialized! Restart OpenCode to pick up the changes.');
|
|
373
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.');
|
|
374
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.');
|
|
375
1136
|
if (plan.workflows.length)
|
|
376
1137
|
writeLine(io.stdout, ` ${plan.workflows.length} workflows available via \`agora use\`.`);
|
|
377
1138
|
for (const note of plan.notes)
|
|
@@ -381,6 +1142,7 @@ async function commandInit(parsed, io) {
|
|
|
381
1142
|
writeLine(io.stdout, '\n--- Dry run ---');
|
|
382
1143
|
writeLine(io.stdout, `Target config: ${configPath}`);
|
|
383
1144
|
writeLine(io.stdout, formatConfigJson(plan.config));
|
|
1145
|
+
writeLine(io.stdout, `\nSlash command: ${join(cwd, '.opencode', 'command', 'agora.md')}`);
|
|
384
1146
|
writeLine(io.stdout, '\nPackages to install:');
|
|
385
1147
|
for (const cmd of plan.commands)
|
|
386
1148
|
writeLine(io.stdout, ` ${cmd}`);
|
|
@@ -390,8 +1152,14 @@ async function commandInit(parsed, io) {
|
|
|
390
1152
|
}
|
|
391
1153
|
async function commandUse(parsed, io) {
|
|
392
1154
|
const id = parsed.args[0];
|
|
393
|
-
if (!id)
|
|
394
|
-
|
|
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
|
+
}
|
|
395
1163
|
const workflow = sampleWorkflows.find((w) => w.id === id || w.name.toLowerCase() === id.toLowerCase());
|
|
396
1164
|
if (!workflow)
|
|
397
1165
|
return usageError(io, `Workflow not found: ${id}. Run \`agora workflows\` to see available workflows.`);
|
|
@@ -412,11 +1180,13 @@ ${workflow.prompt}
|
|
|
412
1180
|
writeFileSync(skillPath, skillContent, 'utf8');
|
|
413
1181
|
const configPath = detectOpenCodeConfigPath({ cwd, env: io.env });
|
|
414
1182
|
const loaded = loadOpenCodeConfig(configPath);
|
|
415
|
-
|
|
1183
|
+
if (loaded.error)
|
|
1184
|
+
return usageError(io, `${loaded.path}: ${loaded.error}`);
|
|
1185
|
+
const plugins = new Set(loaded.config.plugin || []);
|
|
416
1186
|
plugins.add(skillId);
|
|
417
1187
|
const updatedConfig = {
|
|
418
1188
|
...loaded.config,
|
|
419
|
-
|
|
1189
|
+
plugin: Array.from(plugins)
|
|
420
1190
|
};
|
|
421
1191
|
writeOpenCodeConfig(configPath, updatedConfig);
|
|
422
1192
|
if (parsed.flags.json) {
|
|
@@ -444,51 +1214,133 @@ function commandConfig(parsed, io) {
|
|
|
444
1214
|
writeJson(io.stdout, report);
|
|
445
1215
|
return report.valid ? 0 : 1;
|
|
446
1216
|
}
|
|
447
|
-
writeLine(io.stdout,
|
|
448
|
-
writeLine(io.stdout,
|
|
449
|
-
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'}`);
|
|
450
1220
|
if (report.error)
|
|
451
|
-
writeLine(io.stdout,
|
|
452
|
-
writeLine(io.stdout,
|
|
453
|
-
writeLine(io.stdout,
|
|
454
|
-
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'}`);
|
|
455
1225
|
return report.valid ? 0 : 1;
|
|
456
1226
|
}
|
|
457
|
-
function commandAuth(parsed, io) {
|
|
1227
|
+
async function commandAuth(parsed, io) {
|
|
458
1228
|
const subcommand = parsed.args[0] || 'status';
|
|
459
1229
|
const dataDir = detectDataDir(parsed, io);
|
|
460
1230
|
const state = loadAgoraState(dataDir);
|
|
461
1231
|
const existingAuth = getAuthState(state);
|
|
462
1232
|
if (subcommand === 'login') {
|
|
463
|
-
const
|
|
464
|
-
if (
|
|
465
|
-
|
|
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;
|
|
466
1248
|
}
|
|
1249
|
+
// ── Device-code flow ─────────────────────────────────────────────────
|
|
467
1250
|
const apiUrl = stringFlag(parsed, 'apiUrl') || envString(io, 'AGORA_API_URL') || existingAuth?.apiUrl;
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
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'}`);
|
|
474
1330
|
}
|
|
475
|
-
writeLine(io.stdout, 'Stored Agora API token');
|
|
476
|
-
writeLine(io.stdout, `API URL: ${auth?.apiUrl || 'not stored'}`);
|
|
477
|
-
writeLine(io.stdout, `State: ${getAgoraStatePath(dataDir)}`);
|
|
478
|
-
return 0;
|
|
479
1331
|
}
|
|
480
1332
|
if (subcommand === 'status') {
|
|
481
1333
|
if (parsed.flags.json) {
|
|
482
1334
|
writeJson(io.stdout, authStatusPayload(dataDir, existingAuth));
|
|
483
1335
|
return 0;
|
|
484
1336
|
}
|
|
485
|
-
writeLine(io.stdout,
|
|
1337
|
+
writeLine(io.stdout, `${style.dim('Authenticated')} ${existingAuth ? 'yes' : 'no'}`);
|
|
486
1338
|
if (existingAuth) {
|
|
487
|
-
writeLine(io.stdout,
|
|
488
|
-
writeLine(io.stdout,
|
|
489
|
-
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)}`);
|
|
490
1342
|
}
|
|
491
|
-
writeLine(io.stdout,
|
|
1343
|
+
writeLine(io.stdout, `${style.dim('State')} ${getAgoraStatePath(dataDir)}`);
|
|
492
1344
|
return 0;
|
|
493
1345
|
}
|
|
494
1346
|
if (subcommand === 'logout') {
|
|
@@ -514,7 +1366,11 @@ async function commandSave(parsed, io) {
|
|
|
514
1366
|
const id = parsed.args[0];
|
|
515
1367
|
if (!id)
|
|
516
1368
|
return usageError(io, 'save requires an item id');
|
|
517
|
-
const source = await findMarketplaceSource({
|
|
1369
|
+
const source = await findMarketplaceSource({
|
|
1370
|
+
...sourceOptions(parsed, io),
|
|
1371
|
+
id,
|
|
1372
|
+
type: stringFlag(parsed, 'type', 't')
|
|
1373
|
+
});
|
|
518
1374
|
const item = source.data;
|
|
519
1375
|
warnFallback(source, io);
|
|
520
1376
|
if (!item)
|
|
@@ -535,8 +1391,8 @@ async function commandSave(parsed, io) {
|
|
|
535
1391
|
});
|
|
536
1392
|
return 0;
|
|
537
1393
|
}
|
|
538
|
-
writeLine(io.stdout, result.added ? `Saved ${item.id}` : `${item.id} is already saved`);
|
|
539
|
-
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)}`);
|
|
540
1396
|
return 0;
|
|
541
1397
|
}
|
|
542
1398
|
function commandSaved(parsed, io) {
|
|
@@ -558,7 +1414,7 @@ function commandSaved(parsed, io) {
|
|
|
558
1414
|
writeLine(io.stdout, 'Run agora save <id> to save a package or workflow.');
|
|
559
1415
|
return 0;
|
|
560
1416
|
}
|
|
561
|
-
writeLine(io.stdout,
|
|
1417
|
+
writeLine(io.stdout, header('agora saved', [`${saved.length} items`]));
|
|
562
1418
|
writeLine(io.stdout, formatSavedList(saved));
|
|
563
1419
|
return 0;
|
|
564
1420
|
}
|
|
@@ -585,7 +1441,7 @@ function commandRemove(parsed, io) {
|
|
|
585
1441
|
if (!result.removed) {
|
|
586
1442
|
return usageError(io, `Saved item not found: ${id}`);
|
|
587
1443
|
}
|
|
588
|
-
writeLine(io.stdout, `Removed ${targetId}`);
|
|
1444
|
+
writeLine(io.stdout, `Removed ${style.accent(targetId)}`);
|
|
589
1445
|
return 0;
|
|
590
1446
|
}
|
|
591
1447
|
async function commandPublish(parsed, io) {
|
|
@@ -621,12 +1477,12 @@ async function commandPublish(parsed, io) {
|
|
|
621
1477
|
writeJson(io.stdout, sourcePayload(result, { item: result.data }));
|
|
622
1478
|
return 0;
|
|
623
1479
|
}
|
|
624
|
-
writeLine(io.stdout, `Published package ${result.data.id}`);
|
|
625
|
-
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)})`);
|
|
626
1482
|
return 0;
|
|
627
1483
|
}
|
|
628
|
-
const prompt = promptInput(parsed);
|
|
629
|
-
if (
|
|
1484
|
+
const prompt = promptInput(parsed, io);
|
|
1485
|
+
if (prompt === undefined) {
|
|
630
1486
|
return usageError(io, 'publish workflow requires --prompt or --prompt-file');
|
|
631
1487
|
}
|
|
632
1488
|
const result = await publishWorkflowSource(source.options, {
|
|
@@ -641,8 +1497,8 @@ async function commandPublish(parsed, io) {
|
|
|
641
1497
|
writeJson(io.stdout, sourcePayload(result, { item: result.data }));
|
|
642
1498
|
return 0;
|
|
643
1499
|
}
|
|
644
|
-
writeLine(io.stdout, `Published workflow ${result.data.id}`);
|
|
645
|
-
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)})`);
|
|
646
1502
|
return 0;
|
|
647
1503
|
}
|
|
648
1504
|
async function commandReview(parsed, io) {
|
|
@@ -667,8 +1523,8 @@ async function commandReview(parsed, io) {
|
|
|
667
1523
|
writeJson(io.stdout, sourcePayload(result, { review: result.data }));
|
|
668
1524
|
return 0;
|
|
669
1525
|
}
|
|
670
|
-
writeLine(io.stdout, `Reviewed ${result.data.itemId}`);
|
|
671
|
-
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)}`);
|
|
672
1528
|
return 0;
|
|
673
1529
|
}
|
|
674
1530
|
async function commandReviews(parsed, io) {
|
|
@@ -685,7 +1541,8 @@ async function commandReviews(parsed, io) {
|
|
|
685
1541
|
writeLine(io.stdout, itemId ? `No reviews found for ${itemId}.` : 'No reviews found.');
|
|
686
1542
|
return 0;
|
|
687
1543
|
}
|
|
688
|
-
writeLine(io.stdout,
|
|
1544
|
+
writeLine(io.stdout, header('agora reviews', [`${result.data.length} results`, sourceLabel(result)]));
|
|
1545
|
+
writeLine(io.stdout, '');
|
|
689
1546
|
writeLine(io.stdout, formatReviewList(result.data));
|
|
690
1547
|
return 0;
|
|
691
1548
|
}
|
|
@@ -706,177 +1563,268 @@ async function commandProfile(parsed, io) {
|
|
|
706
1563
|
writeLine(io.stdout, formatProfileDetail(result.data));
|
|
707
1564
|
return 0;
|
|
708
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
|
+
}
|
|
709
1631
|
function formatItemList(items) {
|
|
710
|
-
|
|
711
|
-
|
|
1632
|
+
const idWidth = Math.max(...items.map((item) => item.id.length));
|
|
1633
|
+
return items
|
|
1634
|
+
.map((item) => {
|
|
1635
|
+
const metrics = item.kind === 'package'
|
|
1636
|
+
? `${formatNumber(item.installs)} installs · ${formatNumber(item.stars)} ★`
|
|
1637
|
+
: `${formatNumber(item.stars)} ★`;
|
|
712
1638
|
return [
|
|
713
|
-
`${
|
|
714
|
-
|
|
715
|
-
|
|
716
|
-
|
|
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}`)
|
|
717
1643
|
].join('\n');
|
|
718
|
-
})
|
|
1644
|
+
})
|
|
1645
|
+
.join('\n\n');
|
|
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');
|
|
719
1659
|
}
|
|
720
1660
|
function formatItemDetail(item) {
|
|
721
1661
|
const lines = [
|
|
722
|
-
|
|
723
|
-
|
|
724
|
-
|
|
725
|
-
|
|
726
|
-
|
|
727
|
-
|
|
728
|
-
|
|
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)}`,
|
|
729
1669
|
'',
|
|
730
1670
|
item.description,
|
|
731
1671
|
'',
|
|
732
|
-
|
|
1672
|
+
`${style.dim('tags')} ${item.tags.join(', ')}`
|
|
733
1673
|
];
|
|
734
1674
|
if (item.kind === 'package') {
|
|
735
|
-
lines.splice(5, 0,
|
|
736
|
-
lines.push(
|
|
1675
|
+
lines.splice(5, 0, `${style.dim('version')} ${item.version}`);
|
|
1676
|
+
lines.push(`${style.dim('installs')} ${formatNumber(item.installs)}`);
|
|
737
1677
|
if (item.repository)
|
|
738
|
-
lines.push(
|
|
1678
|
+
lines.push(`${style.dim('repo')} ${item.repository}`);
|
|
739
1679
|
if (item.npmPackage)
|
|
740
|
-
lines.push(
|
|
1680
|
+
lines.push(`${style.dim('npm')} ${item.npmPackage}`);
|
|
741
1681
|
}
|
|
742
1682
|
if (item.kind === 'workflow') {
|
|
743
|
-
lines.push(
|
|
1683
|
+
lines.push(`${style.dim('forks')} ${item.forks}`);
|
|
744
1684
|
if (item.model)
|
|
745
|
-
lines.push(
|
|
746
|
-
lines.push('', 'prompt
|
|
1685
|
+
lines.push(`${style.dim('model')} ${item.model}`);
|
|
1686
|
+
lines.push('', style.dim('prompt'), item.prompt);
|
|
747
1687
|
}
|
|
748
1688
|
return lines.join('\n');
|
|
749
1689
|
}
|
|
750
1690
|
function formatSavedList(items) {
|
|
751
|
-
return items
|
|
1691
|
+
return items
|
|
1692
|
+
.map((entry, index) => {
|
|
752
1693
|
if (!entry.item) {
|
|
753
1694
|
return [
|
|
754
|
-
`${index + 1}. ${entry.saved.id} [missing]`,
|
|
755
|
-
` saved
|
|
1695
|
+
`${index + 1}. ${style.accent(entry.saved.id)} ${style.dim('[missing]')}`,
|
|
1696
|
+
` ${style.dim('saved ' + formatDate(entry.saved.savedAt))}`
|
|
756
1697
|
].join('\n');
|
|
757
1698
|
}
|
|
758
1699
|
return [
|
|
759
|
-
`${index + 1}. ${entry.item.id}
|
|
760
|
-
` ${entry.item.name}`,
|
|
1700
|
+
`${index + 1}. ${style.accent(entry.item.id)} ${style.dim('[' + entry.item.category + ']')}`,
|
|
1701
|
+
` ${style.dim(entry.item.name)}`,
|
|
761
1702
|
` ${truncate(entry.item.description, 88)}`,
|
|
762
|
-
` saved
|
|
1703
|
+
` ${style.dim('saved ' + formatDate(entry.saved.savedAt))}`
|
|
763
1704
|
].join('\n');
|
|
764
|
-
})
|
|
1705
|
+
})
|
|
1706
|
+
.join('\n\n');
|
|
765
1707
|
}
|
|
766
1708
|
function formatReviewList(reviews) {
|
|
767
|
-
return reviews
|
|
1709
|
+
return reviews
|
|
1710
|
+
.map((review, index) => {
|
|
768
1711
|
return [
|
|
769
|
-
`${index + 1}. ${review.itemId}
|
|
770
|
-
` rating
|
|
1712
|
+
`${index + 1}. ${style.accent(review.itemId)} ${style.dim('[' + review.itemType + ']')}`,
|
|
1713
|
+
` ${style.dim('rating ' + review.rating + '/5 by ' + review.author)}`,
|
|
771
1714
|
` ${truncate(review.content, 88)}`
|
|
772
1715
|
].join('\n');
|
|
773
|
-
})
|
|
1716
|
+
})
|
|
1717
|
+
.join('\n\n');
|
|
774
1718
|
}
|
|
775
1719
|
function formatProfileDetail(profile) {
|
|
776
1720
|
const lines = [
|
|
777
|
-
profile.displayName,
|
|
778
|
-
|
|
779
|
-
|
|
780
|
-
|
|
781
|
-
|
|
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)}`
|
|
782
1726
|
];
|
|
783
1727
|
if (profile.bio)
|
|
784
|
-
lines.splice(2, 0,
|
|
1728
|
+
lines.splice(2, 0, `${style.dim('bio')} ${profile.bio}`);
|
|
785
1729
|
if (profile.avatarUrl)
|
|
786
|
-
lines.push(
|
|
1730
|
+
lines.push(`${style.dim('avatar')} ${profile.avatarUrl}`);
|
|
787
1731
|
if (profile.joinedAt)
|
|
788
|
-
lines.push(
|
|
1732
|
+
lines.push(`${style.dim('joined')} ${formatDate(profile.joinedAt)}`);
|
|
789
1733
|
return lines.join('\n');
|
|
790
1734
|
}
|
|
791
1735
|
function formatTutorialList(tutorials) {
|
|
792
|
-
return tutorials
|
|
1736
|
+
return tutorials
|
|
1737
|
+
.map((tutorial, index) => {
|
|
793
1738
|
return [
|
|
794
|
-
`${index + 1}. ${tutorial.id}
|
|
795
|
-
` ${tutorial.title}`,
|
|
1739
|
+
`${index + 1}. ${style.accent(tutorial.id)} ${style.dim('[' + tutorial.level + ']')}`,
|
|
1740
|
+
` ${style.dim(tutorial.title)}`,
|
|
796
1741
|
` ${truncate(tutorial.description, 88)}`,
|
|
797
|
-
` ${tutorial.duration
|
|
1742
|
+
` ${style.dim(tutorial.duration + ' | ' + tutorial.steps.length + ' steps')}`
|
|
798
1743
|
].join('\n');
|
|
799
|
-
})
|
|
1744
|
+
})
|
|
1745
|
+
.join('\n\n');
|
|
800
1746
|
}
|
|
801
1747
|
function formatTutorialStep(tutorial, stepNumber) {
|
|
802
1748
|
const payload = tutorialStepPayload(tutorial, stepNumber);
|
|
803
1749
|
if (payload.completed) {
|
|
804
1750
|
return [
|
|
805
|
-
|
|
806
|
-
`Completed ${tutorial.steps.length}/${tutorial.steps.length} steps
|
|
1751
|
+
style.bold(tutorial.title),
|
|
1752
|
+
style.dim(`Completed ${tutorial.steps.length}/${tutorial.steps.length} steps.`),
|
|
807
1753
|
'Run agora tutorials for more tutorials.'
|
|
808
1754
|
].join('\n');
|
|
809
1755
|
}
|
|
810
1756
|
const lines = [
|
|
811
|
-
|
|
812
|
-
|
|
813
|
-
|
|
814
|
-
|
|
815
|
-
|
|
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}`,
|
|
816
1762
|
'',
|
|
817
1763
|
payload.title || '',
|
|
818
1764
|
payload.content || ''
|
|
819
1765
|
];
|
|
820
1766
|
if (payload.code) {
|
|
821
|
-
lines.push('', 'code:', payload.code);
|
|
1767
|
+
lines.push('', style.dim('code:'), payload.code);
|
|
822
1768
|
}
|
|
823
1769
|
return lines.join('\n');
|
|
824
1770
|
}
|
|
825
|
-
function
|
|
826
|
-
|
|
827
|
-
|
|
828
|
-
|
|
829
|
-
|
|
830
|
-
|
|
831
|
-
|
|
832
|
-
|
|
833
|
-
|
|
834
|
-
|
|
835
|
-
|
|
836
|
-
|
|
837
|
-
|
|
838
|
-
|
|
839
|
-
|
|
840
|
-
|
|
841
|
-
|
|
842
|
-
'
|
|
843
|
-
|
|
844
|
-
|
|
845
|
-
|
|
846
|
-
'
|
|
847
|
-
'
|
|
848
|
-
'
|
|
849
|
-
'
|
|
850
|
-
'
|
|
851
|
-
'
|
|
852
|
-
' agora config doctor [--config path] [--json]',
|
|
853
|
-
'',
|
|
854
|
-
'Data source:',
|
|
855
|
-
' --api Use the live Agora API',
|
|
856
|
-
' --api-url <url> Override AGORA_API_URL',
|
|
857
|
-
' --api-timeout <ms> API timeout before offline fallback',
|
|
858
|
-
' --token <token> API auth token, defaults to env vars or agora auth login',
|
|
859
|
-
' --offline Force local bundled marketplace data',
|
|
860
|
-
'',
|
|
861
|
-
'Examples:',
|
|
862
|
-
' agora init',
|
|
863
|
-
' agora init --dry-run',
|
|
864
|
-
' agora use wf-tdd-cycle',
|
|
865
|
-
' agora search filesystem',
|
|
866
|
-
' agora search filesystem --api',
|
|
867
|
-
' agora browse mcp-github',
|
|
868
|
-
' agora tutorials mcp',
|
|
869
|
-
' agora tutorial tut-mcp-basics 2',
|
|
870
|
-
' agora install mcp-github',
|
|
871
|
-
' agora install mcp-github --write',
|
|
872
|
-
' agora save wf-security-audit',
|
|
873
|
-
' agora saved',
|
|
874
|
-
' agora auth login --token $AGORA_TOKEN --api-url https://agora.example.com',
|
|
875
|
-
' agora discuss --title "MCP question" --content "How are you composing servers?" --category question',
|
|
876
|
-
' agora profile alice',
|
|
877
|
-
' agora publish package --name @you/server --description "MCP server" --npm @you/server',
|
|
878
|
-
' 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>]`
|
|
879
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);
|
|
880
1828
|
}
|
|
881
1829
|
function usageError(io, message) {
|
|
882
1830
|
writeLine(io.stderr, message);
|
|
@@ -898,9 +1846,9 @@ function numberFlag(parsed, longName, shortName) {
|
|
|
898
1846
|
return Number.isFinite(parsedValue) ? parsedValue : undefined;
|
|
899
1847
|
}
|
|
900
1848
|
function authTokenInput(parsed, io) {
|
|
901
|
-
return requiredStringFlag(parsed, 'token') ||
|
|
1849
|
+
return (requiredStringFlag(parsed, 'token') ||
|
|
902
1850
|
envString(io, 'AGORA_TOKEN') ||
|
|
903
|
-
envString(io, 'AGORA_API_TOKEN');
|
|
1851
|
+
envString(io, 'AGORA_API_TOKEN'));
|
|
904
1852
|
}
|
|
905
1853
|
function envString(io, name) {
|
|
906
1854
|
const value = io.env?.[name];
|
|
@@ -912,6 +1860,8 @@ function shortFlag(arg) {
|
|
|
912
1860
|
return 'help';
|
|
913
1861
|
if (flag === 'j')
|
|
914
1862
|
return 'json';
|
|
1863
|
+
if (flag === 'm')
|
|
1864
|
+
return 'model';
|
|
915
1865
|
if (flag === 'c')
|
|
916
1866
|
return 'c';
|
|
917
1867
|
if (flag === 'n')
|
|
@@ -933,12 +1883,13 @@ function sourceOptions(parsed, io) {
|
|
|
933
1883
|
const storedAuth = getAuthState(loadAgoraState(detectDataDir(parsed, io)));
|
|
934
1884
|
const storedApiUrl = storedAuth?.apiUrl;
|
|
935
1885
|
const apiUrl = explicitApiUrl || envApiUrl || storedApiUrl || '';
|
|
936
|
-
const useApi = !parsed.flags.offline &&
|
|
937
|
-
parsed.flags.
|
|
938
|
-
|
|
939
|
-
|
|
940
|
-
|
|
941
|
-
|
|
1886
|
+
const useApi = !parsed.flags.offline &&
|
|
1887
|
+
Boolean(parsed.flags.api ||
|
|
1888
|
+
parsed.flags.live ||
|
|
1889
|
+
explicitApiUrl ||
|
|
1890
|
+
envApiUrl ||
|
|
1891
|
+
storedApiUrl ||
|
|
1892
|
+
io.env?.AGORA_USE_API === 'true');
|
|
942
1893
|
return {
|
|
943
1894
|
useApi,
|
|
944
1895
|
apiUrl,
|
|
@@ -950,10 +1901,16 @@ function sourceOptions(parsed, io) {
|
|
|
950
1901
|
function writeSourceOptions(parsed, io) {
|
|
951
1902
|
const options = sourceOptions(parsed, io);
|
|
952
1903
|
if (!options.apiUrl) {
|
|
953
|
-
return {
|
|
1904
|
+
return {
|
|
1905
|
+
ok: false,
|
|
1906
|
+
error: 'This command requires --api-url, AGORA_API_URL, or an auth login API URL'
|
|
1907
|
+
};
|
|
954
1908
|
}
|
|
955
1909
|
if (!options.token) {
|
|
956
|
-
return {
|
|
1910
|
+
return {
|
|
1911
|
+
ok: false,
|
|
1912
|
+
error: 'This command requires --token, AGORA_TOKEN, AGORA_API_TOKEN, or agora auth login'
|
|
1913
|
+
};
|
|
957
1914
|
}
|
|
958
1915
|
return {
|
|
959
1916
|
ok: true,
|
|
@@ -966,7 +1923,10 @@ function writeSourceOptions(parsed, io) {
|
|
|
966
1923
|
function readSourceOptions(parsed, io) {
|
|
967
1924
|
const options = sourceOptions(parsed, io);
|
|
968
1925
|
if (!options.apiUrl) {
|
|
969
|
-
return {
|
|
1926
|
+
return {
|
|
1927
|
+
ok: false,
|
|
1928
|
+
error: 'This command requires --api-url, AGORA_API_URL, or an auth login API URL'
|
|
1929
|
+
};
|
|
970
1930
|
}
|
|
971
1931
|
return { ok: true, options: { ...options, useApi: true } };
|
|
972
1932
|
}
|
|
@@ -974,24 +1934,35 @@ function tagsFlag(parsed) {
|
|
|
974
1934
|
const value = stringFlag(parsed, 'tags');
|
|
975
1935
|
if (!value)
|
|
976
1936
|
return [];
|
|
977
|
-
return value
|
|
1937
|
+
return value
|
|
1938
|
+
.split(',')
|
|
1939
|
+
.map((tag) => tag.trim())
|
|
1940
|
+
.filter(Boolean);
|
|
978
1941
|
}
|
|
979
|
-
function promptInput(parsed) {
|
|
1942
|
+
function promptInput(parsed, io) {
|
|
980
1943
|
const prompt = stringFlag(parsed, 'prompt');
|
|
981
1944
|
if (prompt)
|
|
982
1945
|
return prompt;
|
|
983
1946
|
const promptFile = stringFlag(parsed, 'promptFile');
|
|
984
1947
|
if (!promptFile)
|
|
985
1948
|
return undefined;
|
|
1949
|
+
if (!existsSync(promptFile)) {
|
|
1950
|
+
usageError(io, `prompt-file not found: ${promptFile}`);
|
|
1951
|
+
return undefined;
|
|
1952
|
+
}
|
|
986
1953
|
return readFileSync(promptFile, 'utf8');
|
|
987
1954
|
}
|
|
988
|
-
function contentInput(parsed) {
|
|
1955
|
+
function contentInput(parsed, io) {
|
|
989
1956
|
const content = requiredStringFlag(parsed, 'content');
|
|
990
1957
|
if (content)
|
|
991
1958
|
return content;
|
|
992
1959
|
const contentFile = stringFlag(parsed, 'contentFile');
|
|
993
1960
|
if (!contentFile)
|
|
994
1961
|
return undefined;
|
|
1962
|
+
if (!existsSync(contentFile)) {
|
|
1963
|
+
usageError(io, `content-file not found: ${contentFile}`);
|
|
1964
|
+
return undefined;
|
|
1965
|
+
}
|
|
995
1966
|
return readFileSync(contentFile, 'utf8').trim();
|
|
996
1967
|
}
|
|
997
1968
|
function itemTypeFlag(parsed, itemId) {
|
|
@@ -1002,10 +1973,16 @@ function itemTypeFlag(parsed, itemId) {
|
|
|
1002
1973
|
}
|
|
1003
1974
|
function discussionCategoryFlag(parsed) {
|
|
1004
1975
|
const category = stringFlag(parsed, 'category', 'c') || 'discussion';
|
|
1005
|
-
if (category === 'question' ||
|
|
1976
|
+
if (category === 'question' ||
|
|
1977
|
+
category === 'idea' ||
|
|
1978
|
+
category === 'showcase' ||
|
|
1979
|
+
category === 'discussion') {
|
|
1006
1980
|
return { ok: true, value: category };
|
|
1007
1981
|
}
|
|
1008
|
-
return {
|
|
1982
|
+
return {
|
|
1983
|
+
ok: false,
|
|
1984
|
+
error: 'discussion category must be question, idea, showcase, or discussion'
|
|
1985
|
+
};
|
|
1009
1986
|
}
|
|
1010
1987
|
function tutorialLevelFlag(parsed) {
|
|
1011
1988
|
const level = stringFlag(parsed, 'level') || 'all';
|
|
@@ -1035,6 +2012,11 @@ function tutorialStepPayload(tutorial, stepNumber) {
|
|
|
1035
2012
|
code: step?.code
|
|
1036
2013
|
};
|
|
1037
2014
|
}
|
|
2015
|
+
function sourceLabel(result) {
|
|
2016
|
+
return result.source === 'offline'
|
|
2017
|
+
? `source: offline · refreshed ${dataRefreshedAt}`
|
|
2018
|
+
: `source: ${result.source}`;
|
|
2019
|
+
}
|
|
1038
2020
|
function warnFallback(result, io) {
|
|
1039
2021
|
if (result.fallbackReason) {
|
|
1040
2022
|
writeLine(io.stderr, `API unavailable, using offline data (refreshed ${dataRefreshedAt}): ${result.fallbackReason}`);
|
|
@@ -1095,13 +2077,6 @@ function truncate(value, max) {
|
|
|
1095
2077
|
return value;
|
|
1096
2078
|
return `${value.slice(0, max - 3)}...`;
|
|
1097
2079
|
}
|
|
1098
|
-
function formatCount(value) {
|
|
1099
|
-
if (value >= 1000000)
|
|
1100
|
-
return `${(value / 1000000).toFixed(1)}M`;
|
|
1101
|
-
if (value >= 1000)
|
|
1102
|
-
return `${(value / 1000).toFixed(1)}K`;
|
|
1103
|
-
return String(value);
|
|
1104
|
-
}
|
|
1105
2080
|
function formatDate(value) {
|
|
1106
2081
|
return value.slice(0, 10);
|
|
1107
2082
|
}
|