arc-1 1.0.2 → 1.1.0
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/bin/arc1-cli.js +3 -7
- package/bin/arc1.js +4 -9
- package/dist/adt/abapgit.d.ts +10 -5
- package/dist/adt/abapgit.d.ts.map +1 -1
- package/dist/adt/abapgit.js +357 -76
- package/dist/adt/abapgit.js.map +1 -1
- package/dist/adt/atc.d.ts +49 -0
- package/dist/adt/atc.d.ts.map +1 -0
- package/dist/adt/atc.js +294 -0
- package/dist/adt/atc.js.map +1 -0
- package/dist/adt/aunit-package.d.ts +22 -0
- package/dist/adt/aunit-package.d.ts.map +1 -0
- package/dist/adt/aunit-package.js +78 -0
- package/dist/adt/aunit-package.js.map +1 -0
- package/dist/adt/aunit.d.ts +143 -0
- package/dist/adt/aunit.d.ts.map +1 -0
- package/dist/adt/aunit.js +987 -0
- package/dist/adt/aunit.js.map +1 -0
- package/dist/adt/client.d.ts +9 -7
- package/dist/adt/client.d.ts.map +1 -1
- package/dist/adt/client.js +55 -64
- package/dist/adt/client.js.map +1 -1
- package/dist/adt/config.d.ts +2 -0
- package/dist/adt/config.d.ts.map +1 -1
- package/dist/adt/config.js +1 -0
- package/dist/adt/config.js.map +1 -1
- package/dist/adt/devtools.d.ts +8 -30
- package/dist/adt/devtools.d.ts.map +1 -1
- package/dist/adt/devtools.js +41 -218
- package/dist/adt/devtools.js.map +1 -1
- package/dist/adt/diagnostics.d.ts +1 -1
- package/dist/adt/diagnostics.d.ts.map +1 -1
- package/dist/adt/diagnostics.js +12 -12
- package/dist/adt/diagnostics.js.map +1 -1
- package/dist/adt/errors.d.ts +7 -0
- package/dist/adt/errors.d.ts.map +1 -1
- package/dist/adt/errors.js +7 -0
- package/dist/adt/errors.js.map +1 -1
- package/dist/adt/features.d.ts +9 -3
- package/dist/adt/features.d.ts.map +1 -1
- package/dist/adt/features.js +40 -8
- package/dist/adt/features.js.map +1 -1
- package/dist/adt/gcts.d.ts +12 -45
- package/dist/adt/gcts.d.ts.map +1 -1
- package/dist/adt/gcts.js +299 -171
- package/dist/adt/gcts.js.map +1 -1
- package/dist/adt/http-deadline.d.ts +23 -0
- package/dist/adt/http-deadline.d.ts.map +1 -0
- package/dist/adt/http-deadline.js +106 -0
- package/dist/adt/http-deadline.js.map +1 -0
- package/dist/adt/http-wire-body.d.ts +2 -0
- package/dist/adt/http-wire-body.d.ts.map +1 -0
- package/dist/adt/http-wire-body.js +14 -0
- package/dist/adt/http-wire-body.js.map +1 -0
- package/dist/adt/http.d.ts +10 -24
- package/dist/adt/http.d.ts.map +1 -1
- package/dist/adt/http.js +89 -53
- package/dist/adt/http.js.map +1 -1
- package/dist/adt/path-safety.d.ts +16 -0
- package/dist/adt/path-safety.d.ts.map +1 -0
- package/dist/adt/path-safety.js +101 -0
- package/dist/adt/path-safety.js.map +1 -0
- package/dist/adt/semaphore.d.ts +2 -2
- package/dist/adt/semaphore.d.ts.map +1 -1
- package/dist/adt/semaphore.js +26 -9
- package/dist/adt/semaphore.js.map +1 -1
- package/dist/adt/source-diff.d.ts +10 -1
- package/dist/adt/source-diff.d.ts.map +1 -1
- package/dist/adt/source-diff.js +33 -7
- package/dist/adt/source-diff.js.map +1 -1
- package/dist/adt/text-search.d.ts +25 -0
- package/dist/adt/text-search.d.ts.map +1 -0
- package/dist/adt/text-search.js +152 -0
- package/dist/adt/text-search.js.map +1 -0
- package/dist/adt/transport-diff.d.ts +128 -0
- package/dist/adt/transport-diff.d.ts.map +1 -0
- package/dist/adt/transport-diff.js +524 -0
- package/dist/adt/transport-diff.js.map +1 -0
- package/dist/adt/transport.d.ts +78 -13
- package/dist/adt/transport.d.ts.map +1 -1
- package/dist/adt/transport.js +443 -41
- package/dist/adt/transport.js.map +1 -1
- package/dist/adt/types.d.ts +23 -12
- package/dist/adt/types.d.ts.map +1 -1
- package/dist/adt/xml-parser.d.ts +1 -8
- package/dist/adt/xml-parser.d.ts.map +1 -1
- package/dist/adt/xml-parser.js +54 -60
- package/dist/adt/xml-parser.js.map +1 -1
- package/dist/authz/policy.d.ts.map +1 -1
- package/dist/authz/policy.js +5 -2
- package/dist/authz/policy.js.map +1 -1
- package/dist/cli-args.d.ts +2 -2
- package/dist/cli-args.js +2 -2
- package/dist/cli-checks.d.ts +56 -0
- package/dist/cli-checks.d.ts.map +1 -0
- package/dist/cli-checks.js +368 -0
- package/dist/cli-checks.js.map +1 -0
- package/dist/cli.d.ts +68 -15
- package/dist/cli.d.ts.map +1 -1
- package/dist/cli.js +677 -297
- package/dist/cli.js.map +1 -1
- package/dist/handlers/cds-hints.d.ts.map +1 -1
- package/dist/handlers/cds-hints.js +1 -10
- package/dist/handlers/cds-hints.js.map +1 -1
- package/dist/handlers/diagnose.d.ts +4 -0
- package/dist/handlers/diagnose.d.ts.map +1 -1
- package/dist/handlers/diagnose.js +616 -9
- package/dist/handlers/diagnose.js.map +1 -1
- package/dist/handlers/dispatch.d.ts.map +1 -1
- package/dist/handlers/dispatch.js +40 -1
- package/dist/handlers/dispatch.js.map +1 -1
- package/dist/handlers/git.d.ts.map +1 -1
- package/dist/handlers/git.js +165 -84
- package/dist/handlers/git.js.map +1 -1
- package/dist/handlers/read.d.ts.map +1 -1
- package/dist/handlers/read.js +15 -0
- package/dist/handlers/read.js.map +1 -1
- package/dist/handlers/schemas.d.ts +15 -2
- package/dist/handlers/schemas.d.ts.map +1 -1
- package/dist/handlers/schemas.js +75 -6
- package/dist/handlers/schemas.js.map +1 -1
- package/dist/handlers/search.js +1 -1
- package/dist/handlers/search.js.map +1 -1
- package/dist/handlers/tools.d.ts.map +1 -1
- package/dist/handlers/tools.js +51 -42
- package/dist/handlers/tools.js.map +1 -1
- package/dist/handlers/transport.d.ts +2 -1
- package/dist/handlers/transport.d.ts.map +1 -1
- package/dist/handlers/transport.js +128 -27
- package/dist/handlers/transport.js.map +1 -1
- package/dist/handlers/write/create.d.ts.map +1 -1
- package/dist/handlers/write/create.js +11 -0
- package/dist/handlers/write/create.js.map +1 -1
- package/dist/handlers/write/update-delete.d.ts.map +1 -1
- package/dist/handlers/write/update-delete.js +33 -10
- package/dist/handlers/write/update-delete.js.map +1 -1
- package/dist/handlers/write-helpers.d.ts.map +1 -1
- package/dist/handlers/write-helpers.js +3 -2
- package/dist/handlers/write-helpers.js.map +1 -1
- package/dist/server/audit.d.ts +1 -1
- package/dist/server/audit.d.ts.map +1 -1
- package/dist/server/audit.js +228 -21
- package/dist/server/audit.js.map +1 -1
- package/dist/server/config.d.ts +16 -0
- package/dist/server/config.d.ts.map +1 -1
- package/dist/server/config.js +145 -17
- package/dist/server/config.js.map +1 -1
- package/dist/server/effective-policy-log.d.ts +2 -1
- package/dist/server/effective-policy-log.d.ts.map +1 -1
- package/dist/server/effective-policy-log.js +8 -2
- package/dist/server/effective-policy-log.js.map +1 -1
- package/dist/server/multi-target-runtime.d.ts.map +1 -1
- package/dist/server/multi-target-runtime.js +3 -0
- package/dist/server/multi-target-runtime.js.map +1 -1
- package/dist/server/multi-target-tools.d.ts.map +1 -1
- package/dist/server/multi-target-tools.js +5 -0
- package/dist/server/multi-target-tools.js.map +1 -1
- package/dist/server/server.d.ts +15 -13
- package/dist/server/server.d.ts.map +1 -1
- package/dist/server/server.js +36 -26
- package/dist/server/server.js.map +1 -1
- package/dist/server/types.d.ts +2 -0
- package/dist/server/types.d.ts.map +1 -1
- package/dist/server/types.js +1 -0
- package/dist/server/types.js.map +1 -1
- package/package.json +13 -13
package/dist/cli.js
CHANGED
|
@@ -1,341 +1,721 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* ARC-1
|
|
2
|
+
* ARC-1 command-line interface.
|
|
3
3
|
*
|
|
4
|
-
*
|
|
5
|
-
*
|
|
6
|
-
* Two layers:
|
|
7
|
-
* - `arc1-cli serve` Start the MCP server (same as `arc1`).
|
|
8
|
-
* - `arc1-cli call <tool> [...]` Call any of the 12 MCP tools directly.
|
|
9
|
-
* - `arc1-cli tools [<tool>]` List tools / show a tool's JSON schema.
|
|
10
|
-
* - Shortcuts: `read`, `source` (alias), `activate`, `syntax`, `sql`, `lint`,
|
|
11
|
-
* `search`, `extract-cookies`, `version` — one-liners over `call` or helpers
|
|
12
|
-
* for common operations.
|
|
13
|
-
*
|
|
14
|
-
* The `call` command bypasses the MCP transport but reuses the same dispatch
|
|
15
|
-
* path (`handleToolCall` in src/handlers/dispatch.ts), so Zod validation,
|
|
16
|
-
* safety gates (`SAP_READ_ONLY`, `SAP_ALLOWED_PACKAGES`, ...), and audit
|
|
17
|
-
* logging all apply exactly as they do under `arc1 serve` stdio mode.
|
|
4
|
+
* Direct calls reuse the MCP dispatcher, validation, safety policy, audit
|
|
5
|
+
* pipeline, startup authentication check, and target-local feature evidence.
|
|
18
6
|
*/
|
|
19
7
|
import { readFileSync } from 'node:fs';
|
|
20
|
-
import {
|
|
21
|
-
import {
|
|
8
|
+
import { writeFile } from 'node:fs/promises';
|
|
9
|
+
import { pathToFileURL } from 'node:url';
|
|
10
|
+
import { Command, CommanderError, Option } from 'commander';
|
|
11
|
+
import { config as loadDotEnv } from 'dotenv';
|
|
12
|
+
import { appendAunitJunitDiagnostic, parseNativeJunitSummary } from './adt/aunit.js';
|
|
22
13
|
import { AdtClient } from './adt/client.js';
|
|
23
14
|
import { buildArgs } from './cli-args.js';
|
|
15
|
+
import { assertAtcPriority, assertPercent, atcToCheckstyle, evaluateAtc, evaluateAunit, evaluateDiff, evaluateLint, formatAtcText, formatAunitText, formatLintText, lintToCheckstyle, } from './cli-checks.js';
|
|
24
16
|
import { getToolRegistry, handleToolCall } from './handlers/dispatch.js';
|
|
25
|
-
import {
|
|
26
|
-
import {
|
|
27
|
-
import {
|
|
17
|
+
import { setCachedDiscovery, setCachedFeatures } from './handlers/feature-cache.js';
|
|
18
|
+
import { errorResult } from './handlers/shared.js';
|
|
19
|
+
import { sanitizeArgs } from './server/audit.js';
|
|
20
|
+
import { assertNoRemovedCliFlags, CLI_CONFIG_OPTION_SPECS, resolveConfig } from './server/config.js';
|
|
21
|
+
import { generateRequestId } from './server/context.js';
|
|
22
|
+
import { initLogger, logger } from './server/logger.js';
|
|
28
23
|
import { loadPlugins } from './server/plugin-loader.js';
|
|
29
|
-
import { buildAdtConfig, getConfiguredToolDefinitions, VERSION } from './server/server.js';
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
const
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
.
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
await createAndStartServer(serverConfig);
|
|
50
|
-
});
|
|
51
|
-
// ─── Direct tool invocation ────────────────────────────────────────────
|
|
52
|
-
const outputOption = new Option('--output <mode>', 'Output mode').choices(['text', 'json']).default('text');
|
|
24
|
+
import { buildAdtConfig, createCachingLayer, formatStartupAuthPreflightToolError, getConfiguredToolDefinitions, probeClientFeatures, runStartupAuthPreflightWithClient, VERSION, } from './server/server.js';
|
|
25
|
+
import { FileSink } from './server/sinks/file.js';
|
|
26
|
+
loadDotEnv({ quiet: true });
|
|
27
|
+
const defaultDependencies = {
|
|
28
|
+
resolveConfiguration: resolveConfig,
|
|
29
|
+
createClient: (config) => new AdtClient(config),
|
|
30
|
+
createCache: createCachingLayer,
|
|
31
|
+
dispatchToolCall: handleToolCall,
|
|
32
|
+
probeFeatures: probeClientFeatures,
|
|
33
|
+
authPreflight: runStartupAuthPreflightWithClient,
|
|
34
|
+
startServer: async (config, sources) => {
|
|
35
|
+
const { createAndStartServer } = await import('./server/server.js');
|
|
36
|
+
await createAndStartServer(config, sources);
|
|
37
|
+
},
|
|
38
|
+
flushLogger: () => logger.flush(),
|
|
39
|
+
runCookieExtractor: async (args) => {
|
|
40
|
+
const { run } = await import('./extract-sap-cookies.js');
|
|
41
|
+
await run(args);
|
|
42
|
+
},
|
|
43
|
+
};
|
|
53
44
|
function collect(value, previous) {
|
|
54
45
|
return [...previous, value];
|
|
55
46
|
}
|
|
56
|
-
|
|
57
|
-
.
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
.
|
|
61
|
-
.
|
|
62
|
-
|
|
47
|
+
function addOutputOption(command) {
|
|
48
|
+
return command.addOption(new Option('--output <mode>', 'Output mode').choices(['text', 'json']).default('text'));
|
|
49
|
+
}
|
|
50
|
+
function configArgv(argv) {
|
|
51
|
+
const separator = argv.indexOf('--');
|
|
52
|
+
return [...(separator < 0 ? argv : argv.slice(0, separator))];
|
|
53
|
+
}
|
|
54
|
+
function errorMessage(err) {
|
|
55
|
+
return err instanceof Error ? err.message : String(err);
|
|
56
|
+
}
|
|
57
|
+
function renderToolResult(result, mode) {
|
|
58
|
+
if (mode === 'json') {
|
|
59
|
+
console.log(JSON.stringify(result, null, 2));
|
|
60
|
+
}
|
|
61
|
+
else {
|
|
62
|
+
const stream = result.isError ? console.error : console.log;
|
|
63
|
+
for (const block of result.content) {
|
|
64
|
+
if (block.type === 'text')
|
|
65
|
+
stream(block.text);
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
return result.isError ? 1 : 0;
|
|
69
|
+
}
|
|
70
|
+
function parseCiToolResult(result) {
|
|
71
|
+
const text = result.content.find((entry) => entry.type === 'text' && typeof entry.text === 'string')?.text;
|
|
72
|
+
if (!text)
|
|
73
|
+
throw new Error('Tool returned no text result for the CI command.');
|
|
63
74
|
try {
|
|
64
|
-
|
|
65
|
-
const code = await runToolCall(tool, args, opts.output);
|
|
66
|
-
process.exit(code);
|
|
75
|
+
return JSON.parse(text);
|
|
67
76
|
}
|
|
68
|
-
catch (
|
|
69
|
-
|
|
70
|
-
// comes from runToolCall failures (resolveCliContext config-parse
|
|
71
|
-
// errors, "Unknown tool", buildArgs validation) — none interpolate
|
|
72
|
-
// api-key material. Pinned by tests/unit/cli/clear-text-logging-regression.test.ts.
|
|
73
|
-
console.error(err instanceof Error ? err.message : String(err));
|
|
74
|
-
process.exit(2);
|
|
77
|
+
catch (error) {
|
|
78
|
+
throw new Error(`Tool returned a non-JSON CI result: ${errorMessage(error)}`);
|
|
75
79
|
}
|
|
76
|
-
}
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
.
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
80
|
+
}
|
|
81
|
+
async function executeCiJson(runtime, toolName, args, options) {
|
|
82
|
+
const outcome = await executeCliToolCall(runtime, toolName, args, options);
|
|
83
|
+
if (outcome.kind === 'usage') {
|
|
84
|
+
console.error(outcome.message);
|
|
85
|
+
if (outcome.knownTools)
|
|
86
|
+
console.error(`Known tools: ${outcome.knownTools.join(', ')}`);
|
|
87
|
+
runtime.state.exitCode = 2;
|
|
88
|
+
return { ok: false };
|
|
85
89
|
}
|
|
86
|
-
|
|
87
|
-
.
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
90
|
+
if (outcome.result.isError) {
|
|
91
|
+
runtime.state.exitCode = renderToolResult(outcome.result, 'text');
|
|
92
|
+
return { ok: false };
|
|
93
|
+
}
|
|
94
|
+
try {
|
|
95
|
+
return { ok: true, value: parseCiToolResult(outcome.result) };
|
|
96
|
+
}
|
|
97
|
+
catch (error) {
|
|
98
|
+
console.error(errorMessage(error));
|
|
99
|
+
runtime.state.exitCode = 1;
|
|
100
|
+
return { ok: false };
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
async function emitCiReport(content, reportFile) {
|
|
104
|
+
try {
|
|
105
|
+
if (reportFile && reportFile !== '-') {
|
|
106
|
+
await writeFile(reportFile, content.endsWith('\n') ? content : `${content}\n`, 'utf8');
|
|
96
107
|
}
|
|
97
|
-
|
|
108
|
+
else
|
|
109
|
+
console.log(content);
|
|
110
|
+
return true;
|
|
98
111
|
}
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
console.error(`Available: ${defs.map((d) => d.name).join(', ')}`);
|
|
103
|
-
process.exit(2);
|
|
112
|
+
catch (error) {
|
|
113
|
+
console.error(`Could not write CI report: ${errorMessage(error)}`);
|
|
114
|
+
return false;
|
|
104
115
|
}
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
}
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
.addOption(outputOption)
|
|
133
|
-
.action(async (type, name, opts) => {
|
|
134
|
-
process.exit(await runToolCall('SAPRead', { type: type.toUpperCase(), name, format: 'text' }, opts.output));
|
|
135
|
-
});
|
|
136
|
-
program
|
|
137
|
-
.command('activate <type> <name>')
|
|
138
|
-
.description('Activate an ADT object (SAPActivate) — e.g. `activate CLAS ZCL_FOO`')
|
|
139
|
-
.addOption(outputOption)
|
|
140
|
-
.action(async (type, name, opts) => {
|
|
141
|
-
process.exit(await runToolCall('SAPActivate', { action: 'activate', type: type.toUpperCase(), name }, opts.output));
|
|
142
|
-
});
|
|
143
|
-
program
|
|
144
|
-
.command('syntax <type> <name>')
|
|
145
|
-
.description('Remote syntax check on an ABAP object (SAPDiagnose syntax)')
|
|
146
|
-
.addOption(outputOption)
|
|
147
|
-
.action(async (type, name, opts) => {
|
|
148
|
-
process.exit(await runToolCall('SAPDiagnose', { action: 'syntax', type: type.toUpperCase(), name }, opts.output));
|
|
149
|
-
});
|
|
150
|
-
program
|
|
151
|
-
.command('sql <query>')
|
|
152
|
-
.description('Execute an OpenSQL query (SAPQuery; requires SAP_ALLOW_FREE_SQL=true)')
|
|
153
|
-
.addOption(outputOption)
|
|
154
|
-
.action(async (query, opts) => {
|
|
155
|
-
// SAPQuerySchema is { sql, maxRows } — no `action`, and the query field is `sql` (not `query`).
|
|
156
|
-
process.exit(await runToolCall('SAPQuery', { sql: query }, opts.output));
|
|
157
|
-
});
|
|
158
|
-
// ─── Legacy / local-only commands ──────────────────────────────────────
|
|
159
|
-
program
|
|
160
|
-
.command('search <query>')
|
|
161
|
-
.description('Search for ABAP objects (SAPSearch)')
|
|
162
|
-
.option('--max <number>', 'Maximum results', '50')
|
|
163
|
-
.addOption(outputOption)
|
|
164
|
-
.action(async (query, opts) => {
|
|
165
|
-
// SAPSearchSchema has no `action` field (object search is the default searchType); pass query + maxResults.
|
|
166
|
-
process.exit(await runToolCall('SAPSearch', { query, maxResults: Number(opts.max) }, opts.output));
|
|
167
|
-
});
|
|
168
|
-
program
|
|
169
|
-
.command('extract-cookies [args...]')
|
|
170
|
-
.description('Launch a browser, log into SAP, and write a Netscape cookie file. Pass --help for options.')
|
|
171
|
-
.allowUnknownOption(true)
|
|
172
|
-
.helpOption(false)
|
|
173
|
-
.action(async () => {
|
|
174
|
-
const idx = process.argv.indexOf('extract-cookies');
|
|
175
|
-
const forwarded = idx >= 0 ? process.argv.slice(idx + 1) : [];
|
|
176
|
-
const { run } = await import('./extract-sap-cookies.js');
|
|
116
|
+
}
|
|
117
|
+
function representAunitExitInJunit(junit, exitCode) {
|
|
118
|
+
const summary = parseNativeJunitSummary(junit);
|
|
119
|
+
if (summary.failures > 0 || summary.errors > 0)
|
|
120
|
+
return junit;
|
|
121
|
+
const incomplete = exitCode === 3;
|
|
122
|
+
return appendAunitJunitDiagnostic(junit, summary, incomplete ? 'incomplete' : 'failed', incomplete
|
|
123
|
+
? 'ARC-1 could not evaluate a complete ABAP Unit CI result.'
|
|
124
|
+
: 'ARC-1 ABAP Unit CI policy failed although SAP reported no failing test case.');
|
|
125
|
+
}
|
|
126
|
+
function formatConfigSource(source) {
|
|
127
|
+
if (source === undefined || source === 'default')
|
|
128
|
+
return 'default';
|
|
129
|
+
if (typeof source === 'object') {
|
|
130
|
+
if ('env' in source)
|
|
131
|
+
return `env ${source.env}`;
|
|
132
|
+
if ('flag' in source)
|
|
133
|
+
return `flag ${source.flag}`;
|
|
134
|
+
if ('file' in source)
|
|
135
|
+
return `file ${source.file}`;
|
|
136
|
+
}
|
|
137
|
+
return 'unknown';
|
|
138
|
+
}
|
|
139
|
+
function directModeError(config) {
|
|
140
|
+
const sapUrl = config.url.trim();
|
|
141
|
+
if (!sapUrl)
|
|
142
|
+
return 'Direct CLI calls require SAP_URL to be configured.';
|
|
177
143
|
try {
|
|
178
|
-
|
|
144
|
+
const parsed = new URL(sapUrl);
|
|
145
|
+
if (parsed.protocol !== 'http:' && parsed.protocol !== 'https:')
|
|
146
|
+
throw new Error('unsupported protocol');
|
|
179
147
|
}
|
|
180
|
-
catch
|
|
181
|
-
|
|
182
|
-
process.stderr.write(`${message}\n`);
|
|
183
|
-
process.exit(1);
|
|
148
|
+
catch {
|
|
149
|
+
return 'Direct CLI calls require SAP_URL to be a valid HTTP(S) URL.';
|
|
184
150
|
}
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
.command('lint <file>')
|
|
188
|
-
.description('Lint a local ABAP source file (offline; no SAP connection)')
|
|
189
|
-
.action((file) => {
|
|
190
|
-
const source = readFileSync(file, 'utf-8');
|
|
191
|
-
const filename = detectFilename(source, file.replace(/\.abap$/, ''));
|
|
192
|
-
const issues = lintAbapSource(source, filename);
|
|
193
|
-
if (issues.length === 0) {
|
|
194
|
-
console.log('No issues found.');
|
|
195
|
-
return;
|
|
151
|
+
if (config.multiTargetEndpoints) {
|
|
152
|
+
return 'Direct CLI calls do not support ARC1_MULTI_TARGET_ENDPOINTS. Use the MCP server endpoint for multi-target routing.';
|
|
196
153
|
}
|
|
197
|
-
|
|
198
|
-
|
|
154
|
+
if (config.ppEnabled) {
|
|
155
|
+
return 'Direct CLI calls do not support principal propagation. Use the MCP HTTP server so the caller JWT can be propagated.';
|
|
199
156
|
}
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
157
|
+
if (config.btpServiceKey ||
|
|
158
|
+
config.btpServiceKeyFile ||
|
|
159
|
+
process.env.SAP_BTP_DESTINATION ||
|
|
160
|
+
process.env.SAP_BTP_PP_DESTINATION) {
|
|
161
|
+
return ('Direct CLI calls currently support a single SAP_URL target with Basic or cookie authentication only. ' +
|
|
162
|
+
'BTP service-key and Destination Service bootstrap is available through the MCP server.');
|
|
163
|
+
}
|
|
164
|
+
return undefined;
|
|
165
|
+
}
|
|
166
|
+
function shouldSkipFeatureProbe(toolName, args) {
|
|
167
|
+
return toolName === 'SAPManage' && String(args.action ?? '').toLowerCase() === 'probe';
|
|
168
|
+
}
|
|
169
|
+
function isLocalOnlyCall(toolName, args) {
|
|
170
|
+
const action = String(args.action ?? '').toLowerCase();
|
|
171
|
+
return (toolName === 'SAPManage' && action === 'cache_stats') || (toolName === 'SAPLint' && action === 'list_rules');
|
|
172
|
+
}
|
|
173
|
+
function isDisabledWriteTool(toolName, config) {
|
|
174
|
+
return !config.allowWrites && (toolName === 'SAPWrite' || toolName === 'SAPActivate');
|
|
175
|
+
}
|
|
176
|
+
function auditBlockedDirectPreflight(config, toolName, args, startedAt) {
|
|
177
|
+
const requestId = generateRequestId();
|
|
178
|
+
const pluginName = getToolRegistry().get(toolName)?.pluginName;
|
|
179
|
+
logger.emitAudit({
|
|
180
|
+
timestamp: new Date(startedAt).toISOString(),
|
|
181
|
+
level: 'info',
|
|
182
|
+
event: 'tool_call_start',
|
|
183
|
+
destination: config.destinationName,
|
|
184
|
+
requestId,
|
|
185
|
+
tool: toolName,
|
|
186
|
+
pluginName,
|
|
187
|
+
args: sanitizeArgs(args),
|
|
188
|
+
});
|
|
189
|
+
logger.emitAudit({
|
|
190
|
+
timestamp: new Date().toISOString(),
|
|
191
|
+
level: 'error',
|
|
192
|
+
event: 'tool_call_end',
|
|
193
|
+
destination: config.destinationName,
|
|
194
|
+
requestId,
|
|
195
|
+
tool: toolName,
|
|
196
|
+
pluginName,
|
|
197
|
+
durationMs: Date.now() - startedAt,
|
|
198
|
+
status: 'error',
|
|
199
|
+
errorClass: 'StartupAuthPreflightError',
|
|
200
|
+
errorMessage: 'Shared SAP authentication preflight blocked this direct CLI call (HTTP authentication/authorization response details suppressed).',
|
|
201
|
+
});
|
|
202
|
+
}
|
|
203
|
+
function createRuntime(argv, state, overrides) {
|
|
204
|
+
const runtimeState = state;
|
|
205
|
+
const deps = { ...defaultDependencies, ...overrides };
|
|
206
|
+
const getResolvedConfig = () => {
|
|
207
|
+
if (!runtimeState.resolvedConfig) {
|
|
208
|
+
runtimeState.resolvedConfig = deps.resolveConfiguration(configArgv(argv));
|
|
209
|
+
const serverConfig = runtimeState.resolvedConfig.config;
|
|
210
|
+
initLogger(serverConfig.logFormat, serverConfig.verbose);
|
|
211
|
+
}
|
|
212
|
+
return runtimeState.resolvedConfig;
|
|
213
|
+
};
|
|
214
|
+
const getDirectContext = () => {
|
|
215
|
+
runtimeState.directContext ??= (async () => {
|
|
216
|
+
const { config } = getResolvedConfig();
|
|
217
|
+
if (config.logFile) {
|
|
218
|
+
logger.addSink(new FileSink(config.logFile));
|
|
219
|
+
}
|
|
220
|
+
const client = deps.createClient(buildAdtConfig(config));
|
|
221
|
+
const cachingLayer = await deps.createCache(config);
|
|
222
|
+
return { client, cachingLayer };
|
|
223
|
+
})();
|
|
224
|
+
return runtimeState.directContext;
|
|
225
|
+
};
|
|
226
|
+
const loadConfiguredPlugins = async (config) => {
|
|
227
|
+
if (!runtimeState.pluginsLoaded) {
|
|
228
|
+
runtimeState.pluginsLoaded = config.plugins?.length
|
|
229
|
+
? loadPlugins(config.plugins, getToolRegistry()).then(() => undefined)
|
|
230
|
+
: Promise.resolve();
|
|
231
|
+
}
|
|
232
|
+
await runtimeState.pluginsLoaded;
|
|
233
|
+
};
|
|
234
|
+
const dispose = async () => {
|
|
235
|
+
if (!runtimeState.directContext)
|
|
236
|
+
return;
|
|
237
|
+
try {
|
|
238
|
+
const context = await runtimeState.directContext;
|
|
239
|
+
context.cachingLayer?.cache.close();
|
|
240
|
+
}
|
|
241
|
+
catch {
|
|
242
|
+
// Context construction failed, so there is no successfully opened cache to close.
|
|
243
|
+
}
|
|
244
|
+
};
|
|
245
|
+
return { state: runtimeState, deps, getResolvedConfig, getDirectContext, loadConfiguredPlugins, dispose };
|
|
246
|
+
}
|
|
247
|
+
/** Build a fresh Commander program. Importing this module never parses argv or exits. */
|
|
248
|
+
export function createCliProgram(options = {}) {
|
|
249
|
+
const argv = options.argv ?? [];
|
|
250
|
+
const executionState = options.state ?? { exitCode: 0 };
|
|
251
|
+
const runtime = createRuntime(argv, executionState, options.dependencies ?? {});
|
|
252
|
+
const program = new Command();
|
|
253
|
+
program
|
|
254
|
+
.name('arc1')
|
|
255
|
+
.description('ARC-1 — MCP Server and CLI for SAP ABAP Systems')
|
|
256
|
+
.version(VERSION)
|
|
257
|
+
.exitOverride()
|
|
258
|
+
.allowUnknownOption(false)
|
|
259
|
+
.allowExcessArguments(false);
|
|
260
|
+
for (const spec of CLI_CONFIG_OPTION_SPECS) {
|
|
261
|
+
const value = spec.valueOptional ? `[${spec.valueName}]` : `<${spec.valueName}>`;
|
|
262
|
+
program.addOption(new Option(`--${spec.name} ${value}`, spec.description));
|
|
263
|
+
}
|
|
264
|
+
program
|
|
265
|
+
.command('serve', { isDefault: true })
|
|
266
|
+
.description('Start the MCP server (default when no subcommand is supplied)')
|
|
267
|
+
.allowUnknownOption(false)
|
|
268
|
+
.allowExcessArguments(false)
|
|
269
|
+
.action(async () => {
|
|
270
|
+
const { config, sources } = runtime.getResolvedConfig();
|
|
271
|
+
await runtime.deps.startServer(config, sources);
|
|
272
|
+
runtime.state.longLivedServerStarted = true;
|
|
273
|
+
});
|
|
274
|
+
addOutputOption(program
|
|
275
|
+
.command('call <tool>')
|
|
276
|
+
.description('Call an MCP tool directly')
|
|
277
|
+
.option('--arg <key=value>', 'Tool argument; repeatable', collect, [])
|
|
278
|
+
.option('--json <source>', 'JSON args: inline object, file path, or "-" for stdin')).action(async (tool, opts) => {
|
|
279
|
+
const args = buildArgs(opts);
|
|
280
|
+
runtime.state.exitCode = await runToolCall(runtime, tool, args, opts.output);
|
|
281
|
+
});
|
|
282
|
+
program
|
|
283
|
+
.command('tools [tool]')
|
|
284
|
+
.description("List advertised MCP tools, or show one tool's JSON input schema")
|
|
285
|
+
.action(async (tool) => {
|
|
286
|
+
const { config } = runtime.getResolvedConfig();
|
|
287
|
+
await runtime.loadConfiguredPlugins(config);
|
|
288
|
+
const pluginDefs = getToolRegistry()
|
|
289
|
+
.list()
|
|
290
|
+
.flatMap((entry) => entry.source === 'plugin' && entry.listing
|
|
291
|
+
? [
|
|
292
|
+
{
|
|
293
|
+
name: entry.name,
|
|
294
|
+
description: entry.listing.description,
|
|
295
|
+
inputSchema: entry.listing.inputSchema,
|
|
296
|
+
},
|
|
297
|
+
]
|
|
298
|
+
: []);
|
|
299
|
+
const definitions = [...getConfiguredToolDefinitions(config), ...pluginDefs];
|
|
300
|
+
if (!tool) {
|
|
301
|
+
for (const definition of definitions) {
|
|
302
|
+
console.log(`${definition.name.padEnd(14)} ${definition.description.split('\n')[0].trim()}`);
|
|
303
|
+
}
|
|
304
|
+
return;
|
|
305
|
+
}
|
|
306
|
+
const match = definitions.find((definition) => definition.name.toLowerCase() === tool.toLowerCase());
|
|
307
|
+
if (!match) {
|
|
308
|
+
console.error(`Unknown or non-advertised tool: ${tool}`);
|
|
309
|
+
console.error(`Advertised tools: ${definitions.map((definition) => definition.name).join(', ')}`);
|
|
310
|
+
runtime.state.exitCode = 2;
|
|
311
|
+
return;
|
|
312
|
+
}
|
|
313
|
+
console.log(match.description);
|
|
314
|
+
console.log('\nInput schema:');
|
|
315
|
+
console.log(JSON.stringify(match.inputSchema, null, 2));
|
|
316
|
+
});
|
|
317
|
+
addOutputOption(program
|
|
318
|
+
.command('read <type> <name>')
|
|
319
|
+
.description('Read an ABAP object via SAPRead')
|
|
320
|
+
.option('--flat', 'Return flat source for CLAS/INTF')
|
|
321
|
+
.option('--source-version <version>', 'Source version: active, inactive, or auto')).action(async (type, name, opts) => {
|
|
322
|
+
const args = { type: type.toUpperCase(), name };
|
|
323
|
+
if (opts.flat)
|
|
324
|
+
args.format = 'text';
|
|
325
|
+
if (opts.sourceVersion)
|
|
326
|
+
args.version = opts.sourceVersion;
|
|
327
|
+
runtime.state.exitCode = await runToolCall(runtime, 'SAPRead', args, opts.output);
|
|
328
|
+
});
|
|
329
|
+
addOutputOption(program.command('source <type> <name>').description('Alias of `read --flat` (legacy)')).action(async (type, name, opts) => {
|
|
330
|
+
runtime.state.exitCode = await runToolCall(runtime, 'SAPRead', { type: type.toUpperCase(), name, format: 'text' }, opts.output);
|
|
331
|
+
});
|
|
332
|
+
addOutputOption(program.command('activate <type> <name>').description('Activate an ADT object')).action(async (type, name, opts) => {
|
|
333
|
+
runtime.state.exitCode = await runToolCall(runtime, 'SAPActivate', { action: 'activate', type: type.toUpperCase(), name }, opts.output);
|
|
334
|
+
});
|
|
335
|
+
addOutputOption(program.command('syntax <type> <name>').description('Run a remote syntax check')).action(async (type, name, opts) => {
|
|
336
|
+
runtime.state.exitCode = await runToolCall(runtime, 'SAPDiagnose', { action: 'syntax', type: type.toUpperCase(), name }, opts.output);
|
|
337
|
+
});
|
|
338
|
+
addOutputOption(program.command('sql <query>').description('Execute an OpenSQL query through SAPQuery')).action(async (query, opts) => {
|
|
339
|
+
runtime.state.exitCode = await runToolCall(runtime, 'SAPQuery', { sql: query }, opts.output);
|
|
340
|
+
});
|
|
341
|
+
addOutputOption(program
|
|
342
|
+
.command('search <query>')
|
|
343
|
+
.description('Search for ABAP objects')
|
|
344
|
+
.option('--max <number>', 'Maximum results', '50')).action(async (query, opts) => {
|
|
345
|
+
runtime.state.exitCode = await runToolCall(runtime, 'SAPSearch', { query, maxResults: Number(opts.max) }, opts.output);
|
|
346
|
+
});
|
|
347
|
+
program
|
|
348
|
+
.command('unittest <type> <name>')
|
|
349
|
+
.description('Run harmless-only ABAP Unit tests for an object or DEVC package')
|
|
350
|
+
.option('--coverage', 'Collect statement, branch, and procedure coverage')
|
|
351
|
+
.option('--include-subpackages', 'For DEVC only, include the complete package subtree')
|
|
352
|
+
.option('--min-statement <percent>', 'Minimum statement coverage percentage')
|
|
353
|
+
.option('--min-branch <percent>', 'Minimum branch coverage percentage')
|
|
354
|
+
.option('--min-procedure <percent>', 'Minimum procedure coverage percentage')
|
|
355
|
+
.addOption(new Option('--format <format>', 'Report format').choices(['text', 'json', 'junit']).default('text'))
|
|
356
|
+
.option('--report-file <path>', 'Write the report to a file; use "-" for stdout')
|
|
357
|
+
.option('--timeout <seconds>', 'End-to-end ABAP Unit evidence budget (1-3600 seconds; default 300)')
|
|
358
|
+
.option('--allow-empty', 'Allow only a sound no-tests result to pass')
|
|
359
|
+
.option('--fail-on-skipped', 'Fail when any executed method is skipped')
|
|
360
|
+
.action(async (type, name, opts) => {
|
|
361
|
+
const normalizedType = type.toUpperCase();
|
|
362
|
+
if (opts.includeSubpackages === true && normalizedType !== 'DEVC') {
|
|
363
|
+
console.error('--include-subpackages is only valid when unittest type is DEVC.');
|
|
364
|
+
runtime.state.exitCode = 2;
|
|
365
|
+
return;
|
|
366
|
+
}
|
|
367
|
+
const coverage = {
|
|
368
|
+
...(opts.minStatement !== undefined ? { statement: assertPercent(opts.minStatement, '--min-statement') } : {}),
|
|
369
|
+
...(opts.minBranch !== undefined ? { branch: assertPercent(opts.minBranch, '--min-branch') } : {}),
|
|
370
|
+
...(opts.minProcedure !== undefined ? { procedure: assertPercent(opts.minProcedure, '--min-procedure') } : {}),
|
|
371
|
+
};
|
|
372
|
+
const coverageRequested = opts.coverage === true || Object.keys(coverage).length > 0;
|
|
373
|
+
const timeoutSeconds = opts.timeout === undefined ? undefined : Number(opts.timeout);
|
|
374
|
+
if (timeoutSeconds !== undefined &&
|
|
375
|
+
(!Number.isSafeInteger(timeoutSeconds) || timeoutSeconds < 1 || timeoutSeconds > 3600)) {
|
|
376
|
+
console.error('--timeout must be an integer from 1 to 3600 seconds.');
|
|
377
|
+
runtime.state.exitCode = 2;
|
|
378
|
+
return;
|
|
379
|
+
}
|
|
380
|
+
const outcome = await executeCiJson(runtime, 'SAPDiagnose', {
|
|
381
|
+
action: 'unittest',
|
|
382
|
+
type: normalizedType,
|
|
383
|
+
name,
|
|
384
|
+
coverage: coverageRequested,
|
|
385
|
+
...(opts.includeSubpackages === true ? { includeSubpackages: true } : {}),
|
|
386
|
+
...(timeoutSeconds === undefined ? {} : { timeoutSeconds }),
|
|
387
|
+
resultFormat: opts.format === 'junit' ? 'junit' : 'structured',
|
|
388
|
+
});
|
|
389
|
+
if (!outcome.ok)
|
|
390
|
+
return;
|
|
391
|
+
const exitCode = evaluateAunit(outcome.value, {
|
|
392
|
+
allowEmpty: opts.allowEmpty,
|
|
393
|
+
failOnSkipped: opts.failOnSkipped,
|
|
394
|
+
requireCoverage: opts.coverage === true,
|
|
395
|
+
coverage,
|
|
396
|
+
});
|
|
397
|
+
let report;
|
|
398
|
+
try {
|
|
399
|
+
report =
|
|
400
|
+
opts.format === 'json'
|
|
401
|
+
? JSON.stringify(outcome.value, null, 2)
|
|
402
|
+
: opts.format === 'junit'
|
|
403
|
+
? outcome.value.junit
|
|
404
|
+
: formatAunitText(outcome.value);
|
|
405
|
+
if (opts.format === 'junit' && report && exitCode !== 0) {
|
|
406
|
+
report = representAunitExitInJunit(report, exitCode);
|
|
407
|
+
}
|
|
408
|
+
}
|
|
409
|
+
catch {
|
|
410
|
+
console.error('ABAP Unit returned malformed or incomplete structured evidence; no report was emitted.');
|
|
411
|
+
runtime.state.exitCode = exitCode === 3 ? 3 : 1;
|
|
412
|
+
return;
|
|
413
|
+
}
|
|
414
|
+
if (typeof report !== 'string' || !report) {
|
|
415
|
+
console.error(exitCode === 3
|
|
416
|
+
? 'ABAP Unit returned malformed or incomplete structured evidence; no report was emitted.'
|
|
417
|
+
: 'ABAP Unit did not return the requested JUnit report.');
|
|
418
|
+
runtime.state.exitCode = exitCode === 3 ? 3 : 1;
|
|
419
|
+
return;
|
|
420
|
+
}
|
|
421
|
+
if (!(await emitCiReport(report, opts.reportFile))) {
|
|
422
|
+
runtime.state.exitCode = 1;
|
|
423
|
+
return;
|
|
424
|
+
}
|
|
425
|
+
runtime.state.exitCode = exitCode;
|
|
426
|
+
});
|
|
427
|
+
program
|
|
428
|
+
.command('atc <type> <name>')
|
|
429
|
+
.description('Run ATC with completeness evidence and CI thresholds')
|
|
430
|
+
.option('--variant <name>', 'ATC check variant; omit for the system default')
|
|
431
|
+
.option('--timeout <seconds>', 'ATC execution and verification budget (1-3600 seconds; default 300)')
|
|
432
|
+
.option('--max-priority <priority>', 'Fail on findings with priority <= N (1=error, 2=warning, 3=info)', '1')
|
|
433
|
+
.addOption(new Option('--format <format>', 'Report format').choices(['text', 'json', 'checkstyle']).default('text'))
|
|
434
|
+
.option('--report-file <path>', 'Write the report to a file; use "-" for stdout')
|
|
435
|
+
.action(async (type, name, opts) => {
|
|
436
|
+
const maxPriority = assertAtcPriority(opts.maxPriority);
|
|
437
|
+
const timeoutSeconds = opts.timeout === undefined ? undefined : Number(opts.timeout);
|
|
438
|
+
if (timeoutSeconds !== undefined &&
|
|
439
|
+
(!Number.isSafeInteger(timeoutSeconds) || timeoutSeconds < 1 || timeoutSeconds > 3600)) {
|
|
440
|
+
console.error('--timeout must be an integer from 1 to 3600 seconds.');
|
|
441
|
+
runtime.state.exitCode = 2;
|
|
442
|
+
return;
|
|
443
|
+
}
|
|
444
|
+
const outcome = await executeCiJson(runtime, 'SAPDiagnose', {
|
|
445
|
+
action: 'atc',
|
|
446
|
+
type: type.toUpperCase(),
|
|
447
|
+
name,
|
|
448
|
+
...(opts.variant ? { variant: opts.variant } : {}),
|
|
449
|
+
...(timeoutSeconds === undefined ? {} : { timeoutSeconds }),
|
|
450
|
+
resultFormat: 'structured',
|
|
451
|
+
});
|
|
452
|
+
if (!outcome.ok)
|
|
453
|
+
return;
|
|
454
|
+
const exitCode = evaluateAtc(outcome.value, maxPriority);
|
|
455
|
+
if (exitCode === 3) {
|
|
456
|
+
const reasons = Array.isArray(outcome.value.incompleteReasons)
|
|
457
|
+
? outcome.value.incompleteReasons.filter((reason) => typeof reason === 'string')
|
|
458
|
+
: [];
|
|
459
|
+
console.error(reasons.length > 0
|
|
460
|
+
? `ATC returned incomplete evidence: ${reasons.join(' ')}`
|
|
461
|
+
: 'ATC returned malformed or incomplete structured evidence; no report was emitted.');
|
|
462
|
+
runtime.state.exitCode = exitCode;
|
|
463
|
+
return;
|
|
464
|
+
}
|
|
465
|
+
const report = opts.format === 'json'
|
|
466
|
+
? JSON.stringify(outcome.value, null, 2)
|
|
467
|
+
: opts.format === 'checkstyle'
|
|
468
|
+
? atcToCheckstyle(outcome.value)
|
|
469
|
+
: formatAtcText(outcome.value);
|
|
470
|
+
if (!(await emitCiReport(report, opts.reportFile))) {
|
|
471
|
+
runtime.state.exitCode = 1;
|
|
472
|
+
return;
|
|
473
|
+
}
|
|
474
|
+
runtime.state.exitCode = exitCode;
|
|
475
|
+
});
|
|
476
|
+
program
|
|
477
|
+
.command('diff <type> <name>')
|
|
478
|
+
.description('Compare two SAP source versions with optional CI failure on differences')
|
|
479
|
+
.option('--from <version>', 'Old side: active, inactive, revision id, or revision URI', 'active')
|
|
480
|
+
.option('--to <version>', 'New side: active, inactive, revision id, or revision URI', 'inactive')
|
|
481
|
+
.option('--from-label <label>', 'Display label for the old side')
|
|
482
|
+
.option('--to-label <label>', 'Display label for the new side')
|
|
483
|
+
.option('--include <include>', 'Class include to compare')
|
|
484
|
+
.option('--group <group>', 'Function group for FUNC revisions')
|
|
485
|
+
.option('--check', 'Exit 1 when differences exist')
|
|
486
|
+
.option('--fail-on-diff', 'Alias for --check')
|
|
487
|
+
.addOption(new Option('--format <format>', 'Report format').choices(['text', 'json']).default('text'))
|
|
488
|
+
.option('--report-file <path>', 'Write the report to a file; use "-" for stdout')
|
|
489
|
+
.action(async (type, name, opts) => {
|
|
490
|
+
const outcome = await executeCiJson(runtime, 'SAPRead', {
|
|
491
|
+
action: 'diff',
|
|
492
|
+
type: type.toUpperCase(),
|
|
493
|
+
name,
|
|
494
|
+
from: opts.from,
|
|
495
|
+
to: opts.to,
|
|
496
|
+
format: 'structured',
|
|
497
|
+
...(opts.fromLabel ? { fromLabel: opts.fromLabel } : {}),
|
|
498
|
+
...(opts.toLabel ? { toLabel: opts.toLabel } : {}),
|
|
499
|
+
...(opts.include ? { include: opts.include } : {}),
|
|
500
|
+
...(opts.group ? { group: opts.group } : {}),
|
|
501
|
+
});
|
|
502
|
+
if (!outcome.ok)
|
|
503
|
+
return;
|
|
504
|
+
const exitCode = evaluateDiff(outcome.value, opts.check === true || opts.failOnDiff === true);
|
|
505
|
+
if (exitCode === 3) {
|
|
506
|
+
console.error('SAPRead diff returned malformed or incomplete structured evidence; no report was emitted.');
|
|
507
|
+
runtime.state.exitCode = exitCode;
|
|
508
|
+
return;
|
|
509
|
+
}
|
|
510
|
+
const report = opts.format === 'json'
|
|
511
|
+
? JSON.stringify(outcome.value, null, 2)
|
|
512
|
+
: outcome.value.identical
|
|
513
|
+
? `No differences between ${outcome.value.fromLabel} and ${outcome.value.toLabel} for ${outcome.value.type} ${outcome.value.name}.`
|
|
514
|
+
: `Diff ${outcome.value.type} ${outcome.value.name}: ${outcome.value.fromLabel} → ${outcome.value.toLabel} (+${outcome.value.added} -${outcome.value.removed})\n\n${outcome.value.diff}`;
|
|
515
|
+
if (!(await emitCiReport(report, opts.reportFile))) {
|
|
516
|
+
runtime.state.exitCode = 1;
|
|
517
|
+
return;
|
|
518
|
+
}
|
|
519
|
+
runtime.state.exitCode = exitCode;
|
|
520
|
+
});
|
|
521
|
+
program
|
|
522
|
+
.command('extract-cookies [args...]')
|
|
523
|
+
.description('Launch a browser and write a Netscape SAP cookie file')
|
|
524
|
+
.allowUnknownOption(true)
|
|
525
|
+
.allowExcessArguments(true)
|
|
526
|
+
.helpOption(false)
|
|
527
|
+
.action(async () => {
|
|
528
|
+
const index = argv.indexOf('extract-cookies');
|
|
529
|
+
const forwarded = index < 0 ? [] : [...argv.slice(index + 1)];
|
|
530
|
+
try {
|
|
531
|
+
await runtime.deps.runCookieExtractor(forwarded);
|
|
532
|
+
}
|
|
533
|
+
catch (err) {
|
|
534
|
+
process.stderr.write(`${errorMessage(err)}\n`);
|
|
535
|
+
runtime.state.exitCode = 1;
|
|
536
|
+
}
|
|
537
|
+
});
|
|
538
|
+
program
|
|
539
|
+
.command('lint <file>')
|
|
540
|
+
.description('Lint a local ABAP source file with CI reports')
|
|
541
|
+
.addOption(new Option('--format <format>', 'Report format').choices(['text', 'json', 'checkstyle']).default('text'))
|
|
542
|
+
.option('--report-file <path>', 'Write the report to a file; use "-" for stdout')
|
|
543
|
+
.addOption(new Option('--fail-on <severity>', 'Failure threshold')
|
|
544
|
+
.choices(['error', 'warning', 'info', 'none'])
|
|
545
|
+
.default('error'))
|
|
546
|
+
.action(async (file, opts) => {
|
|
547
|
+
const source = readFileSync(file, 'utf-8');
|
|
548
|
+
const outcome = await executeCiJson(runtime, 'SAPLint', {
|
|
549
|
+
action: 'lint',
|
|
550
|
+
source,
|
|
551
|
+
name: file.replace(/\.abap$/, ''),
|
|
552
|
+
}, { localOnly: true });
|
|
553
|
+
if (!outcome.ok)
|
|
554
|
+
return;
|
|
555
|
+
const lintExitCode = evaluateLint(outcome.value, opts.failOn);
|
|
556
|
+
if (lintExitCode === 3) {
|
|
557
|
+
console.error('SAPLint returned malformed or incomplete structured evidence; no report was emitted.');
|
|
558
|
+
runtime.state.exitCode = 3;
|
|
559
|
+
return;
|
|
560
|
+
}
|
|
561
|
+
const report = opts.format === 'json'
|
|
562
|
+
? JSON.stringify(outcome.value, null, 2)
|
|
563
|
+
: opts.format === 'checkstyle'
|
|
564
|
+
? lintToCheckstyle(outcome.value, file)
|
|
565
|
+
: formatLintText(outcome.value);
|
|
566
|
+
if (!(await emitCiReport(report, opts.reportFile))) {
|
|
567
|
+
runtime.state.exitCode = 1;
|
|
568
|
+
return;
|
|
569
|
+
}
|
|
570
|
+
runtime.state.exitCode = lintExitCode;
|
|
571
|
+
});
|
|
572
|
+
program
|
|
573
|
+
.command('version')
|
|
574
|
+
.description('Show ARC-1 version')
|
|
575
|
+
.action(() => console.log(`ARC-1 v${VERSION}`));
|
|
576
|
+
const configCommand = program.command('config').description('Configuration inspection');
|
|
577
|
+
configCommand
|
|
578
|
+
.command('show')
|
|
579
|
+
.description('Show the resolved effective safety policy with source attribution')
|
|
580
|
+
.addOption(new Option('--format <format>', 'Output format').choices(['table', 'json']).default('table'))
|
|
581
|
+
.action((opts) => {
|
|
582
|
+
const { config, sources } = runtime.getResolvedConfig();
|
|
583
|
+
if (opts.format === 'json') {
|
|
584
|
+
console.log(JSON.stringify({
|
|
222
585
|
effectivePolicy: {
|
|
223
|
-
allowWrites:
|
|
224
|
-
allowDataPreview:
|
|
225
|
-
allowFreeSQL:
|
|
226
|
-
allowTransportWrites:
|
|
227
|
-
allowGitWrites:
|
|
228
|
-
allowedPackages:
|
|
229
|
-
allowedTransports:
|
|
230
|
-
denyActions:
|
|
586
|
+
allowWrites: config.allowWrites,
|
|
587
|
+
allowDataPreview: config.allowDataPreview,
|
|
588
|
+
allowFreeSQL: config.allowFreeSQL,
|
|
589
|
+
allowTransportWrites: config.allowTransportWrites,
|
|
590
|
+
allowGitWrites: config.allowGitWrites,
|
|
591
|
+
allowedPackages: config.allowedPackages,
|
|
592
|
+
allowedTransports: config.allowedTransports,
|
|
593
|
+
denyActions: config.denyActions,
|
|
231
594
|
},
|
|
232
595
|
sources,
|
|
233
|
-
};
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
596
|
+
}, null, 2));
|
|
597
|
+
return;
|
|
598
|
+
}
|
|
599
|
+
console.log('ARC-1 effective authorization policy');
|
|
600
|
+
console.log('────────────────────────────────────');
|
|
601
|
+
const fields = [
|
|
602
|
+
['allowWrites', config.allowWrites],
|
|
603
|
+
['allowDataPreview', config.allowDataPreview],
|
|
604
|
+
['allowFreeSQL', config.allowFreeSQL],
|
|
605
|
+
['allowTransportWrites', config.allowTransportWrites],
|
|
606
|
+
['allowGitWrites', config.allowGitWrites],
|
|
607
|
+
['allowedPackages', JSON.stringify(config.allowedPackages)],
|
|
608
|
+
['allowedTransports', JSON.stringify(config.allowedTransports)],
|
|
609
|
+
];
|
|
610
|
+
for (const [name, value] of fields) {
|
|
611
|
+
console.log(` ${name.padEnd(22)} = ${String(value).padEnd(30)} [${formatConfigSource(sources[name])}]`);
|
|
612
|
+
}
|
|
613
|
+
console.log('\nDeny actions:');
|
|
614
|
+
if (config.denyActions.length === 0) {
|
|
615
|
+
console.log(` (none) [${formatConfigSource(sources.denyActions)}]`);
|
|
240
616
|
}
|
|
241
617
|
else {
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
const fields = [
|
|
245
|
-
['allowWrites', serverConfig.allowWrites],
|
|
246
|
-
['allowDataPreview', serverConfig.allowDataPreview],
|
|
247
|
-
['allowFreeSQL', serverConfig.allowFreeSQL],
|
|
248
|
-
['allowTransportWrites', serverConfig.allowTransportWrites],
|
|
249
|
-
['allowGitWrites', serverConfig.allowGitWrites],
|
|
250
|
-
['allowedPackages', JSON.stringify(serverConfig.allowedPackages)],
|
|
251
|
-
['allowedTransports', JSON.stringify(serverConfig.allowedTransports)],
|
|
252
|
-
];
|
|
253
|
-
for (const [name, value] of fields) {
|
|
254
|
-
const src = formatConfigSource(sources[name]);
|
|
255
|
-
console.log(` ${name.padEnd(22)} = ${String(value).padEnd(30)} [${src}]`);
|
|
256
|
-
}
|
|
257
|
-
console.log('\nDeny actions:');
|
|
258
|
-
if (serverConfig.denyActions.length === 0) {
|
|
259
|
-
console.log(` (none) [${formatConfigSource(sources.denyActions)}]`);
|
|
260
|
-
}
|
|
261
|
-
else {
|
|
262
|
-
const src = formatConfigSource(sources.denyActions);
|
|
263
|
-
for (const pattern of serverConfig.denyActions) {
|
|
264
|
-
console.log(` ${pattern} [${src}]`);
|
|
265
|
-
}
|
|
618
|
+
for (const pattern of config.denyActions) {
|
|
619
|
+
console.log(` ${pattern} [${formatConfigSource(sources.denyActions)}]`);
|
|
266
620
|
}
|
|
267
621
|
}
|
|
268
|
-
|
|
269
|
-
}
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
// comes from resolveConfig() parser failures — config-validation errors
|
|
273
|
-
// like "invalid --api-keys format", not the credential VALUE itself.
|
|
274
|
-
// Pinned by tests/unit/cli/clear-text-logging-regression.test.ts.
|
|
275
|
-
console.error(`Error: ${err.message}`);
|
|
276
|
-
process.exit(1);
|
|
277
|
-
}
|
|
278
|
-
});
|
|
279
|
-
function formatConfigSource(s) {
|
|
280
|
-
if (s === undefined)
|
|
281
|
-
return 'default';
|
|
282
|
-
if (s === 'default')
|
|
283
|
-
return 'default';
|
|
284
|
-
if (typeof s === 'object') {
|
|
285
|
-
if ('env' in s)
|
|
286
|
-
return `env ${s.env}`;
|
|
287
|
-
if ('flag' in s)
|
|
288
|
-
return `flag ${s.flag}`;
|
|
289
|
-
if ('file' in s)
|
|
290
|
-
return `file ${s.file}`;
|
|
291
|
-
}
|
|
292
|
-
return 'unknown';
|
|
622
|
+
});
|
|
623
|
+
Object.defineProperty(program, '__arc1Dispose', { value: runtime.dispose });
|
|
624
|
+
Object.defineProperty(program, '__arc1Flush', { value: runtime.deps.flushLogger });
|
|
625
|
+
return program;
|
|
293
626
|
}
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
627
|
+
/**
|
|
628
|
+
* Execute a configured direct tool call and return the unrendered MCP result.
|
|
629
|
+
* Dedicated CI commands reuse this to apply domain-specific exit/report policy
|
|
630
|
+
* while retaining the normal dispatcher, validation, safety, and audit path.
|
|
631
|
+
*/
|
|
632
|
+
export async function executeCliToolCall(runtime, toolName, args, options = {}) {
|
|
633
|
+
const { config } = runtime.getResolvedConfig();
|
|
634
|
+
const localOnly = options.localOnly === true || isLocalOnlyCall(toolName, args);
|
|
635
|
+
await runtime.loadConfiguredPlugins(config);
|
|
636
|
+
if (!getToolRegistry().get(toolName)) {
|
|
637
|
+
return {
|
|
638
|
+
kind: 'usage',
|
|
639
|
+
message: `Unknown tool: ${toolName}`,
|
|
640
|
+
knownTools: getToolRegistry()
|
|
641
|
+
.list()
|
|
642
|
+
.map((entry) => entry.name),
|
|
643
|
+
};
|
|
298
644
|
}
|
|
299
|
-
|
|
300
|
-
const
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
645
|
+
if (!localOnly) {
|
|
646
|
+
const unsupported = directModeError(config);
|
|
647
|
+
if (unsupported)
|
|
648
|
+
return { kind: 'usage', message: unsupported };
|
|
649
|
+
}
|
|
650
|
+
const direct = await runtime.getDirectContext();
|
|
651
|
+
if (!localOnly && !isDisabledWriteTool(toolName, config)) {
|
|
652
|
+
const preflightStartedAt = Date.now();
|
|
653
|
+
const preflight = await runtime.deps.authPreflight(config, direct.client);
|
|
654
|
+
if (preflight.blocking) {
|
|
655
|
+
auditBlockedDirectPreflight(config, toolName, args, preflightStartedAt);
|
|
656
|
+
return { kind: 'tool', result: errorResult(formatStartupAuthPreflightToolError(preflight)) };
|
|
657
|
+
}
|
|
658
|
+
if (config.url && !shouldSkipFeatureProbe(toolName, args)) {
|
|
659
|
+
try {
|
|
660
|
+
await runtime.deps.probeFeatures(config, direct.client);
|
|
661
|
+
}
|
|
662
|
+
catch (err) {
|
|
663
|
+
const featureKey = config.targetId ?? config.destinationName;
|
|
664
|
+
const emptyDiscovery = new Map();
|
|
665
|
+
setCachedFeatures(undefined, featureKey);
|
|
666
|
+
setCachedDiscovery(emptyDiscovery, featureKey);
|
|
667
|
+
direct.client.http.setDiscoveryMap(emptyDiscovery);
|
|
668
|
+
logger.debug('Direct CLI feature probe failed; continuing with unknown feature evidence', {
|
|
669
|
+
error: errorMessage(err),
|
|
670
|
+
});
|
|
671
|
+
}
|
|
304
672
|
}
|
|
305
673
|
}
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
const serverConfig = parseArgs([]);
|
|
310
|
-
initLogger(serverConfig.logFormat, serverConfig.verbose);
|
|
311
|
-
const adtConfig = buildAdtConfig(serverConfig);
|
|
312
|
-
const client = new AdtClient(adtConfig);
|
|
313
|
-
return { client, config: serverConfig };
|
|
314
|
-
}
|
|
315
|
-
async function runToolCall(toolName, args, outputMode) {
|
|
316
|
-
const { client, config: serverConfig } = resolveCliContext();
|
|
317
|
-
// FEAT-61: load extension plugins so `arc1-cli call Custom_*` reaches the same registry the server uses.
|
|
318
|
-
if (serverConfig.plugins?.length) {
|
|
319
|
-
await loadPlugins(serverConfig.plugins, getToolRegistry());
|
|
674
|
+
try {
|
|
675
|
+
const result = await runtime.deps.dispatchToolCall(direct.client, config, toolName, args, undefined, undefined, direct.cachingLayer);
|
|
676
|
+
return { kind: 'tool', result };
|
|
320
677
|
}
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
if (e.source === 'plugin')
|
|
324
|
-
available.add(e.name);
|
|
678
|
+
catch (err) {
|
|
679
|
+
return { kind: 'tool', result: errorResult(errorMessage(err)) };
|
|
325
680
|
}
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
681
|
+
}
|
|
682
|
+
async function runToolCall(runtime, toolName, args, outputMode) {
|
|
683
|
+
const outcome = await executeCliToolCall(runtime, toolName, args);
|
|
684
|
+
if (outcome.kind === 'usage') {
|
|
685
|
+
console.error(outcome.message);
|
|
686
|
+
if (outcome.knownTools)
|
|
687
|
+
console.error(`Known tools: ${outcome.knownTools.join(', ')}`);
|
|
329
688
|
return 2;
|
|
330
689
|
}
|
|
690
|
+
return renderToolResult(outcome.result, outputMode);
|
|
691
|
+
}
|
|
692
|
+
/** Parse one CLI invocation and return a deterministic process exit code. */
|
|
693
|
+
export async function main(argv = process.argv.slice(2), dependencies = {}) {
|
|
694
|
+
const state = { exitCode: 0 };
|
|
695
|
+
const program = createCliProgram({ argv, dependencies, state });
|
|
331
696
|
try {
|
|
332
|
-
|
|
333
|
-
|
|
697
|
+
assertNoRemovedCliFlags(configArgv(argv));
|
|
698
|
+
await program.parseAsync([...argv], { from: 'user' });
|
|
699
|
+
return state.exitCode;
|
|
334
700
|
}
|
|
335
701
|
catch (err) {
|
|
336
|
-
|
|
337
|
-
|
|
702
|
+
if (err instanceof CommanderError) {
|
|
703
|
+
return err.exitCode === 0 ? 0 : 2;
|
|
704
|
+
}
|
|
705
|
+
// Configuration/argument preparation failures are usage errors. Tool and
|
|
706
|
+
// SAP failures are converted to ToolResult/exit 1 inside their action.
|
|
707
|
+
console.error(errorMessage(err));
|
|
708
|
+
return 2;
|
|
338
709
|
}
|
|
710
|
+
finally {
|
|
711
|
+
await program.__arc1Dispose?.();
|
|
712
|
+
if (!state.longLivedServerStarted) {
|
|
713
|
+
await (program.__arc1Flush?.() ?? logger.flush());
|
|
714
|
+
}
|
|
715
|
+
}
|
|
716
|
+
}
|
|
717
|
+
// Keep `npm run cli -- ...` working without making imports execute a command.
|
|
718
|
+
if (process.argv[1] && import.meta.url === pathToFileURL(process.argv[1]).href) {
|
|
719
|
+
process.exitCode = await main(process.argv.slice(2));
|
|
339
720
|
}
|
|
340
|
-
program.parse();
|
|
341
721
|
//# sourceMappingURL=cli.js.map
|