@cspeach/cli 1.1.14 → 1.1.15

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.
@@ -3,6 +3,7 @@
3
3
  import { extractText, getDocumentProxy } from 'unpdf';
4
4
  import mammoth from 'mammoth';
5
5
  import TurndownService from 'turndown';
6
+ import { extractXlsx } from './extract-xlsx.js';
6
7
  /** Thrown when a PDF exceeds the caller's page ceiling — checked before full extraction. */
7
8
  export class DocumentTooLargeError extends Error {
8
9
  pages;
@@ -20,6 +21,11 @@ export async function extractDocument(buffer, ext, opts) {
20
21
  return extractPdf(buffer, opts?.maxPages);
21
22
  if (ext === '.docx')
22
23
  return extractDocx(buffer);
24
+ // 2026-09-23 — specs and object lists arrive as spreadsheets (Glenn).
25
+ if (ext === '.xlsx') {
26
+ const x = extractXlsx(buffer);
27
+ return { text: x.text, sheets: x.sheets, warnings: x.warnings };
28
+ }
23
29
  throw new Error(`extractDocument: unsupported extension "${ext}"`);
24
30
  }
25
31
  async function extractPdf(buffer, maxPages) {
@@ -0,0 +1,139 @@
1
+ /**
2
+ * .xlsx text extraction (2026-09-23 — Glenn: "it cannot read the excel file").
3
+ *
4
+ * Specs and object lists arrive as spreadsheets, so read_document reads them
5
+ * like any other document. An .xlsx IS a zip of XML parts, and adm-zip already
6
+ * ships inside the tree (SAP's own @sap-ux packages pull it), so nothing new
7
+ * enters the product to do this.
8
+ *
9
+ * Output shape: one "## Sheet: <name>" heading per sheet, then one line per
10
+ * row, cells separated by tabs, empty trailing cells dropped and empty leading
11
+ * ones kept so columns still line up. A model reads that as a table; a human
12
+ * reading the transcript can too.
13
+ *
14
+ * Deliberately NOT done: formulas (the cached VALUE is used, which is what the
15
+ * sheet shows), formatting, merged-cell geometry, charts, images. Dates come
16
+ * out as the serial number Excel stores unless the cell is text — the caller is
17
+ * told, rather than us guessing a locale.
18
+ */
19
+ import AdmZip from 'adm-zip';
20
+ /** Hard ceilings so one huge workbook cannot fill a turn. */
21
+ export const MAX_SHEET_ROWS = 2000;
22
+ export const MAX_CELLS = 100_000;
23
+ const decode = (s) => s.replace(/&lt;/g, '<').replace(/&gt;/g, '>').replace(/&quot;/g, '"')
24
+ .replace(/&apos;/g, "'").replace(/&#x([0-9a-fA-F]+);/g, (_, h) => String.fromCodePoint(parseInt(h, 16)))
25
+ .replace(/&#(\d+);/g, (_, d) => String.fromCodePoint(Number(d)))
26
+ .replace(/&amp;/g, '&'); // last: an encoded "&amp;lt;" must not become "<"
27
+ /** All <t> text inside one shared-string <si>, joined (rich text is split across runs). */
28
+ function siText(si) {
29
+ const parts = [...si.matchAll(/<t[^>]*>([\s\S]*?)<\/t>/g)].map((m) => decode(m[1]));
30
+ return parts.join('');
31
+ }
32
+ export function sharedStrings(xml) {
33
+ if (!xml)
34
+ return [];
35
+ return [...xml.matchAll(/<si>([\s\S]*?)<\/si>/g)].map((m) => siText(m[1]));
36
+ }
37
+ /** Column letters → 0-based index: A→0, B→1, AA→26. */
38
+ export function colIndex(ref) {
39
+ const letters = /^([A-Z]+)/.exec(ref.toUpperCase())?.[1] ?? 'A';
40
+ let n = 0;
41
+ for (const ch of letters)
42
+ n = n * 26 + (ch.charCodeAt(0) - 64);
43
+ return n - 1;
44
+ }
45
+ /** One worksheet's cells as a rectangular grid of strings. */
46
+ export function sheetRows(xml, strings, budget) {
47
+ const rows = [];
48
+ for (const rowMatch of xml.matchAll(/<row[^>]*>([\s\S]*?)<\/row>/g)) {
49
+ if (rows.length >= MAX_SHEET_ROWS || budget.cells <= 0)
50
+ break;
51
+ const cells = [];
52
+ for (const cell of rowMatch[1].matchAll(/<c\b([^>]*)>([\s\S]*?)<\/c>|<c\b([^>]*)\/>/g)) {
53
+ const attrs = cell[1] ?? cell[3] ?? '';
54
+ const body = cell[2] ?? '';
55
+ const at = colIndex(/r="([A-Z]+\d+)"/.exec(attrs)?.[1] ?? 'A1');
56
+ const type = /t="([^"]+)"/.exec(attrs)?.[1];
57
+ let value = '';
58
+ if (type === 's') {
59
+ const idx = Number(/<v>([\s\S]*?)<\/v>/.exec(body)?.[1] ?? '-1');
60
+ value = strings[idx] ?? '';
61
+ }
62
+ else if (type === 'inlineStr') {
63
+ value = siText(body);
64
+ }
65
+ else {
66
+ // n (number), str (formula result), b, d, e — the cached value as shown.
67
+ value = decode(/<v>([\s\S]*?)<\/v>/.exec(body)?.[1] ?? '');
68
+ }
69
+ while (cells.length < at)
70
+ cells.push('');
71
+ cells[at] = value.replace(/[\t\r\n]+/g, ' ').trim();
72
+ budget.cells -= 1;
73
+ if (budget.cells <= 0)
74
+ break;
75
+ }
76
+ while (cells.length > 0 && cells[cells.length - 1] === '')
77
+ cells.pop();
78
+ rows.push(cells);
79
+ }
80
+ while (rows.length > 0 && rows[rows.length - 1].length === 0)
81
+ rows.pop();
82
+ return rows;
83
+ }
84
+ /** Sheet name → part path, in the workbook's own order. */
85
+ export function sheetOrder(workbookXml, relsXml) {
86
+ if (!workbookXml)
87
+ return [];
88
+ const rels = new Map();
89
+ for (const m of (relsXml ?? '').matchAll(/<Relationship\b([^>]*)\/>/g)) {
90
+ const id = /Id="([^"]+)"/.exec(m[1])?.[1];
91
+ const target = /Target="([^"]+)"/.exec(m[1])?.[1];
92
+ if (id && target)
93
+ rels.set(id, target.replace(/^\/?xl\//, '').replace(/^\//, ''));
94
+ }
95
+ const out = [];
96
+ for (const m of workbookXml.matchAll(/<sheet\b([^>]*)\/>/g)) {
97
+ const attrs = m[1];
98
+ const name = decode(/name="([^"]*)"/.exec(attrs)?.[1] ?? `Sheet${out.length + 1}`);
99
+ const rid = /r:id="([^"]+)"/.exec(attrs)?.[1] ?? '';
100
+ out.push({ name, path: `xl/${rels.get(rid) ?? `worksheets/sheet${out.length + 1}.xml`}` });
101
+ }
102
+ return out;
103
+ }
104
+ export function extractXlsx(buffer) {
105
+ let zip;
106
+ try {
107
+ zip = new AdmZip(buffer);
108
+ }
109
+ catch {
110
+ return { text: '', sheets: 0, warnings: ['not a readable .xlsx (the file may be .xls, password-protected or damaged)'] };
111
+ }
112
+ const read = (p) => {
113
+ const entry = zip.getEntry(p);
114
+ return entry ? entry.getData().toString('utf-8') : null;
115
+ };
116
+ const strings = sharedStrings(read('xl/sharedStrings.xml'));
117
+ const sheets = sheetOrder(read('xl/workbook.xml'), read('xl/_rels/workbook.xml.rels'));
118
+ if (sheets.length === 0) {
119
+ return { text: '', sheets: 0, warnings: ['no sheets found — the file may be .xls (old format); save it as .xlsx'] };
120
+ }
121
+ const budget = { cells: MAX_CELLS };
122
+ const warnings = [];
123
+ const blocks = [];
124
+ for (const s of sheets) {
125
+ const xml = read(s.path);
126
+ if (xml === null)
127
+ continue;
128
+ const rows = sheetRows(xml, strings, budget);
129
+ if (rows.length >= MAX_SHEET_ROWS)
130
+ warnings.push(`sheet "${s.name}" was cut at ${MAX_SHEET_ROWS} rows`);
131
+ blocks.push(`## Sheet: ${s.name}\n\n${rows.map((r) => r.join('\t')).join('\n')}`.trimEnd());
132
+ }
133
+ if (budget.cells <= 0)
134
+ warnings.push('the workbook was cut short — it holds more cells than one read returns');
135
+ const text = blocks.join('\n\n').trim();
136
+ if (text.length === 0)
137
+ warnings.push('no cell text found — the workbook may be empty or hold only charts/images');
138
+ return { text, sheets: sheets.length, warnings };
139
+ }
@@ -21,6 +21,25 @@ import { promises as fs } from 'node:fs';
21
21
  import * as path from 'node:path';
22
22
  import { registerTool } from '../index.js';
23
23
  import { resolveSafePath, assertRealPathContained, PathOutsideRootError, DENYLIST_DESCRIPTION, isDenylistedPath, blockedExecutableExtension, } from '../_filesystem-shared.js';
24
+ /**
25
+ * 2026-09-23 (Glenn's first run) — a standalone run writes GUIDE.md and said
26
+ * nothing about it. He opened it in a text editor, then tried to drop it into
27
+ * an unrelated page on the site and got an error: "what does this file do …
28
+ * where would this enter back into the CSPEACH process?". Say it once, when
29
+ * the file appears, in the human stream.
30
+ */
31
+ let guideNoteShown = false;
32
+ export function __resetGuideNoteForTests() { guideNoteShown = false; }
33
+ function noteGuideWritten(relPath, ctx) {
34
+ if (guideNoteShown)
35
+ return;
36
+ if (path.basename(relPath).toLowerCase() !== 'guide.md')
37
+ return;
38
+ guideNoteShown = true;
39
+ ctx.chunkEmitter?.emit('info', `${relPath} is your step-by-step list for applying these objects in SAP — `
40
+ + 'open it at https://cspeach.dev/guide, or in any text editor. '
41
+ + 'If a step fails, come back here and say "step N failed" with the error.');
42
+ }
24
43
  export async function fileWriteHandler(args, ctx) {
25
44
  // 1. Path containment — sync phase (pure string math, catches .. traversals)
26
45
  let abs;
@@ -110,6 +129,7 @@ export async function fileWriteHandler(args, ctx) {
110
129
  }
111
130
  const relPath = path.relative(path.resolve(ctx.cwd), target).replace(/\\/g, '/');
112
131
  const bytes = Buffer.byteLength(args.content, 'utf-8');
132
+ noteGuideWritten(relPath, ctx);
113
133
  return { content: `wrote ${relPath} (${bytes} bytes)` };
114
134
  }
115
135
  registerTool({
@@ -12,7 +12,7 @@ import { extractDocument, DocumentTooLargeError } from './extract-document.js';
12
12
  const MAX_LINES = 2000;
13
13
  const MAX_BYTES = 50 * 1024 * 1024; // 50 MB
14
14
  const MAX_PAGES = 150;
15
- const SUPPORTED = new Set(['.pdf', '.docx']);
15
+ const SUPPORTED = new Set(['.pdf', '.docx', '.xlsx']);
16
16
  export async function readDocumentHandler(args, ctx) {
17
17
  if (args.offset !== undefined && args.offset < 0) {
18
18
  return { content: `error: offset must be non-negative (got ${args.offset})`, is_error: true };
@@ -32,7 +32,10 @@ export async function readDocumentHandler(args, ctx) {
32
32
  const ext = path.extname(abs).toLowerCase();
33
33
  if (!SUPPORTED.has(ext)) {
34
34
  return {
35
- content: `error: read_document supports .pdf and .docx; convert "${args.path}" first (.doc/.pptx/scanned images are not supported)`,
35
+ content: ext === '.xls' || ext === '.csv'
36
+ ? `error: read_document reads .xlsx, not ${ext}; save "${args.path}" as .xlsx`
37
+ + (ext === '.csv' ? ' — or read a .csv with file_read, it is plain text' : '')
38
+ : `error: read_document supports .pdf, .docx and .xlsx; convert "${args.path}" first (.doc/.pptx/scanned images are not supported)`,
36
39
  is_error: true,
37
40
  };
38
41
  }
@@ -74,8 +77,14 @@ export async function readDocumentHandler(args, ctx) {
74
77
  }
75
78
  return { content: `error: could not read "${args.path}": ${err instanceof Error ? err.message : String(err)}`, is_error: true };
76
79
  }
77
- // 'document' is the intentional label for .docx: result.pages is PDF-only and undefined for Word files.
78
- const header = `${args.path} — ${result.pages !== undefined ? `${result.pages} pages` : 'document'}`;
80
+ // 'document' is the intentional label for .docx: result.pages is PDF-only,
81
+ // result.sheets is .xlsx-only, and a Word file has neither.
82
+ const scale = result.pages !== undefined
83
+ ? `${result.pages} pages`
84
+ : result.sheets !== undefined
85
+ ? `${result.sheets} sheet${result.sheets === 1 ? '' : 's'}`
86
+ : 'document';
87
+ const header = `${args.path} — ${scale}`;
79
88
  const warnLines = result.warnings.map((w) => `[warning] ${w}`);
80
89
  const allLines = result.text.replace(/\n$/, '').split('\n');
81
90
  const offset = Math.max(0, args.offset ?? 0);
@@ -90,14 +99,14 @@ export async function readDocumentHandler(args, ctx) {
90
99
  }
91
100
  registerTool({
92
101
  name: 'read_document',
93
- description: 'Read a .pdf or .docx and return its extracted text with page/heading markers and line numbers. Optional offset + limit for windowed reads of large documents.',
102
+ description: 'Read a .pdf, .docx or .xlsx and return its text with page/sheet/heading markers and line numbers. A spreadsheet comes back one sheet at a time, rows as tab-separated cells (cached values, not formulas). Optional offset + limit for windowed reads of large documents.',
94
103
  isMutating: false,
95
104
  category: 'filesystem',
96
105
  flagGated: true,
97
106
  input_schema: {
98
107
  type: 'object',
99
108
  properties: {
100
- path: { type: 'string', description: 'Path to a .pdf or .docx, relative to the project root or absolute inside it.' },
109
+ path: { type: 'string', description: 'Path to a .pdf, .docx or .xlsx, relative to the project root or absolute inside it.' },
101
110
  offset: { type: 'number', description: 'Optional 0-based line offset into the extracted text.' },
102
111
  limit: { type: 'number', description: 'Optional max lines to return (cap 2000).' },
103
112
  },
@@ -0,0 +1,24 @@
1
+ /**
2
+ * Snapshot-store key for a class LOCAL INCLUDE (CCDEF / CCIMP / CCMAC / test
3
+ * classes). Pure — no fs, no SAP.
4
+ *
5
+ * Why a separate key (2026-09-21, found on the first clean-core fix): the
6
+ * include write used to snapshot the class MAIN source. For a RAP behaviour
7
+ * pool the main source is an 8-line shell and the change lives in CCIMP — so
8
+ * the Rule-7 backup never covered the change, and rewind wrote the unchanged
9
+ * main source back and reported success while the include change stayed.
10
+ *
11
+ * The include is stored under type `CLAS.<INCLUDE>` (e.g. `CLAS.LOCALS_IMP`),
12
+ * name = class name. A dot keeps the store's two-level type/name layout (no
13
+ * nested dirs) and can never collide with a real ABAP type code.
14
+ */
15
+ const INCLUDE_TYPE_RE = /^CLAS\.(LOCALS_DEF|LOCALS_IMP|MACROS|TESTCLASSES)$/i;
16
+ /** Snapshot type key for one include of a class. */
17
+ export function includeSnapshotType(includeType) {
18
+ return `CLAS.${includeType.toUpperCase()}`;
19
+ }
20
+ /** The include a snapshot type key names, or null for an ordinary object type. */
21
+ export function parseIncludeSnapshotType(type) {
22
+ const m = INCLUDE_TYPE_RE.exec(type);
23
+ return m ? m[1].toLowerCase() : null;
24
+ }
@@ -24,7 +24,8 @@
24
24
  import crypto from 'node:crypto';
25
25
  import { registerTool } from './index.js';
26
26
  import { verifyAndSpendApprovalId } from '../approvals/jwt.js';
27
- import { autoSnapshot } from './snapshot.js';
27
+ import { autoSnapshot, autoSnapshotClassInclude } from './snapshot.js';
28
+ import { includeSnapshotType } from './include-snapshot.js';
28
29
  import { buildWriteChainLine } from '../renderer/verify-chain.js';
29
30
  import { verifySyntax, verifyActive } from './verify.js';
30
31
  import { appendPending, finalizeToolCall } from '../session/pending.js';
@@ -663,9 +664,9 @@ registerTool({
663
664
  // Writes a class local-include source (CCDEF/CCIMP/CCMAC/test classes). This is
664
665
  // how RAP behaviour-pool handlers (which live in CCIMP / locals_imp) get landed.
665
666
  // AdtClient locks the CLASS object, tries a ladder of PUT URL/content-type
666
- // variants (first 2xx wins), and always unlocks. Snapshot is best-effort:
667
- // the include source may not be independently readable, so a failed snapshot
668
- // must not block the write (consistent with sap_delete_object).
667
+ // variants (first 2xx wins), and always unlocks. The Rule-7 snapshot is of the
668
+ // INCLUDE itself (autoSnapshotClassInclude). It is non-blocking — a fresh
669
+ // class's include may not be readable yet — but a failed one warns loudly.
669
670
  registerTool({
670
671
  name: 'sap_set_class_include',
671
672
  description: 'Write a class local-include source (CCDEF locals_def, CCIMP locals_imp, '
@@ -713,15 +714,24 @@ registerTool({
713
714
  // The CCIMP piece (LIMU CINC) is exactly the kind of object that sits
714
715
  // locked in a request from earlier work — battery S1's 409 root cause.
715
716
  const resolved = await resolveWriteTransport(ctx, 'CLAS', args.className, effectiveTransport);
716
- // ── Gate 2: Snapshot (Rule 7) — best-effort, include may not be readable ─
717
+ // ── Gate 2: Snapshot (Rule 7) — of the INCLUDE being overwritten ─────────
718
+ // Not the class main source: for a behaviour pool that is an empty shell and
719
+ // the change lives in CCIMP (2026-09-21 — the old main-source snapshot left
720
+ // the change with no restore point, and rewind "restored" the wrong thing).
721
+ // Still non-blocking (a fresh class's include may not be readable yet), but
722
+ // a missing restore point is now said out loud instead of swallowed.
717
723
  let snapOutcome;
718
724
  try {
719
- snapOutcome = await autoSnapshot(args.className, 'CLAS', ctx.adt);
725
+ snapOutcome = await autoSnapshotClassInclude(args.className, args.includeType, ctx.adt);
720
726
  }
721
- catch {
722
- // Silently ignore — include source may not be independently readable.
727
+ catch (err) {
728
+ snapOutcome = { taken: false, reason: 'source_read_failed', detail: String(err) };
729
+ }
730
+ emitSnapshotNotice(ctx, snapOutcome, includeSnapshotType(args.includeType), args.className);
731
+ if (snapOutcome?.reason === 'source_read_failed') {
732
+ ctx.chunkEmitter?.emit('warn', `CLAS ${args.className} ${args.includeType}: could not snapshot the include before writing `
733
+ + `(${snapOutcome.detail ?? 'read failed'}) — this write has NO restore point; rewind cannot undo it`);
723
734
  }
724
- emitSnapshotNotice(ctx, snapOutcome, 'CLAS', args.className);
725
735
  // ── WAL: record pending entry ────────────────────────────────────────────
726
736
  await appendPending(ctx.session, {
727
737
  tool_use_id: toolUseId,
@@ -15,6 +15,7 @@
15
15
  */
16
16
  import { registerTool } from './index.js';
17
17
  import { snapshots, SapError } from '@cspeach/sap-client';
18
+ import { includeSnapshotType } from './include-snapshot.js';
18
19
  /** True only for a genuine ADT 404 — the object does not exist. Any other
19
20
  * failure (network, auth, 5xx, timeout) is a READ failure, not proof of
20
21
  * absence, and must never be labelled "object not found". */
@@ -100,6 +101,32 @@ export async function autoSnapshot(objectName, objectType, adt) {
100
101
  const entry = await snapshots.take(objectType, objectName, src, 'before_write');
101
102
  return { taken: true, entry };
102
103
  }
104
+ /**
105
+ * Rule-7 snapshot of ONE class local include, taken before sap_set_class_include
106
+ * overwrites it. Stored under includeSnapshotType(includeType) + class name so
107
+ * rewind can put the include (not the class main source) back.
108
+ *
109
+ * 404 → 'object_not_found' (include has no source yet — nothing to protect)
110
+ * other read error → 'source_read_failed' (+ detail) — caller decides; no restore point
111
+ * read OK (even '') → snapshot taken
112
+ * A snapshots.take failure THROWS, like autoSnapshot.
113
+ */
114
+ export async function autoSnapshotClassInclude(className, includeType, adt) {
115
+ let src;
116
+ try {
117
+ src = await adt.classIncludes(className, includeType);
118
+ }
119
+ catch (err) {
120
+ if (isNotFound(err))
121
+ return { taken: false, reason: 'object_not_found' };
122
+ return { taken: false, reason: 'source_read_failed', detail: shortErrorMessage(err) };
123
+ }
124
+ if (typeof src !== 'string') {
125
+ return { taken: false, reason: 'source_read_failed', detail: 'include read returned no source text' };
126
+ }
127
+ const entry = await snapshots.take(includeSnapshotType(includeType), className, src, 'before_write');
128
+ return { taken: true, entry };
129
+ }
103
130
  // ── sap_snapshot_list ─────────────────────────────────────────────────────────
104
131
  registerTool({
105
132
  name: 'sap_snapshot_list',
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@cspeach/cli",
3
- "version": "1.1.14",
4
- "description": "CSPeach — AI-assisted ABAP development CLI. Direct SAP access, 34 skills, safety gates.",
3
+ "version": "1.1.15",
4
+ "description": "CSPeach \u2014 AI-assisted ABAP development CLI. Direct SAP access, 34 skills, safety gates.",
5
5
  "keywords": [
6
6
  "abap",
7
7
  "sap",
@@ -32,7 +32,7 @@
32
32
  "prebuild": "tsx scripts/prebuild.ts",
33
33
  "build": "tsc && node scripts/make-bin-executable.js",
34
34
  "install:global": "tsx scripts/install-global.ts",
35
- "pretest": "tsx scripts/sync-signing-key.ts || node -e \"console.warn('WARN: sync-signing-key skipped — using stub')\"",
35
+ "pretest": "tsx scripts/sync-signing-key.ts || node -e \"console.warn('WARN: sync-signing-key skipped \u2014 using stub')\"",
36
36
  "start": "node dist/index.js",
37
37
  "typecheck": "tsc --noEmit",
38
38
  "test": "vitest run",
@@ -44,22 +44,25 @@
44
44
  "bench:audit": "tsx scripts/bench-audit-task.ts",
45
45
  "fiori": "tsx src/tools/fiori/bin.ts",
46
46
  "prepare": "husky",
47
- "prepublishOnly": "node -e \"const d=(require('./package.json').dependencies||{})['@cspeach/sap-client']||''; if(d.startsWith('file:')||d.startsWith('link:')){console.error('publish blocked: @cspeach/sap-client is a local link ('+d+') — point it at the published version before npm publish'); process.exit(1)}\""
47
+ "prepublishOnly": "node -e \"const deps=require('./package.json').dependencies||{}; for (const n of ['@cspeach/sap-client','@cspeach/register-core']) { const d=deps[n]||''; if(d.startsWith('file:')||d.startsWith('link:')){console.error('publish blocked: '+n+' is a local link ('+d+') \u2014 point it at the published version before npm publish'); process.exit(1)} }\""
48
48
  },
49
49
  "engines": {
50
50
  "node": ">=20.0.0"
51
51
  },
52
52
  "dependencies": {
53
53
  "@anthropic-ai/sdk": "^0.90.0",
54
- "@cspeach/sap-client": "^0.1.11",
54
+ "@cspeach/register-core": "0.1.0",
55
+ "@cspeach/sap-client": "0.1.12",
55
56
  "@iarna/toml": "^2.2.5",
56
57
  "@inkjs/ui": "^2.0.0",
57
58
  "@inquirer/prompts": "^7.0.0",
59
+ "@napi-rs/keyring": "2.1.0",
58
60
  "@noble/ed25519": "2.1.0",
59
61
  "@noble/hashes": "1.4.0",
60
62
  "@sap-ux/fe-fpm-writer": "0.43.7",
61
63
  "@sap-ux/fiori-elements-writer": "^3.0.39",
62
64
  "@sap-ux/fiori-freestyle-writer": "3.0.12",
65
+ "adm-zip": "0.5.17",
63
66
  "chalk": "^5.3.0",
64
67
  "cli-highlight": "^2.1.11",
65
68
  "cli-table3": "^0.6.5",
@@ -71,7 +74,6 @@
71
74
  "ink": "^5.0.0",
72
75
  "ink-text-input": "^6.0.0",
73
76
  "jose": "^5.9.0",
74
- "@napi-rs/keyring": "2.1.0",
75
77
  "mammoth": "^1.8.0",
76
78
  "marked": "^11.2.0",
77
79
  "marked-terminal": "^6.2.0",
@@ -86,6 +88,7 @@
86
88
  "zod": "^3.23.0"
87
89
  },
88
90
  "devDependencies": {
91
+ "@types/adm-zip": "0.5.8",
89
92
  "@types/cross-spawn": "^6.0.6",
90
93
  "@types/diff": "^7.0.2",
91
94
  "@types/ejs": "^3.1.5",