@jackwener/opencli 1.7.2 → 1.7.4
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 +18 -15
- package/README.zh-CN.md +31 -15
- package/cli-manifest.json +1265 -101
- 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/favorite.js +18 -13
- package/clis/bilibili/feed.js +202 -48
- package/clis/binance/depth.js +3 -4
- package/clis/boss/utils.js +2 -2
- 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}/ax.js +6 -3
- 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/search.js +1 -0
- package/clis/douban/search.test.js +11 -0
- package/clis/douban/subject.js +20 -93
- package/clis/douban/subject.test.js +11 -0
- package/clis/douban/utils.js +279 -10
- package/clis/douban/utils.test.js +296 -1
- package/clis/doubao/utils.js +319 -130
- package/clis/doubao/utils.test.js +241 -2
- package/clis/eastmoney/hot-rank.js +50 -0
- package/clis/eastmoney/hot-rank.test.js +59 -0
- package/clis/grok/image.test.ts +107 -0
- package/clis/grok/image.ts +356 -0
- 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/tdx/hot-rank.js +47 -0
- package/clis/tdx/hot-rank.test.js +59 -0
- package/clis/ths/hot-rank.js +49 -0
- package/clis/ths/hot-rank.test.js +64 -0
- package/clis/twitter/bookmarks.js +2 -1
- package/clis/uiverse/_shared.js +368 -0
- package/clis/uiverse/_shared.test.js +55 -0
- package/clis/uiverse/code.js +47 -0
- package/clis/uiverse/preview.js +71 -0
- package/clis/xiaohongshu/comments.js +20 -8
- package/clis/xiaohongshu/comments.test.js +69 -12
- 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 +18 -7
- package/clis/xiaohongshu/download.test.js +42 -0
- package/clis/xiaohongshu/navigation.test.js +34 -0
- package/clis/xiaohongshu/note-helpers.js +46 -12
- package/clis/xiaohongshu/note.js +17 -10
- package/clis/xiaohongshu/note.test.js +66 -11
- package/clis/xiaohongshu/publish.js +1 -0
- package/clis/xiaohongshu/search.js +1 -0
- package/clis/xiaohongshu/user.js +1 -0
- package/clis/xiaoyuzhou/auth.js +303 -0
- package/clis/xiaoyuzhou/auth.test.js +124 -0
- package/clis/xiaoyuzhou/download.js +49 -0
- package/clis/xiaoyuzhou/download.test.js +125 -0
- package/clis/xiaoyuzhou/transcript.js +76 -0
- package/clis/xiaoyuzhou/transcript.test.js +195 -0
- package/clis/yahoo-finance/quote.js +1 -1
- package/clis/youtube/feed.js +120 -0
- package/clis/youtube/history.js +118 -0
- package/clis/youtube/like.js +62 -0
- package/clis/youtube/playlist.js +97 -0
- package/clis/youtube/subscribe.js +71 -0
- package/clis/youtube/subscriptions.js +57 -0
- package/clis/youtube/unlike.js +62 -0
- package/clis/youtube/unsubscribe.js +71 -0
- package/clis/youtube/utils.js +122 -0
- package/clis/youtube/utils.test.js +32 -1
- package/clis/youtube/watch-later.js +76 -0
- package/dist/src/browser/base-page.d.ts +9 -0
- package/dist/src/browser/base-page.js +44 -5
- package/dist/src/browser/bridge.d.ts +2 -0
- package/dist/src/browser/bridge.js +51 -14
- package/dist/src/browser/cdp.js +11 -2
- package/dist/src/browser/daemon-client.d.ts +2 -0
- package/dist/src/browser/dom-snapshot.js +13 -1
- package/dist/src/browser/page.d.ts +4 -1
- package/dist/src/browser/page.js +48 -8
- package/dist/src/browser/page.test.js +61 -1
- package/dist/src/browser/target-errors.d.ts +23 -0
- package/dist/src/browser/target-errors.js +29 -0
- package/dist/src/browser/target-errors.test.d.ts +1 -0
- package/dist/src/browser/target-errors.test.js +61 -0
- package/dist/src/browser/target-resolver.d.ts +57 -0
- package/dist/src/browser/target-resolver.js +298 -0
- package/dist/src/browser/target-resolver.test.d.ts +1 -0
- package/dist/src/browser/target-resolver.test.js +43 -0
- package/dist/src/browser.test.js +38 -1
- package/dist/src/cli.js +45 -35
- package/dist/src/commands/daemon.d.ts +4 -2
- package/dist/src/commands/daemon.js +22 -2
- package/dist/src/commands/daemon.test.js +65 -2
- package/dist/src/daemon.js +7 -0
- package/dist/src/doctor.d.ts +2 -0
- package/dist/src/doctor.js +82 -10
- package/dist/src/doctor.test.js +28 -12
- package/dist/src/electron-apps.js +1 -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/external-clis.yaml +2 -2
- package/dist/src/logger.d.ts +2 -2
- package/dist/src/logger.js +3 -8
- package/dist/src/output.js +1 -5
- package/dist/src/output.test.js +0 -21
- package/dist/src/pipeline/steps/transform.js +1 -1
- package/dist/src/pipeline/template.d.ts +1 -0
- package/dist/src/pipeline/template.js +11 -3
- package/dist/src/pipeline/template.test.js +3 -0
- package/dist/src/pipeline/transform.test.js +14 -0
- package/dist/src/plugin.d.ts +7 -1
- package/dist/src/plugin.js +23 -1
- package/dist/src/plugin.test.js +15 -1
- package/dist/src/registry.js +3 -4
- package/dist/src/types.d.ts +3 -1
- package/dist/src/update-check.d.ts +14 -0
- package/dist/src/update-check.js +48 -3
- package/dist/src/update-check.test.d.ts +1 -0
- package/dist/src/update-check.test.js +31 -0
- package/package.json +1 -1
- package/scripts/fetch-adapters.js +35 -8
|
@@ -26,6 +26,7 @@ export function evalExpr(expr, ctx) {
|
|
|
26
26
|
const args = ctx.args ?? {};
|
|
27
27
|
const item = ctx.item ?? {};
|
|
28
28
|
const data = ctx.data;
|
|
29
|
+
const root = ctx.root;
|
|
29
30
|
const index = ctx.index ?? 0;
|
|
30
31
|
// ── Pipe filters: expr | filter1(arg) | filter2 ──
|
|
31
32
|
// Split on single | (not ||) so "item.a || item.b | upper" works correctly.
|
|
@@ -45,12 +46,12 @@ export function evalExpr(expr, ctx) {
|
|
|
45
46
|
if (/^\d+(\.\d+)?$/.test(expr))
|
|
46
47
|
return Number(expr);
|
|
47
48
|
// Try resolving as a simple dotted path (item.foo.bar, args.limit, index)
|
|
48
|
-
const resolved = resolvePath(expr, { args, item, data, index });
|
|
49
|
+
const resolved = resolvePath(expr, { args, item, data, root, index });
|
|
49
50
|
if (resolved !== null && resolved !== undefined)
|
|
50
51
|
return resolved;
|
|
51
52
|
// Fallback: evaluate as JS in a sandboxed VM.
|
|
52
53
|
// Handles ||, ??, arithmetic, ternary, method calls, etc. natively.
|
|
53
|
-
return evalJsExpr(expr, { args, item, data, index });
|
|
54
|
+
return evalJsExpr(expr, { args, item, data, root, index });
|
|
54
55
|
}
|
|
55
56
|
/**
|
|
56
57
|
* Apply a named filter to a value.
|
|
@@ -143,6 +144,7 @@ export function resolvePath(pathStr, ctx) {
|
|
|
143
144
|
const args = ctx.args ?? {};
|
|
144
145
|
const item = ctx.item ?? {};
|
|
145
146
|
const data = ctx.data;
|
|
147
|
+
const root = ctx.root;
|
|
146
148
|
const index = ctx.index ?? 0;
|
|
147
149
|
const parts = pathStr.split('.');
|
|
148
150
|
const rootName = parts[0];
|
|
@@ -160,6 +162,10 @@ export function resolvePath(pathStr, ctx) {
|
|
|
160
162
|
obj = data;
|
|
161
163
|
rest = parts.slice(1);
|
|
162
164
|
}
|
|
165
|
+
else if (rootName === 'root') {
|
|
166
|
+
obj = root;
|
|
167
|
+
rest = parts.slice(1);
|
|
168
|
+
}
|
|
163
169
|
else if (rootName === 'index')
|
|
164
170
|
return index;
|
|
165
171
|
else {
|
|
@@ -261,6 +267,7 @@ function getReusableContext() {
|
|
|
261
267
|
args: {},
|
|
262
268
|
item: {},
|
|
263
269
|
data: null,
|
|
270
|
+
root: null,
|
|
264
271
|
index: 0,
|
|
265
272
|
encodeURIComponent,
|
|
266
273
|
decodeURIComponent,
|
|
@@ -279,7 +286,7 @@ function getReusableContext() {
|
|
|
279
286
|
}
|
|
280
287
|
/** Properties that are part of the sandbox's initial shape and safe to keep. */
|
|
281
288
|
const SANDBOX_WHITELIST = new Set([
|
|
282
|
-
'args', 'item', 'data', 'index',
|
|
289
|
+
'args', 'item', 'data', 'root', 'index',
|
|
283
290
|
'encodeURIComponent', 'decodeURIComponent',
|
|
284
291
|
'JSON', 'Math', 'Number', 'String', 'Boolean', 'Array', 'Date',
|
|
285
292
|
]);
|
|
@@ -304,6 +311,7 @@ function evalJsExpr(expr, ctx) {
|
|
|
304
311
|
sandbox.args = sanitizeContext(ctx.args ?? {});
|
|
305
312
|
sandbox.item = sanitizeContext(ctx.item ?? {});
|
|
306
313
|
sandbox.data = sanitizeContext(ctx.data);
|
|
314
|
+
sandbox.root = sanitizeContext(ctx.root);
|
|
307
315
|
sandbox.index = ctx.index ?? 0;
|
|
308
316
|
return script.runInContext(context, { timeout: 50 });
|
|
309
317
|
}
|
|
@@ -22,6 +22,9 @@ describe('resolvePath', () => {
|
|
|
22
22
|
it('resolves data path', () => {
|
|
23
23
|
expect(resolvePath('data.items', { data: { items: [1, 2, 3] } })).toEqual([1, 2, 3]);
|
|
24
24
|
});
|
|
25
|
+
it('resolves root path', () => {
|
|
26
|
+
expect(resolvePath('root.items', { root: { items: [1, 2, 3] } })).toEqual([1, 2, 3]);
|
|
27
|
+
});
|
|
25
28
|
it('returns null for missing path', () => {
|
|
26
29
|
expect(resolvePath('args.missing', { args: {} })).toBeUndefined();
|
|
27
30
|
});
|
|
@@ -60,6 +60,20 @@ describe('stepMap', () => {
|
|
|
60
60
|
{ title: 'Two', rank: 2 },
|
|
61
61
|
]);
|
|
62
62
|
});
|
|
63
|
+
it('keeps data bound to the selected source and exposes root separately', async () => {
|
|
64
|
+
const result = await stepMap(null, {
|
|
65
|
+
select: 'bids',
|
|
66
|
+
bid_price: '${{ data[index][0] }}',
|
|
67
|
+
ask_price: '${{ root.asks[index][0] }}',
|
|
68
|
+
}, {
|
|
69
|
+
bids: [['100', '2'], ['99', '3']],
|
|
70
|
+
asks: [['101', '1'], ['102', '4']],
|
|
71
|
+
}, {});
|
|
72
|
+
expect(result).toEqual([
|
|
73
|
+
{ bid_price: '100', ask_price: '101' },
|
|
74
|
+
{ bid_price: '99', ask_price: '102' },
|
|
75
|
+
]);
|
|
76
|
+
});
|
|
63
77
|
});
|
|
64
78
|
describe('stepFilter', () => {
|
|
65
79
|
it('filters by expression', async () => {
|
package/dist/src/plugin.d.ts
CHANGED
|
@@ -86,7 +86,13 @@ export declare function getCommitHash(dir: string): string | undefined;
|
|
|
86
86
|
export declare function validatePluginStructure(pluginDir: string): ValidationResult;
|
|
87
87
|
declare function installDependencies(dir: string): void;
|
|
88
88
|
/**
|
|
89
|
-
* Monorepo lifecycle: install shared deps
|
|
89
|
+
* Monorepo lifecycle: install shared deps at repo root, then install and finalize each sub-plugin.
|
|
90
|
+
*
|
|
91
|
+
* The root install covers monorepos that use npm workspaces to hoist dependencies.
|
|
92
|
+
* For monorepos that do NOT use workspaces, sub-plugins may declare their own
|
|
93
|
+
* production dependencies in their package.json. We install those per sub-plugin
|
|
94
|
+
* so that runtime imports (e.g. `undici`) can be resolved from the sub-plugin
|
|
95
|
+
* directory. When the root already satisfies all deps this is a fast no-op.
|
|
90
96
|
*/
|
|
91
97
|
declare function postInstallMonorepoLifecycle(repoDir: string, pluginDirs: string[]): void;
|
|
92
98
|
/**
|
package/dist/src/plugin.js
CHANGED
|
@@ -492,6 +492,19 @@ export function validatePluginStructure(pluginDir) {
|
|
|
492
492
|
}
|
|
493
493
|
return { valid: errors.length === 0, errors };
|
|
494
494
|
}
|
|
495
|
+
/** Check whether a directory has its own production dependencies in package.json. */
|
|
496
|
+
function hasOwnDependencies(dir) {
|
|
497
|
+
const pkgPath = path.join(dir, 'package.json');
|
|
498
|
+
if (!fs.existsSync(pkgPath))
|
|
499
|
+
return false;
|
|
500
|
+
try {
|
|
501
|
+
const pkg = JSON.parse(fs.readFileSync(pkgPath, 'utf-8'));
|
|
502
|
+
return pkg.dependencies != null && Object.keys(pkg.dependencies).length > 0;
|
|
503
|
+
}
|
|
504
|
+
catch {
|
|
505
|
+
return false;
|
|
506
|
+
}
|
|
507
|
+
}
|
|
495
508
|
function installDependencies(dir) {
|
|
496
509
|
const pkgJsonPath = path.join(dir, 'package.json');
|
|
497
510
|
if (!fs.existsSync(pkgJsonPath))
|
|
@@ -523,11 +536,20 @@ function postInstallLifecycle(pluginDir) {
|
|
|
523
536
|
finalizePluginRuntime(pluginDir);
|
|
524
537
|
}
|
|
525
538
|
/**
|
|
526
|
-
* Monorepo lifecycle: install shared deps
|
|
539
|
+
* Monorepo lifecycle: install shared deps at repo root, then install and finalize each sub-plugin.
|
|
540
|
+
*
|
|
541
|
+
* The root install covers monorepos that use npm workspaces to hoist dependencies.
|
|
542
|
+
* For monorepos that do NOT use workspaces, sub-plugins may declare their own
|
|
543
|
+
* production dependencies in their package.json. We install those per sub-plugin
|
|
544
|
+
* so that runtime imports (e.g. `undici`) can be resolved from the sub-plugin
|
|
545
|
+
* directory. When the root already satisfies all deps this is a fast no-op.
|
|
527
546
|
*/
|
|
528
547
|
function postInstallMonorepoLifecycle(repoDir, pluginDirs) {
|
|
529
548
|
installDependencies(repoDir);
|
|
530
549
|
for (const pluginDir of pluginDirs) {
|
|
550
|
+
if (pluginDir !== repoDir && hasOwnDependencies(pluginDir)) {
|
|
551
|
+
installDependencies(pluginDir);
|
|
552
|
+
}
|
|
531
553
|
finalizePluginRuntime(pluginDir);
|
|
532
554
|
}
|
|
533
555
|
}
|
package/dist/src/plugin.test.js
CHANGED
|
@@ -545,13 +545,27 @@ describe('postInstallMonorepoLifecycle', () => {
|
|
|
545
545
|
afterEach(() => {
|
|
546
546
|
fs.rmSync(repoDir, { recursive: true, force: true });
|
|
547
547
|
});
|
|
548
|
-
it('installs dependencies
|
|
548
|
+
it('installs dependencies at the monorepo root and skips sub-plugins without own dependencies', () => {
|
|
549
549
|
_postInstallMonorepoLifecycle(repoDir, [subDir]);
|
|
550
550
|
const npmCalls = mockExecFileSync.mock.calls.filter(([cmd, args]) => cmd === 'npm' && Array.isArray(args) && args[0] === 'install');
|
|
551
551
|
expect(npmCalls).toHaveLength(1);
|
|
552
552
|
expect(npmCalls[0][2]).toMatchObject({ cwd: repoDir });
|
|
553
553
|
expect(npmCalls.some(([, , opts]) => opts?.cwd === subDir)).toBe(false);
|
|
554
554
|
});
|
|
555
|
+
it('also installs dependencies in sub-plugins that declare their own production dependencies', () => {
|
|
556
|
+
// Give the sub-plugin its own production dependencies
|
|
557
|
+
fs.writeFileSync(path.join(subDir, 'package.json'), JSON.stringify({
|
|
558
|
+
name: 'opencli-plugin-alpha',
|
|
559
|
+
version: '1.0.0',
|
|
560
|
+
type: 'module',
|
|
561
|
+
dependencies: { undici: '^8.0.0' },
|
|
562
|
+
}));
|
|
563
|
+
_postInstallMonorepoLifecycle(repoDir, [subDir]);
|
|
564
|
+
const npmCalls = mockExecFileSync.mock.calls.filter(([cmd, args]) => cmd === 'npm' && Array.isArray(args) && args[0] === 'install');
|
|
565
|
+
expect(npmCalls).toHaveLength(2);
|
|
566
|
+
expect(npmCalls[0][2]).toMatchObject({ cwd: repoDir });
|
|
567
|
+
expect(npmCalls[1][2]).toMatchObject({ cwd: subDir });
|
|
568
|
+
});
|
|
555
569
|
});
|
|
556
570
|
describe('updateAllPlugins', () => {
|
|
557
571
|
const testDirA = path.join(PLUGINS_DIR, 'plugin-a');
|
package/dist/src/registry.js
CHANGED
|
@@ -77,10 +77,9 @@ export function registerCommand(cmd) {
|
|
|
77
77
|
const normalized = normalizeCommand(cmd);
|
|
78
78
|
const canonicalKey = fullName(normalized);
|
|
79
79
|
const existing = _registry.get(canonicalKey);
|
|
80
|
-
if (existing) {
|
|
81
|
-
for (const
|
|
82
|
-
|
|
83
|
-
_registry.delete(key);
|
|
80
|
+
if (existing?.aliases) {
|
|
81
|
+
for (const alias of existing.aliases) {
|
|
82
|
+
_registry.delete(`${existing.site}/${alias}`);
|
|
84
83
|
}
|
|
85
84
|
}
|
|
86
85
|
const aliases = normalizeAliases(normalized.aliases, normalized.name);
|
package/dist/src/types.d.ts
CHANGED
|
@@ -44,6 +44,8 @@ export interface IPage {
|
|
|
44
44
|
settleMs?: number;
|
|
45
45
|
}): Promise<void>;
|
|
46
46
|
evaluate(js: string): Promise<any>;
|
|
47
|
+
/** Safely evaluate JS with pre-serialized arguments — prevents injection. */
|
|
48
|
+
evaluateWithArgs?(js: string, args: Record<string, unknown>): Promise<any>;
|
|
47
49
|
getCookies(opts?: {
|
|
48
50
|
domain?: string;
|
|
49
51
|
url?: string;
|
|
@@ -70,7 +72,7 @@ export interface IPage {
|
|
|
70
72
|
getInterceptedRequests(): Promise<any[]>;
|
|
71
73
|
waitForCapture(timeout?: number): Promise<void>;
|
|
72
74
|
screenshot(options?: ScreenshotOptions): Promise<string>;
|
|
73
|
-
startNetworkCapture?(pattern?: string): Promise<
|
|
75
|
+
startNetworkCapture?(pattern?: string): Promise<boolean>;
|
|
74
76
|
readNetworkCapture?(): Promise<unknown[]>;
|
|
75
77
|
/**
|
|
76
78
|
* Set local file paths on a file input element via CDP DOM.setFileInputFiles.
|
|
@@ -8,6 +8,13 @@
|
|
|
8
8
|
* - Notice appears AFTER command output, not before (same as npm/gh/yarn)
|
|
9
9
|
* - Never delays or blocks the CLI command
|
|
10
10
|
*/
|
|
11
|
+
interface GitHubReleaseAsset {
|
|
12
|
+
name: string;
|
|
13
|
+
}
|
|
14
|
+
interface GitHubRelease {
|
|
15
|
+
tag_name: string;
|
|
16
|
+
assets?: GitHubReleaseAsset[];
|
|
17
|
+
}
|
|
11
18
|
/**
|
|
12
19
|
* Register a process exit hook that prints an update notice if a newer
|
|
13
20
|
* version was found on the last background check.
|
|
@@ -15,8 +22,15 @@
|
|
|
15
22
|
* Skipped during --get-completions to avoid polluting shell completion output.
|
|
16
23
|
*/
|
|
17
24
|
export declare function registerUpdateNoticeOnExit(): void;
|
|
25
|
+
declare function extractLatestExtensionVersionFromReleases(releases: GitHubRelease[]): string | undefined;
|
|
18
26
|
/**
|
|
19
27
|
* Kick off a background fetch to npm registry. Writes to cache for next run.
|
|
20
28
|
* Fully non-blocking — never awaited.
|
|
21
29
|
*/
|
|
22
30
|
export declare function checkForUpdateBackground(): void;
|
|
31
|
+
/**
|
|
32
|
+
* Get the cached latest extension version (if available).
|
|
33
|
+
* Used by `opencli doctor` to report extension updates.
|
|
34
|
+
*/
|
|
35
|
+
export declare function getCachedLatestExtensionVersion(): string | undefined;
|
|
36
|
+
export { extractLatestExtensionVersionFromReleases as _extractLatestExtensionVersionFromReleases, };
|
package/dist/src/update-check.js
CHANGED
|
@@ -17,6 +17,7 @@ const CACHE_DIR = path.join(os.homedir(), '.opencli');
|
|
|
17
17
|
const CACHE_FILE = path.join(CACHE_DIR, 'update-check.json');
|
|
18
18
|
const CHECK_INTERVAL_MS = 24 * 60 * 60 * 1000; // 24h
|
|
19
19
|
const NPM_REGISTRY_URL = 'https://registry.npmjs.org/@jackwener/opencli/latest';
|
|
20
|
+
const GITHUB_RELEASES_URL = 'https://api.github.com/repos/jackwener/OpenCLI/releases?per_page=20';
|
|
20
21
|
// Read cache once at module load — shared by both exported functions
|
|
21
22
|
const _cache = (() => {
|
|
22
23
|
try {
|
|
@@ -26,10 +27,13 @@ const _cache = (() => {
|
|
|
26
27
|
return null;
|
|
27
28
|
}
|
|
28
29
|
})();
|
|
29
|
-
function writeCache(latestVersion) {
|
|
30
|
+
function writeCache(latestVersion, latestExtensionVersion) {
|
|
30
31
|
try {
|
|
31
32
|
fs.mkdirSync(CACHE_DIR, { recursive: true });
|
|
32
|
-
|
|
33
|
+
const data = { lastCheck: Date.now(), latestVersion };
|
|
34
|
+
if (latestExtensionVersion)
|
|
35
|
+
data.latestExtensionVersion = latestExtensionVersion;
|
|
36
|
+
fs.writeFileSync(CACHE_FILE, JSON.stringify(data), 'utf-8');
|
|
33
37
|
}
|
|
34
38
|
catch {
|
|
35
39
|
// Best-effort; never fail
|
|
@@ -80,6 +84,38 @@ export function registerUpdateNoticeOnExit() {
|
|
|
80
84
|
}
|
|
81
85
|
});
|
|
82
86
|
}
|
|
87
|
+
function extractLatestExtensionVersionFromReleases(releases) {
|
|
88
|
+
for (const release of releases) {
|
|
89
|
+
for (const asset of release.assets ?? []) {
|
|
90
|
+
const assetMatch = asset.name.match(/^opencli-extension-v(.+)\.zip$/);
|
|
91
|
+
if (assetMatch)
|
|
92
|
+
return assetMatch[1];
|
|
93
|
+
}
|
|
94
|
+
const tagMatch = release.tag_name.match(/^ext-v(.+)$/);
|
|
95
|
+
if (tagMatch)
|
|
96
|
+
return tagMatch[1];
|
|
97
|
+
}
|
|
98
|
+
return undefined;
|
|
99
|
+
}
|
|
100
|
+
/** Fetch the latest extension version from GitHub Releases. */
|
|
101
|
+
async function fetchLatestExtensionVersion() {
|
|
102
|
+
try {
|
|
103
|
+
const controller = new AbortController();
|
|
104
|
+
const timer = setTimeout(() => controller.abort(), 3000);
|
|
105
|
+
const res = await fetch(GITHUB_RELEASES_URL, {
|
|
106
|
+
signal: controller.signal,
|
|
107
|
+
headers: { 'User-Agent': `opencli/${PKG_VERSION}`, Accept: 'application/vnd.github+json' },
|
|
108
|
+
});
|
|
109
|
+
clearTimeout(timer);
|
|
110
|
+
if (!res.ok)
|
|
111
|
+
return undefined;
|
|
112
|
+
const releases = await res.json();
|
|
113
|
+
return extractLatestExtensionVersionFromReleases(releases);
|
|
114
|
+
}
|
|
115
|
+
catch {
|
|
116
|
+
return undefined;
|
|
117
|
+
}
|
|
118
|
+
}
|
|
83
119
|
/**
|
|
84
120
|
* Kick off a background fetch to npm registry. Writes to cache for next run.
|
|
85
121
|
* Fully non-blocking — never awaited.
|
|
@@ -102,7 +138,8 @@ export function checkForUpdateBackground() {
|
|
|
102
138
|
return;
|
|
103
139
|
const data = await res.json();
|
|
104
140
|
if (typeof data.version === 'string') {
|
|
105
|
-
|
|
141
|
+
const extVersion = await fetchLatestExtensionVersion();
|
|
142
|
+
writeCache(data.version, extVersion);
|
|
106
143
|
}
|
|
107
144
|
}
|
|
108
145
|
catch {
|
|
@@ -110,3 +147,11 @@ export function checkForUpdateBackground() {
|
|
|
110
147
|
}
|
|
111
148
|
})();
|
|
112
149
|
}
|
|
150
|
+
/**
|
|
151
|
+
* Get the cached latest extension version (if available).
|
|
152
|
+
* Used by `opencli doctor` to report extension updates.
|
|
153
|
+
*/
|
|
154
|
+
export function getCachedLatestExtensionVersion() {
|
|
155
|
+
return _cache?.latestExtensionVersion;
|
|
156
|
+
}
|
|
157
|
+
export { extractLatestExtensionVersionFromReleases as _extractLatestExtensionVersionFromReleases, };
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import { describe, expect, it } from 'vitest';
|
|
2
|
+
import { _extractLatestExtensionVersionFromReleases as extractLatestExtensionVersionFromReleases } from './update-check.js';
|
|
3
|
+
describe('extractLatestExtensionVersionFromReleases', () => {
|
|
4
|
+
it('reads the extension version from a versioned asset on a normal CLI release', () => {
|
|
5
|
+
expect(extractLatestExtensionVersionFromReleases([
|
|
6
|
+
{
|
|
7
|
+
tag_name: 'v1.7.3',
|
|
8
|
+
assets: [
|
|
9
|
+
{ name: 'opencli-extension.zip' },
|
|
10
|
+
{ name: 'opencli-extension-v1.0.2.zip' },
|
|
11
|
+
],
|
|
12
|
+
},
|
|
13
|
+
])).toBe('1.0.2');
|
|
14
|
+
});
|
|
15
|
+
it('falls back to ext-v tags for extension-only releases', () => {
|
|
16
|
+
expect(extractLatestExtensionVersionFromReleases([
|
|
17
|
+
{
|
|
18
|
+
tag_name: 'ext-v1.1.0',
|
|
19
|
+
assets: [{ name: 'opencli-extension.zip' }],
|
|
20
|
+
},
|
|
21
|
+
])).toBe('1.1.0');
|
|
22
|
+
});
|
|
23
|
+
it('returns undefined when no extension version source exists', () => {
|
|
24
|
+
expect(extractLatestExtensionVersionFromReleases([
|
|
25
|
+
{
|
|
26
|
+
tag_name: 'v1.7.3',
|
|
27
|
+
assets: [{ name: 'opencli-extension.zip' }],
|
|
28
|
+
},
|
|
29
|
+
])).toBeUndefined();
|
|
30
|
+
});
|
|
31
|
+
});
|
package/package.json
CHANGED
|
@@ -21,7 +21,7 @@
|
|
|
21
21
|
|
|
22
22
|
import { existsSync, mkdirSync, rmSync, readFileSync, writeFileSync, readdirSync, statSync, unlinkSync } from 'node:fs';
|
|
23
23
|
import { createHash } from 'node:crypto';
|
|
24
|
-
import { join, resolve, dirname } from 'node:path';
|
|
24
|
+
import { join, resolve, dirname, relative } from 'node:path';
|
|
25
25
|
import { homedir } from 'node:os';
|
|
26
26
|
|
|
27
27
|
const OPENCLI_DIR = join(homedir(), '.opencli');
|
|
@@ -82,8 +82,11 @@ function walkFiles(dir, prefix = '') {
|
|
|
82
82
|
* Remove empty parent directories up to (but not including) stopAt.
|
|
83
83
|
*/
|
|
84
84
|
function pruneEmptyDirs(filePath, stopAt) {
|
|
85
|
-
|
|
86
|
-
|
|
85
|
+
const boundary = resolve(stopAt);
|
|
86
|
+
let dir = resolve(dirname(filePath));
|
|
87
|
+
while (dir !== boundary) {
|
|
88
|
+
const rel = relative(boundary, dir);
|
|
89
|
+
if (!rel || rel.startsWith('..')) break;
|
|
87
90
|
try {
|
|
88
91
|
const entries = readdirSync(dir);
|
|
89
92
|
if (entries.length > 0) break;
|
|
@@ -113,7 +116,15 @@ export function fetchAdapters() {
|
|
|
113
116
|
|
|
114
117
|
const newOfficialFiles = new Set(walkFiles(BUILTIN_CLIS));
|
|
115
118
|
const oldOfficialFiles = new Set(oldManifest?.files ?? []);
|
|
116
|
-
const
|
|
119
|
+
const rawHashes = oldManifest?.hashes;
|
|
120
|
+
// Guard against corrupted manifest: if hashes is a non-object type (string, number,
|
|
121
|
+
// array), skip sync to avoid false-positive "changed" detection that deletes overrides.
|
|
122
|
+
// null/undefined are treated as empty (old manifests may lack the field).
|
|
123
|
+
if (rawHashes != null && (typeof rawHashes !== 'object' || Array.isArray(rawHashes))) {
|
|
124
|
+
log('Warning: adapter-manifest.json has corrupted hashes — skipping sync. Will fix on next run.');
|
|
125
|
+
return;
|
|
126
|
+
}
|
|
127
|
+
const oldHashes = rawHashes ?? {};
|
|
117
128
|
mkdirSync(USER_CLIS_DIR, { recursive: true });
|
|
118
129
|
|
|
119
130
|
// 1. Compute new hashes and detect which sites have changes
|
|
@@ -175,6 +186,24 @@ export function fetchAdapters() {
|
|
|
175
186
|
}
|
|
176
187
|
if (tsCleaned > 0) log(`Cleaned up ${tsCleaned} stale .ts adapter files`);
|
|
177
188
|
|
|
189
|
+
// 3b. Clean up stale .yaml/.yml adapter files left by older versions (pre-1.7.0)
|
|
190
|
+
// Older versions shipped adapters as YAML; current versions use .js only.
|
|
191
|
+
// These cause "Ignoring YAML adapter" warnings on every run (issue #953).
|
|
192
|
+
let yamlCleaned = 0;
|
|
193
|
+
for (const relPath of walkFiles(USER_CLIS_DIR)) {
|
|
194
|
+
if (relPath.endsWith('.yaml') || relPath.endsWith('.yml')) {
|
|
195
|
+
const jsCounterpart = relPath.replace(/\.ya?ml$/, '.js');
|
|
196
|
+
if (newOfficialFiles.has(jsCounterpart)) {
|
|
197
|
+
try {
|
|
198
|
+
unlinkSync(join(USER_CLIS_DIR, relPath));
|
|
199
|
+
pruneEmptyDirs(join(USER_CLIS_DIR, relPath), USER_CLIS_DIR);
|
|
200
|
+
yamlCleaned++;
|
|
201
|
+
} catch { /* ignore */ }
|
|
202
|
+
}
|
|
203
|
+
}
|
|
204
|
+
}
|
|
205
|
+
if (yamlCleaned > 0) log(`Cleaned up ${yamlCleaned} stale .yaml adapter files`);
|
|
206
|
+
|
|
178
207
|
// 4. Clean up legacy compat shim files from ~/.opencli/
|
|
179
208
|
// These were created by an older approach that placed re-export shims directly
|
|
180
209
|
// in ~/.opencli/ (e.g., registry.js, errors.js, browser/). The current approach
|
|
@@ -245,15 +274,13 @@ export function fetchAdapters() {
|
|
|
245
274
|
}, null, 2));
|
|
246
275
|
|
|
247
276
|
log(`Synced adapters: ${cleared} local override(s) cleared` +
|
|
248
|
-
(tsCleaned > 0 ? `, ${tsCleaned} stale .ts files removed` : '')
|
|
277
|
+
(tsCleaned > 0 ? `, ${tsCleaned} stale .ts files removed` : '') +
|
|
278
|
+
(yamlCleaned > 0 ? `, ${yamlCleaned} stale .yaml files removed` : ''));
|
|
249
279
|
}
|
|
250
280
|
|
|
251
281
|
function main() {
|
|
252
282
|
// Skip in CI
|
|
253
283
|
if (process.env.CI || process.env.CONTINUOUS_INTEGRATION) return;
|
|
254
|
-
// Allow opt-out
|
|
255
|
-
if (process.env.OPENCLI_SKIP_FETCH === '1') return;
|
|
256
|
-
|
|
257
284
|
// Only run on global install, explicit trigger, or first-run fallback
|
|
258
285
|
const isGlobal = process.env.npm_config_global === 'true';
|
|
259
286
|
const isExplicit = process.env.OPENCLI_FETCH === '1';
|