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