@nseng-ai/kernel 0.1.2

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.
@@ -0,0 +1,744 @@
1
+ import { existsSync, readFileSync, statSync } from "node:fs";
2
+ import { basename, extname, join, relative, resolve } from "node:path";
3
+
4
+ import { isPathInside, optionalEntry, type ZodIssueLike } from "@nseng-ai/foundation/primitives";
5
+ import {
6
+ nsExtensionManifestCommandSchema,
7
+ nsExtensionPackageManifestSchema,
8
+ type NsExtensionManifestCommand,
9
+ type NsExtensionPackageManifest,
10
+ } from "../sdk/index.ts";
11
+
12
+ import {
13
+ commandLeafName,
14
+ formatUnknownError,
15
+ type NsCommandCandidate,
16
+ } from "./command-registry.ts";
17
+ import { NS_COMMAND_NAME_PATTERN, NS_COMMAND_NAME_RULE } from "../sdk/command-name.ts";
18
+ import type { NsCommandModuleReference } from "./module-reference.ts";
19
+ import { makeKernelDiagnostic } from "../runtime/diagnostics.ts";
20
+ import { scanExtensionRoot } from "../runtime/extension-root-discovery.ts";
21
+ import { classifyFirstMatchingZodIssuePath, type ZodIssuePathRule } from "./zod-issue-path.ts";
22
+
23
+ export interface DiscoveredExtensionCommand extends Pick<
24
+ NsCommandCandidate,
25
+ | "group"
26
+ | "name"
27
+ | "segments"
28
+ | "groupDescription"
29
+ | "description"
30
+ | "fullDescription"
31
+ | "entryPath"
32
+ > {
33
+ entryPath: string;
34
+ displayPath: string;
35
+ hasStaticCommandInfo: boolean;
36
+ moduleReference?: NsCommandModuleReference;
37
+ }
38
+
39
+ export interface ExtensionDiscoveryDiagnostic {
40
+ severity: "error";
41
+ code: string;
42
+ message: string;
43
+ path?: string;
44
+ commandName?: string;
45
+ }
46
+
47
+ export interface ExtensionDiscoveryResult {
48
+ commands: readonly DiscoveredExtensionCommand[];
49
+ diagnostics: readonly ExtensionDiscoveryDiagnostic[];
50
+ }
51
+
52
+ interface ParsedManifestCommandEntryFields {
53
+ group: string | undefined;
54
+ name: string | undefined;
55
+ segments: readonly string[] | undefined;
56
+ groupDescription?: string;
57
+ description: string | undefined;
58
+ entry: string | undefined;
59
+ fullDescription: string | undefined;
60
+ }
61
+
62
+ type RequiredManifestCommandStringField = "description" | "entry";
63
+
64
+ const requiredManifestCommandStringFields = [
65
+ {
66
+ field: "description",
67
+ code: "extension_manifest_command_description_missing",
68
+ },
69
+ {
70
+ field: "entry",
71
+ code: "extension_manifest_command_entry_missing",
72
+ },
73
+ ] as const satisfies readonly {
74
+ field: RequiredManifestCommandStringField;
75
+ code: string;
76
+ }[];
77
+
78
+ type ManifestStructureIssueKind = "missing-ns" | "commands-not-array" | "other";
79
+
80
+ const manifestStructureIssueRules: readonly ZodIssuePathRule<ManifestStructureIssueKind>[] = [
81
+ { pattern: [], match: "exact", value: "missing-ns" },
82
+ { pattern: ["ns"], match: "exact", value: "missing-ns" },
83
+ { pattern: ["ns", "commands"], match: "prefix", value: "commands-not-array" },
84
+ ];
85
+
86
+ type PackageManifestParseResult =
87
+ | { outcome: "ok"; manifest: NsExtensionPackageManifest }
88
+ | { outcome: "parse-failed"; diagnostic: ExtensionDiscoveryDiagnostic }
89
+ | { outcome: "schema-failed"; issues: readonly ZodIssueLike[] };
90
+
91
+ type PackageManifestDiscoveryResolution =
92
+ | { outcome: "ok"; manifest: NsExtensionPackageManifest }
93
+ | { outcome: "unavailable"; result: ExtensionDiscoveryResult };
94
+
95
+ export function discoverExtensionsInRoot(rootDir: string): ExtensionDiscoveryResult {
96
+ const rootScan = scanExtensionRoot(rootDir);
97
+ if (rootScan.diagnostics.length > 0) {
98
+ return { commands: [], diagnostics: rootScan.diagnostics };
99
+ }
100
+
101
+ const commands: DiscoveredExtensionCommand[] = [];
102
+ const diagnostics: ExtensionDiscoveryDiagnostic[] = [];
103
+ for (const entry of rootScan.entries) {
104
+ if (entry.type === "file") {
105
+ if (isLoadableExtensionFile(entry.name)) {
106
+ pushDirectEntryCommand(
107
+ commands,
108
+ diagnostics,
109
+ commandForDirectEntry({
110
+ rootDir,
111
+ name: basename(entry.name, extname(entry.name)),
112
+ entryPath: entry.path,
113
+ }),
114
+ );
115
+ }
116
+ continue;
117
+ }
118
+
119
+ if (entry.packageJsonPath !== undefined) {
120
+ const packageResult = discoverPackageCommands(rootDir, entry.path, entry.packageJsonPath);
121
+ commands.push(...packageResult.commands);
122
+ diagnostics.push(...packageResult.diagnostics);
123
+ continue;
124
+ }
125
+
126
+ if (entry.indexPath !== undefined) {
127
+ pushDirectEntryCommand(
128
+ commands,
129
+ diagnostics,
130
+ commandForDirectEntry({
131
+ rootDir,
132
+ name: entry.name,
133
+ entryPath: entry.indexPath,
134
+ }),
135
+ );
136
+ continue;
137
+ }
138
+ diagnostics.push(
139
+ diagnostic(
140
+ "extension_directory_missing_entry",
141
+ `Extension directory must contain package.json, index.ts, or index.js: ${entry.path}.`,
142
+ { path: entry.path, commandName: entry.name },
143
+ ),
144
+ );
145
+ }
146
+
147
+ return {
148
+ commands: commands.sort((left, right) =>
149
+ commandSortKey(left).localeCompare(commandSortKey(right)),
150
+ ),
151
+ diagnostics,
152
+ };
153
+ }
154
+
155
+ export function discoverNsPackageCommands(
156
+ rootDir: string,
157
+ packageDir: string,
158
+ ): ExtensionDiscoveryResult {
159
+ const packageJsonPath = join(packageDir, "package.json");
160
+ if (!existsSync(packageJsonPath)) return { commands: [], diagnostics: [] };
161
+
162
+ const resolution = resolvePackageManifest(packageJsonPath, () => ({
163
+ commands: [],
164
+ diagnostics: [],
165
+ }));
166
+ if (resolution.outcome === "unavailable") return resolution.result;
167
+ if (resolution.manifest.ns?.commands === undefined) return { commands: [], diagnostics: [] };
168
+ return discoverPackageCommands(rootDir, packageDir, packageJsonPath, resolution.manifest);
169
+ }
170
+
171
+ function readPackageManifest(packageJsonPath: string): PackageManifestParseResult {
172
+ let parsed: unknown;
173
+ try {
174
+ parsed = JSON.parse(readFileSync(packageJsonPath, "utf8"));
175
+ } catch (error) {
176
+ return {
177
+ outcome: "parse-failed",
178
+ diagnostic: diagnostic(
179
+ "extension_manifest_parse_failed",
180
+ `Could not parse extension manifest ${packageJsonPath}.\n${formatUnknownError(error)}`,
181
+ { path: packageJsonPath },
182
+ ),
183
+ };
184
+ }
185
+ const manifestResult = nsExtensionPackageManifestSchema.safeParse(parsed);
186
+ if (!manifestResult.success) {
187
+ return { outcome: "schema-failed", issues: manifestResult.error.issues };
188
+ }
189
+ return { outcome: "ok", manifest: manifestResult.data };
190
+ }
191
+
192
+ function resolvePackageManifest(
193
+ packageJsonPath: string,
194
+ onSchemaFailed: (issues: readonly ZodIssueLike[]) => ExtensionDiscoveryResult,
195
+ ): PackageManifestDiscoveryResolution {
196
+ const parseResult = readPackageManifest(packageJsonPath);
197
+ if (parseResult.outcome === "parse-failed") {
198
+ return {
199
+ outcome: "unavailable",
200
+ result: manifestReadFailureResult(parseResult.diagnostic),
201
+ };
202
+ }
203
+ if (parseResult.outcome === "schema-failed") {
204
+ return {
205
+ outcome: "unavailable",
206
+ result: onSchemaFailed(parseResult.issues),
207
+ };
208
+ }
209
+ return { outcome: "ok", manifest: parseResult.manifest };
210
+ }
211
+
212
+ function manifestReadFailureResult(
213
+ diagnostic: ExtensionDiscoveryDiagnostic,
214
+ ): ExtensionDiscoveryResult {
215
+ return { commands: [], diagnostics: [diagnostic] };
216
+ }
217
+
218
+ function discoverPackageCommands(
219
+ rootDir: string,
220
+ packageDir: string,
221
+ packageJsonPath: string,
222
+ parsedManifest?: NsExtensionPackageManifest,
223
+ ): ExtensionDiscoveryResult {
224
+ let manifest: NsExtensionPackageManifest;
225
+ if (parsedManifest === undefined) {
226
+ const resolution = resolvePackageManifest(packageJsonPath, (issues) => ({
227
+ commands: [],
228
+ diagnostics: [manifestStructureDiagnostic(issues, packageJsonPath)],
229
+ }));
230
+ if (resolution.outcome === "unavailable") return resolution.result;
231
+ manifest = resolution.manifest;
232
+ } else {
233
+ manifest = parsedManifest;
234
+ }
235
+
236
+ if (manifest.ns === undefined) {
237
+ return {
238
+ commands: [],
239
+ diagnostics: [missingNsDiagnostic(packageJsonPath)],
240
+ };
241
+ }
242
+ const packageGroup = readNonEmptyString(manifest.ns.group);
243
+ const packageGroupDescription =
244
+ readNonEmptyString(manifest.ns.description) ?? readNonEmptyString(manifest.description);
245
+ const packageGroupDiagnostic = groupDiagnostic({
246
+ group: manifest.ns.group,
247
+ packageJsonPath,
248
+ commandName: undefined,
249
+ });
250
+ if (packageGroupDiagnostic !== undefined) {
251
+ return { commands: [], diagnostics: [packageGroupDiagnostic] };
252
+ }
253
+ const entries = manifest.ns.commands;
254
+ if (!Array.isArray(entries)) {
255
+ return {
256
+ commands: [],
257
+ diagnostics: [commandsNotArrayDiagnostic(packageJsonPath)],
258
+ };
259
+ }
260
+
261
+ const commands: DiscoveredExtensionCommand[] = [];
262
+ const diagnostics: ExtensionDiscoveryDiagnostic[] = [];
263
+ for (const entry of entries) {
264
+ const command = commandForManifestEntry({
265
+ rootDir,
266
+ packageDir,
267
+ packageJsonPath,
268
+ packageGroup,
269
+ packageGroupDescription,
270
+ entry,
271
+ });
272
+ if (command.ok) {
273
+ commands.push(command.command);
274
+ } else {
275
+ diagnostics.push(...command.diagnostics);
276
+ }
277
+ }
278
+ return { commands, diagnostics };
279
+ }
280
+
281
+ function manifestStructureDiagnostic(
282
+ issues: readonly ZodIssueLike[],
283
+ packageJsonPath: string,
284
+ ): ExtensionDiscoveryDiagnostic {
285
+ const kind = classifyFirstMatchingZodIssuePath(issues, manifestStructureIssueRules, "other");
286
+ if (kind === "missing-ns") return missingNsDiagnostic(packageJsonPath);
287
+ if (kind === "commands-not-array") return commandsNotArrayDiagnostic(packageJsonPath);
288
+ return diagnostic(
289
+ "extension_manifest_invalid",
290
+ `Extension manifest contains invalid ns metadata: ${packageJsonPath}.`,
291
+ { path: packageJsonPath },
292
+ );
293
+ }
294
+
295
+ function missingNsDiagnostic(packageJsonPath: string): ExtensionDiscoveryDiagnostic {
296
+ return diagnostic(
297
+ "extension_manifest_missing_ns",
298
+ `Extension manifest must contain an ns object: ${packageJsonPath}.`,
299
+ { path: packageJsonPath },
300
+ );
301
+ }
302
+
303
+ function commandsNotArrayDiagnostic(packageJsonPath: string): ExtensionDiscoveryDiagnostic {
304
+ return diagnostic(
305
+ "extension_manifest_commands_not_array",
306
+ `Extension manifest ns.commands must be an array: ${packageJsonPath}.`,
307
+ { path: packageJsonPath },
308
+ );
309
+ }
310
+
311
+ function pushDirectEntryCommand(
312
+ commands: DiscoveredExtensionCommand[],
313
+ diagnostics: ExtensionDiscoveryDiagnostic[],
314
+ command:
315
+ | { ok: true; command: DiscoveredExtensionCommand }
316
+ | { ok: false; diagnostic: ExtensionDiscoveryDiagnostic },
317
+ ): void {
318
+ if (command.ok) commands.push(command.command);
319
+ else diagnostics.push(command.diagnostic);
320
+ }
321
+
322
+ function commandForDirectEntry(options: {
323
+ name: string;
324
+ entryPath: string;
325
+ rootDir: string;
326
+ }):
327
+ | { ok: true; command: DiscoveredExtensionCommand }
328
+ | { ok: false; diagnostic: ExtensionDiscoveryDiagnostic } {
329
+ if (!NS_COMMAND_NAME_PATTERN.test(options.name)) {
330
+ return {
331
+ ok: false,
332
+ diagnostic: diagnostic(
333
+ "extension_command_name_invalid",
334
+ `ns command entry name inferred from ${options.entryPath} must match ${NS_COMMAND_NAME_RULE}.`,
335
+ { path: options.entryPath, commandName: options.name },
336
+ ),
337
+ };
338
+ }
339
+ return { ok: true, command: buildCommand(options) };
340
+ }
341
+
342
+ function commandForManifestEntry(options: {
343
+ rootDir: string;
344
+ packageDir: string;
345
+ packageJsonPath: string;
346
+ packageGroup: string | undefined;
347
+ packageGroupDescription: string | undefined;
348
+ entry: unknown;
349
+ }):
350
+ | { ok: true; command: DiscoveredExtensionCommand }
351
+ | { ok: false; diagnostics: readonly ExtensionDiscoveryDiagnostic[] } {
352
+ const entryResult = nsExtensionManifestCommandSchema.safeParse(options.entry);
353
+ if (!entryResult.success) {
354
+ return {
355
+ ok: false,
356
+ diagnostics: [
357
+ diagnostic(
358
+ "extension_manifest_command_invalid",
359
+ `Extension manifest commands must be objects with supported known fields: ${options.packageJsonPath}.`,
360
+ { path: options.packageJsonPath },
361
+ ),
362
+ ],
363
+ };
364
+ }
365
+ const parsedEntry = parseManifestCommandEntry({
366
+ entry: entryResult.data,
367
+ packageGroup: options.packageGroup,
368
+ packageGroupDescription: options.packageGroupDescription,
369
+ packageJsonPath: options.packageJsonPath,
370
+ });
371
+ const diagnostics: ExtensionDiscoveryDiagnostic[] = [...parsedEntry.diagnostics];
372
+ const commandName = parsedEntry.commandName;
373
+
374
+ if (
375
+ parsedEntry.entry.name !== undefined &&
376
+ !NS_COMMAND_NAME_PATTERN.test(parsedEntry.entry.name)
377
+ ) {
378
+ diagnostics.push(
379
+ diagnostic(
380
+ "extension_manifest_command_name_invalid",
381
+ `Extension manifest command name must match ${NS_COMMAND_NAME_RULE}: ${parsedEntry.entry.name}.`,
382
+ diagnosticLocation(options.packageJsonPath, commandName),
383
+ ),
384
+ );
385
+ }
386
+
387
+ let entryPath: string | undefined;
388
+ if (parsedEntry.entry.entry !== undefined) {
389
+ const entryPathValidation = validateManifestEntryPath({
390
+ packageDir: options.packageDir,
391
+ packageJsonPath: options.packageJsonPath,
392
+ rawEntryPath: parsedEntry.entry.entry,
393
+ commandName,
394
+ });
395
+ if (entryPathValidation.ok) {
396
+ entryPath = entryPathValidation.entryPath;
397
+ } else {
398
+ diagnostics.push(...entryPathValidation.diagnostics);
399
+ }
400
+ }
401
+
402
+ if (
403
+ diagnostics.length > 0 ||
404
+ parsedEntry.entry.name === undefined ||
405
+ parsedEntry.entry.description === undefined ||
406
+ parsedEntry.entry.fullDescription === undefined ||
407
+ entryPath === undefined
408
+ ) {
409
+ return { ok: false, diagnostics };
410
+ }
411
+
412
+ const displayPath = relativeDisplayPath(options.rootDir, entryPath);
413
+ return {
414
+ ok: true,
415
+ command: {
416
+ hasStaticCommandInfo: true,
417
+ ...moduleReferenceForEntryPath(entryPath),
418
+ ...(parsedEntry.entry.group === undefined ? {} : { group: parsedEntry.entry.group }),
419
+ ...(parsedEntry.entry.segments === undefined ? {} : { segments: parsedEntry.entry.segments }),
420
+ ...(parsedEntry.entry.groupDescription === undefined
421
+ ? {}
422
+ : { groupDescription: parsedEntry.entry.groupDescription }),
423
+ name: parsedEntry.entry.name,
424
+ description: parsedEntry.entry.description,
425
+ fullDescription: parsedEntry.entry.fullDescription,
426
+ entryPath,
427
+ displayPath,
428
+ },
429
+ };
430
+ }
431
+
432
+ function validateManifestEntryPath(options: {
433
+ packageDir: string;
434
+ packageJsonPath: string;
435
+ rawEntryPath: string;
436
+ commandName: string | undefined;
437
+ }):
438
+ | { ok: true; entryPath: string }
439
+ | { ok: false; diagnostics: readonly ExtensionDiscoveryDiagnostic[] } {
440
+ if (options.rawEntryPath.startsWith("/") || options.rawEntryPath.includes("\\")) {
441
+ return {
442
+ ok: false,
443
+ diagnostics: [
444
+ diagnostic(
445
+ "extension_manifest_entry_not_relative",
446
+ `Extension manifest command entry must be a relative POSIX-style path inside the package: ${options.rawEntryPath}.`,
447
+ diagnosticLocation(options.packageJsonPath, options.commandName),
448
+ ),
449
+ ],
450
+ };
451
+ }
452
+
453
+ const resolvedEntry = resolve(options.packageDir, options.rawEntryPath);
454
+ if (!isPathInside(options.packageDir, resolvedEntry)) {
455
+ return {
456
+ ok: false,
457
+ diagnostics: [
458
+ diagnostic(
459
+ "extension_manifest_entry_escapes",
460
+ `Extension manifest command entry must not escape its package directory: ${options.rawEntryPath}.`,
461
+ diagnosticLocation(options.packageJsonPath, options.commandName),
462
+ ),
463
+ ],
464
+ };
465
+ }
466
+ if (!isLoadableExtensionFile(basename(resolvedEntry))) {
467
+ return {
468
+ ok: false,
469
+ diagnostics: [
470
+ diagnostic(
471
+ "extension_manifest_entry_unsupported",
472
+ `Extension manifest command entry must be a .ts or .js file, excluding .d.ts: ${options.rawEntryPath}.`,
473
+ diagnosticLocation(options.packageJsonPath, options.commandName),
474
+ ),
475
+ ],
476
+ };
477
+ }
478
+
479
+ let entryStat;
480
+ try {
481
+ entryStat = statSync(resolvedEntry);
482
+ } catch {
483
+ return {
484
+ ok: false,
485
+ diagnostics: [
486
+ diagnostic(
487
+ "extension_manifest_entry_missing",
488
+ `Extension manifest command entry does not exist: ${options.rawEntryPath}.`,
489
+ diagnosticLocation(options.packageJsonPath, options.commandName),
490
+ ),
491
+ ],
492
+ };
493
+ }
494
+ if (!entryStat.isFile()) {
495
+ return {
496
+ ok: false,
497
+ diagnostics: [
498
+ diagnostic(
499
+ "extension_manifest_entry_not_file",
500
+ `Extension manifest command entry must be a file: ${options.rawEntryPath}.`,
501
+ diagnosticLocation(options.packageJsonPath, options.commandName),
502
+ ),
503
+ ],
504
+ };
505
+ }
506
+ return { ok: true, entryPath: resolvedEntry };
507
+ }
508
+
509
+ function commandNameFromManifestPath(segments: readonly string[] | undefined): string | undefined {
510
+ if (segments === undefined || segments.length === 0) return undefined;
511
+ return commandLeafName({ name: segments.at(-1) ?? "", segments });
512
+ }
513
+
514
+ function parseManifestCommandEntry(options: {
515
+ entry: NsExtensionManifestCommand;
516
+ packageGroup: string | undefined;
517
+ packageGroupDescription: string | undefined;
518
+ packageJsonPath: string;
519
+ }): {
520
+ entry: ParsedManifestCommandEntryFields;
521
+ diagnostics: readonly ExtensionDiscoveryDiagnostic[];
522
+ commandName: string | undefined;
523
+ } {
524
+ const rawPath = parseManifestPath(options.entry.path);
525
+ const explicitName = readNonEmptyString(options.entry.name);
526
+ const commandName = explicitName ?? commandNameFromManifestPath(rawPath.value);
527
+ const rawEntryGroup = options.entry.group;
528
+ const entryGroup =
529
+ rawEntryGroup === undefined ? options.packageGroup : readNonEmptyString(rawEntryGroup);
530
+ const segments =
531
+ rawPath.value === undefined
532
+ ? undefined
533
+ : [...(entryGroup === undefined ? [] : [entryGroup]), ...rawPath.value];
534
+ const packageGroupDescription = options.packageGroupDescription;
535
+ const shouldApplyPackageGroupDescription =
536
+ packageGroupDescription !== undefined &&
537
+ (entryGroup !== undefined || (segments?.length ?? 0) > 1);
538
+ const fields: ParsedManifestCommandEntryFields = {
539
+ group: entryGroup,
540
+ name: commandName,
541
+ segments,
542
+ ...(shouldApplyPackageGroupDescription ? { groupDescription: packageGroupDescription } : {}),
543
+ description: undefined,
544
+ entry: undefined,
545
+ fullDescription: undefined,
546
+ };
547
+ const diagnostics: ExtensionDiscoveryDiagnostic[] = [...rawPath.diagnostics];
548
+ const entryGroupDiagnostic = groupDiagnostic({
549
+ group: rawEntryGroup,
550
+ packageJsonPath: options.packageJsonPath,
551
+ commandName,
552
+ });
553
+ if (entryGroupDiagnostic !== undefined) diagnostics.push(entryGroupDiagnostic);
554
+
555
+ if (explicitName !== undefined) {
556
+ fields.name = explicitName;
557
+ } else if (rawPath.value === undefined || options.entry.name !== undefined) {
558
+ pushManifestStringDiagnostic({
559
+ diagnostics,
560
+ code: "extension_manifest_command_name_missing",
561
+ field: "name",
562
+ packageJsonPath: options.packageJsonPath,
563
+ commandName,
564
+ });
565
+ }
566
+
567
+ for (const { field, code } of requiredManifestCommandStringFields) {
568
+ const value = readNonEmptyString(options.entry[field]);
569
+ if (value !== undefined) {
570
+ fields[field] = value;
571
+ continue;
572
+ }
573
+ pushManifestStringDiagnostic({
574
+ diagnostics,
575
+ code,
576
+ field,
577
+ packageJsonPath: options.packageJsonPath,
578
+ commandName,
579
+ });
580
+ }
581
+
582
+ const fullDescription = readNonEmptyString(options.entry.fullDescription);
583
+ if (fullDescription !== undefined) {
584
+ fields.fullDescription = fullDescription;
585
+ } else if (options.entry.fullDescription === undefined) {
586
+ fields.fullDescription = fields.description;
587
+ } else {
588
+ pushManifestStringDiagnostic({
589
+ diagnostics,
590
+ code: "extension_manifest_command_full_description_invalid",
591
+ field: "fullDescription",
592
+ packageJsonPath: options.packageJsonPath,
593
+ commandName,
594
+ });
595
+ }
596
+
597
+ return { entry: fields, diagnostics, commandName };
598
+ }
599
+
600
+ function pushManifestStringDiagnostic(options: {
601
+ diagnostics: ExtensionDiscoveryDiagnostic[];
602
+ code: string;
603
+ field: string;
604
+ packageJsonPath: string;
605
+ commandName: string | undefined;
606
+ }): void {
607
+ options.diagnostics.push(
608
+ diagnostic(
609
+ options.code,
610
+ `Extension manifest command ${options.field} must be a non-empty string: ${options.packageJsonPath}.`,
611
+ diagnosticLocation(options.packageJsonPath, options.commandName),
612
+ ),
613
+ );
614
+ }
615
+
616
+ function groupDiagnostic(options: {
617
+ group: unknown;
618
+ packageJsonPath: string;
619
+ commandName: string | undefined;
620
+ }): ExtensionDiscoveryDiagnostic | undefined {
621
+ if (options.group === undefined) return undefined;
622
+ const group = readNonEmptyString(options.group);
623
+ if (group !== undefined && NS_COMMAND_NAME_PATTERN.test(group)) return undefined;
624
+ return diagnostic(
625
+ "extension_manifest_command_group_invalid",
626
+ `Extension manifest command group must match ${NS_COMMAND_NAME_RULE}.`,
627
+ diagnosticLocation(options.packageJsonPath, options.commandName),
628
+ );
629
+ }
630
+
631
+ function parseManifestPath(value: unknown): {
632
+ value: readonly string[] | undefined;
633
+ diagnostics: readonly ExtensionDiscoveryDiagnostic[];
634
+ } {
635
+ if (value === undefined) return { value: undefined, diagnostics: [] };
636
+ if (!Array.isArray(value) || value.length === 0) {
637
+ return {
638
+ value: undefined,
639
+ diagnostics: [
640
+ diagnostic(
641
+ "extension_manifest_command_path_invalid",
642
+ `Extension manifest command path must be a non-empty array of segments matching ${NS_COMMAND_NAME_RULE}.`,
643
+ ),
644
+ ],
645
+ };
646
+ }
647
+ const segments = value.filter((segment): segment is string => typeof segment === "string");
648
+ if (
649
+ segments.length !== value.length ||
650
+ segments.some((segment) => !NS_COMMAND_NAME_PATTERN.test(segment))
651
+ ) {
652
+ return {
653
+ value: undefined,
654
+ diagnostics: [
655
+ diagnostic(
656
+ "extension_manifest_command_path_invalid",
657
+ `Extension manifest command path segments must match ${NS_COMMAND_NAME_RULE}.`,
658
+ ),
659
+ ],
660
+ };
661
+ }
662
+ return { value: segments, diagnostics: [] };
663
+ }
664
+
665
+ function readNonEmptyString(value: unknown): string | undefined {
666
+ return typeof value === "string" && value.trim() !== "" ? value : undefined;
667
+ }
668
+
669
+ function buildCommand(options: {
670
+ name: string;
671
+ entryPath: string;
672
+ rootDir: string;
673
+ }): DiscoveredExtensionCommand {
674
+ const description = `Run ns command entry '${options.name}'.`;
675
+ return {
676
+ hasStaticCommandInfo: false,
677
+ ...moduleReferenceForEntryPath(options.entryPath),
678
+ name: options.name,
679
+ description,
680
+ fullDescription: description,
681
+ entryPath: options.entryPath,
682
+ displayPath: relativeDisplayPath(options.rootDir, options.entryPath),
683
+ };
684
+ }
685
+
686
+ function moduleReferenceForEntryPath(entryPath: string): {
687
+ moduleReference?: NsCommandModuleReference;
688
+ } {
689
+ let source: string;
690
+ try {
691
+ source = readFileSync(entryPath, "utf8");
692
+ } catch {
693
+ // Entry-file package-shim recognition is best-effort at discovery time; unreadable
694
+ // entries stay as file references and selected loading reports the concrete failure.
695
+ return {};
696
+ }
697
+ const specifier = parseDefaultReexportShimSpecifier(source);
698
+ return optionalEntry(
699
+ "moduleReference",
700
+ specifier === undefined ? undefined : { type: "package", specifier },
701
+ );
702
+ }
703
+
704
+ function parseDefaultReexportShimSpecifier(source: string): string | undefined {
705
+ const match = source.trim().match(/^export\s+\{\s*default\s*\}\s+from\s+["']([^"']+)["'];?$/u);
706
+ return match?.[1];
707
+ }
708
+
709
+ function isLoadableExtensionFile(name: string): boolean {
710
+ if (name.endsWith(".d.ts")) return false;
711
+ const extension = extname(name);
712
+ return extension === ".ts" || extension === ".js";
713
+ }
714
+
715
+ function relativeDisplayPath(rootDir: string, entryPath: string): string {
716
+ return join(basename(rootDir), relative(rootDir, entryPath));
717
+ }
718
+
719
+ function commandSortKey(command: DiscoveredExtensionCommand): string {
720
+ return command.displayPath;
721
+ }
722
+
723
+ function diagnosticLocation(
724
+ path: string,
725
+ commandName: string | undefined,
726
+ ): { path: string; commandName?: string } {
727
+ return {
728
+ path,
729
+ ...optionalEntry("commandName", commandName),
730
+ };
731
+ }
732
+
733
+ function diagnostic(
734
+ code: string,
735
+ message: string,
736
+ options: { path?: string; commandName?: string } = {},
737
+ ): ExtensionDiscoveryDiagnostic {
738
+ return makeKernelDiagnostic({
739
+ code,
740
+ message,
741
+ ...optionalEntry("path", options.path),
742
+ extra: optionalEntry("commandName", options.commandName),
743
+ });
744
+ }