@b-apm/cli 0.1.0 → 0.2.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/cli.mjs CHANGED
@@ -1,5 +1,5 @@
1
1
  #!/usr/bin/env node
2
- import { t as runCli } from "./run-DV8BK0XR.mjs";
2
+ import { t as runCli } from "./run-B56hJHe9.mjs";
3
3
  //#region src/cli.ts
4
4
  const code = await runCli(process.argv.slice(2));
5
5
  process.exitCode = code;
package/dist/index.mjs CHANGED
@@ -1,2 +1,2 @@
1
- import { t as runCli } from "./run-DV8BK0XR.mjs";
1
+ import { t as runCli } from "./run-B56hJHe9.mjs";
2
2
  export { runCli };
@@ -1,7 +1,7 @@
1
1
  import { createRequire } from "node:module";
2
- import { BAPM_LOCK_FILE, BAPM_MANIFEST_FILE, BAPM_NAME, ManifestError, MarketplaceFetchError, MarketplaceNotFoundError, addMarketplace, addMarketplacePackage, assertExperimentalRegistriesEnabled, autoDetectMarketplacePath, buildPublishArchive, cacheClean, cacheInfo, checkMarketplaceAuthoring, checkReleaseTag, checkSelfUpdate, classifyMarketplaceHostKind, clearMarketplaceCache, compileAgentsMd, createDefaultDownloader, createDefaultGitRemote, createDefaultTagLister, createMarketplaceSource, createMinimalManifest, createRegistryClient, declaredTargetIntegrationMap, exportSbom, extractPackArchive, fetchMarketplace, findPath, formatAuditCiJson, formatAuditCiSarif, getMarketplace, getVersion, initMarketplaceAuthoring, listDeps, listMarketplaces, loadManifest, loadMarketplaceAuthoringConfig, migrateMarketplaceYml, persistUserExecutableGrant, removeMarketplace, removeMarketplacePackage, resolveAndLock, resolveEffectiveFrozen, resolveRegistryBaseUrl, runAuditCi, runDoctor, runInstall, runOutdated, runPack, runPolicyStatus, runPrune, runUninstall, runUpdate, setMarketplacePackage, treeDeps, urlNamesRemoteManifest, validateMarketplace, validatePluginName, validateProjectName, viewPackage, whyDeps, writeAgentPluginManifest, writePluginJson, writeProducerManifest } from "@b-apm/core";
3
- import { existsSync, mkdirSync, readFileSync, statSync, writeFileSync } from "node:fs";
4
- import { basename, dirname, isAbsolute, join, relative, resolve } from "node:path";
2
+ import { BAPM_LOCK_FILE, BAPM_MANIFEST_FILE, BAPM_NAME, ManifestError, MarketplaceFetchError, MarketplaceNotFoundError, addMarketplace, addMarketplacePackage, assertExperimentalRegistriesEnabled, autoDetectMarketplacePath, buildPublishArchive, cacheClean, cacheInfo, checkMarketplaceAuthoring, checkReleaseTag, checkSelfUpdate, checkVersionAlignment, classifyMarketplaceHostKind, clearMarketplaceCache, compileAgentsMd, createDefaultDownloader, createDefaultGitRemote, createDefaultTagLister, createMarketplaceSource, createMinimalManifest, createRegistryClient, declaredTargetIntegrationMap, detectAuthoringConfigSource, exportSbom, extractPackArchive, fetchMarketplace, findPath, formatAuditCiJson, formatAuditCiSarif, getMarketplace, getVersion, initMarketplaceAuthoring, listDeps, listMarketplaces, loadManifest, loadMarketplaceAuthoringConfig, migrateMarketplaceYml, persistUserExecutableGrant, removeMarketplace, removeMarketplacePackage, resolveAndLock, resolveEffectiveFrozen, resolveRegistryBaseUrl, runAuditCi, runDoctor, runInstall, runOutdated, runPack, runPolicyStatus, runPrune, runUninstall, runUpdate, setMarketplacePackage, treeDeps, urlNamesRemoteManifest, validateMarketplace, validatePluginName, validateProjectName, versionAlignmentErrorMessages, viewPackage, whyDeps, writeAgentPluginManifest, writePluginJson, writeProducerManifest } from "@b-apm/core";
3
+ import { existsSync, mkdirSync, readFileSync, realpathSync, statSync, writeFileSync } from "node:fs";
4
+ import { basename, dirname, isAbsolute, join, relative, resolve, sep } from "node:path";
5
5
  import { spawn } from "node:child_process";
6
6
  import { createInterface } from "node:readline";
7
7
  import { pathToFileURL } from "node:url";
@@ -29,17 +29,17 @@ Commands:
29
29
  plugin Scaffold a thin plugin project (plugin init)
30
30
  lock Resolve dependencies and write the lockfile (no host deploy)
31
31
  install Install agentic dependencies from ${deps.manifestFile} (or a pack .zip)
32
- pack Build a plain-zip producer archive (--archive / --check-release)
32
+ pack Build a plain-zip producer archive (--archive / --check-release / --check-versions)
33
33
  publish Publish flat registry zip via PUT (experimental; BAPM_EXPERIMENTAL_REGISTRIES=1)
34
34
  self-update Check / apply CLI updates from npm (--check)
35
- update Re-resolve pins (rs-011/rs-012); --dry-run / -y
35
+ update Re-resolve dependency pins; --dry-run / -y
36
36
  outdated Compare lock pins to remote tips (exit 0 when outdated)
37
37
  uninstall Remove packages from manifest, modules, deploy, lock
38
38
  prune Remove orphan modules not in the resolved graph
39
- deps Inspect lock deps (list | tree | why)
39
+ deps Inspect lock deps (list | tree | why) or wipe modules (clean)
40
40
  audit Integrity checks (--ci gate)
41
41
  doctor Environment and project sanity checks
42
- compile Emit AGENTS.md from discovered primitives (cursor)
42
+ compile Emit the agent's instructions file (AGENTS.md, CLAUDE.md, …)
43
43
  cache Modules-cache info | clean (apm_modules)
44
44
  policy Policy status (read-only governance posture)
45
45
  approve Persist user-local MCP allow (~/.bapm/config.json; not project yml)
@@ -54,6 +54,7 @@ Install flags (see also: bapm help install):
54
54
  --no-frozen Opt out of frozen (including when CI defaults to frozen)
55
55
  --target <id> Force a registered host target (overrides manifest active)
56
56
  --trust-transitive-mcp Deploy dependency MCP (default: direct dependencies.mcp only)
57
+ --trust-bin / --no-trust-bin Per-invocation bin/ consent (non-interactive defaults to skip)
57
58
 
58
59
  Experimental registries:
59
60
  Set BAPM_EXPERIMENTAL_REGISTRIES=1 to enable registry resolve/install and publish.
@@ -70,7 +71,7 @@ Usage:
70
71
  Options:
71
72
  --frozen Fail if lock is missing or pins drift; re-verify deployed_file_hashes when present
72
73
  --no-frozen Opt out of frozen mode (including CI-default frozen)
73
- --force Accept force. Does not refresh refs and does not bypass frozen or policy
74
+ --force Accepted for APM parity; currently has no effect (does not refresh refs or bypass frozen/policy)
74
75
  --allow-insecure Dual-consent half for direct http:// dependencies
75
76
  --allow-insecure-host <hostname>
76
77
  Allow transitive http:// from this FQDN (repeatable)
@@ -81,23 +82,32 @@ Options:
81
82
  --policy <path> Use explicit policy file (wins over apm-policy.yml / bapm-policy.yml)
82
83
  --no-policy Skip policy discovery and checks (also: BAPM_POLICY_DISABLE=1)
83
84
  --trust-transitive-mcp Deploy MCP from dependencies (default: direct dependencies.mcp only)
85
+ --trust-bin Consent to deploy dependency/plugin bin/ for this run (when policy allows)
86
+ --no-trust-bin Skip bin/ deploy for this run (even when policy would allow)
84
87
  --help, -h Show this help
85
88
 
86
89
  Host selection:
87
- Priority: --target <id> → manifest active: [<id>, …] → sole auto-detect → fail.
90
+ Priority: --target <id> → manifest active: { target: <id> } → sole auto-detect → fail.
88
91
  Use active in bapm.yml / apm.yml when detect is missing or ambiguous.
89
92
 
90
- MCP / Cursor:
91
- When the cursor target is active, eligible MCP servers are written to .cursor/mcp.json.
93
+ MCP:
94
+ Eligible MCP servers are written to the active agent's MCP config (for Cursor: .cursor/mcp.json).
92
95
  Direct dependencies.mcp are deployed by default. Transitive MCP requires --trust-transitive-mcp
93
- (or an executables.allow / allowExecutables grant for that package — sc-009).
94
- Auto-detect without .cursor/ does not mkdir solely for MCP.
96
+ or an executables.allow grant for that package. Once any grant exists (bapm.yml executables,
97
+ bapm approve / deny), every dependency with MCP must be approved or denied.
98
+
99
+ Bin consent:
100
+ Non-interactive (CI / frozen / non-TTY) installs skip dependency bin/ unless --trust-bin or a
101
+ persisted executables.allow bin grant applies. Interactive installs deploy with a warning.
102
+ Flags cannot override org/project deny.
95
103
 
96
104
  Unknown flags are rejected. --frozen and --no-frozen cannot be combined.
105
+ --trust-bin and --no-trust-bin cannot be combined.
97
106
  Frozen (explicit or when CI is truthy) cannot be combined with --update.
98
107
  When the CI environment variable is truthy (not "", "0", or "false"), install
99
- defaults to frozen unless --no-frozen is passed (OpenAPM req-lk-018).
100
- A local .zip path is detected as a pack archive (install-from-archive round-trip).
108
+ defaults to frozen unless --no-frozen is passed.
109
+ A local .zip path is installed as a pack archive: members are added to the current directory,
110
+ nothing else is removed, and existing files with different content are never overwritten.
101
111
  `;
102
112
  }
103
113
  //#endregion
@@ -115,8 +125,11 @@ Options:
115
125
 
116
126
  Notes:
117
127
  Requires BAPM_EXPERIMENTAL_REGISTRIES=1 (experimental registries gate).
118
- Auth: set BAPM_REGISTRY_TOKEN (Bearer) for protected registries.
119
- Archive layout: apm.yml + .apm/ at zip root (distinct from M7 pack).
128
+ Auth: set BAPM_REGISTRY_<NAME>_TOKEN or BAPM_REGISTRY_TOKEN (Bearer) for protected registries.
129
+ Requires name: owner/repo, registries.default, and a non-empty .apm/ directory.
130
+ Archive layout: apm.yml + .apm/ at zip root (differs from the bapm pack zip).
131
+ Optional project-root .bapmignore omits matching docs and .apm members (wire apm.yml always kept);
132
+ ignoring all of .apm fails closed. --zip uploads prebuilt bytes without re-applying .bapmignore.
120
133
  Unknown flags are rejected.
121
134
  `;
122
135
  }
@@ -1630,10 +1643,12 @@ async function outdatedCommand(argv, outdated) {
1630
1643
  //#endregion
1631
1644
  //#region src/commands/pack.ts
1632
1645
  async function packCommand(argv, pack) {
1633
- return (await pack.run({
1646
+ const result = await pack.run({
1634
1647
  args: argv,
1635
1648
  cwd: process.cwd()
1636
- })).ok ? 0 : 1;
1649
+ });
1650
+ if (result.ok) return 0;
1651
+ return result.exitCode ?? 1;
1637
1652
  }
1638
1653
  //#endregion
1639
1654
  //#region src/commands/plugin.ts
@@ -1705,7 +1720,7 @@ async function viewCommand(argv, view) {
1705
1720
  //#endregion
1706
1721
  //#region src/modules/Approve/services/runApprove.ts
1707
1722
  function formatApproveHelp(deps) {
1708
- return `${deps.name} approve — Persist user-local executable allow (sc-010)
1723
+ return `${deps.name} approve — Persist user-local executable allow
1709
1724
 
1710
1725
  Usage:
1711
1726
  bapm approve <package-name> [--user]
@@ -1920,7 +1935,7 @@ Usage:
1920
1935
  Options:
1921
1936
  --ci CI gate: lock present + deployed presence + hash re-verify
1922
1937
  -f, --format Output format: text (default), json, or sarif
1923
- -o, --output Write report body to file (mkdir parents; body not on stdout)
1938
+ -o, --output Write json/sarif report to file (mkdir parents); text always prints to stdout
1924
1939
  `;
1925
1940
  }
1926
1941
  function serializeBody(format, result) {
@@ -2091,7 +2106,7 @@ Usage:
2091
2106
  bapm cache clean [-y|--yes] [--dry-run]
2092
2107
 
2093
2108
  Options:
2094
- -y, --yes Confirm clean without interactive prompt
2109
+ -y, --yes Required to clean (there is no interactive prompt)
2095
2110
  --dry-run Preview clean without deleting (no -y required)
2096
2111
  --help, -h Show this help
2097
2112
 
@@ -2278,25 +2293,58 @@ function isContainedUnderRoot(absoluteTarget, projectRoot) {
2278
2293
  if (targetRelativeToRoot === "") return true;
2279
2294
  return !(targetRelativeToRoot === ".." || targetRelativeToRoot.startsWith("..\\") || targetRelativeToRoot.startsWith("../") || isAbsolute(targetRelativeToRoot));
2280
2295
  }
2281
- /** Resolve npm package from the project install (and optionally Node globalPaths). */
2282
- function resolveNpmProjectOrGlobal(specifier, cwd) {
2296
+ let cachedGlobalModuleRoots;
2297
+ /**
2298
+ * Return the node_modules root only when the invoked entrypoint is this CLI
2299
+ * package. Resolving the symlink handles npm and pnpm global bin shims without
2300
+ * spawning a package manager during every CLI process.
2301
+ */
2302
+ function globalModuleRootForCliEntry(entryPath) {
2303
+ if (!entryPath) return void 0;
2304
+ let entry = resolve(entryPath);
2305
+ try {
2306
+ entry = realpathSync(entry);
2307
+ } catch {}
2308
+ for (let directory = dirname(entry);; directory = dirname(directory)) {
2309
+ if (basename(directory) === "node_modules") {
2310
+ const packagePath = relative(directory, entry).split(sep);
2311
+ return packagePath[0] === "@b-apm" && packagePath[1] === "cli" ? directory : void 0;
2312
+ }
2313
+ if (dirname(directory) === directory) return void 0;
2314
+ }
2315
+ }
2316
+ function isWorkspaceModuleRoot(moduleRoot) {
2317
+ for (let directory = dirname(moduleRoot);; directory = dirname(directory)) {
2318
+ if (existsSync(join(directory, "pnpm-workspace.yaml"))) return true;
2319
+ if (dirname(directory) === directory) return false;
2320
+ }
2321
+ }
2322
+ /** Locate the global module root of the invoked CLI without consulting dev dependencies. */
2323
+ function globalModuleRoots() {
2324
+ if (cachedGlobalModuleRoots) return cachedGlobalModuleRoots;
2325
+ const root = globalModuleRootForCliEntry(process.argv[1]);
2326
+ cachedGlobalModuleRoots = root && !isWorkspaceModuleRoot(root) ? [root] : [];
2327
+ return cachedGlobalModuleRoots;
2328
+ }
2329
+ /** Resolve a canonical package from the project, global paths, or global module roots. */
2330
+ function resolveNpmProjectOrGlobal(specifier, cwd, globalRoots = globalModuleRoots()) {
2283
2331
  const requireFromCwd = createRequire(join(cwd, "package.json"));
2284
2332
  try {
2285
2333
  return requireFromCwd.resolve(specifier);
2286
2334
  } catch (cwdErr) {
2287
2335
  const mod = createRequire(import.meta.url)("node:module");
2288
- const globalPaths = Array.isArray(mod.globalPaths) ? mod.globalPaths : [];
2289
- if (globalPaths.length === 0) throw cwdErr;
2336
+ const searchRoots = [...Array.isArray(mod.globalPaths) ? mod.globalPaths.filter((path) => !isWorkspaceModuleRoot(path)) : [], ...globalRoots];
2337
+ if (searchRoots.length === 0) throw cwdErr;
2290
2338
  try {
2291
- return requireFromCwd.resolve(specifier, { paths: [...globalPaths] });
2339
+ return requireFromCwd.resolve(specifier, { paths: searchRoots });
2292
2340
  } catch {
2293
2341
  throw cwdErr;
2294
2342
  }
2295
2343
  }
2296
2344
  }
2297
- /** npm package: project cwd first, then CLI-shipped fallback (map) or globalPaths (canonical). */
2345
+ /** npm package: project cwd first, then CLI fallback (map) or global lookup (canonical). */
2298
2346
  function resolveNpmPackageSpecifier(specifier, cwd, options = {}) {
2299
- if (!(options.allowCliFallback !== false)) return resolveNpmProjectOrGlobal(specifier, cwd);
2347
+ if (!(options.allowCliFallback !== false)) return resolveNpmProjectOrGlobal(specifier, cwd, options.globalRoots);
2300
2348
  const requireFromCwd = createRequire(join(cwd, "package.json"));
2301
2349
  try {
2302
2350
  return requireFromCwd.resolve(specifier);
@@ -2354,7 +2402,10 @@ async function loadIntegrationFromPackage(specifier, expectedId, cwd, options =
2354
2402
  let resolved;
2355
2403
  if (isLocalPathSpecifier(specifier)) resolved = resolveLocalIntegrationPath(specifier, expectedId, cwd);
2356
2404
  else try {
2357
- resolved = resolveNpmPackageSpecifier(specifier, cwd, { allowCliFallback: options.allowCliFallback });
2405
+ resolved = resolveNpmPackageSpecifier(specifier, cwd, {
2406
+ allowCliFallback: options.allowCliFallback,
2407
+ globalRoots: options.globalRoots
2408
+ });
2358
2409
  } catch (cause) {
2359
2410
  throw new ManifestIntegrationLoadError(expectedId, specifier, "unresolvable", `unresolvable module: ${cause instanceof Error ? cause.message : "cannot find module / unresolvable package"}`);
2360
2411
  }
@@ -2433,7 +2484,7 @@ function createCliIntegrationRegistry() {
2433
2484
  /**
2434
2485
  * Append actionable install guidance for unknown-target / missing-detect diagnostics.
2435
2486
  */
2436
- const ALREADY_GUIDED = /@bapm\/integration-|install the (?:host )?integration|npm i |not installed or resolvable/i;
2487
+ const ALREADY_GUIDED = /@b-apm\/integration-|install the (?:host )?integration|npm i |not installed or resolvable/i;
2437
2488
  function extractUnregisteredId(message) {
2438
2489
  return message.match(/unknown or unregistered target:\s*([^\s,;]+)/i)?.[1]?.replace(/[."']+$/, "");
2439
2490
  }
@@ -2568,7 +2619,7 @@ function parseCompileArgs(argv) {
2568
2619
  };
2569
2620
  }
2570
2621
  function formatCompileHelp(deps) {
2571
- return `${deps.name} compile — Emit target-owned output from discovered primitives
2622
+ return `${deps.name} compile — Emit the agent's instructions file (AGENTS.md, CLAUDE.md, …)
2572
2623
 
2573
2624
  Usage:
2574
2625
  bapm compile [-o PATH] [--target <id>] [--dry-run] [-v] [--validate]
@@ -2584,6 +2635,7 @@ Options:
2584
2635
  Host selection: --target <id> → sole manifest active → sole auto-detect → fail.
2585
2636
  When active lists multiple hosts, pass --target <id>. Compile stays single-host.
2586
2637
  The selected target determines the default output path and rendering.
2638
+ Run from the directory that contains ${deps.manifestFile}.
2587
2639
  `;
2588
2640
  }
2589
2641
  async function runCompileCli(deps, options) {
@@ -2668,7 +2720,7 @@ const compile = createCompile({
2668
2720
  //#endregion
2669
2721
  //#region src/modules/Deny/services/runDeny.ts
2670
2722
  function formatDenyHelp(deps) {
2671
- return `${deps.name} deny — Persist user-local executable deny (sc-010)
2723
+ return `${deps.name} deny — Persist user-local executable deny
2672
2724
 
2673
2725
  Usage:
2674
2726
  bapm deny <package-name> [--user]
@@ -2980,7 +3032,7 @@ Options:
2980
3032
 
2981
3033
  Informational:
2982
3034
  auth Whether GITHUB_TOKEN / GH_TOKEN is set (names only, never secrets)
2983
- network With -v: git ls-remote probe (never critical)
3035
+ network With -v: git ls-remote probe of github.com (never critical)
2984
3036
  `;
2985
3037
  }
2986
3038
  async function runDoctorCli(deps, options) {
@@ -3197,12 +3249,14 @@ Usage:
3197
3249
 
3198
3250
  Options:
3199
3251
  -y, --yes Non-interactive defaults (version 0.1.0)
3200
- --target <id> Record host target (e.g. cursor)
3252
+ --target <id> Activate an agent (e.g. cursor); also writes a targets: entry that
3253
+ limits deployment to that agent
3201
3254
  --help, -h Show this help
3202
3255
 
3203
3256
  Notes:
3204
3257
  Writes ${deps.manifestFile} only. Refuses if apm.yml or bapm.yml already exists.
3205
- Unknown flags are rejected. No plugin / marketplace scaffold in M7.
3258
+ Remove the generated targets: block if the project is shared with other agents.
3259
+ Unknown flags are rejected. For plugin or marketplace scaffolds use bapm plugin init / bapm marketplace init.
3206
3260
  `;
3207
3261
  }
3208
3262
  function parseInitArgs(argv) {
@@ -3269,7 +3323,7 @@ function scaffoldTargetFields(targetId) {
3269
3323
  const id = targetId.trim();
3270
3324
  return {
3271
3325
  targets: { [id]: `@b-apm/integration-${id}` },
3272
- active: [id]
3326
+ active: { target: id }
3273
3327
  };
3274
3328
  }
3275
3329
  async function runInit(deps, options) {
@@ -3354,6 +3408,90 @@ const init = createInit({
3354
3408
  detectCursor: (cwd) => existsSync(join(cwd, ".cursor"))
3355
3409
  });
3356
3410
  //#endregion
3411
+ //#region src/common/formatErrorCauseChain.ts
3412
+ const DEFAULT_MAX_DEPTH = 5;
3413
+ const DEFAULT_INDENT = " ";
3414
+ const CAUSE_LABEL = "cause";
3415
+ const CAUSE_INDENT_CONTINUATION = " ";
3416
+ function isObjectLike(value) {
3417
+ return typeof value === "object" && value !== null || typeof value === "function";
3418
+ }
3419
+ function safeString(value) {
3420
+ try {
3421
+ return String(value);
3422
+ } catch {
3423
+ try {
3424
+ return Object.prototype.toString.call(value);
3425
+ } catch {
3426
+ return "[unprintable value]";
3427
+ }
3428
+ }
3429
+ }
3430
+ /** Best-effort message for Errors, error-like objects, strings, and primitives. */
3431
+ function formatErrorMessage(value) {
3432
+ if (value instanceof Error) return value.message;
3433
+ if (typeof value === "string") return value;
3434
+ if (isObjectLike(value)) try {
3435
+ if ("message" in value) return safeString(value.message);
3436
+ } catch {}
3437
+ return safeString(value);
3438
+ }
3439
+ function causeOf(value) {
3440
+ if (!isObjectLike(value)) return { present: false };
3441
+ try {
3442
+ if (!("cause" in value)) return { present: false };
3443
+ return {
3444
+ present: true,
3445
+ cause: value.cause
3446
+ };
3447
+ } catch {
3448
+ return { present: false };
3449
+ }
3450
+ }
3451
+ function renderCause(value, indent) {
3452
+ const [first = "", ...rest] = formatErrorMessage(value).split(/\r?\n/);
3453
+ const continuation = `${indent}${CAUSE_INDENT_CONTINUATION}`;
3454
+ return [`${indent}${CAUSE_LABEL}: ${first}`, ...rest.map((line) => `${continuation}${line}`)].join("\n");
3455
+ }
3456
+ /**
3457
+ * Format `error` for terminal output.
3458
+ *
3459
+ * @example
3460
+ * formatErrorCauseChain(err, { verbose: true })
3461
+ * // "Failed to download https://…\n cause: Local path not found: skills/frontend-design"
3462
+ */
3463
+ function formatErrorCauseChain(error, options = {}) {
3464
+ const rootMessage = options.message ?? formatErrorMessage(error);
3465
+ if (!options.verbose) return rootMessage;
3466
+ const maxDepth = options.maxDepth !== void 0 && Number.isFinite(options.maxDepth) ? Math.max(0, Math.floor(options.maxDepth)) : DEFAULT_MAX_DEPTH;
3467
+ if (maxDepth === 0) return rootMessage;
3468
+ const indent = options.indent ?? DEFAULT_INDENT;
3469
+ const visited = /* @__PURE__ */ new WeakSet();
3470
+ if (isObjectLike(error)) visited.add(error);
3471
+ const lines = [rootMessage];
3472
+ let current = error;
3473
+ let depth = 0;
3474
+ for (;;) {
3475
+ const link = causeOf(current);
3476
+ if (!link.present) break;
3477
+ const cause = link.cause;
3478
+ if (cause === void 0 || cause === null) break;
3479
+ if (isObjectLike(cause) && visited.has(cause)) {
3480
+ lines.push(`${indent}${CAUSE_LABEL}: [circular cause]`);
3481
+ break;
3482
+ }
3483
+ if (depth >= maxDepth) {
3484
+ lines.push(`${indent}${CAUSE_LABEL}: [cause chain truncated after ${maxDepth} level(s)]`);
3485
+ break;
3486
+ }
3487
+ if (isObjectLike(cause)) visited.add(cause);
3488
+ lines.push(renderCause(cause, indent));
3489
+ current = cause;
3490
+ depth += 1;
3491
+ }
3492
+ return lines.join("\n");
3493
+ }
3494
+ //#endregion
3357
3495
  //#region src/modules/Install/services/runInstall.ts
3358
3496
  function formatInstallHelp(deps) {
3359
3497
  return `${deps.name} install — Install agentic dependencies from ${deps.manifestFile}
@@ -3367,7 +3505,7 @@ Options:
3367
3505
  --frozen Fail if lock is missing or pins drift; re-verify deployed_file_hashes when present
3368
3506
  --no-frozen Opt out of frozen mode (including CI-default frozen)
3369
3507
  --dry-run Preview direct deps / would-add; no durable project writes
3370
- --force Accept force (overwrite / future security gates). Does not refresh refs and does not bypass frozen or policy
3508
+ --force Accepted for APM parity; currently has no effect (does not refresh refs or bypass frozen/policy)
3371
3509
  --allow-insecure Allow direct http:// deps that also set allow_insecure: true (dual consent)
3372
3510
  --allow-insecure-host <hostname>
3373
3511
  Allow transitive http:// deps from this FQDN (repeatable)
@@ -3381,22 +3519,29 @@ Options:
3381
3519
  --policy <path> Use explicit policy file (wins over apm-policy.yml / bapm-policy.yml)
3382
3520
  --no-policy Skip policy discovery and checks (also: BAPM_POLICY_DISABLE=1)
3383
3521
  --trust-transitive-mcp Deploy MCP from dependencies (default: direct dependencies.mcp only)
3522
+ --trust-bin Consent to deploy dependency/plugin bin/ for this run (when policy allows)
3523
+ --no-trust-bin Skip bin/ deploy for this run (even when policy would allow)
3384
3524
  --help, -h Show this help
3385
3525
 
3386
3526
  Notes:
3387
3527
  Unknown flags are rejected. Combining --frozen with --no-frozen is an error.
3528
+ Combining --trust-bin with --no-trust-bin is an error.
3388
3529
  Combining frozen (explicit or CI-default) with --update is an error.
3389
- Frozen integrity (lk-015/017/018) is kept; MCP config sync vs pins is optional/default-off.
3530
+ Frozen re-verifies lock pins, deployed file hashes and package tree hashes.
3390
3531
  When the CI environment variable is truthy (not "", "0", or "false"), install
3391
- defaults to frozen unless --no-frozen is passed (OpenAPM req-lk-018).
3392
- A local .zip path is consumed as a pack archive (install-from-archive).
3532
+ defaults to frozen unless --no-frozen is passed.
3533
+ Non-interactive installs (CI, BAPM_NON_INTERACTIVE, non-TTY, or frozen) do not deploy
3534
+ dependency bin/ by default without --trust-bin or a persisted executables.allow bin grant;
3535
+ interactive installs deploy it with a warning.
3536
+ --trust-bin / --no-trust-bin are per-invocation consent and cannot override org/project deny.
3537
+ A local .zip path is installed as a pack archive: members are added to the current directory,
3538
+ nothing else is removed, and existing files with different content are never overwritten.
3393
3539
  Non-zip positionals are package refs added to dependencies.apm (auto-creates
3394
3540
  a minimal manifest when missing). Frozen rejects positional package-ref add.
3395
3541
  --exclude filters MCP/runtime configure only — not a full skip-install.
3396
- --force is distinct from --target (forced-target activation).
3397
- Host selection: --target <id> → manifest active: [<id>, …] → sole auto-detect → fail.
3542
+ Host selection: --target <id> → manifest active: { target: <id> } → sole auto-detect → fail.
3398
3543
  Set active in bapm.yml / apm.yml when detect is missing or ambiguous.
3399
- When cursor is active, eligible MCP servers write .cursor/mcp.json (direct mcp by default).
3544
+ Eligible MCP servers are written to the active agent's MCP config (direct mcp by default).
3400
3545
  `;
3401
3546
  }
3402
3547
  function parseInstallArgs(argv, options = {}) {
@@ -3418,6 +3563,8 @@ function parseInstallArgs(argv, options = {}) {
3418
3563
  let policyPath;
3419
3564
  let noPolicy = false;
3420
3565
  let trustTransitiveMcp = false;
3566
+ let trustBinAllow = false;
3567
+ let trustBinDeny = false;
3421
3568
  let help = false;
3422
3569
  const partial = () => ({
3423
3570
  frozen: frozenFlag,
@@ -3437,7 +3584,8 @@ function parseInstallArgs(argv, options = {}) {
3437
3584
  parallelDownloads,
3438
3585
  policyPath,
3439
3586
  noPolicy,
3440
- trustTransitiveMcp
3587
+ trustTransitiveMcp,
3588
+ trustBin: trustBinAllow ? "allow" : trustBinDeny ? "deny" : "default"
3441
3589
  });
3442
3590
  for (let i = 0; i < argv.length; i++) {
3443
3591
  const arg = argv[i];
@@ -3485,6 +3633,14 @@ function parseInstallArgs(argv, options = {}) {
3485
3633
  trustTransitiveMcp = true;
3486
3634
  continue;
3487
3635
  }
3636
+ if (arg === "--trust-bin") {
3637
+ trustBinAllow = true;
3638
+ continue;
3639
+ }
3640
+ if (arg === "--no-trust-bin") {
3641
+ trustBinDeny = true;
3642
+ continue;
3643
+ }
3488
3644
  if (arg === "--allow-insecure-host") {
3489
3645
  const next = argv[i + 1];
3490
3646
  if (!next || next.startsWith("-")) return {
@@ -3649,6 +3805,11 @@ function parseInstallArgs(argv, options = {}) {
3649
3805
  ...partial(),
3650
3806
  error: "Cannot combine --frozen and --no-frozen (mutually exclusive flags conflict)"
3651
3807
  };
3808
+ if (trustBinAllow && trustBinDeny) return {
3809
+ ...partial(),
3810
+ error: "Cannot combine --trust-bin and --no-trust-bin (mutually exclusive flags conflict)"
3811
+ };
3812
+ const trustBin = trustBinAllow ? "allow" : trustBinDeny ? "deny" : "default";
3652
3813
  const env = options.env ?? process.env;
3653
3814
  let frozen;
3654
3815
  try {
@@ -3661,12 +3822,14 @@ function parseInstallArgs(argv, options = {}) {
3661
3822
  const message = error instanceof Error ? error.message : String(error);
3662
3823
  return {
3663
3824
  ...partial(),
3825
+ trustBin,
3664
3826
  error: message
3665
3827
  };
3666
3828
  }
3667
3829
  if (frozen && update) return {
3668
3830
  ...partial(),
3669
3831
  frozen,
3832
+ trustBin,
3670
3833
  error: "Frozen mode rejects --update (frozen+update mutation rejected)"
3671
3834
  };
3672
3835
  return {
@@ -3688,6 +3851,7 @@ function parseInstallArgs(argv, options = {}) {
3688
3851
  policyPath,
3689
3852
  noPolicy,
3690
3853
  trustTransitiveMcp,
3854
+ trustBin,
3691
3855
  help
3692
3856
  };
3693
3857
  }
@@ -3785,6 +3949,8 @@ async function runCoreInstall(deps, options, parsed, archivePath) {
3785
3949
  policy: parsed.policyPath,
3786
3950
  noPolicy: parsed.noPolicy,
3787
3951
  trustTransitiveMcp: parsed.trustTransitiveMcp,
3952
+ trustBin: parsed.trustBin,
3953
+ env: options.env ?? process.env,
3788
3954
  gitRemote: createDefaultGitRemote(),
3789
3955
  tagLister: createDefaultTagLister(),
3790
3956
  downloader: createDefaultDownloader()
@@ -3797,8 +3963,12 @@ async function runCoreInstall(deps, options, parsed, archivePath) {
3797
3963
  } else if (result.activeTargets && result.activeTargets.length > 0) console.log(`${deps.name}: installed for ${result.activeTargets.join(", ")}`);
3798
3964
  return { ok: true };
3799
3965
  } catch (error) {
3800
- const message = enrichUnregisteredTargetMessage(error instanceof Error ? error.message : typeof error === "object" && error !== null && "message" in error ? String(error.message) : String(error), cwd);
3801
- console.error(`${deps.name}: ${message}`);
3966
+ const message = enrichUnregisteredTargetMessage(formatErrorMessage(error), cwd);
3967
+ const rendered = formatErrorCauseChain(error, {
3968
+ verbose: parsed.verbose,
3969
+ message
3970
+ });
3971
+ console.error(`${deps.name}: ${rendered}`);
3802
3972
  return {
3803
3973
  ok: false,
3804
3974
  message
@@ -4062,10 +4232,12 @@ Usage:
4062
4232
  bapm pack [options]
4063
4233
 
4064
4234
  Options:
4065
- --archive Write a plain zip artifact (M7 MUST path)
4235
+ --archive Write a plain zip artifact (also the default without flags)
4066
4236
  --agent-plugins Pack a validated Agent Plugins v1 portable root
4067
4237
  --dry-run Validate / collect without durable zip or marketplace.json
4068
- --check-release pr-004 tag↔manifest version gate
4238
+ --check-release Check that the manifest version matches the git tag
4239
+ --check-versions Marketplace local-package version alignment (lockstep /
4240
+ tag_pattern / per_package); plugin.json fallback when no YAML
4069
4241
  --tag <name> Tag under check (optional with --check-release; else HEAD)
4070
4242
  --marketplace <all|none|list>
4071
4243
  Filter host marketplace emit (claude,codex); default: all configured
@@ -4077,7 +4249,10 @@ Options:
4077
4249
 
4078
4250
  Notes:
4079
4251
  Unknown flags are rejected. --check-release never creates or pushes tags.
4080
- Pack refuses secret-pattern paths (.env, *.pem, …) per sc-007.
4252
+ --check-versions is distinct from --check-release (marketplace alignment vs tag↔root version).
4253
+ Pack refuses secret-pattern paths (.env, *.pem, …).
4254
+ Optional project-root .bapmignore (gitignore syntax) omits matching files from the zip;
4255
+ root bapm.yml/apm.yml stay included; .gitignore is not used as a fallback.
4081
4256
  When marketplace: is present with outputs selected, pack emits Claude/Codex marketplace.json.
4082
4257
  Marketplace-only projects (no dependencies:) emit JSON and skip empty zip.
4083
4258
  --agent-plugins never emits marketplace output and requires root plugin.json.
@@ -4088,6 +4263,7 @@ function parsePackArgs(argv) {
4088
4263
  let agentPlugins = false;
4089
4264
  let dryRun = false;
4090
4265
  let checkRelease = false;
4266
+ let checkVersions = false;
4091
4267
  let tag;
4092
4268
  let marketplace;
4093
4269
  const marketplacePaths = [];
@@ -4099,6 +4275,7 @@ function parsePackArgs(argv) {
4099
4275
  agentPlugins,
4100
4276
  dryRun,
4101
4277
  checkRelease,
4278
+ checkVersions,
4102
4279
  tag,
4103
4280
  marketplace,
4104
4281
  marketplacePaths,
@@ -4127,6 +4304,10 @@ function parsePackArgs(argv) {
4127
4304
  checkRelease = true;
4128
4305
  continue;
4129
4306
  }
4307
+ if (arg === "--check-versions") {
4308
+ checkVersions = true;
4309
+ continue;
4310
+ }
4130
4311
  if (arg === "--offline") {
4131
4312
  offline = true;
4132
4313
  continue;
@@ -4205,6 +4386,65 @@ function parsePackArgs(argv) {
4205
4386
  help
4206
4387
  };
4207
4388
  }
4389
+ const VERSION_ALIGNMENT_EXIT = 3;
4390
+ function runCheckVersionsGate(deps, cwd) {
4391
+ const root = cwd ?? process.cwd();
4392
+ const detect = deps.detectAuthoringConfigSource;
4393
+ const load = deps.loadMarketplaceAuthoringConfig;
4394
+ const check = deps.checkVersionAlignment;
4395
+ const formatErrors = deps.versionAlignmentErrorMessages;
4396
+ if (!detect || !load || !check || !formatErrors) {
4397
+ const message = "Internal error: --check-versions deps not wired";
4398
+ console.error(`${deps.name}: ${message}`);
4399
+ return {
4400
+ ok: false,
4401
+ message,
4402
+ exitCode: 1
4403
+ };
4404
+ }
4405
+ const detected = detect({ cwd: root });
4406
+ if (!detected.ok || detected.kind === "none") {
4407
+ console.log(`${deps.name}: Version alignment check skipped: no marketplace block; nothing to check.`);
4408
+ return {
4409
+ ok: true,
4410
+ exitCode: 0
4411
+ };
4412
+ }
4413
+ let config;
4414
+ try {
4415
+ config = load({ cwd: root }).config;
4416
+ } catch (error) {
4417
+ const message = error instanceof Error ? error.message : typeof error === "object" && error !== null && "message" in error ? String(error.message) : String(error);
4418
+ console.error(`${deps.name}: ${message}`);
4419
+ return {
4420
+ ok: false,
4421
+ message,
4422
+ exitCode: 1
4423
+ };
4424
+ }
4425
+ const report = check({
4426
+ config,
4427
+ cwd: root,
4428
+ projectRoot: root,
4429
+ root
4430
+ });
4431
+ if (report.ok) {
4432
+ if (report.expected !== null) console.log(`${deps.name}: Version alignment OK [strategy=${report.strategy}, expected=${report.expected}]`);
4433
+ else console.log(`${deps.name}: Version alignment OK [strategy=${report.strategy}]`);
4434
+ return {
4435
+ ok: true,
4436
+ exitCode: 0
4437
+ };
4438
+ }
4439
+ if (report.expected !== null) console.error(`${deps.name}: Version alignment failed [strategy=${report.strategy}, expected=${report.expected}]`);
4440
+ else console.error(`${deps.name}: Version alignment failed [strategy=${report.strategy}]`);
4441
+ for (const msg of formatErrors(report)) console.error(`${deps.name}: ${msg}`);
4442
+ return {
4443
+ ok: false,
4444
+ message: "Version alignment check failed",
4445
+ exitCode: VERSION_ALIGNMENT_EXIT
4446
+ };
4447
+ }
4208
4448
  async function runPackCli(deps, options) {
4209
4449
  const parsed = parsePackArgs(options.args ?? []);
4210
4450
  if (parsed.help) {
@@ -4220,6 +4460,16 @@ async function runPackCli(deps, options) {
4220
4460
  }
4221
4461
  const cwd = options.cwd;
4222
4462
  try {
4463
+ if (parsed.checkVersions && !parsed.archive && parsed.marketplace === void 0) {
4464
+ if (parsed.checkRelease) {
4465
+ const gate = await deps.checkReleaseTag({
4466
+ cwd,
4467
+ tag: parsed.tag
4468
+ });
4469
+ for (const w of gate.warnings ?? []) console.error(`${deps.name}: warning: ${w}`);
4470
+ }
4471
+ return runCheckVersionsGate(deps, cwd);
4472
+ }
4223
4473
  if (parsed.checkRelease && !parsed.archive && parsed.marketplace === void 0) {
4224
4474
  const gate = await deps.checkReleaseTag({
4225
4475
  cwd,
@@ -4228,7 +4478,11 @@ async function runPackCli(deps, options) {
4228
4478
  for (const w of gate.warnings ?? []) console.error(`${deps.name}: warning: ${w}`);
4229
4479
  return { ok: true };
4230
4480
  }
4231
- if (!parsed.archive && !parsed.checkRelease) parsed.archive = true;
4481
+ if (!parsed.archive && !parsed.checkRelease && !parsed.checkVersions) parsed.archive = true;
4482
+ if (parsed.checkVersions) {
4483
+ const versionGate = runCheckVersionsGate(deps, cwd);
4484
+ if (!versionGate.ok) return versionGate;
4485
+ }
4232
4486
  const result = await deps.runPack({
4233
4487
  cwd,
4234
4488
  agentPlugins: parsed.agentPlugins,
@@ -4425,7 +4679,21 @@ const pack = createPack({
4425
4679
  marketplaceOutputs
4426
4680
  });
4427
4681
  },
4428
- checkReleaseTag: (options) => checkReleaseTag(options)
4682
+ checkReleaseTag: (options) => checkReleaseTag(options),
4683
+ checkVersionAlignment: (options) => checkVersionAlignment({
4684
+ config: options.config,
4685
+ cwd: options.cwd,
4686
+ projectRoot: options.projectRoot,
4687
+ root: options.root
4688
+ }),
4689
+ loadMarketplaceAuthoringConfig: (options) => loadMarketplaceAuthoringConfig(options),
4690
+ detectAuthoringConfigSource: (options) => detectAuthoringConfigSource(options),
4691
+ versionAlignmentErrorMessages: (report) => versionAlignmentErrorMessages({
4692
+ strategy: "lockstep",
4693
+ expected: null,
4694
+ ok: false,
4695
+ packages: report.packages
4696
+ })
4429
4697
  });
4430
4698
  //#endregion
4431
4699
  //#region src/modules/Plugin/services/runPlugin.ts
@@ -4457,7 +4725,7 @@ Usage:
4457
4725
 
4458
4726
  Options:
4459
4727
  -y, --yes Non-interactive defaults (version 0.1.0); allow overwrite
4460
- --target <id> Record host target (e.g. cursor)
4728
+ --target <id> Write target: <id> (limits deployment to that agent; not with --agent-plugins)
4461
4729
  --agent-plugins Create an Agent Plugins v1 portable root (no bapm.yml)
4462
4730
  --skills Include a minimal portable skills/example/SKILL.md layout
4463
4731
  -v, --verbose Extra logging (optional)
@@ -5480,7 +5748,7 @@ function parseUpdateArgs(argv) {
5480
5748
  };
5481
5749
  }
5482
5750
  function formatUpdateHelp(deps) {
5483
- return `${deps.name} update — Re-resolve dependency pins (rs-011/rs-012)
5751
+ return `${deps.name} update — Re-resolve dependency pins
5484
5752
 
5485
5753
  Usage:
5486
5754
  bapm update [packages...] [options]
package/package.json CHANGED
@@ -1,8 +1,22 @@
1
1
  {
2
2
  "name": "@b-apm/cli",
3
- "version": "0.1.0",
3
+ "version": "0.2.1",
4
4
  "description": "bapm CLI — Better Agent Package Manager",
5
+ "keywords": [
6
+ "b-apm",
7
+ "bapm",
8
+ "unstable"
9
+ ],
10
+ "homepage": "https://github.com/Sdju/bapm#readme",
11
+ "bugs": {
12
+ "url": "https://github.com/Sdju/bapm/issues"
13
+ },
5
14
  "license": "MIT",
15
+ "repository": {
16
+ "type": "git",
17
+ "url": "git+https://github.com/Sdju/bapm.git",
18
+ "directory": "packages/cli"
19
+ },
6
20
  "bin": {
7
21
  "bapm": "./dist/cli.mjs"
8
22
  },
@@ -19,8 +33,8 @@
19
33
  "access": "public"
20
34
  },
21
35
  "dependencies": {
22
- "@b-apm/integration-api": "0.1.0",
23
- "@b-apm/core": "0.1.0"
36
+ "@b-apm/integration-api": "0.2.1",
37
+ "@b-apm/core": "0.2.1"
24
38
  },
25
39
  "devDependencies": {
26
40
  "@types/node": "^26.1.2",
@@ -28,7 +42,10 @@
28
42
  "typescript": "^7.0.2",
29
43
  "vite": "npm:@voidzero-dev/vite-plus-core@latest",
30
44
  "vite-plus": "latest",
31
- "@b-apm/integration-cursor": "0.1.0"
45
+ "@b-apm/integration-cursor": "0.2.1"
46
+ },
47
+ "engines": {
48
+ "node": ">=22.12.0"
32
49
  },
33
50
  "feod": {
34
51
  "srcRoot": "src",
@@ -54,23 +71,6 @@
54
71
  "privateModulesPrefix": null
55
72
  }
56
73
  },
57
- "engines": {
58
- "node": ">=22.12.0"
59
- },
60
- "repository": {
61
- "type": "git",
62
- "url": "git+https://github.com/Sdju/bapm.git",
63
- "directory": "packages/cli"
64
- },
65
- "homepage": "https://github.com/Sdju/bapm#readme",
66
- "bugs": {
67
- "url": "https://github.com/Sdju/bapm/issues"
68
- },
69
- "keywords": [
70
- "unstable",
71
- "bapm",
72
- "b-apm"
73
- ],
74
74
  "scripts": {
75
75
  "build": "vp pack",
76
76
  "dev": "vp pack --watch",