@cspeach/cli 1.1.27 → 1.1.29
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.
- package/dist/agent/loop.js +33 -4
- package/dist/agent/provider-shape.js +9 -2
- package/dist/agent/providers/ai-hub-provider.js +2 -2
- package/dist/agent/providers/byok-provider.js +2 -2
- package/dist/agent/providers/local-provider.js +4 -4
- package/dist/agent/system-prompt.js +34 -7
- package/dist/agent/tool-dispatch.js +20 -1
- package/dist/agent/turn-error-ux.js +40 -0
- package/dist/approvals/grant-claims.js +70 -0
- package/dist/approvals/jwt.js +21 -1
- package/dist/approvals/plan-grant.js +297 -0
- package/dist/commands/config-set.js +12 -0
- package/dist/commands/deploy-card.js +10 -5
- package/dist/commands/export-audit.js +1 -1
- package/dist/commands/plan-audit-evidence.js +47 -1
- package/dist/commands/plan-audit.js +23 -8
- package/dist/commands/plan-batch-audit.js +349 -0
- package/dist/commands/plan-boundary.js +88 -0
- package/dist/commands/plan-chain.js +409 -566
- package/dist/commands/plan-continue.js +21 -7
- package/dist/commands/plan-gate.js +28 -49
- package/dist/commands/plan-reground.js +174 -0
- package/dist/commands/plan-resume.js +227 -103
- package/dist/commands/plan-sap-pause.js +105 -0
- package/dist/commands/plan-startup.js +218 -0
- package/dist/config/loader.js +9 -0
- package/dist/doctor/checks/forge-rules.js +67 -85
- package/dist/doctor/run.js +4 -4
- package/dist/lane/app-chain.js +88 -9
- package/dist/one-shot.js +1 -0
- package/dist/projects/extract-plan.js +34 -1
- package/dist/projects/handover-md.js +1 -1
- package/dist/projects/plan-objects.js +75 -0
- package/dist/projects/plan-run.js +74 -18
- package/dist/projects/plan-schema.js +6 -0
- package/dist/projects/plan-sources.js +199 -0
- package/dist/projects/promote-command.js +1 -1
- package/dist/projects/save-command.js +11 -13
- package/dist/projects/saved-lines.js +36 -0
- package/dist/renderer/checklist-format.js +5 -5
- package/dist/renderer/machine-blocks.js +141 -0
- package/dist/renderer/tool-labels.js +3 -0
- package/dist/repl/plan-hop.js +22 -0
- package/dist/repl/rule8-detector.js +16 -1
- package/dist/repl.js +320 -55
- package/dist/router/router-core.js +2 -2
- package/dist/sap/reachability-state.js +25 -0
- package/dist/session/audit-export.js +10 -16
- package/dist/session/recap.js +16 -26
- package/dist/skills/bundled-skills.js +81 -81
- package/dist/tools/_filesystem-shared.js +37 -0
- package/dist/tools/approval.js +48 -2
- package/dist/tools/dispatch-skill.js +3 -2
- package/dist/tools/filesystem/file-read.js +5 -3
- package/dist/tools/filesystem/glob.js +27 -4
- package/dist/tools/fiori/tools.js +6 -0
- package/dist/tools/lane/app-lane-resolve.js +1 -1
- package/dist/tools/sap-read.js +37 -3
- package/dist/tools/sap-write.js +65 -0
- package/dist/tools/text-elements/pool.js +250 -0
- package/dist/tools/text-elements/tools.js +433 -0
- package/dist/tools/tool-loading.js +3 -0
- package/dist/tools/transport.js +26 -5
- package/dist/tools/write-mode.js +4 -3
- package/dist/ui/app.js +4 -0
- package/dist/ui/widgets/hold-raw-mode.js +26 -0
- package/package.json +120 -121
- package/dist/projects/email-template.js +0 -56
|
@@ -0,0 +1,250 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Program text pool — pure logic for selection texts, text symbols and list
|
|
3
|
+
* headings. No SAP, no fs.
|
|
4
|
+
*
|
|
5
|
+
* ADT serves each part as a plain-text source (sap-client
|
|
6
|
+
* getTextElementsSource) and a PUT replaces the WHOLE part. So a write is
|
|
7
|
+
* always read → merge → write: the texts already there are kept byte for
|
|
8
|
+
* byte, only the named entries are added or changed. Never a blind replace.
|
|
9
|
+
*
|
|
10
|
+
* Source formats (S4H read-only capture 2026-10-04 + SAP_BASIS 754 captures):
|
|
11
|
+
* selections `P_MAXROW=Maximum Rows`, blank line between entries; SAP pads
|
|
12
|
+
* the name to 8 chars on read (`S_BUDAT =Posting Date`). A
|
|
13
|
+
* dictionary-referenced text carries a bare `@DDICReference`
|
|
14
|
+
* line right above it. Some releases list screen fields with no
|
|
15
|
+
* text as `NAME=?...` (754); S4H leaves them out.
|
|
16
|
+
* symbols `@MaxLength:N` then `001=Text`, blank line between entries.
|
|
17
|
+
* headings `listHeader=…`, blank line, `columnHeader_1..4=…` — always
|
|
18
|
+
* all five lines.
|
|
19
|
+
*/
|
|
20
|
+
export const TEXT_ELEMENT_KINDS = ['selections', 'symbols', 'headings'];
|
|
21
|
+
export const SELECTION_TEXT_MAX = 30;
|
|
22
|
+
export const SYMBOL_TEXT_MAX = 132;
|
|
23
|
+
export const LIST_HEADER_MAX = 70;
|
|
24
|
+
export const COLUMN_HEADER_MAX = 132;
|
|
25
|
+
/** SAP's marker for a selection-screen field that has no text yet. */
|
|
26
|
+
export const UNTEXTED_MARKER = '?...';
|
|
27
|
+
const HEADING_KEYS = ['listHeader', 'columnHeader_1', 'columnHeader_2', 'columnHeader_3', 'columnHeader_4'];
|
|
28
|
+
export function isTextElementKind(k) {
|
|
29
|
+
return typeof k === 'string' && TEXT_ELEMENT_KINDS.includes(k);
|
|
30
|
+
}
|
|
31
|
+
/** Canonical key: selections/symbols upper-cased, headings in SAP's camelCase. */
|
|
32
|
+
export function canonicalKey(kind, key) {
|
|
33
|
+
const k = key.trim();
|
|
34
|
+
if (kind !== 'headings')
|
|
35
|
+
return k.toUpperCase();
|
|
36
|
+
const hit = HEADING_KEYS.find((h) => h.toLowerCase() === k.toLowerCase());
|
|
37
|
+
return hit ?? k;
|
|
38
|
+
}
|
|
39
|
+
export function parseTextPool(kind, body) {
|
|
40
|
+
const out = [];
|
|
41
|
+
let pendingMax;
|
|
42
|
+
let pendingAnn = [];
|
|
43
|
+
for (const raw of body.split(/\r\n|\r|\n/)) {
|
|
44
|
+
if (raw.trim() === '')
|
|
45
|
+
continue;
|
|
46
|
+
if (raw.startsWith('@')) {
|
|
47
|
+
const m = /^@MaxLength:\s*(\d+)\s*$/i.exec(raw);
|
|
48
|
+
if (m && kind === 'symbols')
|
|
49
|
+
pendingMax = parseInt(m[1], 10);
|
|
50
|
+
else
|
|
51
|
+
pendingAnn.push(raw);
|
|
52
|
+
continue;
|
|
53
|
+
}
|
|
54
|
+
const eq = raw.indexOf('=');
|
|
55
|
+
if (eq < 0)
|
|
56
|
+
continue;
|
|
57
|
+
const key = canonicalKey(kind, raw.slice(0, eq));
|
|
58
|
+
const text = raw.slice(eq + 1);
|
|
59
|
+
const entry = { key, text };
|
|
60
|
+
if (kind === 'symbols' && pendingMax !== undefined)
|
|
61
|
+
entry.maxLength = pendingMax;
|
|
62
|
+
if (kind === 'selections' && text === UNTEXTED_MARKER) {
|
|
63
|
+
entry.text = '';
|
|
64
|
+
entry.untexted = true;
|
|
65
|
+
}
|
|
66
|
+
if (pendingAnn.length > 0)
|
|
67
|
+
entry.annotations = pendingAnn;
|
|
68
|
+
out.push(entry);
|
|
69
|
+
pendingMax = undefined;
|
|
70
|
+
pendingAnn = [];
|
|
71
|
+
}
|
|
72
|
+
return out;
|
|
73
|
+
}
|
|
74
|
+
export function formatTextPool(kind, entries) {
|
|
75
|
+
if (kind === 'headings') {
|
|
76
|
+
const byKey = new Map(entries.map((e) => [canonicalKey('headings', e.key), e.text]));
|
|
77
|
+
const v = (k) => byKey.get(k) ?? '';
|
|
78
|
+
return `listHeader=${v('listHeader')}\n\n`
|
|
79
|
+
+ HEADING_KEYS.slice(1).map((k) => `${k}=${v(k)}\n`).join('');
|
|
80
|
+
}
|
|
81
|
+
if (kind === 'symbols') {
|
|
82
|
+
if (entries.length === 0)
|
|
83
|
+
return '';
|
|
84
|
+
return entries
|
|
85
|
+
.map((e) => {
|
|
86
|
+
const max = Math.min(Math.max(e.maxLength ?? 0, e.text.length, 1), SYMBOL_TEXT_MAX);
|
|
87
|
+
return [`@MaxLength:${max}`, ...(e.annotations ?? []), `${e.key}=${e.text}`].join('\n');
|
|
88
|
+
})
|
|
89
|
+
.join('\n\n') + '\n';
|
|
90
|
+
}
|
|
91
|
+
// selections — fields without a text are SAP's placeholder, not a text: never written back.
|
|
92
|
+
// Blank line between entries, `@DDICReference` right above its entry: the shape S4H serves.
|
|
93
|
+
const texted = entries.filter((e) => !e.untexted && e.text !== '');
|
|
94
|
+
if (texted.length === 0)
|
|
95
|
+
return '';
|
|
96
|
+
return texted
|
|
97
|
+
.map((e) => [...(e.annotations ?? []), `${e.key}=${e.text}`].join('\n'))
|
|
98
|
+
.join('\n\n') + '\n';
|
|
99
|
+
}
|
|
100
|
+
export function validateUpdates(kind, updates) {
|
|
101
|
+
const errs = [];
|
|
102
|
+
if (!Array.isArray(updates) || updates.length === 0)
|
|
103
|
+
return ['texts must name at least one entry'];
|
|
104
|
+
const seen = new Set();
|
|
105
|
+
for (const u of updates) {
|
|
106
|
+
if (typeof u?.key !== 'string' || typeof u?.text !== 'string') {
|
|
107
|
+
errs.push('every entry needs a key and a text (strings)');
|
|
108
|
+
continue;
|
|
109
|
+
}
|
|
110
|
+
const key = canonicalKey(kind, u.key);
|
|
111
|
+
const text = u.text;
|
|
112
|
+
if (seen.has(key))
|
|
113
|
+
errs.push(`${key}: named twice`);
|
|
114
|
+
seen.add(key);
|
|
115
|
+
if (/[\r\n]/.test(text))
|
|
116
|
+
errs.push(`${key}: a text must be one line`);
|
|
117
|
+
if (kind === 'selections') {
|
|
118
|
+
if (!/^[A-Z][A-Z0-9_]{0,7}$/.test(key))
|
|
119
|
+
errs.push(`${key}: not a selection-screen name (1-8 letters, digits, _)`);
|
|
120
|
+
if (text.trim() === '')
|
|
121
|
+
errs.push(`${key}: text is empty`);
|
|
122
|
+
if (text.length > SELECTION_TEXT_MAX)
|
|
123
|
+
errs.push(`${key}: selection text is ${text.length} chars — the limit is ${SELECTION_TEXT_MAX}`);
|
|
124
|
+
}
|
|
125
|
+
else if (kind === 'symbols') {
|
|
126
|
+
if (!/^[A-Z0-9]{3}$/.test(key))
|
|
127
|
+
errs.push(`${key}: a text symbol id is 3 letters/digits (TEXT-001 → 001)`);
|
|
128
|
+
if (text.trim() === '')
|
|
129
|
+
errs.push(`${key}: text is empty`);
|
|
130
|
+
if (text.length > SYMBOL_TEXT_MAX)
|
|
131
|
+
errs.push(`${key}: text symbol is ${text.length} chars — the limit is ${SYMBOL_TEXT_MAX}`);
|
|
132
|
+
if (u.max_length !== undefined) {
|
|
133
|
+
if (!Number.isInteger(u.max_length) || u.max_length < text.length || u.max_length > SYMBOL_TEXT_MAX) {
|
|
134
|
+
errs.push(`${key}: max_length must be a whole number from the text length (${text.length}) to ${SYMBOL_TEXT_MAX}`);
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
else {
|
|
139
|
+
if (!HEADING_KEYS.includes(key))
|
|
140
|
+
errs.push(`${key}: headings are ${HEADING_KEYS.join(', ')}`);
|
|
141
|
+
const limit = key === 'listHeader' ? LIST_HEADER_MAX : COLUMN_HEADER_MAX;
|
|
142
|
+
if (text.length > limit)
|
|
143
|
+
errs.push(`${key}: heading is ${text.length} chars — the limit is ${limit}`);
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
return errs;
|
|
147
|
+
}
|
|
148
|
+
/** Merge updates into the existing entries. Validate first (validateUpdates). */
|
|
149
|
+
export function mergeTextPool(kind, existing, updates) {
|
|
150
|
+
const entries = existing.map((e) => ({ ...e, ...(e.annotations ? { annotations: [...e.annotations] } : {}) }));
|
|
151
|
+
const index = new Map(entries.map((e, i) => [e.key, i]));
|
|
152
|
+
const r = { entries, added: [], changed: [], unchanged: [], kept: [], dictionaryReferenceDropped: [] };
|
|
153
|
+
const touched = new Set();
|
|
154
|
+
for (const u of updates) {
|
|
155
|
+
const key = canonicalKey(kind, u.key);
|
|
156
|
+
touched.add(key);
|
|
157
|
+
const at = index.get(key);
|
|
158
|
+
if (at === undefined) {
|
|
159
|
+
const e = { key, text: u.text };
|
|
160
|
+
if (kind === 'symbols')
|
|
161
|
+
e.maxLength = u.max_length ?? u.text.length;
|
|
162
|
+
entries.push(e);
|
|
163
|
+
index.set(key, entries.length - 1);
|
|
164
|
+
if (kind !== 'headings' || u.text !== '')
|
|
165
|
+
r.added.push(key);
|
|
166
|
+
continue;
|
|
167
|
+
}
|
|
168
|
+
const cur = entries[at];
|
|
169
|
+
const newMax = kind === 'symbols' ? (u.max_length ?? Math.max(cur.maxLength ?? 0, u.text.length)) : undefined;
|
|
170
|
+
const sameText = !cur.untexted && cur.text === u.text;
|
|
171
|
+
if (sameText && (kind !== 'symbols' || newMax === cur.maxLength)) {
|
|
172
|
+
r.unchanged.push(key);
|
|
173
|
+
continue;
|
|
174
|
+
}
|
|
175
|
+
if (cur.untexted || (kind === 'headings' && cur.text === ''))
|
|
176
|
+
r.added.push(key);
|
|
177
|
+
else
|
|
178
|
+
r.changed.push(key);
|
|
179
|
+
const next = { ...cur, text: u.text };
|
|
180
|
+
if (kind === 'selections')
|
|
181
|
+
next.untexted = false;
|
|
182
|
+
if (kind === 'symbols')
|
|
183
|
+
next.maxLength = newMax;
|
|
184
|
+
if (kind === 'selections' && cur.annotations?.some((a) => /^@DDIC/i.test(a))) {
|
|
185
|
+
next.annotations = cur.annotations.filter((a) => !/^@DDIC/i.test(a));
|
|
186
|
+
r.dictionaryReferenceDropped.push(key);
|
|
187
|
+
}
|
|
188
|
+
entries[at] = next;
|
|
189
|
+
}
|
|
190
|
+
for (const e of existing) {
|
|
191
|
+
if (touched.has(e.key))
|
|
192
|
+
continue;
|
|
193
|
+
if (e.untexted || e.text === '')
|
|
194
|
+
continue;
|
|
195
|
+
r.kept.push(e.key);
|
|
196
|
+
}
|
|
197
|
+
return r;
|
|
198
|
+
}
|
|
199
|
+
/**
|
|
200
|
+
* Selection-screen field names declared in a program source (PARAMETERS /
|
|
201
|
+
* SELECT-OPTIONS, chained or not). Comments are ignored. `hasIncludes` says
|
|
202
|
+
* the screen may also be declared in an include this scan cannot see.
|
|
203
|
+
*/
|
|
204
|
+
export function selectionScreenNames(source) {
|
|
205
|
+
const lines = source.split(/\r\n|\r|\n/).filter((l) => !l.startsWith('*')).map(stripLineComment);
|
|
206
|
+
const code = lines.join('\n');
|
|
207
|
+
const names = new Set();
|
|
208
|
+
const re = /(?:^|[\s.])(PARAMETERS|PARAMETER|SELECT-OPTIONS|SELECT-OPTION)\b\s*(:?)([^.]*)\./gim;
|
|
209
|
+
let m;
|
|
210
|
+
while ((m = re.exec(code)) !== null) {
|
|
211
|
+
const parts = m[2] === ':' ? m[3].split(',') : [m[3]];
|
|
212
|
+
for (const p of parts) {
|
|
213
|
+
const id = /^\s*([A-Za-z_][A-Za-z0-9_]*)/.exec(p);
|
|
214
|
+
if (id)
|
|
215
|
+
names.add(id[1].toUpperCase());
|
|
216
|
+
}
|
|
217
|
+
}
|
|
218
|
+
const hasIncludes = /^\s*INCLUDE\s+(?!STRUCTURE\b|TYPE\b)[A-Za-z0-9_/]+/im.test(code);
|
|
219
|
+
return { names, hasIncludes };
|
|
220
|
+
}
|
|
221
|
+
/** Drop a `"` comment, leaving quotes inside '…' and `…` literals alone. */
|
|
222
|
+
function stripLineComment(line) {
|
|
223
|
+
let inQ = null;
|
|
224
|
+
for (let i = 0; i < line.length; i++) {
|
|
225
|
+
const c = line[i];
|
|
226
|
+
if (inQ) {
|
|
227
|
+
if (c === inQ)
|
|
228
|
+
inQ = null;
|
|
229
|
+
}
|
|
230
|
+
else if (c === "'" || c === '`' || c === '|') {
|
|
231
|
+
inQ = c;
|
|
232
|
+
}
|
|
233
|
+
else if (c === '"') {
|
|
234
|
+
return line.slice(0, i);
|
|
235
|
+
}
|
|
236
|
+
}
|
|
237
|
+
return line;
|
|
238
|
+
}
|
|
239
|
+
/**
|
|
240
|
+
* Snapshot-store key for one text-pool part: `PROG.TEXT_<KIND>`, name = the
|
|
241
|
+
* program. A dot keeps the store's type/name layout and can never collide
|
|
242
|
+
* with a real ABAP type code (same scheme as class includes, CLAS.<INCLUDE>).
|
|
243
|
+
*/
|
|
244
|
+
export function textElementsSnapshotType(kind) {
|
|
245
|
+
return `PROG.TEXT_${kind.toUpperCase()}`;
|
|
246
|
+
}
|
|
247
|
+
export function parseTextElementsSnapshotType(type) {
|
|
248
|
+
const m = /^PROG\.TEXT_(SELECTIONS|SYMBOLS|HEADINGS)$/i.exec(type);
|
|
249
|
+
return m ? m[1].toLowerCase() : null;
|
|
250
|
+
}
|