@kb-labs/cli-commands 2.30.0 → 2.32.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.
@@ -46,6 +46,7 @@ interface RegisteredCommand {
46
46
  unavailableReason?: string;
47
47
  hint?: string;
48
48
  source: 'workspace' | 'node_modules' | 'linked' | 'builtin';
49
+ scope?: 'platform' | 'project';
49
50
  shadowed: boolean;
50
51
  pkgRoot?: string;
51
52
  packageName?: string;
@@ -55,6 +56,13 @@ interface CommandModule {
55
56
  }
56
57
  interface DiscoveryResult {
57
58
  source: 'workspace' | 'node_modules' | 'linked' | 'builtin';
59
+ /**
60
+ * Scope this result came from. Platform-wide discovery (monorepo workspace,
61
+ * platform node_modules) reports `platform`; discovery from
62
+ * `<projectRoot>/.kb/plugins/` reports `project`. On collision (same
63
+ * packageName in both scopes) the platform entry wins — see ADR-0012.
64
+ */
65
+ scope: 'platform' | 'project';
58
66
  packageName: string;
59
67
  manifestPath: string;
60
68
  pkgRoot: string;
@@ -88,7 +96,14 @@ interface CacheFile {
88
96
  lockfileHash?: string;
89
97
  configHash?: string;
90
98
  pluginsStateHash?: string;
91
- marketplaceLockHash?: string;
99
+ /** Hash of <platformRoot>/.kb/marketplace.lock. Invalidates when global plugins change. */
100
+ platformMarketplaceLockHash?: string;
101
+ /** Hash of <projectRoot>/.kb/marketplace.lock. Invalidates when project-scoped plugins change. */
102
+ projectMarketplaceLockHash?: string;
103
+ /** Absolute platform root at the time of caching — cache is invalid if it differs on next run. */
104
+ platformRoot?: string;
105
+ /** Absolute project root at the time of caching. */
106
+ projectRoot?: string;
92
107
  packages: Record<string, PackageCacheEntry>;
93
108
  }
94
109
  type AvailabilityCheck = {
@@ -261,6 +276,7 @@ declare function resetInProcCache(): void;
261
276
  */
262
277
  declare function discoverManifests(cwd: string, noCache?: boolean, options?: {
263
278
  platformRoot?: string;
279
+ projectRoot?: string;
264
280
  }): Promise<DiscoveryResult[]>;
265
281
  /**
266
282
  * Lazy load manifests for a specific namespace
package/dist/index.d.ts CHANGED
@@ -1,5 +1,5 @@
1
- import { C as CommandGroup, P as ProductGroup, a as Command, R as RegisteredCommand } from './discover-DhqYgS-M.js';
2
- export { A as AvailabilityCheck, b as CacheFile, c as CommandLookupResult, d as CommandManifest, e as CommandModule, f as CommandType, D as DiscoveryResult, F as FlagDefinition, G as GlobalFlags, g as PackageCacheEntry, h as discoverManifests, i as discoverManifestsByNamespace, j as findCommand, k as findCommandWithType, r as registry } from './discover-DhqYgS-M.js';
1
+ import { C as CommandGroup, P as ProductGroup, a as Command, R as RegisteredCommand } from './discover-Cz7X5bQp.js';
2
+ export { A as AvailabilityCheck, b as CacheFile, c as CommandLookupResult, d as CommandManifest, e as CommandModule, f as CommandType, D as DiscoveryResult, F as FlagDefinition, G as GlobalFlags, g as PackageCacheEntry, h as discoverManifests, i as discoverManifestsByNamespace, j as findCommand, k as findCommandWithType, r as registry } from './discover-Cz7X5bQp.js';
3
3
  import { ILogger } from '@kb-labs/core-platform';
4
4
  export { TimingTracker } from '@kb-labs/shared-cli-ui';
5
5
  import * as _kb_labs_shared_command_kit from '@kb-labs/shared-command-kit';
@@ -17,6 +17,13 @@ interface RegisterBuiltinCommandsInput {
17
17
  * dev-mode behavior unchanged.
18
18
  */
19
19
  platformRoot?: string;
20
+ /**
21
+ * The user's project root (directory containing `.kb/kb.config.{json,jsonc}`).
22
+ * Discovery uses this to pick up project-scope plugins from
23
+ * `<projectRoot>/.kb/plugins/` and to invalidate the cache when the
24
+ * project's `marketplace.lock` changes. When omitted, falls back to `cwd`.
25
+ */
26
+ projectRoot?: string;
20
27
  }
21
28
  declare function registerBuiltinCommands(input?: RegisterBuiltinCommandsInput): Promise<void>;
22
29
 
package/dist/index.js CHANGED
@@ -249,8 +249,8 @@ async function computePluginsStateHash(cwd) {
249
249
  return "";
250
250
  }
251
251
  }
252
- async function computeMarketplaceLockHash(cwd) {
253
- const lockPath = path.join(cwd, ".kb", "marketplace.lock");
252
+ async function computeMarketplaceLockHashAt(root) {
253
+ const lockPath = path.join(root, ".kb", "marketplace.lock");
254
254
  try {
255
255
  const content = await promises.readFile(lockPath, "utf8");
256
256
  return createHash("sha256").update(content).digest("hex");
@@ -479,22 +479,19 @@ async function discoverWorkspace(cwd) {
479
479
  }
480
480
  }
481
481
  }
482
- const localPluginPattern = path.join(
483
- ".kb",
484
- "plugins",
485
- "*",
486
- "packages",
487
- "*-entry",
488
- PACKAGE_JSON
489
- );
490
- const localPluginFiles = await glob(localPluginPattern, {
491
- cwd,
482
+ return loadManifestsForPackages(packageInfos, "workspace", "platform");
483
+ }
484
+ async function discoverProjectLocalPlugins(projectRoot) {
485
+ const pattern = path.join(".kb", "plugins", "*", "packages", "*-entry", PACKAGE_JSON);
486
+ const files = await glob(pattern, {
487
+ cwd: projectRoot,
492
488
  absolute: false,
493
489
  ignore: ["**/node_modules/**"]
494
490
  });
495
- for (const pkgFile of localPluginFiles) {
496
- const pkgRoot = path.dirname(path.join(cwd, pkgFile));
497
- const pkg = await readPackageJson(path.join(cwd, pkgFile));
491
+ const packageInfos = [];
492
+ for (const pkgFile of files) {
493
+ const pkgRoot = path.dirname(path.join(projectRoot, pkgFile));
494
+ const pkg = await readPackageJson(path.join(projectRoot, pkgFile));
498
495
  if (!pkg) {
499
496
  continue;
500
497
  }
@@ -503,6 +500,9 @@ async function discoverWorkspace(cwd) {
503
500
  packageInfos.push({ pkgRoot, pkg, manifestPath: manifestInfo.path });
504
501
  }
505
502
  }
503
+ return loadManifestsForPackages(packageInfos, "linked", "project");
504
+ }
505
+ async function loadManifestsForPackages(packageInfos, source, scope) {
506
506
  const loadPromises = packageInfos.map(async ({ pkgRoot, pkg, manifestPath }) => {
507
507
  const pkgStart = Date.now();
508
508
  try {
@@ -515,7 +515,8 @@ async function discoverWorkspace(cwd) {
515
515
  validateUniqueIds(manifests, pkg.name);
516
516
  return {
517
517
  manifests,
518
- source: "workspace",
518
+ source,
519
+ scope,
519
520
  packageName: pkg.name,
520
521
  manifestPath: toPosixPath(manifestPath),
521
522
  pkgRoot: toPosixPath(pkgRoot)
@@ -536,7 +537,8 @@ async function discoverWorkspace(cwd) {
536
537
  const synthetic = createUnavailableManifest(pkg.name, err);
537
538
  return {
538
539
  manifests: [synthetic],
539
- source: "workspace",
540
+ source,
541
+ scope,
540
542
  packageName: pkg.name,
541
543
  manifestPath: toPosixPath(manifestPath),
542
544
  pkgRoot: toPosixPath(pkgRoot)
@@ -570,6 +572,9 @@ async function discoverCurrentPackage(cwd) {
570
572
  return {
571
573
  manifests,
572
574
  source: "workspace",
575
+ // Current-package fallback fires in installed mode when the user's
576
+ // project itself exposes a manifest. That maps to the project scope.
577
+ scope: "project",
573
578
  packageName: pkg.name,
574
579
  manifestPath: toPosixPath(manifestInfo.path),
575
580
  pkgRoot: toPosixPath(cwd)
@@ -674,6 +679,10 @@ async function discoverNodeModules(cwd) {
674
679
  return {
675
680
  manifests,
676
681
  source: isLinked ? "linked" : "node_modules",
682
+ // node_modules discovery runs at the platform root. In dev mode
683
+ // platformRoot === projectRoot so either label is correct; we use
684
+ // platform to reflect where the manifest physically lives.
685
+ scope: "platform",
677
686
  packageName: pkg.name,
678
687
  manifestPath: toPosixPath(manifestPath),
679
688
  pkgRoot: toPosixPath(pkgRoot)
@@ -693,6 +702,7 @@ async function discoverNodeModules(cwd) {
693
702
  return {
694
703
  manifests: [synthetic],
695
704
  source: isLinked ? "linked" : "node_modules",
705
+ scope: "platform",
696
706
  packageName: pkg.name,
697
707
  manifestPath: toPosixPath(manifestPath),
698
708
  pkgRoot: toPosixPath(pkgRoot)
@@ -724,17 +734,35 @@ function deduplicateManifests(all) {
724
734
  const existing = byPackageName.get(result.packageName);
725
735
  if (!existing) {
726
736
  byPackageName.set(result.packageName, result);
727
- } else {
728
- const existingPriority = priority[existing.source] || 0;
729
- const newPriority = priority[result.source] || 0;
730
- if (newPriority > existingPriority) {
731
- byPackageName.set(result.packageName, result);
737
+ continue;
738
+ }
739
+ if (existing.scope !== result.scope) {
740
+ if (existing.pkgRoot === result.pkgRoot) {
741
+ const projectEntry = existing.scope === "project" ? existing : result;
742
+ byPackageName.set(result.packageName, projectEntry);
743
+ continue;
732
744
  }
745
+ const winner = existing.scope === "platform" ? existing : result;
746
+ const loser = existing.scope === "platform" ? result : existing;
747
+ log("warn", JSON.stringify({
748
+ code: "DISCOVERY_SCOPE_COLLISION",
749
+ packageName: result.packageName,
750
+ platformPath: winner.pkgRoot,
751
+ projectPath: loser.pkgRoot,
752
+ message: "Package exists in both platform and project scopes \u2014 platform wins."
753
+ }));
754
+ byPackageName.set(result.packageName, winner);
755
+ continue;
756
+ }
757
+ const existingPriority = priority[existing.source] ?? 0;
758
+ const newPriority = priority[result.source] ?? 0;
759
+ if (newPriority > existingPriority) {
760
+ byPackageName.set(result.packageName, result);
733
761
  }
734
762
  }
735
763
  return Array.from(byPackageName.values());
736
764
  }
737
- async function loadCache(cwd) {
765
+ async function loadCache(cwd, roots) {
738
766
  const cachePath = path.join(cwd, ".kb", "cache", "cli-manifests.json");
739
767
  try {
740
768
  const content = await promises.readFile(cachePath, "utf8");
@@ -771,11 +799,26 @@ async function loadCache(cwd) {
771
799
  log("debug", "Cache invalidated: .kb/plugins.json changed");
772
800
  return null;
773
801
  }
774
- const currentMarketplaceLockHash = await computeMarketplaceLockHash(cwd);
775
- if (currentMarketplaceLockHash && cache.marketplaceLockHash && cache.marketplaceLockHash !== currentMarketplaceLockHash) {
776
- log("debug", "Cache invalidated: .kb/marketplace.lock changed");
802
+ if (cache.platformRoot && cache.platformRoot !== roots.platformRoot) {
803
+ log("debug", `Cache invalidated: platformRoot changed (${cache.platformRoot} \u2192 ${roots.platformRoot})`);
804
+ return null;
805
+ }
806
+ if (cache.projectRoot && cache.projectRoot !== roots.projectRoot) {
807
+ log("debug", `Cache invalidated: projectRoot changed (${cache.projectRoot} \u2192 ${roots.projectRoot})`);
808
+ return null;
809
+ }
810
+ const currentPlatformLockHash = await computeMarketplaceLockHashAt(roots.platformRoot);
811
+ if (currentPlatformLockHash && cache.platformMarketplaceLockHash && cache.platformMarketplaceLockHash !== currentPlatformLockHash) {
812
+ log("debug", `Cache invalidated: ${roots.platformRoot}/.kb/marketplace.lock changed`);
777
813
  return null;
778
814
  }
815
+ if (roots.projectRoot !== roots.platformRoot) {
816
+ const currentProjectLockHash = await computeMarketplaceLockHashAt(roots.projectRoot);
817
+ if (currentProjectLockHash && cache.projectMarketplaceLockHash && cache.projectMarketplaceLockHash !== currentProjectLockHash) {
818
+ log("debug", `Cache invalidated: ${roots.projectRoot}/.kb/marketplace.lock changed`);
819
+ return null;
820
+ }
821
+ }
779
822
  const parsedCache = cache;
780
823
  parsedCache.ttlMs = parsedCache.ttlMs ?? DISK_CACHE_TTL_MS;
781
824
  for (const entry of Object.values(parsedCache.packages)) {
@@ -827,7 +870,7 @@ async function isPackageCacheStale(entry, options) {
827
870
  }
828
871
  return false;
829
872
  }
830
- async function saveCache(cwd, results) {
873
+ async function saveCache(cwd, results, roots) {
831
874
  const cachePath = path.join(cwd, ".kb", "cache", "cli-manifests.json");
832
875
  await promises.mkdir(path.dirname(cachePath), { recursive: true });
833
876
  const packages = {};
@@ -873,7 +916,8 @@ async function saveCache(cwd, results) {
873
916
  const lockfileHash = await computeLockfileHash(cwd);
874
917
  const configHash = await computeConfigHash(cwd);
875
918
  const pluginsStateHash = await computePluginsStateHash(cwd);
876
- const marketplaceLockHash = await computeMarketplaceLockHash(cwd);
919
+ const platformMarketplaceLockHash = await computeMarketplaceLockHashAt(roots.platformRoot);
920
+ const projectMarketplaceLockHash = roots.projectRoot !== roots.platformRoot ? await computeMarketplaceLockHashAt(roots.projectRoot) : "";
877
921
  if (lockfileHash) {
878
922
  stateHasher.update(lockfileHash);
879
923
  }
@@ -883,9 +927,14 @@ async function saveCache(cwd, results) {
883
927
  if (pluginsStateHash) {
884
928
  stateHasher.update(pluginsStateHash);
885
929
  }
886
- if (marketplaceLockHash) {
887
- stateHasher.update(marketplaceLockHash);
930
+ if (platformMarketplaceLockHash) {
931
+ stateHasher.update(platformMarketplaceLockHash);
932
+ }
933
+ if (projectMarketplaceLockHash) {
934
+ stateHasher.update(projectMarketplaceLockHash);
888
935
  }
936
+ stateHasher.update(roots.platformRoot);
937
+ stateHasher.update(roots.projectRoot);
889
938
  const cache = {
890
939
  version: process.version,
891
940
  cliVersion: process.env.CLI_VERSION || "0.1.0",
@@ -895,7 +944,10 @@ async function saveCache(cwd, results) {
895
944
  lockfileHash: lockfileHash || void 0,
896
945
  configHash: configHash || void 0,
897
946
  pluginsStateHash: pluginsStateHash || void 0,
898
- marketplaceLockHash: marketplaceLockHash || void 0,
947
+ platformMarketplaceLockHash: platformMarketplaceLockHash || void 0,
948
+ projectMarketplaceLockHash: projectMarketplaceLockHash || void 0,
949
+ platformRoot: roots.platformRoot,
950
+ projectRoot: roots.projectRoot,
899
951
  packages
900
952
  };
901
953
  try {
@@ -907,6 +959,9 @@ async function saveCache(cwd, results) {
907
959
  async function discoverManifests(cwd, noCache = false, options = {}) {
908
960
  const startTime = Date.now();
909
961
  const timings = {};
962
+ const platformRoot = options.platformRoot ?? cwd;
963
+ const projectRoot = options.projectRoot ?? cwd;
964
+ const roots = { platformRoot, projectRoot };
910
965
  if (noCache) {
911
966
  inProcDiscoveryCache = null;
912
967
  } else if (inProcDiscoveryCache) {
@@ -923,7 +978,7 @@ async function discoverManifests(cwd, noCache = false, options = {}) {
923
978
  }
924
979
  if (!noCache) {
925
980
  const cacheStart = Date.now();
926
- const cached = await loadCache(cwd);
981
+ const cached = await loadCache(cwd, roots);
927
982
  timings.cacheLoad = Date.now() - cacheStart;
928
983
  if (cached) {
929
984
  log("debug", "Using cached manifests");
@@ -966,7 +1021,7 @@ async function discoverManifests(cwd, noCache = false, options = {}) {
966
1021
  let workspace = [];
967
1022
  try {
968
1023
  const wsStart = Date.now();
969
- workspace = await discoverWorkspace(cwd);
1024
+ workspace = await discoverWorkspace(platformRoot);
970
1025
  timings.workspace = Date.now() - wsStart;
971
1026
  log("info", `Discovered ${workspace.length} workspace packages with CLI manifests`);
972
1027
  } catch (_err) {
@@ -980,17 +1035,22 @@ async function discoverManifests(cwd, noCache = false, options = {}) {
980
1035
  }
981
1036
  }
982
1037
  const nmStart = Date.now();
983
- const nodeModulesRoot = options.platformRoot ?? cwd;
984
- const installed = await discoverNodeModules(nodeModulesRoot);
1038
+ const installed = await discoverNodeModules(platformRoot);
985
1039
  timings.nodeModules = Date.now() - nmStart;
986
1040
  if (installed.length > 0) {
987
1041
  log("info", `Discovered ${installed.length} installed packages with CLI manifests`);
988
1042
  }
1043
+ const plStart = Date.now();
1044
+ const projectLocal = await discoverProjectLocalPlugins(projectRoot);
1045
+ timings.projectLocal = Date.now() - plStart;
1046
+ if (projectLocal.length > 0) {
1047
+ log("info", `Discovered ${projectLocal.length} project-local plugins`);
1048
+ }
989
1049
  const dedupStart = Date.now();
990
- const results = deduplicateManifests([...workspace, ...installed]);
1050
+ const results = deduplicateManifests([...workspace, ...installed, ...projectLocal]);
991
1051
  timings.deduplicate = Date.now() - dedupStart;
992
1052
  const saveStart = Date.now();
993
- await saveCache(cwd, results);
1053
+ await saveCache(cwd, results, roots);
994
1054
  timings.cacheSave = Date.now() - saveStart;
995
1055
  const totalTime = Date.now() - startTime;
996
1056
  const sourceCounts = results.reduce((acc, r) => {
@@ -1690,7 +1750,8 @@ var diag = defineSystemCommand({
1690
1750
  });
1691
1751
  try {
1692
1752
  const platformRoot = process.env.KB_PLATFORM_ROOT;
1693
- const discovered = await discoverManifests(cwd, false, { platformRoot });
1753
+ const projectRoot = process.env.KB_PROJECT_ROOT;
1754
+ const discovered = await discoverManifests(cwd, false, { platformRoot, projectRoot });
1694
1755
  const manifests = registry.listManifests();
1695
1756
  const enabled = manifests.filter((m) => m.available && !m.shadowed).length;
1696
1757
  const disabled = manifests.filter((m) => !m.available).length;
@@ -3981,7 +4042,8 @@ async function registerBuiltinCommands(input = {}) {
3981
4042
  const noCache = process.argv.includes("--no-cache") || env.KB_PLUGIN_NO_CACHE === "1";
3982
4043
  const { discoverManifests: discoverManifests2 } = await Promise.resolve().then(() => (init_discover(), discover_exports));
3983
4044
  const discovered = await discoverManifests2(cwd, noCache, {
3984
- platformRoot: input.platformRoot
4045
+ platformRoot: input.platformRoot,
4046
+ projectRoot: input.projectRoot
3985
4047
  });
3986
4048
  if (discovered.length > 0) {
3987
4049
  log3.info(`Discovered ${discovered.length} packages with CLI manifests`);