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