opencode-agora 0.4.0 → 0.4.2
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 +89 -415
- package/dist/atomic-write.d.ts +10 -0
- package/dist/atomic-write.d.ts.map +1 -0
- package/dist/atomic-write.js +23 -0
- package/dist/atomic-write.js.map +1 -0
- package/dist/auth/refresh.d.ts +17 -0
- package/dist/auth/refresh.d.ts.map +1 -0
- package/dist/auth/refresh.js +50 -0
- package/dist/auth/refresh.js.map +1 -0
- package/dist/cli/app.d.ts +11 -19
- package/dist/cli/app.d.ts.map +1 -1
- package/dist/cli/app.js +159 -2028
- package/dist/cli/app.js.map +1 -1
- package/dist/cli/commands/browse.d.ts +4 -0
- package/dist/cli/commands/browse.d.ts.map +1 -0
- package/dist/cli/commands/browse.js +80 -0
- package/dist/cli/commands/browse.js.map +1 -0
- package/dist/cli/commands/chat.d.ts +4 -0
- package/dist/cli/commands/chat.d.ts.map +1 -0
- package/dist/cli/commands/chat.js +125 -0
- package/dist/cli/commands/chat.js.map +1 -0
- package/dist/cli/commands/community.d.ts +12 -0
- package/dist/cli/commands/community.d.ts.map +1 -0
- package/dist/cli/commands/community.js +453 -0
- package/dist/cli/commands/community.js.map +1 -0
- package/dist/cli/commands/export.d.ts +3 -0
- package/dist/cli/commands/export.d.ts.map +1 -0
- package/dist/cli/commands/export.js +108 -0
- package/dist/cli/commands/export.js.map +1 -0
- package/dist/cli/commands/init.d.ts +4 -0
- package/dist/cli/commands/init.d.ts.map +1 -0
- package/dist/cli/commands/init.js +299 -0
- package/dist/cli/commands/init.js.map +1 -0
- package/dist/cli/commands/learn.d.ts +4 -0
- package/dist/cli/commands/learn.d.ts.map +1 -0
- package/dist/cli/commands/learn.js +62 -0
- package/dist/cli/commands/learn.js.map +1 -0
- package/dist/cli/commands/marketplace.d.ts +9 -0
- package/dist/cli/commands/marketplace.d.ts.map +1 -0
- package/dist/cli/commands/marketplace.js +321 -0
- package/dist/cli/commands/marketplace.js.map +1 -0
- package/dist/cli/commands/notify.d.ts +3 -0
- package/dist/cli/commands/notify.d.ts.map +1 -0
- package/dist/cli/commands/notify.js +59 -0
- package/dist/cli/commands/notify.js.map +1 -0
- package/dist/cli/commands/operations.d.ts +16 -0
- package/dist/cli/commands/operations.d.ts.map +1 -0
- package/dist/cli/commands/operations.js +1041 -0
- package/dist/cli/commands/operations.js.map +1 -0
- package/dist/cli/commands/outdated.d.ts +3 -0
- package/dist/cli/commands/outdated.d.ts.map +1 -0
- package/dist/cli/commands/outdated.js +48 -0
- package/dist/cli/commands/outdated.js.map +1 -0
- package/dist/cli/commands/ping.d.ts +3 -0
- package/dist/cli/commands/ping.d.ts.map +1 -0
- package/dist/cli/commands/ping.js +56 -0
- package/dist/cli/commands/ping.js.map +1 -0
- package/dist/cli/commands/scan.d.ts +3 -0
- package/dist/cli/commands/scan.d.ts.map +1 -0
- package/dist/cli/commands/scan.js +35 -0
- package/dist/cli/commands/scan.js.map +1 -0
- package/dist/cli/commands/today.d.ts +3 -0
- package/dist/cli/commands/today.d.ts.map +1 -0
- package/dist/cli/commands/today.js +142 -0
- package/dist/cli/commands/today.js.map +1 -0
- package/dist/cli/commands/types.d.ts +5 -0
- package/dist/cli/commands/types.d.ts.map +1 -0
- package/dist/cli/commands/types.js +2 -0
- package/dist/cli/commands/types.js.map +1 -0
- package/dist/cli/commands/watch.d.ts +3 -0
- package/dist/cli/commands/watch.d.ts.map +1 -0
- package/dist/cli/commands/watch.js +41 -0
- package/dist/cli/commands/watch.js.map +1 -0
- package/dist/cli/commands/welcome.d.ts +3 -0
- package/dist/cli/commands/welcome.d.ts.map +1 -0
- package/dist/cli/commands/welcome.js +97 -0
- package/dist/cli/commands/welcome.js.map +1 -0
- package/dist/cli/commands-meta.d.ts.map +1 -1
- package/dist/cli/commands-meta.js +286 -29
- package/dist/cli/commands-meta.js.map +1 -1
- package/dist/cli/completions-gen.d.ts +2 -0
- package/dist/cli/completions-gen.d.ts.map +1 -0
- package/dist/cli/completions-gen.js +195 -0
- package/dist/cli/completions-gen.js.map +1 -0
- package/dist/cli/completions.d.ts.map +1 -1
- package/dist/cli/completions.js +42 -5
- package/dist/cli/completions.js.map +1 -1
- package/dist/cli/flags.d.ts +19 -0
- package/dist/cli/flags.d.ts.map +1 -0
- package/dist/cli/flags.js +91 -0
- package/dist/cli/flags.js.map +1 -0
- package/dist/cli/format.d.ts +19 -0
- package/dist/cli/format.d.ts.map +1 -0
- package/dist/cli/format.js +249 -0
- package/dist/cli/format.js.map +1 -0
- package/dist/cli/helpers.d.ts +95 -0
- package/dist/cli/helpers.d.ts.map +1 -0
- package/dist/cli/helpers.js +301 -0
- package/dist/cli/helpers.js.map +1 -0
- package/dist/cli/mcp-server.d.ts +7 -1
- package/dist/cli/mcp-server.d.ts.map +1 -1
- package/dist/cli/mcp-server.js +70 -2
- package/dist/cli/mcp-server.js.map +1 -1
- package/dist/cli/menu.d.ts.map +1 -1
- package/dist/cli/menu.js +11 -3
- package/dist/cli/menu.js.map +1 -1
- package/dist/cli/pages/community.d.ts +6 -0
- package/dist/cli/pages/community.d.ts.map +1 -1
- package/dist/cli/pages/community.js +882 -64
- package/dist/cli/pages/community.js.map +1 -1
- package/dist/cli/pages/helpers.d.ts +14 -9
- package/dist/cli/pages/helpers.d.ts.map +1 -1
- package/dist/cli/pages/helpers.js +37 -6
- package/dist/cli/pages/helpers.js.map +1 -1
- package/dist/cli/pages/home.d.ts +1 -0
- package/dist/cli/pages/home.d.ts.map +1 -1
- package/dist/cli/pages/home.js +203 -120
- package/dist/cli/pages/home.js.map +1 -1
- package/dist/cli/pages/marketplace.d.ts +2 -0
- package/dist/cli/pages/marketplace.d.ts.map +1 -1
- package/dist/cli/pages/marketplace.js +524 -62
- package/dist/cli/pages/marketplace.js.map +1 -1
- package/dist/cli/pages/news.d.ts +28 -0
- package/dist/cli/pages/news.d.ts.map +1 -1
- package/dist/cli/pages/news.js +209 -82
- package/dist/cli/pages/news.js.map +1 -1
- package/dist/cli/pages/settings.d.ts.map +1 -1
- package/dist/cli/pages/settings.js +163 -33
- package/dist/cli/pages/settings.js.map +1 -1
- package/dist/cli/pages/types.d.ts +1 -1
- package/dist/cli/pages/types.d.ts.map +1 -1
- package/dist/cli/prompter.d.ts.map +1 -1
- package/dist/cli/prompter.js +43 -8
- package/dist/cli/prompter.js.map +1 -1
- package/dist/cli/shell.d.ts +2 -2
- package/dist/cli/shell.d.ts.map +1 -1
- package/dist/cli/shell.js +321 -18
- package/dist/cli/shell.js.map +1 -1
- package/dist/cli/tui.d.ts +1 -1
- package/dist/cli/tui.d.ts.map +1 -1
- package/dist/cli/tui.js +69 -23
- package/dist/cli/tui.js.map +1 -1
- package/dist/community/client.d.ts +45 -8
- package/dist/community/client.d.ts.map +1 -1
- package/dist/community/client.js +118 -23
- package/dist/community/client.js.map +1 -1
- package/dist/community/search.d.ts +25 -0
- package/dist/community/search.d.ts.map +1 -0
- package/dist/community/search.js +62 -0
- package/dist/community/search.js.map +1 -0
- package/dist/community/types.d.ts +21 -0
- package/dist/community/types.d.ts.map +1 -1
- package/dist/community/types.js +1 -1
- package/dist/config.d.ts +0 -4
- package/dist/config.d.ts.map +1 -1
- package/dist/config.js +0 -15
- package/dist/config.js.map +1 -1
- package/dist/data.d.ts.map +1 -1
- package/dist/data.js +142 -68
- package/dist/data.js.map +1 -1
- package/dist/format.d.ts +0 -2
- package/dist/format.d.ts.map +1 -1
- package/dist/format.js +0 -2
- package/dist/format.js.map +1 -1
- package/dist/hubs/cache.d.ts +6 -0
- package/dist/hubs/cache.d.ts.map +1 -0
- package/dist/hubs/cache.js +46 -0
- package/dist/hubs/cache.js.map +1 -0
- package/dist/hubs/enrichment.d.ts +43 -0
- package/dist/hubs/enrichment.d.ts.map +1 -0
- package/dist/hubs/enrichment.js +239 -0
- package/dist/hubs/enrichment.js.map +1 -0
- package/dist/hubs/github.d.ts +12 -0
- package/dist/hubs/github.d.ts.map +1 -0
- package/dist/hubs/github.js +54 -0
- package/dist/hubs/github.js.map +1 -0
- package/dist/hubs/huggingface.d.ts +27 -0
- package/dist/hubs/huggingface.d.ts.map +1 -0
- package/dist/hubs/huggingface.js +88 -0
- package/dist/hubs/huggingface.js.map +1 -0
- package/dist/hubs/quality.d.ts +26 -0
- package/dist/hubs/quality.d.ts.map +1 -0
- package/dist/hubs/quality.js +57 -0
- package/dist/hubs/quality.js.map +1 -0
- package/dist/hubs/types.d.ts +30 -0
- package/dist/hubs/types.d.ts.map +1 -0
- package/dist/hubs/types.js +2 -0
- package/dist/hubs/types.js.map +1 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +84 -0
- package/dist/index.js.map +1 -1
- package/dist/init.js +2 -2
- package/dist/init.js.map +1 -1
- package/dist/live.d.ts +10 -0
- package/dist/live.d.ts.map +1 -1
- package/dist/live.js +31 -1
- package/dist/live.js.map +1 -1
- package/dist/marketplace.d.ts +16 -3
- package/dist/marketplace.d.ts.map +1 -1
- package/dist/marketplace.js +174 -7
- package/dist/marketplace.js.map +1 -1
- package/dist/news/cache.d.ts.map +1 -1
- package/dist/news/cache.js +4 -3
- package/dist/news/cache.js.map +1 -1
- package/dist/news/score.js +1 -1
- package/dist/news/sources/arxiv.d.ts.map +1 -1
- package/dist/news/sources/arxiv.js +10 -6
- package/dist/news/sources/arxiv.js.map +1 -1
- package/dist/news/sources/github-trending.d.ts.map +1 -1
- package/dist/news/sources/github-trending.js +9 -5
- package/dist/news/sources/github-trending.js.map +1 -1
- package/dist/news/sources/hn.d.ts.map +1 -1
- package/dist/news/sources/hn.js +8 -4
- package/dist/news/sources/hn.js.map +1 -1
- package/dist/news/sources/reddit.d.ts.map +1 -1
- package/dist/news/sources/reddit.js +5 -4
- package/dist/news/sources/reddit.js.map +1 -1
- package/dist/news/sources/rss.d.ts +10 -11
- package/dist/news/sources/rss.d.ts.map +1 -1
- package/dist/news/sources/rss.js +11 -99
- package/dist/news/sources/rss.js.map +1 -1
- package/dist/news/types.d.ts +3 -0
- package/dist/news/types.d.ts.map +1 -1
- package/dist/news/types.js +15 -6
- package/dist/news/types.js.map +1 -1
- package/dist/outdated.d.ts +24 -0
- package/dist/outdated.d.ts.map +1 -0
- package/dist/outdated.js +53 -0
- package/dist/outdated.js.map +1 -0
- package/dist/preferences.d.ts.map +1 -1
- package/dist/preferences.js +4 -4
- package/dist/preferences.js.map +1 -1
- package/dist/scan.d.ts +26 -0
- package/dist/scan.d.ts.map +1 -0
- package/dist/scan.js +179 -0
- package/dist/scan.js.map +1 -0
- package/dist/settings.d.ts.map +1 -1
- package/dist/settings.js +14 -20
- package/dist/settings.js.map +1 -1
- package/dist/state.d.ts +9 -2
- package/dist/state.d.ts.map +1 -1
- package/dist/state.js +41 -19
- package/dist/state.js.map +1 -1
- package/dist/types.d.ts +13 -0
- package/dist/types.d.ts.map +1 -1
- package/dist/ui.d.ts +1 -1
- package/dist/ui.js +1 -1
- package/package.json +4 -2
package/dist/cli/app.js
CHANGED
|
@@ -1,65 +1,77 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import { existsSync, readFileSync, writeFileSync, mkdirSync } from 'node:fs';
|
|
3
|
-
import { join } from 'node:path';
|
|
4
|
-
import { spawn } from 'node:child_process';
|
|
5
|
-
import { formatConfigJson } from '../config.js';
|
|
6
|
-
import { formatNumber } from '../format.js';
|
|
1
|
+
import { readFileSync } from 'node:fs';
|
|
7
2
|
import { COMMANDS, renderManual } from './commands-meta.js';
|
|
8
3
|
import { runInteractiveMenu } from './menu.js';
|
|
9
4
|
import { runTui } from './tui.js';
|
|
10
|
-
import {
|
|
11
|
-
import {
|
|
12
|
-
import {
|
|
13
|
-
import {
|
|
14
|
-
import {
|
|
15
|
-
import
|
|
16
|
-
import
|
|
17
|
-
import
|
|
18
|
-
import
|
|
19
|
-
import
|
|
20
|
-
import
|
|
21
|
-
import
|
|
22
|
-
import
|
|
23
|
-
import
|
|
24
|
-
import
|
|
25
|
-
import
|
|
26
|
-
import
|
|
27
|
-
import
|
|
5
|
+
import { getMarketplaceItems } from '../marketplace.js';
|
|
6
|
+
import { createStyler, shouldUseColor, supportsTrueColor } from '../ui.js';
|
|
7
|
+
import { usage, welcome } from './format.js';
|
|
8
|
+
import { parseArgs } from './flags.js';
|
|
9
|
+
import { writeLine, isInteractive } from './helpers.js';
|
|
10
|
+
import * as marketplace from './commands/marketplace.js';
|
|
11
|
+
import * as browseModule from './commands/browse.js';
|
|
12
|
+
import * as community from './commands/community.js';
|
|
13
|
+
import * as learn from './commands/learn.js';
|
|
14
|
+
import * as chatModule from './commands/chat.js';
|
|
15
|
+
import * as initModule from './commands/init.js';
|
|
16
|
+
import * as operations from './commands/operations.js';
|
|
17
|
+
import * as exportModule from './commands/export.js';
|
|
18
|
+
import * as watchModule from './commands/watch.js';
|
|
19
|
+
import * as notifyModule from './commands/notify.js';
|
|
20
|
+
import * as todayModule from './commands/today.js';
|
|
21
|
+
import * as welcomeModule from './commands/welcome.js';
|
|
22
|
+
import * as pingModule from './commands/ping.js';
|
|
23
|
+
import * as scanModule from './commands/scan.js';
|
|
24
|
+
import * as outdatedModule from './commands/outdated.js';
|
|
28
25
|
const pkg = JSON.parse(readFileSync(new URL('../../package.json', import.meta.url), 'utf8'));
|
|
29
26
|
export const AGORA_VERSION = pkg.version;
|
|
30
27
|
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
28
|
/**
|
|
49
|
-
*
|
|
50
|
-
*
|
|
51
|
-
* the
|
|
52
|
-
* use non-TTY mock streams.
|
|
29
|
+
* Levenshtein-based suggestion: when the user mistypes a command, pick the
|
|
30
|
+
* closest registered name if it's within edit-distance 3 AND no further from
|
|
31
|
+
* the input than half its length (so "z" doesn't suggest "saved").
|
|
53
32
|
*/
|
|
54
|
-
function
|
|
55
|
-
if (
|
|
56
|
-
return
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
const
|
|
61
|
-
|
|
33
|
+
export function nearestCommand(input) {
|
|
34
|
+
if (!input)
|
|
35
|
+
return null;
|
|
36
|
+
const targets = COMMANDS.map((c) => c.name);
|
|
37
|
+
let best = null;
|
|
38
|
+
let bestDist = Infinity;
|
|
39
|
+
for (const t of targets) {
|
|
40
|
+
const d = levenshtein(input, t);
|
|
41
|
+
if (d < bestDist) {
|
|
42
|
+
bestDist = d;
|
|
43
|
+
best = t;
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
const cap = Math.max(2, Math.floor(input.length / 2));
|
|
47
|
+
return best && bestDist <= Math.min(3, cap) ? best : null;
|
|
48
|
+
}
|
|
49
|
+
function levenshtein(a, b) {
|
|
50
|
+
if (a === b)
|
|
51
|
+
return 0;
|
|
52
|
+
if (!a.length)
|
|
53
|
+
return b.length;
|
|
54
|
+
if (!b.length)
|
|
55
|
+
return a.length;
|
|
56
|
+
const m = a.length;
|
|
57
|
+
const n = b.length;
|
|
58
|
+
let prev = new Array(n + 1);
|
|
59
|
+
let curr = new Array(n + 1);
|
|
60
|
+
for (let j = 0; j <= n; j++)
|
|
61
|
+
prev[j] = j;
|
|
62
|
+
for (let i = 1; i <= m; i++) {
|
|
63
|
+
curr[0] = i;
|
|
64
|
+
for (let j = 1; j <= n; j++) {
|
|
65
|
+
const cost = a.charCodeAt(i - 1) === b.charCodeAt(j - 1) ? 0 : 1;
|
|
66
|
+
curr[j] = Math.min(curr[j - 1] + 1, prev[j] + 1, prev[j - 1] + cost);
|
|
67
|
+
}
|
|
68
|
+
[prev, curr] = [curr, prev];
|
|
69
|
+
}
|
|
70
|
+
return prev[n];
|
|
62
71
|
}
|
|
72
|
+
// Active terminal styler. Reassigned once per `runCli` invocation from the
|
|
73
|
+
// caller's stream + env; defaults to plain so any direct formatter use is safe.
|
|
74
|
+
let style = createStyler(false);
|
|
63
75
|
export async function runCli(argv, io) {
|
|
64
76
|
const parsed = parseArgs(argv);
|
|
65
77
|
const env = io.env ?? {};
|
|
@@ -74,7 +86,7 @@ export async function runCli(argv, io) {
|
|
|
74
86
|
writeLine(io.stdout, commandManual(parsed.command));
|
|
75
87
|
}
|
|
76
88
|
else {
|
|
77
|
-
writeLine(io.stdout, usage());
|
|
89
|
+
writeLine(io.stdout, usage(style, VERSION));
|
|
78
90
|
}
|
|
79
91
|
return 0;
|
|
80
92
|
}
|
|
@@ -83,1742 +95,104 @@ export async function runCli(argv, io) {
|
|
|
83
95
|
const { runShell } = await import('./shell.js');
|
|
84
96
|
return runShell(io, style);
|
|
85
97
|
}
|
|
86
|
-
writeLine(io.stdout, welcome(useColor, supportsTrueColor(env)));
|
|
98
|
+
writeLine(io.stdout, welcome(useColor, supportsTrueColor(env), style, VERSION));
|
|
87
99
|
return 0;
|
|
88
100
|
}
|
|
89
101
|
try {
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
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
|
-
}
|
|
181
|
-
return 0;
|
|
182
|
-
}
|
|
183
|
-
default:
|
|
184
|
-
writeLine(io.stderr, `Unknown command: ${parsed.command}`);
|
|
185
|
-
writeLine(io.stderr, 'Run agora help for usage.');
|
|
186
|
-
return 1;
|
|
187
|
-
}
|
|
188
|
-
}
|
|
189
|
-
catch (error) {
|
|
190
|
-
writeLine(io.stderr, error instanceof Error ? error.message : String(error));
|
|
191
|
-
return 1;
|
|
192
|
-
}
|
|
193
|
-
}
|
|
194
|
-
export function parseArgs(argv) {
|
|
195
|
-
const flags = {};
|
|
196
|
-
const positionals = [];
|
|
197
|
-
for (let index = 0; index < argv.length; index += 1) {
|
|
198
|
-
const arg = argv[index];
|
|
199
|
-
if (arg === '--') {
|
|
200
|
-
positionals.push(...argv.slice(index + 1));
|
|
201
|
-
break;
|
|
202
|
-
}
|
|
203
|
-
if (arg.startsWith('--')) {
|
|
204
|
-
const [rawKey, inlineValue] = arg.slice(2).split(/=(.*)/s, 2);
|
|
205
|
-
const key = normalizeFlag(rawKey);
|
|
206
|
-
if (inlineValue !== undefined) {
|
|
207
|
-
flags[key] = inlineValue;
|
|
208
|
-
}
|
|
209
|
-
else if (!booleanFlags.has(key) &&
|
|
210
|
-
argv[index + 1] &&
|
|
211
|
-
(!argv[index + 1].startsWith('-') || /^-\d/.test(argv[index + 1]))) {
|
|
212
|
-
flags[key] = argv[index + 1];
|
|
213
|
-
index += 1;
|
|
214
|
-
}
|
|
215
|
-
else {
|
|
216
|
-
flags[key] = true;
|
|
217
|
-
}
|
|
218
|
-
continue;
|
|
219
|
-
}
|
|
220
|
-
if (arg.startsWith('-') && arg.length > 1) {
|
|
221
|
-
const key = shortFlag(arg);
|
|
222
|
-
if (!booleanFlags.has(key) &&
|
|
223
|
-
argv[index + 1] &&
|
|
224
|
-
(!argv[index + 1].startsWith('-') || /^-\d/.test(argv[index + 1]))) {
|
|
225
|
-
flags[key] = argv[index + 1];
|
|
226
|
-
index += 1;
|
|
102
|
+
const cmd = {
|
|
103
|
+
search: marketplace.commandSearch,
|
|
104
|
+
browse: marketplace.commandBrowse,
|
|
105
|
+
trending: marketplace.commandTrending,
|
|
106
|
+
workflows: marketplace.commandWorkflows,
|
|
107
|
+
similar: marketplace.commandSimilar,
|
|
108
|
+
compare: marketplace.commandCompare,
|
|
109
|
+
news: community.commandNews,
|
|
110
|
+
community: community.commandCommunity,
|
|
111
|
+
thread: community.commandThread,
|
|
112
|
+
post: community.commandPost,
|
|
113
|
+
reply: community.commandReply,
|
|
114
|
+
vote: community.commandVote,
|
|
115
|
+
flag: community.commandFlag,
|
|
116
|
+
admin: community.commandAdmin,
|
|
117
|
+
discussions: community.commandDiscussions,
|
|
118
|
+
discuss: community.commandDiscuss,
|
|
119
|
+
tutorials: learn.commandTutorials,
|
|
120
|
+
tutorial: learn.commandTutorial,
|
|
121
|
+
chat: chatModule.commandChat,
|
|
122
|
+
init: initModule.commandInit,
|
|
123
|
+
use: initModule.commandUse,
|
|
124
|
+
install: operations.commandInstall,
|
|
125
|
+
mcp: operations.commandMcp,
|
|
126
|
+
save: operations.commandSave,
|
|
127
|
+
saved: operations.commandSaved,
|
|
128
|
+
remove: operations.commandRemove,
|
|
129
|
+
publish: operations.commandPublish,
|
|
130
|
+
review: operations.commandReview,
|
|
131
|
+
reviews: operations.commandReviews,
|
|
132
|
+
profile: operations.commandProfile,
|
|
133
|
+
preferences: operations.commandPreferences,
|
|
134
|
+
history: operations.commandHistory,
|
|
135
|
+
config: operations.commandConfig,
|
|
136
|
+
show: (p, io2, style2) => operations.commandConfig({ ...p, args: ['show', ...p.args], command: 'config' }, io2, style2),
|
|
137
|
+
edit: (p, io2, style2) => operations.commandConfig({ ...p, args: ['edit', ...p.args], command: 'config' }, io2, style2),
|
|
138
|
+
diff: (p, io2, style2) => operations.commandConfig({ ...p, args: ['diff', ...p.args], command: 'config' }, io2, style2),
|
|
139
|
+
export: exportModule.commandExport,
|
|
140
|
+
watch: watchModule.commandWatch,
|
|
141
|
+
notify: notifyModule.commandNotify,
|
|
142
|
+
today: todayModule.commandToday,
|
|
143
|
+
open: browseModule.commandOpen,
|
|
144
|
+
share: browseModule.commandShare,
|
|
145
|
+
ping: pingModule.commandPing,
|
|
146
|
+
scan: scanModule.commandScan,
|
|
147
|
+
outdated: outdatedModule.commandOutdated,
|
|
148
|
+
author: marketplace.commandAuthor,
|
|
149
|
+
bookmarks: operations.commandBookmarks,
|
|
150
|
+
welcome: welcomeModule.commandWelcome,
|
|
151
|
+
auth: operations.commandAuth,
|
|
152
|
+
login: (p, io2, style2) => operations.commandAuth({ ...p, args: ['login', ...p.args], command: 'auth' }, io2, style2),
|
|
153
|
+
logout: (p, io2, style2) => operations.commandAuth({ ...p, args: ['logout'], command: 'auth' }, io2, style2),
|
|
154
|
+
whoami: (p, io2, style2) => operations.commandAuth({ ...p, args: ['status'], command: 'auth', flags: { ...p.flags, json: true } }, io2, style2)
|
|
155
|
+
};
|
|
156
|
+
const handler = cmd[parsed.command];
|
|
157
|
+
if (handler)
|
|
158
|
+
return await handler(parsed, io, style);
|
|
159
|
+
if (parsed.command === 'help') {
|
|
160
|
+
const helpTarget = parsed.args[0];
|
|
161
|
+
if (helpTarget) {
|
|
162
|
+
const meta = COMMANDS.find((c) => c.name === helpTarget);
|
|
163
|
+
if (!meta) {
|
|
164
|
+
writeLine(io.stderr, `Unknown command: ${helpTarget}`);
|
|
165
|
+
writeLine(io.stderr, 'Run `agora help` for a list of commands.');
|
|
166
|
+
return 1;
|
|
167
|
+
}
|
|
168
|
+
writeLine(io.stdout, commandManual(helpTarget));
|
|
227
169
|
}
|
|
228
170
|
else {
|
|
229
|
-
|
|
171
|
+
writeLine(io.stdout, usage(style, VERSION));
|
|
230
172
|
}
|
|
231
|
-
continue;
|
|
232
|
-
}
|
|
233
|
-
positionals.push(arg);
|
|
234
|
-
}
|
|
235
|
-
return {
|
|
236
|
-
command: positionals[0],
|
|
237
|
-
args: positionals.slice(1),
|
|
238
|
-
flags
|
|
239
|
-
};
|
|
240
|
-
}
|
|
241
|
-
async function commandSearch(parsed, io) {
|
|
242
|
-
const query = parsed.args.join(' ');
|
|
243
|
-
const category = stringFlag(parsed, 'category', 'c') || 'all';
|
|
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
|
-
});
|
|
260
|
-
const results = result.data;
|
|
261
|
-
warnFallback(result, io);
|
|
262
|
-
if (parsed.flags.json) {
|
|
263
|
-
writeJson(io.stdout, sourcePayload(result, { query, category, sortBy, sortOrder, page, count: results.length, items: results }));
|
|
264
|
-
return 0;
|
|
265
|
-
}
|
|
266
|
-
if (results.length === 0) {
|
|
267
|
-
writeLine(io.stdout, `No results found for "${query}".`);
|
|
268
|
-
return 0;
|
|
269
|
-
}
|
|
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
|
-
});
|
|
288
|
-
return 0;
|
|
289
|
-
}
|
|
290
|
-
async function commandBrowse(parsed, io) {
|
|
291
|
-
const id = parsed.args[0];
|
|
292
|
-
if (!id)
|
|
293
|
-
return usageError(io, 'browse requires an item id');
|
|
294
|
-
const result = await findMarketplaceSource({
|
|
295
|
-
...sourceOptions(parsed, io),
|
|
296
|
-
id,
|
|
297
|
-
type: stringFlag(parsed, 'type', 't')
|
|
298
|
-
});
|
|
299
|
-
const item = result.data;
|
|
300
|
-
warnFallback(result, io);
|
|
301
|
-
if (!item)
|
|
302
|
-
return usageError(io, `Item not found: ${id}`);
|
|
303
|
-
if (parsed.flags.json) {
|
|
304
|
-
writeJson(io.stdout, sourcePayload(result, { item }));
|
|
305
|
-
return 0;
|
|
306
|
-
}
|
|
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
|
-
}
|
|
318
|
-
return 0;
|
|
319
|
-
}
|
|
320
|
-
async function commandTrending(parsed, io) {
|
|
321
|
-
const category = stringFlag(parsed, 'category', 'c') || parsed.args[0] || 'all';
|
|
322
|
-
const limit = numberFlag(parsed, 'limit', 'n') || 5;
|
|
323
|
-
const table = Boolean(parsed.flags.table);
|
|
324
|
-
const result = await trendingMarketplaceSource({ ...sourceOptions(parsed, io), category, limit });
|
|
325
|
-
const items = result.data;
|
|
326
|
-
warnFallback(result, io);
|
|
327
|
-
if (parsed.flags.json) {
|
|
328
|
-
writeJson(io.stdout, sourcePayload(result, { category, count: items.length, tags: getTrendingTags(), items }));
|
|
329
|
-
return 0;
|
|
330
|
-
}
|
|
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(', ')}`);
|
|
341
|
-
return 0;
|
|
342
|
-
}
|
|
343
|
-
async function commandWorkflows(parsed, io) {
|
|
344
|
-
const query = parsed.args.join(' ');
|
|
345
|
-
const limit = numberFlag(parsed, 'limit', 'n') || 10;
|
|
346
|
-
const result = await searchMarketplaceSource({
|
|
347
|
-
...sourceOptions(parsed, io),
|
|
348
|
-
query,
|
|
349
|
-
category: 'workflow',
|
|
350
|
-
limit
|
|
351
|
-
});
|
|
352
|
-
const workflows = result.data;
|
|
353
|
-
warnFallback(result, io);
|
|
354
|
-
if (parsed.flags.json) {
|
|
355
|
-
writeJson(io.stdout, sourcePayload(result, { query, count: workflows.length, workflows }));
|
|
356
|
-
return 0;
|
|
357
|
-
}
|
|
358
|
-
writeLine(io.stdout, header('agora workflows', [`${workflows.length} results`, sourceLabel(result)]));
|
|
359
|
-
writeLine(io.stdout, '');
|
|
360
|
-
writeLine(io.stdout, formatItemList(workflows));
|
|
361
|
-
return 0;
|
|
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
173
|
return 0;
|
|
538
174
|
}
|
|
539
|
-
if (
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
const
|
|
547
|
-
|
|
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
|
-
}
|
|
686
|
-
async function commandTutorials(parsed, io) {
|
|
687
|
-
const query = parsed.args.join(' ');
|
|
688
|
-
const level = tutorialLevelFlag(parsed);
|
|
689
|
-
if (!level.ok)
|
|
690
|
-
return usageError(io, level.error);
|
|
691
|
-
const limit = numberFlag(parsed, 'limit', 'n') || 20;
|
|
692
|
-
const result = await tutorialsSource({
|
|
693
|
-
...sourceOptions(parsed, io),
|
|
694
|
-
query,
|
|
695
|
-
level: level.value,
|
|
696
|
-
limit
|
|
697
|
-
});
|
|
698
|
-
const tutorials = result.data;
|
|
699
|
-
warnFallback(result, io);
|
|
700
|
-
if (parsed.flags.json) {
|
|
701
|
-
writeJson(io.stdout, sourcePayload(result, { query, level: level.value, count: tutorials.length, tutorials }));
|
|
702
|
-
return 0;
|
|
703
|
-
}
|
|
704
|
-
if (tutorials.length === 0) {
|
|
705
|
-
writeLine(io.stdout, query ? `No tutorials match "${query}".` : 'No tutorials found.');
|
|
706
|
-
return 0;
|
|
707
|
-
}
|
|
708
|
-
writeLine(io.stdout, header('agora tutorials', [`${tutorials.length} results`, sourceLabel(result)]));
|
|
709
|
-
writeLine(io.stdout, '');
|
|
710
|
-
writeLine(io.stdout, formatTutorialList(tutorials));
|
|
711
|
-
return 0;
|
|
712
|
-
}
|
|
713
|
-
async function commandTutorial(parsed, io) {
|
|
714
|
-
const id = parsed.args[0];
|
|
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
|
-
}
|
|
724
|
-
const step = tutorialStepNumber(parsed);
|
|
725
|
-
if (!step.ok)
|
|
726
|
-
return usageError(io, step.error);
|
|
727
|
-
const result = await findTutorialSource({ ...sourceOptions(parsed, io), id });
|
|
728
|
-
const tutorial = result.data;
|
|
729
|
-
warnFallback(result, io);
|
|
730
|
-
if (!tutorial)
|
|
731
|
-
return usageError(io, `Tutorial not found: ${id}`);
|
|
732
|
-
if (parsed.flags.json) {
|
|
733
|
-
writeJson(io.stdout, sourcePayload(result, {
|
|
734
|
-
tutorial,
|
|
735
|
-
step: tutorialStepPayload(tutorial, step.value)
|
|
736
|
-
}));
|
|
737
|
-
return 0;
|
|
738
|
-
}
|
|
739
|
-
writeLine(io.stdout, formatTutorialStep(tutorial, step.value));
|
|
740
|
-
return 0;
|
|
741
|
-
}
|
|
742
|
-
async function commandDiscussions(parsed, io) {
|
|
743
|
-
const category = stringFlag(parsed, 'category', 'c') || 'all';
|
|
744
|
-
const query = parsed.args.join(' ');
|
|
745
|
-
const result = await discussionsSource({ ...sourceOptions(parsed, io), category, query });
|
|
746
|
-
const discussions = result.data;
|
|
747
|
-
warnFallback(result, io);
|
|
748
|
-
if (parsed.flags.json) {
|
|
749
|
-
writeJson(io.stdout, sourcePayload(result, { category, query, count: discussions.length, discussions }));
|
|
750
|
-
return 0;
|
|
751
|
-
}
|
|
752
|
-
if (discussions.length === 0) {
|
|
753
|
-
writeLine(io.stdout, 'No discussions found.');
|
|
754
|
-
return 0;
|
|
755
|
-
}
|
|
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) => {
|
|
760
|
-
return [
|
|
761
|
-
`${index + 1}. ${style.accent(discussion.title)} ${style.dim('[' + discussion.category + ']')}`,
|
|
762
|
-
` ${truncate(discussion.content, 88)}`,
|
|
763
|
-
` ${style.dim('replies ' + discussion.replies + ' · stars ' + discussion.stars + ' · by ' + discussion.author)}`
|
|
764
|
-
].join('\n');
|
|
765
|
-
})
|
|
766
|
-
.join('\n\n'));
|
|
767
|
-
return 0;
|
|
768
|
-
}
|
|
769
|
-
async function commandDiscuss(parsed, io) {
|
|
770
|
-
const source = writeSourceOptions(parsed, io);
|
|
771
|
-
if (!source.ok)
|
|
772
|
-
return usageError(io, source.error);
|
|
773
|
-
const title = requiredStringFlag(parsed, 'title');
|
|
774
|
-
const content = contentInput(parsed, io);
|
|
775
|
-
if (!title || !content) {
|
|
776
|
-
return usageError(io, 'discuss requires --title and --content or --content-file');
|
|
777
|
-
}
|
|
778
|
-
const category = discussionCategoryFlag(parsed);
|
|
779
|
-
if (!category.ok)
|
|
780
|
-
return usageError(io, category.error);
|
|
781
|
-
const result = await createDiscussionSource(source.options, {
|
|
782
|
-
title,
|
|
783
|
-
content,
|
|
784
|
-
category: category.value
|
|
785
|
-
});
|
|
786
|
-
if (parsed.flags.json) {
|
|
787
|
-
writeJson(io.stdout, sourcePayload(result, { discussion: result.data }));
|
|
788
|
-
return 0;
|
|
789
|
-
}
|
|
790
|
-
writeLine(io.stdout, `Created discussion ${style.accent(result.data.id)}`);
|
|
791
|
-
writeLine(io.stdout, `${result.data.title} (${sourceLabel(result)})`);
|
|
792
|
-
return 0;
|
|
793
|
-
}
|
|
794
|
-
async function commandInstall(parsed, io) {
|
|
795
|
-
const id = parsed.args[0];
|
|
796
|
-
if (!id)
|
|
797
|
-
return usageError(io, 'install requires an item id');
|
|
798
|
-
const source = await findMarketplaceSource({
|
|
799
|
-
...sourceOptions(parsed, io),
|
|
800
|
-
id,
|
|
801
|
-
type: stringFlag(parsed, 'type', 't')
|
|
802
|
-
});
|
|
803
|
-
const item = source.data;
|
|
804
|
-
warnFallback(source, io);
|
|
805
|
-
if (!item)
|
|
806
|
-
return usageError(io, `Item not found: ${id}`);
|
|
807
|
-
const configPath = detectOpenCodeConfigPath({
|
|
808
|
-
explicitPath: stringFlag(parsed, 'config'),
|
|
809
|
-
cwd: io.cwd,
|
|
810
|
-
env: io.env
|
|
811
|
-
});
|
|
812
|
-
const loaded = loadOpenCodeConfig(configPath);
|
|
813
|
-
if (loaded.error)
|
|
814
|
-
return usageError(io, `${loaded.path}: ${loaded.error}`);
|
|
815
|
-
const plan = createInstallPlan(item, loaded.config);
|
|
816
|
-
if (!plan.installable)
|
|
817
|
-
return usageError(io, plan.reason || `${item.name} is not installable`);
|
|
818
|
-
if (parsed.flags.json) {
|
|
819
|
-
writeJson(io.stdout, {
|
|
820
|
-
source: source.source,
|
|
821
|
-
apiUrl: source.apiUrl,
|
|
822
|
-
fallbackReason: source.fallbackReason,
|
|
823
|
-
item,
|
|
824
|
-
configPath,
|
|
825
|
-
write: Boolean(parsed.flags.write),
|
|
826
|
-
commands: plan.commands,
|
|
827
|
-
notes: plan.notes,
|
|
828
|
-
config: plan.config
|
|
829
|
-
});
|
|
830
|
-
return 0;
|
|
831
|
-
}
|
|
832
|
-
if (parsed.flags.write) {
|
|
833
|
-
writeOpenCodeConfig(configPath, plan.config);
|
|
834
|
-
writeLine(io.stdout, `Installed ${style.accent(item.name)}`);
|
|
835
|
-
writeLine(io.stdout, `${style.dim('Config')} ${configPath}`);
|
|
836
|
-
if (plan.commands.length) {
|
|
837
|
-
writeLine(io.stdout, 'Installing packages...');
|
|
838
|
-
for (const cmd of plan.commands) {
|
|
839
|
-
try {
|
|
840
|
-
execSync(cmd, { stdio: 'pipe', timeout: 120000 });
|
|
841
|
-
writeLine(io.stdout, ` ✓ ${cmd}`);
|
|
842
|
-
}
|
|
843
|
-
catch {
|
|
844
|
-
writeLine(io.stdout, ` ! Failed: ${cmd} (may already be installed)`);
|
|
845
|
-
}
|
|
846
|
-
}
|
|
175
|
+
if (parsed.command === 'menu')
|
|
176
|
+
return await runInteractiveMenu(io, style);
|
|
177
|
+
if (parsed.command === 'tui')
|
|
178
|
+
return await runTui(io, { initial: 'home' });
|
|
179
|
+
if (parsed.command === 'completions')
|
|
180
|
+
return await commandCompletions(parsed, io, style);
|
|
181
|
+
if (parsed.command === 'shell') {
|
|
182
|
+
const { runShell } = await import('./shell.js');
|
|
183
|
+
return runShell(io, style);
|
|
847
184
|
}
|
|
848
|
-
|
|
849
|
-
|
|
850
|
-
|
|
851
|
-
|
|
852
|
-
|
|
853
|
-
|
|
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
|
-
}
|
|
862
|
-
if (plan.commands.length) {
|
|
863
|
-
writeLine(io.stdout, '\nCommands:');
|
|
864
|
-
writeLine(io.stdout, plan.commands.join('\n'));
|
|
865
|
-
}
|
|
866
|
-
writeLine(io.stdout, '\nopencode.json preview:');
|
|
867
|
-
writeLine(io.stdout, formatConfigJson(plan.config));
|
|
868
|
-
writeLine(io.stdout, '\nRun with --write to update the config file and install packages.');
|
|
869
|
-
return 0;
|
|
870
|
-
}
|
|
871
|
-
async function commandMcp(_parsed, io) {
|
|
872
|
-
const { runMcpServer } = await import('./mcp-server.js');
|
|
873
|
-
try {
|
|
874
|
-
await runMcpServer();
|
|
185
|
+
writeLine(io.stderr, `Unknown command: ${parsed.command}`);
|
|
186
|
+
const suggestion = nearestCommand(parsed.command);
|
|
187
|
+
if (suggestion)
|
|
188
|
+
writeLine(io.stderr, `Did you mean: ${suggestion}?`);
|
|
189
|
+
writeLine(io.stderr, 'Run agora help for usage.');
|
|
190
|
+
return 1;
|
|
875
191
|
}
|
|
876
192
|
catch (error) {
|
|
877
193
|
writeLine(io.stderr, error instanceof Error ? error.message : String(error));
|
|
878
194
|
return 1;
|
|
879
195
|
}
|
|
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
|
-
}
|
|
1040
|
-
async function commandInit(parsed, io) {
|
|
1041
|
-
const cwd = io.cwd || process.cwd();
|
|
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
|
-
}
|
|
1056
|
-
if (parsed.flags.json) {
|
|
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
|
-
});
|
|
1085
|
-
return 0;
|
|
1086
|
-
}
|
|
1087
|
-
writeLine(io.stdout, `Scanning ${cwd}...`);
|
|
1088
|
-
writeLine(io.stdout, ` ${style.dim('Project type')} ${scan.type}`);
|
|
1089
|
-
if (scan.frameworks.length)
|
|
1090
|
-
writeLine(io.stdout, ` ${style.dim('Frameworks')} ${scan.frameworks.join(', ')}`);
|
|
1091
|
-
if (scan.hasDocker)
|
|
1092
|
-
writeLine(io.stdout, ` ${style.dim('Docker')} detected`);
|
|
1093
|
-
if (scan.hasTests)
|
|
1094
|
-
writeLine(io.stdout, ` ${style.dim('Tests')} detected`);
|
|
1095
|
-
if (scan.hasDatabase)
|
|
1096
|
-
writeLine(io.stdout, ` ${style.dim('Database')} detected`);
|
|
1097
|
-
if (!parsed.flags.dryRun) {
|
|
1098
|
-
applyInitPlan(plan, configPath);
|
|
1099
|
-
writeLine(io.stdout, `\nWrote config to ${configPath}`);
|
|
1100
|
-
const commandPath = installAgoraCommand(cwd);
|
|
1101
|
-
writeLine(io.stdout, `Installed /agora slash command at ${commandPath}`);
|
|
1102
|
-
if (plan.commands.length) {
|
|
1103
|
-
writeLine(io.stdout, '\nInstalling MCP server packages...');
|
|
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)` : ''}`);
|
|
1129
|
-
}
|
|
1130
|
-
writeLine(io.stdout, '\n✓ Agora initialized! Restart OpenCode to pick up the changes.');
|
|
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.');
|
|
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.');
|
|
1136
|
-
if (plan.workflows.length)
|
|
1137
|
-
writeLine(io.stdout, ` ${plan.workflows.length} workflows available via \`agora use\`.`);
|
|
1138
|
-
for (const note of plan.notes)
|
|
1139
|
-
writeLine(io.stdout, ` ${note}`);
|
|
1140
|
-
}
|
|
1141
|
-
else {
|
|
1142
|
-
writeLine(io.stdout, '\n--- Dry run ---');
|
|
1143
|
-
writeLine(io.stdout, `Target config: ${configPath}`);
|
|
1144
|
-
writeLine(io.stdout, formatConfigJson(plan.config));
|
|
1145
|
-
writeLine(io.stdout, `\nSlash command: ${join(cwd, '.opencode', 'command', 'agora.md')}`);
|
|
1146
|
-
writeLine(io.stdout, '\nPackages to install:');
|
|
1147
|
-
for (const cmd of plan.commands)
|
|
1148
|
-
writeLine(io.stdout, ` ${cmd}`);
|
|
1149
|
-
writeLine(io.stdout, '\nRun without --dry-run to apply.');
|
|
1150
|
-
}
|
|
1151
|
-
return 0;
|
|
1152
|
-
}
|
|
1153
|
-
async function commandUse(parsed, io) {
|
|
1154
|
-
const id = parsed.args[0];
|
|
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
|
-
}
|
|
1163
|
-
const workflow = sampleWorkflows.find((w) => w.id === id || w.name.toLowerCase() === id.toLowerCase());
|
|
1164
|
-
if (!workflow)
|
|
1165
|
-
return usageError(io, `Workflow not found: ${id}. Run \`agora workflows\` to see available workflows.`);
|
|
1166
|
-
const cwd = io.cwd || process.cwd();
|
|
1167
|
-
const skillsDir = join(cwd, '.opencode', 'skills');
|
|
1168
|
-
mkdirSync(skillsDir, { recursive: true });
|
|
1169
|
-
const skillId = workflow.id.replace(/^wf-/, 'skill-');
|
|
1170
|
-
const skillPath = join(skillsDir, `${skillId}.md`);
|
|
1171
|
-
const skillContent = `---
|
|
1172
|
-
name: ${workflow.name}
|
|
1173
|
-
description: ${workflow.description}
|
|
1174
|
-
model: ${workflow.model || ''}
|
|
1175
|
-
tags: [${workflow.tags.map((t) => `"${t}"`).join(', ')}]
|
|
1176
|
-
---
|
|
1177
|
-
|
|
1178
|
-
${workflow.prompt}
|
|
1179
|
-
`;
|
|
1180
|
-
writeFileSync(skillPath, skillContent, 'utf8');
|
|
1181
|
-
const configPath = detectOpenCodeConfigPath({ cwd, env: io.env });
|
|
1182
|
-
const loaded = loadOpenCodeConfig(configPath);
|
|
1183
|
-
if (loaded.error)
|
|
1184
|
-
return usageError(io, `${loaded.path}: ${loaded.error}`);
|
|
1185
|
-
const plugins = new Set(loaded.config.plugin || []);
|
|
1186
|
-
plugins.add(skillId);
|
|
1187
|
-
const updatedConfig = {
|
|
1188
|
-
...loaded.config,
|
|
1189
|
-
plugin: Array.from(plugins)
|
|
1190
|
-
};
|
|
1191
|
-
writeOpenCodeConfig(configPath, updatedConfig);
|
|
1192
|
-
if (parsed.flags.json) {
|
|
1193
|
-
writeJson(io.stdout, { workflow: workflow.id, skillPath, registered: true });
|
|
1194
|
-
return 0;
|
|
1195
|
-
}
|
|
1196
|
-
writeLine(io.stdout, `✓ Applied "${workflow.name}" as an OpenCode skill.`);
|
|
1197
|
-
writeLine(io.stdout, ` Skill file: ${skillPath}`);
|
|
1198
|
-
writeLine(io.stdout, ` Registered in: ${configPath}`);
|
|
1199
|
-
writeLine(io.stdout, ' Restart OpenCode to start using it.');
|
|
1200
|
-
return 0;
|
|
1201
|
-
}
|
|
1202
|
-
function commandConfig(parsed, io) {
|
|
1203
|
-
const subcommand = parsed.args[0] || 'doctor';
|
|
1204
|
-
if (subcommand !== 'doctor') {
|
|
1205
|
-
return usageError(io, `Unknown config command: ${subcommand}`);
|
|
1206
|
-
}
|
|
1207
|
-
const configPath = detectOpenCodeConfigPath({
|
|
1208
|
-
explicitPath: stringFlag(parsed, 'config'),
|
|
1209
|
-
cwd: io.cwd,
|
|
1210
|
-
env: io.env
|
|
1211
|
-
});
|
|
1212
|
-
const report = doctorOpenCodeConfig(configPath);
|
|
1213
|
-
if (parsed.flags.json) {
|
|
1214
|
-
writeJson(io.stdout, report);
|
|
1215
|
-
return report.valid ? 0 : 1;
|
|
1216
|
-
}
|
|
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'}`);
|
|
1220
|
-
if (report.error)
|
|
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'}`);
|
|
1225
|
-
return report.valid ? 0 : 1;
|
|
1226
|
-
}
|
|
1227
|
-
async function commandAuth(parsed, io) {
|
|
1228
|
-
const subcommand = parsed.args[0] || 'status';
|
|
1229
|
-
const dataDir = detectDataDir(parsed, io);
|
|
1230
|
-
const state = loadAgoraState(dataDir);
|
|
1231
|
-
const existingAuth = getAuthState(state);
|
|
1232
|
-
if (subcommand === 'login') {
|
|
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;
|
|
1248
|
-
}
|
|
1249
|
-
// ── Device-code flow ─────────────────────────────────────────────────
|
|
1250
|
-
const apiUrl = stringFlag(parsed, 'apiUrl') || envString(io, 'AGORA_API_URL') || existingAuth?.apiUrl;
|
|
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'}`);
|
|
1330
|
-
}
|
|
1331
|
-
}
|
|
1332
|
-
if (subcommand === 'status') {
|
|
1333
|
-
if (parsed.flags.json) {
|
|
1334
|
-
writeJson(io.stdout, authStatusPayload(dataDir, existingAuth));
|
|
1335
|
-
return 0;
|
|
1336
|
-
}
|
|
1337
|
-
writeLine(io.stdout, `${style.dim('Authenticated')} ${existingAuth ? 'yes' : 'no'}`);
|
|
1338
|
-
if (existingAuth) {
|
|
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)}`);
|
|
1342
|
-
}
|
|
1343
|
-
writeLine(io.stdout, `${style.dim('State')} ${getAgoraStatePath(dataDir)}`);
|
|
1344
|
-
return 0;
|
|
1345
|
-
}
|
|
1346
|
-
if (subcommand === 'logout') {
|
|
1347
|
-
if (!existingAuth) {
|
|
1348
|
-
if (parsed.flags.json) {
|
|
1349
|
-
writeJson(io.stdout, authStatusPayload(dataDir, undefined));
|
|
1350
|
-
return 0;
|
|
1351
|
-
}
|
|
1352
|
-
writeLine(io.stdout, 'No stored Agora API token');
|
|
1353
|
-
return 0;
|
|
1354
|
-
}
|
|
1355
|
-
writeAgoraState(dataDir, clearAuthState(state));
|
|
1356
|
-
if (parsed.flags.json) {
|
|
1357
|
-
writeJson(io.stdout, authStatusPayload(dataDir, undefined));
|
|
1358
|
-
return 0;
|
|
1359
|
-
}
|
|
1360
|
-
writeLine(io.stdout, 'Removed stored Agora API token');
|
|
1361
|
-
return 0;
|
|
1362
|
-
}
|
|
1363
|
-
return usageError(io, `Unknown auth command: ${subcommand}`);
|
|
1364
|
-
}
|
|
1365
|
-
async function commandSave(parsed, io) {
|
|
1366
|
-
const id = parsed.args[0];
|
|
1367
|
-
if (!id)
|
|
1368
|
-
return usageError(io, 'save requires an item id');
|
|
1369
|
-
const source = await findMarketplaceSource({
|
|
1370
|
-
...sourceOptions(parsed, io),
|
|
1371
|
-
id,
|
|
1372
|
-
type: stringFlag(parsed, 'type', 't')
|
|
1373
|
-
});
|
|
1374
|
-
const item = source.data;
|
|
1375
|
-
warnFallback(source, io);
|
|
1376
|
-
if (!item)
|
|
1377
|
-
return usageError(io, `Item not found: ${id}`);
|
|
1378
|
-
const dataDir = detectDataDir(parsed, io);
|
|
1379
|
-
const state = loadAgoraState(dataDir);
|
|
1380
|
-
const result = saveItemToState(state, item);
|
|
1381
|
-
writeAgoraState(dataDir, result.state);
|
|
1382
|
-
if (parsed.flags.json) {
|
|
1383
|
-
writeJson(io.stdout, {
|
|
1384
|
-
source: source.source,
|
|
1385
|
-
apiUrl: source.apiUrl,
|
|
1386
|
-
fallbackReason: source.fallbackReason,
|
|
1387
|
-
dataDir,
|
|
1388
|
-
statePath: getAgoraStatePath(dataDir),
|
|
1389
|
-
added: result.added,
|
|
1390
|
-
item
|
|
1391
|
-
});
|
|
1392
|
-
return 0;
|
|
1393
|
-
}
|
|
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)}`);
|
|
1396
|
-
return 0;
|
|
1397
|
-
}
|
|
1398
|
-
function commandSaved(parsed, io) {
|
|
1399
|
-
const query = parsed.args.join(' ').trim().toLowerCase();
|
|
1400
|
-
const dataDir = detectDataDir(parsed, io);
|
|
1401
|
-
const state = loadAgoraState(dataDir);
|
|
1402
|
-
const saved = resolveSavedItems(state).filter((entry) => matchesSavedQuery(entry, query));
|
|
1403
|
-
if (parsed.flags.json) {
|
|
1404
|
-
writeJson(io.stdout, {
|
|
1405
|
-
dataDir,
|
|
1406
|
-
statePath: getAgoraStatePath(dataDir),
|
|
1407
|
-
count: saved.length,
|
|
1408
|
-
items: saved
|
|
1409
|
-
});
|
|
1410
|
-
return 0;
|
|
1411
|
-
}
|
|
1412
|
-
if (saved.length === 0) {
|
|
1413
|
-
writeLine(io.stdout, query ? `No saved items match "${query}".` : 'No saved items yet.');
|
|
1414
|
-
writeLine(io.stdout, 'Run agora save <id> to save a package or workflow.');
|
|
1415
|
-
return 0;
|
|
1416
|
-
}
|
|
1417
|
-
writeLine(io.stdout, header('agora saved', [`${saved.length} items`]));
|
|
1418
|
-
writeLine(io.stdout, formatSavedList(saved));
|
|
1419
|
-
return 0;
|
|
1420
|
-
}
|
|
1421
|
-
function commandRemove(parsed, io) {
|
|
1422
|
-
const id = parsed.args[0];
|
|
1423
|
-
if (!id)
|
|
1424
|
-
return usageError(io, 'remove requires an item id');
|
|
1425
|
-
const dataDir = detectDataDir(parsed, io);
|
|
1426
|
-
const state = loadAgoraState(dataDir);
|
|
1427
|
-
const targetId = resolveSavedItems(state).find((entry) => {
|
|
1428
|
-
return entry.saved.id === id || entry.item?.id === id || entry.item?.name === id;
|
|
1429
|
-
})?.saved.id || id;
|
|
1430
|
-
const result = removeItemFromState(state, targetId);
|
|
1431
|
-
writeAgoraState(dataDir, result.state);
|
|
1432
|
-
if (parsed.flags.json) {
|
|
1433
|
-
writeJson(io.stdout, {
|
|
1434
|
-
dataDir,
|
|
1435
|
-
statePath: getAgoraStatePath(dataDir),
|
|
1436
|
-
removed: result.removed,
|
|
1437
|
-
id: targetId
|
|
1438
|
-
});
|
|
1439
|
-
return result.removed ? 0 : 1;
|
|
1440
|
-
}
|
|
1441
|
-
if (!result.removed) {
|
|
1442
|
-
return usageError(io, `Saved item not found: ${id}`);
|
|
1443
|
-
}
|
|
1444
|
-
writeLine(io.stdout, `Removed ${style.accent(targetId)}`);
|
|
1445
|
-
return 0;
|
|
1446
|
-
}
|
|
1447
|
-
async function commandPublish(parsed, io) {
|
|
1448
|
-
const kind = parsed.args[0];
|
|
1449
|
-
if (kind !== 'package' && kind !== 'workflow') {
|
|
1450
|
-
return usageError(io, 'publish requires "package" or "workflow"');
|
|
1451
|
-
}
|
|
1452
|
-
const source = writeSourceOptions(parsed, io);
|
|
1453
|
-
if (!source.ok)
|
|
1454
|
-
return usageError(io, source.error);
|
|
1455
|
-
const name = requiredStringFlag(parsed, 'name');
|
|
1456
|
-
const description = requiredStringFlag(parsed, 'description', 'd');
|
|
1457
|
-
if (!name || !description) {
|
|
1458
|
-
return usageError(io, 'publish requires --name and --description');
|
|
1459
|
-
}
|
|
1460
|
-
if (kind === 'package') {
|
|
1461
|
-
const npmPackage = stringFlag(parsed, 'npm') || stringFlag(parsed, 'npmPackage');
|
|
1462
|
-
const category = stringFlag(parsed, 'category', 'c') || 'mcp';
|
|
1463
|
-
if (category === 'mcp' && !npmPackage) {
|
|
1464
|
-
return usageError(io, 'publish package requires --npm for MCP packages');
|
|
1465
|
-
}
|
|
1466
|
-
const result = await publishPackageSource(source.options, {
|
|
1467
|
-
id: stringFlag(parsed, 'id'),
|
|
1468
|
-
name,
|
|
1469
|
-
description,
|
|
1470
|
-
version: stringFlag(parsed, 'version') || '1.0.0',
|
|
1471
|
-
category,
|
|
1472
|
-
tags: tagsFlag(parsed),
|
|
1473
|
-
repository: stringFlag(parsed, 'repo') || stringFlag(parsed, 'repository'),
|
|
1474
|
-
npmPackage
|
|
1475
|
-
});
|
|
1476
|
-
if (parsed.flags.json) {
|
|
1477
|
-
writeJson(io.stdout, sourcePayload(result, { item: result.data }));
|
|
1478
|
-
return 0;
|
|
1479
|
-
}
|
|
1480
|
-
writeLine(io.stdout, `Published package ${style.accent(result.data.id)}`);
|
|
1481
|
-
writeLine(io.stdout, `${result.data.name} (${sourceLabel(result)})`);
|
|
1482
|
-
return 0;
|
|
1483
|
-
}
|
|
1484
|
-
const prompt = promptInput(parsed, io);
|
|
1485
|
-
if (prompt === undefined) {
|
|
1486
|
-
return usageError(io, 'publish workflow requires --prompt or --prompt-file');
|
|
1487
|
-
}
|
|
1488
|
-
const result = await publishWorkflowSource(source.options, {
|
|
1489
|
-
id: stringFlag(parsed, 'id'),
|
|
1490
|
-
name,
|
|
1491
|
-
description,
|
|
1492
|
-
prompt,
|
|
1493
|
-
model: stringFlag(parsed, 'model'),
|
|
1494
|
-
tags: tagsFlag(parsed)
|
|
1495
|
-
});
|
|
1496
|
-
if (parsed.flags.json) {
|
|
1497
|
-
writeJson(io.stdout, sourcePayload(result, { item: result.data }));
|
|
1498
|
-
return 0;
|
|
1499
|
-
}
|
|
1500
|
-
writeLine(io.stdout, `Published workflow ${style.accent(result.data.id)}`);
|
|
1501
|
-
writeLine(io.stdout, `${result.data.name} (${sourceLabel(result)})`);
|
|
1502
|
-
return 0;
|
|
1503
|
-
}
|
|
1504
|
-
async function commandReview(parsed, io) {
|
|
1505
|
-
const itemId = parsed.args[0];
|
|
1506
|
-
if (!itemId)
|
|
1507
|
-
return usageError(io, 'review requires an item id');
|
|
1508
|
-
const rating = numberFlag(parsed, 'rating', 'r');
|
|
1509
|
-
const content = requiredStringFlag(parsed, 'content');
|
|
1510
|
-
if (!rating || rating < 1 || rating > 5 || !content) {
|
|
1511
|
-
return usageError(io, 'review requires --rating 1-5 and --content');
|
|
1512
|
-
}
|
|
1513
|
-
const source = writeSourceOptions(parsed, io);
|
|
1514
|
-
if (!source.ok)
|
|
1515
|
-
return usageError(io, source.error);
|
|
1516
|
-
const result = await createReviewSource(source.options, {
|
|
1517
|
-
itemId,
|
|
1518
|
-
itemType: itemTypeFlag(parsed, itemId),
|
|
1519
|
-
rating,
|
|
1520
|
-
content
|
|
1521
|
-
});
|
|
1522
|
-
if (parsed.flags.json) {
|
|
1523
|
-
writeJson(io.stdout, sourcePayload(result, { review: result.data }));
|
|
1524
|
-
return 0;
|
|
1525
|
-
}
|
|
1526
|
-
writeLine(io.stdout, `Reviewed ${style.accent(result.data.itemId)}`);
|
|
1527
|
-
writeLine(io.stdout, `${style.dim(result.data.rating + '/5 by ' + result.data.author)}`);
|
|
1528
|
-
return 0;
|
|
1529
|
-
}
|
|
1530
|
-
async function commandReviews(parsed, io) {
|
|
1531
|
-
const itemId = parsed.args[0];
|
|
1532
|
-
const source = readSourceOptions(parsed, io);
|
|
1533
|
-
if (!source.ok)
|
|
1534
|
-
return usageError(io, source.error);
|
|
1535
|
-
const result = await listReviewsSource(source.options, itemId, stringFlag(parsed, 'type', 't'));
|
|
1536
|
-
if (parsed.flags.json) {
|
|
1537
|
-
writeJson(io.stdout, sourcePayload(result, { count: result.data.length, reviews: result.data }));
|
|
1538
|
-
return 0;
|
|
1539
|
-
}
|
|
1540
|
-
if (result.data.length === 0) {
|
|
1541
|
-
writeLine(io.stdout, itemId ? `No reviews found for ${itemId}.` : 'No reviews found.');
|
|
1542
|
-
return 0;
|
|
1543
|
-
}
|
|
1544
|
-
writeLine(io.stdout, header('agora reviews', [`${result.data.length} results`, sourceLabel(result)]));
|
|
1545
|
-
writeLine(io.stdout, '');
|
|
1546
|
-
writeLine(io.stdout, formatReviewList(result.data));
|
|
1547
|
-
return 0;
|
|
1548
|
-
}
|
|
1549
|
-
async function commandProfile(parsed, io) {
|
|
1550
|
-
const username = parsed.args[0] || stringFlag(parsed, 'username');
|
|
1551
|
-
if (!username)
|
|
1552
|
-
return usageError(io, 'profile requires a username');
|
|
1553
|
-
const source = readSourceOptions(parsed, io);
|
|
1554
|
-
if (!source.ok)
|
|
1555
|
-
return usageError(io, source.error);
|
|
1556
|
-
const result = await profileSource(source.options, username);
|
|
1557
|
-
if (!result.data)
|
|
1558
|
-
return usageError(io, `Profile not found: ${username}`);
|
|
1559
|
-
if (parsed.flags.json) {
|
|
1560
|
-
writeJson(io.stdout, sourcePayload(result, { profile: result.data }));
|
|
1561
|
-
return 0;
|
|
1562
|
-
}
|
|
1563
|
-
writeLine(io.stdout, formatProfileDetail(result.data));
|
|
1564
|
-
return 0;
|
|
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
|
-
}
|
|
1631
|
-
function formatItemList(items) {
|
|
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)} ★`;
|
|
1638
|
-
return [
|
|
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}`)
|
|
1643
|
-
].join('\n');
|
|
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');
|
|
1659
|
-
}
|
|
1660
|
-
function formatItemDetail(item) {
|
|
1661
|
-
const lines = [
|
|
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)}`,
|
|
1669
|
-
'',
|
|
1670
|
-
item.description,
|
|
1671
|
-
'',
|
|
1672
|
-
`${style.dim('tags')} ${item.tags.join(', ')}`
|
|
1673
|
-
];
|
|
1674
|
-
if (item.kind === 'package') {
|
|
1675
|
-
lines.splice(5, 0, `${style.dim('version')} ${item.version}`);
|
|
1676
|
-
lines.push(`${style.dim('installs')} ${formatNumber(item.installs)}`);
|
|
1677
|
-
if (item.repository)
|
|
1678
|
-
lines.push(`${style.dim('repo')} ${item.repository}`);
|
|
1679
|
-
if (item.npmPackage)
|
|
1680
|
-
lines.push(`${style.dim('npm')} ${item.npmPackage}`);
|
|
1681
|
-
}
|
|
1682
|
-
if (item.kind === 'workflow') {
|
|
1683
|
-
lines.push(`${style.dim('forks')} ${item.forks}`);
|
|
1684
|
-
if (item.model)
|
|
1685
|
-
lines.push(`${style.dim('model')} ${item.model}`);
|
|
1686
|
-
lines.push('', style.dim('prompt'), item.prompt);
|
|
1687
|
-
}
|
|
1688
|
-
return lines.join('\n');
|
|
1689
|
-
}
|
|
1690
|
-
function formatSavedList(items) {
|
|
1691
|
-
return items
|
|
1692
|
-
.map((entry, index) => {
|
|
1693
|
-
if (!entry.item) {
|
|
1694
|
-
return [
|
|
1695
|
-
`${index + 1}. ${style.accent(entry.saved.id)} ${style.dim('[missing]')}`,
|
|
1696
|
-
` ${style.dim('saved ' + formatDate(entry.saved.savedAt))}`
|
|
1697
|
-
].join('\n');
|
|
1698
|
-
}
|
|
1699
|
-
return [
|
|
1700
|
-
`${index + 1}. ${style.accent(entry.item.id)} ${style.dim('[' + entry.item.category + ']')}`,
|
|
1701
|
-
` ${style.dim(entry.item.name)}`,
|
|
1702
|
-
` ${truncate(entry.item.description, 88)}`,
|
|
1703
|
-
` ${style.dim('saved ' + formatDate(entry.saved.savedAt))}`
|
|
1704
|
-
].join('\n');
|
|
1705
|
-
})
|
|
1706
|
-
.join('\n\n');
|
|
1707
|
-
}
|
|
1708
|
-
function formatReviewList(reviews) {
|
|
1709
|
-
return reviews
|
|
1710
|
-
.map((review, index) => {
|
|
1711
|
-
return [
|
|
1712
|
-
`${index + 1}. ${style.accent(review.itemId)} ${style.dim('[' + review.itemType + ']')}`,
|
|
1713
|
-
` ${style.dim('rating ' + review.rating + '/5 by ' + review.author)}`,
|
|
1714
|
-
` ${truncate(review.content, 88)}`
|
|
1715
|
-
].join('\n');
|
|
1716
|
-
})
|
|
1717
|
-
.join('\n\n');
|
|
1718
|
-
}
|
|
1719
|
-
function formatProfileDetail(profile) {
|
|
1720
|
-
const lines = [
|
|
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)}`
|
|
1726
|
-
];
|
|
1727
|
-
if (profile.bio)
|
|
1728
|
-
lines.splice(2, 0, `${style.dim('bio')} ${profile.bio}`);
|
|
1729
|
-
if (profile.avatarUrl)
|
|
1730
|
-
lines.push(`${style.dim('avatar')} ${profile.avatarUrl}`);
|
|
1731
|
-
if (profile.joinedAt)
|
|
1732
|
-
lines.push(`${style.dim('joined')} ${formatDate(profile.joinedAt)}`);
|
|
1733
|
-
return lines.join('\n');
|
|
1734
|
-
}
|
|
1735
|
-
function formatTutorialList(tutorials) {
|
|
1736
|
-
return tutorials
|
|
1737
|
-
.map((tutorial, index) => {
|
|
1738
|
-
return [
|
|
1739
|
-
`${index + 1}. ${style.accent(tutorial.id)} ${style.dim('[' + tutorial.level + ']')}`,
|
|
1740
|
-
` ${style.dim(tutorial.title)}`,
|
|
1741
|
-
` ${truncate(tutorial.description, 88)}`,
|
|
1742
|
-
` ${style.dim(tutorial.duration + ' | ' + tutorial.steps.length + ' steps')}`
|
|
1743
|
-
].join('\n');
|
|
1744
|
-
})
|
|
1745
|
-
.join('\n\n');
|
|
1746
|
-
}
|
|
1747
|
-
function formatTutorialStep(tutorial, stepNumber) {
|
|
1748
|
-
const payload = tutorialStepPayload(tutorial, stepNumber);
|
|
1749
|
-
if (payload.completed) {
|
|
1750
|
-
return [
|
|
1751
|
-
style.bold(tutorial.title),
|
|
1752
|
-
style.dim(`Completed ${tutorial.steps.length}/${tutorial.steps.length} steps.`),
|
|
1753
|
-
'Run agora tutorials for more tutorials.'
|
|
1754
|
-
].join('\n');
|
|
1755
|
-
}
|
|
1756
|
-
const lines = [
|
|
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}`,
|
|
1762
|
-
'',
|
|
1763
|
-
payload.title || '',
|
|
1764
|
-
payload.content || ''
|
|
1765
|
-
];
|
|
1766
|
-
if (payload.code) {
|
|
1767
|
-
lines.push('', style.dim('code:'), payload.code);
|
|
1768
|
-
}
|
|
1769
|
-
return lines.join('\n');
|
|
1770
|
-
}
|
|
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>]`
|
|
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
196
|
}
|
|
1823
197
|
export function commandManual(name) {
|
|
1824
198
|
const meta = COMMANDS.find((c) => c.name === name);
|
|
@@ -1826,259 +200,16 @@ export function commandManual(name) {
|
|
|
1826
200
|
return '';
|
|
1827
201
|
return renderManual(meta, style);
|
|
1828
202
|
}
|
|
1829
|
-
function
|
|
1830
|
-
|
|
1831
|
-
|
|
1832
|
-
|
|
1833
|
-
|
|
1834
|
-
|
|
1835
|
-
|
|
1836
|
-
}
|
|
1837
|
-
function requiredStringFlag(parsed, longName, shortName) {
|
|
1838
|
-
const value = stringFlag(parsed, longName, shortName);
|
|
1839
|
-
return value?.trim() || undefined;
|
|
1840
|
-
}
|
|
1841
|
-
function numberFlag(parsed, longName, shortName) {
|
|
1842
|
-
const value = stringFlag(parsed, longName, shortName);
|
|
1843
|
-
if (!value)
|
|
1844
|
-
return undefined;
|
|
1845
|
-
const parsedValue = Number(value);
|
|
1846
|
-
return Number.isFinite(parsedValue) ? parsedValue : undefined;
|
|
1847
|
-
}
|
|
1848
|
-
function authTokenInput(parsed, io) {
|
|
1849
|
-
return (requiredStringFlag(parsed, 'token') ||
|
|
1850
|
-
envString(io, 'AGORA_TOKEN') ||
|
|
1851
|
-
envString(io, 'AGORA_API_TOKEN'));
|
|
1852
|
-
}
|
|
1853
|
-
function envString(io, name) {
|
|
1854
|
-
const value = io.env?.[name];
|
|
1855
|
-
return typeof value === 'string' && value.trim() ? value.trim() : undefined;
|
|
1856
|
-
}
|
|
1857
|
-
function shortFlag(arg) {
|
|
1858
|
-
const flag = arg.slice(1);
|
|
1859
|
-
if (flag === 'h')
|
|
1860
|
-
return 'help';
|
|
1861
|
-
if (flag === 'j')
|
|
1862
|
-
return 'json';
|
|
1863
|
-
if (flag === 'm')
|
|
1864
|
-
return 'model';
|
|
1865
|
-
if (flag === 'c')
|
|
1866
|
-
return 'c';
|
|
1867
|
-
if (flag === 'n')
|
|
1868
|
-
return 'n';
|
|
1869
|
-
if (flag === 't')
|
|
1870
|
-
return 't';
|
|
1871
|
-
return flag;
|
|
1872
|
-
}
|
|
1873
|
-
function detectDataDir(parsed, io) {
|
|
1874
|
-
return detectAgoraDataDir({
|
|
1875
|
-
explicitDir: stringFlag(parsed, 'dataDir'),
|
|
1876
|
-
cwd: io.cwd,
|
|
1877
|
-
env: io.env
|
|
1878
|
-
});
|
|
1879
|
-
}
|
|
1880
|
-
function sourceOptions(parsed, io) {
|
|
1881
|
-
const explicitApiUrl = stringFlag(parsed, 'apiUrl');
|
|
1882
|
-
const envApiUrl = envString(io, 'AGORA_API_URL');
|
|
1883
|
-
const storedAuth = getAuthState(loadAgoraState(detectDataDir(parsed, io)));
|
|
1884
|
-
const storedApiUrl = storedAuth?.apiUrl;
|
|
1885
|
-
const apiUrl = explicitApiUrl || envApiUrl || storedApiUrl || '';
|
|
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');
|
|
1893
|
-
return {
|
|
1894
|
-
useApi,
|
|
1895
|
-
apiUrl,
|
|
1896
|
-
token: authTokenInput(parsed, io) || storedAuth?.token,
|
|
1897
|
-
fetcher: io.fetcher,
|
|
1898
|
-
timeoutMs: numberFlag(parsed, 'apiTimeout')
|
|
1899
|
-
};
|
|
1900
|
-
}
|
|
1901
|
-
function writeSourceOptions(parsed, io) {
|
|
1902
|
-
const options = sourceOptions(parsed, io);
|
|
1903
|
-
if (!options.apiUrl) {
|
|
1904
|
-
return {
|
|
1905
|
-
ok: false,
|
|
1906
|
-
error: 'This command requires --api-url, AGORA_API_URL, or an auth login API URL'
|
|
1907
|
-
};
|
|
1908
|
-
}
|
|
1909
|
-
if (!options.token) {
|
|
1910
|
-
return {
|
|
1911
|
-
ok: false,
|
|
1912
|
-
error: 'This command requires --token, AGORA_TOKEN, AGORA_API_TOKEN, or agora auth login'
|
|
1913
|
-
};
|
|
1914
|
-
}
|
|
1915
|
-
return {
|
|
1916
|
-
ok: true,
|
|
1917
|
-
options: {
|
|
1918
|
-
...options,
|
|
1919
|
-
useApi: true
|
|
1920
|
-
}
|
|
1921
|
-
};
|
|
1922
|
-
}
|
|
1923
|
-
function readSourceOptions(parsed, io) {
|
|
1924
|
-
const options = sourceOptions(parsed, io);
|
|
1925
|
-
if (!options.apiUrl) {
|
|
1926
|
-
return {
|
|
1927
|
-
ok: false,
|
|
1928
|
-
error: 'This command requires --api-url, AGORA_API_URL, or an auth login API URL'
|
|
1929
|
-
};
|
|
1930
|
-
}
|
|
1931
|
-
return { ok: true, options: { ...options, useApi: true } };
|
|
1932
|
-
}
|
|
1933
|
-
function tagsFlag(parsed) {
|
|
1934
|
-
const value = stringFlag(parsed, 'tags');
|
|
1935
|
-
if (!value)
|
|
1936
|
-
return [];
|
|
1937
|
-
return value
|
|
1938
|
-
.split(',')
|
|
1939
|
-
.map((tag) => tag.trim())
|
|
1940
|
-
.filter(Boolean);
|
|
1941
|
-
}
|
|
1942
|
-
function promptInput(parsed, io) {
|
|
1943
|
-
const prompt = stringFlag(parsed, 'prompt');
|
|
1944
|
-
if (prompt)
|
|
1945
|
-
return prompt;
|
|
1946
|
-
const promptFile = stringFlag(parsed, 'promptFile');
|
|
1947
|
-
if (!promptFile)
|
|
1948
|
-
return undefined;
|
|
1949
|
-
if (!existsSync(promptFile)) {
|
|
1950
|
-
usageError(io, `prompt-file not found: ${promptFile}`);
|
|
1951
|
-
return undefined;
|
|
1952
|
-
}
|
|
1953
|
-
return readFileSync(promptFile, 'utf8');
|
|
1954
|
-
}
|
|
1955
|
-
function contentInput(parsed, io) {
|
|
1956
|
-
const content = requiredStringFlag(parsed, 'content');
|
|
1957
|
-
if (content)
|
|
1958
|
-
return content;
|
|
1959
|
-
const contentFile = stringFlag(parsed, 'contentFile');
|
|
1960
|
-
if (!contentFile)
|
|
1961
|
-
return undefined;
|
|
1962
|
-
if (!existsSync(contentFile)) {
|
|
1963
|
-
usageError(io, `content-file not found: ${contentFile}`);
|
|
1964
|
-
return undefined;
|
|
1965
|
-
}
|
|
1966
|
-
return readFileSync(contentFile, 'utf8').trim();
|
|
1967
|
-
}
|
|
1968
|
-
function itemTypeFlag(parsed, itemId) {
|
|
1969
|
-
const type = stringFlag(parsed, 'type', 't');
|
|
1970
|
-
if (type === 'workflow' || itemId.startsWith('wf-'))
|
|
1971
|
-
return 'workflow';
|
|
1972
|
-
return 'package';
|
|
1973
|
-
}
|
|
1974
|
-
function discussionCategoryFlag(parsed) {
|
|
1975
|
-
const category = stringFlag(parsed, 'category', 'c') || 'discussion';
|
|
1976
|
-
if (category === 'question' ||
|
|
1977
|
-
category === 'idea' ||
|
|
1978
|
-
category === 'showcase' ||
|
|
1979
|
-
category === 'discussion') {
|
|
1980
|
-
return { ok: true, value: category };
|
|
1981
|
-
}
|
|
1982
|
-
return {
|
|
1983
|
-
ok: false,
|
|
1984
|
-
error: 'discussion category must be question, idea, showcase, or discussion'
|
|
1985
|
-
};
|
|
1986
|
-
}
|
|
1987
|
-
function tutorialLevelFlag(parsed) {
|
|
1988
|
-
const level = stringFlag(parsed, 'level') || 'all';
|
|
1989
|
-
if (level === 'all' || level === 'beginner' || level === 'intermediate' || level === 'advanced') {
|
|
1990
|
-
return { ok: true, value: level };
|
|
1991
|
-
}
|
|
1992
|
-
return { ok: false, error: 'tutorial level must be beginner, intermediate, advanced, or all' };
|
|
1993
|
-
}
|
|
1994
|
-
function tutorialStepNumber(parsed) {
|
|
1995
|
-
const rawStep = parsed.args[1] || stringFlag(parsed, 'step');
|
|
1996
|
-
if (!rawStep)
|
|
1997
|
-
return { ok: true, value: 1 };
|
|
1998
|
-
const step = Number(rawStep);
|
|
1999
|
-
if (!Number.isInteger(step) || step < 1) {
|
|
2000
|
-
return { ok: false, error: 'tutorial step must be a positive integer' };
|
|
2001
|
-
}
|
|
2002
|
-
return { ok: true, value: step };
|
|
2003
|
-
}
|
|
2004
|
-
function tutorialStepPayload(tutorial, stepNumber) {
|
|
2005
|
-
const step = tutorial.steps[stepNumber - 1];
|
|
2006
|
-
return {
|
|
2007
|
-
stepNumber,
|
|
2008
|
-
totalSteps: tutorial.steps.length,
|
|
2009
|
-
completed: !step,
|
|
2010
|
-
title: step?.title,
|
|
2011
|
-
content: step?.content,
|
|
2012
|
-
code: step?.code
|
|
2013
|
-
};
|
|
2014
|
-
}
|
|
2015
|
-
function sourceLabel(result) {
|
|
2016
|
-
return result.source === 'offline'
|
|
2017
|
-
? `source: offline · refreshed ${dataRefreshedAt}`
|
|
2018
|
-
: `source: ${result.source}`;
|
|
2019
|
-
}
|
|
2020
|
-
function warnFallback(result, io) {
|
|
2021
|
-
if (result.fallbackReason) {
|
|
2022
|
-
writeLine(io.stderr, `API unavailable, using offline data (refreshed ${dataRefreshedAt}): ${result.fallbackReason}`);
|
|
203
|
+
export async function commandCompletions(parsed, io, _style) {
|
|
204
|
+
const shell = parsed.args[0] || 'bash';
|
|
205
|
+
const { generateCompletions } = await import('./completions-gen.js');
|
|
206
|
+
const output = generateCompletions(shell);
|
|
207
|
+
if (output.startsWith('Unknown shell')) {
|
|
208
|
+
writeLine(io.stderr, output);
|
|
209
|
+
return 1;
|
|
2023
210
|
}
|
|
2024
|
-
|
|
2025
|
-
|
|
2026
|
-
return {
|
|
2027
|
-
source: result.source,
|
|
2028
|
-
apiUrl: result.apiUrl,
|
|
2029
|
-
fallbackReason: result.fallbackReason,
|
|
2030
|
-
...payload
|
|
2031
|
-
};
|
|
2032
|
-
}
|
|
2033
|
-
function authStatusPayload(dataDir, auth) {
|
|
2034
|
-
return {
|
|
2035
|
-
dataDir,
|
|
2036
|
-
statePath: getAgoraStatePath(dataDir),
|
|
2037
|
-
authenticated: Boolean(auth),
|
|
2038
|
-
apiUrl: auth?.apiUrl,
|
|
2039
|
-
tokenPreview: auth ? maskToken(auth.token) : undefined,
|
|
2040
|
-
savedAt: auth?.savedAt
|
|
2041
|
-
};
|
|
2042
|
-
}
|
|
2043
|
-
function maskToken(token) {
|
|
2044
|
-
const value = token.trim();
|
|
2045
|
-
if (value.length <= 4)
|
|
2046
|
-
return '****';
|
|
2047
|
-
if (value.length <= 8)
|
|
2048
|
-
return `${'*'.repeat(value.length - 4)}${value.slice(-4)}`;
|
|
2049
|
-
return `${value.slice(0, 4)}...${value.slice(-4)}`;
|
|
2050
|
-
}
|
|
2051
|
-
function matchesSavedQuery(entry, query) {
|
|
2052
|
-
if (!query)
|
|
2053
|
-
return true;
|
|
2054
|
-
const searchable = entry.item
|
|
2055
|
-
? [
|
|
2056
|
-
entry.item.id,
|
|
2057
|
-
entry.item.name,
|
|
2058
|
-
entry.item.description,
|
|
2059
|
-
entry.item.author,
|
|
2060
|
-
entry.item.category,
|
|
2061
|
-
...entry.item.tags
|
|
2062
|
-
].join(' ')
|
|
2063
|
-
: entry.saved.id;
|
|
2064
|
-
return searchable.toLowerCase().includes(query);
|
|
2065
|
-
}
|
|
2066
|
-
function normalizeFlag(flag) {
|
|
2067
|
-
return flag.trim().replace(/-([a-z])/g, (_, char) => char.toUpperCase());
|
|
2068
|
-
}
|
|
2069
|
-
function writeJson(stream, value) {
|
|
2070
|
-
writeLine(stream, JSON.stringify(value, null, 2));
|
|
2071
|
-
}
|
|
2072
|
-
function writeLine(stream, value = '') {
|
|
2073
|
-
stream.write(value.endsWith('\n') ? value : `${value}\n`);
|
|
2074
|
-
}
|
|
2075
|
-
function truncate(value, max) {
|
|
2076
|
-
if (value.length <= max)
|
|
2077
|
-
return value;
|
|
2078
|
-
return `${value.slice(0, max - 3)}...`;
|
|
2079
|
-
}
|
|
2080
|
-
function formatDate(value) {
|
|
2081
|
-
return value.slice(0, 10);
|
|
211
|
+
writeLine(io.stdout, output);
|
|
212
|
+
return 0;
|
|
2082
213
|
}
|
|
2083
214
|
export function listKnownItems() {
|
|
2084
215
|
return getMarketplaceItems();
|