@dozimple/abap-adt 1.0.0-rc.1
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/CHANGELOG.md +75 -0
- package/LICENSE +202 -0
- package/NOTICE +19 -0
- package/README.es.md +356 -0
- package/README.md +356 -0
- package/SECURITY.md +68 -0
- package/THIRD_PARTY_NOTICES.md +134 -0
- package/config/systems.example.json +39 -0
- package/dist/core/activation.js +18 -0
- package/dist/core/atc.js +94 -0
- package/dist/core/audit.js +93 -0
- package/dist/core/catalog.en.js +92 -0
- package/dist/core/catalog.js +139 -0
- package/dist/core/checks.js +19 -0
- package/dist/core/config.js +136 -0
- package/dist/core/confirm.js +43 -0
- package/dist/core/connection.js +159 -0
- package/dist/core/credentials.js +44 -0
- package/dist/core/datapolicy.js +175 -0
- package/dist/core/diff.js +115 -0
- package/dist/core/edits.js +31 -0
- package/dist/core/errors.js +68 -0
- package/dist/core/feeds.js +60 -0
- package/dist/core/notes.js +18 -0
- package/dist/core/objects.js +143 -0
- package/dist/core/output.js +27 -0
- package/dist/core/policy.js +126 -0
- package/dist/core/prompts.js +89 -0
- package/dist/core/registry.js +316 -0
- package/dist/core/revisions.js +97 -0
- package/dist/core/sidecar.js +85 -0
- package/dist/core/sql.js +54 -0
- package/dist/core/telemetry.js +42 -0
- package/dist/core/tool.js +5 -0
- package/dist/core/transport.js +109 -0
- package/dist/index.js +55 -0
- package/dist/scripts/audit-verify.js +19 -0
- package/dist/scripts/smoke.js +87 -0
- package/dist/tools/core/activate.js +27 -0
- package/dist/tools/core/api_release_state.js +58 -0
- package/dist/tools/core/atc_quickfix.js +159 -0
- package/dist/tools/core/co_change.js +73 -0
- package/dist/tools/core/create_transport.js +44 -0
- package/dist/tools/core/ddic_type_info.js +81 -0
- package/dist/tools/core/dumps.js +55 -0
- package/dist/tools/core/edit_preflight.js +112 -0
- package/dist/tools/core/function_modules.js +75 -0
- package/dist/tools/core/gateway_errors.js +45 -0
- package/dist/tools/core/get_source.js +70 -0
- package/dist/tools/core/inactive_objects.js +29 -0
- package/dist/tools/core/object_versions.js +35 -0
- package/dist/tools/core/package_contents.js +59 -0
- package/dist/tools/core/run_atc.js +117 -0
- package/dist/tools/core/run_unit_tests.js +60 -0
- package/dist/tools/core/search_objects.js +26 -0
- package/dist/tools/core/sql_query.js +25 -0
- package/dist/tools/core/support.js +95 -0
- package/dist/tools/core/syntax_check.js +35 -0
- package/dist/tools/core/table_contents.js +57 -0
- package/dist/tools/core/text_elements.js +108 -0
- package/dist/tools/core/transaction_info.js +43 -0
- package/dist/tools/core/transport_contents.js +49 -0
- package/dist/tools/core/transport_diff.js +111 -0
- package/dist/tools/core/where_used.js +37 -0
- package/dist/tools/core/write_source.js +95 -0
- package/dist/tools/docs/docs.js +159 -0
- package/dist/tools/local/growth.js +104 -0
- package/dist/tools/local/sap_systems.js +65 -0
- package/dist/tools/transport-risk/_service.js +63 -0
- package/dist/tools/transport-risk/risk_extras.js +90 -0
- package/dist/tools/transport-risk/transport_risk.js +42 -0
- package/docs/THREAT_MODEL.md +70 -0
- package/docs/TOOLS.md +980 -0
- package/package.json +68 -0
- package/scripts/set-password.sh +31 -0
package/dist/core/atc.js
ADDED
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* ATC recordados por sistema Y alcance (objeto u orden), para que «el hallazgo 3» sea siempre del objeto que se
|
|
3
|
+
* nombra. Antes se recordaba solo el último por sistema, y explain/finding sobre un objeto devolvían el hallazgo de
|
|
4
|
+
* otro objeto analizado antes: con apariencia correcta, que es lo peor.
|
|
5
|
+
*/
|
|
6
|
+
const runs = new Map();
|
|
7
|
+
const lastScope = new Map();
|
|
8
|
+
const MAX_RUNS = 100;
|
|
9
|
+
export const objectScope = (name) => `OBJ:${name.trim().toUpperCase()}`;
|
|
10
|
+
export const transportScope = (trkorr) => `TR:${trkorr.trim().toUpperCase()}`;
|
|
11
|
+
export function rememberRun(systemId, scopeKey, r) {
|
|
12
|
+
const k = `${systemId.toUpperCase()}|${scopeKey}`;
|
|
13
|
+
runs.delete(k); // reinsertar = más reciente
|
|
14
|
+
runs.set(k, r);
|
|
15
|
+
if (runs.size > MAX_RUNS)
|
|
16
|
+
runs.delete(runs.keys().next().value);
|
|
17
|
+
lastScope.set(systemId.toUpperCase(), scopeKey);
|
|
18
|
+
}
|
|
19
|
+
/** ATC de ese alcance; sin alcance, el último del sistema (quien llama debe decir de qué objeto es). */
|
|
20
|
+
export function recallRun(systemId, scopeKey) {
|
|
21
|
+
const key = scopeKey ?? lastScope.get(systemId.toUpperCase());
|
|
22
|
+
return key ? runs.get(`${systemId.toUpperCase()}|${key}`) : undefined;
|
|
23
|
+
}
|
|
24
|
+
/** «hace 3 min» para que se vea si el resultado recordado es de hace un rato. */
|
|
25
|
+
export function runAge(r, now = Date.now()) {
|
|
26
|
+
const min = Math.max(0, Math.round((now - Date.parse(r.at)) / 60_000));
|
|
27
|
+
return min < 1 ? "hace menos de 1 min" : `hace ${min} min`;
|
|
28
|
+
}
|
|
29
|
+
export async function defaultVariant(c) {
|
|
30
|
+
const cust = await c.atcCustomizing();
|
|
31
|
+
const v = cust.properties.find((p) => p.name === "systemCheckVariant")?.value;
|
|
32
|
+
return typeof v === "string" && v ? v : "DEFAULT";
|
|
33
|
+
}
|
|
34
|
+
const xmlEsc = (s) => s.replace(/&/g, "&").replace(/"/g, """).replace(/</g, "<");
|
|
35
|
+
/**
|
|
36
|
+
* Ejecución ATC sobre varios objetos a la vez. La librería solo admite una
|
|
37
|
+
* URI; en NW 7.50 una orden no sirve como conjunto («No URI-Mapping defined
|
|
38
|
+
* for URI»), así que se envían sus objetos en la misma ejecución.
|
|
39
|
+
*/
|
|
40
|
+
async function createMultiRun(c, worklistId, uris, maxResults) {
|
|
41
|
+
const refs = uris.map((u) => `<adtcore:objectReference adtcore:uri="${xmlEsc(u)}"/>`).join("");
|
|
42
|
+
const body = `<?xml version="1.0" encoding="UTF-8"?><atc:run maximumVerdicts="${maxResults}" xmlns:atc="http://www.sap.com/adt/atc">` +
|
|
43
|
+
`<objectSets xmlns:adtcore="http://www.sap.com/adt/core"><objectSet kind="inclusive"><adtcore:objectReferences>${refs}` +
|
|
44
|
+
`</adtcore:objectReferences></objectSet></objectSets></atc:run>`;
|
|
45
|
+
const r = await c.httpClient.request(`/sap/bc/adt/atc/runs?worklistId=${encodeURIComponent(worklistId)}`, {
|
|
46
|
+
method: "POST",
|
|
47
|
+
headers: { Accept: "application/xml", "Content-Type": "application/xml" },
|
|
48
|
+
body,
|
|
49
|
+
});
|
|
50
|
+
const xml = String(r.body);
|
|
51
|
+
const tag = (t) => new RegExp(`<(?:\\w+:)?${t}>([^<]*)</(?:\\w+:)?${t}>`).exec(xml)?.[1] ?? "";
|
|
52
|
+
const infos = [...xml.matchAll(/<(?:\w+:)?info>([\s\S]*?)<\/(?:\w+:)?info>/g)].map((m) => ({
|
|
53
|
+
type: /<(?:\w+:)?type>([^<]*)</.exec(m[1])?.[1] ?? "",
|
|
54
|
+
description: /<(?:\w+:)?description>([^<]*)</.exec(m[1])?.[1] ?? "",
|
|
55
|
+
}));
|
|
56
|
+
return { id: tag("worklistId"), timestamp: new Date(tag("worklistTimestamp")).getTime() / 1000, infos };
|
|
57
|
+
}
|
|
58
|
+
/** Ejecuta ATC sobre una o varias URI (objeto, paquete, orden u objetos de una orden). */
|
|
59
|
+
export async function runAtc(c, uri, variant, maxResults, includeExempted) {
|
|
60
|
+
const worklistId = await c.atcCheckVariant(variant);
|
|
61
|
+
const run = Array.isArray(uri) ? await createMultiRun(c, worklistId, uri, maxResults) : await c.createAtcRun(worklistId, uri, maxResults);
|
|
62
|
+
const wl = await c.atcWorklists(run.id, run.timestamp, "99999999999999999999999999999999", includeExempted);
|
|
63
|
+
const statsInfo = run.infos.find((i) => i.type === "FINDING_STATS")?.description;
|
|
64
|
+
const [p1, p2, p3] = (statsInfo ?? "").split(",").map((x) => Number(x));
|
|
65
|
+
const findings = [];
|
|
66
|
+
for (const o of wl.objects) {
|
|
67
|
+
for (const f of o.findings) {
|
|
68
|
+
findings.push({
|
|
69
|
+
n: 0,
|
|
70
|
+
objectName: o.name,
|
|
71
|
+
objectType: o.type,
|
|
72
|
+
objectUri: o.uri,
|
|
73
|
+
sourceUri: f.location.uri.split("#")[0],
|
|
74
|
+
line: f.location.range.start.line,
|
|
75
|
+
column: f.location.range.start.column,
|
|
76
|
+
priority: f.priority,
|
|
77
|
+
checkTitle: f.checkTitle,
|
|
78
|
+
messageTitle: f.messageTitle,
|
|
79
|
+
docUri: f.link?.href,
|
|
80
|
+
exempted: !!f.exemptionKind,
|
|
81
|
+
});
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
findings.sort((a, b) => a.priority - b.priority || a.objectName.localeCompare(b.objectName) || a.line - b.line);
|
|
85
|
+
findings.forEach((f, i) => (f.n = i + 1));
|
|
86
|
+
return {
|
|
87
|
+
variant,
|
|
88
|
+
scope: Array.isArray(uri) ? `${uri.length} objetos` : uri,
|
|
89
|
+
at: new Date().toISOString(),
|
|
90
|
+
stats: statsInfo && [p1, p2, p3].every(Number.isFinite) ? { p1, p2, p3 } : undefined,
|
|
91
|
+
findings,
|
|
92
|
+
};
|
|
93
|
+
}
|
|
94
|
+
//# sourceMappingURL=atc.js.map
|
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
import { createHash } from "node:crypto";
|
|
2
|
+
import { appendFileSync, existsSync, mkdirSync, readFileSync } from "node:fs";
|
|
3
|
+
import { join } from "node:path";
|
|
4
|
+
import { stateDir } from "./telemetry.js";
|
|
5
|
+
/**
|
|
6
|
+
* Registro de auditoría de todo lo que modifica o ejecuta en SAP. Solo
|
|
7
|
+
* añade líneas y cada una lleva el hash de la anterior: borrar o retocar una
|
|
8
|
+
* entrada rompe la cadena y `npm run audit:verify` lo detecta.
|
|
9
|
+
*
|
|
10
|
+
* Nunca guarda fuente, textos ni datos: de los argumentos largos o
|
|
11
|
+
* compuestos queda solo su huella (sha256 y tamaño), suficiente para probar
|
|
12
|
+
* qué se escribió comparando con la versión en SAP.
|
|
13
|
+
*/
|
|
14
|
+
export const AUDIT_FILE = "audit.jsonl";
|
|
15
|
+
const GENESIS = "0".repeat(64);
|
|
16
|
+
const INLINE_MAX = 120;
|
|
17
|
+
/** E/S del registro, sustituible en tests para simular disco lleno o permisos. */
|
|
18
|
+
export const auditIo = { appendFileSync, existsSync, mkdirSync, readFileSync };
|
|
19
|
+
const sha256 = (s) => createHash("sha256").update(s).digest("hex");
|
|
20
|
+
/** Argumentos aptos para el registro: valores cortos tal cual, el resto como huella. */
|
|
21
|
+
export function auditArgs(args) {
|
|
22
|
+
const out = {};
|
|
23
|
+
for (const [k, v] of Object.entries(args)) {
|
|
24
|
+
if (v === undefined)
|
|
25
|
+
continue;
|
|
26
|
+
if (typeof v === "string" && v.length > INLINE_MAX) {
|
|
27
|
+
out[k] = { sha256: sha256(v), chars: v.length, lines: v.split("\n").length };
|
|
28
|
+
}
|
|
29
|
+
else if (v !== null && typeof v === "object") {
|
|
30
|
+
const json = JSON.stringify(v);
|
|
31
|
+
out[k] = { sha256: sha256(json), items: Array.isArray(v) ? v.length : Object.keys(v).length };
|
|
32
|
+
}
|
|
33
|
+
else {
|
|
34
|
+
out[k] = v;
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
return out;
|
|
38
|
+
}
|
|
39
|
+
function entryHash(e) {
|
|
40
|
+
return sha256(JSON.stringify(e));
|
|
41
|
+
}
|
|
42
|
+
function lastEntry(path) {
|
|
43
|
+
if (!auditIo.existsSync(path))
|
|
44
|
+
return { seq: 0, hash: GENESIS };
|
|
45
|
+
const lines = auditIo.readFileSync(path, "utf8").trimEnd().split("\n").filter(Boolean);
|
|
46
|
+
if (!lines.length)
|
|
47
|
+
return { seq: 0, hash: GENESIS };
|
|
48
|
+
const last = JSON.parse(lines[lines.length - 1]);
|
|
49
|
+
return { seq: last.seq, hash: last.hash };
|
|
50
|
+
}
|
|
51
|
+
/**
|
|
52
|
+
* Añade una entrada. Lanza si no puede escribir: quien llama decide no
|
|
53
|
+
* seguir (una escritura sin rastro no se hace).
|
|
54
|
+
*/
|
|
55
|
+
export function appendAudit(input, dir = stateDir()) {
|
|
56
|
+
auditIo.mkdirSync(dir, { recursive: true, mode: 0o700 });
|
|
57
|
+
const path = join(dir, AUDIT_FILE);
|
|
58
|
+
const prev = lastEntry(path);
|
|
59
|
+
const base = {
|
|
60
|
+
seq: prev.seq + 1,
|
|
61
|
+
ts: new Date().toISOString(),
|
|
62
|
+
...input,
|
|
63
|
+
args: auditArgs(input.args),
|
|
64
|
+
prev: prev.hash,
|
|
65
|
+
};
|
|
66
|
+
const entry = { ...base, hash: entryHash(base) };
|
|
67
|
+
auditIo.appendFileSync(path, JSON.stringify(entry) + "\n", { mode: 0o600 });
|
|
68
|
+
return entry;
|
|
69
|
+
}
|
|
70
|
+
/** Recorre la cadena completa: secuencia, enlace con la anterior y hash propio. */
|
|
71
|
+
export function verifyAudit(text) {
|
|
72
|
+
const lines = text.split("\n").filter(Boolean);
|
|
73
|
+
let prev = GENESIS;
|
|
74
|
+
for (let i = 0; i < lines.length; i++) {
|
|
75
|
+
let e;
|
|
76
|
+
try {
|
|
77
|
+
e = JSON.parse(lines[i]);
|
|
78
|
+
}
|
|
79
|
+
catch {
|
|
80
|
+
return { ok: false, entries: lines.length, brokenAt: i + 1, reason: "línea que no es JSON" };
|
|
81
|
+
}
|
|
82
|
+
const { hash, ...rest } = e;
|
|
83
|
+
if (e.seq !== i + 1)
|
|
84
|
+
return { ok: false, entries: lines.length, brokenAt: i + 1, reason: `secuencia ${e.seq}, se esperaba ${i + 1}` };
|
|
85
|
+
if (e.prev !== prev)
|
|
86
|
+
return { ok: false, entries: lines.length, brokenAt: i + 1, reason: "no enlaza con la entrada anterior" };
|
|
87
|
+
if (entryHash(rest) !== hash)
|
|
88
|
+
return { ok: false, entries: lines.length, brokenAt: i + 1, reason: "contenido alterado" };
|
|
89
|
+
prev = hash;
|
|
90
|
+
}
|
|
91
|
+
return { ok: true, entries: lines.length };
|
|
92
|
+
}
|
|
93
|
+
//# sourceMappingURL=audit.js.map
|
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* English texts for the generated blocks of README.md. The tool descriptions the
|
|
3
|
+
* model reads stay in the code (Spanish); this is only the public summary. A test
|
|
4
|
+
* requires every catalogued tool, group and prompt to have its English entry.
|
|
5
|
+
*/
|
|
6
|
+
export const GROUPS_EN = {
|
|
7
|
+
revision: { title: "Code review and transports", pitch: "Know what a transport really changes and what it may break, before releasing it." },
|
|
8
|
+
calidad: { title: "Quality, ATC and remediation", pitch: "Find, understand and fix findings with SAP's real syntax check and quick fixes." },
|
|
9
|
+
exploracion: { title: "Repository exploration", pitch: "Read and understand any ABAP object and its relations, on ECC and S/4HANA." },
|
|
10
|
+
datos: { title: "Data queries", pitch: "Query tables with ABAP SQL, read-only, with sensitive and personal data protected." },
|
|
11
|
+
diagnostico: { title: "Incident diagnosis", pitch: "One conversation for what used to take ST22, SM37, SLG1 and /IWFND/ERROR_LOG." },
|
|
12
|
+
documentacion: { title: "SAP documentation", pitch: "Answer from official documentation and check which syntax exists in each release." },
|
|
13
|
+
escritura: { title: "Controlled writes", pitch: "Save changes only in development, in the right transport, previewed and confirmed by a human." },
|
|
14
|
+
"transport-risk": { title: "DoZimple Transport Risk", pitch: "Decide whether a whole release can go to QA or production, with the why in business terms." },
|
|
15
|
+
operacion: { title: "Operations and growth", pitch: "See what works on each system and decide the next tool with data." },
|
|
16
|
+
};
|
|
17
|
+
export const TOOLS_EN = {
|
|
18
|
+
transport_diff: "**What a transport changed (code diff).** Code review of a transport: for each source object (programs, includes, classes, interfaces, function modules, CDS) compares the version recorded with that transport against the previous one and shows a unified diff.",
|
|
19
|
+
transport_contents: "**Transport contents.** Header, tasks (owner and status) and objects of a transport request, read from E070/E07T/E071.",
|
|
20
|
+
co_change: "**What usually travels with this object?** Looks at the transports that touched an object and counts which other objects travelled with it, most frequent first.",
|
|
21
|
+
inactive_objects: "**Inactive objects.** Objects saved but not activated by the connection user, with their transport.",
|
|
22
|
+
edit_preflight: "**Before editing: which transport will it land in?** Tells, BEFORE changing an object, which transport the change will end up in and why: CTS lock by another transport, local object ($TMP), repair (different original system), or free with your open transports.",
|
|
23
|
+
run_atc: "**Run ATC.** Runs the ABAP Test Cockpit on an object or a transport and lists numbered findings (priority, line, check, message) with SAP's P1/P2/P3 totals.",
|
|
24
|
+
atc_quickfix: "**SAP-proposed fixes.** The fixes SAP offers (the same as Ctrl+1 in Eclipse) for an ATC finding or a line: create text symbol, extract constant, etc.",
|
|
25
|
+
api_release_state: "**Is this API released? What is its successor?** Release state of an SAP object (class, function module/BAPI, table, CDS…) by contract C0–C4 and its released successor, read from the system itself.",
|
|
26
|
+
syntax_check: "**SAP syntax check.** SAP's real syntax check (not abaplint), also on code not saved yet.",
|
|
27
|
+
run_unit_tests: "**Run ABAP Unit.** Runs the ABAP Unit tests of a class or program (harmless and short only) and returns the result per method, with each failure in detail.",
|
|
28
|
+
search_objects: "**Search ABAP objects.** Searches repository objects by name (supports * wildcards).",
|
|
29
|
+
get_source: "**Read ABAP source.** Reads the source of any object: program, include, class (and its includes), interface, function module (without knowing its group), CDS, table/structure, etc.",
|
|
30
|
+
where_used: "**Where used.** Where-used list of an object: who references it, with package and owner.",
|
|
31
|
+
object_versions: "**Object versions.** Version history of an object (date, author, transport).",
|
|
32
|
+
package_contents: "**Package contents.** Objects of a development package grouped by type, with subpackages (TADIR/TDEVC, any release).",
|
|
33
|
+
ddic_type_info: "**Data element, domain or table type.** Definition of a DDIC type: data element (domain, type, length, texts), domain (type, length, fixed values, value table) or table type (line type, key).",
|
|
34
|
+
function_modules: "**Function modules of a group.** Lists the function modules of a function group with their text and whether they are RFC or update modules; given a module, finds its group and siblings. Works with /XXX/ namespaces.",
|
|
35
|
+
transaction_info: "**What a transaction runs.** Program, screen and parameters of a transaction (TSTC/TSTCP), with its text.",
|
|
36
|
+
text_elements: "**Text symbols and selection texts.** Reads the text symbols (TEXT-001…), selection texts or headings of a program, class or function group.",
|
|
37
|
+
sql_query: "**ABAP SQL query.** Runs an ABAP SQL SELECT (WHERE, JOIN, ORDER BY, subqueries) through ADT data preview, with personal columns masked and row caps on production-data systems.",
|
|
38
|
+
table_contents: "**Table contents.** Rows of a table, view or CDS, with optional columns and filter.",
|
|
39
|
+
dumps: "**Dumps (ST22).** Lists runtime dumps: date, error, program, user and short text.",
|
|
40
|
+
jobs: "**Background jobs (SM37).** Background jobs by name (supports *), user, status and date, with their steps (program and variant).",
|
|
41
|
+
application_log: "**Application log (SLG1) headers.** Application log headers (BALHDR) by object/subobject, external number, user and date, with error and warning counts.",
|
|
42
|
+
gateway_errors: "**SAP Gateway errors (/IWFND/ERROR_LOG).** Lists SAP Gateway (OData) errors: service, error, user, date.",
|
|
43
|
+
abap_feature_matrix: "**Since which release does this syntax exist?** Availability of each ABAP language feature per release (7.40 … 7.58, 2025).",
|
|
44
|
+
docs_search: "**Search ABAP documentation.** Searches the official ABAP keyword documentation (standard and cloud), Clean ABAP, the DSAG guide, ABAP cheat sheets and RAP samples, locally.",
|
|
45
|
+
docs_fetch: "**Read a documentation page.** Returns the full content of a document by the id docs_search gives.",
|
|
46
|
+
clean_core_objects: "**Released objects catalog (Clean Core).** Searches SAP's public catalog (abap-atc-cr-cv-s4hc, local) for released/deprecated objects by name or topic, with Clean Core level (A released … D all) and successors.",
|
|
47
|
+
clean_core_object: "**Clean Core state of an SAP object.** Release state, Clean Core level and successor of an SAP object according to the public catalog (local).",
|
|
48
|
+
abap_lint: "**abaplint on a snippet.** Runs abaplint locally (code never leaves the machine) on an ABAP snippet or source.",
|
|
49
|
+
docs_community_search: "**Search SAP Community.** Searches SAP Community (blogs and questions) by error message, class or concept.",
|
|
50
|
+
write_source: "**Save source to SAP.** Replaces the FULL source of an existing object (or class include) in the given transport, after a preview with syntax check and diff and a human confirmation.",
|
|
51
|
+
activate: "**Activate object.** Activates an object and returns SAP's messages as they are (errors with line, warnings, objects left inactive).",
|
|
52
|
+
write_text_elements: "**Create or change text symbols.** Adds or changes text symbols (or selection texts) of a program/class/group, merging with the existing ones: nothing not mentioned is deleted.",
|
|
53
|
+
create_transport: "**Create transport request.** Creates a workbench request for an object's package BEFORE the first edit, so the change lands in the ticket's transport and not in a reused task.",
|
|
54
|
+
analyze_transport_risk: "**Transport risk.** Tells whether a transport (or a release of several, comma-separated) is safe to move to QA or production: unreleased tasks, import status, dependencies that don't travel, positional access, CTS locks, import queue.",
|
|
55
|
+
import_health: "**Import health.** Health of the imports into a target (QA or production): answers “how are the releases to production going?”.",
|
|
56
|
+
failure_ranking: "**Objects that fail most on import.** Ranking of objects by import failure history in a target.",
|
|
57
|
+
change_audit: "**Change audit evidence.** Evidence for a change management audit on a target: what went in, with which ticket, initiative and origin.",
|
|
58
|
+
object_transport_history: "**Transport history of an object.** Which transports touched an object, when, and which already reached the target.",
|
|
59
|
+
remote_source: "**Source on the target.** The source of an object AS IT IS in QA or production, read through TMS (like “Retrieve remote versions”).",
|
|
60
|
+
transport_source_check: "**Transport code against the target.** Compares the code of a transport's objects with the target: objects missing there (R3.4) and version drift — signatures, fields or parameters that differ and don't travel in the transport (R3.5).",
|
|
61
|
+
sap_systems: "**SAP systems and available tools.** Lists the configured systems (role, writes, data class, modules) and, optionally, checks connectivity and which tools work on each.",
|
|
62
|
+
report_gap: "**Record a missing tool.** Records a need no tool covers (e.g. something you had to do manually in a transaction), to decide what to build next.",
|
|
63
|
+
close_gap: "**Close a recorded gap.** Marks a gap recorded with report_gap as resolved, with a note (a tool now covers it, or the note turned out to be wrong); nothing is deleted.",
|
|
64
|
+
usage_stats: "**Tool usage and gaps.** Summary of the local log: calls per tool, failure rate and type, systems, and the gaps recorded with report_gap.",
|
|
65
|
+
};
|
|
66
|
+
export const PROMPTS_EN = {
|
|
67
|
+
revisar_pase: {
|
|
68
|
+
title: "Review a transport before release",
|
|
69
|
+
description: "Full review of a transport: code, missing companions, locks and, with the risk module, its analysis.",
|
|
70
|
+
chain: "transport_contents → transport_diff → inactive_objects → co_change + edit_preflight → analyze_transport_risk (if module) → three-layer report: business, consultant, Basis",
|
|
71
|
+
},
|
|
72
|
+
remediar_atc: {
|
|
73
|
+
title: "Remediate ATC findings of an object",
|
|
74
|
+
description: "ATC → documentation and SAP note → released successor → fix → syntax → save in the transport.",
|
|
75
|
+
chain: "edit_preflight → run_atc (BEFORE) → explain + api_release_state + where_used/object_versions → CHANGE/INVESTIGATE/KEEP classification → atc_quickfix → syntax_check → write_source in the transport (with human OK) → run_atc (AFTER) and P1 reduction",
|
|
76
|
+
},
|
|
77
|
+
diagnosticar_ticket: {
|
|
78
|
+
title: "Diagnose an incident",
|
|
79
|
+
description: "Dumps, jobs, application log and Gateway errors around an incident.",
|
|
80
|
+
chain: "dumps → jobs → application_log → gateway_errors → transaction_info / get_source / object_versions / transport_contents → probable cause with evidence and what could not be checked",
|
|
81
|
+
},
|
|
82
|
+
};
|
|
83
|
+
const PHRASES_EN = [
|
|
84
|
+
[/ y contribuidores/g, " and contributors"],
|
|
85
|
+
[/ y autores de la comunidad/g, " and community authors"],
|
|
86
|
+
[/^según el repositorio$/, "per repository"],
|
|
87
|
+
[/^términos de SAP$/, "SAP terms"],
|
|
88
|
+
[/^algoritmo publicado$/, "published algorithm"],
|
|
89
|
+
];
|
|
90
|
+
export const toEn = (s) => PHRASES_EN.reduce((acc, [re, en]) => acc.replace(re, en), s);
|
|
91
|
+
export const KIND_EN = { dependencia: "dependency", datos: "data", idea: "idea", algoritmo: "algorithm" };
|
|
92
|
+
//# sourceMappingURL=catalog.en.js.map
|
|
@@ -0,0 +1,139 @@
|
|
|
1
|
+
export const CREDITS = {
|
|
2
|
+
abapAdtApi: { what: "abap-adt-api", by: "Marcello Urbani", url: "https://github.com/marcellourbani/abap-adt-api", license: "MIT", kind: "dependencia" },
|
|
3
|
+
abapFs: { what: "ABAP Remote FS (vscode_abap_remote_fs)", by: "Marcello Urbani", url: "https://github.com/marcellourbani/vscode_abap_remote_fs", license: "MIT", kind: "idea" },
|
|
4
|
+
mcpSdk: { what: "Model Context Protocol TypeScript SDK", by: "Model Context Protocol", url: "https://github.com/modelcontextprotocol/typescript-sdk", license: "MIT", kind: "dependencia" },
|
|
5
|
+
marioAdt: { what: "mcp-abap-adt", by: "mario-andreschak", url: "https://github.com/mario-andreschak/mcp-abap-adt", license: "MIT", kind: "idea" },
|
|
6
|
+
arc1: { what: "ARC-1", by: "arc-mcp (Marian Zeis y contribuidores)", url: "https://github.com/arc-mcp/arc-1", license: "MIT", kind: "idea" },
|
|
7
|
+
vsp: { what: "vibing-steampunk", by: "oisee y contribuidores", url: "https://github.com/oisee/vibing-steampunk", license: "MIT", kind: "idea" },
|
|
8
|
+
awsAccel: { what: "ABAP Accelerator for Amazon Q Developer", by: "AWS Solutions Library Samples", url: "https://github.com/aws-solutions-library-samples/guidance-for-deploying-sap-abap-accelerator-for-amazon-q-developer", license: "MIT-0", kind: "idea" },
|
|
9
|
+
sapDocsMcp: { what: "mcp-sap-docs", by: "Marian Zeis (marianfoo)", url: "https://github.com/marianfoo/mcp-sap-docs", license: "Apache-2.0", kind: "dependencia" },
|
|
10
|
+
abapDocs: { what: "ABAP Keyword Documentation", by: "SAP SE", url: "https://help.sap.com/doc/abapdocu_latest_index_htm/latest/en-US/index.htm", license: "© SAP SE", kind: "datos" },
|
|
11
|
+
cheatSheets: { what: "ABAP Cheat Sheets", by: "SAP (SAP-samples)", url: "https://github.com/SAP-samples/abap-cheat-sheets", license: "Apache-2.0", kind: "datos" },
|
|
12
|
+
cleanAbap: { what: "Clean ABAP (SAP Style Guides)", by: "SAP", url: "https://github.com/SAP/styleguides", license: "según el repositorio", kind: "datos" },
|
|
13
|
+
dsag: { what: "DSAG ABAP-Leitfaden", by: "DSAG e.V.", url: "https://github.com/marianfoo/DSAG-ABAP-Guide", license: "según el repositorio", kind: "datos" },
|
|
14
|
+
featureMatrix: { what: "ABAP Feature Matrix", by: "Software-Heroes", url: "https://software-heroes.com/en/abap-feature-matrix", license: "© Software-Heroes", kind: "datos" },
|
|
15
|
+
releasedObjects: { what: "Released objects / Cloudification Repository (abap-atc-cr-cv-s4hc)", by: "SAP", url: "https://github.com/SAP/abap-atc-cr-cv-s4hc", license: "Apache-2.0", kind: "datos" },
|
|
16
|
+
abaplint: { what: "abaplint", by: "Lars Hvam y contribuidores", url: "https://github.com/abaplint/abaplint", license: "MIT", kind: "dependencia" },
|
|
17
|
+
sapCommunity: { what: "SAP Community / SAP Help Portal", by: "SAP SE y autores de la comunidad", url: "https://community.sap.com", license: "términos de SAP", kind: "datos" },
|
|
18
|
+
myers: { what: "An O(ND) Difference Algorithm and Its Variations (1986)", by: "Eugene W. Myers", url: "https://doi.org/10.1007/BF01840446", license: "algoritmo publicado", kind: "algoritmo" },
|
|
19
|
+
zod: { what: "zod", by: "Colin McDonnell y contribuidores", url: "https://github.com/colinhacks/zod", license: "MIT", kind: "dependencia" },
|
|
20
|
+
};
|
|
21
|
+
const ADT = ["abapAdtApi"];
|
|
22
|
+
export const GROUPS = [
|
|
23
|
+
{
|
|
24
|
+
id: "revision",
|
|
25
|
+
title: "Revisión de código y pases",
|
|
26
|
+
pitch: "Saber qué cambia de verdad una orden y qué puede romper, antes de liberarla.",
|
|
27
|
+
tools: [
|
|
28
|
+
{ name: "transport_diff", credits: ["abapAdtApi", "arc1", "myers"] },
|
|
29
|
+
{ name: "transport_contents", credits: ADT },
|
|
30
|
+
{ name: "co_change", credits: ["vsp"] },
|
|
31
|
+
{ name: "inactive_objects", credits: ADT },
|
|
32
|
+
{ name: "edit_preflight", credits: ADT },
|
|
33
|
+
],
|
|
34
|
+
},
|
|
35
|
+
{
|
|
36
|
+
id: "calidad",
|
|
37
|
+
title: "Calidad, ATC y remediación",
|
|
38
|
+
pitch: "Encontrar, entender y corregir hallazgos con la sintaxis y las correcciones reales de SAP.",
|
|
39
|
+
tools: [
|
|
40
|
+
{ name: "run_atc", credits: ADT },
|
|
41
|
+
{ name: "atc_quickfix", credits: ["abapAdtApi", "arc1", "myers"] },
|
|
42
|
+
{ name: "api_release_state", credits: ["abapAdtApi", "vsp"] },
|
|
43
|
+
{ name: "syntax_check", credits: ["abapAdtApi", "abapFs"] },
|
|
44
|
+
{ name: "run_unit_tests", credits: ADT },
|
|
45
|
+
],
|
|
46
|
+
},
|
|
47
|
+
{
|
|
48
|
+
id: "exploracion",
|
|
49
|
+
title: "Exploración del repositorio",
|
|
50
|
+
pitch: "Leer y entender cualquier objeto ABAP y sus relaciones, en ECC y en S/4HANA.",
|
|
51
|
+
tools: [
|
|
52
|
+
{ name: "search_objects", credits: ["abapAdtApi", "marioAdt"] },
|
|
53
|
+
{ name: "get_source", credits: ["abapAdtApi", "marioAdt"] },
|
|
54
|
+
{ name: "where_used", credits: ADT },
|
|
55
|
+
{ name: "object_versions", credits: ADT },
|
|
56
|
+
{ name: "package_contents", credits: ["abapAdtApi", "marioAdt"] },
|
|
57
|
+
{ name: "ddic_type_info", credits: ["abapAdtApi", "marioAdt"] },
|
|
58
|
+
{ name: "transaction_info", credits: ["abapAdtApi", "marioAdt"] },
|
|
59
|
+
{ name: "function_modules", credits: ADT },
|
|
60
|
+
{ name: "text_elements", credits: ADT },
|
|
61
|
+
],
|
|
62
|
+
},
|
|
63
|
+
{
|
|
64
|
+
id: "datos",
|
|
65
|
+
title: "Consulta de datos",
|
|
66
|
+
pitch: "Preguntar a las tablas con ABAP SQL, de solo lectura y sin tocar material de credenciales.",
|
|
67
|
+
tools: [
|
|
68
|
+
{ name: "sql_query", credits: ADT },
|
|
69
|
+
{ name: "table_contents", credits: ["abapAdtApi", "marioAdt"] },
|
|
70
|
+
],
|
|
71
|
+
},
|
|
72
|
+
{
|
|
73
|
+
id: "diagnostico",
|
|
74
|
+
title: "Diagnóstico de incidentes",
|
|
75
|
+
pitch: "Reunir en una conversación lo que antes exigía ST22, SM37, SLG1 y /IWFND/ERROR_LOG.",
|
|
76
|
+
tools: [
|
|
77
|
+
{ name: "dumps", credits: ADT },
|
|
78
|
+
{ name: "jobs", credits: ["vsp"] },
|
|
79
|
+
{ name: "application_log", credits: ["vsp"] },
|
|
80
|
+
{ name: "gateway_errors", credits: ["abapAdtApi", "arc1"] },
|
|
81
|
+
],
|
|
82
|
+
},
|
|
83
|
+
{
|
|
84
|
+
id: "documentacion",
|
|
85
|
+
title: "Documentación SAP",
|
|
86
|
+
pitch: "Responder con la documentación oficial y comprobar qué sintaxis existe en cada release.",
|
|
87
|
+
tools: [
|
|
88
|
+
{ name: "abap_feature_matrix", credits: ["sapDocsMcp", "featureMatrix"] },
|
|
89
|
+
{ name: "docs_search", credits: ["sapDocsMcp", "abapDocs", "cheatSheets", "cleanAbap", "dsag"] },
|
|
90
|
+
{ name: "docs_fetch", credits: ["sapDocsMcp", "abapDocs"] },
|
|
91
|
+
{ name: "clean_core_objects", credits: ["sapDocsMcp", "releasedObjects"] },
|
|
92
|
+
{ name: "clean_core_object", credits: ["sapDocsMcp", "releasedObjects"] },
|
|
93
|
+
{ name: "abap_lint", credits: ["sapDocsMcp", "abaplint"] },
|
|
94
|
+
{ name: "docs_community_search", credits: ["sapDocsMcp", "sapCommunity"] },
|
|
95
|
+
],
|
|
96
|
+
},
|
|
97
|
+
{
|
|
98
|
+
id: "escritura",
|
|
99
|
+
title: "Escritura controlada",
|
|
100
|
+
pitch: "Guardar cambios solo en desarrollo, en la orden correcta y con la sintaxis verificada antes.",
|
|
101
|
+
tools: [
|
|
102
|
+
{ name: "write_source", credits: ADT },
|
|
103
|
+
{ name: "activate", credits: ADT },
|
|
104
|
+
{ name: "write_text_elements", credits: ADT },
|
|
105
|
+
{ name: "create_transport", credits: ADT },
|
|
106
|
+
],
|
|
107
|
+
},
|
|
108
|
+
{
|
|
109
|
+
id: "transport-risk",
|
|
110
|
+
title: "DoZimple Transport Risk",
|
|
111
|
+
pitch: "Decidir si un pase entero puede ir a calidad o productivo, con el porqué en lenguaje de negocio.",
|
|
112
|
+
tools: [
|
|
113
|
+
{ name: "analyze_transport_risk", credits: [] },
|
|
114
|
+
{ name: "import_health", credits: [] },
|
|
115
|
+
{ name: "failure_ranking", credits: [] },
|
|
116
|
+
{ name: "change_audit", credits: [] },
|
|
117
|
+
{ name: "object_transport_history", credits: [] },
|
|
118
|
+
{ name: "remote_source", credits: [] },
|
|
119
|
+
{ name: "transport_source_check", credits: [] },
|
|
120
|
+
],
|
|
121
|
+
},
|
|
122
|
+
{
|
|
123
|
+
id: "operacion",
|
|
124
|
+
title: "Operación y crecimiento",
|
|
125
|
+
pitch: "Ver qué funciona en cada sistema y decidir con datos cuál es la siguiente tool.",
|
|
126
|
+
tools: [
|
|
127
|
+
{ name: "sap_systems", credits: ADT },
|
|
128
|
+
{ name: "report_gap", credits: [] },
|
|
129
|
+
{ name: "close_gap", credits: [] },
|
|
130
|
+
{ name: "usage_stats", credits: ["awsAccel"] },
|
|
131
|
+
],
|
|
132
|
+
},
|
|
133
|
+
];
|
|
134
|
+
/** Créditos del núcleo (todas las tools se apoyan en ellos). */
|
|
135
|
+
export const CORE_CREDITS = ["mcpSdk", "zod", "abapAdtApi", "awsAccel"];
|
|
136
|
+
export function groupOf(tool) {
|
|
137
|
+
return GROUPS.find((g) => g.tools.some((t) => t.name === tool));
|
|
138
|
+
}
|
|
139
|
+
//# sourceMappingURL=catalog.js.map
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Chequeo de sintaxis de SAP (no abaplint) sobre un contenido dado, esté
|
|
3
|
+
* guardado o no. Para includes se pasa el programa principal como contexto.
|
|
4
|
+
*/
|
|
5
|
+
export async function syntaxCheck(c, obj, srcUrl, content, mainProgram) {
|
|
6
|
+
let context = mainProgram;
|
|
7
|
+
if (!context && obj.type.startsWith("PROG/I")) {
|
|
8
|
+
const mains = await c.mainPrograms(obj.uri).catch(() => []);
|
|
9
|
+
context = mains[0]?.["adtcore:uri"];
|
|
10
|
+
}
|
|
11
|
+
return c.syntaxCheck(obj.uri, srcUrl, content, context ?? "");
|
|
12
|
+
}
|
|
13
|
+
export const isError = (m) => ["E", "A", "X"].includes(m.severity?.toUpperCase());
|
|
14
|
+
export function renderSyntax(msgs) {
|
|
15
|
+
return msgs
|
|
16
|
+
.map((m) => ` ${m.severity} L${m.line}${m.offset ? `:${m.offset}` : ""} ${m.text}${m.uri && !/source\/main$/.test(m.uri) ? ` [${m.uri}]` : ""}`)
|
|
17
|
+
.join("\n");
|
|
18
|
+
}
|
|
19
|
+
//# sourceMappingURL=checks.js.map
|
|
@@ -0,0 +1,136 @@
|
|
|
1
|
+
import { existsSync, readFileSync, statSync } from "node:fs";
|
|
2
|
+
import { homedir } from "node:os";
|
|
3
|
+
import { join, resolve } from "node:path";
|
|
4
|
+
import { z } from "zod";
|
|
5
|
+
/**
|
|
6
|
+
* Sistemas SAP a los que puede hablar el servidor.
|
|
7
|
+
*
|
|
8
|
+
* El archivo NO lleva contraseñas: `password` solo dice de dónde sacarla
|
|
9
|
+
* (llavero de macOS por defecto). Así el archivo se puede versionar o
|
|
10
|
+
* compartir sin revisar nada.
|
|
11
|
+
*/
|
|
12
|
+
const SystemSchema = z.object({
|
|
13
|
+
id: z.string().regex(/^[A-Za-z0-9_]+$/, "id: solo letras, números y _"),
|
|
14
|
+
description: z.string().optional(),
|
|
15
|
+
sid: z.string().regex(/^[A-Z0-9]{3}$/).optional(),
|
|
16
|
+
url: z.string().url(),
|
|
17
|
+
client: z.string().regex(/^\d{3}$/),
|
|
18
|
+
user: z.string().min(1),
|
|
19
|
+
language: z.string().length(2).default("ES"),
|
|
20
|
+
role: z.enum(["DEV", "QAS", "PRD"]),
|
|
21
|
+
/** Solo tiene efecto si role es DEV: QAS y PRD nunca se escriben. */
|
|
22
|
+
allowWrite: z.boolean().default(false),
|
|
23
|
+
/** Módulos de cliente habilitados en este sistema, p. ej. "dz-transport-risk". */
|
|
24
|
+
modules: z.array(z.string()).default([]),
|
|
25
|
+
/** Certificado (PEM) de la CA o del servidor, para verificar TLS en sistemas con certificado propio. */
|
|
26
|
+
caFile: z.string().optional(),
|
|
27
|
+
/** Último recurso: desactiva la verificación TLS. Preferir caFile. */
|
|
28
|
+
allowSelfSigned: z.boolean().default(false),
|
|
29
|
+
/**
|
|
30
|
+
* Qué datos hay en el sistema: test (ficticios), masked (anonimizados) o
|
|
31
|
+
* prod (reales). Por defecto DEV = test y QAS/PRD = prod. En masked y prod
|
|
32
|
+
* se ocultan columnas personales y se limita el número de filas.
|
|
33
|
+
*/
|
|
34
|
+
dataClass: z.enum(["test", "masked", "prod"]).optional(),
|
|
35
|
+
/** Tope de filas por consulta; por defecto test 5000, masked 1000, prod 200. */
|
|
36
|
+
maxRows: z.number().int().min(1).max(5000).optional(),
|
|
37
|
+
password: z
|
|
38
|
+
.string()
|
|
39
|
+
.regex(/^(keychain|env:[A-Z0-9_]+)$/, 'password: "keychain" o "env:NOMBRE_VARIABLE"')
|
|
40
|
+
.default("keychain"),
|
|
41
|
+
});
|
|
42
|
+
/**
|
|
43
|
+
* MCP de terceros que corren como proceso hijo aislado y cuyas tools publica
|
|
44
|
+
* este servidor (con su política). Hoy: "docs" = mcp-sap-docs.
|
|
45
|
+
*/
|
|
46
|
+
const SidecarSchema = z.object({
|
|
47
|
+
command: z.string().min(1),
|
|
48
|
+
args: z.array(z.string()).default([]),
|
|
49
|
+
/** Si false (por defecto), ninguna consulta del usuario sale a internet. */
|
|
50
|
+
allowOnline: z.boolean().default(false),
|
|
51
|
+
/** Términos que nunca pueden salir en una consulta online (clientes, proyectos…). */
|
|
52
|
+
blockTerms: z.array(z.string()).default([]),
|
|
53
|
+
});
|
|
54
|
+
const ConfigSchema = z.object({
|
|
55
|
+
defaultSystem: z.string().optional(),
|
|
56
|
+
systems: z.array(SystemSchema).min(1),
|
|
57
|
+
sidecars: z.record(SidecarSchema).default({}),
|
|
58
|
+
});
|
|
59
|
+
export function configPath() {
|
|
60
|
+
return process.env.ABAP_DZ_CONFIG
|
|
61
|
+
? resolve(process.env.ABAP_DZ_CONFIG)
|
|
62
|
+
: join(homedir(), ".config", "abap-adt-dozimple", "systems.json");
|
|
63
|
+
}
|
|
64
|
+
export function parseConfig(raw) {
|
|
65
|
+
const cfg = ConfigSchema.parse(raw);
|
|
66
|
+
const ids = new Set();
|
|
67
|
+
for (const s of cfg.systems) {
|
|
68
|
+
const key = s.id.toUpperCase();
|
|
69
|
+
if (ids.has(key))
|
|
70
|
+
throw new Error(`Sistema duplicado en la configuración: ${s.id}`);
|
|
71
|
+
ids.add(key);
|
|
72
|
+
}
|
|
73
|
+
if (cfg.defaultSystem && !ids.has(cfg.defaultSystem.toUpperCase())) {
|
|
74
|
+
throw new Error(`defaultSystem "${cfg.defaultSystem}" no está en systems[]`);
|
|
75
|
+
}
|
|
76
|
+
return cfg;
|
|
77
|
+
}
|
|
78
|
+
export function loadConfig(path = configPath()) {
|
|
79
|
+
if (!existsSync(path)) {
|
|
80
|
+
throw new Error(`No existe la configuración de sistemas en ${path}. ` +
|
|
81
|
+
`Copia config/systems.example.json ahí (o define ABAP_DZ_CONFIG).`);
|
|
82
|
+
}
|
|
83
|
+
assertPrivateFile(path);
|
|
84
|
+
return parseConfig(JSON.parse(readFileSync(path, "utf8")));
|
|
85
|
+
}
|
|
86
|
+
/**
|
|
87
|
+
* Quien pueda escribir systems.json decide a qué sistema se conecta el
|
|
88
|
+
* servidor y si puede escribir en él: tiene que ser solo del usuario.
|
|
89
|
+
*/
|
|
90
|
+
export function assertPrivateFile(path, st = statSync(path)) {
|
|
91
|
+
if (process.platform === "win32")
|
|
92
|
+
return;
|
|
93
|
+
if (st.mode & 0o022) {
|
|
94
|
+
throw new Error(`${path} lo pueden modificar otros usuarios (permisos ${(st.mode & 0o777).toString(8)}). Corrígelo con: chmod 600 "${path}"`);
|
|
95
|
+
}
|
|
96
|
+
const uid = process.getuid?.();
|
|
97
|
+
if (uid !== undefined && st.uid !== uid) {
|
|
98
|
+
throw new Error(`${path} pertenece a otro usuario (uid ${st.uid}). Debe ser tuyo y con permisos 600.`);
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
/** Escritura efectiva: hace falta allowWrite y ser un sistema de desarrollo. */
|
|
102
|
+
export function canWrite(s) {
|
|
103
|
+
return s.allowWrite && s.role === "DEV";
|
|
104
|
+
}
|
|
105
|
+
/**
|
|
106
|
+
* Qué sistema usar, y por qué. Prioridad: el parámetro de la tool, luego
|
|
107
|
+
* defaultSystem (o ABAP_DZ_DEFAULT_SYSTEM). Si hay varios y ninguno está
|
|
108
|
+
* indicado, se pide en vez de adivinar.
|
|
109
|
+
*/
|
|
110
|
+
export function resolveSystem(cfg, requested) {
|
|
111
|
+
const find = (id) => cfg.systems.find((s) => s.id.toUpperCase() === id.trim().toUpperCase());
|
|
112
|
+
if (requested && requested.trim()) {
|
|
113
|
+
const s = find(requested);
|
|
114
|
+
if (!s) {
|
|
115
|
+
throw new Error(`Sistema "${requested}" no configurado. Disponibles: ${cfg.systems.map((x) => x.id).join(", ")}`);
|
|
116
|
+
}
|
|
117
|
+
return { system: s, source: "parámetro" };
|
|
118
|
+
}
|
|
119
|
+
const fromEnv = process.env.ABAP_DZ_DEFAULT_SYSTEM;
|
|
120
|
+
if (fromEnv) {
|
|
121
|
+
const s = find(fromEnv);
|
|
122
|
+
// Una variable de entorno que apunta a un sistema que no existe es un error, no un «siguiente candidato».
|
|
123
|
+
if (!s)
|
|
124
|
+
throw new Error(`ABAP_DZ_DEFAULT_SYSTEM="${fromEnv}" no está configurado. Disponibles: ${cfg.systems.map((x) => x.id).join(", ")}`);
|
|
125
|
+
return { system: s, source: "por defecto: ABAP_DZ_DEFAULT_SYSTEM" };
|
|
126
|
+
}
|
|
127
|
+
if (cfg.defaultSystem) {
|
|
128
|
+
const s = find(cfg.defaultSystem);
|
|
129
|
+
if (s)
|
|
130
|
+
return { system: s, source: "por defecto" };
|
|
131
|
+
}
|
|
132
|
+
if (cfg.systems.length === 1)
|
|
133
|
+
return { system: cfg.systems[0], source: "único configurado" };
|
|
134
|
+
throw new Error(`Hay varios sistemas y ninguno por defecto: indica "system" (${cfg.systems.map((x) => x.id).join(", ")})`);
|
|
135
|
+
}
|
|
136
|
+
//# sourceMappingURL=config.js.map
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
import { createHash, randomBytes } from "node:crypto";
|
|
2
|
+
/**
|
|
3
|
+
* Confirmación en dos fases de las escrituras cuando el cliente MCP no
|
|
4
|
+
* soporta elicitación: la primera llamada devuelve la vista previa y un token
|
|
5
|
+
* de un solo uso atado a tool + sistema + argumentos exactos. Si el modelo
|
|
6
|
+
* cambia una coma de la fuente entre la vista previa y la escritura, el token
|
|
7
|
+
* deja de valer y hay que volver a previsualizar.
|
|
8
|
+
*/
|
|
9
|
+
export const CONFIRM_TTL_MS = 10 * 60_000;
|
|
10
|
+
const pending = new Map();
|
|
11
|
+
function canonical(v) {
|
|
12
|
+
if (Array.isArray(v))
|
|
13
|
+
return v.map(canonical);
|
|
14
|
+
if (v && typeof v === "object") {
|
|
15
|
+
return Object.fromEntries(Object.keys(v)
|
|
16
|
+
.sort()
|
|
17
|
+
.filter((k) => v[k] !== undefined)
|
|
18
|
+
.map((k) => [k, canonical(v[k])]));
|
|
19
|
+
}
|
|
20
|
+
return v;
|
|
21
|
+
}
|
|
22
|
+
export function argsDigest(tool, system, args) {
|
|
23
|
+
return createHash("sha256").update(JSON.stringify([tool, system.toUpperCase(), canonical(args)])).digest("hex");
|
|
24
|
+
}
|
|
25
|
+
export function issueToken(tool, system, args, now = Date.now()) {
|
|
26
|
+
for (const [t, p] of pending)
|
|
27
|
+
if (p.exp <= now)
|
|
28
|
+
pending.delete(t);
|
|
29
|
+
const token = randomBytes(16).toString("hex");
|
|
30
|
+
pending.set(token, { digest: argsDigest(tool, system, args), exp: now + CONFIRM_TTL_MS });
|
|
31
|
+
return token;
|
|
32
|
+
}
|
|
33
|
+
/** Consume el token: vale una sola vez, también cuando no coincide. */
|
|
34
|
+
export function consumeToken(token, tool, system, args, now = Date.now()) {
|
|
35
|
+
const p = pending.get(token);
|
|
36
|
+
if (!p)
|
|
37
|
+
return "unknown";
|
|
38
|
+
pending.delete(token);
|
|
39
|
+
if (p.exp <= now)
|
|
40
|
+
return "expired";
|
|
41
|
+
return p.digest === argsDigest(tool, system, args) ? "ok" : "mismatch";
|
|
42
|
+
}
|
|
43
|
+
//# sourceMappingURL=confirm.js.map
|