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.
Files changed (248) hide show
  1. package/README.md +89 -415
  2. package/dist/atomic-write.d.ts +10 -0
  3. package/dist/atomic-write.d.ts.map +1 -0
  4. package/dist/atomic-write.js +23 -0
  5. package/dist/atomic-write.js.map +1 -0
  6. package/dist/auth/refresh.d.ts +17 -0
  7. package/dist/auth/refresh.d.ts.map +1 -0
  8. package/dist/auth/refresh.js +50 -0
  9. package/dist/auth/refresh.js.map +1 -0
  10. package/dist/cli/app.d.ts +11 -19
  11. package/dist/cli/app.d.ts.map +1 -1
  12. package/dist/cli/app.js +159 -2028
  13. package/dist/cli/app.js.map +1 -1
  14. package/dist/cli/commands/browse.d.ts +4 -0
  15. package/dist/cli/commands/browse.d.ts.map +1 -0
  16. package/dist/cli/commands/browse.js +80 -0
  17. package/dist/cli/commands/browse.js.map +1 -0
  18. package/dist/cli/commands/chat.d.ts +4 -0
  19. package/dist/cli/commands/chat.d.ts.map +1 -0
  20. package/dist/cli/commands/chat.js +125 -0
  21. package/dist/cli/commands/chat.js.map +1 -0
  22. package/dist/cli/commands/community.d.ts +12 -0
  23. package/dist/cli/commands/community.d.ts.map +1 -0
  24. package/dist/cli/commands/community.js +453 -0
  25. package/dist/cli/commands/community.js.map +1 -0
  26. package/dist/cli/commands/export.d.ts +3 -0
  27. package/dist/cli/commands/export.d.ts.map +1 -0
  28. package/dist/cli/commands/export.js +108 -0
  29. package/dist/cli/commands/export.js.map +1 -0
  30. package/dist/cli/commands/init.d.ts +4 -0
  31. package/dist/cli/commands/init.d.ts.map +1 -0
  32. package/dist/cli/commands/init.js +299 -0
  33. package/dist/cli/commands/init.js.map +1 -0
  34. package/dist/cli/commands/learn.d.ts +4 -0
  35. package/dist/cli/commands/learn.d.ts.map +1 -0
  36. package/dist/cli/commands/learn.js +62 -0
  37. package/dist/cli/commands/learn.js.map +1 -0
  38. package/dist/cli/commands/marketplace.d.ts +9 -0
  39. package/dist/cli/commands/marketplace.d.ts.map +1 -0
  40. package/dist/cli/commands/marketplace.js +321 -0
  41. package/dist/cli/commands/marketplace.js.map +1 -0
  42. package/dist/cli/commands/notify.d.ts +3 -0
  43. package/dist/cli/commands/notify.d.ts.map +1 -0
  44. package/dist/cli/commands/notify.js +59 -0
  45. package/dist/cli/commands/notify.js.map +1 -0
  46. package/dist/cli/commands/operations.d.ts +16 -0
  47. package/dist/cli/commands/operations.d.ts.map +1 -0
  48. package/dist/cli/commands/operations.js +1041 -0
  49. package/dist/cli/commands/operations.js.map +1 -0
  50. package/dist/cli/commands/outdated.d.ts +3 -0
  51. package/dist/cli/commands/outdated.d.ts.map +1 -0
  52. package/dist/cli/commands/outdated.js +48 -0
  53. package/dist/cli/commands/outdated.js.map +1 -0
  54. package/dist/cli/commands/ping.d.ts +3 -0
  55. package/dist/cli/commands/ping.d.ts.map +1 -0
  56. package/dist/cli/commands/ping.js +56 -0
  57. package/dist/cli/commands/ping.js.map +1 -0
  58. package/dist/cli/commands/scan.d.ts +3 -0
  59. package/dist/cli/commands/scan.d.ts.map +1 -0
  60. package/dist/cli/commands/scan.js +35 -0
  61. package/dist/cli/commands/scan.js.map +1 -0
  62. package/dist/cli/commands/today.d.ts +3 -0
  63. package/dist/cli/commands/today.d.ts.map +1 -0
  64. package/dist/cli/commands/today.js +142 -0
  65. package/dist/cli/commands/today.js.map +1 -0
  66. package/dist/cli/commands/types.d.ts +5 -0
  67. package/dist/cli/commands/types.d.ts.map +1 -0
  68. package/dist/cli/commands/types.js +2 -0
  69. package/dist/cli/commands/types.js.map +1 -0
  70. package/dist/cli/commands/watch.d.ts +3 -0
  71. package/dist/cli/commands/watch.d.ts.map +1 -0
  72. package/dist/cli/commands/watch.js +41 -0
  73. package/dist/cli/commands/watch.js.map +1 -0
  74. package/dist/cli/commands/welcome.d.ts +3 -0
  75. package/dist/cli/commands/welcome.d.ts.map +1 -0
  76. package/dist/cli/commands/welcome.js +97 -0
  77. package/dist/cli/commands/welcome.js.map +1 -0
  78. package/dist/cli/commands-meta.d.ts.map +1 -1
  79. package/dist/cli/commands-meta.js +286 -29
  80. package/dist/cli/commands-meta.js.map +1 -1
  81. package/dist/cli/completions-gen.d.ts +2 -0
  82. package/dist/cli/completions-gen.d.ts.map +1 -0
  83. package/dist/cli/completions-gen.js +195 -0
  84. package/dist/cli/completions-gen.js.map +1 -0
  85. package/dist/cli/completions.d.ts.map +1 -1
  86. package/dist/cli/completions.js +42 -5
  87. package/dist/cli/completions.js.map +1 -1
  88. package/dist/cli/flags.d.ts +19 -0
  89. package/dist/cli/flags.d.ts.map +1 -0
  90. package/dist/cli/flags.js +91 -0
  91. package/dist/cli/flags.js.map +1 -0
  92. package/dist/cli/format.d.ts +19 -0
  93. package/dist/cli/format.d.ts.map +1 -0
  94. package/dist/cli/format.js +249 -0
  95. package/dist/cli/format.js.map +1 -0
  96. package/dist/cli/helpers.d.ts +95 -0
  97. package/dist/cli/helpers.d.ts.map +1 -0
  98. package/dist/cli/helpers.js +301 -0
  99. package/dist/cli/helpers.js.map +1 -0
  100. package/dist/cli/mcp-server.d.ts +7 -1
  101. package/dist/cli/mcp-server.d.ts.map +1 -1
  102. package/dist/cli/mcp-server.js +70 -2
  103. package/dist/cli/mcp-server.js.map +1 -1
  104. package/dist/cli/menu.d.ts.map +1 -1
  105. package/dist/cli/menu.js +11 -3
  106. package/dist/cli/menu.js.map +1 -1
  107. package/dist/cli/pages/community.d.ts +6 -0
  108. package/dist/cli/pages/community.d.ts.map +1 -1
  109. package/dist/cli/pages/community.js +882 -64
  110. package/dist/cli/pages/community.js.map +1 -1
  111. package/dist/cli/pages/helpers.d.ts +14 -9
  112. package/dist/cli/pages/helpers.d.ts.map +1 -1
  113. package/dist/cli/pages/helpers.js +37 -6
  114. package/dist/cli/pages/helpers.js.map +1 -1
  115. package/dist/cli/pages/home.d.ts +1 -0
  116. package/dist/cli/pages/home.d.ts.map +1 -1
  117. package/dist/cli/pages/home.js +203 -120
  118. package/dist/cli/pages/home.js.map +1 -1
  119. package/dist/cli/pages/marketplace.d.ts +2 -0
  120. package/dist/cli/pages/marketplace.d.ts.map +1 -1
  121. package/dist/cli/pages/marketplace.js +524 -62
  122. package/dist/cli/pages/marketplace.js.map +1 -1
  123. package/dist/cli/pages/news.d.ts +28 -0
  124. package/dist/cli/pages/news.d.ts.map +1 -1
  125. package/dist/cli/pages/news.js +209 -82
  126. package/dist/cli/pages/news.js.map +1 -1
  127. package/dist/cli/pages/settings.d.ts.map +1 -1
  128. package/dist/cli/pages/settings.js +163 -33
  129. package/dist/cli/pages/settings.js.map +1 -1
  130. package/dist/cli/pages/types.d.ts +1 -1
  131. package/dist/cli/pages/types.d.ts.map +1 -1
  132. package/dist/cli/prompter.d.ts.map +1 -1
  133. package/dist/cli/prompter.js +43 -8
  134. package/dist/cli/prompter.js.map +1 -1
  135. package/dist/cli/shell.d.ts +2 -2
  136. package/dist/cli/shell.d.ts.map +1 -1
  137. package/dist/cli/shell.js +321 -18
  138. package/dist/cli/shell.js.map +1 -1
  139. package/dist/cli/tui.d.ts +1 -1
  140. package/dist/cli/tui.d.ts.map +1 -1
  141. package/dist/cli/tui.js +69 -23
  142. package/dist/cli/tui.js.map +1 -1
  143. package/dist/community/client.d.ts +45 -8
  144. package/dist/community/client.d.ts.map +1 -1
  145. package/dist/community/client.js +118 -23
  146. package/dist/community/client.js.map +1 -1
  147. package/dist/community/search.d.ts +25 -0
  148. package/dist/community/search.d.ts.map +1 -0
  149. package/dist/community/search.js +62 -0
  150. package/dist/community/search.js.map +1 -0
  151. package/dist/community/types.d.ts +21 -0
  152. package/dist/community/types.d.ts.map +1 -1
  153. package/dist/community/types.js +1 -1
  154. package/dist/config.d.ts +0 -4
  155. package/dist/config.d.ts.map +1 -1
  156. package/dist/config.js +0 -15
  157. package/dist/config.js.map +1 -1
  158. package/dist/data.d.ts.map +1 -1
  159. package/dist/data.js +142 -68
  160. package/dist/data.js.map +1 -1
  161. package/dist/format.d.ts +0 -2
  162. package/dist/format.d.ts.map +1 -1
  163. package/dist/format.js +0 -2
  164. package/dist/format.js.map +1 -1
  165. package/dist/hubs/cache.d.ts +6 -0
  166. package/dist/hubs/cache.d.ts.map +1 -0
  167. package/dist/hubs/cache.js +46 -0
  168. package/dist/hubs/cache.js.map +1 -0
  169. package/dist/hubs/enrichment.d.ts +43 -0
  170. package/dist/hubs/enrichment.d.ts.map +1 -0
  171. package/dist/hubs/enrichment.js +239 -0
  172. package/dist/hubs/enrichment.js.map +1 -0
  173. package/dist/hubs/github.d.ts +12 -0
  174. package/dist/hubs/github.d.ts.map +1 -0
  175. package/dist/hubs/github.js +54 -0
  176. package/dist/hubs/github.js.map +1 -0
  177. package/dist/hubs/huggingface.d.ts +27 -0
  178. package/dist/hubs/huggingface.d.ts.map +1 -0
  179. package/dist/hubs/huggingface.js +88 -0
  180. package/dist/hubs/huggingface.js.map +1 -0
  181. package/dist/hubs/quality.d.ts +26 -0
  182. package/dist/hubs/quality.d.ts.map +1 -0
  183. package/dist/hubs/quality.js +57 -0
  184. package/dist/hubs/quality.js.map +1 -0
  185. package/dist/hubs/types.d.ts +30 -0
  186. package/dist/hubs/types.d.ts.map +1 -0
  187. package/dist/hubs/types.js +2 -0
  188. package/dist/hubs/types.js.map +1 -0
  189. package/dist/index.d.ts.map +1 -1
  190. package/dist/index.js +84 -0
  191. package/dist/index.js.map +1 -1
  192. package/dist/init.js +2 -2
  193. package/dist/init.js.map +1 -1
  194. package/dist/live.d.ts +10 -0
  195. package/dist/live.d.ts.map +1 -1
  196. package/dist/live.js +31 -1
  197. package/dist/live.js.map +1 -1
  198. package/dist/marketplace.d.ts +16 -3
  199. package/dist/marketplace.d.ts.map +1 -1
  200. package/dist/marketplace.js +174 -7
  201. package/dist/marketplace.js.map +1 -1
  202. package/dist/news/cache.d.ts.map +1 -1
  203. package/dist/news/cache.js +4 -3
  204. package/dist/news/cache.js.map +1 -1
  205. package/dist/news/score.js +1 -1
  206. package/dist/news/sources/arxiv.d.ts.map +1 -1
  207. package/dist/news/sources/arxiv.js +10 -6
  208. package/dist/news/sources/arxiv.js.map +1 -1
  209. package/dist/news/sources/github-trending.d.ts.map +1 -1
  210. package/dist/news/sources/github-trending.js +9 -5
  211. package/dist/news/sources/github-trending.js.map +1 -1
  212. package/dist/news/sources/hn.d.ts.map +1 -1
  213. package/dist/news/sources/hn.js +8 -4
  214. package/dist/news/sources/hn.js.map +1 -1
  215. package/dist/news/sources/reddit.d.ts.map +1 -1
  216. package/dist/news/sources/reddit.js +5 -4
  217. package/dist/news/sources/reddit.js.map +1 -1
  218. package/dist/news/sources/rss.d.ts +10 -11
  219. package/dist/news/sources/rss.d.ts.map +1 -1
  220. package/dist/news/sources/rss.js +11 -99
  221. package/dist/news/sources/rss.js.map +1 -1
  222. package/dist/news/types.d.ts +3 -0
  223. package/dist/news/types.d.ts.map +1 -1
  224. package/dist/news/types.js +15 -6
  225. package/dist/news/types.js.map +1 -1
  226. package/dist/outdated.d.ts +24 -0
  227. package/dist/outdated.d.ts.map +1 -0
  228. package/dist/outdated.js +53 -0
  229. package/dist/outdated.js.map +1 -0
  230. package/dist/preferences.d.ts.map +1 -1
  231. package/dist/preferences.js +4 -4
  232. package/dist/preferences.js.map +1 -1
  233. package/dist/scan.d.ts +26 -0
  234. package/dist/scan.d.ts.map +1 -0
  235. package/dist/scan.js +179 -0
  236. package/dist/scan.js.map +1 -0
  237. package/dist/settings.d.ts.map +1 -1
  238. package/dist/settings.js +14 -20
  239. package/dist/settings.js.map +1 -1
  240. package/dist/state.d.ts +9 -2
  241. package/dist/state.d.ts.map +1 -1
  242. package/dist/state.js +41 -19
  243. package/dist/state.js.map +1 -1
  244. package/dist/types.d.ts +13 -0
  245. package/dist/types.d.ts.map +1 -1
  246. package/dist/ui.d.ts +1 -1
  247. package/dist/ui.js +1 -1
  248. package/package.json +4 -2
package/dist/cli/app.js CHANGED
@@ -1,65 +1,77 @@
1
- import { execSync } from 'node:child_process';
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 { detectOpenCodeConfigPath, doctorOpenCodeConfig, loadOpenCodeConfig, writeOpenCodeConfig } from '../config-files.js';
11
- import { createInstallPlan, getInstallKind, getMarketplaceItems, getTrendingTags, similarItems } from '../marketplace.js';
12
- import { communityBoardsSource, communityThreadsSource, communityThreadSource, createThreadSource, createReplySource, voteSource, flagSource } from '../community/client.js';
13
- import { normalizeNewsSource, DEFAULT_NEWS_CONFIG, hostFromUrl } from '../news/types.js';
14
- import { rankItems } from '../news/score.js';
15
- import { readCache, writeCache, isStale } from '../news/cache.js';
16
- import { hnSource } from '../news/sources/hn.js';
17
- import { redditSource } from '../news/sources/reddit.js';
18
- import { githubTrendingSource } from '../news/sources/github-trending.js';
19
- import { arxivSource } from '../news/sources/arxiv.js';
20
- import { scanProject, generateInitPlan, applyInitPlan, runCommands } from '../init.js';
21
- import { installAgoraCommand } from '../commands.js';
22
- import { sampleWorkflows, dataRefreshedAt } from '../data.js';
23
- import { createDiscussionSource, discussionsSource, findMarketplaceSource, createReviewSource, findTutorialSource, listReviewsSource, profileSource, publishPackageSource, publishWorkflowSource, searchMarketplaceSource, trendingMarketplaceSource, tutorialsSource } from '../live.js';
24
- import { clearAuthState, detectAgoraDataDir, getAuthState, getAgoraStatePath, loadAgoraState, removeItemFromState, resolveSavedItems, saveItemToState, setAuthState, writeAgoraState } from '../state.js';
25
- import { createStyler, renderBanner, renderBox, renderMeander, shouldUseColor, supportsTrueColor } from '../ui.js';
26
- import { loadPreferences, writePreferences, prefsPath } from '../preferences.js';
27
- import { appendHistory, loadHistory, clearHistory } from '../history.js';
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
- * Returns true only when both stdout and stdin are real interactive TTYs AND the
50
- * environment supports colour (i.e. not NO_COLOR or TERM=dumb). The gate keeps
51
- * the interactive menu away from pipes, CI, and the test harness, all of which
52
- * use non-TTY mock streams.
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 isInteractive(io, env) {
55
- if (env.NO_COLOR != null)
56
- return false;
57
- if (env.TERM === 'dumb')
58
- return false;
59
- const stdoutTTY = Boolean(io.stdout.isTTY);
60
- const stdinTTY = Boolean(process.stdin.isTTY);
61
- return stdoutTTY && stdinTTY;
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
- switch (parsed.command) {
91
- case 'search':
92
- return await commandSearch(parsed, io);
93
- case 'browse':
94
- return await commandBrowse(parsed, io);
95
- case 'trending':
96
- return await commandTrending(parsed, io);
97
- case 'workflows':
98
- return await commandWorkflows(parsed, io);
99
- case 'tutorials':
100
- return await commandTutorials(parsed, io);
101
- case 'tutorial':
102
- return await commandTutorial(parsed, io);
103
- case 'discussions':
104
- return await commandDiscussions(parsed, io);
105
- case 'discuss':
106
- return await commandDiscuss(parsed, io);
107
- case 'install':
108
- return await commandInstall(parsed, io);
109
- case 'save':
110
- return await commandSave(parsed, io);
111
- case 'saved':
112
- return await commandSaved(parsed, io);
113
- case 'remove':
114
- return await commandRemove(parsed, io);
115
- case 'publish':
116
- return await commandPublish(parsed, io);
117
- case 'review':
118
- return await commandReview(parsed, io);
119
- case 'reviews':
120
- return await commandReviews(parsed, io);
121
- case 'profile':
122
- return await commandProfile(parsed, io);
123
- case 'auth':
124
- return await commandAuth(parsed, io);
125
- case 'login':
126
- return await commandAuth({ ...parsed, args: ['login', ...parsed.args], command: 'auth' }, io);
127
- case 'logout':
128
- return await commandAuth({ ...parsed, args: ['logout'], command: 'auth' }, io);
129
- case 'whoami':
130
- return await commandAuth({ ...parsed, args: ['status'], command: 'auth', flags: { ...parsed.flags, json: true } }, io);
131
- case 'config':
132
- return await commandConfig(parsed, io);
133
- case 'mcp':
134
- return await commandMcp(parsed, io);
135
- case 'chat':
136
- return await commandChat(parsed, io);
137
- case 'init':
138
- return await commandInit(parsed, io);
139
- case 'use':
140
- return await commandUse(parsed, io);
141
- case 'similar':
142
- return await commandSimilar(parsed, io);
143
- case 'compare':
144
- return await commandCompare(parsed, io);
145
- case 'news':
146
- return await commandNews(parsed, io);
147
- case 'community':
148
- return await commandCommunity(parsed, io);
149
- case 'thread':
150
- return await commandThread(parsed, io);
151
- case 'post':
152
- return await commandPost(parsed, io);
153
- case 'reply':
154
- return await commandReply(parsed, io);
155
- case 'vote':
156
- return await commandVote(parsed, io);
157
- case 'flag':
158
- return await commandFlag(parsed, io);
159
- case 'preferences':
160
- return await commandPreferences(parsed, io);
161
- case 'history':
162
- return await commandHistory(parsed, io);
163
- case 'menu':
164
- return await runInteractiveMenu(io, style);
165
- case 'tui':
166
- return await runTui(io, { initial: 'home' });
167
- case 'help': {
168
- const helpTarget = parsed.args[0];
169
- if (helpTarget) {
170
- const meta = COMMANDS.find((c) => c.name === helpTarget);
171
- if (!meta) {
172
- writeLine(io.stderr, `Unknown command: ${helpTarget}`);
173
- writeLine(io.stderr, 'Run `agora help` for a list of commands.');
174
- return 1;
175
- }
176
- writeLine(io.stdout, commandManual(helpTarget));
177
- }
178
- else {
179
- writeLine(io.stdout, usage());
180
- }
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
- flags[key] = true;
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 (threads.length === 0) {
540
- writeLine(io.stdout, `No threads in /${board}.`);
541
- return 0;
542
- }
543
- writeLine(io.stdout, header(`agora community /${board}`, [`${threads.length} threads`, `sort: ${sort}`]));
544
- writeLine(io.stdout, '');
545
- for (const t of threads) {
546
- const ageH = Math.round((Date.now() - new Date(t.createdAt).getTime()) / 3600000);
547
- writeLine(io.stdout, ` ${style.accent(t.title)}`);
548
- writeLine(io.stdout, ` ${style.dim(t.author + ' \u00b7 ' + ageH + 'h \u00b7 ' + t.score + '\u2191 \u00b7 ' + t.replyCount + ' replies')}`);
549
- }
550
- return 0;
551
- }
552
- const boardsResult = await communityBoardsSource(opts);
553
- const boards = boardsResult.data.boards;
554
- if (parsed.flags.json) {
555
- writeJson(io.stdout, { boards });
556
- return 0;
557
- }
558
- writeLine(io.stdout, header('agora community', [`${boards.length} boards`]));
559
- writeLine(io.stdout, '');
560
- for (const b of boards) {
561
- writeLine(io.stdout, ` ${style.accent('/' + b.id.padEnd(12))} ${style.dim(b.threadCount + ' threads, ' + b.newToday + ' new today')}`);
562
- }
563
- writeLine(io.stdout, '');
564
- writeLine(io.stdout, style.dim('Run `agora community <board>` to see threads.'));
565
- return 0;
566
- }
567
- async function commandThread(parsed, io) {
568
- const id = parsed.args[0];
569
- if (!id)
570
- return usageError(io, 'thread requires a thread id');
571
- const opts = sourceOptions(parsed, io);
572
- const result = await communityThreadSource(opts, id);
573
- const { thread, replies } = result.data;
574
- if (!thread)
575
- return usageError(io, `Thread not found: ${id}`);
576
- if (parsed.flags.json) {
577
- writeJson(io.stdout, { thread, replies });
578
- return 0;
579
- }
580
- const ageH = Math.round((Date.now() - new Date(thread.createdAt).getTime()) / 3600000);
581
- writeLine(io.stdout, style.bold(thread.title));
582
- writeLine(io.stdout, `${style.dim(thread.author + ' \u00b7 ' + ageH + 'h \u00b7 ' + thread.score + '\u2191 \u00b7 ' + thread.replyCount + ' replies')}`);
583
- writeLine(io.stdout, '');
584
- writeLine(io.stdout, thread.content);
585
- if (replies.length > 0) {
586
- writeLine(io.stdout, '');
587
- writeLine(io.stdout, style.dim('--- replies ---'));
588
- for (const r of replies) {
589
- const rAgeH = Math.round((Date.now() - new Date(r.createdAt).getTime()) / 3600000);
590
- writeLine(io.stdout, ` ${style.dim(r.author + ' \u00b7 ' + rAgeH + 'h')} ${style.accent(r.score + '\u2191')}`);
591
- writeLine(io.stdout, ` ${r.content}`);
592
- }
593
- }
594
- return 0;
595
- }
596
- async function commandPost(parsed, io) {
597
- const source = writeSourceOptions(parsed, io);
598
- if (!source.ok)
599
- return usageError(io, source.error);
600
- const board = (stringFlag(parsed, 'board') || stringFlag(parsed, 'b'));
601
- const title = requiredStringFlag(parsed, 'title');
602
- const content = contentInput(parsed, io);
603
- if (!board || !title || !content) {
604
- return usageError(io, 'post requires --board, --title and --content or --content-file');
605
- }
606
- const result = await createThreadSource(source.options, { board, title, content });
607
- if (parsed.flags.json) {
608
- writeJson(io.stdout, sourcePayload(result, { thread: result.data.thread }));
609
- return 0;
610
- }
611
- writeLine(io.stdout, `Posted to /${board}: ${style.accent(result.data.thread?.title || title)}`);
612
- writeLine(io.stdout, `${sourceLabel(result)}`);
613
- return 0;
614
- }
615
- async function commandReply(parsed, io) {
616
- const parentId = parsed.args[0];
617
- if (!parentId)
618
- return usageError(io, 'reply requires a thread or reply id');
619
- const source = writeSourceOptions(parsed, io);
620
- if (!source.ok)
621
- return usageError(io, source.error);
622
- const content = contentInput(parsed, io);
623
- if (!content)
624
- return usageError(io, 'reply requires --content or --content-file');
625
- const result = await createReplySource(source.options, parentId, {
626
- content,
627
- parentId: stringFlag(parsed, 'parentId'),
628
- });
629
- if (parsed.flags.json) {
630
- writeJson(io.stdout, sourcePayload(result, { reply: result.data.reply }));
631
- return 0;
632
- }
633
- writeLine(io.stdout, `Replied to ${style.accent(parentId)}`);
634
- writeLine(io.stdout, `${sourceLabel(result)}`);
635
- return 0;
636
- }
637
- async function commandVote(parsed, io) {
638
- const id = parsed.args[0];
639
- if (!id)
640
- return usageError(io, 'vote requires a thread or reply id');
641
- const source = writeSourceOptions(parsed, io);
642
- if (!source.ok)
643
- return usageError(io, source.error);
644
- const up = parsed.flags.up === true;
645
- const down = parsed.flags.down === true;
646
- if (!up && !down)
647
- return usageError(io, 'vote requires --up or --down');
648
- const value = up ? 1 : -1;
649
- const targetType = (stringFlag(parsed, 'type') || 'discussion');
650
- await voteSource(source.options, id, { value, targetType });
651
- if (parsed.flags.json) {
652
- writeJson(io.stdout, { id, value, targetType, success: true });
653
- return 0;
654
- }
655
- writeLine(io.stdout, up ? `Upvoted ${style.accent(id)}` : `Downvoted ${style.accent(id)}`);
656
- return 0;
657
- }
658
- async function commandFlag(parsed, io) {
659
- const id = parsed.args[0];
660
- if (!id)
661
- return usageError(io, 'flag requires an item id');
662
- const flagMarketplace = (kind) => !kind || kind === 'package' || kind === 'workflow';
663
- const reasonOpt = stringFlag(parsed, 'reason');
664
- const validReasons = ['spam', 'harassment', 'undisclosed-llm', 'malicious', 'other'];
665
- const reason = (reasonOpt && validReasons.includes(reasonOpt) ? reasonOpt : 'other');
666
- const type = stringFlag(parsed, 'type') || 'discussion';
667
- if (flagMarketplace(type)) {
668
- writeLine(io.stdout, `Flagged ${style.accent(id)} for ${reason} (${type})`);
669
- if (parsed.flags.json) {
670
- writeJson(io.stdout, { id, reason, type, success: true });
671
- }
672
- return 0;
673
- }
674
- const source = writeSourceOptions(parsed, io);
675
- if (!source.ok)
676
- return usageError(io, source.error);
677
- const targetType = (type === 'discussion' || type === 'reply' ? type : 'discussion');
678
- await flagSource(source.options, id, { reason, targetType, notes: stringFlag(parsed, 'notes') });
679
- if (parsed.flags.json) {
680
- writeJson(io.stdout, { id, reason, targetType, success: true });
681
- return 0;
682
- }
683
- writeLine(io.stdout, `Flagged ${style.accent(id)} for ${reason}`);
684
- return 0;
685
- }
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
- return 0;
849
- }
850
- writeLine(io.stdout, `Install preview: ${item.name}`);
851
- writeLine(io.stdout, `Target config: ${configPath}`);
852
- const perms = item.permissions;
853
- if (perms) {
854
- writeLine(io.stdout, '\nDeclared permissions:');
855
- if (perms.fs?.length)
856
- writeLine(io.stdout, ` ${style.dim('filesystem')} ${perms.fs.join(', ')}`);
857
- if (perms.net?.length)
858
- writeLine(io.stdout, ` ${style.dim('network')} ${perms.net.join(', ')}`);
859
- if (perms.exec?.length)
860
- writeLine(io.stdout, ` ${style.dim('exec')} ${perms.exec.join(', ')}`);
861
- }
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 usageError(io, message) {
1830
- writeLine(io.stderr, message);
1831
- return 1;
1832
- }
1833
- function stringFlag(parsed, longName, shortName) {
1834
- const value = parsed.flags[longName] ?? (shortName ? parsed.flags[shortName] : undefined);
1835
- return typeof value === 'string' ? value : undefined;
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
- function sourcePayload(result, payload) {
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();