apcore-cli 0.9.1 → 0.10.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +24 -0
- package/dist/bin/apcore-cli.js +1606 -1579
- package/dist/bin/apcore-cli.js.map +1 -1
- package/dist/index.d.ts +43 -10
- package/dist/index.js +2268 -2251
- package/dist/index.js.map +1 -1
- package/package.json +8 -5
package/dist/bin/apcore-cli.js
CHANGED
|
@@ -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.
|
|
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
|
-
|
|
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.
|
|
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.
|
|
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({
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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
|
|
1432
|
-
|
|
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/
|
|
1947
|
-
|
|
1948
|
-
|
|
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
|
|
1965
|
-
|
|
1966
|
-
|
|
1967
|
-
const
|
|
1968
|
-
const
|
|
1969
|
-
|
|
1970
|
-
|
|
1971
|
-
}
|
|
1972
|
-
|
|
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
|
|
1975
|
-
var
|
|
1976
|
-
"src/
|
|
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
|
-
|
|
1981
|
-
|
|
1982
|
-
|
|
1983
|
-
|
|
1984
|
-
|
|
1985
|
-
|
|
1986
|
-
|
|
1987
|
-
|
|
1988
|
-
|
|
1989
|
-
|
|
1990
|
-
|
|
1991
|
-
|
|
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
|
-
|
|
2005
|
-
|
|
2006
|
-
|
|
2007
|
-
|
|
2008
|
-
|
|
2009
|
-
|
|
2010
|
-
|
|
2011
|
-
|
|
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
|
-
|
|
2028
|
-
|
|
2029
|
-
const
|
|
2030
|
-
|
|
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
|
-
|
|
2033
|
-
|
|
2034
|
-
|
|
2035
|
-
|
|
2036
|
-
|
|
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/
|
|
2044
|
-
|
|
2045
|
-
|
|
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
|
|
2097
|
-
|
|
2098
|
-
|
|
2099
|
-
|
|
2100
|
-
|
|
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
|
|
2121
|
-
var
|
|
2122
|
-
"src/
|
|
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
|
-
|
|
2159
|
-
|
|
2160
|
-
|
|
2161
|
-
|
|
2162
|
-
|
|
2163
|
-
|
|
2164
|
-
|
|
2165
|
-
|
|
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
|
-
*
|
|
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
|
-
*
|
|
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
|
-
|
|
2181
|
-
|
|
2182
|
-
|
|
2183
|
-
|
|
2184
|
-
|
|
2185
|
-
|
|
2186
|
-
|
|
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
|
-
*
|
|
2152
|
+
* Tier 3 constructor — config came from `apcore.yaml`.
|
|
2153
|
+
*
|
|
2154
|
+
* Env var (Tier 2) may override the yaml-supplied mode.
|
|
2199
2155
|
*/
|
|
2200
|
-
|
|
2201
|
-
if (
|
|
2202
|
-
const
|
|
2203
|
-
|
|
2204
|
-
|
|
2205
|
-
|
|
2206
|
-
|
|
2207
|
-
|
|
2208
|
-
|
|
2209
|
-
|
|
2210
|
-
|
|
2211
|
-
|
|
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
|
-
|
|
2228
|
-
|
|
2229
|
-
|
|
2230
|
-
|
|
2231
|
-
|
|
2232
|
-
|
|
2233
|
-
|
|
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 (
|
|
2238
|
-
const
|
|
2239
|
-
|
|
2240
|
-
|
|
2241
|
-
|
|
2242
|
-
|
|
2243
|
-
|
|
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
|
|
2198
|
+
return [_ApcliGroup.fromYaml(config, opts), null];
|
|
2248
2199
|
}
|
|
2249
|
-
//
|
|
2250
|
-
//
|
|
2251
|
-
//
|
|
2252
|
-
|
|
2253
|
-
|
|
2254
|
-
|
|
2255
|
-
|
|
2256
|
-
|
|
2257
|
-
|
|
2258
|
-
|
|
2259
|
-
|
|
2260
|
-
|
|
2261
|
-
|
|
2262
|
-
|
|
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 (
|
|
2265
|
-
|
|
2266
|
-
|
|
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
|
-
|
|
2244
|
+
process.exit(EXIT_CODES.INVALID_CLI_INPUT);
|
|
2269
2245
|
}
|
|
2270
|
-
const
|
|
2271
|
-
|
|
2272
|
-
|
|
2273
|
-
|
|
2274
|
-
|
|
2275
|
-
|
|
2276
|
-
|
|
2277
|
-
|
|
2278
|
-
|
|
2279
|
-
|
|
2280
|
-
|
|
2281
|
-
|
|
2282
|
-
|
|
2283
|
-
|
|
2284
|
-
|
|
2285
|
-
|
|
2286
|
-
|
|
2287
|
-
|
|
2288
|
-
|
|
2289
|
-
const
|
|
2290
|
-
const
|
|
2291
|
-
const
|
|
2292
|
-
|
|
2293
|
-
|
|
2294
|
-
|
|
2295
|
-
|
|
2296
|
-
|
|
2297
|
-
|
|
2298
|
-
|
|
2299
|
-
|
|
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
|
-
|
|
2318
|
-
|
|
2319
|
-
|
|
2320
|
-
|
|
2321
|
-
|
|
2322
|
-
|
|
2323
|
-
|
|
2324
|
-
|
|
2325
|
-
|
|
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
|
-
*
|
|
2289
|
+
* Normalize an include/exclude list. Non-array → warn and return [].
|
|
2340
2290
|
*
|
|
2341
|
-
*
|
|
2342
|
-
*
|
|
2343
|
-
*
|
|
2344
|
-
*
|
|
2345
|
-
*
|
|
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
|
-
|
|
2359
|
-
|
|
2360
|
-
|
|
2361
|
-
|
|
2362
|
-
|
|
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
|
|
2368
|
-
|
|
2369
|
-
|
|
2370
|
-
|
|
2371
|
-
|
|
2372
|
-
|
|
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
|
-
|
|
2311
|
+
out.push(entry);
|
|
2312
|
+
} else {
|
|
2313
|
+
warn(`apcli.${label} contains non-string entry; skipping.`);
|
|
2378
2314
|
}
|
|
2379
2315
|
}
|
|
2380
|
-
return
|
|
2316
|
+
return out;
|
|
2381
2317
|
}
|
|
2318
|
+
// -------------------------------------------------------------------------
|
|
2319
|
+
// Public API
|
|
2320
|
+
// -------------------------------------------------------------------------
|
|
2382
2321
|
/**
|
|
2383
|
-
*
|
|
2322
|
+
* Resolve effective visibility mode after applying tier precedence.
|
|
2384
2323
|
*
|
|
2385
|
-
*
|
|
2386
|
-
*
|
|
2387
|
-
*
|
|
2388
|
-
*
|
|
2389
|
-
* `
|
|
2390
|
-
*
|
|
2391
|
-
*
|
|
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
|
-
|
|
2395
|
-
|
|
2396
|
-
|
|
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 (
|
|
2402
|
-
|
|
2403
|
-
|
|
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
|
-
|
|
2407
|
-
|
|
2342
|
+
if (this._mode !== "auto") {
|
|
2343
|
+
return this._mode;
|
|
2344
|
+
}
|
|
2345
|
+
return this._registryInjected ? "none" : "all";
|
|
2408
2346
|
}
|
|
2409
2347
|
/**
|
|
2410
|
-
*
|
|
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
|
-
|
|
2413
|
-
|
|
2414
|
-
|
|
2415
|
-
|
|
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
|
-
//
|
|
2424
|
-
|
|
2425
|
-
|
|
2426
|
-
|
|
2427
|
-
|
|
2428
|
-
|
|
2429
|
-
|
|
2430
|
-
|
|
2431
|
-
|
|
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
|
-
|
|
2434
|
-
|
|
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
|
-
|
|
2438
|
-
|
|
2439
|
-
|
|
2440
|
-
|
|
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/
|
|
2445
|
-
|
|
2446
|
-
|
|
2447
|
-
|
|
2448
|
-
|
|
2449
|
-
|
|
2450
|
-
|
|
2451
|
-
|
|
2452
|
-
|
|
2453
|
-
|
|
2454
|
-
|
|
2455
|
-
|
|
2456
|
-
|
|
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
|
|
2459
|
-
return
|
|
2433
|
+
function getAuditLogger() {
|
|
2434
|
+
return _auditLogger;
|
|
2460
2435
|
}
|
|
2461
|
-
function
|
|
2462
|
-
|
|
2463
|
-
if (
|
|
2464
|
-
const
|
|
2465
|
-
const
|
|
2466
|
-
|
|
2467
|
-
|
|
2468
|
-
|
|
2469
|
-
|
|
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
|
-
|
|
2478
|
-
|
|
2479
|
-
|
|
2480
|
-
|
|
2481
|
-
|
|
2482
|
-
|
|
2483
|
-
|
|
2484
|
-
|
|
2485
|
-
|
|
2486
|
-
|
|
2487
|
-
|
|
2488
|
-
|
|
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
|
-
|
|
2508
|
-
|
|
2509
|
-
|
|
2510
|
-
|
|
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
|
-
|
|
2530
|
-
|
|
2531
|
-
|
|
2532
|
-
|
|
2533
|
-
|
|
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
|
-
|
|
2538
|
-
|
|
2539
|
-
|
|
2540
|
-
|
|
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
|
-
|
|
2544
|
-
|
|
2545
|
-
|
|
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
|
-
|
|
2552
|
-
|
|
2553
|
-
|
|
2554
|
-
|
|
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
|
-
|
|
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
|
-
|
|
2615
|
-
|
|
2616
|
-
|
|
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
|
|
2667
|
-
if (
|
|
2668
|
-
|
|
2669
|
-
|
|
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
|
-
|
|
2672
|
-
|
|
2673
|
-
|
|
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
|
-
|
|
2707
|
-
|
|
2708
|
-
|
|
2709
|
-
|
|
2710
|
-
|
|
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
|
|
2566
|
+
return out;
|
|
2713
2567
|
}
|
|
2714
|
-
function
|
|
2715
|
-
|
|
2716
|
-
|
|
2717
|
-
|
|
2718
|
-
|
|
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
|
-
|
|
2722
|
-
|
|
2723
|
-
|
|
2724
|
-
|
|
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
|
-
|
|
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
|
-
|
|
2736
|
-
|
|
2737
|
-
|
|
2738
|
-
|
|
2739
|
-
|
|
2740
|
-
|
|
2741
|
-
|
|
2742
|
-
|
|
2743
|
-
|
|
2744
|
-
|
|
2745
|
-
|
|
2746
|
-
|
|
2747
|
-
|
|
2748
|
-
|
|
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
|
-
|
|
2760
|
-
|
|
2761
|
-
|
|
2762
|
-
|
|
2763
|
-
|
|
2764
|
-
|
|
2765
|
-
|
|
2766
|
-
|
|
2767
|
-
|
|
2768
|
-
|
|
2769
|
-
|
|
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
|
-
|
|
2796
|
-
|
|
2797
|
-
|
|
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
|
-
|
|
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/
|
|
3001
|
-
|
|
3002
|
-
|
|
3003
|
-
|
|
3004
|
-
|
|
3005
|
-
|
|
3006
|
-
|
|
3007
|
-
|
|
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
|
-
|
|
3010
|
-
|
|
3011
|
-
|
|
3012
|
-
|
|
3013
|
-
|
|
3014
|
-
|
|
3015
|
-
|
|
3016
|
-
|
|
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
|
-
|
|
3068
|
-
|
|
3069
|
-
|
|
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
|
-
|
|
3100
|
-
|
|
3101
|
-
|
|
3102
|
-
|
|
3103
|
-
|
|
3104
|
-
|
|
3105
|
-
|
|
3106
|
-
|
|
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
|
-
|
|
3119
|
-
|
|
3120
|
-
|
|
3121
|
-
|
|
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/
|
|
3176
|
-
|
|
3177
|
-
|
|
3178
|
-
|
|
3179
|
-
|
|
3180
|
-
|
|
3181
|
-
|
|
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
|
-
|
|
3187
|
-
|
|
3188
|
-
|
|
3189
|
-
|
|
3190
|
-
|
|
3191
|
-
|
|
3192
|
-
|
|
3193
|
-
|
|
3194
|
-
|
|
3195
|
-
|
|
3196
|
-
|
|
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
|
-
|
|
3200
|
-
|
|
3201
|
-
|
|
3202
|
-
|
|
3203
|
-
|
|
3204
|
-
|
|
3205
|
-
|
|
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
|
-
|
|
3243
|
-
|
|
3244
|
-
|
|
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
|
-
|
|
3252
|
-
|
|
3253
|
-
|
|
3254
|
-
|
|
3255
|
-
|
|
3256
|
-
|
|
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
|
-
|
|
3265
|
-
|
|
3266
|
-
|
|
3267
|
-
|
|
3268
|
-
|
|
3269
|
-
|
|
3270
|
-
|
|
3271
|
-
|
|
3272
|
-
|
|
3273
|
-
|
|
3274
|
-
|
|
3275
|
-
|
|
3276
|
-
|
|
3277
|
-
|
|
3278
|
-
|
|
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
|
-
|
|
3282
|
-
|
|
3283
|
-
|
|
3284
|
-
|
|
3285
|
-
|
|
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
|
-
|
|
3290
|
-
|
|
3291
|
-
|
|
3292
|
-
|
|
3293
|
-
|
|
3294
|
-
|
|
3295
|
-
|
|
3296
|
-
|
|
3297
|
-
|
|
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
|
|
3095
|
+
return;
|
|
3311
3096
|
}
|
|
3312
|
-
|
|
3313
|
-
|
|
3314
|
-
|
|
3315
|
-
|
|
3316
|
-
|
|
3317
|
-
|
|
3318
|
-
|
|
3319
|
-
|
|
3320
|
-
|
|
3321
|
-
|
|
3322
|
-
|
|
3323
|
-
|
|
3324
|
-
|
|
3325
|
-
|
|
3326
|
-
|
|
3327
|
-
|
|
3328
|
-
|
|
3329
|
-
|
|
3330
|
-
|
|
3331
|
-
|
|
3332
|
-
|
|
3333
|
-
|
|
3334
|
-
|
|
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
|
-
|
|
3352
|
-
|
|
3353
|
-
|
|
3354
|
-
`
|
|
3355
|
-
|
|
3356
|
-
|
|
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
|
-
|
|
3359
|
-
|
|
3360
|
-
|
|
3361
|
-
|
|
3362
|
-
|
|
3363
|
-
|
|
3364
|
-
|
|
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
|
-
|
|
3368
|
-
|
|
3369
|
-
|
|
3370
|
-
|
|
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
|
-
|
|
3374
|
-
|
|
3375
|
-
|
|
3376
|
-
|
|
3377
|
-
|
|
3378
|
-
|
|
3379
|
-
|
|
3380
|
-
|
|
3381
|
-
|
|
3382
|
-
|
|
3383
|
-
|
|
3384
|
-
|
|
3385
|
-
|
|
3386
|
-
|
|
3387
|
-
|
|
3388
|
-
|
|
3389
|
-
|
|
3390
|
-
|
|
3391
|
-
|
|
3392
|
-
|
|
3393
|
-
|
|
3394
|
-
|
|
3395
|
-
|
|
3396
|
-
|
|
3397
|
-
|
|
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
|
-
|
|
3402
|
-
|
|
3403
|
-
|
|
3404
|
-
|
|
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
|
-
|
|
3458
|
-
|
|
3459
|
-
|
|
3460
|
-
|
|
3461
|
-
|
|
3462
|
-
|
|
3463
|
-
|
|
3464
|
-
|
|
3465
|
-
|
|
3466
|
-
|
|
3467
|
-
|
|
3468
|
-
|
|
3469
|
-
|
|
3470
|
-
|
|
3471
|
-
|
|
3472
|
-
|
|
3473
|
-
|
|
3474
|
-
|
|
3475
|
-
|
|
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
|
-
|
|
3479
|
-
|
|
3480
|
-
|
|
3481
|
-
|
|
3482
|
-
|
|
3483
|
-
|
|
3484
|
-
|
|
3485
|
-
|
|
3486
|
-
|
|
3487
|
-
|
|
3488
|
-
|
|
3489
|
-
|
|
3490
|
-
|
|
3491
|
-
|
|
3492
|
-
|
|
3493
|
-
|
|
3494
|
-
|
|
3495
|
-
|
|
3496
|
-
|
|
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
|
-
|
|
3508
|
-
|
|
3509
|
-
|
|
3510
|
-
}
|
|
3511
|
-
|
|
3512
|
-
|
|
3513
|
-
|
|
3514
|
-
|
|
3515
|
-
|
|
3516
|
-
|
|
3517
|
-
|
|
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
|
-
|
|
3520
|
-
regex = regex.replaceAll("\\*\\*", ".+");
|
|
3521
|
-
return new RegExp(`^${regex}$`);
|
|
3582
|
+
cli.addCommand(pipelineCmd);
|
|
3522
3583
|
}
|
|
3523
|
-
var
|
|
3524
|
-
var
|
|
3525
|
-
"src/
|
|
3584
|
+
var PRESET_STEPS;
|
|
3585
|
+
var init_strategy = __esm({
|
|
3586
|
+
"src/strategy.ts"() {
|
|
3526
3587
|
"use strict";
|
|
3527
3588
|
init_esm_shims();
|
|
3528
|
-
|
|
3529
|
-
|
|
3530
|
-
|
|
3531
|
-
|
|
3532
|
-
|
|
3533
|
-
|
|
3534
|
-
|
|
3535
|
-
|
|
3536
|
-
|
|
3537
|
-
|
|
3538
|
-
|
|
3539
|
-
|
|
3540
|
-
|
|
3541
|
-
|
|
3542
|
-
|
|
3543
|
-
|
|
3544
|
-
|
|
3545
|
-
|
|
3546
|
-
|
|
3547
|
-
|
|
3548
|
-
|
|
3549
|
-
|
|
3550
|
-
|
|
3551
|
-
|
|
3552
|
-
|
|
3553
|
-
|
|
3554
|
-
|
|
3555
|
-
|
|
3556
|
-
|
|
3557
|
-
|
|
3558
|
-
|
|
3559
|
-
|
|
3560
|
-
|
|
3561
|
-
|
|
3562
|
-
|
|
3563
|
-
|
|
3564
|
-
|
|
3565
|
-
|
|
3566
|
-
|
|
3567
|
-
|
|
3568
|
-
|
|
3569
|
-
|
|
3570
|
-
|
|
3571
|
-
|
|
3572
|
-
|
|
3573
|
-
|
|
3574
|
-
|
|
3575
|
-
|
|
3576
|
-
|
|
3577
|
-
|
|
3578
|
-
|
|
3579
|
-
|
|
3580
|
-
|
|
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
|
|
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
|
|
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
|
-
|
|
4042
|
-
|
|
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(
|
|
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(
|
|
4113
|
-
const
|
|
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
|
|
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.
|
|
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
|
|
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);
|