@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
package/dist/src/execution.js
CHANGED
|
@@ -16,7 +16,7 @@ import * as fs from 'node:fs';
|
|
|
16
16
|
import * as path from 'node:path';
|
|
17
17
|
import { getUserClisDir } from './config-paths.js';
|
|
18
18
|
import { executePipeline } from './pipeline/index.js';
|
|
19
|
-
import { adapterLoadError, ArgumentError, CommandExecutionError, attachTraceReceipt, getErrorMessage } from './errors.js';
|
|
19
|
+
import { adapterLoadError, ArgumentError, CliError, CommandExecutionError, attachTraceReceipt, getErrorMessage } from './errors.js';
|
|
20
20
|
import { shouldUseBrowserSession } from './capabilityRouting.js';
|
|
21
21
|
import { getBrowserFactory, browserSession, runWithTimeout, DEFAULT_BROWSER_COMMAND_TIMEOUT } from './runtime.js';
|
|
22
22
|
import { resolveProfileContextId } from './browser/profile.js';
|
|
@@ -26,13 +26,33 @@ import { isElectronApp } from './electron-apps.js';
|
|
|
26
26
|
import { probeCDP, resolveElectronEndpoint } from './launcher.js';
|
|
27
27
|
import { ObservationSession, exportObservationSession } from './observation/index.js';
|
|
28
28
|
import { resolveAdapterSourcePath } from './adapter-source.js';
|
|
29
|
+
import { canonicalizeManifestArgSchema, ManifestSchemaError } from './manifest-schema.js';
|
|
30
|
+
import { capturedRegistryValues, closeRegistryTransaction, createRegistryTransaction, finalizeRegistryTransaction, resetRegistryTransactionStateForTests, rollbackRegistryTransaction, runRegistryTransaction, transactionGroupsForKey, } from './registry-transaction.js';
|
|
29
31
|
const _loadedModules = new Map();
|
|
30
|
-
/**
|
|
31
|
-
const
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
32
|
+
/** Independent cache-busting generation; retained when an import promise is discarded. */
|
|
33
|
+
const _moduleImportGenerations = new Map();
|
|
34
|
+
let _registrationImportTail = Promise.resolve();
|
|
35
|
+
let _registrationImportsPoisoned = false;
|
|
36
|
+
let _fingerprintReadCount = 0;
|
|
37
|
+
const DEFAULT_ADAPTER_IMPORT_TIMEOUT_MS = 30_000;
|
|
38
|
+
function adapterImportTimeoutMs() {
|
|
39
|
+
const configured = Number(process.env.BYCLI_ADAPTER_IMPORT_TIMEOUT_MS);
|
|
40
|
+
return Number.isFinite(configured) && configured > 0 ? configured : DEFAULT_ADAPTER_IMPORT_TIMEOUT_MS;
|
|
41
|
+
}
|
|
42
|
+
function poisonedImportError() {
|
|
43
|
+
return adapterLoadError('Adapter registration imports are unavailable because a previous import timed out.', 'Restart byCLI before retrying; the timed-out module may still complete and register stale commands in this process.');
|
|
44
|
+
}
|
|
45
|
+
/** Internal test-only reset. Never use this to recover a production process. */
|
|
46
|
+
export function _resetLazyModuleStateForTests() {
|
|
47
|
+
_loadedModules.clear();
|
|
48
|
+
_moduleImportGenerations.clear();
|
|
49
|
+
_registrationImportTail = Promise.resolve();
|
|
50
|
+
_registrationImportsPoisoned = false;
|
|
51
|
+
_fingerprintReadCount = 0;
|
|
52
|
+
resetRegistryTransactionStateForTests();
|
|
53
|
+
}
|
|
54
|
+
export function _getLazyModuleFingerprintReadCountForTests() {
|
|
55
|
+
return _fingerprintReadCount;
|
|
36
56
|
}
|
|
37
57
|
function normalizeTraceMode(raw) {
|
|
38
58
|
if (raw === undefined || raw === null || raw === '' || raw === 'off')
|
|
@@ -86,41 +106,19 @@ export function coerceAndValidateArgs(cmdArgs, kwargs) {
|
|
|
86
106
|
async function runCommand(cmd, page, kwargs, debug) {
|
|
87
107
|
const internal = cmd;
|
|
88
108
|
if (internal._lazy && internal._modulePath) {
|
|
89
|
-
const
|
|
90
|
-
//
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
if (prevMtime !== undefined && stat.mtimeMs !== prevMtime) {
|
|
97
|
-
_loadedModules.delete(modulePath);
|
|
98
|
-
_moduleMtimes.delete(modulePath);
|
|
99
|
-
}
|
|
100
|
-
}
|
|
101
|
-
catch { /* file may have been deleted; let import below handle it */ }
|
|
102
|
-
}
|
|
103
|
-
if (!_loadedModules.has(modulePath)) {
|
|
104
|
-
const url = pathToFileURL(modulePath).href;
|
|
105
|
-
const importUrl = _moduleMtimes.has(modulePath) ? `${url}?t=${Date.now()}` : url;
|
|
106
|
-
const loadPromise = import(importUrl).then(() => {
|
|
107
|
-
try {
|
|
108
|
-
_moduleMtimes.set(modulePath, fs.statSync(modulePath).mtimeMs);
|
|
109
|
-
}
|
|
110
|
-
catch { }
|
|
111
|
-
}, (err) => {
|
|
112
|
-
_loadedModules.delete(modulePath);
|
|
113
|
-
throw adapterLoadError(`Failed to load adapter module ${modulePath}: ${getErrorMessage(err)}`, 'Check that the adapter file exists and has no syntax errors.');
|
|
114
|
-
});
|
|
115
|
-
_loadedModules.set(modulePath, loadPromise);
|
|
116
|
-
}
|
|
117
|
-
await _loadedModules.get(modulePath);
|
|
118
|
-
const updated = getRegistry().get(fullName(cmd));
|
|
119
|
-
if (updated?.func) {
|
|
109
|
+
const loadedEntry = await loadLazyModule(internal);
|
|
110
|
+
// Use the command registered by this import generation. A newer file
|
|
111
|
+
// generation may already be queued/current, but an execution that began
|
|
112
|
+
// on the older generation is allowed to finish with its own command.
|
|
113
|
+
const updated = loadedEntry?.registeredCommands.get(fullName(cmd));
|
|
114
|
+
if (loadedEntry && updated?.func) {
|
|
115
|
+
finalizeCommandRegistration(loadedEntry, fullName(cmd));
|
|
120
116
|
return runCommandFunc(updated, page, kwargs, debug);
|
|
121
117
|
}
|
|
122
|
-
if (updated?.pipeline)
|
|
118
|
+
if (loadedEntry && updated?.pipeline) {
|
|
119
|
+
finalizeCommandRegistration(loadedEntry, fullName(cmd));
|
|
123
120
|
return executePipeline(page, updated.pipeline, { args: kwargs, debug });
|
|
121
|
+
}
|
|
124
122
|
}
|
|
125
123
|
if (cmd.func)
|
|
126
124
|
return runCommandFunc(cmd, page, kwargs, debug);
|
|
@@ -128,14 +126,212 @@ async function runCommand(cmd, page, kwargs, debug) {
|
|
|
128
126
|
return executePipeline(page, cmd.pipeline, { args: kwargs, debug });
|
|
129
127
|
throw new CommandExecutionError(`Command ${fullName(cmd)} has no func or pipeline`, 'This is likely a bug in the adapter definition. Please report this issue.');
|
|
130
128
|
}
|
|
129
|
+
function moduleFingerprint(modulePath) {
|
|
130
|
+
try {
|
|
131
|
+
_fingerprintReadCount += 1;
|
|
132
|
+
return crypto.createHash('sha256').update(fs.readFileSync(modulePath)).digest('hex');
|
|
133
|
+
}
|
|
134
|
+
catch {
|
|
135
|
+
return undefined;
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
function isHotReloadableModule(modulePath) {
|
|
139
|
+
const prefix = getUserClisDir() + path.sep;
|
|
140
|
+
return modulePath.startsWith(prefix);
|
|
141
|
+
}
|
|
142
|
+
function refreshCapturedCommands(entry) {
|
|
143
|
+
entry.registeredCommands = capturedRegistryValues(entry.transaction);
|
|
144
|
+
}
|
|
145
|
+
function rollbackImport(entry) {
|
|
146
|
+
rollbackRegistryTransaction(entry.transaction, getRegistry());
|
|
147
|
+
}
|
|
148
|
+
function rollbackCommandRegistration(entry, key) {
|
|
149
|
+
const groups = transactionGroupsForKey(entry.transaction, key);
|
|
150
|
+
rollbackRegistryTransaction(entry.transaction, getRegistry(), groups);
|
|
151
|
+
}
|
|
152
|
+
function finalizeCommandRegistration(entry, key) {
|
|
153
|
+
const groups = transactionGroupsForKey(entry.transaction, key);
|
|
154
|
+
finalizeRegistryTransaction(entry.transaction, getRegistry(), groups);
|
|
155
|
+
}
|
|
156
|
+
async function performRegistrationImport(entry, importUrl, modulePath) {
|
|
157
|
+
if (_registrationImportsPoisoned)
|
|
158
|
+
throw poisonedImportError();
|
|
159
|
+
const settlement = runRegistryTransaction(entry.transaction, () => import(importUrl)).then(() => ({ ok: true }), (error) => ({ ok: false, error }));
|
|
160
|
+
let timeoutId;
|
|
161
|
+
const timeout = new Promise((resolve) => {
|
|
162
|
+
timeoutId = setTimeout(() => resolve({ timeout: true }), adapterImportTimeoutMs());
|
|
163
|
+
});
|
|
164
|
+
const outcome = await Promise.race([settlement, timeout]);
|
|
165
|
+
if ('timeout' in outcome) {
|
|
166
|
+
_registrationImportsPoisoned = true;
|
|
167
|
+
closeRegistryTransaction(entry.transaction);
|
|
168
|
+
rollbackImport(entry);
|
|
169
|
+
void settlement.then(() => {
|
|
170
|
+
refreshCapturedCommands(entry);
|
|
171
|
+
rollbackImport(entry);
|
|
172
|
+
});
|
|
173
|
+
throw adapterLoadError(`Adapter module ${modulePath} registration timed out after ${adapterImportTimeoutMs()}ms.`, 'Restart byCLI before retrying; ESM evaluation cannot be cancelled safely.');
|
|
174
|
+
}
|
|
175
|
+
if (timeoutId)
|
|
176
|
+
clearTimeout(timeoutId);
|
|
177
|
+
refreshCapturedCommands(entry);
|
|
178
|
+
if (!outcome.ok) {
|
|
179
|
+
rollbackImport(entry);
|
|
180
|
+
throw adapterLoadError(`Failed to load adapter module ${modulePath}: ${getErrorMessage(outcome.error)}`, 'Check that the adapter file exists and has no syntax errors.');
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
function scheduleRegistrationImport(entry, importUrl, modulePath) {
|
|
184
|
+
const scheduled = _registrationImportTail.then(() => performRegistrationImport(entry, importUrl, modulePath), () => performRegistrationImport(entry, importUrl, modulePath));
|
|
185
|
+
_registrationImportTail = scheduled.then(() => undefined, () => undefined);
|
|
186
|
+
return scheduled;
|
|
187
|
+
}
|
|
188
|
+
async function loadLazyModule(internal) {
|
|
189
|
+
const modulePath = internal._modulePath;
|
|
190
|
+
if (!internal._lazy || !modulePath)
|
|
191
|
+
return;
|
|
192
|
+
let entry = _loadedModules.get(modulePath);
|
|
193
|
+
if (entry) {
|
|
194
|
+
if (!entry.hotReloadable) {
|
|
195
|
+
await entry.promise;
|
|
196
|
+
return entry;
|
|
197
|
+
}
|
|
198
|
+
const currentFingerprint = moduleFingerprint(modulePath);
|
|
199
|
+
if (entry.fingerprint === currentFingerprint) {
|
|
200
|
+
await entry.promise;
|
|
201
|
+
return entry;
|
|
202
|
+
}
|
|
203
|
+
if (_registrationImportsPoisoned)
|
|
204
|
+
throw poisonedImportError();
|
|
205
|
+
invalidateLazyModule(modulePath, entry);
|
|
206
|
+
entry = undefined;
|
|
207
|
+
}
|
|
208
|
+
if (_registrationImportsPoisoned)
|
|
209
|
+
throw poisonedImportError();
|
|
210
|
+
if (!entry) {
|
|
211
|
+
const hotReloadable = isHotReloadableModule(modulePath);
|
|
212
|
+
const fingerprint = hotReloadable ? moduleFingerprint(modulePath) : undefined;
|
|
213
|
+
const url = pathToFileURL(modulePath).href;
|
|
214
|
+
const generation = _moduleImportGenerations.get(modulePath) ?? 0;
|
|
215
|
+
const importUrl = generation === 0 ? url : `${url}?v=${generation}`;
|
|
216
|
+
const createdEntry = {
|
|
217
|
+
promise: Promise.resolve(),
|
|
218
|
+
fingerprint,
|
|
219
|
+
generation,
|
|
220
|
+
registeredCommands: new Map(),
|
|
221
|
+
transaction: createRegistryTransaction(),
|
|
222
|
+
hotReloadable,
|
|
223
|
+
};
|
|
224
|
+
createdEntry.promise = scheduleRegistrationImport(createdEntry, importUrl, modulePath).catch((error) => {
|
|
225
|
+
invalidateLazyModule(modulePath, createdEntry);
|
|
226
|
+
throw error;
|
|
227
|
+
});
|
|
228
|
+
_loadedModules.set(modulePath, createdEntry);
|
|
229
|
+
entry = createdEntry;
|
|
230
|
+
}
|
|
231
|
+
await entry.promise;
|
|
232
|
+
return entry;
|
|
233
|
+
}
|
|
234
|
+
function invalidateLazyModule(modulePath, expectedEntry) {
|
|
235
|
+
if (!modulePath)
|
|
236
|
+
return;
|
|
237
|
+
const currentEntry = _loadedModules.get(modulePath);
|
|
238
|
+
if (!currentEntry || (expectedEntry && currentEntry !== expectedEntry))
|
|
239
|
+
return;
|
|
240
|
+
if (currentEntry.transaction.active) {
|
|
241
|
+
void currentEntry.promise.then(() => finalizeRegistryTransaction(currentEntry.transaction, getRegistry()), () => finalizeRegistryTransaction(currentEntry.transaction, getRegistry()));
|
|
242
|
+
}
|
|
243
|
+
else {
|
|
244
|
+
finalizeRegistryTransaction(currentEntry.transaction, getRegistry());
|
|
245
|
+
}
|
|
246
|
+
_loadedModules.delete(modulePath);
|
|
247
|
+
_moduleImportGenerations.set(modulePath, Math.max(_moduleImportGenerations.get(modulePath) ?? 0, currentEntry.generation) + 1);
|
|
248
|
+
}
|
|
249
|
+
function assertMatchingArgSchema(placeholder, hydrated, loadedEntry) {
|
|
250
|
+
const key = fullName(placeholder);
|
|
251
|
+
try {
|
|
252
|
+
const manifestSchema = canonicalizeManifestArgSchema(placeholder.args, `Manifest command ${key}`);
|
|
253
|
+
const moduleSchema = canonicalizeManifestArgSchema(hydrated.args, `Hydrated command ${key}`);
|
|
254
|
+
if (manifestSchema === moduleSchema)
|
|
255
|
+
return;
|
|
256
|
+
}
|
|
257
|
+
catch (error) {
|
|
258
|
+
if (loadedEntry)
|
|
259
|
+
rollbackCommandRegistration(loadedEntry, key);
|
|
260
|
+
invalidateLazyModule(placeholder._modulePath, loadedEntry);
|
|
261
|
+
if (!(error instanceof ManifestSchemaError))
|
|
262
|
+
throw error;
|
|
263
|
+
throw new CommandExecutionError(`Conditional adapter ${key} has an unsafe argument schema: ${error.message}`, 'Use only JSON-safe defaults and string choices, then rebuild the CLI manifest.');
|
|
264
|
+
}
|
|
265
|
+
if (loadedEntry)
|
|
266
|
+
rollbackCommandRegistration(loadedEntry, key);
|
|
267
|
+
invalidateLazyModule(placeholder._modulePath, loadedEntry);
|
|
268
|
+
throw new CommandExecutionError(`Conditional adapter ${key} argument schema does not match its manifest`, 'Rebuild the CLI manifest so defaults, coercion, and validation use the adapter module schema.');
|
|
269
|
+
}
|
|
270
|
+
async function hydrateConditionalCommand(cmd) {
|
|
271
|
+
const internal = cmd;
|
|
272
|
+
const key = fullName(cmd);
|
|
273
|
+
if (cmd.browser !== 'conditional') {
|
|
274
|
+
throw new CommandExecutionError(`Conditional adapter placeholder ${key} has invalid browser metadata`, 'Rebuild the CLI manifest with the current byCLI version.');
|
|
275
|
+
}
|
|
276
|
+
const placeholderResolver = cmd.requiresBrowser;
|
|
277
|
+
let loadedEntry;
|
|
278
|
+
try {
|
|
279
|
+
loadedEntry = await loadLazyModule(internal);
|
|
280
|
+
}
|
|
281
|
+
catch (error) {
|
|
282
|
+
if (error instanceof CliError)
|
|
283
|
+
throw error;
|
|
284
|
+
throw new CommandExecutionError(`Failed to hydrate conditional adapter ${key}: ${getErrorMessage(error)}`, 'Rebuild or reinstall the adapter so its manifest and module are both available.');
|
|
285
|
+
}
|
|
286
|
+
const hydrated = loadedEntry?.registeredCommands.get(key);
|
|
287
|
+
const invalidReplacement = !hydrated
|
|
288
|
+
|| hydrated === cmd
|
|
289
|
+
|| fullName(hydrated) !== key
|
|
290
|
+
|| hydrated.browser !== 'conditional';
|
|
291
|
+
if (invalidReplacement) {
|
|
292
|
+
if (loadedEntry)
|
|
293
|
+
rollbackCommandRegistration(loadedEntry, key);
|
|
294
|
+
invalidateLazyModule(internal._modulePath, loadedEntry);
|
|
295
|
+
throw new CommandExecutionError(`Conditional adapter ${key} did not register a valid hydrated conditional command`, 'Ensure the module registers the same site/name with a browser predicate and is rebuilt with the current byCLI version.');
|
|
296
|
+
}
|
|
297
|
+
const hydratedInternal = hydrated;
|
|
298
|
+
if (typeof hydrated.requiresBrowser !== 'function'
|
|
299
|
+
|| hydrated.requiresBrowser === placeholderResolver
|
|
300
|
+
|| hydratedInternal._hydrateBeforeBrowserRouting === true
|
|
301
|
+
|| hydratedInternal._lazy === true
|
|
302
|
+
|| hydratedInternal._modulePath !== undefined) {
|
|
303
|
+
if (loadedEntry)
|
|
304
|
+
rollbackCommandRegistration(loadedEntry, key);
|
|
305
|
+
invalidateLazyModule(internal._modulePath, loadedEntry);
|
|
306
|
+
throw new CommandExecutionError(`Conditional adapter ${key} did not register a valid hydrated conditional command`, 'Ensure the module registers the same site/name with a browser predicate and is rebuilt with the current byCLI version.');
|
|
307
|
+
}
|
|
308
|
+
assertMatchingArgSchema(cmd, hydrated, loadedEntry);
|
|
309
|
+
if (loadedEntry)
|
|
310
|
+
finalizeCommandRegistration(loadedEntry, key);
|
|
311
|
+
return hydrated;
|
|
312
|
+
}
|
|
131
313
|
function runCommandFunc(cmd, page, kwargs, debug) {
|
|
132
314
|
if (cmd.browser === false)
|
|
133
315
|
return cmd.func(kwargs, debug);
|
|
316
|
+
if (cmd.browser === 'conditional')
|
|
317
|
+
return cmd.func(page, kwargs, debug);
|
|
134
318
|
if (!page) {
|
|
135
319
|
throw new CommandExecutionError(`Command ${fullName(cmd)} requires a browser session but none was provided`);
|
|
136
320
|
}
|
|
137
321
|
return cmd.func(page, kwargs, debug);
|
|
138
322
|
}
|
|
323
|
+
function resolveBrowserRequirement(cmd, kwargs) {
|
|
324
|
+
if (cmd.browser !== 'conditional')
|
|
325
|
+
return cmd.browser;
|
|
326
|
+
try {
|
|
327
|
+
return Boolean(cmd.requiresBrowser(kwargs));
|
|
328
|
+
}
|
|
329
|
+
catch (error) {
|
|
330
|
+
if (error instanceof CliError)
|
|
331
|
+
throw error;
|
|
332
|
+
throw new CommandExecutionError(`Browser requirement evaluation failed for ${fullName(cmd)}: ${getErrorMessage(error)}`);
|
|
333
|
+
}
|
|
334
|
+
}
|
|
139
335
|
function resolvePreNav(cmd) {
|
|
140
336
|
if (cmd.navigateBefore === false)
|
|
141
337
|
return null;
|
|
@@ -177,16 +373,24 @@ async function shouldRunPreNav(cmd, page, siteSession, preNavUrl) {
|
|
|
177
373
|
return !urlMatchesDomain(currentUrl, cmd.domain);
|
|
178
374
|
}
|
|
179
375
|
export async function executeCommand(cmd, rawKwargs, debug = false, opts = {}) {
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
376
|
+
const initialTraceCommand = cmd;
|
|
377
|
+
let kwargs = opts.prepared
|
|
378
|
+
? rawKwargs
|
|
379
|
+
: prepareCommandArgsOrThrowArgumentError(cmd, rawKwargs);
|
|
380
|
+
if (cmd._hydrateBeforeBrowserRouting) {
|
|
381
|
+
cmd = await hydrateConditionalCommand(cmd);
|
|
382
|
+
// Manifest placeholders cannot serialize adapter validators. The matching
|
|
383
|
+
// schema above guarantees coercion/defaults stay valid; run only the real
|
|
384
|
+
// module validator here so custom validation has exactly one side effect.
|
|
385
|
+
try {
|
|
386
|
+
cmd.validateArgs?.(kwargs);
|
|
387
|
+
}
|
|
388
|
+
catch (error) {
|
|
389
|
+
if (error instanceof ArgumentError)
|
|
390
|
+
throw error;
|
|
391
|
+
throw new ArgumentError(getErrorMessage(error));
|
|
392
|
+
}
|
|
188
393
|
}
|
|
189
|
-
const userTimeoutSec = readUserTimeoutSeconds(cmd, kwargs);
|
|
190
394
|
const traceMode = normalizeTraceMode(opts.trace);
|
|
191
395
|
const hookCtx = {
|
|
192
396
|
command: fullName(cmd),
|
|
@@ -194,9 +398,12 @@ export async function executeCommand(cmd, rawKwargs, debug = false, opts = {}) {
|
|
|
194
398
|
startedAt: Date.now(),
|
|
195
399
|
};
|
|
196
400
|
await emitHook('onBeforeExecute', hookCtx);
|
|
401
|
+
kwargs = hookCtx.args;
|
|
197
402
|
let result;
|
|
198
403
|
try {
|
|
199
|
-
|
|
404
|
+
const resolvedBrowser = resolveBrowserRequirement(cmd, kwargs);
|
|
405
|
+
const userTimeoutSec = readUserTimeoutSeconds(cmd, kwargs);
|
|
406
|
+
if (shouldUseBrowserSession(cmd, resolvedBrowser)) {
|
|
200
407
|
const electron = isElectronApp(cmd.site);
|
|
201
408
|
let cdpEndpoint;
|
|
202
409
|
if (electron) {
|
|
@@ -230,7 +437,8 @@ export async function executeCommand(cmd, rawKwargs, debug = false, opts = {}) {
|
|
|
230
437
|
target: page.getActivePage?.(),
|
|
231
438
|
site: cmd.site,
|
|
232
439
|
command: fullName(cmd),
|
|
233
|
-
adapterSourcePath: resolveAdapterSourcePath(internal)
|
|
440
|
+
adapterSourcePath: resolveAdapterSourcePath(internal)
|
|
441
|
+
?? resolveAdapterSourcePath(initialTraceCommand),
|
|
234
442
|
},
|
|
235
443
|
});
|
|
236
444
|
if (observation) {
|
|
@@ -438,6 +646,17 @@ export function prepareCommandArgs(cmd, rawKwargs) {
|
|
|
438
646
|
cmd.validateArgs?.(kwargs);
|
|
439
647
|
return kwargs;
|
|
440
648
|
}
|
|
649
|
+
/** Prepare adapter arguments using the execution boundary's public error contract. */
|
|
650
|
+
export function prepareCommandArgsOrThrowArgumentError(cmd, rawKwargs) {
|
|
651
|
+
try {
|
|
652
|
+
return prepareCommandArgs(cmd, rawKwargs);
|
|
653
|
+
}
|
|
654
|
+
catch (err) {
|
|
655
|
+
if (err instanceof ArgumentError)
|
|
656
|
+
throw err;
|
|
657
|
+
throw new ArgumentError(getErrorMessage(err));
|
|
658
|
+
}
|
|
659
|
+
}
|
|
441
660
|
/**
|
|
442
661
|
* Runtime ceiling padding (seconds) added on top of the user's `--timeout`.
|
|
443
662
|
* The adapter's polling loop typically uses the full user value; the padding
|
package/dist/src/help.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import yaml from 'js-yaml';
|
|
2
|
-
import { fullName } from './registry.js';
|
|
2
|
+
import { browserRequirementLabel, fullName, hasBrowserCapability } from './registry.js';
|
|
3
3
|
import { formatCommandExample } from './serialization.js';
|
|
4
4
|
const COMMON_OPTIONS = [
|
|
5
5
|
{
|
|
@@ -363,12 +363,12 @@ function compactCommand(cmd) {
|
|
|
363
363
|
usage: formatUsage(cmd),
|
|
364
364
|
access: cmd.access,
|
|
365
365
|
description: cmd.description,
|
|
366
|
-
browser:
|
|
366
|
+
browser: cmd.browser,
|
|
367
367
|
...(cmd.domain ? { domain: cmd.domain } : {}),
|
|
368
368
|
...(cmd.aliases?.length ? { aliases: cmd.aliases } : {}),
|
|
369
369
|
positionals: positionals(cmd).map(compactArg),
|
|
370
370
|
command_options: commandOptions(cmd).map(compactArg),
|
|
371
|
-
...(cmd
|
|
371
|
+
...(hasBrowserCapability(cmd) ? { browser_common_options: BROWSER_COMMON_OPTIONS.map(compactCommonOption) } : {}),
|
|
372
372
|
example: formatCommandExample(cmd),
|
|
373
373
|
...(cmd.siteSession ? { siteSession: cmd.siteSession } : {}),
|
|
374
374
|
...(cmd.defaultFormat ? { defaultFormat: cmd.defaultFormat } : {}),
|
|
@@ -416,7 +416,7 @@ export function siteHelpData(site, commands) {
|
|
|
416
416
|
command_count: unique.length,
|
|
417
417
|
commands: unique.map(cmd => compactCommand(cmd)),
|
|
418
418
|
common_options: COMMON_OPTIONS.map(compactCommonOption),
|
|
419
|
-
...(unique.some(
|
|
419
|
+
...(unique.some(hasBrowserCapability) ? { browser_common_options: BROWSER_COMMON_OPTIONS.map(compactCommonOption) } : {}),
|
|
420
420
|
next: [
|
|
421
421
|
`bycli ${site} <command> --help -f yaml`,
|
|
422
422
|
`bycli ${site} <command> -f yaml`,
|
|
@@ -428,7 +428,7 @@ export function commandHelpData(cmd) {
|
|
|
428
428
|
site: cmd.site,
|
|
429
429
|
...compactCommand(cmd),
|
|
430
430
|
common_options: COMMON_OPTIONS.map(compactCommonOption),
|
|
431
|
-
...(cmd
|
|
431
|
+
...(hasBrowserCapability(cmd) ? { browser_common_options: BROWSER_COMMON_OPTIONS.map(compactCommonOption) } : {}),
|
|
432
432
|
output_formats: ['table', 'plain', 'yaml', 'json', 'md', 'csv'],
|
|
433
433
|
};
|
|
434
434
|
}
|
|
@@ -480,7 +480,7 @@ export function formatSiteHelpText(site, commands) {
|
|
|
480
480
|
...formatRows(unique.map(cmd => [formatCommandListTerm(cmd), formatSiteCommandDescription(cmd)])),
|
|
481
481
|
'',
|
|
482
482
|
formatCommonOptionsHelpText(),
|
|
483
|
-
...(unique.some(
|
|
483
|
+
...(unique.some(hasBrowserCapability) ? ['', formatBrowserCommonOptionsHelpText()] : []),
|
|
484
484
|
'',
|
|
485
485
|
`Agent tip: use 'bycli ${site} --help -f yaml' to get all command args/options in one structured response.`,
|
|
486
486
|
'',
|
|
@@ -509,11 +509,11 @@ export function formatCommandHelpText(cmd) {
|
|
|
509
509
|
lines.push('Command options:', ...formatRows(optionRows), '');
|
|
510
510
|
}
|
|
511
511
|
lines.push(formatCommonOptionsHelpText(), '');
|
|
512
|
-
if (cmd
|
|
512
|
+
if (hasBrowserCapability(cmd))
|
|
513
513
|
lines.push(formatBrowserCommonOptionsHelpText(), '');
|
|
514
514
|
const meta = [];
|
|
515
515
|
meta.push(`Access: ${cmd.access}`);
|
|
516
|
-
meta.push(`Browser: ${cmd
|
|
516
|
+
meta.push(`Browser: ${browserRequirementLabel(cmd)}`);
|
|
517
517
|
if (cmd.domain)
|
|
518
518
|
meta.push(`Domain: ${cmd.domain}`);
|
|
519
519
|
if (cmd.defaultFormat)
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import type { Arg } from './registry.js';
|
|
2
|
+
export declare class ManifestSchemaError extends Error {
|
|
3
|
+
constructor(message: string);
|
|
4
|
+
}
|
|
5
|
+
/**
|
|
6
|
+
* Canonical execution-relevant argument schema used by both manifest build and
|
|
7
|
+
* runtime hydration. Objects are key-order independent; arrays remain ordered.
|
|
8
|
+
*/
|
|
9
|
+
export declare function canonicalizeManifestArgSchema(args: readonly Arg[], context: string): string;
|
|
@@ -0,0 +1,162 @@
|
|
|
1
|
+
export class ManifestSchemaError extends Error {
|
|
2
|
+
constructor(message) {
|
|
3
|
+
super(message);
|
|
4
|
+
this.name = 'ManifestSchemaError';
|
|
5
|
+
}
|
|
6
|
+
}
|
|
7
|
+
function fail(context, path, reason) {
|
|
8
|
+
throw new ManifestSchemaError(`${context} ${path}: ${reason}`);
|
|
9
|
+
}
|
|
10
|
+
function markSeen(value, context, path, seen) {
|
|
11
|
+
if (seen.has(value))
|
|
12
|
+
fail(context, path, 'cyclic or repeated/shared object identities cannot be represented faithfully in JSON');
|
|
13
|
+
seen.add(value);
|
|
14
|
+
}
|
|
15
|
+
function denseArrayValues(value, context, path) {
|
|
16
|
+
if (Object.getPrototypeOf(value) !== Array.prototype) {
|
|
17
|
+
fail(context, path, 'must be a standard JSON array (custom array instances are not supported)');
|
|
18
|
+
}
|
|
19
|
+
if (Object.getOwnPropertySymbols(value).length > 0) {
|
|
20
|
+
fail(context, path, 'symbol-keyed array properties cannot be represented in JSON');
|
|
21
|
+
}
|
|
22
|
+
const propertyNames = Object.getOwnPropertyNames(value);
|
|
23
|
+
const expectedNames = new Set(['length']);
|
|
24
|
+
for (let index = 0; index < value.length; index += 1)
|
|
25
|
+
expectedNames.add(String(index));
|
|
26
|
+
for (const propertyName of propertyNames) {
|
|
27
|
+
if (!expectedNames.has(propertyName)) {
|
|
28
|
+
fail(context, `${path}.${propertyName}`, 'extra array properties cannot be represented in JSON');
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
if (propertyNames.length !== expectedNames.size) {
|
|
32
|
+
fail(context, path, 'sparse arrays cannot be represented faithfully in JSON');
|
|
33
|
+
}
|
|
34
|
+
const lengthDescriptor = Object.getOwnPropertyDescriptor(value, 'length');
|
|
35
|
+
if (!lengthDescriptor
|
|
36
|
+
|| !('value' in lengthDescriptor)
|
|
37
|
+
|| lengthDescriptor.value !== value.length
|
|
38
|
+
|| lengthDescriptor.enumerable
|
|
39
|
+
|| lengthDescriptor.configurable
|
|
40
|
+
|| !lengthDescriptor.writable) {
|
|
41
|
+
fail(context, `${path}.length`, 'non-standard array length descriptors cannot be represented faithfully in JSON');
|
|
42
|
+
}
|
|
43
|
+
const values = [];
|
|
44
|
+
for (let index = 0; index < value.length; index += 1) {
|
|
45
|
+
const descriptor = Object.getOwnPropertyDescriptor(value, String(index));
|
|
46
|
+
if (!descriptor)
|
|
47
|
+
fail(context, `${path}[${index}]`, 'sparse arrays cannot be represented faithfully in JSON');
|
|
48
|
+
if (!('value' in descriptor)
|
|
49
|
+
|| !descriptor.enumerable
|
|
50
|
+
|| !descriptor.configurable
|
|
51
|
+
|| !descriptor.writable) {
|
|
52
|
+
fail(context, `${path}[${index}]`, 'non-standard array item descriptors cannot be represented faithfully in JSON');
|
|
53
|
+
}
|
|
54
|
+
values.push(descriptor.value);
|
|
55
|
+
}
|
|
56
|
+
return values;
|
|
57
|
+
}
|
|
58
|
+
function canonicalJsonValue(value, context, path, seen) {
|
|
59
|
+
if (value === null)
|
|
60
|
+
return 'null';
|
|
61
|
+
if (typeof value === 'string')
|
|
62
|
+
return `string:${JSON.stringify(value)}`;
|
|
63
|
+
if (typeof value === 'boolean')
|
|
64
|
+
return `boolean:${value}`;
|
|
65
|
+
if (typeof value === 'number') {
|
|
66
|
+
if (!Number.isFinite(value))
|
|
67
|
+
fail(context, path, 'must be a finite JSON number');
|
|
68
|
+
if (Object.is(value, -0))
|
|
69
|
+
fail(context, path, 'negative zero cannot be represented faithfully in JSON');
|
|
70
|
+
return `number:${String(value)}`;
|
|
71
|
+
}
|
|
72
|
+
if (value === undefined)
|
|
73
|
+
fail(context, path, 'explicit undefined cannot be represented in JSON');
|
|
74
|
+
if (typeof value === 'function')
|
|
75
|
+
fail(context, path, 'functions cannot be represented in JSON');
|
|
76
|
+
if (typeof value === 'symbol')
|
|
77
|
+
fail(context, path, 'symbols cannot be represented in JSON');
|
|
78
|
+
if (typeof value === 'bigint')
|
|
79
|
+
fail(context, path, 'bigints cannot be represented in JSON');
|
|
80
|
+
markSeen(value, context, path, seen);
|
|
81
|
+
{
|
|
82
|
+
if (Array.isArray(value)) {
|
|
83
|
+
const items = denseArrayValues(value, context, path)
|
|
84
|
+
.map((item, index) => canonicalJsonValue(item, context, `${path}[${index}]`, seen));
|
|
85
|
+
return `array:[${items.join(',')}]`;
|
|
86
|
+
}
|
|
87
|
+
if (Object.getPrototypeOf(value) !== Object.prototype) {
|
|
88
|
+
fail(context, path, 'must be a plain JSON object (Date and custom instances are not supported)');
|
|
89
|
+
}
|
|
90
|
+
if (Object.getOwnPropertySymbols(value).length > 0) {
|
|
91
|
+
fail(context, path, 'symbol-keyed properties cannot be represented in JSON');
|
|
92
|
+
}
|
|
93
|
+
const propertyNames = Object.getOwnPropertyNames(value);
|
|
94
|
+
const entries = [];
|
|
95
|
+
for (const key of propertyNames.sort()) {
|
|
96
|
+
const descriptor = Object.getOwnPropertyDescriptor(value, key);
|
|
97
|
+
if (!descriptor?.enumerable
|
|
98
|
+
|| !('value' in descriptor)
|
|
99
|
+
|| !descriptor.configurable
|
|
100
|
+
|| !descriptor.writable) {
|
|
101
|
+
fail(context, `${path}.${key}`, 'only enumerable data properties can be represented faithfully in JSON');
|
|
102
|
+
}
|
|
103
|
+
entries.push(`${JSON.stringify(key)}:${canonicalJsonValue(descriptor.value, context, `${path}.${key}`, seen)}`);
|
|
104
|
+
}
|
|
105
|
+
return `object:{${entries.join(',')}}`;
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
function optionalBoolean(value, context, path) {
|
|
109
|
+
if (value === undefined)
|
|
110
|
+
return false;
|
|
111
|
+
if (typeof value !== 'boolean')
|
|
112
|
+
fail(context, path, 'must be a boolean when present');
|
|
113
|
+
return value;
|
|
114
|
+
}
|
|
115
|
+
/**
|
|
116
|
+
* Canonical execution-relevant argument schema used by both manifest build and
|
|
117
|
+
* runtime hydration. Objects are key-order independent; arrays remain ordered.
|
|
118
|
+
*/
|
|
119
|
+
export function canonicalizeManifestArgSchema(args, context) {
|
|
120
|
+
if (!Array.isArray(args))
|
|
121
|
+
fail(context, 'args', 'must be a dense array');
|
|
122
|
+
const seen = new Set();
|
|
123
|
+
markSeen(args, context, 'args', seen);
|
|
124
|
+
const argValues = denseArrayValues(args, context, 'args');
|
|
125
|
+
return argValues.map((arg, index) => {
|
|
126
|
+
const argContext = `${context} argument "${String(arg.name)}"`;
|
|
127
|
+
if (typeof arg.name !== 'string' || arg.name.length === 0)
|
|
128
|
+
fail(argContext, 'name', 'must be a non-empty string');
|
|
129
|
+
if (arg.type !== undefined && typeof arg.type !== 'string')
|
|
130
|
+
fail(argContext, 'type', 'must be a string when present');
|
|
131
|
+
if (arg.help !== undefined && typeof arg.help !== 'string')
|
|
132
|
+
fail(argContext, 'help', 'must be a string when present');
|
|
133
|
+
const hasDefault = Object.prototype.hasOwnProperty.call(arg, 'default');
|
|
134
|
+
const defaultValue = hasDefault
|
|
135
|
+
? canonicalJsonValue(arg.default, argContext, 'default', seen)
|
|
136
|
+
: 'missing';
|
|
137
|
+
const hasChoices = Object.prototype.hasOwnProperty.call(arg, 'choices');
|
|
138
|
+
let choices = 'missing';
|
|
139
|
+
if (hasChoices) {
|
|
140
|
+
if (!Array.isArray(arg.choices))
|
|
141
|
+
fail(argContext, 'choices', 'must be an array of strings when present');
|
|
142
|
+
markSeen(arg.choices, argContext, 'choices', seen);
|
|
143
|
+
choices = `array:[${denseArrayValues(arg.choices, argContext, 'choices').map((choice, choiceIndex) => {
|
|
144
|
+
if (typeof choice !== 'string') {
|
|
145
|
+
fail(argContext, `choices[${choiceIndex}]`, 'choice values must be strings');
|
|
146
|
+
}
|
|
147
|
+
return `string:${JSON.stringify(choice)}`;
|
|
148
|
+
}).join(',')}]`;
|
|
149
|
+
}
|
|
150
|
+
return [
|
|
151
|
+
`arg:${index}`,
|
|
152
|
+
`name:${JSON.stringify(arg.name)}`,
|
|
153
|
+
`type:${JSON.stringify(arg.type ?? 'str')}`,
|
|
154
|
+
`default:${defaultValue}`,
|
|
155
|
+
`required:${optionalBoolean(arg.required, argContext, 'required')}`,
|
|
156
|
+
`valueRequired:${optionalBoolean(arg.valueRequired, argContext, 'valueRequired')}`,
|
|
157
|
+
`positional:${optionalBoolean(arg.positional, argContext, 'positional')}`,
|
|
158
|
+
`help:${JSON.stringify(arg.help ?? '')}`,
|
|
159
|
+
`choices:${choices}`,
|
|
160
|
+
].join('|');
|
|
161
|
+
}).join('||');
|
|
162
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|