apcore-cli 0.9.1 → 0.10.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -288,7 +288,7 @@ var init_sandbox = __esm({
288
288
  */
289
289
  async execute(moduleId, inputData, executor) {
290
290
  if (!this.enabled) {
291
- return executor.execute(moduleId, inputData);
291
+ return executor.call(moduleId, inputData);
292
292
  }
293
293
  return this._sandboxedExecute(moduleId, inputData);
294
294
  }
@@ -583,9 +583,17 @@ function mapType(propName, propSchema) {
583
583
  array: "string"
584
584
  };
585
585
  if (!schemaType) {
586
+ warn(`No type specified for property '${propName}', defaulting to string.`);
586
587
  return "string";
587
588
  }
588
- return typeMap[schemaType] ?? "string";
589
+ const mapped = typeMap[schemaType];
590
+ if (mapped === void 0) {
591
+ warn(
592
+ `Unknown schema type '${schemaType}' for property '${propName}', defaulting to string.`
593
+ );
594
+ return "string";
595
+ }
596
+ return mapped;
589
597
  }
590
598
  function extractHelp(propSchema, maxLength = 1e3) {
591
599
  let text = propSchema["x-llm-description"];
@@ -766,7 +774,7 @@ async function checkApproval(moduleDef, autoApprove, timeout) {
766
774
  if (!requiresApproval) {
767
775
  return;
768
776
  }
769
- const moduleId = moduleDef.id;
777
+ const moduleId = moduleDef.moduleId;
770
778
  if (autoApprove) {
771
779
  return;
772
780
  }
@@ -790,7 +798,7 @@ async function checkApproval(moduleDef, autoApprove, timeout) {
790
798
  }
791
799
  async function promptWithTimeout(moduleDef, timeout) {
792
800
  timeout = Math.max(1, Math.min(timeout, 3600));
793
- const moduleId = moduleDef.id;
801
+ const moduleId = moduleDef.moduleId;
794
802
  const annotations = moduleDef.annotations;
795
803
  const message = (annotations ? getAnnotation(annotations, "approval_message") : void 0) ?? `Module '${moduleId}' requires approval to execute.`;
796
804
  process.stderr.write(message + "\n");
@@ -870,7 +878,7 @@ var init_approval = __esm({
870
878
  const message = extra.approval_message ?? `Module '${moduleId}' requires approval to execute.`;
871
879
  process.stderr.write(message + "\n");
872
880
  try {
873
- await promptWithTimeout({ id: moduleId }, this.timeout);
881
+ await promptWithTimeout({ moduleId }, this.timeout);
874
882
  return { status: "approved", approved_by: "tty_user" };
875
883
  } catch {
876
884
  return { status: "rejected", reason: "User rejected or timed out" };
@@ -890,7 +898,7 @@ function descriptorToScanned(m) {
890
898
  const metadata = m.metadata ?? {};
891
899
  const display = metadata["display"] ?? null;
892
900
  return {
893
- moduleId: m.id,
901
+ moduleId: m.moduleId,
894
902
  description: m.description ?? "",
895
903
  inputSchema: m.inputSchema ?? {},
896
904
  outputSchema: m.outputSchema ?? {},
@@ -946,13 +954,13 @@ async function formatModuleList(modules, format, filterTags, showDeps = false, e
946
954
  if (showDeps) headers.push("Deps");
947
955
  if (exposureFilter) headers.push("Exposure");
948
956
  const rows = modules.map((m) => {
949
- const base = [m.id, truncate(m.description, 80), (m.tags ?? []).join(", ")];
957
+ const base = [m.moduleId, truncate(m.description, 80), (m.tags ?? []).join(", ")];
950
958
  if (showDeps) {
951
959
  const deps = m.dependencies;
952
960
  base.push(String(Array.isArray(deps) ? deps.length : 0));
953
961
  }
954
962
  if (exposureFilter) {
955
- base.push(exposureFilter.isExposed(m.id ?? "") ? "\u2713" : "\u2014");
963
+ base.push(exposureFilter.isExposed(m.moduleId ?? "") ? "\u2713" : "\u2014");
956
964
  }
957
965
  return base;
958
966
  });
@@ -960,7 +968,7 @@ async function formatModuleList(modules, format, filterTags, showDeps = false, e
960
968
  } else if (format === "json") {
961
969
  const result = modules.map((m) => {
962
970
  const entry = {
963
- id: m.id,
971
+ id: m.moduleId,
964
972
  description: m.description,
965
973
  tags: m.tags ?? []
966
974
  };
@@ -969,7 +977,7 @@ async function formatModuleList(modules, format, filterTags, showDeps = false, e
969
977
  entry.dependency_count = Array.isArray(deps) ? deps.length : 0;
970
978
  }
971
979
  if (exposureFilter) {
972
- entry.exposed = exposureFilter.isExposed(m.id ?? "");
980
+ entry.exposed = exposureFilter.isExposed(m.moduleId ?? "");
973
981
  }
974
982
  return entry;
975
983
  });
@@ -1001,7 +1009,7 @@ function annotationsToDict(annotations) {
1001
1009
  async function formatModuleDetail(moduleDef, format) {
1002
1010
  if (format === "table") {
1003
1011
  process.stdout.write(`
1004
- Module: ${moduleDef.id}
1012
+ Module: ${moduleDef.moduleId}
1005
1013
  `);
1006
1014
  process.stdout.write(`
1007
1015
  Description:
@@ -1049,7 +1057,7 @@ Tags: ${tags.join(", ")}
1049
1057
  }
1050
1058
  } else if (format === "json") {
1051
1059
  const result = {
1052
- id: moduleDef.id,
1060
+ id: moduleDef.moduleId,
1053
1061
  description: moduleDef.description
1054
1062
  };
1055
1063
  if (moduleDef.inputSchema) result.input_schema = moduleDef.inputSchema;
@@ -1409,7 +1417,7 @@ function getDisplay(descriptor) {
1409
1417
  if (display && typeof display === "object" && !Array.isArray(display)) {
1410
1418
  return display;
1411
1419
  }
1412
- const overlay = lookupBindingDisplay(descriptor.id);
1420
+ const overlay = lookupBindingDisplay(descriptor.moduleId);
1413
1421
  return overlay ?? {};
1414
1422
  }
1415
1423
  var init_display_helpers = __esm({
@@ -1427,9 +1435,9 @@ import yaml2 from "js-yaml";
1427
1435
  function registerConfigNamespace() {
1428
1436
  try {
1429
1437
  const nodeRequire = createRequire(import.meta.url);
1430
- const { Config } = nodeRequire("apcore-js");
1431
- if (typeof Config?.registerNamespace === "function") {
1432
- Config.registerNamespace({
1438
+ const { Config: Config2 } = nodeRequire("apcore-js");
1439
+ if (typeof Config2?.registerNamespace === "function") {
1440
+ Config2.registerNamespace({
1433
1441
  name: "apcore-cli",
1434
1442
  envPrefix: "APCORE_CLI",
1435
1443
  defaults: NAMESPACE_DEFAULTS
@@ -1943,1672 +1951,1694 @@ var init_shell = __esm({
1943
1951
  }
1944
1952
  });
1945
1953
 
1946
- // src/security/audit.ts
1947
- var audit_exports = {};
1948
- __export(audit_exports, {
1949
- AuditLogger: () => AuditLogger,
1950
- canonicalizeForHash: () => canonicalizeForHash,
1951
- getAuditLogger: () => getAuditLogger,
1952
- setAuditLogger: () => setAuditLogger
1953
- });
1954
- import * as crypto from "crypto";
1955
- import * as fs3 from "fs";
1956
- import * as os from "os";
1957
- import * as path3 from "path";
1958
- function setAuditLogger(auditLogger) {
1959
- _auditLogger = auditLogger;
1960
- }
1961
- function getAuditLogger() {
1962
- return _auditLogger;
1954
+ // src/exposure.ts
1955
+ function escapeRegex(str) {
1956
+ return str.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
1963
1957
  }
1964
- function canonicalizeForHash(value) {
1965
- if (value === null || typeof value !== "object") return value;
1966
- if (Array.isArray(value)) return value.map(canonicalizeForHash);
1967
- const src = value;
1968
- const sorted = {};
1969
- for (const key of Object.keys(src).sort()) {
1970
- sorted[key] = canonicalizeForHash(src[key]);
1971
- }
1972
- return sorted;
1958
+ function compilePattern(pattern) {
1959
+ const sentinel = "\0GLOB\0";
1960
+ const escaped = pattern.replaceAll("**", sentinel);
1961
+ const parts = escaped.split("*");
1962
+ const regexParts = parts.map((p) => {
1963
+ const restored = p.replaceAll(sentinel, "**");
1964
+ return escapeRegex(restored);
1965
+ });
1966
+ let regex = regexParts.join("[^.]*");
1967
+ regex = regex.replaceAll("\\*\\*", ".+");
1968
+ return new RegExp(`^${regex}$`);
1973
1969
  }
1974
- var _auditLogger, AuditLogger;
1975
- var init_audit = __esm({
1976
- "src/security/audit.ts"() {
1970
+ var ExposureFilter;
1971
+ var init_exposure = __esm({
1972
+ "src/exposure.ts"() {
1977
1973
  "use strict";
1978
1974
  init_esm_shims();
1979
1975
  init_logger();
1980
- _auditLogger = null;
1981
- AuditLogger = class _AuditLogger {
1982
- static DEFAULT_PATH = path3.join(
1983
- os.homedir(),
1984
- ".apcore-cli",
1985
- "audit.jsonl"
1986
- );
1987
- logPath;
1988
- writeFailureWarned = false;
1989
- constructor(path6) {
1990
- this.logPath = path6 ?? _AuditLogger.DEFAULT_PATH;
1991
- this.ensureDirectory();
1992
- }
1993
- ensureDirectory() {
1994
- const dir = path3.dirname(this.logPath);
1995
- try {
1996
- fs3.mkdirSync(dir, { recursive: true });
1997
- try {
1998
- fs3.chmodSync(dir, 448);
1999
- } catch {
2000
- }
2001
- } catch {
1976
+ ExposureFilter = class _ExposureFilter {
1977
+ static VALID_MODES = ["all", "include", "exclude", "none"];
1978
+ _mode;
1979
+ _compiledInclude;
1980
+ _compiledExclude;
1981
+ constructor(mode = "all", include, exclude) {
1982
+ if (!_ExposureFilter.VALID_MODES.includes(mode)) {
1983
+ process.stderr.write(
1984
+ `Warning: Unknown ExposureFilter mode '${mode}' \u2014 defaulting to 'none'. Valid modes: ${_ExposureFilter.VALID_MODES.join(", ")}.
1985
+ `
1986
+ );
1987
+ mode = "none";
2002
1988
  }
1989
+ this._mode = mode;
1990
+ const dedup = (arr) => [...new Set(arr)];
1991
+ this._compiledInclude = dedup(include ?? []).map(compilePattern);
1992
+ this._compiledExclude = dedup(exclude ?? []).map(compilePattern);
2003
1993
  }
2004
- logExecution(moduleId, inputData, status, exitCode, durationMs) {
2005
- const entry = {
2006
- timestamp: (/* @__PURE__ */ new Date()).toISOString(),
2007
- user: this.getUser(),
2008
- module_id: moduleId,
2009
- input_hash: this.hashInput(inputData),
2010
- status,
2011
- exit_code: exitCode,
2012
- duration_ms: durationMs
2013
- };
2014
- try {
2015
- fs3.appendFileSync(this.logPath, JSON.stringify(entry) + "\n");
2016
- try {
2017
- fs3.chmodSync(this.logPath, 384);
2018
- } catch {
2019
- }
2020
- } catch (err) {
2021
- if (!this.writeFailureWarned) {
2022
- this.writeFailureWarned = true;
2023
- warn(`Could not write audit log: ${err}`);
2024
- }
1994
+ /** Return true if the module should be exposed as a CLI command. */
1995
+ isExposed(moduleId) {
1996
+ if (this._mode === "all") return true;
1997
+ if (this._mode === "include") {
1998
+ return this._compiledInclude.some((rx) => rx.test(moduleId));
1999
+ }
2000
+ if (this._mode === "exclude") {
2001
+ return !this._compiledExclude.some((rx) => rx.test(moduleId));
2025
2002
  }
2003
+ return false;
2026
2004
  }
2027
- hashInput(inputData) {
2028
- const salt = crypto.randomBytes(16);
2029
- const payload = JSON.stringify(canonicalizeForHash(inputData));
2030
- return crypto.createHash("sha256").update(Buffer.concat([salt, Buffer.from(payload, "utf-8")])).digest("hex");
2005
+ /** Partition moduleIds into [exposed, hidden] lists. */
2006
+ filterModules(moduleIds) {
2007
+ const exposed = [];
2008
+ const hidden = [];
2009
+ for (const mid of moduleIds) {
2010
+ (this.isExposed(mid) ? exposed : hidden).push(mid);
2011
+ }
2012
+ return [exposed, hidden];
2031
2013
  }
2032
- getUser() {
2033
- try {
2034
- return os.userInfo().username;
2035
- } catch {
2036
- return process.env.USER ?? process.env.LOGNAME ?? process.env.USERNAME ?? "unknown";
2014
+ /**
2015
+ * Create an ExposureFilter from a parsed config dict.
2016
+ *
2017
+ * Expected: `{ expose: { mode: "include", include: ["admin.*"] } }`
2018
+ */
2019
+ static fromConfig(config) {
2020
+ const expose = config.expose ?? {};
2021
+ if (typeof expose !== "object" || expose === null || Array.isArray(expose)) {
2022
+ warn("Invalid 'expose' config (expected dict), using mode: all.");
2023
+ return new _ExposureFilter();
2024
+ }
2025
+ const exposeObj = expose;
2026
+ const mode = exposeObj.mode ?? "all";
2027
+ if (!["all", "include", "exclude"].includes(mode)) {
2028
+ throw new Error(
2029
+ `Invalid expose mode: '${mode}'. Must be one of: all, include, exclude.`
2030
+ );
2031
+ }
2032
+ let include = exposeObj.include ?? [];
2033
+ if (!Array.isArray(include)) {
2034
+ warn("Invalid 'expose.include' (expected list), ignoring.");
2035
+ include = [];
2036
+ }
2037
+ let exclude = exposeObj.exclude ?? [];
2038
+ if (!Array.isArray(exclude)) {
2039
+ warn("Invalid 'expose.exclude' (expected list), ignoring.");
2040
+ exclude = [];
2037
2041
  }
2042
+ const filterList = (arr, label) => {
2043
+ const result = [];
2044
+ for (const p of arr) {
2045
+ if (!p) {
2046
+ warn(`Empty pattern in expose.${label}, skipping.`);
2047
+ } else {
2048
+ result.push(String(p));
2049
+ }
2050
+ }
2051
+ return result;
2052
+ };
2053
+ return new _ExposureFilter(
2054
+ mode,
2055
+ filterList(include, "include"),
2056
+ filterList(exclude, "exclude")
2057
+ );
2038
2058
  }
2039
2059
  };
2040
2060
  }
2041
2061
  });
2042
2062
 
2043
- // src/system-usage.ts
2044
- import * as fs4 from "fs";
2045
- import * as os2 from "os";
2046
- import * as path4 from "path";
2047
- function computeSummary(options = {}) {
2048
- const auditPath = options.auditPath ?? DEFAULT_AUDIT_PATH;
2049
- const period = options.period ?? "24h";
2050
- const cutoff = (options.now ?? /* @__PURE__ */ new Date()).getTime() - PERIOD_TO_MS[period];
2051
- if (!fs4.existsSync(auditPath)) {
2052
- return /* @__PURE__ */ new Map();
2053
- }
2054
- let raw;
2055
- try {
2056
- raw = fs4.readFileSync(auditPath, "utf-8");
2057
- } catch {
2058
- return /* @__PURE__ */ new Map();
2059
- }
2060
- const counts = /* @__PURE__ */ new Map();
2061
- const errors = /* @__PURE__ */ new Map();
2062
- const latencySum = /* @__PURE__ */ new Map();
2063
- for (const line of raw.split("\n")) {
2064
- const trimmed = line.trim();
2065
- if (!trimmed) continue;
2066
- let entry;
2067
- try {
2068
- entry = JSON.parse(trimmed);
2069
- } catch {
2070
- continue;
2071
- }
2072
- const ts = typeof entry.timestamp === "string" ? Date.parse(entry.timestamp) : NaN;
2073
- if (Number.isNaN(ts) || ts < cutoff) continue;
2074
- const moduleId = typeof entry.module_id === "string" ? entry.module_id : null;
2075
- if (!moduleId) continue;
2076
- counts.set(moduleId, (counts.get(moduleId) ?? 0) + 1);
2077
- if (entry.status === "error") {
2078
- errors.set(moduleId, (errors.get(moduleId) ?? 0) + 1);
2079
- }
2080
- const duration = entry.duration_ms;
2081
- if (typeof duration === "number") {
2082
- latencySum.set(moduleId, (latencySum.get(moduleId) ?? 0) + duration);
2083
- }
2084
- }
2085
- const out = /* @__PURE__ */ new Map();
2086
- for (const [id, calls] of counts) {
2087
- out.set(id, {
2088
- module_id: id,
2089
- calls,
2090
- errors: errors.get(id) ?? 0,
2091
- latency_ms: calls > 0 ? (latencySum.get(id) ?? 0) / calls : 0
2092
- });
2093
- }
2094
- return out;
2063
+ // src/builtin-group.ts
2064
+ function setReservedGroupNames(names) {
2065
+ _effectiveReservedNames = names;
2095
2066
  }
2096
- function sortModulesByUsage(modules, field, options = {}) {
2097
- const reverse = options.reverse ?? true;
2098
- const summary = computeSummary({
2099
- auditPath: options.auditPath,
2100
- period: options.period
2101
- });
2102
- if (summary.size === 0) {
2103
- modules.sort((a, b) => (a.id ?? a.module_id ?? "").localeCompare(b.id ?? b.module_id ?? ""));
2104
- if (reverse) modules.reverse();
2105
- return { used: false };
2067
+ function _validateBuiltinGroupName(name) {
2068
+ if (!name || !_NAME_REGEX.test(name)) {
2069
+ throw new ApcliGroupError(
2070
+ `builtinGroupName ${JSON.stringify(name)} must match /^[a-z][a-z0-9_-]*$/ (non-empty, lowercase, alphanumeric + '_' / '-', leading letter).`
2071
+ );
2106
2072
  }
2107
- const key = (m) => {
2108
- const id = m.id ?? m.module_id ?? "";
2109
- const s = summary.get(id);
2110
- if (!s) return 0;
2111
- return field === "latency" ? s.latency_ms : field === "calls" ? s.calls : s.errors;
2112
- };
2113
- modules.sort((a, b) => {
2114
- const diff = key(a) - key(b);
2115
- if (diff !== 0) return reverse ? -diff : diff;
2116
- return (a.id ?? a.module_id ?? "").localeCompare(b.id ?? b.module_id ?? "");
2117
- });
2118
- return { used: true };
2119
2073
  }
2120
- var PERIOD_TO_MS, DEFAULT_AUDIT_PATH;
2121
- var init_system_usage = __esm({
2122
- "src/system-usage.ts"() {
2123
- "use strict";
2124
- init_esm_shims();
2125
- PERIOD_TO_MS = {
2126
- "1h": 60 * 60 * 1e3,
2127
- "24h": 24 * 60 * 60 * 1e3,
2128
- "7d": 7 * 24 * 60 * 60 * 1e3,
2129
- "30d": 30 * 24 * 60 * 60 * 1e3
2130
- };
2131
- DEFAULT_AUDIT_PATH = path4.join(
2132
- os2.homedir(),
2133
- ".apcore-cli",
2134
- "audit.jsonl"
2135
- );
2136
- }
2137
- });
2138
-
2139
- // src/security/config-encryptor.ts
2140
- import * as crypto2 from "crypto";
2141
- import * as os3 from "os";
2142
- async function getKeytar() {
2143
- if (keytarModule) return keytarModule;
2144
- try {
2145
- keytarModule = await import("keytar");
2146
- return keytarModule;
2147
- } catch {
2148
- return null;
2149
- }
2150
- }
2151
- var PBKDF2_ITERATIONS, V1_STATIC_SALT, keytarModule, ConfigEncryptor;
2152
- var init_config_encryptor = __esm({
2153
- "src/security/config-encryptor.ts"() {
2074
+ var ApcliGroupError, DEFAULT_BUILTIN_GROUP_NAME, RESERVED_GROUP_NAMES, _effectiveReservedNames, _NAME_REGEX, VALID_USER_MODES, APCLI_SUBCOMMAND_NAMES, ApcliGroup;
2075
+ var init_builtin_group = __esm({
2076
+ "src/builtin-group.ts"() {
2154
2077
  "use strict";
2155
2078
  init_esm_shims();
2156
2079
  init_errors();
2157
2080
  init_logger();
2158
- PBKDF2_ITERATIONS = 6e5;
2159
- V1_STATIC_SALT = Buffer.from("apcore-cli-config-v1");
2160
- keytarModule = null;
2161
- ConfigEncryptor = class _ConfigEncryptor {
2162
- static SERVICE_NAME = "apcore-cli";
2163
- // One-shot flag so the "obfuscation only" warning fires exactly once
2164
- // per process instead of once per encrypt/decrypt call.
2165
- static weakFallbackWarned = false;
2081
+ ApcliGroupError = class extends Error {
2082
+ constructor(message) {
2083
+ super(message);
2084
+ this.name = "ApcliGroupError";
2085
+ }
2086
+ };
2087
+ DEFAULT_BUILTIN_GROUP_NAME = "apcli";
2088
+ RESERVED_GROUP_NAMES = /* @__PURE__ */ new Set([DEFAULT_BUILTIN_GROUP_NAME]);
2089
+ _effectiveReservedNames = RESERVED_GROUP_NAMES;
2090
+ _NAME_REGEX = /^[a-z][a-z0-9_-]*$/;
2091
+ VALID_USER_MODES = /* @__PURE__ */ new Set([
2092
+ "all",
2093
+ "none",
2094
+ "include",
2095
+ "exclude"
2096
+ ]);
2097
+ APCLI_SUBCOMMAND_NAMES = /* @__PURE__ */ new Set([
2098
+ "list",
2099
+ "describe",
2100
+ "exec",
2101
+ "validate",
2102
+ "init",
2103
+ "health",
2104
+ "usage",
2105
+ "enable",
2106
+ "disable",
2107
+ "reload",
2108
+ "config",
2109
+ "completion",
2110
+ "describe-pipeline"
2111
+ ]);
2112
+ ApcliGroup = class _ApcliGroup {
2113
+ _mode;
2114
+ _include;
2115
+ _exclude;
2116
+ _disableEnv;
2117
+ _registryInjected;
2118
+ _fromCliConfig;
2119
+ _name;
2120
+ constructor(init) {
2121
+ this._mode = init.mode;
2122
+ this._include = init.include;
2123
+ this._exclude = init.exclude;
2124
+ this._disableEnv = init.disableEnv;
2125
+ this._registryInjected = init.registryInjected;
2126
+ this._fromCliConfig = init.fromCliConfig;
2127
+ this._name = init.name;
2128
+ }
2166
2129
  /**
2167
- * Encrypt and store a configuration value.
2130
+ * Resolved name for the built-in command group (default `"apcli"`).
2131
+ * Overridable via createCli's `builtinGroupName` option for downstream
2132
+ * branded CLIs that want a custom namespace. Cross-SDK parity with
2133
+ * Python `ApcliGroup.name` (2026-05-08).
2134
+ */
2135
+ get name() {
2136
+ return this._name;
2137
+ }
2138
+ /**
2139
+ * Tier 1 constructor — config came from `createCli({ apcli })`.
2168
2140
  *
2169
- * Cross-SDK contract (D10-003, 2026-04-26): when the OS keyring is
2170
- * detected as available but `setPassword` then throws (locked keyring,
2171
- * transient backend failure, permission revoked, etc.), the error is
2172
- * propagated wrapped in a `ConfigDecryptionError`. Previously TS
2173
- * caught the exception and silently fell through to AES file encryption
2174
- * — a quiet downgrade that surprised users who expected a hard failure.
2175
- * Python lets the keyring exception propagate raw; Rust returns
2176
- * `ConfigDecryptionError::KeyringError`. The fall-through to AES is
2177
- * still reached when `getKeytar()` returns `null` (keyring
2178
- * genuinely unavailable on this platform / install).
2141
+ * A non-auto mode from this tier wins over env var and yaml.
2179
2142
  */
2180
- async store(key, value) {
2181
- const keytar = await getKeytar();
2182
- if (keytar) {
2183
- try {
2184
- await keytar.setPassword(_ConfigEncryptor.SERVICE_NAME, key, value);
2185
- return `keyring:${key}`;
2186
- } catch (err) {
2187
- const detail = err instanceof Error ? err.message : String(err);
2188
- throw new ConfigDecryptionError(
2189
- `Failed to store '${key}' in OS keyring: ${detail}. Unset APCORE_CLI_CONFIG_PASSPHRASE-aware backends or unlock the keyring before retrying.`
2190
- );
2191
- }
2192
- }
2193
- warn("OS keyring unavailable. Using file-based encryption.");
2194
- const ciphertext = this.aesEncrypt(value);
2195
- return `enc:v2:${ciphertext.toString("base64")}`;
2143
+ static fromCliConfig(config, opts) {
2144
+ return _ApcliGroup._build(
2145
+ config,
2146
+ opts,
2147
+ /*fromCliConfig*/
2148
+ true
2149
+ );
2196
2150
  }
2197
2151
  /**
2198
- * Retrieve and decrypt a configuration value.
2152
+ * Tier 3 constructor config came from `apcore.yaml`.
2153
+ *
2154
+ * Env var (Tier 2) may override the yaml-supplied mode.
2199
2155
  */
2200
- async retrieve(configValue, key) {
2201
- if (configValue.startsWith("keyring:")) {
2202
- const keytar = await getKeytar();
2203
- if (!keytar) {
2204
- throw new ConfigDecryptionError(
2205
- `Keyring module not available to retrieve '${key}'.`
2206
- );
2207
- }
2208
- try {
2209
- const refKey = configValue.slice("keyring:".length);
2210
- const result = await keytar.getPassword(
2211
- _ConfigEncryptor.SERVICE_NAME,
2212
- refKey
2213
- );
2214
- if (result === null || result === void 0) {
2215
- throw new ConfigDecryptionError(
2216
- `Keyring entry not found for '${refKey}'.`
2217
- );
2218
- }
2219
- return result;
2220
- } catch (err) {
2221
- if (err instanceof ConfigDecryptionError) throw err;
2222
- throw new ConfigDecryptionError(
2223
- `Failed to retrieve from keyring: ${err}`
2224
- );
2225
- }
2156
+ static fromYaml(config, opts) {
2157
+ if (config !== null && config !== void 0 && typeof config !== "boolean" && (typeof config !== "object" || Array.isArray(config))) {
2158
+ const got = Array.isArray(config) ? "array" : typeof config;
2159
+ warn(
2160
+ `apcore.yaml apcli has unexpected type ${got}; using auto-detect.`
2161
+ );
2162
+ return _ApcliGroup._build(
2163
+ void 0,
2164
+ opts,
2165
+ /*fromCliConfig*/
2166
+ false
2167
+ );
2226
2168
  }
2227
- if (configValue.startsWith("enc:v2:")) {
2228
- const data = Buffer.from(configValue.slice("enc:v2:".length), "base64");
2229
- try {
2230
- return this.aesDecrypt(data);
2231
- } catch {
2232
- throw new ConfigDecryptionError(
2233
- `Failed to decrypt configuration value '${key}'. Re-configure with 'apcore-cli config set ${key}'.`
2234
- );
2235
- }
2169
+ return _ApcliGroup._build(
2170
+ config,
2171
+ opts,
2172
+ /*fromCliConfig*/
2173
+ false
2174
+ );
2175
+ }
2176
+ /**
2177
+ * Non-panicking Tier 3 factory (A-001 parity with Rust's `try_from_yaml`).
2178
+ * Returns `[instance, null]` on success or `[null, errorMessage]` on invalid input.
2179
+ * Use this in programmatic contexts where throwing/exiting is unwanted.
2180
+ */
2181
+ static tryFromYaml(config, opts) {
2182
+ if (config !== null && config !== void 0 && typeof config !== "boolean" && (typeof config !== "object" || Array.isArray(config))) {
2183
+ const got = Array.isArray(config) ? "array" : typeof config;
2184
+ return [
2185
+ null,
2186
+ `apcore.yaml 'apcli:' must be a bool, object, or null; got ${got}`
2187
+ ];
2236
2188
  }
2237
- if (configValue.startsWith("enc:")) {
2238
- const data = Buffer.from(configValue.slice("enc:".length), "base64");
2239
- try {
2240
- return this.aesDecryptV1(data);
2241
- } catch {
2242
- throw new ConfigDecryptionError(
2243
- `Failed to decrypt configuration value '${key}'. Re-configure with 'apcore-cli config set ${key}'.`
2244
- );
2189
+ if (config !== null && config !== void 0 && typeof config === "object" && !Array.isArray(config)) {
2190
+ const mode = config["mode"];
2191
+ if (mode !== void 0 && mode !== null) {
2192
+ const validModes = ["all", "none", "include", "exclude"];
2193
+ if (typeof mode !== "string" || !validModes.includes(mode)) {
2194
+ return [null, `Invalid apcli mode: '${mode}'. Must be one of: all, none, include, exclude.`];
2195
+ }
2245
2196
  }
2246
2197
  }
2247
- return configValue;
2198
+ return [_ApcliGroup.fromYaml(config, opts), null];
2248
2199
  }
2249
- // Derive an AES-256 key with a provided salt (v2 format).
2250
- //
2251
- // Order of preference:
2252
- // 1. APCORE_CLI_CONFIG_PASSPHRASE env var — a real secret supplied by
2253
- // the user; produces a key an attacker with filesystem read cannot
2254
- // reconstruct without also knowing the passphrase.
2255
- // 2. hostname + username — obfuscation-only derivation for backward
2256
- // compatibility. Emits a loud stderr warning on first use so
2257
- // operators know the stored value is NOT protected against a
2258
- // filesystem-read attacker.
2259
- deriveKey(salt) {
2260
- const passphrase = process.env.APCORE_CLI_CONFIG_PASSPHRASE;
2261
- if (passphrase && passphrase.length > 0) {
2262
- return crypto2.pbkdf2Sync(passphrase, salt, PBKDF2_ITERATIONS, 32, "sha256");
2200
+ // -------------------------------------------------------------------------
2201
+ // Internal builder — shared by both factories
2202
+ // -------------------------------------------------------------------------
2203
+ static _build(config, opts, fromCliConfig) {
2204
+ const name = opts.name ?? DEFAULT_BUILTIN_GROUP_NAME;
2205
+ _validateBuiltinGroupName(name);
2206
+ if (config === true) {
2207
+ return new _ApcliGroup({
2208
+ mode: "all",
2209
+ include: [],
2210
+ exclude: [],
2211
+ disableEnv: false,
2212
+ registryInjected: opts.registryInjected,
2213
+ fromCliConfig,
2214
+ name
2215
+ });
2263
2216
  }
2264
- if (!_ConfigEncryptor.weakFallbackWarned) {
2265
- warn(
2266
- "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."
2217
+ if (config === false) {
2218
+ return new _ApcliGroup({
2219
+ mode: "none",
2220
+ include: [],
2221
+ exclude: [],
2222
+ disableEnv: false,
2223
+ registryInjected: opts.registryInjected,
2224
+ fromCliConfig,
2225
+ name
2226
+ });
2227
+ }
2228
+ if (config === void 0 || config === null) {
2229
+ return new _ApcliGroup({
2230
+ mode: "auto",
2231
+ include: [],
2232
+ exclude: [],
2233
+ disableEnv: false,
2234
+ registryInjected: opts.registryInjected,
2235
+ fromCliConfig,
2236
+ name
2237
+ });
2238
+ }
2239
+ if (typeof config !== "object" || Array.isArray(config)) {
2240
+ process.stderr.write(
2241
+ `Error: apcli config must be a boolean or object; got ${Array.isArray(config) ? "array" : typeof config}.
2242
+ `
2267
2243
  );
2268
- _ConfigEncryptor.weakFallbackWarned = true;
2244
+ process.exit(EXIT_CODES.INVALID_CLI_INPUT);
2269
2245
  }
2270
- const hostname2 = os3.hostname();
2271
- const username = process.env.USER ?? process.env.LOGNAME ?? process.env.USERNAME ?? "unknown";
2272
- const material = `${hostname2}:${username}`;
2273
- return crypto2.pbkdf2Sync(material, salt, PBKDF2_ITERATIONS, 32, "sha256");
2274
- }
2275
- aesEncrypt(plaintext) {
2276
- const salt = crypto2.randomBytes(16);
2277
- const key = this.deriveKey(salt);
2278
- const nonce = crypto2.randomBytes(12);
2279
- const cipher = crypto2.createCipheriv("aes-256-gcm", key, nonce);
2280
- const ct = Buffer.concat([
2281
- cipher.update(plaintext, "utf-8"),
2282
- cipher.final()
2283
- ]);
2284
- const tag = cipher.getAuthTag();
2285
- return Buffer.concat([salt, nonce, tag, ct]);
2286
- }
2287
- aesDecrypt(data) {
2288
- const salt = data.subarray(0, 16);
2289
- const nonce = data.subarray(16, 28);
2290
- const tag = data.subarray(28, 44);
2291
- const ct = data.subarray(44);
2292
- const key = this.deriveKey(salt);
2293
- const decipher = crypto2.createDecipheriv("aes-256-gcm", key, nonce);
2294
- decipher.setAuthTag(tag);
2295
- return Buffer.concat([decipher.update(ct), decipher.final()]).toString("utf-8");
2296
- }
2297
- /** Decrypt legacy v1-format values: nonce(12)+tag(16)+ct, static salt. */
2298
- aesDecryptV1(data) {
2299
- const nonce = data.subarray(0, 12);
2300
- const tag = data.subarray(12, 28);
2301
- const ct = data.subarray(28);
2302
- const hostname2 = os3.hostname();
2303
- const username = process.env.USER ?? process.env.LOGNAME ?? process.env.USERNAME ?? "unknown";
2304
- const passphrase = process.env.APCORE_CLI_CONFIG_PASSPHRASE;
2305
- const materials = passphrase ? [passphrase, `${hostname2}:${username}`] : [`${hostname2}:${username}`];
2306
- for (const material of materials) {
2307
- for (const iterations of [6e5, 1e5]) {
2308
- try {
2309
- const key = crypto2.pbkdf2Sync(material, V1_STATIC_SALT, iterations, 32, "sha256");
2310
- const decipher = crypto2.createDecipheriv("aes-256-gcm", key, nonce);
2311
- decipher.setAuthTag(tag);
2312
- return Buffer.concat([decipher.update(ct), decipher.final()]).toString("utf-8");
2313
- } catch {
2314
- }
2246
+ const cfg = config;
2247
+ let mode;
2248
+ if (cfg.mode === void 0 || cfg.mode === null) {
2249
+ mode = "auto";
2250
+ } else if (typeof cfg.mode !== "string") {
2251
+ process.stderr.write(
2252
+ `Error: apcli.mode must be a string; got ${typeof cfg.mode}. Expected one of all|none|include|exclude.
2253
+ `
2254
+ );
2255
+ process.exit(EXIT_CODES.INVALID_CLI_INPUT);
2256
+ } else if (!VALID_USER_MODES.has(cfg.mode)) {
2257
+ process.stderr.write(
2258
+ `Error: apcli.mode '${cfg.mode}' is invalid. Expected one of all|none|include|exclude.
2259
+ `
2260
+ );
2261
+ process.exit(EXIT_CODES.INVALID_CLI_INPUT);
2262
+ } else {
2263
+ mode = cfg.mode;
2264
+ }
2265
+ const include = _ApcliGroup._normalizeList(cfg.include, "include");
2266
+ const exclude = _ApcliGroup._normalizeList(cfg.exclude, "exclude");
2267
+ const rawDisableEnv = cfg.disableEnv !== void 0 ? cfg.disableEnv : cfg["disable_env"];
2268
+ let disableEnv = false;
2269
+ if (rawDisableEnv !== void 0) {
2270
+ if (typeof rawDisableEnv === "boolean") {
2271
+ disableEnv = rawDisableEnv;
2272
+ } else {
2273
+ warn(
2274
+ `apcli.disable_env must be boolean; got ${typeof rawDisableEnv}. Treating as false.`
2275
+ );
2315
2276
  }
2316
2277
  }
2317
- throw new Error("v1 decryption failed with all material+iteration combinations");
2318
- }
2319
- };
2320
- }
2321
- });
2322
-
2323
- // src/security/auth.ts
2324
- var AuthProvider;
2325
- var init_auth = __esm({
2326
- "src/security/auth.ts"() {
2327
- "use strict";
2328
- init_esm_shims();
2329
- init_errors();
2330
- init_config_encryptor();
2331
- AuthProvider = class {
2332
- config;
2333
- _encryptor;
2334
- constructor(config, encryptor) {
2335
- this.config = config;
2336
- this._encryptor = encryptor;
2278
+ return new _ApcliGroup({
2279
+ mode,
2280
+ include,
2281
+ exclude,
2282
+ disableEnv,
2283
+ registryInjected: opts.registryInjected,
2284
+ fromCliConfig,
2285
+ name
2286
+ });
2337
2287
  }
2338
2288
  /**
2339
- * Resolve the active ConfigEncryptor instance.
2289
+ * Normalize an include/exclude list. Non-array → warn and return [].
2340
2290
  *
2341
- * D11-005 (2026-05-12): three-tier fallback chain matching Python's
2342
- * `_get_encryptor` (auth.py:33): explicit constructor arg > peer attribute
2343
- * `config.encryptor` (set by embedders injecting forced-AES test fixtures
2344
- * or shared instances) > fresh `new ConfigEncryptor()`. Previously TS
2345
- * skipped the peer-attribute tier, silently giving embedders a different
2346
- * encryptor than the one they wired on the config.
2347
- */
2348
- getEncryptor() {
2349
- if (this._encryptor) return this._encryptor;
2350
- const fromConfig = this.config.encryptor;
2351
- if (fromConfig) return fromConfig;
2352
- return new ConfigEncryptor();
2353
- }
2354
- /**
2355
- * Retrieve the API key from the configured sources.
2356
- * Handles keyring: and enc: prefixes via ConfigEncryptor.
2291
+ * Unknown but well-formed entries emit a WARNING (spec §7 error table,
2292
+ * T-APCLI-25) but are retained in the returned list for forward-compat —
2293
+ * if apcore-cli later adds a subcommand named `foo`, existing configs
2294
+ * continue to work without a config change. At runtime, unknown names
2295
+ * simply never match any registered subcommand.
2357
2296
  */
2358
- async getApiKey() {
2359
- const result = this.config.resolve(
2360
- "auth.api_key",
2361
- "--api-key",
2362
- "APCORE_AUTH_API_KEY"
2363
- );
2364
- if (result === null || result === void 0) {
2365
- return null;
2297
+ static _normalizeList(raw, label) {
2298
+ if (raw === void 0 || raw === null) return [];
2299
+ if (!Array.isArray(raw)) {
2300
+ warn(`apcli.${label} must be a list; got ${typeof raw}. Ignoring.`);
2301
+ return [];
2366
2302
  }
2367
- const strResult = String(result);
2368
- if (strResult.startsWith("keyring:") || strResult.startsWith("enc:")) {
2369
- try {
2370
- return await this.getEncryptor().retrieve(strResult, "auth.api_key");
2371
- } catch (err) {
2372
- if (err instanceof ConfigDecryptionError) {
2373
- throw new AuthenticationError(
2374
- "Failed to decrypt stored API key. Re-store with 'apcli config set auth.api_key'."
2303
+ const out = [];
2304
+ for (const entry of raw) {
2305
+ if (typeof entry === "string" && entry.length > 0) {
2306
+ if (!APCLI_SUBCOMMAND_NAMES.has(entry)) {
2307
+ warn(
2308
+ `Unknown apcli subcommand '${entry}' in ${label} list \u2014 ignoring.`
2375
2309
  );
2376
2310
  }
2377
- throw err;
2311
+ out.push(entry);
2312
+ } else {
2313
+ warn(`apcli.${label} contains non-string entry; skipping.`);
2378
2314
  }
2379
2315
  }
2380
- return strResult;
2316
+ return out;
2381
2317
  }
2318
+ // -------------------------------------------------------------------------
2319
+ // Public API
2320
+ // -------------------------------------------------------------------------
2382
2321
  /**
2383
- * Add authentication headers to an outgoing request.
2322
+ * Resolve effective visibility mode after applying tier precedence.
2384
2323
  *
2385
- * Cross-SDK contract (D10-002, 2026-04-26): the input `headers` object
2386
- * is mutated **in place** and the same reference is returned. Callers
2387
- * that share the headers reference (the documented pattern in
2388
- * apcore-cli/docs/features/security.md §AuthProvider) can read
2389
- * `headers.Authorization` after the call without re-binding the
2390
- * return value. Python and Rust both mutate-and-return; TS previously
2391
- * spread into a new object, which silently broke shared-reference
2392
- * callers.
2324
+ * Returns one of `"all" | "none" | "include" | "exclude"` — never `"auto"`.
2325
+ *
2326
+ * Tier order (spec §4.4):
2327
+ * 1. CliConfig non-auto wins outright.
2328
+ * 2. `APCORE_CLI_APCLI` env var (unless sealed by disableEnv).
2329
+ * 3. yaml non-auto.
2330
+ * 4. Auto-detect from registryInjected.
2393
2331
  */
2394
- async authenticateRequest(headers) {
2395
- const key = await this.getApiKey();
2396
- if (!key) {
2397
- throw new AuthenticationError(
2398
- "Remote registry requires authentication. Set --api-key, APCORE_AUTH_API_KEY, or auth.api_key in config."
2399
- );
2332
+ resolveVisibility() {
2333
+ if (this._fromCliConfig && this._mode !== "auto") {
2334
+ return this._mode;
2400
2335
  }
2401
- if (/[\r\n]/.test(key)) {
2402
- throw new AuthenticationError(
2403
- "Malformed API key: contains invalid characters (CR/LF). Re-store with 'apcli config set auth.api_key'."
2404
- );
2336
+ if (!this._disableEnv) {
2337
+ const envMode = this._parseEnv(process.env.APCORE_CLI_APCLI);
2338
+ if (envMode !== null) {
2339
+ return envMode;
2340
+ }
2405
2341
  }
2406
- headers.Authorization = `Bearer ${key.trim()}`;
2407
- return headers;
2342
+ if (this._mode !== "auto") {
2343
+ return this._mode;
2344
+ }
2345
+ return this._registryInjected ? "none" : "all";
2408
2346
  }
2409
2347
  /**
2410
- * Handle an HTTP response status code for auth-related errors.
2348
+ * True iff `subcommand` passes the include/exclude filter.
2349
+ *
2350
+ * Callers MUST first check {@link resolveVisibility} — this method throws
2351
+ * under modes `"all"` or `"none"` (caller bug per spec §4.6).
2411
2352
  */
2412
- handleResponse(statusCode) {
2413
- if (statusCode === 401 || statusCode === 403) {
2414
- throw new AuthenticationError(
2415
- "Authentication failed. Verify your API key."
2416
- );
2417
- }
2353
+ isSubcommandIncluded(subcommand) {
2354
+ const mode = this.resolveVisibility();
2355
+ if (mode === "include") return this._include.includes(subcommand);
2356
+ if (mode === "exclude") return !this._exclude.includes(subcommand);
2357
+ throw new Error(
2358
+ `isSubcommandIncluded called under mode '${mode}'; caller should bypass.`
2359
+ );
2418
2360
  }
2419
- };
2420
- }
2421
- });
2422
-
2423
- // src/security/index.ts
2424
- var security_exports = {};
2425
- __export(security_exports, {
2426
- AuditLogger: () => AuditLogger,
2427
- AuthProvider: () => AuthProvider,
2428
- ConfigEncryptor: () => ConfigEncryptor,
2429
- Sandbox: () => Sandbox,
2430
- getAuditLogger: () => getAuditLogger,
2431
- setAuditLogger: () => setAuditLogger
2361
+ /** True iff the `apcli` group itself should appear in root `--help`. */
2362
+ isGroupVisible() {
2363
+ return this.resolveVisibility() !== "none";
2364
+ }
2365
+ // -------------------------------------------------------------------------
2366
+ // Env parser (Tier 2) — co-located per spec §4.4
2367
+ // -------------------------------------------------------------------------
2368
+ /**
2369
+ * Parse APCORE_CLI_APCLI. Case-insensitive.
2370
+ *
2371
+ * - `show` / `1` / `true` → `"all"`
2372
+ * - `hide` / `0` / `false` → `"none"`
2373
+ * - Empty / unset → `null`
2374
+ * - Anything else → warn and return `null`
2375
+ */
2376
+ _parseEnv(raw) {
2377
+ if (raw === void 0 || raw === "") return null;
2378
+ const normalized = raw.trim().toLowerCase();
2379
+ if (normalized === "") return null;
2380
+ if (normalized === "show" || normalized === "1" || normalized === "true") {
2381
+ return "all";
2382
+ }
2383
+ if (normalized === "hide" || normalized === "0" || normalized === "false") {
2384
+ return "none";
2385
+ }
2386
+ warn(
2387
+ `Unknown APCORE_CLI_APCLI value '${raw}', ignoring. Expected: show, hide, 1, 0, true, false.`
2388
+ );
2389
+ return null;
2390
+ }
2391
+ };
2392
+ }
2432
2393
  });
2433
- var init_security = __esm({
2434
- "src/security/index.ts"() {
2394
+
2395
+ // src/cli.ts
2396
+ import { Command as Command2 } from "commander";
2397
+ function listAllDefinitions(registry) {
2398
+ const defs = [];
2399
+ for (const id of registry.list()) {
2400
+ const def = registry.getDefinition(id);
2401
+ if (def) defs.push(def);
2402
+ }
2403
+ return defs;
2404
+ }
2405
+ var init_cli = __esm({
2406
+ "src/cli.ts"() {
2435
2407
  "use strict";
2436
2408
  init_esm_shims();
2437
- init_audit();
2438
- init_auth();
2439
- init_config_encryptor();
2440
- init_sandbox();
2409
+ init_main();
2410
+ init_display_helpers();
2411
+ init_exposure();
2412
+ init_logger();
2413
+ init_builtin_group();
2414
+ init_errors();
2441
2415
  }
2442
2416
  });
2443
2417
 
2444
- // src/discovery.ts
2445
- import { Command as Command2, Option as Option2 } from "commander";
2446
- function validateTag(tag) {
2447
- if (!TAG_PATTERN.test(tag)) {
2448
- process.stderr.write(
2449
- `Error: Invalid tag format: '${tag}'. Tags must match [a-z][a-z0-9_-]*.
2450
- `
2451
- );
2452
- process.exit(EXIT_CODES.INVALID_CLI_INPUT);
2453
- }
2454
- }
2455
- function collectTag(value, previous) {
2456
- return previous.concat([value]);
2418
+ // src/security/audit.ts
2419
+ var audit_exports = {};
2420
+ __export(audit_exports, {
2421
+ AuditLogger: () => AuditLogger,
2422
+ canonicalizeForHash: () => canonicalizeForHash,
2423
+ getAuditLogger: () => getAuditLogger,
2424
+ setAuditLogger: () => setAuditLogger
2425
+ });
2426
+ import * as crypto from "crypto";
2427
+ import * as fs3 from "fs";
2428
+ import * as os from "os";
2429
+ import * as path3 from "path";
2430
+ function setAuditLogger(auditLogger) {
2431
+ _auditLogger = auditLogger;
2457
2432
  }
2458
- function collectAnnotation(value, previous) {
2459
- return previous.concat([value]);
2433
+ function getAuditLogger() {
2434
+ return _auditLogger;
2460
2435
  }
2461
- function getAnnotationFlag(moduleDef, flag) {
2462
- const annotations = moduleDef.annotations;
2463
- if (!annotations || typeof annotations !== "object") return false;
2464
- const ann = annotations;
2465
- const map = {
2466
- "destructive": "destructive",
2467
- "requires-approval": "requires_approval",
2468
- "readonly": "readonly",
2469
- "streaming": "streaming",
2470
- "cacheable": "cacheable",
2471
- "idempotent": "idempotent",
2472
- "paginated": "paginated"
2473
- };
2474
- const attr = map[flag] ?? flag;
2475
- return ann[attr] === true;
2436
+ function canonicalizeForHash(value) {
2437
+ if (value === null || typeof value !== "object") return value;
2438
+ if (Array.isArray(value)) return value.map(canonicalizeForHash);
2439
+ const src = value;
2440
+ const sorted = {};
2441
+ for (const key of Object.keys(src).sort()) {
2442
+ sorted[key] = canonicalizeForHash(src[key]);
2443
+ }
2444
+ return sorted;
2476
2445
  }
2477
- function registerListCommand(apcliGroup, registry, exposureFilter) {
2478
- 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(
2479
- new Option2("--format <format>", "Output format.").choices(["table", "json", "csv", "yaml", "jsonl", "markdown", "skill"])
2480
- ).option("-s, --search <query>", "Filter by substring match on ID and description.").addOption(
2481
- new Option2("--status <status>", "Filter by module status.").choices(["enabled", "disabled", "all"]).default("enabled")
2482
- ).option("-a, --annotation <flag>", "Filter by annotation flag (AND logic). Repeatable.", collectAnnotation, []).addOption(
2483
- new Option2("--sort <field>", "Sort order.").choices(["id", "calls", "errors", "latency"]).default("id")
2484
- ).option("--reverse", "Reverse sort order.", false).option("--deprecated", "Include deprecated modules.", false).option("--deps", "Show dependency count column.", false).addOption(
2485
- new Option2("--exposure <mode>", "Filter by exposure status.").choices(["exposed", "hidden", "all"]).default("exposed")
2486
- ).action((opts) => {
2487
- for (const t of opts.tag) {
2488
- validateTag(t);
2489
- }
2490
- let modules = [];
2491
- for (const m of registry.listModules()) {
2492
- modules.push(m);
2493
- }
2494
- if (opts.tag.length > 0) {
2495
- const filterTags = new Set(opts.tag);
2496
- modules = modules.filter((m) => {
2497
- const mTags = m.tags ?? [];
2498
- return [...filterTags].every((t) => mTags.includes(t));
2499
- });
2500
- }
2501
- if (opts.search) {
2502
- const query = opts.search.toLowerCase();
2503
- modules = modules.filter(
2504
- (m) => (m.id ?? "").toLowerCase().includes(query) || (m.description ?? "").toLowerCase().includes(query)
2446
+ var _auditLogger, AuditLogger;
2447
+ var init_audit = __esm({
2448
+ "src/security/audit.ts"() {
2449
+ "use strict";
2450
+ init_esm_shims();
2451
+ init_logger();
2452
+ _auditLogger = null;
2453
+ AuditLogger = class _AuditLogger {
2454
+ static DEFAULT_PATH = path3.join(
2455
+ os.homedir(),
2456
+ ".apcore-cli",
2457
+ "audit.jsonl"
2505
2458
  );
2506
- }
2507
- if (opts.status === "enabled") {
2508
- modules = modules.filter((m) => {
2509
- const enabled = m.enabled;
2510
- return enabled !== false;
2511
- });
2512
- } else if (opts.status === "disabled") {
2513
- modules = modules.filter((m) => {
2514
- const enabled = m.enabled;
2515
- return enabled === false;
2516
- });
2517
- }
2518
- if (!opts.deprecated) {
2519
- modules = modules.filter((m) => {
2520
- const deprecated = m.deprecated;
2521
- return deprecated !== true;
2522
- });
2523
- }
2524
- if (opts.annotation.length > 0) {
2525
- for (const annFlag of opts.annotation) {
2526
- modules = modules.filter((m) => getAnnotationFlag(m, annFlag));
2459
+ logPath;
2460
+ writeFailureWarned = false;
2461
+ constructor(path6) {
2462
+ this.logPath = path6 ?? _AuditLogger.DEFAULT_PATH;
2463
+ this.ensureDirectory();
2527
2464
  }
2528
- }
2529
- if (opts.sort === "calls" || opts.sort === "errors" || opts.sort === "latency") {
2530
- const { used } = sortModulesByUsage(modules, opts.sort, { reverse: !opts.reverse });
2531
- if (!used) {
2532
- process.stderr.write(
2533
- `note: no usage data available for --sort ${opts.sort}; sorted by id. Run some modules first to populate ~/.apcore-cli/audit.jsonl.
2534
- `
2535
- );
2465
+ ensureDirectory() {
2466
+ const dir = path3.dirname(this.logPath);
2467
+ try {
2468
+ fs3.mkdirSync(dir, { recursive: true });
2469
+ try {
2470
+ fs3.chmodSync(dir, 448);
2471
+ } catch {
2472
+ }
2473
+ } catch {
2474
+ }
2536
2475
  }
2537
- } else {
2538
- modules.sort((a, b) => (a.id ?? "").localeCompare(b.id ?? ""));
2539
- if (opts.reverse) {
2540
- modules.reverse();
2476
+ logExecution(moduleId, inputData, status, exitCode, durationMs) {
2477
+ const entry = {
2478
+ timestamp: (/* @__PURE__ */ new Date()).toISOString(),
2479
+ user: this.getUser(),
2480
+ module_id: moduleId,
2481
+ input_hash: this.hashInput(inputData),
2482
+ status,
2483
+ exit_code: exitCode,
2484
+ duration_ms: durationMs
2485
+ };
2486
+ try {
2487
+ fs3.appendFileSync(this.logPath, JSON.stringify(entry) + "\n");
2488
+ try {
2489
+ fs3.chmodSync(this.logPath, 384);
2490
+ } catch {
2491
+ }
2492
+ } catch (err) {
2493
+ if (!this.writeFailureWarned) {
2494
+ this.writeFailureWarned = true;
2495
+ warn(`Could not write audit log: ${err}`);
2496
+ }
2497
+ }
2541
2498
  }
2542
- }
2543
- let showExposureCol = false;
2544
- if (exposureFilter && opts.exposure !== "all") {
2545
- if (opts.exposure === "exposed") {
2546
- modules = modules.filter((m) => exposureFilter.isExposed(m.id ?? ""));
2547
- } else if (opts.exposure === "hidden") {
2548
- modules = modules.filter((m) => !exposureFilter.isExposed(m.id ?? ""));
2499
+ hashInput(inputData) {
2500
+ const salt = crypto.randomBytes(16);
2501
+ const payload = JSON.stringify(canonicalizeForHash(inputData));
2502
+ return crypto.createHash("sha256").update(Buffer.concat([salt, Buffer.from(payload, "utf-8")])).digest("hex");
2549
2503
  }
2550
- }
2551
- if (opts.exposure === "all" && exposureFilter) {
2552
- showExposureCol = true;
2553
- }
2554
- const fmt = resolveFormat(opts.format);
2555
- const filterTagsArg = opts.tag.length > 0 ? opts.tag : void 0;
2556
- void formatModuleList(modules, fmt, filterTagsArg, opts.deps, showExposureCol ? exposureFilter : void 0);
2557
- });
2558
- apcliGroup.addCommand(listCmd);
2559
- }
2560
- function registerDescribeCommand(apcliGroup, registry) {
2561
- const describeCmd = new Command2("describe").description("Show metadata, schema, and annotations for a module.").argument("<module-id>", "Module ID to describe").addOption(
2562
- new Option2("--format <format>", "Output format.").choices(["table", "json", "csv", "yaml", "jsonl", "markdown", "skill"])
2563
- ).action((moduleId, opts) => {
2564
- validateModuleId(moduleId);
2565
- const moduleDef = registry.getModule(moduleId);
2566
- if (!moduleDef) {
2567
- process.stderr.write(
2568
- `Error: Module '${moduleId}' not found.
2569
- `
2570
- );
2571
- process.exit(EXIT_CODES.MODULE_NOT_FOUND);
2572
- }
2573
- const fmt = resolveFormat(opts.format);
2574
- void formatModuleDetail(moduleDef, fmt);
2575
- });
2576
- apcliGroup.addCommand(describeCmd);
2577
- }
2578
- function registerExecCommand(apcliGroup, registry, executor) {
2579
- 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(
2580
- "--input <json>",
2581
- "JSON object passed as input to the module. Use '-' to read JSON from stdin."
2582
- ).option("-y, --yes", "Auto-approve if the module declares requires_approval.", false).option(
2583
- "--approval-timeout <seconds>",
2584
- "Seconds to wait for interactive approval.",
2585
- parseInt
2586
- ).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) => {
2587
- validateModuleId(moduleId);
2588
- const moduleDef = registry.getModule(moduleId);
2589
- if (!moduleDef) {
2590
- process.stderr.write(`Error: Module '${moduleId}' not found.
2591
- `);
2592
- process.exit(EXIT_CODES.MODULE_NOT_FOUND);
2593
- }
2594
- let merged = {};
2595
- if (opts.input === "-") {
2596
- merged = await collectInput("-", {}, false);
2597
- } else if (opts.input !== void 0) {
2598
- try {
2599
- const parsed = JSON.parse(opts.input);
2600
- if (typeof parsed !== "object" || parsed === null || Array.isArray(parsed)) {
2601
- process.stderr.write("Error: --input JSON must be an object.\n");
2602
- process.exit(EXIT_CODES.INVALID_CLI_INPUT);
2504
+ getUser() {
2505
+ try {
2506
+ return os.userInfo().username;
2507
+ } catch {
2508
+ return process.env.USER ?? process.env.LOGNAME ?? process.env.USERNAME ?? "unknown";
2603
2509
  }
2604
- merged = parsed;
2605
- } catch (err) {
2606
- const msg = err instanceof Error ? err.message : String(err);
2607
- process.stderr.write(`Error: --input is not valid JSON: ${msg}
2608
- `);
2609
- process.exit(EXIT_CODES.INVALID_CLI_INPUT);
2610
2510
  }
2611
- }
2612
- const startTime = performance.now();
2511
+ };
2512
+ }
2513
+ });
2514
+
2515
+ // src/system-usage.ts
2516
+ import * as fs4 from "fs";
2517
+ import * as os2 from "os";
2518
+ import * as path4 from "path";
2519
+ function computeSummary(options = {}) {
2520
+ const auditPath = options.auditPath ?? DEFAULT_AUDIT_PATH;
2521
+ const period = options.period ?? "24h";
2522
+ const cutoff = (options.now ?? /* @__PURE__ */ new Date()).getTime() - PERIOD_TO_MS[period];
2523
+ if (!fs4.existsSync(auditPath)) {
2524
+ return /* @__PURE__ */ new Map();
2525
+ }
2526
+ let raw;
2527
+ try {
2528
+ raw = fs4.readFileSync(auditPath, "utf-8");
2529
+ } catch {
2530
+ return /* @__PURE__ */ new Map();
2531
+ }
2532
+ const counts = /* @__PURE__ */ new Map();
2533
+ const errors = /* @__PURE__ */ new Map();
2534
+ const latencySum = /* @__PURE__ */ new Map();
2535
+ for (const line of raw.split("\n")) {
2536
+ const trimmed = line.trim();
2537
+ if (!trimmed) continue;
2538
+ let entry;
2613
2539
  try {
2614
- await checkApproval(moduleDef, opts.yes, opts.approvalTimeout);
2615
- if (opts.dryRun) {
2616
- if (executor.validate) {
2617
- const preflight = await executor.validate(moduleId, merged);
2618
- formatPreflightResult(preflight, opts.format);
2619
- } else {
2620
- process.stdout.write(JSON.stringify({ valid: true }) + "\n");
2621
- }
2622
- return;
2623
- }
2624
- let result;
2625
- if ((opts.trace || opts.strategy) && executor.callWithTrace) {
2626
- const [res] = await executor.callWithTrace(moduleId, merged, { strategy: opts.strategy });
2627
- result = res;
2628
- } else {
2629
- const { Sandbox: Sandbox2 } = await Promise.resolve().then(() => (init_security(), security_exports));
2630
- const sandbox = new Sandbox2(opts.sandbox);
2631
- result = await sandbox.execute(moduleId, merged, executor);
2632
- }
2633
- const durationMs = Math.round(performance.now() - startTime);
2634
- const fmt = resolveFormat(opts.format);
2635
- formatExecResult(result, fmt, opts.fields);
2636
- const auditLogger = getAuditLogger();
2637
- if (auditLogger) {
2638
- auditLogger.logExecution(moduleId, merged, "success", 0, durationMs);
2639
- }
2640
- } catch (err) {
2641
- const exitCode = exitCodeForError(err);
2642
- const durationMs = Math.round(performance.now() - startTime);
2643
- try {
2644
- const auditLogger = getAuditLogger();
2645
- if (auditLogger) {
2646
- auditLogger.logExecution(moduleId, merged, "error", exitCode, durationMs);
2647
- }
2648
- } catch {
2649
- }
2650
- process.stderr.write(`Error: ${err instanceof Error ? err.message : err}
2651
- `);
2652
- process.exit(exitCode);
2653
- }
2654
- });
2655
- apcliGroup.addCommand(execCmd);
2656
- }
2657
- function registerValidateCommand(cli, registry, executor) {
2658
- 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) => {
2659
- validateModuleId(moduleId);
2660
- const moduleDef = registry.getModule(moduleId);
2661
- if (!moduleDef) {
2662
- process.stderr.write(`Error: Module '${moduleId}' not found.
2663
- `);
2664
- process.exit(EXIT_CODES.MODULE_NOT_FOUND);
2540
+ entry = JSON.parse(trimmed);
2541
+ } catch {
2542
+ continue;
2665
2543
  }
2666
- const merged = opts.input ? await collectInput(opts.input, {}, false) : {};
2667
- if (!executor.validate) {
2668
- process.stderr.write("Error: Executor does not support validate.\n");
2669
- process.exit(EXIT_CODES.MODULE_EXECUTE_ERROR);
2544
+ const ts = typeof entry.timestamp === "string" ? Date.parse(entry.timestamp) : NaN;
2545
+ if (Number.isNaN(ts) || ts < cutoff) continue;
2546
+ const moduleId = typeof entry.module_id === "string" ? entry.module_id : null;
2547
+ if (!moduleId) continue;
2548
+ counts.set(moduleId, (counts.get(moduleId) ?? 0) + 1);
2549
+ if (entry.status === "error") {
2550
+ errors.set(moduleId, (errors.get(moduleId) ?? 0) + 1);
2670
2551
  }
2671
- try {
2672
- const preflight = await executor.validate(moduleId, merged);
2673
- formatPreflightResult(preflight, opts.format);
2674
- process.exit(preflight.valid ? 0 : firstFailedExitCode(preflight));
2675
- } catch (err) {
2676
- const exitCode = exitCodeForError(err);
2677
- try {
2678
- const auditLogger = getAuditLogger();
2679
- if (auditLogger) {
2680
- auditLogger.logExecution(moduleId, merged, "error", exitCode, 0);
2681
- }
2682
- } catch {
2683
- }
2684
- process.stderr.write(`Error: ${err instanceof Error ? err.message : err}
2685
- `);
2686
- process.exit(exitCode);
2552
+ const duration = entry.duration_ms;
2553
+ if (typeof duration === "number") {
2554
+ latencySum.set(moduleId, (latencySum.get(moduleId) ?? 0) + duration);
2687
2555
  }
2688
- });
2689
- cli.addCommand(validateCmd);
2690
- }
2691
- var TAG_PATTERN;
2692
- var init_discovery = __esm({
2693
- "src/discovery.ts"() {
2694
- "use strict";
2695
- init_esm_shims();
2696
- init_approval();
2697
- init_errors();
2698
- init_main();
2699
- init_output();
2700
- init_audit();
2701
- init_system_usage();
2702
- TAG_PATTERN = /^[a-z][a-z0-9_-]*$/;
2703
2556
  }
2704
- });
2705
-
2706
- // src/system-cmd.ts
2707
- import { Command as Command3 } from "commander";
2708
- async function callSystemModule(executor, moduleId, inputs) {
2709
- if (executor.call) {
2710
- return executor.call(moduleId, inputs);
2557
+ const out = /* @__PURE__ */ new Map();
2558
+ for (const [id, calls] of counts) {
2559
+ out.set(id, {
2560
+ module_id: id,
2561
+ calls,
2562
+ errors: errors.get(id) ?? 0,
2563
+ latency_ms: calls > 0 ? (latencySum.get(id) ?? 0) / calls : 0
2564
+ });
2711
2565
  }
2712
- return executor.execute(moduleId, inputs);
2566
+ return out;
2713
2567
  }
2714
- function emitResult(jsonPayload, fmt, ttyRender) {
2715
- if (fmt === "json" || !process.stdout.isTTY) {
2716
- process.stdout.write(JSON.stringify(jsonPayload, null, 2) + "\n");
2717
- } else {
2718
- ttyRender();
2568
+ function sortModulesByUsage(modules, field, options = {}) {
2569
+ const reverse = options.reverse ?? true;
2570
+ const summary = computeSummary({
2571
+ auditPath: options.auditPath,
2572
+ period: options.period
2573
+ });
2574
+ const idOf = (m) => m.moduleId ?? m.id ?? m.module_id ?? "";
2575
+ if (summary.size === 0) {
2576
+ modules.sort((a, b) => idOf(a).localeCompare(idOf(b)));
2577
+ if (reverse) modules.reverse();
2578
+ return { used: false };
2719
2579
  }
2720
- }
2721
- function emitErrorAndExit(e) {
2722
- process.stderr.write(`Error: ${e instanceof Error ? e.message : e}
2723
- `);
2724
- process.exit(exitCodeForError(e));
2725
- }
2726
- async function requireApprovalForSystemCommand(moduleId, autoApprove) {
2727
- const syntheticModuleDef = {
2728
- id: moduleId,
2729
- name: moduleId,
2730
- description: `system command: ${moduleId}`,
2731
- annotations: { requires_approval: true }
2580
+ const key = (m) => {
2581
+ const id = idOf(m);
2582
+ const s = summary.get(id);
2583
+ if (!s) return 0;
2584
+ return field === "latency" ? s.latency_ms : field === "calls" ? s.calls : s.errors;
2732
2585
  };
2733
- await checkApproval(syntheticModuleDef, autoApprove, void 0);
2586
+ modules.sort((a, b) => {
2587
+ const diff = key(a) - key(b);
2588
+ if (diff !== 0) return reverse ? -diff : diff;
2589
+ return idOf(a).localeCompare(idOf(b));
2590
+ });
2591
+ return { used: true };
2734
2592
  }
2735
- function formatHealthSummaryTty(result) {
2736
- const summary = result.summary ?? {};
2737
- const modules = result.modules ?? [];
2738
- if (modules.length === 0) {
2739
- process.stdout.write("No modules found.\n");
2740
- return;
2741
- }
2742
- const total = summary.total_modules ?? modules.length;
2743
- process.stdout.write(`Health Overview (${total} modules)
2744
-
2745
- `);
2746
- process.stdout.write(` ${"Module".padEnd(28)} ${"Status".padEnd(12)} ${"Error Rate".padEnd(12)} Top Error
2747
- `);
2748
- process.stdout.write(` ${"-".repeat(28)} ${"-".repeat(12)} ${"-".repeat(12)} ${"-".repeat(20)}
2749
- `);
2750
- for (const m of modules) {
2751
- const top = m.top_error;
2752
- const topStr = top ? `${top.code} (${top.count ?? "?"})` : "\u2014";
2753
- const rate = `${((m.error_rate ?? 0) * 100).toFixed(1)}%`;
2754
- process.stdout.write(
2755
- ` ${String(m.module_id).padEnd(28)} ${String(m.status).padEnd(12)} ${rate.padEnd(12)} ${topStr}
2756
- `
2593
+ var PERIOD_TO_MS, DEFAULT_AUDIT_PATH;
2594
+ var init_system_usage = __esm({
2595
+ "src/system-usage.ts"() {
2596
+ "use strict";
2597
+ init_esm_shims();
2598
+ PERIOD_TO_MS = {
2599
+ "1h": 60 * 60 * 1e3,
2600
+ "24h": 24 * 60 * 60 * 1e3,
2601
+ "7d": 7 * 24 * 60 * 60 * 1e3,
2602
+ "30d": 30 * 24 * 60 * 60 * 1e3
2603
+ };
2604
+ DEFAULT_AUDIT_PATH = path4.join(
2605
+ os2.homedir(),
2606
+ ".apcore-cli",
2607
+ "audit.jsonl"
2757
2608
  );
2758
2609
  }
2759
- const parts = [];
2760
- for (const key of ["healthy", "degraded", "error"]) {
2761
- const count = summary[key];
2762
- if (count) parts.push(`${count} ${key}`);
2763
- }
2764
- process.stdout.write(`
2765
- Summary: ${parts.join(", ") || "no data"}
2766
- `);
2767
- }
2768
- function formatHealthModuleTty(result) {
2769
- process.stdout.write(`Module: ${result.module_id ?? "?"}
2770
- `);
2771
- process.stdout.write(`Status: ${result.status ?? "unknown"}
2772
- `);
2773
- const total = result.total_calls ?? 0;
2774
- const errors = result.error_count ?? 0;
2775
- const rate = result.error_rate ?? 0;
2776
- const avg = result.avg_latency_ms ?? 0;
2777
- const p99 = result.p99_latency_ms ?? 0;
2778
- process.stdout.write(`Calls: ${total.toLocaleString()} total | ${errors.toLocaleString()} errors | ${(rate * 100).toFixed(1)}% error rate
2779
- `);
2780
- process.stdout.write(`Latency: ${avg.toFixed(0)}ms avg | ${p99.toFixed(0)}ms p99
2781
- `);
2782
- const recent = result.recent_errors ?? [];
2783
- if (recent.length > 0) {
2784
- process.stdout.write(`
2785
- Recent Errors (top ${recent.length}):
2786
- `);
2787
- for (const e of recent) {
2788
- const count = e.count ?? "?";
2789
- const last = e.last_occurred ?? "?";
2790
- process.stdout.write(` ${String(e.code ?? "?").padEnd(24)} x${count} (last: ${last})
2791
- `);
2792
- }
2610
+ });
2611
+
2612
+ // src/security/config-encryptor.ts
2613
+ import * as crypto2 from "crypto";
2614
+ import * as os3 from "os";
2615
+ async function getKeytar() {
2616
+ if (keytarModule) return keytarModule;
2617
+ try {
2618
+ keytarModule = await import("keytar");
2619
+ return keytarModule;
2620
+ } catch {
2621
+ return null;
2793
2622
  }
2794
2623
  }
2795
- function formatUsageSummaryTty(result) {
2796
- const modules = result.modules ?? [];
2797
- const period = result.period ?? "?";
2798
- if (modules.length === 0) {
2799
- process.stdout.write(`No usage data for period ${period}.
2800
- `);
2801
- return;
2802
- }
2803
- process.stdout.write(`Usage Summary (last ${period})
2804
-
2805
- `);
2806
- process.stdout.write(` ${"Module".padEnd(24)} ${"Calls".padStart(8)} ${"Errors".padStart(8)} ${"Avg Latency".padStart(12)} ${"Trend".padStart(10)}
2807
- `);
2808
- process.stdout.write(` ${"-".repeat(24)} ${"-".repeat(8)} ${"-".repeat(8)} ${"-".repeat(12)} ${"-".repeat(10)}
2809
- `);
2810
- for (const m of modules) {
2811
- const avg = `${(m.avg_latency_ms ?? 0).toFixed(0)}ms`;
2812
- process.stdout.write(
2813
- ` ${String(m.module_id).padEnd(24)} ${String(m.call_count ?? 0).padStart(8)} ${String(m.error_count ?? 0).padStart(8)} ${avg.padStart(12)} ${String(m.trend ?? "").padStart(10)}
2814
- `
2815
- );
2816
- }
2817
- const totalCalls = result.total_calls ?? modules.reduce((s, m) => s + (m.call_count ?? 0), 0);
2818
- const totalErrors = result.total_errors ?? modules.reduce((s, m) => s + (m.error_count ?? 0), 0);
2819
- process.stdout.write(`
2820
- Total: ${totalCalls.toLocaleString()} calls | ${totalErrors.toLocaleString()} errors
2821
- `);
2822
- }
2823
- function registerHealthCommand(apcliGroup, executor) {
2824
- 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) => {
2825
- const fmt = resolveFormat(opts.format);
2826
- try {
2827
- if (moduleId) {
2828
- const result = await callSystemModule(executor, "system.health.module", {
2829
- module_id: moduleId,
2830
- error_limit: opts.errors
2831
- });
2832
- emitResult(result, fmt, () => formatHealthModuleTty(result));
2833
- } else {
2834
- const result = await callSystemModule(executor, "system.health.summary", {
2835
- error_rate_threshold: opts.threshold,
2836
- include_healthy: opts.all
2837
- });
2838
- emitResult(result, fmt, () => formatHealthSummaryTty(result));
2839
- }
2840
- } catch (e) {
2841
- emitErrorAndExit(e);
2842
- }
2843
- });
2844
- apcliGroup.addCommand(healthCmd);
2845
- }
2846
- function registerUsageCommand(apcliGroup, executor) {
2847
- 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) => {
2848
- const fmt = resolveFormat(opts.format);
2849
- try {
2850
- let result;
2851
- if (moduleId) {
2852
- result = await callSystemModule(executor, "system.usage.module", {
2853
- module_id: moduleId,
2854
- period: opts.period
2855
- });
2856
- } else {
2857
- result = await callSystemModule(executor, "system.usage.summary", {
2858
- period: opts.period
2859
- });
2860
- }
2861
- emitResult(result, fmt, () => {
2862
- if (moduleId) {
2863
- formatExecResult(result, fmt);
2864
- } else {
2865
- formatUsageSummaryTty(result);
2866
- }
2867
- });
2868
- } catch (e) {
2869
- emitErrorAndExit(e);
2870
- }
2871
- });
2872
- apcliGroup.addCommand(usageCmd);
2873
- }
2874
- function registerEnableCommand(apcliGroup, executor) {
2875
- 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) => {
2876
- const fmt = resolveFormat(opts.format);
2877
- try {
2878
- await requireApprovalForSystemCommand("system.control.toggle_feature", opts.yes);
2879
- const result = await callSystemModule(executor, "system.control.toggle_feature", {
2880
- module_id: moduleId,
2881
- enabled: true,
2882
- reason: opts.reason
2883
- });
2884
- emitResult(result, fmt, () => {
2885
- process.stdout.write(`Module '${moduleId}' enabled.
2886
- Reason: ${opts.reason}
2887
- `);
2888
- });
2889
- } catch (e) {
2890
- emitErrorAndExit(e);
2891
- }
2892
- });
2893
- apcliGroup.addCommand(enableCmd);
2894
- }
2895
- function registerDisableCommand(apcliGroup, executor) {
2896
- 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) => {
2897
- const fmt = resolveFormat(opts.format);
2898
- try {
2899
- await requireApprovalForSystemCommand("system.control.toggle_feature", opts.yes);
2900
- const result = await callSystemModule(executor, "system.control.toggle_feature", {
2901
- module_id: moduleId,
2902
- enabled: false,
2903
- reason: opts.reason
2904
- });
2905
- emitResult(result, fmt, () => {
2906
- process.stdout.write(`Module '${moduleId}' disabled.
2907
- Reason: ${opts.reason}
2908
- `);
2909
- });
2910
- } catch (e) {
2911
- emitErrorAndExit(e);
2912
- }
2913
- });
2914
- apcliGroup.addCommand(disableCmd);
2915
- }
2916
- function registerReloadCommand(apcliGroup, executor) {
2917
- 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) => {
2918
- const fmt = resolveFormat(opts.format);
2919
- try {
2920
- await requireApprovalForSystemCommand("system.control.reload_module", opts.yes);
2921
- const result = await callSystemModule(executor, "system.control.reload_module", {
2922
- module_id: moduleId,
2923
- reason: opts.reason
2924
- });
2925
- emitResult(result, fmt, () => {
2926
- const prev = result.previous_version ?? "?";
2927
- const newVer = result.new_version ?? "?";
2928
- const dur = result.reload_duration_ms ?? "?";
2929
- process.stdout.write(`Module '${moduleId}' reloaded.
2930
- `);
2931
- process.stdout.write(` Version: ${prev} -> ${newVer}
2932
- `);
2933
- process.stdout.write(` Duration: ${dur}ms
2934
- `);
2935
- });
2936
- } catch (e) {
2937
- emitErrorAndExit(e);
2938
- }
2939
- });
2940
- apcliGroup.addCommand(reloadCmd);
2941
- }
2942
- function registerConfigCommand(apcliGroup, executor) {
2943
- const configGroup = new Command3("config").description("Read or update runtime configuration.");
2944
- 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) => {
2945
- const fmt = resolveFormat(opts.format);
2946
- try {
2947
- const result = await callSystemModule(executor, "system.config.get", { key });
2948
- const value = result?.value ?? result;
2949
- emitResult({ key, value }, fmt, () => {
2950
- process.stdout.write(`${key} = ${JSON.stringify(value)}
2951
- `);
2952
- });
2953
- } catch (e) {
2954
- emitErrorAndExit(e);
2955
- }
2956
- });
2957
- configGroup.addCommand(configGetCmd);
2958
- 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) => {
2959
- const fmt = resolveFormat(opts.format);
2960
- let parsedValue;
2961
- try {
2962
- parsedValue = JSON.parse(value);
2963
- } catch {
2964
- parsedValue = value;
2965
- }
2966
- try {
2967
- await requireApprovalForSystemCommand("system.control.update_config", opts.yes);
2968
- const result = await callSystemModule(executor, "system.control.update_config", {
2969
- key,
2970
- value: parsedValue,
2971
- reason: opts.reason
2972
- });
2973
- emitResult(result, fmt, () => {
2974
- const old = result.old_value ?? "?";
2975
- const newVal = result.new_value ?? "?";
2976
- process.stdout.write(`Config updated: ${key}
2977
- `);
2978
- process.stdout.write(` ${JSON.stringify(old)} -> ${JSON.stringify(newVal)}
2979
- `);
2980
- process.stdout.write(` Reason: ${opts.reason}
2981
- `);
2982
- });
2983
- } catch (e) {
2984
- emitErrorAndExit(e);
2985
- }
2986
- });
2987
- configGroup.addCommand(configSetCmd);
2988
- apcliGroup.addCommand(configGroup);
2989
- }
2990
- var init_system_cmd = __esm({
2991
- "src/system-cmd.ts"() {
2624
+ var PBKDF2_ITERATIONS, V1_STATIC_SALT, keytarModule, ConfigEncryptor;
2625
+ var init_config_encryptor = __esm({
2626
+ "src/security/config-encryptor.ts"() {
2992
2627
  "use strict";
2993
2628
  init_esm_shims();
2994
- init_approval();
2995
2629
  init_errors();
2996
- init_output();
2630
+ init_logger();
2631
+ PBKDF2_ITERATIONS = 6e5;
2632
+ V1_STATIC_SALT = Buffer.from("apcore-cli-config-v1");
2633
+ keytarModule = null;
2634
+ ConfigEncryptor = class _ConfigEncryptor {
2635
+ static SERVICE_NAME = "apcore-cli";
2636
+ // One-shot flag so the "obfuscation only" warning fires exactly once
2637
+ // per process instead of once per encrypt/decrypt call.
2638
+ static weakFallbackWarned = false;
2639
+ /**
2640
+ * Encrypt and store a configuration value.
2641
+ *
2642
+ * Cross-SDK contract (D10-003, 2026-04-26): when the OS keyring is
2643
+ * detected as available but `setPassword` then throws (locked keyring,
2644
+ * transient backend failure, permission revoked, etc.), the error is
2645
+ * propagated wrapped in a `ConfigDecryptionError`. Previously TS
2646
+ * caught the exception and silently fell through to AES file encryption
2647
+ * — a quiet downgrade that surprised users who expected a hard failure.
2648
+ * Python lets the keyring exception propagate raw; Rust returns
2649
+ * `ConfigDecryptionError::KeyringError`. The fall-through to AES is
2650
+ * still reached when `getKeytar()` returns `null` (keyring
2651
+ * genuinely unavailable on this platform / install).
2652
+ */
2653
+ async store(key, value) {
2654
+ const keytar = await getKeytar();
2655
+ if (keytar) {
2656
+ try {
2657
+ await keytar.setPassword(_ConfigEncryptor.SERVICE_NAME, key, value);
2658
+ return `keyring:${key}`;
2659
+ } catch (err) {
2660
+ const detail = err instanceof Error ? err.message : String(err);
2661
+ throw new ConfigDecryptionError(
2662
+ `Failed to store '${key}' in OS keyring: ${detail}. Unset APCORE_CLI_CONFIG_PASSPHRASE-aware backends or unlock the keyring before retrying.`
2663
+ );
2664
+ }
2665
+ }
2666
+ warn("OS keyring unavailable. Using file-based encryption.");
2667
+ const ciphertext = this.aesEncrypt(value);
2668
+ return `enc:v2:${ciphertext.toString("base64")}`;
2669
+ }
2670
+ /**
2671
+ * Retrieve and decrypt a configuration value.
2672
+ */
2673
+ async retrieve(configValue, key) {
2674
+ if (configValue.startsWith("keyring:")) {
2675
+ const keytar = await getKeytar();
2676
+ if (!keytar) {
2677
+ throw new ConfigDecryptionError(
2678
+ `Keyring module not available to retrieve '${key}'.`
2679
+ );
2680
+ }
2681
+ try {
2682
+ const refKey = configValue.slice("keyring:".length);
2683
+ const result = await keytar.getPassword(
2684
+ _ConfigEncryptor.SERVICE_NAME,
2685
+ refKey
2686
+ );
2687
+ if (result === null || result === void 0) {
2688
+ throw new ConfigDecryptionError(
2689
+ `Keyring entry not found for '${refKey}'.`
2690
+ );
2691
+ }
2692
+ return result;
2693
+ } catch (err) {
2694
+ if (err instanceof ConfigDecryptionError) throw err;
2695
+ throw new ConfigDecryptionError(
2696
+ `Failed to retrieve from keyring: ${err}`
2697
+ );
2698
+ }
2699
+ }
2700
+ if (configValue.startsWith("enc:v2:")) {
2701
+ const data = Buffer.from(configValue.slice("enc:v2:".length), "base64");
2702
+ try {
2703
+ return this.aesDecrypt(data);
2704
+ } catch {
2705
+ throw new ConfigDecryptionError(
2706
+ `Failed to decrypt configuration value '${key}'. Re-configure with 'apcore-cli config set ${key}'.`
2707
+ );
2708
+ }
2709
+ }
2710
+ if (configValue.startsWith("enc:")) {
2711
+ const data = Buffer.from(configValue.slice("enc:".length), "base64");
2712
+ try {
2713
+ return this.aesDecryptV1(data);
2714
+ } catch {
2715
+ throw new ConfigDecryptionError(
2716
+ `Failed to decrypt configuration value '${key}'. Re-configure with 'apcore-cli config set ${key}'.`
2717
+ );
2718
+ }
2719
+ }
2720
+ return configValue;
2721
+ }
2722
+ // Derive an AES-256 key with a provided salt (v2 format).
2723
+ //
2724
+ // Order of preference:
2725
+ // 1. APCORE_CLI_CONFIG_PASSPHRASE env var — a real secret supplied by
2726
+ // the user; produces a key an attacker with filesystem read cannot
2727
+ // reconstruct without also knowing the passphrase.
2728
+ // 2. hostname + username — obfuscation-only derivation for backward
2729
+ // compatibility. Emits a loud stderr warning on first use so
2730
+ // operators know the stored value is NOT protected against a
2731
+ // filesystem-read attacker.
2732
+ deriveKey(salt) {
2733
+ const passphrase = process.env.APCORE_CLI_CONFIG_PASSPHRASE;
2734
+ if (passphrase && passphrase.length > 0) {
2735
+ return crypto2.pbkdf2Sync(passphrase, salt, PBKDF2_ITERATIONS, 32, "sha256");
2736
+ }
2737
+ if (!_ConfigEncryptor.weakFallbackWarned) {
2738
+ warn(
2739
+ "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."
2740
+ );
2741
+ _ConfigEncryptor.weakFallbackWarned = true;
2742
+ }
2743
+ const hostname2 = os3.hostname();
2744
+ const username = process.env.USER ?? process.env.LOGNAME ?? process.env.USERNAME ?? "unknown";
2745
+ const material = `${hostname2}:${username}`;
2746
+ return crypto2.pbkdf2Sync(material, salt, PBKDF2_ITERATIONS, 32, "sha256");
2747
+ }
2748
+ aesEncrypt(plaintext) {
2749
+ const salt = crypto2.randomBytes(16);
2750
+ const key = this.deriveKey(salt);
2751
+ const nonce = crypto2.randomBytes(12);
2752
+ const cipher = crypto2.createCipheriv("aes-256-gcm", key, nonce);
2753
+ const ct = Buffer.concat([
2754
+ cipher.update(plaintext, "utf-8"),
2755
+ cipher.final()
2756
+ ]);
2757
+ const tag = cipher.getAuthTag();
2758
+ return Buffer.concat([salt, nonce, tag, ct]);
2759
+ }
2760
+ aesDecrypt(data) {
2761
+ const salt = data.subarray(0, 16);
2762
+ const nonce = data.subarray(16, 28);
2763
+ const tag = data.subarray(28, 44);
2764
+ const ct = data.subarray(44);
2765
+ const key = this.deriveKey(salt);
2766
+ const decipher = crypto2.createDecipheriv("aes-256-gcm", key, nonce);
2767
+ decipher.setAuthTag(tag);
2768
+ return Buffer.concat([decipher.update(ct), decipher.final()]).toString("utf-8");
2769
+ }
2770
+ /** Decrypt legacy v1-format values: nonce(12)+tag(16)+ct, static salt. */
2771
+ aesDecryptV1(data) {
2772
+ const nonce = data.subarray(0, 12);
2773
+ const tag = data.subarray(12, 28);
2774
+ const ct = data.subarray(28);
2775
+ const hostname2 = os3.hostname();
2776
+ const username = process.env.USER ?? process.env.LOGNAME ?? process.env.USERNAME ?? "unknown";
2777
+ const passphrase = process.env.APCORE_CLI_CONFIG_PASSPHRASE;
2778
+ const materials = passphrase ? [passphrase, `${hostname2}:${username}`] : [`${hostname2}:${username}`];
2779
+ for (const material of materials) {
2780
+ for (const iterations of [6e5, 1e5]) {
2781
+ try {
2782
+ const key = crypto2.pbkdf2Sync(material, V1_STATIC_SALT, iterations, 32, "sha256");
2783
+ const decipher = crypto2.createDecipheriv("aes-256-gcm", key, nonce);
2784
+ decipher.setAuthTag(tag);
2785
+ return Buffer.concat([decipher.update(ct), decipher.final()]).toString("utf-8");
2786
+ } catch {
2787
+ }
2788
+ }
2789
+ }
2790
+ throw new Error("v1 decryption failed with all material+iteration combinations");
2791
+ }
2792
+ };
2997
2793
  }
2998
2794
  });
2999
2795
 
3000
- // src/strategy.ts
3001
- import { Command as Command4, Option as Option3 } from "commander";
3002
- function lookupStrategyInfo(executor, strategyName) {
3003
- if (typeof executor.describePipeline === "function") {
3004
- try {
3005
- const current = executor.describePipeline();
3006
- if (current && current.name === strategyName) {
3007
- return { info: current, isCurrent: true };
2796
+ // src/security/auth.ts
2797
+ var AuthProvider;
2798
+ var init_auth = __esm({
2799
+ "src/security/auth.ts"() {
2800
+ "use strict";
2801
+ init_esm_shims();
2802
+ init_errors();
2803
+ init_config_encryptor();
2804
+ AuthProvider = class {
2805
+ config;
2806
+ _encryptor;
2807
+ constructor(config, encryptor) {
2808
+ this.config = config;
2809
+ this._encryptor = encryptor;
2810
+ }
2811
+ /**
2812
+ * Resolve the active ConfigEncryptor instance.
2813
+ *
2814
+ * D11-005 (2026-05-12): three-tier fallback chain matching Python's
2815
+ * `_get_encryptor` (auth.py:33): explicit constructor arg > peer attribute
2816
+ * `config.encryptor` (set by embedders injecting forced-AES test fixtures
2817
+ * or shared instances) > fresh `new ConfigEncryptor()`. Previously TS
2818
+ * skipped the peer-attribute tier, silently giving embedders a different
2819
+ * encryptor than the one they wired on the config.
2820
+ */
2821
+ getEncryptor() {
2822
+ if (this._encryptor) return this._encryptor;
2823
+ const fromConfig = this.config.encryptor;
2824
+ if (fromConfig) return fromConfig;
2825
+ return new ConfigEncryptor();
2826
+ }
2827
+ /**
2828
+ * Retrieve the API key from the configured sources.
2829
+ * Handles keyring: and enc: prefixes via ConfigEncryptor.
2830
+ */
2831
+ async getApiKey() {
2832
+ const result = this.config.resolve(
2833
+ "auth.api_key",
2834
+ "--api-key",
2835
+ "APCORE_AUTH_API_KEY"
2836
+ );
2837
+ if (result === null || result === void 0) {
2838
+ return null;
2839
+ }
2840
+ const strResult = String(result);
2841
+ if (strResult.startsWith("keyring:") || strResult.startsWith("enc:")) {
2842
+ try {
2843
+ return await this.getEncryptor().retrieve(strResult, "auth.api_key");
2844
+ } catch (err) {
2845
+ if (err instanceof ConfigDecryptionError) {
2846
+ throw new AuthenticationError(
2847
+ "Failed to decrypt stored API key. Re-store with 'apcli config set auth.api_key'."
2848
+ );
2849
+ }
2850
+ throw err;
2851
+ }
2852
+ }
2853
+ return strResult;
2854
+ }
2855
+ /**
2856
+ * Add authentication headers to an outgoing request.
2857
+ *
2858
+ * Cross-SDK contract (D10-002, 2026-04-26): the input `headers` object
2859
+ * is mutated **in place** and the same reference is returned. Callers
2860
+ * that share the headers reference (the documented pattern in
2861
+ * apcore-cli/docs/features/security.md §AuthProvider) can read
2862
+ * `headers.Authorization` after the call without re-binding the
2863
+ * return value. Python and Rust both mutate-and-return; TS previously
2864
+ * spread into a new object, which silently broke shared-reference
2865
+ * callers.
2866
+ */
2867
+ async authenticateRequest(headers) {
2868
+ const key = await this.getApiKey();
2869
+ if (!key) {
2870
+ throw new AuthenticationError(
2871
+ "Remote registry requires authentication. Set --api-key, APCORE_AUTH_API_KEY, or auth.api_key in config."
2872
+ );
2873
+ }
2874
+ if (/[\r\n]/.test(key)) {
2875
+ throw new AuthenticationError(
2876
+ "Malformed API key: contains invalid characters (CR/LF). Re-store with 'apcli config set auth.api_key'."
2877
+ );
2878
+ }
2879
+ headers.Authorization = `Bearer ${key.trim()}`;
2880
+ return headers;
3008
2881
  }
3009
- } catch {
3010
- }
3011
- }
3012
- const ctor = executor.constructor;
3013
- if (ctor && typeof ctor.listStrategies === "function") {
3014
- try {
3015
- const all = ctor.listStrategies();
3016
- const info = all.find((s) => s.name === strategyName) ?? null;
3017
- return { info, isCurrent: false };
3018
- } catch {
3019
- return { info: null, isCurrent: false };
3020
- }
3021
- }
3022
- return { info: null, isCurrent: false };
3023
- }
3024
- function registerPipelineCommand(cli, executor) {
3025
- const pipelineCmd = new Command4("describe-pipeline").description("Show the execution pipeline steps for a strategy.").addOption(
3026
- new Option3("--strategy <name>", "Strategy to describe (default: standard).").choices(["standard", "internal", "testing", "performance", "minimal"]).default("standard")
3027
- ).option("--format <format>", "Output format.").action((opts) => {
3028
- const fmt = resolveFormat(opts.format);
3029
- const { info, isCurrent } = lookupStrategyInfo(executor, opts.strategy);
3030
- if (info) {
3031
- const strategySteps = isCurrent ? executor.currentStrategy?.steps ?? [] : [];
3032
- const header = `Pipeline: ${info.name} (${info.stepCount} steps)`;
3033
- if (fmt === "json" || !process.stdout.isTTY) {
3034
- const payload = {
3035
- strategy: info.name,
3036
- step_count: info.stepCount,
3037
- description: info.description,
3038
- steps: info.stepNames.map((name, i) => {
3039
- const stepMeta = strategySteps[i];
3040
- return {
3041
- index: i + 1,
3042
- name,
3043
- pure: stepMeta?.pure ?? false,
3044
- removable: stepMeta?.removable ?? true,
3045
- timeout_ms: stepMeta?.timeoutMs ?? null
3046
- };
3047
- })
3048
- };
3049
- process.stdout.write(JSON.stringify(payload, null, 2) + "\n");
3050
- } else {
3051
- process.stdout.write(`${header}
3052
-
3053
- `);
3054
- process.stdout.write(` ${"#".padEnd(4)} ${"Step".padEnd(28)} ${"Pure".padEnd(6)} ${"Removable".padEnd(11)} Timeout
3055
- `);
3056
- process.stdout.write(` ${"-".repeat(4)} ${"-".repeat(28)} ${"-".repeat(6)} ${"-".repeat(11)} ${"-".repeat(8)}
3057
- `);
3058
- for (let i = 0; i < info.stepNames.length; i++) {
3059
- const stepMeta = strategySteps[i];
3060
- const pure = stepMeta?.pure ? "yes" : "no";
3061
- const removable = stepMeta?.removable !== false ? "yes" : "no";
3062
- const timeout = stepMeta?.timeoutMs ? `${stepMeta.timeoutMs}ms` : "\u2014";
3063
- process.stdout.write(` ${String(i + 1).padEnd(4)} ${info.stepNames[i].padEnd(28)} ${pure.padEnd(6)} ${removable.padEnd(11)} ${timeout}
3064
- `);
2882
+ /**
2883
+ * Handle an HTTP response status code for auth-related errors.
2884
+ */
2885
+ handleResponse(statusCode) {
2886
+ if (statusCode === 401 || statusCode === 403) {
2887
+ throw new AuthenticationError(
2888
+ "Authentication failed. Verify your API key."
2889
+ );
3065
2890
  }
3066
2891
  }
3067
- return;
3068
- }
3069
- const steps = PRESET_STEPS[opts.strategy] ?? [];
3070
- const pureSteps = /* @__PURE__ */ new Set([
3071
- "context_creation",
3072
- "call_chain_guard",
3073
- "module_lookup",
3074
- "acl_check",
3075
- "input_validation"
3076
- ]);
3077
- const nonRemovable = /* @__PURE__ */ new Set([
3078
- "context_creation",
3079
- "module_lookup",
3080
- "execute",
3081
- "return_result"
3082
- ]);
3083
- if (fmt === "json" || !process.stdout.isTTY) {
3084
- const payload = {
3085
- strategy: opts.strategy,
3086
- step_count: steps.length,
3087
- steps: steps.map((s, i) => ({
3088
- index: i + 1,
3089
- name: s,
3090
- pure: pureSteps.has(s),
3091
- removable: !nonRemovable.has(s)
3092
- }))
3093
- };
3094
- process.stdout.write(JSON.stringify(payload, null, 2) + "\n");
3095
- } else {
3096
- process.stdout.write(`Pipeline: ${opts.strategy} (${steps.length} steps)
2892
+ };
2893
+ }
2894
+ });
3097
2895
 
3098
- `);
3099
- process.stdout.write(` ${"#".padEnd(4)} ${"Step".padEnd(28)} ${"Pure".padEnd(6)} ${"Removable".padEnd(11)} Timeout
3100
- `);
3101
- process.stdout.write(` ${"-".repeat(4)} ${"-".repeat(28)} ${"-".repeat(6)} ${"-".repeat(11)} ${"-".repeat(8)}
3102
- `);
3103
- for (let i = 0; i < steps.length; i++) {
3104
- const pure = pureSteps.has(steps[i]) ? "yes" : "no";
3105
- const removable = nonRemovable.has(steps[i]) ? "no" : "yes";
3106
- process.stdout.write(` ${String(i + 1).padEnd(4)} ${steps[i].padEnd(28)} ${pure.padEnd(6)} ${removable.padEnd(11)} \u2014
3107
- `);
3108
- }
3109
- }
3110
- });
3111
- cli.addCommand(pipelineCmd);
3112
- }
3113
- var PRESET_STEPS;
3114
- var init_strategy = __esm({
3115
- "src/strategy.ts"() {
2896
+ // src/security/index.ts
2897
+ var security_exports = {};
2898
+ __export(security_exports, {
2899
+ AuditLogger: () => AuditLogger,
2900
+ AuthProvider: () => AuthProvider,
2901
+ ConfigEncryptor: () => ConfigEncryptor,
2902
+ Sandbox: () => Sandbox,
2903
+ getAuditLogger: () => getAuditLogger,
2904
+ setAuditLogger: () => setAuditLogger
2905
+ });
2906
+ var init_security = __esm({
2907
+ "src/security/index.ts"() {
3116
2908
  "use strict";
3117
2909
  init_esm_shims();
3118
- init_output();
3119
- PRESET_STEPS = {
3120
- standard: [
3121
- "context_creation",
3122
- "call_chain_guard",
3123
- "module_lookup",
3124
- "acl_check",
3125
- "approval_gate",
3126
- "middleware_before",
3127
- "input_validation",
3128
- "execute",
3129
- "output_validation",
3130
- "middleware_after",
3131
- "return_result"
3132
- ],
3133
- internal: [
3134
- "context_creation",
3135
- "call_chain_guard",
3136
- "module_lookup",
3137
- "middleware_before",
3138
- "input_validation",
3139
- "execute",
3140
- "output_validation",
3141
- "middleware_after",
3142
- "return_result"
3143
- ],
3144
- testing: [
3145
- "context_creation",
3146
- "module_lookup",
3147
- "middleware_before",
3148
- "input_validation",
3149
- "execute",
3150
- "output_validation",
3151
- "middleware_after",
3152
- "return_result"
3153
- ],
3154
- performance: [
3155
- "context_creation",
3156
- "call_chain_guard",
3157
- "module_lookup",
3158
- "acl_check",
3159
- "approval_gate",
3160
- "input_validation",
3161
- "execute",
3162
- "output_validation",
3163
- "return_result"
3164
- ],
3165
- minimal: [
3166
- "context_creation",
3167
- "module_lookup",
3168
- "execute",
3169
- "return_result"
3170
- ]
3171
- };
2910
+ init_audit();
2911
+ init_auth();
2912
+ init_config_encryptor();
2913
+ init_sandbox();
3172
2914
  }
3173
2915
  });
3174
2916
 
3175
- // src/builtin-group.ts
3176
- function setReservedGroupNames(names) {
3177
- _effectiveReservedNames = names;
3178
- }
3179
- function _validateBuiltinGroupName(name) {
3180
- if (!name || !_NAME_REGEX.test(name)) {
3181
- throw new ApcliGroupError(
3182
- `builtinGroupName ${JSON.stringify(name)} must match /^[a-z][a-z0-9_-]*$/ (non-empty, lowercase, alphanumeric + '_' / '-', leading letter).`
2917
+ // src/discovery.ts
2918
+ import { Command as Command3, Option as Option2 } from "commander";
2919
+ function validateTag(tag) {
2920
+ if (!TAG_PATTERN.test(tag)) {
2921
+ process.stderr.write(
2922
+ `Error: Invalid tag format: '${tag}'. Tags must match [a-z][a-z0-9_-]*.
2923
+ `
3183
2924
  );
2925
+ process.exit(EXIT_CODES.INVALID_CLI_INPUT);
3184
2926
  }
3185
2927
  }
3186
- var ApcliGroupError, DEFAULT_BUILTIN_GROUP_NAME, RESERVED_GROUP_NAMES, _effectiveReservedNames, _NAME_REGEX, VALID_USER_MODES, APCLI_SUBCOMMAND_NAMES, ApcliGroup;
3187
- var init_builtin_group = __esm({
3188
- "src/builtin-group.ts"() {
3189
- "use strict";
3190
- init_esm_shims();
3191
- init_errors();
3192
- init_logger();
3193
- ApcliGroupError = class extends Error {
3194
- constructor(message) {
3195
- super(message);
3196
- this.name = "ApcliGroupError";
2928
+ function collectTag(value, previous) {
2929
+ return previous.concat([value]);
2930
+ }
2931
+ function collectAnnotation(value, previous) {
2932
+ return previous.concat([value]);
2933
+ }
2934
+ function getAnnotationFlag(moduleDef, flag) {
2935
+ const annotations = moduleDef.annotations;
2936
+ if (!annotations || typeof annotations !== "object") return false;
2937
+ const ann = annotations;
2938
+ const map = {
2939
+ "destructive": "destructive",
2940
+ "requires-approval": "requires_approval",
2941
+ "readonly": "readonly",
2942
+ "streaming": "streaming",
2943
+ "cacheable": "cacheable",
2944
+ "idempotent": "idempotent",
2945
+ "paginated": "paginated"
2946
+ };
2947
+ const attr = map[flag] ?? flag;
2948
+ return ann[attr] === true;
2949
+ }
2950
+ function registerListCommand(apcliGroup, registry, exposureFilter) {
2951
+ const listCmd = new Command3("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(
2952
+ new Option2("--format <format>", "Output format.").choices(["table", "json", "csv", "yaml", "jsonl", "markdown", "skill"])
2953
+ ).option("-s, --search <query>", "Filter by substring match on ID and description.").addOption(
2954
+ new Option2("--status <status>", "Filter by module status.").choices(["enabled", "disabled", "all"]).default("enabled")
2955
+ ).option("-a, --annotation <flag>", "Filter by annotation flag (AND logic). Repeatable.", collectAnnotation, []).addOption(
2956
+ new Option2("--sort <field>", "Sort order.").choices(["id", "calls", "errors", "latency"]).default("id")
2957
+ ).option("--reverse", "Reverse sort order.", false).option("--deprecated", "Include deprecated modules.", false).option("--deps", "Show dependency count column.", false).addOption(
2958
+ new Option2("--exposure <mode>", "Filter by exposure status.").choices(["exposed", "hidden", "all"]).default("exposed")
2959
+ ).action((opts) => {
2960
+ for (const t of opts.tag) {
2961
+ validateTag(t);
2962
+ }
2963
+ let modules = [];
2964
+ for (const m of listAllDefinitions(registry)) {
2965
+ modules.push(m);
2966
+ }
2967
+ if (opts.tag.length > 0) {
2968
+ const filterTags = new Set(opts.tag);
2969
+ modules = modules.filter((m) => {
2970
+ const mTags = m.tags ?? [];
2971
+ return [...filterTags].every((t) => mTags.includes(t));
2972
+ });
2973
+ }
2974
+ if (opts.search) {
2975
+ const query = opts.search.toLowerCase();
2976
+ modules = modules.filter(
2977
+ (m) => (m.moduleId ?? "").toLowerCase().includes(query) || (m.description ?? "").toLowerCase().includes(query)
2978
+ );
2979
+ }
2980
+ if (opts.status === "enabled") {
2981
+ modules = modules.filter((m) => {
2982
+ const enabled = m.enabled;
2983
+ return enabled !== false;
2984
+ });
2985
+ } else if (opts.status === "disabled") {
2986
+ modules = modules.filter((m) => {
2987
+ const enabled = m.enabled;
2988
+ return enabled === false;
2989
+ });
2990
+ }
2991
+ if (!opts.deprecated) {
2992
+ modules = modules.filter((m) => {
2993
+ const deprecated = m.deprecated;
2994
+ return deprecated !== true;
2995
+ });
2996
+ }
2997
+ if (opts.annotation.length > 0) {
2998
+ for (const annFlag of opts.annotation) {
2999
+ modules = modules.filter((m) => getAnnotationFlag(m, annFlag));
3197
3000
  }
3198
- };
3199
- DEFAULT_BUILTIN_GROUP_NAME = "apcli";
3200
- RESERVED_GROUP_NAMES = /* @__PURE__ */ new Set([DEFAULT_BUILTIN_GROUP_NAME]);
3201
- _effectiveReservedNames = RESERVED_GROUP_NAMES;
3202
- _NAME_REGEX = /^[a-z][a-z0-9_-]*$/;
3203
- VALID_USER_MODES = /* @__PURE__ */ new Set([
3204
- "all",
3205
- "none",
3206
- "include",
3207
- "exclude"
3208
- ]);
3209
- APCLI_SUBCOMMAND_NAMES = /* @__PURE__ */ new Set([
3210
- "list",
3211
- "describe",
3212
- "exec",
3213
- "validate",
3214
- "init",
3215
- "health",
3216
- "usage",
3217
- "enable",
3218
- "disable",
3219
- "reload",
3220
- "config",
3221
- "completion",
3222
- "describe-pipeline"
3223
- ]);
3224
- ApcliGroup = class _ApcliGroup {
3225
- _mode;
3226
- _include;
3227
- _exclude;
3228
- _disableEnv;
3229
- _registryInjected;
3230
- _fromCliConfig;
3231
- _name;
3232
- constructor(init) {
3233
- this._mode = init.mode;
3234
- this._include = init.include;
3235
- this._exclude = init.exclude;
3236
- this._disableEnv = init.disableEnv;
3237
- this._registryInjected = init.registryInjected;
3238
- this._fromCliConfig = init.fromCliConfig;
3239
- this._name = init.name;
3001
+ }
3002
+ if (opts.sort === "calls" || opts.sort === "errors" || opts.sort === "latency") {
3003
+ const { used } = sortModulesByUsage(modules, opts.sort, { reverse: !opts.reverse });
3004
+ if (!used) {
3005
+ process.stderr.write(
3006
+ `note: no usage data available for --sort ${opts.sort}; sorted by id. Run some modules first to populate ~/.apcore-cli/audit.jsonl.
3007
+ `
3008
+ );
3240
3009
  }
3241
- /**
3242
- * Resolved name for the built-in command group (default `"apcli"`).
3243
- * Overridable via createCli's `builtinGroupName` option for downstream
3244
- * branded CLIs that want a custom namespace. Cross-SDK parity with
3245
- * Python `ApcliGroup.name` (2026-05-08).
3246
- */
3247
- get name() {
3248
- return this._name;
3010
+ } else {
3011
+ modules.sort((a, b) => (a.moduleId ?? "").localeCompare(b.moduleId ?? ""));
3012
+ if (opts.reverse) {
3013
+ modules.reverse();
3249
3014
  }
3250
- /**
3251
- * Tier 1 constructor — config came from `createCli({ apcli })`.
3252
- *
3253
- * A non-auto mode from this tier wins over env var and yaml.
3254
- */
3255
- static fromCliConfig(config, opts) {
3256
- return _ApcliGroup._build(
3257
- config,
3258
- opts,
3259
- /*fromCliConfig*/
3260
- true
3261
- );
3015
+ }
3016
+ let showExposureCol = false;
3017
+ if (exposureFilter && opts.exposure !== "all") {
3018
+ if (opts.exposure === "exposed") {
3019
+ modules = modules.filter((m) => exposureFilter.isExposed(m.moduleId ?? ""));
3020
+ } else if (opts.exposure === "hidden") {
3021
+ modules = modules.filter((m) => !exposureFilter.isExposed(m.moduleId ?? ""));
3262
3022
  }
3263
- /**
3264
- * Tier 3 constructor config came from `apcore.yaml`.
3265
- *
3266
- * Env var (Tier 2) may override the yaml-supplied mode.
3267
- */
3268
- static fromYaml(config, opts) {
3269
- if (config !== null && config !== void 0 && typeof config !== "boolean" && (typeof config !== "object" || Array.isArray(config))) {
3270
- const got = Array.isArray(config) ? "array" : typeof config;
3271
- warn(
3272
- `apcore.yaml apcli has unexpected type ${got}; using auto-detect.`
3273
- );
3274
- return _ApcliGroup._build(
3275
- void 0,
3276
- opts,
3277
- /*fromCliConfig*/
3278
- false
3279
- );
3023
+ }
3024
+ if (opts.exposure === "all" && exposureFilter) {
3025
+ showExposureCol = true;
3026
+ }
3027
+ const fmt = resolveFormat(opts.format);
3028
+ const filterTagsArg = opts.tag.length > 0 ? opts.tag : void 0;
3029
+ void formatModuleList(modules, fmt, filterTagsArg, opts.deps, showExposureCol ? exposureFilter : void 0);
3030
+ });
3031
+ apcliGroup.addCommand(listCmd);
3032
+ }
3033
+ function registerDescribeCommand(apcliGroup, registry) {
3034
+ const describeCmd = new Command3("describe").description("Show metadata, schema, and annotations for a module.").argument("<module-id>", "Module ID to describe").addOption(
3035
+ new Option2("--format <format>", "Output format.").choices(["table", "json", "csv", "yaml", "jsonl", "markdown", "skill"])
3036
+ ).action((moduleId, opts) => {
3037
+ validateModuleId(moduleId);
3038
+ const moduleDef = registry.getDefinition(moduleId);
3039
+ if (!moduleDef) {
3040
+ process.stderr.write(
3041
+ `Error: Module '${moduleId}' not found.
3042
+ `
3043
+ );
3044
+ process.exit(EXIT_CODES.MODULE_NOT_FOUND);
3045
+ }
3046
+ const fmt = resolveFormat(opts.format);
3047
+ void formatModuleDetail(moduleDef, fmt);
3048
+ });
3049
+ apcliGroup.addCommand(describeCmd);
3050
+ }
3051
+ function registerExecCommand(apcliGroup, registry, executor) {
3052
+ const execCmd = new Command3("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(
3053
+ "--input <json>",
3054
+ "JSON object passed as input to the module. Use '-' to read JSON from stdin."
3055
+ ).option("-y, --yes", "Auto-approve if the module declares requires_approval.", false).option(
3056
+ "--approval-timeout <seconds>",
3057
+ "Seconds to wait for interactive approval.",
3058
+ parseInt
3059
+ ).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) => {
3060
+ validateModuleId(moduleId);
3061
+ const moduleDef = registry.getDefinition(moduleId);
3062
+ if (!moduleDef) {
3063
+ process.stderr.write(`Error: Module '${moduleId}' not found.
3064
+ `);
3065
+ process.exit(EXIT_CODES.MODULE_NOT_FOUND);
3066
+ }
3067
+ let merged = {};
3068
+ if (opts.input === "-") {
3069
+ merged = await collectInput("-", {}, false);
3070
+ } else if (opts.input !== void 0) {
3071
+ try {
3072
+ const parsed = JSON.parse(opts.input);
3073
+ if (typeof parsed !== "object" || parsed === null || Array.isArray(parsed)) {
3074
+ process.stderr.write("Error: --input JSON must be an object.\n");
3075
+ process.exit(EXIT_CODES.INVALID_CLI_INPUT);
3280
3076
  }
3281
- return _ApcliGroup._build(
3282
- config,
3283
- opts,
3284
- /*fromCliConfig*/
3285
- false
3286
- );
3077
+ merged = parsed;
3078
+ } catch (err) {
3079
+ const msg = err instanceof Error ? err.message : String(err);
3080
+ process.stderr.write(`Error: --input is not valid JSON: ${msg}
3081
+ `);
3082
+ process.exit(EXIT_CODES.INVALID_CLI_INPUT);
3287
3083
  }
3288
- /**
3289
- * Non-panicking Tier 3 factory (A-001 parity with Rust's `try_from_yaml`).
3290
- * Returns `[instance, null]` on success or `[null, errorMessage]` on invalid input.
3291
- * Use this in programmatic contexts where throwing/exiting is unwanted.
3292
- */
3293
- static tryFromYaml(config, opts) {
3294
- if (config !== null && config !== void 0 && typeof config !== "boolean" && (typeof config !== "object" || Array.isArray(config))) {
3295
- const got = Array.isArray(config) ? "array" : typeof config;
3296
- return [
3297
- null,
3298
- `apcore.yaml 'apcli:' must be a bool, object, or null; got ${got}`
3299
- ];
3300
- }
3301
- if (config !== null && config !== void 0 && typeof config === "object" && !Array.isArray(config)) {
3302
- const mode = config["mode"];
3303
- if (mode !== void 0 && mode !== null) {
3304
- const validModes = ["all", "none", "include", "exclude"];
3305
- if (typeof mode !== "string" || !validModes.includes(mode)) {
3306
- return [null, `Invalid apcli mode: '${mode}'. Must be one of: all, none, include, exclude.`];
3307
- }
3308
- }
3084
+ }
3085
+ const startTime = performance.now();
3086
+ try {
3087
+ await checkApproval(moduleDef, opts.yes, opts.approvalTimeout);
3088
+ if (opts.dryRun) {
3089
+ if (executor.validate) {
3090
+ const preflight = await executor.validate(moduleId, merged);
3091
+ formatPreflightResult(preflight, opts.format);
3092
+ } else {
3093
+ process.stdout.write(JSON.stringify({ valid: true }) + "\n");
3309
3094
  }
3310
- return [_ApcliGroup.fromYaml(config, opts), null];
3095
+ return;
3311
3096
  }
3312
- // -------------------------------------------------------------------------
3313
- // Internal builder shared by both factories
3314
- // -------------------------------------------------------------------------
3315
- static _build(config, opts, fromCliConfig) {
3316
- const name = opts.name ?? DEFAULT_BUILTIN_GROUP_NAME;
3317
- _validateBuiltinGroupName(name);
3318
- if (config === true) {
3319
- return new _ApcliGroup({
3320
- mode: "all",
3321
- include: [],
3322
- exclude: [],
3323
- disableEnv: false,
3324
- registryInjected: opts.registryInjected,
3325
- fromCliConfig,
3326
- name
3327
- });
3328
- }
3329
- if (config === false) {
3330
- return new _ApcliGroup({
3331
- mode: "none",
3332
- include: [],
3333
- exclude: [],
3334
- disableEnv: false,
3335
- registryInjected: opts.registryInjected,
3336
- fromCliConfig,
3337
- name
3338
- });
3339
- }
3340
- if (config === void 0 || config === null) {
3341
- return new _ApcliGroup({
3342
- mode: "auto",
3343
- include: [],
3344
- exclude: [],
3345
- disableEnv: false,
3346
- registryInjected: opts.registryInjected,
3347
- fromCliConfig,
3348
- name
3349
- });
3097
+ let result;
3098
+ if ((opts.trace || opts.strategy) && executor.callWithTrace) {
3099
+ const [res] = await executor.callWithTrace(moduleId, merged, { strategy: opts.strategy });
3100
+ result = res;
3101
+ } else {
3102
+ const { Sandbox: Sandbox2 } = await Promise.resolve().then(() => (init_security(), security_exports));
3103
+ const sandbox = new Sandbox2(opts.sandbox);
3104
+ result = await sandbox.execute(moduleId, merged, executor);
3105
+ }
3106
+ const durationMs = Math.round(performance.now() - startTime);
3107
+ const fmt = resolveFormat(opts.format);
3108
+ formatExecResult(result, fmt, opts.fields);
3109
+ const auditLogger = getAuditLogger();
3110
+ if (auditLogger) {
3111
+ auditLogger.logExecution(moduleId, merged, "success", 0, durationMs);
3112
+ }
3113
+ } catch (err) {
3114
+ const exitCode = exitCodeForError(err);
3115
+ const durationMs = Math.round(performance.now() - startTime);
3116
+ try {
3117
+ const auditLogger = getAuditLogger();
3118
+ if (auditLogger) {
3119
+ auditLogger.logExecution(moduleId, merged, "error", exitCode, durationMs);
3350
3120
  }
3351
- if (typeof config !== "object" || Array.isArray(config)) {
3352
- process.stderr.write(
3353
- `Error: apcli config must be a boolean or object; got ${Array.isArray(config) ? "array" : typeof config}.
3354
- `
3355
- );
3356
- process.exit(EXIT_CODES.INVALID_CLI_INPUT);
3121
+ } catch {
3122
+ }
3123
+ process.stderr.write(`Error: ${err instanceof Error ? err.message : err}
3124
+ `);
3125
+ process.exit(exitCode);
3126
+ }
3127
+ });
3128
+ apcliGroup.addCommand(execCmd);
3129
+ }
3130
+ function registerValidateCommand(cli, registry, executor) {
3131
+ const validateCmd = new Command3("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) => {
3132
+ validateModuleId(moduleId);
3133
+ const moduleDef = registry.getDefinition(moduleId);
3134
+ if (!moduleDef) {
3135
+ process.stderr.write(`Error: Module '${moduleId}' not found.
3136
+ `);
3137
+ process.exit(EXIT_CODES.MODULE_NOT_FOUND);
3138
+ }
3139
+ const merged = opts.input ? await collectInput(opts.input, {}, false) : {};
3140
+ if (!executor.validate) {
3141
+ process.stderr.write("Error: Executor does not support validate.\n");
3142
+ process.exit(EXIT_CODES.MODULE_EXECUTE_ERROR);
3143
+ }
3144
+ try {
3145
+ const preflight = await executor.validate(moduleId, merged);
3146
+ formatPreflightResult(preflight, opts.format);
3147
+ process.exit(preflight.valid ? 0 : firstFailedExitCode(preflight));
3148
+ } catch (err) {
3149
+ const exitCode = exitCodeForError(err);
3150
+ try {
3151
+ const auditLogger = getAuditLogger();
3152
+ if (auditLogger) {
3153
+ auditLogger.logExecution(moduleId, merged, "error", exitCode, 0);
3357
3154
  }
3358
- const cfg = config;
3359
- let mode;
3360
- if (cfg.mode === void 0 || cfg.mode === null) {
3361
- mode = "auto";
3362
- } else if (typeof cfg.mode !== "string") {
3363
- process.stderr.write(
3364
- `Error: apcli.mode must be a string; got ${typeof cfg.mode}. Expected one of all|none|include|exclude.
3155
+ } catch {
3156
+ }
3157
+ process.stderr.write(`Error: ${err instanceof Error ? err.message : err}
3158
+ `);
3159
+ process.exit(exitCode);
3160
+ }
3161
+ });
3162
+ cli.addCommand(validateCmd);
3163
+ }
3164
+ var TAG_PATTERN;
3165
+ var init_discovery = __esm({
3166
+ "src/discovery.ts"() {
3167
+ "use strict";
3168
+ init_esm_shims();
3169
+ init_approval();
3170
+ init_cli();
3171
+ init_errors();
3172
+ init_main();
3173
+ init_output();
3174
+ init_audit();
3175
+ init_system_usage();
3176
+ TAG_PATTERN = /^[a-z][a-z0-9_-]*$/;
3177
+ }
3178
+ });
3179
+
3180
+ // src/system-cmd.ts
3181
+ import { Command as Command4 } from "commander";
3182
+ import { Config } from "apcore-js";
3183
+ async function callSystemModule(executor, moduleId, inputs) {
3184
+ return executor.call(moduleId, inputs);
3185
+ }
3186
+ function emitResult(jsonPayload, fmt, ttyRender) {
3187
+ if (fmt === "json" || !process.stdout.isTTY) {
3188
+ process.stdout.write(JSON.stringify(jsonPayload, null, 2) + "\n");
3189
+ } else {
3190
+ ttyRender();
3191
+ }
3192
+ }
3193
+ function emitErrorAndExit(e) {
3194
+ process.stderr.write(`Error: ${e instanceof Error ? e.message : e}
3195
+ `);
3196
+ process.exit(exitCodeForError(e));
3197
+ }
3198
+ async function requireApprovalForSystemCommand(moduleId, autoApprove) {
3199
+ const syntheticModuleDef = {
3200
+ moduleId,
3201
+ name: moduleId,
3202
+ description: `system command: ${moduleId}`,
3203
+ annotations: { requires_approval: true }
3204
+ };
3205
+ await checkApproval(syntheticModuleDef, autoApprove, void 0);
3206
+ }
3207
+ function formatHealthSummaryTty(result) {
3208
+ const summary = result.summary ?? {};
3209
+ const modules = result.modules ?? [];
3210
+ if (modules.length === 0) {
3211
+ process.stdout.write("No modules found.\n");
3212
+ return;
3213
+ }
3214
+ const total = summary.total_modules ?? modules.length;
3215
+ process.stdout.write(`Health Overview (${total} modules)
3216
+
3217
+ `);
3218
+ process.stdout.write(` ${"Module".padEnd(28)} ${"Status".padEnd(12)} ${"Error Rate".padEnd(12)} Top Error
3219
+ `);
3220
+ process.stdout.write(` ${"-".repeat(28)} ${"-".repeat(12)} ${"-".repeat(12)} ${"-".repeat(20)}
3221
+ `);
3222
+ for (const m of modules) {
3223
+ const top = m.top_error;
3224
+ const topStr = top ? `${top.code} (${top.count ?? "?"})` : "\u2014";
3225
+ const rate = `${((m.error_rate ?? 0) * 100).toFixed(1)}%`;
3226
+ process.stdout.write(
3227
+ ` ${String(m.module_id).padEnd(28)} ${String(m.status).padEnd(12)} ${rate.padEnd(12)} ${topStr}
3365
3228
  `
3366
- );
3367
- process.exit(EXIT_CODES.INVALID_CLI_INPUT);
3368
- } else if (!VALID_USER_MODES.has(cfg.mode)) {
3369
- process.stderr.write(
3370
- `Error: apcli.mode '${cfg.mode}' is invalid. Expected one of all|none|include|exclude.
3229
+ );
3230
+ }
3231
+ const parts = [];
3232
+ for (const key of ["healthy", "degraded", "error"]) {
3233
+ const count = summary[key];
3234
+ if (count) parts.push(`${count} ${key}`);
3235
+ }
3236
+ process.stdout.write(`
3237
+ Summary: ${parts.join(", ") || "no data"}
3238
+ `);
3239
+ }
3240
+ function formatHealthModuleTty(result) {
3241
+ process.stdout.write(`Module: ${result.module_id ?? "?"}
3242
+ `);
3243
+ process.stdout.write(`Status: ${result.status ?? "unknown"}
3244
+ `);
3245
+ const total = result.total_calls ?? 0;
3246
+ const errors = result.error_count ?? 0;
3247
+ const rate = result.error_rate ?? 0;
3248
+ const avg = result.avg_latency_ms ?? 0;
3249
+ const p99 = result.p99_latency_ms ?? 0;
3250
+ process.stdout.write(`Calls: ${total.toLocaleString()} total | ${errors.toLocaleString()} errors | ${(rate * 100).toFixed(1)}% error rate
3251
+ `);
3252
+ process.stdout.write(`Latency: ${avg.toFixed(0)}ms avg | ${p99.toFixed(0)}ms p99
3253
+ `);
3254
+ const recent = result.recent_errors ?? [];
3255
+ if (recent.length > 0) {
3256
+ process.stdout.write(`
3257
+ Recent Errors (top ${recent.length}):
3258
+ `);
3259
+ for (const e of recent) {
3260
+ const count = e.count ?? "?";
3261
+ const last = e.last_occurred ?? "?";
3262
+ process.stdout.write(` ${String(e.code ?? "?").padEnd(24)} x${count} (last: ${last})
3263
+ `);
3264
+ }
3265
+ }
3266
+ }
3267
+ function formatUsageSummaryTty(result) {
3268
+ const modules = result.modules ?? [];
3269
+ const period = result.period ?? "?";
3270
+ if (modules.length === 0) {
3271
+ process.stdout.write(`No usage data for period ${period}.
3272
+ `);
3273
+ return;
3274
+ }
3275
+ process.stdout.write(`Usage Summary (last ${period})
3276
+
3277
+ `);
3278
+ process.stdout.write(` ${"Module".padEnd(24)} ${"Calls".padStart(8)} ${"Errors".padStart(8)} ${"Avg Latency".padStart(12)} ${"Trend".padStart(10)}
3279
+ `);
3280
+ process.stdout.write(` ${"-".repeat(24)} ${"-".repeat(8)} ${"-".repeat(8)} ${"-".repeat(12)} ${"-".repeat(10)}
3281
+ `);
3282
+ for (const m of modules) {
3283
+ const avg = `${(m.avg_latency_ms ?? 0).toFixed(0)}ms`;
3284
+ process.stdout.write(
3285
+ ` ${String(m.module_id).padEnd(24)} ${String(m.call_count ?? 0).padStart(8)} ${String(m.error_count ?? 0).padStart(8)} ${avg.padStart(12)} ${String(m.trend ?? "").padStart(10)}
3371
3286
  `
3372
- );
3373
- process.exit(EXIT_CODES.INVALID_CLI_INPUT);
3374
- } else {
3375
- mode = cfg.mode;
3376
- }
3377
- const include = _ApcliGroup._normalizeList(cfg.include, "include");
3378
- const exclude = _ApcliGroup._normalizeList(cfg.exclude, "exclude");
3379
- const rawDisableEnv = cfg.disableEnv !== void 0 ? cfg.disableEnv : cfg["disable_env"];
3380
- let disableEnv = false;
3381
- if (rawDisableEnv !== void 0) {
3382
- if (typeof rawDisableEnv === "boolean") {
3383
- disableEnv = rawDisableEnv;
3384
- } else {
3385
- warn(
3386
- `apcli.disable_env must be boolean; got ${typeof rawDisableEnv}. Treating as false.`
3387
- );
3388
- }
3389
- }
3390
- return new _ApcliGroup({
3391
- mode,
3392
- include,
3393
- exclude,
3394
- disableEnv,
3395
- registryInjected: opts.registryInjected,
3396
- fromCliConfig,
3397
- name
3287
+ );
3288
+ }
3289
+ const totalCalls = result.total_calls ?? modules.reduce((s, m) => s + (m.call_count ?? 0), 0);
3290
+ const totalErrors = result.total_errors ?? modules.reduce((s, m) => s + (m.error_count ?? 0), 0);
3291
+ process.stdout.write(`
3292
+ Total: ${totalCalls.toLocaleString()} calls | ${totalErrors.toLocaleString()} errors
3293
+ `);
3294
+ }
3295
+ function registerHealthCommand(apcliGroup, executor) {
3296
+ const healthCmd = new Command4("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) => {
3297
+ const fmt = resolveFormat(opts.format);
3298
+ try {
3299
+ if (moduleId) {
3300
+ const result = await callSystemModule(executor, "system.health.module", {
3301
+ module_id: moduleId,
3302
+ error_limit: opts.errors
3303
+ });
3304
+ emitResult(result, fmt, () => formatHealthModuleTty(result));
3305
+ } else {
3306
+ const result = await callSystemModule(executor, "system.health.summary", {
3307
+ error_rate_threshold: opts.threshold,
3308
+ include_healthy: opts.all
3309
+ });
3310
+ emitResult(result, fmt, () => formatHealthSummaryTty(result));
3311
+ }
3312
+ } catch (e) {
3313
+ emitErrorAndExit(e);
3314
+ }
3315
+ });
3316
+ apcliGroup.addCommand(healthCmd);
3317
+ }
3318
+ function registerUsageCommand(apcliGroup, executor) {
3319
+ const usageCmd = new Command4("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) => {
3320
+ const fmt = resolveFormat(opts.format);
3321
+ try {
3322
+ let result;
3323
+ if (moduleId) {
3324
+ result = await callSystemModule(executor, "system.usage.module", {
3325
+ module_id: moduleId,
3326
+ period: opts.period
3327
+ });
3328
+ } else {
3329
+ result = await callSystemModule(executor, "system.usage.summary", {
3330
+ period: opts.period
3398
3331
  });
3399
3332
  }
3400
- /**
3401
- * Normalize an include/exclude list. Non-array → warn and return [].
3402
- *
3403
- * Unknown but well-formed entries emit a WARNING (spec §7 error table,
3404
- * T-APCLI-25) but are retained in the returned list for forward-compat —
3405
- * if apcore-cli later adds a subcommand named `foo`, existing configs
3406
- * continue to work without a config change. At runtime, unknown names
3407
- * simply never match any registered subcommand.
3408
- */
3409
- static _normalizeList(raw, label) {
3410
- if (raw === void 0 || raw === null) return [];
3411
- if (!Array.isArray(raw)) {
3412
- warn(`apcli.${label} must be a list; got ${typeof raw}. Ignoring.`);
3413
- return [];
3414
- }
3415
- const out = [];
3416
- for (const entry of raw) {
3417
- if (typeof entry === "string" && entry.length > 0) {
3418
- if (!APCLI_SUBCOMMAND_NAMES.has(entry)) {
3419
- warn(
3420
- `Unknown apcli subcommand '${entry}' in ${label} list \u2014 ignoring.`
3421
- );
3422
- }
3423
- out.push(entry);
3424
- } else {
3425
- warn(`apcli.${label} contains non-string entry; skipping.`);
3426
- }
3427
- }
3428
- return out;
3429
- }
3430
- // -------------------------------------------------------------------------
3431
- // Public API
3432
- // -------------------------------------------------------------------------
3433
- /**
3434
- * Resolve effective visibility mode after applying tier precedence.
3435
- *
3436
- * Returns one of `"all" | "none" | "include" | "exclude"` — never `"auto"`.
3437
- *
3438
- * Tier order (spec §4.4):
3439
- * 1. CliConfig non-auto wins outright.
3440
- * 2. `APCORE_CLI_APCLI` env var (unless sealed by disableEnv).
3441
- * 3. yaml non-auto.
3442
- * 4. Auto-detect from registryInjected.
3443
- */
3444
- resolveVisibility() {
3445
- if (this._fromCliConfig && this._mode !== "auto") {
3446
- return this._mode;
3447
- }
3448
- if (!this._disableEnv) {
3449
- const envMode = this._parseEnv(process.env.APCORE_CLI_APCLI);
3450
- if (envMode !== null) {
3451
- return envMode;
3452
- }
3453
- }
3454
- if (this._mode !== "auto") {
3455
- return this._mode;
3333
+ emitResult(result, fmt, () => {
3334
+ if (moduleId) {
3335
+ formatExecResult(result, fmt);
3336
+ } else {
3337
+ formatUsageSummaryTty(result);
3456
3338
  }
3457
- return this._registryInjected ? "none" : "all";
3458
- }
3459
- /**
3460
- * True iff `subcommand` passes the include/exclude filter.
3461
- *
3462
- * Callers MUST first check {@link resolveVisibility} — this method throws
3463
- * under modes `"all"` or `"none"` (caller bug per spec §4.6).
3464
- */
3465
- isSubcommandIncluded(subcommand) {
3466
- const mode = this.resolveVisibility();
3467
- if (mode === "include") return this._include.includes(subcommand);
3468
- if (mode === "exclude") return !this._exclude.includes(subcommand);
3469
- throw new Error(
3470
- `isSubcommandIncluded called under mode '${mode}'; caller should bypass.`
3471
- );
3472
- }
3473
- /** True iff the `apcli` group itself should appear in root `--help`. */
3474
- isGroupVisible() {
3475
- return this.resolveVisibility() !== "none";
3339
+ });
3340
+ } catch (e) {
3341
+ emitErrorAndExit(e);
3342
+ }
3343
+ });
3344
+ apcliGroup.addCommand(usageCmd);
3345
+ }
3346
+ function registerEnableCommand(apcliGroup, executor) {
3347
+ const enableCmd = new Command4("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) => {
3348
+ const fmt = resolveFormat(opts.format);
3349
+ try {
3350
+ await requireApprovalForSystemCommand("system.control.toggle_feature", opts.yes);
3351
+ const result = await callSystemModule(executor, "system.control.toggle_feature", {
3352
+ module_id: moduleId,
3353
+ enabled: true,
3354
+ reason: opts.reason
3355
+ });
3356
+ emitResult(result, fmt, () => {
3357
+ process.stdout.write(`Module '${moduleId}' enabled.
3358
+ Reason: ${opts.reason}
3359
+ `);
3360
+ });
3361
+ } catch (e) {
3362
+ emitErrorAndExit(e);
3363
+ }
3364
+ });
3365
+ apcliGroup.addCommand(enableCmd);
3366
+ }
3367
+ function registerDisableCommand(apcliGroup, executor) {
3368
+ const disableCmd = new Command4("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) => {
3369
+ const fmt = resolveFormat(opts.format);
3370
+ try {
3371
+ await requireApprovalForSystemCommand("system.control.toggle_feature", opts.yes);
3372
+ const result = await callSystemModule(executor, "system.control.toggle_feature", {
3373
+ module_id: moduleId,
3374
+ enabled: false,
3375
+ reason: opts.reason
3376
+ });
3377
+ emitResult(result, fmt, () => {
3378
+ process.stdout.write(`Module '${moduleId}' disabled.
3379
+ Reason: ${opts.reason}
3380
+ `);
3381
+ });
3382
+ } catch (e) {
3383
+ emitErrorAndExit(e);
3384
+ }
3385
+ });
3386
+ apcliGroup.addCommand(disableCmd);
3387
+ }
3388
+ function registerReloadCommand(apcliGroup, executor) {
3389
+ const reloadCmd = new Command4("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) => {
3390
+ const fmt = resolveFormat(opts.format);
3391
+ try {
3392
+ await requireApprovalForSystemCommand("system.control.reload_module", opts.yes);
3393
+ const result = await callSystemModule(executor, "system.control.reload_module", {
3394
+ module_id: moduleId,
3395
+ reason: opts.reason
3396
+ });
3397
+ emitResult(result, fmt, () => {
3398
+ const prev = result.previous_version ?? "?";
3399
+ const newVer = result.new_version ?? "?";
3400
+ const dur = result.reload_duration_ms ?? "?";
3401
+ process.stdout.write(`Module '${moduleId}' reloaded.
3402
+ `);
3403
+ process.stdout.write(` Version: ${prev} -> ${newVer}
3404
+ `);
3405
+ process.stdout.write(` Duration: ${dur}ms
3406
+ `);
3407
+ });
3408
+ } catch (e) {
3409
+ emitErrorAndExit(e);
3410
+ }
3411
+ });
3412
+ apcliGroup.addCommand(reloadCmd);
3413
+ }
3414
+ function registerConfigCommand(apcliGroup, executor) {
3415
+ const configGroup = new Command4("config").description("Read or update runtime configuration.");
3416
+ const configGetCmd = new Command4("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) => {
3417
+ const fmt = resolveFormat(opts.format);
3418
+ try {
3419
+ const value = new Config().get(key);
3420
+ emitResult({ key, value }, fmt, () => {
3421
+ process.stdout.write(`${key} = ${JSON.stringify(value)}
3422
+ `);
3423
+ });
3424
+ } catch (e) {
3425
+ emitErrorAndExit(e);
3426
+ }
3427
+ });
3428
+ configGroup.addCommand(configGetCmd);
3429
+ const configSetCmd = new Command4("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) => {
3430
+ const fmt = resolveFormat(opts.format);
3431
+ let parsedValue;
3432
+ try {
3433
+ parsedValue = JSON.parse(value);
3434
+ } catch {
3435
+ parsedValue = value;
3436
+ }
3437
+ try {
3438
+ await requireApprovalForSystemCommand("system.control.update_config", opts.yes);
3439
+ const result = await callSystemModule(executor, "system.control.update_config", {
3440
+ key,
3441
+ value: parsedValue,
3442
+ reason: opts.reason
3443
+ });
3444
+ emitResult(result, fmt, () => {
3445
+ const old = result.old_value ?? "?";
3446
+ const newVal = result.new_value ?? "?";
3447
+ process.stdout.write(`Config updated: ${key}
3448
+ `);
3449
+ process.stdout.write(` ${JSON.stringify(old)} -> ${JSON.stringify(newVal)}
3450
+ `);
3451
+ process.stdout.write(` Reason: ${opts.reason}
3452
+ `);
3453
+ });
3454
+ } catch (e) {
3455
+ emitErrorAndExit(e);
3456
+ }
3457
+ });
3458
+ configGroup.addCommand(configSetCmd);
3459
+ apcliGroup.addCommand(configGroup);
3460
+ }
3461
+ var init_system_cmd = __esm({
3462
+ "src/system-cmd.ts"() {
3463
+ "use strict";
3464
+ init_esm_shims();
3465
+ init_approval();
3466
+ init_errors();
3467
+ init_output();
3468
+ }
3469
+ });
3470
+
3471
+ // src/strategy.ts
3472
+ import { Command as Command5, Option as Option3 } from "commander";
3473
+ function lookupStrategyInfo(executor, strategyName) {
3474
+ if (typeof executor.describePipeline === "function") {
3475
+ try {
3476
+ const current = executor.describePipeline();
3477
+ if (current && current.name === strategyName) {
3478
+ return { info: current, isCurrent: true };
3476
3479
  }
3477
- // -------------------------------------------------------------------------
3478
- // Env parser (Tier 2) — co-located per spec §4.4
3479
- // -------------------------------------------------------------------------
3480
- /**
3481
- * Parse APCORE_CLI_APCLI. Case-insensitive.
3482
- *
3483
- * - `show` / `1` / `true` → `"all"`
3484
- * - `hide` / `0` / `false` `"none"`
3485
- * - Empty / unset → `null`
3486
- * - Anything else → warn and return `null`
3487
- */
3488
- _parseEnv(raw) {
3489
- if (raw === void 0 || raw === "") return null;
3490
- const normalized = raw.trim().toLowerCase();
3491
- if (normalized === "") return null;
3492
- if (normalized === "show" || normalized === "1" || normalized === "true") {
3493
- return "all";
3494
- }
3495
- if (normalized === "hide" || normalized === "0" || normalized === "false") {
3496
- return "none";
3480
+ } catch {
3481
+ }
3482
+ }
3483
+ const ctor = executor.constructor;
3484
+ if (ctor && typeof ctor.listStrategies === "function") {
3485
+ try {
3486
+ const all = ctor.listStrategies();
3487
+ const info = all.find((s) => s.name === strategyName) ?? null;
3488
+ return { info, isCurrent: false };
3489
+ } catch {
3490
+ return { info: null, isCurrent: false };
3491
+ }
3492
+ }
3493
+ return { info: null, isCurrent: false };
3494
+ }
3495
+ function registerPipelineCommand(cli, executor) {
3496
+ const pipelineCmd = new Command5("describe-pipeline").description("Show the execution pipeline steps for a strategy.").addOption(
3497
+ new Option3("--strategy <name>", "Strategy to describe (default: standard).").choices(["standard", "internal", "testing", "performance", "minimal"]).default("standard")
3498
+ ).option("--format <format>", "Output format.").action((opts) => {
3499
+ const fmt = resolveFormat(opts.format);
3500
+ const { info, isCurrent } = lookupStrategyInfo(executor, opts.strategy);
3501
+ if (info) {
3502
+ const strategySteps = isCurrent ? executor.currentStrategy?.steps ?? [] : [];
3503
+ const header = `Pipeline: ${info.name} (${info.stepCount} steps)`;
3504
+ if (fmt === "json" || !process.stdout.isTTY) {
3505
+ const payload = {
3506
+ strategy: info.name,
3507
+ step_count: info.stepCount,
3508
+ description: info.description,
3509
+ steps: info.stepNames.map((name, i) => {
3510
+ const stepMeta = strategySteps[i];
3511
+ return {
3512
+ index: i + 1,
3513
+ name,
3514
+ pure: stepMeta?.pure ?? false,
3515
+ removable: stepMeta?.removable ?? true,
3516
+ timeout_ms: stepMeta?.timeoutMs ?? null
3517
+ };
3518
+ })
3519
+ };
3520
+ process.stdout.write(JSON.stringify(payload, null, 2) + "\n");
3521
+ } else {
3522
+ process.stdout.write(`${header}
3523
+
3524
+ `);
3525
+ process.stdout.write(` ${"#".padEnd(4)} ${"Step".padEnd(28)} ${"Pure".padEnd(6)} ${"Removable".padEnd(11)} Timeout
3526
+ `);
3527
+ process.stdout.write(` ${"-".repeat(4)} ${"-".repeat(28)} ${"-".repeat(6)} ${"-".repeat(11)} ${"-".repeat(8)}
3528
+ `);
3529
+ for (let i = 0; i < info.stepNames.length; i++) {
3530
+ const stepMeta = strategySteps[i];
3531
+ const pure = stepMeta?.pure ? "yes" : "no";
3532
+ const removable = stepMeta?.removable !== false ? "yes" : "no";
3533
+ const timeout = stepMeta?.timeoutMs ? `${stepMeta.timeoutMs}ms` : "\u2014";
3534
+ process.stdout.write(` ${String(i + 1).padEnd(4)} ${info.stepNames[i].padEnd(28)} ${pure.padEnd(6)} ${removable.padEnd(11)} ${timeout}
3535
+ `);
3497
3536
  }
3498
- warn(
3499
- `Unknown APCORE_CLI_APCLI value '${raw}', ignoring. Expected: show, hide, 1, 0, true, false.`
3500
- );
3501
- return null;
3502
3537
  }
3503
- };
3504
- }
3505
- });
3538
+ return;
3539
+ }
3540
+ const steps = PRESET_STEPS[opts.strategy] ?? [];
3541
+ const pureSteps = /* @__PURE__ */ new Set([
3542
+ "context_creation",
3543
+ "call_chain_guard",
3544
+ "module_lookup",
3545
+ "acl_check",
3546
+ "input_validation"
3547
+ ]);
3548
+ const nonRemovable = /* @__PURE__ */ new Set([
3549
+ "context_creation",
3550
+ "module_lookup",
3551
+ "execute",
3552
+ "return_result"
3553
+ ]);
3554
+ if (fmt === "json" || !process.stdout.isTTY) {
3555
+ const payload = {
3556
+ strategy: opts.strategy,
3557
+ step_count: steps.length,
3558
+ steps: steps.map((s, i) => ({
3559
+ index: i + 1,
3560
+ name: s,
3561
+ pure: pureSteps.has(s),
3562
+ removable: !nonRemovable.has(s)
3563
+ }))
3564
+ };
3565
+ process.stdout.write(JSON.stringify(payload, null, 2) + "\n");
3566
+ } else {
3567
+ process.stdout.write(`Pipeline: ${opts.strategy} (${steps.length} steps)
3506
3568
 
3507
- // src/exposure.ts
3508
- function escapeRegex(str) {
3509
- return str.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
3510
- }
3511
- function compilePattern(pattern) {
3512
- const sentinel = "\0GLOB\0";
3513
- const escaped = pattern.replaceAll("**", sentinel);
3514
- const parts = escaped.split("*");
3515
- const regexParts = parts.map((p) => {
3516
- const restored = p.replaceAll(sentinel, "**");
3517
- return escapeRegex(restored);
3569
+ `);
3570
+ process.stdout.write(` ${"#".padEnd(4)} ${"Step".padEnd(28)} ${"Pure".padEnd(6)} ${"Removable".padEnd(11)} Timeout
3571
+ `);
3572
+ process.stdout.write(` ${"-".repeat(4)} ${"-".repeat(28)} ${"-".repeat(6)} ${"-".repeat(11)} ${"-".repeat(8)}
3573
+ `);
3574
+ for (let i = 0; i < steps.length; i++) {
3575
+ const pure = pureSteps.has(steps[i]) ? "yes" : "no";
3576
+ const removable = nonRemovable.has(steps[i]) ? "no" : "yes";
3577
+ process.stdout.write(` ${String(i + 1).padEnd(4)} ${steps[i].padEnd(28)} ${pure.padEnd(6)} ${removable.padEnd(11)} \u2014
3578
+ `);
3579
+ }
3580
+ }
3518
3581
  });
3519
- let regex = regexParts.join("[^.]*");
3520
- regex = regex.replaceAll("\\*\\*", ".+");
3521
- return new RegExp(`^${regex}$`);
3582
+ cli.addCommand(pipelineCmd);
3522
3583
  }
3523
- var ExposureFilter;
3524
- var init_exposure = __esm({
3525
- "src/exposure.ts"() {
3584
+ var PRESET_STEPS;
3585
+ var init_strategy = __esm({
3586
+ "src/strategy.ts"() {
3526
3587
  "use strict";
3527
3588
  init_esm_shims();
3528
- init_logger();
3529
- ExposureFilter = class _ExposureFilter {
3530
- static VALID_MODES = ["all", "include", "exclude", "none"];
3531
- _mode;
3532
- _compiledInclude;
3533
- _compiledExclude;
3534
- constructor(mode = "all", include, exclude) {
3535
- if (!_ExposureFilter.VALID_MODES.includes(mode)) {
3536
- process.stderr.write(
3537
- `Warning: Unknown ExposureFilter mode '${mode}' \u2014 defaulting to 'none'. Valid modes: ${_ExposureFilter.VALID_MODES.join(", ")}.
3538
- `
3539
- );
3540
- mode = "none";
3541
- }
3542
- this._mode = mode;
3543
- const dedup = (arr) => [...new Set(arr)];
3544
- this._compiledInclude = dedup(include ?? []).map(compilePattern);
3545
- this._compiledExclude = dedup(exclude ?? []).map(compilePattern);
3546
- }
3547
- /** Return true if the module should be exposed as a CLI command. */
3548
- isExposed(moduleId) {
3549
- if (this._mode === "all") return true;
3550
- if (this._mode === "include") {
3551
- return this._compiledInclude.some((rx) => rx.test(moduleId));
3552
- }
3553
- if (this._mode === "exclude") {
3554
- return !this._compiledExclude.some((rx) => rx.test(moduleId));
3555
- }
3556
- return false;
3557
- }
3558
- /** Partition moduleIds into [exposed, hidden] lists. */
3559
- filterModules(moduleIds) {
3560
- const exposed = [];
3561
- const hidden = [];
3562
- for (const mid of moduleIds) {
3563
- (this.isExposed(mid) ? exposed : hidden).push(mid);
3564
- }
3565
- return [exposed, hidden];
3566
- }
3567
- /**
3568
- * Create an ExposureFilter from a parsed config dict.
3569
- *
3570
- * Expected: `{ expose: { mode: "include", include: ["admin.*"] } }`
3571
- */
3572
- static fromConfig(config) {
3573
- const expose = config.expose ?? {};
3574
- if (typeof expose !== "object" || expose === null || Array.isArray(expose)) {
3575
- warn("Invalid 'expose' config (expected dict), using mode: all.");
3576
- return new _ExposureFilter();
3577
- }
3578
- const exposeObj = expose;
3579
- const mode = exposeObj.mode ?? "all";
3580
- if (!["all", "include", "exclude"].includes(mode)) {
3581
- throw new Error(
3582
- `Invalid expose mode: '${mode}'. Must be one of: all, include, exclude.`
3583
- );
3584
- }
3585
- let include = exposeObj.include ?? [];
3586
- if (!Array.isArray(include)) {
3587
- warn("Invalid 'expose.include' (expected list), ignoring.");
3588
- include = [];
3589
- }
3590
- let exclude = exposeObj.exclude ?? [];
3591
- if (!Array.isArray(exclude)) {
3592
- warn("Invalid 'expose.exclude' (expected list), ignoring.");
3593
- exclude = [];
3594
- }
3595
- const filterList = (arr, label) => {
3596
- const result = [];
3597
- for (const p of arr) {
3598
- if (!p) {
3599
- warn(`Empty pattern in expose.${label}, skipping.`);
3600
- } else {
3601
- result.push(String(p));
3602
- }
3603
- }
3604
- return result;
3605
- };
3606
- return new _ExposureFilter(
3607
- mode,
3608
- filterList(include, "include"),
3609
- filterList(exclude, "exclude")
3610
- );
3611
- }
3589
+ init_output();
3590
+ PRESET_STEPS = {
3591
+ standard: [
3592
+ "context_creation",
3593
+ "call_chain_guard",
3594
+ "module_lookup",
3595
+ "acl_check",
3596
+ "approval_gate",
3597
+ "middleware_before",
3598
+ "input_validation",
3599
+ "execute",
3600
+ "output_validation",
3601
+ "middleware_after",
3602
+ "return_result"
3603
+ ],
3604
+ internal: [
3605
+ "context_creation",
3606
+ "call_chain_guard",
3607
+ "module_lookup",
3608
+ "middleware_before",
3609
+ "input_validation",
3610
+ "execute",
3611
+ "output_validation",
3612
+ "middleware_after",
3613
+ "return_result"
3614
+ ],
3615
+ testing: [
3616
+ "context_creation",
3617
+ "module_lookup",
3618
+ "middleware_before",
3619
+ "input_validation",
3620
+ "execute",
3621
+ "output_validation",
3622
+ "middleware_after",
3623
+ "return_result"
3624
+ ],
3625
+ performance: [
3626
+ "context_creation",
3627
+ "call_chain_guard",
3628
+ "module_lookup",
3629
+ "acl_check",
3630
+ "approval_gate",
3631
+ "input_validation",
3632
+ "execute",
3633
+ "output_validation",
3634
+ "return_result"
3635
+ ],
3636
+ minimal: [
3637
+ "context_creation",
3638
+ "module_lookup",
3639
+ "execute",
3640
+ "return_result"
3641
+ ]
3612
3642
  };
3613
3643
  }
3614
3644
  });
@@ -3746,7 +3776,8 @@ __export(main_exports, {
3746
3776
  import { readFileSync as readFileSync3 } from "fs";
3747
3777
  import { fileURLToPath as fileURLToPath2 } from "url";
3748
3778
  import * as path5 from "path";
3749
- import { Command as Command5, CommanderError, Option as Option4 } from "commander";
3779
+ import { Command as Command6, CommanderError, Option as Option4 } from "commander";
3780
+ import { BindingLoader, DisplayResolver } from "apcore-toolkit";
3750
3781
  function setAllOptionsHelp(allOptions) {
3751
3782
  verboseHelp = allOptions;
3752
3783
  }
@@ -3925,7 +3956,7 @@ function createCli(extensionsDirOrOpts, progName, allOptions = false) {
3925
3956
  }
3926
3957
  }
3927
3958
  const registryInjected = registry !== void 0;
3928
- 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("--all-options", "Show all options in help output (including built-in options)");
3959
+ const program = new Command6(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("--all-options", "Show all options in help output (including built-in options)");
3929
3960
  if (appVersion) {
3930
3961
  program.version(appVersion, "-V, --version", "Print version");
3931
3962
  }
@@ -4038,8 +4069,8 @@ function _registerApcliSubcommands(apcliGroup, apcliCfg, registry, executor, exp
4038
4069
  process.exit(EXIT_CODES.CONFIG_INVALID);
4039
4070
  };
4040
4071
  const effectiveRegistry = registry ?? {
4041
- listModules: () => emitUnwiredError(),
4042
- getModule: () => emitUnwiredError()
4072
+ list: () => emitUnwiredError(),
4073
+ getDefinition: () => emitUnwiredError()
4043
4074
  };
4044
4075
  const TABLE = [
4045
4076
  { name: "list", requiresExecutor: false, register: (g) => registerListCommand(g, effectiveRegistry, exposureFilter) },
@@ -4056,6 +4087,17 @@ function _registerApcliSubcommands(apcliGroup, apcliCfg, registry, executor, exp
4056
4087
  { name: "completion", requiresExecutor: false, register: (g) => registerCompletionCommand(g) },
4057
4088
  { name: "describe-pipeline", requiresExecutor: true, register: (g, _r, ex) => registerPipelineCommand(g, ex) }
4058
4089
  ];
4090
+ const _SYSTEM_COMMANDS = /* @__PURE__ */ new Set(["health", "usage", "enable", "disable", "reload", "config"]);
4091
+ const systemModulesAvailable = (() => {
4092
+ if (!executor) return false;
4093
+ const reg = executor.registry ?? registry;
4094
+ if (!reg) return false;
4095
+ try {
4096
+ return reg.getDefinition("system.health.summary") != null;
4097
+ } catch {
4098
+ return false;
4099
+ }
4100
+ })();
4059
4101
  const mode = apcliCfg.resolveVisibility();
4060
4102
  for (const entry of TABLE) {
4061
4103
  let shouldRegister;
@@ -4065,6 +4107,7 @@ function _registerApcliSubcommands(apcliGroup, apcliCfg, registry, executor, exp
4065
4107
  shouldRegister = _ALWAYS_REGISTERED.has(entry.name) || apcliCfg.isSubcommandIncluded(entry.name);
4066
4108
  }
4067
4109
  if (!shouldRegister) continue;
4110
+ if (_SYSTEM_COMMANDS.has(entry.name) && !systemModulesAvailable) continue;
4068
4111
  if (entry.requiresExecutor && !executor) {
4069
4112
  if (_ALWAYS_REGISTERED.has(entry.name)) {
4070
4113
  warn(
@@ -4086,38 +4129,22 @@ async function applyToolkitIntegration(commandsDir, bindingPath, options = {}) {
4086
4129
  if (!commandsDir && !bindingPath) {
4087
4130
  return;
4088
4131
  }
4089
- let toolkit;
4090
- try {
4091
- const toolkitModule = "apcore-toolkit";
4092
- toolkit = await import(
4093
- /* @vite-ignore */
4094
- toolkitModule
4095
- );
4096
- } catch {
4097
- warn("apcore-toolkit not installed \u2014 toolkit features unavailable");
4098
- return;
4099
- }
4100
4132
  if (commandsDir) {
4101
4133
  warn("Convention scanning not available in the TypeScript toolkit");
4102
4134
  }
4103
4135
  if (bindingPath) {
4104
4136
  try {
4105
- await loadBindingDisplayOverlay(toolkit, bindingPath, options.allowedPrefixes);
4137
+ await loadBindingDisplayOverlay(bindingPath, options.allowedPrefixes);
4106
4138
  } catch (err) {
4107
4139
  const msg = err instanceof Error ? err.message : String(err);
4108
4140
  warn(`apcore-toolkit: failed to load binding '${bindingPath}': ${msg}`);
4109
4141
  }
4110
4142
  }
4111
4143
  }
4112
- async function loadBindingDisplayOverlay(toolkit, bindingPath, allowedPrefixes) {
4113
- const BindingLoaderCtor = toolkit.BindingLoader;
4114
- const DisplayResolverCtor = toolkit.DisplayResolver;
4115
- if (!BindingLoaderCtor || !DisplayResolverCtor) {
4116
- return;
4117
- }
4118
- const loader = new BindingLoaderCtor();
4144
+ async function loadBindingDisplayOverlay(bindingPath, allowedPrefixes) {
4145
+ const loader = new BindingLoader();
4119
4146
  const scanned = loader.load(bindingPath);
4120
- const resolver = new DisplayResolverCtor();
4147
+ const resolver = new DisplayResolver();
4121
4148
  const resolved = resolver.resolve(scanned, { bindingPath });
4122
4149
  const prefixes = allowedPrefixes && allowedPrefixes.length > 0 ? allowedPrefixes : null;
4123
4150
  const isTargetAllowed = (target) => {
@@ -4166,7 +4193,7 @@ function main(progName) {
4166
4193
  }
4167
4194
  }
4168
4195
  function buildModuleCommand(moduleDef, executor, helpTextMaxLength = 1e3, cmdName, verbose = verboseHelp) {
4169
- const moduleId = moduleDef.id;
4196
+ const moduleId = moduleDef.moduleId;
4170
4197
  let resolvedSchema = {};
4171
4198
  let schemaOptions = [];
4172
4199
  const display = getDisplay(moduleDef);
@@ -4192,7 +4219,7 @@ function buildModuleCommand(moduleDef, executor, helpTextMaxLength = 1e3, cmdNam
4192
4219
  }
4193
4220
  schemaOptions = schemaToCliOptions(resolvedSchema, helpTextMaxLength);
4194
4221
  }
4195
- const cmd = new Command5(effectiveCmdName).description(cmdHelp);
4222
+ const cmd = new Command6(effectiveCmdName).description(cmdHelp);
4196
4223
  const inputOpt = new Option4("--input <source>", "Read JSON input from a file path, or use '-' to read from stdin pipe");
4197
4224
  const yesOpt = new Option4("-y, --yes", "Skip interactive approval prompts (for scripts and CI)").default(false);
4198
4225
  const largeInputOpt = new Option4("--large-input", "Allow stdin input larger than 10MB (default limit protects against accidental pipes)").default(false);