@jackwener/opencli 1.7.3 → 1.7.5

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 (197) hide show
  1. package/README.md +81 -59
  2. package/README.zh-CN.md +93 -67
  3. package/cli-manifest.json +5015 -2975
  4. package/clis/antigravity/serve.js +71 -25
  5. package/clis/baidu-scholar/search.js +87 -0
  6. package/clis/baidu-scholar/search.test.js +23 -0
  7. package/clis/bilibili/favorite.js +18 -13
  8. package/clis/binance/depth.js +3 -4
  9. package/clis/boss/utils.js +2 -3
  10. package/clis/chatgpt-app/ax.js +6 -3
  11. package/clis/deepseek/ask.js +74 -0
  12. package/clis/deepseek/history.js +25 -0
  13. package/clis/deepseek/new.js +20 -0
  14. package/clis/deepseek/read.js +22 -0
  15. package/clis/deepseek/status.js +24 -0
  16. package/clis/deepseek/utils.js +208 -0
  17. package/clis/douban/search.js +1 -0
  18. package/clis/douban/search.test.js +11 -0
  19. package/clis/douban/subject.js +20 -93
  20. package/clis/douban/subject.test.js +11 -0
  21. package/clis/douban/utils.js +250 -8
  22. package/clis/douban/utils.test.js +179 -4
  23. package/clis/doubao/utils.js +319 -130
  24. package/clis/doubao/utils.test.js +241 -2
  25. package/clis/eastmoney/_secid.js +78 -0
  26. package/clis/eastmoney/announcement.js +52 -0
  27. package/clis/eastmoney/convertible.js +73 -0
  28. package/clis/eastmoney/etf.js +65 -0
  29. package/clis/eastmoney/holders.js +78 -0
  30. package/clis/eastmoney/hot-rank.js +50 -0
  31. package/clis/eastmoney/hot-rank.test.js +59 -0
  32. package/clis/eastmoney/index-board.js +96 -0
  33. package/clis/eastmoney/kline.js +87 -0
  34. package/clis/eastmoney/kuaixun.js +54 -0
  35. package/clis/eastmoney/longhu.js +67 -0
  36. package/clis/eastmoney/money-flow.js +78 -0
  37. package/clis/eastmoney/northbound.js +57 -0
  38. package/clis/eastmoney/quote.js +107 -0
  39. package/clis/eastmoney/rank.js +94 -0
  40. package/clis/eastmoney/sectors.js +76 -0
  41. package/clis/google-scholar/search.js +58 -0
  42. package/clis/google-scholar/search.test.js +23 -0
  43. package/clis/gov-law/commands.test.js +39 -0
  44. package/clis/gov-law/recent.js +22 -0
  45. package/clis/gov-law/search.js +41 -0
  46. package/clis/gov-law/shared.js +51 -0
  47. package/clis/gov-policy/commands.test.js +27 -0
  48. package/clis/gov-policy/recent.js +47 -0
  49. package/clis/gov-policy/search.js +48 -0
  50. package/clis/grok/image.test.ts +107 -0
  51. package/clis/grok/image.ts +356 -0
  52. package/clis/nowcoder/companies.js +23 -0
  53. package/clis/nowcoder/creators.js +27 -0
  54. package/clis/nowcoder/detail.js +61 -0
  55. package/clis/nowcoder/experience.js +36 -0
  56. package/clis/nowcoder/hot.js +24 -0
  57. package/clis/nowcoder/jobs.js +21 -0
  58. package/clis/nowcoder/notifications.js +29 -0
  59. package/clis/nowcoder/papers.js +40 -0
  60. package/clis/nowcoder/practice.js +37 -0
  61. package/clis/nowcoder/recommend.js +30 -0
  62. package/clis/nowcoder/referral.js +39 -0
  63. package/clis/nowcoder/salary.js +40 -0
  64. package/clis/nowcoder/search.js +49 -0
  65. package/clis/nowcoder/suggest.js +33 -0
  66. package/clis/nowcoder/topics.js +27 -0
  67. package/clis/nowcoder/trending.js +25 -0
  68. package/clis/tdx/hot-rank.js +47 -0
  69. package/clis/tdx/hot-rank.test.js +59 -0
  70. package/clis/ths/hot-rank.js +49 -0
  71. package/clis/ths/hot-rank.test.js +64 -0
  72. package/clis/twitter/bookmarks.js +2 -1
  73. package/clis/twitter/list-add.js +337 -0
  74. package/clis/twitter/list-add.test.js +15 -0
  75. package/clis/twitter/list-remove.js +297 -0
  76. package/clis/twitter/list-remove.test.js +14 -0
  77. package/clis/twitter/list-tweets.js +185 -0
  78. package/clis/twitter/list-tweets.test.js +108 -0
  79. package/clis/twitter/lists.js +134 -47
  80. package/clis/twitter/lists.test.js +105 -38
  81. package/clis/uiverse/_shared.js +368 -0
  82. package/clis/uiverse/_shared.test.js +55 -0
  83. package/clis/uiverse/code.js +47 -0
  84. package/clis/uiverse/preview.js +71 -0
  85. package/clis/wanfang/search.js +66 -0
  86. package/clis/wanfang/search.test.js +23 -0
  87. package/clis/web/read.js +1 -1
  88. package/clis/weixin/download.js +3 -2
  89. package/clis/xiaohongshu/comments.js +2 -2
  90. package/clis/xiaohongshu/comments.test.js +46 -25
  91. package/clis/xiaohongshu/download.js +6 -7
  92. package/clis/xiaohongshu/download.test.js +17 -5
  93. package/clis/xiaohongshu/note-helpers.js +46 -12
  94. package/clis/xiaohongshu/note.js +3 -5
  95. package/clis/xiaohongshu/note.test.js +52 -25
  96. package/clis/xiaohongshu/publish.js +149 -28
  97. package/clis/xiaohongshu/publish.test.js +319 -6
  98. package/clis/xiaoyuzhou/auth.js +303 -0
  99. package/clis/xiaoyuzhou/auth.test.js +124 -0
  100. package/clis/xiaoyuzhou/download.js +53 -0
  101. package/clis/xiaoyuzhou/download.test.js +135 -0
  102. package/clis/xiaoyuzhou/episode.js +9 -4
  103. package/clis/xiaoyuzhou/podcast-episodes.js +15 -11
  104. package/clis/xiaoyuzhou/podcast.js +9 -4
  105. package/clis/xiaoyuzhou/transcript.js +76 -0
  106. package/clis/xiaoyuzhou/transcript.test.js +195 -0
  107. package/clis/xiaoyuzhou/utils.js +0 -40
  108. package/clis/xiaoyuzhou/utils.test.js +15 -75
  109. package/clis/youtube/feed.js +120 -0
  110. package/clis/youtube/history.js +118 -0
  111. package/clis/youtube/like.js +62 -0
  112. package/clis/youtube/playlist.js +97 -0
  113. package/clis/youtube/subscribe.js +71 -0
  114. package/clis/youtube/subscriptions.js +57 -0
  115. package/clis/youtube/unlike.js +62 -0
  116. package/clis/youtube/unsubscribe.js +71 -0
  117. package/clis/youtube/utils.js +122 -0
  118. package/clis/youtube/utils.test.js +32 -1
  119. package/clis/youtube/watch-later.js +76 -0
  120. package/clis/zsxq/dynamics.js +1 -1
  121. package/clis/zsxq/utils.js +6 -3
  122. package/clis/zsxq/utils.test.js +31 -0
  123. package/dist/src/browser/base-page.d.ts +1 -1
  124. package/dist/src/browser/base-page.js +25 -5
  125. package/dist/src/browser/bridge.d.ts +3 -0
  126. package/dist/src/browser/bridge.js +52 -15
  127. package/dist/src/browser/cdp.js +2 -1
  128. package/dist/src/browser/daemon-client.d.ts +7 -4
  129. package/dist/src/browser/daemon-client.js +6 -1
  130. package/dist/src/browser/daemon-client.test.js +40 -1
  131. package/dist/src/browser/dom-snapshot.js +20 -3
  132. package/dist/src/browser/page.d.ts +18 -5
  133. package/dist/src/browser/page.js +96 -15
  134. package/dist/src/browser/page.test.js +158 -1
  135. package/dist/src/browser/target-errors.d.ts +23 -0
  136. package/dist/src/browser/target-errors.js +29 -0
  137. package/dist/src/browser/target-errors.test.js +61 -0
  138. package/dist/src/browser/target-resolver.d.ts +57 -0
  139. package/dist/src/browser/target-resolver.js +298 -0
  140. package/dist/src/browser/target-resolver.test.js +43 -0
  141. package/dist/src/browser.test.js +38 -1
  142. package/dist/src/cli.js +272 -187
  143. package/dist/src/cli.test.js +167 -90
  144. package/dist/src/commanderAdapter.d.ts +0 -1
  145. package/dist/src/commanderAdapter.js +2 -16
  146. package/dist/src/commanderAdapter.test.js +1 -1
  147. package/dist/src/commands/daemon.d.ts +4 -2
  148. package/dist/src/commands/daemon.js +22 -2
  149. package/dist/src/commands/daemon.test.js +65 -2
  150. package/dist/src/completion-shared.js +2 -5
  151. package/dist/src/daemon.js +10 -0
  152. package/dist/src/doctor.d.ts +1 -0
  153. package/dist/src/doctor.js +32 -9
  154. package/dist/src/doctor.test.js +28 -12
  155. package/dist/src/download/article-download.d.ts +1 -0
  156. package/dist/src/download/article-download.js +3 -0
  157. package/dist/src/download/article-download.test.js +39 -0
  158. package/dist/src/external-clis.yaml +2 -2
  159. package/dist/src/logger.d.ts +2 -2
  160. package/dist/src/logger.js +3 -3
  161. package/dist/src/output.js +1 -5
  162. package/dist/src/output.test.js +0 -21
  163. package/dist/src/pipeline/steps/transform.js +1 -1
  164. package/dist/src/pipeline/template.d.ts +1 -0
  165. package/dist/src/pipeline/template.js +11 -3
  166. package/dist/src/pipeline/template.test.js +3 -0
  167. package/dist/src/pipeline/transform.test.js +14 -0
  168. package/dist/src/plugin.d.ts +8 -9
  169. package/dist/src/plugin.js +24 -28
  170. package/dist/src/plugin.test.js +16 -60
  171. package/dist/src/registry.d.ts +1 -0
  172. package/dist/src/registry.js +3 -2
  173. package/dist/src/registry.test.js +22 -0
  174. package/dist/src/types.d.ts +15 -6
  175. package/package.json +1 -1
  176. package/clis/twitter/lists-parser.js +0 -77
  177. package/clis/twitter/lists.d.ts +0 -5
  178. package/dist/src/cascade.d.ts +0 -46
  179. package/dist/src/cascade.js +0 -135
  180. package/dist/src/explore.d.ts +0 -99
  181. package/dist/src/explore.js +0 -402
  182. package/dist/src/generate-verified.d.ts +0 -105
  183. package/dist/src/generate-verified.js +0 -696
  184. package/dist/src/generate-verified.test.js +0 -925
  185. package/dist/src/generate.d.ts +0 -46
  186. package/dist/src/generate.js +0 -117
  187. package/dist/src/record.d.ts +0 -96
  188. package/dist/src/record.js +0 -657
  189. package/dist/src/record.test.js +0 -293
  190. package/dist/src/skill-generate.d.ts +0 -30
  191. package/dist/src/skill-generate.js +0 -75
  192. package/dist/src/skill-generate.test.js +0 -173
  193. package/dist/src/synthesize.d.ts +0 -97
  194. package/dist/src/synthesize.js +0 -208
  195. /package/dist/src/{generate-verified.test.d.ts → browser/target-errors.test.d.ts} +0 -0
  196. /package/dist/src/{record.test.d.ts → browser/target-resolver.test.d.ts} +0 -0
  197. /package/dist/src/{skill-generate.test.d.ts → download/article-download.test.d.ts} +0 -0
@@ -25,10 +25,11 @@ describe('doctor report rendering', () => {
25
25
  const text = strip(renderBrowserDoctorReport({
26
26
  daemonRunning: true,
27
27
  extensionConnected: true,
28
+ extensionVersion: '1.6.8',
28
29
  issues: [],
29
30
  }));
30
31
  expect(text).toContain('[OK] Daemon: running on port 19825');
31
- expect(text).toContain('[OK] Extension: connected');
32
+ expect(text).toContain('[OK] Extension: connected (v1.6.8)');
32
33
  expect(text).toContain('Everything looks good!');
33
34
  });
34
35
  it('renders MISSING when daemon not running', () => {
@@ -50,6 +51,16 @@ describe('doctor report rendering', () => {
50
51
  expect(text).toContain('[OK] Daemon: running on port 19825');
51
52
  expect(text).toContain('[MISSING] Extension: not connected');
52
53
  });
54
+ it('renders a warning when the extension version is unknown', () => {
55
+ const text = strip(renderBrowserDoctorReport({
56
+ daemonRunning: true,
57
+ extensionConnected: true,
58
+ issues: ['Extension is connected but did not report a version.'],
59
+ }));
60
+ expect(text).toContain('[WARN] Extension: connected (version unknown)');
61
+ expect(text).toContain('Extension is connected but did not report a version.');
62
+ expect(text).not.toContain('Everything looks good!');
63
+ });
53
64
  it('renders connectivity OK when live test succeeds', () => {
54
65
  const text = strip(renderBrowserDoctorReport({
55
66
  daemonRunning: true,
@@ -90,12 +101,8 @@ describe('doctor report rendering', () => {
90
101
  expect(text).toContain('Daemon connectivity is unstable.');
91
102
  });
92
103
  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
104
  mockGetDaemonHealth.mockResolvedValueOnce({ state: 'stopped', status: null });
96
- // Auto-start fails
97
105
  mockConnect.mockRejectedValueOnce(new Error('Could not start daemon'));
98
- // Final: still stopped
99
106
  mockGetDaemonHealth.mockResolvedValueOnce({ state: 'stopped', status: null });
100
107
  const report = await runBrowserDoctor({ live: false });
101
108
  expect(report.daemonRunning).toBe(false);
@@ -106,12 +113,10 @@ describe('doctor report rendering', () => {
106
113
  ]));
107
114
  });
108
115
  it('reports flapping when live check succeeds but final status shows extension disconnected', async () => {
109
- // Live check succeeds
110
116
  mockConnect.mockResolvedValueOnce({
111
117
  evaluate: vi.fn().mockResolvedValue(2),
112
118
  });
113
119
  mockClose.mockResolvedValueOnce(undefined);
114
- // After live check, getDaemonHealth shows no-extension
115
120
  mockGetDaemonHealth.mockResolvedValueOnce({ state: 'no-extension', status: { extensionConnected: false } });
116
121
  const report = await runBrowserDoctor({ live: true });
117
122
  expect(report.daemonRunning).toBe(true);
@@ -122,12 +127,10 @@ describe('doctor report rendering', () => {
122
127
  ]));
123
128
  });
124
129
  it('reports daemon flapping when live check succeeds but daemon disappears afterward', async () => {
125
- // Live check succeeds
126
130
  mockConnect.mockResolvedValueOnce({
127
131
  evaluate: vi.fn().mockResolvedValue(2),
128
132
  });
129
133
  mockClose.mockResolvedValueOnce(undefined);
130
- // After live check, getDaemonHealth shows stopped
131
134
  mockGetDaemonHealth.mockResolvedValueOnce({ state: 'stopped', status: null });
132
135
  const report = await runBrowserDoctor({ live: true });
133
136
  expect(report.daemonRunning).toBe(false);
@@ -151,14 +154,27 @@ describe('doctor report rendering', () => {
151
154
  expect(timeoutSeen).toBe(8);
152
155
  });
153
156
  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
157
  mockGetDaemonHealth.mockResolvedValueOnce({ state: 'no-extension', status: { extensionConnected: false } });
156
- // Final status: same
157
158
  mockGetDaemonHealth.mockResolvedValueOnce({ state: 'no-extension', status: { extensionConnected: false } });
158
159
  const report = await runBrowserDoctor({ live: false });
159
- // Should NOT have tried auto-start since daemon was already running
160
160
  expect(mockConnect).not.toHaveBeenCalled();
161
161
  expect(report.daemonRunning).toBe(true);
162
162
  expect(report.extensionConnected).toBe(false);
163
163
  });
164
+ it('reports an issue when the extension is connected but does not report a version', async () => {
165
+ const status = {
166
+ state: 'ready',
167
+ status: {
168
+ extensionConnected: true,
169
+ extensionVersion: undefined,
170
+ },
171
+ };
172
+ mockGetDaemonHealth
173
+ .mockResolvedValueOnce(status)
174
+ .mockResolvedValueOnce(status);
175
+ const report = await runBrowserDoctor({ live: false });
176
+ expect(report.issues).toEqual(expect.arrayContaining([
177
+ expect.stringContaining('did not report a version'),
178
+ ]));
179
+ });
164
180
  });
@@ -44,6 +44,7 @@ export interface ArticleDownloadResult {
44
44
  publish_time: string;
45
45
  status: string;
46
46
  size: string;
47
+ saved: string;
47
48
  }
48
49
  /**
49
50
  * Download an article to Markdown with optional image localization.
@@ -129,6 +129,7 @@ export async function downloadArticle(data, options) {
129
129
  publish_time: '-',
130
130
  status: 'failed — no title',
131
131
  size: '-',
132
+ saved: '-',
132
133
  }];
133
134
  }
134
135
  if (!data.contentHtml) {
@@ -138,6 +139,7 @@ export async function downloadArticle(data, options) {
138
139
  publish_time: data.publishTime || '-',
139
140
  status: 'failed — no content',
140
141
  size: '-',
142
+ saved: '-',
141
143
  }];
142
144
  }
143
145
  // Convert HTML to Markdown
@@ -174,5 +176,6 @@ export async function downloadArticle(data, options) {
174
176
  publish_time: data.publishTime || '-',
175
177
  status: 'success',
176
178
  size: formatBytes(size),
179
+ saved: filePath,
177
180
  }];
178
181
  }
@@ -0,0 +1,39 @@
1
+ import * as fs from 'node:fs';
2
+ import * as os from 'node:os';
3
+ import * as path from 'node:path';
4
+ import { afterEach, describe, expect, it } from 'vitest';
5
+ import { downloadArticle } from './article-download.js';
6
+ const tempDirs = [];
7
+ afterEach(() => {
8
+ for (const dir of tempDirs) {
9
+ try {
10
+ fs.rmSync(dir, { recursive: true, force: true });
11
+ }
12
+ catch {
13
+ // Ignore cleanup errors in tests.
14
+ }
15
+ }
16
+ tempDirs.length = 0;
17
+ });
18
+ describe('downloadArticle', () => {
19
+ it('returns the saved markdown file path on success', async () => {
20
+ const tempDir = await fs.promises.mkdtemp(path.join(os.tmpdir(), 'opencli-article-'));
21
+ tempDirs.push(tempDir);
22
+ const result = await downloadArticle({
23
+ title: 'Test Article',
24
+ author: 'Author',
25
+ publishTime: '2026-04-20 12:00:00',
26
+ sourceUrl: 'https://example.com/article',
27
+ contentHtml: '<p>Hello world</p>',
28
+ }, {
29
+ output: tempDir,
30
+ downloadImages: false,
31
+ });
32
+ expect(result).toHaveLength(1);
33
+ expect(result[0].status).toBe('success');
34
+ expect(result[0].saved).toMatch(new RegExp(`^${tempDir.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')}`));
35
+ expect(path.extname(result[0].saved)).toBe('.md');
36
+ expect(fs.existsSync(result[0].saved)).toBe(true);
37
+ expect(fs.readFileSync(result[0].saved, 'utf8')).toContain('Hello world');
38
+ });
39
+ });
@@ -36,8 +36,8 @@
36
36
  homepage: "https://github.com/DingTalk-Real-AI/dingtalk-workspace-cli"
37
37
  tags: [dingtalk, collaboration, productivity, ai-agent]
38
38
  install:
39
- mac: "curl -fsSL https://raw.githubusercontent.com/DingTalk-Real-AI/dingtalk-workspace-cli/main/scripts/install.sh | sh"
40
- linux: "curl -fsSL https://raw.githubusercontent.com/DingTalk-Real-AI/dingtalk-workspace-cli/main/scripts/install.sh | sh"
39
+ mac: "npm install -g dingtalk-workspace-cli"
40
+ linux: "npm install -g dingtalk-workspace-cli"
41
41
 
42
42
  - name: wecom-cli
43
43
  binary: wecom-cli
@@ -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 (shown when -v flag, OPENCLI_VERBOSE, or DEBUG=opencli is set) */
18
+ /** Verbose output (shown when -v flag or OPENCLI_VERBOSE is set) */
19
19
  verbose(msg: string): void;
20
- /** @deprecated Use log.verbose() instead. Kept as alias for backward compatibility. */
20
+ /** Alias for verbose output. */
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;
@@ -6,7 +6,7 @@
6
6
  */
7
7
  import { styleText } from 'node:util';
8
8
  function isVerbose() {
9
- return !!process.env.OPENCLI_VERBOSE || !!process.env.DEBUG?.includes('opencli');
9
+ return !!process.env.OPENCLI_VERBOSE;
10
10
  }
11
11
  export const log = {
12
12
  /** Informational message (always shown) */
@@ -29,13 +29,13 @@ export const log = {
29
29
  error(msg) {
30
30
  process.stderr.write(`${styleText('red', '✖')} ${msg}\n`);
31
31
  },
32
- /** Verbose output (shown when -v flag, OPENCLI_VERBOSE, or DEBUG=opencli is set) */
32
+ /** Verbose output (shown when -v flag or OPENCLI_VERBOSE is set) */
33
33
  verbose(msg) {
34
34
  if (isVerbose()) {
35
35
  process.stderr.write(`${styleText('dim', '[verbose]')} ${msg}\n`);
36
36
  }
37
37
  },
38
- /** @deprecated Use log.verbose() instead. Kept as alias for backward compatibility. */
38
+ /** Alias for verbose output. */
39
39
  debug(msg) {
40
40
  this.verbose(msg);
41
41
  },
@@ -17,12 +17,8 @@ function resolveColumns(rows, opts) {
17
17
  export function render(data, opts = {}) {
18
18
  let fmt = opts.fmt ?? 'table';
19
19
  // Non-TTY auto-downgrade only when format was NOT explicitly passed by user.
20
- // Priority: explicit -f (any value) > OUTPUT env var > TTY auto-detect > table
21
20
  if (!opts.fmtExplicit) {
22
- const envFmt = process.env.OUTPUT?.trim().toLowerCase();
23
- if (envFmt)
24
- fmt = envFmt;
25
- else if (fmt === 'table' && !process.stdout.isTTY)
21
+ if (fmt === 'table' && !process.stdout.isTTY)
26
22
  fmt = 'yaml';
27
23
  }
28
24
  if (data === null || data === undefined) {
@@ -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 () => {
@@ -60,13 +60,6 @@ declare function resolveStoredPluginSource(lockEntry: LockEntry | undefined, plu
60
60
  */
61
61
  type MoveDirFsOps = Pick<typeof fs, 'renameSync' | 'cpSync' | 'rmSync'>;
62
62
  declare function moveDir(src: string, dest: string, fsOps?: MoveDirFsOps): void;
63
- type PromoteDirFsOps = MoveDirFsOps & Pick<typeof fs, 'existsSync' | 'mkdirSync'>;
64
- /**
65
- * Promote a prepared staging directory into its final location.
66
- * The final path is only exposed after the directory has been fully prepared.
67
- */
68
- declare function promoteDir(stagingDir: string, dest: string, fsOps?: PromoteDirFsOps): void;
69
- declare function replaceDir(stagingDir: string, dest: string, fsOps?: PromoteDirFsOps): void;
70
63
  export interface ValidationResult {
71
64
  valid: boolean;
72
65
  errors: string[];
@@ -86,7 +79,13 @@ export declare function getCommitHash(dir: string): string | undefined;
86
79
  export declare function validatePluginStructure(pluginDir: string): ValidationResult;
87
80
  declare function installDependencies(dir: string): void;
88
81
  /**
89
- * Monorepo lifecycle: install shared deps once at repo root, then finalize each sub-plugin.
82
+ * Monorepo lifecycle: install shared deps at repo root, then install and finalize each sub-plugin.
83
+ *
84
+ * The root install covers monorepos that use npm workspaces to hoist dependencies.
85
+ * For monorepos that do NOT use workspaces, sub-plugins may declare their own
86
+ * production dependencies in their package.json. We install those per sub-plugin
87
+ * so that runtime imports (e.g. `undici`) can be resolved from the sub-plugin
88
+ * directory. When the root already satisfies all deps this is a fast no-op.
90
89
  */
91
90
  declare function postInstallMonorepoLifecycle(repoDir: string, pluginDirs: string[]): void;
92
91
  /**
@@ -143,4 +142,4 @@ declare function parseSource(source: string): ParsedSource | null;
143
142
  */
144
143
  export declare function resolveEsbuildBin(): string | null;
145
144
  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, };
145
+ 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, resolvePluginSource as _resolvePluginSource, resolveStoredPluginSource as _resolveStoredPluginSource, toStoredPluginSource as _toStoredPluginSource, toLocalPluginSource as _toLocalPluginSource, };
@@ -159,32 +159,6 @@ function createSiblingTempPath(dest, kind) {
159
159
  const suffix = `${process.pid}-${Date.now()}-${Math.random().toString(16).slice(2)}`;
160
160
  return path.join(path.dirname(dest), `.${path.basename(dest)}.${kind}-${suffix}`);
161
161
  }
162
- /**
163
- * Promote a prepared staging directory into its final location.
164
- * The final path is only exposed after the directory has been fully prepared.
165
- */
166
- function promoteDir(stagingDir, dest, fsOps = fs) {
167
- if (fsOps.existsSync(dest)) {
168
- throw new PluginError(`Destination already exists: ${dest}`);
169
- }
170
- fsOps.mkdirSync(path.dirname(dest), { recursive: true });
171
- const tempDest = createSiblingTempPath(dest, 'tmp');
172
- try {
173
- moveDir(stagingDir, tempDest, fsOps);
174
- fsOps.renameSync(tempDest, dest);
175
- }
176
- catch (err) {
177
- try {
178
- fsOps.rmSync(tempDest, { recursive: true, force: true });
179
- }
180
- catch { }
181
- throw err;
182
- }
183
- }
184
- function replaceDir(stagingDir, dest, fsOps = fs) {
185
- const replacement = beginReplaceDir(stagingDir, dest, fsOps);
186
- replacement.finalize();
187
- }
188
162
  function cloneRepoToTemp(cloneUrl) {
189
163
  const tmpCloneDir = path.join(os.tmpdir(), `opencli-clone-${process.pid}-${Date.now()}-${Math.random().toString(16).slice(2)}`);
190
164
  try {
@@ -492,6 +466,19 @@ export function validatePluginStructure(pluginDir) {
492
466
  }
493
467
  return { valid: errors.length === 0, errors };
494
468
  }
469
+ /** Check whether a directory has its own production dependencies in package.json. */
470
+ function hasOwnDependencies(dir) {
471
+ const pkgPath = path.join(dir, 'package.json');
472
+ if (!fs.existsSync(pkgPath))
473
+ return false;
474
+ try {
475
+ const pkg = JSON.parse(fs.readFileSync(pkgPath, 'utf-8'));
476
+ return pkg.dependencies != null && Object.keys(pkg.dependencies).length > 0;
477
+ }
478
+ catch {
479
+ return false;
480
+ }
481
+ }
495
482
  function installDependencies(dir) {
496
483
  const pkgJsonPath = path.join(dir, 'package.json');
497
484
  if (!fs.existsSync(pkgJsonPath))
@@ -523,11 +510,20 @@ function postInstallLifecycle(pluginDir) {
523
510
  finalizePluginRuntime(pluginDir);
524
511
  }
525
512
  /**
526
- * Monorepo lifecycle: install shared deps once at repo root, then finalize each sub-plugin.
513
+ * Monorepo lifecycle: install shared deps at repo root, then install and finalize each sub-plugin.
514
+ *
515
+ * The root install covers monorepos that use npm workspaces to hoist dependencies.
516
+ * For monorepos that do NOT use workspaces, sub-plugins may declare their own
517
+ * production dependencies in their package.json. We install those per sub-plugin
518
+ * so that runtime imports (e.g. `undici`) can be resolved from the sub-plugin
519
+ * directory. When the root already satisfies all deps this is a fast no-op.
527
520
  */
528
521
  function postInstallMonorepoLifecycle(repoDir, pluginDirs) {
529
522
  installDependencies(repoDir);
530
523
  for (const pluginDir of pluginDirs) {
524
+ if (pluginDir !== repoDir && hasOwnDependencies(pluginDir)) {
525
+ installDependencies(pluginDir);
526
+ }
531
527
  finalizePluginRuntime(pluginDir);
532
528
  }
533
529
  }
@@ -1246,4 +1242,4 @@ function transpilePluginTs(pluginDir) {
1246
1242
  log.warn(`TS transpilation setup failed: ${getErrorMessage(err)}`);
1247
1243
  }
1248
1244
  }
1249
- 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, };
1245
+ 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, 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, _resolveHostOpencliRoot, uninstallPlugin, updatePlugin, _parseSource, _updateAllPlugins, _validatePluginStructure, _writeLockFile, _writeLockFileWithFs, _isSymlinkSync, _getMonoreposDir, getLockFilePath, _installLocalPlugin, _isLocalPluginSource, _moveDir, _resolvePluginSource, _resolveStoredPluginSource, _toStoredPluginSource, _toLocalPluginSource, } = pluginModule;
15
+ const { _getCommitHash, _installDependencies, _postInstallMonorepoLifecycle, 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');
@@ -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');
@@ -910,64 +924,6 @@ describe('moveDir', () => {
910
924
  expect(rmSync).toHaveBeenCalledWith(dest, { recursive: true, force: true });
911
925
  });
912
926
  });
913
- describe('promoteDir', () => {
914
- it('cleans up temporary publish dir when final rename fails', () => {
915
- const staging = path.join(os.tmpdir(), 'opencli-promote-stage');
916
- const dest = path.join(os.tmpdir(), 'opencli-promote-dest');
917
- const publishErr = new Error('publish failed');
918
- const existsSync = vi.fn(() => false);
919
- const mkdirSync = vi.fn(() => undefined);
920
- const cpSync = vi.fn(() => undefined);
921
- const rmSync = vi.fn(() => undefined);
922
- const renameSync = vi.fn((src, _target) => {
923
- if (String(src) === staging)
924
- return;
925
- throw publishErr;
926
- });
927
- expect(() => _promoteDir(staging, dest, { existsSync, mkdirSync, renameSync, cpSync, rmSync })).toThrow(publishErr);
928
- const tempDest = renameSync.mock.calls[0][1];
929
- expect(renameSync).toHaveBeenNthCalledWith(1, staging, tempDest);
930
- expect(renameSync).toHaveBeenNthCalledWith(2, tempDest, dest);
931
- expect(rmSync).toHaveBeenCalledWith(tempDest, { recursive: true, force: true });
932
- });
933
- });
934
- describe('replaceDir', () => {
935
- it('rolls back the original destination when swap fails', () => {
936
- const staging = path.join(os.tmpdir(), 'opencli-replace-stage');
937
- const dest = path.join(os.tmpdir(), 'opencli-replace-dest');
938
- const publishErr = new Error('swap failed');
939
- const existingPaths = new Set([dest]);
940
- const existsSync = vi.fn((p) => existingPaths.has(String(p)));
941
- const mkdirSync = vi.fn(() => undefined);
942
- const cpSync = vi.fn(() => undefined);
943
- const rmSync = vi.fn(() => undefined);
944
- const renameSync = vi.fn((src, target) => {
945
- if (String(src) === staging) {
946
- existingPaths.add(String(target));
947
- return;
948
- }
949
- if (String(src) === dest) {
950
- existingPaths.delete(dest);
951
- existingPaths.add(String(target));
952
- return;
953
- }
954
- if (String(target) === dest)
955
- throw publishErr;
956
- if (existingPaths.has(String(src))) {
957
- existingPaths.delete(String(src));
958
- existingPaths.add(String(target));
959
- }
960
- });
961
- expect(() => _replaceDir(staging, dest, { existsSync, mkdirSync, renameSync, cpSync, rmSync })).toThrow(publishErr);
962
- const tempDest = renameSync.mock.calls[0][1];
963
- const backupDest = renameSync.mock.calls[1][1];
964
- expect(renameSync).toHaveBeenNthCalledWith(1, staging, tempDest);
965
- expect(renameSync).toHaveBeenNthCalledWith(2, dest, backupDest);
966
- expect(renameSync).toHaveBeenNthCalledWith(3, tempDest, dest);
967
- expect(renameSync).toHaveBeenNthCalledWith(4, backupDest, dest);
968
- expect(rmSync).toHaveBeenCalledWith(tempDest, { recursive: true, force: true });
969
- });
970
- });
971
927
  describe('installPlugin transactional staging', () => {
972
928
  const standaloneSource = 'github:user/opencli-plugin-__test-transactional-standalone__';
973
929
  const standaloneName = '__test-transactional-standalone__';
@@ -4,6 +4,7 @@
4
4
  import type { IPage } from './types.js';
5
5
  export declare enum Strategy {
6
6
  PUBLIC = "public",
7
+ LOCAL = "local",
7
8
  COOKIE = "cookie",
8
9
  HEADER = "header",
9
10
  INTERCEPT = "intercept",
@@ -4,6 +4,7 @@
4
4
  export var Strategy;
5
5
  (function (Strategy) {
6
6
  Strategy["PUBLIC"] = "public";
7
+ Strategy["LOCAL"] = "local";
7
8
  Strategy["COOKIE"] = "cookie";
8
9
  Strategy["HEADER"] = "header";
9
10
  Strategy["INTERCEPT"] = "intercept";
@@ -58,13 +59,13 @@ export function strategyLabel(cmd) {
58
59
  */
59
60
  function normalizeCommand(cmd) {
60
61
  const strategy = cmd.strategy ?? (cmd.browser === false ? Strategy.PUBLIC : Strategy.COOKIE);
61
- const browser = cmd.browser ?? (strategy !== Strategy.PUBLIC);
62
+ const browser = cmd.browser ?? (strategy !== Strategy.PUBLIC && strategy !== Strategy.LOCAL);
62
63
  let navigateBefore = cmd.navigateBefore;
63
64
  if (navigateBefore === undefined) {
64
65
  if ((strategy === Strategy.COOKIE || strategy === Strategy.HEADER) && cmd.domain) {
65
66
  navigateBefore = `https://${cmd.domain}`;
66
67
  }
67
- else if (strategy !== Strategy.PUBLIC) {
68
+ else if (strategy !== Strategy.PUBLIC && strategy !== Strategy.LOCAL) {
68
69
  // Non-PUBLIC without domain: needs authenticated browser context
69
70
  // but no specific pre-navigation URL. `true` signals this to
70
71
  // shouldUseBrowserSession without triggering resolvePreNav.