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.
Files changed (166) hide show
  1. package/bin/arc1-cli.js +3 -7
  2. package/bin/arc1.js +4 -9
  3. package/dist/adt/abapgit.d.ts +10 -5
  4. package/dist/adt/abapgit.d.ts.map +1 -1
  5. package/dist/adt/abapgit.js +357 -76
  6. package/dist/adt/abapgit.js.map +1 -1
  7. package/dist/adt/atc.d.ts +49 -0
  8. package/dist/adt/atc.d.ts.map +1 -0
  9. package/dist/adt/atc.js +294 -0
  10. package/dist/adt/atc.js.map +1 -0
  11. package/dist/adt/aunit-package.d.ts +22 -0
  12. package/dist/adt/aunit-package.d.ts.map +1 -0
  13. package/dist/adt/aunit-package.js +78 -0
  14. package/dist/adt/aunit-package.js.map +1 -0
  15. package/dist/adt/aunit.d.ts +143 -0
  16. package/dist/adt/aunit.d.ts.map +1 -0
  17. package/dist/adt/aunit.js +987 -0
  18. package/dist/adt/aunit.js.map +1 -0
  19. package/dist/adt/client.d.ts +9 -7
  20. package/dist/adt/client.d.ts.map +1 -1
  21. package/dist/adt/client.js +55 -64
  22. package/dist/adt/client.js.map +1 -1
  23. package/dist/adt/config.d.ts +2 -0
  24. package/dist/adt/config.d.ts.map +1 -1
  25. package/dist/adt/config.js +1 -0
  26. package/dist/adt/config.js.map +1 -1
  27. package/dist/adt/devtools.d.ts +8 -30
  28. package/dist/adt/devtools.d.ts.map +1 -1
  29. package/dist/adt/devtools.js +41 -218
  30. package/dist/adt/devtools.js.map +1 -1
  31. package/dist/adt/diagnostics.d.ts +1 -1
  32. package/dist/adt/diagnostics.d.ts.map +1 -1
  33. package/dist/adt/diagnostics.js +12 -12
  34. package/dist/adt/diagnostics.js.map +1 -1
  35. package/dist/adt/errors.d.ts +7 -0
  36. package/dist/adt/errors.d.ts.map +1 -1
  37. package/dist/adt/errors.js +7 -0
  38. package/dist/adt/errors.js.map +1 -1
  39. package/dist/adt/features.d.ts +9 -3
  40. package/dist/adt/features.d.ts.map +1 -1
  41. package/dist/adt/features.js +40 -8
  42. package/dist/adt/features.js.map +1 -1
  43. package/dist/adt/gcts.d.ts +12 -45
  44. package/dist/adt/gcts.d.ts.map +1 -1
  45. package/dist/adt/gcts.js +299 -171
  46. package/dist/adt/gcts.js.map +1 -1
  47. package/dist/adt/http-deadline.d.ts +23 -0
  48. package/dist/adt/http-deadline.d.ts.map +1 -0
  49. package/dist/adt/http-deadline.js +106 -0
  50. package/dist/adt/http-deadline.js.map +1 -0
  51. package/dist/adt/http-wire-body.d.ts +2 -0
  52. package/dist/adt/http-wire-body.d.ts.map +1 -0
  53. package/dist/adt/http-wire-body.js +14 -0
  54. package/dist/adt/http-wire-body.js.map +1 -0
  55. package/dist/adt/http.d.ts +10 -24
  56. package/dist/adt/http.d.ts.map +1 -1
  57. package/dist/adt/http.js +89 -53
  58. package/dist/adt/http.js.map +1 -1
  59. package/dist/adt/path-safety.d.ts +16 -0
  60. package/dist/adt/path-safety.d.ts.map +1 -0
  61. package/dist/adt/path-safety.js +101 -0
  62. package/dist/adt/path-safety.js.map +1 -0
  63. package/dist/adt/semaphore.d.ts +2 -2
  64. package/dist/adt/semaphore.d.ts.map +1 -1
  65. package/dist/adt/semaphore.js +26 -9
  66. package/dist/adt/semaphore.js.map +1 -1
  67. package/dist/adt/source-diff.d.ts +10 -1
  68. package/dist/adt/source-diff.d.ts.map +1 -1
  69. package/dist/adt/source-diff.js +33 -7
  70. package/dist/adt/source-diff.js.map +1 -1
  71. package/dist/adt/text-search.d.ts +25 -0
  72. package/dist/adt/text-search.d.ts.map +1 -0
  73. package/dist/adt/text-search.js +152 -0
  74. package/dist/adt/text-search.js.map +1 -0
  75. package/dist/adt/transport-diff.d.ts +128 -0
  76. package/dist/adt/transport-diff.d.ts.map +1 -0
  77. package/dist/adt/transport-diff.js +524 -0
  78. package/dist/adt/transport-diff.js.map +1 -0
  79. package/dist/adt/transport.d.ts +78 -13
  80. package/dist/adt/transport.d.ts.map +1 -1
  81. package/dist/adt/transport.js +443 -41
  82. package/dist/adt/transport.js.map +1 -1
  83. package/dist/adt/types.d.ts +23 -12
  84. package/dist/adt/types.d.ts.map +1 -1
  85. package/dist/adt/xml-parser.d.ts +1 -8
  86. package/dist/adt/xml-parser.d.ts.map +1 -1
  87. package/dist/adt/xml-parser.js +54 -60
  88. package/dist/adt/xml-parser.js.map +1 -1
  89. package/dist/authz/policy.d.ts.map +1 -1
  90. package/dist/authz/policy.js +5 -2
  91. package/dist/authz/policy.js.map +1 -1
  92. package/dist/cli-args.d.ts +2 -2
  93. package/dist/cli-args.js +2 -2
  94. package/dist/cli-checks.d.ts +56 -0
  95. package/dist/cli-checks.d.ts.map +1 -0
  96. package/dist/cli-checks.js +368 -0
  97. package/dist/cli-checks.js.map +1 -0
  98. package/dist/cli.d.ts +68 -15
  99. package/dist/cli.d.ts.map +1 -1
  100. package/dist/cli.js +677 -297
  101. package/dist/cli.js.map +1 -1
  102. package/dist/handlers/cds-hints.d.ts.map +1 -1
  103. package/dist/handlers/cds-hints.js +1 -10
  104. package/dist/handlers/cds-hints.js.map +1 -1
  105. package/dist/handlers/diagnose.d.ts +4 -0
  106. package/dist/handlers/diagnose.d.ts.map +1 -1
  107. package/dist/handlers/diagnose.js +616 -9
  108. package/dist/handlers/diagnose.js.map +1 -1
  109. package/dist/handlers/dispatch.d.ts.map +1 -1
  110. package/dist/handlers/dispatch.js +40 -1
  111. package/dist/handlers/dispatch.js.map +1 -1
  112. package/dist/handlers/git.d.ts.map +1 -1
  113. package/dist/handlers/git.js +165 -84
  114. package/dist/handlers/git.js.map +1 -1
  115. package/dist/handlers/read.d.ts.map +1 -1
  116. package/dist/handlers/read.js +15 -0
  117. package/dist/handlers/read.js.map +1 -1
  118. package/dist/handlers/schemas.d.ts +15 -2
  119. package/dist/handlers/schemas.d.ts.map +1 -1
  120. package/dist/handlers/schemas.js +75 -6
  121. package/dist/handlers/schemas.js.map +1 -1
  122. package/dist/handlers/search.js +1 -1
  123. package/dist/handlers/search.js.map +1 -1
  124. package/dist/handlers/tools.d.ts.map +1 -1
  125. package/dist/handlers/tools.js +51 -42
  126. package/dist/handlers/tools.js.map +1 -1
  127. package/dist/handlers/transport.d.ts +2 -1
  128. package/dist/handlers/transport.d.ts.map +1 -1
  129. package/dist/handlers/transport.js +128 -27
  130. package/dist/handlers/transport.js.map +1 -1
  131. package/dist/handlers/write/create.d.ts.map +1 -1
  132. package/dist/handlers/write/create.js +11 -0
  133. package/dist/handlers/write/create.js.map +1 -1
  134. package/dist/handlers/write/update-delete.d.ts.map +1 -1
  135. package/dist/handlers/write/update-delete.js +33 -10
  136. package/dist/handlers/write/update-delete.js.map +1 -1
  137. package/dist/handlers/write-helpers.d.ts.map +1 -1
  138. package/dist/handlers/write-helpers.js +3 -2
  139. package/dist/handlers/write-helpers.js.map +1 -1
  140. package/dist/server/audit.d.ts +1 -1
  141. package/dist/server/audit.d.ts.map +1 -1
  142. package/dist/server/audit.js +228 -21
  143. package/dist/server/audit.js.map +1 -1
  144. package/dist/server/config.d.ts +16 -0
  145. package/dist/server/config.d.ts.map +1 -1
  146. package/dist/server/config.js +145 -17
  147. package/dist/server/config.js.map +1 -1
  148. package/dist/server/effective-policy-log.d.ts +2 -1
  149. package/dist/server/effective-policy-log.d.ts.map +1 -1
  150. package/dist/server/effective-policy-log.js +8 -2
  151. package/dist/server/effective-policy-log.js.map +1 -1
  152. package/dist/server/multi-target-runtime.d.ts.map +1 -1
  153. package/dist/server/multi-target-runtime.js +3 -0
  154. package/dist/server/multi-target-runtime.js.map +1 -1
  155. package/dist/server/multi-target-tools.d.ts.map +1 -1
  156. package/dist/server/multi-target-tools.js +5 -0
  157. package/dist/server/multi-target-tools.js.map +1 -1
  158. package/dist/server/server.d.ts +15 -13
  159. package/dist/server/server.d.ts.map +1 -1
  160. package/dist/server/server.js +36 -26
  161. package/dist/server/server.js.map +1 -1
  162. package/dist/server/types.d.ts +2 -0
  163. package/dist/server/types.d.ts.map +1 -1
  164. package/dist/server/types.js +1 -0
  165. package/dist/server/types.js.map +1 -1
  166. package/package.json +13 -13
package/dist/cli.js CHANGED
@@ -1,341 +1,721 @@
1
1
  /**
2
- * ARC-1 CLI — command-line interface for SAP ADT operations.
2
+ * ARC-1 command-line interface.
3
3
  *
4
- * Exposed via the `arc1-cli` bin (separate from `arc1`, which is the MCP server entry).
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 { Command, Option } from 'commander';
21
- import { config } from 'dotenv';
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 { detectFilename, lintAbapSource } from './lint/lint.js';
26
- import { parseArgs, resolveConfig } from './server/config.js';
27
- import { initLogger } from './server/logger.js';
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
- // Load .env without printing dotenv tips to stdout.
31
- config({ quiet: true });
32
- const program = new Command();
33
- program
34
- .name('arc1')
35
- .description('ARC-1 — MCP Server for SAP ABAP Systems')
36
- .version(VERSION)
37
- .allowUnknownOption(true)
38
- .allowExcessArguments(true);
39
- // Server mode (default)
40
- program
41
- .command('serve', { isDefault: true })
42
- .description('Start MCP server (default)')
43
- .allowUnknownOption(true)
44
- .allowExcessArguments(true)
45
- .action(async () => {
46
- // Dynamic import to avoid loading MCP SDK for CLI-only usage
47
- const { createAndStartServer } = await import('./server/server.js');
48
- const serverConfig = parseArgs(process.argv.slice(2));
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
- program
57
- .command('call <tool>')
58
- .description('Call any MCP tool directly (e.g. SAPRead, SAPWrite, SAPGit...)')
59
- .option('--arg <key=value>', 'Tool argument; repeatable. Values are coerced: true/false/number/JSON.', collect, [])
60
- .option('--json <source>', 'JSON args: inline object, path to a file, or "-" for stdin')
61
- .addOption(outputOption)
62
- .action(async (tool, opts) => {
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
- const args = buildArgs(opts);
65
- const code = await runToolCall(tool, args, opts.output);
66
- process.exit(code);
75
+ return JSON.parse(text);
67
76
  }
68
- catch (err) {
69
- // codeql[js/clear-text-logging]: false-positive (alert #9). err.message
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
- program
78
- .command('tools [tool]')
79
- .description('List MCP tools, or show the JSON input schema for a specific tool')
80
- .action(async (tool) => {
81
- const { config: serverConfig } = resolveCliContext();
82
- // FEAT-61: load plugins so `tools` discovery matches `call` invocation (both see Custom_* tools).
83
- if (serverConfig.plugins?.length) {
84
- await loadPlugins(serverConfig.plugins, getToolRegistry());
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
- const pluginDefs = getToolRegistry()
87
- .list()
88
- .flatMap((e) => e.source === 'plugin' && e.listing
89
- ? [{ name: e.name, description: e.listing.description, inputSchema: e.listing.inputSchema }]
90
- : []);
91
- const defs = [...getConfiguredToolDefinitions(serverConfig), ...pluginDefs];
92
- if (!tool) {
93
- for (const def of defs) {
94
- const firstLine = def.description.split('\n')[0].trim();
95
- console.log(`${def.name.padEnd(14)} ${firstLine}`);
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
- return;
108
+ else
109
+ console.log(content);
110
+ return true;
98
111
  }
99
- const match = defs.find((d) => d.name.toLowerCase() === tool.toLowerCase());
100
- if (!match) {
101
- console.error(`Unknown tool: ${tool}`);
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
- console.log(match.description);
106
- console.log('');
107
- console.log('Input schema:');
108
- console.log(JSON.stringify(match.inputSchema, null, 2));
109
- });
110
- // ─── Ergonomic shortcuts (thin wrappers over `call`) ───────────────────
111
- program
112
- .command('read <type> <name>')
113
- .description('Read an ABAP object via SAPRead (PROG, CLAS, INTF, DDLS, TABL, DOMA, DTEL, ...)')
114
- .option('--flat', 'Return flat source for CLAS/INTF (instead of structured sections)')
115
- .option('--source-version <version>', 'Source version: active (default) | inactive | auto. "auto" returns the user\'s draft if any, else active.')
116
- .addOption(outputOption)
117
- .action(async (type, name, opts) => {
118
- const args = { type: type.toUpperCase(), name };
119
- // `--flat` predates `format`; it means "raw source, not decomposed sections" = format:"text",
120
- // which is already the default. It used to be sent as a phantom `flat` arg that no handler read
121
- // and Zod silently stripped — now that the schemas are strict, send the real parameter.
122
- if (opts.flat)
123
- args.format = 'text';
124
- if (opts.sourceVersion)
125
- args.version = opts.sourceVersion;
126
- process.exit(await runToolCall('SAPRead', args, opts.output));
127
- });
128
- // `source` kept as an alias of `read --flat` to preserve legacy CLI behavior.
129
- program
130
- .command('source <type> <name>')
131
- .description('Alias of `read --flat` (legacy)')
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
- await run(forwarded);
144
+ const parsed = new URL(sapUrl);
145
+ if (parsed.protocol !== 'http:' && parsed.protocol !== 'https:')
146
+ throw new Error('unsupported protocol');
179
147
  }
180
- catch (err) {
181
- const message = err instanceof Error ? err.message : String(err);
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
- program
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
- for (const issue of issues) {
198
- console.log(`${issue.line}:${issue.column} [${issue.severity}] ${issue.rule}: ${issue.message}`);
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
- process.exit(issues.some((i) => i.severity === 'error') ? 1 : 0);
201
- });
202
- program
203
- .command('version')
204
- .description('Show ARC-1 version')
205
- .action(() => {
206
- console.log(`ARC-1 v${VERSION}`);
207
- });
208
- // Config show command — dumps resolved effective policy + source attribution
209
- const configCmd = program.command('config').description('Configuration inspection');
210
- configCmd
211
- .command('show')
212
- .description('Show the resolved effective safety config with per-field source attribution')
213
- .option('--format <fmt>', 'Output format: table or json', 'table')
214
- .allowUnknownOption(true)
215
- .allowExcessArguments(true)
216
- .action((opts) => {
217
- try {
218
- const { config: serverConfig, sources } = resolveConfig(process.argv.slice(3));
219
- const fmt = opts.format === 'json' ? 'json' : 'table';
220
- if (fmt === 'json') {
221
- const out = {
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: serverConfig.allowWrites,
224
- allowDataPreview: serverConfig.allowDataPreview,
225
- allowFreeSQL: serverConfig.allowFreeSQL,
226
- allowTransportWrites: serverConfig.allowTransportWrites,
227
- allowGitWrites: serverConfig.allowGitWrites,
228
- allowedPackages: serverConfig.allowedPackages,
229
- allowedTransports: serverConfig.allowedTransports,
230
- denyActions: serverConfig.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
- // codeql[js/clear-text-logging]: false-positive (alert #10). `out` is
235
- // constructed explicitly with only the `allow*` policy flags and
236
- // `sources` (field-source attributions like `'env SAP_URL'`). Neither
237
- // `apiKeys` / `apiKeysRaw` nor `oauthDcrTtlSeconds` appear in `out`.
238
- // Pinned by tests/unit/cli/clear-text-logging-regression.test.ts.
239
- console.log(JSON.stringify(out, null, 2));
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
- console.log('ARC-1 effective authorization policy');
243
- console.log('────────────────────────────────────');
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
- process.exit(0);
269
- }
270
- catch (err) {
271
- // codeql[js/clear-text-logging]: false-positive (alert #11). err.message
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
- // ─── Helpers ───────────────────────────────────────────────────────────
295
- function renderToolResult(result, mode) {
296
- if (mode === 'json') {
297
- console.log(JSON.stringify(result, null, 2));
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
- else {
300
- const stream = result.isError ? console.error : console.log;
301
- for (const block of result.content) {
302
- if (block.type === 'text')
303
- stream(block.text);
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
- return result.isError ? 1 : 0;
307
- }
308
- function resolveCliContext() {
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
- const available = new Set(getConfiguredToolDefinitions(serverConfig).map((t) => t.name));
322
- for (const e of getToolRegistry().list()) {
323
- if (e.source === 'plugin')
324
- available.add(e.name);
678
+ catch (err) {
679
+ return { kind: 'tool', result: errorResult(errorMessage(err)) };
325
680
  }
326
- if (!available.has(toolName)) {
327
- console.error(`Unknown tool: ${toolName}`);
328
- console.error(`Available tools: ${[...available].join(', ')}`);
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
- const result = await handleToolCall(client, serverConfig, toolName, args);
333
- return renderToolResult(result, outputMode);
697
+ assertNoRemovedCliFlags(configArgv(argv));
698
+ await program.parseAsync([...argv], { from: 'user' });
699
+ return state.exitCode;
334
700
  }
335
701
  catch (err) {
336
- console.error(err instanceof Error ? err.message : String(err));
337
- return 1;
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