@sovovs/bycli 2.0.0 → 2.1.1

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 (71) hide show
  1. package/cli-manifest.json +169 -0
  2. package/clis/twitter/search.js +3 -3
  3. package/clis/weixin/_wechat/args.js +48 -0
  4. package/clis/weixin/_wechat/article-content.js +53 -0
  5. package/clis/weixin/_wechat/article-service.js +124 -0
  6. package/clis/weixin/_wechat/auth-session.js +142 -0
  7. package/clis/weixin/_wechat/fingerprint.js +443 -0
  8. package/clis/weixin/_wechat/fixtures/articles-auth-expired.json +3 -0
  9. package/clis/weixin/_wechat/fixtures/articles-page.json +4 -0
  10. package/clis/weixin/_wechat/fixtures/search-auth-expired.json +4 -0
  11. package/clis/weixin/_wechat/fixtures/search-success.json +7 -0
  12. package/clis/weixin/_wechat/markdown.js +29 -0
  13. package/clis/weixin/_wechat/redact.js +405 -0
  14. package/clis/weixin/_wechat/save-service.js +175 -0
  15. package/clis/weixin/_wechat/search-biz.js +102 -0
  16. package/clis/weixin/_wechat/wechat-api.js +133 -0
  17. package/clis/weixin/accounts.js +38 -0
  18. package/clis/weixin/articles.js +35 -0
  19. package/clis/weixin/download.js +5 -47
  20. package/clis/weixin/save-articles.js +175 -0
  21. package/dist/src/browser/cdp.js +3 -0
  22. package/dist/src/browser/daemon-client.d.ts +2 -0
  23. package/dist/src/browser/daemon-client.js +1 -1
  24. package/dist/src/browser/extension-capabilities.d.ts +13 -0
  25. package/dist/src/browser/extension-capabilities.js +22 -0
  26. package/dist/src/browser/extension-capabilities.test.d.ts +1 -0
  27. package/dist/src/browser/extension-version-metadata.test.d.ts +1 -0
  28. package/dist/src/browser/page.d.ts +1 -0
  29. package/dist/src/browser/page.js +20 -1
  30. package/dist/src/build-manifest.js +4 -2
  31. package/dist/src/capabilityRouting.d.ts +3 -2
  32. package/dist/src/capabilityRouting.js +10 -2
  33. package/dist/src/cli.js +1 -1
  34. package/dist/src/commanderAdapter.js +5 -5
  35. package/dist/src/daemon.js +16 -0
  36. package/dist/src/discovery.d.ts +5 -0
  37. package/dist/src/discovery.js +12 -4
  38. package/dist/src/discovery.test.d.ts +1 -0
  39. package/dist/src/download/article-download.d.ts +6 -0
  40. package/dist/src/download/article-download.js +78 -17
  41. package/dist/src/download/wechat-article.d.ts +8 -0
  42. package/dist/src/download/wechat-article.js +137 -0
  43. package/dist/src/download/wechat-article.test.d.ts +1 -0
  44. package/dist/src/execution.d.ts +5 -0
  45. package/dist/src/execution.js +269 -50
  46. package/dist/src/help.js +8 -8
  47. package/dist/src/manifest-schema.d.ts +9 -0
  48. package/dist/src/manifest-schema.js +162 -0
  49. package/dist/src/manifest-schema.test.d.ts +1 -0
  50. package/dist/src/manifest-types.d.ts +1 -1
  51. package/dist/src/observation/redaction.js +10 -4
  52. package/dist/src/recorder/highlevel/verify.d.ts +3 -0
  53. package/dist/src/recorder/highlevel/verify.js +4 -0
  54. package/dist/src/recorder/highlevel/verify.test.d.ts +1 -0
  55. package/dist/src/recorder/runner/runner-port.js +1 -0
  56. package/dist/src/recorder/runner/verify-runner-main.d.ts +23 -7
  57. package/dist/src/recorder/runner/verify-runner-main.js +92 -19
  58. package/dist/src/registry-api.d.ts +1 -1
  59. package/dist/src/registry-api.types.test.d.ts +1 -0
  60. package/dist/src/registry-transaction.d.ts +42 -0
  61. package/dist/src/registry-transaction.js +194 -0
  62. package/dist/src/registry-transaction.test.d.ts +1 -0
  63. package/dist/src/registry.d.ts +58 -16
  64. package/dist/src/registry.js +131 -15
  65. package/dist/src/serialization.d.ts +1 -1
  66. package/dist/src/serialization.js +3 -3
  67. package/dist/src/types.d.ts +2 -0
  68. package/dist/src/weixin-built-in-docs.test.d.ts +1 -0
  69. package/package.json +7 -3
  70. package/scripts/check-package-install.mjs +71 -0
  71. package/scripts/recorder.sh +0 -186
@@ -20,7 +20,11 @@ export interface Arg {
20
20
  choices?: string[];
21
21
  }
22
22
  export type CommandArgs = Record<string, any>;
23
+ export type BrowserRequirementResolver = (args: CommandArgs) => boolean;
24
+ export type BrowserDeclaration = boolean | BrowserRequirementResolver;
25
+ export type NormalizedBrowserRequirement = boolean | 'conditional';
23
26
  export type BrowserCommandFunc = (page: IPage, kwargs: CommandArgs, debug?: boolean) => Promise<unknown>;
27
+ export type ConditionalBrowserCommandFunc = (page: IPage | null, kwargs: CommandArgs, debug?: boolean) => Promise<unknown>;
24
28
  export type NonBrowserCommandFunc = (kwargs: CommandArgs, debug?: boolean) => Promise<unknown>;
25
29
  export type CommandAccess = 'read' | 'write';
26
30
  export type SiteSessionMode = 'ephemeral' | 'persistent';
@@ -28,8 +32,9 @@ export type SiteSessionMode = 'ephemeral' | 'persistent';
28
32
  * 所有已注册 adapter command 的共享元数据和运行选项。
29
33
  *
30
34
  * 这里故意不包含 `browser` 和 `func` 这类执行形态字段,因为浏览器命令和
31
- * 非浏览器命令的执行签名不同。`BrowserCliCommand` 和 `NonBrowserCliCommand`
32
- * 会在 normalize 之后扩展这个共同底座,形成最终可执行的命令类型。
35
+ * 非浏览器命令、条件浏览器命令的执行签名不同。`BrowserCliCommand`、
36
+ * `NonBrowserCliCommand` 和 `ConditionalBrowserCliCommand` 会在 normalize 之后
37
+ * 扩展这个共同底座,形成最终可执行的命令类型。
33
38
  */
34
39
  interface BaseCliCommand {
35
40
  /** 站点或命名空间名称,对应命令中的 `<site>`,例如 `devto`、`brave`。 */
@@ -81,8 +86,8 @@ interface BaseCliCommand {
81
86
  defaultFormat?: 'table' | 'plain' | 'json' | 'yaml' | 'yml' | 'md' | 'markdown' | 'csv';
82
87
  }
83
88
  export interface BrowserCliCommand extends BaseCliCommand {
84
- /** Browser commands receive an IPage. Omitted means true after normalization. */
85
- browser?: true;
89
+ /** Browser commands receive an IPage. */
90
+ browser: true;
86
91
  func?: BrowserCommandFunc;
87
92
  }
88
93
  export interface NonBrowserCliCommand extends BaseCliCommand {
@@ -90,25 +95,50 @@ export interface NonBrowserCliCommand extends BaseCliCommand {
90
95
  browser: false;
91
96
  func?: NonBrowserCommandFunc;
92
97
  }
93
- export type CliCommand = BrowserCliCommand | NonBrowserCliCommand;
98
+ export interface ConditionalBrowserCliCommand extends BaseCliCommand {
99
+ /** Browser use is resolved from the final command arguments at execution time. */
100
+ browser: 'conditional';
101
+ requiresBrowser: BrowserRequirementResolver;
102
+ func?: ConditionalBrowserCommandFunc;
103
+ }
104
+ export type CliCommand = BrowserCliCommand | NonBrowserCliCommand | ConditionalBrowserCliCommand;
94
105
  /**
95
106
  * `cli()` 注册 adapter 时使用的内部预归一化命令形态。
96
107
  *
97
108
  * adapter 作者传入的是 `CliOptions`,它的 TypeScript union 会保证公开调用点足够精确。
98
- * registry 内部会先把这些选项复制成这个更宽松的形态,再交给 `normalizeCommand()`
99
- * 根据 `strategy` 推导 `browser`、`navigateBefore` 等运行时意图,最后存成具体的
100
- * `CliCommand`。
109
+ * registry 内部会先把这些选项复制成对应的预归一化分支,再交给
110
+ * `normalizeCommand()` 根据 `strategy` 推导 `browser`、`navigateBefore` 等运行时意图,
111
+ * 最后存成具体的 `CliCommand`。
101
112
  */
102
- type RawCliCommand = BaseCliCommand & {
103
- /** 预归一化阶段的浏览器需求标记;可省略,之后会由 strategy 推导。 */
104
- browser?: boolean;
105
- /** 预归一化阶段的执行函数;可能是浏览器签名,也可能是非浏览器签名。 */
106
- func?: BrowserCommandFunc | NonBrowserCommandFunc;
113
+ type RawCliCommandBase = Omit<BaseCliCommand, 'strategy'>;
114
+ type RawBrowserCliCommand = RawCliCommandBase & {
115
+ func?: BrowserCommandFunc;
116
+ } & ({
117
+ browser: true;
118
+ strategy?: Strategy;
119
+ } | {
120
+ browser?: true;
121
+ strategy?: BrowserStrategy;
122
+ });
123
+ type RawNonBrowserCliCommand = RawCliCommandBase & {
124
+ func?: NonBrowserCommandFunc;
125
+ } & ({
126
+ browser: false;
127
+ strategy?: Strategy;
128
+ } | {
129
+ browser?: false;
130
+ strategy: Strategy.PUBLIC | Strategy.LOCAL;
131
+ });
132
+ type RawConditionalBrowserCliCommand = RawCliCommandBase & {
133
+ browser: BrowserRequirementResolver;
134
+ strategy?: Strategy;
135
+ func?: ConditionalBrowserCommandFunc;
107
136
  };
108
137
  /** Internal extension for lazy-loaded TS modules (not exposed in public API) */
109
138
  export type InternalCliCommand = CliCommand & {
110
139
  _lazy?: boolean;
111
140
  _modulePath?: string;
141
+ _hydrateBeforeBrowserRouting?: boolean;
112
142
  };
113
143
  type RequiredCliOptions = {
114
144
  site: string;
@@ -131,13 +161,25 @@ type NonBrowserCliOptions = Partial<Omit<NonBrowserCliCommand, 'args' | 'descrip
131
161
  strategy: Strategy.PUBLIC | Strategy.LOCAL;
132
162
  browser?: false;
133
163
  });
134
- export type CliOptions = BrowserCliOptions | NonBrowserCliOptions;
164
+ type ConditionalBrowserCliOptions = Partial<Omit<ConditionalBrowserCliCommand, 'args' | 'description' | 'browser' | 'requiresBrowser'>> & RequiredCliOptions & {
165
+ browser: BrowserRequirementResolver;
166
+ };
167
+ export type CliOptions = BrowserCliOptions | NonBrowserCliOptions | ConditionalBrowserCliOptions;
135
168
  declare global {
136
169
  var __bycli_registry__: Map<string, CliCommand> | undefined;
137
170
  }
138
- export declare function cli(opts: CliOptions): CliCommand;
171
+ export declare function cli(opts: ConditionalBrowserCliOptions): ConditionalBrowserCliCommand;
172
+ export declare function cli(opts: NonBrowserCliOptions): NonBrowserCliCommand;
173
+ export declare function cli(opts: BrowserCliOptions): BrowserCliCommand;
139
174
  export declare function getRegistry(): Map<string, CliCommand>;
140
175
  export declare function fullName(cmd: Pick<BaseCliCommand, 'site' | 'name'>): string;
141
176
  export declare function strategyLabel(cmd: CliCommand): string;
142
- export declare function registerCommand(cmd: RawCliCommand): void;
177
+ /** Whether a command may use browser-backed execution for some invocation. */
178
+ export declare function hasBrowserCapability(cmd: CliCommand): boolean;
179
+ /** Stable human-readable label for the normalized browser requirement. */
180
+ export declare function browserRequirementLabel(cmd: CliCommand): 'yes' | 'no' | 'conditional';
181
+ export declare function registerCommand(cmd: RawConditionalBrowserCliCommand): void;
182
+ export declare function registerCommand(cmd: RawNonBrowserCliCommand): void;
183
+ export declare function registerCommand(cmd: RawBrowserCliCommand): void;
184
+ export declare function registerCommand(cmd: CliCommand): void;
143
185
  export {};
@@ -1,6 +1,7 @@
1
1
  /**
2
2
  * Core registry: Strategy enum, Arg/CliCommand interfaces, cli() registration.
3
3
  */
4
+ import { pruneRegistryMutationKey, recordRegistryMutation, registryMutationKeys, withRegistryMutationGroup, } from './registry-transaction.js';
4
5
  export var Strategy;
5
6
  (function (Strategy) {
6
7
  Strategy["PUBLIC"] = "public";
@@ -9,9 +10,77 @@ export var Strategy;
9
10
  Strategy["INTERCEPT"] = "intercept";
10
11
  Strategy["UI"] = "ui";
11
12
  })(Strategy || (Strategy = {}));
12
- const _registry = globalThis.__bycli_registry__ ??= new Map();
13
+ const TRACKED_REGISTRY_MARKER = Symbol.for('@sovovs/bycli/tracked-registry');
14
+ function isTrackedRegistry(registry) {
15
+ return registry !== undefined
16
+ && registry[TRACKED_REGISTRY_MARKER] === true;
17
+ }
18
+ function instrumentRegistry(registry) {
19
+ if (isTrackedRegistry(registry))
20
+ return registry;
21
+ Object.defineProperties(registry, {
22
+ set: {
23
+ value(key, value) {
24
+ const before = { present: this.has(key), value: this.get(key) };
25
+ recordRegistryMutation(key, before, { present: true, value });
26
+ return Map.prototype.set.call(this, key, value);
27
+ },
28
+ },
29
+ delete: {
30
+ value(key) {
31
+ const before = { present: this.has(key), value: this.get(key) };
32
+ recordRegistryMutation(key, before, { present: false, value: undefined });
33
+ const deleted = Map.prototype.delete.call(this, key);
34
+ pruneRegistryMutationKey(key, this);
35
+ return deleted;
36
+ },
37
+ },
38
+ clear: {
39
+ value() {
40
+ const keys = new Set([...this.keys(), ...registryMutationKeys()]);
41
+ if (keys.size === 0)
42
+ return;
43
+ withRegistryMutationGroup(() => {
44
+ for (const key of keys) {
45
+ const present = this.has(key);
46
+ recordRegistryMutation(key, { present, value: present ? this.get(key) : undefined }, { present: false, value: undefined });
47
+ }
48
+ Map.prototype.clear.call(this);
49
+ for (const key of keys)
50
+ pruneRegistryMutationKey(key, this);
51
+ });
52
+ },
53
+ },
54
+ [TRACKED_REGISTRY_MARKER]: { value: true },
55
+ });
56
+ return registry;
57
+ }
58
+ const existingRegistry = globalThis.__bycli_registry__ ?? new Map();
59
+ const _registry = instrumentRegistry(existingRegistry);
60
+ globalThis.__bycli_registry__ = _registry;
13
61
  export function cli(opts) {
14
- const cmd = {
62
+ const base = rawCommandBase(opts);
63
+ let cmd;
64
+ if (typeof opts.browser === 'function') {
65
+ cmd = { ...base, strategy: opts.strategy, browser: opts.browser, func: opts.func };
66
+ }
67
+ else if (opts.browser === false) {
68
+ cmd = { ...base, strategy: opts.strategy, browser: false, func: opts.func };
69
+ }
70
+ else if (opts.browser === true) {
71
+ cmd = { ...base, strategy: opts.strategy, browser: true, func: opts.func };
72
+ }
73
+ else if (isImplicitNonBrowserOptions(opts)) {
74
+ cmd = { ...base, strategy: opts.strategy, browser: opts.browser, func: opts.func };
75
+ }
76
+ else {
77
+ cmd = { ...base, strategy: opts.strategy, browser: opts.browser, func: opts.func };
78
+ }
79
+ registerCommandInput(cmd);
80
+ return _registry.get(fullName(cmd));
81
+ }
82
+ function rawCommandBase(opts) {
83
+ return {
15
84
  site: opts.site,
16
85
  name: opts.name,
17
86
  aliases: opts.aliases,
@@ -19,11 +88,8 @@ export function cli(opts) {
19
88
  access: opts.access,
20
89
  example: opts.example,
21
90
  domain: opts.domain,
22
- strategy: opts.strategy,
23
- browser: opts.browser,
24
91
  args: opts.args ?? [],
25
92
  columns: opts.columns,
26
- func: opts.func,
27
93
  pipeline: opts.pipeline,
28
94
  footerExtra: opts.footerExtra,
29
95
  validateArgs: opts.validateArgs,
@@ -31,8 +97,10 @@ export function cli(opts) {
31
97
  siteSession: opts.siteSession,
32
98
  defaultFormat: opts.defaultFormat,
33
99
  };
34
- registerCommand(cmd);
35
- return _registry.get(fullName(cmd));
100
+ }
101
+ function isImplicitNonBrowserOptions(opts) {
102
+ return opts.browser === undefined
103
+ && (opts.strategy === Strategy.PUBLIC || opts.strategy === Strategy.LOCAL);
36
104
  }
37
105
  export function getRegistry() {
38
106
  return _registry;
@@ -43,6 +111,14 @@ export function fullName(cmd) {
43
111
  export function strategyLabel(cmd) {
44
112
  return cmd.strategy ?? Strategy.PUBLIC;
45
113
  }
114
+ /** Whether a command may use browser-backed execution for some invocation. */
115
+ export function hasBrowserCapability(cmd) {
116
+ return cmd.browser !== false;
117
+ }
118
+ /** Stable human-readable label for the normalized browser requirement. */
119
+ export function browserRequirementLabel(cmd) {
120
+ return cmd.browser === 'conditional' ? 'conditional' : cmd.browser ? 'yes' : 'no';
121
+ }
46
122
  /**
47
123
  * Normalize a command's runtime fields. This is the single place where
48
124
  * `strategy` is decoded into the concrete fields that the execution path
@@ -57,10 +133,8 @@ export function strategyLabel(cmd) {
57
133
  * 2. Derived from strategy + domain (the defaults below)
58
134
  */
59
135
  function normalizeCommand(cmd) {
60
- assertCommandAccess(cmd);
61
- assertSiteSession(cmd);
62
- const strategy = cmd.strategy ?? (cmd.browser === false ? Strategy.PUBLIC : Strategy.COOKIE);
63
- const browser = cmd.browser ?? (strategy !== Strategy.PUBLIC && strategy !== Strategy.LOCAL);
136
+ const declaredBrowser = cmd.browser;
137
+ const strategy = cmd.strategy ?? (declaredBrowser === false ? Strategy.PUBLIC : Strategy.COOKIE);
64
138
  let navigateBefore = cmd.navigateBefore;
65
139
  if (navigateBefore === undefined) {
66
140
  if (strategy === Strategy.COOKIE && cmd.domain) {
@@ -73,9 +147,34 @@ function normalizeCommand(cmd) {
73
147
  navigateBefore = true;
74
148
  }
75
149
  }
76
- return browser
77
- ? { ...cmd, strategy, browser: true, navigateBefore }
78
- : { ...cmd, strategy, browser: false, navigateBefore };
150
+ if (typeof cmd.browser === 'function') {
151
+ const normalized = {
152
+ ...cmd,
153
+ strategy,
154
+ browser: 'conditional',
155
+ requiresBrowser: cmd.browser,
156
+ navigateBefore,
157
+ };
158
+ return normalized;
159
+ }
160
+ if (cmd.browser === false) {
161
+ const normalized = { ...cmd, strategy, browser: false, navigateBefore };
162
+ return normalized;
163
+ }
164
+ if (cmd.browser === true) {
165
+ const normalized = { ...cmd, strategy, browser: true, navigateBefore };
166
+ return normalized;
167
+ }
168
+ if (isImplicitNonBrowserCommand(cmd)) {
169
+ const normalized = { ...cmd, strategy, browser: false, navigateBefore };
170
+ return normalized;
171
+ }
172
+ const normalized = { ...cmd, strategy, browser: true, navigateBefore };
173
+ return normalized;
174
+ }
175
+ function isImplicitNonBrowserCommand(cmd) {
176
+ return cmd.browser === undefined
177
+ && (cmd.strategy === Strategy.PUBLIC || cmd.strategy === Strategy.LOCAL);
79
178
  }
80
179
  function assertCommandAccess(cmd) {
81
180
  if (cmd.access === 'read' || cmd.access === 'write')
@@ -92,7 +191,24 @@ function assertSiteSession(cmd) {
92
191
  }
93
192
  }
94
193
  export function registerCommand(cmd) {
95
- const normalized = normalizeCommand(cmd);
194
+ registerCommandInput(cmd);
195
+ }
196
+ function registerCommandInput(cmd) {
197
+ withRegistryMutationGroup(() => {
198
+ assertCommandAccess(cmd);
199
+ assertSiteSession(cmd);
200
+ if (cmd.browser === 'conditional') {
201
+ if (typeof cmd.requiresBrowser !== 'function') {
202
+ const key = `${cmd.site}/${cmd.name}`;
203
+ throw new Error(`Command ${key} requiresBrowser must be a function`);
204
+ }
205
+ insertNormalizedCommand(cmd);
206
+ return;
207
+ }
208
+ insertNormalizedCommand(normalizeCommand(cmd));
209
+ });
210
+ }
211
+ function insertNormalizedCommand(normalized) {
96
212
  const canonicalKey = fullName(normalized);
97
213
  const existing = _registry.get(canonicalKey);
98
214
  if (existing?.aliases) {
@@ -26,7 +26,7 @@ export declare function serializeCommand(cmd: CliCommand): {
26
26
  description: string;
27
27
  access: import("./registry.js").CommandAccess;
28
28
  strategy: string;
29
- browser: boolean;
29
+ browser: boolean | "conditional";
30
30
  args: SerializedArg[];
31
31
  columns: string[];
32
32
  domain: string | null;
@@ -4,7 +4,7 @@
4
4
  * Used by the `list` command, Commander --help, and build-manifest.
5
5
  * Separated from registry.ts to keep the registry focused on types + registration.
6
6
  */
7
- import { fullName, strategyLabel } from './registry.js';
7
+ import { browserRequirementLabel, fullName, strategyLabel } from './registry.js';
8
8
  /** Stable arg schema — every field is always present (no sparse objects). */
9
9
  export function serializeArg(a) {
10
10
  return {
@@ -28,7 +28,7 @@ export function serializeCommand(cmd) {
28
28
  description: cmd.description,
29
29
  access: cmd.access,
30
30
  strategy: strategyLabel(cmd),
31
- browser: !!cmd.browser,
31
+ browser: cmd.browser,
32
32
  args: cmd.args.map(serializeArg),
33
33
  columns: cmd.columns ?? [],
34
34
  domain: cmd.domain ?? null,
@@ -87,7 +87,7 @@ export function formatRegistryHelpText(cmd) {
87
87
  }
88
88
  const meta = [];
89
89
  meta.push(`Access: ${cmd.access}`);
90
- meta.push(`Browser: ${cmd.browser ? 'yes' : 'no'}`);
90
+ meta.push(`Browser: ${browserRequirementLabel(cmd)}`);
91
91
  if (cmd.domain)
92
92
  meta.push(`Domain: ${cmd.domain}`);
93
93
  if (cmd.defaultFormat)
@@ -207,6 +207,8 @@ export interface IPage {
207
207
  * Useful for rich editors that ignore synthetic DOM value/text mutations.
208
208
  */
209
209
  insertText?(text: string): Promise<void>;
210
+ /** Focus the browser window containing the active page for interactive login. */
211
+ focusWindow?(): Promise<void>;
210
212
  closeWindow?(): Promise<void>;
211
213
  /** Returns the current page URL, or null if unavailable. */
212
214
  getCurrentUrl?(): Promise<string | null>;
@@ -0,0 +1 @@
1
+ export {};
package/package.json CHANGED
@@ -1,12 +1,12 @@
1
1
  {
2
2
  "name": "@sovovs/bycli",
3
- "version": "2.0.0",
3
+ "version": "2.1.1",
4
4
  "publishConfig": {
5
5
  "access": "public"
6
6
  },
7
7
  "description": "Make any website or Electron App your CLI. AI-powered.",
8
8
  "engines": {
9
- "node": ">=20.0.0"
9
+ "node": ">=20.6.0"
10
10
  },
11
11
  "type": "module",
12
12
  "workspaces": [
@@ -32,6 +32,7 @@
32
32
  "./download/article-download": "./dist/src/download/article-download.js",
33
33
  "./download/media-download": "./dist/src/download/media-download.js",
34
34
  "./download/progress": "./dist/src/download/progress.js",
35
+ "./download/wechat-article": "./dist/src/download/wechat-article.js",
35
36
  "./pipeline": "./dist/src/pipeline/index.js"
36
37
  },
37
38
  "files": [
@@ -66,6 +67,7 @@
66
67
  "advise:listing-id-pairing": "node scripts/check-listing-id-pairing.mjs",
67
68
  "check:silent-column-drop": "node scripts/check-silent-column-drop.mjs",
68
69
  "check:typed-error-lint": "node scripts/check-typed-error-lint.mjs",
70
+ "check:package-install": "node scripts/check-package-install.mjs",
69
71
  "docs:dev": "vitepress dev docs",
70
72
  "docs:build": "vitepress build docs",
71
73
  "docs:preview": "vitepress preview docs"
@@ -87,17 +89,19 @@
87
89
  },
88
90
  "dependencies": {
89
91
  "@mozilla/readability": "^0.6.0",
92
+ "@sovovs/bycli-recorder-core": "^0.1.0",
90
93
  "cli-table3": "^0.6.5",
91
94
  "commander": "^14.0.3",
92
95
  "js-yaml": "^4.1.0",
96
+ "parse5": "^7.3.0",
93
97
  "turndown": "^7.2.2",
94
98
  "turndown-plugin-gfm": "^1.0.2",
95
99
  "undici": "^6.25.0",
96
100
  "ws": "^8.18.0"
97
101
  },
98
102
  "devDependencies": {
99
- "@types/jsdom": "^27.0.0",
100
103
  "@types/js-yaml": "^4.0.9",
104
+ "@types/jsdom": "^27.0.0",
101
105
  "@types/node": "^25.5.2",
102
106
  "@types/turndown": "^5.0.6",
103
107
  "@types/ws": "^8.5.13",
@@ -0,0 +1,71 @@
1
+ import assert from 'node:assert/strict';
2
+ import { execFileSync } from 'node:child_process';
3
+ import { cpSync, mkdirSync, mkdtempSync, readFileSync, rmSync, writeFileSync } from 'node:fs';
4
+ import { tmpdir } from 'node:os';
5
+ import { dirname, join, resolve } from 'node:path';
6
+ import { fileURLToPath, pathToFileURL } from 'node:url';
7
+
8
+ const root = resolve(dirname(fileURLToPath(import.meta.url)), '..');
9
+ const temp = mkdtempSync(join(tmpdir(), 'bycli-package-install-'));
10
+ const artifacts = join(temp, 'artifacts');
11
+ const project = join(temp, 'project');
12
+ const mainStage = join(temp, 'main-package');
13
+
14
+ function run(command, args, cwd = root) {
15
+ return execFileSync(command, args, {
16
+ cwd,
17
+ encoding: 'utf8',
18
+ stdio: ['ignore', 'pipe', 'pipe'],
19
+ });
20
+ }
21
+
22
+ function pack(cwd) {
23
+ const result = JSON.parse(run('npm', [
24
+ 'pack', '--json', '--ignore-scripts', '--pack-destination', artifacts,
25
+ ], cwd));
26
+ assert.equal(result.length, 1);
27
+ return {
28
+ tarball: join(artifacts, result[0].filename),
29
+ files: new Set(result[0].files.map(({ path }) => path)),
30
+ };
31
+ }
32
+
33
+ try {
34
+ mkdirSync(artifacts, { recursive: true });
35
+ mkdirSync(project, { recursive: true });
36
+ mkdirSync(mainStage, { recursive: true });
37
+ for (const path of [
38
+ 'package.json', 'dist', 'clis', 'cli-manifest.json', 'scripts',
39
+ 'README.md', 'README.zh-CN.md', 'LICENSE', 'NOTICE',
40
+ ]) {
41
+ cpSync(join(root, path), join(mainStage, path), { recursive: true });
42
+ }
43
+
44
+ const core = pack(join(root, 'packages/recorder-core'));
45
+ const main = pack(mainStage);
46
+
47
+ for (const file of ['dist/index.js', 'dist/index.d.ts', 'README.md', 'LICENSE']) {
48
+ assert(core.files.has(file), `recorder-core tarball is missing ${file}`);
49
+ }
50
+ assert(![...core.files].some((file) => file.startsWith('src/')), 'recorder-core tarball includes src/');
51
+
52
+ writeFileSync(join(project, 'package.json'), JSON.stringify({ private: true, type: 'module' }));
53
+ run('npm', [
54
+ 'install', '--ignore-scripts', '--no-audit', '--no-fund', core.tarball, main.tarball,
55
+ ], project);
56
+
57
+ const mainManifest = JSON.parse(readFileSync(join(
58
+ project, 'node_modules/@sovovs/bycli/package.json',
59
+ ), 'utf8'));
60
+ assert.equal(mainManifest.dependencies?.['@sovovs/bycli-recorder-core'], '^0.1.0');
61
+
62
+ const coreDirectory = join(project, 'node_modules/@sovovs/bycli-recorder-core');
63
+ const recorderEntry = join(
64
+ project, 'node_modules/@sovovs/bycli/dist/src/browser/analyze.js',
65
+ );
66
+ await import(pathToFileURL(recorderEntry).href);
67
+ await import(pathToFileURL(join(coreDirectory, 'dist/index.js')).href);
68
+ console.log('package install smoke test passed');
69
+ } finally {
70
+ rmSync(temp, { recursive: true, force: true });
71
+ }