@cspeach/cli 1.1.20 → 1.1.22

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.
@@ -90,7 +90,7 @@ const S4_YEAR_TO_ABAP = {
90
90
  '2021': '7.56',
91
91
  '2022': '7.57',
92
92
  '2023': '7.58',
93
- '2025': '7.59',
93
+ '2025': '8.16', // ABAP Platform 2025 = SAP_BASIS 816 (matches system-info.ts abapVersionFor)
94
94
  };
95
95
  function releaseYear(release) {
96
96
  for (const year of Object.keys(S4_YEAR_TO_ABAP)) {
@@ -19,6 +19,12 @@ import { join } from 'node:path';
19
19
  import { auditLog, xmlParser } from '@cspeach/sap-client';
20
20
  import { cspeachRoot } from '../config/paths.js';
21
21
  const CACHE_TTL_MS = 30 * 24 * 60 * 60 * 1000; // 30 days
22
+ /**
23
+ * Bump when the cached shape or its meaning changes, so files written by
24
+ * an older CLI are re-queried instead of served. v2 (2026-09-29): CVERS
25
+ * now includes SAP_APPL; v1 files classified ECC EHP8 as NetWeaver.
26
+ */
27
+ const CACHE_VERSION = 2;
22
28
  const memCache = new Map();
23
29
  /**
24
30
  * Top-level entry point. Returns cached info when available, queries
@@ -68,7 +74,8 @@ export function _resetSystemInfoCache() {
68
74
  async function queryLive(adt, alias) {
69
75
  // S4FND covers some private-cloud variants where S4CORE isn't shipped
70
76
  // — including both broadens detection without extra cost.
71
- const sql = `SELECT COMPONENT, RELEASE FROM CVERS WHERE COMPONENT IN ('SAP_BASIS', 'S4CORE', 'S4FND', 'SAP_ABA')`;
77
+ // SAP_APPL tells ECC apart from plain NetWeaver (see classify()).
78
+ const sql = `SELECT COMPONENT, RELEASE FROM CVERS WHERE COMPONENT IN ('SAP_BASIS', 'S4CORE', 'S4FND', 'SAP_ABA', 'SAP_APPL')`;
72
79
  // Dispatch order is INTENTIONALLY connection-first: AdtClient.sqlQuery
73
80
  // internally uses parseDataPreview which silently returns empty rows on
74
81
  // shapes it doesn't recognise — exactly the failure mode that surfaced
@@ -380,11 +387,17 @@ export async function parseCversTree(xml) {
380
387
  */
381
388
  export function classify(alias, components) {
382
389
  const sapBasis = components['SAP_BASIS'];
390
+ // SAP_APPL is the ERP application component: present on ECC, replaced by
391
+ // S4CORE on S/4, absent on BW / SolMan / PI. It — not the SAP_BASIS
392
+ // number — is what tells ECC apart from a plain NetWeaver system
393
+ // (ECC EHP8 runs on SAP_BASIS 750).
394
+ const sapAppl = components['SAP_APPL'];
383
395
  const s4Core = components['S4CORE'] || components['S4FND'];
384
396
  const platform = s4Core ? 'S/4HANA'
385
- : sapBasis ? (parseInt(sapBasis, 10) >= 750 ? 'NetWeaver' : 'ECC')
386
- : 'unknown';
387
- const release = humanReadableRelease(platform, sapBasis, s4Core);
397
+ : sapAppl ? 'ECC'
398
+ : sapBasis ? 'NetWeaver'
399
+ : 'unknown';
400
+ const release = humanReadableRelease(platform, sapBasis, s4Core, sapAppl);
388
401
  const abapVersion = abapVersionFor(sapBasis);
389
402
  return {
390
403
  alias,
@@ -405,38 +418,42 @@ export function classify(alias, components) {
405
418
  * so we anchor on S4CORE when present and fall back to SAP_BASIS only
406
419
  * for ECC/NW systems.
407
420
  */
408
- function humanReadableRelease(platform, sapBasis, s4Core) {
421
+ function humanReadableRelease(platform, sapBasis, s4Core, sapAppl) {
409
422
  if (platform === 'S/4HANA' && s4Core) {
423
+ // S4CORE release → S/4HANA edition. FPS/SPS levels do not change the
424
+ // S4CORE release, so there are no "FPS" entries here.
410
425
  const map = {
411
- '100': 'S/4HANA 1503',
412
- '101': 'S/4HANA 1511',
413
- '102': 'S/4HANA 1610',
414
- '103': 'S/4HANA 1709',
415
- '104': 'S/4HANA 1809',
416
- '105': 'S/4HANA 1909',
417
- '106': 'S/4HANA 2020',
426
+ '100': 'S/4HANA 1511',
427
+ '101': 'S/4HANA 1610',
428
+ '102': 'S/4HANA 1709',
429
+ '103': 'S/4HANA 1809',
430
+ '104': 'S/4HANA 1909',
431
+ '105': 'S/4HANA 2020',
432
+ '106': 'S/4HANA 2021',
418
433
  '107': 'S/4HANA 2022',
419
434
  '108': 'S/4HANA 2023',
420
- '109': 'S/4HANA 2023 FPS01',
421
- '110': 'S/4HANA 2025',
435
+ '109': 'S/4HANA 2025',
422
436
  };
423
437
  return map[s4Core] ?? `S/4HANA (S4CORE ${s4Core})`;
424
438
  }
425
439
  if (platform === 'NetWeaver' && sapBasis) {
426
- return `NetWeaver / ECC (SAP_BASIS ${sapBasis})`;
440
+ return `NetWeaver (SAP_BASIS ${sapBasis})`;
427
441
  }
428
- if (platform === 'ECC' && sapBasis) {
442
+ if (platform === 'ECC' && sapAppl) {
443
+ // Enhancement pack is read from SAP_APPL: SAP_BASIS is ambiguous
444
+ // (730 covers EHP5 and EHP6; 750 is EHP8 but also plain NW 7.50).
429
445
  const map = {
430
- '700': 'ECC 6.0',
431
- '701': 'ECC 6.0 EHP1',
432
- '702': 'ECC 6.0 EHP2',
433
- '703': 'ECC 6.0 EHP3',
434
- '704': 'ECC 6.0 EHP4',
435
- '730': 'ECC 6.0 EHP5/6',
436
- '731': 'ECC 6.0 EHP6',
437
- '740': 'ECC 6.0 EHP7',
446
+ '600': 'ECC 6.0',
447
+ '602': 'ECC 6.0 EHP2',
448
+ '603': 'ECC 6.0 EHP3',
449
+ '604': 'ECC 6.0 EHP4',
450
+ '605': 'ECC 6.0 EHP5',
451
+ '606': 'ECC 6.0 EHP6',
452
+ '616': 'ECC 6.0 EHP6 (HANA)',
453
+ '617': 'ECC 6.0 EHP7',
454
+ '618': 'ECC 6.0 EHP8',
438
455
  };
439
- return map[sapBasis] ?? `ECC 6.0 (SAP_BASIS ${sapBasis})`;
456
+ return map[sapAppl] ?? `ECC 6.0 (SAP_APPL ${sapAppl})`;
440
457
  }
441
458
  return 'unknown';
442
459
  }
@@ -455,6 +472,9 @@ function abapVersionFor(sapBasis) {
455
472
  // Modern releases — return as N.NN (e.g. 757 → 7.57).
456
473
  if (n >= 740 && n < 800)
457
474
  return `7.${n - 700}`;
475
+ // ABAP Platform 2025 moved to 8.xx (SAP_BASIS 816 → 8.16).
476
+ if (n >= 800 && n < 900)
477
+ return `8.${String(n - 800).padStart(2, '0')}`;
458
478
  return sapBasis;
459
479
  }
460
480
  // ── Disk cache ─────────────────────────────────────────────────────────────
@@ -473,12 +493,15 @@ function readDiskCache(alias) {
473
493
  if (!existsSync(path))
474
494
  return null;
475
495
  const raw = JSON.parse(readFileSync(path, 'utf8'));
496
+ if (raw.cacheVersion !== CACHE_VERSION)
497
+ return null;
476
498
  if (!raw.queriedAt)
477
499
  return null;
478
500
  const age = Date.now() - new Date(raw.queriedAt).getTime();
479
501
  if (age > CACHE_TTL_MS)
480
502
  return null;
481
- return raw;
503
+ const { cacheVersion: _v, ...info } = raw;
504
+ return info;
482
505
  }
483
506
  catch {
484
507
  return null;
@@ -489,7 +512,7 @@ function writeDiskCache(alias, info) {
489
512
  const dir = cacheDir();
490
513
  if (!existsSync(dir))
491
514
  mkdirSync(dir, { recursive: true });
492
- writeFileSync(cacheFile(alias), JSON.stringify(info, null, 2), 'utf8');
515
+ writeFileSync(cacheFile(alias), JSON.stringify({ ...info, cacheVersion: CACHE_VERSION }, null, 2), 'utf8');
493
516
  }
494
517
  catch {
495
518
  // best-effort — we still return the in-memory result