@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/README.md +3 -3
- package/README.zh-CN.md +3 -3
- package/cli-manifest.json +26 -0
- package/clis/instagram/collection-delete.js +91 -0
- package/dist/src/adapter-shadow.d.ts +11 -0
- package/dist/src/adapter-shadow.js +72 -0
- package/dist/src/adapter-shadow.test.d.ts +1 -0
- package/dist/src/adapter-shadow.test.js +49 -0
- package/dist/src/browser/base-page.d.ts +6 -2
- package/dist/src/browser/base-page.js +88 -6
- package/dist/src/browser/base-page.test.js +61 -1
- package/dist/src/browser/cdp.js +48 -0
- package/dist/src/browser/cdp.test.js +23 -0
- package/dist/src/browser/dom-helpers.d.ts +1 -1
- package/dist/src/browser/dom-helpers.js +15 -3
- package/dist/src/browser/page.js +1 -1
- package/dist/src/browser/target-resolver.d.ts +8 -0
- package/dist/src/browser/target-resolver.js +75 -0
- package/dist/src/browser/verify-fixture.d.ts +1 -0
- package/dist/src/browser/verify-fixture.js +18 -0
- package/dist/src/browser/verify-fixture.test.js +16 -1
- package/dist/src/build-manifest.d.ts +68 -33
- package/dist/src/build-manifest.js +175 -29
- package/dist/src/build-manifest.test.js +75 -1
- package/dist/src/cli.js +10 -7
- package/dist/src/cli.test.js +58 -0
- package/dist/src/doctor.d.ts +2 -0
- package/dist/src/doctor.js +7 -0
- package/dist/src/doctor.test.js +36 -1
- package/dist/src/manifest-types.d.ts +39 -0
- package/dist/src/manifest-types.js +9 -0
- package/package.json +2 -2
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
|
|
1811
|
-
const
|
|
1812
|
-
|
|
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
|
|
1862
|
-
?
|
|
1864
|
+
const fixtureArgs = explicitArgs !== undefined
|
|
1865
|
+
? explicitArgs
|
|
1863
1866
|
: (hasLimitArg ? { limit: 3 } : undefined);
|
|
1864
|
-
const derived = deriveFixture(rows,
|
|
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).`);
|
package/dist/src/cli.test.js
CHANGED
|
@@ -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(() => { });
|
package/dist/src/doctor.d.ts
CHANGED
|
@@ -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
|
/**
|
package/dist/src/doctor.js
CHANGED
|
@@ -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
|
}
|
package/dist/src/doctor.test.js
CHANGED
|
@@ -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.
|
|
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": "
|
|
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",
|