@kb-labs/cli-commands 2.93.0 → 2.96.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/dist/{discover-D_Is1Qqq.d.ts → discover-DxmYCrcJ.d.ts} +138 -106
- package/dist/index.d.ts +24 -13
- package/dist/index.js +2140 -1537
- package/dist/index.js.map +1 -1
- package/dist/registry/index.d.ts +12 -12
- package/dist/registry/index.js +601 -666
- package/dist/registry/index.js.map +1 -1
- package/package.json +23 -23
package/dist/registry/index.js
CHANGED
|
@@ -3,11 +3,11 @@ import path2 from 'path';
|
|
|
3
3
|
import { fileURLToPath, pathToFileURL } from 'url';
|
|
4
4
|
import { existsSync, promises } from 'fs';
|
|
5
5
|
import { createHash } from 'crypto';
|
|
6
|
+
import { computeManifestIntegrity } from '@kb-labs/core-discovery';
|
|
6
7
|
import { parse } from 'yaml';
|
|
7
8
|
import { glob } from 'glob';
|
|
8
9
|
import { toPosixPath } from '@kb-labs/shared-cli-ui';
|
|
9
10
|
import { z } from 'zod';
|
|
10
|
-
import Ajv from 'ajv';
|
|
11
11
|
import { platform } from '@kb-labs/core-runtime';
|
|
12
12
|
import { getLogLevel } from '@kb-labs/cli-runtime';
|
|
13
13
|
|
|
@@ -107,62 +107,37 @@ var FlagDefinitionSchema = z.object({
|
|
|
107
107
|
choices: z.array(z.string()).optional(),
|
|
108
108
|
required: z.boolean().optional()
|
|
109
109
|
}).refine(
|
|
110
|
-
(data) =>
|
|
111
|
-
if (data.choices && data.type !== "string") {
|
|
112
|
-
return false;
|
|
113
|
-
}
|
|
114
|
-
return true;
|
|
115
|
-
},
|
|
110
|
+
(data) => !(data.choices && data.type !== "string"),
|
|
116
111
|
{ message: "Choices are only allowed for string type flags" }
|
|
117
112
|
);
|
|
118
113
|
var EngineSchema = z.object({
|
|
119
114
|
node: z.string().optional(),
|
|
120
|
-
// e.g., ">=18", "^18.0.0"
|
|
121
115
|
kbCli: z.string().optional(),
|
|
122
|
-
// e.g., "^1.5.0"
|
|
123
116
|
module: z.enum(["esm", "cjs"]).optional()
|
|
124
117
|
}).optional();
|
|
125
118
|
var CommandManifestSchema = z.object({
|
|
126
119
|
manifestVersion: z.literal("1.0"),
|
|
127
|
-
|
|
128
|
-
id: z.string().min(1)
|
|
129
|
-
aliases: z.array(z.string()).optional(),
|
|
120
|
+
segments: z.array(z.string().min(1)).min(1),
|
|
121
|
+
id: z.string().min(1),
|
|
130
122
|
group: z.string().min(1),
|
|
123
|
+
subgroup: z.string().optional(),
|
|
124
|
+
aliases: z.array(z.string()).optional(),
|
|
125
|
+
category: z.string().optional(),
|
|
131
126
|
describe: z.string().min(1),
|
|
132
127
|
longDescription: z.string().optional(),
|
|
133
128
|
requires: z.array(z.string()).optional(),
|
|
134
|
-
// Package names with optional semver
|
|
135
129
|
flags: z.array(FlagDefinitionSchema).optional(),
|
|
136
130
|
examples: z.array(z.string()).optional(),
|
|
137
131
|
loader: z.any(),
|
|
138
|
-
// Function validation happens at runtime
|
|
139
|
-
// New fields (optional for backward compatibility)
|
|
140
132
|
package: z.string().optional(),
|
|
141
|
-
// Full package name
|
|
142
|
-
namespace: z.string().optional(),
|
|
143
|
-
// Derived from id if not provided
|
|
144
133
|
engine: EngineSchema,
|
|
145
134
|
permissions: z.array(z.string()).optional(),
|
|
146
|
-
// e.g., ["fs.read", "git.read", "net.fetch"]
|
|
147
135
|
telemetry: z.enum(["opt-in", "off"]).optional(),
|
|
148
136
|
manifestV2: z.any().optional(),
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
// Auto-generated setup command
|
|
153
|
-
isSetupRollback: z.boolean().optional(),
|
|
154
|
-
// Auto-generated setup rollback command
|
|
155
|
-
pkgRoot: z.string().optional()
|
|
156
|
-
// Package root path for handler resolution
|
|
137
|
+
pkgRoot: z.string().optional(),
|
|
138
|
+
_synthetic: z.boolean().optional(),
|
|
139
|
+
operationType: z.enum(["read", "mutate", "execute", "analyze"]).optional()
|
|
157
140
|
}).refine(
|
|
158
|
-
(data) => {
|
|
159
|
-
if (data.namespace && data.group && data.namespace !== data.group) {
|
|
160
|
-
return false;
|
|
161
|
-
}
|
|
162
|
-
return true;
|
|
163
|
-
},
|
|
164
|
-
{ message: "namespace must match group" }
|
|
165
|
-
).refine(
|
|
166
141
|
(data) => {
|
|
167
142
|
if (data.requires) {
|
|
168
143
|
for (const req2 of data.requires) {
|
|
@@ -189,46 +164,36 @@ function validateManifests(manifests) {
|
|
|
189
164
|
if (result.success) {
|
|
190
165
|
validated.push(result.data);
|
|
191
166
|
} else {
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
...issue,
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
]);
|
|
198
|
-
errors.push(errorWithIndex);
|
|
167
|
+
errors.push(
|
|
168
|
+
new z.ZodError(
|
|
169
|
+
result.error.issues.map((issue) => ({ ...issue, path: [`[${i}]`, ...issue.path] }))
|
|
170
|
+
)
|
|
171
|
+
);
|
|
199
172
|
}
|
|
200
173
|
}
|
|
201
|
-
|
|
202
|
-
return { success: true, data: validated };
|
|
203
|
-
}
|
|
204
|
-
return { success: false, errors };
|
|
174
|
+
return errors.length === 0 ? { success: true, data: validated } : { success: false, errors };
|
|
205
175
|
}
|
|
206
|
-
function normalizeManifest(manifest, packageName
|
|
207
|
-
const
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
}
|
|
216
|
-
if (!normalized.package) {
|
|
217
|
-
normalized.package = packageName;
|
|
218
|
-
}
|
|
219
|
-
return normalized;
|
|
176
|
+
function normalizeManifest(manifest, packageName) {
|
|
177
|
+
const segs = manifest.segments;
|
|
178
|
+
return {
|
|
179
|
+
...manifest,
|
|
180
|
+
id: segs[segs.length - 1] ?? "",
|
|
181
|
+
group: segs[0] ?? "",
|
|
182
|
+
subgroup: segs.length >= 3 ? segs[1] : void 0,
|
|
183
|
+
package: manifest.package ?? packageName
|
|
184
|
+
};
|
|
220
185
|
}
|
|
221
186
|
|
|
222
187
|
// src/registry/discover.ts
|
|
188
|
+
var DISCOVERY_VERSION = 2;
|
|
223
189
|
var DEBUG_MODE = process.env.DEBUG_SANDBOX === "1" || process.env.NODE_ENV === "development";
|
|
224
190
|
var log = (level, message, fields) => {
|
|
225
191
|
if (!DEBUG_MODE) {
|
|
226
192
|
return;
|
|
227
193
|
}
|
|
228
194
|
const prefix = level === "error" ? "\u2717" : level === "warn" ? "\u26A0" : level === "info" ? "\u2139" : "\u{1F50D}";
|
|
229
|
-
const logFn = level === "error" ? console.error : level === "warn" ? console.warn : console.log;
|
|
230
195
|
{
|
|
231
|
-
|
|
196
|
+
console.error(`${prefix} [discover] ${message}`);
|
|
232
197
|
}
|
|
233
198
|
};
|
|
234
199
|
function createManifestV3Loader(commandId) {
|
|
@@ -250,6 +215,14 @@ var __test = {
|
|
|
250
215
|
createManifestV3Loader
|
|
251
216
|
// resetInProcCache is exported directly (not via __test) since it's part of the public API
|
|
252
217
|
};
|
|
218
|
+
var NonPluginManifestError = class extends Error {
|
|
219
|
+
schema;
|
|
220
|
+
constructor(pkgName, schema) {
|
|
221
|
+
super(`${pkgName} uses manifest schema "${schema}", not kb.plugin/3 \u2014 skipping`);
|
|
222
|
+
this.name = "NonPluginManifestError";
|
|
223
|
+
this.schema = schema;
|
|
224
|
+
}
|
|
225
|
+
};
|
|
253
226
|
function createUnavailableManifest(pkgName, error) {
|
|
254
227
|
const rawMsg = (error instanceof Error ? error.message : String(error) || "").toString();
|
|
255
228
|
let missing = null;
|
|
@@ -264,9 +237,11 @@ function createUnavailableManifest(pkgName, error) {
|
|
|
264
237
|
const group = (seg || pkgName).replace(/-cli$/, "");
|
|
265
238
|
const short = seg || pkgName;
|
|
266
239
|
const requires = missing ? [missing] : [];
|
|
240
|
+
const unavailableId = `manifest:${short}`;
|
|
267
241
|
const manifest = {
|
|
268
242
|
manifestVersion: "1.0",
|
|
269
|
-
|
|
243
|
+
segments: [group, unavailableId],
|
|
244
|
+
id: unavailableId,
|
|
270
245
|
group,
|
|
271
246
|
describe: `Commands from ${pkgName} are unavailable`,
|
|
272
247
|
requires,
|
|
@@ -289,14 +264,6 @@ var inProcDiscoveryCache = null;
|
|
|
289
264
|
function resetInProcCache() {
|
|
290
265
|
inProcDiscoveryCache = null;
|
|
291
266
|
}
|
|
292
|
-
async function computeManifestHash(manifestPath) {
|
|
293
|
-
try {
|
|
294
|
-
const content = await promises.readFile(manifestPath, "utf8");
|
|
295
|
-
return createHash("sha256").update(content).digest("hex");
|
|
296
|
-
} catch {
|
|
297
|
-
return "unknown";
|
|
298
|
-
}
|
|
299
|
-
}
|
|
300
267
|
async function computeLockfileHash(cwd) {
|
|
301
268
|
const lockfilePath = path2.join(cwd, "pnpm-lock.yaml");
|
|
302
269
|
try {
|
|
@@ -402,8 +369,12 @@ async function loadManifest(manifestPath, pkgName, pkgRoot) {
|
|
|
402
369
|
const mod = await import(fileUrl);
|
|
403
370
|
const modTyped = mod;
|
|
404
371
|
const rawManifest = modTyped.manifest || modTyped.default;
|
|
405
|
-
if (!rawManifest || typeof rawManifest !== "object"
|
|
406
|
-
throw new Error(`
|
|
372
|
+
if (!rawManifest || typeof rawManifest !== "object") {
|
|
373
|
+
throw new Error(`No manifest export found in ${pkgName}`);
|
|
374
|
+
}
|
|
375
|
+
const schema = rawManifest.schema;
|
|
376
|
+
if (schema !== "kb.plugin/3") {
|
|
377
|
+
throw new NonPluginManifestError(pkgName, typeof schema === "string" ? schema : String(schema ?? "unknown"));
|
|
407
378
|
}
|
|
408
379
|
const manifest = rawManifest;
|
|
409
380
|
const namespace = getNamespaceFromManifest(manifest, pkgName);
|
|
@@ -414,20 +385,23 @@ async function loadManifest(manifestPath, pkgName, pkgRoot) {
|
|
|
414
385
|
log("warn", `ManifestV3 ${manifest.id || pkgName} has no CLI commands or setup entry`);
|
|
415
386
|
}
|
|
416
387
|
const commandManifests = cliCommands.map((cmd) => {
|
|
417
|
-
const
|
|
388
|
+
const segments = cmd.path.trim().split(/\s+/).filter(Boolean);
|
|
389
|
+
const commandId = segments[segments.length - 1] ?? namespace;
|
|
418
390
|
const commandManifest = {
|
|
419
391
|
manifestVersion: "1.0",
|
|
392
|
+
segments,
|
|
420
393
|
id: commandId,
|
|
421
|
-
group:
|
|
422
|
-
subgroup:
|
|
394
|
+
group: segments[0] ?? namespace,
|
|
395
|
+
subgroup: segments.length >= 3 ? segments[1] : void 0,
|
|
396
|
+
category: cmd.category,
|
|
423
397
|
describe: cmd.describe || "",
|
|
424
398
|
longDescription: cmd.longDescription,
|
|
425
399
|
aliases: cmd.aliases,
|
|
426
400
|
flags: cmd.flags,
|
|
427
401
|
examples: cmd.examples,
|
|
428
|
-
loader: createManifestV3Loader(
|
|
402
|
+
loader: createManifestV3Loader(cmd.path),
|
|
429
403
|
package: pkgName,
|
|
430
|
-
|
|
404
|
+
operationType: cmd.operationType
|
|
431
405
|
};
|
|
432
406
|
commandManifest.manifestV2 = manifest;
|
|
433
407
|
commandManifest.pkgRoot = baseRoot;
|
|
@@ -441,7 +415,7 @@ async function loadManifest(manifestPath, pkgName, pkgRoot) {
|
|
|
441
415
|
log("warn", `ManifestV3 validation warnings for ${pkgName}: ${errorMessages}`);
|
|
442
416
|
}
|
|
443
417
|
return (validation.success ? validation.data : commandManifests).map(
|
|
444
|
-
(m) => normalizeManifest(m, pkgName
|
|
418
|
+
(m) => normalizeManifest(m, pkgName)
|
|
445
419
|
);
|
|
446
420
|
}
|
|
447
421
|
async function readPackageJson(pkgPath) {
|
|
@@ -512,10 +486,11 @@ function validateUniqueIds(manifests, pkgName) {
|
|
|
512
486
|
const ids = /* @__PURE__ */ new Set();
|
|
513
487
|
const aliases = /* @__PURE__ */ new Set();
|
|
514
488
|
for (const m of manifests) {
|
|
515
|
-
|
|
489
|
+
const key = m.segments.join("/");
|
|
490
|
+
if (ids.has(key)) {
|
|
516
491
|
throw new Error(`Duplicate command ID "${m.id}" in package ${pkgName}`);
|
|
517
492
|
}
|
|
518
|
-
ids.add(
|
|
493
|
+
ids.add(key);
|
|
519
494
|
if (m.aliases) {
|
|
520
495
|
for (const alias of m.aliases) {
|
|
521
496
|
if (aliases.has(alias) || ids.has(alias)) {
|
|
@@ -608,6 +583,10 @@ async function loadManifestsForPackages(packageInfos, source, scope) {
|
|
|
608
583
|
log("debug", `[plugins][perf] ${pkgName} failed after ${pkgTime}ms`);
|
|
609
584
|
const errMsg = err instanceof Error ? err.message : String(err);
|
|
610
585
|
const errCode = err.code ?? "UNKNOWN";
|
|
586
|
+
if (err instanceof NonPluginManifestError) {
|
|
587
|
+
log("debug", `[plugins] ${pkgName} skipped: ${err.message}`);
|
|
588
|
+
return null;
|
|
589
|
+
}
|
|
611
590
|
log("warn", JSON.stringify({
|
|
612
591
|
code: "DISCOVERY_MANIFEST_LOAD_FAIL",
|
|
613
592
|
packageName: pkgName,
|
|
@@ -778,6 +757,10 @@ async function discoverNodeModules(cwd) {
|
|
|
778
757
|
} catch (err) {
|
|
779
758
|
const errMsg = err instanceof Error ? err.message : String(err);
|
|
780
759
|
const errCode = err.code ?? "UNKNOWN";
|
|
760
|
+
if (err instanceof NonPluginManifestError) {
|
|
761
|
+
log("debug", `[plugins] ${pkgName} skipped: ${err.message}`);
|
|
762
|
+
return null;
|
|
763
|
+
}
|
|
781
764
|
log("warn", JSON.stringify({
|
|
782
765
|
code: "DISCOVERY_MANIFEST_LOAD_FAIL",
|
|
783
766
|
packageName: pkgName,
|
|
@@ -830,14 +813,14 @@ function deduplicateManifests(all) {
|
|
|
830
813
|
byPackageName.set(result.packageName, projectEntry);
|
|
831
814
|
continue;
|
|
832
815
|
}
|
|
833
|
-
const winner = existing.scope === "
|
|
834
|
-
const loser = existing.scope === "
|
|
835
|
-
log("
|
|
836
|
-
code: "
|
|
816
|
+
const winner = existing.scope === "project" ? existing : result;
|
|
817
|
+
const loser = existing.scope === "project" ? result : existing;
|
|
818
|
+
log("debug", JSON.stringify({
|
|
819
|
+
code: "DISCOVERY_SCOPE_OVERRIDE",
|
|
837
820
|
packageName: result.packageName,
|
|
838
|
-
|
|
839
|
-
|
|
840
|
-
message: "Package exists in both platform and project scopes \u2014
|
|
821
|
+
projectPath: winner.pkgRoot,
|
|
822
|
+
platformPath: loser.pkgRoot,
|
|
823
|
+
message: "Package exists in both platform and project scopes \u2014 project wins."
|
|
841
824
|
}));
|
|
842
825
|
byPackageName.set(result.packageName, winner);
|
|
843
826
|
continue;
|
|
@@ -867,6 +850,10 @@ async function loadCache(cwd, roots) {
|
|
|
867
850
|
log("debug", "Cache invalidated: Node version changed");
|
|
868
851
|
return null;
|
|
869
852
|
}
|
|
853
|
+
if ((cache.discoveryVersion ?? 0) !== DISCOVERY_VERSION) {
|
|
854
|
+
log("debug", `Cache invalidated: discovery logic changed (${cache.discoveryVersion ?? 0} \u2192 ${DISCOVERY_VERSION})`);
|
|
855
|
+
return null;
|
|
856
|
+
}
|
|
870
857
|
const currentCliVersion = process.env.CLI_VERSION || "0.1.0";
|
|
871
858
|
if (cache.cliVersion !== currentCliVersion) {
|
|
872
859
|
log("debug", "Cache invalidated: CLI version changed");
|
|
@@ -920,7 +907,7 @@ async function loadCache(cwd, roots) {
|
|
|
920
907
|
return null;
|
|
921
908
|
}
|
|
922
909
|
}
|
|
923
|
-
async function isPackageCacheStale(entry
|
|
910
|
+
async function isPackageCacheStale(entry) {
|
|
924
911
|
const manifestFsPath = entry.manifestPath.split("/").join(path2.sep);
|
|
925
912
|
const pkgJsonPath = path2.join(entry.result.pkgRoot.split("/").join(path2.sep), PACKAGE_JSON);
|
|
926
913
|
try {
|
|
@@ -933,26 +920,23 @@ async function isPackageCacheStale(entry, options) {
|
|
|
933
920
|
log("debug", `Package cache invalidated: missing package.json for ${entry.result.packageName} (${error instanceof Error ? error.message : "unknown"})`);
|
|
934
921
|
return true;
|
|
935
922
|
}
|
|
936
|
-
let
|
|
923
|
+
let manifestMtimeChanged = false;
|
|
937
924
|
try {
|
|
938
|
-
manifestStat = await promises.stat(manifestFsPath);
|
|
939
|
-
|
|
940
|
-
log("debug", `Package cache invalidated: manifest mtime changed for ${entry.result.packageName}`);
|
|
941
|
-
return true;
|
|
942
|
-
}
|
|
925
|
+
const manifestStat = await promises.stat(manifestFsPath);
|
|
926
|
+
manifestMtimeChanged = manifestStat.mtimeMs !== entry.manifestMtime;
|
|
943
927
|
} catch (error) {
|
|
944
928
|
log("debug", `Package cache invalidated: manifest deleted for ${entry.result.packageName} (${error instanceof Error ? error.message : "unknown"})`);
|
|
945
929
|
return true;
|
|
946
930
|
}
|
|
947
|
-
if (
|
|
931
|
+
if (manifestMtimeChanged) {
|
|
948
932
|
try {
|
|
949
|
-
const currentHash = await
|
|
933
|
+
const currentHash = await computeManifestIntegrity(manifestFsPath);
|
|
950
934
|
if (currentHash !== entry.manifestHash) {
|
|
951
|
-
log("debug", `Package cache invalidated: manifest
|
|
935
|
+
log("debug", `Package cache invalidated: manifest content changed for ${entry.result.packageName}`);
|
|
952
936
|
return true;
|
|
953
937
|
}
|
|
954
938
|
} catch (error) {
|
|
955
|
-
log("debug", `Package cache hash
|
|
939
|
+
log("debug", `Package cache hash check failed for ${entry.result.packageName}: ${error instanceof Error ? error.message : "unknown"}`);
|
|
956
940
|
return true;
|
|
957
941
|
}
|
|
958
942
|
}
|
|
@@ -971,7 +955,7 @@ async function saveCache(cwd, results, roots) {
|
|
|
971
955
|
continue;
|
|
972
956
|
}
|
|
973
957
|
try {
|
|
974
|
-
const manifestHash = await
|
|
958
|
+
const manifestHash = await computeManifestIntegrity(result.manifestPath);
|
|
975
959
|
const pkgJsonPath = path2.join(result.pkgRoot.split("/").join(path2.sep), PACKAGE_JSON);
|
|
976
960
|
const pkgStat = await promises.stat(pkgJsonPath);
|
|
977
961
|
const pkg = await readPackageJson(pkgJsonPath);
|
|
@@ -1026,6 +1010,7 @@ async function saveCache(cwd, results, roots) {
|
|
|
1026
1010
|
const cache = {
|
|
1027
1011
|
version: process.version,
|
|
1028
1012
|
cliVersion: process.env.CLI_VERSION || "0.1.0",
|
|
1013
|
+
discoveryVersion: DISCOVERY_VERSION,
|
|
1029
1014
|
timestamp: now,
|
|
1030
1015
|
ttlMs: DISK_CACHE_TTL_MS,
|
|
1031
1016
|
stateHash: stateHasher.digest("hex"),
|
|
@@ -1073,11 +1058,10 @@ async function discoverManifests(cwd, noCache = false, options = {}) {
|
|
|
1073
1058
|
const freshResults = [];
|
|
1074
1059
|
const cacheAge = Date.now() - cached.timestamp;
|
|
1075
1060
|
const ttlMs = cached.ttlMs ?? DISK_CACHE_TTL_MS;
|
|
1076
|
-
|
|
1077
|
-
log("debug", `[plugins][cache] hit age=${cacheAge}ms ttl=${ttlMs}ms validateHash=${enforceHashValidation}`);
|
|
1061
|
+
log("debug", `[plugins][cache] hit age=${cacheAge}ms ttl=${ttlMs}ms`);
|
|
1078
1062
|
let staleCount = 0;
|
|
1079
1063
|
for (const entry of Object.values(cached.packages)) {
|
|
1080
|
-
const stale = await isPackageCacheStale(entry
|
|
1064
|
+
const stale = await isPackageCacheStale(entry);
|
|
1081
1065
|
if (!stale) {
|
|
1082
1066
|
freshResults.push(entry.result);
|
|
1083
1067
|
} else {
|
|
@@ -1134,8 +1118,33 @@ async function discoverManifests(cwd, noCache = false, options = {}) {
|
|
|
1134
1118
|
if (projectLocal.length > 0) {
|
|
1135
1119
|
log("info", `Discovered ${projectLocal.length} project-local plugins`);
|
|
1136
1120
|
}
|
|
1121
|
+
let projectWorkspace = [];
|
|
1122
|
+
if (roots.projectRoot !== roots.platformRoot) {
|
|
1123
|
+
try {
|
|
1124
|
+
const pwStart = Date.now();
|
|
1125
|
+
const raw = await discoverWorkspace(roots.projectRoot);
|
|
1126
|
+
projectWorkspace = raw.map((r) => ({ ...r, scope: "project" }));
|
|
1127
|
+
timings.projectWorkspace = Date.now() - pwStart;
|
|
1128
|
+
if (projectWorkspace.length > 0) {
|
|
1129
|
+
log("info", `Discovered ${projectWorkspace.length} project workspace packages with CLI manifests`);
|
|
1130
|
+
}
|
|
1131
|
+
} catch (err) {
|
|
1132
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
1133
|
+
const isMissingYaml = msg.includes("ENOENT") || msg.includes("no such file");
|
|
1134
|
+
if (isMissingYaml) {
|
|
1135
|
+
log("debug", `[plugins][discover] no pnpm-workspace.yaml at projectRoot=${roots.projectRoot} \u2014 skipping project workspace scan`);
|
|
1136
|
+
} else {
|
|
1137
|
+
log("warn", JSON.stringify({
|
|
1138
|
+
code: "PROJECT_WORKSPACE_DISCOVERY_FAILED",
|
|
1139
|
+
projectRoot: roots.projectRoot,
|
|
1140
|
+
error: msg,
|
|
1141
|
+
hint: "Project workspace plugins may be missing from CLI discovery"
|
|
1142
|
+
}));
|
|
1143
|
+
}
|
|
1144
|
+
}
|
|
1145
|
+
}
|
|
1137
1146
|
const dedupStart = Date.now();
|
|
1138
|
-
const results = deduplicateManifests([...workspace, ...installed, ...projectLocal]);
|
|
1147
|
+
const results = deduplicateManifests([...workspace, ...installed, ...projectLocal, ...projectWorkspace]);
|
|
1139
1148
|
timings.deduplicate = Date.now() - dedupStart;
|
|
1140
1149
|
const saveStart = Date.now();
|
|
1141
1150
|
await saveCache(cwd, results, roots);
|
|
@@ -1157,81 +1166,37 @@ async function discoverManifestsByNamespace(cwd, namespace, noCache = false) {
|
|
|
1157
1166
|
const allResults = await discoverManifests(cwd, noCache);
|
|
1158
1167
|
return allResults.filter((result) => {
|
|
1159
1168
|
return result.manifests.some((m) => {
|
|
1160
|
-
|
|
1161
|
-
return manifestNamespace === namespace;
|
|
1169
|
+
return m.group === namespace;
|
|
1162
1170
|
});
|
|
1163
1171
|
});
|
|
1164
1172
|
}
|
|
1165
|
-
|
|
1166
|
-
|
|
1167
|
-
type: "object",
|
|
1168
|
-
required: ["name", "type"],
|
|
1169
|
-
additionalProperties: false,
|
|
1170
|
-
properties: {
|
|
1171
|
-
name: { type: "string" },
|
|
1172
|
-
type: { enum: ["string", "boolean", "number", "array"] },
|
|
1173
|
-
alias: { type: "string", pattern: "^[a-z]$" },
|
|
1174
|
-
// single letter
|
|
1175
|
-
description: { type: "string" },
|
|
1176
|
-
choices: { type: "array", items: { type: "string" } },
|
|
1177
|
-
required: { type: "boolean" },
|
|
1178
|
-
default: {}
|
|
1179
|
-
// must match type
|
|
1180
|
-
}
|
|
1181
|
-
};
|
|
1182
|
-
var manifestSchema = {
|
|
1183
|
-
type: "object",
|
|
1184
|
-
required: ["manifestVersion", "id", "group", "describe", "loader"],
|
|
1185
|
-
additionalProperties: true,
|
|
1186
|
-
properties: {
|
|
1187
|
-
manifestVersion: { type: "string", enum: ["1.0"] },
|
|
1188
|
-
id: { type: "string", pattern: "^[a-z0-9-]+(?::[a-z0-9-]+)*$" },
|
|
1189
|
-
// Simple or namespaced (colon-separated)
|
|
1190
|
-
aliases: { type: "array", items: { type: "string" } },
|
|
1191
|
-
group: { type: "string" },
|
|
1192
|
-
describe: { type: "string" },
|
|
1193
|
-
longDescription: { type: "string" },
|
|
1194
|
-
requires: { type: "array", items: { type: "string" } },
|
|
1195
|
-
flags: {
|
|
1196
|
-
type: "array",
|
|
1197
|
-
items: flagSchema
|
|
1198
|
-
},
|
|
1199
|
-
examples: { type: "array", items: { type: "string" } }
|
|
1200
|
-
}
|
|
1201
|
-
};
|
|
1202
|
-
var validateManifest = ajv.compile(manifestSchema);
|
|
1203
|
-
var validateFlag = ajv.compile(flagSchema);
|
|
1204
|
-
function validateFlagDef(flag, manifestId) {
|
|
1205
|
-
if (!validateFlag(flag)) {
|
|
1173
|
+
function validateManifestStructure(manifest) {
|
|
1174
|
+
if (manifest.manifestVersion !== "1.0") {
|
|
1206
1175
|
throw new Error(
|
|
1207
|
-
`
|
|
1176
|
+
`Unsupported manifestVersion "${manifest.manifestVersion}" in ${manifest.segments?.join(" ") ?? manifest.id} (expected "1.0")`
|
|
1208
1177
|
);
|
|
1209
1178
|
}
|
|
1210
|
-
if (
|
|
1211
|
-
throw new Error(`
|
|
1212
|
-
}
|
|
1213
|
-
if (flag.default !== void 0) {
|
|
1214
|
-
const typeMatches = flag.type === "string" && typeof flag.default === "string" || flag.type === "boolean" && typeof flag.default === "boolean" || flag.type === "number" && typeof flag.default === "number" || flag.type === "array" && Array.isArray(flag.default);
|
|
1215
|
-
if (!typeMatches) {
|
|
1216
|
-
throw new Error(
|
|
1217
|
-
`Flag "${flag.name}" in ${manifestId}: default value type mismatch (expected ${flag.type})`
|
|
1218
|
-
);
|
|
1219
|
-
}
|
|
1179
|
+
if (!Array.isArray(manifest.segments) || manifest.segments.length === 0) {
|
|
1180
|
+
throw new Error(`Missing or empty segments in manifest ${manifest.id}`);
|
|
1220
1181
|
}
|
|
1221
|
-
|
|
1222
|
-
|
|
1223
|
-
if (!validateManifest(manifest)) {
|
|
1224
|
-
throw new Error(`Invalid manifest ${manifest.id}: ${ajv.errorsText(validateManifest.errors)}`);
|
|
1182
|
+
if (!manifest.describe) {
|
|
1183
|
+
throw new Error(`Missing describe in manifest ${manifest.segments.join(" ")}`);
|
|
1225
1184
|
}
|
|
1226
|
-
if (manifest.
|
|
1227
|
-
throw new Error(
|
|
1228
|
-
`Unsupported manifestVersion "${manifest.manifestVersion}" in ${manifest.id} (expected "1.0")`
|
|
1229
|
-
);
|
|
1185
|
+
if (!manifest.loader && !manifest.manifestV2) {
|
|
1186
|
+
throw new Error(`Command ${manifest.segments.join(" ")} must have either loader or manifestV2`);
|
|
1230
1187
|
}
|
|
1231
|
-
if (manifest.flags
|
|
1188
|
+
if (manifest.flags) {
|
|
1232
1189
|
for (const flag of manifest.flags) {
|
|
1233
|
-
if (flag
|
|
1234
|
-
|
|
1190
|
+
if (!flag.name || !flag.type) {
|
|
1191
|
+
throw new Error(`Invalid flag in ${manifest.segments.join(" ")}: missing name or type`);
|
|
1192
|
+
}
|
|
1193
|
+
if (flag.alias && flag.alias.length !== 1) {
|
|
1194
|
+
throw new Error(`Flag "${flag.name}" in ${manifest.segments.join(" ")}: alias must be a single character`);
|
|
1195
|
+
}
|
|
1196
|
+
if (flag.choices && flag.type !== "string") {
|
|
1197
|
+
throw new Error(
|
|
1198
|
+
`Flag "${flag.name}" in ${manifest.segments.join(" ")}: choices only allowed for string type`
|
|
1199
|
+
);
|
|
1235
1200
|
}
|
|
1236
1201
|
}
|
|
1237
1202
|
}
|
|
@@ -1242,61 +1207,8 @@ var SOURCE_PRIORITY = {
|
|
|
1242
1207
|
linked: 2,
|
|
1243
1208
|
node_modules: 1
|
|
1244
1209
|
};
|
|
1245
|
-
function normalizeCommandId(id, _namespace) {
|
|
1246
|
-
return id;
|
|
1247
|
-
}
|
|
1248
|
-
function generateWhitespaceAliases(id) {
|
|
1249
|
-
if (!id.includes(":")) {
|
|
1250
|
-
return [];
|
|
1251
|
-
}
|
|
1252
|
-
return [id.replace(":", " ")];
|
|
1253
|
-
}
|
|
1254
|
-
function normalizeAliases(manifest, logger) {
|
|
1255
|
-
const log2 = logger ?? platform.logger;
|
|
1256
|
-
const aliases = /* @__PURE__ */ new Set();
|
|
1257
|
-
manifest.namespace || manifest.group;
|
|
1258
|
-
if (manifest.aliases) {
|
|
1259
|
-
for (const alias of manifest.aliases) {
|
|
1260
|
-
if (!/^[a-z0-9-:]+$/i.test(alias)) {
|
|
1261
|
-
log2.warn(`Invalid alias "${alias}" in ${manifest.id}: aliases must be alphanumeric with hyphens or colons`);
|
|
1262
|
-
continue;
|
|
1263
|
-
}
|
|
1264
|
-
aliases.add(alias);
|
|
1265
|
-
}
|
|
1266
|
-
}
|
|
1267
|
-
const whitespaceAliases = generateWhitespaceAliases(manifest.id);
|
|
1268
|
-
for (const alias of whitespaceAliases) {
|
|
1269
|
-
aliases.add(alias);
|
|
1270
|
-
}
|
|
1271
|
-
return Array.from(aliases);
|
|
1272
|
-
}
|
|
1273
|
-
function checkNamespaceCollision(manifest, existing, namespace) {
|
|
1274
|
-
const existingGroup = existing.manifest.group || "";
|
|
1275
|
-
const currentGroup = manifest.group || "";
|
|
1276
|
-
if (existingGroup === currentGroup && existingGroup === namespace && existing.manifest.id === manifest.id) {
|
|
1277
|
-
throw new Error(
|
|
1278
|
-
`Command collision in group "${namespace}": "${manifest.id}" conflicts with existing "${existing.manifest.id}". Rename one of the commands to use a different ID (e.g., "${manifest.id}2" or use --alias to create a different alias).`
|
|
1279
|
-
);
|
|
1280
|
-
}
|
|
1281
|
-
}
|
|
1282
1210
|
function getSourcePriority(source) {
|
|
1283
|
-
return SOURCE_PRIORITY[source]
|
|
1284
|
-
}
|
|
1285
|
-
function checkCollision(manifest, existing, currentSource, namespace) {
|
|
1286
|
-
const existingPriority = getSourcePriority(existing.source);
|
|
1287
|
-
const currentPriority = getSourcePriority(currentSource);
|
|
1288
|
-
checkNamespaceCollision(manifest, existing, namespace);
|
|
1289
|
-
if (currentSource === "workspace" && existing.source === "workspace") {
|
|
1290
|
-
throw new Error(
|
|
1291
|
-
`Command ID collision: "${manifest.id}" is exported by multiple workspace packages. This is not allowed. Please rename one of the commands to use a different ID.`
|
|
1292
|
-
);
|
|
1293
|
-
}
|
|
1294
|
-
if (currentPriority > existingPriority) {
|
|
1295
|
-
return { shouldShadow: true };
|
|
1296
|
-
} else if (currentPriority < existingPriority) {
|
|
1297
|
-
return { shouldShadow: false };
|
|
1298
|
-
}
|
|
1299
|
-
return { shouldShadow: false };
|
|
1211
|
+
return SOURCE_PRIORITY[source] ?? 0;
|
|
1300
1212
|
}
|
|
1301
1213
|
function preflightManifests(discoveryResults, logger) {
|
|
1302
1214
|
const log2 = logger ?? platform.logger;
|
|
@@ -1311,24 +1223,17 @@ function preflightManifests(discoveryResults, logger) {
|
|
|
1311
1223
|
allowed.push(manifest);
|
|
1312
1224
|
} catch (error) {
|
|
1313
1225
|
const reason = error instanceof Error ? error.message : "Validation failed";
|
|
1314
|
-
const
|
|
1315
|
-
skipped.push({
|
|
1316
|
-
|
|
1317
|
-
source: result.source,
|
|
1318
|
-
reason
|
|
1319
|
-
});
|
|
1320
|
-
log2.warn(`Preflight skipped manifest ${manifestId}: ${reason}`);
|
|
1226
|
+
const id = manifest?.segments?.join(" ") ?? manifest?.id ?? result.packageName ?? "unknown";
|
|
1227
|
+
skipped.push({ id, source: result.source, reason });
|
|
1228
|
+
log2.warn(`Preflight skipped manifest ${id}: ${reason}`);
|
|
1321
1229
|
if (logLevel === "debug") {
|
|
1322
|
-
process.stderr.write(`[debug][preflight] skipped ${
|
|
1230
|
+
process.stderr.write(`[debug][preflight] skipped ${id} (${result.source}): ${reason}
|
|
1323
1231
|
`);
|
|
1324
1232
|
}
|
|
1325
1233
|
}
|
|
1326
1234
|
}
|
|
1327
1235
|
if (allowed.length > 0) {
|
|
1328
|
-
valid.push({
|
|
1329
|
-
...result,
|
|
1330
|
-
manifests: allowed
|
|
1331
|
-
});
|
|
1236
|
+
valid.push({ ...result, manifests: allowed });
|
|
1332
1237
|
}
|
|
1333
1238
|
}
|
|
1334
1239
|
return { valid, skipped };
|
|
@@ -1338,41 +1243,27 @@ async function registerManifests(discoveryResults, registry2, options = {}) {
|
|
|
1338
1243
|
const registered = [];
|
|
1339
1244
|
const skipped = [];
|
|
1340
1245
|
const globalIds = /* @__PURE__ */ new Map();
|
|
1341
|
-
const globalAliases = /* @__PURE__ */ new Map();
|
|
1342
1246
|
const logLevel = getLogLevel();
|
|
1343
1247
|
let collisions = 0;
|
|
1344
1248
|
let errors = 0;
|
|
1345
1249
|
const sorted = [...discoveryResults].sort((a, b) => {
|
|
1346
|
-
|
|
1347
|
-
const priorityB = getSourcePriority(b.source);
|
|
1348
|
-
return priorityB - priorityA;
|
|
1250
|
+
return getSourcePriority(b.source) - getSourcePriority(a.source);
|
|
1349
1251
|
});
|
|
1350
1252
|
for (const result of sorted) {
|
|
1351
1253
|
for (const manifest of result.manifests) {
|
|
1352
|
-
const manifestId = manifest.
|
|
1353
|
-
const namespace = manifest.namespace || manifest.group;
|
|
1254
|
+
const manifestId = manifest.segments?.join(" ") ?? manifest.id ?? "unknown";
|
|
1354
1255
|
try {
|
|
1355
1256
|
try {
|
|
1356
1257
|
validateManifestStructure(manifest);
|
|
1357
1258
|
} catch (err) {
|
|
1358
1259
|
throw new Error(`Validation failed: ${err instanceof Error ? err.message : String(err)}`);
|
|
1359
1260
|
}
|
|
1360
|
-
const normalizedId = normalizeCommandId(manifest.id, namespace);
|
|
1361
|
-
if (normalizedId !== manifest.id) {
|
|
1362
|
-
log2.warn(`Command ID "${manifest.id}" normalized to "${normalizedId}"`);
|
|
1363
|
-
manifest.id = normalizedId;
|
|
1364
|
-
}
|
|
1365
|
-
const normalizedAliases = normalizeAliases(manifest, log2);
|
|
1366
|
-
if (normalizedAliases.length > 0) {
|
|
1367
|
-
manifest.aliases = normalizedAliases;
|
|
1368
|
-
}
|
|
1369
1261
|
const availability = checkRequires(manifest, {
|
|
1370
1262
|
cwd: options.cwd ?? result.pkgRoot
|
|
1371
1263
|
});
|
|
1372
1264
|
const cmd = {
|
|
1373
1265
|
manifest,
|
|
1374
1266
|
v3Manifest: manifest.manifestV2,
|
|
1375
|
-
// Extract V3 manifest from legacy field
|
|
1376
1267
|
available: availability.available,
|
|
1377
1268
|
unavailableReason: availability.available ? void 0 : availability.reason,
|
|
1378
1269
|
hint: availability.available ? void 0 : availability.hint,
|
|
@@ -1383,14 +1274,14 @@ async function registerManifests(discoveryResults, registry2, options = {}) {
|
|
|
1383
1274
|
};
|
|
1384
1275
|
try {
|
|
1385
1276
|
const manifestModule = await import(result.manifestPath);
|
|
1386
|
-
if (
|
|
1277
|
+
if (typeof manifestModule.init === "function") {
|
|
1387
1278
|
await manifestModule.init({
|
|
1388
1279
|
cwd: result.pkgRoot,
|
|
1389
1280
|
package: result.packageName,
|
|
1390
1281
|
manifest: cmd.manifest
|
|
1391
1282
|
});
|
|
1392
1283
|
}
|
|
1393
|
-
if (
|
|
1284
|
+
if (typeof manifestModule.register === "function") {
|
|
1394
1285
|
await manifestModule.register({
|
|
1395
1286
|
registry: registry2,
|
|
1396
1287
|
command: cmd,
|
|
@@ -1398,18 +1289,25 @@ async function registerManifests(discoveryResults, registry2, options = {}) {
|
|
|
1398
1289
|
package: result.packageName
|
|
1399
1290
|
});
|
|
1400
1291
|
}
|
|
1401
|
-
if (
|
|
1292
|
+
if (typeof manifestModule.dispose === "function") {
|
|
1402
1293
|
cmd._disposeHook = manifestModule.dispose;
|
|
1403
1294
|
}
|
|
1404
1295
|
} catch (hookError) {
|
|
1405
1296
|
const hookMsg = hookError instanceof Error ? hookError.message : String(hookError);
|
|
1406
|
-
log2.debug(`Lifecycle hooks unavailable for ${
|
|
1297
|
+
log2.debug(`Lifecycle hooks unavailable for ${manifestId}: ${hookMsg}`);
|
|
1407
1298
|
}
|
|
1408
|
-
const canonicalKey = manifest.
|
|
1299
|
+
const canonicalKey = manifest.segments.join(":");
|
|
1409
1300
|
const existing = globalIds.get(canonicalKey);
|
|
1410
1301
|
if (existing) {
|
|
1411
|
-
|
|
1412
|
-
|
|
1302
|
+
if (existing.source === result.source && existing.source === "workspace") {
|
|
1303
|
+
collisions++;
|
|
1304
|
+
throw new Error(
|
|
1305
|
+
`Command path collision: "${manifestId}" exported by multiple workspace packages.`
|
|
1306
|
+
);
|
|
1307
|
+
}
|
|
1308
|
+
const existPri = getSourcePriority(existing.source);
|
|
1309
|
+
const curPri = getSourcePriority(result.source);
|
|
1310
|
+
if (curPri > existPri) {
|
|
1413
1311
|
existing.shadowed = true;
|
|
1414
1312
|
globalIds.set(canonicalKey, cmd);
|
|
1415
1313
|
if (logLevel === "info" || logLevel === "debug") {
|
|
@@ -1424,63 +1322,15 @@ async function registerManifests(discoveryResults, registry2, options = {}) {
|
|
|
1424
1322
|
} else {
|
|
1425
1323
|
globalIds.set(canonicalKey, cmd);
|
|
1426
1324
|
}
|
|
1427
|
-
const aliasesToCheck = manifest.aliases || [];
|
|
1428
|
-
for (const alias of aliasesToCheck) {
|
|
1429
|
-
const existingAlias = globalAliases.get(alias);
|
|
1430
|
-
if (existingAlias) {
|
|
1431
|
-
if (existingAlias.manifest.id === manifest.id) {
|
|
1432
|
-
continue;
|
|
1433
|
-
}
|
|
1434
|
-
const existingPriority = getSourcePriority(existingAlias.source);
|
|
1435
|
-
const currentPriority = getSourcePriority(result.source);
|
|
1436
|
-
if (currentPriority > existingPriority) {
|
|
1437
|
-
existingAlias.shadowed = true;
|
|
1438
|
-
globalAliases.set(alias, cmd);
|
|
1439
|
-
if (logLevel === "info" || logLevel === "debug") {
|
|
1440
|
-
log2.info(`Alias "${alias}" from ${result.source} shadows ${existingAlias.source} version`);
|
|
1441
|
-
}
|
|
1442
|
-
} else if (currentPriority < existingPriority) {
|
|
1443
|
-
if (logLevel === "info" || logLevel === "debug") {
|
|
1444
|
-
log2.info(`Alias "${alias}" from ${result.source} shadowed by ${existingAlias.source} version`);
|
|
1445
|
-
}
|
|
1446
|
-
continue;
|
|
1447
|
-
} else {
|
|
1448
|
-
collisions++;
|
|
1449
|
-
throw new Error(
|
|
1450
|
-
`Alias collision: "${alias}" used by both ${manifest.id} and ${existingAlias.manifest.id}. Rename one alias or use a different namespace.`
|
|
1451
|
-
);
|
|
1452
|
-
}
|
|
1453
|
-
}
|
|
1454
|
-
if (globalIds.has(alias)) {
|
|
1455
|
-
const conflictingCmd = globalIds.get(alias);
|
|
1456
|
-
const existingPriority = getSourcePriority(conflictingCmd.source);
|
|
1457
|
-
const currentPriority = getSourcePriority(result.source);
|
|
1458
|
-
if (currentPriority > existingPriority) {
|
|
1459
|
-
log2.warn(`Alias "${alias}" conflicts with command ID "${alias}". Alias will shadow command.`);
|
|
1460
|
-
globalAliases.set(alias, cmd);
|
|
1461
|
-
} else {
|
|
1462
|
-
throw new Error(
|
|
1463
|
-
`Alias "${alias}" conflicts with existing command ID "${alias}". Rename the alias or use a different name.`
|
|
1464
|
-
);
|
|
1465
|
-
}
|
|
1466
|
-
} else {
|
|
1467
|
-
globalAliases.set(alias, cmd);
|
|
1468
|
-
}
|
|
1469
|
-
}
|
|
1470
1325
|
if (!cmd.shadowed) {
|
|
1471
1326
|
registry2.registerManifest(cmd);
|
|
1327
|
+
registered.push(cmd);
|
|
1472
1328
|
}
|
|
1473
|
-
registered.push(cmd);
|
|
1474
1329
|
} catch (error) {
|
|
1475
1330
|
errors++;
|
|
1476
1331
|
const reason = error instanceof Error ? error.message : String(error);
|
|
1477
|
-
skipped.push({
|
|
1478
|
-
id: manifestId,
|
|
1479
|
-
source: result.source,
|
|
1480
|
-
reason
|
|
1481
|
-
});
|
|
1332
|
+
skipped.push({ id: manifestId, source: result.source, reason });
|
|
1482
1333
|
log2.error(`Skipped manifest ${manifestId} (${result.source}): ${reason}`);
|
|
1483
|
-
continue;
|
|
1484
1334
|
}
|
|
1485
1335
|
}
|
|
1486
1336
|
}
|
|
@@ -1490,24 +1340,18 @@ async function registerManifests(discoveryResults, registry2, options = {}) {
|
|
|
1490
1340
|
log2.warn(` \u2022 ${skip.id} [${skip.source}] \u2192 ${skip.reason}`);
|
|
1491
1341
|
}
|
|
1492
1342
|
}
|
|
1493
|
-
return {
|
|
1494
|
-
registered,
|
|
1495
|
-
skipped,
|
|
1496
|
-
collisions,
|
|
1497
|
-
errors
|
|
1498
|
-
};
|
|
1343
|
+
return { registered, skipped, collisions, errors };
|
|
1499
1344
|
}
|
|
1500
1345
|
async function disposeAllPlugins(registry2, logger) {
|
|
1501
1346
|
const log2 = logger ?? platform.logger;
|
|
1502
|
-
const manifests = registry2.
|
|
1347
|
+
const manifests = registry2.listCommands();
|
|
1503
1348
|
const disposePromises = [];
|
|
1504
1349
|
for (const cmd of manifests) {
|
|
1505
1350
|
const disposeHook = cmd._disposeHook;
|
|
1506
|
-
if (
|
|
1351
|
+
if (typeof disposeHook === "function") {
|
|
1507
1352
|
disposePromises.push(
|
|
1508
1353
|
Promise.resolve(disposeHook()).catch((err) => {
|
|
1509
|
-
|
|
1510
|
-
log2.warn(`Dispose hook failed for ${cmd.manifest.id}: ${errMsg}`);
|
|
1354
|
+
log2.warn(`Dispose hook failed for ${cmd.manifest.segments.join(" ")}: ${err instanceof Error ? err.message : String(err)}`);
|
|
1511
1355
|
})
|
|
1512
1356
|
);
|
|
1513
1357
|
}
|
|
@@ -1538,421 +1382,512 @@ async function runCommand(cmd, ctx, argv, flags) {
|
|
|
1538
1382
|
return typeof result === "number" ? result : 0;
|
|
1539
1383
|
}
|
|
1540
1384
|
|
|
1541
|
-
// src/registry/
|
|
1542
|
-
function
|
|
1543
|
-
return {
|
|
1544
|
-
name: registered.manifest.id,
|
|
1545
|
-
category: registered.manifest.group,
|
|
1546
|
-
describe: registered.manifest.describe,
|
|
1547
|
-
longDescription: registered.manifest.longDescription,
|
|
1548
|
-
aliases: registered.manifest.aliases || [],
|
|
1549
|
-
flags: registered.manifest.flags,
|
|
1550
|
-
examples: registered.manifest.examples,
|
|
1551
|
-
async run() {
|
|
1552
|
-
throw new Error(`Command ${registered.manifest.id} should be executed via plugin-executor, not via legacy run() path.`);
|
|
1553
|
-
}
|
|
1554
|
-
};
|
|
1385
|
+
// src/registry/trie-router.ts
|
|
1386
|
+
function makeNode() {
|
|
1387
|
+
return { children: /* @__PURE__ */ new Map() };
|
|
1555
1388
|
}
|
|
1556
|
-
function
|
|
1557
|
-
const
|
|
1558
|
-
|
|
1559
|
-
|
|
1389
|
+
function levenshtein(a, b) {
|
|
1390
|
+
const m = a.length;
|
|
1391
|
+
const n = b.length;
|
|
1392
|
+
const dp = Array.from(
|
|
1393
|
+
{ length: m + 1 },
|
|
1394
|
+
(_, i) => Array.from({ length: n + 1 }, (__, j) => i === 0 ? j : j === 0 ? i : 0)
|
|
1395
|
+
);
|
|
1396
|
+
for (let i = 1; i <= m; i++) {
|
|
1397
|
+
for (let j = 1; j <= n; j++) {
|
|
1398
|
+
if (a[i - 1] === b[j - 1]) {
|
|
1399
|
+
dp[i][j] = dp[i - 1][j - 1];
|
|
1400
|
+
} else {
|
|
1401
|
+
dp[i][j] = 1 + Math.min(dp[i - 1][j], dp[i][j - 1], dp[i - 1][j - 1]);
|
|
1402
|
+
}
|
|
1403
|
+
}
|
|
1560
1404
|
}
|
|
1561
|
-
|
|
1562
|
-
|
|
1405
|
+
return dp[m][n];
|
|
1406
|
+
}
|
|
1407
|
+
function findDeep(node, targetSegments, prefix) {
|
|
1408
|
+
const results = [];
|
|
1409
|
+
function dfs(current, path3) {
|
|
1410
|
+
if (current.command) {
|
|
1411
|
+
if (targetSegments.length <= path3.length) {
|
|
1412
|
+
const tail = path3.slice(path3.length - targetSegments.length);
|
|
1413
|
+
const matches = tail.every((seg, i) => seg === targetSegments[i]);
|
|
1414
|
+
if (matches) {
|
|
1415
|
+
results.push(path3.join(" "));
|
|
1416
|
+
}
|
|
1417
|
+
}
|
|
1418
|
+
}
|
|
1419
|
+
for (const [key, child] of current.children) {
|
|
1420
|
+
dfs(child, [...path3, key]);
|
|
1421
|
+
}
|
|
1563
1422
|
}
|
|
1564
|
-
|
|
1423
|
+
dfs(node, [...prefix]);
|
|
1424
|
+
return results;
|
|
1565
1425
|
}
|
|
1566
|
-
var
|
|
1567
|
-
|
|
1568
|
-
|
|
1569
|
-
|
|
1570
|
-
|
|
1571
|
-
|
|
1572
|
-
|
|
1573
|
-
|
|
1574
|
-
|
|
1575
|
-
|
|
1576
|
-
|
|
1577
|
-
|
|
1578
|
-
|
|
1579
|
-
|
|
1580
|
-
|
|
1581
|
-
|
|
1582
|
-
|
|
1583
|
-
|
|
1584
|
-
|
|
1585
|
-
this.
|
|
1426
|
+
var TrieRouter = class {
|
|
1427
|
+
root = makeNode();
|
|
1428
|
+
/**
|
|
1429
|
+
* Insert a plugin command into the trie.
|
|
1430
|
+
*
|
|
1431
|
+
* Enforces 1-manifest-1-namespace: the first packageName to register any command
|
|
1432
|
+
* under a top-level group becomes the owner. Subsequent packages with a different
|
|
1433
|
+
* packageName are rejected (collides: true) — the caller is responsible for warning
|
|
1434
|
+
* and marking the command as shadowed.
|
|
1435
|
+
*
|
|
1436
|
+
* Returns { collides: false } on success, or { collides: true, ownerPackage } on conflict.
|
|
1437
|
+
*/
|
|
1438
|
+
insertCommand(segments, cmd) {
|
|
1439
|
+
if (segments.length === 0) {
|
|
1440
|
+
return { collides: false };
|
|
1441
|
+
}
|
|
1442
|
+
const incoming = cmd.packageName ?? "__unknown__";
|
|
1443
|
+
const topSeg = segments[0];
|
|
1444
|
+
if (!this.root.children.has(topSeg)) {
|
|
1445
|
+
this.root.children.set(topSeg, makeNode());
|
|
1446
|
+
}
|
|
1447
|
+
const groupNode = this.root.children.get(topSeg);
|
|
1448
|
+
if (groupNode.ownerPackage === void 0) {
|
|
1449
|
+
groupNode.ownerPackage = incoming;
|
|
1450
|
+
} else if (groupNode.ownerPackage !== incoming) {
|
|
1451
|
+
return { collides: true, ownerPackage: groupNode.ownerPackage };
|
|
1452
|
+
}
|
|
1453
|
+
let node = groupNode;
|
|
1454
|
+
for (const seg of segments.slice(1)) {
|
|
1455
|
+
if (!node.children.has(seg)) {
|
|
1456
|
+
node.children.set(seg, makeNode());
|
|
1457
|
+
}
|
|
1458
|
+
node = node.children.get(seg);
|
|
1586
1459
|
}
|
|
1460
|
+
node.command = cmd;
|
|
1461
|
+
return { collides: false };
|
|
1587
1462
|
}
|
|
1588
|
-
|
|
1589
|
-
|
|
1590
|
-
this.
|
|
1463
|
+
insertSystemCommand(cmd) {
|
|
1464
|
+
const segments = [cmd.name];
|
|
1465
|
+
let node = this.root;
|
|
1466
|
+
for (const seg of segments) {
|
|
1467
|
+
if (!node.children.has(seg)) {
|
|
1468
|
+
node.children.set(seg, makeNode());
|
|
1469
|
+
}
|
|
1470
|
+
node = node.children.get(seg);
|
|
1471
|
+
}
|
|
1472
|
+
node.systemCommand = cmd;
|
|
1473
|
+
for (const alias of cmd.aliases ?? []) {
|
|
1474
|
+
let aliasNode = this.root;
|
|
1475
|
+
if (!aliasNode.children.has(alias)) {
|
|
1476
|
+
aliasNode.children.set(alias, makeNode());
|
|
1477
|
+
}
|
|
1478
|
+
aliasNode = aliasNode.children.get(alias);
|
|
1479
|
+
aliasNode.systemCommand = cmd;
|
|
1480
|
+
}
|
|
1481
|
+
}
|
|
1482
|
+
insertSystemGroup(group) {
|
|
1483
|
+
let groupNode = this.root;
|
|
1484
|
+
if (!groupNode.children.has(group.name)) {
|
|
1485
|
+
groupNode.children.set(group.name, makeNode());
|
|
1486
|
+
}
|
|
1487
|
+
groupNode = groupNode.children.get(group.name);
|
|
1488
|
+
groupNode.systemGroup = group;
|
|
1489
|
+
groupNode.groupDescribe = group.describe;
|
|
1591
1490
|
for (const cmd of group.commands) {
|
|
1592
|
-
|
|
1593
|
-
|
|
1594
|
-
|
|
1595
|
-
|
|
1596
|
-
|
|
1597
|
-
|
|
1598
|
-
|
|
1491
|
+
let cmdNode = groupNode;
|
|
1492
|
+
if (!cmdNode.children.has(cmd.name)) {
|
|
1493
|
+
cmdNode.children.set(cmd.name, makeNode());
|
|
1494
|
+
}
|
|
1495
|
+
cmdNode = cmdNode.children.get(cmd.name);
|
|
1496
|
+
cmdNode.systemCommand = cmd;
|
|
1497
|
+
for (const alias of cmd.aliases ?? []) {
|
|
1498
|
+
let aliasNode = groupNode;
|
|
1499
|
+
if (!aliasNode.children.has(alias)) {
|
|
1500
|
+
aliasNode.children.set(alias, makeNode());
|
|
1501
|
+
}
|
|
1502
|
+
aliasNode = aliasNode.children.get(alias);
|
|
1503
|
+
aliasNode.systemCommand = cmd;
|
|
1599
1504
|
}
|
|
1600
1505
|
}
|
|
1601
|
-
|
|
1602
|
-
|
|
1603
|
-
|
|
1604
|
-
|
|
1605
|
-
|
|
1606
|
-
|
|
1607
|
-
|
|
1608
|
-
|
|
1609
|
-
|
|
1610
|
-
|
|
1611
|
-
|
|
1612
|
-
|
|
1613
|
-
}
|
|
1506
|
+
for (const sub of group.subgroups ?? []) {
|
|
1507
|
+
let subNode = groupNode;
|
|
1508
|
+
if (!subNode.children.has(sub.name)) {
|
|
1509
|
+
subNode.children.set(sub.name, makeNode());
|
|
1510
|
+
}
|
|
1511
|
+
subNode = subNode.children.get(sub.name);
|
|
1512
|
+
subNode.systemGroup = sub;
|
|
1513
|
+
subNode.groupDescribe = sub.describe;
|
|
1514
|
+
for (const cmd of sub.commands) {
|
|
1515
|
+
let cmdNode = subNode;
|
|
1516
|
+
if (!cmdNode.children.has(cmd.name)) {
|
|
1517
|
+
cmdNode.children.set(cmd.name, makeNode());
|
|
1614
1518
|
}
|
|
1519
|
+
cmdNode = cmdNode.children.get(cmd.name);
|
|
1520
|
+
cmdNode.systemCommand = cmd;
|
|
1615
1521
|
}
|
|
1616
1522
|
}
|
|
1617
1523
|
}
|
|
1618
|
-
|
|
1619
|
-
|
|
1620
|
-
const
|
|
1621
|
-
|
|
1622
|
-
|
|
1623
|
-
|
|
1624
|
-
|
|
1524
|
+
getGroupDescribe(segments) {
|
|
1525
|
+
let node = this.root;
|
|
1526
|
+
for (const seg of segments) {
|
|
1527
|
+
const child = node.children.get(seg);
|
|
1528
|
+
if (!child) {
|
|
1529
|
+
return void 0;
|
|
1530
|
+
}
|
|
1531
|
+
node = child;
|
|
1625
1532
|
}
|
|
1626
|
-
|
|
1627
|
-
|
|
1628
|
-
|
|
1629
|
-
|
|
1630
|
-
|
|
1533
|
+
return node.groupDescribe;
|
|
1534
|
+
}
|
|
1535
|
+
setGroupDescribe(segments, describe) {
|
|
1536
|
+
let node = this.root;
|
|
1537
|
+
for (const seg of segments) {
|
|
1538
|
+
if (!node.children.has(seg)) {
|
|
1539
|
+
node.children.set(seg, makeNode());
|
|
1631
1540
|
}
|
|
1541
|
+
node = node.children.get(seg);
|
|
1632
1542
|
}
|
|
1633
|
-
|
|
1634
|
-
|
|
1635
|
-
this.manifests.set(cmd.manifest.id, cmd);
|
|
1636
|
-
if (cmd.shadowed) {
|
|
1637
|
-
return;
|
|
1543
|
+
if (!node.groupDescribe) {
|
|
1544
|
+
node.groupDescribe = describe;
|
|
1638
1545
|
}
|
|
1639
|
-
this._registerPluginAliases(cmd, canonicalId, collisionAliases);
|
|
1640
|
-
const commandAdapter = manifestToCommand(cmd);
|
|
1641
|
-
this.byName.set(canonicalId, commandAdapter);
|
|
1642
|
-
const spaceCanonical = canonicalId.replace(/:/g, " ");
|
|
1643
|
-
this.byName.set(spaceCanonical, commandAdapter);
|
|
1644
|
-
this._registerSyntheticGroups(cmd, commandAdapter);
|
|
1645
1546
|
}
|
|
1646
|
-
|
|
1647
|
-
|
|
1648
|
-
|
|
1649
|
-
|
|
1650
|
-
|
|
1651
|
-
|
|
1652
|
-
|
|
1547
|
+
resolve(tokens) {
|
|
1548
|
+
if (tokens.length === 0) {
|
|
1549
|
+
return {
|
|
1550
|
+
type: "group",
|
|
1551
|
+
segments: [],
|
|
1552
|
+
childKeys: [...this.root.children.keys()]
|
|
1553
|
+
};
|
|
1653
1554
|
}
|
|
1654
|
-
|
|
1655
|
-
|
|
1656
|
-
|
|
1555
|
+
let node = this.root;
|
|
1556
|
+
let i = 0;
|
|
1557
|
+
while (i < tokens.length) {
|
|
1558
|
+
const tok = tokens[i];
|
|
1559
|
+
const child = node.children.get(tok);
|
|
1560
|
+
if (!child) {
|
|
1561
|
+
return this._notFound(node, tokens, i);
|
|
1562
|
+
}
|
|
1563
|
+
node = child;
|
|
1564
|
+
i++;
|
|
1565
|
+
if (node.command && i < tokens.length) {
|
|
1566
|
+
return { type: "command", command: node.command, rest: tokens.slice(i) };
|
|
1567
|
+
}
|
|
1568
|
+
if (node.systemCommand && i < tokens.length) {
|
|
1569
|
+
return { type: "system-cmd", cmd: node.systemCommand, rest: tokens.slice(i) };
|
|
1570
|
+
}
|
|
1657
1571
|
}
|
|
1658
|
-
|
|
1572
|
+
if (node.command) {
|
|
1573
|
+
return { type: "command", command: node.command, rest: [] };
|
|
1574
|
+
}
|
|
1575
|
+
if (node.systemCommand) {
|
|
1576
|
+
return { type: "system-cmd", cmd: node.systemCommand, rest: [] };
|
|
1577
|
+
}
|
|
1578
|
+
if (node.systemGroup) {
|
|
1579
|
+
return { type: "system-group", group: node.systemGroup, rest: [] };
|
|
1580
|
+
}
|
|
1581
|
+
if (node.children.size > 0 || node.groupDescribe !== void 0) {
|
|
1582
|
+
const segs = tokens.slice(0, i);
|
|
1583
|
+
return {
|
|
1584
|
+
type: "group",
|
|
1585
|
+
segments: segs,
|
|
1586
|
+
describe: node.groupDescribe,
|
|
1587
|
+
childKeys: [...node.children.keys()]
|
|
1588
|
+
};
|
|
1589
|
+
}
|
|
1590
|
+
return { type: "not-found", input: tokens, suggestions: [] };
|
|
1659
1591
|
}
|
|
1660
|
-
|
|
1661
|
-
|
|
1662
|
-
|
|
1663
|
-
|
|
1664
|
-
|
|
1665
|
-
|
|
1666
|
-
|
|
1667
|
-
|
|
1668
|
-
|
|
1669
|
-
* - 2-part shorthand ("marketplace:list", "marketplace list")
|
|
1670
|
-
* - manifest.aliases[] (user-defined aliases, except collisions)
|
|
1671
|
-
*/
|
|
1672
|
-
_registerPluginAliases(cmd, canonicalId, collisionAliases) {
|
|
1673
|
-
const { id, group, subgroup } = cmd.manifest;
|
|
1674
|
-
const register = (key) => {
|
|
1675
|
-
if (!this.systemCommands.has(key) && !this.systemCommands.has(key.replace(/:/g, " "))) {
|
|
1676
|
-
if (!this.pluginAliases.has(key)) {
|
|
1677
|
-
this.pluginAliases.set(key, canonicalId);
|
|
1678
|
-
this.manifests.set(key, cmd);
|
|
1679
|
-
}
|
|
1592
|
+
complete(tokens) {
|
|
1593
|
+
if (tokens.length === 0) {
|
|
1594
|
+
return [...this.root.children.keys()];
|
|
1595
|
+
}
|
|
1596
|
+
let node = this.root;
|
|
1597
|
+
for (let i = 0; i < tokens.length - 1; i++) {
|
|
1598
|
+
const child = node.children.get(tokens[i]);
|
|
1599
|
+
if (!child) {
|
|
1600
|
+
return [];
|
|
1680
1601
|
}
|
|
1681
|
-
|
|
1682
|
-
|
|
1683
|
-
const
|
|
1684
|
-
|
|
1685
|
-
|
|
1686
|
-
|
|
1687
|
-
|
|
1688
|
-
|
|
1689
|
-
register(twoPartColon);
|
|
1690
|
-
register(twoPartSpace);
|
|
1691
|
-
this.manifests.set(twoPartColon, cmd);
|
|
1692
|
-
this.manifests.set(twoPartSpace, cmd);
|
|
1693
|
-
const fullPath = `${group} ${subgroup} ${id}`;
|
|
1694
|
-
this.pluginAliases.set(fullPath, canonicalId);
|
|
1695
|
-
this.manifests.set(fullPath, cmd);
|
|
1696
|
-
const colonPath = `${group}:${subgroup}:${id}`;
|
|
1697
|
-
this.pluginAliases.set(colonPath, canonicalId);
|
|
1698
|
-
this.manifests.set(colonPath, cmd);
|
|
1699
|
-
} else if (group) {
|
|
1700
|
-
const colonName = `${group}:${id}`;
|
|
1701
|
-
const spaceName = `${group} ${id}`;
|
|
1702
|
-
this.pluginAliases.set(colonName, canonicalId);
|
|
1703
|
-
this.pluginAliases.set(spaceName, canonicalId);
|
|
1704
|
-
this.manifests.set(colonName, cmd);
|
|
1705
|
-
this.manifests.set(spaceName, cmd);
|
|
1706
|
-
}
|
|
1707
|
-
register(id);
|
|
1708
|
-
for (const alias of cmd.manifest.aliases || []) {
|
|
1709
|
-
if (!collisionAliases.has(alias)) {
|
|
1710
|
-
register(alias);
|
|
1602
|
+
node = child;
|
|
1603
|
+
}
|
|
1604
|
+
const partial = tokens[tokens.length - 1] ?? "";
|
|
1605
|
+
const results = [];
|
|
1606
|
+
const prefix = tokens.slice(0, tokens.length - 1);
|
|
1607
|
+
for (const [key] of node.children) {
|
|
1608
|
+
if (key.startsWith(partial)) {
|
|
1609
|
+
results.push([...prefix, key].join(" "));
|
|
1711
1610
|
}
|
|
1712
1611
|
}
|
|
1612
|
+
return results;
|
|
1713
1613
|
}
|
|
1714
1614
|
/**
|
|
1715
|
-
*
|
|
1615
|
+
* List all leaf plugin commands under a given node path.
|
|
1716
1616
|
*/
|
|
1717
|
-
|
|
1718
|
-
|
|
1719
|
-
|
|
1720
|
-
const
|
|
1721
|
-
if (!
|
|
1722
|
-
|
|
1723
|
-
name: subgroupKey,
|
|
1724
|
-
describe: subgroup,
|
|
1725
|
-
commands: []
|
|
1726
|
-
});
|
|
1727
|
-
this.byName.set(subgroupKey, this.groups.get(subgroupKey));
|
|
1617
|
+
listUnder(segments) {
|
|
1618
|
+
let node = this.root;
|
|
1619
|
+
for (const seg of segments) {
|
|
1620
|
+
const child = node.children.get(seg);
|
|
1621
|
+
if (!child) {
|
|
1622
|
+
return [];
|
|
1728
1623
|
}
|
|
1729
|
-
|
|
1730
|
-
|
|
1731
|
-
|
|
1732
|
-
|
|
1624
|
+
node = child;
|
|
1625
|
+
}
|
|
1626
|
+
const results = [];
|
|
1627
|
+
this._collectCommands(node, results);
|
|
1628
|
+
return results;
|
|
1629
|
+
}
|
|
1630
|
+
/**
|
|
1631
|
+
* Get exact command at path.
|
|
1632
|
+
*/
|
|
1633
|
+
getAt(segments) {
|
|
1634
|
+
let node = this.root;
|
|
1635
|
+
for (const seg of segments) {
|
|
1636
|
+
const child = node.children.get(seg);
|
|
1637
|
+
if (!child) {
|
|
1638
|
+
return null;
|
|
1733
1639
|
}
|
|
1734
|
-
|
|
1735
|
-
|
|
1736
|
-
|
|
1640
|
+
node = child;
|
|
1641
|
+
}
|
|
1642
|
+
return node.command ?? null;
|
|
1643
|
+
}
|
|
1644
|
+
/**
|
|
1645
|
+
* List top-level system groups and standalone system commands (depth=1 nodes).
|
|
1646
|
+
*/
|
|
1647
|
+
listSystemTopLevel() {
|
|
1648
|
+
const results = [];
|
|
1649
|
+
for (const [name, node] of this.root.children) {
|
|
1650
|
+
if (node.systemGroup) {
|
|
1651
|
+
results.push({ name, describe: node.systemGroup.describe, isGroup: true });
|
|
1652
|
+
} else if (node.systemCommand) {
|
|
1653
|
+
results.push({ name, describe: node.systemCommand.describe, isGroup: false });
|
|
1737
1654
|
}
|
|
1738
|
-
const fullPath = `${group} ${subgroup} ${id}`;
|
|
1739
|
-
this.byName.set(fullPath, commandAdapter);
|
|
1740
|
-
} else if (group) {
|
|
1741
|
-
const fullName = `${group} ${id}`;
|
|
1742
|
-
const colonName = `${group}:${id}`;
|
|
1743
|
-
this.byName.set(fullName, commandAdapter);
|
|
1744
|
-
this.byName.set(colonName, commandAdapter);
|
|
1745
1655
|
}
|
|
1656
|
+
return results;
|
|
1746
1657
|
}
|
|
1747
|
-
// ─── Resolution ──────────────────────────────────────────────────────────
|
|
1748
1658
|
/**
|
|
1749
|
-
*
|
|
1750
|
-
*
|
|
1751
|
-
* Returns canonicalId if found in plugin aliases, undefined otherwise.
|
|
1659
|
+
* Collect all plugin commands reachable from node.
|
|
1752
1660
|
*/
|
|
1753
|
-
|
|
1754
|
-
|
|
1755
|
-
|
|
1661
|
+
listAll() {
|
|
1662
|
+
const results = [];
|
|
1663
|
+
this._collectCommands(this.root, results);
|
|
1664
|
+
return results;
|
|
1665
|
+
}
|
|
1666
|
+
_collectCommands(node, out) {
|
|
1667
|
+
if (node.command) {
|
|
1668
|
+
out.push(node.command);
|
|
1756
1669
|
}
|
|
1757
|
-
const
|
|
1758
|
-
|
|
1759
|
-
return this.pluginAliases.get(converted);
|
|
1670
|
+
for (const child of node.children.values()) {
|
|
1671
|
+
this._collectCommands(child, out);
|
|
1760
1672
|
}
|
|
1761
|
-
return void 0;
|
|
1762
1673
|
}
|
|
1763
|
-
|
|
1764
|
-
|
|
1765
|
-
|
|
1674
|
+
_notFound(node, tokens, failIdx) {
|
|
1675
|
+
const failToken = tokens[failIdx];
|
|
1676
|
+
const prefix = tokens.slice(0, failIdx);
|
|
1677
|
+
const siblings = [...node.children.keys()];
|
|
1678
|
+
const fuzzy = siblings.map((s) => ({ s, d: levenshtein(failToken, s) })).filter(({ d }) => d <= 2).sort((a, b) => a.d - b.d).map(({ s }) => [...prefix, s, ...tokens.slice(failIdx + 1)].join(" "));
|
|
1679
|
+
if (fuzzy.length === 1) {
|
|
1680
|
+
return { type: "not-found", input: tokens, suggestions: fuzzy };
|
|
1681
|
+
}
|
|
1682
|
+
if (fuzzy.length > 1) {
|
|
1683
|
+
return { type: "ambiguous", input: tokens, candidates: fuzzy };
|
|
1684
|
+
}
|
|
1685
|
+
const remaining = tokens.slice(failIdx);
|
|
1686
|
+
const deep = findDeep(node, remaining, prefix);
|
|
1687
|
+
if (deep.length === 0) {
|
|
1688
|
+
return { type: "not-found", input: tokens, suggestions: [] };
|
|
1689
|
+
}
|
|
1690
|
+
if (deep.length === 1) {
|
|
1691
|
+
return { type: "not-found", input: tokens, suggestions: deep };
|
|
1692
|
+
}
|
|
1693
|
+
return { type: "ambiguous", input: tokens, candidates: deep };
|
|
1766
1694
|
}
|
|
1767
|
-
|
|
1768
|
-
|
|
1695
|
+
};
|
|
1696
|
+
|
|
1697
|
+
// src/registry/archetype-flags.ts
|
|
1698
|
+
var outputFlag = { name: "output", type: "string", choices: ["json", "table", "csv"], description: "Output format" };
|
|
1699
|
+
var dryRunFlag = { name: "dry-run", type: "boolean", description: "Show what would happen without executing" };
|
|
1700
|
+
var yesFlag = { name: "yes", type: "boolean", alias: "y", description: "Skip confirmation prompts" };
|
|
1701
|
+
var waitFlag = { name: "wait", type: "boolean", description: "Block until execution completes" };
|
|
1702
|
+
var watchFlag = { name: "watch", type: "boolean", description: "Stream events as NDJSON" };
|
|
1703
|
+
var timeoutFlag = { name: "timeout", type: "string", description: "Max wait time (e.g. 30s, 5m)" };
|
|
1704
|
+
var limitFlag = { name: "limit", type: "number", description: "Max results to return" };
|
|
1705
|
+
var offsetFlag = { name: "offset", type: "number", description: "Offset for pagination" };
|
|
1706
|
+
var formatFlag = { name: "format", type: "string", choices: ["json", "text", "md"], description: "Output format" };
|
|
1707
|
+
var streamFlag = { name: "stream", type: "boolean", description: "Stream output progressively" };
|
|
1708
|
+
var schemaFlag = {
|
|
1709
|
+
name: "schema",
|
|
1710
|
+
type: "boolean",
|
|
1711
|
+
description: "Output JSON Schema for this command (flags, types, examples)"
|
|
1712
|
+
};
|
|
1713
|
+
var ARCHETYPE_FLAGS = {
|
|
1714
|
+
read: [outputFlag, limitFlag, offsetFlag],
|
|
1715
|
+
mutate: [outputFlag, dryRunFlag, yesFlag],
|
|
1716
|
+
execute: [outputFlag, waitFlag, watchFlag, timeoutFlag, yesFlag],
|
|
1717
|
+
analyze: [outputFlag, formatFlag, streamFlag]
|
|
1718
|
+
};
|
|
1719
|
+
function getArchetypeFlags(operationType, existing = []) {
|
|
1720
|
+
const toInject = ARCHETYPE_FLAGS[operationType] ?? [];
|
|
1721
|
+
const existingNames = new Set(existing.map((f) => f.name));
|
|
1722
|
+
return toInject.filter((f) => !existingNames.has(f.name));
|
|
1723
|
+
}
|
|
1724
|
+
|
|
1725
|
+
// src/registry/service.ts
|
|
1726
|
+
var RESERVED_NAMESPACES = /* @__PURE__ */ new Set(["__complete", "__internal"]);
|
|
1727
|
+
var MAX_PATH_DEPTH = 6;
|
|
1728
|
+
function validateSegments(segs) {
|
|
1729
|
+
if (segs.length === 0) {
|
|
1730
|
+
return "empty path \u2014 segments must not be empty";
|
|
1769
1731
|
}
|
|
1770
|
-
|
|
1771
|
-
return
|
|
1732
|
+
if (segs.length > MAX_PATH_DEPTH) {
|
|
1733
|
+
return `path too deep: ${segs.length} segments (max ${MAX_PATH_DEPTH})`;
|
|
1772
1734
|
}
|
|
1773
|
-
|
|
1774
|
-
|
|
1775
|
-
|
|
1776
|
-
|
|
1777
|
-
|
|
1778
|
-
|
|
1735
|
+
if (RESERVED_NAMESPACES.has(segs[0])) {
|
|
1736
|
+
return `"${segs[0]}" is a reserved namespace`;
|
|
1737
|
+
}
|
|
1738
|
+
return null;
|
|
1739
|
+
}
|
|
1740
|
+
var noopLogger = {
|
|
1741
|
+
debug: () => {
|
|
1742
|
+
},
|
|
1743
|
+
info: () => {
|
|
1744
|
+
},
|
|
1745
|
+
warn: () => {
|
|
1746
|
+
},
|
|
1747
|
+
error: () => {
|
|
1748
|
+
},
|
|
1749
|
+
child: () => noopLogger
|
|
1750
|
+
};
|
|
1751
|
+
var TrieBackedRegistry = class {
|
|
1752
|
+
systemRouter = new TrieRouter();
|
|
1753
|
+
pluginRouter = new TrieRouter();
|
|
1754
|
+
partial = false;
|
|
1755
|
+
logger = noopLogger;
|
|
1756
|
+
// ─── Logger ────────────────────────────────────────────────────────────────
|
|
1757
|
+
setLogger(logger) {
|
|
1758
|
+
this.logger = logger;
|
|
1779
1759
|
}
|
|
1780
|
-
|
|
1781
|
-
|
|
1760
|
+
// ─── Registration ──────────────────────────────────────────────────────────
|
|
1761
|
+
register(cmd) {
|
|
1762
|
+
this.systemRouter.insertSystemCommand(cmd);
|
|
1782
1763
|
}
|
|
1783
|
-
|
|
1784
|
-
|
|
1785
|
-
|
|
1786
|
-
|
|
1787
|
-
|
|
1788
|
-
* Returns type='plugin' for plugin commands (subprocess execution).
|
|
1789
|
-
*/
|
|
1790
|
-
getWithType(nameOrPath) {
|
|
1791
|
-
const normalized = typeof nameOrPath === "string" ? nameOrPath : nameOrPath.join(" ");
|
|
1792
|
-
if (this.systemCommands.has(normalized)) {
|
|
1793
|
-
return { cmd: this.systemCommands.get(normalized), type: "system" };
|
|
1794
|
-
}
|
|
1795
|
-
const colonVariant = normalized.replace(/ /g, ":");
|
|
1796
|
-
if (this.systemCommands.has(colonVariant)) {
|
|
1797
|
-
return { cmd: this.systemCommands.get(colonVariant), type: "system" };
|
|
1798
|
-
}
|
|
1799
|
-
if (this.groups.has(normalized)) {
|
|
1800
|
-
return { cmd: this.groups.get(normalized), type: "system" };
|
|
1801
|
-
}
|
|
1802
|
-
const canonicalId = this.resolveToCanonical(normalized);
|
|
1803
|
-
if (canonicalId) {
|
|
1804
|
-
const pluginCmd = this.pluginByCanonical.get(canonicalId);
|
|
1805
|
-
if (pluginCmd && !pluginCmd.shadowed) {
|
|
1806
|
-
const cmd2 = this.byName.get(canonicalId) ?? this.byName.get(normalized);
|
|
1807
|
-
if (cmd2) {
|
|
1808
|
-
return { cmd: cmd2, type: "plugin" };
|
|
1809
|
-
}
|
|
1764
|
+
registerGroup(group) {
|
|
1765
|
+
this.systemRouter.insertSystemGroup(group);
|
|
1766
|
+
if (group.subgroups) {
|
|
1767
|
+
for (const sub of group.subgroups) {
|
|
1768
|
+
this.systemRouter.setGroupDescribe([group.name, sub.name], sub.describe ?? "");
|
|
1810
1769
|
}
|
|
1811
1770
|
}
|
|
1812
|
-
|
|
1813
|
-
|
|
1814
|
-
|
|
1815
|
-
|
|
1816
|
-
if ("commands" in cmd) {
|
|
1817
|
-
return { cmd, type: "system" };
|
|
1771
|
+
}
|
|
1772
|
+
registerManifest(cmd) {
|
|
1773
|
+
if (cmd.manifest._synthetic) {
|
|
1774
|
+
return;
|
|
1818
1775
|
}
|
|
1819
|
-
|
|
1820
|
-
|
|
1776
|
+
const segs = cmd.manifest.segments;
|
|
1777
|
+
const pkg = cmd.packageName ?? "unknown";
|
|
1778
|
+
const validationError = validateSegments(segs);
|
|
1779
|
+
if (validationError) {
|
|
1780
|
+
this.logger.warn(`[registry] Plugin "${pkg}" skipped: ${validationError} (path: "${segs.join(" ")}")`);
|
|
1781
|
+
cmd.shadowed = true;
|
|
1782
|
+
return;
|
|
1821
1783
|
}
|
|
1822
|
-
const
|
|
1823
|
-
if (
|
|
1824
|
-
|
|
1784
|
+
const sysResult = this.systemRouter.resolve([...segs]);
|
|
1785
|
+
if (sysResult.type === "system-cmd" || sysResult.type === "system-group") {
|
|
1786
|
+
this.logger.warn(`[registry] Plugin "${pkg}" tried to register "${segs.join(" ")}" but it collides with a system command. Command will be shadowed.`);
|
|
1787
|
+
cmd.shadowed = true;
|
|
1788
|
+
return;
|
|
1825
1789
|
}
|
|
1826
|
-
|
|
1827
|
-
|
|
1828
|
-
|
|
1829
|
-
|
|
1830
|
-
if (this.byName.has(nameOrPath)) {
|
|
1831
|
-
return this.byName.get(nameOrPath);
|
|
1832
|
-
}
|
|
1833
|
-
if (nameOrPath.includes(":")) {
|
|
1834
|
-
const spaceKey = nameOrPath.replace(/:/g, " ");
|
|
1835
|
-
if (this.byName.has(spaceKey)) {
|
|
1836
|
-
return this.byName.get(spaceKey);
|
|
1837
|
-
}
|
|
1838
|
-
const parts = nameOrPath.split(":");
|
|
1839
|
-
if (parts.length === 2) {
|
|
1840
|
-
const spaceKey2 = parts.join(" ");
|
|
1841
|
-
if (this.byName.has(spaceKey2)) {
|
|
1842
|
-
return this.byName.get(spaceKey2);
|
|
1843
|
-
}
|
|
1844
|
-
}
|
|
1790
|
+
if (cmd.manifest.operationType) {
|
|
1791
|
+
const injected = getArchetypeFlags(cmd.manifest.operationType, cmd.manifest.flags ?? []);
|
|
1792
|
+
if (injected.length > 0) {
|
|
1793
|
+
cmd.manifest.flags = [...cmd.manifest.flags ?? [], ...injected];
|
|
1845
1794
|
}
|
|
1846
1795
|
}
|
|
1847
|
-
|
|
1848
|
-
|
|
1849
|
-
return this.byName.get(key);
|
|
1796
|
+
if (cmd.manifest.operationType && !cmd.manifest.flags?.some((f) => f.name === "schema")) {
|
|
1797
|
+
cmd.manifest.flags = [...cmd.manifest.flags ?? [], schemaFlag];
|
|
1850
1798
|
}
|
|
1851
|
-
|
|
1852
|
-
|
|
1853
|
-
|
|
1799
|
+
const result = this.pluginRouter.insertCommand(segs, cmd);
|
|
1800
|
+
if (result.collides) {
|
|
1801
|
+
this.logger.warn(
|
|
1802
|
+
`[registry] Plugin "${pkg}" tried to register "${segs.join(" ")}" but namespace "${segs[0]}" is owned by "${result.ownerPackage ?? "unknown"}". Command will be shadowed.`
|
|
1803
|
+
);
|
|
1804
|
+
cmd.shadowed = true;
|
|
1805
|
+
return;
|
|
1806
|
+
}
|
|
1807
|
+
for (const alias of cmd.manifest.aliases ?? []) {
|
|
1808
|
+
const aliasSegs = alias.trim().split(/\s+/).filter(Boolean);
|
|
1809
|
+
if (aliasSegs.length === 0) {
|
|
1810
|
+
continue;
|
|
1854
1811
|
}
|
|
1855
|
-
const
|
|
1856
|
-
|
|
1857
|
-
|
|
1858
|
-
|
|
1812
|
+
const aliasValidationError = validateSegments(aliasSegs);
|
|
1813
|
+
if (aliasValidationError) {
|
|
1814
|
+
this.logger.warn(`[registry] Plugin "${pkg}" alias "${alias}" skipped: ${aliasValidationError}`);
|
|
1815
|
+
continue;
|
|
1859
1816
|
}
|
|
1860
|
-
|
|
1861
|
-
|
|
1862
|
-
|
|
1863
|
-
|
|
1864
|
-
|
|
1817
|
+
const aliasSysResult = this.systemRouter.resolve([...aliasSegs]);
|
|
1818
|
+
if (aliasSysResult.type === "system-cmd" || aliasSysResult.type === "system-group") {
|
|
1819
|
+
this.logger.warn(`[registry] Plugin "${pkg}" alias "${alias}" collides with a system command and will be skipped.`);
|
|
1820
|
+
continue;
|
|
1821
|
+
}
|
|
1822
|
+
const aliasResult = this.pluginRouter.insertCommand(aliasSegs, cmd);
|
|
1823
|
+
if (aliasResult.collides) {
|
|
1824
|
+
this.logger.warn(
|
|
1825
|
+
`[registry] Plugin "${pkg}" alias "${alias}" conflicts with namespace "${aliasSegs[0]}" owned by "${aliasResult.ownerPackage ?? "unknown"}". Alias skipped.`
|
|
1826
|
+
);
|
|
1865
1827
|
}
|
|
1866
1828
|
}
|
|
1867
|
-
|
|
1868
|
-
|
|
1869
|
-
const
|
|
1870
|
-
|
|
1871
|
-
|
|
1872
|
-
const fullName = `${group.name} ${cmdName}`;
|
|
1873
|
-
if (this.byName.has(fullName)) {
|
|
1874
|
-
return this.byName.get(fullName);
|
|
1875
|
-
}
|
|
1876
|
-
}
|
|
1829
|
+
const v3 = cmd.manifest.manifestV2;
|
|
1830
|
+
if (v3?.cli?.groupMeta) {
|
|
1831
|
+
for (const meta of v3.cli.groupMeta) {
|
|
1832
|
+
const metaSegs = meta.path.trim().split(/\s+/).filter(Boolean);
|
|
1833
|
+
this.pluginRouter.setGroupDescribe(metaSegs, meta.describe);
|
|
1877
1834
|
}
|
|
1878
1835
|
}
|
|
1879
|
-
return void 0;
|
|
1880
1836
|
}
|
|
1881
|
-
|
|
1882
|
-
|
|
1883
|
-
|
|
1884
|
-
|
|
1885
|
-
|
|
1837
|
+
// ─── Resolution ────────────────────────────────────────────────────────────
|
|
1838
|
+
resolve(tokens) {
|
|
1839
|
+
if (tokens.length > 0) {
|
|
1840
|
+
const sysResult = this.systemRouter.resolve(tokens);
|
|
1841
|
+
if (sysResult.type !== "not-found") {
|
|
1842
|
+
return sysResult;
|
|
1886
1843
|
}
|
|
1887
1844
|
}
|
|
1888
|
-
return
|
|
1845
|
+
return this.pluginRouter.resolve(tokens);
|
|
1889
1846
|
}
|
|
1890
|
-
|
|
1891
|
-
|
|
1847
|
+
// ─── Tab completion ─────────────────────────────────────────────────────────
|
|
1848
|
+
complete(tokens) {
|
|
1849
|
+
const sysCompletions = this.systemRouter.complete(tokens);
|
|
1850
|
+
const pluginCompletions = this.pluginRouter.complete(tokens);
|
|
1851
|
+
return [.../* @__PURE__ */ new Set([...sysCompletions, ...pluginCompletions])];
|
|
1892
1852
|
}
|
|
1893
|
-
|
|
1894
|
-
|
|
1895
|
-
|
|
1896
|
-
if (group.name === prefix || group.name.startsWith(prefix + ":")) {
|
|
1897
|
-
result.push(group);
|
|
1898
|
-
}
|
|
1899
|
-
}
|
|
1900
|
-
return result;
|
|
1853
|
+
// ─── Query ──────────────────────────────────────────────────────────────────
|
|
1854
|
+
listSystemTopLevel() {
|
|
1855
|
+
return this.systemRouter.listSystemTopLevel();
|
|
1901
1856
|
}
|
|
1902
|
-
|
|
1903
|
-
|
|
1904
|
-
for (const group of this.groups.values()) {
|
|
1905
|
-
if (group.name === prefix || group.name.startsWith(prefix + ":")) {
|
|
1906
|
-
result.push(...group.commands);
|
|
1907
|
-
}
|
|
1908
|
-
}
|
|
1909
|
-
return result;
|
|
1910
|
-
}
|
|
1911
|
-
listProductGroups() {
|
|
1912
|
-
const groups = /* @__PURE__ */ new Map();
|
|
1913
|
-
for (const cmd of this.listManifests()) {
|
|
1914
|
-
const groupName = cmd.manifest.group;
|
|
1915
|
-
if (!groups.has(groupName)) {
|
|
1916
|
-
groups.set(groupName, {
|
|
1917
|
-
name: groupName,
|
|
1918
|
-
describe: cmd.manifest.group,
|
|
1919
|
-
commands: []
|
|
1920
|
-
});
|
|
1921
|
-
}
|
|
1922
|
-
groups.get(groupName).commands.push(cmd);
|
|
1923
|
-
}
|
|
1924
|
-
return Array.from(groups.values());
|
|
1857
|
+
getPluginGroupDescribe(segments) {
|
|
1858
|
+
return this.pluginRouter.getGroupDescribe(segments);
|
|
1925
1859
|
}
|
|
1926
|
-
|
|
1927
|
-
return this.
|
|
1860
|
+
listCommands() {
|
|
1861
|
+
return this.pluginRouter.listAll();
|
|
1928
1862
|
}
|
|
1929
|
-
|
|
1930
|
-
|
|
1931
|
-
|
|
1932
|
-
|
|
1933
|
-
|
|
1934
|
-
|
|
1935
|
-
|
|
1936
|
-
|
|
1937
|
-
|
|
1938
|
-
|
|
1939
|
-
|
|
1940
|
-
|
|
1941
|
-
|
|
1942
|
-
|
|
1943
|
-
|
|
1944
|
-
|
|
1945
|
-
|
|
1863
|
+
listCommandsUnder(segments) {
|
|
1864
|
+
return this.pluginRouter.listUnder(segments);
|
|
1865
|
+
}
|
|
1866
|
+
getCommandAt(segments) {
|
|
1867
|
+
return this.pluginRouter.getAt(segments);
|
|
1868
|
+
}
|
|
1869
|
+
// ─── Partial loading state ─────────────────────────────────────────────────
|
|
1870
|
+
markPartial(v) {
|
|
1871
|
+
this.partial = v;
|
|
1872
|
+
}
|
|
1873
|
+
isPartial() {
|
|
1874
|
+
return this.partial;
|
|
1875
|
+
}
|
|
1876
|
+
// ─── Diagnostics ──────────────────────────────────────────────────────────
|
|
1877
|
+
getDiagnostics() {
|
|
1878
|
+
return {
|
|
1879
|
+
systemCommandCount: this.systemRouter.listAll().length,
|
|
1880
|
+
systemGroupCount: 0,
|
|
1881
|
+
pluginCommandCount: this.pluginRouter.listAll().length,
|
|
1882
|
+
partialLoad: this.partial
|
|
1883
|
+
};
|
|
1946
1884
|
}
|
|
1947
1885
|
};
|
|
1948
|
-
|
|
1949
|
-
|
|
1950
|
-
return registry.get(nameOrPath);
|
|
1951
|
-
}
|
|
1952
|
-
function findCommandWithType(nameOrPath) {
|
|
1953
|
-
return registry.getWithType(nameOrPath);
|
|
1886
|
+
function createRegistry() {
|
|
1887
|
+
return new TrieBackedRegistry();
|
|
1954
1888
|
}
|
|
1889
|
+
var registry = new TrieBackedRegistry();
|
|
1955
1890
|
|
|
1956
|
-
export { __test, checkRequires, discoverManifests, discoverManifestsByNamespace, disposeAllPlugins,
|
|
1891
|
+
export { TrieBackedRegistry, TrieRouter, __test, checkRequires, createRegistry, discoverManifests, discoverManifestsByNamespace, disposeAllPlugins, levenshtein, loadConfig, preflightManifests, registerManifests, registry, resetInProcCache, runCommand };
|
|
1957
1892
|
//# sourceMappingURL=index.js.map
|
|
1958
1893
|
//# sourceMappingURL=index.js.map
|