@sovovs/bycli 2.0.0 → 2.1.0

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 (41) hide show
  1. package/clis/twitter/search.js +3 -3
  2. package/dist/src/browser/cdp.js +3 -0
  3. package/dist/src/browser/daemon-client.d.ts +2 -0
  4. package/dist/src/browser/daemon-client.js +1 -1
  5. package/dist/src/browser/extension-capabilities.d.ts +13 -0
  6. package/dist/src/browser/extension-capabilities.js +22 -0
  7. package/dist/src/browser/extension-capabilities.test.d.ts +1 -0
  8. package/dist/src/browser/extension-version-metadata.test.d.ts +1 -0
  9. package/dist/src/browser/page.d.ts +1 -0
  10. package/dist/src/browser/page.js +20 -1
  11. package/dist/src/build-manifest.js +4 -2
  12. package/dist/src/capabilityRouting.d.ts +3 -2
  13. package/dist/src/capabilityRouting.js +10 -2
  14. package/dist/src/cli.js +1 -1
  15. package/dist/src/commanderAdapter.js +5 -5
  16. package/dist/src/daemon.js +16 -0
  17. package/dist/src/discovery.d.ts +5 -0
  18. package/dist/src/discovery.js +12 -4
  19. package/dist/src/discovery.test.d.ts +1 -0
  20. package/dist/src/execution.d.ts +5 -0
  21. package/dist/src/execution.js +269 -50
  22. package/dist/src/help.js +8 -8
  23. package/dist/src/manifest-schema.d.ts +9 -0
  24. package/dist/src/manifest-schema.js +162 -0
  25. package/dist/src/manifest-schema.test.d.ts +1 -0
  26. package/dist/src/manifest-types.d.ts +1 -1
  27. package/dist/src/observation/redaction.js +10 -4
  28. package/dist/src/recorder/runner/verify-runner-main.d.ts +6 -5
  29. package/dist/src/recorder/runner/verify-runner-main.js +22 -5
  30. package/dist/src/registry-api.d.ts +1 -1
  31. package/dist/src/registry-api.types.test.d.ts +1 -0
  32. package/dist/src/registry-transaction.d.ts +42 -0
  33. package/dist/src/registry-transaction.js +194 -0
  34. package/dist/src/registry-transaction.test.d.ts +1 -0
  35. package/dist/src/registry.d.ts +58 -16
  36. package/dist/src/registry.js +131 -15
  37. package/dist/src/serialization.d.ts +1 -1
  38. package/dist/src/serialization.js +3 -3
  39. package/dist/src/types.d.ts +2 -0
  40. package/package.json +1 -1
  41. package/scripts/recorder.sh +0 -186
@@ -10,7 +10,13 @@ const SENSITIVE_HEADER_NAMES = new Set([
10
10
  'x-xsrf-token',
11
11
  ]);
12
12
  const SENSITIVE_FIELD_PATTERN = /(password|passwd|pwd|token|secret|authorization|cookie|set-cookie|api[_-]?key|access[_-]?token|refresh[_-]?token|session[_-]?id|csrf|xsrf)/i;
13
- const SENSITIVE_URL_PARAMS = /([?&])(token|key|secret|password|auth|access_token|api_key|session_id|csrf|xsrf)=[^&]*/gi;
13
+ const SENSITIVE_URL_PARAMS = /([?&])(token|key|secret|fingerprint|password|auth|access_token|api_key|session_id|csrf|xsrf)=[^&]*/gi;
14
+ function hasFingerprintFieldSegment(name) {
15
+ return name
16
+ .replace(/([a-z0-9])([A-Z])/g, '$1 $2')
17
+ .split(/[-_.\s]+/)
18
+ .some((segment) => segment.toLowerCase() === 'fingerprint');
19
+ }
14
20
  export function redactUrl(url) {
15
21
  return url.replace(SENSITIVE_URL_PARAMS, '$1$2=[REDACTED]');
16
22
  }
@@ -31,8 +37,8 @@ export function redactText(text, opts = {}) {
31
37
  const max = opts.maxStringLength ?? 50_000;
32
38
  let out = text
33
39
  .replace(/Bearer\s+[A-Za-z0-9\-._~+/]+=*/gi, 'Bearer [REDACTED]')
34
- .replace(/(["'])(password|passwd|pwd|token|secret|api_key|apikey|access_token|session_id)\1\s*:\s*(["'])(.*?)\3/gi, '$1$2$1:$3[REDACTED]$3')
35
- .replace(/(token|secret|password|api_key|apikey|access_token|session_id)[=:]\s*['"]?[^'"\s,;}&]+['"]?/gi, '$1=[REDACTED]')
40
+ .replace(/(["'])(password|passwd|pwd|token|secret|fingerprint|api_key|apikey|access_token|session_id)\1\s*:\s*(["'])(.*?)\3/gi, '$1$2$1:$3[REDACTED]$3')
41
+ .replace(/(token|secret|fingerprint|password|api_key|apikey|access_token|session_id)\s*[=:]\s*['"]?[^'"\s,;}&]+['"]?/gi, '$1=[REDACTED]')
36
42
  .replace(/(cookie[=:]\s*)[^\n;]{3,}/gi, '$1[REDACTED]')
37
43
  .replace(/eyJ[A-Za-z0-9_-]{10,}\.eyJ[A-Za-z0-9_-]{10,}\.[A-Za-z0-9_-]{10,}/g, '[REDACTED_JWT]');
38
44
  if (out.length > max)
@@ -41,7 +47,7 @@ export function redactText(text, opts = {}) {
41
47
  }
42
48
  export function redactValue(value, opts = {}, keyHint, depth = 0) {
43
49
  const allow = new Set((opts.allowlist ?? []).map((key) => key.toLowerCase()));
44
- if (keyHint && SENSITIVE_FIELD_PATTERN.test(keyHint) && !allow.has(keyHint.toLowerCase())) {
50
+ if (keyHint && (SENSITIVE_FIELD_PATTERN.test(keyHint) || hasFingerprintFieldSegment(keyHint)) && !allow.has(keyHint.toLowerCase())) {
45
51
  return DEFAULT_REDACTION;
46
52
  }
47
53
  if (typeof value === 'string') {
@@ -11,10 +11,11 @@
11
11
  * adapters report not-yet (their Page must come from the daemon — M6b). The spawn /
12
12
  * input.json security / timeout / byte-cap mechanism lives in `runner-port.ts`.
13
13
  *
14
- * SECURITY (07:123-124): raw executionSeedArgs arrive via input.json (0600) and are used
15
- * as the adapter's call args; they are never echoed into the emitted result/started events.
14
+ * SECURITY (07:123-124): raw executionSeedArgs arrive via input.json (0600), then are
15
+ * defaulted, coerced, and validated before execution. Neither raw nor prepared arguments
16
+ * are echoed into the emitted result/started events.
16
17
  */
17
- import { type BrowserCliCommand, type CliCommand } from '../../registry.js';
18
+ import { type BrowserCliCommand, type CliCommand, type ConditionalBrowserCliCommand } from '../../registry.js';
18
19
  import type { RunnerEvent, RunnerResultEvent } from '@sovovs/bycli-recorder-core';
19
20
  /** Parsed input.json (written by RunnerPort; raw seed args are execution-only). */
20
21
  export interface RunnerInput {
@@ -24,7 +25,7 @@ export interface RunnerInput {
24
25
  adapterPath: string;
25
26
  /** Browser profile contextId for browser adapters (M6b). Omitted → daemon default profile. */
26
27
  contextId?: string;
27
- /** Raw seed args — used as the adapter call args, never echoed into events. */
28
+ /** Raw seed args — prepared before resolver/adapter calls and never echoed into events. */
28
29
  executionSeedArgs?: Record<string, unknown>;
29
30
  fixture?: 'ignore' | 'match' | 'update';
30
31
  trace?: 'off' | 'retain-on-failure' | 'always';
@@ -52,7 +53,7 @@ export declare function loadAdapterByName(adapterPath: string, name: string): Pr
52
53
  * default implementation connects BACK to the running daemon for one. Injectable so unit
53
54
  * tests can exercise executeAdapterForVerify without a real daemon/browser.
54
55
  */
55
- export type BrowserAdapterRunner = (command: BrowserCliCommand, opts: {
56
+ export type BrowserAdapterRunner = (command: BrowserCliCommand | ConditionalBrowserCliCommand, opts: {
56
57
  seedArgs: Record<string, unknown>;
57
58
  contextId?: string;
58
59
  preNavUrl: string | null;
@@ -11,13 +11,15 @@
11
11
  * adapters report not-yet (their Page must come from the daemon — M6b). The spawn /
12
12
  * input.json security / timeout / byte-cap mechanism lives in `runner-port.ts`.
13
13
  *
14
- * SECURITY (07:123-124): raw executionSeedArgs arrive via input.json (0600) and are used
15
- * as the adapter's call args; they are never echoed into the emitted result/started events.
14
+ * SECURITY (07:123-124): raw executionSeedArgs arrive via input.json (0600), then are
15
+ * defaulted, coerced, and validated before execution. Neither raw nor prepared arguments
16
+ * are echoed into the emitted result/started events.
16
17
  */
17
18
  import * as fs from 'node:fs';
18
19
  import { randomUUID } from 'node:crypto';
19
20
  import { pathToFileURL } from 'node:url';
20
- import { getRegistry } from '../../registry.js';
21
+ import { getRegistry, } from '../../registry.js';
22
+ import { prepareCommandArgsOrThrowArgumentError } from '../../execution.js';
21
23
  // ── Lease cleanup on signal / orphan watchdog (Codex #4, #7) ────────────────
22
24
  // A browser adapter holds a daemon-side tab lease. If the parent SIGTERMs/cancels this child
23
25
  // (timeout/cancel) or this child is orphaned (parent crashed → no SIGTERM, and on win32 the reaper
@@ -131,9 +133,24 @@ export async function executeAdapterForVerify(command, opts) {
131
133
  return { ok: false, data: { stage: 'load', trace }, error: { code: 'runner_protocol_error', message: `adapter "${opts.name}" has no func` } };
132
134
  }
133
135
  try {
136
+ const preparedArgs = prepareCommandArgsOrThrowArgumentError(command, opts.seedArgs);
134
137
  let rows;
135
138
  if (command.browser === false) {
136
- rows = await command.func(opts.seedArgs, false);
139
+ rows = await command.func(preparedArgs, false);
140
+ }
141
+ else if (command.browser === 'conditional') {
142
+ const browserRequired = Boolean(command.requiresBrowser(preparedArgs));
143
+ if (!browserRequired) {
144
+ rows = await command.func(null, preparedArgs, false);
145
+ }
146
+ else {
147
+ const runner = opts.browserRunner ?? defaultBrowserAdapterRunner;
148
+ rows = await runner(command, {
149
+ seedArgs: preparedArgs,
150
+ contextId: opts.contextId,
151
+ preNavUrl: typeof command.navigateBefore === 'string' ? command.navigateBefore : null,
152
+ });
153
+ }
137
154
  }
138
155
  else {
139
156
  // M6b: browser adapter connects back to the daemon for a Page. navigateBefore is a
@@ -141,7 +158,7 @@ export async function executeAdapterForVerify(command, opts) {
141
158
  // `true`/`undefined` mean "adapter handles its own navigation".
142
159
  const runner = opts.browserRunner ?? defaultBrowserAdapterRunner;
143
160
  rows = await runner(command, {
144
- seedArgs: opts.seedArgs,
161
+ seedArgs: preparedArgs,
145
162
  contextId: opts.contextId,
146
163
  preNavUrl: typeof command.navigateBefore === 'string' ? command.navigateBefore : null,
147
164
  });
@@ -7,7 +7,7 @@
7
7
  * plugins are dynamically imported during discoverPlugins().
8
8
  */
9
9
  export { cli, Strategy, getRegistry, fullName, registerCommand } from './registry.js';
10
- export type { CliCommand, Arg, CliOptions, CommandArgs, SiteSessionMode } from './registry.js';
10
+ export type { CliCommand, Arg, CliOptions, CommandArgs, SiteSessionMode, BrowserDeclaration, BrowserRequirementResolver, ConditionalBrowserCommandFunc, NormalizedBrowserRequirement, } from './registry.js';
11
11
  export type { IPage } from './types.js';
12
12
  export { onStartup, onBeforeExecute, onAfterExecute } from './hooks.js';
13
13
  export type { HookFn, HookContext, HookName } from './hooks.js';
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,42 @@
1
+ import type { CliCommand } from './registry.js';
2
+ interface RegistrySlot {
3
+ present: boolean;
4
+ value: CliCommand | undefined;
5
+ }
6
+ export interface RegistryTransactionWrite {
7
+ key: string;
8
+ before: RegistrySlot;
9
+ after: RegistrySlot;
10
+ group: number;
11
+ beforeRevision: number;
12
+ afterRevision: number;
13
+ }
14
+ export interface RegistryTransaction {
15
+ readonly writes: RegistryTransactionWrite[];
16
+ readonly id: number;
17
+ active: boolean;
18
+ currentGroup: number | undefined;
19
+ finalized: boolean;
20
+ readonly finalizedGroups: Set<number>;
21
+ }
22
+ export declare function createRegistryTransaction(): RegistryTransaction;
23
+ export declare function closeRegistryTransaction(transaction: RegistryTransaction): void;
24
+ export declare function runRegistryTransaction<T>(transaction: RegistryTransaction, operation: () => Promise<T>): Promise<T>;
25
+ export declare function withRegistryMutationGroup<T>(operation: () => T): T;
26
+ export declare function recordRegistryMutation(key: string, before: RegistrySlot, after: RegistrySlot): void;
27
+ /**
28
+ * Keys with revision ownership, including absent-key tombstones.
29
+ *
30
+ * Tombstones intentionally remain until a later mutation or rollback supersedes
31
+ * them. Pruning an absent revision while an overlapping transaction may still
32
+ * reference it would allow an older rollback to resurrect the key.
33
+ */
34
+ export declare function registryMutationKeys(): string[];
35
+ export declare function pruneRegistryMutationKey(key: string, registry: ReadonlyMap<string, CliCommand>): void;
36
+ export declare function finalizeRegistryTransaction(transaction: RegistryTransaction, registry: ReadonlyMap<string, CliCommand>, groups?: ReadonlySet<number>): void;
37
+ export declare function capturedRegistryValue(transaction: RegistryTransaction, key: string): RegistrySlot;
38
+ export declare function capturedRegistryValues(transaction: RegistryTransaction): Map<string, CliCommand>;
39
+ export declare function transactionGroupsForKey(transaction: RegistryTransaction, key: string): Set<number>;
40
+ export declare function rollbackRegistryTransaction(transaction: RegistryTransaction, registry: Map<string, CliCommand>, groups?: ReadonlySet<number>): void;
41
+ export declare function resetRegistryTransactionStateForTests(): void;
42
+ export {};
@@ -0,0 +1,194 @@
1
+ import { AsyncLocalStorage } from 'node:async_hooks';
2
+ const TRANSACTION_STATE_KEY = Symbol.for('@sovovs/bycli/registry-transaction-state');
3
+ const globalState = globalThis;
4
+ const state = globalState[TRANSACTION_STATE_KEY] ??= {
5
+ storage: new AsyncLocalStorage(),
6
+ transactionCounter: 0,
7
+ groupCounter: 0,
8
+ revisionCounter: 0,
9
+ revisions: new Map(),
10
+ transactions: new Map(),
11
+ owners: new Map(),
12
+ };
13
+ state.transactions ??= new Map();
14
+ state.owners ??= new Map();
15
+ export function createRegistryTransaction() {
16
+ const transaction = {
17
+ writes: [],
18
+ id: ++state.transactionCounter,
19
+ active: false,
20
+ currentGroup: undefined,
21
+ finalized: false,
22
+ finalizedGroups: new Set(),
23
+ };
24
+ state.transactions.set(transaction.id, transaction);
25
+ return transaction;
26
+ }
27
+ export function closeRegistryTransaction(transaction) {
28
+ transaction.active = false;
29
+ transaction.currentGroup = undefined;
30
+ }
31
+ export async function runRegistryTransaction(transaction, operation) {
32
+ if (transaction.finalized) {
33
+ throw new Error('Adapter registration transaction is finalized');
34
+ }
35
+ transaction.active = true;
36
+ return state.storage.run(transaction, async () => {
37
+ try {
38
+ return await operation();
39
+ }
40
+ finally {
41
+ closeRegistryTransaction(transaction);
42
+ }
43
+ });
44
+ }
45
+ export function withRegistryMutationGroup(operation) {
46
+ const transaction = state.storage.getStore();
47
+ if (!transaction)
48
+ return operation();
49
+ if (!transaction.active) {
50
+ throw new Error('Adapter registration transaction is closed; delayed registration is not allowed');
51
+ }
52
+ const previousGroup = transaction.currentGroup;
53
+ transaction.currentGroup = ++state.groupCounter;
54
+ try {
55
+ return operation();
56
+ }
57
+ finally {
58
+ transaction.currentGroup = previousGroup;
59
+ }
60
+ }
61
+ export function recordRegistryMutation(key, before, after) {
62
+ const transaction = state.storage.getStore();
63
+ if (transaction && !transaction.active) {
64
+ throw new Error('Adapter registration transaction is closed; delayed registration is not allowed');
65
+ }
66
+ if (transaction?.finalized) {
67
+ throw new Error('Adapter registration transaction is finalized');
68
+ }
69
+ const beforeRevision = state.revisions.get(key) ?? 0;
70
+ const afterRevision = ++state.revisionCounter;
71
+ state.revisions.set(key, afterRevision);
72
+ if (!transaction)
73
+ return;
74
+ const group = transaction.currentGroup ?? ++state.groupCounter;
75
+ transaction.writes.push({
76
+ key,
77
+ before,
78
+ after,
79
+ group,
80
+ beforeRevision,
81
+ afterRevision,
82
+ });
83
+ const owners = state.owners.get(key) ?? new Set();
84
+ owners.add(transaction.id);
85
+ state.owners.set(key, owners);
86
+ }
87
+ /**
88
+ * Keys with revision ownership, including absent-key tombstones.
89
+ *
90
+ * Tombstones intentionally remain until a later mutation or rollback supersedes
91
+ * them. Pruning an absent revision while an overlapping transaction may still
92
+ * reference it would allow an older rollback to resurrect the key.
93
+ */
94
+ export function registryMutationKeys() {
95
+ return [...state.revisions.keys()];
96
+ }
97
+ export function pruneRegistryMutationKey(key, registry) {
98
+ if (registry.has(key))
99
+ return;
100
+ if ((state.owners.get(key)?.size ?? 0) > 0)
101
+ return;
102
+ state.revisions.delete(key);
103
+ }
104
+ export function finalizeRegistryTransaction(transaction, registry, groups) {
105
+ if (transaction.active) {
106
+ throw new Error('Cannot finalize an active adapter registration transaction');
107
+ }
108
+ const selectedGroups = groups ?? new Set(transaction.writes.map(write => write.group));
109
+ const newGroups = new Set([...selectedGroups].filter(group => !transaction.finalizedGroups.has(group)));
110
+ const affectedKeys = new Set(transaction.writes
111
+ .filter(write => newGroups.has(write.group))
112
+ .map(write => write.key));
113
+ for (const group of newGroups)
114
+ transaction.finalizedGroups.add(group);
115
+ for (const key of affectedKeys) {
116
+ const stillOwned = transaction.writes.some(write => write.key === key && !transaction.finalizedGroups.has(write.group));
117
+ if (!stillOwned) {
118
+ const owners = state.owners.get(key);
119
+ owners?.delete(transaction.id);
120
+ if (owners?.size === 0)
121
+ state.owners.delete(key);
122
+ }
123
+ pruneRegistryMutationKey(key, registry);
124
+ }
125
+ const hasUnfinalizedWrites = transaction.writes.some(write => !transaction.finalizedGroups.has(write.group));
126
+ if (!hasUnfinalizedWrites) {
127
+ transaction.finalized = true;
128
+ state.transactions.delete(transaction.id);
129
+ }
130
+ }
131
+ export function capturedRegistryValue(transaction, key) {
132
+ for (let index = transaction.writes.length - 1; index >= 0; index -= 1) {
133
+ const write = transaction.writes[index];
134
+ if (write.key === key)
135
+ return write.after;
136
+ }
137
+ return { present: false, value: undefined };
138
+ }
139
+ export function capturedRegistryValues(transaction) {
140
+ const keys = new Set(transaction.writes.map(write => write.key));
141
+ const values = new Map();
142
+ for (const key of keys) {
143
+ const captured = capturedRegistryValue(transaction, key);
144
+ if (captured.present && captured.value)
145
+ values.set(key, captured.value);
146
+ }
147
+ return values;
148
+ }
149
+ export function transactionGroupsForKey(transaction, key) {
150
+ return new Set(transaction.writes.filter(write => write.key === key).map(write => write.group));
151
+ }
152
+ export function rollbackRegistryTransaction(transaction, registry, groups) {
153
+ const groupIds = [...new Set(transaction.writes.map(write => write.group))]
154
+ .filter(group => !transaction.finalizedGroups.has(group) && (!groups || groups.has(group)))
155
+ .sort((a, b) => b - a);
156
+ for (const group of groupIds) {
157
+ const writesByKey = new Map();
158
+ for (const write of transaction.writes) {
159
+ if (write.group !== group)
160
+ continue;
161
+ const writes = writesByKey.get(write.key) ?? [];
162
+ writes.push(write);
163
+ writesByKey.set(write.key, writes);
164
+ }
165
+ for (const writes of writesByKey.values()) {
166
+ const finalWrite = writes.at(-1);
167
+ if ((state.revisions.get(finalWrite.key) ?? 0) !== finalWrite.afterRevision)
168
+ continue;
169
+ for (let index = writes.length - 1; index >= 0; index -= 1) {
170
+ const write = writes[index];
171
+ if (write.before.present)
172
+ Map.prototype.set.call(registry, write.key, write.before.value);
173
+ else
174
+ Map.prototype.delete.call(registry, write.key);
175
+ if (write.beforeRevision === 0)
176
+ state.revisions.delete(write.key);
177
+ else
178
+ state.revisions.set(write.key, write.beforeRevision);
179
+ }
180
+ }
181
+ }
182
+ finalizeRegistryTransaction(transaction, registry, new Set(groupIds));
183
+ }
184
+ export function resetRegistryTransactionStateForTests() {
185
+ for (const transaction of state.transactions.values()) {
186
+ closeRegistryTransaction(transaction);
187
+ transaction.finalized = true;
188
+ for (const write of transaction.writes)
189
+ transaction.finalizedGroups.add(write.group);
190
+ }
191
+ state.transactions.clear();
192
+ state.owners.clear();
193
+ state.revisions.clear();
194
+ }
@@ -0,0 +1 @@
1
+ export {};
@@ -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 {};