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.
- package/README.md +86 -255
- package/dist/atomic-write.d.ts +10 -0
- package/dist/atomic-write.d.ts.map +1 -0
- package/dist/atomic-write.js +23 -0
- package/dist/atomic-write.js.map +1 -0
- package/dist/auth/refresh.d.ts +17 -0
- package/dist/auth/refresh.d.ts.map +1 -0
- package/dist/auth/refresh.js +50 -0
- package/dist/auth/refresh.js.map +1 -0
- package/dist/cli/app.d.ts +13 -18
- package/dist/cli/app.d.ts.map +1 -1
- package/dist/cli/app.js +184 -1187
- package/dist/cli/app.js.map +1 -1
- package/dist/cli/chat-renderer.d.ts +31 -0
- package/dist/cli/chat-renderer.d.ts.map +1 -0
- package/dist/cli/chat-renderer.js +275 -0
- package/dist/cli/chat-renderer.js.map +1 -0
- package/dist/cli/commands/browse.d.ts +4 -0
- package/dist/cli/commands/browse.d.ts.map +1 -0
- package/dist/cli/commands/browse.js +80 -0
- package/dist/cli/commands/browse.js.map +1 -0
- package/dist/cli/commands/chat.d.ts +4 -0
- package/dist/cli/commands/chat.d.ts.map +1 -0
- package/dist/cli/commands/chat.js +125 -0
- package/dist/cli/commands/chat.js.map +1 -0
- package/dist/cli/commands/community.d.ts +12 -0
- package/dist/cli/commands/community.d.ts.map +1 -0
- package/dist/cli/commands/community.js +453 -0
- package/dist/cli/commands/community.js.map +1 -0
- package/dist/cli/commands/export.d.ts +3 -0
- package/dist/cli/commands/export.d.ts.map +1 -0
- package/dist/cli/commands/export.js +108 -0
- package/dist/cli/commands/export.js.map +1 -0
- package/dist/cli/commands/init.d.ts +4 -0
- package/dist/cli/commands/init.d.ts.map +1 -0
- package/dist/cli/commands/init.js +299 -0
- package/dist/cli/commands/init.js.map +1 -0
- package/dist/cli/commands/learn.d.ts +4 -0
- package/dist/cli/commands/learn.d.ts.map +1 -0
- package/dist/cli/commands/learn.js +62 -0
- package/dist/cli/commands/learn.js.map +1 -0
- package/dist/cli/commands/marketplace.d.ts +9 -0
- package/dist/cli/commands/marketplace.d.ts.map +1 -0
- package/dist/cli/commands/marketplace.js +321 -0
- package/dist/cli/commands/marketplace.js.map +1 -0
- package/dist/cli/commands/notify.d.ts +3 -0
- package/dist/cli/commands/notify.d.ts.map +1 -0
- package/dist/cli/commands/notify.js +59 -0
- package/dist/cli/commands/notify.js.map +1 -0
- package/dist/cli/commands/operations.d.ts +16 -0
- package/dist/cli/commands/operations.d.ts.map +1 -0
- package/dist/cli/commands/operations.js +1006 -0
- package/dist/cli/commands/operations.js.map +1 -0
- package/dist/cli/commands/ping.d.ts +3 -0
- package/dist/cli/commands/ping.d.ts.map +1 -0
- package/dist/cli/commands/ping.js +56 -0
- package/dist/cli/commands/ping.js.map +1 -0
- package/dist/cli/commands/today.d.ts +3 -0
- package/dist/cli/commands/today.d.ts.map +1 -0
- package/dist/cli/commands/today.js +142 -0
- package/dist/cli/commands/today.js.map +1 -0
- package/dist/cli/commands/types.d.ts +5 -0
- package/dist/cli/commands/types.d.ts.map +1 -0
- package/dist/cli/commands/types.js +2 -0
- package/dist/cli/commands/types.js.map +1 -0
- package/dist/cli/commands/watch.d.ts +3 -0
- package/dist/cli/commands/watch.d.ts.map +1 -0
- package/dist/cli/commands/watch.js +41 -0
- package/dist/cli/commands/watch.js.map +1 -0
- package/dist/cli/commands/welcome.d.ts +3 -0
- package/dist/cli/commands/welcome.d.ts.map +1 -0
- package/dist/cli/commands/welcome.js +97 -0
- package/dist/cli/commands/welcome.js.map +1 -0
- package/dist/cli/commands-meta.d.ts +21 -0
- package/dist/cli/commands-meta.d.ts.map +1 -0
- package/dist/cli/commands-meta.js +828 -0
- package/dist/cli/commands-meta.js.map +1 -0
- package/dist/cli/completions-gen.d.ts +2 -0
- package/dist/cli/completions-gen.d.ts.map +1 -0
- package/dist/cli/completions-gen.js +195 -0
- package/dist/cli/completions-gen.js.map +1 -0
- package/dist/cli/completions.d.ts +18 -0
- package/dist/cli/completions.d.ts.map +1 -0
- package/dist/cli/completions.js +227 -0
- package/dist/cli/completions.js.map +1 -0
- package/dist/cli/flags.d.ts +19 -0
- package/dist/cli/flags.d.ts.map +1 -0
- package/dist/cli/flags.js +91 -0
- package/dist/cli/flags.js.map +1 -0
- package/dist/cli/format.d.ts +19 -0
- package/dist/cli/format.d.ts.map +1 -0
- package/dist/cli/format.js +249 -0
- package/dist/cli/format.js.map +1 -0
- package/dist/cli/helpers.d.ts +95 -0
- package/dist/cli/helpers.d.ts.map +1 -0
- package/dist/cli/helpers.js +301 -0
- package/dist/cli/helpers.js.map +1 -0
- package/dist/cli/mcp-server.d.ts +4 -0
- package/dist/cli/mcp-server.d.ts.map +1 -0
- package/dist/cli/mcp-server.js +277 -0
- package/dist/cli/mcp-server.js.map +1 -0
- package/dist/cli/menu.d.ts +7 -0
- package/dist/cli/menu.d.ts.map +1 -0
- package/dist/cli/menu.js +172 -0
- package/dist/cli/menu.js.map +1 -0
- package/dist/cli/pages/community.d.ts +9 -0
- package/dist/cli/pages/community.d.ts.map +1 -0
- package/dist/cli/pages/community.js +1094 -0
- package/dist/cli/pages/community.js.map +1 -0
- package/dist/cli/pages/helpers.d.ts +37 -0
- package/dist/cli/pages/helpers.d.ts.map +1 -0
- package/dist/cli/pages/helpers.js +98 -0
- package/dist/cli/pages/helpers.js.map +1 -0
- package/dist/cli/pages/home.d.ts +4 -0
- package/dist/cli/pages/home.d.ts.map +1 -0
- package/dist/cli/pages/home.js +231 -0
- package/dist/cli/pages/home.js.map +1 -0
- package/dist/cli/pages/marketplace.d.ts +5 -0
- package/dist/cli/pages/marketplace.d.ts.map +1 -0
- package/dist/cli/pages/marketplace.js +583 -0
- package/dist/cli/pages/marketplace.js.map +1 -0
- package/dist/cli/pages/news.d.ts +31 -0
- package/dist/cli/pages/news.d.ts.map +1 -0
- package/dist/cli/pages/news.js +688 -0
- package/dist/cli/pages/news.js.map +1 -0
- package/dist/cli/pages/settings.d.ts +3 -0
- package/dist/cli/pages/settings.d.ts.map +1 -0
- package/dist/cli/pages/settings.js +296 -0
- package/dist/cli/pages/settings.js.map +1 -0
- package/dist/cli/pages/types.d.ts +67 -0
- package/dist/cli/pages/types.d.ts.map +1 -0
- package/dist/cli/pages/types.js +2 -0
- package/dist/cli/pages/types.js.map +1 -0
- package/dist/cli/prompter.d.ts +135 -0
- package/dist/cli/prompter.d.ts.map +1 -0
- package/dist/cli/prompter.js +710 -0
- package/dist/cli/prompter.js.map +1 -0
- package/dist/cli/shell.d.ts +23 -0
- package/dist/cli/shell.d.ts.map +1 -0
- package/dist/cli/shell.js +1106 -0
- package/dist/cli/shell.js.map +1 -0
- package/dist/cli/tui.d.ts +7 -0
- package/dist/cli/tui.d.ts.map +1 -0
- package/dist/cli/tui.js +419 -0
- package/dist/cli/tui.js.map +1 -0
- package/dist/cli.js +1 -1
- package/dist/cli.js.map +1 -1
- package/dist/commands.d.ts +14 -0
- package/dist/commands.d.ts.map +1 -0
- package/dist/commands.js +28 -0
- package/dist/commands.js.map +1 -0
- package/dist/community/client.d.ts +84 -0
- package/dist/community/client.d.ts.map +1 -0
- package/dist/community/client.js +340 -0
- package/dist/community/client.js.map +1 -0
- package/dist/community/search.d.ts +25 -0
- package/dist/community/search.d.ts.map +1 -0
- package/dist/community/search.js +62 -0
- package/dist/community/search.js.map +1 -0
- package/dist/community/types.d.ts +71 -0
- package/dist/community/types.d.ts.map +1 -0
- package/dist/community/types.js +11 -0
- package/dist/community/types.js.map +1 -0
- package/dist/config.d.ts +1 -7
- package/dist/config.d.ts.map +1 -1
- package/dist/config.js +0 -32
- package/dist/config.js.map +1 -1
- package/dist/data.d.ts +1 -1
- package/dist/data.d.ts.map +1 -1
- package/dist/data.js +778 -40
- package/dist/data.js.map +1 -1
- package/dist/format.d.ts +5 -39
- package/dist/format.d.ts.map +1 -1
- package/dist/format.js +5 -120
- package/dist/format.js.map +1 -1
- package/dist/history.d.ts +13 -0
- package/dist/history.d.ts.map +1 -0
- package/dist/history.js +37 -0
- package/dist/history.js.map +1 -0
- package/dist/hubs/cache.d.ts +6 -0
- package/dist/hubs/cache.d.ts.map +1 -0
- package/dist/hubs/cache.js +46 -0
- package/dist/hubs/cache.js.map +1 -0
- package/dist/hubs/enrichment.d.ts +43 -0
- package/dist/hubs/enrichment.d.ts.map +1 -0
- package/dist/hubs/enrichment.js +239 -0
- package/dist/hubs/enrichment.js.map +1 -0
- package/dist/hubs/github.d.ts +12 -0
- package/dist/hubs/github.d.ts.map +1 -0
- package/dist/hubs/github.js +54 -0
- package/dist/hubs/github.js.map +1 -0
- package/dist/hubs/huggingface.d.ts +27 -0
- package/dist/hubs/huggingface.d.ts.map +1 -0
- package/dist/hubs/huggingface.js +88 -0
- package/dist/hubs/huggingface.js.map +1 -0
- package/dist/hubs/quality.d.ts +26 -0
- package/dist/hubs/quality.d.ts.map +1 -0
- package/dist/hubs/quality.js +57 -0
- package/dist/hubs/quality.js.map +1 -0
- package/dist/hubs/types.d.ts +30 -0
- package/dist/hubs/types.d.ts.map +1 -0
- package/dist/hubs/types.js +2 -0
- package/dist/hubs/types.js.map +1 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +188 -224
- package/dist/index.js.map +1 -1
- package/dist/init.d.ts.map +1 -1
- package/dist/init.js +6 -11
- package/dist/init.js.map +1 -1
- package/dist/live.d.ts +14 -0
- package/dist/live.d.ts.map +1 -1
- package/dist/live.js +35 -3
- package/dist/live.js.map +1 -1
- package/dist/marketplace.d.ts +25 -3
- package/dist/marketplace.d.ts.map +1 -1
- package/dist/marketplace.js +279 -22
- package/dist/marketplace.js.map +1 -1
- package/dist/news/cache.d.ts +13 -0
- package/dist/news/cache.d.ts.map +1 -0
- package/dist/news/cache.js +66 -0
- package/dist/news/cache.js.map +1 -0
- package/dist/news/score.d.ts +4 -0
- package/dist/news/score.d.ts.map +1 -0
- package/dist/news/score.js +43 -0
- package/dist/news/score.js.map +1 -0
- package/dist/news/sources/arxiv.d.ts +9 -0
- package/dist/news/sources/arxiv.d.ts.map +1 -0
- package/dist/news/sources/arxiv.js +107 -0
- package/dist/news/sources/arxiv.js.map +1 -0
- package/dist/news/sources/github-trending.d.ts +9 -0
- package/dist/news/sources/github-trending.d.ts.map +1 -0
- package/dist/news/sources/github-trending.js +97 -0
- package/dist/news/sources/github-trending.js.map +1 -0
- package/dist/news/sources/hn.d.ts +9 -0
- package/dist/news/sources/hn.d.ts.map +1 -0
- package/dist/news/sources/hn.js +57 -0
- package/dist/news/sources/hn.js.map +1 -0
- package/dist/news/sources/reddit.d.ts +9 -0
- package/dist/news/sources/reddit.d.ts.map +1 -0
- package/dist/news/sources/reddit.js +69 -0
- package/dist/news/sources/reddit.js.map +1 -0
- package/dist/news/sources/rss.d.ts +13 -0
- package/dist/news/sources/rss.d.ts.map +1 -0
- package/dist/news/sources/rss.js +14 -0
- package/dist/news/sources/rss.js.map +1 -0
- package/dist/news/types.d.ts +42 -0
- package/dist/news/types.d.ts.map +1 -0
- package/dist/news/types.js +56 -0
- package/dist/news/types.js.map +1 -0
- package/dist/preferences.d.ts +14 -0
- package/dist/preferences.d.ts.map +1 -0
- package/dist/preferences.js +31 -0
- package/dist/preferences.js.map +1 -0
- package/dist/settings.d.ts +26 -0
- package/dist/settings.d.ts.map +1 -0
- package/dist/settings.js +251 -0
- package/dist/settings.js.map +1 -0
- package/dist/state.d.ts +9 -2
- package/dist/state.d.ts.map +1 -1
- package/dist/state.js +41 -19
- package/dist/state.js.map +1 -1
- package/dist/transcript.d.ts +28 -0
- package/dist/transcript.d.ts.map +1 -0
- package/dist/transcript.js +79 -0
- package/dist/transcript.js.map +1 -0
- package/dist/types.d.ts +19 -1
- package/dist/types.d.ts.map +1 -1
- package/dist/ui.d.ts +157 -0
- package/dist/ui.d.ts.map +1 -0
- package/dist/ui.js +296 -0
- package/dist/ui.js.map +1 -0
- package/package.json +11 -9
- package/dist/api.d.ts +0 -72
- package/dist/api.d.ts.map +0 -1
- package/dist/api.js +0 -109
- package/dist/api.js.map +0 -1
- package/dist/logger.d.ts +0 -20
- package/dist/logger.d.ts.map +0 -1
- package/dist/logger.js +0 -59
- package/dist/logger.js.map +0 -1
|
@@ -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
|