apcore-cli 0.6.0 → 0.7.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/dist/index.js CHANGED
@@ -1,11 +1,5 @@
1
1
  var __defProp = Object.defineProperty;
2
2
  var __getOwnPropNames = Object.getOwnPropertyNames;
3
- var __require = /* @__PURE__ */ ((x) => typeof require !== "undefined" ? require : typeof Proxy !== "undefined" ? new Proxy(x, {
4
- get: (a, b) => (typeof require !== "undefined" ? require : a)[b]
5
- }) : x)(function(x) {
6
- if (typeof require !== "undefined") return require.apply(this, arguments);
7
- throw Error('Dynamic require of "' + x + '" is not supported');
8
- });
9
3
  var __esm = (fn, res) => function __init() {
10
4
  return fn && (res = (0, fn[__getOwnPropNames(fn)[0]])(fn = 0)), res;
11
5
  };
@@ -24,34 +18,36 @@ var init_esm_shims = __esm({
24
18
  });
25
19
 
26
20
  // src/errors.ts
27
- function exitCodeForError(error2) {
28
- if (error2 instanceof ApprovalTimeoutError) {
21
+ function exitCodeForError(error) {
22
+ if (error instanceof ApprovalTimeoutError) {
29
23
  return EXIT_CODES.APPROVAL_TIMEOUT;
30
24
  }
31
- if (error2 instanceof ApprovalDeniedError) {
25
+ if (error instanceof ApprovalDeniedError) {
32
26
  return EXIT_CODES.APPROVAL_DENIED;
33
27
  }
34
- if (error2 instanceof AuthenticationError) {
28
+ if (error instanceof AuthenticationError) {
35
29
  return EXIT_CODES.ACL_DENIED;
36
30
  }
37
- if (error2 instanceof ConfigDecryptionError) {
31
+ if (error instanceof ConfigDecryptionError) {
38
32
  return EXIT_CODES.CONFIG_INVALID;
39
33
  }
40
- if (error2 instanceof SchemaValidationError) {
34
+ if (error instanceof SchemaValidationError) {
41
35
  return EXIT_CODES.SCHEMA_VALIDATION_ERROR;
42
36
  }
43
- if (error2 instanceof ModuleNotFoundError) {
37
+ if (error instanceof ModuleNotFoundError) {
44
38
  return EXIT_CODES.MODULE_NOT_FOUND;
45
39
  }
46
- if (error2 instanceof ModuleExecutionError) {
40
+ if (error instanceof ModuleExecutionError) {
47
41
  return EXIT_CODES.MODULE_EXECUTE_ERROR;
48
42
  }
49
- if (error2 instanceof Error) {
50
- const code = error2.code;
43
+ if (error instanceof Error) {
44
+ const code = error.code;
51
45
  const codeMap = {
52
46
  MODULE_NOT_FOUND: EXIT_CODES.MODULE_NOT_FOUND,
53
47
  MODULE_LOAD_ERROR: EXIT_CODES.MODULE_LOAD_ERROR,
54
48
  MODULE_DISABLED: EXIT_CODES.MODULE_DISABLED,
49
+ DEPENDENCY_NOT_FOUND: EXIT_CODES.DEPENDENCY_NOT_FOUND,
50
+ DEPENDENCY_VERSION_MISMATCH: EXIT_CODES.DEPENDENCY_VERSION_MISMATCH,
55
51
  SCHEMA_VALIDATION_ERROR: EXIT_CODES.SCHEMA_VALIDATION_ERROR,
56
52
  SCHEMA_CIRCULAR_REF: EXIT_CODES.SCHEMA_CIRCULAR_REF,
57
53
  APPROVAL_DENIED: EXIT_CODES.APPROVAL_DENIED,
@@ -66,6 +62,7 @@ function exitCodeForError(error2) {
66
62
  CONFIG_NAMESPACE_RESERVED: EXIT_CODES.CONFIG_NAMESPACE_RESERVED,
67
63
  CONFIG_NAMESPACE_DUPLICATE: EXIT_CODES.CONFIG_NAMESPACE_DUPLICATE,
68
64
  CONFIG_ENV_PREFIX_CONFLICT: EXIT_CODES.CONFIG_ENV_PREFIX_CONFLICT,
65
+ CONFIG_ENV_MAP_CONFLICT: EXIT_CODES.CONFIG_ENV_MAP_CONFLICT,
69
66
  CONFIG_MOUNT_ERROR: EXIT_CODES.CONFIG_MOUNT_ERROR,
70
67
  CONFIG_BIND_ERROR: EXIT_CODES.CONFIG_BIND_ERROR,
71
68
  ERROR_FORMATTER_DUPLICATE: EXIT_CODES.ERROR_FORMATTER_DUPLICATE
@@ -131,6 +128,8 @@ var init_errors = __esm({
131
128
  MODULE_NOT_FOUND: 44,
132
129
  MODULE_LOAD_ERROR: 44,
133
130
  MODULE_DISABLED: 44,
131
+ DEPENDENCY_NOT_FOUND: 44,
132
+ DEPENDENCY_VERSION_MISMATCH: 44,
134
133
  SCHEMA_VALIDATION_ERROR: 45,
135
134
  APPROVAL_DENIED: 46,
136
135
  APPROVAL_TIMEOUT: 46,
@@ -151,43 +150,88 @@ var init_errors = __esm({
151
150
  }
152
151
  });
153
152
 
153
+ // src/logger.ts
154
+ function setLogLevel(level) {
155
+ const upper = level.toUpperCase();
156
+ if (upper in LEVELS) {
157
+ currentLevel = upper;
158
+ }
159
+ }
160
+ function getLogLevel() {
161
+ return currentLevel;
162
+ }
163
+ function shouldLog(level) {
164
+ return LEVELS[level] >= LEVELS[currentLevel];
165
+ }
166
+ function warn(message) {
167
+ if (shouldLog("WARNING")) process.stderr.write(`WARNING: ${message}
168
+ `);
169
+ }
170
+ var LEVELS, currentLevel;
171
+ var init_logger = __esm({
172
+ "src/logger.ts"() {
173
+ "use strict";
174
+ init_esm_shims();
175
+ LEVELS = { DEBUG: 0, INFO: 1, WARNING: 2, ERROR: 3 };
176
+ currentLevel = "WARNING";
177
+ }
178
+ });
179
+
154
180
  // src/security/audit.ts
155
181
  var audit_exports = {};
156
182
  __export(audit_exports, {
157
183
  AuditLogger: () => AuditLogger,
184
+ canonicalizeForHash: () => canonicalizeForHash,
158
185
  getAuditLogger: () => getAuditLogger,
159
186
  setAuditLogger: () => setAuditLogger
160
187
  });
161
188
  import * as crypto from "crypto";
162
189
  import * as fs3 from "fs";
163
190
  import * as os from "os";
164
- import * as path4 from "path";
191
+ import * as path3 from "path";
165
192
  function setAuditLogger(auditLogger) {
166
193
  _auditLogger = auditLogger;
167
194
  }
168
195
  function getAuditLogger() {
169
196
  return _auditLogger;
170
197
  }
198
+ function canonicalizeForHash(value) {
199
+ if (value === null || typeof value !== "object") return value;
200
+ if (Array.isArray(value)) return value.map(canonicalizeForHash);
201
+ const src = value;
202
+ const sorted = {};
203
+ for (const key of Object.keys(src).sort()) {
204
+ sorted[key] = canonicalizeForHash(src[key]);
205
+ }
206
+ return sorted;
207
+ }
171
208
  var _auditLogger, AuditLogger;
172
209
  var init_audit = __esm({
173
210
  "src/security/audit.ts"() {
174
211
  "use strict";
175
212
  init_esm_shims();
213
+ init_logger();
176
214
  _auditLogger = null;
177
215
  AuditLogger = class _AuditLogger {
178
- static DEFAULT_PATH = path4.join(
216
+ static DEFAULT_PATH = path3.join(
179
217
  os.homedir(),
180
218
  ".apcore-cli",
181
219
  "audit.jsonl"
182
220
  );
183
221
  logPath;
184
- constructor(path7) {
185
- this.logPath = path7 ?? _AuditLogger.DEFAULT_PATH;
222
+ writeFailureWarned = false;
223
+ constructor(path5) {
224
+ this.logPath = path5 ?? _AuditLogger.DEFAULT_PATH;
186
225
  this.ensureDirectory();
187
226
  }
188
227
  ensureDirectory() {
228
+ const dir = path3.dirname(this.logPath);
189
229
  try {
190
- fs3.mkdirSync(path4.dirname(this.logPath), { recursive: true });
230
+ fs3.mkdirSync(dir, { recursive: true });
231
+ try {
232
+ fs3.chmodSync(dir, 448);
233
+ } catch {
234
+ }
191
235
  } catch {
192
236
  }
193
237
  }
@@ -203,14 +247,20 @@ var init_audit = __esm({
203
247
  };
204
248
  try {
205
249
  fs3.appendFileSync(this.logPath, JSON.stringify(entry) + "\n");
250
+ try {
251
+ fs3.chmodSync(this.logPath, 384);
252
+ } catch {
253
+ }
206
254
  } catch (err) {
207
- console.warn(`Could not write audit log: ${err}`);
255
+ if (!this.writeFailureWarned) {
256
+ this.writeFailureWarned = true;
257
+ warn(`Could not write audit log: ${err}`);
258
+ }
208
259
  }
209
260
  }
210
261
  hashInput(inputData) {
211
262
  const salt = crypto.randomBytes(16);
212
- const sortedKeys = Object.keys(inputData).sort();
213
- const payload = JSON.stringify(inputData, sortedKeys);
263
+ const payload = JSON.stringify(canonicalizeForHash(inputData));
214
264
  return crypto.createHash("sha256").update(Buffer.concat([salt, Buffer.from(payload, "utf-8")])).digest("hex");
215
265
  }
216
266
  getUser() {
@@ -236,17 +286,34 @@ async function getKeytar() {
236
286
  return null;
237
287
  }
238
288
  }
239
- var keytarModule, ConfigEncryptor;
289
+ var PBKDF2_ITERATIONS, V1_STATIC_SALT, keytarModule, ConfigEncryptor;
240
290
  var init_config_encryptor = __esm({
241
291
  "src/security/config-encryptor.ts"() {
242
292
  "use strict";
243
293
  init_esm_shims();
244
294
  init_errors();
295
+ init_logger();
296
+ PBKDF2_ITERATIONS = 6e5;
297
+ V1_STATIC_SALT = Buffer.from("apcore-cli-config-v1");
245
298
  keytarModule = null;
246
299
  ConfigEncryptor = class _ConfigEncryptor {
247
300
  static SERVICE_NAME = "apcore-cli";
301
+ // One-shot flag so the "obfuscation only" warning fires exactly once
302
+ // per process instead of once per encrypt/decrypt call.
303
+ static weakFallbackWarned = false;
248
304
  /**
249
305
  * Encrypt and store a configuration value.
306
+ *
307
+ * Cross-SDK contract (D10-003, 2026-04-26): when the OS keyring is
308
+ * detected as available but `setPassword` then throws (locked keyring,
309
+ * transient backend failure, permission revoked, etc.), the error is
310
+ * propagated wrapped in a `ConfigDecryptionError`. Previously TS
311
+ * caught the exception and silently fell through to AES file encryption
312
+ * — a quiet downgrade that surprised users who expected a hard failure.
313
+ * Python lets the keyring exception propagate raw; Rust returns
314
+ * `ConfigDecryptionError::KeyringError`. The fall-through to AES is
315
+ * still reached when `getKeytar()` returns `null` (keyring
316
+ * genuinely unavailable on this platform / install).
250
317
  */
251
318
  async store(key, value) {
252
319
  const keytar = await getKeytar();
@@ -254,12 +321,16 @@ var init_config_encryptor = __esm({
254
321
  try {
255
322
  await keytar.setPassword(_ConfigEncryptor.SERVICE_NAME, key, value);
256
323
  return `keyring:${key}`;
257
- } catch {
324
+ } catch (err) {
325
+ const detail = err instanceof Error ? err.message : String(err);
326
+ throw new ConfigDecryptionError(
327
+ `Failed to store '${key}' in OS keyring: ${detail}. Unset APCORE_CLI_CONFIG_PASSPHRASE-aware backends or unlock the keyring before retrying.`
328
+ );
258
329
  }
259
330
  }
260
- console.warn("OS keyring unavailable. Using file-based encryption.");
331
+ warn("OS keyring unavailable. Using file-based encryption.");
261
332
  const ciphertext = this.aesEncrypt(value);
262
- return `enc:${Buffer.from(ciphertext).toString("base64")}`;
333
+ return `enc:v2:${ciphertext.toString("base64")}`;
263
334
  }
264
335
  /**
265
336
  * Retrieve and decrypt a configuration value.
@@ -291,13 +362,20 @@ var init_config_encryptor = __esm({
291
362
  );
292
363
  }
293
364
  }
365
+ if (configValue.startsWith("enc:v2:")) {
366
+ const data = Buffer.from(configValue.slice("enc:v2:".length), "base64");
367
+ try {
368
+ return this.aesDecrypt(data);
369
+ } catch {
370
+ throw new ConfigDecryptionError(
371
+ `Failed to decrypt configuration value '${key}'. Re-configure with 'apcore-cli config set ${key}'.`
372
+ );
373
+ }
374
+ }
294
375
  if (configValue.startsWith("enc:")) {
295
- const ciphertext = Buffer.from(
296
- configValue.slice("enc:".length),
297
- "base64"
298
- );
376
+ const data = Buffer.from(configValue.slice("enc:".length), "base64");
299
377
  try {
300
- return this.aesDecrypt(ciphertext);
378
+ return this.aesDecryptV1(data);
301
379
  } catch {
302
380
  throw new ConfigDecryptionError(
303
381
  `Failed to decrypt configuration value '${key}'. Re-configure with 'apcore-cli config set ${key}'.`
@@ -306,18 +384,35 @@ var init_config_encryptor = __esm({
306
384
  }
307
385
  return configValue;
308
386
  }
309
- // NOTE: Best-effort fallback when OS keyring is unavailable.
310
- // The key is derived from hostname + username (non-secret inputs).
311
- // For production security, ensure the OS keyring is accessible.
312
- deriveKey() {
387
+ // Derive an AES-256 key with a provided salt (v2 format).
388
+ //
389
+ // Order of preference:
390
+ // 1. APCORE_CLI_CONFIG_PASSPHRASE env var — a real secret supplied by
391
+ // the user; produces a key an attacker with filesystem read cannot
392
+ // reconstruct without also knowing the passphrase.
393
+ // 2. hostname + username — obfuscation-only derivation for backward
394
+ // compatibility. Emits a loud stderr warning on first use so
395
+ // operators know the stored value is NOT protected against a
396
+ // filesystem-read attacker.
397
+ deriveKey(salt) {
398
+ const passphrase = process.env.APCORE_CLI_CONFIG_PASSPHRASE;
399
+ if (passphrase && passphrase.length > 0) {
400
+ return crypto2.pbkdf2Sync(passphrase, salt, PBKDF2_ITERATIONS, 32, "sha256");
401
+ }
402
+ if (!_ConfigEncryptor.weakFallbackWarned) {
403
+ warn(
404
+ "APCORE_CLI_CONFIG_PASSPHRASE is not set. The `enc:v2:` fallback uses a key derived from hostname+username (non-secret inputs) and is OBFUSCATION ONLY \u2014 an attacker with filesystem read access can reconstruct the key. Set APCORE_CLI_CONFIG_PASSPHRASE or ensure the OS keyring is available for real encryption."
405
+ );
406
+ _ConfigEncryptor.weakFallbackWarned = true;
407
+ }
313
408
  const hostname2 = os2.hostname();
314
409
  const username = process.env.USER ?? process.env.USERNAME ?? "unknown";
315
- const salt = Buffer.from("apcore-cli-config-v1");
316
410
  const material = `${hostname2}:${username}`;
317
- return crypto2.pbkdf2Sync(material, salt, 1e5, 32, "sha256");
411
+ return crypto2.pbkdf2Sync(material, salt, PBKDF2_ITERATIONS, 32, "sha256");
318
412
  }
319
413
  aesEncrypt(plaintext) {
320
- const key = this.deriveKey();
414
+ const salt = crypto2.randomBytes(16);
415
+ const key = this.deriveKey(salt);
321
416
  const nonce = crypto2.randomBytes(12);
322
417
  const cipher = crypto2.createCipheriv("aes-256-gcm", key, nonce);
323
418
  const ct = Buffer.concat([
@@ -325,17 +420,39 @@ var init_config_encryptor = __esm({
325
420
  cipher.final()
326
421
  ]);
327
422
  const tag = cipher.getAuthTag();
328
- return Buffer.concat([nonce, tag, ct]);
423
+ return Buffer.concat([salt, nonce, tag, ct]);
329
424
  }
330
425
  aesDecrypt(data) {
331
- const key = this.deriveKey();
426
+ const salt = data.subarray(0, 16);
427
+ const nonce = data.subarray(16, 28);
428
+ const tag = data.subarray(28, 44);
429
+ const ct = data.subarray(44);
430
+ const key = this.deriveKey(salt);
431
+ const decipher = crypto2.createDecipheriv("aes-256-gcm", key, nonce);
432
+ decipher.setAuthTag(tag);
433
+ return Buffer.concat([decipher.update(ct), decipher.final()]).toString("utf-8");
434
+ }
435
+ /** Decrypt legacy v1-format values: nonce(12)+tag(16)+ct, static salt. */
436
+ aesDecryptV1(data) {
332
437
  const nonce = data.subarray(0, 12);
333
438
  const tag = data.subarray(12, 28);
334
439
  const ct = data.subarray(28);
335
- const decipher = crypto2.createDecipheriv("aes-256-gcm", key, nonce);
336
- decipher.setAuthTag(tag);
337
- const plaintext = Buffer.concat([decipher.update(ct), decipher.final()]);
338
- return plaintext.toString("utf-8");
440
+ const hostname2 = os2.hostname();
441
+ const username = process.env.USER ?? process.env.USERNAME ?? "unknown";
442
+ const passphrase = process.env.APCORE_CLI_CONFIG_PASSPHRASE;
443
+ const materials = passphrase ? [passphrase, `${hostname2}:${username}`] : [`${hostname2}:${username}`];
444
+ for (const material of materials) {
445
+ for (const iterations of [6e5, 1e5]) {
446
+ try {
447
+ const key = crypto2.pbkdf2Sync(material, V1_STATIC_SALT, iterations, 32, "sha256");
448
+ const decipher = crypto2.createDecipheriv("aes-256-gcm", key, nonce);
449
+ decipher.setAuthTag(tag);
450
+ return Buffer.concat([decipher.update(ct), decipher.final()]).toString("utf-8");
451
+ } catch {
452
+ }
453
+ }
454
+ }
455
+ throw new Error("v1 decryption failed with all material+iteration combinations");
339
456
  }
340
457
  };
341
458
  }
@@ -371,12 +488,30 @@ var init_auth = __esm({
371
488
  }
372
489
  const strResult = String(result);
373
490
  if (strResult.startsWith("keyring:") || strResult.startsWith("enc:")) {
374
- return this.encryptor.retrieve(strResult, "auth.api_key");
491
+ try {
492
+ return await this.encryptor.retrieve(strResult, "auth.api_key");
493
+ } catch (err) {
494
+ if (err instanceof ConfigDecryptionError) {
495
+ throw new AuthenticationError(
496
+ "Failed to decrypt stored API key. Re-configure with 'apcore-cli config set auth.api_key'."
497
+ );
498
+ }
499
+ throw err;
500
+ }
375
501
  }
376
502
  return strResult;
377
503
  }
378
504
  /**
379
505
  * Add authentication headers to an outgoing request.
506
+ *
507
+ * Cross-SDK contract (D10-002, 2026-04-26): the input `headers` object
508
+ * is mutated **in place** and the same reference is returned. Callers
509
+ * that share the headers reference (the documented pattern in
510
+ * apcore-cli/docs/features/security.md §AuthProvider) can read
511
+ * `headers.Authorization` after the call without re-binding the
512
+ * return value. Python and Rust both mutate-and-return; TS previously
513
+ * spread into a new object, which silently broke shared-reference
514
+ * callers.
380
515
  */
381
516
  async authenticateRequest(headers) {
382
517
  const key = await this.getApiKey();
@@ -385,7 +520,13 @@ var init_auth = __esm({
385
520
  "Remote registry requires authentication. Set --api-key, APCORE_AUTH_API_KEY, or auth.api_key in config."
386
521
  );
387
522
  }
388
- return { ...headers, Authorization: `Bearer ${key}` };
523
+ if (/[\r\n]/.test(key)) {
524
+ throw new AuthenticationError(
525
+ "Malformed API key: contains invalid characters (CR/LF). Re-configure with 'apcore-cli config set auth.api_key'."
526
+ );
527
+ }
528
+ headers.Authorization = `Bearer ${key.trim()}`;
529
+ return headers;
389
530
  }
390
531
  /**
391
532
  * Handle an HTTP response status code for auth-related errors.
@@ -402,20 +543,37 @@ var init_auth = __esm({
402
543
  });
403
544
 
404
545
  // src/security/sandbox.ts
405
- import * as child_process from "child_process";
406
- import * as fs4 from "fs";
407
- import * as os3 from "os";
408
- import * as path5 from "path";
409
- var Sandbox;
546
+ function buildSandboxEnv(tmpDir) {
547
+ const env = {};
548
+ for (const key of SANDBOX_ALLOW_KEYS) {
549
+ if (process.env[key]) env[key] = process.env[key];
550
+ }
551
+ for (const [key, val] of Object.entries(process.env)) {
552
+ if (key.startsWith(SANDBOX_ALLOW_PREFIX) && !key.startsWith(SANDBOX_DENY_PREFIX) && !SANDBOX_DENY_KEYS.includes(key)) {
553
+ env[key] = val;
554
+ }
555
+ }
556
+ env.HOME = tmpDir;
557
+ env.TMPDIR = tmpDir;
558
+ return env;
559
+ }
560
+ var SANDBOX_ALLOW_KEYS, SANDBOX_ALLOW_PREFIX, SANDBOX_DENY_PREFIX, SANDBOX_DENY_KEYS, SANDBOX_OUTPUT_SIZE_LIMIT, Sandbox;
410
561
  var init_sandbox = __esm({
411
562
  "src/security/sandbox.ts"() {
412
563
  "use strict";
413
564
  init_esm_shims();
414
565
  init_errors();
566
+ SANDBOX_ALLOW_KEYS = ["PATH", "LANG", "LC_ALL"];
567
+ SANDBOX_ALLOW_PREFIX = "APCORE_";
568
+ SANDBOX_DENY_PREFIX = "APCORE_AUTH_";
569
+ SANDBOX_DENY_KEYS = ["APCORE_AUTH_API_KEY"];
570
+ SANDBOX_OUTPUT_SIZE_LIMIT = 64 * 1024 * 1024;
415
571
  Sandbox = class {
416
572
  enabled;
417
- constructor(enabled = false) {
573
+ timeoutSeconds;
574
+ constructor(enabled = false, timeoutSeconds = 300) {
418
575
  this.enabled = enabled;
576
+ this.timeoutSeconds = timeoutSeconds;
419
577
  }
420
578
  /**
421
579
  * Execute a module, optionally inside a sandboxed subprocess.
@@ -424,63 +582,77 @@ var init_sandbox = __esm({
424
582
  if (!this.enabled) {
425
583
  return executor.execute(moduleId, inputData);
426
584
  }
427
- return this.sandboxedExecute(moduleId, inputData);
585
+ return this._sandboxedExecute(moduleId, inputData);
428
586
  }
429
- sandboxedExecute(moduleId, inputData) {
430
- const env = {};
431
- for (const key of ["PATH", "NODE_PATH", "LANG", "LC_ALL"]) {
432
- if (process.env[key]) {
433
- env[key] = process.env[key];
434
- }
435
- }
436
- for (const [key, value] of Object.entries(process.env)) {
437
- if (key.startsWith("APCORE_") && value) {
438
- env[key] = value;
587
+ async _sandboxedExecute(moduleId, inputData) {
588
+ const { spawn } = await import("child_process");
589
+ const { tmpdir } = await import("os");
590
+ const { join: join3 } = await import("path");
591
+ const { mkdtempSync, rmSync } = await import("fs");
592
+ const tmpDir = mkdtempSync(join3(tmpdir(), "apcore_sandbox_"));
593
+ const env = buildSandboxEnv(tmpDir);
594
+ const binaryPath = process.argv[1];
595
+ const child = spawn(process.execPath, [binaryPath, "--internal-sandbox-runner", moduleId], {
596
+ env,
597
+ cwd: tmpDir,
598
+ stdio: ["pipe", "pipe", "pipe"]
599
+ });
600
+ let stdout = "";
601
+ let stderr = "";
602
+ let totalBytes = 0;
603
+ let sizeExceeded = false;
604
+ child.stdout.on("data", (chunk) => {
605
+ totalBytes += chunk.length;
606
+ if (totalBytes > SANDBOX_OUTPUT_SIZE_LIMIT) {
607
+ sizeExceeded = true;
608
+ child.kill("SIGKILL");
609
+ return;
439
610
  }
440
- }
441
- const tmpDir = fs4.mkdtempSync(
442
- path5.join(os3.tmpdir(), "apcore_sandbox_")
443
- );
444
- try {
445
- env.HOME = tmpDir;
446
- env.TMPDIR = tmpDir;
447
- const script = [
448
- "let d='';",
449
- "process.stdin.setEncoding('utf-8');",
450
- "process.stdin.on('data',c=>d+=c);",
451
- "process.stdin.on('end',()=>{",
452
- " const input=JSON.parse(d);",
453
- ` process.stdout.write(JSON.stringify({error:"Sandbox runner not yet implemented for module: ${moduleId}"}));`,
454
- "});"
455
- ].join("");
456
- const result = child_process.execFileSync(
457
- process.execPath,
458
- ["-e", script],
459
- {
460
- input: JSON.stringify(inputData),
461
- env,
462
- cwd: tmpDir,
463
- timeout: 3e5,
464
- maxBuffer: 10 * 1024 * 1024
465
- }
466
- );
467
- return JSON.parse(result.toString("utf-8"));
468
- } catch (err) {
469
- if (err instanceof Error && "killed" in err && err.killed) {
470
- throw new ModuleExecutionError(
471
- `Error: Module '${moduleId}' timed out in sandbox.`
611
+ stdout += chunk.toString();
612
+ });
613
+ child.stderr.on("data", (chunk) => {
614
+ stderr += chunk.toString();
615
+ });
616
+ child.stdin.write(JSON.stringify(inputData));
617
+ child.stdin.end();
618
+ return new Promise((resolve2, reject) => {
619
+ const timer = setTimeout(() => {
620
+ child.kill("SIGKILL");
621
+ reject(
622
+ new ModuleExecutionError(
623
+ `Sandbox module '${moduleId}' timed out after ${this.timeoutSeconds}s.`
624
+ )
472
625
  );
473
- }
474
- const stderr = err instanceof Error && "stderr" in err ? String(err.stderr) : String(err);
475
- throw new ModuleExecutionError(
476
- `Error: Module '${moduleId}' execution failed: ${stderr}`
477
- );
478
- } finally {
479
- try {
480
- fs4.rmSync(tmpDir, { recursive: true, force: true });
481
- } catch {
482
- }
483
- }
626
+ }, this.timeoutSeconds * 1e3);
627
+ child.on("close", (code) => {
628
+ clearTimeout(timer);
629
+ try {
630
+ rmSync(tmpDir, { recursive: true, force: true });
631
+ } catch {
632
+ }
633
+ if (sizeExceeded) {
634
+ reject(new ModuleExecutionError(`Sandbox module '${moduleId}' output exceeded 64MiB limit.`));
635
+ return;
636
+ }
637
+ if (code !== 0) {
638
+ reject(new ModuleExecutionError(
639
+ `Sandbox module '${moduleId}' exited with code ${code}.${stderr ? ` stderr: ${stderr}` : ""}`
640
+ ));
641
+ return;
642
+ }
643
+ try {
644
+ resolve2(JSON.parse(stdout));
645
+ } catch {
646
+ reject(new ModuleExecutionError(
647
+ `Sandbox module '${moduleId}' returned non-JSON output: ${stdout.slice(0, 200)}`
648
+ ));
649
+ }
650
+ });
651
+ child.on("error", (err) => {
652
+ clearTimeout(timer);
653
+ reject(new ModuleExecutionError(`Failed to spawn sandbox process: ${err.message}`));
654
+ });
655
+ });
484
656
  }
485
657
  };
486
658
  }
@@ -513,10 +685,10 @@ init_esm_shims();
513
685
  // src/main.ts
514
686
  init_esm_shims();
515
687
  init_errors();
516
- import { readFileSync as readFileSync3 } from "fs";
517
- import { fileURLToPath as fileURLToPath3 } from "url";
518
- import * as path6 from "path";
519
- import { Command as Command6, CommanderError, Option as Option4 } from "commander";
688
+ import { readFileSync as readFileSync2 } from "fs";
689
+ import { fileURLToPath as fileURLToPath2 } from "url";
690
+ import * as path4 from "path";
691
+ import { Command as Command5, CommanderError, Option as Option4 } from "commander";
520
692
 
521
693
  // src/ref-resolver.ts
522
694
  init_esm_shims();
@@ -575,6 +747,12 @@ function resolveNode(node, defs, visited, depth, maxDepth, moduleId) {
575
747
  properties: {},
576
748
  required: []
577
749
  };
750
+ if (typeof obj.properties === "object" && obj.properties !== null) {
751
+ Object.assign(merged.properties, obj.properties);
752
+ }
753
+ if (Array.isArray(obj.required)) {
754
+ merged.required.push(...obj.required);
755
+ }
578
756
  for (const subSchema of obj.allOf) {
579
757
  const resolved = resolveNode(
580
758
  subSchema,
@@ -698,7 +876,21 @@ function extractHelp(propSchema, maxLength = 1e3) {
698
876
  }
699
877
  return text;
700
878
  }
701
- var RESERVED_NAMES = /* @__PURE__ */ new Set(["input", "yes", "large_input", "format", "sandbox"]);
879
+ var RESERVED_NAMES = /* @__PURE__ */ new Set([
880
+ "input",
881
+ "yes",
882
+ "large_input",
883
+ "format",
884
+ "fields",
885
+ "sandbox",
886
+ "verbose",
887
+ "dry_run",
888
+ "trace",
889
+ "stream",
890
+ "strategy",
891
+ "approval_timeout",
892
+ "approval_token"
893
+ ]);
702
894
  function schemaToCliOptions(schema, maxHelpLength = 1e3) {
703
895
  const properties = schema.properties ?? {};
704
896
  const requiredList = schema.required ?? [];
@@ -706,21 +898,21 @@ function schemaToCliOptions(schema, maxHelpLength = 1e3) {
706
898
  const flagNames = {};
707
899
  for (const [propName, propSchema] of Object.entries(properties)) {
708
900
  const flagName = "--" + propName.replace(/_/g, "-");
709
- if (flagName in flagNames) {
901
+ if (RESERVED_NAMES.has(propName)) {
710
902
  process.stderr.write(
711
- `Error: Flag name collision: properties '${propName}' and '${flagNames[flagName]}' both map to '${flagName}'.
903
+ `Error: Module schema property '${propName}' conflicts with a reserved CLI option name. Rename the property.
712
904
  `
713
905
  );
714
906
  process.exit(EXIT_CODES.INVALID_CLI_INPUT);
715
907
  }
716
- flagNames[flagName] = propName;
717
- if (RESERVED_NAMES.has(propName)) {
908
+ if (flagName in flagNames) {
718
909
  process.stderr.write(
719
- `Error: Module schema property '${propName}' conflicts with a reserved CLI option name. Rename the property.
910
+ `Error: Flag name collision: properties '${propName}' and '${flagNames[flagName]}' both map to '${flagName}'.
720
911
  `
721
912
  );
722
913
  process.exit(EXIT_CODES.INVALID_CLI_INPUT);
723
914
  }
915
+ flagNames[flagName] = propName;
724
916
  const typeResult = mapType(propName, propSchema);
725
917
  const isRequired = requiredList.includes(propName);
726
918
  const helpBase = extractHelp(propSchema, maxHelpLength);
@@ -728,6 +920,15 @@ function schemaToCliOptions(schema, maxHelpLength = 1e3) {
728
920
  const defaultValue = propSchema.default;
729
921
  if (typeResult === BOOLEAN_FLAG) {
730
922
  const flagBase = propName.replace(/_/g, "-");
923
+ const noFlag = `--no-${flagBase}`;
924
+ if (noFlag in flagNames) {
925
+ process.stderr.write(
926
+ `Error: Flag name collision: boolean property '${propName}' auto-generates '${noFlag}' which is already used by property '${flagNames[noFlag]}'.
927
+ `
928
+ );
929
+ process.exit(EXIT_CODES.INVALID_CLI_INPUT);
930
+ }
931
+ flagNames[noFlag] = propName;
731
932
  const defaultVal = propSchema.default ?? false;
732
933
  options.push({
733
934
  name: propName,
@@ -806,12 +1007,20 @@ function getAnnotation(annotations, key, defaultValue = void 0) {
806
1007
  const ann = annotations;
807
1008
  return key in ann ? ann[key] : defaultValue;
808
1009
  }
1010
+ function readTimeoutFromEnv() {
1011
+ const raw = process.env.APCORE_CLI_APPROVAL_TIMEOUT;
1012
+ if (raw === void 0 || raw === "") return void 0;
1013
+ const parsed = parseInt(raw, 10);
1014
+ if (!Number.isFinite(parsed) || parsed <= 0) return void 0;
1015
+ return parsed;
1016
+ }
809
1017
  var CliApprovalHandler = class {
810
1018
  autoApprove;
811
1019
  timeout;
812
- constructor(autoApprove = false, timeout = 60) {
1020
+ constructor(autoApprove = false, timeout) {
813
1021
  this.autoApprove = autoApprove;
814
- this.timeout = Math.max(1, Math.min(timeout, 3600));
1022
+ const resolved = timeout ?? readTimeoutFromEnv() ?? 60;
1023
+ this.timeout = Math.max(1, Math.min(resolved, 3600));
815
1024
  }
816
1025
  async requestApproval(request) {
817
1026
  const moduleId = request.module_id ?? "unknown";
@@ -822,6 +1031,12 @@ var CliApprovalHandler = class {
822
1031
  if (envVal === "1") {
823
1032
  return { status: "approved", approved_by: "env_auto_approve" };
824
1033
  }
1034
+ if (envVal !== "" && envVal !== "1") {
1035
+ process.stderr.write(
1036
+ `Warning: APCORE_CLI_AUTO_APPROVE is set to '${envVal}', expected '1'. Ignoring.
1037
+ `
1038
+ );
1039
+ }
825
1040
  if (!process.stdin.isTTY) {
826
1041
  return { status: "rejected", reason: "Non-interactive session without --yes" };
827
1042
  }
@@ -840,7 +1055,7 @@ var CliApprovalHandler = class {
840
1055
  return { status: "rejected", reason: "CLI does not support async approval polling" };
841
1056
  }
842
1057
  };
843
- async function checkApproval(moduleDef, autoApprove, timeout = 60) {
1058
+ async function checkApproval(moduleDef, autoApprove, timeout) {
844
1059
  const annotations = moduleDef.annotations;
845
1060
  let requiresApproval;
846
1061
  if (moduleDef.requiresApproval !== void 0) {
@@ -868,13 +1083,12 @@ async function checkApproval(moduleDef, autoApprove, timeout = 60) {
868
1083
  );
869
1084
  }
870
1085
  if (!process.stdin.isTTY) {
871
- process.stderr.write(
872
- `Error: Module '${moduleId}' requires approval but no interactive terminal is available. Use --yes or set APCORE_CLI_AUTO_APPROVE=1 to bypass.
873
- `
1086
+ throw new ApprovalDeniedError(
1087
+ `Module '${moduleId}' requires approval but no interactive terminal is available. Use --yes or set APCORE_CLI_AUTO_APPROVE=1 to bypass.`
874
1088
  );
875
- process.exit(EXIT_CODES.APPROVAL_DENIED);
876
1089
  }
877
- await promptWithTimeout(moduleDef, timeout);
1090
+ const effectiveTimeout = timeout ?? readTimeoutFromEnv() ?? 60;
1091
+ await promptWithTimeout(moduleDef, effectiveTimeout);
878
1092
  }
879
1093
  async function promptWithTimeout(moduleDef, timeout) {
880
1094
  timeout = Math.max(1, Math.min(timeout, 3600));
@@ -889,8 +1103,8 @@ async function promptWithTimeout(moduleDef, timeout) {
889
1103
  let timer;
890
1104
  try {
891
1105
  const answer = await Promise.race([
892
- new Promise((resolve3) => {
893
- rl.question("Proceed? [y/N] ", (ans) => resolve3(ans));
1106
+ new Promise((resolve2) => {
1107
+ rl.question("Proceed? [y/N] ", (ans) => resolve2(ans));
894
1108
  }),
895
1109
  new Promise((_, reject) => {
896
1110
  timer = setTimeout(() => {
@@ -905,17 +1119,9 @@ async function promptWithTimeout(moduleDef, timeout) {
905
1119
  if (normalized === "y" || normalized === "yes") {
906
1120
  return;
907
1121
  }
908
- process.stderr.write("Error: Approval denied.\n");
909
- process.exit(EXIT_CODES.APPROVAL_DENIED);
1122
+ throw new ApprovalDeniedError("Approval denied");
910
1123
  } catch (err) {
911
1124
  if (timer) clearTimeout(timer);
912
- if (err instanceof ApprovalTimeoutError) {
913
- process.stderr.write(
914
- `Error: Approval prompt timed out after ${timeout} seconds.
915
- `
916
- );
917
- process.exit(EXIT_CODES.APPROVAL_TIMEOUT);
918
- }
919
1125
  throw err;
920
1126
  } finally {
921
1127
  rl.close();
@@ -924,6 +1130,13 @@ async function promptWithTimeout(moduleDef, timeout) {
924
1130
 
925
1131
  // src/output.ts
926
1132
  init_esm_shims();
1133
+ init_errors();
1134
+ import yaml from "js-yaml";
1135
+ function csvCellString(value) {
1136
+ if (value === null || value === void 0) return "";
1137
+ if (typeof value === "object") return JSON.stringify(value);
1138
+ return String(value);
1139
+ }
927
1140
  function resolveFormat(explicitFormat) {
928
1141
  if (explicitFormat !== void 0) {
929
1142
  return explicitFormat;
@@ -947,7 +1160,7 @@ function formatTable(headers, rows) {
947
1160
  );
948
1161
  return [headerLine, sep2, ...dataLines].join("\n") + "\n";
949
1162
  }
950
- function formatModuleList(modules, format, filterTags, showDeps = false) {
1163
+ function formatModuleList(modules, format, filterTags, showDeps = false, exposureFilter) {
951
1164
  if (format === "table") {
952
1165
  if (modules.length === 0 && filterTags && filterTags.length > 0) {
953
1166
  process.stdout.write(
@@ -960,13 +1173,18 @@ function formatModuleList(modules, format, filterTags, showDeps = false) {
960
1173
  process.stdout.write("No modules found.\n");
961
1174
  return;
962
1175
  }
963
- const headers = showDeps ? ["ID", "Description", "Tags", "Deps"] : ["ID", "Description", "Tags"];
1176
+ const headers = ["ID", "Description", "Tags"];
1177
+ if (showDeps) headers.push("Deps");
1178
+ if (exposureFilter) headers.push("Exposure");
964
1179
  const rows = modules.map((m) => {
965
1180
  const base = [m.id, truncate(m.description, 80), (m.tags ?? []).join(", ")];
966
1181
  if (showDeps) {
967
1182
  const deps = m.dependencies;
968
1183
  base.push(String(Array.isArray(deps) ? deps.length : 0));
969
1184
  }
1185
+ if (exposureFilter) {
1186
+ base.push(exposureFilter.isExposed(m.id ?? "") ? "\u2713" : "\u2014");
1187
+ }
970
1188
  return base;
971
1189
  });
972
1190
  process.stdout.write(formatTable(headers, rows));
@@ -981,6 +1199,9 @@ function formatModuleList(modules, format, filterTags, showDeps = false) {
981
1199
  const deps = m.dependencies;
982
1200
  entry.dependency_count = Array.isArray(deps) ? deps.length : 0;
983
1201
  }
1202
+ if (exposureFilter) {
1203
+ entry.exposed = exposureFilter.isExposed(m.id ?? "");
1204
+ }
984
1205
  return entry;
985
1206
  });
986
1207
  process.stdout.write(JSON.stringify(result, null, 2) + "\n");
@@ -1101,42 +1322,21 @@ function formatExecResult(result, format, fields) {
1101
1322
  const obj = effective_result;
1102
1323
  const keys = Object.keys(obj);
1103
1324
  const header = keys.map(escapeCsvField).join(",");
1104
- const row = keys.map((k) => escapeCsvField(String(obj[k]))).join(",");
1325
+ const row = keys.map((k) => escapeCsvField(csvCellString(obj[k]))).join(",");
1105
1326
  process.stdout.write(header + "\n" + row + "\n");
1106
1327
  } else if (Array.isArray(effective_result) && effective_result.length > 0 && typeof effective_result[0] === "object") {
1107
1328
  const keys = Object.keys(effective_result[0]);
1108
1329
  const header = keys.map(escapeCsvField).join(",");
1109
1330
  const rows = effective_result.map((item) => {
1110
1331
  const obj = item;
1111
- return keys.map((k) => escapeCsvField(String(obj[k]))).join(",");
1332
+ return keys.map((k) => escapeCsvField(csvCellString(obj[k]))).join(",");
1112
1333
  });
1113
1334
  process.stdout.write(header + "\n" + rows.join("\n") + "\n");
1114
1335
  } else {
1115
1336
  process.stdout.write(JSON.stringify(effective_result) + "\n");
1116
1337
  }
1117
1338
  } else if (effective === "yaml") {
1118
- if (typeof effective_result === "object" && !Array.isArray(effective_result) && effective_result !== null) {
1119
- const obj = effective_result;
1120
- const lines = Object.entries(obj).map(([k, v]) => {
1121
- if (v === null || v === void 0) return `${k}: null`;
1122
- if (typeof v === "object") return `${k}: ${JSON.stringify(v)}`;
1123
- return `${k}: ${v}`;
1124
- });
1125
- process.stdout.write(lines.join("\n") + "\n");
1126
- } else if (Array.isArray(effective_result)) {
1127
- for (const item of effective_result) {
1128
- if (typeof item === "object" && item !== null) {
1129
- const obj = item;
1130
- const lines = Object.entries(obj).map(([k, v]) => ` ${k}: ${v}`);
1131
- process.stdout.write("- " + lines.join("\n ") + "\n");
1132
- } else {
1133
- process.stdout.write(`- ${item}
1134
- `);
1135
- }
1136
- }
1137
- } else {
1138
- process.stdout.write(String(effective_result) + "\n");
1139
- }
1339
+ process.stdout.write(yaml.dump(effective_result, { lineWidth: -1 }));
1140
1340
  } else if (effective === "jsonl") {
1141
1341
  if (Array.isArray(effective_result)) {
1142
1342
  for (const item of effective_result) {
@@ -1159,7 +1359,7 @@ function formatExecResult(result, format, fields) {
1159
1359
  }
1160
1360
  }
1161
1361
  function escapeCsvField(value) {
1162
- if (value.includes(",") || value.includes('"') || value.includes("\n")) {
1362
+ if (value.includes(",") || value.includes('"') || value.includes("\n") || value.includes("\r")) {
1163
1363
  return '"' + value.replace(/"/g, '""') + '"';
1164
1364
  }
1165
1365
  return value;
@@ -1169,7 +1369,9 @@ function formatPreflightResult(result, format) {
1169
1369
  if (resolved === "json" || !process.stdout.isTTY) {
1170
1370
  const payload = {
1171
1371
  valid: result.valid,
1172
- requires_approval: result.requires_approval,
1372
+ // JSON output key stays snake_case for cross-language CLI contract;
1373
+ // runtime read uses camelCase to match apcore-js PreflightResult.
1374
+ requires_approval: result.requiresApproval,
1173
1375
  checks: result.checks.map((c) => {
1174
1376
  const entry = { check: c.check, passed: c.passed };
1175
1377
  if (c.error !== void 0 && c.error !== null) {
@@ -1220,59 +1422,46 @@ Result: ${tag} (${errors} error(s), ${warnings} warning(s))
1220
1422
  }
1221
1423
  function firstFailedExitCode(result) {
1222
1424
  const checkToExit = {
1223
- module_id: 2,
1224
- module_lookup: 44,
1225
- call_chain: 1,
1226
- acl: 77,
1227
- schema: 45,
1228
- approval: 46,
1229
- module_preflight: 1
1425
+ module_id: EXIT_CODES.INVALID_CLI_INPUT,
1426
+ module_lookup: EXIT_CODES.MODULE_NOT_FOUND,
1427
+ call_chain: EXIT_CODES.MODULE_EXECUTE_ERROR,
1428
+ acl: EXIT_CODES.ACL_DENIED,
1429
+ schema: EXIT_CODES.SCHEMA_VALIDATION_ERROR,
1430
+ approval: EXIT_CODES.APPROVAL_DENIED,
1431
+ module_preflight: EXIT_CODES.MODULE_EXECUTE_ERROR
1230
1432
  };
1231
1433
  for (const check of result.checks) {
1232
1434
  if (!check.passed) {
1233
- return checkToExit[check.check] ?? 1;
1435
+ return checkToExit[check.check] ?? EXIT_CODES.MODULE_EXECUTE_ERROR;
1234
1436
  }
1235
1437
  }
1236
- return 1;
1438
+ return EXIT_CODES.MODULE_EXECUTE_ERROR;
1237
1439
  }
1238
1440
 
1239
- // src/logger.ts
1240
- init_esm_shims();
1241
- var LEVELS = { DEBUG: 0, INFO: 1, WARNING: 2, ERROR: 3 };
1242
- var currentLevel = "WARNING";
1243
- function setLogLevel(level) {
1244
- const upper = level.toUpperCase();
1245
- if (upper in LEVELS) {
1246
- currentLevel = upper;
1247
- }
1248
- }
1249
- function getLogLevel() {
1250
- return currentLevel;
1251
- }
1252
- function shouldLog(level) {
1253
- return LEVELS[level] >= LEVELS[currentLevel];
1254
- }
1255
- function debug(message) {
1256
- if (shouldLog("DEBUG")) process.stderr.write(`DEBUG: ${message}
1257
- `);
1258
- }
1259
- function info(message) {
1260
- if (shouldLog("INFO")) process.stderr.write(`INFO: ${message}
1261
- `);
1262
- }
1263
- function warn(message) {
1264
- if (shouldLog("WARNING")) process.stderr.write(`WARNING: ${message}
1265
- `);
1266
- }
1267
- function error(message) {
1268
- if (shouldLog("ERROR")) process.stderr.write(`ERROR: ${message}
1269
- `);
1270
- }
1441
+ // src/main.ts
1442
+ init_logger();
1271
1443
 
1272
1444
  // src/init-cmd.ts
1273
1445
  init_esm_shims();
1446
+ init_errors();
1274
1447
  import * as fs from "fs";
1275
1448
  import * as path2 from "path";
1449
+ function runFsOp(op, targetPath, fn, partial) {
1450
+ try {
1451
+ return fn();
1452
+ } catch (err) {
1453
+ const msg = err instanceof Error ? err.message : String(err);
1454
+ process.stderr.write(`Error: failed to ${op} ${targetPath}: ${msg}
1455
+ `);
1456
+ if (partial && partial.length > 0) {
1457
+ process.stderr.write(
1458
+ ` Partial scaffold left on disk \u2014 you may want to remove: ${partial.join(", ")}
1459
+ `
1460
+ );
1461
+ }
1462
+ process.exit(EXIT_CODES.MODULE_EXECUTE_ERROR);
1463
+ }
1464
+ }
1276
1465
  var DECORATOR_TEMPLATE = `import { module } from "apcore-js";
1277
1466
  import { Type } from "@sinclair/typebox";
1278
1467
 
@@ -1296,7 +1485,8 @@ export function {funcName}(): Record<string, unknown> {
1296
1485
  return { status: "ok" };
1297
1486
  }
1298
1487
  `;
1299
- var BINDING_TEMPLATE = `bindings:
1488
+ var BINDING_TEMPLATE = `spec_version: "1.0"
1489
+ bindings:
1300
1490
  - module_id: "{moduleId}"
1301
1491
  target: "{target}"
1302
1492
  description: "{description}"
@@ -1345,7 +1535,7 @@ function registerInitCommand(cli) {
1345
1535
  });
1346
1536
  }
1347
1537
  function createDecoratorModule(moduleId, _prefix, funcName, description, outputDir) {
1348
- fs.mkdirSync(outputDir, { recursive: true });
1538
+ runFsOp("create directory", outputDir, () => fs.mkdirSync(outputDir, { recursive: true }));
1349
1539
  const filename = moduleId.replace(/\./g, "_") + ".ts";
1350
1540
  const filepath = path2.join(outputDir, filename);
1351
1541
  const varName = funcName + "Module";
@@ -1355,14 +1545,14 @@ function createDecoratorModule(moduleId, _prefix, funcName, description, outputD
1355
1545
  funcName,
1356
1546
  description
1357
1547
  });
1358
- fs.writeFileSync(filepath, content);
1548
+ runFsOp("write file", filepath, () => fs.writeFileSync(filepath, content));
1359
1549
  process.stdout.write(`Created ${filepath}
1360
1550
  `);
1361
1551
  }
1362
1552
  function createConventionModule(moduleId, prefix, funcName, description, outputDir) {
1363
1553
  const prefixParts = prefix.split(".");
1364
1554
  const dirPath = prefixParts.length > 1 ? path2.join(outputDir, ...prefixParts.slice(0, -1)) : outputDir;
1365
- fs.mkdirSync(dirPath, { recursive: true });
1555
+ runFsOp("create directory", dirPath, () => fs.mkdirSync(dirPath, { recursive: true }));
1366
1556
  let filename;
1367
1557
  if (prefixParts.length > 1) {
1368
1558
  filename = prefixParts[prefixParts.length - 1] + ".ts";
@@ -1380,12 +1570,13 @@ function createConventionModule(moduleId, prefix, funcName, description, outputD
1380
1570
  description,
1381
1571
  cliGroupLine
1382
1572
  });
1383
- fs.writeFileSync(filepath, content);
1573
+ runFsOp("write file", filepath, () => fs.writeFileSync(filepath, content));
1384
1574
  process.stdout.write(`Created ${filepath}
1385
1575
  `);
1386
1576
  }
1387
1577
  function createBindingModule(moduleId, prefix, funcName, description, outputDir) {
1388
- fs.mkdirSync(outputDir, { recursive: true });
1578
+ const partial = [];
1579
+ runFsOp("create directory", outputDir, () => fs.mkdirSync(outputDir, { recursive: true }));
1389
1580
  const yamlFile = path2.join(outputDir, moduleId.replace(/\./g, "_") + ".binding.yaml");
1390
1581
  const target = `commands.${prefix}:${funcName}`;
1391
1582
  const yamlContent = renderTemplate(BINDING_TEMPLATE, {
@@ -1393,11 +1584,12 @@ function createBindingModule(moduleId, prefix, funcName, description, outputDir)
1393
1584
  target,
1394
1585
  description
1395
1586
  });
1396
- fs.writeFileSync(yamlFile, yamlContent);
1587
+ runFsOp("write file", yamlFile, () => fs.writeFileSync(yamlFile, yamlContent));
1588
+ partial.push(yamlFile);
1397
1589
  process.stdout.write(`Created ${yamlFile}
1398
1590
  `);
1399
1591
  const baseSrc = "commands";
1400
- fs.mkdirSync(baseSrc, { recursive: true });
1592
+ runFsOp("create directory", baseSrc, () => fs.mkdirSync(baseSrc, { recursive: true }), partial);
1401
1593
  const srcFile = path2.join(baseSrc, prefix.replace(/\./g, "_") + ".ts");
1402
1594
  if (!fs.existsSync(srcFile)) {
1403
1595
  const srcContent = `export function ${funcName}(): Record<string, unknown> {
@@ -1406,7 +1598,7 @@ function createBindingModule(moduleId, prefix, funcName, description, outputDir)
1406
1598
  return { status: "ok" };
1407
1599
  }
1408
1600
  `;
1409
- fs.writeFileSync(srcFile, srcContent);
1601
+ runFsOp("write file", srcFile, () => fs.writeFileSync(srcFile, srcContent), partial);
1410
1602
  process.stdout.write(`Created ${srcFile}
1411
1603
  `);
1412
1604
  }
@@ -1420,40 +1612,48 @@ function getDisplay(descriptor) {
1420
1612
  if (display && typeof display === "object" && !Array.isArray(display)) {
1421
1613
  return display;
1422
1614
  }
1423
- return {};
1424
- }
1425
- function getCliDisplayFields(descriptor) {
1426
- const display = getDisplay(descriptor);
1427
- const cli = display.cli && typeof display.cli === "object" && !Array.isArray(display.cli) ? display.cli : {};
1428
- const name = cli.alias ?? display.alias ?? descriptor.id;
1429
- const desc = cli.description ?? descriptor.description;
1430
- const tags = display.tags ?? descriptor.tags ?? [];
1431
- return [name, desc, tags];
1615
+ const overlay = lookupBindingDisplay(descriptor.id);
1616
+ return overlay ?? {};
1432
1617
  }
1433
1618
 
1434
1619
  // src/config.ts
1435
1620
  init_esm_shims();
1621
+ init_logger();
1436
1622
  import * as fs2 from "fs";
1437
- import yaml from "js-yaml";
1623
+ import { createRequire } from "module";
1624
+ import yaml2 from "js-yaml";
1438
1625
  var DEFAULTS = {
1439
1626
  "extensions.root": "./extensions",
1440
1627
  "logging.level": "WARNING",
1441
- "sandbox.enabled": false,
1442
- "cli.stdin_buffer_limit": 10485760,
1443
- "cli.auto_approve": false,
1444
1628
  "cli.help_text_max_length": 1e3,
1445
- // Namespace-mode aliases (apcore >= 0.15.0 Config Bus)
1446
- "apcore-cli.stdin_buffer_limit": 10485760,
1447
- "apcore-cli.auto_approve": false,
1448
- "apcore-cli.help_text_max_length": 1e3,
1449
- "apcore-cli.logging_level": "WARNING",
1450
1629
  // FE-11 config keys
1451
1630
  "cli.approval_timeout": 60,
1452
1631
  "cli.strategy": "standard",
1453
1632
  "cli.group_depth": 1,
1454
- "apcore-cli.approval_timeout": 60,
1455
- "apcore-cli.strategy": "standard",
1456
- "apcore-cli.group_depth": 1
1633
+ // Exposure filtering (FE-12)
1634
+ "expose.mode": "all",
1635
+ "expose.include": [],
1636
+ "expose.exclude": []
1637
+ // Builtin group visibility (FE-13) — apcli.* keys are NOT in DEFAULTS.
1638
+ // The runtime reads them via resolveObject('apcli') (raw yaml walk) and
1639
+ // does not use the flat-key resolve() path. Python and Rust have no such
1640
+ // entries either. (D11-008 cleanup)
1641
+ };
1642
+ var NAMESPACE_DEFAULTS = {
1643
+ stdin_buffer_limit: 10485760,
1644
+ auto_approve: false,
1645
+ help_text_max_length: 1e3,
1646
+ logging_level: "WARNING",
1647
+ approval_timeout: 60,
1648
+ strategy: "standard",
1649
+ group_depth: 1,
1650
+ // FE-13 — builtin group visibility configuration
1651
+ apcli: {
1652
+ mode: null,
1653
+ include: [],
1654
+ exclude: [],
1655
+ disable_env: false
1656
+ }
1457
1657
  };
1458
1658
  var NAMESPACE_TO_LEGACY = {
1459
1659
  "apcore-cli.stdin_buffer_limit": "cli.stdin_buffer_limit",
@@ -1466,20 +1666,13 @@ var LEGACY_TO_NAMESPACE = Object.fromEntries(
1466
1666
  );
1467
1667
  function registerConfigNamespace() {
1468
1668
  try {
1469
- const { Config } = __require("apcore-js");
1669
+ const nodeRequire = createRequire(import.meta.url);
1670
+ const { Config } = nodeRequire("apcore-js");
1470
1671
  if (typeof Config?.registerNamespace === "function") {
1471
1672
  Config.registerNamespace({
1472
1673
  name: "apcore-cli",
1473
1674
  envPrefix: "APCORE_CLI",
1474
- defaults: {
1475
- stdin_buffer_limit: 10485760,
1476
- auto_approve: false,
1477
- help_text_max_length: 1e3,
1478
- logging_level: "WARNING",
1479
- approval_timeout: 60,
1480
- strategy: "standard",
1481
- group_depth: 1
1482
- }
1675
+ defaults: NAMESPACE_DEFAULTS
1483
1676
  });
1484
1677
  }
1485
1678
  } catch {
@@ -1490,6 +1683,13 @@ var ConfigResolver = class {
1490
1683
  configPath;
1491
1684
  fileCache = null;
1492
1685
  fileCacheLoaded = false;
1686
+ /**
1687
+ * Raw parsed yaml root (pre-flatten). Populated alongside `fileCache`
1688
+ * on load. Used by `resolveObject()` to walk nested paths without
1689
+ * invoking `flattenDict` — see FE-13 spec §4.8 M1 note.
1690
+ * `null` when no config file is present or parsing fails.
1691
+ */
1692
+ _rawConfig = null;
1493
1693
  constructor(cliFlags, configPath) {
1494
1694
  this.cliFlags = cliFlags ?? {};
1495
1695
  this.configPath = configPath ?? "apcore.yaml";
@@ -1498,9 +1698,8 @@ var ConfigResolver = class {
1498
1698
  * Resolve a single configuration key across all four tiers.
1499
1699
  */
1500
1700
  resolve(key, cliFlag, envVar) {
1501
- const flagKey = cliFlag ?? key;
1502
- if (flagKey in this.cliFlags) {
1503
- const value = this.cliFlags[flagKey];
1701
+ if (cliFlag !== void 0 && cliFlag in this.cliFlags) {
1702
+ const value = this.cliFlags[cliFlag];
1504
1703
  if (value !== null && value !== void 0) {
1505
1704
  return value;
1506
1705
  }
@@ -1537,6 +1736,44 @@ var ConfigResolver = class {
1537
1736
  }
1538
1737
  return this.fileCache[key];
1539
1738
  }
1739
+ /**
1740
+ * Resolve a configuration key to its raw nested value (FE-13).
1741
+ *
1742
+ * Unlike `resolve()`, this method does NOT flatten the yaml tree — it
1743
+ * walks the dot-separated path directly against the parsed yaml root.
1744
+ * This lets callers retrieve non-leaf structures (booleans, arrays,
1745
+ * objects) such as the `apcli` visibility config, which is naturally
1746
+ * shaped as a nested object in apcore.yaml.
1747
+ *
1748
+ * Semantics:
1749
+ * - Returns `null` when no config file is loaded or when the path is
1750
+ * not present / descends into a non-object node (including arrays).
1751
+ * - Returns the raw value (boolean / array / object / scalar) when the
1752
+ * full path resolves to a leaf or intermediate node.
1753
+ *
1754
+ * Intentionally DOES NOT consult DEFAULTS, env vars, or CLI flags — it is
1755
+ * strictly a yaml-tree accessor. Scalar `resolve()` semantics are
1756
+ * unaffected.
1757
+ */
1758
+ resolveObject(key) {
1759
+ if (!this.fileCacheLoaded) {
1760
+ this.fileCache = this.loadConfigFile();
1761
+ this.fileCacheLoaded = true;
1762
+ }
1763
+ if (this._rawConfig == null) {
1764
+ return null;
1765
+ }
1766
+ const parts = key.split(".");
1767
+ let cur = this._rawConfig;
1768
+ for (const p of parts) {
1769
+ if (cur != null && typeof cur === "object" && !Array.isArray(cur) && p in cur) {
1770
+ cur = cur[p];
1771
+ } else {
1772
+ return null;
1773
+ }
1774
+ }
1775
+ return cur;
1776
+ }
1540
1777
  /**
1541
1778
  * Load and flatten a YAML config file.
1542
1779
  */
@@ -1548,26 +1785,27 @@ var ConfigResolver = class {
1548
1785
  if (err instanceof Error && "code" in err && err.code === "ENOENT") {
1549
1786
  return null;
1550
1787
  }
1551
- console.warn(
1788
+ warn(
1552
1789
  `Configuration file '${this.configPath}' is malformed, using defaults.`
1553
1790
  );
1554
1791
  return null;
1555
1792
  }
1556
1793
  let parsed;
1557
1794
  try {
1558
- parsed = yaml.load(content);
1795
+ parsed = yaml2.load(content);
1559
1796
  } catch {
1560
- console.warn(
1797
+ warn(
1561
1798
  `Configuration file '${this.configPath}' is malformed, using defaults.`
1562
1799
  );
1563
1800
  return null;
1564
1801
  }
1565
1802
  if (typeof parsed !== "object" || parsed === null || Array.isArray(parsed)) {
1566
- console.warn(
1803
+ warn(
1567
1804
  `Configuration file '${this.configPath}' is malformed, using defaults.`
1568
1805
  );
1569
1806
  return null;
1570
1807
  }
1808
+ this._rawConfig = parsed;
1571
1809
  return this.flattenDict(parsed);
1572
1810
  }
1573
1811
  /**
@@ -1593,95 +1831,89 @@ var ConfigResolver = class {
1593
1831
  // src/shell.ts
1594
1832
  init_esm_shims();
1595
1833
  init_errors();
1596
- import { readFileSync as readFileSync2 } from "fs";
1597
- import { fileURLToPath as fileURLToPath2 } from "url";
1598
- import * as path3 from "path";
1599
1834
  import { spawnSync } from "child_process";
1600
1835
  import { Command, Help, Option } from "commander";
1601
- var __dirname2 = path3.dirname(fileURLToPath2(import.meta.url));
1602
- var SHELL_VERSION = "0.0.0";
1603
- try {
1604
- const pkg = JSON.parse(readFileSync2(path3.resolve(__dirname2, "../package.json"), "utf-8"));
1605
- SHELL_VERSION = pkg.version;
1606
- } catch {
1607
- }
1608
1836
  function makeFunctionName(progName) {
1609
1837
  return "_" + progName.replace(/[^a-zA-Z0-9]/g, "_");
1610
1838
  }
1611
1839
  function shellQuote(s) {
1612
1840
  return "'" + s.replace(/'/g, "'\\''") + "'";
1613
1841
  }
1614
- function generateBashCompletion(progName) {
1842
+ function enumerateApcliSubcommands(apcliGroup) {
1843
+ if (!apcliGroup) return [];
1844
+ return apcliGroup.commands.filter((c) => !isCmdHidden(c)).map((c) => c.name());
1845
+ }
1846
+ function enumerateRootCommands(program) {
1847
+ return program.commands.filter((c) => !isCmdHidden(c)).map((c) => c.name());
1848
+ }
1849
+ function findApcliGroup(program) {
1850
+ return program.commands.find((c) => c.name() === "apcli");
1851
+ }
1852
+ function isCmdHidden(cmd) {
1853
+ const withHiddenFn = cmd;
1854
+ const withHiddenField = cmd;
1855
+ if (typeof withHiddenFn.hidden === "function") return !!withHiddenFn.hidden();
1856
+ return !!withHiddenField._hidden;
1857
+ }
1858
+ function generateBashCompletion(progName, program) {
1615
1859
  const fn = makeFunctionName(progName);
1616
1860
  const quoted = shellQuote(progName);
1617
- const moduleListCmd = `${quoted} list --format json 2>/dev/null | node -e "process.stdin.on('data',d=>{JSON.parse(d).forEach(m=>console.log(m.id))})" 2>/dev/null`;
1618
- const groupsAndTopCmd = `${quoted} list --format json 2>/dev/null | node -e "
1619
- const m=require('fs').readFileSync('/dev/stdin','utf8');
1620
- const ids=JSON.parse(m).map(x=>x.id);
1621
- const g=new Set(),t=[];
1622
- ids.forEach(i=>{if(i.includes('.'))g.add(i.split('.')[0]);else t.push(i)});
1623
- console.log([...g].sort().concat(t.sort()).join(' '))
1624
- " 2>/dev/null`;
1625
- const groupCmdsCmd = `${quoted} list --format json 2>/dev/null | node -e "
1626
- const m=require('fs').readFileSync('/dev/stdin','utf8');
1627
- const g=process.env._APCORE_GRP;
1628
- JSON.parse(m).map(x=>x.id).filter(i=>i.includes('.')&&i.split('.')[0]===g).forEach(i=>console.log(i.split('.',2)[1]))
1629
- " 2>/dev/null`;
1630
- return `${fn}() {
1861
+ const apcliGroup = program ? findApcliGroup(program) : void 0;
1862
+ const apcliVisible = apcliGroup !== void 0 && !isCmdHidden(apcliGroup);
1863
+ const rootCmds = program ? enumerateRootCommands(program).filter(
1864
+ (n) => n !== "apcli" || apcliVisible
1865
+ ) : [];
1866
+ const apcliCmds = apcliVisible ? enumerateApcliSubcommands(apcliGroup) : [];
1867
+ const rootOpts = rootCmds.join(" ");
1868
+ const apcliOpts = apcliCmds.join(" ");
1869
+ let body = `${fn}() {
1631
1870
  local cur prev opts
1632
1871
  COMPREPLY=()
1633
1872
  cur="\${COMP_WORDS[COMP_CWORD]}"
1634
1873
  prev="\${COMP_WORDS[COMP_CWORD-1]}"
1635
1874
 
1636
1875
  if [[ \${COMP_CWORD} -eq 1 ]]; then
1637
- opts="completion describe exec init list man"
1638
- local groups_and_top=$(${groupsAndTopCmd})
1639
- COMPREPLY=( $(compgen -W "\${opts} \${groups_and_top}" -- \${cur}) )
1876
+ opts="${rootOpts}"
1877
+ COMPREPLY=( $(compgen -W "\${opts}" -- \${cur}) )
1640
1878
  return 0
1641
1879
  fi
1642
-
1880
+ `;
1881
+ if (apcliVisible) {
1882
+ body += `
1643
1883
  if [[ \${COMP_CWORD} -eq 2 ]]; then
1644
- if [[ "\${COMP_WORDS[1]}" == "exec" ]]; then
1645
- local modules=$(${moduleListCmd})
1646
- COMPREPLY=( $(compgen -W "\${modules}" -- \${cur}) )
1884
+ if [[ "\${COMP_WORDS[1]}" == "apcli" ]]; then
1885
+ local apcli_cmds="${apcliOpts}"
1886
+ COMPREPLY=( $(compgen -W "\${apcli_cmds}" -- \${cur}) )
1647
1887
  return 0
1648
1888
  fi
1649
- export _APCORE_GRP="\${COMP_WORDS[1]}"
1650
- local group_cmds=$(${groupCmdsCmd})
1651
- COMPREPLY=( $(compgen -W "\${group_cmds}" -- \${cur}) )
1652
- return 0
1653
1889
  fi
1654
- }
1890
+ `;
1891
+ }
1892
+ body += `}
1655
1893
  complete -F ${fn} ${quoted}
1656
1894
  `;
1895
+ return body;
1657
1896
  }
1658
- function generateZshCompletion(progName) {
1897
+ function generateZshCompletion(progName, program) {
1659
1898
  const fn = makeFunctionName(progName);
1660
1899
  const quoted = shellQuote(progName);
1661
- const moduleListCmd = `${quoted} list --format json 2>/dev/null | node -e "process.stdin.on('data',d=>{JSON.parse(d).forEach(m=>console.log(m.id))})" 2>/dev/null`;
1662
- const groupsAndTopCmd = `${quoted} list --format json 2>/dev/null | node -e "
1663
- const m=require('fs').readFileSync('/dev/stdin','utf8');
1664
- const ids=JSON.parse(m).map(x=>x.id);
1665
- const g=new Set(),t=[];
1666
- ids.forEach(i=>{if(i.includes('.'))g.add(i.split('.')[0]);else t.push(i)});
1667
- console.log([...g].sort().concat(t.sort()).join(' '))
1668
- " 2>/dev/null`;
1669
- const groupCmdsCmd = `${quoted} list --format json 2>/dev/null | node -e "
1670
- const m=require('fs').readFileSync('/dev/stdin','utf8');
1671
- const g=process.env._APCORE_GRP;
1672
- JSON.parse(m).map(x=>x.id).filter(i=>i.includes('.')&&i.split('.')[0]===g).forEach(i=>console.log(i.split('.',2)[1]))
1673
- " 2>/dev/null`;
1900
+ const apcliGroup = program ? findApcliGroup(program) : void 0;
1901
+ const apcliVisible = apcliGroup !== void 0 && !isCmdHidden(apcliGroup);
1902
+ const rootCmds = program ? enumerateRootCommands(program).filter(
1903
+ (n) => n !== "apcli" || apcliVisible
1904
+ ) : [];
1905
+ const apcliCmds = apcliVisible ? enumerateApcliSubcommands(apcliGroup) : [];
1906
+ const rootEntries = rootCmds.map((n) => ` '${n}:${n}'`).join("\n");
1907
+ const apcliEntries = apcliCmds.map((n) => ` '${n}:${n}'`).join("\n");
1674
1908
  return `#compdef ${progName}
1675
1909
 
1676
1910
  ${fn}() {
1677
1911
  local -a commands
1678
1912
  commands=(
1679
- 'list:List available modules'
1680
- 'describe:Show module metadata and schema'
1681
- 'completion:Generate shell completion script'
1682
- 'init:Scaffolding commands'
1683
- 'man:Generate man page'
1684
- )
1913
+ ` + (rootEntries ? rootEntries + "\n" : "") + ` )
1914
+ local -a apcli_cmds
1915
+ apcli_cmds=(
1916
+ ` + (apcliEntries ? apcliEntries + "\n" : "") + ` )
1685
1917
 
1686
1918
  _arguments -C \\
1687
1919
  '1:command:->command' \\
@@ -1690,22 +1922,11 @@ ${fn}() {
1690
1922
  case "$state" in
1691
1923
  command)
1692
1924
  _describe -t commands '${progName} commands' commands
1693
- local -a groups_and_top
1694
- groups_and_top=($(${groupsAndTopCmd}))
1695
- compadd -a groups_and_top
1696
1925
  ;;
1697
1926
  args)
1698
1927
  case "\${words[1]}" in
1699
- exec)
1700
- local modules
1701
- modules=($(${moduleListCmd}))
1702
- compadd -a modules
1703
- ;;
1704
- *)
1705
- export _APCORE_GRP="\${words[1]}"
1706
- local -a group_cmds
1707
- group_cmds=($(${groupCmdsCmd}))
1708
- compadd -a group_cmds
1928
+ apcli)
1929
+ _describe -t apcli_cmds '${progName} apcli commands' apcli_cmds
1709
1930
  ;;
1710
1931
  esac
1711
1932
  ;;
@@ -1715,158 +1936,30 @@ ${fn}() {
1715
1936
  compdef ${fn} ${quoted}
1716
1937
  `;
1717
1938
  }
1718
- function generateFishCompletion(progName) {
1939
+ function generateFishCompletion(progName, program) {
1719
1940
  const quoted = shellQuote(progName);
1720
- const moduleListCmd = `${quoted} list --format json 2>/dev/null | node -e \\"process.stdin.on('data',d=>{JSON.parse(d).forEach(m=>console.log(m.id))})\\" 2>/dev/null`;
1721
- const groupsAndTopCmd = `${quoted} list --format json 2>/dev/null | node -e \\"const m=require('fs').readFileSync('/dev/stdin','utf8');const ids=JSON.parse(m).map(x=>x.id);const g=new Set(),t=[];ids.forEach(i=>{if(i.includes('.'))g.add(i.split('.')[0]);else t.push(i)});console.log([...g].sort().concat(t.sort()).join('\\\\n'))\\" 2>/dev/null`;
1722
- return `# Fish completions for ${progName}
1723
- complete -c ${quoted} -n "__fish_use_subcommand" -a list -d "List available modules"
1724
- complete -c ${quoted} -n "__fish_use_subcommand" -a describe -d "Show module metadata and schema"
1725
- complete -c ${quoted} -n "__fish_use_subcommand" -a completion -d "Generate shell completion script"
1726
- complete -c ${quoted} -n "__fish_use_subcommand" -a init -d "Scaffolding commands"
1727
- complete -c ${quoted} -n "__fish_use_subcommand" -a man -d "Generate man page"
1728
- complete -c ${quoted} -n "__fish_use_subcommand" -a "(${groupsAndTopCmd})" -d "Module group"
1729
-
1730
- complete -c ${quoted} -n "__fish_seen_subcommand_from exec" -a "(${moduleListCmd})"
1731
-
1732
- function __apcore_group_cmds
1733
- set -l grp (commandline -opc)[2]
1734
- set -x _APCORE_GRP $grp
1735
- ${quoted} list --format json 2>/dev/null | node -e "
1736
- const m=require('fs').readFileSync('/dev/stdin','utf8');
1737
- const g=process.env._APCORE_GRP;
1738
- JSON.parse(m).map(x=>x.id).filter(i=>i.includes('.')&&i.split('.')[0]===g).forEach(i=>console.log(i.split('.',2)[1]))
1739
- " 2>/dev/null
1740
- end
1741
-
1742
- # Group subcommand completion \u2014 matches when position 1 is not a builtin
1743
- complete -c ${quoted} -n "not __fish_use_subcommand; and not __fish_seen_subcommand_from list describe completion init man exec" -a "(__apcore_group_cmds)"
1744
- `;
1745
- }
1746
- function buildSynopsis(command, progName, commandName) {
1747
- if (!command) {
1748
- return `\\fB${progName} ${commandName}\\fR [OPTIONS]`;
1749
- }
1750
- const parts = [`\\fB${progName} ${commandName}\\fR`];
1751
- for (const opt of command.options) {
1752
- const flag = opt.long ?? opt.short ?? "";
1753
- if (opt.isBoolean?.()) {
1754
- parts.push(`[${flag}]`);
1755
- } else if (opt.required) {
1756
- const typeName = (opt.argChoices ? "CHOICE" : "VALUE").toUpperCase();
1757
- parts.push(`${flag} \\fI${typeName}\\fR`);
1758
- } else {
1759
- const typeName = (opt.argChoices ? "CHOICE" : "VALUE").toUpperCase();
1760
- parts.push(`[${flag} \\fI${typeName}\\fR]`);
1761
- }
1762
- }
1763
- for (const arg of command.registeredArguments ?? []) {
1764
- const meta = arg.name().toUpperCase();
1765
- if (arg.required) {
1766
- parts.push(`\\fI${meta}\\fR`);
1767
- } else {
1768
- parts.push(`[\\fI${meta}\\fR]`);
1769
- }
1770
- }
1771
- return parts.join(" ");
1772
- }
1773
- function generateManPage(commandName, command, progName, version = SHELL_VERSION) {
1774
- const today = (/* @__PURE__ */ new Date()).toISOString().slice(0, 10);
1775
- const title = `${progName}-${commandName}`.toUpperCase();
1776
- const pkgLabel = `${progName} ${version}`;
1777
- const manualLabel = `${progName} Manual`;
1778
- const sections = [];
1779
- sections.push(`.TH "${title}" "1" "${today}" "${pkgLabel}" "${manualLabel}"`);
1780
- sections.push(".SH NAME");
1781
- const desc = command?.description() ?? commandName;
1782
- const nameDesc = desc.split("\n")[0].replace(/\.$/, "");
1783
- sections.push(`${progName}-${commandName} \\- ${nameDesc}`);
1784
- sections.push(".SH SYNOPSIS");
1785
- sections.push(buildSynopsis(command, progName, commandName));
1786
- if (command?.description()) {
1787
- sections.push(".SH DESCRIPTION");
1788
- sections.push(
1789
- command.description().replace(/\\/g, "\\\\").replace(/-/g, "\\-")
1941
+ const apcliGroup = program ? findApcliGroup(program) : void 0;
1942
+ const apcliVisible = apcliGroup !== void 0 && !isCmdHidden(apcliGroup);
1943
+ const rootCmds = program ? enumerateRootCommands(program).filter(
1944
+ (n) => n !== "apcli" || apcliVisible
1945
+ ) : [];
1946
+ const apcliCmds = apcliVisible ? enumerateApcliSubcommands(apcliGroup) : [];
1947
+ const lines = [];
1948
+ lines.push(`# Fish completions for ${progName}`);
1949
+ for (const name of rootCmds) {
1950
+ lines.push(
1951
+ `complete -c ${quoted} -n "__fish_use_subcommand" -a ${name} -d "${name}"`
1790
1952
  );
1791
1953
  }
1792
- if (command && command.options.length > 0) {
1793
- sections.push(".SH OPTIONS");
1794
- for (const opt of command.options) {
1795
- const flag = [opt.short, opt.long].filter(Boolean).join(", ");
1796
- sections.push(".TP");
1797
- if (opt.isBoolean?.()) {
1798
- sections.push(`\\fB${flag}\\fR`);
1799
- } else {
1800
- sections.push(`\\fB${flag}\\fR \\fIVALUE\\fR`);
1801
- }
1802
- if (opt.description) {
1803
- sections.push(opt.description);
1804
- }
1805
- if (opt.defaultValue !== void 0 && !opt.isBoolean?.()) {
1806
- sections.push(`Default: ${opt.defaultValue}.`);
1807
- }
1954
+ if (apcliVisible && apcliCmds.length > 0) {
1955
+ lines.push("");
1956
+ for (const name of apcliCmds) {
1957
+ lines.push(
1958
+ `complete -c ${quoted} -n "__fish_seen_subcommand_from apcli" -a ${name} -d "${name}"`
1959
+ );
1808
1960
  }
1809
1961
  }
1810
- sections.push(".SH ENVIRONMENT");
1811
- sections.push(".TP");
1812
- sections.push("\\fBAPCORE_EXTENSIONS_ROOT\\fR");
1813
- sections.push(
1814
- "Path to the apcore extensions directory. Overrides the default \\fI./extensions\\fR."
1815
- );
1816
- sections.push(".TP");
1817
- sections.push("\\fBAPCORE_CLI_AUTO_APPROVE\\fR");
1818
- sections.push(
1819
- "Set to \\fB1\\fR to bypass approval prompts for modules that require human-in-the-loop confirmation."
1820
- );
1821
- sections.push(".TP");
1822
- sections.push("\\fBAPCORE_CLI_LOGGING_LEVEL\\fR");
1823
- sections.push(
1824
- "CLI-specific logging verbosity. One of: DEBUG, INFO, WARNING, ERROR. Takes priority over \\fBAPCORE_LOGGING_LEVEL\\fR. Default: WARNING."
1825
- );
1826
- sections.push(".TP");
1827
- sections.push("\\fBAPCORE_LOGGING_LEVEL\\fR");
1828
- sections.push(
1829
- "Global apcore logging verbosity. One of: DEBUG, INFO, WARNING, ERROR. Used as fallback when \\fBAPCORE_CLI_LOGGING_LEVEL\\fR is not set. Default: WARNING."
1830
- );
1831
- sections.push(".TP");
1832
- sections.push("\\fBAPCORE_AUTH_API_KEY\\fR");
1833
- sections.push(
1834
- "API key for authenticating with the apcore registry."
1835
- );
1836
- sections.push(".SH EXIT CODES");
1837
- const exitCodes = [
1838
- ["0", "Success."],
1839
- ["1", "Module execution error."],
1840
- ["2", "Invalid CLI input or missing argument."],
1841
- ["44", "Module not found, disabled, or failed to load."],
1842
- ["45", "Input failed JSON Schema validation."],
1843
- [
1844
- "46",
1845
- "Approval denied, timed out, or no interactive terminal available."
1846
- ],
1847
- [
1848
- "47",
1849
- "Configuration error (extensions directory not found or unreadable)."
1850
- ],
1851
- ["48", "Schema contains a circular \\fB$ref\\fR."],
1852
- ["77", "ACL denied \u2014 insufficient permissions for this module."],
1853
- ["130", "Execution cancelled by user (SIGINT / Ctrl\\-C)."]
1854
- ];
1855
- for (const [code, meaning] of exitCodes) {
1856
- sections.push(`.TP
1857
- \\fB${code}\\fR
1858
- ${meaning}`);
1859
- }
1860
- sections.push(".SH SEE ALSO");
1861
- sections.push(
1862
- [
1863
- `\\fB${progName}\\fR(1)`,
1864
- `\\fB${progName}\\-list\\fR(1)`,
1865
- `\\fB${progName}\\-describe\\fR(1)`,
1866
- `\\fB${progName}\\-completion\\fR(1)`
1867
- ].join(", ")
1868
- );
1869
- return sections.join("\n");
1962
+ return lines.join("\n") + "\n";
1870
1963
  }
1871
1964
  function roffEscape(s) {
1872
1965
  return s.replace(/\\/g, "\\\\").replace(/-/g, "\\-").replace(/'/g, "\\(aq");
@@ -2008,10 +2101,13 @@ function configureManHelp(program, progName, version, description, docsUrl2) {
2008
2101
  return "";
2009
2102
  });
2010
2103
  }
2011
- function registerShellCommands(cli, progName = "apcore-cli") {
2012
- const completionCmd = new Command("completion").description(
2013
- "Generate a shell completion script and print it to stdout."
2014
- ).argument("<shell>", "Shell type: bash, zsh, or fish").action((shell) => {
2104
+ function findRootProgram(host) {
2105
+ let cur = host;
2106
+ while (cur.parent) cur = cur.parent;
2107
+ return cur;
2108
+ }
2109
+ function registerCompletionCommand(host) {
2110
+ const completionCmd = new Command("completion").description("Generate a shell completion script and print it to stdout.").argument("<shell>", "Shell type: bash, zsh, or fish").action((shell) => {
2015
2111
  const validShells = ["bash", "zsh", "fish"];
2016
2112
  if (!validShells.includes(shell)) {
2017
2113
  process.stderr.write(
@@ -2020,36 +2116,23 @@ function registerShellCommands(cli, progName = "apcore-cli") {
2020
2116
  );
2021
2117
  process.exit(EXIT_CODES.INVALID_CLI_INPUT);
2022
2118
  }
2023
- const resolved = cli.name() || progName;
2119
+ const root = findRootProgram(host);
2120
+ const resolved = root.name() || "apcore-cli";
2024
2121
  const generators = {
2025
- bash: () => generateBashCompletion(resolved),
2026
- zsh: () => generateZshCompletion(resolved),
2027
- fish: () => generateFishCompletion(resolved)
2122
+ bash: () => generateBashCompletion(resolved, root),
2123
+ zsh: () => generateZshCompletion(resolved, root),
2124
+ fish: () => generateFishCompletion(resolved, root)
2028
2125
  };
2029
2126
  process.stdout.write(generators[shell]());
2030
2127
  });
2031
- cli.addCommand(completionCmd);
2032
- const manCmd = new Command("man").description("Generate a roff man page for COMMAND and print it to stdout.").argument("<command>", "Command to generate man page for").action((commandName) => {
2033
- const knownBuiltins = /* @__PURE__ */ new Set(["completion", "describe", "exec", "init", "list", "man"]);
2034
- const cmd = cli.commands.find((c) => c.name() === commandName) ?? null;
2035
- if (!cmd && !knownBuiltins.has(commandName)) {
2036
- process.stderr.write(
2037
- `Error: Unknown command '${commandName}'.
2038
- `
2039
- );
2040
- process.exit(EXIT_CODES.INVALID_CLI_INPUT);
2041
- }
2042
- const resolved = cli.name() || progName;
2043
- const roff = generateManPage(commandName, cmd, resolved);
2044
- process.stdout.write(roff);
2045
- });
2046
- cli.addCommand(manCmd);
2128
+ host.addCommand(completionCmd);
2047
2129
  }
2048
2130
 
2049
2131
  // src/discovery.ts
2050
2132
  init_esm_shims();
2051
- init_errors();
2052
2133
  import { Command as Command2, Option as Option2 } from "commander";
2134
+ init_errors();
2135
+ init_audit();
2053
2136
  var TAG_PATTERN = /^[a-z][a-z0-9_-]*$/;
2054
2137
  function validateTag(tag) {
2055
2138
  if (!TAG_PATTERN.test(tag)) {
@@ -2076,17 +2159,20 @@ function getAnnotationFlag(moduleDef, flag) {
2076
2159
  "readonly": "readonly",
2077
2160
  "streaming": "streaming",
2078
2161
  "cacheable": "cacheable",
2079
- "idempotent": "idempotent"
2162
+ "idempotent": "idempotent",
2163
+ "paginated": "paginated"
2080
2164
  };
2081
2165
  const attr = map[flag] ?? flag;
2082
2166
  return ann[attr] === true;
2083
2167
  }
2084
- function registerDiscoveryCommands(cli, registry) {
2168
+ function registerListCommand(apcliGroup, registry, exposureFilter) {
2085
2169
  const listCmd = new Command2("list").description("List available modules in the registry.").option("--tag <tag>", "Filter modules by tag (AND logic). Repeatable.", collectTag, []).option("--flat", "Show flat list (no grouping).", false).option("--format <format>", "Output format.", void 0).option("-s, --search <query>", "Filter by substring match on ID and description.").addOption(
2086
2170
  new Option2("--status <status>", "Filter by module status.").choices(["enabled", "disabled", "all"]).default("enabled")
2087
2171
  ).option("-a, --annotation <flag>", "Filter by annotation flag (AND logic). Repeatable.", collectAnnotation, []).addOption(
2088
2172
  new Option2("--sort <field>", "Sort order.").choices(["id", "calls", "errors", "latency"]).default("id")
2089
- ).option("--reverse", "Reverse sort order.", false).option("--deprecated", "Include deprecated modules.", false).option("--deps", "Show dependency count column.", false).action((opts) => {
2173
+ ).option("--reverse", "Reverse sort order.", false).option("--deprecated", "Include deprecated modules.", false).option("--deps", "Show dependency count column.", false).addOption(
2174
+ new Option2("--exposure <mode>", "Filter by exposure status.").choices(["exposed", "hidden", "all"]).default("exposed")
2175
+ ).action((opts) => {
2090
2176
  for (const t of opts.tag) {
2091
2177
  validateTag(t);
2092
2178
  }
@@ -2139,11 +2225,24 @@ function registerDiscoveryCommands(cli, registry) {
2139
2225
  if (opts.reverse) {
2140
2226
  modules.reverse();
2141
2227
  }
2228
+ let showExposureCol = false;
2229
+ if (exposureFilter && opts.exposure !== "all") {
2230
+ if (opts.exposure === "exposed") {
2231
+ modules = modules.filter((m) => exposureFilter.isExposed(m.id ?? ""));
2232
+ } else if (opts.exposure === "hidden") {
2233
+ modules = modules.filter((m) => !exposureFilter.isExposed(m.id ?? ""));
2234
+ }
2235
+ }
2236
+ if (opts.exposure === "all" && exposureFilter) {
2237
+ showExposureCol = true;
2238
+ }
2142
2239
  const fmt = resolveFormat(opts.format);
2143
2240
  const filterTagsArg = opts.tag.length > 0 ? opts.tag : void 0;
2144
- formatModuleList(modules, fmt, filterTagsArg, opts.deps);
2241
+ formatModuleList(modules, fmt, filterTagsArg, opts.deps, showExposureCol ? exposureFilter : void 0);
2145
2242
  });
2146
- cli.addCommand(listCmd);
2243
+ apcliGroup.addCommand(listCmd);
2244
+ }
2245
+ function registerDescribeCommand(apcliGroup, registry) {
2147
2246
  const describeCmd = new Command2("describe").description("Show metadata, schema, and annotations for a module.").argument("<module-id>", "Module ID to describe").option("--format <format>", "Output format.", void 0).action((moduleId, opts) => {
2148
2247
  validateModuleId(moduleId);
2149
2248
  const moduleDef = registry.getModule(moduleId);
@@ -2157,7 +2256,86 @@ function registerDiscoveryCommands(cli, registry) {
2157
2256
  const fmt = resolveFormat(opts.format);
2158
2257
  formatModuleDetail(moduleDef, fmt);
2159
2258
  });
2160
- cli.addCommand(describeCmd);
2259
+ apcliGroup.addCommand(describeCmd);
2260
+ }
2261
+ function registerExecCommand(apcliGroup, registry, executor) {
2262
+ const execCmd = new Command2("exec").description("Execute a module by ID with JSON input.").argument("<module-id>", "Module ID to execute").option("--format <format>", "Output format (json, table, csv, yaml, jsonl).").option("--fields <fields>", "Comma-separated dot-paths to select from the result.").option(
2263
+ "--input <json>",
2264
+ "JSON object passed as input to the module. Use '-' to read JSON from stdin."
2265
+ ).option("-y, --yes", "Auto-approve if the module declares requires_approval.", false).option(
2266
+ "--approval-timeout <seconds>",
2267
+ "Seconds to wait for interactive approval.",
2268
+ parseInt
2269
+ ).option("--sandbox", "Run module in an isolated subprocess with restricted env.", false).option("--strategy <name>", "Execution strategy (standard, parallel, sequential, etc.).").option("--trace", "Enable pipeline trace output.", false).option("--dry-run", "Validate inputs without executing the module.", false).option("--stream", "Stream output as JSONL instead of buffering.", false).action(async (moduleId, opts) => {
2270
+ validateModuleId(moduleId);
2271
+ const moduleDef = registry.getModule(moduleId);
2272
+ if (!moduleDef) {
2273
+ process.stderr.write(`Error: Module '${moduleId}' not found.
2274
+ `);
2275
+ process.exit(EXIT_CODES.MODULE_NOT_FOUND);
2276
+ }
2277
+ let merged = {};
2278
+ if (opts.input === "-") {
2279
+ merged = await collectInput("-", {}, false);
2280
+ } else if (opts.input !== void 0) {
2281
+ try {
2282
+ const parsed = JSON.parse(opts.input);
2283
+ if (typeof parsed !== "object" || parsed === null || Array.isArray(parsed)) {
2284
+ process.stderr.write("Error: --input JSON must be an object.\n");
2285
+ process.exit(EXIT_CODES.INVALID_CLI_INPUT);
2286
+ }
2287
+ merged = parsed;
2288
+ } catch (err) {
2289
+ const msg = err instanceof Error ? err.message : String(err);
2290
+ process.stderr.write(`Error: --input is not valid JSON: ${msg}
2291
+ `);
2292
+ process.exit(EXIT_CODES.INVALID_CLI_INPUT);
2293
+ }
2294
+ }
2295
+ const startTime = performance.now();
2296
+ try {
2297
+ await checkApproval(moduleDef, opts.yes, opts.approvalTimeout);
2298
+ if (opts.dryRun) {
2299
+ if (executor.validate) {
2300
+ const preflight = await executor.validate(moduleId, merged);
2301
+ formatPreflightResult(preflight, opts.format);
2302
+ } else {
2303
+ process.stdout.write(JSON.stringify({ valid: true }) + "\n");
2304
+ }
2305
+ return;
2306
+ }
2307
+ let result;
2308
+ if (opts.strategy && executor.callWithTrace) {
2309
+ const [res] = await executor.callWithTrace(moduleId, merged, { strategy: opts.strategy });
2310
+ result = res;
2311
+ } else {
2312
+ const { Sandbox: Sandbox2 } = await Promise.resolve().then(() => (init_security(), security_exports));
2313
+ const sandbox = new Sandbox2(opts.sandbox);
2314
+ result = await sandbox.execute(moduleId, merged, executor);
2315
+ }
2316
+ const durationMs = Math.round(performance.now() - startTime);
2317
+ const fmt = resolveFormat(opts.format);
2318
+ formatExecResult(result, fmt, opts.fields);
2319
+ const auditLogger = getAuditLogger();
2320
+ if (auditLogger) {
2321
+ auditLogger.logExecution(moduleId, merged, "success", 0, durationMs);
2322
+ }
2323
+ } catch (err) {
2324
+ const exitCode = exitCodeForError(err);
2325
+ const durationMs = Math.round(performance.now() - startTime);
2326
+ try {
2327
+ const auditLogger = getAuditLogger();
2328
+ if (auditLogger) {
2329
+ auditLogger.logExecution(moduleId, merged, "error", exitCode, durationMs);
2330
+ }
2331
+ } catch {
2332
+ }
2333
+ process.stderr.write(`Error: ${err instanceof Error ? err.message : err}
2334
+ `);
2335
+ process.exit(exitCode);
2336
+ }
2337
+ });
2338
+ apcliGroup.addCommand(execCmd);
2161
2339
  }
2162
2340
  function registerValidateCommand(cli, registry, executor) {
2163
2341
  const validateCmd = new Command2("validate").description("Run preflight checks without executing a module.").argument("<module-id>", "Module ID to validate").option("--input <source>", "JSON input file or '-' for stdin.").option("--format <format>", "Output format.").action(async (moduleId, opts) => {
@@ -2171,17 +2349,32 @@ function registerValidateCommand(cli, registry, executor) {
2171
2349
  const merged = opts.input ? await collectInput(opts.input, {}, false) : {};
2172
2350
  if (!executor.validate) {
2173
2351
  process.stderr.write("Error: Executor does not support validate.\n");
2174
- process.exit(1);
2352
+ process.exit(EXIT_CODES.MODULE_EXECUTE_ERROR);
2353
+ }
2354
+ try {
2355
+ const preflight = await executor.validate(moduleId, merged);
2356
+ formatPreflightResult(preflight, opts.format);
2357
+ process.exit(preflight.valid ? 0 : firstFailedExitCode(preflight));
2358
+ } catch (err) {
2359
+ const exitCode = exitCodeForError(err);
2360
+ try {
2361
+ const auditLogger = getAuditLogger();
2362
+ if (auditLogger) {
2363
+ auditLogger.logExecution(moduleId, merged, "error", exitCode, 0);
2364
+ }
2365
+ } catch {
2366
+ }
2367
+ process.stderr.write(`Error: ${err instanceof Error ? err.message : err}
2368
+ `);
2369
+ process.exit(exitCode);
2175
2370
  }
2176
- const preflight = await executor.validate(moduleId, merged);
2177
- formatPreflightResult(preflight, opts.format);
2178
- process.exit(preflight.valid ? 0 : firstFailedExitCode(preflight));
2179
2371
  });
2180
2372
  cli.addCommand(validateCmd);
2181
2373
  }
2182
2374
 
2183
2375
  // src/system-cmd.ts
2184
2376
  init_esm_shims();
2377
+ init_errors();
2185
2378
  import { Command as Command3 } from "commander";
2186
2379
  async function callSystemModule(executor, moduleId, inputs) {
2187
2380
  if (executor.call) {
@@ -2189,6 +2382,18 @@ async function callSystemModule(executor, moduleId, inputs) {
2189
2382
  }
2190
2383
  return executor.execute(moduleId, inputs);
2191
2384
  }
2385
+ function emitResult(jsonPayload, fmt, ttyRender) {
2386
+ if (fmt === "json" || !process.stdout.isTTY) {
2387
+ process.stdout.write(JSON.stringify(jsonPayload, null, 2) + "\n");
2388
+ } else {
2389
+ ttyRender();
2390
+ }
2391
+ }
2392
+ function emitErrorAndExit(e) {
2393
+ process.stderr.write(`Error: ${e instanceof Error ? e.message : e}
2394
+ `);
2395
+ process.exit(exitCodeForError(e));
2396
+ }
2192
2397
  function formatHealthSummaryTty(result) {
2193
2398
  const summary = result.summary ?? {};
2194
2399
  const modules = result.modules ?? [];
@@ -2277,17 +2482,7 @@ function formatUsageSummaryTty(result) {
2277
2482
  Total: ${totalCalls.toLocaleString()} calls | ${totalErrors.toLocaleString()} errors
2278
2483
  `);
2279
2484
  }
2280
- async function registerSystemCommands(cli, executor) {
2281
- try {
2282
- if (executor.validate) {
2283
- await executor.validate("system.health.summary", {});
2284
- } else {
2285
- await callSystemModule(executor, "system.health.summary", { include_healthy: true });
2286
- }
2287
- } catch {
2288
- debug("System modules not available; skipping system command registration.");
2289
- return;
2290
- }
2485
+ function registerHealthCommand(apcliGroup, executor) {
2291
2486
  const healthCmd = new Command3("health").description("Show module health status. Optionally specify a module ID for details.").argument("[module-id]", "Module ID for detailed health").option("--threshold <number>", "Error rate threshold (default: 0.01).", parseFloat, 0.01).option("--all", "Include healthy modules.", false).option("--errors <count>", "Max recent errors (module detail only).", parseInt, 10).option("--format <format>", "Output format.").action(async (moduleId, opts) => {
2292
2487
  const fmt = resolveFormat(opts.format);
2293
2488
  try {
@@ -2296,29 +2491,21 @@ async function registerSystemCommands(cli, executor) {
2296
2491
  module_id: moduleId,
2297
2492
  error_limit: opts.errors
2298
2493
  });
2299
- if (fmt === "json" || !process.stdout.isTTY) {
2300
- process.stdout.write(JSON.stringify(result, null, 2) + "\n");
2301
- } else {
2302
- formatHealthModuleTty(result);
2303
- }
2494
+ emitResult(result, fmt, () => formatHealthModuleTty(result));
2304
2495
  } else {
2305
2496
  const result = await callSystemModule(executor, "system.health.summary", {
2306
2497
  error_rate_threshold: opts.threshold,
2307
2498
  include_healthy: opts.all
2308
2499
  });
2309
- if (fmt === "json" || !process.stdout.isTTY) {
2310
- process.stdout.write(JSON.stringify(result, null, 2) + "\n");
2311
- } else {
2312
- formatHealthSummaryTty(result);
2313
- }
2500
+ emitResult(result, fmt, () => formatHealthSummaryTty(result));
2314
2501
  }
2315
2502
  } catch (e) {
2316
- process.stderr.write(`Error: ${e instanceof Error ? e.message : e}
2317
- `);
2318
- process.exit(1);
2503
+ emitErrorAndExit(e);
2319
2504
  }
2320
2505
  });
2321
- cli.addCommand(healthCmd);
2506
+ apcliGroup.addCommand(healthCmd);
2507
+ }
2508
+ function registerUsageCommand(apcliGroup, executor) {
2322
2509
  const usageCmd = new Command3("usage").description("Show module usage statistics. Optionally specify a module ID for details.").argument("[module-id]", "Module ID for detailed usage").option("--period <period>", "Time window: 1h, 24h, 7d, 30d.", "24h").option("--format <format>", "Output format.").action(async (moduleId, opts) => {
2323
2510
  const fmt = resolveFormat(opts.format);
2324
2511
  try {
@@ -2333,24 +2520,21 @@ async function registerSystemCommands(cli, executor) {
2333
2520
  period: opts.period
2334
2521
  });
2335
2522
  }
2336
- if (fmt === "json" || !process.stdout.isTTY) {
2337
- process.stdout.write(JSON.stringify(result, null, 2) + "\n");
2338
- } else if (moduleId) {
2339
- formatExecResult(result, fmt);
2340
- } else {
2341
- formatUsageSummaryTty(result);
2342
- }
2523
+ emitResult(result, fmt, () => {
2524
+ if (moduleId) {
2525
+ formatExecResult(result, fmt);
2526
+ } else {
2527
+ formatUsageSummaryTty(result);
2528
+ }
2529
+ });
2343
2530
  } catch (e) {
2344
- process.stderr.write(`Error: ${e instanceof Error ? e.message : e}
2345
- `);
2346
- process.exit(1);
2531
+ emitErrorAndExit(e);
2347
2532
  }
2348
2533
  });
2349
- cli.addCommand(usageCmd);
2350
- const enableCmd = new Command3("enable").description("Enable a disabled module at runtime.").argument("<module-id>", "Module ID to enable").requiredOption("--reason <reason>", "Reason for enabling (required for audit).").option("-y, --yes", "Skip approval prompt.", false).option("--format <format>", "Output format.").action(async (moduleId, opts) => {
2351
- if (!opts.yes) {
2352
- process.stderr.write("Note: This command requires approval. Use --yes to bypass.\n");
2353
- }
2534
+ apcliGroup.addCommand(usageCmd);
2535
+ }
2536
+ function registerEnableCommand(apcliGroup, executor) {
2537
+ const enableCmd = new Command3("enable").description("Enable a disabled module at runtime.").argument("<module-id>", "Module ID to enable").requiredOption("--reason <reason>", "Reason for enabling (required for audit).").option("-y, --yes", "Signal explicit intent (forwarded to server-side approval gate).", false).option("--format <format>", "Output format.").action(async (moduleId, opts) => {
2354
2538
  const fmt = resolveFormat(opts.format);
2355
2539
  try {
2356
2540
  const result = await callSystemModule(executor, "system.control.toggle_feature", {
@@ -2358,24 +2542,19 @@ async function registerSystemCommands(cli, executor) {
2358
2542
  enabled: true,
2359
2543
  reason: opts.reason
2360
2544
  });
2361
- if (fmt === "json" || !process.stdout.isTTY) {
2362
- process.stdout.write(JSON.stringify(result, null, 2) + "\n");
2363
- } else {
2545
+ emitResult(result, fmt, () => {
2364
2546
  process.stdout.write(`Module '${moduleId}' enabled.
2365
2547
  Reason: ${opts.reason}
2366
2548
  `);
2367
- }
2549
+ });
2368
2550
  } catch (e) {
2369
- process.stderr.write(`Error: ${e instanceof Error ? e.message : e}
2370
- `);
2371
- process.exit(1);
2551
+ emitErrorAndExit(e);
2372
2552
  }
2373
2553
  });
2374
- cli.addCommand(enableCmd);
2375
- const disableCmd = new Command3("disable").description("Disable a module at runtime (calls are rejected until re-enabled).").argument("<module-id>", "Module ID to disable").requiredOption("--reason <reason>", "Reason for disabling (required for audit).").option("-y, --yes", "Skip approval prompt.", false).option("--format <format>", "Output format.").action(async (moduleId, opts) => {
2376
- if (!opts.yes) {
2377
- process.stderr.write("Note: This command requires approval. Use --yes to bypass.\n");
2378
- }
2554
+ apcliGroup.addCommand(enableCmd);
2555
+ }
2556
+ function registerDisableCommand(apcliGroup, executor) {
2557
+ const disableCmd = new Command3("disable").description("Disable a module at runtime (calls are rejected until re-enabled).").argument("<module-id>", "Module ID to disable").requiredOption("--reason <reason>", "Reason for disabling (required for audit).").option("-y, --yes", "Signal explicit intent (forwarded to server-side approval gate).", false).option("--format <format>", "Output format.").action(async (moduleId, opts) => {
2379
2558
  const fmt = resolveFormat(opts.format);
2380
2559
  try {
2381
2560
  const result = await callSystemModule(executor, "system.control.toggle_feature", {
@@ -2383,33 +2562,26 @@ async function registerSystemCommands(cli, executor) {
2383
2562
  enabled: false,
2384
2563
  reason: opts.reason
2385
2564
  });
2386
- if (fmt === "json" || !process.stdout.isTTY) {
2387
- process.stdout.write(JSON.stringify(result, null, 2) + "\n");
2388
- } else {
2565
+ emitResult(result, fmt, () => {
2389
2566
  process.stdout.write(`Module '${moduleId}' disabled.
2390
2567
  Reason: ${opts.reason}
2391
2568
  `);
2392
- }
2569
+ });
2393
2570
  } catch (e) {
2394
- process.stderr.write(`Error: ${e instanceof Error ? e.message : e}
2395
- `);
2396
- process.exit(1);
2571
+ emitErrorAndExit(e);
2397
2572
  }
2398
2573
  });
2399
- cli.addCommand(disableCmd);
2400
- const reloadCmd = new Command3("reload").description("Hot-reload a module from disk.").argument("<module-id>", "Module ID to reload").requiredOption("--reason <reason>", "Reason for reload (required for audit).").option("-y, --yes", "Skip approval prompt.", false).option("--format <format>", "Output format.").action(async (moduleId, opts) => {
2401
- if (!opts.yes) {
2402
- process.stderr.write("Note: This command requires approval. Use --yes to bypass.\n");
2403
- }
2574
+ apcliGroup.addCommand(disableCmd);
2575
+ }
2576
+ function registerReloadCommand(apcliGroup, executor) {
2577
+ const reloadCmd = new Command3("reload").description("Hot-reload a module from disk.").argument("<module-id>", "Module ID to reload").requiredOption("--reason <reason>", "Reason for reload (required for audit).").option("-y, --yes", "Signal explicit intent (forwarded to server-side approval gate).", false).option("--format <format>", "Output format.").action(async (moduleId, opts) => {
2404
2578
  const fmt = resolveFormat(opts.format);
2405
2579
  try {
2406
2580
  const result = await callSystemModule(executor, "system.control.reload_module", {
2407
2581
  module_id: moduleId,
2408
2582
  reason: opts.reason
2409
2583
  });
2410
- if (fmt === "json" || !process.stdout.isTTY) {
2411
- process.stdout.write(JSON.stringify(result, null, 2) + "\n");
2412
- } else {
2584
+ emitResult(result, fmt, () => {
2413
2585
  const prev = result.previous_version ?? "?";
2414
2586
  const newVer = result.new_version ?? "?";
2415
2587
  const dur = result.reload_duration_ms ?? "?";
@@ -2419,34 +2591,30 @@ async function registerSystemCommands(cli, executor) {
2419
2591
  `);
2420
2592
  process.stdout.write(` Duration: ${dur}ms
2421
2593
  `);
2422
- }
2594
+ });
2423
2595
  } catch (e) {
2424
- process.stderr.write(`Error: ${e instanceof Error ? e.message : e}
2425
- `);
2426
- process.exit(1);
2596
+ emitErrorAndExit(e);
2427
2597
  }
2428
2598
  });
2429
- cli.addCommand(reloadCmd);
2599
+ apcliGroup.addCommand(reloadCmd);
2600
+ }
2601
+ function registerConfigCommand(apcliGroup, executor) {
2430
2602
  const configGroup = new Command3("config").description("Read or update runtime configuration.");
2431
2603
  const configGetCmd = new Command3("get").description("Read a configuration value by dot-path key.").argument("<key>", "Configuration key (dot-path)").option("--format <format>", "Output format.", "table").action(async (key, opts) => {
2432
2604
  const fmt = resolveFormat(opts.format);
2433
2605
  try {
2434
2606
  const result = await callSystemModule(executor, "system.config.get", { key });
2435
2607
  const value = result?.value ?? result;
2436
- if (fmt === "json" || !process.stdout.isTTY) {
2437
- process.stdout.write(JSON.stringify({ key, value }, null, 2) + "\n");
2438
- } else {
2608
+ emitResult({ key, value }, fmt, () => {
2439
2609
  process.stdout.write(`${key} = ${JSON.stringify(value)}
2440
2610
  `);
2441
- }
2611
+ });
2442
2612
  } catch (e) {
2443
- process.stderr.write(`Error: ${e instanceof Error ? e.message : e}
2444
- `);
2445
- process.exit(1);
2613
+ emitErrorAndExit(e);
2446
2614
  }
2447
2615
  });
2448
2616
  configGroup.addCommand(configGetCmd);
2449
- const configSetCmd = new Command3("set").description("Update a runtime configuration value (requires approval).").argument("<key>", "Configuration key (dot-path)").argument("<value>", "New value").requiredOption("--reason <reason>", "Reason for config change (required for audit).").option("--format <format>", "Output format.").action(async (key, value, opts) => {
2617
+ const configSetCmd = new Command3("set").description("Update a runtime configuration value (audit-logged; server-side approval gate applies).").argument("<key>", "Configuration key (dot-path)").argument("<value>", "New value").requiredOption("--reason <reason>", "Reason for config change (required for audit).").option("--format <format>", "Output format.").action(async (key, value, opts) => {
2450
2618
  const fmt = resolveFormat(opts.format);
2451
2619
  let parsedValue;
2452
2620
  try {
@@ -2460,9 +2628,7 @@ async function registerSystemCommands(cli, executor) {
2460
2628
  value: parsedValue,
2461
2629
  reason: opts.reason
2462
2630
  });
2463
- if (fmt === "json" || !process.stdout.isTTY) {
2464
- process.stdout.write(JSON.stringify(result, null, 2) + "\n");
2465
- } else {
2631
+ emitResult(result, fmt, () => {
2466
2632
  const old = result.old_value ?? "?";
2467
2633
  const newVal = result.new_value ?? "?";
2468
2634
  process.stdout.write(`Config updated: ${key}
@@ -2471,20 +2637,40 @@ async function registerSystemCommands(cli, executor) {
2471
2637
  `);
2472
2638
  process.stdout.write(` Reason: ${opts.reason}
2473
2639
  `);
2474
- }
2640
+ });
2475
2641
  } catch (e) {
2476
- process.stderr.write(`Error: ${e instanceof Error ? e.message : e}
2477
- `);
2478
- process.exit(1);
2642
+ emitErrorAndExit(e);
2479
2643
  }
2480
2644
  });
2481
2645
  configGroup.addCommand(configSetCmd);
2482
- cli.addCommand(configGroup);
2646
+ apcliGroup.addCommand(configGroup);
2483
2647
  }
2484
2648
 
2485
2649
  // src/strategy.ts
2486
2650
  init_esm_shims();
2487
2651
  import { Command as Command4, Option as Option3 } from "commander";
2652
+ function lookupStrategyInfo(executor, strategyName) {
2653
+ if (typeof executor.describePipeline === "function") {
2654
+ try {
2655
+ const current = executor.describePipeline();
2656
+ if (current && current.name === strategyName) {
2657
+ return { info: current, isCurrent: true };
2658
+ }
2659
+ } catch {
2660
+ }
2661
+ }
2662
+ const ctor = executor.constructor;
2663
+ if (ctor && typeof ctor.listStrategies === "function") {
2664
+ try {
2665
+ const all = ctor.listStrategies();
2666
+ const info = all.find((s) => s.name === strategyName) ?? null;
2667
+ return { info, isCurrent: false };
2668
+ } catch {
2669
+ return { info: null, isCurrent: false };
2670
+ }
2671
+ }
2672
+ return { info: null, isCurrent: false };
2673
+ }
2488
2674
  var PRESET_STEPS = {
2489
2675
  standard: [
2490
2676
  "context_creation",
@@ -2543,87 +2729,84 @@ function registerPipelineCommand(cli, executor) {
2543
2729
  new Option3("--strategy <name>", "Strategy to describe (default: standard).").choices(["standard", "internal", "testing", "performance", "minimal"]).default("standard")
2544
2730
  ).option("--format <format>", "Output format.").action((opts) => {
2545
2731
  const fmt = resolveFormat(opts.format);
2546
- let strategyObj = null;
2547
- const ex = executor;
2548
- if (typeof ex._resolve_strategy_name === "function" || typeof ex._resolveStrategyName === "function") {
2549
- try {
2550
- const fn = ex._resolve_strategy_name ?? ex._resolveStrategyName;
2551
- strategyObj = fn(opts.strategy);
2552
- } catch {
2553
- strategyObj = null;
2554
- }
2555
- }
2556
- if (!strategyObj) {
2557
- const steps = PRESET_STEPS[opts.strategy] ?? [];
2558
- const pureSteps = /* @__PURE__ */ new Set([
2559
- "context_creation",
2560
- "call_chain_guard",
2561
- "module_lookup",
2562
- "acl_check",
2563
- "input_validation"
2564
- ]);
2565
- const nonRemovable = /* @__PURE__ */ new Set([
2566
- "context_creation",
2567
- "module_lookup",
2568
- "execute",
2569
- "return_result"
2570
- ]);
2732
+ const { info, isCurrent } = lookupStrategyInfo(executor, opts.strategy);
2733
+ if (info) {
2734
+ const strategySteps = isCurrent ? executor.currentStrategy?.steps ?? [] : [];
2735
+ const header = `Pipeline: ${info.name} (${info.stepCount} steps)`;
2571
2736
  if (fmt === "json" || !process.stdout.isTTY) {
2572
2737
  const payload = {
2573
- strategy: opts.strategy,
2574
- step_count: steps.length,
2575
- steps: steps.map((s, i) => ({
2576
- index: i + 1,
2577
- name: s,
2578
- pure: pureSteps.has(s),
2579
- removable: !nonRemovable.has(s)
2580
- }))
2738
+ strategy: info.name,
2739
+ step_count: info.stepCount,
2740
+ description: info.description,
2741
+ steps: info.stepNames.map((name, i) => {
2742
+ const stepMeta = strategySteps[i];
2743
+ return {
2744
+ index: i + 1,
2745
+ name,
2746
+ pure: stepMeta?.pure ?? false,
2747
+ removable: stepMeta?.removable ?? true,
2748
+ timeout_ms: stepMeta?.timeoutMs ?? null
2749
+ };
2750
+ })
2581
2751
  };
2582
2752
  process.stdout.write(JSON.stringify(payload, null, 2) + "\n");
2583
2753
  } else {
2584
- process.stdout.write(`Pipeline: ${opts.strategy} (${steps.length} steps)
2754
+ process.stdout.write(`${header}
2585
2755
 
2586
2756
  `);
2587
2757
  process.stdout.write(` ${"#".padEnd(4)} ${"Step".padEnd(28)} ${"Pure".padEnd(6)} ${"Removable".padEnd(11)} Timeout
2588
2758
  `);
2589
2759
  process.stdout.write(` ${"-".repeat(4)} ${"-".repeat(28)} ${"-".repeat(6)} ${"-".repeat(11)} ${"-".repeat(8)}
2590
2760
  `);
2591
- for (let i = 0; i < steps.length; i++) {
2592
- const pure = pureSteps.has(steps[i]) ? "yes" : "no";
2593
- const removable = nonRemovable.has(steps[i]) ? "no" : "yes";
2594
- process.stdout.write(` ${String(i + 1).padEnd(4)} ${steps[i].padEnd(28)} ${pure.padEnd(6)} ${removable.padEnd(11)} \u2014
2761
+ for (let i = 0; i < info.stepNames.length; i++) {
2762
+ const stepMeta = strategySteps[i];
2763
+ const pure = stepMeta?.pure ? "yes" : "no";
2764
+ const removable = stepMeta?.removable !== false ? "yes" : "no";
2765
+ const timeout = stepMeta?.timeoutMs ? `${stepMeta.timeoutMs}ms` : "\u2014";
2766
+ process.stdout.write(` ${String(i + 1).padEnd(4)} ${info.stepNames[i].padEnd(28)} ${pure.padEnd(6)} ${removable.padEnd(11)} ${timeout}
2595
2767
  `);
2596
2768
  }
2597
2769
  }
2598
2770
  return;
2599
2771
  }
2600
- const stepsInfo = strategyObj.steps.map((step) => ({
2601
- name: step.name,
2602
- pure: step.pure ?? false,
2603
- removable: step.removable ?? true,
2604
- timeout_ms: step.timeout_ms ?? null
2605
- }));
2772
+ const steps = PRESET_STEPS[opts.strategy] ?? [];
2773
+ const pureSteps = /* @__PURE__ */ new Set([
2774
+ "context_creation",
2775
+ "call_chain_guard",
2776
+ "module_lookup",
2777
+ "acl_check",
2778
+ "input_validation"
2779
+ ]);
2780
+ const nonRemovable = /* @__PURE__ */ new Set([
2781
+ "context_creation",
2782
+ "module_lookup",
2783
+ "execute",
2784
+ "return_result"
2785
+ ]);
2606
2786
  if (fmt === "json" || !process.stdout.isTTY) {
2607
2787
  const payload = {
2608
2788
  strategy: opts.strategy,
2609
- step_count: stepsInfo.length,
2610
- steps: stepsInfo.map((s, i) => ({ index: i + 1, ...s }))
2789
+ step_count: steps.length,
2790
+ steps: steps.map((s, i) => ({
2791
+ index: i + 1,
2792
+ name: s,
2793
+ pure: pureSteps.has(s),
2794
+ removable: !nonRemovable.has(s)
2795
+ }))
2611
2796
  };
2612
2797
  process.stdout.write(JSON.stringify(payload, null, 2) + "\n");
2613
2798
  } else {
2614
- process.stdout.write(`Pipeline: ${opts.strategy} (${stepsInfo.length} steps)
2799
+ process.stdout.write(`Pipeline: ${opts.strategy} (${steps.length} steps)
2615
2800
 
2616
2801
  `);
2617
2802
  process.stdout.write(` ${"#".padEnd(4)} ${"Step".padEnd(28)} ${"Pure".padEnd(6)} ${"Removable".padEnd(11)} Timeout
2618
2803
  `);
2619
2804
  process.stdout.write(` ${"-".repeat(4)} ${"-".repeat(28)} ${"-".repeat(6)} ${"-".repeat(11)} ${"-".repeat(8)}
2620
2805
  `);
2621
- for (let i = 0; i < stepsInfo.length; i++) {
2622
- const s = stepsInfo[i];
2623
- const pure = s.pure ? "yes" : "no";
2624
- const removable = s.removable ? "yes" : "no";
2625
- const timeout = s.timeout_ms !== null ? `${s.timeout_ms}ms` : "\u2014";
2626
- process.stdout.write(` ${String(i + 1).padEnd(4)} ${s.name.padEnd(28)} ${pure.padEnd(6)} ${removable.padEnd(11)} ${timeout}
2806
+ for (let i = 0; i < steps.length; i++) {
2807
+ const pure = pureSteps.has(steps[i]) ? "yes" : "no";
2808
+ const removable = nonRemovable.has(steps[i]) ? "no" : "yes";
2809
+ process.stdout.write(` ${String(i + 1).padEnd(4)} ${steps[i].padEnd(28)} ${pure.padEnd(6)} ${removable.padEnd(11)} \u2014
2627
2810
  `);
2628
2811
  }
2629
2812
  }
@@ -2631,284 +2814,466 @@ function registerPipelineCommand(cli, executor) {
2631
2814
  cli.addCommand(pipelineCmd);
2632
2815
  }
2633
2816
 
2634
- // src/cli.ts
2817
+ // src/builtin-group.ts
2635
2818
  init_esm_shims();
2636
- import { Command as Command5 } from "commander";
2637
- var BUILTIN_COMMANDS = [
2638
- "completion",
2639
- "config",
2819
+ init_errors();
2820
+ init_logger();
2821
+ var RESERVED_GROUP_NAMES = /* @__PURE__ */ new Set(["apcli"]);
2822
+ var VALID_USER_MODES = /* @__PURE__ */ new Set([
2823
+ "all",
2824
+ "none",
2825
+ "include",
2826
+ "exclude"
2827
+ ]);
2828
+ var APCLI_SUBCOMMAND_NAMES = /* @__PURE__ */ new Set([
2829
+ "list",
2640
2830
  "describe",
2641
- "describe-pipeline",
2642
- "disable",
2643
- "enable",
2644
2831
  "exec",
2645
- "health",
2832
+ "validate",
2646
2833
  "init",
2647
- "list",
2648
- "man",
2649
- "reload",
2834
+ "health",
2650
2835
  "usage",
2651
- "validate"
2652
- ];
2653
- var LazyModuleGroup = class {
2654
- registry;
2655
- executor;
2656
- helpTextMaxLength;
2657
- commandCache = /* @__PURE__ */ new Map();
2658
- /** alias -> canonical module_id (populated lazily) */
2659
- aliasMap = /* @__PURE__ */ new Map();
2660
- /** module_id -> descriptor cache (populated during alias map build) */
2661
- descriptorCache = /* @__PURE__ */ new Map();
2662
- aliasMapBuilt = false;
2663
- constructor(registry, executor, helpTextMaxLength = 1e3) {
2664
- this.registry = registry;
2665
- this.executor = executor;
2666
- this.helpTextMaxLength = helpTextMaxLength;
2836
+ "enable",
2837
+ "disable",
2838
+ "reload",
2839
+ "config",
2840
+ "completion",
2841
+ "describe-pipeline"
2842
+ ]);
2843
+ var ApcliGroup = class _ApcliGroup {
2844
+ _mode;
2845
+ _include;
2846
+ _exclude;
2847
+ _disableEnv;
2848
+ _registryInjected;
2849
+ _fromCliConfig;
2850
+ constructor(init) {
2851
+ this._mode = init.mode;
2852
+ this._include = init.include;
2853
+ this._exclude = init.exclude;
2854
+ this._disableEnv = init.disableEnv;
2855
+ this._registryInjected = init.registryInjected;
2856
+ this._fromCliConfig = init.fromCliConfig;
2667
2857
  }
2668
2858
  /**
2669
- * Build alias->module_id map from display overlay metadata.
2859
+ * Tier 1 constructor — config came from `createCli({ apcli })`.
2860
+ *
2861
+ * A non-auto mode from this tier wins over env var and yaml.
2670
2862
  */
2671
- buildAliasMap() {
2672
- if (this.aliasMapBuilt) {
2673
- return;
2863
+ static fromCliConfig(config, opts) {
2864
+ return _ApcliGroup._build(
2865
+ config,
2866
+ opts,
2867
+ /*fromCliConfig*/
2868
+ true
2869
+ );
2870
+ }
2871
+ /**
2872
+ * Tier 3 constructor — config came from `apcore.yaml`.
2873
+ *
2874
+ * Env var (Tier 2) may override the yaml-supplied mode.
2875
+ */
2876
+ static fromYaml(config, opts) {
2877
+ return _ApcliGroup._build(
2878
+ config,
2879
+ opts,
2880
+ /*fromCliConfig*/
2881
+ false
2882
+ );
2883
+ }
2884
+ /**
2885
+ * Non-panicking Tier 3 factory (A-001 parity with Rust's `try_from_yaml`).
2886
+ * Returns `[instance, null]` on success or `[null, errorMessage]` on invalid input.
2887
+ * Use this in programmatic contexts where throwing/exiting is unwanted.
2888
+ */
2889
+ static tryFromYaml(config, opts) {
2890
+ if (config !== null && config !== void 0 && typeof config !== "boolean" && typeof config !== "object") {
2891
+ return [null, `apcore.yaml 'apcli:' must be a bool, object, or null; got ${typeof config}`];
2674
2892
  }
2675
- try {
2676
- for (const descriptor of this.registry.listModules()) {
2677
- const moduleId = descriptor.id;
2678
- this.descriptorCache.set(moduleId, descriptor);
2679
- const display = getDisplay(descriptor);
2680
- const cliDisplay = display.cli && typeof display.cli === "object" && !Array.isArray(display.cli) ? display.cli : {};
2681
- const cliAlias = cliDisplay.alias;
2682
- if (cliAlias && cliAlias !== moduleId) {
2683
- this.aliasMap.set(cliAlias, moduleId);
2893
+ if (config !== null && config !== void 0 && typeof config === "object" && !Array.isArray(config)) {
2894
+ const mode = config["mode"];
2895
+ if (mode !== void 0 && mode !== null) {
2896
+ const validModes = ["all", "none", "include", "exclude"];
2897
+ if (typeof mode !== "string" || !validModes.includes(mode)) {
2898
+ return [null, `Invalid apcli mode: '${mode}'. Must be one of: all, none, include, exclude.`];
2684
2899
  }
2685
2900
  }
2686
- this.aliasMapBuilt = true;
2687
- } catch {
2688
- warn("Failed to build alias map from registry");
2689
2901
  }
2902
+ return [_ApcliGroup.fromYaml(config, opts), null];
2903
+ }
2904
+ // -------------------------------------------------------------------------
2905
+ // Internal builder — shared by both factories
2906
+ // -------------------------------------------------------------------------
2907
+ static _build(config, opts, fromCliConfig) {
2908
+ if (config === true) {
2909
+ return new _ApcliGroup({
2910
+ mode: "all",
2911
+ include: [],
2912
+ exclude: [],
2913
+ disableEnv: false,
2914
+ registryInjected: opts.registryInjected,
2915
+ fromCliConfig
2916
+ });
2917
+ }
2918
+ if (config === false) {
2919
+ return new _ApcliGroup({
2920
+ mode: "none",
2921
+ include: [],
2922
+ exclude: [],
2923
+ disableEnv: false,
2924
+ registryInjected: opts.registryInjected,
2925
+ fromCliConfig
2926
+ });
2927
+ }
2928
+ if (config === void 0 || config === null) {
2929
+ return new _ApcliGroup({
2930
+ mode: "auto",
2931
+ include: [],
2932
+ exclude: [],
2933
+ disableEnv: false,
2934
+ registryInjected: opts.registryInjected,
2935
+ fromCliConfig
2936
+ });
2937
+ }
2938
+ if (typeof config !== "object" || Array.isArray(config)) {
2939
+ process.stderr.write(
2940
+ `Error: apcli config must be a boolean or object; got ${Array.isArray(config) ? "array" : typeof config}.
2941
+ `
2942
+ );
2943
+ process.exit(EXIT_CODES.INVALID_CLI_INPUT);
2944
+ }
2945
+ const cfg = config;
2946
+ let mode;
2947
+ if (cfg.mode === void 0 || cfg.mode === null) {
2948
+ mode = "auto";
2949
+ } else if (typeof cfg.mode !== "string") {
2950
+ process.stderr.write(
2951
+ `Error: apcli.mode must be a string; got ${typeof cfg.mode}. Expected one of all|none|include|exclude.
2952
+ `
2953
+ );
2954
+ process.exit(EXIT_CODES.INVALID_CLI_INPUT);
2955
+ } else if (!VALID_USER_MODES.has(cfg.mode)) {
2956
+ process.stderr.write(
2957
+ `Error: apcli.mode '${cfg.mode}' is invalid. Expected one of all|none|include|exclude.
2958
+ `
2959
+ );
2960
+ process.exit(EXIT_CODES.INVALID_CLI_INPUT);
2961
+ } else {
2962
+ mode = cfg.mode;
2963
+ }
2964
+ const include = _ApcliGroup._normalizeList(cfg.include, "include");
2965
+ const exclude = _ApcliGroup._normalizeList(cfg.exclude, "exclude");
2966
+ const rawDisableEnv = cfg.disableEnv !== void 0 ? cfg.disableEnv : cfg["disable_env"];
2967
+ let disableEnv = false;
2968
+ if (rawDisableEnv !== void 0) {
2969
+ if (typeof rawDisableEnv === "boolean") {
2970
+ disableEnv = rawDisableEnv;
2971
+ } else {
2972
+ warn(
2973
+ `apcli.disable_env must be boolean; got ${typeof rawDisableEnv}. Treating as false.`
2974
+ );
2975
+ }
2976
+ }
2977
+ return new _ApcliGroup({
2978
+ mode,
2979
+ include,
2980
+ exclude,
2981
+ disableEnv,
2982
+ registryInjected: opts.registryInjected,
2983
+ fromCliConfig
2984
+ });
2690
2985
  }
2691
2986
  /**
2692
- * List all available command names from the Registry.
2987
+ * Normalize an include/exclude list. Non-array warn and return [].
2988
+ *
2989
+ * Unknown but well-formed entries emit a WARNING (spec §7 error table,
2990
+ * T-APCLI-25) but are retained in the returned list for forward-compat —
2991
+ * if apcore-cli later adds a subcommand named `foo`, existing configs
2992
+ * continue to work without a config change. At runtime, unknown names
2993
+ * simply never match any registered subcommand.
2693
2994
  */
2694
- listCommands() {
2695
- this.buildAliasMap();
2696
- const reverse = /* @__PURE__ */ new Map();
2697
- for (const [alias, moduleId] of this.aliasMap) {
2698
- reverse.set(moduleId, alias);
2995
+ static _normalizeList(raw, label) {
2996
+ if (raw === void 0 || raw === null) return [];
2997
+ if (!Array.isArray(raw)) {
2998
+ warn(`apcli.${label} must be a list; got ${typeof raw}. Ignoring.`);
2999
+ return [];
2699
3000
  }
2700
- const moduleIds = this.registry.listModules().map((m) => m.id);
2701
- const names = moduleIds.map((mid) => reverse.get(mid) ?? mid);
2702
- return [...new Set(names)].sort();
3001
+ const out = [];
3002
+ for (const entry of raw) {
3003
+ if (typeof entry === "string" && entry.length > 0) {
3004
+ if (!APCLI_SUBCOMMAND_NAMES.has(entry)) {
3005
+ warn(
3006
+ `Unknown apcli subcommand '${entry}' in ${label} list \u2014 ignoring.`
3007
+ );
3008
+ }
3009
+ out.push(entry);
3010
+ } else {
3011
+ warn(`apcli.${label} contains non-string entry; skipping.`);
3012
+ }
3013
+ }
3014
+ return out;
2703
3015
  }
3016
+ // -------------------------------------------------------------------------
3017
+ // Public API
3018
+ // -------------------------------------------------------------------------
2704
3019
  /**
2705
- * Get or lazily build a Commander Command for the given module.
3020
+ * Resolve effective visibility mode after applying tier precedence.
3021
+ *
3022
+ * Returns one of `"all" | "none" | "include" | "exclude"` — never `"auto"`.
3023
+ *
3024
+ * Tier order (spec §4.4):
3025
+ * 1. CliConfig non-auto wins outright.
3026
+ * 2. `APCORE_CLI_APCLI` env var (unless sealed by disableEnv).
3027
+ * 3. yaml non-auto.
3028
+ * 4. Auto-detect from registryInjected.
2706
3029
  */
2707
- getCommand(cmdName) {
2708
- if (this.commandCache.has(cmdName)) {
2709
- return this.commandCache.get(cmdName);
3030
+ resolveVisibility() {
3031
+ if (this._fromCliConfig && this._mode !== "auto") {
3032
+ return this._mode;
2710
3033
  }
2711
- this.buildAliasMap();
2712
- const moduleId = this.aliasMap.get(cmdName) ?? cmdName;
2713
- let moduleDef = this.descriptorCache.get(moduleId);
2714
- if (!moduleDef) {
2715
- moduleDef = this.registry.getModule(moduleId) ?? void 0;
3034
+ if (!this._disableEnv) {
3035
+ const envMode = this._parseEnv(process.env.APCORE_CLI_APCLI);
3036
+ if (envMode !== null) {
3037
+ return envMode;
3038
+ }
2716
3039
  }
2717
- if (!moduleDef) {
2718
- return null;
3040
+ if (this._mode !== "auto") {
3041
+ return this._mode;
2719
3042
  }
2720
- const cmd = buildModuleCommand(moduleDef, this.executor, this.helpTextMaxLength, cmdName);
2721
- this.commandCache.set(cmdName, cmd);
2722
- return cmd;
3043
+ return this._registryInjected ? "none" : "all";
2723
3044
  }
2724
- };
2725
- var LazyGroup = class {
2726
- members;
2727
- _executor;
2728
- _helpTextMaxLength;
2729
- _cmdCache = /* @__PURE__ */ new Map();
2730
- command;
2731
- constructor(members, executor, name, helpTextMaxLength = 1e3) {
2732
- this.members = members;
2733
- this._executor = executor;
2734
- this._helpTextMaxLength = helpTextMaxLength;
2735
- this.command = new Command5(name).description(`${name} commands`);
2736
- for (const [cmdName, [, descriptor]] of this.members) {
2737
- const cmd = buildModuleCommand(
2738
- descriptor,
2739
- this._executor,
2740
- this._helpTextMaxLength,
2741
- cmdName
2742
- );
2743
- this._cmdCache.set(cmdName, cmd);
2744
- this.command.addCommand(cmd);
2745
- }
3045
+ /**
3046
+ * True iff `subcommand` passes the include/exclude filter.
3047
+ *
3048
+ * Callers MUST first check {@link resolveVisibility} — this method throws
3049
+ * under modes `"all"` or `"none"` (caller bug per spec §4.6).
3050
+ */
3051
+ isSubcommandIncluded(subcommand) {
3052
+ const mode = this.resolveVisibility();
3053
+ if (mode === "include") return this._include.includes(subcommand);
3054
+ if (mode === "exclude") return !this._exclude.includes(subcommand);
3055
+ throw new Error(
3056
+ `isSubcommandIncluded called under mode '${mode}'; caller should bypass.`
3057
+ );
2746
3058
  }
2747
- listCommands() {
2748
- return [...this.members.keys()].sort();
3059
+ /** True iff the `apcli` group itself should appear in root `--help`. */
3060
+ isGroupVisible() {
3061
+ return this.resolveVisibility() !== "none";
2749
3062
  }
2750
- getCommand(cmdName) {
2751
- if (this._cmdCache.has(cmdName)) {
2752
- return this._cmdCache.get(cmdName);
3063
+ // -------------------------------------------------------------------------
3064
+ // Env parser (Tier 2) — co-located per spec §4.4
3065
+ // -------------------------------------------------------------------------
3066
+ /**
3067
+ * Parse APCORE_CLI_APCLI. Case-insensitive.
3068
+ *
3069
+ * - `show` / `1` / `true` → `"all"`
3070
+ * - `hide` / `0` / `false` → `"none"`
3071
+ * - Empty / unset → `null`
3072
+ * - Anything else → warn and return `null`
3073
+ */
3074
+ _parseEnv(raw) {
3075
+ if (raw === void 0 || raw === "") return null;
3076
+ const normalized = raw.toLowerCase();
3077
+ if (normalized === "show" || normalized === "1" || normalized === "true") {
3078
+ return "all";
2753
3079
  }
2754
- const entry = this.members.get(cmdName);
2755
- if (!entry) {
2756
- return null;
3080
+ if (normalized === "hide" || normalized === "0" || normalized === "false") {
3081
+ return "none";
2757
3082
  }
2758
- const [, descriptor] = entry;
2759
- const cmd = buildModuleCommand(
2760
- descriptor,
2761
- this._executor,
2762
- this._helpTextMaxLength,
2763
- cmdName
3083
+ warn(
3084
+ `Unknown APCORE_CLI_APCLI value '${raw}', ignoring. Expected: show, hide, 1, 0, true, false.`
2764
3085
  );
2765
- this._cmdCache.set(cmdName, cmd);
2766
- return cmd;
3086
+ return null;
2767
3087
  }
2768
3088
  };
2769
- var GroupedModuleGroup = class _GroupedModuleGroup extends LazyModuleGroup {
2770
- /** groupName -> { cmdName -> [moduleId, descriptor] } */
2771
- groupMap = /* @__PURE__ */ new Map();
2772
- /** cmdName -> [moduleId, descriptor] for top-level (ungrouped) modules */
2773
- topLevelModules = /* @__PURE__ */ new Map();
2774
- /** Cached LazyGroup instances */
2775
- groupCache = /* @__PURE__ */ new Map();
2776
- groupMapBuilt = false;
2777
- /**
2778
- * Determine (groupName | null, commandName) for a module from its display overlay.
2779
- *
2780
- * @param groupDepth Number of dotted segments to consume as the group prefix.
2781
- * Defaults to 1 (e.g., "math.add" → group="math", cmd="add").
2782
- * Set to 2 for multi-level grouping (e.g., "math.trig.sin" →
2783
- * group="math.trig", cmd="sin").
2784
- */
2785
- static resolveGroup(moduleId, descriptor, groupDepth = 1) {
2786
- if (!moduleId) {
2787
- warn("Empty module_id encountered in resolveGroup");
2788
- return [null, ""];
3089
+
3090
+ // src/exposure.ts
3091
+ init_esm_shims();
3092
+ init_logger();
3093
+ function escapeRegex(str) {
3094
+ return str.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
3095
+ }
3096
+ function compilePattern(pattern) {
3097
+ const sentinel = "\0GLOB\0";
3098
+ const escaped = pattern.replaceAll("**", sentinel);
3099
+ const parts = escaped.split("*");
3100
+ const regexParts = parts.map((p) => {
3101
+ const restored = p.replaceAll(sentinel, "**");
3102
+ return escapeRegex(restored);
3103
+ });
3104
+ let regex = regexParts.join("[^.]*");
3105
+ regex = regex.replaceAll("\\*\\*", ".+");
3106
+ return new RegExp(`^${regex}$`);
3107
+ }
3108
+ var ExposureFilter = class _ExposureFilter {
3109
+ static VALID_MODES = ["all", "include", "exclude", "none"];
3110
+ _mode;
3111
+ _compiledInclude;
3112
+ _compiledExclude;
3113
+ constructor(mode = "all", include, exclude) {
3114
+ if (!_ExposureFilter.VALID_MODES.includes(mode)) {
3115
+ process.stderr.write(
3116
+ `Warning: Unknown ExposureFilter mode '${mode}' \u2014 defaulting to 'none'. Valid modes: ${_ExposureFilter.VALID_MODES.join(", ")}.
3117
+ `
3118
+ );
3119
+ mode = "none";
2789
3120
  }
2790
- const display = getDisplay(descriptor);
2791
- const cliDisplay = display.cli && typeof display.cli === "object" && !Array.isArray(display.cli) ? display.cli : {};
2792
- const explicitGroup = cliDisplay.group;
2793
- if (typeof explicitGroup === "string" && explicitGroup !== "") {
2794
- return [explicitGroup, cliDisplay.alias ?? moduleId];
3121
+ this._mode = mode;
3122
+ const dedup = (arr) => [...new Set(arr)];
3123
+ this._compiledInclude = dedup(include ?? []).map(compilePattern);
3124
+ this._compiledExclude = dedup(exclude ?? []).map(compilePattern);
3125
+ }
3126
+ /** Return true if the module should be exposed as a CLI command. */
3127
+ isExposed(moduleId) {
3128
+ if (this._mode === "all") return true;
3129
+ if (this._mode === "include") {
3130
+ return this._compiledInclude.some((rx) => rx.test(moduleId));
2795
3131
  }
2796
- if (explicitGroup === "") {
2797
- return [null, cliDisplay.alias ?? moduleId];
3132
+ if (this._mode === "exclude") {
3133
+ return !this._compiledExclude.some((rx) => rx.test(moduleId));
2798
3134
  }
2799
- const cliName = cliDisplay.alias ?? moduleId;
2800
- if (cliName.includes(".")) {
2801
- const parts = cliName.split(".");
2802
- const depth = Math.max(1, Math.min(groupDepth, parts.length - 1));
2803
- const group = parts.slice(0, depth).join(".");
2804
- const cmd = parts.slice(depth).join(".");
2805
- return [group, cmd];
3135
+ return false;
3136
+ }
3137
+ /** Partition moduleIds into [exposed, hidden] lists. */
3138
+ filterModules(moduleIds) {
3139
+ const exposed = [];
3140
+ const hidden = [];
3141
+ for (const mid of moduleIds) {
3142
+ (this.isExposed(mid) ? exposed : hidden).push(mid);
2806
3143
  }
2807
- return [null, cliName];
3144
+ return [exposed, hidden];
2808
3145
  }
2809
3146
  /**
2810
- * Build the group map from registry modules.
3147
+ * Create an ExposureFilter from a parsed config dict.
3148
+ *
3149
+ * Expected: `{ expose: { mode: "include", include: ["admin.*"] } }`
2811
3150
  */
2812
- buildGroupMap() {
2813
- if (this.groupMapBuilt) {
2814
- return;
3151
+ static fromConfig(config) {
3152
+ const expose = config.expose ?? {};
3153
+ if (typeof expose !== "object" || expose === null || Array.isArray(expose)) {
3154
+ warn("Invalid 'expose' config (expected dict), using mode: all.");
3155
+ return new _ExposureFilter();
2815
3156
  }
2816
- try {
2817
- this.buildAliasMap();
2818
- for (const descriptor of this.registry.listModules()) {
2819
- const moduleId = descriptor.id;
2820
- const cached = this.descriptorCache.get(moduleId);
2821
- if (!cached) {
2822
- continue;
2823
- }
2824
- const [group, cmd] = _GroupedModuleGroup.resolveGroup(moduleId, cached);
2825
- if (group === null) {
2826
- this.topLevelModules.set(cmd, [moduleId, cached]);
2827
- } else if (!/^[a-z][a-z0-9_-]*$/.test(group)) {
2828
- warn(
2829
- `Module '${moduleId}': group name '${group}' is not shell-safe \u2014 treating as top-level.`
2830
- );
2831
- this.topLevelModules.set(cmd, [moduleId, cached]);
3157
+ const exposeObj = expose;
3158
+ const mode = exposeObj.mode ?? "all";
3159
+ if (!["all", "include", "exclude"].includes(mode)) {
3160
+ throw new Error(
3161
+ `Invalid expose mode: '${mode}'. Must be one of: all, include, exclude.`
3162
+ );
3163
+ }
3164
+ let include = exposeObj.include ?? [];
3165
+ if (!Array.isArray(include)) {
3166
+ warn("Invalid 'expose.include' (expected list), ignoring.");
3167
+ include = [];
3168
+ }
3169
+ let exclude = exposeObj.exclude ?? [];
3170
+ if (!Array.isArray(exclude)) {
3171
+ warn("Invalid 'expose.exclude' (expected list), ignoring.");
3172
+ exclude = [];
3173
+ }
3174
+ const filterList = (arr, label) => {
3175
+ const result = [];
3176
+ for (const p of arr) {
3177
+ if (!p) {
3178
+ warn(`Empty pattern in expose.${label}, skipping.`);
2832
3179
  } else {
2833
- if (!this.groupMap.has(group)) {
2834
- this.groupMap.set(group, /* @__PURE__ */ new Map());
2835
- }
2836
- this.groupMap.get(group).set(cmd, [moduleId, cached]);
2837
- }
2838
- }
2839
- for (const groupName of this.groupMap.keys()) {
2840
- if (BUILTIN_COMMANDS.includes(groupName)) {
2841
- warn(
2842
- `Group name '${groupName}' collides with a built-in command and will be ignored`
2843
- );
3180
+ result.push(String(p));
2844
3181
  }
2845
3182
  }
2846
- this.groupMapBuilt = true;
2847
- } catch {
2848
- warn("Failed to build group map");
2849
- }
2850
- }
2851
- /**
2852
- * List all available command names: builtins + group names + top-level module names.
2853
- */
2854
- listCommands() {
2855
- this.buildGroupMap();
2856
- const groupNames = [...this.groupMap.keys()].filter(
2857
- (g) => !BUILTIN_COMMANDS.includes(g)
3183
+ return result;
3184
+ };
3185
+ return new _ExposureFilter(
3186
+ mode,
3187
+ filterList(include, "include"),
3188
+ filterList(exclude, "exclude")
2858
3189
  );
2859
- const topNames = [...this.topLevelModules.keys()];
2860
- return [.../* @__PURE__ */ new Set([...BUILTIN_COMMANDS, ...groupNames, ...topNames])].sort();
2861
3190
  }
2862
- /**
2863
- * Get a command by name: check builtins -> group cache -> group map -> top-level modules.
2864
- */
2865
- getCommand(cmdName) {
2866
- this.buildGroupMap();
2867
- if (this.groupCache.has(cmdName)) {
2868
- return this.groupCache.get(cmdName).command;
2869
- }
2870
- if (this.groupMap.has(cmdName)) {
2871
- const lazyGrp = new LazyGroup(
2872
- this.groupMap.get(cmdName),
2873
- this.executor,
2874
- cmdName,
2875
- this.helpTextMaxLength
2876
- );
2877
- this.groupCache.set(cmdName, lazyGrp);
2878
- return lazyGrp.command;
2879
- }
2880
- if (this.topLevelModules.has(cmdName)) {
2881
- if (this.commandCache.has(cmdName)) {
2882
- return this.commandCache.get(cmdName);
2883
- }
2884
- const [, descriptor] = this.topLevelModules.get(cmdName);
2885
- const cmd = buildModuleCommand(
2886
- descriptor,
2887
- this.executor,
2888
- this.helpTextMaxLength,
2889
- cmdName
2890
- );
2891
- this.commandCache.set(cmdName, cmd);
2892
- return cmd;
2893
- }
2894
- return null;
3191
+ };
3192
+
3193
+ // src/main.ts
3194
+ init_audit();
3195
+
3196
+ // src/canonical-help.ts
3197
+ init_esm_shims();
3198
+ function resolveHelpText(cmd, section) {
3199
+ const bag = cmd._helpText;
3200
+ const v = bag?.[section];
3201
+ if (typeof v === "function") return v({ error: false, command: cmd });
3202
+ return v ?? "";
3203
+ }
3204
+ function uppercasePlaceholders(flags) {
3205
+ return flags.replace(/<([a-zA-Z0-9_-]+)>/g, (_, name) => `<${name.toUpperCase()}>`).replace(/\[([a-zA-Z0-9_-]+)\]/g, (_, name) => `[${name.toUpperCase()}]`);
3206
+ }
3207
+ function optionTerm(opt) {
3208
+ const flags = uppercasePlaceholders(opt.flags);
3209
+ if (!opt.short && flags.startsWith("--")) return " " + flags;
3210
+ return flags;
3211
+ }
3212
+ function optionDescription(opt) {
3213
+ let desc = opt.description;
3214
+ const d = opt.defaultValue;
3215
+ if (d !== void 0 && d !== false && d !== "" && d !== null) {
3216
+ desc = `${desc} [default: ${String(d)}]`;
2895
3217
  }
2896
- /** Expose groupMap for testing. */
2897
- getGroupMap() {
2898
- return this.groupMap;
3218
+ return desc;
3219
+ }
3220
+ function reorderHelpVersionLast(opts) {
3221
+ const helpOpts = [];
3222
+ const versionOpts = [];
3223
+ const rest = [];
3224
+ for (const o of opts) {
3225
+ if (o.long === "--help") helpOpts.push(o);
3226
+ else if (o.long === "--version") versionOpts.push(o);
3227
+ else rest.push(o);
2899
3228
  }
2900
- /** Expose topLevelModules for testing. */
2901
- getTopLevelModules() {
2902
- return this.topLevelModules;
3229
+ return [...rest, ...helpOpts, ...versionOpts];
3230
+ }
3231
+ function canonicalFormatHelp(cmd, helper) {
3232
+ const sections = [];
3233
+ const beforeAll = resolveHelpText(cmd, "beforeAll");
3234
+ if (beforeAll) sections.push(beforeAll);
3235
+ const desc = cmd.description();
3236
+ if (desc) sections.push(desc);
3237
+ const before = resolveHelpText(cmd, "before");
3238
+ if (before) sections.push(before);
3239
+ const visibleOpts = reorderHelpVersionLast(helper.visibleOptions(cmd));
3240
+ const visibleCmds = helper.visibleCommands(cmd);
3241
+ const args = cmd.registeredArguments ?? [];
3242
+ let usage = `Usage: ${cmd.name()}`;
3243
+ if (visibleOpts.length > 0) usage += " [OPTIONS]";
3244
+ for (const a of args) {
3245
+ const n = a.name().toUpperCase();
3246
+ usage += a.required ? ` <${n}>` : ` [${n}]`;
2903
3247
  }
2904
- /** Expose groupMapBuilt for testing. */
2905
- isGroupMapBuilt() {
2906
- return this.groupMapBuilt;
3248
+ if (visibleCmds.length > 0) usage += " [COMMAND]";
3249
+ sections.push(usage);
3250
+ if (visibleCmds.length > 0) {
3251
+ const terms = visibleCmds.map((c) => c.name());
3252
+ const w = Math.max(...terms.map((t) => t.length));
3253
+ const lines = ["Commands:"];
3254
+ visibleCmds.forEach((sub, i) => {
3255
+ lines.push(` ${terms[i].padEnd(w)} ${sub.description()}`);
3256
+ });
3257
+ sections.push(lines.join("\n"));
2907
3258
  }
2908
- };
3259
+ if (visibleOpts.length > 0) {
3260
+ const terms = visibleOpts.map(optionTerm);
3261
+ const w = Math.max(...terms.map((t) => t.length));
3262
+ const lines = ["Options:"];
3263
+ visibleOpts.forEach((opt, i) => {
3264
+ lines.push(` ${terms[i].padEnd(w)} ${optionDescription(opt)}`);
3265
+ });
3266
+ sections.push(lines.join("\n"));
3267
+ }
3268
+ const after = resolveHelpText(cmd, "after");
3269
+ if (after) sections.push(after);
3270
+ const afterAll = resolveHelpText(cmd, "afterAll");
3271
+ if (afterAll) sections.push(afterAll);
3272
+ return sections.join("\n\n") + "\n";
3273
+ }
2909
3274
 
2910
3275
  // src/main.ts
2911
- var __dirname3 = path6.dirname(fileURLToPath3(import.meta.url));
3276
+ var __dirname2 = path4.dirname(fileURLToPath2(import.meta.url));
2912
3277
  var verboseHelp = false;
2913
3278
  function setVerboseHelp(verbose) {
2914
3279
  verboseHelp = verbose;
@@ -2920,34 +3285,37 @@ function setDocsUrl(url) {
2920
3285
  function hasVerboseFlag() {
2921
3286
  return process.argv.includes("--verbose");
2922
3287
  }
3288
+ function resolveIntOption(cliValue, envValue, defaultValue) {
3289
+ if (cliValue !== void 0 && Number.isFinite(cliValue) && cliValue > 0) {
3290
+ return cliValue;
3291
+ }
3292
+ if (envValue !== void 0 && envValue !== "") {
3293
+ const parsed = parseInt(envValue, 10);
3294
+ if (Number.isFinite(parsed) && parsed > 0) {
3295
+ return parsed;
3296
+ }
3297
+ process.stderr.write(
3298
+ `Warning: invalid integer env value '${envValue}'; using default ${defaultValue}.
3299
+ `
3300
+ );
3301
+ }
3302
+ return defaultValue;
3303
+ }
3304
+ function resolveStringOption(cliValue, envValue) {
3305
+ if (typeof cliValue === "string" && cliValue !== "") {
3306
+ return cliValue;
3307
+ }
3308
+ if (envValue !== void 0 && envValue !== "") {
3309
+ return envValue;
3310
+ }
3311
+ return void 0;
3312
+ }
2923
3313
  var VERSION = "0.0.0";
2924
3314
  try {
2925
- const pkg = JSON.parse(readFileSync3(path6.resolve(__dirname3, "../package.json"), "utf-8"));
3315
+ const pkg = JSON.parse(readFileSync2(path4.resolve(__dirname2, "../package.json"), "utf-8"));
2926
3316
  VERSION = pkg.version;
2927
3317
  } catch {
2928
3318
  }
2929
- var ERROR_CODE_MAP = {
2930
- MODULE_NOT_FOUND: 44,
2931
- MODULE_LOAD_ERROR: 44,
2932
- MODULE_DISABLED: 44,
2933
- SCHEMA_VALIDATION_ERROR: 45,
2934
- SCHEMA_CIRCULAR_REF: 48,
2935
- APPROVAL_DENIED: 46,
2936
- APPROVAL_TIMEOUT: 46,
2937
- APPROVAL_PENDING: 46,
2938
- CONFIG_NOT_FOUND: 47,
2939
- CONFIG_INVALID: 47,
2940
- MODULE_EXECUTE_ERROR: 1,
2941
- MODULE_TIMEOUT: 1,
2942
- ACL_DENIED: 77,
2943
- CONFIG_NAMESPACE_RESERVED: 78,
2944
- CONFIG_NAMESPACE_DUPLICATE: 78,
2945
- CONFIG_ENV_PREFIX_CONFLICT: 78,
2946
- CONFIG_ENV_MAP_CONFLICT: 78,
2947
- CONFIG_MOUNT_ERROR: 66,
2948
- CONFIG_BIND_ERROR: 65,
2949
- ERROR_FORMATTER_DUPLICATE: 70
2950
- };
2951
3319
  function emitErrorJson(e, exitCode) {
2952
3320
  const err = e instanceof Error ? e : new Error(String(e));
2953
3321
  const errRecord = err;
@@ -3001,60 +3369,139 @@ function createCli(extensionsDirOrOpts, progName, verbose = false) {
3001
3369
  let registry;
3002
3370
  let executor;
3003
3371
  let extraCommands;
3372
+ let app;
3373
+ let expose;
3374
+ let apcliOption;
3004
3375
  if (typeof extensionsDirOrOpts === "object" && extensionsDirOrOpts !== null) {
3005
3376
  extensionsDir = extensionsDirOrOpts.extensionsDir;
3006
3377
  progName = extensionsDirOrOpts.progName ?? progName;
3007
3378
  verbose = extensionsDirOrOpts.verbose ?? verbose;
3379
+ app = extensionsDirOrOpts.app;
3008
3380
  registry = extensionsDirOrOpts.registry;
3009
3381
  executor = extensionsDirOrOpts.executor;
3010
3382
  extraCommands = extensionsDirOrOpts.extraCommands;
3383
+ expose = extensionsDirOrOpts.expose;
3384
+ apcliOption = extensionsDirOrOpts.apcli;
3011
3385
  } else {
3012
3386
  extensionsDir = extensionsDirOrOpts;
3013
3387
  }
3014
3388
  verboseHelp = verbose;
3015
3389
  registerConfigNamespace();
3016
- const resolvedProgName = progName ?? path6.basename(process.argv[1] ?? "apcore-cli") ?? "apcore-cli";
3390
+ try {
3391
+ const auditLogger = new AuditLogger();
3392
+ setAuditLogger(auditLogger);
3393
+ } catch {
3394
+ }
3395
+ const resolvedProgName = progName ?? path4.basename(process.argv[1] ?? "apcore-cli") ?? "apcore-cli";
3017
3396
  const cliLogLevel = process.env.APCORE_CLI_LOGGING_LEVEL ?? process.env.APCORE_LOGGING_LEVEL ?? "WARNING";
3018
3397
  setLogLevel(cliLogLevel);
3019
- const program = new Command6(resolvedProgName).exitOverride().version(VERSION, "--version", `Show ${resolvedProgName} version`).description("apcore CLI \u2014 execute apcore modules from the command line").option("--extensions-dir <path>", "Path to extensions directory").option("--commands-dir <path>", "Path to convention-based commands directory").option("--binding <path>", "Path to binding.yaml for display overlay").option("--log-level <level>", "Logging level (DEBUG|INFO|WARNING|ERROR)", "WARNING").option("--verbose", "Show all options in help output (including built-in apcore options)");
3398
+ if (app && (registry || executor)) {
3399
+ process.stderr.write("Error: app is mutually exclusive with registry/executor\n");
3400
+ process.exit(EXIT_CODES.INVALID_CLI_INPUT);
3401
+ }
3402
+ if (app) {
3403
+ registry = app.registry;
3404
+ executor = app.executor;
3405
+ }
3020
3406
  if (executor && !registry) {
3021
- throw new Error("executor requires registry \u2014 pass both or neither");
3407
+ process.stderr.write("Error: executor requires registry \u2014 pass both or neither\n");
3408
+ process.exit(EXIT_CODES.INVALID_CLI_INPUT);
3409
+ }
3410
+ if (executor && typeof executor.setApprovalHandler === "function") {
3411
+ try {
3412
+ const handler = new CliApprovalHandler(
3413
+ /*autoApprove*/
3414
+ false
3415
+ );
3416
+ executor.setApprovalHandler(handler);
3417
+ } catch {
3418
+ }
3419
+ }
3420
+ const registryInjected = registry !== void 0;
3421
+ const program = new Command5(resolvedProgName).exitOverride().version(VERSION, "-V, --version", "Print version").helpOption("-h, --help", "Print help").addHelpCommand("help [command]", "Print this message or the help of the given subcommand(s)").description("apcore CLI \u2014 execute apcore modules from the command line").option("--log-level <level>", "Logging level (DEBUG|INFO|WARNING|ERROR)", "WARNING").option("--verbose", "Show all options in help output (including built-in apcore options)");
3422
+ program.configureHelp({ formatHelp: canonicalFormatHelp });
3423
+ if (!registryInjected) {
3424
+ program.option("--extensions-dir <path>", "Path to extensions directory");
3425
+ program.option("--commands-dir <path>", "Path to convention-based commands directory");
3426
+ program.option("--binding <path>", "Path to binding.yaml for display overlay");
3427
+ }
3428
+ let apcliCfg;
3429
+ if (apcliOption instanceof ApcliGroup) {
3430
+ apcliCfg = apcliOption;
3431
+ } else if (apcliOption !== void 0) {
3432
+ apcliCfg = ApcliGroup.fromCliConfig(apcliOption, { registryInjected });
3433
+ } else {
3434
+ let yamlVal = null;
3435
+ try {
3436
+ const resolver = new ConfigResolver();
3437
+ yamlVal = resolver.resolveObject("apcli");
3438
+ } catch {
3439
+ yamlVal = null;
3440
+ }
3441
+ apcliCfg = ApcliGroup.fromYaml(yamlVal, { registryInjected });
3022
3442
  }
3443
+ const apcliGroup = program.command("apcli", { hidden: !apcliCfg.isGroupVisible() }).description("apcore-cli built-in commands");
3023
3444
  if (registry) {
3024
3445
  program._registry = registry;
3025
3446
  if (executor) {
3026
3447
  program._executor = executor;
3027
- registerValidateCommand(program, registry, executor);
3028
- void registerSystemCommands(program, executor);
3029
- registerPipelineCommand(program, executor);
3030
3448
  }
3031
3449
  } else {
3032
3450
  const resolvedExtDir = extensionsDir ?? process.env.APCORE_EXTENSIONS_ROOT ?? "./extensions";
3033
3451
  void resolvedExtDir;
3034
3452
  }
3453
+ let exposureFilter;
3454
+ try {
3455
+ if (expose instanceof ExposureFilter) {
3456
+ exposureFilter = expose;
3457
+ } else if (typeof expose === "object" && expose !== null) {
3458
+ exposureFilter = ExposureFilter.fromConfig({ expose });
3459
+ } else {
3460
+ exposureFilter = new ExposureFilter();
3461
+ }
3462
+ } catch (err) {
3463
+ const msg = err instanceof Error ? err.message : String(err);
3464
+ process.stderr.write(`Error: invalid 'expose' option \u2014 ${msg}
3465
+ `);
3466
+ process.exit(EXIT_CODES.INVALID_CLI_INPUT);
3467
+ }
3468
+ _registerApcliSubcommands(apcliGroup, apcliCfg, registry, executor, exposureFilter);
3469
+ _registerDeprecationShims(program, apcliGroup, registryInjected, resolvedProgName);
3035
3470
  program.addHelpText("after", [
3036
3471
  "",
3037
3472
  "Use --help --verbose to show all options (including built-in apcore options).",
3038
3473
  "Use --help --man to display a formatted man page."
3039
3474
  ].join("\n"));
3040
- registerInitCommand(program);
3041
3475
  configureManHelp(program, resolvedProgName, VERSION);
3042
3476
  if (extraCommands && extraCommands.length > 0) {
3043
- const existingNames = /* @__PURE__ */ new Set([
3044
- ...BUILTIN_COMMANDS,
3045
- ...program.commands.map((c) => c.name())
3046
- ]);
3047
3477
  for (const cmd of extraCommands) {
3048
3478
  const cmdName = cmd.name();
3049
- if (existingNames.has(cmdName)) {
3479
+ if (RESERVED_GROUP_NAMES.has(cmdName)) {
3050
3480
  process.stderr.write(
3051
- `Warning: Extra command '${cmdName}' collides with a built-in command and will be skipped.
3481
+ `Error: extraCommands name '${cmdName}' is reserved
3052
3482
  `
3053
3483
  );
3054
- continue;
3484
+ process.exit(EXIT_CODES.INVALID_CLI_INPUT);
3485
+ }
3486
+ const existing = program.commands.find((c) => c.name() === cmdName);
3487
+ if (existing) {
3488
+ const isShim = existing.__isDeprecationShim === true;
3489
+ if (isShim) {
3490
+ warn(
3491
+ `extraCommands '${cmdName}' overrides the deprecation shim for the same name. The shim will be removed.`
3492
+ );
3493
+ const cmds = program.commands;
3494
+ const idx = cmds.indexOf(existing);
3495
+ if (idx >= 0) cmds.splice(idx, 1);
3496
+ } else {
3497
+ process.stderr.write(
3498
+ `Error: extraCommands name '${cmdName}' collides with an existing command
3499
+ `
3500
+ );
3501
+ process.exit(EXIT_CODES.INVALID_CLI_INPUT);
3502
+ }
3055
3503
  }
3056
3504
  program.addCommand(cmd);
3057
- existingNames.add(cmdName);
3058
3505
  }
3059
3506
  }
3060
3507
  program.hook("preAction", async (thisCommand) => {
@@ -3065,39 +3512,160 @@ function createCli(extensionsDirOrOpts, progName, verbose = false) {
3065
3512
  });
3066
3513
  return program;
3067
3514
  }
3515
+ var _ALWAYS_REGISTERED = /* @__PURE__ */ new Set(["exec"]);
3516
+ function _registerApcliSubcommands(apcliGroup, apcliCfg, registry, executor, exposureFilter) {
3517
+ const emitUnwiredError = () => {
3518
+ process.stderr.write(
3519
+ "Error: no apcore-js registry wired. In standalone mode, pass --extensions-dir <path> to enable module discovery.\n"
3520
+ );
3521
+ process.exit(EXIT_CODES.CONFIG_INVALID);
3522
+ };
3523
+ const effectiveRegistry = registry ?? {
3524
+ listModules: () => emitUnwiredError(),
3525
+ getModule: () => emitUnwiredError()
3526
+ };
3527
+ const TABLE = [
3528
+ { name: "list", requiresExecutor: false, register: (g) => registerListCommand(g, effectiveRegistry, exposureFilter) },
3529
+ { name: "describe", requiresExecutor: false, register: (g) => registerDescribeCommand(g, effectiveRegistry) },
3530
+ { name: "exec", requiresExecutor: true, register: (g, _r, ex) => registerExecCommand(g, effectiveRegistry, ex) },
3531
+ { name: "validate", requiresExecutor: true, register: (g, _r, ex) => registerValidateCommand(g, effectiveRegistry, ex) },
3532
+ { name: "init", requiresExecutor: false, register: (g) => registerInitCommand(g) },
3533
+ { name: "health", requiresExecutor: true, register: (g, _r, ex) => registerHealthCommand(g, ex) },
3534
+ { name: "usage", requiresExecutor: true, register: (g, _r, ex) => registerUsageCommand(g, ex) },
3535
+ { name: "enable", requiresExecutor: true, register: (g, _r, ex) => registerEnableCommand(g, ex) },
3536
+ { name: "disable", requiresExecutor: true, register: (g, _r, ex) => registerDisableCommand(g, ex) },
3537
+ { name: "reload", requiresExecutor: true, register: (g, _r, ex) => registerReloadCommand(g, ex) },
3538
+ { name: "config", requiresExecutor: true, register: (g, _r, ex) => registerConfigCommand(g, ex) },
3539
+ { name: "completion", requiresExecutor: false, register: (g) => registerCompletionCommand(g) },
3540
+ { name: "describe-pipeline", requiresExecutor: true, register: (g, _r, ex) => registerPipelineCommand(g, ex) }
3541
+ ];
3542
+ const mode = apcliCfg.resolveVisibility();
3543
+ for (const entry of TABLE) {
3544
+ let shouldRegister;
3545
+ if (mode === "all" || mode === "none") {
3546
+ shouldRegister = true;
3547
+ } else {
3548
+ shouldRegister = _ALWAYS_REGISTERED.has(entry.name) || apcliCfg.isSubcommandIncluded(entry.name);
3549
+ }
3550
+ if (!shouldRegister) continue;
3551
+ if (entry.requiresExecutor && !executor) {
3552
+ if (_ALWAYS_REGISTERED.has(entry.name)) {
3553
+ warn(
3554
+ `apcli.${entry.name} is in _ALWAYS_REGISTERED but no executor is wired \u2014 subcommand unavailable. Pass executor to createCli() or avoid ${entry.name} invocations.`
3555
+ );
3556
+ }
3557
+ continue;
3558
+ }
3559
+ entry.register(apcliGroup, registry, executor);
3560
+ }
3561
+ }
3562
+ var _DEPRECATED_ROOT_COMMANDS = [
3563
+ "list",
3564
+ "describe",
3565
+ "exec",
3566
+ "init",
3567
+ "validate",
3568
+ "health",
3569
+ "usage",
3570
+ "enable",
3571
+ "disable",
3572
+ "reload",
3573
+ "config",
3574
+ "completion",
3575
+ "describe-pipeline"
3576
+ ];
3577
+ function _registerDeprecationShims(root, apcliGroup, registryInjected, cliName) {
3578
+ if (registryInjected) return;
3579
+ for (const name of _DEPRECATED_ROOT_COMMANDS) {
3580
+ const apcliSub = apcliGroup.commands.find((c) => c.name() === name);
3581
+ if (!apcliSub) continue;
3582
+ if (root.commands.some((c) => c.name() === name)) continue;
3583
+ const shim = root.command(name).description(`[DEPRECATED] Use '${cliName} apcli ${name}' instead.`).allowUnknownOption(true).allowExcessArguments(true).helpOption(false);
3584
+ shim.__isDeprecationShim = true;
3585
+ shim.action(async function() {
3586
+ process.stderr.write(
3587
+ `WARNING: '${name}' as a root-level command is deprecated. Use '${cliName} apcli ${name}' instead.
3588
+ Will be removed in v0.8. See: https://aiperceivable.github.io/apcore-cli/features/builtin-group/#11-migration
3589
+ `
3590
+ );
3591
+ const tail = _collectShimForwardArgs(this);
3592
+ await apcliSub.parseAsync(tail, { from: "user" });
3593
+ });
3594
+ }
3595
+ }
3596
+ function _collectShimForwardArgs(shim) {
3597
+ const shimArgs = (shim.args ?? []).slice();
3598
+ if (shimArgs.length > 0) return shimArgs;
3599
+ const shimName = shim.name();
3600
+ const idx = process.argv.indexOf(shimName);
3601
+ if (idx < 0) return [];
3602
+ return process.argv.slice(idx + 1);
3603
+ }
3604
+ var bindingDisplayMap = /* @__PURE__ */ new Map();
3605
+ function lookupBindingDisplay(moduleId) {
3606
+ return bindingDisplayMap.get(moduleId);
3607
+ }
3068
3608
  async function applyToolkitIntegration(commandsDir, bindingPath) {
3069
3609
  if (!commandsDir && !bindingPath) {
3070
3610
  return;
3071
3611
  }
3612
+ let toolkit;
3072
3613
  try {
3073
3614
  const toolkitModule = "apcore-toolkit";
3074
- const toolkit = await import(
3615
+ toolkit = await import(
3075
3616
  /* @vite-ignore */
3076
3617
  toolkitModule
3077
3618
  );
3078
- if (commandsDir) {
3079
- console.warn("Convention scanning not yet available in TypeScript toolkit");
3080
- }
3081
- if (bindingPath) {
3082
- const resolver = new toolkit.DisplayResolver();
3083
- void resolver;
3084
- }
3085
3619
  } catch {
3086
- console.warn("apcore-toolkit not installed \u2014 toolkit features unavailable");
3620
+ warn("apcore-toolkit not installed \u2014 toolkit features unavailable");
3621
+ return;
3087
3622
  }
3088
- }
3089
- function main(progName) {
3623
+ if (commandsDir) {
3624
+ warn("Convention scanning not available in the TypeScript toolkit");
3625
+ }
3626
+ if (bindingPath) {
3627
+ try {
3628
+ await loadBindingDisplayOverlay(toolkit, bindingPath);
3629
+ } catch (err) {
3630
+ const msg = err instanceof Error ? err.message : String(err);
3631
+ warn(`apcore-toolkit: failed to load binding '${bindingPath}': ${msg}`);
3632
+ }
3633
+ }
3634
+ }
3635
+ async function loadBindingDisplayOverlay(toolkit, bindingPath) {
3636
+ const BindingLoaderCtor = toolkit.BindingLoader;
3637
+ const DisplayResolverCtor = toolkit.DisplayResolver;
3638
+ if (!BindingLoaderCtor || !DisplayResolverCtor) {
3639
+ return;
3640
+ }
3641
+ const loader = new BindingLoaderCtor();
3642
+ const scanned = loader.load(bindingPath);
3643
+ const resolver = new DisplayResolverCtor();
3644
+ const resolved = resolver.resolve(scanned, { bindingPath });
3645
+ for (const mod of resolved) {
3646
+ if (!mod || typeof mod !== "object") continue;
3647
+ const entry = mod;
3648
+ const id = typeof entry.moduleId === "string" ? entry.moduleId : null;
3649
+ if (!id) continue;
3650
+ const meta = entry.metadata ?? {};
3651
+ const display = meta.display;
3652
+ if (display && typeof display === "object" && !Array.isArray(display)) {
3653
+ bindingDisplayMap.set(id, display);
3654
+ }
3655
+ }
3656
+ }
3657
+ function main(progName) {
3090
3658
  verboseHelp = hasVerboseFlag();
3091
3659
  const program = createCli(void 0, progName, verboseHelp);
3092
3660
  try {
3093
3661
  program.parse(process.argv);
3094
- } catch (error2) {
3095
- if (error2 instanceof CommanderError) {
3096
- process.exit(error2.exitCode);
3662
+ } catch (error) {
3663
+ if (error instanceof CommanderError) {
3664
+ process.exit(error.exitCode);
3097
3665
  }
3098
- const code = exitCodeForError(error2);
3099
- if (error2 instanceof Error) {
3100
- process.stderr.write(`Error: ${error2.message}
3666
+ const code = exitCodeForError(error);
3667
+ if (error instanceof Error) {
3668
+ process.stderr.write(`Error: ${error.message}
3101
3669
  `);
3102
3670
  }
3103
3671
  process.exit(code);
@@ -3120,7 +3688,7 @@ function buildModuleCommand(moduleDef, executor, helpTextMaxLength = 1e3, cmdNam
3120
3688
  }
3121
3689
  schemaOptions = schemaToCliOptions(resolvedSchema, helpTextMaxLength);
3122
3690
  }
3123
- const cmd = new Command6(effectiveCmdName).description(cmdHelp);
3691
+ const cmd = new Command5(effectiveCmdName).description(cmdHelp);
3124
3692
  const inputOpt = new Option4("--input <source>", "Read JSON input from a file path, or use '-' to read from stdin pipe");
3125
3693
  const yesOpt = new Option4("-y, --yes", "Skip interactive approval prompts (for scripts and CI)").default(false);
3126
3694
  const largeInputOpt = new Option4("--large-input", "Allow stdin input larger than 10MB (default limit protects against accidental pipes)").default(false);
@@ -3168,30 +3736,6 @@ function buildModuleCommand(moduleDef, executor, helpTextMaxLength = 1e3, cmdNam
3168
3736
  if (footerParts.length > 0) {
3169
3737
  cmd.addHelpText("after", "\n" + footerParts.join("\n") + "\n");
3170
3738
  }
3171
- const reservedNames = /* @__PURE__ */ new Set([
3172
- "input",
3173
- "yes",
3174
- "largeInput",
3175
- "format",
3176
- "fields",
3177
- "sandbox",
3178
- "verbose",
3179
- "dryRun",
3180
- "trace",
3181
- "stream",
3182
- "strategy",
3183
- "approvalTimeout",
3184
- "approvalToken"
3185
- ]);
3186
- for (const opt of schemaOptions) {
3187
- if (reservedNames.has(opt.name)) {
3188
- process.stderr.write(
3189
- `Error: Module '${moduleId}' schema property '${opt.name}' conflicts with a reserved CLI option name. Rename the property.
3190
- `
3191
- );
3192
- process.exit(EXIT_CODES.INVALID_CLI_INPUT);
3193
- }
3194
- }
3195
3739
  for (const opt of schemaOptions) {
3196
3740
  if (opt.parseArg) {
3197
3741
  cmd.option(opt.flags, opt.description, opt.parseArg, opt.defaultValue);
@@ -3209,8 +3753,12 @@ function buildModuleCommand(moduleDef, executor, helpTextMaxLength = 1e3, cmdNam
3209
3753
  const dryRun = options.dryRun;
3210
3754
  const traceFlag = options.trace;
3211
3755
  const streamFlag = options.stream;
3212
- const strategyName = options.strategy;
3213
- const approvalTimeout = options.approvalTimeout ?? 60;
3756
+ const strategyName = resolveStringOption(options.strategy, process.env.APCORE_CLI_STRATEGY);
3757
+ const approvalTimeout = resolveIntOption(
3758
+ options.approvalTimeout,
3759
+ process.env.APCORE_CLI_APPROVAL_TIMEOUT,
3760
+ 60
3761
+ );
3214
3762
  const approvalToken = options.approvalToken;
3215
3763
  const schemaKwargs = {};
3216
3764
  const builtinKeys = /* @__PURE__ */ new Set([
@@ -3234,6 +3782,7 @@ function buildModuleCommand(moduleDef, executor, helpTextMaxLength = 1e3, cmdNam
3234
3782
  }
3235
3783
  }
3236
3784
  let merged = {};
3785
+ const startTime = performance.now();
3237
3786
  try {
3238
3787
  merged = await collectInput(stdinFlag, schemaKwargs, largeInput);
3239
3788
  const reconverted = reconvertEnumValues(merged, schemaOptions);
@@ -3241,7 +3790,7 @@ function buildModuleCommand(moduleDef, executor, helpTextMaxLength = 1e3, cmdNam
3241
3790
  if (dryRun) {
3242
3791
  if (!executor.validate) {
3243
3792
  process.stderr.write("Error: Executor does not support validate.\n");
3244
- process.exit(1);
3793
+ process.exit(EXIT_CODES.MODULE_EXECUTE_ERROR);
3245
3794
  }
3246
3795
  const preflight = await executor.validate(moduleId, merged);
3247
3796
  formatPreflightResult(preflight, outputFormat);
@@ -3283,7 +3832,6 @@ function buildModuleCommand(moduleDef, executor, helpTextMaxLength = 1e3, cmdNam
3283
3832
  merged._approval_token = approvalToken;
3284
3833
  }
3285
3834
  await checkApproval(moduleDef, autoApprove, approvalTimeout);
3286
- const startTime = performance.now();
3287
3835
  if (streamFlag) {
3288
3836
  if (resolveFormat(outputFormat) === "table") {
3289
3837
  process.stderr.write("Warning: Streaming mode always outputs JSONL; --format table is ignored.\n");
@@ -3324,22 +3872,17 @@ function buildModuleCommand(moduleDef, executor, helpTextMaxLength = 1e3, cmdNam
3324
3872
  strategyName ? { strategy: strategyName } : void 0
3325
3873
  );
3326
3874
  const durationMs2 = Math.round(performance.now() - startTime);
3327
- const { getAuditLogger: getAuditLogger3 } = await Promise.resolve().then(() => (init_audit(), audit_exports));
3328
- const auditLogger2 = getAuditLogger3();
3329
- if (auditLogger2) {
3330
- auditLogger2.logExecution(moduleId, merged, "success", 0, durationMs2);
3331
- }
3332
3875
  const resolved = resolveFormat(outputFormat);
3333
3876
  if (resolved === "json" || !process.stdout.isTTY) {
3334
3877
  const traceData = {
3335
- strategy: trace.strategy_name,
3336
- total_duration_ms: trace.total_duration_ms,
3878
+ strategy: trace.strategyName,
3879
+ total_duration_ms: trace.totalDurationMs,
3337
3880
  success: trace.success,
3338
3881
  steps: trace.steps.map((s) => ({
3339
3882
  name: s.name,
3340
- duration_ms: s.duration_ms,
3883
+ duration_ms: s.durationMs,
3341
3884
  skipped: s.skipped,
3342
- ...s.skipped ? { skip_reason: s.skip_reason } : {}
3885
+ ...s.skipped ? { skip_reason: s.skipReason ?? null } : {}
3343
3886
  }))
3344
3887
  };
3345
3888
  let output;
@@ -3354,20 +3897,25 @@ function buildModuleCommand(moduleDef, executor, helpTextMaxLength = 1e3, cmdNam
3354
3897
  const stepCount = trace.steps.length;
3355
3898
  process.stderr.write(
3356
3899
  `
3357
- Pipeline Trace (strategy: ${trace.strategy_name}, ${stepCount} steps, ${trace.total_duration_ms.toFixed(1)}ms)
3900
+ Pipeline Trace (strategy: ${trace.strategyName}, ${stepCount} steps, ${trace.totalDurationMs.toFixed(1)}ms)
3358
3901
  `
3359
3902
  );
3360
3903
  for (const s of trace.steps) {
3361
3904
  if (s.skipped) {
3362
- const reason = s.skip_reason ?? "n/a";
3905
+ const reason = s.skipReason ?? "n/a";
3363
3906
  process.stderr.write(` \u25CB ${s.name.padEnd(24)} ${"\u2014".padStart(8)} skipped (${reason})
3364
3907
  `);
3365
3908
  } else {
3366
- process.stderr.write(` \u2713 ${s.name.padEnd(24)} ${(s.duration_ms.toFixed(1) + "ms").padStart(8)}
3909
+ process.stderr.write(` \u2713 ${s.name.padEnd(24)} ${(s.durationMs.toFixed(1) + "ms").padStart(8)}
3367
3910
  `);
3368
3911
  }
3369
3912
  }
3370
3913
  }
3914
+ const { getAuditLogger: getAL2 } = await Promise.resolve().then(() => (init_audit(), audit_exports));
3915
+ const al2 = getAL2();
3916
+ if (al2) {
3917
+ al2.logExecution(moduleId, merged, "success", 0, durationMs2);
3918
+ }
3371
3919
  return;
3372
3920
  }
3373
3921
  let result;
@@ -3388,21 +3936,20 @@ Pipeline Trace (strategy: ${trace.strategy_name}, ${stepCount} steps, ${trace.to
3388
3936
  result = await sandbox.execute(moduleId, merged, executor);
3389
3937
  }
3390
3938
  const durationMs = Math.round(performance.now() - startTime);
3939
+ formatExecResult(result, outputFormat, outputFields);
3391
3940
  const { getAuditLogger: getAuditLogger2 } = await Promise.resolve().then(() => (init_audit(), audit_exports));
3392
3941
  const auditLogger = getAuditLogger2();
3393
3942
  if (auditLogger) {
3394
3943
  auditLogger.logExecution(moduleId, merged, "success", 0, durationMs);
3395
3944
  }
3396
- formatExecResult(result, outputFormat, outputFields);
3397
3945
  } catch (err) {
3398
- const errRecord = err;
3399
- const errorCode = typeof errRecord?.code === "string" ? errRecord.code : void 0;
3400
- const exitCode = errorCode && errorCode in ERROR_CODE_MAP ? ERROR_CODE_MAP[errorCode] : exitCodeForError(err);
3946
+ const exitCode = exitCodeForError(err);
3947
+ const durationMs = Math.round(performance.now() - startTime);
3401
3948
  try {
3402
3949
  const { getAuditLogger: getAuditLogger2 } = await Promise.resolve().then(() => (init_audit(), audit_exports));
3403
3950
  const auditLogger = getAuditLogger2();
3404
3951
  if (auditLogger) {
3405
- auditLogger.logExecution(moduleId, merged, "error", exitCode, 0);
3952
+ auditLogger.logExecution(moduleId, merged, "error", exitCode, durationMs);
3406
3953
  }
3407
3954
  } catch {
3408
3955
  }
@@ -3417,9 +3964,9 @@ Pipeline Trace (strategy: ${trace.strategy_name}, ${stepCount} steps, ${trace.to
3417
3964
  return cmd;
3418
3965
  }
3419
3966
  function validateModuleId(moduleId) {
3420
- if (moduleId.length > 128) {
3967
+ if (moduleId.length > 192) {
3421
3968
  process.stderr.write(
3422
- `Error: Invalid module ID format: '${moduleId}'. Maximum length is 128 characters.
3969
+ `Error: Invalid module ID format: '${moduleId}'. Maximum length is 192 characters.
3423
3970
  `
3424
3971
  );
3425
3972
  process.exit(EXIT_CODES.INVALID_CLI_INPUT);
@@ -3442,45 +3989,57 @@ async function collectInput(stdinFlag, cliKwargs = {}, largeInput) {
3442
3989
  if (!stdinFlag) {
3443
3990
  return cliKwargsNonNull;
3444
3991
  }
3992
+ let raw;
3993
+ let source;
3445
3994
  if (stdinFlag === "-") {
3446
- const raw = await readStdin();
3447
- const rawSize = Buffer.byteLength(raw, "utf-8");
3448
- if (rawSize > 10485760 && !largeInput) {
3449
- process.stderr.write(
3450
- "Error: STDIN input exceeds 10MB limit. Use --large-input to override.\n"
3451
- );
3452
- process.exit(EXIT_CODES.INVALID_CLI_INPUT);
3453
- }
3454
- if (!raw) {
3455
- return cliKwargsNonNull;
3456
- }
3457
- let stdinData;
3995
+ raw = await readStdin();
3996
+ source = "STDIN";
3997
+ } else {
3998
+ source = `file '${stdinFlag}'`;
3458
3999
  try {
3459
- stdinData = JSON.parse(raw);
3460
- } catch {
3461
- process.stderr.write(
3462
- "Error: STDIN does not contain valid JSON.\n"
3463
- );
4000
+ raw = readFileSync2(stdinFlag, "utf-8");
4001
+ } catch (err) {
4002
+ const msg = err instanceof Error ? err.message : String(err);
4003
+ process.stderr.write(`Error: Could not read input ${source}: ${msg}
4004
+ `);
3464
4005
  process.exit(EXIT_CODES.INVALID_CLI_INPUT);
3465
4006
  }
3466
- if (typeof stdinData !== "object" || stdinData === null || Array.isArray(stdinData)) {
3467
- process.stderr.write(
3468
- `Error: STDIN JSON must be an object, got ${Array.isArray(stdinData) ? "array" : typeof stdinData}.
4007
+ }
4008
+ const rawSize = Buffer.byteLength(raw, "utf-8");
4009
+ if (rawSize > 10485760 && !largeInput) {
4010
+ process.stderr.write(
4011
+ `Error: ${source} input exceeds 10MB limit. Use --large-input to override.
3469
4012
  `
3470
- );
3471
- process.exit(EXIT_CODES.INVALID_CLI_INPUT);
3472
- }
3473
- return { ...stdinData, ...cliKwargsNonNull };
4013
+ );
4014
+ process.exit(EXIT_CODES.INVALID_CLI_INPUT);
4015
+ }
4016
+ if (!raw) {
4017
+ return cliKwargsNonNull;
3474
4018
  }
3475
- return cliKwargsNonNull;
4019
+ let parsed;
4020
+ try {
4021
+ parsed = JSON.parse(raw);
4022
+ } catch {
4023
+ process.stderr.write(`Error: ${source} does not contain valid JSON.
4024
+ `);
4025
+ process.exit(EXIT_CODES.INVALID_CLI_INPUT);
4026
+ }
4027
+ if (typeof parsed !== "object" || parsed === null || Array.isArray(parsed)) {
4028
+ process.stderr.write(
4029
+ `Error: ${source} JSON must be an object, got ${Array.isArray(parsed) ? "array" : typeof parsed}.
4030
+ `
4031
+ );
4032
+ process.exit(EXIT_CODES.INVALID_CLI_INPUT);
4033
+ }
4034
+ return { ...parsed, ...cliKwargsNonNull };
3476
4035
  }
3477
4036
  function readStdin() {
3478
- return new Promise((resolve3, reject) => {
4037
+ return new Promise((resolve2, reject) => {
3479
4038
  const chunks = [];
3480
4039
  const onData = (chunk) => chunks.push(chunk);
3481
4040
  const onEnd = () => {
3482
4041
  cleanup();
3483
- resolve3(Buffer.concat(chunks).toString("utf-8"));
4042
+ resolve2(Buffer.concat(chunks).toString("utf-8"));
3484
4043
  };
3485
4044
  const onError = (err) => {
3486
4045
  cleanup();
@@ -3518,73 +4077,380 @@ function reconvertEnumValues(kwargs, options) {
3518
4077
  return result;
3519
4078
  }
3520
4079
 
4080
+ // src/cli.ts
4081
+ init_esm_shims();
4082
+ import { Command as Command6 } from "commander";
4083
+ init_logger();
4084
+ init_errors();
4085
+ function assertNotReserved(kind, name, moduleId) {
4086
+ if (!RESERVED_GROUP_NAMES.has(name)) return;
4087
+ let msg;
4088
+ if (kind === "group") {
4089
+ msg = `Error: Module '${moduleId}': display.cli.group '${name}' is reserved. Use a different CLI alias or set display.cli.group to another value.
4090
+ `;
4091
+ } else if (kind === "auto-group") {
4092
+ msg = `Error: Module '${moduleId}': auto-group '${name}' is reserved. Rename the module id or set display.cli.group to another value.
4093
+ `;
4094
+ } else {
4095
+ msg = `Error: Module '${moduleId}': top-level CLI name '${name}' is reserved. Use a different CLI alias.
4096
+ `;
4097
+ }
4098
+ process.stderr.write(msg);
4099
+ process.exit(EXIT_CODES.INVALID_CLI_INPUT);
4100
+ }
4101
+ var LazyModuleGroup = class {
4102
+ registry;
4103
+ executor;
4104
+ helpTextMaxLength;
4105
+ commandCache = /* @__PURE__ */ new Map();
4106
+ /** alias -> canonical module_id (populated lazily) */
4107
+ aliasMap = /* @__PURE__ */ new Map();
4108
+ /** module_id -> descriptor cache (populated during alias map build) */
4109
+ descriptorCache = /* @__PURE__ */ new Map();
4110
+ aliasMapBuilt = false;
4111
+ constructor(registry, executor, helpTextMaxLength = 1e3) {
4112
+ this.registry = registry;
4113
+ this.executor = executor;
4114
+ this.helpTextMaxLength = helpTextMaxLength;
4115
+ }
4116
+ /**
4117
+ * Build alias->module_id map from display overlay metadata.
4118
+ */
4119
+ buildAliasMap() {
4120
+ if (this.aliasMapBuilt) {
4121
+ return;
4122
+ }
4123
+ try {
4124
+ for (const descriptor of this.registry.listModules()) {
4125
+ const moduleId = descriptor.id;
4126
+ this.descriptorCache.set(moduleId, descriptor);
4127
+ const display = getDisplay(descriptor);
4128
+ const cliDisplay = display.cli && typeof display.cli === "object" && !Array.isArray(display.cli) ? display.cli : {};
4129
+ const cliAlias = cliDisplay.alias;
4130
+ if (cliAlias && cliAlias !== moduleId) {
4131
+ this.aliasMap.set(cliAlias, moduleId);
4132
+ }
4133
+ }
4134
+ this.aliasMapBuilt = true;
4135
+ } catch {
4136
+ warn("Failed to build alias map from registry");
4137
+ }
4138
+ }
4139
+ /**
4140
+ * List all available command names from the Registry.
4141
+ */
4142
+ listCommands() {
4143
+ this.buildAliasMap();
4144
+ const reverse = /* @__PURE__ */ new Map();
4145
+ for (const [alias, moduleId] of this.aliasMap) {
4146
+ reverse.set(moduleId, alias);
4147
+ }
4148
+ const moduleIds = this.registry.listModules().map((m) => m.id);
4149
+ const names = moduleIds.map((mid) => reverse.get(mid) ?? mid);
4150
+ return [...new Set(names)].sort();
4151
+ }
4152
+ /**
4153
+ * Get or lazily build a Commander Command for the given module.
4154
+ */
4155
+ getCommand(cmdName) {
4156
+ if (this.commandCache.has(cmdName)) {
4157
+ return this.commandCache.get(cmdName);
4158
+ }
4159
+ this.buildAliasMap();
4160
+ const moduleId = this.aliasMap.get(cmdName) ?? cmdName;
4161
+ let moduleDef = this.descriptorCache.get(moduleId);
4162
+ if (!moduleDef) {
4163
+ moduleDef = this.registry.getModule(moduleId) ?? void 0;
4164
+ }
4165
+ if (!moduleDef) {
4166
+ return null;
4167
+ }
4168
+ const cmd = buildModuleCommand(moduleDef, this.executor, this.helpTextMaxLength, cmdName);
4169
+ this.commandCache.set(cmdName, cmd);
4170
+ return cmd;
4171
+ }
4172
+ };
4173
+ var LazyGroup = class {
4174
+ members;
4175
+ _executor;
4176
+ _helpTextMaxLength;
4177
+ _cmdCache = /* @__PURE__ */ new Map();
4178
+ command;
4179
+ constructor(members, executor, name, helpTextMaxLength = 1e3) {
4180
+ this.members = members;
4181
+ this._executor = executor;
4182
+ this._helpTextMaxLength = helpTextMaxLength;
4183
+ this.command = new Command6(name).description(`${name} commands`);
4184
+ for (const [cmdName, [, descriptor]] of this.members) {
4185
+ const cmd = buildModuleCommand(
4186
+ descriptor,
4187
+ this._executor,
4188
+ this._helpTextMaxLength,
4189
+ cmdName
4190
+ );
4191
+ this._cmdCache.set(cmdName, cmd);
4192
+ this.command.addCommand(cmd);
4193
+ }
4194
+ }
4195
+ listCommands() {
4196
+ return [...this.members.keys()].sort();
4197
+ }
4198
+ getCommand(cmdName) {
4199
+ if (this._cmdCache.has(cmdName)) {
4200
+ return this._cmdCache.get(cmdName);
4201
+ }
4202
+ const entry = this.members.get(cmdName);
4203
+ if (!entry) {
4204
+ return null;
4205
+ }
4206
+ const [, descriptor] = entry;
4207
+ const cmd = buildModuleCommand(
4208
+ descriptor,
4209
+ this._executor,
4210
+ this._helpTextMaxLength,
4211
+ cmdName
4212
+ );
4213
+ this._cmdCache.set(cmdName, cmd);
4214
+ return cmd;
4215
+ }
4216
+ };
4217
+ var GroupedModuleGroup = class _GroupedModuleGroup extends LazyModuleGroup {
4218
+ /** groupName -> { cmdName -> [moduleId, descriptor] } */
4219
+ groupMap = /* @__PURE__ */ new Map();
4220
+ /** cmdName -> [moduleId, descriptor] for top-level (ungrouped) modules */
4221
+ topLevelModules = /* @__PURE__ */ new Map();
4222
+ /** Cached LazyGroup instances */
4223
+ groupCache = /* @__PURE__ */ new Map();
4224
+ groupMapBuilt = false;
4225
+ /** Exposure filter (FE-12) — controls which modules appear as CLI commands */
4226
+ exposureFilter;
4227
+ /** Effective group depth (CLAUDE.md v0.6.0): constructor arg > APCORE_CLI_GROUP_DEPTH env > 1. */
4228
+ groupDepth;
4229
+ constructor(registry, executor, helpTextMaxLength = 1e3, exposureFilter, groupDepth) {
4230
+ super(registry, executor, helpTextMaxLength);
4231
+ this.exposureFilter = exposureFilter ?? new ExposureFilter();
4232
+ this.groupDepth = _GroupedModuleGroup.resolveGroupDepth(groupDepth);
4233
+ }
4234
+ /**
4235
+ * Resolve group depth from constructor arg > APCORE_CLI_GROUP_DEPTH env > default 1.
4236
+ * Invalid env values (non-integer, non-positive) fall through to the default.
4237
+ */
4238
+ static resolveGroupDepth(explicit) {
4239
+ if (explicit !== void 0 && Number.isFinite(explicit) && explicit > 0) {
4240
+ return Math.floor(explicit);
4241
+ }
4242
+ const raw = process.env.APCORE_CLI_GROUP_DEPTH;
4243
+ if (raw !== void 0 && raw !== "") {
4244
+ const parsed = parseInt(raw, 10);
4245
+ if (Number.isFinite(parsed) && parsed > 0) {
4246
+ return parsed;
4247
+ }
4248
+ }
4249
+ return 1;
4250
+ }
4251
+ /**
4252
+ * Determine (groupName | null, commandName) for a module from its display overlay.
4253
+ *
4254
+ * @param groupDepth Number of dotted segments to consume as the group prefix.
4255
+ * Defaults to 1 (e.g., "math.add" → group="math", cmd="add").
4256
+ * Set to 2 for multi-level grouping (e.g., "math.trig.sin" →
4257
+ * group="math.trig", cmd="sin").
4258
+ */
4259
+ static resolveGroup(moduleId, descriptor, groupDepth = 1) {
4260
+ if (!moduleId) {
4261
+ warn("Empty module_id encountered in resolveGroup");
4262
+ return [null, ""];
4263
+ }
4264
+ const display = getDisplay(descriptor);
4265
+ const cliDisplay = display.cli && typeof display.cli === "object" && !Array.isArray(display.cli) ? display.cli : {};
4266
+ const explicitGroup = cliDisplay.group;
4267
+ if (typeof explicitGroup === "string" && explicitGroup !== "") {
4268
+ return [explicitGroup, cliDisplay.alias ?? moduleId];
4269
+ }
4270
+ if (explicitGroup === "") {
4271
+ return [null, cliDisplay.alias ?? moduleId];
4272
+ }
4273
+ const cliName = cliDisplay.alias ?? moduleId;
4274
+ if (cliName.includes(".")) {
4275
+ const parts = cliName.split(".");
4276
+ const depth = Math.max(1, Math.min(groupDepth, parts.length - 1));
4277
+ const group = parts.slice(0, depth).join(".");
4278
+ const cmd = parts.slice(depth).join(".");
4279
+ return [group, cmd];
4280
+ }
4281
+ return [null, cliName];
4282
+ }
4283
+ /**
4284
+ * Build the group map from registry modules.
4285
+ *
4286
+ * FE-13: hard-fails with exit 2 when a module resolves to the reserved
4287
+ * `apcli` namespace in any of three ways — explicit `display.cli.group`,
4288
+ * auto-grouped dotted prefix, or top-level alias/id. See spec §4.10.
4289
+ */
4290
+ buildGroupMap() {
4291
+ if (this.groupMapBuilt) {
4292
+ return;
4293
+ }
4294
+ this.buildAliasMap();
4295
+ for (const descriptor of this.registry.listModules()) {
4296
+ const moduleId = descriptor.id;
4297
+ const cached = this.descriptorCache.get(moduleId);
4298
+ if (!cached) {
4299
+ continue;
4300
+ }
4301
+ if (!this.exposureFilter.isExposed(moduleId)) {
4302
+ continue;
4303
+ }
4304
+ const display = getDisplay(cached);
4305
+ const cliDisplay = display.cli && typeof display.cli === "object" && !Array.isArray(display.cli) ? display.cli : {};
4306
+ const explicitGroup = typeof cliDisplay.group === "string" && cliDisplay.group !== "" ? cliDisplay.group : void 0;
4307
+ if (explicitGroup !== void 0) {
4308
+ assertNotReserved("group", explicitGroup, moduleId);
4309
+ }
4310
+ const [group, cmd] = _GroupedModuleGroup.resolveGroup(moduleId, cached, this.groupDepth);
4311
+ if (group !== null && explicitGroup === void 0) {
4312
+ assertNotReserved("auto-group", group, moduleId);
4313
+ }
4314
+ if (group === null) {
4315
+ assertNotReserved("top-level", cmd, moduleId);
4316
+ this.topLevelModules.set(cmd, [moduleId, cached]);
4317
+ } else if (!/^[a-z][a-z0-9_-]*(\.[a-z][a-z0-9_-]*)*$/.test(group)) {
4318
+ warn(
4319
+ `Module '${moduleId}': group name '${group}' is not shell-safe \u2014 treating as top-level.`
4320
+ );
4321
+ this.topLevelModules.set(cmd, [moduleId, cached]);
4322
+ } else {
4323
+ if (!this.groupMap.has(group)) {
4324
+ this.groupMap.set(group, /* @__PURE__ */ new Map());
4325
+ }
4326
+ this.groupMap.get(group).set(cmd, [moduleId, cached]);
4327
+ }
4328
+ }
4329
+ this.groupMapBuilt = true;
4330
+ }
4331
+ /**
4332
+ * List all available command names: group names + top-level module names.
4333
+ *
4334
+ * FE-13: the built-in subcommand list is no longer folded in here — those
4335
+ * commands live under the `apcli` prefix and are registered directly by
4336
+ * `createCli`.
4337
+ */
4338
+ listCommands() {
4339
+ this.buildGroupMap();
4340
+ const groupNames = [...this.groupMap.keys()].filter(
4341
+ (g) => !RESERVED_GROUP_NAMES.has(g)
4342
+ );
4343
+ const topNames = [...this.topLevelModules.keys()];
4344
+ return [.../* @__PURE__ */ new Set([...groupNames, ...topNames])].sort();
4345
+ }
4346
+ /**
4347
+ * Get a command by name: check builtins -> group cache -> group map -> top-level modules.
4348
+ */
4349
+ getCommand(cmdName) {
4350
+ this.buildGroupMap();
4351
+ if (this.groupCache.has(cmdName)) {
4352
+ return this.groupCache.get(cmdName).command;
4353
+ }
4354
+ if (this.groupMap.has(cmdName)) {
4355
+ const lazyGrp = new LazyGroup(
4356
+ this.groupMap.get(cmdName),
4357
+ this.executor,
4358
+ cmdName,
4359
+ this.helpTextMaxLength
4360
+ );
4361
+ this.groupCache.set(cmdName, lazyGrp);
4362
+ return lazyGrp.command;
4363
+ }
4364
+ if (this.topLevelModules.has(cmdName)) {
4365
+ if (this.commandCache.has(cmdName)) {
4366
+ return this.commandCache.get(cmdName);
4367
+ }
4368
+ const [, descriptor] = this.topLevelModules.get(cmdName);
4369
+ const cmd = buildModuleCommand(
4370
+ descriptor,
4371
+ this.executor,
4372
+ this.helpTextMaxLength,
4373
+ cmdName
4374
+ );
4375
+ this.commandCache.set(cmdName, cmd);
4376
+ return cmd;
4377
+ }
4378
+ return null;
4379
+ }
4380
+ /** Expose groupMap for testing. */
4381
+ getGroupMap() {
4382
+ return this.groupMap;
4383
+ }
4384
+ /** Expose topLevelModules for testing. */
4385
+ getTopLevelModules() {
4386
+ return this.topLevelModules;
4387
+ }
4388
+ /** Expose groupMapBuilt for testing. */
4389
+ isGroupMapBuilt() {
4390
+ return this.groupMapBuilt;
4391
+ }
4392
+ };
4393
+
3521
4394
  // src/index.ts
3522
4395
  init_errors();
4396
+ init_logger();
3523
4397
  init_security();
3524
4398
  export {
4399
+ ApcliGroup,
3525
4400
  ApprovalDeniedError,
3526
4401
  ApprovalTimeoutError,
3527
4402
  AuditLogger,
3528
4403
  AuthProvider,
3529
4404
  AuthenticationError,
3530
- BUILTIN_COMMANDS,
3531
4405
  CliApprovalHandler,
3532
4406
  ConfigDecryptionError,
3533
4407
  ConfigEncryptor,
3534
4408
  ConfigResolver,
3535
4409
  DEFAULTS,
3536
4410
  EXIT_CODES,
4411
+ ExposureFilter,
3537
4412
  GroupedModuleGroup,
3538
4413
  LazyGroup,
3539
4414
  LazyModuleGroup,
3540
4415
  ModuleExecutionError,
3541
4416
  ModuleNotFoundError,
4417
+ RESERVED_GROUP_NAMES,
3542
4418
  Sandbox,
3543
4419
  SchemaValidationError,
3544
4420
  applyToolkitIntegration,
3545
4421
  buildModuleCommand,
3546
- buildProgramManPage,
3547
4422
  checkApproval,
3548
4423
  collectInput,
3549
4424
  configureManHelp,
3550
4425
  createCli,
3551
- debug,
3552
- docsUrl,
3553
4426
  emitErrorJson,
3554
4427
  emitErrorTty,
3555
- error,
3556
4428
  exitCodeForError,
3557
- extractHelp,
3558
- firstFailedExitCode,
3559
4429
  formatExecResult,
3560
- formatModuleDetail,
3561
- formatModuleList,
3562
- formatPreflightResult,
3563
4430
  getAuditLogger,
3564
- getCliDisplayFields,
3565
- getDisplay,
3566
4431
  getLogLevel,
3567
- info,
3568
4432
  main,
3569
- mapType,
3570
4433
  reconvertEnumValues,
4434
+ registerCompletionCommand,
4435
+ registerConfigCommand,
3571
4436
  registerConfigNamespace,
3572
- registerDiscoveryCommands,
4437
+ registerDescribeCommand,
4438
+ registerDisableCommand,
4439
+ registerEnableCommand,
4440
+ registerExecCommand,
4441
+ registerHealthCommand,
3573
4442
  registerInitCommand,
4443
+ registerListCommand,
3574
4444
  registerPipelineCommand,
3575
- registerShellCommands,
3576
- registerSystemCommands,
4445
+ registerReloadCommand,
4446
+ registerUsageCommand,
3577
4447
  registerValidateCommand,
3578
- resolveFormat,
3579
4448
  resolveRefs,
3580
4449
  schemaToCliOptions,
3581
4450
  setAuditLogger,
3582
4451
  setDocsUrl,
3583
4452
  setLogLevel,
3584
4453
  setVerboseHelp,
3585
- truncate,
3586
- validateModuleId,
3587
- verboseHelp,
3588
- warn
4454
+ validateModuleId
3589
4455
  };
3590
4456
  //# sourceMappingURL=index.js.map