@jackwener/opencli 1.7.1 → 1.7.3
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 +5 -2
- package/README.zh-CN.md +6 -3
- package/cli-manifest.json +1085 -73
- package/clis/barchart/flow.js +1 -1
- package/clis/barchart/greeks.js +2 -2
- package/clis/barchart/options.js +2 -2
- package/clis/barchart/quote.js +1 -1
- package/clis/bilibili/feed.js +202 -48
- package/clis/binance/asks.js +21 -0
- package/clis/binance/commands.test.js +70 -0
- package/clis/binance/depth.js +21 -0
- package/clis/binance/gainers.js +22 -0
- package/clis/binance/klines.js +21 -0
- package/clis/binance/losers.js +22 -0
- package/clis/binance/pairs.js +21 -0
- package/clis/binance/price.js +18 -0
- package/clis/binance/prices.js +19 -0
- package/clis/binance/ticker.js +21 -0
- package/clis/binance/top.js +21 -0
- package/clis/binance/trades.js +20 -0
- package/clis/boss/utils.js +2 -1
- package/clis/chatgpt/image.js +97 -0
- package/clis/chatgpt/utils.js +297 -0
- package/clis/{chatgpt → chatgpt-app}/ask.js +1 -1
- package/clis/{chatgpt → chatgpt-app}/model.js +1 -1
- package/clis/{chatgpt → chatgpt-app}/new.js +1 -1
- package/clis/{chatgpt → chatgpt-app}/read.js +1 -1
- package/clis/{chatgpt → chatgpt-app}/send.js +1 -1
- package/clis/{chatgpt → chatgpt-app}/status.js +1 -1
- package/clis/discord-app/delete.js +114 -0
- package/clis/douban/utils.js +29 -2
- package/clis/douban/utils.test.js +121 -1
- package/clis/ke/chengjiao.js +77 -0
- package/clis/ke/ershoufang.js +100 -0
- package/clis/ke/utils.js +104 -0
- package/clis/ke/xiaoqu.js +77 -0
- package/clis/ke/zufang.js +94 -0
- package/clis/maimai/search-talents.js +172 -0
- package/clis/mubu/doc.js +40 -0
- package/clis/mubu/docs.js +43 -0
- package/clis/mubu/notes.js +244 -0
- package/clis/mubu/recent.js +27 -0
- package/clis/mubu/search.js +62 -0
- package/clis/mubu/utils.js +304 -0
- package/clis/reuters/search.js +1 -1
- package/clis/twitter/lists-parser.js +77 -0
- package/clis/twitter/lists.d.ts +5 -0
- package/clis/twitter/lists.js +62 -0
- package/clis/twitter/lists.test.js +50 -0
- package/clis/weibo/feed.js +18 -5
- package/clis/xiaohongshu/comments.js +18 -6
- package/clis/xiaohongshu/comments.test.js +36 -0
- package/clis/xiaohongshu/creator-note-detail.js +2 -0
- package/clis/xiaohongshu/creator-note-detail.test.js +32 -0
- package/clis/xiaohongshu/creator-notes-summary.js +4 -0
- package/clis/xiaohongshu/creator-notes-summary.test.js +39 -1
- package/clis/xiaohongshu/creator-notes.js +1 -0
- package/clis/xiaohongshu/creator-profile.js +1 -0
- package/clis/xiaohongshu/creator-stats.js +1 -0
- package/clis/xiaohongshu/download.js +12 -0
- package/clis/xiaohongshu/download.test.js +30 -0
- package/clis/xiaohongshu/navigation.test.js +34 -0
- package/clis/xiaohongshu/note.js +14 -5
- package/clis/xiaohongshu/note.test.js +28 -0
- package/clis/xiaohongshu/publish.js +1 -0
- package/clis/xiaohongshu/search.js +1 -0
- package/clis/xiaohongshu/user.js +1 -0
- package/clis/yahoo-finance/quote.js +1 -1
- package/clis/zsxq/topic.js +5 -3
- package/clis/zsxq/topic.test.js +4 -3
- package/clis/zsxq/utils.js +1 -1
- package/dist/src/browser/base-page.d.ts +9 -0
- package/dist/src/browser/base-page.js +19 -0
- package/dist/src/browser/cdp.js +10 -2
- package/dist/src/browser/daemon-client.d.ts +1 -0
- package/dist/src/cli.js +112 -2
- package/dist/src/daemon.js +5 -0
- package/dist/src/discovery.d.ts +5 -2
- package/dist/src/discovery.js +7 -35
- package/dist/src/doctor.d.ts +1 -0
- package/dist/src/doctor.js +51 -2
- package/dist/src/electron-apps.js +1 -1
- package/dist/src/engine.test.js +29 -1
- package/dist/src/errors.d.ts +1 -0
- package/dist/src/errors.js +13 -0
- package/dist/src/execution.js +36 -9
- package/dist/src/execution.test.js +23 -0
- package/dist/src/logger.d.ts +2 -2
- package/dist/src/logger.js +4 -9
- package/dist/src/main.js +6 -5
- package/dist/src/registry.js +3 -4
- package/dist/src/types.d.ts +2 -0
- package/dist/src/update-check.d.ts +14 -0
- package/dist/src/update-check.js +48 -3
- package/dist/src/update-check.test.js +31 -0
- package/package.json +3 -3
- package/scripts/fetch-adapters.js +92 -34
- package/dist/src/clis/binance/asks.js +0 -20
- package/dist/src/clis/binance/commands.test.d.ts +0 -3
- package/dist/src/clis/binance/commands.test.js +0 -58
- package/dist/src/clis/binance/depth.d.ts +0 -1
- package/dist/src/clis/binance/depth.js +0 -20
- package/dist/src/clis/binance/gainers.d.ts +0 -1
- package/dist/src/clis/binance/gainers.js +0 -21
- package/dist/src/clis/binance/klines.d.ts +0 -1
- package/dist/src/clis/binance/klines.js +0 -20
- package/dist/src/clis/binance/losers.d.ts +0 -1
- package/dist/src/clis/binance/losers.js +0 -21
- package/dist/src/clis/binance/pairs.d.ts +0 -1
- package/dist/src/clis/binance/pairs.js +0 -20
- package/dist/src/clis/binance/price.d.ts +0 -1
- package/dist/src/clis/binance/price.js +0 -17
- package/dist/src/clis/binance/prices.d.ts +0 -1
- package/dist/src/clis/binance/prices.js +0 -18
- package/dist/src/clis/binance/ticker.d.ts +0 -1
- package/dist/src/clis/binance/ticker.js +0 -20
- package/dist/src/clis/binance/top.d.ts +0 -1
- package/dist/src/clis/binance/top.js +0 -20
- package/dist/src/clis/binance/trades.d.ts +0 -1
- package/dist/src/clis/binance/trades.js +0 -19
- /package/clis/{chatgpt → chatgpt-app}/ax.js +0 -0
- /package/dist/src/{clis/binance/asks.d.ts → update-check.test.d.ts} +0 -0
package/dist/src/cli.js
CHANGED
|
@@ -369,9 +369,10 @@ export function createProgram(BUILTIN_CLIS, USER_CLIS) {
|
|
|
369
369
|
await page.wait(0.3);
|
|
370
370
|
await page.typeText(index, text);
|
|
371
371
|
// Detect autocomplete/combobox fields and wait for dropdown suggestions
|
|
372
|
+
const safeIndex = JSON.stringify(String(index));
|
|
372
373
|
const isAutocomplete = await page.evaluate(`
|
|
373
374
|
(() => {
|
|
374
|
-
const el = document.querySelector('[data-opencli-ref="${
|
|
375
|
+
const el = document.querySelector('[data-opencli-ref="' + ${safeIndex} + '"]');
|
|
375
376
|
if (!el) return false;
|
|
376
377
|
const role = el.getAttribute('role');
|
|
377
378
|
const ac = el.getAttribute('aria-autocomplete');
|
|
@@ -390,9 +391,10 @@ export function createProgram(BUILTIN_CLIS, USER_CLIS) {
|
|
|
390
391
|
browser.command('select').argument('<index>', 'Element index of <select>').argument('<option>', 'Option text')
|
|
391
392
|
.description('Select dropdown option')
|
|
392
393
|
.action(browserAction(async (page, index, option) => {
|
|
394
|
+
const safeIdx = JSON.stringify(String(index));
|
|
393
395
|
const result = await page.evaluate(`
|
|
394
396
|
(function() {
|
|
395
|
-
var sel = document.querySelector('[data-opencli-ref="${
|
|
397
|
+
var sel = document.querySelector('[data-opencli-ref="' + ${safeIdx} + '"]');
|
|
396
398
|
if (!sel || sel.tagName !== 'SELECT') return { error: 'Not a <select>' };
|
|
397
399
|
var match = Array.from(sel.options).find(o => o.text.trim() === ${JSON.stringify(option)} || o.value === ${JSON.stringify(option)});
|
|
398
400
|
if (!match) return { error: 'Option not found', available: Array.from(sel.options).map(o => o.text.trim()) };
|
|
@@ -891,6 +893,114 @@ cli({
|
|
|
891
893
|
process.exitCode = EXIT_CODES.GENERIC_ERROR;
|
|
892
894
|
}
|
|
893
895
|
});
|
|
896
|
+
// ── Built-in: adapter management ─────────────────────────────────────────
|
|
897
|
+
const adapterCmd = program.command('adapter').description('Manage CLI adapters');
|
|
898
|
+
adapterCmd
|
|
899
|
+
.command('status')
|
|
900
|
+
.description('Show which sites have local overrides vs using official baseline')
|
|
901
|
+
.action(async () => {
|
|
902
|
+
const os = await import('node:os');
|
|
903
|
+
const userClisDir = path.join(os.homedir(), '.opencli', 'clis');
|
|
904
|
+
const builtinClisDir = BUILTIN_CLIS;
|
|
905
|
+
try {
|
|
906
|
+
const userEntries = await fs.promises.readdir(userClisDir, { withFileTypes: true });
|
|
907
|
+
const userSites = userEntries.filter(e => e.isDirectory()).map(e => e.name).sort();
|
|
908
|
+
let builtinSites = [];
|
|
909
|
+
try {
|
|
910
|
+
const builtinEntries = await fs.promises.readdir(builtinClisDir, { withFileTypes: true });
|
|
911
|
+
builtinSites = builtinEntries.filter(e => e.isDirectory()).map(e => e.name).sort();
|
|
912
|
+
}
|
|
913
|
+
catch { /* no builtin dir */ }
|
|
914
|
+
if (userSites.length === 0) {
|
|
915
|
+
console.log('No local adapter overrides. All sites use the official baseline.');
|
|
916
|
+
return;
|
|
917
|
+
}
|
|
918
|
+
console.log(`Local overrides in ~/.opencli/clis/ (${userSites.length} sites):\n`);
|
|
919
|
+
for (const site of userSites) {
|
|
920
|
+
const isOfficial = builtinSites.includes(site);
|
|
921
|
+
const label = isOfficial ? 'override' : 'custom';
|
|
922
|
+
console.log(` ${site} [${label}]`);
|
|
923
|
+
}
|
|
924
|
+
console.log(`\nOfficial baseline: ${builtinSites.length} sites in package`);
|
|
925
|
+
}
|
|
926
|
+
catch {
|
|
927
|
+
console.log('No local adapter overrides. All sites use the official baseline.');
|
|
928
|
+
}
|
|
929
|
+
});
|
|
930
|
+
adapterCmd
|
|
931
|
+
.command('eject')
|
|
932
|
+
.description('Copy an official adapter to ~/.opencli/clis/ for local editing')
|
|
933
|
+
.argument('<site>', 'Site name (e.g. twitter, bilibili)')
|
|
934
|
+
.action(async (site) => {
|
|
935
|
+
const os = await import('node:os');
|
|
936
|
+
const userClisDir = path.join(os.homedir(), '.opencli', 'clis');
|
|
937
|
+
const builtinSiteDir = path.join(BUILTIN_CLIS, site);
|
|
938
|
+
const userSiteDir = path.join(userClisDir, site);
|
|
939
|
+
try {
|
|
940
|
+
await fs.promises.access(builtinSiteDir);
|
|
941
|
+
}
|
|
942
|
+
catch {
|
|
943
|
+
console.error(styleText('red', `Error: Site "${site}" not found in official adapters.`));
|
|
944
|
+
process.exitCode = EXIT_CODES.USAGE_ERROR;
|
|
945
|
+
return;
|
|
946
|
+
}
|
|
947
|
+
try {
|
|
948
|
+
await fs.promises.access(userSiteDir);
|
|
949
|
+
console.error(styleText('yellow', `Site "${site}" already exists in ~/.opencli/clis/. Use "opencli adapter reset ${site}" first to restore official version.`));
|
|
950
|
+
process.exitCode = EXIT_CODES.USAGE_ERROR;
|
|
951
|
+
return;
|
|
952
|
+
}
|
|
953
|
+
catch { /* good, doesn't exist yet */ }
|
|
954
|
+
fs.cpSync(builtinSiteDir, userSiteDir, { recursive: true });
|
|
955
|
+
console.log(styleText('green', `✅ Ejected "${site}" to ~/.opencli/clis/${site}/`));
|
|
956
|
+
console.log('You can now edit the adapter files. Changes take effect immediately.');
|
|
957
|
+
console.log(styleText('yellow', 'Note: Official updates to this adapter will overwrite your changes.'));
|
|
958
|
+
});
|
|
959
|
+
adapterCmd
|
|
960
|
+
.command('reset')
|
|
961
|
+
.description('Remove local override and restore official adapter version')
|
|
962
|
+
.argument('[site]', 'Site name (e.g. twitter, bilibili)')
|
|
963
|
+
.option('--all', 'Reset all local overrides')
|
|
964
|
+
.action(async (site, opts) => {
|
|
965
|
+
const os = await import('node:os');
|
|
966
|
+
const userClisDir = path.join(os.homedir(), '.opencli', 'clis');
|
|
967
|
+
if (opts.all) {
|
|
968
|
+
try {
|
|
969
|
+
const userEntries = await fs.promises.readdir(userClisDir, { withFileTypes: true });
|
|
970
|
+
const dirs = userEntries.filter(e => e.isDirectory());
|
|
971
|
+
if (dirs.length === 0) {
|
|
972
|
+
console.log('No local sites to reset.');
|
|
973
|
+
return;
|
|
974
|
+
}
|
|
975
|
+
for (const dir of dirs) {
|
|
976
|
+
fs.rmSync(path.join(userClisDir, dir.name), { recursive: true, force: true });
|
|
977
|
+
}
|
|
978
|
+
console.log(styleText('green', `✅ Reset ${dirs.length} site(s). All adapters now use official baseline.`));
|
|
979
|
+
}
|
|
980
|
+
catch {
|
|
981
|
+
console.log('No local sites to reset.');
|
|
982
|
+
}
|
|
983
|
+
return;
|
|
984
|
+
}
|
|
985
|
+
if (!site) {
|
|
986
|
+
console.error(styleText('red', 'Error: Please specify a site name or use --all.'));
|
|
987
|
+
process.exitCode = EXIT_CODES.USAGE_ERROR;
|
|
988
|
+
return;
|
|
989
|
+
}
|
|
990
|
+
const userSiteDir = path.join(userClisDir, site);
|
|
991
|
+
try {
|
|
992
|
+
await fs.promises.access(userSiteDir);
|
|
993
|
+
}
|
|
994
|
+
catch {
|
|
995
|
+
console.error(styleText('yellow', `Site "${site}" has no local override.`));
|
|
996
|
+
return;
|
|
997
|
+
}
|
|
998
|
+
const isOfficial = fs.existsSync(path.join(BUILTIN_CLIS, site));
|
|
999
|
+
fs.rmSync(userSiteDir, { recursive: true, force: true });
|
|
1000
|
+
console.log(styleText('green', isOfficial
|
|
1001
|
+
? `✅ Reset "${site}". Now using official baseline.`
|
|
1002
|
+
: `✅ Removed custom site "${site}".`));
|
|
1003
|
+
});
|
|
894
1004
|
// ── Built-in: daemon ──────────────────────────────────────────────────────
|
|
895
1005
|
const daemonCmd = program.command('daemon').description('Manage the opencli daemon');
|
|
896
1006
|
daemonCmd
|
package/dist/src/daemon.js
CHANGED
|
@@ -27,6 +27,7 @@ const PORT = parseInt(process.env.OPENCLI_DAEMON_PORT ?? String(DEFAULT_DAEMON_P
|
|
|
27
27
|
// ─── State ───────────────────────────────────────────────────────────
|
|
28
28
|
let extensionWs = null;
|
|
29
29
|
let extensionVersion = null;
|
|
30
|
+
let extensionCompatRange = null;
|
|
30
31
|
const pending = new Map();
|
|
31
32
|
const LOG_BUFFER_SIZE = 200;
|
|
32
33
|
const logBuffer = [];
|
|
@@ -111,6 +112,7 @@ async function handleRequest(req, res) {
|
|
|
111
112
|
uptime,
|
|
112
113
|
extensionConnected: extensionWs?.readyState === WebSocket.OPEN,
|
|
113
114
|
extensionVersion,
|
|
115
|
+
extensionCompatRange,
|
|
114
116
|
pending: pending.size,
|
|
115
117
|
memoryMB: Math.round(mem.rss / 1024 / 1024 * 10) / 10,
|
|
116
118
|
port: PORT,
|
|
@@ -188,6 +190,7 @@ wss.on('connection', (ws) => {
|
|
|
188
190
|
log.info('[daemon] Extension connected');
|
|
189
191
|
extensionWs = ws;
|
|
190
192
|
extensionVersion = null; // cleared until hello message arrives
|
|
193
|
+
extensionCompatRange = null;
|
|
191
194
|
// ── Heartbeat: ping every 15s, close if 2 pongs missed ──
|
|
192
195
|
let missedPongs = 0;
|
|
193
196
|
const heartbeatInterval = setInterval(() => {
|
|
@@ -213,6 +216,7 @@ wss.on('connection', (ws) => {
|
|
|
213
216
|
// Handle hello message from extension (version handshake)
|
|
214
217
|
if (msg.type === 'hello') {
|
|
215
218
|
extensionVersion = typeof msg.version === 'string' ? msg.version : null;
|
|
219
|
+
extensionCompatRange = typeof msg.compatRange === 'string' ? msg.compatRange : null;
|
|
216
220
|
return;
|
|
217
221
|
}
|
|
218
222
|
// Handle log messages from extension
|
|
@@ -244,6 +248,7 @@ wss.on('connection', (ws) => {
|
|
|
244
248
|
if (extensionWs === ws) {
|
|
245
249
|
extensionWs = null;
|
|
246
250
|
extensionVersion = null;
|
|
251
|
+
extensionCompatRange = null;
|
|
247
252
|
// Reject all pending requests since the extension is gone
|
|
248
253
|
for (const [id, p] of pending) {
|
|
249
254
|
clearTimeout(p.timer);
|
package/dist/src/discovery.d.ts
CHANGED
|
@@ -23,8 +23,11 @@ export declare const PLUGINS_DIR: string;
|
|
|
23
23
|
*/
|
|
24
24
|
export declare function ensureUserCliCompatShims(baseDir?: string): Promise<void>;
|
|
25
25
|
/**
|
|
26
|
-
*
|
|
27
|
-
*
|
|
26
|
+
* Ensure the user adapters directory exists.
|
|
27
|
+
*
|
|
28
|
+
* With smart sync, ~/.opencli/clis/ only holds files that differ from the
|
|
29
|
+
* package baseline (upstream-synced cache + autofix output + user overrides).
|
|
30
|
+
* Built-in adapters are loaded directly from the installed package.
|
|
28
31
|
*/
|
|
29
32
|
export declare function ensureUserAdapters(): Promise<void>;
|
|
30
33
|
/**
|
package/dist/src/discovery.js
CHANGED
|
@@ -14,7 +14,7 @@ import { fileURLToPath, pathToFileURL } from 'node:url';
|
|
|
14
14
|
import { Strategy, registerCommand } from './registry.js';
|
|
15
15
|
import { getErrorMessage } from './errors.js';
|
|
16
16
|
import { log } from './logger.js';
|
|
17
|
-
import { findPackageRoot, getCliManifestPath
|
|
17
|
+
import { findPackageRoot, getCliManifestPath } from './package-paths.js';
|
|
18
18
|
/** User runtime directory: ~/.opencli */
|
|
19
19
|
export const USER_OPENCLI_DIR = path.join(os.homedir(), '.opencli');
|
|
20
20
|
/** User CLIs directory: ~/.opencli/clis */
|
|
@@ -77,43 +77,15 @@ export async function ensureUserCliCompatShims(baseDir = USER_OPENCLI_DIR) {
|
|
|
77
77
|
log.warn(`Could not create symlink at ${symlinkPath}: ${getErrorMessage(err)}`);
|
|
78
78
|
}
|
|
79
79
|
}
|
|
80
|
-
const ADAPTER_MANIFEST_PATH = path.join(USER_OPENCLI_DIR, 'adapter-manifest.json');
|
|
81
80
|
/**
|
|
82
|
-
*
|
|
83
|
-
*
|
|
81
|
+
* Ensure the user adapters directory exists.
|
|
82
|
+
*
|
|
83
|
+
* With smart sync, ~/.opencli/clis/ only holds files that differ from the
|
|
84
|
+
* package baseline (upstream-synced cache + autofix output + user overrides).
|
|
85
|
+
* Built-in adapters are loaded directly from the installed package.
|
|
84
86
|
*/
|
|
85
87
|
export async function ensureUserAdapters() {
|
|
86
|
-
|
|
87
|
-
try {
|
|
88
|
-
await fs.promises.access(ADAPTER_MANIFEST_PATH);
|
|
89
|
-
return;
|
|
90
|
-
}
|
|
91
|
-
catch {
|
|
92
|
-
// No manifest — first run or postinstall was skipped
|
|
93
|
-
}
|
|
94
|
-
// Check if clis dir has any content (could be manually populated)
|
|
95
|
-
try {
|
|
96
|
-
const entries = await fs.promises.readdir(USER_CLIS_DIR);
|
|
97
|
-
if (entries.length > 0)
|
|
98
|
-
return;
|
|
99
|
-
}
|
|
100
|
-
catch {
|
|
101
|
-
// Dir doesn't exist — needs fetch
|
|
102
|
-
}
|
|
103
|
-
log.info('First run detected — copying adapters (one-time setup)...');
|
|
104
|
-
try {
|
|
105
|
-
const { execFileSync } = await import('node:child_process');
|
|
106
|
-
const scriptPath = getFetchAdaptersScriptPath(PACKAGE_ROOT);
|
|
107
|
-
execFileSync(process.execPath, [scriptPath], {
|
|
108
|
-
stdio: 'inherit',
|
|
109
|
-
env: { ...process.env, _OPENCLI_FIRST_RUN: '1' },
|
|
110
|
-
timeout: 120_000,
|
|
111
|
-
});
|
|
112
|
-
}
|
|
113
|
-
catch (err) {
|
|
114
|
-
log.warn(`Could not fetch adapters on first run: ${getErrorMessage(err)}`);
|
|
115
|
-
log.warn('Built-in adapters from the package will be used.');
|
|
116
|
-
}
|
|
88
|
+
await fs.promises.mkdir(USER_CLIS_DIR, { recursive: true });
|
|
117
89
|
}
|
|
118
90
|
/**
|
|
119
91
|
* Discover and register CLI commands.
|
package/dist/src/doctor.d.ts
CHANGED
package/dist/src/doctor.js
CHANGED
|
@@ -9,7 +9,37 @@ import { BrowserBridge } from './browser/index.js';
|
|
|
9
9
|
import { getDaemonHealth, listSessions } from './browser/daemon-client.js';
|
|
10
10
|
import { getErrorMessage } from './errors.js';
|
|
11
11
|
import { getRuntimeLabel } from './runtime-detect.js';
|
|
12
|
+
import { getCachedLatestExtensionVersion } from './update-check.js';
|
|
12
13
|
const DOCTOR_LIVE_TIMEOUT_SECONDS = 8;
|
|
14
|
+
/** Parse a semver string into [major, minor, patch]. Returns null on invalid input. */
|
|
15
|
+
function parseSemver(v) {
|
|
16
|
+
const parts = v.replace(/^v/, '').split('-')[0].split('.').map(Number);
|
|
17
|
+
if (parts.length < 3 || parts.some(isNaN))
|
|
18
|
+
return null;
|
|
19
|
+
return [parts[0], parts[1], parts[2]];
|
|
20
|
+
}
|
|
21
|
+
/** Returns true if `a` is strictly newer than `b`. */
|
|
22
|
+
function isNewerVersion(a, b) {
|
|
23
|
+
const va = parseSemver(a);
|
|
24
|
+
const vb = parseSemver(b);
|
|
25
|
+
if (!va || !vb)
|
|
26
|
+
return false;
|
|
27
|
+
const cmp = va[0] - vb[0] || va[1] - vb[1] || va[2] - vb[2];
|
|
28
|
+
return cmp > 0;
|
|
29
|
+
}
|
|
30
|
+
/** Check if version satisfies a simple range like ">=1.7.0". */
|
|
31
|
+
function satisfiesRange(version, range) {
|
|
32
|
+
const match = range.match(/^(>=?)\s*(\S+)$/);
|
|
33
|
+
if (!match)
|
|
34
|
+
return true; // Unknown range format — don't block
|
|
35
|
+
const [, op, rangeVer] = match;
|
|
36
|
+
const v = parseSemver(version);
|
|
37
|
+
const r = parseSemver(rangeVer);
|
|
38
|
+
if (!v || !r)
|
|
39
|
+
return true;
|
|
40
|
+
const cmp = v[0] - r[0] || v[1] - r[1] || v[2] - r[2];
|
|
41
|
+
return op === '>=' ? cmp >= 0 : cmp > 0;
|
|
42
|
+
}
|
|
13
43
|
/**
|
|
14
44
|
* Test connectivity by attempting a real browser command.
|
|
15
45
|
*/
|
|
@@ -80,7 +110,16 @@ export async function runBrowserDoctor(opts = {}) {
|
|
|
80
110
|
issues.push(`Browser connectivity test failed: ${connectivity.error ?? 'unknown'}`);
|
|
81
111
|
}
|
|
82
112
|
const extensionVersion = health.status?.extensionVersion;
|
|
83
|
-
|
|
113
|
+
const extensionCompatRange = health.status?.extensionCompatRange;
|
|
114
|
+
if (extensionVersion && opts.cliVersion && extensionCompatRange) {
|
|
115
|
+
if (!satisfiesRange(opts.cliVersion, extensionCompatRange)) {
|
|
116
|
+
issues.push(`CLI version incompatible with extension: extension v${extensionVersion} requires CLI ${extensionCompatRange}, but CLI is v${opts.cliVersion}\n` +
|
|
117
|
+
' Update the CLI: npm install -g @jackwener/opencli\n' +
|
|
118
|
+
' Or download a compatible extension from: https://github.com/jackwener/opencli/releases');
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
else if (extensionVersion && opts.cliVersion) {
|
|
122
|
+
// Fallback for older extensions that don't send compatRange
|
|
84
123
|
const extMajor = extensionVersion.split('.')[0];
|
|
85
124
|
const cliMajor = opts.cliVersion.split('.')[0];
|
|
86
125
|
if (extMajor !== cliMajor) {
|
|
@@ -88,6 +127,12 @@ export async function runBrowserDoctor(opts = {}) {
|
|
|
88
127
|
' Download the latest extension from: https://github.com/jackwener/opencli/releases');
|
|
89
128
|
}
|
|
90
129
|
}
|
|
130
|
+
// Extension update check (from cached background fetch)
|
|
131
|
+
const latestExtensionVersion = getCachedLatestExtensionVersion();
|
|
132
|
+
if (extensionVersion && latestExtensionVersion && isNewerVersion(latestExtensionVersion, extensionVersion)) {
|
|
133
|
+
issues.push(`Extension update available: v${extensionVersion} → v${latestExtensionVersion}\n` +
|
|
134
|
+
' Download from: https://github.com/jackwener/opencli/releases');
|
|
135
|
+
}
|
|
91
136
|
return {
|
|
92
137
|
cliVersion: opts.cliVersion,
|
|
93
138
|
daemonRunning,
|
|
@@ -95,6 +140,7 @@ export async function runBrowserDoctor(opts = {}) {
|
|
|
95
140
|
extensionConnected,
|
|
96
141
|
extensionFlaky,
|
|
97
142
|
extensionVersion,
|
|
143
|
+
latestExtensionVersion,
|
|
98
144
|
connectivity,
|
|
99
145
|
sessions,
|
|
100
146
|
issues,
|
|
@@ -114,7 +160,10 @@ export function renderBrowserDoctorReport(report) {
|
|
|
114
160
|
const extIcon = report.extensionFlaky
|
|
115
161
|
? styleText('yellow', '[WARN]')
|
|
116
162
|
: report.extensionConnected ? styleText('green', '[OK]') : styleText('yellow', '[MISSING]');
|
|
117
|
-
const
|
|
163
|
+
const extUpdateHint = report.extensionVersion && report.latestExtensionVersion && isNewerVersion(report.latestExtensionVersion, report.extensionVersion)
|
|
164
|
+
? styleText('yellow', ` → v${report.latestExtensionVersion} available`)
|
|
165
|
+
: '';
|
|
166
|
+
const extVersion = report.extensionVersion ? styleText('dim', ` (v${report.extensionVersion})`) + extUpdateHint : '';
|
|
118
167
|
const extLabel = report.extensionFlaky
|
|
119
168
|
? 'unstable (connected during live check, then disconnected)'
|
|
120
169
|
: report.extensionConnected ? 'connected' : 'not connected';
|
|
@@ -22,7 +22,7 @@ export const builtinApps = {
|
|
|
22
22
|
bundleId: 'dev.antigravity.app',
|
|
23
23
|
displayName: 'Antigravity',
|
|
24
24
|
},
|
|
25
|
-
chatgpt: { port: 9236, processName: 'ChatGPT', bundleId: 'com.openai.chat', displayName: 'ChatGPT' },
|
|
25
|
+
'chatgpt-app': { port: 9236, processName: 'ChatGPT', bundleId: 'com.openai.chat', displayName: 'ChatGPT' },
|
|
26
26
|
};
|
|
27
27
|
/** Merge builtin + user-defined apps. User entries are additive only. */
|
|
28
28
|
export function loadApps(userApps) {
|
package/dist/src/engine.test.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest';
|
|
2
|
-
import { discoverClis, discoverPlugins, ensureUserCliCompatShims, PLUGINS_DIR } from './discovery.js';
|
|
2
|
+
import { discoverClis, discoverPlugins, ensureUserCliCompatShims, ensureUserAdapters, PLUGINS_DIR } from './discovery.js';
|
|
3
3
|
import { executeCommand } from './execution.js';
|
|
4
4
|
import { getRegistry, cli, Strategy } from './registry.js';
|
|
5
5
|
import { clearAllHooks, onAfterExecute } from './hooks.js';
|
|
@@ -103,6 +103,34 @@ cli({
|
|
|
103
103
|
}
|
|
104
104
|
});
|
|
105
105
|
});
|
|
106
|
+
describe('ensureUserAdapters', () => {
|
|
107
|
+
it('creates user clis directory without triggering full copy', async () => {
|
|
108
|
+
const tempDir = await fs.promises.mkdtemp(path.join(os.tmpdir(), 'opencli-ensure-'));
|
|
109
|
+
const clisDir = path.join(tempDir, 'clis');
|
|
110
|
+
try {
|
|
111
|
+
// Patch USER_CLIS_DIR is not easy, so we test the function behavior indirectly:
|
|
112
|
+
// ensureUserAdapters should not throw and should be very fast (no fetch script)
|
|
113
|
+
const start = Date.now();
|
|
114
|
+
await ensureUserAdapters();
|
|
115
|
+
const elapsed = Date.now() - start;
|
|
116
|
+
// Should complete quickly (< 1s) since it only creates a directory
|
|
117
|
+
expect(elapsed).toBeLessThan(1000);
|
|
118
|
+
}
|
|
119
|
+
finally {
|
|
120
|
+
await fs.promises.rm(tempDir, { recursive: true, force: true });
|
|
121
|
+
}
|
|
122
|
+
});
|
|
123
|
+
it('discoverClis handles empty user directory gracefully', async () => {
|
|
124
|
+
const emptyDir = await fs.promises.mkdtemp(path.join(os.tmpdir(), 'opencli-empty-'));
|
|
125
|
+
try {
|
|
126
|
+
// Should not throw for an empty directory (no adapters to discover)
|
|
127
|
+
await expect(discoverClis(emptyDir)).resolves.not.toThrow();
|
|
128
|
+
}
|
|
129
|
+
finally {
|
|
130
|
+
await fs.promises.rm(emptyDir, { recursive: true, force: true });
|
|
131
|
+
}
|
|
132
|
+
});
|
|
133
|
+
});
|
|
106
134
|
describe('discoverPlugins', () => {
|
|
107
135
|
const testPluginDir = path.join(PLUGINS_DIR, '__test-plugin__');
|
|
108
136
|
const yamlPath = path.join(testPluginDir, 'greeting.yaml');
|
package/dist/src/errors.d.ts
CHANGED
package/dist/src/errors.js
CHANGED
|
@@ -106,8 +106,19 @@ export class PluginError extends CliError {
|
|
|
106
106
|
export function getErrorMessage(error) {
|
|
107
107
|
return error instanceof Error ? error.message : String(error);
|
|
108
108
|
}
|
|
109
|
+
/** Serialize an error cause chain into a readable string. */
|
|
110
|
+
function serializeCause(cause) {
|
|
111
|
+
if (cause instanceof Error) {
|
|
112
|
+
const parts = [cause.message];
|
|
113
|
+
if (cause.cause)
|
|
114
|
+
parts.push(` caused by: ${serializeCause(cause.cause)}`);
|
|
115
|
+
return parts.join('\n');
|
|
116
|
+
}
|
|
117
|
+
return String(cause);
|
|
118
|
+
}
|
|
109
119
|
/** Build an ErrorEnvelope from any caught value. */
|
|
110
120
|
export function toEnvelope(err) {
|
|
121
|
+
const cause = err instanceof Error && err.cause ? serializeCause(err.cause) : undefined;
|
|
111
122
|
if (err instanceof CliError) {
|
|
112
123
|
return {
|
|
113
124
|
ok: false,
|
|
@@ -116,6 +127,7 @@ export function toEnvelope(err) {
|
|
|
116
127
|
message: err.message,
|
|
117
128
|
...(err.hint ? { help: err.hint } : {}),
|
|
118
129
|
exitCode: err.exitCode,
|
|
130
|
+
...(cause ? { cause } : {}),
|
|
119
131
|
},
|
|
120
132
|
};
|
|
121
133
|
}
|
|
@@ -126,6 +138,7 @@ export function toEnvelope(err) {
|
|
|
126
138
|
code: 'UNKNOWN',
|
|
127
139
|
message: msg,
|
|
128
140
|
exitCode: EXIT_CODES.GENERIC_ERROR,
|
|
141
|
+
...(cause ? { cause } : {}),
|
|
129
142
|
},
|
|
130
143
|
};
|
|
131
144
|
}
|
package/dist/src/execution.js
CHANGED
|
@@ -11,16 +11,20 @@
|
|
|
11
11
|
*/
|
|
12
12
|
import { getRegistry, fullName } from './registry.js';
|
|
13
13
|
import { pathToFileURL } from 'node:url';
|
|
14
|
+
import * as fs from 'node:fs';
|
|
15
|
+
import * as os from 'node:os';
|
|
14
16
|
import { executePipeline } from './pipeline/index.js';
|
|
15
17
|
import { AdapterLoadError, ArgumentError, CommandExecutionError, getErrorMessage } from './errors.js';
|
|
16
18
|
import { isDiagnosticEnabled, collectDiagnostic, emitDiagnostic } from './diagnostic.js';
|
|
17
19
|
import { shouldUseBrowserSession } from './capabilityRouting.js';
|
|
18
20
|
import { getBrowserFactory, browserSession, runWithTimeout, DEFAULT_BROWSER_COMMAND_TIMEOUT } from './runtime.js';
|
|
19
21
|
import { emitHook } from './hooks.js';
|
|
20
|
-
import { log } from './logger.js';
|
|
21
22
|
import { isElectronApp } from './electron-apps.js';
|
|
22
23
|
import { probeCDP, resolveElectronEndpoint } from './launcher.js';
|
|
23
|
-
const _loadedModules = new
|
|
24
|
+
const _loadedModules = new Map();
|
|
25
|
+
/** Track mtime of loaded user adapter files for hot-reload in daemon mode. */
|
|
26
|
+
const _moduleMtimes = new Map();
|
|
27
|
+
const _userClisDir = `${os.homedir()}/.opencli/clis/`;
|
|
24
28
|
export function coerceAndValidateArgs(cmdArgs, kwargs) {
|
|
25
29
|
const result = { ...kwargs };
|
|
26
30
|
for (const argDef of cmdArgs) {
|
|
@@ -67,15 +71,34 @@ async function runCommand(cmd, page, kwargs, debug) {
|
|
|
67
71
|
const internal = cmd;
|
|
68
72
|
if (internal._lazy && internal._modulePath) {
|
|
69
73
|
const modulePath = internal._modulePath;
|
|
70
|
-
if
|
|
74
|
+
// Hot-reload: if a user adapter's file has changed on disk, invalidate cache
|
|
75
|
+
const isUserAdapter = modulePath.startsWith(_userClisDir);
|
|
76
|
+
if (isUserAdapter && _loadedModules.has(modulePath)) {
|
|
71
77
|
try {
|
|
72
|
-
|
|
73
|
-
|
|
78
|
+
const stat = fs.statSync(modulePath);
|
|
79
|
+
const prevMtime = _moduleMtimes.get(modulePath);
|
|
80
|
+
if (prevMtime !== undefined && stat.mtimeMs !== prevMtime) {
|
|
81
|
+
_loadedModules.delete(modulePath);
|
|
82
|
+
_moduleMtimes.delete(modulePath);
|
|
83
|
+
}
|
|
74
84
|
}
|
|
75
|
-
catch
|
|
85
|
+
catch { /* file may have been deleted; let import below handle it */ }
|
|
86
|
+
}
|
|
87
|
+
if (!_loadedModules.has(modulePath)) {
|
|
88
|
+
const url = pathToFileURL(modulePath).href;
|
|
89
|
+
const importUrl = _moduleMtimes.has(modulePath) ? `${url}?t=${Date.now()}` : url;
|
|
90
|
+
const loadPromise = import(importUrl).then(() => {
|
|
91
|
+
try {
|
|
92
|
+
_moduleMtimes.set(modulePath, fs.statSync(modulePath).mtimeMs);
|
|
93
|
+
}
|
|
94
|
+
catch { }
|
|
95
|
+
}, (err) => {
|
|
96
|
+
_loadedModules.delete(modulePath);
|
|
76
97
|
throw new AdapterLoadError(`Failed to load adapter module ${modulePath}: ${getErrorMessage(err)}`, 'Check that the adapter file exists and has no syntax errors.');
|
|
77
|
-
}
|
|
98
|
+
});
|
|
99
|
+
_loadedModules.set(modulePath, loadPromise);
|
|
78
100
|
}
|
|
101
|
+
await _loadedModules.get(modulePath);
|
|
79
102
|
const updated = getRegistry().get(fullName(cmd));
|
|
80
103
|
if (updated?.func) {
|
|
81
104
|
if (!page && updated.browser !== false) {
|
|
@@ -159,7 +182,7 @@ export async function executeCommand(cmd, rawKwargs, debug = false, opts = {}) {
|
|
|
159
182
|
await page.goto(preNavUrl);
|
|
160
183
|
}
|
|
161
184
|
catch (err) {
|
|
162
|
-
|
|
185
|
+
throw new CommandExecutionError(`Pre-navigation to ${preNavUrl} failed: ${err instanceof Error ? err.message : err}`, 'Check that the site is reachable and the browser extension is running.');
|
|
163
186
|
}
|
|
164
187
|
}
|
|
165
188
|
try {
|
|
@@ -173,13 +196,17 @@ export async function executeCommand(cmd, rawKwargs, debug = false, opts = {}) {
|
|
|
173
196
|
return result;
|
|
174
197
|
}
|
|
175
198
|
catch (err) {
|
|
176
|
-
// Collect diagnostic while page is still alive (before
|
|
199
|
+
// Collect diagnostic while page is still alive (before closing the window).
|
|
177
200
|
if (isDiagnosticEnabled()) {
|
|
178
201
|
const internal = cmd;
|
|
179
202
|
const ctx = await collectDiagnostic(err, internal, page);
|
|
180
203
|
emitDiagnostic(ctx);
|
|
181
204
|
diagnosticEmitted = true;
|
|
182
205
|
}
|
|
206
|
+
// Close the automation window on failure too — without this, the window
|
|
207
|
+
// lingers until the extension's idle timer fires (unreliable on Windows
|
|
208
|
+
// where MV3 service workers may be suspended before setTimeout triggers).
|
|
209
|
+
await page.closeWindow?.().catch(() => { });
|
|
183
210
|
throw err;
|
|
184
211
|
}
|
|
185
212
|
}, { workspace: `site:${cmd.site}`, cdpEndpoint });
|
|
@@ -3,6 +3,8 @@ import { executeCommand, prepareCommandArgs } from './execution.js';
|
|
|
3
3
|
import { TimeoutError } from './errors.js';
|
|
4
4
|
import { cli, Strategy } from './registry.js';
|
|
5
5
|
import { withTimeoutMs } from './runtime.js';
|
|
6
|
+
import * as runtime from './runtime.js';
|
|
7
|
+
import * as capRouting from './capabilityRouting.js';
|
|
6
8
|
describe('executeCommand — non-browser timeout', () => {
|
|
7
9
|
it('applies timeoutSeconds to non-browser commands', async () => {
|
|
8
10
|
const cmd = cli({
|
|
@@ -37,6 +39,27 @@ describe('executeCommand — non-browser timeout', () => {
|
|
|
37
39
|
// With timeout guard skipped, the sentinel fires instead.
|
|
38
40
|
await expect(withTimeoutMs(executeCommand(cmd, {}), 50, 'sentinel timeout')).rejects.toThrow('sentinel timeout');
|
|
39
41
|
});
|
|
42
|
+
it('calls closeWindow on browser command failure', async () => {
|
|
43
|
+
const closeWindow = vi.fn().mockResolvedValue(undefined);
|
|
44
|
+
const mockPage = { closeWindow };
|
|
45
|
+
// Mock shouldUseBrowserSession to return true
|
|
46
|
+
vi.spyOn(capRouting, 'shouldUseBrowserSession').mockReturnValue(true);
|
|
47
|
+
// Mock browserSession to invoke the callback with our mock page
|
|
48
|
+
vi.spyOn(runtime, 'browserSession').mockImplementation(async (_Factory, fn) => {
|
|
49
|
+
return fn(mockPage);
|
|
50
|
+
});
|
|
51
|
+
const cmd = cli({
|
|
52
|
+
site: 'test-execution',
|
|
53
|
+
name: 'browser-close-on-error',
|
|
54
|
+
description: 'test closeWindow on failure',
|
|
55
|
+
browser: true,
|
|
56
|
+
strategy: Strategy.PUBLIC,
|
|
57
|
+
func: async () => { throw new Error('adapter failure'); },
|
|
58
|
+
});
|
|
59
|
+
await expect(executeCommand(cmd, {})).rejects.toThrow('adapter failure');
|
|
60
|
+
expect(closeWindow).toHaveBeenCalledTimes(1);
|
|
61
|
+
vi.restoreAllMocks();
|
|
62
|
+
});
|
|
40
63
|
it('does not re-run custom validation when args are already prepared', async () => {
|
|
41
64
|
const validateArgs = vi.fn();
|
|
42
65
|
const cmd = {
|
package/dist/src/logger.d.ts
CHANGED
|
@@ -15,9 +15,9 @@ export declare const log: {
|
|
|
15
15
|
warn(msg: string): void;
|
|
16
16
|
/** Error (always shown) */
|
|
17
17
|
error(msg: string): void;
|
|
18
|
-
/** Verbose output (
|
|
18
|
+
/** Verbose output (shown when -v flag, OPENCLI_VERBOSE, or DEBUG=opencli is set) */
|
|
19
19
|
verbose(msg: string): void;
|
|
20
|
-
/**
|
|
20
|
+
/** @deprecated Use log.verbose() instead. Kept as alias for backward compatibility. */
|
|
21
21
|
debug(msg: string): void;
|
|
22
22
|
/** Step-style debug (for pipeline steps, etc.) */
|
|
23
23
|
step(stepNum: number, total: number, op: string, preview?: string): void;
|
package/dist/src/logger.js
CHANGED
|
@@ -6,10 +6,7 @@
|
|
|
6
6
|
*/
|
|
7
7
|
import { styleText } from 'node:util';
|
|
8
8
|
function isVerbose() {
|
|
9
|
-
return !!process.env.OPENCLI_VERBOSE;
|
|
10
|
-
}
|
|
11
|
-
function isDebug() {
|
|
12
|
-
return !!process.env.DEBUG?.includes('opencli');
|
|
9
|
+
return !!process.env.OPENCLI_VERBOSE || !!process.env.DEBUG?.includes('opencli');
|
|
13
10
|
}
|
|
14
11
|
export const log = {
|
|
15
12
|
/** Informational message (always shown) */
|
|
@@ -32,17 +29,15 @@ export const log = {
|
|
|
32
29
|
error(msg) {
|
|
33
30
|
process.stderr.write(`${styleText('red', '✖')} ${msg}\n`);
|
|
34
31
|
},
|
|
35
|
-
/** Verbose output (
|
|
32
|
+
/** Verbose output (shown when -v flag, OPENCLI_VERBOSE, or DEBUG=opencli is set) */
|
|
36
33
|
verbose(msg) {
|
|
37
34
|
if (isVerbose()) {
|
|
38
35
|
process.stderr.write(`${styleText('dim', '[verbose]')} ${msg}\n`);
|
|
39
36
|
}
|
|
40
37
|
},
|
|
41
|
-
/**
|
|
38
|
+
/** @deprecated Use log.verbose() instead. Kept as alias for backward compatibility. */
|
|
42
39
|
debug(msg) {
|
|
43
|
-
|
|
44
|
-
process.stderr.write(`${styleText('dim', '[debug]')} ${msg}\n`);
|
|
45
|
-
}
|
|
40
|
+
this.verbose(msg);
|
|
46
41
|
},
|
|
47
42
|
/** Step-style debug (for pipeline steps, etc.) */
|
|
48
43
|
step(stepNum, total, op, preview = '') {
|
package/dist/src/main.js
CHANGED
|
@@ -45,14 +45,15 @@ if (argv[0] === 'completion' && argv.length >= 2) {
|
|
|
45
45
|
// Fast path: --get-completions — read from manifest, skip discovery
|
|
46
46
|
const getCompIdx = process.argv.indexOf('--get-completions');
|
|
47
47
|
if (getCompIdx !== -1) {
|
|
48
|
-
// Only
|
|
49
|
-
//
|
|
48
|
+
// Only include manifests that actually exist on disk.
|
|
49
|
+
// With sparse override, the user clis dir may exist but have no manifest.
|
|
50
50
|
const manifestPaths = [getCliManifestPath(BUILTIN_CLIS)];
|
|
51
|
+
const userManifest = getCliManifestPath(USER_CLIS);
|
|
51
52
|
try {
|
|
52
|
-
fs.accessSync(
|
|
53
|
-
manifestPaths.push(
|
|
53
|
+
fs.accessSync(userManifest);
|
|
54
|
+
manifestPaths.push(userManifest);
|
|
54
55
|
}
|
|
55
|
-
catch { /* no user
|
|
56
|
+
catch { /* no user manifest */ }
|
|
56
57
|
if (hasAllManifests(manifestPaths)) {
|
|
57
58
|
const rest = process.argv.slice(getCompIdx + 1);
|
|
58
59
|
let cursor;
|