@jackwener/opencli 1.6.8 → 1.6.9

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 (84) hide show
  1. package/README.md +2 -0
  2. package/README.zh-CN.md +2 -1
  3. package/dist/clis/jianyu/search.d.ts +14 -0
  4. package/dist/clis/jianyu/search.js +135 -0
  5. package/dist/clis/jianyu/search.test.d.ts +1 -0
  6. package/dist/clis/jianyu/search.test.js +23 -0
  7. package/dist/clis/quark/ls.d.ts +1 -0
  8. package/dist/clis/quark/ls.js +63 -0
  9. package/dist/clis/quark/mkdir.d.ts +1 -0
  10. package/dist/clis/quark/mkdir.js +36 -0
  11. package/dist/clis/quark/mv.d.ts +1 -0
  12. package/dist/clis/quark/mv.js +53 -0
  13. package/dist/clis/quark/rename.d.ts +1 -0
  14. package/dist/clis/quark/rename.js +26 -0
  15. package/dist/clis/quark/rm.d.ts +1 -0
  16. package/dist/clis/quark/rm.js +24 -0
  17. package/dist/clis/quark/save.d.ts +1 -0
  18. package/dist/clis/quark/save.js +80 -0
  19. package/dist/clis/quark/share-tree.d.ts +1 -0
  20. package/dist/clis/quark/share-tree.js +45 -0
  21. package/dist/clis/quark/utils.d.ts +50 -0
  22. package/dist/clis/quark/utils.js +146 -0
  23. package/dist/clis/quark/utils.test.d.ts +1 -0
  24. package/dist/clis/quark/utils.test.js +58 -0
  25. package/dist/clis/twitter/reply.js +3 -8
  26. package/dist/clis/twitter/reply.test.js +5 -5
  27. package/dist/clis/xiaohongshu/note.js +8 -3
  28. package/dist/clis/xiaohongshu/note.test.js +11 -0
  29. package/dist/clis/zhihu/answer.d.ts +1 -0
  30. package/dist/clis/zhihu/answer.js +194 -0
  31. package/dist/clis/zhihu/answer.test.d.ts +1 -0
  32. package/dist/clis/zhihu/answer.test.js +81 -0
  33. package/dist/clis/zhihu/comment.d.ts +1 -0
  34. package/dist/clis/zhihu/comment.js +335 -0
  35. package/dist/clis/zhihu/comment.test.d.ts +1 -0
  36. package/dist/clis/zhihu/comment.test.js +54 -0
  37. package/dist/clis/zhihu/favorite.d.ts +1 -0
  38. package/dist/clis/zhihu/favorite.js +224 -0
  39. package/dist/clis/zhihu/favorite.test.d.ts +1 -0
  40. package/dist/clis/zhihu/favorite.test.js +196 -0
  41. package/dist/clis/zhihu/follow.d.ts +1 -0
  42. package/dist/clis/zhihu/follow.js +80 -0
  43. package/dist/clis/zhihu/follow.test.d.ts +1 -0
  44. package/dist/clis/zhihu/follow.test.js +45 -0
  45. package/dist/clis/zhihu/like.d.ts +1 -0
  46. package/dist/clis/zhihu/like.js +91 -0
  47. package/dist/clis/zhihu/like.test.d.ts +1 -0
  48. package/dist/clis/zhihu/like.test.js +64 -0
  49. package/dist/clis/zhihu/target.d.ts +24 -0
  50. package/dist/clis/zhihu/target.js +91 -0
  51. package/dist/clis/zhihu/target.test.d.ts +1 -0
  52. package/dist/clis/zhihu/target.test.js +77 -0
  53. package/dist/clis/zhihu/write-shared.d.ts +32 -0
  54. package/dist/clis/zhihu/write-shared.js +221 -0
  55. package/dist/clis/zhihu/write-shared.test.d.ts +1 -0
  56. package/dist/clis/zhihu/write-shared.test.js +175 -0
  57. package/dist/src/browser/bridge.d.ts +2 -0
  58. package/dist/src/browser/bridge.js +30 -24
  59. package/dist/src/browser/daemon-client.d.ts +17 -8
  60. package/dist/src/browser/daemon-client.js +12 -13
  61. package/dist/src/browser/daemon-client.test.js +32 -25
  62. package/dist/src/browser/index.d.ts +2 -1
  63. package/dist/src/browser/index.js +1 -1
  64. package/dist/src/browser.test.js +2 -3
  65. package/dist/src/cli.js +3 -3
  66. package/dist/src/clis/binance/commands.test.d.ts +1 -0
  67. package/dist/src/clis/binance/commands.test.js +54 -0
  68. package/dist/src/commanderAdapter.js +19 -6
  69. package/dist/src/diagnostic.d.ts +1 -0
  70. package/dist/src/diagnostic.js +64 -2
  71. package/dist/src/diagnostic.test.js +91 -1
  72. package/dist/src/doctor.d.ts +2 -0
  73. package/dist/src/doctor.js +59 -31
  74. package/dist/src/doctor.test.js +89 -16
  75. package/dist/src/execution.js +1 -13
  76. package/dist/src/explore.js +1 -1
  77. package/dist/src/generate.d.ts +2 -5
  78. package/dist/src/generate.js +2 -5
  79. package/dist/src/plugin.d.ts +2 -1
  80. package/dist/src/plugin.js +25 -8
  81. package/dist/src/plugin.test.js +16 -1
  82. package/package.json +3 -3
  83. package/dist/src/browser/discover.d.ts +0 -15
  84. package/dist/src/browser/discover.js +0 -19
@@ -5,11 +5,11 @@
5
5
  */
6
6
  import chalk from 'chalk';
7
7
  import { DEFAULT_DAEMON_PORT } from './constants.js';
8
- import { checkDaemonStatus } from './browser/discover.js';
9
8
  import { BrowserBridge } from './browser/index.js';
10
- import { listSessions } from './browser/daemon-client.js';
9
+ import { getDaemonHealth, listSessions } from './browser/daemon-client.js';
11
10
  import { getErrorMessage } from './errors.js';
12
11
  import { getRuntimeLabel } from './runtime-detect.js';
12
+ const DOCTOR_LIVE_TIMEOUT_SECONDS = 8;
13
13
  /**
14
14
  * Test connectivity by attempting a real browser command.
15
15
  */
@@ -17,7 +17,7 @@ export async function checkConnectivity(opts) {
17
17
  const start = Date.now();
18
18
  try {
19
19
  const bridge = new BrowserBridge();
20
- const page = await bridge.connect({ timeout: opts?.timeout ?? 8 });
20
+ const page = await bridge.connect({ timeout: opts?.timeout ?? DOCTOR_LIVE_TIMEOUT_SECONDS });
21
21
  // Try a simple eval to verify end-to-end connectivity
22
22
  await page.evaluate('1 + 1');
23
23
  await bridge.close();
@@ -28,33 +28,48 @@ export async function checkConnectivity(opts) {
28
28
  }
29
29
  }
30
30
  export async function runBrowserDoctor(opts = {}) {
31
- // Try to auto-start daemon if it's not running, so we show accurate status.
32
- let initialStatus = await checkDaemonStatus();
33
- if (!initialStatus.running) {
34
- try {
35
- const bridge = new BrowserBridge();
36
- await bridge.connect({ timeout: 5 });
37
- await bridge.close();
38
- }
39
- catch {
40
- // Auto-start failed; we'll report it below.
41
- }
42
- }
43
- // Run the live connectivity check — it may also auto-start the daemon as a
44
- // side-effect, so we read daemon status only *after* all side-effects settle.
31
+ // Live connectivity check doubles as auto-start (bridge.connect spawns daemon).
45
32
  let connectivity;
46
33
  if (opts.live) {
47
34
  connectivity = await checkConnectivity();
48
35
  }
49
- const status = await checkDaemonStatus();
50
- const sessions = opts.sessions && status.running && status.extensionConnected
36
+ else {
37
+ // No live probe daemon may have idle-exited. Do a minimal auto-start
38
+ // so we don't misreport a lazy-lifecycle stop as a real failure.
39
+ const initialHealth = await getDaemonHealth();
40
+ if (initialHealth.state === 'stopped') {
41
+ try {
42
+ const bridge = new BrowserBridge();
43
+ await bridge.connect({ timeout: 5 });
44
+ await bridge.close();
45
+ }
46
+ catch {
47
+ // Auto-start failed; we'll report it below.
48
+ }
49
+ }
50
+ }
51
+ // Single status read *after* all side-effects (live check / auto-start) settle.
52
+ const health = await getDaemonHealth();
53
+ const daemonRunning = health.state !== 'stopped';
54
+ const extensionConnected = health.state === 'ready';
55
+ const daemonFlaky = !!(connectivity?.ok && !daemonRunning);
56
+ const extensionFlaky = !!(connectivity?.ok && daemonRunning && !extensionConnected);
57
+ const sessions = opts.sessions && health.state === 'ready'
51
58
  ? await listSessions()
52
59
  : undefined;
53
60
  const issues = [];
54
- if (!status.running) {
61
+ if (daemonFlaky) {
62
+ issues.push('Daemon connectivity is unstable. The live browser test succeeded, but the daemon was no longer running immediately afterward.\n' +
63
+ 'This usually means the daemon crashed or exited right after serving the live probe.');
64
+ }
65
+ else if (!daemonRunning) {
55
66
  issues.push('Daemon is not running. It should start automatically when you run an opencli browser command.');
56
67
  }
57
- if (status.running && !status.extensionConnected) {
68
+ if (extensionFlaky) {
69
+ issues.push('Extension connection is unstable. The live browser test succeeded, but the daemon reported the extension disconnected immediately afterward.\n' +
70
+ 'This usually means the Browser Bridge service worker is reconnecting slowly or Chrome suspended it.');
71
+ }
72
+ else if (daemonRunning && !extensionConnected) {
58
73
  issues.push('Daemon is running but the Chrome/Chromium extension is not connected.\n' +
59
74
  'Please install the opencli Browser Bridge extension:\n' +
60
75
  ' 1. Download from https://github.com/jackwener/opencli/releases\n' +
@@ -64,19 +79,22 @@ export async function runBrowserDoctor(opts = {}) {
64
79
  if (connectivity && !connectivity.ok) {
65
80
  issues.push(`Browser connectivity test failed: ${connectivity.error ?? 'unknown'}`);
66
81
  }
67
- if (status.extensionVersion && opts.cliVersion) {
68
- const extMajor = status.extensionVersion.split('.')[0];
82
+ const extensionVersion = health.status?.extensionVersion;
83
+ if (extensionVersion && opts.cliVersion) {
84
+ const extMajor = extensionVersion.split('.')[0];
69
85
  const cliMajor = opts.cliVersion.split('.')[0];
70
86
  if (extMajor !== cliMajor) {
71
- issues.push(`Extension major version mismatch: extension v${status.extensionVersion} ≠ CLI v${opts.cliVersion}\n` +
87
+ issues.push(`Extension major version mismatch: extension v${extensionVersion} ≠ CLI v${opts.cliVersion}\n` +
72
88
  ' Download the latest extension from: https://github.com/jackwener/opencli/releases');
73
89
  }
74
90
  }
75
91
  return {
76
92
  cliVersion: opts.cliVersion,
77
- daemonRunning: status.running,
78
- extensionConnected: status.extensionConnected,
79
- extensionVersion: status.extensionVersion,
93
+ daemonRunning,
94
+ daemonFlaky,
95
+ extensionConnected,
96
+ extensionFlaky,
97
+ extensionVersion,
80
98
  connectivity,
81
99
  sessions,
82
100
  issues,
@@ -85,12 +103,22 @@ export async function runBrowserDoctor(opts = {}) {
85
103
  export function renderBrowserDoctorReport(report) {
86
104
  const lines = [chalk.bold(`opencli v${report.cliVersion ?? 'unknown'} doctor`) + chalk.dim(` (${getRuntimeLabel()})`), ''];
87
105
  // Daemon status
88
- const daemonIcon = report.daemonRunning ? chalk.green('[OK]') : chalk.red('[MISSING]');
89
- lines.push(`${daemonIcon} Daemon: ${report.daemonRunning ? `running on port ${DEFAULT_DAEMON_PORT}` : 'not running'}`);
106
+ const daemonIcon = report.daemonFlaky
107
+ ? chalk.yellow('[WARN]')
108
+ : report.daemonRunning ? chalk.green('[OK]') : chalk.red('[MISSING]');
109
+ const daemonLabel = report.daemonFlaky
110
+ ? 'unstable (running during live check, then stopped)'
111
+ : report.daemonRunning ? `running on port ${DEFAULT_DAEMON_PORT}` : 'not running';
112
+ lines.push(`${daemonIcon} Daemon: ${daemonLabel}`);
90
113
  // Extension status
91
- const extIcon = report.extensionConnected ? chalk.green('[OK]') : chalk.yellow('[MISSING]');
114
+ const extIcon = report.extensionFlaky
115
+ ? chalk.yellow('[WARN]')
116
+ : report.extensionConnected ? chalk.green('[OK]') : chalk.yellow('[MISSING]');
92
117
  const extVersion = report.extensionVersion ? chalk.dim(` (v${report.extensionVersion})`) : '';
93
- lines.push(`${extIcon} Extension: ${report.extensionConnected ? 'connected' : 'not connected'}${extVersion}`);
118
+ const extLabel = report.extensionFlaky
119
+ ? 'unstable (connected during live check, then disconnected)'
120
+ : report.extensionConnected ? 'connected' : 'not connected';
121
+ lines.push(`${extIcon} Extension: ${extLabel}${extVersion}`);
94
122
  // Connectivity
95
123
  if (report.connectivity) {
96
124
  const connIcon = report.connectivity.ok ? chalk.green('[OK]') : chalk.red('[FAIL]');
@@ -1,14 +1,12 @@
1
1
  import { beforeEach, describe, expect, it, vi } from 'vitest';
2
- const { mockCheckDaemonStatus, mockListSessions, mockConnect, mockClose } = vi.hoisted(() => ({
3
- mockCheckDaemonStatus: vi.fn(),
2
+ const { mockGetDaemonHealth, mockListSessions, mockConnect, mockClose } = vi.hoisted(() => ({
3
+ mockGetDaemonHealth: vi.fn(),
4
4
  mockListSessions: vi.fn(),
5
5
  mockConnect: vi.fn(),
6
6
  mockClose: vi.fn(),
7
7
  }));
8
- vi.mock('./browser/discover.js', () => ({
9
- checkDaemonStatus: mockCheckDaemonStatus,
10
- }));
11
8
  vi.mock('./browser/daemon-client.js', () => ({
9
+ getDaemonHealth: mockGetDaemonHealth,
12
10
  listSessions: mockListSessions,
13
11
  }));
14
12
  vi.mock('./browser/index.js', () => ({
@@ -69,23 +67,98 @@ describe('doctor report rendering', () => {
69
67
  }));
70
68
  expect(text).toContain('[SKIP] Connectivity: skipped (--no-live)');
71
69
  });
72
- it('reports consistent status when live check auto-starts the daemon', async () => {
73
- // checkDaemonStatus is called twice: once for auto-start check, once for final status.
74
- // First call: daemon not running (triggers auto-start attempt)
75
- mockCheckDaemonStatus.mockResolvedValueOnce({ running: false, extensionConnected: false });
76
- // Auto-start attempt via BrowserBridge.connect fails
70
+ it('renders unstable extension state when live connectivity and status disagree', () => {
71
+ const text = strip(renderBrowserDoctorReport({
72
+ daemonRunning: true,
73
+ extensionConnected: true,
74
+ extensionFlaky: true,
75
+ connectivity: { ok: true, durationMs: 1234 },
76
+ issues: ['Extension connection is unstable.'],
77
+ }));
78
+ expect(text).toContain('[WARN] Extension: unstable');
79
+ expect(text).toContain('Extension connection is unstable.');
80
+ });
81
+ it('renders unstable daemon state when live connectivity and status disagree', () => {
82
+ const text = strip(renderBrowserDoctorReport({
83
+ daemonRunning: false,
84
+ daemonFlaky: true,
85
+ extensionConnected: false,
86
+ connectivity: { ok: true, durationMs: 1234 },
87
+ issues: ['Daemon connectivity is unstable.'],
88
+ }));
89
+ expect(text).toContain('[WARN] Daemon: unstable');
90
+ expect(text).toContain('Daemon connectivity is unstable.');
91
+ });
92
+ it('reports daemon not running when no-live and auto-start fails', async () => {
93
+ // no-live mode: getDaemonHealth called twice (initial check + final status)
94
+ // Initial: stopped → triggers auto-start attempt
95
+ mockGetDaemonHealth.mockResolvedValueOnce({ state: 'stopped', status: null });
96
+ // Auto-start fails
77
97
  mockConnect.mockRejectedValueOnce(new Error('Could not start daemon'));
78
- // Second call: daemon still not running after failed auto-start
79
- mockCheckDaemonStatus.mockResolvedValueOnce({ running: false, extensionConnected: false });
98
+ // Final: still stopped
99
+ mockGetDaemonHealth.mockResolvedValueOnce({ state: 'stopped', status: null });
80
100
  const report = await runBrowserDoctor({ live: false });
81
- // Status reflects daemon not running
82
101
  expect(report.daemonRunning).toBe(false);
83
102
  expect(report.extensionConnected).toBe(false);
84
- // checkDaemonStatus called twice (initial + final)
85
- expect(mockCheckDaemonStatus).toHaveBeenCalledTimes(2);
86
- // Should report daemon not running
103
+ expect(mockGetDaemonHealth).toHaveBeenCalledTimes(2);
87
104
  expect(report.issues).toEqual(expect.arrayContaining([
88
105
  expect.stringContaining('Daemon is not running'),
89
106
  ]));
90
107
  });
108
+ it('reports flapping when live check succeeds but final status shows extension disconnected', async () => {
109
+ // Live check succeeds
110
+ mockConnect.mockResolvedValueOnce({
111
+ evaluate: vi.fn().mockResolvedValue(2),
112
+ });
113
+ mockClose.mockResolvedValueOnce(undefined);
114
+ // After live check, getDaemonHealth shows no-extension
115
+ mockGetDaemonHealth.mockResolvedValueOnce({ state: 'no-extension', status: { extensionConnected: false } });
116
+ const report = await runBrowserDoctor({ live: true });
117
+ expect(report.daemonRunning).toBe(true);
118
+ expect(report.extensionConnected).toBe(false);
119
+ expect(report.extensionFlaky).toBe(true);
120
+ expect(report.issues).toEqual(expect.arrayContaining([
121
+ expect.stringContaining('Extension connection is unstable'),
122
+ ]));
123
+ });
124
+ it('reports daemon flapping when live check succeeds but daemon disappears afterward', async () => {
125
+ // Live check succeeds
126
+ mockConnect.mockResolvedValueOnce({
127
+ evaluate: vi.fn().mockResolvedValue(2),
128
+ });
129
+ mockClose.mockResolvedValueOnce(undefined);
130
+ // After live check, getDaemonHealth shows stopped
131
+ mockGetDaemonHealth.mockResolvedValueOnce({ state: 'stopped', status: null });
132
+ const report = await runBrowserDoctor({ live: true });
133
+ expect(report.daemonRunning).toBe(false);
134
+ expect(report.daemonFlaky).toBe(true);
135
+ expect(report.extensionConnected).toBe(false);
136
+ expect(report.issues).toEqual(expect.arrayContaining([
137
+ expect.stringContaining('Daemon connectivity is unstable'),
138
+ ]));
139
+ });
140
+ it('uses the fast default timeout for live connectivity checks', async () => {
141
+ let timeoutSeen;
142
+ mockConnect.mockImplementationOnce(async (opts) => {
143
+ timeoutSeen = opts?.timeout;
144
+ return {
145
+ evaluate: vi.fn().mockResolvedValue(2),
146
+ };
147
+ });
148
+ mockClose.mockResolvedValueOnce(undefined);
149
+ mockGetDaemonHealth.mockResolvedValueOnce({ state: 'ready', status: { extensionConnected: true } });
150
+ await runBrowserDoctor({ live: true });
151
+ expect(timeoutSeen).toBe(8);
152
+ });
153
+ it('skips auto-start in no-live mode when daemon is already running', async () => {
154
+ // no-live mode but daemon already running (no-extension)
155
+ mockGetDaemonHealth.mockResolvedValueOnce({ state: 'no-extension', status: { extensionConnected: false } });
156
+ // Final status: same
157
+ mockGetDaemonHealth.mockResolvedValueOnce({ state: 'no-extension', status: { extensionConnected: false } });
158
+ const report = await runBrowserDoctor({ live: false });
159
+ // Should NOT have tried auto-start since daemon was already running
160
+ expect(mockConnect).not.toHaveBeenCalled();
161
+ expect(report.daemonRunning).toBe(true);
162
+ expect(report.extensionConnected).toBe(false);
163
+ });
91
164
  });
@@ -12,12 +12,11 @@
12
12
  import { Strategy, getRegistry, fullName } from './registry.js';
13
13
  import { pathToFileURL } from 'node:url';
14
14
  import { executePipeline } from './pipeline/index.js';
15
- import { AdapterLoadError, ArgumentError, BrowserConnectError, CommandExecutionError, getErrorMessage } from './errors.js';
15
+ import { AdapterLoadError, ArgumentError, CommandExecutionError, getErrorMessage } from './errors.js';
16
16
  import { isDiagnosticEnabled, collectDiagnostic, emitDiagnostic } from './diagnostic.js';
17
17
  import { shouldUseBrowserSession } from './capabilityRouting.js';
18
18
  import { getBrowserFactory, browserSession, runWithTimeout, DEFAULT_BROWSER_COMMAND_TIMEOUT } from './runtime.js';
19
19
  import { emitHook } from './hooks.js';
20
- import { checkDaemonStatus } from './browser/discover.js';
21
20
  import { log } from './logger.js';
22
21
  import { isElectronApp } from './electron-apps.js';
23
22
  import { probeCDP, resolveElectronEndpoint } from './launcher.js';
@@ -149,17 +148,6 @@ export async function executeCommand(cmd, rawKwargs, debug = false) {
149
148
  cdpEndpoint = await resolveElectronEndpoint(cmd.site);
150
149
  }
151
150
  }
152
- else {
153
- // Browser Bridge: fail-fast when daemon is up but extension is missing.
154
- // 300ms timeout avoids a full 2s wait on cold-start.
155
- const status = await checkDaemonStatus({ timeout: 300 });
156
- if (status.running && !status.extensionConnected) {
157
- throw new BrowserConnectError('Browser Bridge extension not connected', 'Install the Browser Bridge:\n' +
158
- ' 1. Download: https://github.com/jackwener/opencli/releases\n' +
159
- ' 2. In Chrome or Chromium, open chrome://extensions → Developer Mode → Load unpacked\n' +
160
- ' Then run: opencli doctor');
161
- }
162
- }
163
151
  ensureRequiredEnv(cmd);
164
152
  const BrowserFactory = getBrowserFactory(cmd.site);
165
153
  result = await browserSession(BrowserFactory, async (page) => {
@@ -257,7 +257,7 @@ export async function exploreUrl(url, opts) {
257
257
  return browserSession(opts.BrowserFactory, async (page) => {
258
258
  return runWithTimeout((async () => {
259
259
  // Step 1: Navigate
260
- await page.startNetworkCapture?.();
260
+ await page.startNetworkCapture?.().catch(() => { });
261
261
  await page.goto(url);
262
262
  await page.wait(waitSeconds);
263
263
  // Step 2: Auto-scroll to trigger lazy loading intelligently
@@ -1,11 +1,8 @@
1
1
  /**
2
2
  * Generate: one-shot CLI creation from URL.
3
3
  *
4
- * Orchestrates the full pipeline:
5
- * explore (Deep Explore) → synthesize (YAML generation) register → verify
6
- *
7
- * Includes Strategy Cascade: if the initial strategy fails,
8
- * automatically downgrades and retries.
4
+ * Orchestrates the pipeline:
5
+ * explore (Deep Explore) → synthesize (YAML generation + candidate ranking)
9
6
  */
10
7
  import type { IBrowserFactory } from './runtime.js';
11
8
  import { type SynthesizeCandidateSummary } from './synthesize.js';
@@ -1,11 +1,8 @@
1
1
  /**
2
2
  * Generate: one-shot CLI creation from URL.
3
3
  *
4
- * Orchestrates the full pipeline:
5
- * explore (Deep Explore) → synthesize (YAML generation) register → verify
6
- *
7
- * Includes Strategy Cascade: if the initial strategy fails,
8
- * automatically downgrades and retries.
4
+ * Orchestrates the pipeline:
5
+ * explore (Deep Explore) → synthesize (YAML generation + candidate ranking)
9
6
  */
10
7
  import { exploreUrl } from './explore.js';
11
8
  import { synthesizeFromExplore } from './synthesize.js';
@@ -142,4 +142,5 @@ declare function parseSource(source: string): ParsedSource | null;
142
142
  * Resolve the path to the esbuild CLI executable with fallback strategies.
143
143
  */
144
144
  export declare function resolveEsbuildBin(): string | null;
145
- export { resolveEsbuildBin as _resolveEsbuildBin, getCommitHash as _getCommitHash, installDependencies as _installDependencies, parseSource as _parseSource, postInstallMonorepoLifecycle as _postInstallMonorepoLifecycle, readLockFile as _readLockFile, readLockFileWithWriter as _readLockFileWithWriter, updateAllPlugins as _updateAllPlugins, validatePluginStructure as _validatePluginStructure, writeLockFile as _writeLockFile, writeLockFileWithFs as _writeLockFileWithFs, isSymlinkSync as _isSymlinkSync, getMonoreposDir as _getMonoreposDir, installLocalPlugin as _installLocalPlugin, isLocalPluginSource as _isLocalPluginSource, moveDir as _moveDir, promoteDir as _promoteDir, replaceDir as _replaceDir, resolvePluginSource as _resolvePluginSource, resolveStoredPluginSource as _resolveStoredPluginSource, toStoredPluginSource as _toStoredPluginSource, toLocalPluginSource as _toLocalPluginSource, };
145
+ declare function resolveHostOpencliRoot(startFile?: string): string;
146
+ export { resolveHostOpencliRoot as _resolveHostOpencliRoot, resolveEsbuildBin as _resolveEsbuildBin, getCommitHash as _getCommitHash, installDependencies as _installDependencies, parseSource as _parseSource, postInstallMonorepoLifecycle as _postInstallMonorepoLifecycle, readLockFile as _readLockFile, readLockFileWithWriter as _readLockFileWithWriter, updateAllPlugins as _updateAllPlugins, validatePluginStructure as _validatePluginStructure, writeLockFile as _writeLockFile, writeLockFileWithFs as _writeLockFileWithFs, isSymlinkSync as _isSymlinkSync, getMonoreposDir as _getMonoreposDir, installLocalPlugin as _installLocalPlugin, isLocalPluginSource as _isLocalPluginSource, moveDir as _moveDir, promoteDir as _promoteDir, replaceDir as _replaceDir, resolvePluginSource as _resolvePluginSource, resolveStoredPluginSource as _resolveStoredPluginSource, toStoredPluginSource as _toStoredPluginSource, toLocalPluginSource as _toLocalPluginSource, };
@@ -1100,11 +1100,7 @@ function parseSource(source) {
1100
1100
  */
1101
1101
  function linkHostOpencli(pluginDir) {
1102
1102
  try {
1103
- // Determine the host opencli package root from this module's location.
1104
- // Both dev (tsx src/plugin.ts) and prod (node dist/plugin.js) are one level
1105
- // deep, so path.dirname + '..' always gives us the package root.
1106
- const thisFile = fileURLToPath(import.meta.url);
1107
- const hostRoot = path.resolve(path.dirname(thisFile), '..');
1103
+ const hostRoot = resolveHostOpencliRoot();
1108
1104
  const targetLink = path.join(pluginDir, 'node_modules', '@jackwener', 'opencli');
1109
1105
  // Remove existing (npm-installed copy or stale symlink)
1110
1106
  if (fs.existsSync(targetLink)) {
@@ -1126,8 +1122,7 @@ function linkHostOpencli(pluginDir) {
1126
1122
  * Resolve the path to the esbuild CLI executable with fallback strategies.
1127
1123
  */
1128
1124
  export function resolveEsbuildBin() {
1129
- const thisFile = fileURLToPath(import.meta.url);
1130
- const hostRoot = path.resolve(path.dirname(thisFile), '..');
1125
+ const hostRoot = resolveHostOpencliRoot();
1131
1126
  // Strategy 1 (Windows): prefer the .cmd wrapper which is executable via shell
1132
1127
  if (isWindows) {
1133
1128
  const cmdPath = path.join(hostRoot, 'node_modules', '.bin', 'esbuild.cmd');
@@ -1180,6 +1175,28 @@ export function resolveEsbuildBin() {
1180
1175
  }
1181
1176
  return null;
1182
1177
  }
1178
+ function resolveHostOpencliRoot(startFile = fileURLToPath(import.meta.url)) {
1179
+ let dir = path.dirname(startFile);
1180
+ while (true) {
1181
+ const pkgPath = path.join(dir, 'package.json');
1182
+ if (fs.existsSync(pkgPath)) {
1183
+ try {
1184
+ const pkg = JSON.parse(fs.readFileSync(pkgPath, 'utf-8'));
1185
+ if (pkg?.name === '@jackwener/opencli') {
1186
+ return dir;
1187
+ }
1188
+ }
1189
+ catch {
1190
+ // Keep walking; a malformed package.json should not hide an ancestor package root.
1191
+ }
1192
+ }
1193
+ const parent = path.dirname(dir);
1194
+ if (parent === dir)
1195
+ break;
1196
+ dir = parent;
1197
+ }
1198
+ return path.resolve(path.dirname(startFile), '..');
1199
+ }
1183
1200
  /**
1184
1201
  * Transpile TS plugin files to JS so they work in production mode.
1185
1202
  * Uses esbuild from the host opencli's node_modules for fast single-file transpilation.
@@ -1218,4 +1235,4 @@ function transpilePluginTs(pluginDir) {
1218
1235
  log.warn(`TS transpilation setup failed: ${getErrorMessage(err)}`);
1219
1236
  }
1220
1237
  }
1221
- export { resolveEsbuildBin as _resolveEsbuildBin, getCommitHash as _getCommitHash, installDependencies as _installDependencies, parseSource as _parseSource, postInstallMonorepoLifecycle as _postInstallMonorepoLifecycle, readLockFile as _readLockFile, readLockFileWithWriter as _readLockFileWithWriter, updateAllPlugins as _updateAllPlugins, validatePluginStructure as _validatePluginStructure, writeLockFile as _writeLockFile, writeLockFileWithFs as _writeLockFileWithFs, isSymlinkSync as _isSymlinkSync, getMonoreposDir as _getMonoreposDir, installLocalPlugin as _installLocalPlugin, isLocalPluginSource as _isLocalPluginSource, moveDir as _moveDir, promoteDir as _promoteDir, replaceDir as _replaceDir, resolvePluginSource as _resolvePluginSource, resolveStoredPluginSource as _resolveStoredPluginSource, toStoredPluginSource as _toStoredPluginSource, toLocalPluginSource as _toLocalPluginSource, };
1238
+ export { resolveHostOpencliRoot as _resolveHostOpencliRoot, resolveEsbuildBin as _resolveEsbuildBin, getCommitHash as _getCommitHash, installDependencies as _installDependencies, parseSource as _parseSource, postInstallMonorepoLifecycle as _postInstallMonorepoLifecycle, readLockFile as _readLockFile, readLockFileWithWriter as _readLockFileWithWriter, updateAllPlugins as _updateAllPlugins, validatePluginStructure as _validatePluginStructure, writeLockFile as _writeLockFile, writeLockFileWithFs as _writeLockFileWithFs, isSymlinkSync as _isSymlinkSync, getMonoreposDir as _getMonoreposDir, installLocalPlugin as _installLocalPlugin, isLocalPluginSource as _isLocalPluginSource, moveDir as _moveDir, promoteDir as _promoteDir, replaceDir as _replaceDir, resolvePluginSource as _resolvePluginSource, resolveStoredPluginSource as _resolveStoredPluginSource, toStoredPluginSource as _toStoredPluginSource, toLocalPluginSource as _toLocalPluginSource, };
@@ -12,7 +12,7 @@ const { mockExecFileSync, mockExecSync } = vi.hoisted(() => ({
12
12
  mockExecFileSync: vi.fn(),
13
13
  mockExecSync: vi.fn(),
14
14
  }));
15
- const { _getCommitHash, _installDependencies, _postInstallMonorepoLifecycle, _promoteDir, _replaceDir, installPlugin, listPlugins, _readLockFile, _readLockFileWithWriter, _resolveEsbuildBin, uninstallPlugin, updatePlugin, _parseSource, _updateAllPlugins, _validatePluginStructure, _writeLockFile, _writeLockFileWithFs, _isSymlinkSync, _getMonoreposDir, getLockFilePath, _installLocalPlugin, _isLocalPluginSource, _moveDir, _resolvePluginSource, _resolveStoredPluginSource, _toStoredPluginSource, _toLocalPluginSource, } = pluginModule;
15
+ const { _getCommitHash, _installDependencies, _postInstallMonorepoLifecycle, _promoteDir, _replaceDir, installPlugin, listPlugins, _readLockFile, _readLockFileWithWriter, _resolveEsbuildBin, _resolveHostOpencliRoot, uninstallPlugin, updatePlugin, _parseSource, _updateAllPlugins, _validatePluginStructure, _writeLockFile, _writeLockFileWithFs, _isSymlinkSync, _getMonoreposDir, getLockFilePath, _installLocalPlugin, _isLocalPluginSource, _moveDir, _resolvePluginSource, _resolveStoredPluginSource, _toStoredPluginSource, _toLocalPluginSource, } = pluginModule;
16
16
  describe('parseSource', () => {
17
17
  it('parses github:user/repo format', () => {
18
18
  const result = _parseSource('github:ByteYue/opencli-plugin-github-trending');
@@ -336,6 +336,21 @@ describe('resolveEsbuildBin', () => {
336
336
  expect(binPath).toMatch(/esbuild(\.cmd)?$/);
337
337
  });
338
338
  });
339
+ describe('resolveHostOpencliRoot', () => {
340
+ let tmpDir;
341
+ beforeEach(() => {
342
+ tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), 'opencli-host-root-test-'));
343
+ });
344
+ afterEach(() => {
345
+ fs.rmSync(tmpDir, { recursive: true, force: true });
346
+ });
347
+ it('walks up from compiled dist/src files to the package root', () => {
348
+ fs.writeFileSync(path.join(tmpDir, 'package.json'), JSON.stringify({ name: '@jackwener/opencli' }));
349
+ const distSrcDir = path.join(tmpDir, 'dist', 'src');
350
+ fs.mkdirSync(distSrcDir, { recursive: true });
351
+ expect(_resolveHostOpencliRoot(path.join(distSrcDir, 'plugin.js'))).toBe(tmpDir);
352
+ });
353
+ });
339
354
  describe('listPlugins', () => {
340
355
  const testDir = path.join(PLUGINS_DIR, '__test-list-plugin__');
341
356
  afterEach(() => {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@jackwener/opencli",
3
- "version": "1.6.8",
3
+ "version": "1.6.9",
4
4
  "publishConfig": {
5
5
  "access": "public"
6
6
  },
@@ -80,12 +80,12 @@
80
80
  "commander": "^14.0.3",
81
81
  "js-yaml": "^4.1.0",
82
82
  "turndown": "^7.2.2",
83
- "undici": "^7.24.6",
83
+ "undici": "^8.0.2",
84
84
  "ws": "^8.18.0"
85
85
  },
86
86
  "devDependencies": {
87
87
  "@types/js-yaml": "^4.0.9",
88
- "@types/node": "^22.13.10",
88
+ "@types/node": "^25.5.2",
89
89
  "@types/turndown": "^5.0.6",
90
90
  "@types/ws": "^8.5.13",
91
91
  "tsx": "^4.19.3",
@@ -1,15 +0,0 @@
1
- /**
2
- * Daemon discovery — checks if the daemon is running.
3
- */
4
- import { isDaemonRunning } from './daemon-client.js';
5
- export { isDaemonRunning };
6
- /**
7
- * Check daemon status and return connection info.
8
- */
9
- export declare function checkDaemonStatus(opts?: {
10
- timeout?: number;
11
- }): Promise<{
12
- running: boolean;
13
- extensionConnected: boolean;
14
- extensionVersion?: string;
15
- }>;
@@ -1,19 +0,0 @@
1
- /**
2
- * Daemon discovery — checks if the daemon is running.
3
- */
4
- import { fetchDaemonStatus, isDaemonRunning } from './daemon-client.js';
5
- export { isDaemonRunning };
6
- /**
7
- * Check daemon status and return connection info.
8
- */
9
- export async function checkDaemonStatus(opts) {
10
- const status = await fetchDaemonStatus({ timeout: opts?.timeout ?? 2000 });
11
- if (!status) {
12
- return { running: false, extensionConnected: false };
13
- }
14
- return {
15
- running: true,
16
- extensionConnected: status.extensionConnected,
17
- extensionVersion: status.extensionVersion,
18
- };
19
- }