@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.
- package/clis/twitter/search.js +3 -3
- package/dist/src/browser/cdp.js +3 -0
- package/dist/src/browser/daemon-client.d.ts +2 -0
- package/dist/src/browser/daemon-client.js +1 -1
- package/dist/src/browser/extension-capabilities.d.ts +13 -0
- package/dist/src/browser/extension-capabilities.js +22 -0
- package/dist/src/browser/extension-capabilities.test.d.ts +1 -0
- package/dist/src/browser/extension-version-metadata.test.d.ts +1 -0
- package/dist/src/browser/page.d.ts +1 -0
- package/dist/src/browser/page.js +20 -1
- package/dist/src/build-manifest.js +4 -2
- package/dist/src/capabilityRouting.d.ts +3 -2
- package/dist/src/capabilityRouting.js +10 -2
- package/dist/src/cli.js +1 -1
- package/dist/src/commanderAdapter.js +5 -5
- package/dist/src/daemon.js +16 -0
- package/dist/src/discovery.d.ts +5 -0
- package/dist/src/discovery.js +12 -4
- package/dist/src/discovery.test.d.ts +1 -0
- package/dist/src/execution.d.ts +5 -0
- package/dist/src/execution.js +269 -50
- package/dist/src/help.js +8 -8
- package/dist/src/manifest-schema.d.ts +9 -0
- package/dist/src/manifest-schema.js +162 -0
- package/dist/src/manifest-schema.test.d.ts +1 -0
- package/dist/src/manifest-types.d.ts +1 -1
- package/dist/src/observation/redaction.js +10 -4
- package/dist/src/recorder/runner/verify-runner-main.d.ts +6 -5
- package/dist/src/recorder/runner/verify-runner-main.js +22 -5
- package/dist/src/registry-api.d.ts +1 -1
- package/dist/src/registry-api.types.test.d.ts +1 -0
- package/dist/src/registry-transaction.d.ts +42 -0
- package/dist/src/registry-transaction.js +194 -0
- package/dist/src/registry-transaction.test.d.ts +1 -0
- package/dist/src/registry.d.ts +58 -16
- package/dist/src/registry.js +131 -15
- package/dist/src/serialization.d.ts +1 -1
- package/dist/src/serialization.js +3 -3
- package/dist/src/types.d.ts +2 -0
- package/package.json +1 -1
- 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)
|
|
15
|
-
*
|
|
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 —
|
|
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)
|
|
15
|
-
*
|
|
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(
|
|
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:
|
|
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 {};
|
package/dist/src/registry.d.ts
CHANGED
|
@@ -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
|
-
*
|
|
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.
|
|
85
|
-
browser
|
|
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
|
|
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
|
|
99
|
-
* 根据 `strategy` 推导 `browser`、`navigateBefore`
|
|
100
|
-
* `CliCommand`。
|
|
109
|
+
* registry 内部会先把这些选项复制成对应的预归一化分支,再交给
|
|
110
|
+
* `normalizeCommand()` 根据 `strategy` 推导 `browser`、`navigateBefore` 等运行时意图,
|
|
111
|
+
* 最后存成具体的 `CliCommand`。
|
|
101
112
|
*/
|
|
102
|
-
type
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
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
|
-
|
|
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:
|
|
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
|
-
|
|
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 {};
|