apcore-cli 0.6.0 → 0.8.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(path6) {
224
+ this.logPath = path6 ?? _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,21 +247,27 @@ 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() {
217
267
  try {
218
268
  return os.userInfo().username;
219
269
  } catch {
220
- return process.env.USER ?? process.env.USERNAME ?? "unknown";
270
+ return process.env.USER ?? process.env.LOGNAME ?? process.env.USERNAME ?? "unknown";
221
271
  }
222
272
  }
223
273
  };
@@ -226,7 +276,7 @@ var init_audit = __esm({
226
276
 
227
277
  // src/security/config-encryptor.ts
228
278
  import * as crypto2 from "crypto";
229
- import * as os2 from "os";
279
+ import * as os3 from "os";
230
280
  async function getKeytar() {
231
281
  if (keytarModule) return keytarModule;
232
282
  try {
@@ -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() {
313
- const hostname2 = os2.hostname();
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
+ }
408
+ const hostname2 = os3.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 = os3.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,64 @@ 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_DEFAULT_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_DEFAULT_OUTPUT_SIZE_LIMIT = 64 * 1024 * 1024;
415
571
  Sandbox = class {
572
+ /** Default post-capture stdout+stderr byte budget for sandboxed children. */
573
+ static DEFAULT_MAX_OUTPUT_BYTES = SANDBOX_DEFAULT_OUTPUT_SIZE_LIMIT;
416
574
  enabled;
417
- constructor(enabled = false) {
575
+ timeoutSeconds;
576
+ extensionsRoot = null;
577
+ maxOutputBytes = SANDBOX_DEFAULT_OUTPUT_SIZE_LIMIT;
578
+ constructor(enabled = false, timeoutSeconds = 300) {
418
579
  this.enabled = enabled;
580
+ this.timeoutSeconds = timeoutSeconds;
581
+ }
582
+ /**
583
+ * Set the extensions root that is forwarded to the sandboxed runner via
584
+ * `APCORE_EXTENSIONS_ROOT`. The path is resolved to absolute when injected
585
+ * so the child (whose cwd is the fresh sandbox tempdir) can locate modules.
586
+ *
587
+ * Builder-style — returns `this` so call sites can chain. Mirrors Python's
588
+ * `Sandbox.with_extensions_root` (D1-004 cross-SDK parity).
589
+ */
590
+ withExtensionsRoot(extensionsRoot) {
591
+ this.extensionsRoot = extensionsRoot;
592
+ return this;
593
+ }
594
+ /**
595
+ * Cap the post-capture stdout+stderr byte budget for the sandboxed
596
+ * subprocess. Default: 64 MiB (`Sandbox.DEFAULT_MAX_OUTPUT_BYTES`).
597
+ *
598
+ * Builder-style — returns `this`. Mirrors Python's
599
+ * `Sandbox.with_max_output_bytes` (D1-004 cross-SDK parity).
600
+ */
601
+ withMaxOutputBytes(maxOutputBytes) {
602
+ this.maxOutputBytes = maxOutputBytes;
603
+ return this;
419
604
  }
420
605
  /**
421
606
  * Execute a module, optionally inside a sandboxed subprocess.
@@ -424,63 +609,96 @@ var init_sandbox = __esm({
424
609
  if (!this.enabled) {
425
610
  return executor.execute(moduleId, inputData);
426
611
  }
427
- return this.sandboxedExecute(moduleId, inputData);
612
+ return this._sandboxedExecute(moduleId, inputData);
428
613
  }
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;
439
- }
614
+ async _sandboxedExecute(moduleId, inputData) {
615
+ const { spawn } = await import("child_process");
616
+ const { tmpdir } = await import("os");
617
+ const { join: join4 } = await import("path");
618
+ const { mkdtempSync, rmSync } = await import("fs");
619
+ const tmpDir = mkdtempSync(join4(tmpdir(), "apcore_sandbox_"));
620
+ const env = buildSandboxEnv(tmpDir);
621
+ const { resolve: resolvePath } = await import("path");
622
+ if (this.extensionsRoot !== null) {
623
+ env.APCORE_EXTENSIONS_ROOT = resolvePath(this.extensionsRoot);
624
+ } else if (env.APCORE_EXTENSIONS_ROOT) {
625
+ env.APCORE_EXTENSIONS_ROOT = resolvePath(env.APCORE_EXTENSIONS_ROOT);
440
626
  }
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.`
472
- );
627
+ const binaryPath = process.argv[1];
628
+ const child = spawn(process.execPath, [binaryPath, "--internal-sandbox-runner", moduleId], {
629
+ env,
630
+ cwd: tmpDir,
631
+ stdio: ["pipe", "pipe", "pipe"]
632
+ });
633
+ let stdout = "";
634
+ let stderr = "";
635
+ let stdoutBytes = 0;
636
+ let stderrBytes = 0;
637
+ let sizeExceeded = false;
638
+ const outputCap = this.maxOutputBytes;
639
+ child.stdout.on("data", (chunk) => {
640
+ if (sizeExceeded) return;
641
+ stdoutBytes += chunk.length;
642
+ if (stdoutBytes > outputCap) {
643
+ sizeExceeded = true;
644
+ child.kill("SIGKILL");
645
+ return;
473
646
  }
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 {
647
+ stdout += chunk.toString();
648
+ });
649
+ child.stderr.on("data", (chunk) => {
650
+ if (sizeExceeded) return;
651
+ stderrBytes += chunk.length;
652
+ if (stderrBytes > outputCap) {
653
+ sizeExceeded = true;
654
+ child.kill("SIGKILL");
655
+ return;
482
656
  }
483
- }
657
+ stderr += chunk.toString();
658
+ });
659
+ child.stdin.write(JSON.stringify(inputData));
660
+ child.stdin.end();
661
+ return new Promise((resolve2, reject) => {
662
+ const timer = setTimeout(() => {
663
+ child.kill("SIGKILL");
664
+ reject(
665
+ new ModuleExecutionError(
666
+ `Sandbox module '${moduleId}' timed out after ${this.timeoutSeconds}s.`
667
+ )
668
+ );
669
+ }, this.timeoutSeconds * 1e3);
670
+ child.on("close", (code) => {
671
+ clearTimeout(timer);
672
+ try {
673
+ rmSync(tmpDir, { recursive: true, force: true });
674
+ } catch {
675
+ }
676
+ if (sizeExceeded) {
677
+ const limitMiB = Math.floor(outputCap / (1024 * 1024));
678
+ reject(new ModuleExecutionError(
679
+ `Sandbox module '${moduleId}' output exceeded ${limitMiB}MiB limit.`
680
+ ));
681
+ return;
682
+ }
683
+ if (code !== 0) {
684
+ reject(new ModuleExecutionError(
685
+ `Sandbox module '${moduleId}' exited with code ${code}.${stderr ? ` stderr: ${stderr}` : ""}`
686
+ ));
687
+ return;
688
+ }
689
+ try {
690
+ resolve2(JSON.parse(stdout));
691
+ } catch {
692
+ reject(new ModuleExecutionError(
693
+ `Sandbox module '${moduleId}' returned non-JSON output: ${stdout.slice(0, 200)}`
694
+ ));
695
+ }
696
+ });
697
+ child.on("error", (err) => {
698
+ clearTimeout(timer);
699
+ reject(new ModuleExecutionError(`Failed to spawn sandbox process: ${err.message}`));
700
+ });
701
+ });
484
702
  }
485
703
  };
486
704
  }
@@ -514,9 +732,9 @@ init_esm_shims();
514
732
  init_esm_shims();
515
733
  init_errors();
516
734
  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";
735
+ import { fileURLToPath as fileURLToPath2 } from "url";
736
+ import * as path5 from "path";
737
+ import { Command as Command5, CommanderError, Option as Option4 } from "commander";
520
738
 
521
739
  // src/ref-resolver.ts
522
740
  init_esm_shims();
@@ -575,6 +793,12 @@ function resolveNode(node, defs, visited, depth, maxDepth, moduleId) {
575
793
  properties: {},
576
794
  required: []
577
795
  };
796
+ if (typeof obj.properties === "object" && obj.properties !== null) {
797
+ Object.assign(merged.properties, obj.properties);
798
+ }
799
+ if (Array.isArray(obj.required)) {
800
+ merged.required.push(...obj.required);
801
+ }
578
802
  for (const subSchema of obj.allOf) {
579
803
  const resolved = resolveNode(
580
804
  subSchema,
@@ -608,6 +832,10 @@ function resolveNode(node, defs, visited, depth, maxDepth, moduleId) {
608
832
  properties: {},
609
833
  required: []
610
834
  };
835
+ if (typeof obj.properties === "object" && obj.properties !== null) {
836
+ Object.assign(merged.properties, obj.properties);
837
+ }
838
+ const siblingRequired = Array.isArray(obj.required) ? obj.required.slice() : [];
611
839
  const allRequiredSets = [];
612
840
  for (const subSchema of obj[keyword]) {
613
841
  const resolved = resolveNode(
@@ -628,6 +856,7 @@ function resolveNode(node, defs, visited, depth, maxDepth, moduleId) {
628
856
  allRequiredSets.push(new Set(resolved.required));
629
857
  }
630
858
  }
859
+ let branchRequired = [];
631
860
  if (allRequiredSets.length > 0) {
632
861
  let intersection = allRequiredSets[0];
633
862
  for (let i = 1; i < allRequiredSets.length; i++) {
@@ -635,10 +864,17 @@ function resolveNode(node, defs, visited, depth, maxDepth, moduleId) {
635
864
  [...intersection].filter((x) => allRequiredSets[i].has(x))
636
865
  );
637
866
  }
638
- merged.required = [...intersection];
639
- } else {
640
- merged.required = [];
867
+ branchRequired = [...intersection];
641
868
  }
869
+ const seen = /* @__PURE__ */ new Set();
870
+ const combinedRequired = [];
871
+ for (const r of [...siblingRequired, ...branchRequired]) {
872
+ if (!seen.has(r)) {
873
+ seen.add(r);
874
+ combinedRequired.push(r);
875
+ }
876
+ }
877
+ merged.required = combinedRequired;
642
878
  for (const [k, v] of Object.entries(obj)) {
643
879
  if (k !== keyword && !(k in merged)) {
644
880
  merged[k] = v;
@@ -654,7 +890,7 @@ function resolveNode(node, defs, visited, depth, maxDepth, moduleId) {
654
890
  propSchema,
655
891
  defs,
656
892
  visited,
657
- depth + 1,
893
+ depth,
658
894
  maxDepth,
659
895
  moduleId
660
896
  );
@@ -666,6 +902,7 @@ function resolveNode(node, defs, visited, depth, maxDepth, moduleId) {
666
902
  // src/schema-parser.ts
667
903
  init_esm_shims();
668
904
  init_errors();
905
+ init_logger();
669
906
  var BOOLEAN_FLAG = /* @__PURE__ */ Symbol("BOOLEAN_FLAG");
670
907
  function mapType(propName, propSchema) {
671
908
  const schemaType = propSchema.type;
@@ -698,29 +935,50 @@ function extractHelp(propSchema, maxLength = 1e3) {
698
935
  }
699
936
  return text;
700
937
  }
701
- var RESERVED_NAMES = /* @__PURE__ */ new Set(["input", "yes", "large_input", "format", "sandbox"]);
938
+ var RESERVED_NAMES = /* @__PURE__ */ new Set([
939
+ "input",
940
+ "yes",
941
+ "large_input",
942
+ "format",
943
+ "fields",
944
+ "sandbox",
945
+ "verbose",
946
+ "dry_run",
947
+ "trace",
948
+ "stream",
949
+ "strategy",
950
+ "approval_timeout",
951
+ "approval_token"
952
+ ]);
702
953
  function schemaToCliOptions(schema, maxHelpLength = 1e3) {
703
954
  const properties = schema.properties ?? {};
704
955
  const requiredList = schema.required ?? [];
705
956
  const options = [];
706
957
  const flagNames = {};
958
+ for (const reqName of requiredList) {
959
+ if (!(reqName in properties)) {
960
+ warn(
961
+ `Required property '${reqName}' not found in properties, skipping.`
962
+ );
963
+ }
964
+ }
707
965
  for (const [propName, propSchema] of Object.entries(properties)) {
708
966
  const flagName = "--" + propName.replace(/_/g, "-");
709
- if (flagName in flagNames) {
967
+ if (RESERVED_NAMES.has(propName)) {
710
968
  process.stderr.write(
711
- `Error: Flag name collision: properties '${propName}' and '${flagNames[flagName]}' both map to '${flagName}'.
969
+ `Error: Module schema property '${propName}' conflicts with a reserved CLI option name. Rename the property.
712
970
  `
713
971
  );
714
- process.exit(EXIT_CODES.INVALID_CLI_INPUT);
972
+ process.exit(EXIT_CODES.SCHEMA_CIRCULAR_REF);
715
973
  }
716
- flagNames[flagName] = propName;
717
- if (RESERVED_NAMES.has(propName)) {
974
+ if (flagName in flagNames) {
718
975
  process.stderr.write(
719
- `Error: Module schema property '${propName}' conflicts with a reserved CLI option name. Rename the property.
976
+ `Error: Flag name collision: properties '${propName}' and '${flagNames[flagName]}' both map to '${flagName}'.
720
977
  `
721
978
  );
722
- process.exit(EXIT_CODES.INVALID_CLI_INPUT);
979
+ process.exit(EXIT_CODES.SCHEMA_CIRCULAR_REF);
723
980
  }
981
+ flagNames[flagName] = propName;
724
982
  const typeResult = mapType(propName, propSchema);
725
983
  const isRequired = requiredList.includes(propName);
726
984
  const helpBase = extractHelp(propSchema, maxHelpLength);
@@ -728,6 +986,15 @@ function schemaToCliOptions(schema, maxHelpLength = 1e3) {
728
986
  const defaultValue = propSchema.default;
729
987
  if (typeResult === BOOLEAN_FLAG) {
730
988
  const flagBase = propName.replace(/_/g, "-");
989
+ const noFlag = `--no-${flagBase}`;
990
+ if (noFlag in flagNames) {
991
+ process.stderr.write(
992
+ `Error: Flag name collision: boolean property '${propName}' auto-generates '${noFlag}' which is already used by property '${flagNames[noFlag]}'.
993
+ `
994
+ );
995
+ process.exit(EXIT_CODES.SCHEMA_CIRCULAR_REF);
996
+ }
997
+ flagNames[noFlag] = propName;
731
998
  const defaultVal = propSchema.default ?? false;
732
999
  options.push({
733
1000
  name: propName,
@@ -806,12 +1073,20 @@ function getAnnotation(annotations, key, defaultValue = void 0) {
806
1073
  const ann = annotations;
807
1074
  return key in ann ? ann[key] : defaultValue;
808
1075
  }
1076
+ function readTimeoutFromEnv() {
1077
+ const raw = process.env.APCORE_CLI_APPROVAL_TIMEOUT;
1078
+ if (raw === void 0 || raw === "") return void 0;
1079
+ const parsed = parseInt(raw, 10);
1080
+ if (!Number.isFinite(parsed) || parsed <= 0) return void 0;
1081
+ return parsed;
1082
+ }
809
1083
  var CliApprovalHandler = class {
810
1084
  autoApprove;
811
1085
  timeout;
812
- constructor(autoApprove = false, timeout = 60) {
1086
+ constructor(autoApprove = false, timeout) {
813
1087
  this.autoApprove = autoApprove;
814
- this.timeout = Math.max(1, Math.min(timeout, 3600));
1088
+ const resolved = timeout ?? readTimeoutFromEnv() ?? 60;
1089
+ this.timeout = Math.max(1, Math.min(resolved, 3600));
815
1090
  }
816
1091
  async requestApproval(request) {
817
1092
  const moduleId = request.module_id ?? "unknown";
@@ -822,6 +1097,12 @@ var CliApprovalHandler = class {
822
1097
  if (envVal === "1") {
823
1098
  return { status: "approved", approved_by: "env_auto_approve" };
824
1099
  }
1100
+ if (envVal !== "" && envVal !== "1") {
1101
+ process.stderr.write(
1102
+ `Warning: APCORE_CLI_AUTO_APPROVE is set to '${envVal}', expected '1'. Ignoring.
1103
+ `
1104
+ );
1105
+ }
825
1106
  if (!process.stdin.isTTY) {
826
1107
  return { status: "rejected", reason: "Non-interactive session without --yes" };
827
1108
  }
@@ -840,7 +1121,7 @@ var CliApprovalHandler = class {
840
1121
  return { status: "rejected", reason: "CLI does not support async approval polling" };
841
1122
  }
842
1123
  };
843
- async function checkApproval(moduleDef, autoApprove, timeout = 60) {
1124
+ async function checkApproval(moduleDef, autoApprove, timeout) {
844
1125
  const annotations = moduleDef.annotations;
845
1126
  let requiresApproval;
846
1127
  if (moduleDef.requiresApproval !== void 0) {
@@ -868,13 +1149,12 @@ async function checkApproval(moduleDef, autoApprove, timeout = 60) {
868
1149
  );
869
1150
  }
870
1151
  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
- `
1152
+ throw new ApprovalDeniedError(
1153
+ `Module '${moduleId}' requires approval but no interactive terminal is available. Use --yes or set APCORE_CLI_AUTO_APPROVE=1 to bypass.`
874
1154
  );
875
- process.exit(EXIT_CODES.APPROVAL_DENIED);
876
1155
  }
877
- await promptWithTimeout(moduleDef, timeout);
1156
+ const effectiveTimeout = timeout ?? readTimeoutFromEnv() ?? 60;
1157
+ await promptWithTimeout(moduleDef, effectiveTimeout);
878
1158
  }
879
1159
  async function promptWithTimeout(moduleDef, timeout) {
880
1160
  timeout = Math.max(1, Math.min(timeout, 3600));
@@ -889,8 +1169,8 @@ async function promptWithTimeout(moduleDef, timeout) {
889
1169
  let timer;
890
1170
  try {
891
1171
  const answer = await Promise.race([
892
- new Promise((resolve3) => {
893
- rl.question("Proceed? [y/N] ", (ans) => resolve3(ans));
1172
+ new Promise((resolve2) => {
1173
+ rl.question("Proceed? [y/N] ", (ans) => resolve2(ans));
894
1174
  }),
895
1175
  new Promise((_, reject) => {
896
1176
  timer = setTimeout(() => {
@@ -905,17 +1185,9 @@ async function promptWithTimeout(moduleDef, timeout) {
905
1185
  if (normalized === "y" || normalized === "yes") {
906
1186
  return;
907
1187
  }
908
- process.stderr.write("Error: Approval denied.\n");
909
- process.exit(EXIT_CODES.APPROVAL_DENIED);
1188
+ throw new ApprovalDeniedError("Approval denied");
910
1189
  } catch (err) {
911
1190
  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
1191
  throw err;
920
1192
  } finally {
921
1193
  rl.close();
@@ -924,6 +1196,34 @@ async function promptWithTimeout(moduleDef, timeout) {
924
1196
 
925
1197
  // src/output.ts
926
1198
  init_esm_shims();
1199
+ init_errors();
1200
+ import yaml from "js-yaml";
1201
+ var TOOLKIT_MISSING_HINT = "The 'markdown' and 'skill' output formats require the apcore-toolkit peer dependency. Install with: npm install apcore-toolkit@^0.6";
1202
+ function descriptorToScanned(m) {
1203
+ const metadata = m.metadata ?? {};
1204
+ const display = metadata["display"] ?? null;
1205
+ return {
1206
+ moduleId: m.id,
1207
+ description: m.description ?? "",
1208
+ inputSchema: m.inputSchema ?? {},
1209
+ outputSchema: m.outputSchema ?? {},
1210
+ tags: m.tags ?? [],
1211
+ target: "",
1212
+ version: "1.0.0",
1213
+ annotations: m.annotations ?? null,
1214
+ documentation: null,
1215
+ suggestedAlias: null,
1216
+ examples: [],
1217
+ metadata,
1218
+ display,
1219
+ warnings: []
1220
+ };
1221
+ }
1222
+ function csvCellString(value) {
1223
+ if (value === null || value === void 0) return "";
1224
+ if (typeof value === "object") return JSON.stringify(value);
1225
+ return String(value);
1226
+ }
927
1227
  function resolveFormat(explicitFormat) {
928
1228
  if (explicitFormat !== void 0) {
929
1229
  return explicitFormat;
@@ -947,7 +1247,7 @@ function formatTable(headers, rows) {
947
1247
  );
948
1248
  return [headerLine, sep2, ...dataLines].join("\n") + "\n";
949
1249
  }
950
- function formatModuleList(modules, format, filterTags, showDeps = false) {
1250
+ async function formatModuleList(modules, format, filterTags, showDeps = false, exposureFilter) {
951
1251
  if (format === "table") {
952
1252
  if (modules.length === 0 && filterTags && filterTags.length > 0) {
953
1253
  process.stdout.write(
@@ -960,13 +1260,18 @@ function formatModuleList(modules, format, filterTags, showDeps = false) {
960
1260
  process.stdout.write("No modules found.\n");
961
1261
  return;
962
1262
  }
963
- const headers = showDeps ? ["ID", "Description", "Tags", "Deps"] : ["ID", "Description", "Tags"];
1263
+ const headers = ["ID", "Description", "Tags"];
1264
+ if (showDeps) headers.push("Deps");
1265
+ if (exposureFilter) headers.push("Exposure");
964
1266
  const rows = modules.map((m) => {
965
1267
  const base = [m.id, truncate(m.description, 80), (m.tags ?? []).join(", ")];
966
1268
  if (showDeps) {
967
1269
  const deps = m.dependencies;
968
1270
  base.push(String(Array.isArray(deps) ? deps.length : 0));
969
1271
  }
1272
+ if (exposureFilter) {
1273
+ base.push(exposureFilter.isExposed(m.id ?? "") ? "\u2713" : "\u2014");
1274
+ }
970
1275
  return base;
971
1276
  });
972
1277
  process.stdout.write(formatTable(headers, rows));
@@ -981,9 +1286,23 @@ function formatModuleList(modules, format, filterTags, showDeps = false) {
981
1286
  const deps = m.dependencies;
982
1287
  entry.dependency_count = Array.isArray(deps) ? deps.length : 0;
983
1288
  }
1289
+ if (exposureFilter) {
1290
+ entry.exposed = exposureFilter.isExposed(m.id ?? "");
1291
+ }
984
1292
  return entry;
985
1293
  });
986
1294
  process.stdout.write(JSON.stringify(result, null, 2) + "\n");
1295
+ } else if (format === "markdown" || format === "skill") {
1296
+ let toolkit;
1297
+ try {
1298
+ toolkit = await import("apcore-toolkit");
1299
+ } catch {
1300
+ throw new Error(TOOLKIT_MISSING_HINT);
1301
+ }
1302
+ const scanned = modules.map(descriptorToScanned);
1303
+ process.stdout.write(
1304
+ toolkit.formatModules(scanned, { style: format, display: true }) + "\n"
1305
+ );
987
1306
  }
988
1307
  }
989
1308
  function annotationsToDict(annotations) {
@@ -997,7 +1316,7 @@ function annotationsToDict(annotations) {
997
1316
  }
998
1317
  return Object.keys(result).length > 0 ? result : null;
999
1318
  }
1000
- function formatModuleDetail(moduleDef, format) {
1319
+ async function formatModuleDetail(moduleDef, format) {
1001
1320
  if (format === "table") {
1002
1321
  process.stdout.write(`
1003
1322
  Module: ${moduleDef.id}
@@ -1068,6 +1387,16 @@ Tags: ${tags.join(", ")}
1068
1387
  }
1069
1388
  }
1070
1389
  process.stdout.write(JSON.stringify(result, null, 2) + "\n");
1390
+ } else if (format === "markdown" || format === "skill") {
1391
+ let toolkit;
1392
+ try {
1393
+ toolkit = await import("apcore-toolkit");
1394
+ } catch {
1395
+ throw new Error(TOOLKIT_MISSING_HINT);
1396
+ }
1397
+ process.stdout.write(
1398
+ toolkit.formatModule(descriptorToScanned(moduleDef), { style: format, display: true }) + "\n"
1399
+ );
1071
1400
  }
1072
1401
  }
1073
1402
  function selectFields(result, fields) {
@@ -1101,42 +1430,21 @@ function formatExecResult(result, format, fields) {
1101
1430
  const obj = effective_result;
1102
1431
  const keys = Object.keys(obj);
1103
1432
  const header = keys.map(escapeCsvField).join(",");
1104
- const row = keys.map((k) => escapeCsvField(String(obj[k]))).join(",");
1433
+ const row = keys.map((k) => escapeCsvField(csvCellString(obj[k]))).join(",");
1105
1434
  process.stdout.write(header + "\n" + row + "\n");
1106
1435
  } else if (Array.isArray(effective_result) && effective_result.length > 0 && typeof effective_result[0] === "object") {
1107
1436
  const keys = Object.keys(effective_result[0]);
1108
1437
  const header = keys.map(escapeCsvField).join(",");
1109
1438
  const rows = effective_result.map((item) => {
1110
1439
  const obj = item;
1111
- return keys.map((k) => escapeCsvField(String(obj[k]))).join(",");
1440
+ return keys.map((k) => escapeCsvField(csvCellString(obj[k]))).join(",");
1112
1441
  });
1113
1442
  process.stdout.write(header + "\n" + rows.join("\n") + "\n");
1114
1443
  } else {
1115
1444
  process.stdout.write(JSON.stringify(effective_result) + "\n");
1116
1445
  }
1117
1446
  } 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
- }
1447
+ process.stdout.write(yaml.dump(effective_result, { lineWidth: -1 }));
1140
1448
  } else if (effective === "jsonl") {
1141
1449
  if (Array.isArray(effective_result)) {
1142
1450
  for (const item of effective_result) {
@@ -1159,7 +1467,7 @@ function formatExecResult(result, format, fields) {
1159
1467
  }
1160
1468
  }
1161
1469
  function escapeCsvField(value) {
1162
- if (value.includes(",") || value.includes('"') || value.includes("\n")) {
1470
+ if (value.includes(",") || value.includes('"') || value.includes("\n") || value.includes("\r")) {
1163
1471
  return '"' + value.replace(/"/g, '""') + '"';
1164
1472
  }
1165
1473
  return value;
@@ -1169,7 +1477,9 @@ function formatPreflightResult(result, format) {
1169
1477
  if (resolved === "json" || !process.stdout.isTTY) {
1170
1478
  const payload = {
1171
1479
  valid: result.valid,
1172
- requires_approval: result.requires_approval,
1480
+ // JSON output key stays snake_case for cross-language CLI contract;
1481
+ // runtime read uses camelCase to match apcore-js PreflightResult.
1482
+ requires_approval: result.requiresApproval,
1173
1483
  checks: result.checks.map((c) => {
1174
1484
  const entry = { check: c.check, passed: c.passed };
1175
1485
  if (c.error !== void 0 && c.error !== null) {
@@ -1220,59 +1530,46 @@ Result: ${tag} (${errors} error(s), ${warnings} warning(s))
1220
1530
  }
1221
1531
  function firstFailedExitCode(result) {
1222
1532
  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
1533
+ module_id: EXIT_CODES.INVALID_CLI_INPUT,
1534
+ module_lookup: EXIT_CODES.MODULE_NOT_FOUND,
1535
+ call_chain: EXIT_CODES.MODULE_EXECUTE_ERROR,
1536
+ acl: EXIT_CODES.ACL_DENIED,
1537
+ schema: EXIT_CODES.SCHEMA_VALIDATION_ERROR,
1538
+ approval: EXIT_CODES.APPROVAL_DENIED,
1539
+ module_preflight: EXIT_CODES.MODULE_EXECUTE_ERROR
1230
1540
  };
1231
1541
  for (const check of result.checks) {
1232
1542
  if (!check.passed) {
1233
- return checkToExit[check.check] ?? 1;
1543
+ return checkToExit[check.check] ?? EXIT_CODES.MODULE_EXECUTE_ERROR;
1234
1544
  }
1235
1545
  }
1236
- return 1;
1546
+ return EXIT_CODES.MODULE_EXECUTE_ERROR;
1237
1547
  }
1238
1548
 
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
- }
1549
+ // src/main.ts
1550
+ init_logger();
1271
1551
 
1272
1552
  // src/init-cmd.ts
1273
1553
  init_esm_shims();
1554
+ init_errors();
1274
1555
  import * as fs from "fs";
1275
1556
  import * as path2 from "path";
1557
+ function runFsOp(op, targetPath, fn, partial) {
1558
+ try {
1559
+ return fn();
1560
+ } catch (err) {
1561
+ const msg = err instanceof Error ? err.message : String(err);
1562
+ process.stderr.write(`Error: failed to ${op} ${targetPath}: ${msg}
1563
+ `);
1564
+ if (partial && partial.length > 0) {
1565
+ process.stderr.write(
1566
+ ` Partial scaffold left on disk \u2014 you may want to remove: ${partial.join(", ")}
1567
+ `
1568
+ );
1569
+ }
1570
+ process.exit(EXIT_CODES.MODULE_EXECUTE_ERROR);
1571
+ }
1572
+ }
1276
1573
  var DECORATOR_TEMPLATE = `import { module } from "apcore-js";
1277
1574
  import { Type } from "@sinclair/typebox";
1278
1575
 
@@ -1296,7 +1593,8 @@ export function {funcName}(): Record<string, unknown> {
1296
1593
  return { status: "ok" };
1297
1594
  }
1298
1595
  `;
1299
- var BINDING_TEMPLATE = `bindings:
1596
+ var BINDING_TEMPLATE = `spec_version: "1.0"
1597
+ bindings:
1300
1598
  - module_id: "{moduleId}"
1301
1599
  target: "{target}"
1302
1600
  description: "{description}"
@@ -1345,7 +1643,7 @@ function registerInitCommand(cli) {
1345
1643
  });
1346
1644
  }
1347
1645
  function createDecoratorModule(moduleId, _prefix, funcName, description, outputDir) {
1348
- fs.mkdirSync(outputDir, { recursive: true });
1646
+ runFsOp("create directory", outputDir, () => fs.mkdirSync(outputDir, { recursive: true }));
1349
1647
  const filename = moduleId.replace(/\./g, "_") + ".ts";
1350
1648
  const filepath = path2.join(outputDir, filename);
1351
1649
  const varName = funcName + "Module";
@@ -1355,14 +1653,14 @@ function createDecoratorModule(moduleId, _prefix, funcName, description, outputD
1355
1653
  funcName,
1356
1654
  description
1357
1655
  });
1358
- fs.writeFileSync(filepath, content);
1656
+ runFsOp("write file", filepath, () => fs.writeFileSync(filepath, content));
1359
1657
  process.stdout.write(`Created ${filepath}
1360
1658
  `);
1361
1659
  }
1362
1660
  function createConventionModule(moduleId, prefix, funcName, description, outputDir) {
1363
1661
  const prefixParts = prefix.split(".");
1364
1662
  const dirPath = prefixParts.length > 1 ? path2.join(outputDir, ...prefixParts.slice(0, -1)) : outputDir;
1365
- fs.mkdirSync(dirPath, { recursive: true });
1663
+ runFsOp("create directory", dirPath, () => fs.mkdirSync(dirPath, { recursive: true }));
1366
1664
  let filename;
1367
1665
  if (prefixParts.length > 1) {
1368
1666
  filename = prefixParts[prefixParts.length - 1] + ".ts";
@@ -1380,12 +1678,13 @@ function createConventionModule(moduleId, prefix, funcName, description, outputD
1380
1678
  description,
1381
1679
  cliGroupLine
1382
1680
  });
1383
- fs.writeFileSync(filepath, content);
1681
+ runFsOp("write file", filepath, () => fs.writeFileSync(filepath, content));
1384
1682
  process.stdout.write(`Created ${filepath}
1385
1683
  `);
1386
1684
  }
1387
1685
  function createBindingModule(moduleId, prefix, funcName, description, outputDir) {
1388
- fs.mkdirSync(outputDir, { recursive: true });
1686
+ const partial = [];
1687
+ runFsOp("create directory", outputDir, () => fs.mkdirSync(outputDir, { recursive: true }));
1389
1688
  const yamlFile = path2.join(outputDir, moduleId.replace(/\./g, "_") + ".binding.yaml");
1390
1689
  const target = `commands.${prefix}:${funcName}`;
1391
1690
  const yamlContent = renderTemplate(BINDING_TEMPLATE, {
@@ -1393,11 +1692,12 @@ function createBindingModule(moduleId, prefix, funcName, description, outputDir)
1393
1692
  target,
1394
1693
  description
1395
1694
  });
1396
- fs.writeFileSync(yamlFile, yamlContent);
1695
+ runFsOp("write file", yamlFile, () => fs.writeFileSync(yamlFile, yamlContent));
1696
+ partial.push(yamlFile);
1397
1697
  process.stdout.write(`Created ${yamlFile}
1398
1698
  `);
1399
1699
  const baseSrc = "commands";
1400
- fs.mkdirSync(baseSrc, { recursive: true });
1700
+ runFsOp("create directory", baseSrc, () => fs.mkdirSync(baseSrc, { recursive: true }), partial);
1401
1701
  const srcFile = path2.join(baseSrc, prefix.replace(/\./g, "_") + ".ts");
1402
1702
  if (!fs.existsSync(srcFile)) {
1403
1703
  const srcContent = `export function ${funcName}(): Record<string, unknown> {
@@ -1406,7 +1706,7 @@ function createBindingModule(moduleId, prefix, funcName, description, outputDir)
1406
1706
  return { status: "ok" };
1407
1707
  }
1408
1708
  `;
1409
- fs.writeFileSync(srcFile, srcContent);
1709
+ runFsOp("write file", srcFile, () => fs.writeFileSync(srcFile, srcContent), partial);
1410
1710
  process.stdout.write(`Created ${srcFile}
1411
1711
  `);
1412
1712
  }
@@ -1420,40 +1720,48 @@ function getDisplay(descriptor) {
1420
1720
  if (display && typeof display === "object" && !Array.isArray(display)) {
1421
1721
  return display;
1422
1722
  }
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];
1723
+ const overlay = lookupBindingDisplay(descriptor.id);
1724
+ return overlay ?? {};
1432
1725
  }
1433
1726
 
1434
1727
  // src/config.ts
1435
1728
  init_esm_shims();
1729
+ init_logger();
1436
1730
  import * as fs2 from "fs";
1437
- import yaml from "js-yaml";
1731
+ import { createRequire } from "module";
1732
+ import yaml2 from "js-yaml";
1438
1733
  var DEFAULTS = {
1439
1734
  "extensions.root": "./extensions",
1440
1735
  "logging.level": "WARNING",
1441
- "sandbox.enabled": false,
1442
- "cli.stdin_buffer_limit": 10485760,
1443
- "cli.auto_approve": false,
1444
1736
  "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
1737
  // FE-11 config keys
1451
1738
  "cli.approval_timeout": 60,
1452
1739
  "cli.strategy": "standard",
1453
1740
  "cli.group_depth": 1,
1454
- "apcore-cli.approval_timeout": 60,
1455
- "apcore-cli.strategy": "standard",
1456
- "apcore-cli.group_depth": 1
1741
+ // Exposure filtering (FE-12)
1742
+ "expose.mode": "all",
1743
+ "expose.include": [],
1744
+ "expose.exclude": []
1745
+ // Builtin group visibility (FE-13) — apcli.* keys are NOT in DEFAULTS.
1746
+ // The runtime reads them via resolveObject('apcli') (raw yaml walk) and
1747
+ // does not use the flat-key resolve() path. Python and Rust have no such
1748
+ // entries either. (D11-008 cleanup)
1749
+ };
1750
+ var NAMESPACE_DEFAULTS = {
1751
+ stdin_buffer_limit: 10485760,
1752
+ auto_approve: false,
1753
+ help_text_max_length: 1e3,
1754
+ logging_level: "WARNING",
1755
+ approval_timeout: 60,
1756
+ strategy: "standard",
1757
+ group_depth: 1,
1758
+ // FE-13 — builtin group visibility configuration
1759
+ apcli: {
1760
+ mode: null,
1761
+ include: [],
1762
+ exclude: [],
1763
+ disable_env: false
1764
+ }
1457
1765
  };
1458
1766
  var NAMESPACE_TO_LEGACY = {
1459
1767
  "apcore-cli.stdin_buffer_limit": "cli.stdin_buffer_limit",
@@ -1466,20 +1774,13 @@ var LEGACY_TO_NAMESPACE = Object.fromEntries(
1466
1774
  );
1467
1775
  function registerConfigNamespace() {
1468
1776
  try {
1469
- const { Config } = __require("apcore-js");
1777
+ const nodeRequire = createRequire(import.meta.url);
1778
+ const { Config } = nodeRequire("apcore-js");
1470
1779
  if (typeof Config?.registerNamespace === "function") {
1471
1780
  Config.registerNamespace({
1472
1781
  name: "apcore-cli",
1473
1782
  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
- }
1783
+ defaults: NAMESPACE_DEFAULTS
1483
1784
  });
1484
1785
  }
1485
1786
  } catch {
@@ -1490,6 +1791,13 @@ var ConfigResolver = class {
1490
1791
  configPath;
1491
1792
  fileCache = null;
1492
1793
  fileCacheLoaded = false;
1794
+ /**
1795
+ * Raw parsed yaml root (pre-flatten). Populated alongside `fileCache`
1796
+ * on load. Used by `resolveObject()` to walk nested paths without
1797
+ * invoking `flattenDict` — see FE-13 spec §4.8 M1 note.
1798
+ * `null` when no config file is present or parsing fails.
1799
+ */
1800
+ _rawConfig = null;
1493
1801
  constructor(cliFlags, configPath) {
1494
1802
  this.cliFlags = cliFlags ?? {};
1495
1803
  this.configPath = configPath ?? "apcore.yaml";
@@ -1498,9 +1806,8 @@ var ConfigResolver = class {
1498
1806
  * Resolve a single configuration key across all four tiers.
1499
1807
  */
1500
1808
  resolve(key, cliFlag, envVar) {
1501
- const flagKey = cliFlag ?? key;
1502
- if (flagKey in this.cliFlags) {
1503
- const value = this.cliFlags[flagKey];
1809
+ if (cliFlag !== void 0 && cliFlag in this.cliFlags) {
1810
+ const value = this.cliFlags[cliFlag];
1504
1811
  if (value !== null && value !== void 0) {
1505
1812
  return value;
1506
1813
  }
@@ -1537,6 +1844,44 @@ var ConfigResolver = class {
1537
1844
  }
1538
1845
  return this.fileCache[key];
1539
1846
  }
1847
+ /**
1848
+ * Resolve a configuration key to its raw nested value (FE-13).
1849
+ *
1850
+ * Unlike `resolve()`, this method does NOT flatten the yaml tree — it
1851
+ * walks the dot-separated path directly against the parsed yaml root.
1852
+ * This lets callers retrieve non-leaf structures (booleans, arrays,
1853
+ * objects) such as the `apcli` visibility config, which is naturally
1854
+ * shaped as a nested object in apcore.yaml.
1855
+ *
1856
+ * Semantics:
1857
+ * - Returns `null` when no config file is loaded or when the path is
1858
+ * not present / descends into a non-object node (including arrays).
1859
+ * - Returns the raw value (boolean / array / object / scalar) when the
1860
+ * full path resolves to a leaf or intermediate node.
1861
+ *
1862
+ * Intentionally DOES NOT consult DEFAULTS, env vars, or CLI flags — it is
1863
+ * strictly a yaml-tree accessor. Scalar `resolve()` semantics are
1864
+ * unaffected.
1865
+ */
1866
+ resolveObject(key) {
1867
+ if (!this.fileCacheLoaded) {
1868
+ this.fileCache = this.loadConfigFile();
1869
+ this.fileCacheLoaded = true;
1870
+ }
1871
+ if (this._rawConfig == null) {
1872
+ return null;
1873
+ }
1874
+ const parts = key.split(".");
1875
+ let cur = this._rawConfig;
1876
+ for (const p of parts) {
1877
+ if (cur != null && typeof cur === "object" && !Array.isArray(cur) && p in cur) {
1878
+ cur = cur[p];
1879
+ } else {
1880
+ return null;
1881
+ }
1882
+ }
1883
+ return cur;
1884
+ }
1540
1885
  /**
1541
1886
  * Load and flatten a YAML config file.
1542
1887
  */
@@ -1548,26 +1893,27 @@ var ConfigResolver = class {
1548
1893
  if (err instanceof Error && "code" in err && err.code === "ENOENT") {
1549
1894
  return null;
1550
1895
  }
1551
- console.warn(
1896
+ warn(
1552
1897
  `Configuration file '${this.configPath}' is malformed, using defaults.`
1553
1898
  );
1554
1899
  return null;
1555
1900
  }
1556
1901
  let parsed;
1557
1902
  try {
1558
- parsed = yaml.load(content);
1903
+ parsed = yaml2.load(content);
1559
1904
  } catch {
1560
- console.warn(
1905
+ warn(
1561
1906
  `Configuration file '${this.configPath}' is malformed, using defaults.`
1562
1907
  );
1563
1908
  return null;
1564
1909
  }
1565
1910
  if (typeof parsed !== "object" || parsed === null || Array.isArray(parsed)) {
1566
- console.warn(
1911
+ warn(
1567
1912
  `Configuration file '${this.configPath}' is malformed, using defaults.`
1568
1913
  );
1569
1914
  return null;
1570
1915
  }
1916
+ this._rawConfig = parsed;
1571
1917
  return this.flattenDict(parsed);
1572
1918
  }
1573
1919
  /**
@@ -1593,95 +1939,89 @@ var ConfigResolver = class {
1593
1939
  // src/shell.ts
1594
1940
  init_esm_shims();
1595
1941
  init_errors();
1596
- import { readFileSync as readFileSync2 } from "fs";
1597
- import { fileURLToPath as fileURLToPath2 } from "url";
1598
- import * as path3 from "path";
1599
1942
  import { spawnSync } from "child_process";
1600
1943
  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
1944
  function makeFunctionName(progName) {
1609
1945
  return "_" + progName.replace(/[^a-zA-Z0-9]/g, "_");
1610
1946
  }
1611
1947
  function shellQuote(s) {
1612
1948
  return "'" + s.replace(/'/g, "'\\''") + "'";
1613
1949
  }
1614
- function generateBashCompletion(progName) {
1950
+ function enumerateApcliSubcommands(apcliGroup) {
1951
+ if (!apcliGroup) return [];
1952
+ return apcliGroup.commands.filter((c) => !isCmdHidden(c)).map((c) => c.name());
1953
+ }
1954
+ function enumerateRootCommands(program) {
1955
+ return program.commands.filter((c) => !isCmdHidden(c)).map((c) => c.name());
1956
+ }
1957
+ function findApcliGroup(program) {
1958
+ return program.commands.find((c) => c.name() === "apcli");
1959
+ }
1960
+ function isCmdHidden(cmd) {
1961
+ const withHiddenFn = cmd;
1962
+ const withHiddenField = cmd;
1963
+ if (typeof withHiddenFn.hidden === "function") return !!withHiddenFn.hidden();
1964
+ return !!withHiddenField._hidden;
1965
+ }
1966
+ function generateBashCompletion(progName, program) {
1615
1967
  const fn = makeFunctionName(progName);
1616
1968
  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}() {
1969
+ const apcliGroup = program ? findApcliGroup(program) : void 0;
1970
+ const apcliVisible = apcliGroup !== void 0 && !isCmdHidden(apcliGroup);
1971
+ const rootCmds = program ? enumerateRootCommands(program).filter(
1972
+ (n) => n !== "apcli" || apcliVisible
1973
+ ) : [];
1974
+ const apcliCmds = apcliVisible ? enumerateApcliSubcommands(apcliGroup) : [];
1975
+ const rootOpts = rootCmds.join(" ");
1976
+ const apcliOpts = apcliCmds.join(" ");
1977
+ let body = `${fn}() {
1631
1978
  local cur prev opts
1632
1979
  COMPREPLY=()
1633
1980
  cur="\${COMP_WORDS[COMP_CWORD]}"
1634
1981
  prev="\${COMP_WORDS[COMP_CWORD-1]}"
1635
1982
 
1636
1983
  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}) )
1984
+ opts="${rootOpts}"
1985
+ COMPREPLY=( $(compgen -W "\${opts}" -- \${cur}) )
1640
1986
  return 0
1641
1987
  fi
1642
-
1988
+ `;
1989
+ if (apcliVisible) {
1990
+ body += `
1643
1991
  if [[ \${COMP_CWORD} -eq 2 ]]; then
1644
- if [[ "\${COMP_WORDS[1]}" == "exec" ]]; then
1645
- local modules=$(${moduleListCmd})
1646
- COMPREPLY=( $(compgen -W "\${modules}" -- \${cur}) )
1992
+ if [[ "\${COMP_WORDS[1]}" == "apcli" ]]; then
1993
+ local apcli_cmds="${apcliOpts}"
1994
+ COMPREPLY=( $(compgen -W "\${apcli_cmds}" -- \${cur}) )
1647
1995
  return 0
1648
1996
  fi
1649
- export _APCORE_GRP="\${COMP_WORDS[1]}"
1650
- local group_cmds=$(${groupCmdsCmd})
1651
- COMPREPLY=( $(compgen -W "\${group_cmds}" -- \${cur}) )
1652
- return 0
1653
1997
  fi
1654
- }
1998
+ `;
1999
+ }
2000
+ body += `}
1655
2001
  complete -F ${fn} ${quoted}
1656
2002
  `;
2003
+ return body;
1657
2004
  }
1658
- function generateZshCompletion(progName) {
2005
+ function generateZshCompletion(progName, program) {
1659
2006
  const fn = makeFunctionName(progName);
1660
2007
  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`;
2008
+ const apcliGroup = program ? findApcliGroup(program) : void 0;
2009
+ const apcliVisible = apcliGroup !== void 0 && !isCmdHidden(apcliGroup);
2010
+ const rootCmds = program ? enumerateRootCommands(program).filter(
2011
+ (n) => n !== "apcli" || apcliVisible
2012
+ ) : [];
2013
+ const apcliCmds = apcliVisible ? enumerateApcliSubcommands(apcliGroup) : [];
2014
+ const rootEntries = rootCmds.map((n) => ` '${n}:${n}'`).join("\n");
2015
+ const apcliEntries = apcliCmds.map((n) => ` '${n}:${n}'`).join("\n");
1674
2016
  return `#compdef ${progName}
1675
2017
 
1676
2018
  ${fn}() {
1677
2019
  local -a commands
1678
2020
  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
- )
2021
+ ` + (rootEntries ? rootEntries + "\n" : "") + ` )
2022
+ local -a apcli_cmds
2023
+ apcli_cmds=(
2024
+ ` + (apcliEntries ? apcliEntries + "\n" : "") + ` )
1685
2025
 
1686
2026
  _arguments -C \\
1687
2027
  '1:command:->command' \\
@@ -1690,22 +2030,11 @@ ${fn}() {
1690
2030
  case "$state" in
1691
2031
  command)
1692
2032
  _describe -t commands '${progName} commands' commands
1693
- local -a groups_and_top
1694
- groups_and_top=($(${groupsAndTopCmd}))
1695
- compadd -a groups_and_top
1696
2033
  ;;
1697
2034
  args)
1698
2035
  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
2036
+ apcli)
2037
+ _describe -t apcli_cmds '${progName} apcli commands' apcli_cmds
1709
2038
  ;;
1710
2039
  esac
1711
2040
  ;;
@@ -1715,158 +2044,30 @@ ${fn}() {
1715
2044
  compdef ${fn} ${quoted}
1716
2045
  `;
1717
2046
  }
1718
- function generateFishCompletion(progName) {
2047
+ function generateFishCompletion(progName, program) {
1719
2048
  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, "\\-")
2049
+ const apcliGroup = program ? findApcliGroup(program) : void 0;
2050
+ const apcliVisible = apcliGroup !== void 0 && !isCmdHidden(apcliGroup);
2051
+ const rootCmds = program ? enumerateRootCommands(program).filter(
2052
+ (n) => n !== "apcli" || apcliVisible
2053
+ ) : [];
2054
+ const apcliCmds = apcliVisible ? enumerateApcliSubcommands(apcliGroup) : [];
2055
+ const lines = [];
2056
+ lines.push(`# Fish completions for ${progName}`);
2057
+ for (const name of rootCmds) {
2058
+ lines.push(
2059
+ `complete -c ${quoted} -n "__fish_use_subcommand" -a ${name} -d "${name}"`
1790
2060
  );
1791
2061
  }
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
- }
2062
+ if (apcliVisible && apcliCmds.length > 0) {
2063
+ lines.push("");
2064
+ for (const name of apcliCmds) {
2065
+ lines.push(
2066
+ `complete -c ${quoted} -n "__fish_seen_subcommand_from apcli" -a ${name} -d "${name}"`
2067
+ );
1808
2068
  }
1809
2069
  }
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");
2070
+ return lines.join("\n") + "\n";
1870
2071
  }
1871
2072
  function roffEscape(s) {
1872
2073
  return s.replace(/\\/g, "\\\\").replace(/-/g, "\\-").replace(/'/g, "\\(aq");
@@ -2008,10 +2209,13 @@ function configureManHelp(program, progName, version, description, docsUrl2) {
2008
2209
  return "";
2009
2210
  });
2010
2211
  }
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) => {
2212
+ function findRootProgram(host) {
2213
+ let cur = host;
2214
+ while (cur.parent) cur = cur.parent;
2215
+ return cur;
2216
+ }
2217
+ function registerCompletionCommand(host) {
2218
+ 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
2219
  const validShells = ["bash", "zsh", "fish"];
2016
2220
  if (!validShells.includes(shell)) {
2017
2221
  process.stderr.write(
@@ -2020,36 +2224,115 @@ function registerShellCommands(cli, progName = "apcore-cli") {
2020
2224
  );
2021
2225
  process.exit(EXIT_CODES.INVALID_CLI_INPUT);
2022
2226
  }
2023
- const resolved = cli.name() || progName;
2227
+ const root = findRootProgram(host);
2228
+ const resolved = root.name() || "apcore-cli";
2024
2229
  const generators = {
2025
- bash: () => generateBashCompletion(resolved),
2026
- zsh: () => generateZshCompletion(resolved),
2027
- fish: () => generateFishCompletion(resolved)
2230
+ bash: () => generateBashCompletion(resolved, root),
2231
+ zsh: () => generateZshCompletion(resolved, root),
2232
+ fish: () => generateFishCompletion(resolved, root)
2028
2233
  };
2029
2234
  process.stdout.write(generators[shell]());
2030
2235
  });
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);
2236
+ host.addCommand(completionCmd);
2047
2237
  }
2048
2238
 
2049
2239
  // src/discovery.ts
2050
2240
  init_esm_shims();
2051
- init_errors();
2052
2241
  import { Command as Command2, Option as Option2 } from "commander";
2242
+ init_errors();
2243
+ init_audit();
2244
+
2245
+ // src/system-usage.ts
2246
+ init_esm_shims();
2247
+ import * as fs4 from "fs";
2248
+ import * as os2 from "os";
2249
+ import * as path4 from "path";
2250
+ var PERIOD_TO_MS = {
2251
+ "1h": 60 * 60 * 1e3,
2252
+ "24h": 24 * 60 * 60 * 1e3,
2253
+ "7d": 7 * 24 * 60 * 60 * 1e3,
2254
+ "30d": 30 * 24 * 60 * 60 * 1e3
2255
+ };
2256
+ var DEFAULT_AUDIT_PATH = path4.join(
2257
+ os2.homedir(),
2258
+ ".apcore-cli",
2259
+ "audit.jsonl"
2260
+ );
2261
+ function computeSummary(options = {}) {
2262
+ const auditPath = options.auditPath ?? DEFAULT_AUDIT_PATH;
2263
+ const period = options.period ?? "24h";
2264
+ const cutoff = (options.now ?? /* @__PURE__ */ new Date()).getTime() - PERIOD_TO_MS[period];
2265
+ if (!fs4.existsSync(auditPath)) {
2266
+ return /* @__PURE__ */ new Map();
2267
+ }
2268
+ let raw;
2269
+ try {
2270
+ raw = fs4.readFileSync(auditPath, "utf-8");
2271
+ } catch {
2272
+ return /* @__PURE__ */ new Map();
2273
+ }
2274
+ const counts = /* @__PURE__ */ new Map();
2275
+ const errors = /* @__PURE__ */ new Map();
2276
+ const latencySum = /* @__PURE__ */ new Map();
2277
+ for (const line of raw.split("\n")) {
2278
+ const trimmed = line.trim();
2279
+ if (!trimmed) continue;
2280
+ let entry;
2281
+ try {
2282
+ entry = JSON.parse(trimmed);
2283
+ } catch {
2284
+ continue;
2285
+ }
2286
+ const ts = typeof entry.timestamp === "string" ? Date.parse(entry.timestamp) : NaN;
2287
+ if (Number.isNaN(ts) || ts < cutoff) continue;
2288
+ const moduleId = typeof entry.module_id === "string" ? entry.module_id : null;
2289
+ if (!moduleId) continue;
2290
+ counts.set(moduleId, (counts.get(moduleId) ?? 0) + 1);
2291
+ if (entry.status === "error") {
2292
+ errors.set(moduleId, (errors.get(moduleId) ?? 0) + 1);
2293
+ }
2294
+ const duration = entry.duration_ms;
2295
+ if (typeof duration === "number") {
2296
+ latencySum.set(moduleId, (latencySum.get(moduleId) ?? 0) + duration);
2297
+ }
2298
+ }
2299
+ const out = /* @__PURE__ */ new Map();
2300
+ for (const [id, calls] of counts) {
2301
+ out.set(id, {
2302
+ module_id: id,
2303
+ calls,
2304
+ errors: errors.get(id) ?? 0,
2305
+ latency_ms: calls > 0 ? (latencySum.get(id) ?? 0) / calls : 0
2306
+ });
2307
+ }
2308
+ return out;
2309
+ }
2310
+ function sortModulesByUsage(modules, field, options = {}) {
2311
+ const reverse = options.reverse ?? true;
2312
+ const summary = computeSummary({
2313
+ auditPath: options.auditPath,
2314
+ period: options.period
2315
+ });
2316
+ if (summary.size === 0) {
2317
+ modules.sort((a, b) => (a.id ?? a.module_id ?? "").localeCompare(b.id ?? b.module_id ?? ""));
2318
+ if (reverse) modules.reverse();
2319
+ return { used: false };
2320
+ }
2321
+ const key = (m) => {
2322
+ const id = m.id ?? m.module_id ?? "";
2323
+ const s = summary.get(id);
2324
+ if (!s) return 0;
2325
+ return field === "latency" ? s.latency_ms : field === "calls" ? s.calls : s.errors;
2326
+ };
2327
+ modules.sort((a, b) => {
2328
+ const diff = key(a) - key(b);
2329
+ if (diff !== 0) return reverse ? -diff : diff;
2330
+ return (a.id ?? a.module_id ?? "").localeCompare(b.id ?? b.module_id ?? "");
2331
+ });
2332
+ return { used: true };
2333
+ }
2334
+
2335
+ // src/discovery.ts
2053
2336
  var TAG_PATTERN = /^[a-z][a-z0-9_-]*$/;
2054
2337
  function validateTag(tag) {
2055
2338
  if (!TAG_PATTERN.test(tag)) {
@@ -2076,17 +2359,22 @@ function getAnnotationFlag(moduleDef, flag) {
2076
2359
  "readonly": "readonly",
2077
2360
  "streaming": "streaming",
2078
2361
  "cacheable": "cacheable",
2079
- "idempotent": "idempotent"
2362
+ "idempotent": "idempotent",
2363
+ "paginated": "paginated"
2080
2364
  };
2081
2365
  const attr = map[flag] ?? flag;
2082
2366
  return ann[attr] === true;
2083
2367
  }
2084
- function registerDiscoveryCommands(cli, registry) {
2085
- 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(
2368
+ function registerListCommand(apcliGroup, registry, exposureFilter) {
2369
+ 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).addOption(
2370
+ new Option2("--format <format>", "Output format.").choices(["table", "json", "csv", "yaml", "jsonl", "markdown", "skill"])
2371
+ ).option("-s, --search <query>", "Filter by substring match on ID and description.").addOption(
2086
2372
  new Option2("--status <status>", "Filter by module status.").choices(["enabled", "disabled", "all"]).default("enabled")
2087
2373
  ).option("-a, --annotation <flag>", "Filter by annotation flag (AND logic). Repeatable.", collectAnnotation, []).addOption(
2088
2374
  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) => {
2375
+ ).option("--reverse", "Reverse sort order.", false).option("--deprecated", "Include deprecated modules.", false).option("--deps", "Show dependency count column.", false).addOption(
2376
+ new Option2("--exposure <mode>", "Filter by exposure status.").choices(["exposed", "hidden", "all"]).default("exposed")
2377
+ ).action((opts) => {
2090
2378
  for (const t of opts.tag) {
2091
2379
  validateTag(t);
2092
2380
  }
@@ -2130,21 +2418,40 @@ function registerDiscoveryCommands(cli, registry) {
2130
2418
  }
2131
2419
  }
2132
2420
  if (opts.sort === "calls" || opts.sort === "errors" || opts.sort === "latency") {
2133
- process.stderr.write(
2134
- `Warning: Usage data not available; sorting by id. Sort by ${opts.sort} requires system.usage modules.
2421
+ const { used } = sortModulesByUsage(modules, opts.sort, { reverse: !opts.reverse });
2422
+ if (!used) {
2423
+ process.stderr.write(
2424
+ `note: no usage data available for --sort ${opts.sort}; sorted by id. Run some modules first to populate ~/.apcore-cli/audit.jsonl.
2135
2425
  `
2136
- );
2426
+ );
2427
+ }
2428
+ } else {
2429
+ modules.sort((a, b) => (a.id ?? "").localeCompare(b.id ?? ""));
2430
+ if (opts.reverse) {
2431
+ modules.reverse();
2432
+ }
2433
+ }
2434
+ let showExposureCol = false;
2435
+ if (exposureFilter && opts.exposure !== "all") {
2436
+ if (opts.exposure === "exposed") {
2437
+ modules = modules.filter((m) => exposureFilter.isExposed(m.id ?? ""));
2438
+ } else if (opts.exposure === "hidden") {
2439
+ modules = modules.filter((m) => !exposureFilter.isExposed(m.id ?? ""));
2440
+ }
2137
2441
  }
2138
- modules.sort((a, b) => (a.id ?? "").localeCompare(b.id ?? ""));
2139
- if (opts.reverse) {
2140
- modules.reverse();
2442
+ if (opts.exposure === "all" && exposureFilter) {
2443
+ showExposureCol = true;
2141
2444
  }
2142
2445
  const fmt = resolveFormat(opts.format);
2143
2446
  const filterTagsArg = opts.tag.length > 0 ? opts.tag : void 0;
2144
- formatModuleList(modules, fmt, filterTagsArg, opts.deps);
2447
+ void formatModuleList(modules, fmt, filterTagsArg, opts.deps, showExposureCol ? exposureFilter : void 0);
2145
2448
  });
2146
- cli.addCommand(listCmd);
2147
- 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) => {
2449
+ apcliGroup.addCommand(listCmd);
2450
+ }
2451
+ function registerDescribeCommand(apcliGroup, registry) {
2452
+ const describeCmd = new Command2("describe").description("Show metadata, schema, and annotations for a module.").argument("<module-id>", "Module ID to describe").addOption(
2453
+ new Option2("--format <format>", "Output format.").choices(["table", "json", "csv", "yaml", "jsonl", "markdown", "skill"])
2454
+ ).action((moduleId, opts) => {
2148
2455
  validateModuleId(moduleId);
2149
2456
  const moduleDef = registry.getModule(moduleId);
2150
2457
  if (!moduleDef) {
@@ -2155,9 +2462,88 @@ function registerDiscoveryCommands(cli, registry) {
2155
2462
  process.exit(EXIT_CODES.MODULE_NOT_FOUND);
2156
2463
  }
2157
2464
  const fmt = resolveFormat(opts.format);
2158
- formatModuleDetail(moduleDef, fmt);
2465
+ void formatModuleDetail(moduleDef, fmt);
2466
+ });
2467
+ apcliGroup.addCommand(describeCmd);
2468
+ }
2469
+ function registerExecCommand(apcliGroup, registry, executor) {
2470
+ 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(
2471
+ "--input <json>",
2472
+ "JSON object passed as input to the module. Use '-' to read JSON from stdin."
2473
+ ).option("-y, --yes", "Auto-approve if the module declares requires_approval.", false).option(
2474
+ "--approval-timeout <seconds>",
2475
+ "Seconds to wait for interactive approval.",
2476
+ parseInt
2477
+ ).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) => {
2478
+ validateModuleId(moduleId);
2479
+ const moduleDef = registry.getModule(moduleId);
2480
+ if (!moduleDef) {
2481
+ process.stderr.write(`Error: Module '${moduleId}' not found.
2482
+ `);
2483
+ process.exit(EXIT_CODES.MODULE_NOT_FOUND);
2484
+ }
2485
+ let merged = {};
2486
+ if (opts.input === "-") {
2487
+ merged = await collectInput("-", {}, false);
2488
+ } else if (opts.input !== void 0) {
2489
+ try {
2490
+ const parsed = JSON.parse(opts.input);
2491
+ if (typeof parsed !== "object" || parsed === null || Array.isArray(parsed)) {
2492
+ process.stderr.write("Error: --input JSON must be an object.\n");
2493
+ process.exit(EXIT_CODES.INVALID_CLI_INPUT);
2494
+ }
2495
+ merged = parsed;
2496
+ } catch (err) {
2497
+ const msg = err instanceof Error ? err.message : String(err);
2498
+ process.stderr.write(`Error: --input is not valid JSON: ${msg}
2499
+ `);
2500
+ process.exit(EXIT_CODES.INVALID_CLI_INPUT);
2501
+ }
2502
+ }
2503
+ const startTime = performance.now();
2504
+ try {
2505
+ await checkApproval(moduleDef, opts.yes, opts.approvalTimeout);
2506
+ if (opts.dryRun) {
2507
+ if (executor.validate) {
2508
+ const preflight = await executor.validate(moduleId, merged);
2509
+ formatPreflightResult(preflight, opts.format);
2510
+ } else {
2511
+ process.stdout.write(JSON.stringify({ valid: true }) + "\n");
2512
+ }
2513
+ return;
2514
+ }
2515
+ let result;
2516
+ if (opts.strategy && executor.callWithTrace) {
2517
+ const [res] = await executor.callWithTrace(moduleId, merged, { strategy: opts.strategy });
2518
+ result = res;
2519
+ } else {
2520
+ const { Sandbox: Sandbox2 } = await Promise.resolve().then(() => (init_security(), security_exports));
2521
+ const sandbox = new Sandbox2(opts.sandbox);
2522
+ result = await sandbox.execute(moduleId, merged, executor);
2523
+ }
2524
+ const durationMs = Math.round(performance.now() - startTime);
2525
+ const fmt = resolveFormat(opts.format);
2526
+ formatExecResult(result, fmt, opts.fields);
2527
+ const auditLogger = getAuditLogger();
2528
+ if (auditLogger) {
2529
+ auditLogger.logExecution(moduleId, merged, "success", 0, durationMs);
2530
+ }
2531
+ } catch (err) {
2532
+ const exitCode = exitCodeForError(err);
2533
+ const durationMs = Math.round(performance.now() - startTime);
2534
+ try {
2535
+ const auditLogger = getAuditLogger();
2536
+ if (auditLogger) {
2537
+ auditLogger.logExecution(moduleId, merged, "error", exitCode, durationMs);
2538
+ }
2539
+ } catch {
2540
+ }
2541
+ process.stderr.write(`Error: ${err instanceof Error ? err.message : err}
2542
+ `);
2543
+ process.exit(exitCode);
2544
+ }
2159
2545
  });
2160
- cli.addCommand(describeCmd);
2546
+ apcliGroup.addCommand(execCmd);
2161
2547
  }
2162
2548
  function registerValidateCommand(cli, registry, executor) {
2163
2549
  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,11 +2557,25 @@ function registerValidateCommand(cli, registry, executor) {
2171
2557
  const merged = opts.input ? await collectInput(opts.input, {}, false) : {};
2172
2558
  if (!executor.validate) {
2173
2559
  process.stderr.write("Error: Executor does not support validate.\n");
2174
- process.exit(1);
2560
+ process.exit(EXIT_CODES.MODULE_EXECUTE_ERROR);
2561
+ }
2562
+ try {
2563
+ const preflight = await executor.validate(moduleId, merged);
2564
+ formatPreflightResult(preflight, opts.format);
2565
+ process.exit(preflight.valid ? 0 : firstFailedExitCode(preflight));
2566
+ } catch (err) {
2567
+ const exitCode = exitCodeForError(err);
2568
+ try {
2569
+ const auditLogger = getAuditLogger();
2570
+ if (auditLogger) {
2571
+ auditLogger.logExecution(moduleId, merged, "error", exitCode, 0);
2572
+ }
2573
+ } catch {
2574
+ }
2575
+ process.stderr.write(`Error: ${err instanceof Error ? err.message : err}
2576
+ `);
2577
+ process.exit(exitCode);
2175
2578
  }
2176
- const preflight = await executor.validate(moduleId, merged);
2177
- formatPreflightResult(preflight, opts.format);
2178
- process.exit(preflight.valid ? 0 : firstFailedExitCode(preflight));
2179
2579
  });
2180
2580
  cli.addCommand(validateCmd);
2181
2581
  }
@@ -2183,12 +2583,34 @@ function registerValidateCommand(cli, registry, executor) {
2183
2583
  // src/system-cmd.ts
2184
2584
  init_esm_shims();
2185
2585
  import { Command as Command3 } from "commander";
2586
+ init_errors();
2186
2587
  async function callSystemModule(executor, moduleId, inputs) {
2187
2588
  if (executor.call) {
2188
2589
  return executor.call(moduleId, inputs);
2189
2590
  }
2190
2591
  return executor.execute(moduleId, inputs);
2191
2592
  }
2593
+ function emitResult(jsonPayload, fmt, ttyRender) {
2594
+ if (fmt === "json" || !process.stdout.isTTY) {
2595
+ process.stdout.write(JSON.stringify(jsonPayload, null, 2) + "\n");
2596
+ } else {
2597
+ ttyRender();
2598
+ }
2599
+ }
2600
+ function emitErrorAndExit(e) {
2601
+ process.stderr.write(`Error: ${e instanceof Error ? e.message : e}
2602
+ `);
2603
+ process.exit(exitCodeForError(e));
2604
+ }
2605
+ async function requireApprovalForSystemCommand(moduleId, autoApprove) {
2606
+ const syntheticModuleDef = {
2607
+ id: moduleId,
2608
+ name: moduleId,
2609
+ description: `system command: ${moduleId}`,
2610
+ annotations: { requires_approval: true }
2611
+ };
2612
+ await checkApproval(syntheticModuleDef, autoApprove, void 0);
2613
+ }
2192
2614
  function formatHealthSummaryTty(result) {
2193
2615
  const summary = result.summary ?? {};
2194
2616
  const modules = result.modules ?? [];
@@ -2277,17 +2699,7 @@ function formatUsageSummaryTty(result) {
2277
2699
  Total: ${totalCalls.toLocaleString()} calls | ${totalErrors.toLocaleString()} errors
2278
2700
  `);
2279
2701
  }
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
- }
2702
+ function registerHealthCommand(apcliGroup, executor) {
2291
2703
  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
2704
  const fmt = resolveFormat(opts.format);
2293
2705
  try {
@@ -2296,29 +2708,21 @@ async function registerSystemCommands(cli, executor) {
2296
2708
  module_id: moduleId,
2297
2709
  error_limit: opts.errors
2298
2710
  });
2299
- if (fmt === "json" || !process.stdout.isTTY) {
2300
- process.stdout.write(JSON.stringify(result, null, 2) + "\n");
2301
- } else {
2302
- formatHealthModuleTty(result);
2303
- }
2711
+ emitResult(result, fmt, () => formatHealthModuleTty(result));
2304
2712
  } else {
2305
2713
  const result = await callSystemModule(executor, "system.health.summary", {
2306
2714
  error_rate_threshold: opts.threshold,
2307
2715
  include_healthy: opts.all
2308
2716
  });
2309
- if (fmt === "json" || !process.stdout.isTTY) {
2310
- process.stdout.write(JSON.stringify(result, null, 2) + "\n");
2311
- } else {
2312
- formatHealthSummaryTty(result);
2313
- }
2717
+ emitResult(result, fmt, () => formatHealthSummaryTty(result));
2314
2718
  }
2315
2719
  } catch (e) {
2316
- process.stderr.write(`Error: ${e instanceof Error ? e.message : e}
2317
- `);
2318
- process.exit(1);
2720
+ emitErrorAndExit(e);
2319
2721
  }
2320
2722
  });
2321
- cli.addCommand(healthCmd);
2723
+ apcliGroup.addCommand(healthCmd);
2724
+ }
2725
+ function registerUsageCommand(apcliGroup, executor) {
2322
2726
  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
2727
  const fmt = resolveFormat(opts.format);
2324
2728
  try {
@@ -2333,83 +2737,71 @@ async function registerSystemCommands(cli, executor) {
2333
2737
  period: opts.period
2334
2738
  });
2335
2739
  }
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
- }
2740
+ emitResult(result, fmt, () => {
2741
+ if (moduleId) {
2742
+ formatExecResult(result, fmt);
2743
+ } else {
2744
+ formatUsageSummaryTty(result);
2745
+ }
2746
+ });
2343
2747
  } catch (e) {
2344
- process.stderr.write(`Error: ${e instanceof Error ? e.message : e}
2345
- `);
2346
- process.exit(1);
2748
+ emitErrorAndExit(e);
2347
2749
  }
2348
2750
  });
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
- }
2751
+ apcliGroup.addCommand(usageCmd);
2752
+ }
2753
+ function registerEnableCommand(apcliGroup, executor) {
2754
+ 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 (audit D11-B-001 cross-SDK parity).", false).option("--format <format>", "Output format.").action(async (moduleId, opts) => {
2354
2755
  const fmt = resolveFormat(opts.format);
2355
2756
  try {
2757
+ await requireApprovalForSystemCommand("system.control.toggle_feature", opts.yes);
2356
2758
  const result = await callSystemModule(executor, "system.control.toggle_feature", {
2357
2759
  module_id: moduleId,
2358
2760
  enabled: true,
2359
2761
  reason: opts.reason
2360
2762
  });
2361
- if (fmt === "json" || !process.stdout.isTTY) {
2362
- process.stdout.write(JSON.stringify(result, null, 2) + "\n");
2363
- } else {
2763
+ emitResult(result, fmt, () => {
2364
2764
  process.stdout.write(`Module '${moduleId}' enabled.
2365
2765
  Reason: ${opts.reason}
2366
2766
  `);
2367
- }
2767
+ });
2368
2768
  } catch (e) {
2369
- process.stderr.write(`Error: ${e instanceof Error ? e.message : e}
2370
- `);
2371
- process.exit(1);
2769
+ emitErrorAndExit(e);
2372
2770
  }
2373
2771
  });
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
- }
2772
+ apcliGroup.addCommand(enableCmd);
2773
+ }
2774
+ function registerDisableCommand(apcliGroup, executor) {
2775
+ 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 (audit D11-B-001 cross-SDK parity).", false).option("--format <format>", "Output format.").action(async (moduleId, opts) => {
2379
2776
  const fmt = resolveFormat(opts.format);
2380
2777
  try {
2778
+ await requireApprovalForSystemCommand("system.control.toggle_feature", opts.yes);
2381
2779
  const result = await callSystemModule(executor, "system.control.toggle_feature", {
2382
2780
  module_id: moduleId,
2383
2781
  enabled: false,
2384
2782
  reason: opts.reason
2385
2783
  });
2386
- if (fmt === "json" || !process.stdout.isTTY) {
2387
- process.stdout.write(JSON.stringify(result, null, 2) + "\n");
2388
- } else {
2784
+ emitResult(result, fmt, () => {
2389
2785
  process.stdout.write(`Module '${moduleId}' disabled.
2390
2786
  Reason: ${opts.reason}
2391
2787
  `);
2392
- }
2788
+ });
2393
2789
  } catch (e) {
2394
- process.stderr.write(`Error: ${e instanceof Error ? e.message : e}
2395
- `);
2396
- process.exit(1);
2790
+ emitErrorAndExit(e);
2397
2791
  }
2398
2792
  });
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
- }
2793
+ apcliGroup.addCommand(disableCmd);
2794
+ }
2795
+ function registerReloadCommand(apcliGroup, executor) {
2796
+ 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 (audit D11-B-001 cross-SDK parity).", false).option("--format <format>", "Output format.").action(async (moduleId, opts) => {
2404
2797
  const fmt = resolveFormat(opts.format);
2405
2798
  try {
2799
+ await requireApprovalForSystemCommand("system.control.reload_module", opts.yes);
2406
2800
  const result = await callSystemModule(executor, "system.control.reload_module", {
2407
2801
  module_id: moduleId,
2408
2802
  reason: opts.reason
2409
2803
  });
2410
- if (fmt === "json" || !process.stdout.isTTY) {
2411
- process.stdout.write(JSON.stringify(result, null, 2) + "\n");
2412
- } else {
2804
+ emitResult(result, fmt, () => {
2413
2805
  const prev = result.previous_version ?? "?";
2414
2806
  const newVer = result.new_version ?? "?";
2415
2807
  const dur = result.reload_duration_ms ?? "?";
@@ -2419,34 +2811,30 @@ async function registerSystemCommands(cli, executor) {
2419
2811
  `);
2420
2812
  process.stdout.write(` Duration: ${dur}ms
2421
2813
  `);
2422
- }
2814
+ });
2423
2815
  } catch (e) {
2424
- process.stderr.write(`Error: ${e instanceof Error ? e.message : e}
2425
- `);
2426
- process.exit(1);
2816
+ emitErrorAndExit(e);
2427
2817
  }
2428
2818
  });
2429
- cli.addCommand(reloadCmd);
2819
+ apcliGroup.addCommand(reloadCmd);
2820
+ }
2821
+ function registerConfigCommand(apcliGroup, executor) {
2430
2822
  const configGroup = new Command3("config").description("Read or update runtime configuration.");
2431
2823
  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
2824
  const fmt = resolveFormat(opts.format);
2433
2825
  try {
2434
2826
  const result = await callSystemModule(executor, "system.config.get", { key });
2435
2827
  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 {
2828
+ emitResult({ key, value }, fmt, () => {
2439
2829
  process.stdout.write(`${key} = ${JSON.stringify(value)}
2440
2830
  `);
2441
- }
2831
+ });
2442
2832
  } catch (e) {
2443
- process.stderr.write(`Error: ${e instanceof Error ? e.message : e}
2444
- `);
2445
- process.exit(1);
2833
+ emitErrorAndExit(e);
2446
2834
  }
2447
2835
  });
2448
2836
  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) => {
2837
+ const configSetCmd = new Command3("set").description("Update a runtime configuration value (audit-logged; client-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("-y, --yes", "Skip approval prompt (audit D11-B-001 cross-SDK parity).", false).option("--format <format>", "Output format.").action(async (key, value, opts) => {
2450
2838
  const fmt = resolveFormat(opts.format);
2451
2839
  let parsedValue;
2452
2840
  try {
@@ -2455,14 +2843,13 @@ async function registerSystemCommands(cli, executor) {
2455
2843
  parsedValue = value;
2456
2844
  }
2457
2845
  try {
2846
+ await requireApprovalForSystemCommand("system.control.update_config", opts.yes);
2458
2847
  const result = await callSystemModule(executor, "system.control.update_config", {
2459
2848
  key,
2460
2849
  value: parsedValue,
2461
2850
  reason: opts.reason
2462
2851
  });
2463
- if (fmt === "json" || !process.stdout.isTTY) {
2464
- process.stdout.write(JSON.stringify(result, null, 2) + "\n");
2465
- } else {
2852
+ emitResult(result, fmt, () => {
2466
2853
  const old = result.old_value ?? "?";
2467
2854
  const newVal = result.new_value ?? "?";
2468
2855
  process.stdout.write(`Config updated: ${key}
@@ -2471,20 +2858,40 @@ async function registerSystemCommands(cli, executor) {
2471
2858
  `);
2472
2859
  process.stdout.write(` Reason: ${opts.reason}
2473
2860
  `);
2474
- }
2861
+ });
2475
2862
  } catch (e) {
2476
- process.stderr.write(`Error: ${e instanceof Error ? e.message : e}
2477
- `);
2478
- process.exit(1);
2863
+ emitErrorAndExit(e);
2479
2864
  }
2480
2865
  });
2481
2866
  configGroup.addCommand(configSetCmd);
2482
- cli.addCommand(configGroup);
2867
+ apcliGroup.addCommand(configGroup);
2483
2868
  }
2484
2869
 
2485
2870
  // src/strategy.ts
2486
2871
  init_esm_shims();
2487
2872
  import { Command as Command4, Option as Option3 } from "commander";
2873
+ function lookupStrategyInfo(executor, strategyName) {
2874
+ if (typeof executor.describePipeline === "function") {
2875
+ try {
2876
+ const current = executor.describePipeline();
2877
+ if (current && current.name === strategyName) {
2878
+ return { info: current, isCurrent: true };
2879
+ }
2880
+ } catch {
2881
+ }
2882
+ }
2883
+ const ctor = executor.constructor;
2884
+ if (ctor && typeof ctor.listStrategies === "function") {
2885
+ try {
2886
+ const all = ctor.listStrategies();
2887
+ const info = all.find((s) => s.name === strategyName) ?? null;
2888
+ return { info, isCurrent: false };
2889
+ } catch {
2890
+ return { info: null, isCurrent: false };
2891
+ }
2892
+ }
2893
+ return { info: null, isCurrent: false };
2894
+ }
2488
2895
  var PRESET_STEPS = {
2489
2896
  standard: [
2490
2897
  "context_creation",
@@ -2543,87 +2950,84 @@ function registerPipelineCommand(cli, executor) {
2543
2950
  new Option3("--strategy <name>", "Strategy to describe (default: standard).").choices(["standard", "internal", "testing", "performance", "minimal"]).default("standard")
2544
2951
  ).option("--format <format>", "Output format.").action((opts) => {
2545
2952
  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
- ]);
2953
+ const { info, isCurrent } = lookupStrategyInfo(executor, opts.strategy);
2954
+ if (info) {
2955
+ const strategySteps = isCurrent ? executor.currentStrategy?.steps ?? [] : [];
2956
+ const header = `Pipeline: ${info.name} (${info.stepCount} steps)`;
2571
2957
  if (fmt === "json" || !process.stdout.isTTY) {
2572
2958
  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
- }))
2959
+ strategy: info.name,
2960
+ step_count: info.stepCount,
2961
+ description: info.description,
2962
+ steps: info.stepNames.map((name, i) => {
2963
+ const stepMeta = strategySteps[i];
2964
+ return {
2965
+ index: i + 1,
2966
+ name,
2967
+ pure: stepMeta?.pure ?? false,
2968
+ removable: stepMeta?.removable ?? true,
2969
+ timeout_ms: stepMeta?.timeoutMs ?? null
2970
+ };
2971
+ })
2581
2972
  };
2582
2973
  process.stdout.write(JSON.stringify(payload, null, 2) + "\n");
2583
2974
  } else {
2584
- process.stdout.write(`Pipeline: ${opts.strategy} (${steps.length} steps)
2975
+ process.stdout.write(`${header}
2585
2976
 
2586
2977
  `);
2587
2978
  process.stdout.write(` ${"#".padEnd(4)} ${"Step".padEnd(28)} ${"Pure".padEnd(6)} ${"Removable".padEnd(11)} Timeout
2588
2979
  `);
2589
2980
  process.stdout.write(` ${"-".repeat(4)} ${"-".repeat(28)} ${"-".repeat(6)} ${"-".repeat(11)} ${"-".repeat(8)}
2590
2981
  `);
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
2982
+ for (let i = 0; i < info.stepNames.length; i++) {
2983
+ const stepMeta = strategySteps[i];
2984
+ const pure = stepMeta?.pure ? "yes" : "no";
2985
+ const removable = stepMeta?.removable !== false ? "yes" : "no";
2986
+ const timeout = stepMeta?.timeoutMs ? `${stepMeta.timeoutMs}ms` : "\u2014";
2987
+ process.stdout.write(` ${String(i + 1).padEnd(4)} ${info.stepNames[i].padEnd(28)} ${pure.padEnd(6)} ${removable.padEnd(11)} ${timeout}
2595
2988
  `);
2596
2989
  }
2597
2990
  }
2598
2991
  return;
2599
2992
  }
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
- }));
2993
+ const steps = PRESET_STEPS[opts.strategy] ?? [];
2994
+ const pureSteps = /* @__PURE__ */ new Set([
2995
+ "context_creation",
2996
+ "call_chain_guard",
2997
+ "module_lookup",
2998
+ "acl_check",
2999
+ "input_validation"
3000
+ ]);
3001
+ const nonRemovable = /* @__PURE__ */ new Set([
3002
+ "context_creation",
3003
+ "module_lookup",
3004
+ "execute",
3005
+ "return_result"
3006
+ ]);
2606
3007
  if (fmt === "json" || !process.stdout.isTTY) {
2607
3008
  const payload = {
2608
3009
  strategy: opts.strategy,
2609
- step_count: stepsInfo.length,
2610
- steps: stepsInfo.map((s, i) => ({ index: i + 1, ...s }))
3010
+ step_count: steps.length,
3011
+ steps: steps.map((s, i) => ({
3012
+ index: i + 1,
3013
+ name: s,
3014
+ pure: pureSteps.has(s),
3015
+ removable: !nonRemovable.has(s)
3016
+ }))
2611
3017
  };
2612
3018
  process.stdout.write(JSON.stringify(payload, null, 2) + "\n");
2613
3019
  } else {
2614
- process.stdout.write(`Pipeline: ${opts.strategy} (${stepsInfo.length} steps)
3020
+ process.stdout.write(`Pipeline: ${opts.strategy} (${steps.length} steps)
2615
3021
 
2616
3022
  `);
2617
3023
  process.stdout.write(` ${"#".padEnd(4)} ${"Step".padEnd(28)} ${"Pure".padEnd(6)} ${"Removable".padEnd(11)} Timeout
2618
3024
  `);
2619
3025
  process.stdout.write(` ${"-".repeat(4)} ${"-".repeat(28)} ${"-".repeat(6)} ${"-".repeat(11)} ${"-".repeat(8)}
2620
3026
  `);
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}
3027
+ for (let i = 0; i < steps.length; i++) {
3028
+ const pure = pureSteps.has(steps[i]) ? "yes" : "no";
3029
+ const removable = nonRemovable.has(steps[i]) ? "no" : "yes";
3030
+ process.stdout.write(` ${String(i + 1).padEnd(4)} ${steps[i].padEnd(28)} ${pure.padEnd(6)} ${removable.padEnd(11)} \u2014
2627
3031
  `);
2628
3032
  }
2629
3033
  }
@@ -2631,333 +3035,596 @@ function registerPipelineCommand(cli, executor) {
2631
3035
  cli.addCommand(pipelineCmd);
2632
3036
  }
2633
3037
 
2634
- // src/cli.ts
3038
+ // src/builtin-group.ts
2635
3039
  init_esm_shims();
2636
- import { Command as Command5 } from "commander";
2637
- var BUILTIN_COMMANDS = [
2638
- "completion",
2639
- "config",
3040
+ init_errors();
3041
+ init_logger();
3042
+ var ApcliGroupError = class extends Error {
3043
+ constructor(message) {
3044
+ super(message);
3045
+ this.name = "ApcliGroupError";
3046
+ }
3047
+ };
3048
+ var DEFAULT_BUILTIN_GROUP_NAME = "apcli";
3049
+ var RESERVED_GROUP_NAMES = /* @__PURE__ */ new Set([DEFAULT_BUILTIN_GROUP_NAME]);
3050
+ var _effectiveReservedNames = RESERVED_GROUP_NAMES;
3051
+ function getReservedGroupNames() {
3052
+ return _effectiveReservedNames;
3053
+ }
3054
+ function setReservedGroupNames(names) {
3055
+ _effectiveReservedNames = names;
3056
+ }
3057
+ var _NAME_REGEX = /^[a-z][a-z0-9_-]*$/;
3058
+ function _validateBuiltinGroupName(name) {
3059
+ if (!name || !_NAME_REGEX.test(name)) {
3060
+ throw new ApcliGroupError(
3061
+ `builtinGroupName ${JSON.stringify(name)} must match /^[a-z][a-z0-9_-]*$/ (non-empty, lowercase, alphanumeric + '_' / '-', leading letter).`
3062
+ );
3063
+ }
3064
+ }
3065
+ var VALID_USER_MODES = /* @__PURE__ */ new Set([
3066
+ "all",
3067
+ "none",
3068
+ "include",
3069
+ "exclude"
3070
+ ]);
3071
+ var APCLI_SUBCOMMAND_NAMES = /* @__PURE__ */ new Set([
3072
+ "list",
2640
3073
  "describe",
2641
- "describe-pipeline",
2642
- "disable",
2643
- "enable",
2644
3074
  "exec",
2645
- "health",
3075
+ "validate",
2646
3076
  "init",
2647
- "list",
2648
- "man",
2649
- "reload",
3077
+ "health",
2650
3078
  "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;
3079
+ "enable",
3080
+ "disable",
3081
+ "reload",
3082
+ "config",
3083
+ "completion",
3084
+ "describe-pipeline"
3085
+ ]);
3086
+ var ApcliGroup = class _ApcliGroup {
3087
+ _mode;
3088
+ _include;
3089
+ _exclude;
3090
+ _disableEnv;
3091
+ _registryInjected;
3092
+ _fromCliConfig;
3093
+ _name;
3094
+ constructor(init) {
3095
+ this._mode = init.mode;
3096
+ this._include = init.include;
3097
+ this._exclude = init.exclude;
3098
+ this._disableEnv = init.disableEnv;
3099
+ this._registryInjected = init.registryInjected;
3100
+ this._fromCliConfig = init.fromCliConfig;
3101
+ this._name = init.name;
2667
3102
  }
2668
3103
  /**
2669
- * Build alias->module_id map from display overlay metadata.
3104
+ * Resolved name for the built-in command group (default `"apcli"`).
3105
+ * Overridable via createCli's `builtinGroupName` option for downstream
3106
+ * branded CLIs that want a custom namespace. Cross-SDK parity with
3107
+ * Python `ApcliGroup.name` (2026-05-08).
2670
3108
  */
2671
- buildAliasMap() {
2672
- if (this.aliasMapBuilt) {
2673
- return;
2674
- }
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);
2684
- }
2685
- }
2686
- this.aliasMapBuilt = true;
2687
- } catch {
2688
- warn("Failed to build alias map from registry");
3109
+ get name() {
3110
+ return this._name;
3111
+ }
3112
+ /**
3113
+ * Tier 1 constructor — config came from `createCli({ apcli })`.
3114
+ *
3115
+ * A non-auto mode from this tier wins over env var and yaml.
3116
+ */
3117
+ static fromCliConfig(config, opts) {
3118
+ return _ApcliGroup._build(
3119
+ config,
3120
+ opts,
3121
+ /*fromCliConfig*/
3122
+ true
3123
+ );
3124
+ }
3125
+ /**
3126
+ * Tier 3 constructor config came from `apcore.yaml`.
3127
+ *
3128
+ * Env var (Tier 2) may override the yaml-supplied mode.
3129
+ */
3130
+ static fromYaml(config, opts) {
3131
+ if (config !== null && config !== void 0 && typeof config !== "boolean" && (typeof config !== "object" || Array.isArray(config))) {
3132
+ const got = Array.isArray(config) ? "array" : typeof config;
3133
+ warn(
3134
+ `apcore.yaml apcli has unexpected type ${got}; using auto-detect.`
3135
+ );
3136
+ return _ApcliGroup._build(
3137
+ void 0,
3138
+ opts,
3139
+ /*fromCliConfig*/
3140
+ false
3141
+ );
2689
3142
  }
3143
+ return _ApcliGroup._build(
3144
+ config,
3145
+ opts,
3146
+ /*fromCliConfig*/
3147
+ false
3148
+ );
2690
3149
  }
2691
3150
  /**
2692
- * List all available command names from the Registry.
3151
+ * Non-panicking Tier 3 factory (A-001 parity with Rust's `try_from_yaml`).
3152
+ * Returns `[instance, null]` on success or `[null, errorMessage]` on invalid input.
3153
+ * Use this in programmatic contexts where throwing/exiting is unwanted.
2693
3154
  */
2694
- listCommands() {
2695
- this.buildAliasMap();
2696
- const reverse = /* @__PURE__ */ new Map();
2697
- for (const [alias, moduleId] of this.aliasMap) {
2698
- reverse.set(moduleId, alias);
3155
+ static tryFromYaml(config, opts) {
3156
+ if (config !== null && config !== void 0 && typeof config !== "boolean" && (typeof config !== "object" || Array.isArray(config))) {
3157
+ const got = Array.isArray(config) ? "array" : typeof config;
3158
+ return [
3159
+ null,
3160
+ `apcore.yaml 'apcli:' must be a bool, object, or null; got ${got}`
3161
+ ];
2699
3162
  }
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();
3163
+ if (config !== null && config !== void 0 && typeof config === "object" && !Array.isArray(config)) {
3164
+ const mode = config["mode"];
3165
+ if (mode !== void 0 && mode !== null) {
3166
+ const validModes = ["all", "none", "include", "exclude"];
3167
+ if (typeof mode !== "string" || !validModes.includes(mode)) {
3168
+ return [null, `Invalid apcli mode: '${mode}'. Must be one of: all, none, include, exclude.`];
3169
+ }
3170
+ }
3171
+ }
3172
+ return [_ApcliGroup.fromYaml(config, opts), null];
2703
3173
  }
2704
- /**
2705
- * Get or lazily build a Commander Command for the given module.
2706
- */
2707
- getCommand(cmdName) {
2708
- if (this.commandCache.has(cmdName)) {
2709
- return this.commandCache.get(cmdName);
3174
+ // -------------------------------------------------------------------------
3175
+ // Internal builder shared by both factories
3176
+ // -------------------------------------------------------------------------
3177
+ static _build(config, opts, fromCliConfig) {
3178
+ const name = opts.name ?? DEFAULT_BUILTIN_GROUP_NAME;
3179
+ _validateBuiltinGroupName(name);
3180
+ if (config === true) {
3181
+ return new _ApcliGroup({
3182
+ mode: "all",
3183
+ include: [],
3184
+ exclude: [],
3185
+ disableEnv: false,
3186
+ registryInjected: opts.registryInjected,
3187
+ fromCliConfig,
3188
+ name
3189
+ });
2710
3190
  }
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;
3191
+ if (config === false) {
3192
+ return new _ApcliGroup({
3193
+ mode: "none",
3194
+ include: [],
3195
+ exclude: [],
3196
+ disableEnv: false,
3197
+ registryInjected: opts.registryInjected,
3198
+ fromCliConfig,
3199
+ name
3200
+ });
2716
3201
  }
2717
- if (!moduleDef) {
2718
- return null;
3202
+ if (config === void 0 || config === null) {
3203
+ return new _ApcliGroup({
3204
+ mode: "auto",
3205
+ include: [],
3206
+ exclude: [],
3207
+ disableEnv: false,
3208
+ registryInjected: opts.registryInjected,
3209
+ fromCliConfig,
3210
+ name
3211
+ });
2719
3212
  }
2720
- const cmd = buildModuleCommand(moduleDef, this.executor, this.helpTextMaxLength, cmdName);
2721
- this.commandCache.set(cmdName, cmd);
2722
- return cmd;
2723
- }
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
3213
+ if (typeof config !== "object" || Array.isArray(config)) {
3214
+ process.stderr.write(
3215
+ `Error: apcli config must be a boolean or object; got ${Array.isArray(config) ? "array" : typeof config}.
3216
+ `
2742
3217
  );
2743
- this._cmdCache.set(cmdName, cmd);
2744
- this.command.addCommand(cmd);
3218
+ process.exit(EXIT_CODES.INVALID_CLI_INPUT);
2745
3219
  }
2746
- }
2747
- listCommands() {
2748
- return [...this.members.keys()].sort();
2749
- }
2750
- getCommand(cmdName) {
2751
- if (this._cmdCache.has(cmdName)) {
2752
- return this._cmdCache.get(cmdName);
3220
+ const cfg = config;
3221
+ let mode;
3222
+ if (cfg.mode === void 0 || cfg.mode === null) {
3223
+ mode = "auto";
3224
+ } else if (typeof cfg.mode !== "string") {
3225
+ process.stderr.write(
3226
+ `Error: apcli.mode must be a string; got ${typeof cfg.mode}. Expected one of all|none|include|exclude.
3227
+ `
3228
+ );
3229
+ process.exit(EXIT_CODES.INVALID_CLI_INPUT);
3230
+ } else if (!VALID_USER_MODES.has(cfg.mode)) {
3231
+ process.stderr.write(
3232
+ `Error: apcli.mode '${cfg.mode}' is invalid. Expected one of all|none|include|exclude.
3233
+ `
3234
+ );
3235
+ process.exit(EXIT_CODES.INVALID_CLI_INPUT);
3236
+ } else {
3237
+ mode = cfg.mode;
2753
3238
  }
2754
- const entry = this.members.get(cmdName);
2755
- if (!entry) {
2756
- return null;
3239
+ const include = _ApcliGroup._normalizeList(cfg.include, "include");
3240
+ const exclude = _ApcliGroup._normalizeList(cfg.exclude, "exclude");
3241
+ const rawDisableEnv = cfg.disableEnv !== void 0 ? cfg.disableEnv : cfg["disable_env"];
3242
+ let disableEnv = false;
3243
+ if (rawDisableEnv !== void 0) {
3244
+ if (typeof rawDisableEnv === "boolean") {
3245
+ disableEnv = rawDisableEnv;
3246
+ } else {
3247
+ warn(
3248
+ `apcli.disable_env must be boolean; got ${typeof rawDisableEnv}. Treating as false.`
3249
+ );
3250
+ }
2757
3251
  }
2758
- const [, descriptor] = entry;
2759
- const cmd = buildModuleCommand(
2760
- descriptor,
2761
- this._executor,
2762
- this._helpTextMaxLength,
2763
- cmdName
2764
- );
2765
- this._cmdCache.set(cmdName, cmd);
2766
- return cmd;
3252
+ return new _ApcliGroup({
3253
+ mode,
3254
+ include,
3255
+ exclude,
3256
+ disableEnv,
3257
+ registryInjected: opts.registryInjected,
3258
+ fromCliConfig,
3259
+ name
3260
+ });
2767
3261
  }
2768
- };
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
3262
  /**
2778
- * Determine (groupName | null, commandName) for a module from its display overlay.
3263
+ * Normalize an include/exclude list. Non-array warn and return [].
2779
3264
  *
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").
3265
+ * Unknown but well-formed entries emit a WARNING (spec §7 error table,
3266
+ * T-APCLI-25) but are retained in the returned list for forward-compat —
3267
+ * if apcore-cli later adds a subcommand named `foo`, existing configs
3268
+ * continue to work without a config change. At runtime, unknown names
3269
+ * simply never match any registered subcommand.
2784
3270
  */
2785
- static resolveGroup(moduleId, descriptor, groupDepth = 1) {
2786
- if (!moduleId) {
2787
- warn("Empty module_id encountered in resolveGroup");
2788
- return [null, ""];
2789
- }
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];
2795
- }
2796
- if (explicitGroup === "") {
2797
- return [null, cliDisplay.alias ?? moduleId];
3271
+ static _normalizeList(raw, label) {
3272
+ if (raw === void 0 || raw === null) return [];
3273
+ if (!Array.isArray(raw)) {
3274
+ warn(`apcli.${label} must be a list; got ${typeof raw}. Ignoring.`);
3275
+ return [];
2798
3276
  }
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];
3277
+ const out = [];
3278
+ for (const entry of raw) {
3279
+ if (typeof entry === "string" && entry.length > 0) {
3280
+ if (!APCLI_SUBCOMMAND_NAMES.has(entry)) {
3281
+ warn(
3282
+ `Unknown apcli subcommand '${entry}' in ${label} list \u2014 ignoring.`
3283
+ );
3284
+ }
3285
+ out.push(entry);
3286
+ } else {
3287
+ warn(`apcli.${label} contains non-string entry; skipping.`);
3288
+ }
2806
3289
  }
2807
- return [null, cliName];
3290
+ return out;
2808
3291
  }
3292
+ // -------------------------------------------------------------------------
3293
+ // Public API
3294
+ // -------------------------------------------------------------------------
2809
3295
  /**
2810
- * Build the group map from registry modules.
3296
+ * Resolve effective visibility mode after applying tier precedence.
3297
+ *
3298
+ * Returns one of `"all" | "none" | "include" | "exclude"` — never `"auto"`.
3299
+ *
3300
+ * Tier order (spec §4.4):
3301
+ * 1. CliConfig non-auto wins outright.
3302
+ * 2. `APCORE_CLI_APCLI` env var (unless sealed by disableEnv).
3303
+ * 3. yaml non-auto.
3304
+ * 4. Auto-detect from registryInjected.
2811
3305
  */
2812
- buildGroupMap() {
2813
- if (this.groupMapBuilt) {
2814
- return;
3306
+ resolveVisibility() {
3307
+ if (this._fromCliConfig && this._mode !== "auto") {
3308
+ return this._mode;
2815
3309
  }
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]);
2832
- } 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
- );
2844
- }
3310
+ if (!this._disableEnv) {
3311
+ const envMode = this._parseEnv(process.env.APCORE_CLI_APCLI);
3312
+ if (envMode !== null) {
3313
+ return envMode;
2845
3314
  }
2846
- this.groupMapBuilt = true;
2847
- } catch {
2848
- warn("Failed to build group map");
2849
3315
  }
3316
+ if (this._mode !== "auto") {
3317
+ return this._mode;
3318
+ }
3319
+ return this._registryInjected ? "none" : "all";
2850
3320
  }
2851
3321
  /**
2852
- * List all available command names: builtins + group names + top-level module names.
3322
+ * True iff `subcommand` passes the include/exclude filter.
3323
+ *
3324
+ * Callers MUST first check {@link resolveVisibility} — this method throws
3325
+ * under modes `"all"` or `"none"` (caller bug per spec §4.6).
2853
3326
  */
2854
- listCommands() {
2855
- this.buildGroupMap();
2856
- const groupNames = [...this.groupMap.keys()].filter(
2857
- (g) => !BUILTIN_COMMANDS.includes(g)
3327
+ isSubcommandIncluded(subcommand) {
3328
+ const mode = this.resolveVisibility();
3329
+ if (mode === "include") return this._include.includes(subcommand);
3330
+ if (mode === "exclude") return !this._exclude.includes(subcommand);
3331
+ throw new Error(
3332
+ `isSubcommandIncluded called under mode '${mode}'; caller should bypass.`
2858
3333
  );
2859
- const topNames = [...this.topLevelModules.keys()];
2860
- return [.../* @__PURE__ */ new Set([...BUILTIN_COMMANDS, ...groupNames, ...topNames])].sort();
2861
3334
  }
3335
+ /** True iff the `apcli` group itself should appear in root `--help`. */
3336
+ isGroupVisible() {
3337
+ return this.resolveVisibility() !== "none";
3338
+ }
3339
+ // -------------------------------------------------------------------------
3340
+ // Env parser (Tier 2) — co-located per spec §4.4
3341
+ // -------------------------------------------------------------------------
2862
3342
  /**
2863
- * Get a command by name: check builtins -> group cache -> group map -> top-level modules.
3343
+ * Parse APCORE_CLI_APCLI. Case-insensitive.
3344
+ *
3345
+ * - `show` / `1` / `true` → `"all"`
3346
+ * - `hide` / `0` / `false` → `"none"`
3347
+ * - Empty / unset → `null`
3348
+ * - Anything else → warn and return `null`
2864
3349
  */
2865
- getCommand(cmdName) {
2866
- this.buildGroupMap();
2867
- if (this.groupCache.has(cmdName)) {
2868
- return this.groupCache.get(cmdName).command;
3350
+ _parseEnv(raw) {
3351
+ if (raw === void 0 || raw === "") return null;
3352
+ const normalized = raw.trim().toLowerCase();
3353
+ if (normalized === "") return null;
3354
+ if (normalized === "show" || normalized === "1" || normalized === "true") {
3355
+ return "all";
2869
3356
  }
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;
3357
+ if (normalized === "hide" || normalized === "0" || normalized === "false") {
3358
+ return "none";
2879
3359
  }
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
3360
+ warn(
3361
+ `Unknown APCORE_CLI_APCLI value '${raw}', ignoring. Expected: show, hide, 1, 0, true, false.`
3362
+ );
3363
+ return null;
3364
+ }
3365
+ };
3366
+
3367
+ // src/exposure.ts
3368
+ init_esm_shims();
3369
+ init_logger();
3370
+ function escapeRegex(str) {
3371
+ return str.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
3372
+ }
3373
+ function compilePattern(pattern) {
3374
+ const sentinel = "\0GLOB\0";
3375
+ const escaped = pattern.replaceAll("**", sentinel);
3376
+ const parts = escaped.split("*");
3377
+ const regexParts = parts.map((p) => {
3378
+ const restored = p.replaceAll(sentinel, "**");
3379
+ return escapeRegex(restored);
3380
+ });
3381
+ let regex = regexParts.join("[^.]*");
3382
+ regex = regex.replaceAll("\\*\\*", ".+");
3383
+ return new RegExp(`^${regex}$`);
3384
+ }
3385
+ var ExposureFilter = class _ExposureFilter {
3386
+ static VALID_MODES = ["all", "include", "exclude", "none"];
3387
+ _mode;
3388
+ _compiledInclude;
3389
+ _compiledExclude;
3390
+ constructor(mode = "all", include, exclude) {
3391
+ if (!_ExposureFilter.VALID_MODES.includes(mode)) {
3392
+ process.stderr.write(
3393
+ `Warning: Unknown ExposureFilter mode '${mode}' \u2014 defaulting to 'none'. Valid modes: ${_ExposureFilter.VALID_MODES.join(", ")}.
3394
+ `
2890
3395
  );
2891
- this.commandCache.set(cmdName, cmd);
2892
- return cmd;
3396
+ mode = "none";
2893
3397
  }
2894
- return null;
3398
+ this._mode = mode;
3399
+ const dedup = (arr) => [...new Set(arr)];
3400
+ this._compiledInclude = dedup(include ?? []).map(compilePattern);
3401
+ this._compiledExclude = dedup(exclude ?? []).map(compilePattern);
2895
3402
  }
2896
- /** Expose groupMap for testing. */
2897
- getGroupMap() {
2898
- return this.groupMap;
3403
+ /** Return true if the module should be exposed as a CLI command. */
3404
+ isExposed(moduleId) {
3405
+ if (this._mode === "all") return true;
3406
+ if (this._mode === "include") {
3407
+ return this._compiledInclude.some((rx) => rx.test(moduleId));
3408
+ }
3409
+ if (this._mode === "exclude") {
3410
+ return !this._compiledExclude.some((rx) => rx.test(moduleId));
3411
+ }
3412
+ return false;
2899
3413
  }
2900
- /** Expose topLevelModules for testing. */
2901
- getTopLevelModules() {
2902
- return this.topLevelModules;
3414
+ /** Partition moduleIds into [exposed, hidden] lists. */
3415
+ filterModules(moduleIds) {
3416
+ const exposed = [];
3417
+ const hidden = [];
3418
+ for (const mid of moduleIds) {
3419
+ (this.isExposed(mid) ? exposed : hidden).push(mid);
3420
+ }
3421
+ return [exposed, hidden];
2903
3422
  }
2904
- /** Expose groupMapBuilt for testing. */
2905
- isGroupMapBuilt() {
2906
- return this.groupMapBuilt;
3423
+ /**
3424
+ * Create an ExposureFilter from a parsed config dict.
3425
+ *
3426
+ * Expected: `{ expose: { mode: "include", include: ["admin.*"] } }`
3427
+ */
3428
+ static fromConfig(config) {
3429
+ const expose = config.expose ?? {};
3430
+ if (typeof expose !== "object" || expose === null || Array.isArray(expose)) {
3431
+ warn("Invalid 'expose' config (expected dict), using mode: all.");
3432
+ return new _ExposureFilter();
3433
+ }
3434
+ const exposeObj = expose;
3435
+ const mode = exposeObj.mode ?? "all";
3436
+ if (!["all", "include", "exclude"].includes(mode)) {
3437
+ throw new Error(
3438
+ `Invalid expose mode: '${mode}'. Must be one of: all, include, exclude.`
3439
+ );
3440
+ }
3441
+ let include = exposeObj.include ?? [];
3442
+ if (!Array.isArray(include)) {
3443
+ warn("Invalid 'expose.include' (expected list), ignoring.");
3444
+ include = [];
3445
+ }
3446
+ let exclude = exposeObj.exclude ?? [];
3447
+ if (!Array.isArray(exclude)) {
3448
+ warn("Invalid 'expose.exclude' (expected list), ignoring.");
3449
+ exclude = [];
3450
+ }
3451
+ const filterList = (arr, label) => {
3452
+ const result = [];
3453
+ for (const p of arr) {
3454
+ if (!p) {
3455
+ warn(`Empty pattern in expose.${label}, skipping.`);
3456
+ } else {
3457
+ result.push(String(p));
3458
+ }
3459
+ }
3460
+ return result;
3461
+ };
3462
+ return new _ExposureFilter(
3463
+ mode,
3464
+ filterList(include, "include"),
3465
+ filterList(exclude, "exclude")
3466
+ );
2907
3467
  }
2908
3468
  };
2909
3469
 
2910
3470
  // src/main.ts
2911
- var __dirname3 = path6.dirname(fileURLToPath3(import.meta.url));
2912
- var verboseHelp = false;
2913
- function setVerboseHelp(verbose) {
2914
- verboseHelp = verbose;
3471
+ init_audit();
3472
+
3473
+ // src/canonical-help.ts
3474
+ init_esm_shims();
3475
+ function resolveHelpText(cmd, section) {
3476
+ const bag = cmd._helpText;
3477
+ const v = bag?.[section];
3478
+ if (typeof v === "function") return v({ error: false, command: cmd });
3479
+ return v ?? "";
2915
3480
  }
2916
- var docsUrl = null;
2917
- function setDocsUrl(url) {
2918
- docsUrl = url;
3481
+ function uppercasePlaceholders(flags) {
3482
+ return flags.replace(/<([a-zA-Z0-9_-]+)>/g, (_, name) => `<${name.toUpperCase()}>`).replace(/\[([a-zA-Z0-9_-]+)\]/g, (_, name) => `[${name.toUpperCase()}]`);
2919
3483
  }
2920
- function hasVerboseFlag() {
2921
- return process.argv.includes("--verbose");
3484
+ function optionTerm(opt) {
3485
+ const flags = uppercasePlaceholders(opt.flags);
3486
+ if (!opt.short && flags.startsWith("--")) return " " + flags;
3487
+ return flags;
2922
3488
  }
2923
- var VERSION = "0.0.0";
2924
- try {
2925
- const pkg = JSON.parse(readFileSync3(path6.resolve(__dirname3, "../package.json"), "utf-8"));
2926
- VERSION = pkg.version;
2927
- } catch {
3489
+ function optionDescription(opt) {
3490
+ let desc = opt.description;
3491
+ const d = opt.defaultValue;
3492
+ if (d !== void 0 && d !== false && d !== "" && d !== null) {
3493
+ desc = `${desc} [default: ${String(d)}]`;
3494
+ }
3495
+ return desc;
2928
3496
  }
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
- function emitErrorJson(e, exitCode) {
2952
- const err = e instanceof Error ? e : new Error(String(e));
2953
- const errRecord = err;
2954
- const code = errRecord.code ?? "UNKNOWN";
2955
- const payload = {
2956
- error: true,
2957
- code,
2958
- message: err.message,
2959
- exit_code: exitCode
2960
- };
3497
+ function reorderHelpVersionLast(opts) {
3498
+ const helpOpts = [];
3499
+ const versionOpts = [];
3500
+ const rest = [];
3501
+ for (const o of opts) {
3502
+ if (o.long === "--help") helpOpts.push(o);
3503
+ else if (o.long === "--version") versionOpts.push(o);
3504
+ else rest.push(o);
3505
+ }
3506
+ return [...rest, ...helpOpts, ...versionOpts];
3507
+ }
3508
+ function canonicalFormatHelp(cmd, helper) {
3509
+ const sections = [];
3510
+ const beforeAll = resolveHelpText(cmd, "beforeAll");
3511
+ if (beforeAll) sections.push(beforeAll);
3512
+ const desc = cmd.description();
3513
+ if (desc) sections.push(desc);
3514
+ const before = resolveHelpText(cmd, "before");
3515
+ if (before) sections.push(before);
3516
+ const visibleOpts = reorderHelpVersionLast(helper.visibleOptions(cmd));
3517
+ const visibleCmds = helper.visibleCommands(cmd);
3518
+ const args = cmd.registeredArguments ?? [];
3519
+ let usage = `Usage: ${cmd.name()}`;
3520
+ if (visibleOpts.length > 0) usage += " [OPTIONS]";
3521
+ for (const a of args) {
3522
+ const n = a.name().toUpperCase();
3523
+ usage += a.required ? ` <${n}>` : ` [${n}]`;
3524
+ }
3525
+ if (visibleCmds.length > 0) usage += " [COMMAND]";
3526
+ sections.push(usage);
3527
+ if (visibleCmds.length > 0) {
3528
+ const terms = visibleCmds.map((c) => c.name());
3529
+ const w = Math.max(...terms.map((t) => t.length));
3530
+ const lines = ["Commands:"];
3531
+ visibleCmds.forEach((sub, i) => {
3532
+ lines.push(` ${terms[i].padEnd(w)} ${sub.description()}`);
3533
+ });
3534
+ sections.push(lines.join("\n"));
3535
+ }
3536
+ if (visibleOpts.length > 0) {
3537
+ const terms = visibleOpts.map(optionTerm);
3538
+ const w = Math.max(...terms.map((t) => t.length));
3539
+ const lines = ["Options:"];
3540
+ visibleOpts.forEach((opt, i) => {
3541
+ lines.push(` ${terms[i].padEnd(w)} ${optionDescription(opt)}`);
3542
+ });
3543
+ sections.push(lines.join("\n"));
3544
+ }
3545
+ const after = resolveHelpText(cmd, "after");
3546
+ if (after) sections.push(after);
3547
+ const afterAll = resolveHelpText(cmd, "afterAll");
3548
+ if (afterAll) sections.push(afterAll);
3549
+ return sections.join("\n\n") + "\n";
3550
+ }
3551
+
3552
+ // src/validate.ts
3553
+ init_esm_shims();
3554
+ init_errors();
3555
+ var MODULE_ID_PATTERN = /^[a-z][a-z0-9_]*(\.[a-z][a-z0-9_]*)*$/;
3556
+ var MAX_MODULE_ID_LENGTH = 192;
3557
+ function validateModuleId(moduleId) {
3558
+ if (moduleId.length > MAX_MODULE_ID_LENGTH) {
3559
+ process.stderr.write(
3560
+ `Error: Invalid module ID format: '${moduleId}'. Maximum length is ${MAX_MODULE_ID_LENGTH} characters.
3561
+ `
3562
+ );
3563
+ process.exit(EXIT_CODES.INVALID_CLI_INPUT);
3564
+ }
3565
+ if (!MODULE_ID_PATTERN.test(moduleId)) {
3566
+ process.stderr.write(
3567
+ `Error: Invalid module ID format: '${moduleId}'.
3568
+ `
3569
+ );
3570
+ process.exit(EXIT_CODES.INVALID_CLI_INPUT);
3571
+ }
3572
+ }
3573
+
3574
+ // src/main.ts
3575
+ var __dirname2 = path5.dirname(fileURLToPath2(import.meta.url));
3576
+ var verboseHelp = false;
3577
+ function setVerboseHelp(verbose) {
3578
+ verboseHelp = verbose;
3579
+ }
3580
+ var docsUrl = null;
3581
+ function setDocsUrl(url) {
3582
+ docsUrl = url;
3583
+ }
3584
+ function hasVerboseFlag() {
3585
+ return process.argv.includes("--verbose");
3586
+ }
3587
+ function resolveIntOption(cliValue, envValue, defaultValue) {
3588
+ if (cliValue !== void 0 && Number.isFinite(cliValue) && cliValue > 0) {
3589
+ return cliValue;
3590
+ }
3591
+ if (envValue !== void 0 && envValue !== "") {
3592
+ const parsed = parseInt(envValue, 10);
3593
+ if (Number.isFinite(parsed) && parsed > 0) {
3594
+ return parsed;
3595
+ }
3596
+ process.stderr.write(
3597
+ `Warning: invalid integer env value '${envValue}'; using default ${defaultValue}.
3598
+ `
3599
+ );
3600
+ }
3601
+ return defaultValue;
3602
+ }
3603
+ function resolveStringOption(cliValue, envValue) {
3604
+ if (typeof cliValue === "string" && cliValue !== "") {
3605
+ return cliValue;
3606
+ }
3607
+ if (envValue !== void 0 && envValue !== "") {
3608
+ return envValue;
3609
+ }
3610
+ return void 0;
3611
+ }
3612
+ var VERSION = "0.0.0";
3613
+ try {
3614
+ const pkg = JSON.parse(readFileSync3(path5.resolve(__dirname2, "../package.json"), "utf-8"));
3615
+ VERSION = pkg.version;
3616
+ } catch {
3617
+ }
3618
+ function emitErrorJson(e, exitCode) {
3619
+ const err = e instanceof Error ? e : new Error(String(e));
3620
+ const errRecord = err;
3621
+ const code = errRecord.code ?? "UNKNOWN";
3622
+ const payload = {
3623
+ error: true,
3624
+ code,
3625
+ message: err.message,
3626
+ exit_code: exitCode
3627
+ };
2961
3628
  for (const field of ["details", "suggestion", "ai_guidance", "retryable", "user_fixable"]) {
2962
3629
  const val = errRecord[field];
2963
3630
  if (val !== void 0 && val !== null) {
@@ -3001,103 +3668,297 @@ function createCli(extensionsDirOrOpts, progName, verbose = false) {
3001
3668
  let registry;
3002
3669
  let executor;
3003
3670
  let extraCommands;
3671
+ let app;
3672
+ let expose;
3673
+ let apcliOption;
3674
+ let appVersion;
3675
+ let appDescription;
3676
+ let allowedPrefixes;
3677
+ let builtinGroupName;
3004
3678
  if (typeof extensionsDirOrOpts === "object" && extensionsDirOrOpts !== null) {
3005
3679
  extensionsDir = extensionsDirOrOpts.extensionsDir;
3006
3680
  progName = extensionsDirOrOpts.progName ?? progName;
3007
3681
  verbose = extensionsDirOrOpts.verbose ?? verbose;
3682
+ app = extensionsDirOrOpts.app;
3008
3683
  registry = extensionsDirOrOpts.registry;
3009
3684
  executor = extensionsDirOrOpts.executor;
3010
3685
  extraCommands = extensionsDirOrOpts.extraCommands;
3686
+ expose = extensionsDirOrOpts.expose;
3687
+ apcliOption = extensionsDirOrOpts.apcli;
3688
+ appVersion = extensionsDirOrOpts.version;
3689
+ appDescription = extensionsDirOrOpts.description;
3690
+ builtinGroupName = extensionsDirOrOpts.builtinGroupName;
3691
+ allowedPrefixes = extensionsDirOrOpts.allowedPrefixes;
3011
3692
  } else {
3012
3693
  extensionsDir = extensionsDirOrOpts;
3013
3694
  }
3014
3695
  verboseHelp = verbose;
3015
3696
  registerConfigNamespace();
3016
- const resolvedProgName = progName ?? path6.basename(process.argv[1] ?? "apcore-cli") ?? "apcore-cli";
3697
+ try {
3698
+ const auditLogger = new AuditLogger();
3699
+ setAuditLogger(auditLogger);
3700
+ } catch {
3701
+ }
3702
+ const resolvedProgName = progName ?? path5.basename(process.argv[1] ?? "apcore-cli") ?? "apcore-cli";
3017
3703
  const cliLogLevel = process.env.APCORE_CLI_LOGGING_LEVEL ?? process.env.APCORE_LOGGING_LEVEL ?? "WARNING";
3018
3704
  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)");
3705
+ if (app && (registry || executor)) {
3706
+ process.stderr.write("Error: app is mutually exclusive with registry/executor\n");
3707
+ process.exit(EXIT_CODES.INVALID_CLI_INPUT);
3708
+ }
3709
+ if (app) {
3710
+ registry = app.registry;
3711
+ executor = app.executor;
3712
+ }
3020
3713
  if (executor && !registry) {
3021
- throw new Error("executor requires registry \u2014 pass both or neither");
3714
+ process.stderr.write("Error: executor requires registry \u2014 pass both or neither\n");
3715
+ process.exit(EXIT_CODES.INVALID_CLI_INPUT);
3716
+ }
3717
+ if (executor && typeof executor.setApprovalHandler === "function") {
3718
+ try {
3719
+ const handler = new CliApprovalHandler(
3720
+ /*autoApprove*/
3721
+ false
3722
+ );
3723
+ executor.setApprovalHandler(handler);
3724
+ } catch {
3725
+ }
3726
+ }
3727
+ const registryInjected = registry !== void 0;
3728
+ const program = new Command5(resolvedProgName).exitOverride().helpOption("-h, --help", "Print help").addHelpCommand("help [command]", "Print this message or the help of the given subcommand(s)").description(appDescription ?? `${resolvedProgName} CLI`).option("--log-level <level>", "Logging level (DEBUG|INFO|WARNING|ERROR)", "WARNING").option("--verbose", "Show all options in help output (including built-in options)");
3729
+ if (appVersion) {
3730
+ program.version(appVersion, "-V, --version", "Print version");
3022
3731
  }
3732
+ program.configureHelp({ formatHelp: canonicalFormatHelp });
3733
+ if (!registryInjected) {
3734
+ program.option("--extensions-dir <path>", "Path to extensions directory");
3735
+ program.option("--commands-dir <path>", "Path to convention-based commands directory");
3736
+ program.option("--binding <path>", "Path to binding.yaml for display overlay");
3737
+ }
3738
+ let apcliCfg;
3739
+ try {
3740
+ if (apcliOption instanceof ApcliGroup) {
3741
+ if (builtinGroupName !== void 0 && builtinGroupName !== "apcli" && apcliOption.name !== builtinGroupName) {
3742
+ throw new Error(
3743
+ `builtinGroupName=${JSON.stringify(builtinGroupName)} conflicts with the name on the supplied ApcliGroup (${JSON.stringify(apcliOption.name)}). Pass only one.`
3744
+ );
3745
+ }
3746
+ apcliCfg = apcliOption;
3747
+ } else if (apcliOption !== void 0) {
3748
+ apcliCfg = ApcliGroup.fromCliConfig(apcliOption, {
3749
+ registryInjected,
3750
+ name: builtinGroupName
3751
+ });
3752
+ } else {
3753
+ let yamlVal = null;
3754
+ try {
3755
+ const resolver = new ConfigResolver();
3756
+ yamlVal = resolver.resolveObject("apcli");
3757
+ } catch {
3758
+ yamlVal = null;
3759
+ }
3760
+ apcliCfg = ApcliGroup.fromYaml(yamlVal, {
3761
+ registryInjected,
3762
+ name: builtinGroupName
3763
+ });
3764
+ }
3765
+ } catch (e) {
3766
+ process.stderr.write(`Error: ${e instanceof Error ? e.message : String(e)}
3767
+ `);
3768
+ process.exit(EXIT_CODES.INVALID_CLI_INPUT);
3769
+ }
3770
+ setReservedGroupNames(/* @__PURE__ */ new Set([apcliCfg.name]));
3771
+ const apcliGroup = program.command(apcliCfg.name, { hidden: !apcliCfg.isGroupVisible() }).description("Built-in commands");
3023
3772
  if (registry) {
3024
3773
  program._registry = registry;
3025
3774
  if (executor) {
3026
3775
  program._executor = executor;
3027
- registerValidateCommand(program, registry, executor);
3028
- void registerSystemCommands(program, executor);
3029
- registerPipelineCommand(program, executor);
3030
3776
  }
3031
3777
  } else {
3032
3778
  const resolvedExtDir = extensionsDir ?? process.env.APCORE_EXTENSIONS_ROOT ?? "./extensions";
3033
3779
  void resolvedExtDir;
3034
3780
  }
3781
+ let exposureFilter;
3782
+ try {
3783
+ if (expose instanceof ExposureFilter) {
3784
+ exposureFilter = expose;
3785
+ } else if (typeof expose === "object" && expose !== null) {
3786
+ exposureFilter = ExposureFilter.fromConfig({ expose });
3787
+ } else {
3788
+ exposureFilter = new ExposureFilter();
3789
+ }
3790
+ } catch (err) {
3791
+ const msg = err instanceof Error ? err.message : String(err);
3792
+ process.stderr.write(`Error: invalid 'expose' option \u2014 ${msg}
3793
+ `);
3794
+ process.exit(EXIT_CODES.INVALID_CLI_INPUT);
3795
+ }
3796
+ _registerApcliSubcommands(apcliGroup, apcliCfg, registry, executor, exposureFilter);
3035
3797
  program.addHelpText("after", [
3036
3798
  "",
3037
- "Use --help --verbose to show all options (including built-in apcore options).",
3799
+ "Use --help --verbose to show all options (including built-in options).",
3038
3800
  "Use --help --man to display a formatted man page."
3039
3801
  ].join("\n"));
3040
- registerInitCommand(program);
3041
- configureManHelp(program, resolvedProgName, VERSION);
3802
+ configureManHelp(program, resolvedProgName, appVersion ?? VERSION);
3042
3803
  if (extraCommands && extraCommands.length > 0) {
3043
- const existingNames = /* @__PURE__ */ new Set([
3044
- ...BUILTIN_COMMANDS,
3045
- ...program.commands.map((c) => c.name())
3046
- ]);
3804
+ const _reservedForExtra = /* @__PURE__ */ new Set([apcliCfg.name]);
3047
3805
  for (const cmd of extraCommands) {
3048
3806
  const cmdName = cmd.name();
3049
- if (existingNames.has(cmdName)) {
3807
+ if (_reservedForExtra.has(cmdName)) {
3050
3808
  process.stderr.write(
3051
- `Warning: Extra command '${cmdName}' collides with a built-in command and will be skipped.
3809
+ `Error: extraCommands name '${cmdName}' is reserved
3052
3810
  `
3053
3811
  );
3054
- continue;
3812
+ process.exit(EXIT_CODES.INVALID_CLI_INPUT);
3813
+ }
3814
+ const existing = program.commands.find((c) => c.name() === cmdName);
3815
+ if (existing) {
3816
+ process.stderr.write(
3817
+ `Error: extraCommands name '${cmdName}' collides with an existing command
3818
+ `
3819
+ );
3820
+ process.exit(EXIT_CODES.INVALID_CLI_INPUT);
3055
3821
  }
3056
3822
  program.addCommand(cmd);
3057
- existingNames.add(cmdName);
3058
3823
  }
3059
3824
  }
3060
3825
  program.hook("preAction", async (thisCommand) => {
3061
3826
  const opts = thisCommand.opts();
3062
3827
  const commandsDir = opts.commandsDir;
3063
3828
  const bindingPath = opts.binding;
3064
- await applyToolkitIntegration(commandsDir, bindingPath);
3829
+ await applyToolkitIntegration(commandsDir, bindingPath, { allowedPrefixes });
3065
3830
  });
3066
3831
  return program;
3067
3832
  }
3068
- async function applyToolkitIntegration(commandsDir, bindingPath) {
3833
+ var _ALWAYS_REGISTERED = /* @__PURE__ */ new Set(["exec"]);
3834
+ function _registerApcliSubcommands(apcliGroup, apcliCfg, registry, executor, exposureFilter) {
3835
+ const emitUnwiredError = () => {
3836
+ process.stderr.write(
3837
+ "Error: no apcore-js registry wired. In standalone mode, pass --extensions-dir <path> to enable module discovery.\n"
3838
+ );
3839
+ process.exit(EXIT_CODES.CONFIG_INVALID);
3840
+ };
3841
+ const effectiveRegistry = registry ?? {
3842
+ listModules: () => emitUnwiredError(),
3843
+ getModule: () => emitUnwiredError()
3844
+ };
3845
+ const TABLE = [
3846
+ { name: "list", requiresExecutor: false, register: (g) => registerListCommand(g, effectiveRegistry, exposureFilter) },
3847
+ { name: "describe", requiresExecutor: false, register: (g) => registerDescribeCommand(g, effectiveRegistry) },
3848
+ { name: "exec", requiresExecutor: true, register: (g, _r, ex) => registerExecCommand(g, effectiveRegistry, ex) },
3849
+ { name: "validate", requiresExecutor: true, register: (g, _r, ex) => registerValidateCommand(g, effectiveRegistry, ex) },
3850
+ { name: "init", requiresExecutor: false, register: (g) => registerInitCommand(g) },
3851
+ { name: "health", requiresExecutor: true, register: (g, _r, ex) => registerHealthCommand(g, ex) },
3852
+ { name: "usage", requiresExecutor: true, register: (g, _r, ex) => registerUsageCommand(g, ex) },
3853
+ { name: "enable", requiresExecutor: true, register: (g, _r, ex) => registerEnableCommand(g, ex) },
3854
+ { name: "disable", requiresExecutor: true, register: (g, _r, ex) => registerDisableCommand(g, ex) },
3855
+ { name: "reload", requiresExecutor: true, register: (g, _r, ex) => registerReloadCommand(g, ex) },
3856
+ { name: "config", requiresExecutor: true, register: (g, _r, ex) => registerConfigCommand(g, ex) },
3857
+ { name: "completion", requiresExecutor: false, register: (g) => registerCompletionCommand(g) },
3858
+ { name: "describe-pipeline", requiresExecutor: true, register: (g, _r, ex) => registerPipelineCommand(g, ex) }
3859
+ ];
3860
+ const mode = apcliCfg.resolveVisibility();
3861
+ for (const entry of TABLE) {
3862
+ let shouldRegister;
3863
+ if (mode === "all" || mode === "none") {
3864
+ shouldRegister = true;
3865
+ } else {
3866
+ shouldRegister = _ALWAYS_REGISTERED.has(entry.name) || apcliCfg.isSubcommandIncluded(entry.name);
3867
+ }
3868
+ if (!shouldRegister) continue;
3869
+ if (entry.requiresExecutor && !executor) {
3870
+ if (_ALWAYS_REGISTERED.has(entry.name)) {
3871
+ warn(
3872
+ `apcli.${entry.name} is in _ALWAYS_REGISTERED but no executor is wired \u2014 subcommand unavailable. Pass executor to createCli() or avoid ${entry.name} invocations.`
3873
+ );
3874
+ }
3875
+ continue;
3876
+ }
3877
+ entry.register(apcliGroup, registry, executor);
3878
+ }
3879
+ }
3880
+ var bindingDisplayMap = /* @__PURE__ */ new Map();
3881
+ function lookupBindingDisplay(moduleId) {
3882
+ return bindingDisplayMap.get(moduleId);
3883
+ }
3884
+ async function applyToolkitIntegration(commandsDir, bindingPath, options = {}) {
3069
3885
  if (!commandsDir && !bindingPath) {
3070
3886
  return;
3071
3887
  }
3888
+ let toolkit;
3072
3889
  try {
3073
3890
  const toolkitModule = "apcore-toolkit";
3074
- const toolkit = await import(
3891
+ toolkit = await import(
3075
3892
  /* @vite-ignore */
3076
3893
  toolkitModule
3077
3894
  );
3078
- if (commandsDir) {
3079
- console.warn("Convention scanning not yet available in TypeScript toolkit");
3895
+ } catch {
3896
+ warn("apcore-toolkit not installed \u2014 toolkit features unavailable");
3897
+ return;
3898
+ }
3899
+ if (commandsDir) {
3900
+ warn("Convention scanning not available in the TypeScript toolkit");
3901
+ }
3902
+ if (bindingPath) {
3903
+ try {
3904
+ await loadBindingDisplayOverlay(toolkit, bindingPath, options.allowedPrefixes);
3905
+ } catch (err) {
3906
+ const msg = err instanceof Error ? err.message : String(err);
3907
+ warn(`apcore-toolkit: failed to load binding '${bindingPath}': ${msg}`);
3908
+ }
3909
+ }
3910
+ }
3911
+ async function loadBindingDisplayOverlay(toolkit, bindingPath, allowedPrefixes) {
3912
+ const BindingLoaderCtor = toolkit.BindingLoader;
3913
+ const DisplayResolverCtor = toolkit.DisplayResolver;
3914
+ if (!BindingLoaderCtor || !DisplayResolverCtor) {
3915
+ return;
3916
+ }
3917
+ const loader = new BindingLoaderCtor();
3918
+ const scanned = loader.load(bindingPath);
3919
+ const resolver = new DisplayResolverCtor();
3920
+ const resolved = resolver.resolve(scanned, { bindingPath });
3921
+ const prefixes = allowedPrefixes && allowedPrefixes.length > 0 ? allowedPrefixes : null;
3922
+ const isTargetAllowed = (target) => {
3923
+ if (!prefixes) return true;
3924
+ if (typeof target !== "string" || target.length === 0) return true;
3925
+ return prefixes.some((p) => target.startsWith(p));
3926
+ };
3927
+ for (const mod of resolved) {
3928
+ if (!mod || typeof mod !== "object") continue;
3929
+ const entry = mod;
3930
+ const id = typeof entry.moduleId === "string" ? entry.moduleId : null;
3931
+ if (!id) continue;
3932
+ if (!isTargetAllowed(entry.target)) {
3933
+ warn(
3934
+ `apcore-toolkit: dropped binding entry '${id}' \u2014 target '${String(entry.target)}' is outside allowedPrefixes`
3935
+ );
3936
+ continue;
3080
3937
  }
3081
- if (bindingPath) {
3082
- const resolver = new toolkit.DisplayResolver();
3083
- void resolver;
3938
+ const meta = entry.metadata ?? {};
3939
+ const display = meta.display;
3940
+ if (display && typeof display === "object" && !Array.isArray(display)) {
3941
+ bindingDisplayMap.set(id, display);
3084
3942
  }
3085
- } catch {
3086
- console.warn("apcore-toolkit not installed \u2014 toolkit features unavailable");
3087
3943
  }
3088
3944
  }
3089
3945
  function main(progName) {
3090
3946
  verboseHelp = hasVerboseFlag();
3091
- const program = createCli(void 0, progName, verboseHelp);
3947
+ const program = createCli({
3948
+ progName,
3949
+ verbose: verboseHelp,
3950
+ version: VERSION,
3951
+ description: `${progName ?? "apcore-cli"} \u2014 execute apcore modules from the command line`
3952
+ });
3092
3953
  try {
3093
3954
  program.parse(process.argv);
3094
- } catch (error2) {
3095
- if (error2 instanceof CommanderError) {
3096
- process.exit(error2.exitCode);
3955
+ } catch (error) {
3956
+ if (error instanceof CommanderError) {
3957
+ process.exit(error.exitCode);
3097
3958
  }
3098
- const code = exitCodeForError(error2);
3099
- if (error2 instanceof Error) {
3100
- process.stderr.write(`Error: ${error2.message}
3959
+ const code = exitCodeForError(error);
3960
+ if (error instanceof Error) {
3961
+ process.stderr.write(`Error: ${error.message}
3101
3962
  `);
3102
3963
  }
3103
3964
  process.exit(code);
@@ -3120,7 +3981,7 @@ function buildModuleCommand(moduleDef, executor, helpTextMaxLength = 1e3, cmdNam
3120
3981
  }
3121
3982
  schemaOptions = schemaToCliOptions(resolvedSchema, helpTextMaxLength);
3122
3983
  }
3123
- const cmd = new Command6(effectiveCmdName).description(cmdHelp);
3984
+ const cmd = new Command5(effectiveCmdName).description(cmdHelp);
3124
3985
  const inputOpt = new Option4("--input <source>", "Read JSON input from a file path, or use '-' to read from stdin pipe");
3125
3986
  const yesOpt = new Option4("-y, --yes", "Skip interactive approval prompts (for scripts and CI)").default(false);
3126
3987
  const largeInputOpt = new Option4("--large-input", "Allow stdin input larger than 10MB (default limit protects against accidental pipes)").default(false);
@@ -3168,30 +4029,6 @@ function buildModuleCommand(moduleDef, executor, helpTextMaxLength = 1e3, cmdNam
3168
4029
  if (footerParts.length > 0) {
3169
4030
  cmd.addHelpText("after", "\n" + footerParts.join("\n") + "\n");
3170
4031
  }
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
4032
  for (const opt of schemaOptions) {
3196
4033
  if (opt.parseArg) {
3197
4034
  cmd.option(opt.flags, opt.description, opt.parseArg, opt.defaultValue);
@@ -3209,8 +4046,12 @@ function buildModuleCommand(moduleDef, executor, helpTextMaxLength = 1e3, cmdNam
3209
4046
  const dryRun = options.dryRun;
3210
4047
  const traceFlag = options.trace;
3211
4048
  const streamFlag = options.stream;
3212
- const strategyName = options.strategy;
3213
- const approvalTimeout = options.approvalTimeout ?? 60;
4049
+ const strategyName = resolveStringOption(options.strategy, process.env.APCORE_CLI_STRATEGY);
4050
+ const approvalTimeout = resolveIntOption(
4051
+ options.approvalTimeout,
4052
+ process.env.APCORE_CLI_APPROVAL_TIMEOUT,
4053
+ 60
4054
+ );
3214
4055
  const approvalToken = options.approvalToken;
3215
4056
  const schemaKwargs = {};
3216
4057
  const builtinKeys = /* @__PURE__ */ new Set([
@@ -3234,6 +4075,7 @@ function buildModuleCommand(moduleDef, executor, helpTextMaxLength = 1e3, cmdNam
3234
4075
  }
3235
4076
  }
3236
4077
  let merged = {};
4078
+ const startTime = performance.now();
3237
4079
  try {
3238
4080
  merged = await collectInput(stdinFlag, schemaKwargs, largeInput);
3239
4081
  const reconverted = reconvertEnumValues(merged, schemaOptions);
@@ -3241,7 +4083,7 @@ function buildModuleCommand(moduleDef, executor, helpTextMaxLength = 1e3, cmdNam
3241
4083
  if (dryRun) {
3242
4084
  if (!executor.validate) {
3243
4085
  process.stderr.write("Error: Executor does not support validate.\n");
3244
- process.exit(1);
4086
+ process.exit(EXIT_CODES.MODULE_EXECUTE_ERROR);
3245
4087
  }
3246
4088
  const preflight = await executor.validate(moduleId, merged);
3247
4089
  formatPreflightResult(preflight, outputFormat);
@@ -3283,7 +4125,6 @@ function buildModuleCommand(moduleDef, executor, helpTextMaxLength = 1e3, cmdNam
3283
4125
  merged._approval_token = approvalToken;
3284
4126
  }
3285
4127
  await checkApproval(moduleDef, autoApprove, approvalTimeout);
3286
- const startTime = performance.now();
3287
4128
  if (streamFlag) {
3288
4129
  if (resolveFormat(outputFormat) === "table") {
3289
4130
  process.stderr.write("Warning: Streaming mode always outputs JSONL; --format table is ignored.\n");
@@ -3324,22 +4165,17 @@ function buildModuleCommand(moduleDef, executor, helpTextMaxLength = 1e3, cmdNam
3324
4165
  strategyName ? { strategy: strategyName } : void 0
3325
4166
  );
3326
4167
  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
4168
  const resolved = resolveFormat(outputFormat);
3333
4169
  if (resolved === "json" || !process.stdout.isTTY) {
3334
4170
  const traceData = {
3335
- strategy: trace.strategy_name,
3336
- total_duration_ms: trace.total_duration_ms,
4171
+ strategy: trace.strategyName,
4172
+ total_duration_ms: trace.totalDurationMs,
3337
4173
  success: trace.success,
3338
4174
  steps: trace.steps.map((s) => ({
3339
4175
  name: s.name,
3340
- duration_ms: s.duration_ms,
4176
+ duration_ms: s.durationMs,
3341
4177
  skipped: s.skipped,
3342
- ...s.skipped ? { skip_reason: s.skip_reason } : {}
4178
+ ...s.skipped ? { skip_reason: s.skipReason ?? null } : {}
3343
4179
  }))
3344
4180
  };
3345
4181
  let output;
@@ -3354,20 +4190,25 @@ function buildModuleCommand(moduleDef, executor, helpTextMaxLength = 1e3, cmdNam
3354
4190
  const stepCount = trace.steps.length;
3355
4191
  process.stderr.write(
3356
4192
  `
3357
- Pipeline Trace (strategy: ${trace.strategy_name}, ${stepCount} steps, ${trace.total_duration_ms.toFixed(1)}ms)
4193
+ Pipeline Trace (strategy: ${trace.strategyName}, ${stepCount} steps, ${trace.totalDurationMs.toFixed(1)}ms)
3358
4194
  `
3359
4195
  );
3360
4196
  for (const s of trace.steps) {
3361
4197
  if (s.skipped) {
3362
- const reason = s.skip_reason ?? "n/a";
4198
+ const reason = s.skipReason ?? "n/a";
3363
4199
  process.stderr.write(` \u25CB ${s.name.padEnd(24)} ${"\u2014".padStart(8)} skipped (${reason})
3364
4200
  `);
3365
4201
  } else {
3366
- process.stderr.write(` \u2713 ${s.name.padEnd(24)} ${(s.duration_ms.toFixed(1) + "ms").padStart(8)}
4202
+ process.stderr.write(` \u2713 ${s.name.padEnd(24)} ${(s.durationMs.toFixed(1) + "ms").padStart(8)}
3367
4203
  `);
3368
4204
  }
3369
4205
  }
3370
4206
  }
4207
+ const { getAuditLogger: getAL2 } = await Promise.resolve().then(() => (init_audit(), audit_exports));
4208
+ const al2 = getAL2();
4209
+ if (al2) {
4210
+ al2.logExecution(moduleId, merged, "success", 0, durationMs2);
4211
+ }
3371
4212
  return;
3372
4213
  }
3373
4214
  let result;
@@ -3388,21 +4229,20 @@ Pipeline Trace (strategy: ${trace.strategy_name}, ${stepCount} steps, ${trace.to
3388
4229
  result = await sandbox.execute(moduleId, merged, executor);
3389
4230
  }
3390
4231
  const durationMs = Math.round(performance.now() - startTime);
4232
+ formatExecResult(result, outputFormat, outputFields);
3391
4233
  const { getAuditLogger: getAuditLogger2 } = await Promise.resolve().then(() => (init_audit(), audit_exports));
3392
4234
  const auditLogger = getAuditLogger2();
3393
4235
  if (auditLogger) {
3394
4236
  auditLogger.logExecution(moduleId, merged, "success", 0, durationMs);
3395
4237
  }
3396
- formatExecResult(result, outputFormat, outputFields);
3397
4238
  } 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);
4239
+ const exitCode = exitCodeForError(err);
4240
+ const durationMs = Math.round(performance.now() - startTime);
3401
4241
  try {
3402
4242
  const { getAuditLogger: getAuditLogger2 } = await Promise.resolve().then(() => (init_audit(), audit_exports));
3403
4243
  const auditLogger = getAuditLogger2();
3404
4244
  if (auditLogger) {
3405
- auditLogger.logExecution(moduleId, merged, "error", exitCode, 0);
4245
+ auditLogger.logExecution(moduleId, merged, "error", exitCode, durationMs);
3406
4246
  }
3407
4247
  } catch {
3408
4248
  }
@@ -3416,22 +4256,6 @@ Pipeline Trace (strategy: ${trace.strategy_name}, ${stepCount} steps, ${trace.to
3416
4256
  });
3417
4257
  return cmd;
3418
4258
  }
3419
- function validateModuleId(moduleId) {
3420
- if (moduleId.length > 128) {
3421
- process.stderr.write(
3422
- `Error: Invalid module ID format: '${moduleId}'. Maximum length is 128 characters.
3423
- `
3424
- );
3425
- process.exit(EXIT_CODES.INVALID_CLI_INPUT);
3426
- }
3427
- if (!/^[a-z][a-z0-9_]*(\.[a-z][a-z0-9_]*)*$/.test(moduleId)) {
3428
- process.stderr.write(
3429
- `Error: Invalid module ID format: '${moduleId}'.
3430
- `
3431
- );
3432
- process.exit(EXIT_CODES.INVALID_CLI_INPUT);
3433
- }
3434
- }
3435
4259
  async function collectInput(stdinFlag, cliKwargs = {}, largeInput) {
3436
4260
  const cliKwargsNonNull = {};
3437
4261
  for (const [k, v] of Object.entries(cliKwargs)) {
@@ -3442,45 +4266,57 @@ async function collectInput(stdinFlag, cliKwargs = {}, largeInput) {
3442
4266
  if (!stdinFlag) {
3443
4267
  return cliKwargsNonNull;
3444
4268
  }
4269
+ let raw;
4270
+ let source;
3445
4271
  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;
4272
+ raw = await readStdin();
4273
+ source = "STDIN";
4274
+ } else {
4275
+ source = `file '${stdinFlag}'`;
3458
4276
  try {
3459
- stdinData = JSON.parse(raw);
3460
- } catch {
3461
- process.stderr.write(
3462
- "Error: STDIN does not contain valid JSON.\n"
3463
- );
4277
+ raw = readFileSync3(stdinFlag, "utf-8");
4278
+ } catch (err) {
4279
+ const msg = err instanceof Error ? err.message : String(err);
4280
+ process.stderr.write(`Error: Could not read input ${source}: ${msg}
4281
+ `);
3464
4282
  process.exit(EXIT_CODES.INVALID_CLI_INPUT);
3465
4283
  }
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}.
4284
+ }
4285
+ const rawSize = Buffer.byteLength(raw, "utf-8");
4286
+ if (rawSize > 10485760 && !largeInput) {
4287
+ process.stderr.write(
4288
+ `Error: ${source} input exceeds 10MB limit. Use --large-input to override.
3469
4289
  `
3470
- );
3471
- process.exit(EXIT_CODES.INVALID_CLI_INPUT);
3472
- }
3473
- return { ...stdinData, ...cliKwargsNonNull };
4290
+ );
4291
+ process.exit(EXIT_CODES.INVALID_CLI_INPUT);
3474
4292
  }
3475
- return cliKwargsNonNull;
4293
+ if (!raw) {
4294
+ return cliKwargsNonNull;
4295
+ }
4296
+ let parsed;
4297
+ try {
4298
+ parsed = JSON.parse(raw);
4299
+ } catch {
4300
+ process.stderr.write(`Error: ${source} does not contain valid JSON.
4301
+ `);
4302
+ process.exit(EXIT_CODES.INVALID_CLI_INPUT);
4303
+ }
4304
+ if (typeof parsed !== "object" || parsed === null || Array.isArray(parsed)) {
4305
+ process.stderr.write(
4306
+ `Error: ${source} JSON must be an object, got ${Array.isArray(parsed) ? "array" : typeof parsed}.
4307
+ `
4308
+ );
4309
+ process.exit(EXIT_CODES.INVALID_CLI_INPUT);
4310
+ }
4311
+ return { ...parsed, ...cliKwargsNonNull };
3476
4312
  }
3477
4313
  function readStdin() {
3478
- return new Promise((resolve3, reject) => {
4314
+ return new Promise((resolve2, reject) => {
3479
4315
  const chunks = [];
3480
4316
  const onData = (chunk) => chunks.push(chunk);
3481
4317
  const onEnd = () => {
3482
4318
  cleanup();
3483
- resolve3(Buffer.concat(chunks).toString("utf-8"));
4319
+ resolve2(Buffer.concat(chunks).toString("utf-8"));
3484
4320
  };
3485
4321
  const onError = (err) => {
3486
4322
  cleanup();
@@ -3518,62 +4354,375 @@ function reconvertEnumValues(kwargs, options) {
3518
4354
  return result;
3519
4355
  }
3520
4356
 
4357
+ // src/cli.ts
4358
+ init_esm_shims();
4359
+ import { Command as Command6 } from "commander";
4360
+ init_logger();
4361
+ init_errors();
4362
+ function assertNotReserved(kind, name, moduleId) {
4363
+ if (!getReservedGroupNames().has(name)) return;
4364
+ let msg;
4365
+ if (kind === "group") {
4366
+ msg = `Error: Module '${moduleId}': display.cli.group '${name}' is reserved. Use a different CLI alias or set display.cli.group to another value.
4367
+ `;
4368
+ } else if (kind === "auto-group") {
4369
+ msg = `Error: Module '${moduleId}': auto-group '${name}' is reserved. Rename the module id or set display.cli.group to another value.
4370
+ `;
4371
+ } else {
4372
+ msg = `Error: Module '${moduleId}': top-level CLI name '${name}' is reserved. Use a different CLI alias.
4373
+ `;
4374
+ }
4375
+ process.stderr.write(msg);
4376
+ process.exit(EXIT_CODES.INVALID_CLI_INPUT);
4377
+ }
4378
+ var LazyModuleGroup = class {
4379
+ registry;
4380
+ executor;
4381
+ helpTextMaxLength;
4382
+ commandCache = /* @__PURE__ */ new Map();
4383
+ /** alias -> canonical module_id (populated lazily) */
4384
+ aliasMap = /* @__PURE__ */ new Map();
4385
+ /** module_id -> descriptor cache (populated during alias map build) */
4386
+ descriptorCache = /* @__PURE__ */ new Map();
4387
+ aliasMapBuilt = false;
4388
+ constructor(registry, executor, helpTextMaxLength = 1e3) {
4389
+ this.registry = registry;
4390
+ this.executor = executor;
4391
+ this.helpTextMaxLength = helpTextMaxLength;
4392
+ }
4393
+ /**
4394
+ * Build alias->module_id map from display overlay metadata.
4395
+ */
4396
+ buildAliasMap() {
4397
+ if (this.aliasMapBuilt) {
4398
+ return;
4399
+ }
4400
+ try {
4401
+ for (const descriptor of this.registry.listModules()) {
4402
+ const moduleId = descriptor.id;
4403
+ this.descriptorCache.set(moduleId, descriptor);
4404
+ const display = getDisplay(descriptor);
4405
+ const cliDisplay = display.cli && typeof display.cli === "object" && !Array.isArray(display.cli) ? display.cli : {};
4406
+ const cliAlias = cliDisplay.alias;
4407
+ if (cliAlias && cliAlias !== moduleId) {
4408
+ this.aliasMap.set(cliAlias, moduleId);
4409
+ }
4410
+ }
4411
+ this.aliasMapBuilt = true;
4412
+ } catch {
4413
+ warn("Failed to build alias map from registry");
4414
+ }
4415
+ }
4416
+ /**
4417
+ * List all available command names from the Registry.
4418
+ */
4419
+ listCommands() {
4420
+ this.buildAliasMap();
4421
+ const reverse = /* @__PURE__ */ new Map();
4422
+ for (const [alias, moduleId] of this.aliasMap) {
4423
+ reverse.set(moduleId, alias);
4424
+ }
4425
+ const moduleIds = this.registry.listModules().map((m) => m.id);
4426
+ const names = moduleIds.map((mid) => reverse.get(mid) ?? mid);
4427
+ return [...new Set(names)].sort();
4428
+ }
4429
+ /**
4430
+ * Get or lazily build a Commander Command for the given module.
4431
+ */
4432
+ getCommand(cmdName) {
4433
+ if (this.commandCache.has(cmdName)) {
4434
+ return this.commandCache.get(cmdName);
4435
+ }
4436
+ this.buildAliasMap();
4437
+ const moduleId = this.aliasMap.get(cmdName) ?? cmdName;
4438
+ let moduleDef = this.descriptorCache.get(moduleId);
4439
+ if (!moduleDef) {
4440
+ moduleDef = this.registry.getModule(moduleId) ?? void 0;
4441
+ }
4442
+ if (!moduleDef) {
4443
+ return null;
4444
+ }
4445
+ const cmd = buildModuleCommand(moduleDef, this.executor, this.helpTextMaxLength, cmdName);
4446
+ this.commandCache.set(cmdName, cmd);
4447
+ return cmd;
4448
+ }
4449
+ };
4450
+ var LazyGroup = class {
4451
+ members;
4452
+ _executor;
4453
+ _helpTextMaxLength;
4454
+ _cmdCache = /* @__PURE__ */ new Map();
4455
+ command;
4456
+ constructor(members, executor, name, helpTextMaxLength = 1e3) {
4457
+ this.members = members;
4458
+ this._executor = executor;
4459
+ this._helpTextMaxLength = helpTextMaxLength;
4460
+ this.command = new Command6(name).description(`${name} commands`);
4461
+ for (const [cmdName, [, descriptor]] of this.members) {
4462
+ const cmd = buildModuleCommand(
4463
+ descriptor,
4464
+ this._executor,
4465
+ this._helpTextMaxLength,
4466
+ cmdName
4467
+ );
4468
+ this._cmdCache.set(cmdName, cmd);
4469
+ this.command.addCommand(cmd);
4470
+ }
4471
+ }
4472
+ listCommands() {
4473
+ return [...this.members.keys()].sort();
4474
+ }
4475
+ getCommand(cmdName) {
4476
+ if (this._cmdCache.has(cmdName)) {
4477
+ return this._cmdCache.get(cmdName);
4478
+ }
4479
+ const entry = this.members.get(cmdName);
4480
+ if (!entry) {
4481
+ return null;
4482
+ }
4483
+ const [, descriptor] = entry;
4484
+ const cmd = buildModuleCommand(
4485
+ descriptor,
4486
+ this._executor,
4487
+ this._helpTextMaxLength,
4488
+ cmdName
4489
+ );
4490
+ this._cmdCache.set(cmdName, cmd);
4491
+ return cmd;
4492
+ }
4493
+ };
4494
+ var GroupedModuleGroup = class _GroupedModuleGroup extends LazyModuleGroup {
4495
+ /** groupName -> { cmdName -> [moduleId, descriptor] } */
4496
+ groupMap = /* @__PURE__ */ new Map();
4497
+ /** cmdName -> [moduleId, descriptor] for top-level (ungrouped) modules */
4498
+ topLevelModules = /* @__PURE__ */ new Map();
4499
+ /** Cached LazyGroup instances */
4500
+ groupCache = /* @__PURE__ */ new Map();
4501
+ groupMapBuilt = false;
4502
+ /** Exposure filter (FE-12) — controls which modules appear as CLI commands */
4503
+ exposureFilter;
4504
+ /** Effective group depth (CLAUDE.md v0.6.0): constructor arg > APCORE_CLI_GROUP_DEPTH env > 1. */
4505
+ groupDepth;
4506
+ constructor(registry, executor, helpTextMaxLength = 1e3, exposureFilter, groupDepth) {
4507
+ super(registry, executor, helpTextMaxLength);
4508
+ this.exposureFilter = exposureFilter ?? new ExposureFilter();
4509
+ this.groupDepth = _GroupedModuleGroup.resolveGroupDepth(groupDepth);
4510
+ }
4511
+ /**
4512
+ * Resolve group depth from constructor arg > APCORE_CLI_GROUP_DEPTH env > default 1.
4513
+ * Invalid env values (non-integer, non-positive) fall through to the default.
4514
+ */
4515
+ static resolveGroupDepth(explicit) {
4516
+ if (explicit !== void 0 && Number.isFinite(explicit) && explicit > 0) {
4517
+ return Math.floor(explicit);
4518
+ }
4519
+ const raw = process.env.APCORE_CLI_GROUP_DEPTH;
4520
+ if (raw !== void 0 && raw !== "") {
4521
+ const parsed = parseInt(raw, 10);
4522
+ if (Number.isFinite(parsed) && parsed > 0) {
4523
+ return parsed;
4524
+ }
4525
+ }
4526
+ return 1;
4527
+ }
4528
+ /**
4529
+ * Determine (groupName | null, commandName) for a module from its display overlay.
4530
+ *
4531
+ * @param groupDepth Number of dotted segments to consume as the group prefix.
4532
+ * Defaults to 1 (e.g., "math.add" → group="math", cmd="add").
4533
+ * Set to 2 for multi-level grouping (e.g., "math.trig.sin" →
4534
+ * group="math.trig", cmd="sin").
4535
+ */
4536
+ static resolveGroup(moduleId, descriptor, groupDepth = 1) {
4537
+ if (!moduleId) {
4538
+ warn("Empty module_id encountered in resolveGroup");
4539
+ return [null, ""];
4540
+ }
4541
+ const display = getDisplay(descriptor);
4542
+ const cliDisplay = display.cli && typeof display.cli === "object" && !Array.isArray(display.cli) ? display.cli : {};
4543
+ const explicitGroup = cliDisplay.group;
4544
+ if (typeof explicitGroup === "string" && explicitGroup !== "") {
4545
+ return [explicitGroup, cliDisplay.alias ?? moduleId];
4546
+ }
4547
+ if (explicitGroup === "") {
4548
+ return [null, cliDisplay.alias ?? moduleId];
4549
+ }
4550
+ const cliName = cliDisplay.alias ?? moduleId;
4551
+ if (cliName.includes(".")) {
4552
+ const parts = cliName.split(".");
4553
+ const depth = Math.max(1, Math.min(groupDepth, parts.length - 1));
4554
+ const group = parts.slice(0, depth).join(".");
4555
+ const cmd = parts.slice(depth).join(".");
4556
+ return [group, cmd];
4557
+ }
4558
+ return [null, cliName];
4559
+ }
4560
+ /**
4561
+ * Build the group map from registry modules.
4562
+ *
4563
+ * FE-13: hard-fails with exit 2 when a module resolves to the reserved
4564
+ * `apcli` namespace in any of three ways — explicit `display.cli.group`,
4565
+ * auto-grouped dotted prefix, or top-level alias/id. See spec §4.10.
4566
+ */
4567
+ buildGroupMap() {
4568
+ if (this.groupMapBuilt) {
4569
+ return;
4570
+ }
4571
+ this.buildAliasMap();
4572
+ for (const descriptor of this.registry.listModules()) {
4573
+ const moduleId = descriptor.id;
4574
+ const cached = this.descriptorCache.get(moduleId);
4575
+ if (!cached) {
4576
+ continue;
4577
+ }
4578
+ if (!this.exposureFilter.isExposed(moduleId)) {
4579
+ continue;
4580
+ }
4581
+ const display = getDisplay(cached);
4582
+ const cliDisplay = display.cli && typeof display.cli === "object" && !Array.isArray(display.cli) ? display.cli : {};
4583
+ const explicitGroup = typeof cliDisplay.group === "string" && cliDisplay.group !== "" ? cliDisplay.group : void 0;
4584
+ if (explicitGroup !== void 0) {
4585
+ assertNotReserved("group", explicitGroup, moduleId);
4586
+ }
4587
+ const [group, cmd] = _GroupedModuleGroup.resolveGroup(moduleId, cached, this.groupDepth);
4588
+ if (group !== null && explicitGroup === void 0) {
4589
+ assertNotReserved("auto-group", group, moduleId);
4590
+ }
4591
+ if (group === null) {
4592
+ assertNotReserved("top-level", cmd, moduleId);
4593
+ this.topLevelModules.set(cmd, [moduleId, cached]);
4594
+ } else if (!/^[a-z][a-z0-9_-]*(\.[a-z][a-z0-9_-]*)*$/.test(group)) {
4595
+ warn(
4596
+ `Module '${moduleId}': group name '${group}' is not shell-safe \u2014 treating as top-level.`
4597
+ );
4598
+ this.topLevelModules.set(cmd, [moduleId, cached]);
4599
+ } else {
4600
+ if (!this.groupMap.has(group)) {
4601
+ this.groupMap.set(group, /* @__PURE__ */ new Map());
4602
+ }
4603
+ this.groupMap.get(group).set(cmd, [moduleId, cached]);
4604
+ }
4605
+ }
4606
+ this.groupMapBuilt = true;
4607
+ }
4608
+ /**
4609
+ * List all available command names: group names + top-level module names.
4610
+ *
4611
+ * FE-13: the built-in subcommand list is no longer folded in here — those
4612
+ * commands live under the `apcli` prefix and are registered directly by
4613
+ * `createCli`.
4614
+ */
4615
+ listCommands() {
4616
+ this.buildGroupMap();
4617
+ const reserved = getReservedGroupNames();
4618
+ const groupNames = [...this.groupMap.keys()].filter(
4619
+ (g) => !reserved.has(g)
4620
+ );
4621
+ const topNames = [...this.topLevelModules.keys()];
4622
+ return [.../* @__PURE__ */ new Set([...groupNames, ...topNames])].sort();
4623
+ }
4624
+ /**
4625
+ * Get a command by name: check builtins -> group cache -> group map -> top-level modules.
4626
+ */
4627
+ getCommand(cmdName) {
4628
+ this.buildGroupMap();
4629
+ if (this.groupCache.has(cmdName)) {
4630
+ return this.groupCache.get(cmdName).command;
4631
+ }
4632
+ if (this.groupMap.has(cmdName)) {
4633
+ const lazyGrp = new LazyGroup(
4634
+ this.groupMap.get(cmdName),
4635
+ this.executor,
4636
+ cmdName,
4637
+ this.helpTextMaxLength
4638
+ );
4639
+ this.groupCache.set(cmdName, lazyGrp);
4640
+ return lazyGrp.command;
4641
+ }
4642
+ if (this.topLevelModules.has(cmdName)) {
4643
+ if (this.commandCache.has(cmdName)) {
4644
+ return this.commandCache.get(cmdName);
4645
+ }
4646
+ const [, descriptor] = this.topLevelModules.get(cmdName);
4647
+ const cmd = buildModuleCommand(
4648
+ descriptor,
4649
+ this.executor,
4650
+ this.helpTextMaxLength,
4651
+ cmdName
4652
+ );
4653
+ this.commandCache.set(cmdName, cmd);
4654
+ return cmd;
4655
+ }
4656
+ return null;
4657
+ }
4658
+ /** Expose groupMap for testing. */
4659
+ getGroupMap() {
4660
+ return this.groupMap;
4661
+ }
4662
+ /** Expose topLevelModules for testing. */
4663
+ getTopLevelModules() {
4664
+ return this.topLevelModules;
4665
+ }
4666
+ /** Expose groupMapBuilt for testing. */
4667
+ isGroupMapBuilt() {
4668
+ return this.groupMapBuilt;
4669
+ }
4670
+ };
4671
+
3521
4672
  // src/index.ts
3522
4673
  init_errors();
4674
+ init_logger();
3523
4675
  init_security();
3524
4676
  export {
4677
+ ApcliGroup,
4678
+ ApcliGroupError,
3525
4679
  ApprovalDeniedError,
3526
4680
  ApprovalTimeoutError,
3527
4681
  AuditLogger,
3528
4682
  AuthProvider,
3529
4683
  AuthenticationError,
3530
- BUILTIN_COMMANDS,
3531
4684
  CliApprovalHandler,
3532
4685
  ConfigDecryptionError,
3533
4686
  ConfigEncryptor,
3534
4687
  ConfigResolver,
3535
4688
  DEFAULTS,
3536
4689
  EXIT_CODES,
4690
+ ExposureFilter,
3537
4691
  GroupedModuleGroup,
3538
4692
  LazyGroup,
3539
4693
  LazyModuleGroup,
3540
4694
  ModuleExecutionError,
3541
4695
  ModuleNotFoundError,
4696
+ RESERVED_GROUP_NAMES,
3542
4697
  Sandbox,
3543
4698
  SchemaValidationError,
3544
4699
  applyToolkitIntegration,
3545
4700
  buildModuleCommand,
3546
- buildProgramManPage,
3547
4701
  checkApproval,
3548
4702
  collectInput,
3549
4703
  configureManHelp,
3550
4704
  createCli,
3551
- debug,
3552
- docsUrl,
3553
- emitErrorJson,
3554
- emitErrorTty,
3555
- error,
3556
4705
  exitCodeForError,
3557
- extractHelp,
3558
- firstFailedExitCode,
3559
4706
  formatExecResult,
3560
4707
  formatModuleDetail,
3561
4708
  formatModuleList,
3562
- formatPreflightResult,
3563
4709
  getAuditLogger,
3564
- getCliDisplayFields,
3565
- getDisplay,
3566
4710
  getLogLevel,
3567
- info,
3568
4711
  main,
3569
- mapType,
3570
4712
  reconvertEnumValues,
4713
+ registerCompletionCommand,
4714
+ registerConfigCommand,
3571
4715
  registerConfigNamespace,
3572
- registerDiscoveryCommands,
4716
+ registerDescribeCommand,
4717
+ registerDisableCommand,
4718
+ registerEnableCommand,
4719
+ registerExecCommand,
4720
+ registerHealthCommand,
3573
4721
  registerInitCommand,
4722
+ registerListCommand,
3574
4723
  registerPipelineCommand,
3575
- registerShellCommands,
3576
- registerSystemCommands,
4724
+ registerReloadCommand,
4725
+ registerUsageCommand,
3577
4726
  registerValidateCommand,
3578
4727
  resolveFormat,
3579
4728
  resolveRefs,
@@ -3582,9 +4731,6 @@ export {
3582
4731
  setDocsUrl,
3583
4732
  setLogLevel,
3584
4733
  setVerboseHelp,
3585
- truncate,
3586
- validateModuleId,
3587
- verboseHelp,
3588
- warn
4734
+ validateModuleId
3589
4735
  };
3590
4736
  //# sourceMappingURL=index.js.map