@jackwener/opencli 1.7.10 → 1.7.11

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/src/cli.js CHANGED
@@ -1759,6 +1759,7 @@ cli({
1759
1759
  fs.mkdirSync(dir, { recursive: true });
1760
1760
  fs.writeFileSync(filePath, template, 'utf-8');
1761
1761
  console.log(`Created: ${filePath}`);
1762
+ console.log('First time on this site? Run: opencli browser analyze <url>');
1762
1763
  console.log(`Edit the file to implement your adapter, then run: opencli browser verify ${name}`);
1763
1764
  }
1764
1765
  catch (err) {
@@ -1773,6 +1774,7 @@ cli({
1773
1774
  .option('--update-fixture', 'Overwrite an existing fixture with one derived from current output')
1774
1775
  .option('--no-fixture', 'Ignore any fixture file for this run (no value-level validation)')
1775
1776
  .option('--strict-memory', 'Fail (not just warn) when ~/.opencli/sites/<site>/endpoints.json or notes.md is missing')
1777
+ .option('--seed-args <value>', 'Seed args when no fixture exists; use JSON array/object for multiple args or flags')
1776
1778
  .option('--trace <mode>', 'Trace capture for the adapter subprocess: off, on, retain-on-failure', 'off')
1777
1779
  .description('Execute an adapter and validate output; uses fixture at ~/.opencli/sites/<site>/verify/<cmd>.json when present')
1778
1780
  .action(async (name, opts = {}) => {
@@ -1790,7 +1792,7 @@ cli({
1790
1792
  return;
1791
1793
  }
1792
1794
  const { execFileSync } = await import('node:child_process');
1793
- const { loadFixture, writeFixture, deriveFixture, validateRows, fixturePath, expandFixtureArgs } = await import('./browser/verify-fixture.js');
1795
+ const { loadFixture, writeFixture, deriveFixture, validateRows, fixturePath, expandFixtureArgs, parseSeedArgs } = await import('./browser/verify-fixture.js');
1794
1796
  const filePath = path.join(os.homedir(), '.opencli', 'clis', site, `${command}.js`);
1795
1797
  if (!fs.existsSync(filePath)) {
1796
1798
  console.error(`Adapter not found: ${filePath}`);
@@ -1807,9 +1809,10 @@ cli({
1807
1809
  // - array form ["123", "--limit", "3"] → verbatim (for positional subjects)
1808
1810
  const adapterSrc = fs.readFileSync(filePath, 'utf-8');
1809
1811
  const hasLimitArg = /['"]limit['"]/.test(adapterSrc);
1810
- const fixtureArgs = fixture?.args;
1811
- const cliArgs = expandFixtureArgs(fixtureArgs);
1812
- if (cliArgs.length === 0 && hasLimitArg)
1812
+ const seedArgs = parseSeedArgs(opts.seedArgs);
1813
+ const explicitArgs = fixture?.args ?? seedArgs;
1814
+ const cliArgs = expandFixtureArgs(explicitArgs);
1815
+ if (explicitArgs === undefined && cliArgs.length === 0 && hasLimitArg)
1813
1816
  cliArgs.push('--limit', '3');
1814
1817
  const traceArgs = opts.trace && opts.trace !== 'off' ? ['--trace', opts.trace] : [];
1815
1818
  const argDisplay = [...cliArgs, ...traceArgs].join(' ');
@@ -1858,10 +1861,10 @@ cli({
1858
1861
  console.log(` Use --update-fixture to overwrite.`);
1859
1862
  }
1860
1863
  else {
1861
- const seedArgs = fixtureArgs !== undefined
1862
- ? fixtureArgs
1864
+ const fixtureArgs = explicitArgs !== undefined
1865
+ ? explicitArgs
1863
1866
  : (hasLimitArg ? { limit: 3 } : undefined);
1864
- const derived = deriveFixture(rows, seedArgs);
1867
+ const derived = deriveFixture(rows, fixtureArgs);
1865
1868
  const p = writeFixture(site, command, derived);
1866
1869
  console.log(`\n ${fixture ? '↻ Updated' : '✎ Wrote'} fixture: ${p}`);
1867
1870
  console.log(` Review and hand-tune the derived expectations (add patterns / notEmpty, tighten rowCount).`);
@@ -151,6 +151,64 @@ describe('browser verify', () => {
151
151
  fs.rmSync(fakeHome, { recursive: true, force: true });
152
152
  }
153
153
  });
154
+ it('uses --seed-args when no fixture args exist', async () => {
155
+ const originalHome = process.env.HOME;
156
+ const originalUserProfile = process.env.USERPROFILE;
157
+ const fakeHome = fs.mkdtempSync(path.join(os.tmpdir(), 'opencli-browser-verify-seed-'));
158
+ process.env.HOME = fakeHome;
159
+ process.env.USERPROFILE = fakeHome;
160
+ try {
161
+ const adapterDir = path.join(fakeHome, '.opencli', 'clis', 'hn');
162
+ fs.mkdirSync(adapterDir, { recursive: true });
163
+ fs.writeFileSync(path.join(adapterDir, 'top.js'), 'export default {};\n', 'utf-8');
164
+ const program = createProgram('', '');
165
+ await program.parseAsync(['node', 'opencli', 'browser', 'verify', 'hn/top', '--no-fixture', '--seed-args', 'opencli-verify']);
166
+ expect(mockExecFileSync).toHaveBeenCalledTimes(1);
167
+ const [, execArgs] = mockExecFileSync.mock.calls[0];
168
+ expect(execArgs.slice(-5)).toEqual(['hn', 'top', 'opencli-verify', '--format', 'json']);
169
+ }
170
+ finally {
171
+ if (originalHome === undefined)
172
+ delete process.env.HOME;
173
+ else
174
+ process.env.HOME = originalHome;
175
+ if (originalUserProfile === undefined)
176
+ delete process.env.USERPROFILE;
177
+ else
178
+ process.env.USERPROFILE = originalUserProfile;
179
+ fs.rmSync(fakeHome, { recursive: true, force: true });
180
+ }
181
+ });
182
+ it('writes --seed-args into a starter fixture', async () => {
183
+ const originalHome = process.env.HOME;
184
+ const originalUserProfile = process.env.USERPROFILE;
185
+ const fakeHome = fs.mkdtempSync(path.join(os.tmpdir(), 'opencli-browser-verify-write-seed-'));
186
+ process.env.HOME = fakeHome;
187
+ process.env.USERPROFILE = fakeHome;
188
+ mockExecFileSync.mockReturnValue(JSON.stringify([{ title: 'ok' }]));
189
+ try {
190
+ const adapterDir = path.join(fakeHome, '.opencli', 'clis', 'hn');
191
+ fs.mkdirSync(adapterDir, { recursive: true });
192
+ fs.writeFileSync(path.join(adapterDir, 'top.js'), 'export default {};\n', 'utf-8');
193
+ const program = createProgram('', '');
194
+ await program.parseAsync(['node', 'opencli', 'browser', 'verify', 'hn/top', '--write-fixture', '--seed-args', 'opencli-verify']);
195
+ const fixtureFile = path.join(fakeHome, '.opencli', 'sites', 'hn', 'verify', 'top.json');
196
+ const fixture = JSON.parse(fs.readFileSync(fixtureFile, 'utf-8'));
197
+ expect(fixture.args).toEqual(['opencli-verify']);
198
+ expect(fixture.expect.columns).toEqual(['title']);
199
+ }
200
+ finally {
201
+ if (originalHome === undefined)
202
+ delete process.env.HOME;
203
+ else
204
+ process.env.HOME = originalHome;
205
+ if (originalUserProfile === undefined)
206
+ delete process.env.USERPROFILE;
207
+ else
208
+ process.env.USERPROFILE = originalUserProfile;
209
+ fs.rmSync(fakeHome, { recursive: true, force: true });
210
+ }
211
+ });
154
212
  });
155
213
  describe('profile list', () => {
156
214
  const stdoutSpy = vi.spyOn(console, 'log').mockImplementation(() => { });
@@ -5,6 +5,7 @@
5
5
  */
6
6
  import type { BrowserSessionInfo } from './types.js';
7
7
  import type { BrowserProfileStatus } from './browser/daemon-client.js';
8
+ import { type AdapterShadow } from './adapter-shadow.js';
8
9
  export type DoctorOptions = {
9
10
  yes?: boolean;
10
11
  live?: boolean;
@@ -29,6 +30,7 @@ export type DoctorReport = {
29
30
  connectivity?: ConnectivityResult;
30
31
  sessions?: BrowserSessionInfo[];
31
32
  profiles?: BrowserProfileStatus[];
33
+ adapterShadows?: AdapterShadow[];
32
34
  issues: string[];
33
35
  };
34
36
  /**
@@ -12,6 +12,7 @@ import { getRuntimeLabel } from './runtime-detect.js';
12
12
  import { getCachedLatestExtensionVersion } from './update-check.js';
13
13
  import { aliasForContextId, loadProfileConfig } from './browser/profile.js';
14
14
  import { formatDaemonVersion, isDaemonStale, staleDaemonIssue } from './browser/daemon-version.js';
15
+ import { findShadowedUserAdapters, formatAdapterShadowIssue } from './adapter-shadow.js';
15
16
  const DOCTOR_LIVE_TIMEOUT_SECONDS = 8;
16
17
  /** Parse a semver string into [major, minor, patch]. Returns null on invalid input. */
17
18
  function parseSemver(v) {
@@ -107,6 +108,7 @@ export async function runBrowserDoctor(opts = {}) {
107
108
  }
108
109
  }
109
110
  const extensionVersion = health.status?.extensionVersion;
111
+ const adapterShadows = findShadowedUserAdapters();
110
112
  const issues = [];
111
113
  if (daemonFlaky) {
112
114
  issues.push('Daemon connectivity is unstable. The live browser test succeeded, but the daemon was no longer running immediately afterward.\n' +
@@ -171,6 +173,9 @@ export async function runBrowserDoctor(opts = {}) {
171
173
  issues.push(`Extension update available: v${extensionVersion} → v${latestExtensionVersion}\n` +
172
174
  ' Download from: https://github.com/jackwener/opencli/releases');
173
175
  }
176
+ if (adapterShadows.length > 0) {
177
+ issues.push(formatAdapterShadowIssue(adapterShadows));
178
+ }
174
179
  return {
175
180
  cliVersion: opts.cliVersion,
176
181
  daemonRunning,
@@ -184,6 +189,7 @@ export async function runBrowserDoctor(opts = {}) {
184
189
  connectivity,
185
190
  sessions,
186
191
  profiles,
192
+ adapterShadows,
187
193
  issues,
188
194
  };
189
195
  }
@@ -279,6 +285,7 @@ export function renderBrowserDoctorReport(report) {
279
285
  }
280
286
  else if (report.daemonRunning && report.extensionConnected) {
281
287
  lines.push('', styleText('green', 'Everything looks good!'));
288
+ lines.push(styleText('dim', 'Tip: writing a new adapter? Run `opencli browser analyze <url>` for one-shot site recon.'));
282
289
  }
283
290
  return lines.join('\n');
284
291
  }
@@ -1,9 +1,10 @@
1
1
  import { beforeEach, describe, expect, it, vi } from 'vitest';
2
- const { mockGetDaemonHealth, mockListSessions, mockConnect, mockClose } = vi.hoisted(() => ({
2
+ const { mockGetDaemonHealth, mockListSessions, mockConnect, mockClose, mockFindShadowedUserAdapters } = vi.hoisted(() => ({
3
3
  mockGetDaemonHealth: vi.fn(),
4
4
  mockListSessions: vi.fn(),
5
5
  mockConnect: vi.fn(),
6
6
  mockClose: vi.fn(),
7
+ mockFindShadowedUserAdapters: vi.fn(),
7
8
  }));
8
9
  vi.mock('./browser/daemon-client.js', () => ({
9
10
  getDaemonHealth: mockGetDaemonHealth,
@@ -15,11 +16,19 @@ vi.mock('./browser/index.js', () => ({
15
16
  close = mockClose;
16
17
  },
17
18
  }));
19
+ vi.mock('./adapter-shadow.js', async () => {
20
+ const actual = await vi.importActual('./adapter-shadow.js');
21
+ return {
22
+ ...actual,
23
+ findShadowedUserAdapters: mockFindShadowedUserAdapters,
24
+ };
25
+ });
18
26
  import { renderBrowserDoctorReport, runBrowserDoctor } from './doctor.js';
19
27
  describe('doctor report rendering', () => {
20
28
  const strip = (s) => s.replace(/\x1b\[[0-9;]*m/g, '');
21
29
  beforeEach(() => {
22
30
  vi.clearAllMocks();
31
+ mockFindShadowedUserAdapters.mockReturnValue([]);
23
32
  });
24
33
  it('renders OK-style report when daemon and extension connected', () => {
25
34
  const text = strip(renderBrowserDoctorReport({
@@ -34,6 +43,7 @@ describe('doctor report rendering', () => {
34
43
  expect(text).toContain('(v1.7.9)');
35
44
  expect(text).toContain('[OK] Extension: connected (v1.6.8)');
36
45
  expect(text).toContain('Everything looks good!');
46
+ expect(text).toContain('opencli browser analyze <url>');
37
47
  });
38
48
  it('renders a warning when daemon version is stale', () => {
39
49
  const text = strip(renderBrowserDoctorReport({
@@ -273,6 +283,31 @@ describe('doctor report rendering', () => {
273
283
  expect.stringContaining('Stale daemon detected: daemon v1.7.6 != CLI v1.7.9'),
274
284
  ]));
275
285
  });
286
+ it('reports local adapter shadows as a warning issue', async () => {
287
+ const status = {
288
+ state: 'ready',
289
+ status: {
290
+ daemonVersion: '1.7.9',
291
+ extensionConnected: true,
292
+ extensionVersion: '1.0.3',
293
+ },
294
+ };
295
+ mockGetDaemonHealth
296
+ .mockResolvedValueOnce(status)
297
+ .mockResolvedValueOnce(status);
298
+ mockFindShadowedUserAdapters.mockReturnValueOnce([
299
+ {
300
+ name: 'instagram/saved',
301
+ userPath: '/home/me/.opencli/clis/instagram/saved.js',
302
+ builtinPath: '/pkg/clis/instagram/saved.js',
303
+ },
304
+ ]);
305
+ const report = await runBrowserDoctor({ live: false, cliVersion: '1.7.9' });
306
+ expect(report.adapterShadows).toHaveLength(1);
307
+ expect(report.issues).toEqual(expect.arrayContaining([
308
+ expect.stringContaining('Local adapter overrides shadow packaged adapters'),
309
+ ]));
310
+ });
276
311
  it('reports profile-required when multiple profiles are connected without a selection', async () => {
277
312
  const status = {
278
313
  state: 'profile-required',
@@ -0,0 +1,39 @@
1
+ /**
2
+ * Shared manifest types — kept in their own module so both runtime code
3
+ * (discovery.ts) and the build-time compiler (build-manifest.ts) can
4
+ * import them without pulling each other in. This is what lets us
5
+ * exclude `src/build-manifest.ts` from `tsc`'s emit set: the only thing
6
+ * runtime code needs from build-manifest is the `ManifestEntry` type,
7
+ * and that lives here.
8
+ */
9
+ export interface ManifestEntry {
10
+ site: string;
11
+ name: string;
12
+ aliases?: string[];
13
+ description: string;
14
+ domain?: string;
15
+ strategy: string;
16
+ browser: boolean;
17
+ args: Array<{
18
+ name: string;
19
+ type?: string;
20
+ default?: unknown;
21
+ required?: boolean;
22
+ valueRequired?: boolean;
23
+ positional?: boolean;
24
+ help?: string;
25
+ choices?: string[];
26
+ }>;
27
+ columns?: string[];
28
+ pipeline?: Record<string, unknown>[];
29
+ timeout?: number;
30
+ deprecated?: boolean | string;
31
+ replacedBy?: string;
32
+ type: 'js';
33
+ /** Relative path from clis/ dir, e.g. 'bilibili/search.js' */
34
+ modulePath?: string;
35
+ /** Relative path to the source file from clis/ dir (e.g. 'site/cmd.js') */
36
+ sourceFile?: string;
37
+ /** Pre-navigation control — see CliCommand.navigateBefore */
38
+ navigateBefore?: boolean | string;
39
+ }
@@ -0,0 +1,9 @@
1
+ /**
2
+ * Shared manifest types — kept in their own module so both runtime code
3
+ * (discovery.ts) and the build-time compiler (build-manifest.ts) can
4
+ * import them without pulling each other in. This is what lets us
5
+ * exclude `src/build-manifest.ts` from `tsc`'s emit set: the only thing
6
+ * runtime code needs from build-manifest is the `ManifestEntry` type,
7
+ * and that lives here.
8
+ */
9
+ export {};
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@jackwener/opencli",
3
- "version": "1.7.10",
3
+ "version": "1.7.11",
4
4
  "publishConfig": {
5
5
  "access": "public"
6
6
  },
@@ -42,7 +42,7 @@
42
42
  "dev": "tsx src/main.ts",
43
43
  "dev:bun": "bun src/main.ts",
44
44
  "build": "npm run clean-dist && tsc && npm run copy-yaml && npm run build-manifest",
45
- "build-manifest": "node dist/src/build-manifest.js",
45
+ "build-manifest": "tsx src/build-manifest.ts",
46
46
  "clean-dist": "node scripts/clean-dist.cjs",
47
47
  "copy-yaml": "node scripts/copy-yaml.cjs",
48
48
  "start": "node dist/src/main.js",