@hs-x/cli 0.4.2 → 0.4.4

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.
@@ -10,6 +10,7 @@ import { cloudflareResourceName, generateHubSpotRuntimeProject, generateProjectA
10
10
  import { HttpClientRequest, Schema, schemas, signDeploymentAttestationGrant } from '@hs-x/types';
11
11
  import { validateProject } from '@hs-x/validator';
12
12
  import { isLegacyProject } from '@hubspot/project-parsing-lib/projects';
13
+ import { translate as translateHubSpotProject } from '@hubspot/project-parsing-lib/translate';
13
14
  import { Effect, Option } from 'effect';
14
15
  import { isLinked } from '../account-store.js';
15
16
  import { exitWith } from '../cli-error.js';
@@ -27,7 +28,7 @@ import { resolveDeployTargets } from '../deploy-targets.js';
27
28
  import { executeCliHttp } from '../effect-http.js';
28
29
  import { DirectHubSpotAccountResolutionError, createDeployHubSpotDeveloperClient, resolveDirectHubSpotContext, } from '../hubspot-developer-client.js';
29
30
  import { HubSpotProjectArchiveSourceError, fingerprintHubSpotArchiveSource, writeLocalHubSpotProjectArchive, } from '../hubspot-project-archive.js';
30
- import { deployHubSpotProjectBuild, waitForHubSpotAutoDeploy, waitForHubSpotBuildStatus, } from '../hubspot-project-lifecycle.js';
31
+ import { deployHubSpotProjectBuild, readHubSpotSubbuildStatuses, waitForHubSpotAutoDeploy, waitForHubSpotBuildStatus, } from '../hubspot-project-lifecycle.js';
31
32
  import { detectLocalProjectMode, readHubSpotProjectDescriptor, validateHubSpotProject, } from '../hubspot-project.js';
32
33
  import { hydrateAncestorEnv } from '../load-env.js';
33
34
  import { LocalProjectPickerCancelled, resolveLocalProject } from '../local-project-picker.js';
@@ -772,6 +773,15 @@ async function executeHubSpotDirectDeployCommand({ argv, root, json, }) {
772
773
  force,
773
774
  sourceFingerprint,
774
775
  createClient: async () => context.createClient(),
776
+ ...(!isLegacyProject(platformVersion)
777
+ ? {
778
+ translateProject: () => context.runWithEnvironment((selectedAccount) => translateHubSpotProject({
779
+ projectSourceDir: join(root, descriptor.srcDir),
780
+ platformVersion,
781
+ accountId: Number(selectedAccount.id),
782
+ })),
783
+ }
784
+ : {}),
775
785
  };
776
786
  }
777
787
  }
@@ -929,7 +939,6 @@ async function executeHubSpotDirectDeployCommand({ argv, root, json, }) {
929
939
  });
930
940
  }
931
941
  try {
932
- const step = machineOutput ? undefined : reporter.step('Uploading to HubSpot');
933
942
  const uploaded = await executeHubSpotOnlyUpload({
934
943
  argv,
935
944
  root,
@@ -937,6 +946,7 @@ async function executeHubSpotDirectDeployCommand({ argv, root, json, }) {
937
946
  persistBinding: false,
938
947
  uploadOnly,
939
948
  prepared: preparedUpload,
949
+ ...(machineOutput ? {} : { progress: reporter }),
940
950
  });
941
951
  const buildState = readStatus(uploaded.buildStatus) ?? 'UNKNOWN';
942
952
  const deployState = uploaded.deployStatus
@@ -944,7 +954,6 @@ async function executeHubSpotDirectDeployCommand({ argv, root, json, }) {
944
954
  : undefined;
945
955
  const deployUnverified = uploaded.autoDeployExpected && deployState === undefined;
946
956
  const ok = buildState === 'SUCCESS' && !deployUnverified && (!deployState || deployState === 'SUCCESS');
947
- step?.[ok ? 'ok' : 'fail'](`build #${uploaded.buildId} ${buildState}${deployState ? ` · deploy ${deployState}` : deployUnverified ? ' · deploy unverified' : ''}`);
948
957
  const result = {
949
958
  schema_version: 1,
950
959
  ok,
@@ -970,7 +979,7 @@ async function executeHubSpotDirectDeployCommand({ argv, root, json, }) {
970
979
  reporter.done(ok ? directDeployDoneMessage(uploaded) : 'HubSpot deploy failed', ok ? 0 : 1);
971
980
  }
972
981
  else {
973
- streamHubSpotUploadOutcome(reporter, uploaded);
982
+ streamHubSpotUploadOutcome(reporter, uploaded, { componentMode: 'failures' });
974
983
  if (deployUnverified) {
975
984
  reporter.error('HSX_E_DEPLOY_HUBSPOT', 'HubSpot auto-deploy could not be verified before the timeout.', { hint: 'Check the project deploy status in HubSpot, then retry if it did not ship.' });
976
985
  }
@@ -1455,31 +1464,22 @@ export async function deployCommand({ argv, root, json, }) {
1455
1464
  // vs deploy-then-upload). A FAILED build resolves the step as a failure so
1456
1465
  // the failing phase is visible in the transcript, not just the exit code.
1457
1466
  const runHubSpotUploadPhase = async () => {
1458
- const uploadStartedAt = Date.now();
1459
- const uploadStep = progress?.step('Uploading to HubSpot');
1460
- let result;
1461
- try {
1462
- result = await executeHubSpotOnlyUpload({
1463
- argv,
1464
- root,
1465
- local: localHubspotUpload,
1466
- uploadOnly: hubspotUploadOnly,
1467
- });
1468
- }
1469
- catch (error) {
1470
- uploadStep?.fail(firstErrorLine(error));
1471
- throw error;
1472
- }
1473
- const buildState = readStatus(result.buildStatus) ?? 'UNKNOWN';
1474
- const detail = `build #${result.buildId} ${buildState} (${elapsedSince(uploadStartedAt)})`;
1475
- if (buildState === 'SUCCESS') {
1476
- uploadStep?.ok(detail);
1477
- }
1478
- else {
1479
- uploadStep?.fail(detail);
1480
- }
1467
+ const result = await executeHubSpotOnlyUpload({
1468
+ argv,
1469
+ root,
1470
+ local: localHubspotUpload,
1471
+ uploadOnly: hubspotUploadOnly,
1472
+ ...(progress
1473
+ ? {
1474
+ progress: {
1475
+ step: (label) => progress.step(label),
1476
+ rows: (rows) => progress.reporter.rows(rows),
1477
+ },
1478
+ }
1479
+ : {}),
1480
+ });
1481
1481
  if (echo)
1482
- streamHubSpotUploadOutcome(echo, result);
1482
+ streamHubSpotUploadOutcome(echo, result, { componentMode: 'failures' });
1483
1483
  return result;
1484
1484
  };
1485
1485
  if (uploadFirstOrigin && controlPlanePlan) {
@@ -1635,9 +1635,20 @@ export async function deployCommand({ argv, root, json, }) {
1635
1635
  const configDrift = controlPlaneRecord.configDrift;
1636
1636
  echo.warn('HSX_W_DEPLOY_CONFIG_DRIFT', `Worker binding/config changed since the previously recorded deploy (${configDrift.previousDeployId}): fingerprint ${configDrift.previousFingerprint} -> ${configDrift.currentFingerprint}. This is expected if you intentionally changed bindings; investigate if you did not.`);
1637
1637
  }
1638
+ // A combined plan cannot know the Worker URL that a real Cloudflare deploy
1639
+ // will mint. Keep plan mode useful and non-provider-mutating by deferring
1640
+ // runtime-bound HubSpot metadata until the actual deploy. Callers may still
1641
+ // pass --runtime-origin when they intentionally want to preview that bundle.
1642
+ const hubspotBundleDeferred = planOnly &&
1643
+ hubspotUploadRequested &&
1644
+ needsHubSpotBundle &&
1645
+ !resolveFlag(argv, '--runtime-origin');
1646
+ if (hubspotBundleDeferred && echo) {
1647
+ echo.info('HubSpot runtime bundle: deferred until deploy resolves the Worker URL.');
1648
+ }
1638
1649
  // Default order (skipped when we already uploaded first above): generate the
1639
1650
  // HubSpot bundle from the deployed Worker URL, then upload.
1640
- if (!uploadFirstOrigin && hubspotUploadRequested && validation.ok) {
1651
+ if (!uploadFirstOrigin && hubspotUploadRequested && validation.ok && !hubspotBundleDeferred) {
1641
1652
  const bundleStep = needsHubSpotBundle ? progress?.step('Generating HubSpot bundle') : undefined;
1642
1653
  try {
1643
1654
  await ensureHubSpotRuntimeProjectArtifacts({
@@ -1720,6 +1731,7 @@ export async function deployCommand({ argv, root, json, }) {
1720
1731
  },
1721
1732
  hubspot: {
1722
1733
  uploadRequested: hubspotUploadRequested,
1734
+ bundleDeferred: hubspotBundleDeferred,
1723
1735
  },
1724
1736
  },
1725
1737
  root,
@@ -2069,12 +2081,13 @@ function buildDeploySummaryRows(input) {
2069
2081
  * diagnosable from the terminal, not the HubSpot UI (run-008 build #2 failed
2070
2082
  * silently here).
2071
2083
  */
2072
- function streamHubSpotUploadOutcome(reporter, upload) {
2084
+ function streamHubSpotUploadOutcome(reporter, upload, options = {}) {
2085
+ const componentMode = options.componentMode ?? 'all';
2073
2086
  for (const subbuild of readSubbuildStatuses(upload.buildStatus)) {
2074
- if (subbuild.status === 'SUCCESS') {
2087
+ if (subbuild.status === 'SUCCESS' && componentMode === 'all') {
2075
2088
  reporter.info(` [ok] ${subbuild.name}${subbuild.type ? ` (${subbuild.type})` : ''}`);
2076
2089
  }
2077
- else {
2090
+ else if (subbuild.status !== 'SUCCESS' && componentMode !== 'none') {
2078
2091
  reporter.error('HSX_E_DEPLOY_HUBSPOT_BUILD_COMPONENT', `${subbuild.name}${subbuild.type ? ` (${subbuild.type})` : ''}: ${subbuild.errorMessage ?? subbuild.status}`);
2079
2092
  }
2080
2093
  }
@@ -4276,15 +4289,35 @@ function runCloudflareCommand(command, options) {
4276
4289
  },
4277
4290
  })));
4278
4291
  }
4279
- async function executeHubSpotOnlyUpload({ argv, root, local, persistBinding = true, uploadOnly, prepared, }) {
4292
+ async function executeHubSpotOnlyUpload({ argv, root, local, persistBinding = true, uploadOnly, prepared, progress, }) {
4280
4293
  // The HubSpot project name must match the name baked into the generated
4281
4294
  // bundle (hsproject.json + app-hsmeta uid), not the on-disk directory name.
4282
4295
  // Using basename(root) breaks when the project lives in a generic dir (e.g.
4283
4296
  // `app/`): the upload targets a project named "app" while the bundle declares
4284
4297
  // the real project id, and HubSpot rejects the mismatch with an opaque 400.
4285
4298
  const projectName = prepared?.projectName ?? (await resolveHubSpotProjectName(argv, root));
4286
- const intermediateRepresentation = prepared?.intermediateRepresentation ??
4299
+ let intermediateRepresentation = prepared?.intermediateRepresentation ??
4287
4300
  (await readHubSpotProjectIntermediateRepresentation(root));
4301
+ if (prepared?.translateProject) {
4302
+ const translateStartedAt = Date.now();
4303
+ const translateStep = progress?.step('Translating HubSpot components');
4304
+ try {
4305
+ const translated = await prepared.translateProject();
4306
+ if (translated.skippedHsMetaFiles.length > 0 && !prepared.force) {
4307
+ throw new Error(`HubSpot skipped ${translated.skippedHsMetaFiles.length} component metadata file(s): ${translated.skippedHsMetaFiles.join(', ')}. Review them or re-run with --force.`);
4308
+ }
4309
+ intermediateRepresentation = translated.intermediateRepresentation;
4310
+ const nodes = isRecord(intermediateRepresentation)
4311
+ ? intermediateRepresentation.intermediateNodesIndexedByUid
4312
+ : undefined;
4313
+ const componentCount = isRecord(nodes) ? Object.keys(nodes).length : 0;
4314
+ translateStep?.ok(`${componentCount} components · ${elapsedSince(translateStartedAt)}`);
4315
+ }
4316
+ catch (error) {
4317
+ translateStep?.fail(`${firstErrorLine(error)} · ${elapsedSince(translateStartedAt)}`);
4318
+ throw error;
4319
+ }
4320
+ }
4288
4321
  const appUid = prepared?.appUid ??
4289
4322
  readAppUidFromIntermediateRepresentation(intermediateRepresentation) ??
4290
4323
  (await readAppUidFromProjectSource(root));
@@ -4294,10 +4327,20 @@ async function executeHubSpotOnlyUpload({ argv, root, local, persistBinding = tr
4294
4327
  const developerAccountId = prepared?.developerAccountId ??
4295
4328
  resolveFlag(argv, '--developer-account-id') ??
4296
4329
  process.env.HSX_HUBSPOT_DEVELOPER_ACCOUNT_ID;
4297
- const archivePath = await writeLocalHubSpotProjectArchive(root, projectName, {
4298
- sourceOnly: intermediateRepresentation !== undefined,
4299
- ...(prepared ? { expectedSourceFingerprint: prepared.sourceFingerprint } : {}),
4300
- });
4330
+ const packageStartedAt = Date.now();
4331
+ const packageStep = progress?.step('Packaging HubSpot project');
4332
+ let archivePath;
4333
+ try {
4334
+ archivePath = await writeLocalHubSpotProjectArchive(root, projectName, {
4335
+ sourceOnly: intermediateRepresentation !== undefined,
4336
+ ...(prepared ? { expectedSourceFingerprint: prepared.sourceFingerprint } : {}),
4337
+ });
4338
+ packageStep?.ok(elapsedSince(packageStartedAt));
4339
+ }
4340
+ catch (error) {
4341
+ packageStep?.fail(`${firstErrorLine(error)} · ${elapsedSince(packageStartedAt)}`);
4342
+ throw error;
4343
+ }
4301
4344
  const client = prepared
4302
4345
  ? await prepared.createClient()
4303
4346
  : await createDeployHubSpotDeveloperClient({
@@ -4306,29 +4349,52 @@ async function executeHubSpotOnlyUpload({ argv, root, local, persistBinding = tr
4306
4349
  allowedOperations: ['hubspot-project-upload'],
4307
4350
  cwd: root,
4308
4351
  });
4309
- await client.projects.ensureProject({ projectName });
4310
4352
  const hsproject = await readHubSpotProjectConfig(root);
4311
4353
  const configuredPlatformVersion = typeof hsproject.platformVersion === 'string' ? hsproject.platformVersion : undefined;
4312
4354
  const platformVersion = prepared?.platformVersion ??
4313
4355
  resolveFlag(argv, '--platform-version') ??
4314
4356
  configuredPlatformVersion ??
4315
4357
  '2026.03';
4316
- const upload = await client.projects.upload({
4317
- projectName,
4318
- archivePath,
4319
- message: prepared?.buildMessage ?? resolveFlag(argv, '--message') ?? 'HS-X HubSpot-only deploy',
4320
- platformVersion,
4321
- intermediateRepresentation,
4322
- skipAutoDeploy: uploadOnly,
4323
- });
4324
4358
  const timeoutMs = prepared?.buildTimeoutMs ?? Number(resolveFlag(argv, '--hubspot-build-timeout-ms') ?? '60000');
4325
- const buildStatus = await waitForHubSpotBuildStatus({
4326
- client,
4327
- projectName,
4328
- buildId: upload.buildId,
4329
- timeoutMs,
4330
- intervalMs: 1000,
4331
- });
4359
+ const uploadStartedAt = Date.now();
4360
+ const uploadStep = progress?.step('Uploading project');
4361
+ let upload;
4362
+ try {
4363
+ await client.projects.ensureProject({ projectName });
4364
+ upload = await client.projects.upload({
4365
+ projectName,
4366
+ archivePath,
4367
+ message: prepared?.buildMessage ?? resolveFlag(argv, '--message') ?? 'HS-X HubSpot-only deploy',
4368
+ platformVersion,
4369
+ intermediateRepresentation,
4370
+ skipAutoDeploy: uploadOnly,
4371
+ });
4372
+ uploadStep?.ok(`build #${upload.buildId} · ${elapsedSince(uploadStartedAt)}`);
4373
+ }
4374
+ catch (error) {
4375
+ uploadStep?.fail(`${firstErrorLine(error)} · ${elapsedSince(uploadStartedAt)}`);
4376
+ throw error;
4377
+ }
4378
+ const buildStartedAt = Date.now();
4379
+ const buildStep = progress?.step(`Building project (build #${upload.buildId})`);
4380
+ const reportedComponents = new Map();
4381
+ let buildStatus;
4382
+ try {
4383
+ buildStatus = await waitForHubSpotBuildStatus({
4384
+ client,
4385
+ projectName,
4386
+ buildId: upload.buildId,
4387
+ timeoutMs,
4388
+ intervalMs: 1000,
4389
+ onStatus: (status) => streamHubSpotComponentProgress(progress, status, reportedComponents, 'build'),
4390
+ });
4391
+ }
4392
+ catch (error) {
4393
+ buildStep?.fail(`${firstErrorLine(error)} · ${elapsedSince(buildStartedAt)}`);
4394
+ throw error;
4395
+ }
4396
+ const buildState = readStatus(buildStatus) ?? 'UNKNOWN';
4397
+ buildStep?.[buildState === 'SUCCESS' ? 'ok' : 'fail'](`${buildState} · ${elapsedSince(buildStartedAt)}`);
4332
4398
  const autoDeployEnabled = readBooleanProperty(buildStatus, 'isAutoDeployEnabled');
4333
4399
  if (uploadOnly || readStatus(buildStatus) !== 'SUCCESS' || autoDeployEnabled) {
4334
4400
  const appId = readStatus(buildStatus) === 'SUCCESS'
@@ -4346,15 +4412,32 @@ async function executeHubSpotOnlyUpload({ argv, root, local, persistBinding = tr
4346
4412
  // Normal uploads can auto-deploy; uploadOnly sends skipAutoDeploy and is
4347
4413
  // therefore a strict build-only result.
4348
4414
  const autoDeployExpected = !uploadOnly && autoDeployEnabled === true && readStatus(buildStatus) === 'SUCCESS';
4349
- const autoDeployStatus = autoDeployExpected
4350
- ? (await waitForHubSpotAutoDeploy({
4351
- client,
4352
- projectName,
4353
- buildId: upload.buildId,
4354
- buildStatus,
4355
- timeoutMs,
4356
- })).status
4357
- : undefined;
4415
+ let autoDeployStatus;
4416
+ if (autoDeployExpected) {
4417
+ const deployStartedAt = Date.now();
4418
+ const deployStep = progress?.step(`Auto-deploying project (build #${upload.buildId})`);
4419
+ const reportedDeployComponents = new Map();
4420
+ try {
4421
+ const autoDeploy = await waitForHubSpotAutoDeploy({
4422
+ client,
4423
+ projectName,
4424
+ buildId: upload.buildId,
4425
+ buildStatus,
4426
+ timeoutMs,
4427
+ onStatus: (status) => streamHubSpotComponentProgress(progress, status, reportedDeployComponents, 'deploy'),
4428
+ });
4429
+ autoDeployStatus = autoDeploy.status;
4430
+ const state = readStatus(autoDeployStatus) ?? 'UNKNOWN';
4431
+ if (state === 'SUCCESS') {
4432
+ streamHubSpotComponentProgress(progress, buildStatus, reportedDeployComponents, 'deploy');
4433
+ }
4434
+ deployStep?.[state === 'SUCCESS' ? 'ok' : 'fail'](`deploy #${autoDeploy.deployId} ${state} · ${elapsedSince(deployStartedAt)}`);
4435
+ }
4436
+ catch (error) {
4437
+ deployStep?.fail(`${firstErrorLine(error)} · ${elapsedSince(deployStartedAt)}`);
4438
+ throw error;
4439
+ }
4440
+ }
4358
4441
  return {
4359
4442
  projectName,
4360
4443
  archivePath,
@@ -4370,15 +4453,31 @@ async function executeHubSpotOnlyUpload({ argv, root, local, persistBinding = tr
4370
4453
  local,
4371
4454
  };
4372
4455
  }
4373
- const deployed = await deployHubSpotProjectBuild({
4374
- client,
4375
- projectName,
4376
- buildId: upload.buildId,
4377
- force: prepared?.force ?? argv.includes('--force'),
4378
- legacy: isLegacyProject(platformVersion),
4379
- timeoutMs,
4380
- executeDeploy: (request) => withTransientDeployRetry(() => client.projects.deployBuild(request)),
4381
- });
4456
+ const deployStartedAt = Date.now();
4457
+ const deployStep = progress?.step(`Deploying project (build #${upload.buildId})`);
4458
+ const reportedDeployComponents = new Map();
4459
+ let deployed;
4460
+ try {
4461
+ deployed = await deployHubSpotProjectBuild({
4462
+ client,
4463
+ projectName,
4464
+ buildId: upload.buildId,
4465
+ force: prepared?.force ?? argv.includes('--force'),
4466
+ legacy: isLegacyProject(platformVersion),
4467
+ timeoutMs,
4468
+ onStatus: (status) => streamHubSpotComponentProgress(progress, status, reportedDeployComponents, 'deploy'),
4469
+ executeDeploy: (request) => withTransientDeployRetry(() => client.projects.deployBuild(request)),
4470
+ });
4471
+ const state = readStatus(deployed.status) ?? 'UNKNOWN';
4472
+ if (state === 'SUCCESS') {
4473
+ streamHubSpotComponentProgress(progress, buildStatus, reportedDeployComponents, 'deploy');
4474
+ }
4475
+ deployStep?.[state === 'SUCCESS' ? 'ok' : 'fail'](`deploy #${deployed.deployId} ${state} · ${elapsedSince(deployStartedAt)}`);
4476
+ }
4477
+ catch (error) {
4478
+ deployStep?.fail(`${firstErrorLine(error)} · ${elapsedSince(deployStartedAt)}`);
4479
+ throw error;
4480
+ }
4382
4481
  const deployId = deployed.deployId;
4383
4482
  const deployStatus = deployed.status;
4384
4483
  const appId = await optionalHubSpotAppId(client, projectName, appUid);
@@ -4592,18 +4691,30 @@ async function resolveHubSpotAppAuthUrl(argv, root) {
4592
4691
  function readStatus(value) {
4593
4692
  return isRecord(value) && typeof value.status === 'string' ? value.status : undefined;
4594
4693
  }
4595
- /** Tolerant parse of local-dev-lib's build subbuildStatuses for reporting. */
4596
- function readSubbuildStatuses(value) {
4597
- if (!isRecord(value) || !Array.isArray(value.subbuildStatuses))
4598
- return [];
4599
- return value.subbuildStatuses.filter(isRecord).map((subbuild) => ({
4600
- name: String(subbuild.buildName ?? subbuild.componentName ?? 'component'),
4601
- ...(typeof subbuild.buildType === 'string' ? { type: subbuild.buildType } : {}),
4602
- status: String(subbuild.status ?? 'UNKNOWN'),
4603
- ...(typeof subbuild.errorMessage === 'string' && subbuild.errorMessage.length > 0
4604
- ? { errorMessage: subbuild.errorMessage }
4605
- : {}),
4606
- }));
4694
+ const readSubbuildStatuses = readHubSpotSubbuildStatuses;
4695
+ const HUBSPOT_COMPONENT_TERMINAL_STATES = new Set(['SUCCESS', 'FAILURE', 'ERROR', 'CANCELLED']);
4696
+ /** Emit each HubSpot component once, when it reaches a terminal build state. */
4697
+ function streamHubSpotComponentProgress(progress, buildStatus, reported, phase) {
4698
+ if (!progress)
4699
+ return;
4700
+ const rows = [];
4701
+ for (const component of readSubbuildStatuses(buildStatus)) {
4702
+ const state = component.status.toUpperCase();
4703
+ if (!HUBSPOT_COMPONENT_TERMINAL_STATES.has(state))
4704
+ continue;
4705
+ const key = `${component.name}\0${component.type ?? ''}`;
4706
+ if (reported.get(key) === state)
4707
+ continue;
4708
+ reported.set(key, state);
4709
+ rows.push({
4710
+ key: component.name,
4711
+ value: component.type ?? 'COMPONENT',
4712
+ status: state === 'SUCCESS' ? 'ok' : 'fail',
4713
+ detail: `${phase} ${state}`,
4714
+ ...(component.errorMessage ? { hint: component.errorMessage } : {}),
4715
+ });
4716
+ }
4717
+ progress.rows(rows);
4607
4718
  }
4608
4719
  function hasFlag(argv, flag) {
4609
4720
  return argv.includes(flag);