opencode-agora 0.3.0 → 0.4.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (280) hide show
  1. package/README.md +86 -255
  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 +13 -18
  11. package/dist/cli/app.d.ts.map +1 -1
  12. package/dist/cli/app.js +184 -1187
  13. package/dist/cli/app.js.map +1 -1
  14. package/dist/cli/chat-renderer.d.ts +31 -0
  15. package/dist/cli/chat-renderer.d.ts.map +1 -0
  16. package/dist/cli/chat-renderer.js +275 -0
  17. package/dist/cli/chat-renderer.js.map +1 -0
  18. package/dist/cli/commands/browse.d.ts +4 -0
  19. package/dist/cli/commands/browse.d.ts.map +1 -0
  20. package/dist/cli/commands/browse.js +80 -0
  21. package/dist/cli/commands/browse.js.map +1 -0
  22. package/dist/cli/commands/chat.d.ts +4 -0
  23. package/dist/cli/commands/chat.d.ts.map +1 -0
  24. package/dist/cli/commands/chat.js +125 -0
  25. package/dist/cli/commands/chat.js.map +1 -0
  26. package/dist/cli/commands/community.d.ts +12 -0
  27. package/dist/cli/commands/community.d.ts.map +1 -0
  28. package/dist/cli/commands/community.js +453 -0
  29. package/dist/cli/commands/community.js.map +1 -0
  30. package/dist/cli/commands/export.d.ts +3 -0
  31. package/dist/cli/commands/export.d.ts.map +1 -0
  32. package/dist/cli/commands/export.js +108 -0
  33. package/dist/cli/commands/export.js.map +1 -0
  34. package/dist/cli/commands/init.d.ts +4 -0
  35. package/dist/cli/commands/init.d.ts.map +1 -0
  36. package/dist/cli/commands/init.js +299 -0
  37. package/dist/cli/commands/init.js.map +1 -0
  38. package/dist/cli/commands/learn.d.ts +4 -0
  39. package/dist/cli/commands/learn.d.ts.map +1 -0
  40. package/dist/cli/commands/learn.js +62 -0
  41. package/dist/cli/commands/learn.js.map +1 -0
  42. package/dist/cli/commands/marketplace.d.ts +9 -0
  43. package/dist/cli/commands/marketplace.d.ts.map +1 -0
  44. package/dist/cli/commands/marketplace.js +321 -0
  45. package/dist/cli/commands/marketplace.js.map +1 -0
  46. package/dist/cli/commands/notify.d.ts +3 -0
  47. package/dist/cli/commands/notify.d.ts.map +1 -0
  48. package/dist/cli/commands/notify.js +59 -0
  49. package/dist/cli/commands/notify.js.map +1 -0
  50. package/dist/cli/commands/operations.d.ts +16 -0
  51. package/dist/cli/commands/operations.d.ts.map +1 -0
  52. package/dist/cli/commands/operations.js +1006 -0
  53. package/dist/cli/commands/operations.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/today.d.ts +3 -0
  59. package/dist/cli/commands/today.d.ts.map +1 -0
  60. package/dist/cli/commands/today.js +142 -0
  61. package/dist/cli/commands/today.js.map +1 -0
  62. package/dist/cli/commands/types.d.ts +5 -0
  63. package/dist/cli/commands/types.d.ts.map +1 -0
  64. package/dist/cli/commands/types.js +2 -0
  65. package/dist/cli/commands/types.js.map +1 -0
  66. package/dist/cli/commands/watch.d.ts +3 -0
  67. package/dist/cli/commands/watch.d.ts.map +1 -0
  68. package/dist/cli/commands/watch.js +41 -0
  69. package/dist/cli/commands/watch.js.map +1 -0
  70. package/dist/cli/commands/welcome.d.ts +3 -0
  71. package/dist/cli/commands/welcome.d.ts.map +1 -0
  72. package/dist/cli/commands/welcome.js +97 -0
  73. package/dist/cli/commands/welcome.js.map +1 -0
  74. package/dist/cli/commands-meta.d.ts +21 -0
  75. package/dist/cli/commands-meta.d.ts.map +1 -0
  76. package/dist/cli/commands-meta.js +828 -0
  77. package/dist/cli/commands-meta.js.map +1 -0
  78. package/dist/cli/completions-gen.d.ts +2 -0
  79. package/dist/cli/completions-gen.d.ts.map +1 -0
  80. package/dist/cli/completions-gen.js +195 -0
  81. package/dist/cli/completions-gen.js.map +1 -0
  82. package/dist/cli/completions.d.ts +18 -0
  83. package/dist/cli/completions.d.ts.map +1 -0
  84. package/dist/cli/completions.js +227 -0
  85. package/dist/cli/completions.js.map +1 -0
  86. package/dist/cli/flags.d.ts +19 -0
  87. package/dist/cli/flags.d.ts.map +1 -0
  88. package/dist/cli/flags.js +91 -0
  89. package/dist/cli/flags.js.map +1 -0
  90. package/dist/cli/format.d.ts +19 -0
  91. package/dist/cli/format.d.ts.map +1 -0
  92. package/dist/cli/format.js +249 -0
  93. package/dist/cli/format.js.map +1 -0
  94. package/dist/cli/helpers.d.ts +95 -0
  95. package/dist/cli/helpers.d.ts.map +1 -0
  96. package/dist/cli/helpers.js +301 -0
  97. package/dist/cli/helpers.js.map +1 -0
  98. package/dist/cli/mcp-server.d.ts +4 -0
  99. package/dist/cli/mcp-server.d.ts.map +1 -0
  100. package/dist/cli/mcp-server.js +277 -0
  101. package/dist/cli/mcp-server.js.map +1 -0
  102. package/dist/cli/menu.d.ts +7 -0
  103. package/dist/cli/menu.d.ts.map +1 -0
  104. package/dist/cli/menu.js +172 -0
  105. package/dist/cli/menu.js.map +1 -0
  106. package/dist/cli/pages/community.d.ts +9 -0
  107. package/dist/cli/pages/community.d.ts.map +1 -0
  108. package/dist/cli/pages/community.js +1094 -0
  109. package/dist/cli/pages/community.js.map +1 -0
  110. package/dist/cli/pages/helpers.d.ts +37 -0
  111. package/dist/cli/pages/helpers.d.ts.map +1 -0
  112. package/dist/cli/pages/helpers.js +98 -0
  113. package/dist/cli/pages/helpers.js.map +1 -0
  114. package/dist/cli/pages/home.d.ts +4 -0
  115. package/dist/cli/pages/home.d.ts.map +1 -0
  116. package/dist/cli/pages/home.js +231 -0
  117. package/dist/cli/pages/home.js.map +1 -0
  118. package/dist/cli/pages/marketplace.d.ts +5 -0
  119. package/dist/cli/pages/marketplace.d.ts.map +1 -0
  120. package/dist/cli/pages/marketplace.js +583 -0
  121. package/dist/cli/pages/marketplace.js.map +1 -0
  122. package/dist/cli/pages/news.d.ts +31 -0
  123. package/dist/cli/pages/news.d.ts.map +1 -0
  124. package/dist/cli/pages/news.js +688 -0
  125. package/dist/cli/pages/news.js.map +1 -0
  126. package/dist/cli/pages/settings.d.ts +3 -0
  127. package/dist/cli/pages/settings.d.ts.map +1 -0
  128. package/dist/cli/pages/settings.js +296 -0
  129. package/dist/cli/pages/settings.js.map +1 -0
  130. package/dist/cli/pages/types.d.ts +67 -0
  131. package/dist/cli/pages/types.d.ts.map +1 -0
  132. package/dist/cli/pages/types.js +2 -0
  133. package/dist/cli/pages/types.js.map +1 -0
  134. package/dist/cli/prompter.d.ts +135 -0
  135. package/dist/cli/prompter.d.ts.map +1 -0
  136. package/dist/cli/prompter.js +710 -0
  137. package/dist/cli/prompter.js.map +1 -0
  138. package/dist/cli/shell.d.ts +23 -0
  139. package/dist/cli/shell.d.ts.map +1 -0
  140. package/dist/cli/shell.js +1106 -0
  141. package/dist/cli/shell.js.map +1 -0
  142. package/dist/cli/tui.d.ts +7 -0
  143. package/dist/cli/tui.d.ts.map +1 -0
  144. package/dist/cli/tui.js +419 -0
  145. package/dist/cli/tui.js.map +1 -0
  146. package/dist/cli.js +1 -1
  147. package/dist/cli.js.map +1 -1
  148. package/dist/commands.d.ts +14 -0
  149. package/dist/commands.d.ts.map +1 -0
  150. package/dist/commands.js +28 -0
  151. package/dist/commands.js.map +1 -0
  152. package/dist/community/client.d.ts +84 -0
  153. package/dist/community/client.d.ts.map +1 -0
  154. package/dist/community/client.js +340 -0
  155. package/dist/community/client.js.map +1 -0
  156. package/dist/community/search.d.ts +25 -0
  157. package/dist/community/search.d.ts.map +1 -0
  158. package/dist/community/search.js +62 -0
  159. package/dist/community/search.js.map +1 -0
  160. package/dist/community/types.d.ts +71 -0
  161. package/dist/community/types.d.ts.map +1 -0
  162. package/dist/community/types.js +11 -0
  163. package/dist/community/types.js.map +1 -0
  164. package/dist/config.d.ts +1 -7
  165. package/dist/config.d.ts.map +1 -1
  166. package/dist/config.js +0 -32
  167. package/dist/config.js.map +1 -1
  168. package/dist/data.d.ts +1 -1
  169. package/dist/data.d.ts.map +1 -1
  170. package/dist/data.js +778 -40
  171. package/dist/data.js.map +1 -1
  172. package/dist/format.d.ts +5 -39
  173. package/dist/format.d.ts.map +1 -1
  174. package/dist/format.js +5 -120
  175. package/dist/format.js.map +1 -1
  176. package/dist/history.d.ts +13 -0
  177. package/dist/history.d.ts.map +1 -0
  178. package/dist/history.js +37 -0
  179. package/dist/history.js.map +1 -0
  180. package/dist/hubs/cache.d.ts +6 -0
  181. package/dist/hubs/cache.d.ts.map +1 -0
  182. package/dist/hubs/cache.js +46 -0
  183. package/dist/hubs/cache.js.map +1 -0
  184. package/dist/hubs/enrichment.d.ts +43 -0
  185. package/dist/hubs/enrichment.d.ts.map +1 -0
  186. package/dist/hubs/enrichment.js +239 -0
  187. package/dist/hubs/enrichment.js.map +1 -0
  188. package/dist/hubs/github.d.ts +12 -0
  189. package/dist/hubs/github.d.ts.map +1 -0
  190. package/dist/hubs/github.js +54 -0
  191. package/dist/hubs/github.js.map +1 -0
  192. package/dist/hubs/huggingface.d.ts +27 -0
  193. package/dist/hubs/huggingface.d.ts.map +1 -0
  194. package/dist/hubs/huggingface.js +88 -0
  195. package/dist/hubs/huggingface.js.map +1 -0
  196. package/dist/hubs/quality.d.ts +26 -0
  197. package/dist/hubs/quality.d.ts.map +1 -0
  198. package/dist/hubs/quality.js +57 -0
  199. package/dist/hubs/quality.js.map +1 -0
  200. package/dist/hubs/types.d.ts +30 -0
  201. package/dist/hubs/types.d.ts.map +1 -0
  202. package/dist/hubs/types.js +2 -0
  203. package/dist/hubs/types.js.map +1 -0
  204. package/dist/index.d.ts.map +1 -1
  205. package/dist/index.js +188 -224
  206. package/dist/index.js.map +1 -1
  207. package/dist/init.d.ts.map +1 -1
  208. package/dist/init.js +6 -11
  209. package/dist/init.js.map +1 -1
  210. package/dist/live.d.ts +14 -0
  211. package/dist/live.d.ts.map +1 -1
  212. package/dist/live.js +35 -3
  213. package/dist/live.js.map +1 -1
  214. package/dist/marketplace.d.ts +25 -3
  215. package/dist/marketplace.d.ts.map +1 -1
  216. package/dist/marketplace.js +279 -22
  217. package/dist/marketplace.js.map +1 -1
  218. package/dist/news/cache.d.ts +13 -0
  219. package/dist/news/cache.d.ts.map +1 -0
  220. package/dist/news/cache.js +66 -0
  221. package/dist/news/cache.js.map +1 -0
  222. package/dist/news/score.d.ts +4 -0
  223. package/dist/news/score.d.ts.map +1 -0
  224. package/dist/news/score.js +43 -0
  225. package/dist/news/score.js.map +1 -0
  226. package/dist/news/sources/arxiv.d.ts +9 -0
  227. package/dist/news/sources/arxiv.d.ts.map +1 -0
  228. package/dist/news/sources/arxiv.js +107 -0
  229. package/dist/news/sources/arxiv.js.map +1 -0
  230. package/dist/news/sources/github-trending.d.ts +9 -0
  231. package/dist/news/sources/github-trending.d.ts.map +1 -0
  232. package/dist/news/sources/github-trending.js +97 -0
  233. package/dist/news/sources/github-trending.js.map +1 -0
  234. package/dist/news/sources/hn.d.ts +9 -0
  235. package/dist/news/sources/hn.d.ts.map +1 -0
  236. package/dist/news/sources/hn.js +57 -0
  237. package/dist/news/sources/hn.js.map +1 -0
  238. package/dist/news/sources/reddit.d.ts +9 -0
  239. package/dist/news/sources/reddit.d.ts.map +1 -0
  240. package/dist/news/sources/reddit.js +69 -0
  241. package/dist/news/sources/reddit.js.map +1 -0
  242. package/dist/news/sources/rss.d.ts +13 -0
  243. package/dist/news/sources/rss.d.ts.map +1 -0
  244. package/dist/news/sources/rss.js +14 -0
  245. package/dist/news/sources/rss.js.map +1 -0
  246. package/dist/news/types.d.ts +42 -0
  247. package/dist/news/types.d.ts.map +1 -0
  248. package/dist/news/types.js +56 -0
  249. package/dist/news/types.js.map +1 -0
  250. package/dist/preferences.d.ts +14 -0
  251. package/dist/preferences.d.ts.map +1 -0
  252. package/dist/preferences.js +31 -0
  253. package/dist/preferences.js.map +1 -0
  254. package/dist/settings.d.ts +26 -0
  255. package/dist/settings.d.ts.map +1 -0
  256. package/dist/settings.js +251 -0
  257. package/dist/settings.js.map +1 -0
  258. package/dist/state.d.ts +9 -2
  259. package/dist/state.d.ts.map +1 -1
  260. package/dist/state.js +41 -19
  261. package/dist/state.js.map +1 -1
  262. package/dist/transcript.d.ts +28 -0
  263. package/dist/transcript.d.ts.map +1 -0
  264. package/dist/transcript.js +79 -0
  265. package/dist/transcript.js.map +1 -0
  266. package/dist/types.d.ts +19 -1
  267. package/dist/types.d.ts.map +1 -1
  268. package/dist/ui.d.ts +157 -0
  269. package/dist/ui.d.ts.map +1 -0
  270. package/dist/ui.js +296 -0
  271. package/dist/ui.js.map +1 -0
  272. package/package.json +11 -9
  273. package/dist/api.d.ts +0 -72
  274. package/dist/api.d.ts.map +0 -1
  275. package/dist/api.js +0 -109
  276. package/dist/api.js.map +0 -1
  277. package/dist/logger.d.ts +0 -20
  278. package/dist/logger.d.ts.map +0 -1
  279. package/dist/logger.js +0 -59
  280. package/dist/logger.js.map +0 -1
@@ -0,0 +1,1006 @@
1
+ import { execSync, execFileSync, spawnSync } from 'node:child_process';
2
+ import { readNewsMeta, readCache } from '../../news/cache.js';
3
+ import { existsSync, mkdirSync, writeFileSync } from 'node:fs';
4
+ import { formatConfigJson } from '../../config.js';
5
+ import { detectOpenCodeConfigPath, doctorOpenCodeConfig, loadOpenCodeConfig, writeOpenCodeConfig } from '../../config-files.js';
6
+ import { createInstallPlan, hasPermissions, renderPermissionLines } from '../../marketplace.js';
7
+ import { findMarketplaceSource, publishPackageSource, publishWorkflowSource, createReviewSource, listReviewsSource, profileSource } from '../../live.js';
8
+ import { clearAuthState, decodeJwtExp, detectAgoraDataDir, getAuthState, getAgoraStatePath, loadAgoraState, removeItemFromState, resolveSavedItems, saveItemToState, setAuthState, writeAgoraState } from '../../state.js';
9
+ import { loadPreferences, writePreferences, prefsPath } from '../../preferences.js';
10
+ import { loadHistory, clearHistory } from '../../history.js';
11
+ import { stringFlag, requiredStringFlag, numberFlag, envString, authTokenInput, sourceOptions, writeSourceOptions, readSourceOptions, sourceLabel, sourcePayload, warnFallback, writeLine, writeJson, usageError, detectDataDir, authStatusPayload, maskToken, formatRelativeExp, matchesSavedQuery, tagsFlag, promptInput, itemTypeFlag } from '../helpers.js';
12
+ import { header, formatSavedList, formatReviewList, formatProfileDetail, formatDate } from '../format.js';
13
+ export const commandInstall = async (parsed, io, style) => {
14
+ const id = parsed.args[0];
15
+ if (!id)
16
+ return usageError(io, 'install requires an item id');
17
+ const source = await findMarketplaceSource({
18
+ ...(await sourceOptions(parsed, io)),
19
+ id,
20
+ type: stringFlag(parsed, 'type', 't')
21
+ });
22
+ const item = source.data;
23
+ warnFallback(source, io);
24
+ if (!item)
25
+ return usageError(io, `Item not found: ${id}`);
26
+ const configPath = detectOpenCodeConfigPath({
27
+ explicitPath: stringFlag(parsed, 'config'),
28
+ cwd: io.cwd,
29
+ env: io.env
30
+ });
31
+ const loaded = loadOpenCodeConfig(configPath);
32
+ if (loaded.error)
33
+ return usageError(io, `${loaded.path}: ${loaded.error}`);
34
+ const dataDir = detectDataDir(parsed, io);
35
+ const plan = createInstallPlan(item, loaded.config, { dataDir });
36
+ if (!plan.installable)
37
+ return usageError(io, plan.reason || `${item.name} is not installable`);
38
+ if (parsed.flags.json) {
39
+ writeJson(io.stdout, {
40
+ source: source.source,
41
+ apiUrl: source.apiUrl,
42
+ fallbackReason: source.fallbackReason,
43
+ item,
44
+ configPath,
45
+ write: Boolean(parsed.flags.write),
46
+ kind: plan.kind,
47
+ commands: plan.commands,
48
+ notes: plan.notes,
49
+ config: plan.config,
50
+ cloneTarget: plan.cloneTarget,
51
+ postInstallHint: plan.postInstallHint
52
+ });
53
+ return 0;
54
+ }
55
+ if (parsed.flags.write) {
56
+ if (hasPermissions(plan.permissions)) {
57
+ if (!parsed.flags.yes && !parsed.flags.y) {
58
+ for (const line of renderPermissionLines(plan.permissions))
59
+ writeLine(io.stdout, line);
60
+ writeLine(io.stdout, '');
61
+ writeLine(io.stdout, style.dim('This package declares permissions. Re-run with --yes to grant and install.'));
62
+ return 1;
63
+ }
64
+ writeLine(io.stdout, 'Granted permissions:');
65
+ for (const line of renderPermissionLines(plan.permissions))
66
+ writeLine(io.stdout, line);
67
+ writeLine(io.stdout, '');
68
+ }
69
+ if (plan.kind === 'git-clone') {
70
+ if (plan.cloneTarget) {
71
+ try {
72
+ mkdirSync(plan.cloneTarget, { recursive: true });
73
+ }
74
+ catch {
75
+ /* ignore if already exists */
76
+ }
77
+ }
78
+ if (plan.commands.length) {
79
+ writeLine(io.stdout, 'Cloning repository...');
80
+ for (const cmd of plan.commands) {
81
+ try {
82
+ execSync(cmd, { stdio: 'pipe', timeout: 60000 });
83
+ writeLine(io.stdout, ` ✓ ${cmd}`);
84
+ }
85
+ catch (err) {
86
+ writeLine(io.stderr, ` ! Failed: ${cmd}`);
87
+ if (err.stderr)
88
+ writeLine(io.stderr, String(err.stderr));
89
+ }
90
+ }
91
+ }
92
+ writeLine(io.stdout, `Installed ${style.accent(item.name)}`);
93
+ if (plan.cloneTarget)
94
+ writeLine(io.stdout, `${style.dim('Location')} ${plan.cloneTarget}`);
95
+ if (plan.postInstallHint)
96
+ writeLine(io.stdout, `${style.dim('Next steps')} ${plan.postInstallHint}`);
97
+ }
98
+ else if (plan.kind === 'package-install') {
99
+ if (plan.commands.length) {
100
+ writeLine(io.stdout, 'Installing packages...');
101
+ for (const cmd of plan.commands) {
102
+ try {
103
+ execSync(cmd, { stdio: 'pipe', timeout: 120000 });
104
+ writeLine(io.stdout, ` ✓ ${cmd}`);
105
+ }
106
+ catch {
107
+ writeLine(io.stderr, ` ! Failed: ${cmd} (may already be installed)`);
108
+ }
109
+ }
110
+ }
111
+ writeLine(io.stdout, `Installed ${style.accent(item.name)}`);
112
+ }
113
+ else {
114
+ writeOpenCodeConfig(configPath, plan.config);
115
+ writeLine(io.stdout, `Installed ${style.accent(item.name)}`);
116
+ writeLine(io.stdout, `${style.dim('Config')} ${configPath}`);
117
+ if (plan.commands.length) {
118
+ writeLine(io.stdout, 'Installing packages...');
119
+ for (const cmd of plan.commands) {
120
+ try {
121
+ execSync(cmd, { stdio: 'pipe', timeout: 120000 });
122
+ writeLine(io.stdout, ` ✓ ${cmd}`);
123
+ }
124
+ catch {
125
+ writeLine(io.stderr, ` ! Failed: ${cmd} (may already be installed)`);
126
+ }
127
+ }
128
+ }
129
+ }
130
+ return 0;
131
+ }
132
+ writeLine(io.stdout, `Install preview: ${item.name}`);
133
+ const permLines = renderPermissionLines(plan.permissions);
134
+ writeLine(io.stdout, '');
135
+ for (const line of permLines)
136
+ writeLine(io.stdout, line);
137
+ if (plan.kind === 'git-clone') {
138
+ writeLine(io.stdout, `Kind: git-clone`);
139
+ if (plan.cloneTarget)
140
+ writeLine(io.stdout, `Target directory: ${plan.cloneTarget}`);
141
+ if (plan.commands.length) {
142
+ writeLine(io.stdout, '\nCommands:');
143
+ writeLine(io.stdout, plan.commands.join('\n'));
144
+ }
145
+ if (plan.postInstallHint)
146
+ writeLine(io.stdout, `\nNext steps: ${plan.postInstallHint}`);
147
+ }
148
+ else if (plan.kind === 'package-install') {
149
+ writeLine(io.stdout, `Kind: package-install`);
150
+ if (plan.commands.length) {
151
+ writeLine(io.stdout, '\nCommands:');
152
+ writeLine(io.stdout, plan.commands.join('\n'));
153
+ }
154
+ }
155
+ else {
156
+ writeLine(io.stdout, `Target config: ${configPath}`);
157
+ if (plan.commands.length) {
158
+ writeLine(io.stdout, '\nCommands:');
159
+ writeLine(io.stdout, plan.commands.join('\n'));
160
+ }
161
+ writeLine(io.stdout, '\nopencode.json preview:');
162
+ writeLine(io.stdout, formatConfigJson(plan.config));
163
+ }
164
+ if (!parsed.flags.yes && !parsed.flags.y) {
165
+ writeLine(io.stdout, '\nRun with --write to update the config file and install packages.');
166
+ }
167
+ else {
168
+ // --yes/-y: execute immediately (still showed preview above)
169
+ if (plan.kind === 'git-clone') {
170
+ if (plan.cloneTarget) {
171
+ try {
172
+ mkdirSync(plan.cloneTarget, { recursive: true });
173
+ }
174
+ catch {
175
+ /* ignore */
176
+ }
177
+ }
178
+ for (const cmd of plan.commands) {
179
+ try {
180
+ execSync(cmd, { stdio: 'pipe', timeout: 60000 });
181
+ writeLine(io.stdout, ` ✓ ${cmd}`);
182
+ }
183
+ catch (err) {
184
+ writeLine(io.stdout, ` ! Failed: ${cmd}`);
185
+ if (err.stderr)
186
+ writeLine(io.stdout, String(err.stderr));
187
+ }
188
+ }
189
+ if (plan.postInstallHint)
190
+ writeLine(io.stdout, `Next steps: ${plan.postInstallHint}`);
191
+ }
192
+ else if (plan.kind === 'package-install') {
193
+ for (const cmd of plan.commands) {
194
+ try {
195
+ execSync(cmd, { stdio: 'pipe', timeout: 120000 });
196
+ writeLine(io.stdout, ` ✓ ${cmd}`);
197
+ }
198
+ catch {
199
+ writeLine(io.stdout, ` ! Failed: ${cmd} (may already be installed)`);
200
+ }
201
+ }
202
+ }
203
+ else {
204
+ writeOpenCodeConfig(configPath, plan.config);
205
+ for (const cmd of plan.commands) {
206
+ try {
207
+ execSync(cmd, { stdio: 'pipe', timeout: 120000 });
208
+ writeLine(io.stdout, ` ✓ ${cmd}`);
209
+ }
210
+ catch {
211
+ writeLine(io.stdout, ` ! Failed: ${cmd} (may already be installed)`);
212
+ }
213
+ }
214
+ }
215
+ writeLine(io.stdout, `Installed ${style.accent(item.name)}`);
216
+ }
217
+ return 0;
218
+ };
219
+ export const commandMcp = async (_parsed, io, _style) => {
220
+ const { runMcpServer } = await import('../mcp-server.js');
221
+ try {
222
+ await runMcpServer();
223
+ }
224
+ catch (error) {
225
+ writeLine(io.stderr, error instanceof Error ? error.message : String(error));
226
+ return 1;
227
+ }
228
+ return 0;
229
+ };
230
+ export const commandSave = async (parsed, io, style) => {
231
+ const id = parsed.args[0];
232
+ if (!id)
233
+ return usageError(io, 'save requires an item id');
234
+ const source = await findMarketplaceSource({
235
+ ...(await sourceOptions(parsed, io)),
236
+ id,
237
+ type: stringFlag(parsed, 'type', 't')
238
+ });
239
+ const item = source.data;
240
+ warnFallback(source, io);
241
+ if (!item)
242
+ return usageError(io, `Item not found: ${id}`);
243
+ const dataDir = detectDataDir(parsed, io);
244
+ const state = loadAgoraState(dataDir);
245
+ const result = saveItemToState(state, item);
246
+ writeAgoraState(dataDir, result.state);
247
+ if (parsed.flags.json) {
248
+ writeJson(io.stdout, {
249
+ source: source.source,
250
+ apiUrl: source.apiUrl,
251
+ fallbackReason: source.fallbackReason,
252
+ dataDir,
253
+ statePath: getAgoraStatePath(dataDir),
254
+ added: result.added,
255
+ item
256
+ });
257
+ return 0;
258
+ }
259
+ writeLine(io.stdout, result.added ? `Saved ${style.accent(item.id)}` : `${style.accent(item.id)} is already saved`);
260
+ writeLine(io.stdout, `${style.dim('State')} ${getAgoraStatePath(dataDir)}`);
261
+ return 0;
262
+ };
263
+ export const commandSaved = async (parsed, io, style) => {
264
+ const query = parsed.args.join(' ').trim().toLowerCase();
265
+ const dataDir = detectDataDir(parsed, io);
266
+ const state = loadAgoraState(dataDir);
267
+ const saved = resolveSavedItems(state).filter((entry) => matchesSavedQuery(entry, query));
268
+ if (parsed.flags.json) {
269
+ writeJson(io.stdout, {
270
+ dataDir,
271
+ statePath: getAgoraStatePath(dataDir),
272
+ count: saved.length,
273
+ items: saved
274
+ });
275
+ return 0;
276
+ }
277
+ if (saved.length === 0) {
278
+ writeLine(io.stdout, query ? `No saved items match "${query}".` : 'No saved items yet.');
279
+ writeLine(io.stdout, 'Run agora save <id> to save a package or workflow.');
280
+ return 0;
281
+ }
282
+ writeLine(io.stdout, header('agora saved', [`${saved.length} items`], style));
283
+ writeLine(io.stdout, formatSavedList(saved, style));
284
+ return 0;
285
+ };
286
+ export const commandRemove = async (parsed, io, style) => {
287
+ const id = parsed.args[0];
288
+ if (!id)
289
+ return usageError(io, 'remove requires an item id');
290
+ const dataDir = detectDataDir(parsed, io);
291
+ const state = loadAgoraState(dataDir);
292
+ const targetId = resolveSavedItems(state).find((entry) => {
293
+ return entry.saved.id === id || entry.item?.id === id || entry.item?.name === id;
294
+ })?.saved.id || id;
295
+ const result = removeItemFromState(state, targetId);
296
+ writeAgoraState(dataDir, result.state);
297
+ if (parsed.flags.json) {
298
+ writeJson(io.stdout, {
299
+ dataDir,
300
+ statePath: getAgoraStatePath(dataDir),
301
+ removed: result.removed,
302
+ id: targetId
303
+ });
304
+ return result.removed ? 0 : 1;
305
+ }
306
+ if (!result.removed) {
307
+ return usageError(io, `Saved item not found: ${id}`);
308
+ }
309
+ writeLine(io.stdout, `Removed ${style.accent(targetId)}`);
310
+ return 0;
311
+ };
312
+ export const commandPublish = async (parsed, io, style) => {
313
+ const kind = parsed.args[0];
314
+ if (kind !== 'package' && kind !== 'workflow') {
315
+ return usageError(io, 'publish requires "package" or "workflow"');
316
+ }
317
+ const source = await writeSourceOptions(parsed, io);
318
+ if (!source.ok)
319
+ return usageError(io, source.error);
320
+ const name = requiredStringFlag(parsed, 'name');
321
+ const description = requiredStringFlag(parsed, 'description', 'd');
322
+ if (!name || !description) {
323
+ return usageError(io, 'publish requires --name and --description');
324
+ }
325
+ if (kind === 'package') {
326
+ const npmPackage = stringFlag(parsed, 'npm') || stringFlag(parsed, 'npmPackage');
327
+ const category = stringFlag(parsed, 'category', 'c') || 'mcp';
328
+ if (category === 'mcp' && !npmPackage) {
329
+ return usageError(io, 'publish package requires --npm for MCP packages');
330
+ }
331
+ const result = await publishPackageSource(source.options, {
332
+ id: stringFlag(parsed, 'id'),
333
+ name,
334
+ description,
335
+ version: stringFlag(parsed, 'version') || '1.0.0',
336
+ category,
337
+ tags: tagsFlag(parsed),
338
+ repository: stringFlag(parsed, 'repo') || stringFlag(parsed, 'repository'),
339
+ npmPackage
340
+ });
341
+ if (parsed.flags.json) {
342
+ writeJson(io.stdout, sourcePayload(result, { item: result.data }));
343
+ return 0;
344
+ }
345
+ writeLine(io.stdout, `Published package ${style.accent(result.data.id)}`);
346
+ writeLine(io.stdout, `${result.data.name} (${sourceLabel(result)})`);
347
+ return 0;
348
+ }
349
+ const prompt = promptInput(parsed, io);
350
+ if (prompt === undefined) {
351
+ return usageError(io, 'publish workflow requires --prompt or --prompt-file');
352
+ }
353
+ const result = await publishWorkflowSource(source.options, {
354
+ id: stringFlag(parsed, 'id'),
355
+ name,
356
+ description,
357
+ prompt,
358
+ model: stringFlag(parsed, 'model'),
359
+ tags: tagsFlag(parsed)
360
+ });
361
+ if (parsed.flags.json) {
362
+ writeJson(io.stdout, sourcePayload(result, { item: result.data }));
363
+ return 0;
364
+ }
365
+ writeLine(io.stdout, `Published workflow ${style.accent(result.data.id)}`);
366
+ writeLine(io.stdout, `${result.data.name} (${sourceLabel(result)})`);
367
+ return 0;
368
+ };
369
+ export const commandReview = async (parsed, io, style) => {
370
+ const itemId = parsed.args[0];
371
+ if (!itemId)
372
+ return usageError(io, 'review requires an item id');
373
+ const rating = numberFlag(parsed, 'rating', 'r');
374
+ const content = requiredStringFlag(parsed, 'content');
375
+ if (!rating || rating < 1 || rating > 5 || !content) {
376
+ return usageError(io, 'review requires --rating 1-5 and --content');
377
+ }
378
+ const source = await writeSourceOptions(parsed, io);
379
+ if (!source.ok)
380
+ return usageError(io, source.error);
381
+ const result = await createReviewSource(source.options, {
382
+ itemId,
383
+ itemType: itemTypeFlag(parsed, itemId),
384
+ rating,
385
+ content
386
+ });
387
+ if (parsed.flags.json) {
388
+ writeJson(io.stdout, sourcePayload(result, { review: result.data }));
389
+ return 0;
390
+ }
391
+ writeLine(io.stdout, `Reviewed ${style.accent(result.data.itemId)}`);
392
+ writeLine(io.stdout, `${style.dim(result.data.rating + '/5 by ' + result.data.author)}`);
393
+ return 0;
394
+ };
395
+ export const commandReviews = async (parsed, io, style) => {
396
+ const itemId = parsed.args[0];
397
+ const source = await readSourceOptions(parsed, io);
398
+ if (!source.ok)
399
+ return usageError(io, source.error);
400
+ const result = await listReviewsSource(source.options, itemId, stringFlag(parsed, 'type', 't'));
401
+ if (parsed.flags.json) {
402
+ writeJson(io.stdout, sourcePayload(result, { count: result.data.length, reviews: result.data }));
403
+ return 0;
404
+ }
405
+ if (result.data.length === 0) {
406
+ writeLine(io.stdout, itemId ? `No reviews found for ${itemId}.` : 'No reviews found.');
407
+ return 0;
408
+ }
409
+ writeLine(io.stdout, header('agora reviews', [`${result.data.length} results`, sourceLabel(result)], style));
410
+ writeLine(io.stdout, '');
411
+ writeLine(io.stdout, formatReviewList(result.data, style));
412
+ return 0;
413
+ };
414
+ export const commandProfile = async (parsed, io, style) => {
415
+ const username = parsed.args[0] || stringFlag(parsed, 'username');
416
+ if (!username)
417
+ return usageError(io, 'profile requires a username');
418
+ const source = await readSourceOptions(parsed, io);
419
+ if (!source.ok)
420
+ return usageError(io, source.error);
421
+ const result = await profileSource(source.options, username);
422
+ if (!result.data)
423
+ return usageError(io, `Profile not found: ${username}`);
424
+ if (parsed.flags.json) {
425
+ writeJson(io.stdout, sourcePayload(result, { profile: result.data }));
426
+ return 0;
427
+ }
428
+ writeLine(io.stdout, formatProfileDetail(result.data, style));
429
+ return 0;
430
+ };
431
+ export const commandPreferences = async (parsed, io, _style) => {
432
+ const dataDir = detectDataDir(parsed, io);
433
+ const prefs = loadPreferences(dataDir);
434
+ const sub = parsed.args[0];
435
+ if (!sub) {
436
+ if (parsed.flags.json) {
437
+ writeJson(io.stdout, prefs);
438
+ return 0;
439
+ }
440
+ writeLine(io.stdout, `Preferences (${prefsPath(dataDir)})`);
441
+ writeLine(io.stdout, ` theme: ${prefs.theme}`);
442
+ writeLine(io.stdout, ` verbosity: ${prefs.verbosity}`);
443
+ writeLine(io.stdout, ` username: ${prefs.username || '(not set)'}`);
444
+ writeLine(io.stdout, ` email: ${prefs.email || '(not set)'}`);
445
+ writeLine(io.stdout, ` bio: ${prefs.bio ? prefs.bio.slice(0, 60) + (prefs.bio.length > 60 ? '...' : '') : '(not set)'}`);
446
+ writeLine(io.stdout, '');
447
+ writeLine(io.stdout, ' Set values: agora preferences <key> <value>');
448
+ writeLine(io.stdout, ' Keys: theme, verbosity, username, email, bio');
449
+ return 0;
450
+ }
451
+ const key = sub;
452
+ const val = parsed.args.slice(1).join(' ');
453
+ if (!val || !(key in prefs)) {
454
+ return usageError(io, `Usage: agora preferences <key> <value>\nValid keys: theme, verbosity, username, email, bio`);
455
+ }
456
+ if (key === 'theme' && !['dark', 'light', 'auto'].includes(val)) {
457
+ return usageError(io, 'theme must be: dark, light, or auto');
458
+ }
459
+ if (key === 'verbosity' && !['verbose', 'medium', 'quiet'].includes(val)) {
460
+ return usageError(io, 'verbosity must be: verbose, medium, or quiet');
461
+ }
462
+ prefs[key] = val;
463
+ writePreferences(dataDir, prefs);
464
+ writeLine(io.stdout, `\u2713 ${key} set to "${val}"`);
465
+ return 0;
466
+ };
467
+ export const commandHistory = async (parsed, io, style) => {
468
+ const dataDir = detectDataDir(parsed, io);
469
+ const limit = numberFlag(parsed, 'limit', 'n') || 50;
470
+ if (parsed.flags.clear) {
471
+ clearHistory(dataDir);
472
+ writeLine(io.stdout, '\u2713 History cleared');
473
+ return 0;
474
+ }
475
+ const entries = loadHistory(dataDir, limit);
476
+ if (parsed.flags.json) {
477
+ writeJson(io.stdout, entries);
478
+ return 0;
479
+ }
480
+ if (entries.length === 0) {
481
+ writeLine(io.stdout, 'No history yet.');
482
+ writeLine(io.stdout, 'Searches and chat messages are recorded automatically.');
483
+ return 0;
484
+ }
485
+ writeLine(io.stdout, `Recent history (${entries.length}):`);
486
+ for (const entry of entries) {
487
+ const icon = entry.type === 'search' ? '\uD83D\uDD0D' : '\uD83D\uDCAC';
488
+ const date = new Date(entry.timestamp).toLocaleString();
489
+ const query = entry.query.length > 60 ? entry.query.slice(0, 60) + '...' : entry.query;
490
+ writeLine(io.stdout, ` ${icon} ${style.dim(date)} ${query}`);
491
+ }
492
+ writeLine(io.stdout, '');
493
+ writeLine(io.stdout, style.dim('Use --clear to clear history, --json for JSON output.'));
494
+ return 0;
495
+ };
496
+ export const commandConfig = async (parsed, io, style) => {
497
+ const subcommand = parsed.args[0] || 'doctor';
498
+ const doFix = Boolean(parsed.flags.fix);
499
+ if (subcommand === 'show') {
500
+ const configPath = detectOpenCodeConfigPath({
501
+ explicitPath: stringFlag(parsed, 'config'),
502
+ cwd: io.cwd,
503
+ env: io.env
504
+ });
505
+ const loaded = loadOpenCodeConfig(configPath);
506
+ if (parsed.flags.json) {
507
+ writeJson(io.stdout, { path: configPath, exists: loaded.exists, config: loaded.config });
508
+ return 0;
509
+ }
510
+ writeLine(io.stdout, style.accent('OpenCode config'));
511
+ writeLine(io.stdout, `${style.dim('Path')} ${configPath}`);
512
+ writeLine(io.stdout, `${style.dim('Exists')} ${loaded.exists ? 'yes' : 'no'}`);
513
+ if (!loaded.exists)
514
+ return 0;
515
+ writeLine(io.stdout, '');
516
+ writeLine(io.stdout, formatConfigJson(loaded.config));
517
+ return 0;
518
+ }
519
+ if (subcommand === 'edit') {
520
+ const configPath = detectOpenCodeConfigPath({
521
+ explicitPath: stringFlag(parsed, 'config'),
522
+ cwd: io.cwd,
523
+ env: io.env
524
+ });
525
+ if (!existsSync(configPath)) {
526
+ writeFileSync(configPath, '{\n "$schema": "https://opencode.ai/config.json"\n}\n', 'utf8');
527
+ writeLine(io.stdout, `Created ${configPath}`);
528
+ }
529
+ const editorRaw = io.env?.EDITOR || io.env?.VISUAL || 'vi';
530
+ const editorParts = editorRaw.trim().split(/\s+/);
531
+ const editorBin = editorParts[0];
532
+ const editorArgs = [...editorParts.slice(1), configPath];
533
+ try {
534
+ execFileSync(editorBin, editorArgs, { stdio: 'inherit' });
535
+ writeLine(io.stdout, style.dim('Config saved.'));
536
+ }
537
+ catch {
538
+ return usageError(io, `Editor "${editorRaw}" failed. Set $EDITOR or try manually: nano ${configPath}`);
539
+ }
540
+ return 0;
541
+ }
542
+ if (subcommand === 'diff') {
543
+ const paths = parsed.args.slice(1);
544
+ if (paths.length < 2) {
545
+ return usageError(io, 'config diff requires two paths.\nUsage: agora config diff <path1> <path2>');
546
+ }
547
+ const [loaded1, loaded2] = await Promise.all([
548
+ Promise.resolve(loadOpenCodeConfig(paths[0])),
549
+ Promise.resolve(loadOpenCodeConfig(paths[1]))
550
+ ]);
551
+ if (parsed.flags.json) {
552
+ writeJson(io.stdout, { path1: loaded1, path2: loaded2 });
553
+ return 0;
554
+ }
555
+ const diffLines = [];
556
+ const c1 = loaded1.config;
557
+ const c2 = loaded2.config;
558
+ diffLines.push(style.accent('Config diff'));
559
+ diffLines.push(`${style.dim(paths[0])} vs ${style.dim(paths[1])}`);
560
+ diffLines.push('');
561
+ if (c1.$schema !== c2.$schema) {
562
+ diffLines.push(` $schema: ${style.dim(c1.$schema || '(none)')} → ${style.accent(c2.$schema || '(none)')}`);
563
+ }
564
+ const mcpKeys1 = Object.keys(c1.mcp || {});
565
+ const mcpKeys2 = Object.keys(c2.mcp || {});
566
+ const mcpAdded = mcpKeys2.filter((k) => !mcpKeys1.includes(k));
567
+ const mcpRemoved = mcpKeys1.filter((k) => !mcpKeys2.includes(k));
568
+ if (mcpRemoved.length > 0)
569
+ diffLines.push(` MCP removed: ${style.dim(mcpRemoved.join(', '))}`);
570
+ if (mcpAdded.length > 0)
571
+ diffLines.push(` MCP added: ${style.accent(mcpAdded.join(', '))}`);
572
+ const plug1 = new Set(c1.plugin || []);
573
+ const plug2 = new Set(c2.plugin || []);
574
+ const plugAdded = [...plug2].filter((p) => !plug1.has(p));
575
+ const plugRemoved = [...plug1].filter((p) => !plug2.has(p));
576
+ if (plugRemoved.length > 0)
577
+ diffLines.push(` Plugin removed: ${style.dim(plugRemoved.join(', '))}`);
578
+ if (plugAdded.length > 0)
579
+ diffLines.push(` Plugin added: ${style.accent(plugAdded.join(', '))}`);
580
+ diffLines.push('');
581
+ diffLines.push(style.dim('MCP server count: ' + mcpKeys1.length + ' → ' + mcpKeys2.length));
582
+ diffLines.push(style.dim('Plugin count: ' + (c1.plugin?.length || 0) + ' → ' + (c2.plugin?.length || 0)));
583
+ for (const line of diffLines)
584
+ writeLine(io.stdout, line);
585
+ return 0;
586
+ }
587
+ if (subcommand !== 'doctor') {
588
+ return usageError(io, `Unknown config command: ${subcommand}`);
589
+ }
590
+ const configPath = detectOpenCodeConfigPath({
591
+ explicitPath: stringFlag(parsed, 'config'),
592
+ cwd: io.cwd,
593
+ env: io.env
594
+ });
595
+ let report = doctorOpenCodeConfig(configPath);
596
+ if (doFix) {
597
+ const fixes = [];
598
+ const loaded = loadOpenCodeConfig(configPath);
599
+ let changed = false;
600
+ const config = loaded.config;
601
+ // Fix 1: Add missing $schema
602
+ if (!config.$schema) {
603
+ config.$schema = 'https://opencode.ai/config.json';
604
+ fixes.push('Added missing $schema field');
605
+ changed = true;
606
+ }
607
+ // Fix 2: Deduplicate plugins
608
+ if (config.plugin) {
609
+ const originalLen = config.plugin.length;
610
+ const deduped = [...new Set(config.plugin)];
611
+ if (deduped.length !== originalLen) {
612
+ fixes.push(`Removed ${originalLen - deduped.length} duplicate plugin entries`);
613
+ config.plugin = deduped;
614
+ changed = true;
615
+ }
616
+ }
617
+ // Fix 3: Remove MCP entries with empty or invalid commands
618
+ if (config.mcp) {
619
+ for (const [key, entry] of Object.entries(config.mcp)) {
620
+ if (!entry.command || entry.command.length === 0) {
621
+ delete config.mcp[key];
622
+ fixes.push(`Removed MCP entry "${key}" with empty command`);
623
+ changed = true;
624
+ }
625
+ }
626
+ }
627
+ if (changed) {
628
+ writeOpenCodeConfig(configPath, config);
629
+ report = doctorOpenCodeConfig(configPath);
630
+ }
631
+ if (parsed.flags.json) {
632
+ writeJson(io.stdout, { ...report, fixes, fixed: changed });
633
+ return changed ? 0 : 1;
634
+ }
635
+ if (fixes.length > 0) {
636
+ writeLine(io.stdout, style.accent('Config fixed:'));
637
+ for (const f of fixes)
638
+ writeLine(io.stdout, ` ✓ ${f}`);
639
+ writeLine(io.stdout, '');
640
+ }
641
+ else {
642
+ writeLine(io.stdout, style.dim('No fixes needed.'));
643
+ }
644
+ }
645
+ if (parsed.flags.json && !doFix) {
646
+ writeJson(io.stdout, report);
647
+ return report.valid ? 0 : 1;
648
+ }
649
+ writeLine(io.stdout, `${style.dim('Config path')} ${report.path}`);
650
+ writeLine(io.stdout, `${style.dim('Exists')} ${report.exists ? 'yes' : 'no'}`);
651
+ writeLine(io.stdout, `${style.dim('Valid')} ${report.valid ? 'yes' : 'no'}`);
652
+ if (report.error)
653
+ writeLine(io.stdout, `${style.dim('Error')} ${report.error}`);
654
+ writeLine(io.stdout, `${style.dim('MCP servers')} ${report.mcpServers}`);
655
+ writeLine(io.stdout, `${style.dim('Plugins')} ${report.plugins}`);
656
+ writeLine(io.stdout, `${style.dim('Packages')} ${report.packages.length ? report.packages.join(', ') : 'none'}`);
657
+ // Deep checks
658
+ if (!doFix) {
659
+ writeLine(io.stdout, '');
660
+ writeLine(io.stdout, style.dim('Deep checks (--deep for details):'));
661
+ }
662
+ if (parsed.flags.deep || doFix) {
663
+ const loaded = loadOpenCodeConfig(configPath);
664
+ const deepIssues = [];
665
+ const deepOk = [];
666
+ // Check opencode on PATH
667
+ try {
668
+ execSync('which opencode', { stdio: 'pipe', timeout: 2000 });
669
+ deepOk.push('opencode found on PATH');
670
+ }
671
+ catch {
672
+ deepIssues.push('opencode not found on PATH — chat unavailable');
673
+ }
674
+ // Check npm packages in MCP commands
675
+ if (loaded.config.mcp) {
676
+ for (const [key, entry] of Object.entries(loaded.config.mcp)) {
677
+ for (const part of entry.command) {
678
+ const npmMatch = part.match(/^(@[^/]+\/[^@\s]+|[^@\s]+)$/);
679
+ if (npmMatch && (part.startsWith('npx ') || entry.command[0] === 'npx')) {
680
+ const pkgName = npmMatch[1];
681
+ try {
682
+ execSync(`npm view ${pkgName} version`, { stdio: 'pipe', timeout: 10000 });
683
+ deepOk.push(`${key}: npm package ${pkgName} exists`);
684
+ }
685
+ catch {
686
+ deepIssues.push(`${key}: npm package "${pkgName}" not found or network error`);
687
+ }
688
+ }
689
+ }
690
+ }
691
+ }
692
+ // Check GitHub token
693
+ if (io.env?.AGORA_GITHUB_TOKEN) {
694
+ deepOk.push('AGORA_GITHUB_TOKEN set');
695
+ }
696
+ // Check data directory
697
+ const agoraDir = detectAgoraDataDir({ cwd: io.cwd, env: io.env });
698
+ if (existsSync(agoraDir)) {
699
+ deepOk.push(`Agora data dir: ${agoraDir}`);
700
+ }
701
+ else {
702
+ deepIssues.push(`Agora data dir ${agoraDir} does not exist`);
703
+ }
704
+ // Auth state — distinguishes "no backend configured" from "signed out"
705
+ const agoraState = loadAgoraState(agoraDir);
706
+ const auth = getAuthState(agoraState);
707
+ if (auth?.apiUrl && auth?.accessToken) {
708
+ const expIn = (auth.accessExp ?? 0) - Math.floor(Date.now() / 1000);
709
+ const expLabel = expIn > 3600
710
+ ? Math.round(expIn / 3600) + 'h'
711
+ : expIn > 0
712
+ ? Math.round(expIn / 60) + 'm'
713
+ : 'expired';
714
+ deepOk.push(`auth: signed in to ${auth.apiUrl} (token ${expLabel})`);
715
+ }
716
+ else if (io.env?.AGORA_API_URL) {
717
+ deepIssues.push('AGORA_API_URL set but no auth token — run `agora auth login`');
718
+ }
719
+ else {
720
+ deepOk.push('auth: not configured (offline mode)');
721
+ }
722
+ // News cache age — surfaces "the feed is stale" before the user notices
723
+ try {
724
+ const { readCache } = await import('../../news/cache.js');
725
+ const items = readCache(agoraDir);
726
+ if (items.length === 0) {
727
+ deepOk.push('news cache: empty (run `agora news` to populate)');
728
+ }
729
+ else {
730
+ const newest = items.reduce((m, i) => Math.max(m, new Date(i.fetchedAt).getTime()), 0);
731
+ const ageH = (Date.now() - newest) / 3600000;
732
+ const ageLabel = ageH < 1 ? Math.round(ageH * 60) + 'm' : Math.round(ageH) + 'h';
733
+ if (ageH > 24) {
734
+ deepIssues.push(`news cache: stale (${ageLabel} old, ${items.length} items)`);
735
+ }
736
+ else {
737
+ deepOk.push(`news cache: ${items.length} items, newest ${ageLabel} old`);
738
+ }
739
+ }
740
+ }
741
+ catch {
742
+ /* skip news check if cache module fails */
743
+ }
744
+ // Hub cache age (only meaningful when live hubs are on)
745
+ if (io.env?.AGORA_LIVE_HUBS === '1' || process.env.AGORA_LIVE_HUBS === '1') {
746
+ try {
747
+ const { readHubsCache, isHubCacheStale } = await import('../../hubs/cache.js');
748
+ const hubItems = readHubsCache(agoraDir);
749
+ if (hubItems.length === 0) {
750
+ deepIssues.push('hub cache: empty (run `bun scripts/refresh-hubs.ts`)');
751
+ }
752
+ else if (isHubCacheStale(hubItems, 60, new Date())) {
753
+ deepIssues.push(`hub cache: stale (>60min, ${hubItems.length} items) — refresh-hubs to update`);
754
+ }
755
+ else {
756
+ deepOk.push(`hub cache: ${hubItems.length} items, fresh`);
757
+ }
758
+ }
759
+ catch {
760
+ /* skip */
761
+ }
762
+ }
763
+ for (const issue of deepIssues)
764
+ writeLine(io.stdout, ` ${style.dim('⚠')} ${issue}`);
765
+ for (const ok of deepOk)
766
+ writeLine(io.stdout, ` ${style.dim('✓')} ${ok}`);
767
+ }
768
+ writeLine(io.stdout, '');
769
+ writeLine(io.stdout, style.dim('Run with --fix to auto-heal common issues, --deep for full diagnostics.'));
770
+ return report.valid ? 0 : 1;
771
+ };
772
+ export const commandAuth = async (parsed, io, style) => {
773
+ const subcommand = parsed.args[0] || 'status';
774
+ const dataDir = detectDataDir(parsed, io);
775
+ const state = loadAgoraState(dataDir);
776
+ const existingAuth = getAuthState(state);
777
+ if (subcommand === 'login') {
778
+ const explicitToken = authTokenInput(parsed, io);
779
+ if (explicitToken) {
780
+ // Token-paste flow (existing behaviour, for CI/automation)
781
+ const apiUrl = stringFlag(parsed, 'apiUrl') || envString(io, 'AGORA_API_URL') || existingAuth?.apiUrl;
782
+ const nowSec = Math.floor(Date.now() / 1000);
783
+ const accessExp = decodeJwtExp(explicitToken) || nowSec + 3600;
784
+ const nextState = setAuthState(state, { accessToken: explicitToken, accessExp, apiUrl });
785
+ const auth = getAuthState(nextState);
786
+ writeAgoraState(dataDir, nextState);
787
+ if (parsed.flags.json) {
788
+ writeJson(io.stdout, authStatusPayload(dataDir, auth));
789
+ return 0;
790
+ }
791
+ const minutesLeft = Math.max(0, Math.round((accessExp - nowSec) / 60));
792
+ writeLine(io.stdout, 'Stored Agora API token');
793
+ writeLine(io.stdout, `Note: pasted token expires in ${minutesLeft}m. Use \`agora auth login\` (device-code) for a persistent session.`);
794
+ writeLine(io.stdout, `${style.dim('API URL')} ${auth?.apiUrl || 'not stored'}`);
795
+ writeLine(io.stdout, `${style.dim('State')} ${getAgoraStatePath(dataDir)}`);
796
+ return 0;
797
+ }
798
+ // ── Device-code flow ─────────────────────────────────────────────────
799
+ const apiUrl = stringFlag(parsed, 'apiUrl') || envString(io, 'AGORA_API_URL') || existingAuth?.apiUrl;
800
+ if (!apiUrl) {
801
+ return usageError(io, 'auth login requires --api-url, AGORA_API_URL, or stored apiUrl');
802
+ }
803
+ const baseUrl = apiUrl.replace(/\/+$/, '');
804
+ process.stdout.write(`\n${style.accent('Agora Login')}\n`);
805
+ process.stdout.write(`${style.dim('Connecting to')} ${baseUrl}...\n`);
806
+ try {
807
+ const codeRes = await fetch(`${baseUrl}/auth/device/code`, { method: 'POST' });
808
+ if (!codeRes.ok) {
809
+ const err = await codeRes.json().catch(() => ({ error: 'request failed' }));
810
+ return usageError(io, `Device code request failed: ${err.error || codeRes.status}`);
811
+ }
812
+ const codeData = (await codeRes.json());
813
+ if (!codeData ||
814
+ typeof codeData !== 'object' ||
815
+ typeof codeData.verification_uri !== 'string' ||
816
+ typeof codeData.user_code !== 'string' ||
817
+ typeof codeData.device_code !== 'string') {
818
+ return usageError(io, 'Device code response missing required fields');
819
+ }
820
+ const verificationUri = codeData.verification_uri;
821
+ const userCode = codeData.user_code;
822
+ const deviceCode = codeData.device_code;
823
+ const interval = (codeData.interval || 5) * 1000;
824
+ io.stdout.write(`\n${style.accent(userCode.slice(0, 4) + ' ' + userCode.slice(4))}\n\n`);
825
+ io.stdout.write(` ${style.dim('Open in your browser:')} ${verificationUri}\n`);
826
+ io.stdout.write(` ${style.dim('Enter code:')} ${userCode}\n\n`);
827
+ // Try to open browser automatically. verificationUri is server-supplied,
828
+ // so we validate the scheme and pass via spawnSync args (no shell).
829
+ try {
830
+ const parsed = new URL(verificationUri);
831
+ if (parsed.protocol !== 'https:' && parsed.protocol !== 'http:') {
832
+ throw new Error('Refusing to open non-http(s) verification URI');
833
+ }
834
+ const opener = process.platform === 'darwin' ? 'open' : 'xdg-open';
835
+ const result = spawnSync(opener, [parsed.toString()], { timeout: 3000, stdio: 'ignore' });
836
+ if (result.status === 0) {
837
+ io.stdout.write(` ${style.dim('Browser opened.')}\n\n`);
838
+ }
839
+ else {
840
+ io.stdout.write(` ${style.dim('Open the URL manually.')}\n\n`);
841
+ }
842
+ }
843
+ catch {
844
+ io.stdout.write(` ${style.dim('Open the URL manually.')}\n\n`);
845
+ }
846
+ // Poll for token
847
+ const pollStart = Date.now();
848
+ const pollTimeout = 15 * 60 * 1000; // 15 minutes
849
+ for (;;) {
850
+ await new Promise((r) => setTimeout(r, interval));
851
+ if (Date.now() - pollStart > pollTimeout) {
852
+ return usageError(io, 'Login timed out. Run `agora auth login` to try again.');
853
+ }
854
+ process.stdout.write(`\r\x1b[K${style.dim('Waiting for browser authorization...')}`);
855
+ try {
856
+ const tokenRes = await fetch(`${baseUrl}/auth/device/token`, {
857
+ method: 'POST',
858
+ headers: { 'Content-Type': 'application/json' },
859
+ body: JSON.stringify({ device_code: deviceCode })
860
+ });
861
+ if (tokenRes.ok) {
862
+ const tokenData = (await tokenRes.json());
863
+ if (!tokenData ||
864
+ typeof tokenData !== 'object' ||
865
+ typeof tokenData.access_token !== 'string' ||
866
+ typeof tokenData.refresh_token !== 'string') {
867
+ return usageError(io, 'Token response missing access_token / refresh_token');
868
+ }
869
+ process.stdout.write(`\r\x1b[K${style.dim('Authorization received.')}\n`);
870
+ const nowSec = Math.floor(Date.now() / 1000);
871
+ const nextState = setAuthState(state, {
872
+ accessToken: tokenData.access_token,
873
+ accessExp: nowSec + (tokenData.expires_in || 3600),
874
+ refreshToken: tokenData.refresh_token,
875
+ refreshExp: nowSec + (tokenData.refresh_expires_in || 0),
876
+ apiUrl
877
+ });
878
+ writeAgoraState(dataDir, nextState);
879
+ if (parsed.flags.json) {
880
+ writeJson(io.stdout, authStatusPayload(dataDir, getAuthState(nextState)));
881
+ return 0;
882
+ }
883
+ const expiresInMin = Math.round((tokenData.expires_in || 3600) / 60);
884
+ io.stdout.write(`\n${style.accent('✓ Authenticated')}\n`);
885
+ io.stdout.write(`${style.dim('API URL')} ${baseUrl}\n`);
886
+ io.stdout.write(`${style.dim('Token expires')} in ${expiresInMin}m\n`);
887
+ io.stdout.write(`${style.dim('State')} ${getAgoraStatePath(dataDir)}\n`);
888
+ return 0;
889
+ }
890
+ const errData = await tokenRes.json().catch(() => ({ error: 'unknown' }));
891
+ if (errData.error === 'expired') {
892
+ process.stdout.write(`\r\x1b[K`);
893
+ return usageError(io, 'Code expired. Run `agora auth login` again.');
894
+ }
895
+ // "authorization_pending" is expected — keep polling
896
+ }
897
+ catch {
898
+ // Network error, retry
899
+ }
900
+ }
901
+ }
902
+ catch (e) {
903
+ return usageError(io, `Login failed: ${e.message || 'connection error'}`);
904
+ }
905
+ }
906
+ if (subcommand === 'status') {
907
+ if (parsed.flags.json) {
908
+ writeJson(io.stdout, authStatusPayload(dataDir, existingAuth));
909
+ return 0;
910
+ }
911
+ writeLine(io.stdout, `${style.dim('Authenticated')} ${existingAuth ? 'yes' : 'no'}`);
912
+ if (existingAuth) {
913
+ const nowSec = Math.floor(Date.now() / 1000);
914
+ writeLine(io.stdout, `${style.dim('Access')} ${maskToken(existingAuth.accessToken)} (${formatRelativeExp(existingAuth.accessExp, nowSec)})`);
915
+ if (existingAuth.refreshToken) {
916
+ writeLine(io.stdout, `${style.dim('Refresh')} ${maskToken(existingAuth.refreshToken)} (${formatRelativeExp(existingAuth.refreshExp ?? 0, nowSec)})`);
917
+ }
918
+ else {
919
+ writeLine(io.stdout, `${style.dim('Refresh')} (none)`);
920
+ }
921
+ writeLine(io.stdout, `${style.dim('API URL')} ${existingAuth.apiUrl || 'not stored'}`);
922
+ writeLine(io.stdout, `${style.dim('Saved')} ${formatDate(existingAuth.savedAt)}`);
923
+ }
924
+ writeLine(io.stdout, `${style.dim('State')} ${getAgoraStatePath(dataDir)}`);
925
+ return 0;
926
+ }
927
+ if (subcommand === 'logout') {
928
+ if (!existingAuth) {
929
+ if (parsed.flags.json) {
930
+ writeJson(io.stdout, authStatusPayload(dataDir, undefined));
931
+ return 0;
932
+ }
933
+ writeLine(io.stdout, 'No stored Agora API token');
934
+ return 0;
935
+ }
936
+ if (existingAuth.apiUrl && existingAuth.accessToken && existingAuth.refreshToken) {
937
+ try {
938
+ await fetch(`${existingAuth.apiUrl.replace(/\/+$/, '')}/auth/logout`, {
939
+ method: 'POST',
940
+ headers: {
941
+ 'Content-Type': 'application/json',
942
+ Authorization: `Bearer ${existingAuth.accessToken}`
943
+ },
944
+ body: JSON.stringify({ refresh_token: existingAuth.refreshToken })
945
+ });
946
+ }
947
+ catch {
948
+ /* network failure — clear local anyway */
949
+ }
950
+ }
951
+ writeAgoraState(dataDir, clearAuthState(state));
952
+ if (parsed.flags.json) {
953
+ writeJson(io.stdout, authStatusPayload(dataDir, undefined));
954
+ return 0;
955
+ }
956
+ writeLine(io.stdout, 'Removed stored Agora API token');
957
+ return 0;
958
+ }
959
+ return usageError(io, `Unknown auth command: ${subcommand}`);
960
+ };
961
+ export const commandBookmarks = async (parsed, io, style) => {
962
+ const dataDir = detectDataDir(parsed, io);
963
+ const kind = parsed.flags.kind || 'all';
964
+ const state = loadAgoraState(dataDir);
965
+ const marketplaceItems = kind === 'news' ? [] : resolveSavedItems(state);
966
+ const newsMeta = readNewsMeta(dataDir);
967
+ const newsCache = readCache(dataDir);
968
+ const newsItems = kind === 'marketplace'
969
+ ? []
970
+ : newsMeta.saved
971
+ .map((id) => newsCache.find((n) => n.id === id))
972
+ .filter((n) => n !== undefined);
973
+ if (parsed.flags.json) {
974
+ writeJson(io.stdout, {
975
+ marketplace: marketplaceItems,
976
+ news: newsItems
977
+ });
978
+ return 0;
979
+ }
980
+ const hasMarketplace = marketplaceItems.length > 0;
981
+ const hasNews = newsItems.length > 0;
982
+ if (!hasMarketplace && !hasNews) {
983
+ writeLine(io.stdout, 'No bookmarks yet.');
984
+ writeLine(io.stdout, style.dim('Use `agora save <id>` to bookmark marketplace items.'));
985
+ return 0;
986
+ }
987
+ if (kind !== 'news' && hasMarketplace) {
988
+ writeLine(io.stdout, style.accent('Marketplace'));
989
+ writeLine(io.stdout, style.dim('─'.repeat(40)));
990
+ writeLine(io.stdout, formatSavedList(marketplaceItems, style));
991
+ writeLine(io.stdout, '');
992
+ }
993
+ if (kind !== 'marketplace' && hasNews) {
994
+ const now = Date.now();
995
+ writeLine(io.stdout, style.accent('News'));
996
+ writeLine(io.stdout, style.dim('─'.repeat(40)));
997
+ for (const item of newsItems) {
998
+ const ageMs = now - new Date(item.publishedAt).getTime();
999
+ const ageDays = Math.floor(ageMs / 86400000);
1000
+ const age = ageDays === 0 ? 'today' : ageDays === 1 ? '1d ago' : `${ageDays}d ago`;
1001
+ writeLine(io.stdout, `${style.dim(item.source.padEnd(16))} ${style.dim(age.padStart(8))} ${item.title}`);
1002
+ }
1003
+ }
1004
+ return 0;
1005
+ };
1006
+ //# sourceMappingURL=operations.js.map