@web-auto/webauto 0.1.8 → 0.1.9

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 (32) hide show
  1. package/apps/desktop-console/dist/main/index.mjs +800 -89
  2. package/apps/desktop-console/dist/main/preload.mjs +3 -0
  3. package/apps/desktop-console/dist/renderer/index.html +9 -1
  4. package/apps/desktop-console/dist/renderer/index.js +784 -331
  5. package/apps/desktop-console/entry/ui-cli.mjs +23 -8
  6. package/apps/desktop-console/entry/ui-console.mjs +8 -3
  7. package/apps/webauto/entry/account.mjs +69 -8
  8. package/apps/webauto/entry/lib/account-detect.mjs +106 -25
  9. package/apps/webauto/entry/lib/account-store.mjs +121 -22
  10. package/apps/webauto/entry/lib/schedule-store.mjs +0 -12
  11. package/apps/webauto/entry/profilepool.mjs +45 -3
  12. package/apps/webauto/entry/schedule.mjs +44 -2
  13. package/apps/webauto/entry/weibo-unified.mjs +2 -2
  14. package/apps/webauto/entry/xhs-install.mjs +220 -51
  15. package/apps/webauto/entry/xhs-unified.mjs +33 -6
  16. package/bin/webauto.mjs +80 -4
  17. package/dist/modules/camo-runtime/src/utils/browser-service.mjs +4 -0
  18. package/dist/services/unified-api/server.js +5 -0
  19. package/dist/services/unified-api/task-state.js +2 -0
  20. package/modules/camo-runtime/src/autoscript/action-providers/xhs/interaction.mjs +142 -14
  21. package/modules/camo-runtime/src/autoscript/action-providers/xhs/search.mjs +16 -1
  22. package/modules/camo-runtime/src/autoscript/action-providers/xhs.mjs +104 -0
  23. package/modules/camo-runtime/src/autoscript/runtime.mjs +14 -4
  24. package/modules/camo-runtime/src/autoscript/schema.mjs +9 -0
  25. package/modules/camo-runtime/src/autoscript/xhs-unified-template.mjs +9 -2
  26. package/modules/camo-runtime/src/container/runtime-core/checkpoint.mjs +107 -1
  27. package/modules/camo-runtime/src/container/runtime-core/subscription.mjs +24 -2
  28. package/modules/camo-runtime/src/utils/browser-service.mjs +4 -0
  29. package/package.json +6 -3
  30. package/scripts/bump-version.mjs +120 -0
  31. package/services/unified-api/server.ts +4 -0
  32. package/services/unified-api/task-state.ts +5 -0
@@ -11,6 +11,7 @@ import {
11
11
  const INDEX_FILE = 'index.json';
12
12
  const META_FILE = 'meta.json';
13
13
  const DEFAULT_PLATFORM = 'xiaohongshu';
14
+ const DEFAULT_PROFILE_PREFIX = 'profile';
14
15
  const STATUS_ACTIVE = 'active';
15
16
  const STATUS_VALID = 'valid';
16
17
  const STATUS_INVALID = 'invalid';
@@ -213,21 +214,31 @@ function saveIndex(index) {
213
214
  return payload;
214
215
  }
215
216
 
216
- function resolveProfilePrefix(platform) {
217
+ function resolveLegacyProfilePrefix(platform) {
217
218
  if (platform === 'xiaohongshu') return 'xiaohongshu-batch';
218
219
  const slug = toSlug(platform) || 'account';
219
220
  return `${slug}-account`;
220
221
  }
221
222
 
223
+ function resolveProfilePrefix() {
224
+ return DEFAULT_PROFILE_PREFIX;
225
+ }
226
+
222
227
  function resolveProfileSeq(profileId, platform) {
223
228
  const value = String(profileId || '').trim();
224
229
  if (!value) return null;
225
- const prefix = resolveProfilePrefix(platform);
226
- const match = value.match(new RegExp(`^${prefix.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')}-([0-9]+)$`));
227
- if (!match) return null;
228
- const seq = Number(match[1]);
229
- if (!Number.isFinite(seq) || seq < 0) return null;
230
- return seq;
230
+ const prefixes = [
231
+ resolveProfilePrefix(),
232
+ resolveLegacyProfilePrefix(platform),
233
+ ].filter(Boolean);
234
+ for (const prefix of prefixes) {
235
+ const match = value.match(new RegExp(`^${prefix.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')}-([0-9]+)$`));
236
+ if (!match) continue;
237
+ const seq = Number(match[1]);
238
+ if (!Number.isFinite(seq) || seq < 0) return null;
239
+ return seq;
240
+ }
241
+ return null;
231
242
  }
232
243
 
233
244
  function resolveUsedAutoSeq(index, platform) {
@@ -280,23 +291,34 @@ function resolveAccountOrThrow(index, key) {
280
291
  throw new Error(`account not found: ${idOrAlias}`);
281
292
  }
282
293
 
283
- function resolveAccountByProfile(index, profileId) {
294
+ function resolveBindingKey(profileId, platform) {
295
+ const pid = String(profileId || '').trim();
296
+ const pf = normalizePlatform(platform);
297
+ if (!pid) return '';
298
+ return `${pid}::${pf}`;
299
+ }
300
+
301
+ function resolveAccountByProfile(index, profileId, platform = null) {
284
302
  const value = String(profileId || '').trim();
285
303
  if (!value) return null;
304
+ const wantedPlatform = platform ? normalizePlatform(platform) : null;
286
305
  let matched = null;
287
306
  for (const item of index.accounts) {
288
307
  if (String(item?.profileId || '').trim() !== value) continue;
308
+ if (wantedPlatform && normalizePlatform(item?.platform) !== wantedPlatform) continue;
289
309
  matched = pickNewerRecord(matched, item);
290
310
  }
291
311
  return matched;
292
312
  }
293
313
 
294
- function resolveAccountByAccountId(index, accountId) {
314
+ function resolveAccountByAccountId(index, accountId, platform = null) {
295
315
  const value = normalizeText(accountId);
296
316
  if (!value) return null;
317
+ const wantedPlatform = platform ? normalizePlatform(platform) : null;
297
318
  let matched = null;
298
319
  for (const item of index.accounts) {
299
320
  if (normalizeText(item?.accountId) !== value) continue;
321
+ if (wantedPlatform && normalizePlatform(item?.platform) !== wantedPlatform) continue;
300
322
  matched = pickNewerRecord(matched, item);
301
323
  }
302
324
  return matched;
@@ -342,39 +364,76 @@ function buildProfileAccountView(profileId, record = null) {
342
364
  };
343
365
  }
344
366
 
345
- export function listAccountProfiles() {
367
+ export function listAccountProfiles(options = {}) {
346
368
  const index = loadIndex();
369
+ const platformFilter = normalizeText(options?.platform) ? normalizePlatform(options.platform) : null;
347
370
  const byAccountId = new Map();
348
371
  for (const record of index.accounts) {
372
+ const recordPlatform = normalizePlatform(record?.platform);
373
+ if (platformFilter && recordPlatform !== platformFilter) continue;
349
374
  const accountId = normalizeText(record?.accountId);
350
375
  if (!accountId) continue;
351
- byAccountId.set(accountId, pickNewerRecord(byAccountId.get(accountId), record));
376
+ const key = `${recordPlatform}::${accountId}`;
377
+ byAccountId.set(key, pickNewerRecord(byAccountId.get(key), record));
352
378
  }
353
379
  const deduped = [];
354
380
  for (const record of index.accounts) {
381
+ const recordPlatform = normalizePlatform(record?.platform);
382
+ if (platformFilter && recordPlatform !== platformFilter) continue;
355
383
  const accountId = normalizeText(record?.accountId);
356
384
  if (accountId) {
357
- if (byAccountId.get(accountId) !== record) continue;
385
+ const key = `${recordPlatform}::${accountId}`;
386
+ if (byAccountId.get(key) !== record) continue;
358
387
  }
359
388
  deduped.push(record);
360
389
  }
361
- const byProfile = new Map();
390
+ const byBinding = new Map();
362
391
  for (const record of deduped) {
363
392
  const profileId = normalizeText(record?.profileId);
364
393
  if (!profileId) continue;
365
- byProfile.set(profileId, pickNewerRecord(byProfile.get(profileId), record));
394
+ const key = resolveBindingKey(profileId, record?.platform);
395
+ if (!key) continue;
396
+ byBinding.set(key, pickNewerRecord(byBinding.get(key), record));
397
+ }
398
+ const rows = Array.from(byBinding.values())
399
+ .sort((a, b) => {
400
+ const seqCmp = Number(a?.seq || 0) - Number(b?.seq || 0);
401
+ if (seqCmp !== 0) return seqCmp;
402
+ const pCmp = String(a?.profileId || '').localeCompare(String(b?.profileId || ''));
403
+ if (pCmp !== 0) return pCmp;
404
+ return normalizePlatform(a?.platform).localeCompare(normalizePlatform(b?.platform));
405
+ })
406
+ .map((record) => buildProfileAccountView(record?.profileId, record));
407
+ const validProfilesSet = new Set();
408
+ const invalidProfilesSet = new Set();
409
+ const validProfilesByPlatform = {};
410
+ const invalidProfilesByPlatform = {};
411
+ for (const row of rows) {
412
+ const platform = normalizePlatform(row.platform);
413
+ if (!validProfilesByPlatform[platform]) validProfilesByPlatform[platform] = [];
414
+ if (!invalidProfilesByPlatform[platform]) invalidProfilesByPlatform[platform] = [];
415
+ if (row.valid) {
416
+ validProfilesSet.add(row.profileId);
417
+ if (!validProfilesByPlatform[platform].includes(row.profileId)) {
418
+ validProfilesByPlatform[platform].push(row.profileId);
419
+ }
420
+ } else {
421
+ invalidProfilesSet.add(row.profileId);
422
+ if (!invalidProfilesByPlatform[platform].includes(row.profileId)) {
423
+ invalidProfilesByPlatform[platform].push(row.profileId);
424
+ }
425
+ }
366
426
  }
367
- const rows = Array.from(byProfile.entries())
368
- .sort((a, b) => (Number(a[1]?.seq || 0) - Number(b[1]?.seq || 0)))
369
- .map(([profileId, record]) => buildProfileAccountView(profileId, record));
370
- const validProfiles = rows.filter((item) => item.valid).map((item) => item.profileId);
371
- const invalidProfiles = rows.filter((item) => !item.valid).map((item) => item.profileId);
427
+ const validProfiles = Array.from(validProfilesSet);
428
+ const invalidProfiles = Array.from(invalidProfilesSet).filter((profileId) => !validProfilesSet.has(profileId));
372
429
  return {
373
430
  root: resolveAccountsRoot(),
374
431
  count: rows.length,
375
432
  profiles: rows,
376
433
  validProfiles,
377
434
  invalidProfiles,
435
+ validProfilesByPlatform,
436
+ invalidProfilesByPlatform,
378
437
  };
379
438
  }
380
439
 
@@ -426,6 +485,39 @@ export async function addAccount(input = {}) {
426
485
  status === STATUS_VALID
427
486
  ? null
428
487
  : (status === STATUS_PENDING ? 'waiting_login' : 'missing_account_id');
488
+
489
+ if (accountId) {
490
+ const existing = resolveAccountByAccountId(index, accountId, platform);
491
+ if (existing) {
492
+ const next = {
493
+ ...existing,
494
+ platform,
495
+ status,
496
+ valid: status === STATUS_VALID,
497
+ reason,
498
+ accountId,
499
+ name: accountId,
500
+ alias: alias || existing.alias || null,
501
+ username: normalizeText(input.username) || existing.username || null,
502
+ profileId,
503
+ fingerprintId,
504
+ updatedAt: createdAt,
505
+ aliasSource: alias ? (normalizeAlias(input.alias) ? 'manual' : 'username') : existing.aliasSource || null,
506
+ };
507
+ if (alias) ensureAliasUnique(index.accounts, alias, existing.id);
508
+ const rowIndex = index.accounts.findIndex((item) => item?.id === existing.id);
509
+ if (rowIndex < 0) throw new Error(`account not found: ${existing.id}`);
510
+ index.accounts[rowIndex] = next;
511
+ saveIndex(index);
512
+ persistAccountMeta(next);
513
+ return {
514
+ root: resolveAccountsRoot(),
515
+ account: next,
516
+ deduped: true,
517
+ };
518
+ }
519
+ }
520
+
429
521
  const account = {
430
522
  id,
431
523
  seq,
@@ -553,8 +645,8 @@ export function upsertProfileAccountState(input = {}) {
553
645
  const status = accountId ? STATUS_VALID : (pendingMode ? STATUS_PENDING : STATUS_INVALID);
554
646
 
555
647
  const index = loadIndex();
556
- const existingByProfile = resolveAccountByProfile(index, profileId);
557
- const existingByAccountId = accountId ? resolveAccountByAccountId(index, accountId) : null;
648
+ const existingByProfile = resolveAccountByProfile(index, profileId, platform);
649
+ const existingByAccountId = accountId ? resolveAccountByAccountId(index, accountId, platform) : null;
558
650
  let target = existingByAccountId || existingByProfile || null;
559
651
  const purgeIds = new Set();
560
652
 
@@ -611,7 +703,11 @@ export function upsertProfileAccountState(input = {}) {
611
703
  }
612
704
 
613
705
  const staleIds = index.accounts
614
- .filter((item) => String(item?.profileId || '').trim() === profileId && !hasPersistentAccountId(item))
706
+ .filter((item) => (
707
+ String(item?.profileId || '').trim() === profileId
708
+ && normalizePlatform(item?.platform) === platform
709
+ && !hasPersistentAccountId(item)
710
+ ))
615
711
  .map((item) => String(item?.id || '').trim())
616
712
  .filter(Boolean);
617
713
  if (staleIds.length > 0) {
@@ -677,6 +773,7 @@ export function upsertProfileAccountState(input = {}) {
677
773
  if (accountId) {
678
774
  for (const row of index.accounts) {
679
775
  if (!row || row.id === target.id) continue;
776
+ if (normalizePlatform(row?.platform) !== platform) continue;
680
777
  if (normalizeText(row.accountId) === accountId) {
681
778
  purgeIds.add(row.id);
682
779
  }
@@ -727,6 +824,7 @@ export function markProfileInvalid(profileId, reason = 'login_guard') {
727
824
  const id = ensureSafeName(normalizeText(profileId), 'profileId');
728
825
  return upsertProfileAccountState({
729
826
  profileId: id,
827
+ platform: DEFAULT_PLATFORM,
730
828
  accountId: null,
731
829
  reason,
732
830
  });
@@ -736,6 +834,7 @@ export function markProfilePending(profileId, reason = 'waiting_login') {
736
834
  const id = ensureSafeName(normalizeText(profileId), 'profileId');
737
835
  return upsertProfileAccountState({
738
836
  profileId: id,
837
+ platform: DEFAULT_PLATFORM,
739
838
  accountId: null,
740
839
  status: STATUS_PENDING,
741
840
  reason,
@@ -464,20 +464,11 @@ function validateCommand(task) {
464
464
 
465
465
  function validateXhsCommand(argv) {
466
466
  const keyword = normalizeText(argv.keyword || argv.k);
467
- const profile = normalizeText(argv.profile);
468
- const profiles = normalizeText(argv.profiles);
469
- const profilepool = normalizeText(argv.profilepool);
470
467
  if (!keyword) throw new Error('task command argv missing keyword');
471
- if (!profile && !profiles && !profilepool) {
472
- throw new Error('task command argv missing profile/profiles/profilepool');
473
- }
474
468
  }
475
469
 
476
470
  function validateGenericCommand(argv, platform, commandType = '') {
477
471
  const keyword = normalizeText(argv.keyword || argv.k);
478
- const profile = normalizeText(argv.profile);
479
- const profiles = normalizeText(argv.profiles);
480
- const profilepool = normalizeText(argv.profilepool);
481
472
  let weiboTaskType = '';
482
473
  if (platform === 'weibo') {
483
474
  weiboTaskType = String(argv['task-type'] || argv.taskType || '').trim();
@@ -495,9 +486,6 @@ function validateGenericCommand(argv, platform, commandType = '') {
495
486
  if (!keyword && (platform !== 'weibo' || weiboTaskType === 'search')) {
496
487
  throw new Error('task command argv missing keyword');
497
488
  }
498
- if (!profile && !profiles && !profilepool) {
499
- throw new Error('task command argv missing profile/profiles/profilepool');
500
- }
501
489
  }
502
490
 
503
491
  function normalizeScheduleFields(input = {}, fallback = {}) {
@@ -173,6 +173,46 @@ async function cmdMigrateFingerprints(jsonMode) {
173
173
  output({ ok: true, checked: profiles.length, ensured: created.length }, jsonMode);
174
174
  }
175
175
 
176
+ async function cmdGotoProfile(profileId, argv, jsonMode) {
177
+ const id = String(profileId || '').trim();
178
+ if (!id) throw new Error('profileId is required');
179
+ const url = String(argv.url || argv._?.[2] || '').trim();
180
+ if (!url) throw new Error('url is required');
181
+ await ensureProfile(id);
182
+
183
+ const gotoRet = runCamo(['goto', id, url], { rootDir: ROOT, timeoutMs: 30000 });
184
+ if (gotoRet.ok) {
185
+ output({
186
+ ok: true,
187
+ profileId: id,
188
+ url,
189
+ mode: 'goto',
190
+ result: gotoRet.json || null,
191
+ }, jsonMode);
192
+ return;
193
+ }
194
+
195
+ const idleTimeout = String(argv['idle-timeout'] || process.env.WEBAUTO_LOGIN_IDLE_TIMEOUT || '30m').trim() || '30m';
196
+ const startRet = runCamo(['start', id, '--url', url, '--idle-timeout', idleTimeout], { rootDir: ROOT });
197
+ if (!startRet.ok) {
198
+ output({
199
+ ok: false,
200
+ profileId: id,
201
+ url,
202
+ mode: 'start',
203
+ error: startRet.stderr || startRet.stdout || gotoRet.stderr || gotoRet.stdout || 'goto/start failed',
204
+ }, jsonMode);
205
+ process.exit(1);
206
+ }
207
+ output({
208
+ ok: true,
209
+ profileId: id,
210
+ url,
211
+ mode: 'start',
212
+ session: startRet.json || null,
213
+ }, jsonMode);
214
+ }
215
+
176
216
  async function main() {
177
217
  const argv = minimist(process.argv.slice(2));
178
218
  const cmd = String(argv._[0] || '').trim();
@@ -180,14 +220,16 @@ async function main() {
180
220
  const jsonMode = argv.json === true;
181
221
 
182
222
  if (!cmd || cmd === 'help' || cmd === '--help' || cmd === '-h') {
183
- console.log('Usage: node apps/webauto/entry/profilepool.mjs <list|add|login|login-profile|migrate-fingerprints> ... [--json]');
223
+ console.log('Usage: node apps/webauto/entry/profilepool.mjs <list|add|login|login-profile|goto-profile|migrate-fingerprints> ... [--json]');
224
+ console.log('Default profile prefix: profile (e.g. profile-0, profile-1)');
184
225
  return;
185
226
  }
186
227
 
187
228
  if (cmd === 'list') return cmdList(arg1, jsonMode);
188
- if (cmd === 'add') return cmdAdd(arg1 || 'xiaohongshu-batch', jsonMode);
229
+ if (cmd === 'add') return cmdAdd(arg1 || 'profile', jsonMode);
189
230
  if (cmd === 'login-profile') return cmdLoginProfile(arg1, argv, jsonMode);
190
- if (cmd === 'login') return cmdLogin(arg1 || 'xiaohongshu-batch', argv, jsonMode);
231
+ if (cmd === 'goto-profile') return cmdGotoProfile(arg1, argv, jsonMode);
232
+ if (cmd === 'login') return cmdLogin(arg1 || 'profile', argv, jsonMode);
191
233
  if (cmd === 'migrate-fingerprints') return cmdMigrateFingerprints(jsonMode);
192
234
 
193
235
  throw new Error(`unknown command: ${cmd}`);
@@ -22,6 +22,7 @@ import {
22
22
  resolveSchedulesRoot,
23
23
  updateScheduleTask,
24
24
  } from './lib/schedule-store.mjs';
25
+ import { listAccountProfiles } from './lib/account-store.mjs';
25
26
 
26
27
  let xhsRunnerPromise = null;
27
28
  let weiboRunnerPromise = null;
@@ -70,6 +71,46 @@ function parseJson(text, fallback = {}) {
70
71
  return JSON.parse(raw.replace(/^\uFEFF/, ''));
71
72
  }
72
73
 
74
+ function normalizePlatformByCommandType(commandType) {
75
+ const value = String(commandType || '').trim().toLowerCase();
76
+ if (value.startsWith('weibo')) return 'weibo';
77
+ if (value.startsWith('1688')) return '1688';
78
+ return 'xiaohongshu';
79
+ }
80
+
81
+ function hasProfileArg(argv = {}) {
82
+ return Boolean(
83
+ String(argv?.profile || '').trim()
84
+ || String(argv?.profiles || '').trim()
85
+ || String(argv?.profilepool || '').trim(),
86
+ );
87
+ }
88
+
89
+ function pickAutoProfile(platform) {
90
+ const rows = listAccountProfiles({ platform }).profiles || [];
91
+ const validRows = rows
92
+ .filter((row) => row?.valid === true && String(row?.accountId || '').trim())
93
+ .sort((a, b) => {
94
+ const ta = Date.parse(String(a?.updatedAt || '')) || 0;
95
+ const tb = Date.parse(String(b?.updatedAt || '')) || 0;
96
+ if (tb !== ta) return tb - ta;
97
+ return String(a?.profileId || '').localeCompare(String(b?.profileId || ''));
98
+ });
99
+ return String(validRows[0]?.profileId || '').trim();
100
+ }
101
+
102
+ function ensureProfileArgForTask(commandType, commandArgv = {}) {
103
+ const argv = commandArgv && typeof commandArgv === 'object' ? { ...commandArgv } : {};
104
+ if (hasProfileArg(argv)) return argv;
105
+ const platform = normalizePlatformByCommandType(commandType);
106
+ const profile = pickAutoProfile(platform);
107
+ if (!profile) {
108
+ throw new Error(`missing profile/profiles/profilepool and no valid account for platform=${platform}`);
109
+ }
110
+ argv.profile = profile;
111
+ return argv;
112
+ }
113
+
73
114
  function safeReadJsonFile(filePath) {
74
115
  const raw = fs.readFileSync(filePath, 'utf8');
75
116
  return parseJson(raw, {});
@@ -265,14 +306,15 @@ async function executeTask(task, options = {}) {
265
306
  const quietExecutors = options.quietExecutors === true;
266
307
  try {
267
308
  const commandType = String(task?.commandType || 'xhs-unified').trim();
309
+ const commandArgv = ensureProfileArgForTask(commandType, task?.commandArgv || {});
268
310
  const result = await withConsoleSilenced(quietExecutors, async () => {
269
311
  if (commandType === 'xhs-unified') {
270
312
  const runUnified = await getXhsRunner();
271
- return runUnified(task.commandArgv || {});
313
+ return runUnified(commandArgv);
272
314
  }
273
315
  if (commandType.startsWith('weibo-')) {
274
316
  const runWeiboUnified = await getWeiboRunner();
275
- return runWeiboUnified(task.commandArgv || {});
317
+ return runWeiboUnified(commandArgv);
276
318
  }
277
319
  if (commandType === '1688-search') {
278
320
  throw new Error(`executor_not_implemented: ${commandType}`);
@@ -4,7 +4,7 @@ import { runWorkflowById } from '../../../dist/modules/workflow/src/runner.js';
4
4
  import { pathToFileURL } from 'node:url';
5
5
 
6
6
  const WEIBO_HOME_URL = 'https://www.weibo.com';
7
- const DEFAULT_PROFILE = 'xiaohongshu-batch-1'; // Use shared profile for now
7
+ const DEFAULT_PROFILE = 'profile-0';
8
8
 
9
9
  async function runCommand(argv) {
10
10
  const profile = String(argv.profile || DEFAULT_PROFILE).trim();
@@ -106,7 +106,7 @@ if (isDirectExec) {
106
106
  export async function runWeiboUnified(argv) {
107
107
  const workflowId = String(argv.workflow || 'weibo-search-v1').trim();
108
108
  const keyword = String(argv.keyword || argv.k || '').trim();
109
- const profile = String(argv.profile || 'xiaohongshu-batch-1').trim();
109
+ const profile = String(argv.profile || DEFAULT_PROFILE).trim();
110
110
  const targetCount = Number(argv['max-notes'] || argv.target || argv['max-notes'] || 50);
111
111
  const maxComments = Number(argv['max-comments'] || 0);
112
112
  const env = String(argv.env || 'debug').trim();