@cspeach/cli 1.1.44 → 1.1.46

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 (60) hide show
  1. package/dist/agent/chat-prompt.js +47 -0
  2. package/dist/agent/chat-unavailable.js +9 -0
  3. package/dist/agent/level-state.js +11 -0
  4. package/dist/agent/level.js +60 -0
  5. package/dist/agent/loop.js +110 -2
  6. package/dist/agent/parallel-write-guard.js +2 -0
  7. package/dist/agent/provider-shape.js +6 -2
  8. package/dist/agent/providers/ai-hub-provider.js +3 -1
  9. package/dist/agent/providers/azure-provider.js +3 -1
  10. package/dist/agent/providers/byok-provider.js +3 -1
  11. package/dist/agent/providers/local-provider.js +3 -1
  12. package/dist/agent/style-state.js +11 -0
  13. package/dist/agent/style.js +59 -0
  14. package/dist/agent/system-prompt.js +24 -3
  15. package/dist/agent/tool-dispatch.js +55 -0
  16. package/dist/approvals/jwt.js +5 -2
  17. package/dist/approvals/op-labels.js +39 -4
  18. package/dist/approvals/render.js +36 -9
  19. package/dist/approvals/risk-floor.js +14 -3
  20. package/dist/cli.js +8 -0
  21. package/dist/commands/config-set.js +59 -1
  22. package/dist/commands/config-show.js +1 -0
  23. package/dist/commands/export-docx.js +66 -0
  24. package/dist/commands/help.js +28 -2
  25. package/dist/config/loader.js +51 -1
  26. package/dist/docx/doc-meta.js +47 -0
  27. package/dist/docx/markdown-to-docx.js +250 -0
  28. package/dist/docx/write-docx.js +54 -0
  29. package/dist/one-shot.js +11 -0
  30. package/dist/projects/document-save.js +62 -0
  31. package/dist/projects/document-skills.js +9 -0
  32. package/dist/renderer/footer-line.js +6 -1
  33. package/dist/renderer/progress-chatter.js +9 -2
  34. package/dist/renderer/startup-lines.js +6 -2
  35. package/dist/renderer/tool-labels.js +5 -2
  36. package/dist/renderer/tool-widget.js +5 -1
  37. package/dist/repl/builtin-commands.js +4 -1
  38. package/dist/repl/chat-command.js +64 -0
  39. package/dist/repl/chat-lane.js +135 -0
  40. package/dist/repl/level-command.js +79 -0
  41. package/dist/repl/style-command.js +67 -0
  42. package/dist/repl.js +187 -105
  43. package/dist/sap/profile-features.js +5 -0
  44. package/dist/session/audit-export.js +50 -2
  45. package/dist/session/queued-step.js +5 -1
  46. package/dist/session/schema.js +1 -0
  47. package/dist/session/store.js +4 -0
  48. package/dist/tools/approval.js +59 -1
  49. package/dist/tools/hand-off.js +86 -0
  50. package/dist/tools/release-texts.js +9 -0
  51. package/dist/tools/sap-read.js +6 -2
  52. package/dist/tools/sap-run.js +163 -0
  53. package/dist/tools/tool-loading.js +11 -7
  54. package/dist/tools/transport.js +248 -22
  55. package/dist/tools/write-mode.js +2 -0
  56. package/dist/ui/approval-modal.js +11 -3
  57. package/dist/ui/sap-state-store.js +1 -0
  58. package/dist/ui/session-timeline.js +1 -1
  59. package/dist/ui/widgets/ask-card.js +2 -1
  60. package/package.json +3 -3
@@ -7,14 +7,21 @@
7
7
  * transportList(user?, status?) → Promise<TransportListResult> line 1048
8
8
  * TransportListResult { filterUser, filterStatus, transports: TransportEntry[] }
9
9
  * TransportEntry { transportNumber, description?, owner?, status?, type?, target? }
10
- * transportRelease(transportNumber) → Promise<TransportResult> line 2546
11
- * Released status string from SAP is 'released' (not a raw 'R' code).
10
+ * transportRead(number) → Promise<TransportRequestDoc> (Part 3)
11
+ * transportRelease(number) → Promise<TransportReleaseResult> (Part 3)
12
+ * status 'released' only on SAP's verdict for every step AND a status read back of R/N.
12
13
  */
13
14
  import { bindGrantTransport, isPlanGrantInScope } from '../approvals/plan-grant.js';
14
15
  import { registerTool } from './index.js';
15
16
  import { verifyAndSpendApprovalId } from '../approvals/jwt.js';
16
- import { auditLog } from '@cspeach/sap-client';
17
+ import { auditLog, SapNotFoundError, isModifiable, knownObjectCount, allObjectNames } from '@cspeach/sap-client';
18
+ import { typeCode } from '../approvals/risk-floor.js';
19
+ import { spillToolResult } from './result-spill.js';
20
+ import { notModifiableText } from './release-texts.js';
21
+ import { RELEASE_CHECK_LINE } from '../approvals/op-labels.js';
17
22
  import { ERR } from '../errors/codes.js';
23
+ import { presentSafetyConfirmation } from '../repl/safety-confirm.js';
24
+ import { isSafetyConfirmEnabled } from '../repl/safety-mode-state.js';
18
25
  import { setCurrentTransport, noteConfirmedTransport } from '../repl/current-transport.js';
19
26
  import { enrichTransportList } from './transport-fit.js';
20
27
  import { reusedRequestRefusal } from './transport-fit-check.js';
@@ -472,40 +479,259 @@ registerTool({
472
479
  }
473
480
  },
474
481
  });
482
+ // ── sap_transport_read ───────────────────────────────────────────────────────
483
+ //
484
+ // Part 3 (D16) — one transport request as SAP's transport organizer shows it.
485
+ // Read only, no approval.
486
+ /** The 404 text, never SAP's generic "Object not found". */
487
+ function notFoundText(n) {
488
+ return `Transport request ${n} does not exist.`;
489
+ }
490
+ /** Gate 0 of the release and the whole of the read: SAP's own document, or the refusal envelope. */
491
+ async function readRequest(ctx, asked) {
492
+ try {
493
+ return { doc: await ctx.adt.transportRead(asked) };
494
+ }
495
+ catch (err) {
496
+ const body = err instanceof SapNotFoundError
497
+ ? { error: 'not_found', detail: notFoundText(asked) }
498
+ : { error: 'transport_read_failed', detail: err instanceof Error ? err.message : String(err) };
499
+ return { refusal: { content: JSON.stringify(body), is_error: true } };
500
+ }
501
+ }
502
+ registerTool({
503
+ name: 'sap_transport_read',
504
+ description: 'Read one transport request: status, owner, target, its tasks and how many objects it holds. '
505
+ + 'A task number answers its request with that task. Read only.',
506
+ isMutating: false,
507
+ requiresSap: true,
508
+ requires: 'cts.requests',
509
+ input_schema: {
510
+ type: 'object',
511
+ properties: {
512
+ transport: { type: 'string', description: 'Transport request or task number (e.g. DEVK900123)' },
513
+ },
514
+ required: ['transport'],
515
+ },
516
+ handler: async (args, ctx) => {
517
+ const asked = String(args.transport ?? '').trim().toUpperCase();
518
+ const read = await readRequest(ctx, asked);
519
+ if ('refusal' in read)
520
+ return read.refusal;
521
+ const { doc } = read;
522
+ const objectCount = knownObjectCount(doc);
523
+ const notes = [];
524
+ if (doc.kind === 'task')
525
+ notes.push(`${asked} is a task of ${doc.number}.`);
526
+ if (objectCount === null)
527
+ notes.push(`CSPeach could not count the objects of ${doc.number}.`);
528
+ return {
529
+ content: JSON.stringify({
530
+ kind: doc.kind,
531
+ request: doc.number,
532
+ description: doc.description,
533
+ owner: doc.owner,
534
+ type: doc.type,
535
+ status: doc.status,
536
+ status_text: doc.statusText,
537
+ target: doc.target,
538
+ tasks: doc.tasks.map((t) => ({
539
+ number: t.number, owner: t.owner, status: t.status, status_text: t.statusText, objects: t.objectCount,
540
+ })),
541
+ object_count: objectCount,
542
+ can_release: isModifiable(doc.status) && doc.actions.includes('newreleasejobs'),
543
+ ...(notes.length ? { note: notes.join(' ') } : {}),
544
+ }),
545
+ };
546
+ },
547
+ });
475
548
  // ── sap_transport_release ────────────────────────────────────────────────────
476
549
  //
477
- // 1.1.44 hotfix: not available. The request this tool used to send never
478
- // released anything (SAP routes it to the create-request handler), and the
479
- // tool then reported "released". Until the proper release ships, the tool
480
- // refuses before anything else: no approval spent, no card, no SAP request.
481
- const RELEASE_UNAVAILABLE = 'Releasing a transport is not available in this version of CSPeach. '
482
- + 'Release it in SE09 or Eclipse. Nothing was sent to SAP.';
550
+ // Part 3 (D10–D13, D17) — every open task first, then the request, on SAP's
551
+ // own release verdict (sap-client transportRelease). Gates, in order:
552
+ // 0. read the request: missing, a task number, not modifiable → refused;
553
+ // 0b. the request's objects (its tasks' too) against the inactive list →
554
+ // refused (SAP would abort with abortrelinacobj; its consistency check
555
+ // does not see inactive objects);
556
+ // 1. the approval (op release, type CTS) — spent only after 0/0b passed;
557
+ // 2. the safety card (Rule 9) when safety mode is on.
558
+ // The envelope carries every step's verdict, SAP's texts and raw reports, what
559
+ // was never sent, the status read back, and that the release is not yet proven
560
+ // live. sap-client writes the audit rows (one per step and one closing row);
561
+ // the tool writes none of its own, except when sap-client threw before any.
562
+ /** Verdicts that mean "SAP would release it if something were ignored" (D11). */
563
+ const IGNORE_VERDICTS = new Set(['relwithignlock', 'relObjigchkatc', 'relwithignwarning']);
564
+ const NOT_YET_PROVEN = "Built from SAP's own release interface and tested against recorded answers; not yet run in a live release. "
565
+ + 'Ask the user to check the transport in SE09 after the release.';
483
566
  registerTool({
484
567
  name: 'sap_transport_release',
485
- description: 'Releasing a transport is not available in this version of CSPeach. '
486
- + 'This tool sends nothing to SAP; tell the user to release the transport in SE09 or Eclipse.',
568
+ description: 'Release a transport request: every open task first, then the request. The verdict comes from SAP\'s own release '
569
+ + 'report, step by step, and the request status is read back; an HTTP answer alone is never taken as released. '
570
+ + 'Stops at the first step SAP does not release; never ignores locks, object-check errors or warnings (release '
571
+ + 'those in SE09 or Eclipse). Built from SAP\'s own release interface and tested against recorded answers; not '
572
+ + 'yet run in a live release, so ask the user to check the transport in SE09 afterwards. Cannot be undone. '
573
+ + 'Requires approval_id from a request_approval change of op `release`, type CTS (one approval per release; '
574
+ + 'never auto-approved).',
487
575
  isMutating: true,
488
576
  requiresSap: true,
577
+ requires: 'cts.requests',
489
578
  input_schema: {
490
579
  type: 'object',
491
580
  properties: {
492
581
  transport: {
493
582
  type: 'string',
494
- description: 'Transport request number (e.g. DEVK900123)',
583
+ description: 'Transport request number to release (e.g. DEVK900123); not a task number',
495
584
  },
496
585
  approval_id: {
497
586
  type: 'string',
498
- description: 'JWT from request_approval (not used: release is not available)',
587
+ description: 'JWT from request_approval (a change of op release, type CTS)',
499
588
  },
500
589
  },
501
- required: ['transport'],
590
+ required: ['transport', 'approval_id'],
591
+ },
592
+ handler: async (args, ctx) => {
593
+ const asked = String(args.transport ?? '').trim().toUpperCase();
594
+ // ── Gate 0: SAP's own facts, read fresh (D10, D12) ──────────────────────
595
+ const read = await readRequest(ctx, asked);
596
+ if ('refusal' in read)
597
+ return read.refusal;
598
+ const { doc } = read;
599
+ if (doc.kind === 'task') {
600
+ return {
601
+ content: JSON.stringify({
602
+ error: 'task_number',
603
+ message: `${asked} is a task of ${doc.number}. Give the request number; CSPeach releases the tasks first, then the request.`,
604
+ }),
605
+ is_error: true,
606
+ };
607
+ }
608
+ if (!isModifiable(doc.status)) {
609
+ return {
610
+ content: JSON.stringify({
611
+ error: 'not_modifiable',
612
+ message: notModifiableText(doc),
613
+ }),
614
+ is_error: true,
615
+ };
616
+ }
617
+ // ── Gate 0b: inactive objects of the request (T14) ──────────────────────
618
+ let warning;
619
+ let inactiveNames;
620
+ try {
621
+ inactiveNames = new Set((await ctx.adt.inactiveObjects()).map((o) => String(o.name ?? '').trim().toUpperCase()));
622
+ }
623
+ catch {
624
+ warning = 'CSPeach could not read the inactive list; SAP aborts a release that holds inactive objects.';
625
+ }
626
+ if (inactiveNames) {
627
+ const blocked = allObjectNames(doc).filter((n) => inactiveNames.has(n));
628
+ if (blocked.length > 0) {
629
+ return {
630
+ content: JSON.stringify({
631
+ error: 'release_blocked_inactive',
632
+ objects: blocked,
633
+ message: `These objects of ${doc.number} are inactive: ${blocked.join(', ')}. `
634
+ + 'Activate them (sap_activate), then ask again. SAP would abort the release.',
635
+ }),
636
+ is_error: true,
637
+ };
638
+ }
639
+ }
640
+ // ── Gate 1: Approval (op release, type CTS) ─────────────────────────────
641
+ const verify = await verifyAndSpendApprovalId(args.approval_id, args.transport, 'release');
642
+ if (!verify.ok) {
643
+ return {
644
+ content: JSON.stringify({ error: ERR.APPROVAL_INVALID, reason: verify.reason }),
645
+ is_error: true,
646
+ };
647
+ }
648
+ if (typeCode(verify.payload?.type) !== 'CTS') {
649
+ return {
650
+ content: JSON.stringify({ error: ERR.APPROVAL_INVALID, reason: 'type_mismatch' }),
651
+ is_error: true,
652
+ };
653
+ }
654
+ const startedAt = Date.now();
655
+ // ── Gate 2: Safety confirmation (Rule 9 — a release cannot be undone) ───
656
+ if (isSafetyConfirmEnabled()) {
657
+ const openTasks = doc.tasks.filter((t) => isModifiable(t.status)).map((t) => t.number);
658
+ const objects = knownObjectCount(doc);
659
+ const c = await presentSafetyConfirmation({
660
+ rule: 'RULE_9_TRANSPORT_RELEASE',
661
+ op: 'transport_release',
662
+ object: { type: 'TRANSPORT', name: doc.number },
663
+ what_will_happen: `Releases ${openTasks.length} task(s)${openTasks.length ? ` (${openTasks.join(', ')})` : ''} then request ${doc.number} `
664
+ + `(${objects === null ? 'an uncounted number of' : objects} objects, target ${doc.target || 'none'}). `
665
+ + `Cannot be undone; the request may move to other systems on the route. ${RELEASE_CHECK_LINE}`,
666
+ rollback_available: false,
667
+ transport: doc.number,
668
+ args: { ...args },
669
+ });
670
+ if (!c.confirmed) {
671
+ return {
672
+ content: JSON.stringify({ error: 'cancelled_by_user', reason: c.reason }),
673
+ is_error: true,
674
+ };
675
+ }
676
+ }
677
+ // ── Release: tasks, then the request, on SAP's verdict ──────────────────
678
+ let r;
679
+ try {
680
+ r = await ctx.adt.transportRelease(doc.number);
681
+ }
682
+ catch (err) {
683
+ // sap-client threw before its first step (its own read): it wrote no row.
684
+ await auditLog('transportRelease', 'CTS', doc.number, 'error', Date.now() - startedAt);
685
+ return {
686
+ content: JSON.stringify({ error: 'transport_release_failed', detail: err instanceof Error ? err.message : String(err) }),
687
+ is_error: true,
688
+ };
689
+ }
690
+ const finalStatus = r.finalStatus?.status;
691
+ // Never `released: true` beside a status that is not R or N.
692
+ const released = r.status === 'released' && (finalStatus === 'R' || finalStatus === 'N');
693
+ let note;
694
+ switch (r.status) {
695
+ case 'released':
696
+ // Worded from `released`, never from r.status alone: "Released." only beside released: true.
697
+ note = released
698
+ ? "Released. SAP's answer for each step is in steps[] and sap_answer[]."
699
+ : `SAP reported every step released, but the request reads status ${finalStatus || 'unknown'}. Check it in SE09.`;
700
+ break;
701
+ case 'stopped': {
702
+ const last = r.steps[r.steps.length - 1];
703
+ note = `${r.error ?? ''} Nothing was sent after that step.`.trim();
704
+ if (last && IGNORE_VERDICTS.has(last.verdict)) {
705
+ note += ' CSPeach does not release with anything ignored. Release it in SE09 or Eclipse if that is what you want.';
706
+ }
707
+ break;
708
+ }
709
+ case 'refused':
710
+ note = r.refusal ?? 'Nothing was sent.';
711
+ break;
712
+ default:
713
+ note = r.error ?? `The release ended with status ${r.status}.`;
714
+ }
715
+ const detail = r.refusal ?? r.error;
716
+ const stopStep = r.status === 'stopped' ? r.steps[r.steps.length - 1] : undefined;
717
+ const envelope = {
718
+ released,
719
+ status: r.status,
720
+ // Near the top so a spilled preview (and the audit row read from it) keeps the stopping step.
721
+ ...(stopStep ? { stopped_at: stopStep.number, stop_verdict: stopStep.verdict } : {}),
722
+ transport_number: r.transportNumber,
723
+ final_status: finalStatus,
724
+ not_sent: r.notSent ?? [],
725
+ note,
726
+ ...(detail ? { error_detail: detail } : {}),
727
+ ...(warning ? { warning } : {}),
728
+ not_yet_proven: NOT_YET_PROVEN,
729
+ steps: r.steps.map(({ rawReports: _raw, ...s }) => s),
730
+ sap_answer: r.steps.map((s) => ({ step: s.number, raw: s.rawReports ?? '' })),
731
+ };
732
+ // Pretty-printed with the facts first: a spilled preview keeps the verdict
733
+ // lines (a one-line JSON would be cut at the preview's line limit).
734
+ const failed = r.status === 'stopped' || r.status === 'error' || r.status === 'refused';
735
+ return spillToolResult({ content: JSON.stringify(envelope, null, 2), ...(failed ? { is_error: true } : {}) }, ctx, 'report', { label: `release ${r.transportNumber}` });
502
736
  },
503
- handler: async (args) => ({
504
- content: JSON.stringify({
505
- error: 'release_unavailable',
506
- transport: args.transport,
507
- message: RELEASE_UNAVAILABLE,
508
- }),
509
- is_error: true,
510
- }),
511
737
  });
@@ -23,6 +23,8 @@ export const WRITE_TOOLS = new Set([
23
23
  'sap_transport_release',
24
24
  // Final review I2 — SAP's RAP generator creates the skeleton objects.
25
25
  'sap_generate_objects',
26
+ // Part 3 — a run executes code on the SAP system; advisory mode never runs it.
27
+ 'sap_run',
26
28
  ]);
27
29
  export function isWriteTool(name) {
28
30
  return WRITE_TOOLS.has(name);
@@ -2,7 +2,7 @@ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
2
  import { useRef, useState } from 'react';
3
3
  import { Box, Text, useInput, useStdout } from 'ink';
4
4
  import { riskReasonText, shownRiskReasons } from '../approvals/risk-floor.js';
5
- import { verbLine, viewAffordance, toSourceLines, affectsLine } from '../approvals/op-labels.js';
5
+ import { verbLine, viewAffordance, toSourceLines, affectsLine, releaseLines } from '../approvals/op-labels.js';
6
6
  // BUG-1 (2026-07-05 live smoke): the interactive approval modal mounted
7
7
  // DEAD — every keypress ignored — when it appeared after a guarded
8
8
  // /abap-plan turn. Root cause is identical to the one AskCard
@@ -39,10 +39,18 @@ export function riskRows(effectiveRisk, width) {
39
39
  * decision input too: its "Affects:" text wraps onto up to this many rows.
40
40
  */
41
41
  export const AFFECTS_MAX_ROWS = 2;
42
- /** The "Affects: …" rows under the verb line; none when the change names no process. */
42
+ /**
43
+ * The rows under the verb line: "Affects: …" (none when the change names no
44
+ * process), then — Part 3 D12 — a release card's fact lines, each wrapping
45
+ * onto up to AFFECTS_MAX_ROWS rows. Any other change: exactly as before.
46
+ */
43
47
  export function affectsRows(change, width) {
48
+ const cols = Math.max(1, width - MODAL_CHROME_COLS);
44
49
  const line = affectsLine(change);
45
- return line ? wrapRows(line, Math.max(1, width - MODAL_CHROME_COLS), AFFECTS_MAX_ROWS) : [];
50
+ return [
51
+ ...(line ? wrapRows(line, cols, AFFECTS_MAX_ROWS) : []),
52
+ ...releaseLines(change).flatMap((l) => wrapRows(l, cols, AFFECTS_MAX_ROWS)),
53
+ ];
46
54
  }
47
55
  /**
48
56
  * The collapsed card's rendered height at `width` (the live-area budget; the
@@ -20,6 +20,7 @@ const DEFAULT_STATE = {
20
20
  creditsMode: false, creditsSessionTotal: 0, creditsBalance: null,
21
21
  creditsUnitLabel: 'credits', creditsPending: false,
22
22
  standalone: false,
23
+ chatLane: false,
23
24
  };
24
25
  /**
25
26
  * Rate limiter: calls `fn` at most once every `minIntervalMs`. Trailing-only:
@@ -14,7 +14,7 @@ const MUTATING_TOOLS = new Set([
14
14
  'sap_set_source', 'sap_update_method', 'sap_delete_object', 'sap_activate',
15
15
  'sap_create_object', 'sap_transport_release', 'sap_message_maintain',
16
16
  'sap_number_range_intervals', 'sap_service_binding_publish',
17
- 'sap_service_binding_unpublish',
17
+ 'sap_service_binding_unpublish', 'sap_run',
18
18
  ]);
19
19
  export function toTimelineEntry(call) {
20
20
  // args shape depends on the tool; extract defensively.
@@ -178,6 +178,7 @@ export function toCardQuestions(request) {
178
178
  options: freeText ? [] : request.choices,
179
179
  otherRow: false,
180
180
  defaultValue: request.defaultValue,
181
+ cursorValue: request.cursorValue,
181
182
  keys: request.keys,
182
183
  }];
183
184
  }
@@ -323,7 +324,7 @@ export function AskCard({ request, width, showDescriptions, maxRows, onResolveV1
323
324
  // Focus slots: 0..n-1 = questions; n = the Review step (2+ questions,
324
325
  // reachable only once every question has an answer).
325
326
  const [focus, setFocus] = useState(0);
326
- const [cursors, setCursors] = useState(() => questions.map((qq) => Math.max(0, qq.options.findIndex((o) => o.value === qq.defaultValue))));
327
+ const [cursors, setCursors] = useState(() => questions.map((qq) => Math.max(0, qq.options.findIndex((o) => o.value === (qq.defaultValue ?? qq.cursorValue)))));
327
328
  const [ticks, setTicks] = useState(() => questions.map(() => new Set()));
328
329
  const [drafts, setDrafts] = useState(() => questions.map(() => ''));
329
330
  const [answers, setAnswers] = useState(() => questions.map(() => null));
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@cspeach/cli",
3
- "version": "1.1.44",
3
+ "version": "1.1.46",
4
4
  "description": "CSPeach — AI-assisted ABAP development CLI. Direct SAP access, signed skills, safety gates.",
5
5
  "keywords": [
6
6
  "abap",
@@ -54,7 +54,7 @@
54
54
  "@anthropic-ai/sdk": "^0.90.0",
55
55
  "@azure/msal-node": "6.0.1",
56
56
  "@cspeach/register-core": "0.4.0",
57
- "@cspeach/sap-client": "0.1.28",
57
+ "@cspeach/sap-client": "0.1.29",
58
58
  "@iarna/toml": "^2.2.5",
59
59
  "@inkjs/ui": "^2.0.0",
60
60
  "@inquirer/prompts": "^7.0.0",
@@ -74,6 +74,7 @@
74
74
  "clipboardy": "^5.3.1",
75
75
  "cross-spawn": "^7.0.6",
76
76
  "diff": "^9.0.0",
77
+ "docx": "^9.0.2",
77
78
  "ejs": "3.1.10",
78
79
  "fuse.js": "^7.0.0",
79
80
  "ink": "^5.0.0",
@@ -104,7 +105,6 @@
104
105
  "@types/turndown": "^5.0.5",
105
106
  "@ui5/builder": "4.3.2",
106
107
  "@ui5/fs": "4.0.6",
107
- "docx": "^9.0.2",
108
108
  "husky": "^9.1.7",
109
109
  "ink-testing-library": "^4.0.0",
110
110
  "pdf-lib": "^1.17.1",