@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
@@ -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
- /** Track mtime of loaded user adapter files for hot-reload in daemon mode. */
31
- const _moduleMtimes = new Map();
32
- /** User adapter dir with a trailing separator, so `startsWith` can't match a sibling
33
- * like `clis-foo`. Resolved per-call (config-paths.ts rationale) — no import-time freeze. */
34
- function userClisPrefix() {
35
- return getUserClisDir() + path.sep;
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 modulePath = internal._modulePath;
90
- // Hot-reload: if a user adapter's file has changed on disk, invalidate cache
91
- const isUserAdapter = modulePath.startsWith(userClisPrefix());
92
- if (isUserAdapter && _loadedModules.has(modulePath)) {
93
- try {
94
- const stat = fs.statSync(modulePath);
95
- const prevMtime = _moduleMtimes.get(modulePath);
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
- let kwargs;
181
- try {
182
- kwargs = opts.prepared ? rawKwargs : prepareCommandArgs(cmd, rawKwargs);
183
- }
184
- catch (err) {
185
- if (err instanceof ArgumentError)
186
- throw err;
187
- throw new ArgumentError(getErrorMessage(err));
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
- if (shouldUseBrowserSession(cmd)) {
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: !!cmd.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.browser ? { browser_common_options: BROWSER_COMMON_OPTIONS.map(compactCommonOption) } : {}),
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(cmd => cmd.browser) ? { browser_common_options: BROWSER_COMMON_OPTIONS.map(compactCommonOption) } : {}),
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.browser ? { browser_common_options: BROWSER_COMMON_OPTIONS.map(compactCommonOption) } : {}),
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(cmd => cmd.browser) ? ['', formatBrowserCommonOptionsHelpText()] : []),
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.browser)
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.browser ? 'yes' : 'no'}`);
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 {};
@@ -15,7 +15,7 @@ export interface ManifestEntry {
15
15
  example?: string;
16
16
  domain?: string;
17
17
  strategy: string;
18
- browser: boolean;
18
+ browser: boolean | 'conditional';
19
19
  args: Array<{
20
20
  name: string;
21
21
  type?: string;