@kb-labs/cli-commands 2.14.0 → 2.16.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.
@@ -146,7 +146,8 @@ interface CommandLookupResult {
146
146
  }
147
147
  declare class InMemoryRegistry implements CommandRegistry {
148
148
  private systemCommands;
149
- private pluginCommands;
149
+ private pluginByCanonical;
150
+ private pluginAliases;
150
151
  private byName;
151
152
  private groups;
152
153
  private manifests;
@@ -154,18 +155,45 @@ declare class InMemoryRegistry implements CommandRegistry {
154
155
  register(cmd: Command): void;
155
156
  registerGroup(group: CommandGroup): void;
156
157
  registerManifest(cmd: RegisteredCommand): void;
158
+ /**
159
+ * Check whether any system command already owns the canonical id or its parts.
160
+ * Returns the colliding key if found, null otherwise.
161
+ */
162
+ private _findSystemCollision;
163
+ /**
164
+ * Register all lookup aliases for a plugin command.
165
+ *
166
+ * Priority (what beats what) is handled in resolveToCanonical at query time.
167
+ * Here we just build the mapping.
168
+ *
169
+ * Aliases registered:
170
+ * - canonicalId itself ("marketplace:plugins:list")
171
+ * - bare id ("list") ← low priority, may clash
172
+ * - 2-part shorthand ("marketplace:list", "marketplace list")
173
+ * - manifest.aliases[] (user-defined aliases, except collisions)
174
+ */
175
+ private _registerPluginAliases;
176
+ /**
177
+ * Register synthetic subgroups for help display.
178
+ */
179
+ private _registerSyntheticGroups;
180
+ /**
181
+ * Resolve any user input to its canonical plugin ID.
182
+ *
183
+ * Returns canonicalId if found in plugin aliases, undefined otherwise.
184
+ */
185
+ private resolveToCanonical;
157
186
  markPartial(partial: boolean): void;
158
187
  isPartial(): boolean;
159
188
  getManifest(id: string): RegisteredCommand | undefined;
160
189
  listManifests(): RegisteredCommand[];
161
190
  has(name: string): boolean;
162
191
  /**
163
- * Get command with type information for secure routing
164
- *
165
- * Returns type='system' for commands from registerGroup() - execute in-process
166
- * Returns type='plugin' for commands from registerManifest() - execute in subprocess
192
+ * Get command with type information for secure routing.
167
193
  *
168
- * This separation prevents malicious plugins from escaping the sandbox.
194
+ * System commands ALWAYS win checked first before any plugin lookup.
195
+ * Returns type='system' for system commands (in-process execution).
196
+ * Returns type='plugin' for plugin commands (subprocess execution).
169
197
  */
170
198
  getWithType(nameOrPath: string | string[]): CommandLookupResult | undefined;
171
199
  get(nameOrPath: string | string[]): Command | CommandGroup | undefined;
@@ -180,11 +208,13 @@ declare class InMemoryRegistry implements CommandRegistry {
180
208
  declare const registry: InMemoryRegistry;
181
209
  declare function findCommand(nameOrPath: string | string[]): Command | CommandGroup | undefined;
182
210
  /**
183
- * Find command with type information for secure routing
211
+ * Find command with type information for secure routing.
184
212
  *
185
213
  * Use this in bootstrap.ts to determine execution path:
186
214
  * - type='system' → execute via cmd.run() in-process
187
215
  * - type='plugin' → execute via executePlugin() in plugin-executor
216
+ *
217
+ * System commands ALWAYS take priority over plugin commands.
188
218
  */
189
219
  declare function findCommandWithType(nameOrPath: string | string[]): CommandLookupResult | undefined;
190
220
 
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-BY7VE2Hf.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-BY7VE2Hf.js';
1
+ import { C as CommandGroup, P as ProductGroup, a as Command, R as RegisteredCommand } from './discover-Be0ez3AR.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-Be0ez3AR.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';
package/dist/index.js CHANGED
@@ -1017,17 +1017,30 @@ function manifestToCommand(registered) {
1017
1017
  }
1018
1018
  };
1019
1019
  }
1020
+ function buildCanonicalId(manifest) {
1021
+ const { id, group, subgroup } = manifest;
1022
+ if (group && subgroup) {
1023
+ return `${group}:${subgroup}:${id}`;
1024
+ }
1025
+ if (group) {
1026
+ return `${group}:${id}`;
1027
+ }
1028
+ return id;
1029
+ }
1020
1030
  var InMemoryRegistry = class {
1021
- // Separate collections for security isolation
1031
+ // System commands (in-process): key = any registered name/alias
1022
1032
  systemCommands = /* @__PURE__ */ new Map();
1023
- // System commands (in-process)
1024
- pluginCommands = /* @__PURE__ */ new Map();
1025
- // Plugin commands (subprocess)
1026
- // Legacy unified collection for backward compatibility
1033
+ // Plugin commands: key = canonicalId only
1034
+ pluginByCanonical = /* @__PURE__ */ new Map();
1035
+ // Alias canonicalId: covers all user-input variants that resolve to a plugin
1036
+ pluginAliases = /* @__PURE__ */ new Map();
1037
+ // Legacy unified collection for backward compatibility (display/get)
1027
1038
  byName = /* @__PURE__ */ new Map();
1028
1039
  groups = /* @__PURE__ */ new Map();
1040
+ // manifests: any key → RegisteredCommand (for listing/lookup; still stores multi-key for compat)
1029
1041
  manifests = /* @__PURE__ */ new Map();
1030
1042
  partial = false;
1043
+ // ─── System command registration ────────────────────────────────────────
1031
1044
  register(cmd) {
1032
1045
  this.systemCommands.set(cmd.name, cmd);
1033
1046
  this.byName.set(cmd.name, cmd);
@@ -1066,11 +1079,12 @@ var InMemoryRegistry = class {
1066
1079
  }
1067
1080
  }
1068
1081
  }
1082
+ // ─── Plugin command registration ────────────────────────────────────────
1069
1083
  registerManifest(cmd) {
1070
- const cmdId = cmd.manifest.id;
1071
- const hasCollision = this.systemCommands.has(cmdId);
1072
- if (hasCollision) {
1073
- console.warn(`[registry] Plugin command "${cmdId}" collides with system command. System command takes priority.`);
1084
+ const canonicalId = buildCanonicalId(cmd.manifest);
1085
+ const collisionKey = this._findSystemCollision(cmd.manifest, canonicalId);
1086
+ if (collisionKey) {
1087
+ console.warn(`[registry] Plugin command "${canonicalId}" collides with system command "${collisionKey}". System command takes priority.`);
1074
1088
  cmd.shadowed = true;
1075
1089
  }
1076
1090
  const collisionAliases = /* @__PURE__ */ new Set();
@@ -1080,58 +1094,137 @@ var InMemoryRegistry = class {
1080
1094
  collisionAliases.add(alias);
1081
1095
  }
1082
1096
  }
1083
- this.pluginCommands.set(cmdId, cmd);
1084
- this.manifests.set(cmdId, cmd);
1085
- if (!hasCollision) {
1086
- const commandAdapter = manifestToCommand(cmd);
1087
- this.byName.set(cmdId, commandAdapter);
1088
- this.byName.set(commandAdapter.name, commandAdapter);
1089
- if (cmdId.includes(":")) {
1090
- const spaceSeparated = cmdId.replace(/:/g, " ");
1091
- this.byName.set(spaceSeparated, commandAdapter);
1092
- }
1093
- if (cmd.manifest.group && cmd.manifest.subgroup) {
1094
- const fullPath = `${cmd.manifest.group} ${cmd.manifest.subgroup} ${cmd.manifest.id}`;
1095
- const colonPath = `${cmd.manifest.group}:${cmd.manifest.subgroup}:${cmd.manifest.id}`;
1096
- this.byName.set(fullPath, commandAdapter);
1097
- this.byName.set(colonPath, commandAdapter);
1098
- this.manifests.set(fullPath, cmd);
1099
- this.manifests.set(colonPath, cmd);
1100
- this.pluginCommands.set(fullPath, cmd);
1101
- this.pluginCommands.set(colonPath, cmd);
1102
- const twoPartName = `${cmd.manifest.group} ${cmd.manifest.id}`;
1103
- if (!this.byName.has(twoPartName)) {
1104
- this.byName.set(twoPartName, commandAdapter);
1105
- }
1106
- const subgroupKey = `${cmd.manifest.group} ${cmd.manifest.subgroup}`;
1107
- if (!this.groups.has(subgroupKey)) {
1108
- this.groups.set(subgroupKey, {
1109
- name: subgroupKey,
1110
- describe: cmd.manifest.subgroup,
1111
- commands: []
1112
- });
1113
- this.byName.set(subgroupKey, this.groups.get(subgroupKey));
1097
+ this.pluginByCanonical.set(canonicalId, cmd);
1098
+ this.manifests.set(canonicalId, cmd);
1099
+ this.manifests.set(cmd.manifest.id, cmd);
1100
+ if (cmd.shadowed) {
1101
+ return;
1102
+ }
1103
+ this._registerPluginAliases(cmd, canonicalId, collisionAliases);
1104
+ const commandAdapter = manifestToCommand(cmd);
1105
+ this.byName.set(canonicalId, commandAdapter);
1106
+ const spaceCanonical = canonicalId.replace(/:/g, " ");
1107
+ this.byName.set(spaceCanonical, commandAdapter);
1108
+ this._registerSyntheticGroups(cmd, commandAdapter);
1109
+ }
1110
+ /**
1111
+ * Check whether any system command already owns the canonical id or its parts.
1112
+ * Returns the colliding key if found, null otherwise.
1113
+ */
1114
+ _findSystemCollision(manifest, canonicalId) {
1115
+ if (this.systemCommands.has(canonicalId)) {
1116
+ return canonicalId;
1117
+ }
1118
+ const space = canonicalId.replace(/:/g, " ");
1119
+ if (this.systemCommands.has(space)) {
1120
+ return space;
1121
+ }
1122
+ return null;
1123
+ }
1124
+ /**
1125
+ * Register all lookup aliases for a plugin command.
1126
+ *
1127
+ * Priority (what beats what) is handled in resolveToCanonical at query time.
1128
+ * Here we just build the mapping.
1129
+ *
1130
+ * Aliases registered:
1131
+ * - canonicalId itself ("marketplace:plugins:list")
1132
+ * - bare id ("list") ← low priority, may clash
1133
+ * - 2-part shorthand ("marketplace:list", "marketplace list")
1134
+ * - manifest.aliases[] (user-defined aliases, except collisions)
1135
+ */
1136
+ _registerPluginAliases(cmd, canonicalId, collisionAliases) {
1137
+ const { id, group, subgroup } = cmd.manifest;
1138
+ const register = (key) => {
1139
+ if (!this.systemCommands.has(key) && !this.systemCommands.has(key.replace(/:/g, " "))) {
1140
+ if (!this.pluginAliases.has(key)) {
1141
+ this.pluginAliases.set(key, canonicalId);
1142
+ this.manifests.set(key, cmd);
1114
1143
  }
1115
- this.groups.get(subgroupKey).commands.push(commandAdapter);
1116
- } else if (cmd.manifest.group) {
1117
- const fullName = `${cmd.manifest.group} ${cmd.manifest.id}`;
1118
- const colonName = `${cmd.manifest.group}:${cmd.manifest.id}`;
1119
- this.byName.set(fullName, commandAdapter);
1120
- this.byName.set(colonName, commandAdapter);
1121
- this.manifests.set(fullName, cmd);
1122
- this.manifests.set(colonName, cmd);
1123
- this.pluginCommands.set(fullName, cmd);
1124
- this.pluginCommands.set(colonName, cmd);
1125
1144
  }
1126
- if (cmd.manifest.aliases) {
1127
- for (const alias of cmd.manifest.aliases) {
1128
- if (!collisionAliases.has(alias)) {
1129
- this.byName.set(alias, commandAdapter);
1130
- }
1131
- }
1145
+ };
1146
+ this.pluginAliases.set(canonicalId, canonicalId);
1147
+ const spaceCanonical = canonicalId.replace(/:/g, " ");
1148
+ this.pluginAliases.set(spaceCanonical, canonicalId);
1149
+ this.manifests.set(spaceCanonical, cmd);
1150
+ if (group && subgroup) {
1151
+ const twoPartColon = `${group}:${id}`;
1152
+ const twoPartSpace = `${group} ${id}`;
1153
+ register(twoPartColon);
1154
+ register(twoPartSpace);
1155
+ this.manifests.set(twoPartColon, cmd);
1156
+ this.manifests.set(twoPartSpace, cmd);
1157
+ const fullPath = `${group} ${subgroup} ${id}`;
1158
+ this.pluginAliases.set(fullPath, canonicalId);
1159
+ this.manifests.set(fullPath, cmd);
1160
+ const colonPath = `${group}:${subgroup}:${id}`;
1161
+ this.pluginAliases.set(colonPath, canonicalId);
1162
+ this.manifests.set(colonPath, cmd);
1163
+ } else if (group) {
1164
+ const colonName = `${group}:${id}`;
1165
+ const spaceName = `${group} ${id}`;
1166
+ this.pluginAliases.set(colonName, canonicalId);
1167
+ this.pluginAliases.set(spaceName, canonicalId);
1168
+ this.manifests.set(colonName, cmd);
1169
+ this.manifests.set(spaceName, cmd);
1170
+ }
1171
+ register(id);
1172
+ for (const alias of cmd.manifest.aliases || []) {
1173
+ if (!collisionAliases.has(alias)) {
1174
+ register(alias);
1175
+ }
1176
+ }
1177
+ }
1178
+ /**
1179
+ * Register synthetic subgroups for help display.
1180
+ */
1181
+ _registerSyntheticGroups(cmd, commandAdapter) {
1182
+ const { id, group, subgroup } = cmd.manifest;
1183
+ if (group && subgroup) {
1184
+ const subgroupKey = `${group} ${subgroup}`;
1185
+ if (!this.groups.has(subgroupKey)) {
1186
+ this.groups.set(subgroupKey, {
1187
+ name: subgroupKey,
1188
+ describe: subgroup,
1189
+ commands: []
1190
+ });
1191
+ this.byName.set(subgroupKey, this.groups.get(subgroupKey));
1132
1192
  }
1193
+ this.groups.get(subgroupKey).commands.push(commandAdapter);
1194
+ const twoPartSpace = `${group} ${id}`;
1195
+ if (!this.byName.has(twoPartSpace)) {
1196
+ this.byName.set(twoPartSpace, commandAdapter);
1197
+ }
1198
+ const twoPartColon = `${group}:${id}`;
1199
+ if (!this.byName.has(twoPartColon)) {
1200
+ this.byName.set(twoPartColon, commandAdapter);
1201
+ }
1202
+ const fullPath = `${group} ${subgroup} ${id}`;
1203
+ this.byName.set(fullPath, commandAdapter);
1204
+ } else if (group) {
1205
+ const fullName = `${group} ${id}`;
1206
+ const colonName = `${group}:${id}`;
1207
+ this.byName.set(fullName, commandAdapter);
1208
+ this.byName.set(colonName, commandAdapter);
1209
+ }
1210
+ }
1211
+ // ─── Resolution ──────────────────────────────────────────────────────────
1212
+ /**
1213
+ * Resolve any user input to its canonical plugin ID.
1214
+ *
1215
+ * Returns canonicalId if found in plugin aliases, undefined otherwise.
1216
+ */
1217
+ resolveToCanonical(input) {
1218
+ if (this.pluginAliases.has(input)) {
1219
+ return this.pluginAliases.get(input);
1220
+ }
1221
+ const converted = input.includes(":") ? input.replace(/:/g, " ") : input.replace(/ /g, ":");
1222
+ if (this.pluginAliases.has(converted)) {
1223
+ return this.pluginAliases.get(converted);
1133
1224
  }
1225
+ return void 0;
1134
1226
  }
1227
+ // ─── Public API ──────────────────────────────────────────────────────────
1135
1228
  markPartial(partial) {
1136
1229
  this.partial = partial;
1137
1230
  }
@@ -1143,23 +1236,43 @@ var InMemoryRegistry = class {
1143
1236
  }
1144
1237
  listManifests() {
1145
1238
  const unique = /* @__PURE__ */ new Set();
1146
- for (const cmd of this.manifests.values()) {
1239
+ for (const cmd of this.pluginByCanonical.values()) {
1147
1240
  unique.add(cmd);
1148
1241
  }
1149
1242
  return Array.from(unique);
1150
1243
  }
1151
1244
  has(name) {
1152
- return this.byName.has(name);
1245
+ return this.byName.has(name) || this.pluginAliases.has(name);
1153
1246
  }
1154
1247
  /**
1155
- * Get command with type information for secure routing
1248
+ * Get command with type information for secure routing.
1156
1249
  *
1157
- * Returns type='system' for commands from registerGroup() - execute in-process
1158
- * Returns type='plugin' for commands from registerManifest() - execute in subprocess
1159
- *
1160
- * This separation prevents malicious plugins from escaping the sandbox.
1250
+ * System commands ALWAYS win checked first before any plugin lookup.
1251
+ * Returns type='system' for system commands (in-process execution).
1252
+ * Returns type='plugin' for plugin commands (subprocess execution).
1161
1253
  */
1162
1254
  getWithType(nameOrPath) {
1255
+ const normalized = typeof nameOrPath === "string" ? nameOrPath : nameOrPath.join(" ");
1256
+ if (this.systemCommands.has(normalized)) {
1257
+ return { cmd: this.systemCommands.get(normalized), type: "system" };
1258
+ }
1259
+ const colonVariant = normalized.replace(/ /g, ":");
1260
+ if (this.systemCommands.has(colonVariant)) {
1261
+ return { cmd: this.systemCommands.get(colonVariant), type: "system" };
1262
+ }
1263
+ if (this.groups.has(normalized)) {
1264
+ return { cmd: this.groups.get(normalized), type: "system" };
1265
+ }
1266
+ const canonicalId = this.resolveToCanonical(normalized);
1267
+ if (canonicalId) {
1268
+ const pluginCmd = this.pluginByCanonical.get(canonicalId);
1269
+ if (pluginCmd && !pluginCmd.shadowed) {
1270
+ const cmd2 = this.byName.get(canonicalId) ?? this.byName.get(normalized);
1271
+ if (cmd2) {
1272
+ return { cmd: cmd2, type: "plugin" };
1273
+ }
1274
+ }
1275
+ }
1163
1276
  const cmd = this.get(nameOrPath);
1164
1277
  if (!cmd) {
1165
1278
  return void 0;
@@ -1167,18 +1280,11 @@ var InMemoryRegistry = class {
1167
1280
  if ("commands" in cmd) {
1168
1281
  return { cmd, type: "system" };
1169
1282
  }
1170
- const normalizedName = typeof nameOrPath === "string" ? nameOrPath : nameOrPath.join(" ");
1171
- if (this.systemCommands.has(normalizedName)) {
1283
+ if (this.systemCommands.has(normalized) || this.systemCommands.has(colonVariant)) {
1172
1284
  return { cmd, type: "system" };
1173
1285
  }
1174
- if (normalizedName.includes(":")) {
1175
- const spaceVersion = normalizedName.replace(":", " ");
1176
- if (this.systemCommands.has(spaceVersion)) {
1177
- return { cmd, type: "system" };
1178
- }
1179
- }
1180
- const manifestCmd = this.getManifestCommand(normalizedName);
1181
- if (manifestCmd) {
1286
+ const manifestCmd = this.getManifestCommand(normalized);
1287
+ if (manifestCmd && !manifestCmd.shadowed) {
1182
1288
  return { cmd, type: "plugin" };
1183
1289
  }
1184
1290
  return { cmd, type: "system" };
@@ -1189,15 +1295,15 @@ var InMemoryRegistry = class {
1189
1295
  return this.byName.get(nameOrPath);
1190
1296
  }
1191
1297
  if (nameOrPath.includes(":")) {
1298
+ const spaceKey = nameOrPath.replace(/:/g, " ");
1299
+ if (this.byName.has(spaceKey)) {
1300
+ return this.byName.get(spaceKey);
1301
+ }
1192
1302
  const parts = nameOrPath.split(":");
1193
1303
  if (parts.length === 2) {
1194
- const exactMatch = this.byName.get(nameOrPath);
1195
- if (exactMatch) {
1196
- return exactMatch;
1197
- }
1198
- const spaceKey = parts.join(" ");
1199
- if (this.byName.has(spaceKey)) {
1200
- return this.byName.get(spaceKey);
1304
+ const spaceKey2 = parts.join(" ");
1305
+ if (this.byName.has(spaceKey2)) {
1306
+ return this.byName.get(spaceKey2);
1201
1307
  }
1202
1308
  }
1203
1309
  }
@@ -1288,7 +1394,11 @@ var InMemoryRegistry = class {
1288
1394
  if (this.manifests.has(idOrAlias)) {
1289
1395
  return this.manifests.get(idOrAlias);
1290
1396
  }
1291
- for (const cmd of this.manifests.values()) {
1397
+ const canonicalId = this.resolveToCanonical(idOrAlias);
1398
+ if (canonicalId) {
1399
+ return this.pluginByCanonical.get(canonicalId);
1400
+ }
1401
+ for (const cmd of this.pluginByCanonical.values()) {
1292
1402
  if (cmd.manifest.aliases?.includes(idOrAlias)) {
1293
1403
  return cmd;
1294
1404
  }
@@ -3654,23 +3764,24 @@ async function registerManifests(discoveryResults, registry2, options = {}) {
3654
3764
  } catch (hookError) {
3655
3765
  log3.debug(`Lifecycle hooks unavailable for ${manifest.id}: ${hookError.message}`);
3656
3766
  }
3657
- const existing = globalIds.get(manifest.id);
3767
+ const canonicalKey = manifest.group ? `${manifest.group}:${manifest.id}` : manifest.id;
3768
+ const existing = globalIds.get(canonicalKey);
3658
3769
  if (existing) {
3659
3770
  const collision = checkCollision(manifest, existing, result.source, namespace);
3660
3771
  if (collision.shouldShadow) {
3661
3772
  existing.shadowed = true;
3662
- globalIds.set(manifest.id, cmd);
3773
+ globalIds.set(canonicalKey, cmd);
3663
3774
  if (logLevel === "info" || logLevel === "debug") {
3664
- log3.info(`${manifest.id} from ${result.source} shadows ${existing.source} version`);
3775
+ log3.info(`${canonicalKey} from ${result.source} shadows ${existing.source} version`);
3665
3776
  }
3666
3777
  } else {
3667
3778
  cmd.shadowed = true;
3668
3779
  if (logLevel === "info" || logLevel === "debug") {
3669
- log3.info(`${manifest.id} from ${result.source} shadowed by ${existing.source} version`);
3780
+ log3.info(`${canonicalKey} from ${result.source} shadowed by ${existing.source} version`);
3670
3781
  }
3671
3782
  }
3672
3783
  } else {
3673
- globalIds.set(manifest.id, cmd);
3784
+ globalIds.set(canonicalKey, cmd);
3674
3785
  }
3675
3786
  const aliasesToCheck = manifest.aliases || [];
3676
3787
  for (const alias of aliasesToCheck) {