@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.
Files changed (75) hide show
  1. package/CHANGELOG.md +75 -0
  2. package/LICENSE +202 -0
  3. package/NOTICE +19 -0
  4. package/README.es.md +356 -0
  5. package/README.md +356 -0
  6. package/SECURITY.md +68 -0
  7. package/THIRD_PARTY_NOTICES.md +134 -0
  8. package/config/systems.example.json +39 -0
  9. package/dist/core/activation.js +18 -0
  10. package/dist/core/atc.js +94 -0
  11. package/dist/core/audit.js +93 -0
  12. package/dist/core/catalog.en.js +92 -0
  13. package/dist/core/catalog.js +139 -0
  14. package/dist/core/checks.js +19 -0
  15. package/dist/core/config.js +136 -0
  16. package/dist/core/confirm.js +43 -0
  17. package/dist/core/connection.js +159 -0
  18. package/dist/core/credentials.js +44 -0
  19. package/dist/core/datapolicy.js +175 -0
  20. package/dist/core/diff.js +115 -0
  21. package/dist/core/edits.js +31 -0
  22. package/dist/core/errors.js +68 -0
  23. package/dist/core/feeds.js +60 -0
  24. package/dist/core/notes.js +18 -0
  25. package/dist/core/objects.js +143 -0
  26. package/dist/core/output.js +27 -0
  27. package/dist/core/policy.js +126 -0
  28. package/dist/core/prompts.js +89 -0
  29. package/dist/core/registry.js +316 -0
  30. package/dist/core/revisions.js +97 -0
  31. package/dist/core/sidecar.js +85 -0
  32. package/dist/core/sql.js +54 -0
  33. package/dist/core/telemetry.js +42 -0
  34. package/dist/core/tool.js +5 -0
  35. package/dist/core/transport.js +109 -0
  36. package/dist/index.js +55 -0
  37. package/dist/scripts/audit-verify.js +19 -0
  38. package/dist/scripts/smoke.js +87 -0
  39. package/dist/tools/core/activate.js +27 -0
  40. package/dist/tools/core/api_release_state.js +58 -0
  41. package/dist/tools/core/atc_quickfix.js +159 -0
  42. package/dist/tools/core/co_change.js +73 -0
  43. package/dist/tools/core/create_transport.js +44 -0
  44. package/dist/tools/core/ddic_type_info.js +81 -0
  45. package/dist/tools/core/dumps.js +55 -0
  46. package/dist/tools/core/edit_preflight.js +112 -0
  47. package/dist/tools/core/function_modules.js +75 -0
  48. package/dist/tools/core/gateway_errors.js +45 -0
  49. package/dist/tools/core/get_source.js +70 -0
  50. package/dist/tools/core/inactive_objects.js +29 -0
  51. package/dist/tools/core/object_versions.js +35 -0
  52. package/dist/tools/core/package_contents.js +59 -0
  53. package/dist/tools/core/run_atc.js +117 -0
  54. package/dist/tools/core/run_unit_tests.js +60 -0
  55. package/dist/tools/core/search_objects.js +26 -0
  56. package/dist/tools/core/sql_query.js +25 -0
  57. package/dist/tools/core/support.js +95 -0
  58. package/dist/tools/core/syntax_check.js +35 -0
  59. package/dist/tools/core/table_contents.js +57 -0
  60. package/dist/tools/core/text_elements.js +108 -0
  61. package/dist/tools/core/transaction_info.js +43 -0
  62. package/dist/tools/core/transport_contents.js +49 -0
  63. package/dist/tools/core/transport_diff.js +111 -0
  64. package/dist/tools/core/where_used.js +37 -0
  65. package/dist/tools/core/write_source.js +95 -0
  66. package/dist/tools/docs/docs.js +159 -0
  67. package/dist/tools/local/growth.js +104 -0
  68. package/dist/tools/local/sap_systems.js +65 -0
  69. package/dist/tools/transport-risk/_service.js +63 -0
  70. package/dist/tools/transport-risk/risk_extras.js +90 -0
  71. package/dist/tools/transport-risk/transport_risk.js +42 -0
  72. package/docs/THREAT_MODEL.md +70 -0
  73. package/docs/TOOLS.md +980 -0
  74. package/package.json +68 -0
  75. package/scripts/set-password.sh +31 -0
@@ -0,0 +1,117 @@
1
+ import { z } from "zod";
2
+ import { defaultVariant, objectScope, recallRun, rememberRun, runAge, runAtc, transportScope } from "../../core/atc.js";
3
+ import { normalizeError, ToolError } from "../../core/errors.js";
4
+ import { decodeEntities, htmlToText } from "../../core/feeds.js";
5
+ import { sourceObjectsOf } from "../../core/revisions.js";
6
+ import { budget } from "../../core/output.js";
7
+ import { resolveByTypePrefix, resolveObject, sqlLiteral, TYPE_HELP } from "../../core/objects.js";
8
+ import { assertTrkorr } from "../../core/policy.js";
9
+ import { defineTool } from "../../core/tool.js";
10
+ const decode = decodeEntities;
11
+ const strip = htmlToText;
12
+ /** Ejecuta el ATC del alcance y lo recuerda bajo ese alcance. */
13
+ async function execute(c, sap, systemId, t, o) {
14
+ const variant = o.variant ?? (await defaultVariant(c));
15
+ let res;
16
+ try {
17
+ res = await runAtc(c, t.uri, variant, o.max_findings, o.include_exempted);
18
+ res.scope = t.scope;
19
+ }
20
+ catch (e) {
21
+ // NW 7.50 no acepta una orden como conjunto ATC («No URI-Mapping defined for URI»):
22
+ // se ejecuta sobre los objetos de la orden, en una sola corrida.
23
+ const te = normalizeError(e);
24
+ if (!t.transport || te.kind !== "SAP" || !/URI-Mapping|500/i.test(te.message))
25
+ throw te;
26
+ const tr = t.transport;
27
+ const tasks = await sap.query(`SELECT trkorr FROM e070 WHERE strkorr = ${sqlLiteral(tr)}`, 500);
28
+ const ids = [tr, ...tasks.values.map((v) => v.TRKORR)];
29
+ const rows = await sap.query(`SELECT pgmid, object, obj_name FROM e071 WHERE trkorr IN ( ${ids.map(sqlLiteral).join(", ")} )`, 5000);
30
+ const { source, ddic } = sourceObjectsOf(rows.values);
31
+ const refs = [
32
+ ...source.map((s) => ({ name: s.name, types: s.types })),
33
+ ...ddic.map((d) => ({ name: d.split(" ").slice(1).join(" "), types: [d.split(" ")[0].slice(0, 4) + "/"] })),
34
+ ];
35
+ const uris = (await Promise.all(refs.map((r) => resolveByTypePrefix(c, r.name, r.types)))).filter(Boolean).map((r) => r.uri);
36
+ if (!uris.length)
37
+ throw new ToolError("NOT_FOUND", `La orden ${tr} no contiene objetos que el ATC pueda revisar.`);
38
+ res = await runAtc(c, uris, variant, o.max_findings, o.include_exempted);
39
+ res.scope = `orden ${tr} (sus ${uris.length} objetos: este release no admite la orden como conjunto ATC)`;
40
+ }
41
+ res.variant = variant;
42
+ rememberRun(systemId, t.key, res);
43
+ return res;
44
+ }
45
+ export default defineTool({
46
+ name: "run_atc",
47
+ title: "Ejecutar ATC",
48
+ description: "Ejecuta el ATC sobre un objeto o una orden de transporte y lista los hallazgos numerados (prioridad, línea, " +
49
+ "check, mensaje), con los totales P1/P2/P3 que da SAP. El resultado queda recordado POR OBJETO U ORDEN: " +
50
+ "explain=N con object_name (o transport) trae la documentación del hallazgo N de ESE objeto (nota SAP, " +
51
+ "supresión) sin re-ejecutar si ya se analizó, y lo ejecuta si no. atc_quickfix(finding=N) usa la misma memoria.",
52
+ access: "read",
53
+ requires: { adt: ["/sap/bc/adt/atc/runs"] },
54
+ input: {
55
+ object_name: z.string().optional(),
56
+ object_type: z.string().optional().describe(TYPE_HELP),
57
+ transport: z.string().optional().describe("En vez de un objeto: todos los objetos de esta orden"),
58
+ variant: z.string().optional().describe("Variante ATC; por defecto la del sistema"),
59
+ priorities: z.array(z.number().int().min(1).max(4)).optional().describe("Filtrar la lista, p. ej. [1,2]"),
60
+ max_findings: z.number().int().min(1).max(1000).default(200),
61
+ include_exempted: z.boolean().default(false),
62
+ explain: z
63
+ .number()
64
+ .int()
65
+ .min(1)
66
+ .optional()
67
+ .describe("Documentación del hallazgo N del ATC del objeto u orden indicados (sin ellos: del último ATC, y lo dice)"),
68
+ },
69
+ async run(a, { sap, system }) {
70
+ const c = await sap.adt();
71
+ // El alcance (objeto u orden nombrados) se resuelve antes que nada: explain=N es SIEMPRE de ese alcance.
72
+ let target;
73
+ if (a.transport) {
74
+ const tr = assertTrkorr(a.transport);
75
+ target = { key: transportScope(tr), uri: `/sap/bc/adt/cts/transportrequests/${tr}`, scope: `orden ${tr}`, transport: tr };
76
+ }
77
+ else if (a.object_name) {
78
+ const obj = await resolveObject(c, a.object_name, a.object_type);
79
+ target = { key: objectScope(obj.name), uri: obj.uri, scope: `${obj.name} (${obj.type})` };
80
+ }
81
+ const opts = { variant: a.variant, max_findings: a.max_findings, include_exempted: a.include_exempted };
82
+ if (a.explain) {
83
+ let run = target ? recallRun(system.id, target.key) : recallRun(system.id);
84
+ let ranNow = false;
85
+ if (!run && target) {
86
+ run = await execute(c, sap, system.id, target, opts);
87
+ ranNow = true;
88
+ }
89
+ if (!run)
90
+ throw new ToolError("INPUT", "No hay un ATC previo en esta sesión: indica object_name o transport.");
91
+ const origin = `ATC de ${run.scope} · variante ${run.variant} · ${ranNow ? "ejecutado ahora" : runAge(run)}`;
92
+ const f = run.findings.find((x) => x.n === a.explain);
93
+ if (!f)
94
+ throw new ToolError("INPUT", `El ${origin} tiene ${run.findings.length} hallazgos: no existe el ${a.explain}.`);
95
+ if (!f.docUri)
96
+ return `${origin}\nEl hallazgo ${f.n} no trae documentación.`;
97
+ const r = await c.atcDocumentation(f.docUri);
98
+ return budget(`${origin}\nHallazgo ${f.n}: ${f.checkTitle} — ${decode(f.messageTitle)}\n${f.objectName} L${f.line}\n\n${strip(String(r.body))}`);
99
+ }
100
+ if (!target)
101
+ throw new ToolError("INPUT", "Indica object_name o transport.");
102
+ const res = await execute(c, sap, system.id, target, opts);
103
+ const shown = res.findings.filter((f) => !a.priorities || a.priorities.includes(f.priority));
104
+ const byPrio = [1, 2, 3, 4].map((p) => res.findings.filter((f) => f.priority === p).length);
105
+ const stats = res.stats ? `SAP: P1 ${res.stats.p1} · P2 ${res.stats.p2} · P3 ${res.stats.p3}` : `P1 ${byPrio[0]} · P2 ${byPrio[1]} · P3 ${byPrio[2]}`;
106
+ const capped = res.stats && res.stats.p1 + res.stats.p2 + res.stats.p3 > res.findings.length
107
+ ? `\nSe recibieron ${res.findings.length} de ${res.stats.p1 + res.stats.p2 + res.stats.p3}: sube max_findings para verlos todos.`
108
+ : "";
109
+ if (!res.findings.length)
110
+ return `ATC de ${res.scope} (variante ${res.variant}): se ejecutó y no hay hallazgos${a.include_exempted ? "" : " sin excepción"}.`;
111
+ const lines = shown.map((f) => `${String(f.n).padStart(3)}. P${f.priority} ${f.objectName} L${f.line} [${f.checkTitle}] ${decode(f.messageTitle)}${f.exempted ? " (con excepción)" : ""}`);
112
+ return (`ATC de ${res.scope} · variante ${res.variant} · ${stats}${capped}\n` +
113
+ `${a.priorities ? `Filtrado a P${a.priorities.join("/P")}: ${shown.length}\n` : ""}\n${lines.join("\n")}\n\n` +
114
+ `Siguiente: run_atc(object_name, explain=N) para la documentación del check; atc_quickfix(object_name, finding=N) para ver correcciones.`);
115
+ },
116
+ });
117
+ //# sourceMappingURL=run_atc.js.map
@@ -0,0 +1,60 @@
1
+ import { z } from "zod";
2
+ import { resolveObject, TYPE_HELP } from "../../core/objects.js";
3
+ import { defineTool } from "../../core/tool.js";
4
+ /**
5
+ * Solo tests inofensivos y cortos, explícito aunque coincida con el valor por
6
+ * defecto de abap-adt-api: un test «dangerous» o «critical» puede escribir en
7
+ * base de datos o llamar a sistemas externos.
8
+ */
9
+ export const SAFE_TEST_FLAGS = { harmless: true, dangerous: false, critical: false, short: true, medium: false, long: false };
10
+ export default defineTool({
11
+ name: "run_unit_tests",
12
+ title: "Ejecutar ABAP Unit",
13
+ description: "Ejecuta los tests ABAP Unit de una clase o programa y devuelve el resultado por método, con el detalle de cada " +
14
+ "fallo. Solo corre tests RISK LEVEL HARMLESS y DURATION SHORT. Si no hay clases de test lo dice: cero tests no es un éxito.",
15
+ access: "exec",
16
+ requires: { adt: ["/sap/bc/adt/abapunit/testruns"] },
17
+ input: {
18
+ object_name: z.string().min(1),
19
+ object_type: z.string().default("CLAS").describe(TYPE_HELP),
20
+ },
21
+ async run({ object_name, object_type }, { sap }) {
22
+ const c = await sap.adt();
23
+ const obj = await resolveObject(c, object_name, object_type);
24
+ const classes = await c.unitTestRun(obj.uri, { ...SAFE_TEST_FLAGS });
25
+ if (!classes.length)
26
+ return { text: `${obj.name}: no se encontraron clases de test. No se ejecutó nada.`, isError: true };
27
+ let pass = 0;
28
+ let fail = 0;
29
+ const lines = [];
30
+ for (const cl of classes) {
31
+ for (const a of cl.alerts)
32
+ lines.push(` ! ${cl["adtcore:name"]}: ${a.title} ${a.details.join(" ")}`);
33
+ for (const m of cl.testmethods) {
34
+ const bad = m.alerts.filter((a) => a.severity !== "tolerable" && a.kind !== "warning");
35
+ if (bad.length) {
36
+ fail++;
37
+ lines.push(` ✗ ${cl["adtcore:name"]}->${m["adtcore:name"]}`);
38
+ for (const a of bad) {
39
+ lines.push(` ${a.kind}: ${a.title}`);
40
+ for (const d of a.details)
41
+ lines.push(` ${d}`);
42
+ const top = a.stack[0];
43
+ if (top)
44
+ lines.push(` en ${top["adtcore:name"]} ${top["adtcore:uri"]?.match(/#start=(\d+)/)?.[1] ? "L" + top["adtcore:uri"].match(/#start=(\d+)/)[1] : ""}`);
45
+ }
46
+ }
47
+ else {
48
+ pass++;
49
+ lines.push(` ✓ ${cl["adtcore:name"]}->${m["adtcore:name"]} (${m.executionTime}s)`);
50
+ }
51
+ }
52
+ }
53
+ const classAlerts = classes.some((cl) => cl.alerts.length);
54
+ return {
55
+ text: `${obj.name}: ${pass} verdes, ${fail} rojos${classAlerts ? ", con alertas de clase" : ""}.\n\n${lines.join("\n")}`,
56
+ isError: fail > 0 || classAlerts,
57
+ };
58
+ },
59
+ });
60
+ //# sourceMappingURL=run_unit_tests.js.map
@@ -0,0 +1,26 @@
1
+ import { z } from "zod";
2
+ import { adtType, TYPE_HELP } from "../../core/objects.js";
3
+ import { tsv } from "../../core/output.js";
4
+ import { defineTool } from "../../core/tool.js";
5
+ export default defineTool({
6
+ name: "search_objects",
7
+ title: "Buscar objetos ABAP",
8
+ description: "Busca objetos del repositorio por nombre (admite * como comodín). Devuelve nombre, tipo, paquete y descripción. " +
9
+ "Úsala para localizar un objeto antes de leerlo o cuando no sabes el nombre exacto.",
10
+ access: "read",
11
+ input: {
12
+ query: z.string().min(1).describe("Patrón de nombre, p. ej. ZCL_SD_* o ZFI*"),
13
+ object_type: z.string().optional().describe(TYPE_HELP),
14
+ max_results: z.number().int().min(1).max(500).default(50),
15
+ },
16
+ async run({ query, object_type, max_results }, { sap }) {
17
+ const c = await sap.adt();
18
+ const hits = await c.searchObject(query.trim().toUpperCase(), adtType(object_type), max_results);
19
+ if (!hits.length)
20
+ return `La búsqueda se ejecutó y no hay objetos que casen con ${query}.`;
21
+ const table = tsv(["nombre", "tipo", "paquete", "descripción"], hits.map((h) => [h["adtcore:name"], h["adtcore:type"], h["adtcore:packageName"], h["adtcore:description"]]));
22
+ const more = hits.length >= max_results ? `\n\nTope de ${max_results} alcanzado: puede haber más.` : "";
23
+ return `${hits.length} objetos\n\n${table}${more}`;
24
+ },
25
+ });
26
+ //# sourceMappingURL=search_objects.js.map
@@ -0,0 +1,25 @@
1
+ import { z } from "zod";
2
+ import { guardedQuery } from "../../core/datapolicy.js";
3
+ import { tsv } from "../../core/output.js";
4
+ import { defineTool } from "../../core/tool.js";
5
+ export default defineTool({
6
+ name: "sql_query",
7
+ title: "Consulta ABAP SQL",
8
+ description: "Ejecuta un SELECT de ABAP SQL (con WHERE, JOIN, ORDER BY, subconsultas) vía la vista previa de datos de ADT. " +
9
+ "Solo lectura. Sintaxis ABAP SQL: literales entre comillas simples, sin «;», sin UP TO (usa max_rows). " +
10
+ "Útil para E070/E071/TADIR/TLOCK/DD03L y datos de negocio. En sistemas con datos productivos las columnas " +
11
+ "personales salen enmascaradas y hay un tope de filas por sistema.",
12
+ access: "read",
13
+ input: {
14
+ query: z.string().min(1).describe("SELECT ... FROM ... WHERE ..."),
15
+ max_rows: z.number().int().min(1).max(5000).default(100),
16
+ },
17
+ async run({ query, max_rows }, { sap, system }) {
18
+ const r = await guardedQuery(sap, system, query, max_rows);
19
+ if (!r.values.length)
20
+ return `La consulta se ejecutó y no devolvió filas.`;
21
+ const table = tsv(r.columns, r.values.map((v) => r.columns.map((c) => v[c])));
22
+ return `${r.values.length} filas\n\n${table}${r.notes.length ? `\n\n${r.notes.join("\n")}` : ""}`;
23
+ },
24
+ });
25
+ //# sourceMappingURL=sql_query.js.map
@@ -0,0 +1,95 @@
1
+ import { z } from "zod";
2
+ import { sqlLiteral } from "../../core/objects.js";
3
+ import { tsv } from "../../core/output.js";
4
+ import { defineTool } from "../../core/tool.js";
5
+ /**
6
+ * Diagnóstico de tickets sin SM37 ni SLG1: jobs y log de aplicación por SQL de
7
+ * solo lectura. Idea de vsp (MIT), reimplementada con tablas estándar.
8
+ */
9
+ const JOB_STATUS = { A: "cancelado", F: "finalizado", R: "en ejecución", S: "liberado", P: "planificado", Y: "listo", Z: "liberado/suspendido", X: "desconocido" };
10
+ const like = (p) => sqlLiteral(p.toUpperCase().replace(/\*/g, "%"));
11
+ const ymd = (d) => d.replace(/-/g, "");
12
+ const jobs = defineTool({
13
+ name: "jobs",
14
+ title: "Jobs de fondo (SM37)",
15
+ description: "Jobs de fondo por nombre (admite *), usuario, estado y fecha, con sus pasos (programa y variante). Responde " +
16
+ "«¿corrió anoche el job de X?, ¿por qué se canceló?». El log detallado del job sigue en SM37.",
17
+ access: "read",
18
+ input: {
19
+ job_name: z.string().default("*"),
20
+ user: z.string().optional(),
21
+ status: z.enum(["A", "F", "R", "S", "P", "Y"]).optional().describe("A cancelado, F finalizado, R en ejecución, S liberado, P planificado"),
22
+ from_date: z.string().optional().describe("AAAA-MM-DD; por defecto los últimos 3 días"),
23
+ max: z.number().int().min(1).max(500).default(50),
24
+ },
25
+ async run({ job_name, user, status, from_date, max }, { sap }) {
26
+ const from = ymd(from_date ?? new Date(Date.now() - 3 * 86400_000).toISOString().slice(0, 10));
27
+ const where = [`jobname LIKE ${like(job_name)}`, `( strtdate >= ${sqlLiteral(from)} OR sdlstrtdt >= ${sqlLiteral(from)} )`];
28
+ if (user)
29
+ where.push(`sdluname = ${sqlLiteral(user.toUpperCase())}`);
30
+ if (status)
31
+ where.push(`status = ${sqlLiteral(status)}`);
32
+ const r = await sap.query(`SELECT jobname, jobcount, status, sdluname, sdlstrtdt, sdlstrttm, strtdate, strttime, enddate, endtime FROM tbtco ` +
33
+ `WHERE ${where.join(" AND ")} ORDER BY strtdate DESCENDING, strttime DESCENDING, sdlstrtdt DESCENDING`, max);
34
+ if (!r.values.length)
35
+ return `No hay jobs que casen desde ${from}.`;
36
+ const keys = r.values.map((v) => `( jobname = ${sqlLiteral(v.JOBNAME)} AND jobcount = ${sqlLiteral(v.JOBCOUNT)} )`);
37
+ const steps = await sap.query(`SELECT jobname, jobcount, stepcount, progname, variant FROM tbtcp WHERE ${keys.slice(0, 40).join(" OR ")}`, 400);
38
+ const stepOf = (v) => steps.values
39
+ .filter((s) => s.JOBNAME === v.JOBNAME && s.JOBCOUNT === v.JOBCOUNT)
40
+ .map((s) => `${String(s.PROGNAME).trim()}${String(s.VARIANT).trim() ? `/${String(s.VARIANT).trim()}` : ""}`)
41
+ .join(" + ");
42
+ return (`${r.values.length} jobs desde ${from}${r.values.length >= max ? ` (tope ${max})` : ""}\n\n` +
43
+ tsv(["job", "estado", "usuario", "planificado", "inicio", "fin", "pasos"], r.values.map((v) => [
44
+ v.JOBNAME,
45
+ JOB_STATUS[v.STATUS] ?? v.STATUS,
46
+ v.SDLUNAME,
47
+ `${v.SDLSTRTDT} ${v.SDLSTRTTM}`,
48
+ v.STRTDATE && v.STRTDATE !== "00000000" ? `${v.STRTDATE} ${v.STRTTIME}` : "",
49
+ v.ENDDATE && v.ENDDATE !== "00000000" ? `${v.ENDDATE} ${v.ENDTIME}` : "",
50
+ stepOf(v),
51
+ ])));
52
+ },
53
+ });
54
+ const appLog = defineTool({
55
+ name: "application_log",
56
+ title: "Log de aplicación (SLG1), cabeceras",
57
+ description: "Cabeceras del log de aplicación (BALHDR) por objeto/subobjeto, nº externo, usuario y fecha, con el recuento de " +
58
+ "errores y avisos. Los TEXTOS de los mensajes están comprimidos (BALDAT) y no se leen por SQL: para eso, SLG1 con " +
59
+ "el nº de log que devuelve esta tool.",
60
+ access: "read",
61
+ input: {
62
+ object: z.string().optional().describe("Objeto de log (SLG0), admite *"),
63
+ subobject: z.string().optional(),
64
+ external_number: z.string().optional().describe("Nº externo, admite * (p. ej. el número de pedido)"),
65
+ user: z.string().optional(),
66
+ from_date: z.string().optional().describe("AAAA-MM-DD; por defecto los últimos 3 días"),
67
+ only_errors: z.boolean().default(false),
68
+ max: z.number().int().min(1).max(500).default(50),
69
+ },
70
+ async run(a, { sap }) {
71
+ const from = ymd(a.from_date ?? new Date(Date.now() - 3 * 86400_000).toISOString().slice(0, 10));
72
+ const where = [`aldate >= ${sqlLiteral(from)}`];
73
+ if (a.object)
74
+ where.push(`object LIKE ${like(a.object)}`);
75
+ if (a.subobject)
76
+ where.push(`subobject LIKE ${like(a.subobject)}`);
77
+ if (a.external_number)
78
+ where.push(`extnumber LIKE ${like(a.external_number)}`);
79
+ if (a.user)
80
+ where.push(`aluser = ${sqlLiteral(a.user.toUpperCase())}`);
81
+ if (a.only_errors)
82
+ where.push(`( msg_cnt_e > 0 OR msg_cnt_a > 0 )`);
83
+ const r = await sap.query(`SELECT lognumber, object, subobject, extnumber, aldate, altime, aluser, altcode, alprog, msg_cnt_al, msg_cnt_a, msg_cnt_e, msg_cnt_w ` +
84
+ `FROM balhdr WHERE ${where.join(" AND ")} ORDER BY aldate DESCENDING, altime DESCENDING`, a.max);
85
+ if (!r.values.length)
86
+ return `No hay logs que casen desde ${from}.`;
87
+ return (`${r.values.length} logs desde ${from}${r.values.length >= a.max ? ` (tope ${a.max})` : ""}\n\n` +
88
+ tsv(["log", "objeto/sub", "nº externo", "fecha", "usuario", "tx/programa", "msgs", "cancel.", "errores", "avisos"], r.values.map((v) => [
89
+ v.LOGNUMBER, `${v.OBJECT}/${v.SUBOBJECT}`, v.EXTNUMBER, `${v.ALDATE} ${v.ALTIME}`, v.ALUSER,
90
+ String(v.ALTCODE).trim() || v.ALPROG, v.MSG_CNT_AL, v.MSG_CNT_A, v.MSG_CNT_E, v.MSG_CNT_W,
91
+ ])));
92
+ },
93
+ });
94
+ export default [jobs, appLog];
95
+ //# sourceMappingURL=support.js.map
@@ -0,0 +1,35 @@
1
+ import { z } from "zod";
2
+ import { isError, renderSyntax, syntaxCheck } from "../../core/checks.js";
3
+ import { CLASS_INCLUDES, resolveObject, sourceUrl, TYPE_HELP } from "../../core/objects.js";
4
+ import { defineTool } from "../../core/tool.js";
5
+ export default defineTool({
6
+ name: "syntax_check",
7
+ title: "Chequeo de sintaxis SAP",
8
+ description: "Chequeo de sintaxis real de SAP (no abaplint). Si pasas `source`, se comprueba ESE código sin guardarlo: sirve " +
9
+ "para validar un cambio antes de escribirlo. Sin `source`, comprueba lo último guardado.",
10
+ access: "read",
11
+ requires: { adt: ["/sap/bc/adt/checkruns"] },
12
+ input: {
13
+ object_name: z.string().min(1),
14
+ object_type: z.string().optional().describe(TYPE_HELP),
15
+ include: z.enum(CLASS_INCLUDES).default("main"),
16
+ source: z.string().optional().describe("Fuente completa a comprobar (sin guardar)"),
17
+ main_program: z.string().optional().describe("URI del programa principal, solo para includes ambiguos"),
18
+ },
19
+ async run({ object_name, object_type, include, source, main_program }, { sap }) {
20
+ const c = await sap.adt();
21
+ const obj = await resolveObject(c, object_name, object_type);
22
+ const url = await sourceUrl(c, obj, include);
23
+ const content = source ?? (await c.getObjectSource(url, { version: "inactive" }));
24
+ const msgs = await syntaxCheck(c, obj, url, content, main_program);
25
+ const what = source ? "fuente propuesta (no guardada)" : "última versión guardada";
26
+ if (!msgs.length)
27
+ return `${obj.name}: sin errores ni avisos de sintaxis (${what}, comprobado por SAP).`;
28
+ const errs = msgs.filter(isError).length;
29
+ return {
30
+ text: `${obj.name}: ${errs} errores, ${msgs.length - errs} avisos (${what}).\n\n${renderSyntax(msgs)}`,
31
+ isError: errs > 0,
32
+ };
33
+ },
34
+ });
35
+ //# sourceMappingURL=syntax_check.js.map
@@ -0,0 +1,57 @@
1
+ import { z } from "zod";
2
+ import { guardedQuery } from "../../core/datapolicy.js";
3
+ import { ToolError } from "../../core/errors.js";
4
+ import { tsv } from "../../core/output.js";
5
+ import { defineTool } from "../../core/tool.js";
6
+ const NAME_RE = /^[A-Z0-9_/]+$/;
7
+ /**
8
+ * WHERE y ORDER BY se concatenan al SELECT: no pueden abrir otra consulta,
9
+ * comentar el resto ni cambiar de sentencia. Lo que haga falta más allá, con sql_query.
10
+ */
11
+ export function assertSafeFragment(frag, label) {
12
+ const outside = frag.replace(/'(?:[^']|'')*'/g, "''");
13
+ if ((frag.match(/'/g)?.length ?? 0) % 2)
14
+ throw new ToolError("INPUT", `${label}: comilla sin cerrar.`);
15
+ if (/[;"*]|--/.test(outside) || /\b(select|union|intersect|except|into|from|join|delete|update|insert|modify)\b/i.test(outside)) {
16
+ throw new ToolError("INPUT", `${label}: solo condiciones sobre columnas (sin subconsultas, comentarios ni «;»). Para eso usa sql_query.`);
17
+ }
18
+ }
19
+ export default defineTool({
20
+ name: "table_contents",
21
+ title: "Contenido de una tabla",
22
+ description: "Filas de una tabla, vista o CDS, con columnas y filtro opcionales. Atajo de sql_query para el caso típico " +
23
+ "«enséñame lo que hay en ZTABLA donde …». Para JOIN, subconsultas o agregados usa sql_query.",
24
+ access: "read",
25
+ input: {
26
+ table: z.string().min(1),
27
+ columns: z.array(z.string()).optional().describe("Columnas; por defecto todas"),
28
+ where: z.string().optional().describe("Condición ABAP SQL sin la palabra WHERE, p. ej. werks = '1000' AND lvorm = ''"),
29
+ order_by: z.string().optional(),
30
+ max_rows: z.number().int().min(1).max(5000).default(100),
31
+ },
32
+ async run({ table, columns, where, order_by, max_rows }, { sap, system }) {
33
+ const t = table.trim().toUpperCase();
34
+ if (!NAME_RE.test(t))
35
+ throw new ToolError("INPUT", `Nombre de tabla inválido: ${table}`);
36
+ const cols = columns?.map((c) => c.trim().toUpperCase()) ?? [];
37
+ for (const c of cols)
38
+ if (!NAME_RE.test(c))
39
+ throw new ToolError("INPUT", `Columna inválida: ${c}`);
40
+ if (where)
41
+ assertSafeFragment(where, "where");
42
+ if (order_by) {
43
+ assertSafeFragment(order_by, "order_by");
44
+ if (!/^[A-Za-z0-9_/~]+(\s+(asc|desc))?(\s*,\s*[A-Za-z0-9_/~]+(\s+(asc|desc))?)*$/i.test(order_by.trim())) {
45
+ throw new ToolError("INPUT", "order_by: columnas separadas por coma, con ASC/DESC opcional.");
46
+ }
47
+ }
48
+ const sql = `SELECT ${cols.length ? cols.join(", ") : "*"} FROM ${t}` +
49
+ (where ? ` WHERE ${where}` : "") +
50
+ (order_by ? ` ORDER BY ${order_by}` : "");
51
+ const r = await guardedQuery(sap, system, sql, max_rows);
52
+ if (!r.values.length)
53
+ return `${t}: la consulta se ejecutó y no devolvió filas${where ? " con ese filtro" : ""}.`;
54
+ return `${t}: ${r.values.length} filas\n\n${tsv(r.columns, r.values.map((v) => r.columns.map((n) => v[n])))}${r.notes.length ? `\n\n${r.notes.join("\n")}` : ""}`;
55
+ },
56
+ });
57
+ //# sourceMappingURL=table_contents.js.map
@@ -0,0 +1,108 @@
1
+ import { z } from "zod";
2
+ import { textElementsUrl } from "abap-adt-api";
3
+ import { ToolError } from "../../core/errors.js";
4
+ import { resolveObject, TYPE_HELP } from "../../core/objects.js";
5
+ import { assertTrkorr } from "../../core/policy.js";
6
+ import { decideTransport, orderHeaders, transportWarnings } from "../../core/transport.js";
7
+ import { defineTool } from "../../core/tool.js";
8
+ const CATEGORIES = ["symbols", "selections", "headings"];
9
+ /** Fusiona los textos pedidos con los existentes; valida longitudes. */
10
+ async function mergeTexts(c, obj, category, elements) {
11
+ const current = (await c.getTextElements(textElementsUrl(obj.type, obj.name), category)).textElements;
12
+ const merged = new Map(current.map((t) => [t.id.toUpperCase(), t]));
13
+ for (const e of elements) {
14
+ const id = e.id.toUpperCase();
15
+ const prev = merged.get(id);
16
+ const maxLength = e.max_length ?? prev?.maxLength ?? Math.max(e.text.length, 1);
17
+ if (e.text.length > maxLength)
18
+ throw new ToolError("INPUT", `El texto ${id} mide ${e.text.length} y su máximo es ${maxLength}.`);
19
+ merged.set(id, { ...prev, id, text: e.text, maxLength });
20
+ }
21
+ return { current, merged };
22
+ }
23
+ const read = defineTool({
24
+ name: "text_elements",
25
+ title: "Símbolos de texto y textos de selección",
26
+ description: "Lee los símbolos de texto (TEXT-001…), textos de selección o encabezados de un programa, clase o grupo de funciones.",
27
+ access: "read",
28
+ requires: { adt: ["/sap/bc/adt/textelements"] },
29
+ input: {
30
+ object_name: z.string().min(1),
31
+ object_type: z.string().optional().describe(TYPE_HELP),
32
+ category: z.enum(CATEGORIES).default("symbols"),
33
+ },
34
+ async run({ object_name, object_type, category }, { sap }) {
35
+ const c = await sap.adt();
36
+ const obj = await resolveObject(c, object_name, object_type);
37
+ const r = await c.getTextElements(textElementsUrl(obj.type, obj.name), category);
38
+ if (!r.textElements.length)
39
+ return `${obj.name}: no tiene ${category}.`;
40
+ return `${obj.name} · ${category} (${r.textElements.length})\n` + r.textElements.map((t) => ` ${t.id} ${t.text}${t.maxLength ? ` [máx ${t.maxLength}]` : ""}`).join("\n");
41
+ },
42
+ });
43
+ const write = defineTool({
44
+ name: "write_text_elements",
45
+ title: "Crear o cambiar símbolos de texto",
46
+ description: "Añade o modifica símbolos de texto (o textos de selección) de un programa/clase/grupo, fusionando con los " +
47
+ "existentes: no borra los que no se mencionan. Completa la corrección «Create text in text pool» de atc_quickfix. " +
48
+ "Misma regla de orden que write_source.",
49
+ access: "write",
50
+ requires: { adt: ["/sap/bc/adt/textelements"] },
51
+ input: {
52
+ object_name: z.string().min(1),
53
+ object_type: z.string().optional().describe(TYPE_HELP),
54
+ category: z.enum(CATEGORIES).default("symbols"),
55
+ elements: z.array(z.object({ id: z.string().min(1).max(8), text: z.string(), max_length: z.number().int().min(1).max(255).optional() })).min(1),
56
+ transport: z.string().optional(),
57
+ },
58
+ async preview({ object_name, object_type, category, elements, transport }, { sap, system }) {
59
+ const requested = transport ? assertTrkorr(transport) : undefined;
60
+ const c = await sap.adt();
61
+ const obj = await resolveObject(c, object_name, object_type);
62
+ const { current, merged } = await mergeTexts(c, obj, category, elements);
63
+ const before = new Map(current.map((t) => [t.id.toUpperCase(), t]));
64
+ const lines = elements.map((e) => {
65
+ const id = e.id.toUpperCase();
66
+ const prev = before.get(id);
67
+ const now = merged.get(id);
68
+ if (!prev)
69
+ return ` + ${id} «${now.text}» [máx ${now.maxLength}]`;
70
+ if (prev.text === now.text && prev.maxLength === now.maxLength)
71
+ return ` = ${id} sin cambio`;
72
+ return ` ~ ${id} «${prev.text}» → «${now.text}» [máx ${now.maxLength}]`;
73
+ });
74
+ const warn = requested ? await transportWarnings(sap, requested, system.user) : [];
75
+ return [
76
+ ...(warn.length ? [`AVISOS DE LA ORDEN ${requested}:`, ...warn.map((w) => " ⚠ " + w), ""] : []),
77
+ `${obj.name} (${obj.type}) · ${category} · orden ${requested ?? "ninguna indicada"}`,
78
+ `Quedarán ${merged.size} (hoy ${current.length}); no se borra ninguno.`,
79
+ ...lines,
80
+ ].join("\n");
81
+ },
82
+ async run({ object_name, object_type, category, elements, transport }, { sap }) {
83
+ const requested = transport ? assertTrkorr(transport) : undefined;
84
+ const c = await sap.adt();
85
+ const obj = await resolveObject(c, object_name, object_type);
86
+ const url = textElementsUrl(obj.type, obj.name);
87
+ const { merged } = await mergeTexts(c, obj, category, elements);
88
+ const res = await sap.stateful(async (s) => {
89
+ const lock = await s.lock(obj.uri);
90
+ try {
91
+ const parent = lock.CORRNR ? (await orderHeaders(sap, [lock.CORRNR])).get(lock.CORRNR)?.parent : undefined;
92
+ const d = decideTransport(lock, requested, parent);
93
+ if (!d.ok)
94
+ return d;
95
+ await s.setTextElements(url, category, [...merged.values()], lock.LOCK_HANDLE, d.corrNr || undefined);
96
+ return d;
97
+ }
98
+ finally {
99
+ await s.unLock(obj.uri, lock.LOCK_HANDLE).catch(() => undefined);
100
+ }
101
+ });
102
+ if (!res.ok)
103
+ return { text: res.reason, isError: true };
104
+ return `${obj.name}: ${elements.length} ${category} guardados (${merged.size} en total). ${res.note}\nActiva el objeto para que el cambio sea efectivo.`;
105
+ },
106
+ });
107
+ export default [read, write];
108
+ //# sourceMappingURL=text_elements.js.map
@@ -0,0 +1,43 @@
1
+ import { z } from "zod";
2
+ import { ToolError } from "../../core/errors.js";
3
+ import { sqlLiteral } from "../../core/objects.js";
4
+ import { defineTool } from "../../core/tool.js";
5
+ const LANG = { EN: "E", ES: "S", DE: "D", PT: "P", FR: "F", IT: "I" };
6
+ export default defineTool({
7
+ name: "transaction_info",
8
+ title: "Qué ejecuta una transacción",
9
+ description: "Programa, dynpro y parámetros de una transacción (TSTC/TSTCP), con su texto. Resuelve transacciones de " +
10
+ "parámetro y orientadas a objetos (clase/método). Úsala para ir de «la ZMM_01 falla» al código.",
11
+ access: "read",
12
+ input: { tcode: z.string().min(1) },
13
+ async run({ tcode }, { sap, system }) {
14
+ const t = tcode.trim().toUpperCase();
15
+ const r = await sap.query(`SELECT tcode, pgmna, dypno FROM tstc WHERE tcode = ${sqlLiteral(t)}`, 1);
16
+ const row = r.values[0];
17
+ if (!row)
18
+ throw new ToolError("NOT_FOUND", `La transacción ${t} no existe en este sistema.`);
19
+ const texts = await sap.query(`SELECT sprsl, ttext FROM tstct WHERE tcode = ${sqlLiteral(t)}`, 20);
20
+ const lang = LANG[system.language.toUpperCase()] ?? "E";
21
+ const text = (texts.values.find((v) => v.SPRSL === lang) ?? texts.values[0])?.TTEXT ?? "";
22
+ const par = await sap.query(`SELECT param FROM tstcp WHERE tcode = ${sqlLiteral(t)}`, 1);
23
+ const param = par.values[0]?.PARAM ?? "";
24
+ const lines = [`${t} «${text}»`];
25
+ if (row.PGMNA)
26
+ lines.push(` programa ${row.PGMNA}${row.DYPNO && row.DYPNO !== "0000" ? ` · dynpro ${row.DYPNO}` : ""}`);
27
+ if (param) {
28
+ // OO: \PROGRAM=…\CLASS=…\METHOD=… · parámetro: /*TCODE_BASE campo=valor;…
29
+ const oo = /\\CLASS=([^\\]+)/.exec(param);
30
+ const meth = /\\METHOD=([^\\]+)/.exec(param);
31
+ const base = /^\/\*?([A-Z0-9_/]+)/.exec(param);
32
+ if (oo)
33
+ lines.push(` transacción OO: clase ${oo[1]}${meth ? `, método ${meth[1]}` : ""}`);
34
+ else if (base)
35
+ lines.push(` transacción de parámetro sobre ${base[1]}`);
36
+ lines.push(` parámetros: ${param.trim()}`);
37
+ }
38
+ if (!row.PGMNA && !param)
39
+ lines.push(" sin programa ni parámetros registrados (¿transacción de área de menú?)");
40
+ return lines.join("\n");
41
+ },
42
+ });
43
+ //# sourceMappingURL=transaction_info.js.map
@@ -0,0 +1,49 @@
1
+ import { z } from "zod";
2
+ import { ToolError } from "../../core/errors.js";
3
+ import { sqlLiteral } from "../../core/objects.js";
4
+ import { assertTrkorr } from "../../core/policy.js";
5
+ import { describeOrder, orderHeaders } from "../../core/transport.js";
6
+ import { defineTool } from "../../core/tool.js";
7
+ export default defineTool({
8
+ name: "transport_contents",
9
+ title: "Contenido de una orden",
10
+ description: "Cabecera, tareas (con dueño y estado) y objetos de una orden de transporte, leídos de E070/E07T/E071. " +
11
+ "Funciona igual en ECC y S/4. Para el análisis de riesgo de un pase usa la tool del módulo de riesgo si existe.",
12
+ access: "read",
13
+ input: {
14
+ transport: z.string().describe("Orden o tarea, p. ej. DEVK900123"),
15
+ max_objects: z.number().int().min(1).max(5000).default(500),
16
+ },
17
+ async run({ transport, max_objects }, { sap }) {
18
+ const tr = assertTrkorr(transport);
19
+ const children = await sap.query(`SELECT trkorr FROM e070 WHERE strkorr = ${sqlLiteral(tr)}`, 500);
20
+ const ids = [tr, ...children.values.map((v) => v.TRKORR)];
21
+ const heads = await orderHeaders(sap, ids);
22
+ const head = heads.get(tr);
23
+ if (!head)
24
+ throw new ToolError("NOT_FOUND", `La orden ${tr} no existe en este sistema.`);
25
+ const inList = ids.map(sqlLiteral).join(", ");
26
+ const objs = await sap.query(`SELECT trkorr, pgmid, object, obj_name, lockflag FROM e071 WHERE trkorr IN ( ${inList} ) ORDER BY object, obj_name`, max_objects);
27
+ const lines = [describeOrder(head)];
28
+ if (head.parent)
29
+ lines.push(`Es una tarea de ${head.parent}.`);
30
+ const tasks = ids.slice(1).map((id) => heads.get(id)).filter(Boolean);
31
+ if (tasks.length)
32
+ lines.push("", "Tareas:", ...tasks.map((t) => " " + describeOrder(t)));
33
+ // Un mismo objeto suele estar en varias tareas: se agrupa.
34
+ const byObj = new Map();
35
+ for (const o of objs.values) {
36
+ const key = `${o.PGMID} ${o.OBJECT} ${o.OBJ_NAME}`;
37
+ if (!byObj.has(key))
38
+ byObj.set(key, new Set());
39
+ byObj.get(key).add(o.TRKORR);
40
+ }
41
+ lines.push("", `Objetos (${byObj.size}${objs.values.length >= max_objects ? `, tope de ${max_objects} filas alcanzado` : ""}):`);
42
+ for (const [k, where] of byObj)
43
+ lines.push(` ${k}${ids.length > 1 ? ` [${[...where].join(", ")}]` : ""}`);
44
+ if (!byObj.size)
45
+ lines.push(" (la orden no tiene objetos)");
46
+ return lines.join("\n");
47
+ },
48
+ });
49
+ //# sourceMappingURL=transport_contents.js.map