@jackwener/opencli 1.7.3 → 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.
Files changed (93) hide show
  1. package/README.md +16 -16
  2. package/README.zh-CN.md +28 -15
  3. package/cli-manifest.json +547 -10
  4. package/clis/bilibili/favorite.js +18 -13
  5. package/clis/binance/depth.js +3 -4
  6. package/clis/boss/utils.js +2 -3
  7. package/clis/chatgpt-app/ax.js +6 -3
  8. package/clis/douban/search.js +1 -0
  9. package/clis/douban/search.test.js +11 -0
  10. package/clis/douban/subject.js +20 -93
  11. package/clis/douban/subject.test.js +11 -0
  12. package/clis/douban/utils.js +250 -8
  13. package/clis/douban/utils.test.js +179 -4
  14. package/clis/doubao/utils.js +319 -130
  15. package/clis/doubao/utils.test.js +241 -2
  16. package/clis/eastmoney/hot-rank.js +50 -0
  17. package/clis/eastmoney/hot-rank.test.js +59 -0
  18. package/clis/grok/image.test.ts +107 -0
  19. package/clis/grok/image.ts +356 -0
  20. package/clis/tdx/hot-rank.js +47 -0
  21. package/clis/tdx/hot-rank.test.js +59 -0
  22. package/clis/ths/hot-rank.js +49 -0
  23. package/clis/ths/hot-rank.test.js +64 -0
  24. package/clis/twitter/bookmarks.js +2 -1
  25. package/clis/uiverse/_shared.js +368 -0
  26. package/clis/uiverse/_shared.test.js +55 -0
  27. package/clis/uiverse/code.js +47 -0
  28. package/clis/uiverse/preview.js +71 -0
  29. package/clis/xiaohongshu/comments.js +2 -2
  30. package/clis/xiaohongshu/comments.test.js +46 -25
  31. package/clis/xiaohongshu/download.js +6 -7
  32. package/clis/xiaohongshu/download.test.js +17 -5
  33. package/clis/xiaohongshu/note-helpers.js +46 -12
  34. package/clis/xiaohongshu/note.js +3 -5
  35. package/clis/xiaohongshu/note.test.js +52 -25
  36. package/clis/xiaoyuzhou/auth.js +303 -0
  37. package/clis/xiaoyuzhou/auth.test.js +124 -0
  38. package/clis/xiaoyuzhou/download.js +49 -0
  39. package/clis/xiaoyuzhou/download.test.js +125 -0
  40. package/clis/xiaoyuzhou/transcript.js +76 -0
  41. package/clis/xiaoyuzhou/transcript.test.js +195 -0
  42. package/clis/youtube/feed.js +120 -0
  43. package/clis/youtube/history.js +118 -0
  44. package/clis/youtube/like.js +62 -0
  45. package/clis/youtube/playlist.js +97 -0
  46. package/clis/youtube/subscribe.js +71 -0
  47. package/clis/youtube/subscriptions.js +57 -0
  48. package/clis/youtube/unlike.js +62 -0
  49. package/clis/youtube/unsubscribe.js +71 -0
  50. package/clis/youtube/utils.js +122 -0
  51. package/clis/youtube/utils.test.js +32 -1
  52. package/clis/youtube/watch-later.js +76 -0
  53. package/dist/src/browser/base-page.js +25 -5
  54. package/dist/src/browser/bridge.d.ts +2 -0
  55. package/dist/src/browser/bridge.js +51 -14
  56. package/dist/src/browser/cdp.js +1 -0
  57. package/dist/src/browser/daemon-client.d.ts +1 -0
  58. package/dist/src/browser/dom-snapshot.js +13 -1
  59. package/dist/src/browser/page.d.ts +4 -1
  60. package/dist/src/browser/page.js +48 -8
  61. package/dist/src/browser/page.test.js +61 -1
  62. package/dist/src/browser/target-errors.d.ts +23 -0
  63. package/dist/src/browser/target-errors.js +29 -0
  64. package/dist/src/browser/target-errors.test.d.ts +1 -0
  65. package/dist/src/browser/target-errors.test.js +61 -0
  66. package/dist/src/browser/target-resolver.d.ts +57 -0
  67. package/dist/src/browser/target-resolver.js +298 -0
  68. package/dist/src/browser/target-resolver.test.d.ts +1 -0
  69. package/dist/src/browser/target-resolver.test.js +43 -0
  70. package/dist/src/browser.test.js +38 -1
  71. package/dist/src/cli.js +45 -37
  72. package/dist/src/commands/daemon.d.ts +4 -2
  73. package/dist/src/commands/daemon.js +22 -2
  74. package/dist/src/commands/daemon.test.js +65 -2
  75. package/dist/src/daemon.js +2 -0
  76. package/dist/src/doctor.d.ts +1 -0
  77. package/dist/src/doctor.js +32 -9
  78. package/dist/src/doctor.test.js +28 -12
  79. package/dist/src/external-clis.yaml +2 -2
  80. package/dist/src/logger.d.ts +2 -2
  81. package/dist/src/logger.js +3 -3
  82. package/dist/src/output.js +1 -5
  83. package/dist/src/output.test.js +0 -21
  84. package/dist/src/pipeline/steps/transform.js +1 -1
  85. package/dist/src/pipeline/template.d.ts +1 -0
  86. package/dist/src/pipeline/template.js +11 -3
  87. package/dist/src/pipeline/template.test.js +3 -0
  88. package/dist/src/pipeline/transform.test.js +14 -0
  89. package/dist/src/plugin.d.ts +7 -1
  90. package/dist/src/plugin.js +23 -1
  91. package/dist/src/plugin.test.js +15 -1
  92. package/dist/src/types.d.ts +1 -1
  93. package/package.json +1 -1
@@ -2,17 +2,12 @@ import { describe, expect, it, vi, beforeEach, afterEach } from 'vitest';
2
2
  import { render } from './output.js';
3
3
  describe('output TTY detection', () => {
4
4
  const originalIsTTY = process.stdout.isTTY;
5
- const originalEnv = process.env.OUTPUT;
6
5
  let logSpy;
7
6
  beforeEach(() => {
8
7
  logSpy = vi.spyOn(console, 'log').mockImplementation(() => { });
9
8
  });
10
9
  afterEach(() => {
11
10
  Object.defineProperty(process.stdout, 'isTTY', { value: originalIsTTY, writable: true });
12
- if (originalEnv === undefined)
13
- delete process.env.OUTPUT;
14
- else
15
- process.env.OUTPUT = originalEnv;
16
11
  logSpy.mockRestore();
17
12
  });
18
13
  it('outputs YAML in non-TTY when format is default table', () => {
@@ -35,22 +30,6 @@ describe('output TTY detection', () => {
35
30
  const out = logSpy.mock.calls.map((c) => c[0]).join('\n');
36
31
  expect(JSON.parse(out)).toEqual([{ name: 'alice' }]);
37
32
  });
38
- it('OUTPUT env var overrides default table in non-TTY', () => {
39
- Object.defineProperty(process.stdout, 'isTTY', { value: false, writable: true });
40
- process.env.OUTPUT = 'json';
41
- render([{ name: 'alice' }], { fmt: 'table' });
42
- const out = logSpy.mock.calls.map((c) => c[0]).join('\n');
43
- expect(JSON.parse(out)).toEqual([{ name: 'alice' }]);
44
- });
45
- it('explicit -f flag takes precedence over OUTPUT env var', () => {
46
- Object.defineProperty(process.stdout, 'isTTY', { value: false, writable: true });
47
- process.env.OUTPUT = 'json';
48
- render([{ name: 'alice' }], { fmt: 'csv', fmtExplicit: true });
49
- const out = logSpy.mock.calls.map((c) => c[0]).join('\n');
50
- expect(out).toContain('name');
51
- expect(out).toContain('alice');
52
- expect(out).not.toContain('"name"'); // not JSON
53
- });
54
33
  it('explicit -f table overrides non-TTY auto-downgrade', () => {
55
34
  Object.defineProperty(process.stdout, 'isTTY', { value: false, writable: true });
56
35
  render([{ name: 'alice' }], { fmt: 'table', fmtExplicit: true, columns: ['name'] });
@@ -40,7 +40,7 @@ export async function stepMap(_page, params, data, args) {
40
40
  for (const [key, template] of Object.entries(templateParams)) {
41
41
  if (key === 'select')
42
42
  continue;
43
- row[key] = render(template, { args, data: source, item, index: i });
43
+ row[key] = render(template, { args, data: source, root: data, item, index: i });
44
44
  }
45
45
  result.push(row);
46
46
  }
@@ -4,6 +4,7 @@
4
4
  export interface RenderContext {
5
5
  args?: Record<string, unknown>;
6
6
  data?: unknown;
7
+ root?: unknown;
7
8
  item?: unknown;
8
9
  index?: number;
9
10
  }
@@ -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 () => {
@@ -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 once at repo root, then finalize each sub-plugin.
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
  /**
@@ -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 once at repo root, then finalize each sub-plugin.
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
  }
@@ -545,13 +545,27 @@ describe('postInstallMonorepoLifecycle', () => {
545
545
  afterEach(() => {
546
546
  fs.rmSync(repoDir, { recursive: true, force: true });
547
547
  });
548
- it('installs dependencies once at the monorepo root, not in each sub-plugin', () => {
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');
@@ -72,7 +72,7 @@ export interface IPage {
72
72
  getInterceptedRequests(): Promise<any[]>;
73
73
  waitForCapture(timeout?: number): Promise<void>;
74
74
  screenshot(options?: ScreenshotOptions): Promise<string>;
75
- startNetworkCapture?(pattern?: string): Promise<void>;
75
+ startNetworkCapture?(pattern?: string): Promise<boolean>;
76
76
  readNetworkCapture?(): Promise<unknown[]>;
77
77
  /**
78
78
  * Set local file paths on a file input element via CDP DOM.setFileInputFiles.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@jackwener/opencli",
3
- "version": "1.7.3",
3
+ "version": "1.7.4",
4
4
  "publishConfig": {
5
5
  "access": "public"
6
6
  },