@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.
- package/README.md +130 -0
- package/package.json +37 -0
- package/src/cli/completion.ts +30 -0
- package/src/cli/context.ts +110 -0
- package/src/cli/index.ts +668 -0
- package/src/cli/shell.ts +93 -0
- package/src/extensions/built-in-extension-commands.ts +306 -0
- package/src/extensions/command-registry.ts +342 -0
- package/src/extensions/discovery.ts +744 -0
- package/src/extensions/loader.ts +49 -0
- package/src/extensions/module-reference.ts +54 -0
- package/src/extensions/registry.ts +700 -0
- package/src/extensions/repo-local-catalog.ts +31 -0
- package/src/extensions/zod-issue-path.ts +65 -0
- package/src/project-config/points.ts +920 -0
- package/src/runtime/command-io.ts +149 -0
- package/src/runtime/diagnostics.ts +35 -0
- package/src/runtime/extension-root-discovery.ts +118 -0
- package/src/runtime/module-import.ts +4 -0
- package/src/runtime/module-loader.ts +75 -0
- package/src/runtime/pi-text-generation.ts +162 -0
- package/src/sdk/command-name.ts +2 -0
- package/src/sdk/command.ts +109 -0
- package/src/sdk/execution.ts +67 -0
- package/src/sdk/extension-manifest.ts +38 -0
- package/src/sdk/index.ts +73 -0
- package/src/sdk/progress-phase-state.ts +352 -0
- package/src/sdk/repo-local-ns-extension.ts +41 -0
- package/src/sdk/result.ts +11 -0
- package/src/sdk/schema.ts +3 -0
- package/src/sdk/services.ts +68 -0
- package/src/sdk/text-generation.ts +21 -0
|
@@ -0,0 +1,920 @@
|
|
|
1
|
+
import { existsSync, readFileSync } from "node:fs";
|
|
2
|
+
import { dirname, join, resolve } from "node:path";
|
|
3
|
+
|
|
4
|
+
import { formatErrorMessage, isPathInside, optionalEntry } from "@nseng-ai/foundation/primitives";
|
|
5
|
+
import { parse } from "smol-toml";
|
|
6
|
+
import { z, type ZodType } from "zod";
|
|
7
|
+
|
|
8
|
+
import { makeKernelDiagnostic } from "../runtime/diagnostics.ts";
|
|
9
|
+
import { scanExtensionRoot } from "../runtime/extension-root-discovery.ts";
|
|
10
|
+
import { NS_COMMAND_NAME_PATTERN } from "../sdk/command-name.ts";
|
|
11
|
+
import {
|
|
12
|
+
nsExtensionManifestPointSchema,
|
|
13
|
+
nsExtensionPackageManifestSchema,
|
|
14
|
+
nsExtensionPointAcceptsValues,
|
|
15
|
+
nsExtensionPointSemanticsValues,
|
|
16
|
+
} from "../sdk/extension-manifest.ts";
|
|
17
|
+
|
|
18
|
+
export type PointAccepts = (typeof nsExtensionPointAcceptsValues)[number];
|
|
19
|
+
export type PointSemantics = (typeof nsExtensionPointSemanticsValues)[number];
|
|
20
|
+
|
|
21
|
+
export interface PointDefinition {
|
|
22
|
+
id: string;
|
|
23
|
+
accepts: PointAccepts;
|
|
24
|
+
semantics: PointSemantics;
|
|
25
|
+
description?: string;
|
|
26
|
+
defaultPath?: string;
|
|
27
|
+
manifestPath?: string;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export interface SettingsSchema<T = unknown> {
|
|
31
|
+
path: readonly [string, ...string[]];
|
|
32
|
+
schema: ZodType<T>;
|
|
33
|
+
invalidMessage?: (context: { pathLabel: string }) => string;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
export interface ProjectConfigGateway {
|
|
37
|
+
readTextFile: (request: { repoRoot: string; relativePath: string }) => ProjectConfigReadResult;
|
|
38
|
+
pathExists: (request: {
|
|
39
|
+
repoRoot: string;
|
|
40
|
+
relativePath: string;
|
|
41
|
+
}) => ProjectConfigPathExistsResult;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
type ProjectConfigProbeResult<T> = T | { type: "missing" } | { type: "error"; message: string };
|
|
45
|
+
|
|
46
|
+
export type ProjectConfigReadResult = ProjectConfigProbeResult<{ type: "found"; text: string }>;
|
|
47
|
+
|
|
48
|
+
export type ProjectConfigPathExistsResult = ProjectConfigProbeResult<{ type: "present" }>;
|
|
49
|
+
|
|
50
|
+
export interface ProjectConfigDiagnostic {
|
|
51
|
+
severity: "error" | "info";
|
|
52
|
+
code: string;
|
|
53
|
+
message: string;
|
|
54
|
+
path?: string;
|
|
55
|
+
causeMessage?: string;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
export interface ProjectConfigDiagnosticErrorMapping<TCode extends string> {
|
|
59
|
+
invalidToml: TCode;
|
|
60
|
+
invalidSettingsByPath?: Readonly<Record<string, TCode>>;
|
|
61
|
+
defaultCode: TCode;
|
|
62
|
+
defaultMessage?: string;
|
|
63
|
+
pathLabel?: string;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
export interface ProjectConfigMappedError<TCode extends string> {
|
|
67
|
+
code: TCode;
|
|
68
|
+
message: string;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
export function primaryProjectConfigDiagnostic(
|
|
72
|
+
diagnostics: readonly ProjectConfigDiagnostic[],
|
|
73
|
+
): ProjectConfigDiagnostic | undefined {
|
|
74
|
+
return diagnostics.find((candidate) => candidate.severity === "error") ?? diagnostics[0];
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
export function projectConfigErrorFromDiagnostics<TCode extends string>(
|
|
78
|
+
diagnostics: readonly ProjectConfigDiagnostic[],
|
|
79
|
+
mapping: ProjectConfigDiagnosticErrorMapping<TCode>,
|
|
80
|
+
): ProjectConfigMappedError<TCode> {
|
|
81
|
+
const diagnostic = primaryProjectConfigDiagnostic(diagnostics);
|
|
82
|
+
if (diagnostic?.code === "ns_toml_invalid") {
|
|
83
|
+
return {
|
|
84
|
+
code: mapping.invalidToml,
|
|
85
|
+
message: formatProjectConfigInvalidTomlMessage(diagnostic, mapping.pathLabel),
|
|
86
|
+
};
|
|
87
|
+
}
|
|
88
|
+
if (diagnostic?.code === "settings_table_invalid" && diagnostic.path !== undefined) {
|
|
89
|
+
const settingsCode = projectConfigSettingsCode(mapping.invalidSettingsByPath, diagnostic.path);
|
|
90
|
+
return {
|
|
91
|
+
code: settingsCode ?? mapping.defaultCode,
|
|
92
|
+
message: diagnostic.message,
|
|
93
|
+
};
|
|
94
|
+
}
|
|
95
|
+
return {
|
|
96
|
+
code: mapping.defaultCode,
|
|
97
|
+
message: diagnostic?.message ?? mapping.defaultMessage ?? "invalid ns.toml",
|
|
98
|
+
};
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
function projectConfigSettingsCode<TCode extends string>(
|
|
102
|
+
codes: ProjectConfigDiagnosticErrorMapping<TCode>["invalidSettingsByPath"],
|
|
103
|
+
path: string,
|
|
104
|
+
): TCode | undefined {
|
|
105
|
+
if (codes === undefined) return undefined;
|
|
106
|
+
return codes[path];
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
function formatProjectConfigInvalidTomlMessage(
|
|
110
|
+
diagnostic: ProjectConfigDiagnostic,
|
|
111
|
+
pathLabel: string | undefined,
|
|
112
|
+
): string {
|
|
113
|
+
if (diagnostic.causeMessage === undefined) return diagnostic.message;
|
|
114
|
+
if (pathLabel !== undefined) return `Invalid TOML in ${pathLabel}: ${diagnostic.causeMessage}`;
|
|
115
|
+
return `Invalid TOML.\n${diagnostic.causeMessage}`;
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
export type ProjectPointInstallation =
|
|
119
|
+
| { pointId: string; accepts: "hook"; commands: readonly string[] }
|
|
120
|
+
| { pointId: string; accepts: "prompt"; path: string };
|
|
121
|
+
|
|
122
|
+
export interface LoadedProjectConfig {
|
|
123
|
+
points: readonly ProjectPointInstallation[];
|
|
124
|
+
settings: ReadonlyMap<string, unknown>;
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
export type ProjectConfigPointsTableMode =
|
|
128
|
+
| { mode: "validate"; pointDefinitions: readonly PointDefinition[] }
|
|
129
|
+
| { mode: "skip" };
|
|
130
|
+
|
|
131
|
+
export function getProjectConfigSetting<T>(
|
|
132
|
+
config: LoadedProjectConfig,
|
|
133
|
+
schema: SettingsSchema<T>,
|
|
134
|
+
): T | undefined {
|
|
135
|
+
return config.settings.get(schema.path.join(".")) as T | undefined;
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
export const emptyLoadedProjectConfig: LoadedProjectConfig = { points: [], settings: new Map() };
|
|
139
|
+
|
|
140
|
+
export type LoadProjectConfigResult =
|
|
141
|
+
| { ok: true; config: LoadedProjectConfig; diagnostics: readonly ProjectConfigDiagnostic[] }
|
|
142
|
+
| { ok: false; diagnostics: readonly ProjectConfigDiagnostic[]; config?: LoadedProjectConfig };
|
|
143
|
+
|
|
144
|
+
export interface PointDefinitionDiscoveryResult {
|
|
145
|
+
pointDefinitions: readonly PointDefinition[];
|
|
146
|
+
diagnostics: readonly ProjectConfigDiagnostic[];
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
export interface ResolvedPromptEnvOverride {
|
|
150
|
+
pointId: string;
|
|
151
|
+
envVar: string;
|
|
152
|
+
path: string;
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
export type PointCatalogInstallation =
|
|
156
|
+
| ({ source: "env-prompt" } & ResolvedPromptEnvOverride)
|
|
157
|
+
| { source: "ns.toml"; installation: ProjectPointInstallation }
|
|
158
|
+
| { source: "conventional-prompt"; pointId: string; path: string };
|
|
159
|
+
|
|
160
|
+
export interface PromptPointEnvOverride {
|
|
161
|
+
pointId: string;
|
|
162
|
+
envVar: string;
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
export interface PointCatalogEntry {
|
|
166
|
+
definition: PointDefinition;
|
|
167
|
+
installations: readonly PointCatalogInstallation[];
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
export interface PointCatalog {
|
|
171
|
+
entries: readonly PointCatalogEntry[];
|
|
172
|
+
diagnostics: readonly ProjectConfigDiagnostic[];
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
export type PromptPointSource =
|
|
176
|
+
| ({ type: "env" } & ResolvedPromptEnvOverride)
|
|
177
|
+
| { type: "ns.toml"; pointId: string; path: string }
|
|
178
|
+
| { type: "conventional"; pointId: string; path: string }
|
|
179
|
+
| { type: "default"; pointId: string; path: string; manifestPath: string }
|
|
180
|
+
| { type: "missing"; pointId: string };
|
|
181
|
+
|
|
182
|
+
function tryProjectConfigProbe<T>(
|
|
183
|
+
probe: () => ProjectConfigProbeResult<T>,
|
|
184
|
+
): ProjectConfigProbeResult<T> {
|
|
185
|
+
try {
|
|
186
|
+
return probe();
|
|
187
|
+
} catch (error) {
|
|
188
|
+
if (isNodeFileNotFound(error)) return { type: "missing" };
|
|
189
|
+
return { type: "error", message: formatErrorMessage(error) };
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
export const nodeProjectConfigGateway: ProjectConfigGateway = {
|
|
194
|
+
readTextFile(request) {
|
|
195
|
+
return tryProjectConfigProbe(() => ({
|
|
196
|
+
type: "found",
|
|
197
|
+
text: readFileSync(join(request.repoRoot, request.relativePath), "utf8"),
|
|
198
|
+
}));
|
|
199
|
+
},
|
|
200
|
+
pathExists(request) {
|
|
201
|
+
return tryProjectConfigProbe(() =>
|
|
202
|
+
existsSync(join(request.repoRoot, request.relativePath))
|
|
203
|
+
? { type: "present" }
|
|
204
|
+
: { type: "missing" },
|
|
205
|
+
);
|
|
206
|
+
},
|
|
207
|
+
};
|
|
208
|
+
|
|
209
|
+
export function loadProjectConfig(request: {
|
|
210
|
+
repoRoot: string;
|
|
211
|
+
gateway: ProjectConfigGateway;
|
|
212
|
+
pointDefinitions: readonly PointDefinition[];
|
|
213
|
+
settingsSchemas?: readonly SettingsSchema[];
|
|
214
|
+
}): LoadProjectConfigResult {
|
|
215
|
+
const readResult = request.gateway.readTextFile({
|
|
216
|
+
repoRoot: request.repoRoot,
|
|
217
|
+
relativePath: "ns.toml",
|
|
218
|
+
});
|
|
219
|
+
if (readResult.type === "missing") {
|
|
220
|
+
return {
|
|
221
|
+
ok: true,
|
|
222
|
+
config: emptyLoadedProjectConfig,
|
|
223
|
+
diagnostics: [],
|
|
224
|
+
};
|
|
225
|
+
}
|
|
226
|
+
if (readResult.type === "error") {
|
|
227
|
+
return {
|
|
228
|
+
ok: false,
|
|
229
|
+
diagnostics: [
|
|
230
|
+
diagnostic("ns_toml_read_failed", `Failed to read ns.toml: ${readResult.message}`),
|
|
231
|
+
],
|
|
232
|
+
};
|
|
233
|
+
}
|
|
234
|
+
return parseProjectConfigToml(readResult.text, {
|
|
235
|
+
pathLabel: "ns.toml",
|
|
236
|
+
pointsTable: { mode: "validate", pointDefinitions: request.pointDefinitions },
|
|
237
|
+
settingsSchemas: request.settingsSchemas ?? [],
|
|
238
|
+
});
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
export function parseProjectConfigToml(
|
|
242
|
+
source: string,
|
|
243
|
+
request: {
|
|
244
|
+
pathLabel?: string;
|
|
245
|
+
pointsTable: ProjectConfigPointsTableMode;
|
|
246
|
+
settingsSchemas?: readonly SettingsSchema[];
|
|
247
|
+
},
|
|
248
|
+
): LoadProjectConfigResult {
|
|
249
|
+
const pathLabel = request.pathLabel ?? "ns.toml";
|
|
250
|
+
let parsed: unknown;
|
|
251
|
+
try {
|
|
252
|
+
parsed = parse(source);
|
|
253
|
+
} catch (error) {
|
|
254
|
+
const causeMessage = formatErrorMessage(error);
|
|
255
|
+
return {
|
|
256
|
+
ok: false,
|
|
257
|
+
diagnostics: [
|
|
258
|
+
diagnostic("ns_toml_invalid", `${pathLabel}: Invalid TOML.\n${causeMessage}`, {
|
|
259
|
+
causeMessage,
|
|
260
|
+
}),
|
|
261
|
+
],
|
|
262
|
+
};
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
const documentResult = tomlDocumentSchema.safeParse(parsed);
|
|
266
|
+
if (!documentResult.success) {
|
|
267
|
+
return {
|
|
268
|
+
ok: false,
|
|
269
|
+
diagnostics: [
|
|
270
|
+
diagnostic("ns_toml_invalid", `${pathLabel}: top-level TOML document must be a table.`),
|
|
271
|
+
],
|
|
272
|
+
};
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
const document = documentResult.data;
|
|
276
|
+
const pointsResult =
|
|
277
|
+
request.pointsTable.mode === "skip"
|
|
278
|
+
? { installations: [], diagnostics: [] }
|
|
279
|
+
: parsePointsTable({
|
|
280
|
+
pathLabel,
|
|
281
|
+
value: document["points"],
|
|
282
|
+
pointDefinitions: request.pointsTable.pointDefinitions,
|
|
283
|
+
});
|
|
284
|
+
const settingsResult = parseDeclaredSettings({
|
|
285
|
+
pathLabel,
|
|
286
|
+
document,
|
|
287
|
+
settingsSchemas: request.settingsSchemas ?? [],
|
|
288
|
+
});
|
|
289
|
+
const diagnostics = [...pointsResult.diagnostics, ...settingsResult.diagnostics];
|
|
290
|
+
|
|
291
|
+
const config = { points: pointsResult.installations, settings: settingsResult.settings };
|
|
292
|
+
if (diagnostics.length > 0) return { ok: false, config, diagnostics };
|
|
293
|
+
return { ok: true, config, diagnostics: [] };
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
export function discoverPointDefinitionsInRoot(rootDir: string): PointDefinitionDiscoveryResult {
|
|
297
|
+
const rootScan = scanExtensionRoot(rootDir);
|
|
298
|
+
if (rootScan.diagnostics.length > 0) {
|
|
299
|
+
return { pointDefinitions: [], diagnostics: rootScan.diagnostics };
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
const pointDefinitions: PointDefinition[] = [];
|
|
303
|
+
const diagnostics: ProjectConfigDiagnostic[] = [];
|
|
304
|
+
for (const entry of rootScan.entries) {
|
|
305
|
+
if (entry.type !== "directory" || entry.packageJsonPath === undefined) continue;
|
|
306
|
+
const packageResult = discoverPackagePointDefinitions(entry.packageJsonPath);
|
|
307
|
+
pointDefinitions.push(...packageResult.pointDefinitions);
|
|
308
|
+
diagnostics.push(...packageResult.diagnostics);
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
return { pointDefinitions, diagnostics };
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
export function loadPointCatalog(request: {
|
|
315
|
+
repoRoot: string;
|
|
316
|
+
gateway: ProjectConfigGateway;
|
|
317
|
+
pointDefinitions?: readonly PointDefinition[];
|
|
318
|
+
extensionRoot?: string;
|
|
319
|
+
settingsSchemas?: readonly SettingsSchema[];
|
|
320
|
+
promptEnvOverride?: PromptPointEnvOverride;
|
|
321
|
+
env?: Record<string, string | undefined>;
|
|
322
|
+
}): PointCatalog {
|
|
323
|
+
const definitionResult =
|
|
324
|
+
request.pointDefinitions === undefined
|
|
325
|
+
? discoverPointDefinitionsInRoot(
|
|
326
|
+
join(request.repoRoot, request.extensionRoot ?? ".ns/extensions"),
|
|
327
|
+
)
|
|
328
|
+
: { pointDefinitions: request.pointDefinitions, diagnostics: [] };
|
|
329
|
+
const configResult = loadProjectConfig({
|
|
330
|
+
repoRoot: request.repoRoot,
|
|
331
|
+
gateway: request.gateway,
|
|
332
|
+
pointDefinitions: definitionResult.pointDefinitions,
|
|
333
|
+
settingsSchemas: request.settingsSchemas ?? [],
|
|
334
|
+
});
|
|
335
|
+
return buildPointCatalog({
|
|
336
|
+
repoRoot: request.repoRoot,
|
|
337
|
+
gateway: request.gateway,
|
|
338
|
+
pointDefinitions: definitionResult.pointDefinitions,
|
|
339
|
+
config: configResult.config ?? emptyLoadedProjectConfig,
|
|
340
|
+
diagnostics: [...definitionResult.diagnostics, ...configResult.diagnostics],
|
|
341
|
+
...optionalEntry("promptEnvOverride", request.promptEnvOverride),
|
|
342
|
+
env: request.env ?? {},
|
|
343
|
+
});
|
|
344
|
+
}
|
|
345
|
+
|
|
346
|
+
export function resolvePromptPointSource(
|
|
347
|
+
catalog: PointCatalog,
|
|
348
|
+
pointId: string,
|
|
349
|
+
): PromptPointSource {
|
|
350
|
+
const entry = catalog.entries.find((catalogEntry) => catalogEntry.definition.id === pointId);
|
|
351
|
+
if (entry === undefined || entry.definition.accepts !== "prompt")
|
|
352
|
+
return { type: "missing", pointId };
|
|
353
|
+
|
|
354
|
+
const envOverride = findCatalogInstallation(entry, "env-prompt");
|
|
355
|
+
if (envOverride !== undefined) {
|
|
356
|
+
return {
|
|
357
|
+
type: "env",
|
|
358
|
+
pointId,
|
|
359
|
+
envVar: envOverride.envVar,
|
|
360
|
+
path: envOverride.path,
|
|
361
|
+
};
|
|
362
|
+
}
|
|
363
|
+
|
|
364
|
+
const configured = findPromptConfigInstallation(entry);
|
|
365
|
+
if (configured !== undefined) {
|
|
366
|
+
return { type: "ns.toml", pointId, path: configured.installation.path };
|
|
367
|
+
}
|
|
368
|
+
|
|
369
|
+
const conventional = findCatalogInstallation(entry, "conventional-prompt");
|
|
370
|
+
if (conventional !== undefined) {
|
|
371
|
+
return { type: "conventional", pointId, path: conventional.path };
|
|
372
|
+
}
|
|
373
|
+
|
|
374
|
+
if (entry.definition.defaultPath !== undefined && entry.definition.manifestPath !== undefined) {
|
|
375
|
+
return {
|
|
376
|
+
type: "default",
|
|
377
|
+
pointId,
|
|
378
|
+
path: entry.definition.defaultPath,
|
|
379
|
+
manifestPath: entry.definition.manifestPath,
|
|
380
|
+
};
|
|
381
|
+
}
|
|
382
|
+
|
|
383
|
+
return { type: "missing", pointId };
|
|
384
|
+
}
|
|
385
|
+
|
|
386
|
+
export interface BuildPointCatalogRequest {
|
|
387
|
+
repoRoot: string;
|
|
388
|
+
gateway: Pick<ProjectConfigGateway, "pathExists">;
|
|
389
|
+
pointDefinitions: readonly PointDefinition[];
|
|
390
|
+
config: LoadedProjectConfig;
|
|
391
|
+
diagnostics?: readonly ProjectConfigDiagnostic[];
|
|
392
|
+
promptEnvOverride?: PromptPointEnvOverride;
|
|
393
|
+
env?: Record<string, string | undefined>;
|
|
394
|
+
}
|
|
395
|
+
|
|
396
|
+
export function buildPointCatalog(request: BuildPointCatalogRequest): PointCatalog {
|
|
397
|
+
const diagnostics: ProjectConfigDiagnostic[] = [...(request.diagnostics ?? [])];
|
|
398
|
+
const installationsByPoint = new Map<string, PointCatalogInstallation[]>();
|
|
399
|
+
for (const installation of request.config.points) {
|
|
400
|
+
const existing = installationsByPoint.get(installation.pointId) ?? [];
|
|
401
|
+
installationsByPoint.set(installation.pointId, [
|
|
402
|
+
...existing,
|
|
403
|
+
{ source: "ns.toml", installation },
|
|
404
|
+
]);
|
|
405
|
+
}
|
|
406
|
+
|
|
407
|
+
const entries: PointCatalogEntry[] = [];
|
|
408
|
+
for (const definition of [...request.pointDefinitions].sort((left, right) =>
|
|
409
|
+
left.id.localeCompare(right.id),
|
|
410
|
+
)) {
|
|
411
|
+
let installations = installationsByPoint.get(definition.id) ?? [];
|
|
412
|
+
if (definition.accepts === "prompt") {
|
|
413
|
+
const envOverride = findPromptEnvOverride({
|
|
414
|
+
pointId: definition.id,
|
|
415
|
+
env: request.env ?? {},
|
|
416
|
+
...optionalEntry("override", request.promptEnvOverride),
|
|
417
|
+
});
|
|
418
|
+
if (envOverride !== undefined) {
|
|
419
|
+
installations = [{ source: "env-prompt", ...envOverride }, ...installations];
|
|
420
|
+
diagnostics.push(
|
|
421
|
+
diagnostic(
|
|
422
|
+
"point_prompt_env_override_in_effect",
|
|
423
|
+
`Prompt point ${definition.id} is overridden by env var ${envOverride.envVar}.`,
|
|
424
|
+
{ path: definition.id, severity: "info" },
|
|
425
|
+
),
|
|
426
|
+
);
|
|
427
|
+
}
|
|
428
|
+
}
|
|
429
|
+
if (definition.accepts === "prompt" && installations.length === 0) {
|
|
430
|
+
const conventionalPath = `.ns/prompts/${definition.id}.md`;
|
|
431
|
+
const existsResult = request.gateway.pathExists({
|
|
432
|
+
repoRoot: request.repoRoot,
|
|
433
|
+
relativePath: conventionalPath,
|
|
434
|
+
});
|
|
435
|
+
if (existsResult.type === "present") {
|
|
436
|
+
installations = [
|
|
437
|
+
{ source: "conventional-prompt", pointId: definition.id, path: conventionalPath },
|
|
438
|
+
];
|
|
439
|
+
} else if (existsResult.type === "error") {
|
|
440
|
+
diagnostics.push(
|
|
441
|
+
diagnostic(
|
|
442
|
+
"point_conventional_prompt_probe_failed",
|
|
443
|
+
`Failed to inspect conventional prompt installation ${conventionalPath}: ${existsResult.message}`,
|
|
444
|
+
{ path: conventionalPath },
|
|
445
|
+
),
|
|
446
|
+
);
|
|
447
|
+
}
|
|
448
|
+
}
|
|
449
|
+
|
|
450
|
+
if (definition.semantics === "override" && installations.length > 0) {
|
|
451
|
+
diagnostics.push(
|
|
452
|
+
diagnostic(
|
|
453
|
+
"point_override_in_effect",
|
|
454
|
+
`Override point ${definition.id} has a repo installation in effect.`,
|
|
455
|
+
{ path: definition.id, severity: "info" },
|
|
456
|
+
),
|
|
457
|
+
);
|
|
458
|
+
} else if (installations.length === 0) {
|
|
459
|
+
diagnostics.push(
|
|
460
|
+
diagnostic(
|
|
461
|
+
"point_defined_uninstalled",
|
|
462
|
+
`Point ${definition.id} is defined but not installed in this repo.`,
|
|
463
|
+
{ path: definition.id, severity: "info" },
|
|
464
|
+
),
|
|
465
|
+
);
|
|
466
|
+
}
|
|
467
|
+
|
|
468
|
+
entries.push({ definition, installations });
|
|
469
|
+
}
|
|
470
|
+
|
|
471
|
+
return { entries, diagnostics };
|
|
472
|
+
}
|
|
473
|
+
|
|
474
|
+
export function hookCommandsForPoint(catalog: PointCatalog, pointId: string): readonly string[] {
|
|
475
|
+
const entry = catalog.entries.find((catalogEntry) => catalogEntry.definition.id === pointId);
|
|
476
|
+
if (entry === undefined) return [];
|
|
477
|
+
const installation = findCatalogInstallation(entry, "ns.toml");
|
|
478
|
+
if (installation?.installation.accepts !== "hook") return [];
|
|
479
|
+
return installation.installation.commands;
|
|
480
|
+
}
|
|
481
|
+
|
|
482
|
+
export function resolvePromptPointPath(
|
|
483
|
+
repoRoot: string,
|
|
484
|
+
source: Exclude<PromptPointSource, { type: "env" }>,
|
|
485
|
+
): { path: string; label: string } | undefined {
|
|
486
|
+
switch (source.type) {
|
|
487
|
+
case "ns.toml":
|
|
488
|
+
return { path: join(repoRoot, source.path), label: `ns.toml prompt ${source.path}` };
|
|
489
|
+
case "conventional":
|
|
490
|
+
return { path: join(repoRoot, source.path), label: source.path };
|
|
491
|
+
case "default":
|
|
492
|
+
return {
|
|
493
|
+
path: join(dirname(source.manifestPath), source.path),
|
|
494
|
+
label: `manifest default ${source.path}`,
|
|
495
|
+
};
|
|
496
|
+
case "missing":
|
|
497
|
+
return undefined;
|
|
498
|
+
}
|
|
499
|
+
}
|
|
500
|
+
|
|
501
|
+
function findCatalogInstallation<TSource extends PointCatalogInstallation["source"]>(
|
|
502
|
+
entry: PointCatalogEntry,
|
|
503
|
+
source: TSource,
|
|
504
|
+
): Extract<PointCatalogInstallation, { source: TSource }> | undefined {
|
|
505
|
+
return entry.installations.find(
|
|
506
|
+
(installation): installation is Extract<PointCatalogInstallation, { source: TSource }> =>
|
|
507
|
+
installation.source === source,
|
|
508
|
+
);
|
|
509
|
+
}
|
|
510
|
+
|
|
511
|
+
function findPromptConfigInstallation(entry: PointCatalogEntry):
|
|
512
|
+
| {
|
|
513
|
+
source: "ns.toml";
|
|
514
|
+
installation: Extract<ProjectPointInstallation, { accepts: "prompt" }>;
|
|
515
|
+
}
|
|
516
|
+
| undefined {
|
|
517
|
+
return entry.installations.find(
|
|
518
|
+
(
|
|
519
|
+
installation,
|
|
520
|
+
): installation is {
|
|
521
|
+
source: "ns.toml";
|
|
522
|
+
installation: Extract<ProjectPointInstallation, { accepts: "prompt" }>;
|
|
523
|
+
} => installation.source === "ns.toml" && installation.installation.accepts === "prompt",
|
|
524
|
+
);
|
|
525
|
+
}
|
|
526
|
+
|
|
527
|
+
function findPromptEnvOverride(request: {
|
|
528
|
+
pointId: string;
|
|
529
|
+
env: Record<string, string | undefined>;
|
|
530
|
+
override?: PromptPointEnvOverride;
|
|
531
|
+
}): ResolvedPromptEnvOverride | undefined {
|
|
532
|
+
if (request.override?.pointId !== request.pointId) return undefined;
|
|
533
|
+
const path = request.env[request.override.envVar]?.trim();
|
|
534
|
+
if (!path) return undefined;
|
|
535
|
+
return { pointId: request.pointId, envVar: request.override.envVar, path };
|
|
536
|
+
}
|
|
537
|
+
|
|
538
|
+
function parsePointsTable(request: {
|
|
539
|
+
pathLabel: string;
|
|
540
|
+
value: unknown;
|
|
541
|
+
pointDefinitions: readonly PointDefinition[];
|
|
542
|
+
}): {
|
|
543
|
+
installations: readonly ProjectPointInstallation[];
|
|
544
|
+
diagnostics: readonly ProjectConfigDiagnostic[];
|
|
545
|
+
} {
|
|
546
|
+
if (request.value === undefined) return { installations: [], diagnostics: [] };
|
|
547
|
+
const diagnostics: ProjectConfigDiagnostic[] = [];
|
|
548
|
+
const tableResult = recordSchema.safeParse(request.value);
|
|
549
|
+
if (!tableResult.success) {
|
|
550
|
+
diagnostics.push(
|
|
551
|
+
diagnostic("points_table_invalid", `${request.pathLabel}: [points] must be a TOML table.`, {
|
|
552
|
+
path: "points",
|
|
553
|
+
}),
|
|
554
|
+
);
|
|
555
|
+
return { installations: [], diagnostics };
|
|
556
|
+
}
|
|
557
|
+
|
|
558
|
+
const definitions = new Map(
|
|
559
|
+
request.pointDefinitions.map((definition) => [definition.id, definition]),
|
|
560
|
+
);
|
|
561
|
+
const installations: ProjectPointInstallation[] = [];
|
|
562
|
+
for (const [pointId, value] of Object.entries(tableResult.data)) {
|
|
563
|
+
const definition = definitions.get(pointId);
|
|
564
|
+
if (definition === undefined) {
|
|
565
|
+
diagnostics.push(
|
|
566
|
+
diagnostic(
|
|
567
|
+
"point_installation_undefined",
|
|
568
|
+
`${request.pathLabel}: [points].${JSON.stringify(pointId)} installs an undefined point.`,
|
|
569
|
+
{ path: `points.${pointId}` },
|
|
570
|
+
),
|
|
571
|
+
);
|
|
572
|
+
continue;
|
|
573
|
+
}
|
|
574
|
+
const parsed = parsePointInstallation({
|
|
575
|
+
pathLabel: request.pathLabel,
|
|
576
|
+
pointId,
|
|
577
|
+
definition,
|
|
578
|
+
value,
|
|
579
|
+
});
|
|
580
|
+
if (parsed.ok) installations.push(parsed.installation);
|
|
581
|
+
else diagnostics.push(parsed.diagnostic);
|
|
582
|
+
}
|
|
583
|
+
return { installations, diagnostics };
|
|
584
|
+
}
|
|
585
|
+
|
|
586
|
+
function parsePointInstallation(request: {
|
|
587
|
+
pathLabel: string;
|
|
588
|
+
pointId: string;
|
|
589
|
+
definition: PointDefinition;
|
|
590
|
+
value: unknown;
|
|
591
|
+
}):
|
|
592
|
+
| { ok: true; installation: ProjectPointInstallation }
|
|
593
|
+
| { ok: false; diagnostic: ProjectConfigDiagnostic } {
|
|
594
|
+
if (request.definition.accepts === "hook") {
|
|
595
|
+
return parseInstallationValue({
|
|
596
|
+
...request,
|
|
597
|
+
schema: z.array(z.string()),
|
|
598
|
+
invalidMessage: `${request.pathLabel}: hook point ${request.pointId} must be an array of command strings.`,
|
|
599
|
+
buildInstallation: (commands) => ({
|
|
600
|
+
pointId: request.pointId,
|
|
601
|
+
accepts: "hook",
|
|
602
|
+
commands,
|
|
603
|
+
}),
|
|
604
|
+
});
|
|
605
|
+
}
|
|
606
|
+
|
|
607
|
+
return parseInstallationValue({
|
|
608
|
+
...request,
|
|
609
|
+
schema: z.string().min(1),
|
|
610
|
+
invalidMessage: `${request.pathLabel}: prompt point ${request.pointId} must be a non-empty path string.`,
|
|
611
|
+
buildInstallation: (path) => ({ pointId: request.pointId, accepts: "prompt", path }),
|
|
612
|
+
});
|
|
613
|
+
}
|
|
614
|
+
|
|
615
|
+
function parseInstallationValue<T>(request: {
|
|
616
|
+
pointId: string;
|
|
617
|
+
value: unknown;
|
|
618
|
+
schema: ZodType<T>;
|
|
619
|
+
invalidMessage: string;
|
|
620
|
+
buildInstallation: (value: T) => ProjectPointInstallation;
|
|
621
|
+
}):
|
|
622
|
+
| { ok: true; installation: ProjectPointInstallation }
|
|
623
|
+
| { ok: false; diagnostic: ProjectConfigDiagnostic } {
|
|
624
|
+
const valueResult = request.schema.safeParse(request.value);
|
|
625
|
+
if (!valueResult.success) {
|
|
626
|
+
return {
|
|
627
|
+
ok: false,
|
|
628
|
+
diagnostic: diagnostic("point_installation_invalid", request.invalidMessage, {
|
|
629
|
+
path: `points.${request.pointId}`,
|
|
630
|
+
}),
|
|
631
|
+
};
|
|
632
|
+
}
|
|
633
|
+
return { ok: true, installation: request.buildInstallation(valueResult.data) };
|
|
634
|
+
}
|
|
635
|
+
|
|
636
|
+
function parseDeclaredSettings(request: {
|
|
637
|
+
pathLabel: string;
|
|
638
|
+
document: Record<string, unknown>;
|
|
639
|
+
settingsSchemas: readonly SettingsSchema[];
|
|
640
|
+
}): {
|
|
641
|
+
settings: ReadonlyMap<string, unknown>;
|
|
642
|
+
diagnostics: readonly ProjectConfigDiagnostic[];
|
|
643
|
+
} {
|
|
644
|
+
const settings = new Map<string, unknown>();
|
|
645
|
+
const diagnostics: ProjectConfigDiagnostic[] = [];
|
|
646
|
+
for (const setting of request.settingsSchemas) {
|
|
647
|
+
const settingValue = valueAtPath(request.document, setting.path);
|
|
648
|
+
if (settingValue === undefined) continue;
|
|
649
|
+
const schemaResult = setting.schema.safeParse(settingValue);
|
|
650
|
+
const key = setting.path.join(".");
|
|
651
|
+
if (!schemaResult.success) {
|
|
652
|
+
const message =
|
|
653
|
+
setting.invalidMessage?.({ pathLabel: request.pathLabel }) ??
|
|
654
|
+
`${request.pathLabel}: [${key}] does not match its declared settings schema.`;
|
|
655
|
+
diagnostics.push(diagnostic("settings_table_invalid", message, { path: key }));
|
|
656
|
+
continue;
|
|
657
|
+
}
|
|
658
|
+
settings.set(key, schemaResult.data);
|
|
659
|
+
}
|
|
660
|
+
return { settings, diagnostics };
|
|
661
|
+
}
|
|
662
|
+
|
|
663
|
+
function valueAtPath(
|
|
664
|
+
document: Record<string, unknown>,
|
|
665
|
+
path: readonly [string, ...string[]],
|
|
666
|
+
): unknown {
|
|
667
|
+
let current: unknown = document;
|
|
668
|
+
for (const segment of path) {
|
|
669
|
+
const tableResult = recordSchema.safeParse(current);
|
|
670
|
+
if (!tableResult.success) return undefined;
|
|
671
|
+
current = tableResult.data[segment];
|
|
672
|
+
if (current === undefined) return undefined;
|
|
673
|
+
}
|
|
674
|
+
return current;
|
|
675
|
+
}
|
|
676
|
+
|
|
677
|
+
function discoverPackagePointDefinitions(packageJsonPath: string): PointDefinitionDiscoveryResult {
|
|
678
|
+
let parsed: unknown;
|
|
679
|
+
try {
|
|
680
|
+
parsed = JSON.parse(readFileSync(packageJsonPath, "utf8"));
|
|
681
|
+
} catch (error) {
|
|
682
|
+
return {
|
|
683
|
+
pointDefinitions: [],
|
|
684
|
+
diagnostics: [
|
|
685
|
+
diagnostic(
|
|
686
|
+
"extension_manifest_parse_failed",
|
|
687
|
+
`Could not parse extension manifest ${packageJsonPath}.\n${formatErrorMessage(error)}`,
|
|
688
|
+
{ path: packageJsonPath },
|
|
689
|
+
),
|
|
690
|
+
],
|
|
691
|
+
};
|
|
692
|
+
}
|
|
693
|
+
|
|
694
|
+
const manifestResult = nsExtensionPackageManifestSchema.safeParse(parsed);
|
|
695
|
+
if (!manifestResult.success) {
|
|
696
|
+
return {
|
|
697
|
+
pointDefinitions: [],
|
|
698
|
+
diagnostics: [
|
|
699
|
+
diagnostic(
|
|
700
|
+
"extension_manifest_invalid",
|
|
701
|
+
`Extension manifest contains invalid ns metadata: ${packageJsonPath}.`,
|
|
702
|
+
{ path: packageJsonPath },
|
|
703
|
+
),
|
|
704
|
+
],
|
|
705
|
+
};
|
|
706
|
+
}
|
|
707
|
+
|
|
708
|
+
const manifest = manifestResult.data;
|
|
709
|
+
if (manifest.ns?.points === undefined) return { pointDefinitions: [], diagnostics: [] };
|
|
710
|
+
const group = readManifestNameSegment(manifest.ns.group);
|
|
711
|
+
if (group === undefined) {
|
|
712
|
+
return {
|
|
713
|
+
pointDefinitions: [],
|
|
714
|
+
diagnostics: [
|
|
715
|
+
diagnostic(
|
|
716
|
+
"extension_manifest_point_group_invalid",
|
|
717
|
+
`Extension manifest point group must be a non-empty segment: ${packageJsonPath}.`,
|
|
718
|
+
{ path: packageJsonPath },
|
|
719
|
+
),
|
|
720
|
+
],
|
|
721
|
+
};
|
|
722
|
+
}
|
|
723
|
+
|
|
724
|
+
const pointDefinitions: PointDefinition[] = [];
|
|
725
|
+
const diagnostics: ProjectConfigDiagnostic[] = [];
|
|
726
|
+
const location = { packageJsonPath, packageDir: dirname(packageJsonPath) };
|
|
727
|
+
for (const point of manifest.ns.points) {
|
|
728
|
+
const pointResult = pointDefinitionFromManifestPoint({
|
|
729
|
+
group,
|
|
730
|
+
location,
|
|
731
|
+
point,
|
|
732
|
+
});
|
|
733
|
+
if (pointResult.ok) pointDefinitions.push(pointResult.definition);
|
|
734
|
+
else diagnostics.push(...pointResult.diagnostics);
|
|
735
|
+
}
|
|
736
|
+
return { pointDefinitions, diagnostics };
|
|
737
|
+
}
|
|
738
|
+
|
|
739
|
+
interface PackageManifestLocation {
|
|
740
|
+
packageJsonPath: string;
|
|
741
|
+
packageDir: string;
|
|
742
|
+
}
|
|
743
|
+
|
|
744
|
+
function pointDefinitionFromManifestPoint(request: {
|
|
745
|
+
group: string;
|
|
746
|
+
location: PackageManifestLocation;
|
|
747
|
+
point: unknown;
|
|
748
|
+
}):
|
|
749
|
+
| { ok: true; definition: PointDefinition }
|
|
750
|
+
| { ok: false; diagnostics: readonly ProjectConfigDiagnostic[] } {
|
|
751
|
+
const pointResult = nsExtensionManifestPointSchema.safeParse(request.point);
|
|
752
|
+
if (!pointResult.success) {
|
|
753
|
+
return {
|
|
754
|
+
ok: false,
|
|
755
|
+
diagnostics: [
|
|
756
|
+
diagnostic(
|
|
757
|
+
"extension_manifest_point_invalid",
|
|
758
|
+
`Extension manifest points must be objects with supported known fields: ${request.location.packageJsonPath}.`,
|
|
759
|
+
{ path: request.location.packageJsonPath },
|
|
760
|
+
),
|
|
761
|
+
],
|
|
762
|
+
};
|
|
763
|
+
}
|
|
764
|
+
|
|
765
|
+
const point = pointResult.data;
|
|
766
|
+
const path = parsePointManifestPath(point.path);
|
|
767
|
+
const diagnostics: ProjectConfigDiagnostic[] = [...path.diagnostics];
|
|
768
|
+
if (point.accepts === undefined) {
|
|
769
|
+
diagnostics.push(manifestPointFieldDiagnostic("accepts", request.location.packageJsonPath));
|
|
770
|
+
}
|
|
771
|
+
if (point.semantics === undefined) {
|
|
772
|
+
diagnostics.push(manifestPointFieldDiagnostic("semantics", request.location.packageJsonPath));
|
|
773
|
+
}
|
|
774
|
+
|
|
775
|
+
let defaultPath: string | undefined;
|
|
776
|
+
if (point.default !== undefined) {
|
|
777
|
+
const defaultValidation = validateManifestRelativePath({
|
|
778
|
+
location: request.location,
|
|
779
|
+
rawPath: point.default,
|
|
780
|
+
});
|
|
781
|
+
if (defaultValidation.ok) defaultPath = point.default;
|
|
782
|
+
else diagnostics.push(defaultValidation.diagnostic);
|
|
783
|
+
}
|
|
784
|
+
|
|
785
|
+
if (
|
|
786
|
+
diagnostics.length > 0 ||
|
|
787
|
+
path.value === undefined ||
|
|
788
|
+
point.accepts === undefined ||
|
|
789
|
+
point.semantics === undefined
|
|
790
|
+
) {
|
|
791
|
+
return { ok: false, diagnostics };
|
|
792
|
+
}
|
|
793
|
+
|
|
794
|
+
const description = readNonEmptyString(point.description);
|
|
795
|
+
return {
|
|
796
|
+
ok: true,
|
|
797
|
+
definition: {
|
|
798
|
+
id: [request.group, ...path.value].join("."),
|
|
799
|
+
accepts: point.accepts,
|
|
800
|
+
semantics: point.semantics,
|
|
801
|
+
...(description === undefined ? {} : { description }),
|
|
802
|
+
...(defaultPath === undefined ? {} : { defaultPath }),
|
|
803
|
+
manifestPath: request.location.packageJsonPath,
|
|
804
|
+
},
|
|
805
|
+
};
|
|
806
|
+
}
|
|
807
|
+
|
|
808
|
+
function parsePointManifestPath(value: unknown): {
|
|
809
|
+
value: readonly string[] | undefined;
|
|
810
|
+
diagnostics: readonly ProjectConfigDiagnostic[];
|
|
811
|
+
} {
|
|
812
|
+
if (!Array.isArray(value) || value.length === 0) {
|
|
813
|
+
return {
|
|
814
|
+
value: undefined,
|
|
815
|
+
diagnostics: [manifestPointFieldDiagnostic("path", undefined)],
|
|
816
|
+
};
|
|
817
|
+
}
|
|
818
|
+
const segments = value.filter((segment): segment is string => typeof segment === "string");
|
|
819
|
+
if (
|
|
820
|
+
segments.length !== value.length ||
|
|
821
|
+
segments.some((segment) => readManifestNameSegment(segment) === undefined)
|
|
822
|
+
) {
|
|
823
|
+
return {
|
|
824
|
+
value: undefined,
|
|
825
|
+
diagnostics: [manifestPointFieldDiagnostic("path", undefined)],
|
|
826
|
+
};
|
|
827
|
+
}
|
|
828
|
+
return { value: segments, diagnostics: [] };
|
|
829
|
+
}
|
|
830
|
+
|
|
831
|
+
function validateManifestRelativePath(request: {
|
|
832
|
+
location: PackageManifestLocation;
|
|
833
|
+
rawPath: string;
|
|
834
|
+
}): { ok: true } | { ok: false; diagnostic: ProjectConfigDiagnostic } {
|
|
835
|
+
if (
|
|
836
|
+
request.rawPath.trim() === "" ||
|
|
837
|
+
request.rawPath.startsWith("/") ||
|
|
838
|
+
request.rawPath.includes("\\")
|
|
839
|
+
) {
|
|
840
|
+
return {
|
|
841
|
+
ok: false,
|
|
842
|
+
diagnostic: diagnostic(
|
|
843
|
+
"extension_manifest_point_default_not_relative",
|
|
844
|
+
`Extension manifest point default must be a relative POSIX-style path inside the package: ${request.rawPath}.`,
|
|
845
|
+
{ path: request.location.packageJsonPath },
|
|
846
|
+
),
|
|
847
|
+
};
|
|
848
|
+
}
|
|
849
|
+
if (!request.rawPath.endsWith(".md")) {
|
|
850
|
+
return {
|
|
851
|
+
ok: false,
|
|
852
|
+
diagnostic: diagnostic(
|
|
853
|
+
"extension_manifest_point_default_not_markdown",
|
|
854
|
+
`Extension manifest point default must be a markdown file path: ${request.rawPath}.`,
|
|
855
|
+
{ path: request.location.packageJsonPath },
|
|
856
|
+
),
|
|
857
|
+
};
|
|
858
|
+
}
|
|
859
|
+
const resolvedPath = resolve(request.location.packageDir, request.rawPath);
|
|
860
|
+
if (!isPathInside(request.location.packageDir, resolvedPath)) {
|
|
861
|
+
return {
|
|
862
|
+
ok: false,
|
|
863
|
+
diagnostic: diagnostic(
|
|
864
|
+
"extension_manifest_point_default_escapes",
|
|
865
|
+
`Extension manifest point default must not escape its package directory: ${request.rawPath}.`,
|
|
866
|
+
{ path: request.location.packageJsonPath },
|
|
867
|
+
),
|
|
868
|
+
};
|
|
869
|
+
}
|
|
870
|
+
return { ok: true };
|
|
871
|
+
}
|
|
872
|
+
|
|
873
|
+
function manifestPointFieldDiagnostic(
|
|
874
|
+
field: string,
|
|
875
|
+
packageJsonPath: string | undefined,
|
|
876
|
+
): ProjectConfigDiagnostic {
|
|
877
|
+
return diagnostic(
|
|
878
|
+
"extension_manifest_point_field_invalid",
|
|
879
|
+
`Extension manifest point ${field} is required and must match the point manifest schema.`,
|
|
880
|
+
optionalEntry("path", packageJsonPath),
|
|
881
|
+
);
|
|
882
|
+
}
|
|
883
|
+
|
|
884
|
+
function readManifestNameSegment(value: unknown): string | undefined {
|
|
885
|
+
return typeof value === "string" && NS_COMMAND_NAME_PATTERN.test(value) ? value : undefined;
|
|
886
|
+
}
|
|
887
|
+
|
|
888
|
+
function readNonEmptyString(value: unknown): string | undefined {
|
|
889
|
+
return typeof value === "string" && value.trim() !== "" ? value : undefined;
|
|
890
|
+
}
|
|
891
|
+
|
|
892
|
+
function diagnostic(
|
|
893
|
+
code: string,
|
|
894
|
+
message: string,
|
|
895
|
+
options: {
|
|
896
|
+
path?: string;
|
|
897
|
+
causeMessage?: string;
|
|
898
|
+
severity?: ProjectConfigDiagnostic["severity"];
|
|
899
|
+
} = {},
|
|
900
|
+
): ProjectConfigDiagnostic {
|
|
901
|
+
return makeKernelDiagnostic({
|
|
902
|
+
code,
|
|
903
|
+
message,
|
|
904
|
+
...optionalEntry("path", options.path),
|
|
905
|
+
extra: optionalEntry("causeMessage", options.causeMessage),
|
|
906
|
+
...optionalEntry("severity", options.severity),
|
|
907
|
+
});
|
|
908
|
+
}
|
|
909
|
+
|
|
910
|
+
function isNodeFileNotFound(error: unknown): boolean {
|
|
911
|
+
return (
|
|
912
|
+
typeof error === "object" &&
|
|
913
|
+
error !== null &&
|
|
914
|
+
"code" in error &&
|
|
915
|
+
(error as { code?: unknown }).code === "ENOENT"
|
|
916
|
+
);
|
|
917
|
+
}
|
|
918
|
+
|
|
919
|
+
const recordSchema = z.record(z.string(), z.unknown());
|
|
920
|
+
const tomlDocumentSchema = recordSchema;
|