@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
package/src/cli/index.ts
ADDED
|
@@ -0,0 +1,668 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
import { z } from "zod";
|
|
4
|
+
|
|
5
|
+
import {
|
|
6
|
+
ClinkrGroup,
|
|
7
|
+
clinkrFormatFromArgs,
|
|
8
|
+
ok,
|
|
9
|
+
renderCapabilitiesForTerminal,
|
|
10
|
+
type Caps,
|
|
11
|
+
type ClinkrCommandSpec,
|
|
12
|
+
type ClinkrDynamicCompletionRequest,
|
|
13
|
+
} from "@nseng-ai/clinkr";
|
|
14
|
+
import { renderCompletionCandidatesNewline } from "@nseng-ai/clinkr/completion";
|
|
15
|
+
import { rawCommand } from "@nseng-ai/clinkr/raw";
|
|
16
|
+
import { defineCli, readStdin, type CliEntrypointDeps } from "@nseng-ai/foundation/cli-runtime";
|
|
17
|
+
import { optionalEntries, optionalEntry, resolveHomeDir } from "@nseng-ai/foundation/primitives";
|
|
18
|
+
|
|
19
|
+
import {
|
|
20
|
+
buildNsCompletionScript,
|
|
21
|
+
renderNsCompletionScriptResult,
|
|
22
|
+
nsCompletionScriptResultSchema,
|
|
23
|
+
} from "./completion.ts";
|
|
24
|
+
import {
|
|
25
|
+
createRealNsCommandContext,
|
|
26
|
+
createNsCliInteraction,
|
|
27
|
+
type NsCliContext,
|
|
28
|
+
} from "./context.ts";
|
|
29
|
+
import {
|
|
30
|
+
renderNsShellInstall,
|
|
31
|
+
renderNsShellShow,
|
|
32
|
+
runNsShellInstall,
|
|
33
|
+
runNsShellShow,
|
|
34
|
+
nsShellInstallRequestSchema,
|
|
35
|
+
nsShellInstallResultSchema,
|
|
36
|
+
nsShellShowRequestSchema,
|
|
37
|
+
nsShellShowResultSchema,
|
|
38
|
+
} from "./shell.ts";
|
|
39
|
+
import { createCliCommandIo, noopNsProgress } from "../runtime/command-io.ts";
|
|
40
|
+
import {
|
|
41
|
+
classifyExtensionDiagnosticsForInvocation,
|
|
42
|
+
commandInfosForSelectedCommand,
|
|
43
|
+
formatExtensionErrorDiagnostics,
|
|
44
|
+
formatExtensionWarningDiagnostics,
|
|
45
|
+
loadListingCommandInfos,
|
|
46
|
+
loadNsCommandCatalog,
|
|
47
|
+
loadSelectedNsCommand,
|
|
48
|
+
type ExtensionCommandCandidate,
|
|
49
|
+
type LoadNsCommandCatalogOptions,
|
|
50
|
+
type PreinstalledNsCommandCatalogLoader,
|
|
51
|
+
type SelectedNsCommandLoadResult,
|
|
52
|
+
type NsCommandCatalog,
|
|
53
|
+
} from "../extensions/registry.ts";
|
|
54
|
+
import type {
|
|
55
|
+
RenderCapabilities,
|
|
56
|
+
NsCommand,
|
|
57
|
+
NsConfirmPrompt,
|
|
58
|
+
NsExtensionApi,
|
|
59
|
+
NsOutputStream,
|
|
60
|
+
NsProgressPhaseListener,
|
|
61
|
+
} from "../sdk/index.ts";
|
|
62
|
+
import {
|
|
63
|
+
commandDisplayName,
|
|
64
|
+
commandKey,
|
|
65
|
+
commandLeafName,
|
|
66
|
+
commandPathMatches,
|
|
67
|
+
commandSegments,
|
|
68
|
+
executeNsCommand,
|
|
69
|
+
formatUnknownError,
|
|
70
|
+
listStaticNsCommandInfos,
|
|
71
|
+
validateNsClinkrExit,
|
|
72
|
+
type NsCommandInfo,
|
|
73
|
+
type NsCommandCliInfo,
|
|
74
|
+
type NsCommandPath,
|
|
75
|
+
} from "../extensions/command-registry.ts";
|
|
76
|
+
|
|
77
|
+
export type { NsCliContext } from "./context.ts";
|
|
78
|
+
export type { NsCommandInfo } from "../extensions/command-registry.ts";
|
|
79
|
+
export type {
|
|
80
|
+
PreinstalledNsCommandCatalogEntry,
|
|
81
|
+
PreinstalledNsCommandCatalogLoader,
|
|
82
|
+
} from "../extensions/registry.ts";
|
|
83
|
+
export {
|
|
84
|
+
repoLocalNsCommandDescriptorToPreinstalledCatalogEntry,
|
|
85
|
+
repoLocalNsExtensionToPreinstalledCatalog,
|
|
86
|
+
} from "../extensions/repo-local-catalog.ts";
|
|
87
|
+
|
|
88
|
+
interface NsCliExtensionRegistryDeps {
|
|
89
|
+
loadCommandCatalog?: (options: LoadNsCommandCatalogOptions) => Promise<NsCommandCatalog>;
|
|
90
|
+
loadSelectedCommand?: (
|
|
91
|
+
candidate: ExtensionCommandCandidate,
|
|
92
|
+
) => Promise<SelectedNsCommandLoadResult>;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
export interface NsCliDeps extends Pick<CliEntrypointDeps, "cwd" | "env" | "stdout" | "stderr"> {
|
|
96
|
+
context?: NsExtensionApi;
|
|
97
|
+
homeDir?: string;
|
|
98
|
+
onOutput?: (stream: NsOutputStream, text: string) => void;
|
|
99
|
+
onProgress?: NsProgressPhaseListener;
|
|
100
|
+
confirm?: NsConfirmPrompt;
|
|
101
|
+
preinstalledCommandCatalog?: PreinstalledNsCommandCatalogLoader;
|
|
102
|
+
extensionRegistry?: NsCliExtensionRegistryDeps;
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
export interface BuildNsCliOptions {
|
|
106
|
+
commandInfos?: readonly NsCommandCliInfo[];
|
|
107
|
+
selectedCommand?: NsCommand;
|
|
108
|
+
selectedCommandPath?: NsCommandPath;
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
interface NsCliBuildState {
|
|
112
|
+
commandInfos: readonly NsCommandCliInfo[];
|
|
113
|
+
selectedCommand?: NsCommand;
|
|
114
|
+
selectedCommandPath?: NsCommandPath;
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
const entry = defineCli<NsCliContext, NsCliDeps, NsCliBuildState>({
|
|
118
|
+
metaUrl: new URL("../cli.ts", import.meta.url).href,
|
|
119
|
+
runtime: "typescript",
|
|
120
|
+
description: "ns tools.",
|
|
121
|
+
prepareRun: async ({ args, deps, cwd, env, stdout, stderr, io }) => {
|
|
122
|
+
const injectedContext = deps.context;
|
|
123
|
+
const resolvedStdout = deps.stdout ?? injectedContext?.stdout ?? stdout;
|
|
124
|
+
const resolvedStderr = deps.stderr ?? injectedContext?.stderr ?? stderr;
|
|
125
|
+
const resolvedCwd = deps.cwd ?? injectedContext?.cwd ?? cwd;
|
|
126
|
+
const resolvedEnv = deps.env ?? injectedContext?.env ?? env;
|
|
127
|
+
const homeDir = resolveHomeDir(deps.homeDir, resolvedEnv) ?? injectedContext?.homeDir;
|
|
128
|
+
const commandCatalog = await (
|
|
129
|
+
deps.extensionRegistry?.loadCommandCatalog ?? loadNsCommandCatalog
|
|
130
|
+
)({
|
|
131
|
+
cwd: resolvedCwd,
|
|
132
|
+
env: resolvedEnv,
|
|
133
|
+
...optionalEntry("homeDir", homeDir),
|
|
134
|
+
...optionalEntry("preinstalledCommandCatalog", deps.preinstalledCommandCatalog),
|
|
135
|
+
});
|
|
136
|
+
if (isCompletionResolverInvocation(args)) {
|
|
137
|
+
return await handleCompletionResolverInvocation({
|
|
138
|
+
args,
|
|
139
|
+
commandCatalog,
|
|
140
|
+
cwd: resolvedCwd,
|
|
141
|
+
env: resolvedEnv,
|
|
142
|
+
...optionalEntry("homeDir", homeDir),
|
|
143
|
+
stdout: resolvedStdout,
|
|
144
|
+
stderr: resolvedStderr,
|
|
145
|
+
...optionalEntries({
|
|
146
|
+
loadSelectedCommand: deps.extensionRegistry?.loadSelectedCommand,
|
|
147
|
+
injectedContext,
|
|
148
|
+
onOutput: deps.onOutput,
|
|
149
|
+
onProgress: deps.onProgress,
|
|
150
|
+
confirm: deps.confirm,
|
|
151
|
+
caps: io.caps,
|
|
152
|
+
}),
|
|
153
|
+
});
|
|
154
|
+
}
|
|
155
|
+
const isCompletionScriptRequest = isCompletionScriptInvocation(args);
|
|
156
|
+
const selectedCommandKey = isCompletionScriptRequest
|
|
157
|
+
? undefined
|
|
158
|
+
: requestedCommandKey(args, commandCatalog.commandInfos);
|
|
159
|
+
const selectedCandidate =
|
|
160
|
+
selectedCommandKey === undefined
|
|
161
|
+
? undefined
|
|
162
|
+
: commandCatalog.candidates.get(selectedCommandKey);
|
|
163
|
+
const diagnosticClassification = classifyExtensionDiagnosticsForInvocation({
|
|
164
|
+
diagnostics: commandCatalog.diagnostics,
|
|
165
|
+
requestedCommandName: selectedCommandKey,
|
|
166
|
+
selectedCandidate,
|
|
167
|
+
});
|
|
168
|
+
if (!isCompletionScriptRequest && diagnosticClassification.fatal.length > 0) {
|
|
169
|
+
resolvedStderr(`${formatExtensionErrorDiagnostics(diagnosticClassification.fatal)}\n`);
|
|
170
|
+
return { type: "handled", exitCode: 2 };
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
let commandInfos = commandCatalog.commandInfos;
|
|
174
|
+
let listingDiagnostics: typeof diagnosticClassification.warnings = [];
|
|
175
|
+
if (
|
|
176
|
+
!isCompletionScriptRequest &&
|
|
177
|
+
selectedCommandKey === undefined &&
|
|
178
|
+
!isStaticTopLevelMetadataRequest(args)
|
|
179
|
+
) {
|
|
180
|
+
const loadedListing = await loadListingCommandInfos(commandCatalog);
|
|
181
|
+
commandInfos = loadedListing.commandInfos;
|
|
182
|
+
listingDiagnostics = loadedListing.diagnostics;
|
|
183
|
+
}
|
|
184
|
+
const warnings = isCompletionScriptRequest
|
|
185
|
+
? []
|
|
186
|
+
: [...diagnosticClassification.warnings, ...listingDiagnostics];
|
|
187
|
+
if (warnings.length > 0) {
|
|
188
|
+
resolvedStderr(`${formatExtensionWarningDiagnostics(warnings)}\n`);
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
const selectedCommandResolution = await resolveSelectedNsCommand({
|
|
192
|
+
candidate: selectedCandidate,
|
|
193
|
+
commandInfos,
|
|
194
|
+
...optionalEntry("loadSelectedCommand", deps.extensionRegistry?.loadSelectedCommand),
|
|
195
|
+
stderr: resolvedStderr,
|
|
196
|
+
failureExitCode: 2,
|
|
197
|
+
});
|
|
198
|
+
if (!selectedCommandResolution.ok) return selectedCommandResolution.handled;
|
|
199
|
+
|
|
200
|
+
const contextWithIO = await buildNsCliContext({
|
|
201
|
+
args,
|
|
202
|
+
cwd: resolvedCwd,
|
|
203
|
+
env: resolvedEnv,
|
|
204
|
+
...optionalEntry("homeDir", homeDir),
|
|
205
|
+
stdout: resolvedStdout,
|
|
206
|
+
stderr: resolvedStderr,
|
|
207
|
+
...optionalEntries({
|
|
208
|
+
injectedContext,
|
|
209
|
+
onOutput: deps.onOutput,
|
|
210
|
+
onProgress: deps.onProgress,
|
|
211
|
+
confirm: deps.confirm,
|
|
212
|
+
caps: io.caps,
|
|
213
|
+
}),
|
|
214
|
+
});
|
|
215
|
+
return {
|
|
216
|
+
type: "run",
|
|
217
|
+
context: contextWithIO,
|
|
218
|
+
buildState: selectedCommandResolution.resolution,
|
|
219
|
+
};
|
|
220
|
+
},
|
|
221
|
+
configureCli: ({ root, buildState }) => {
|
|
222
|
+
const groups = new Map<string, ClinkrGroup<NsCliContext>>();
|
|
223
|
+
for (const commandInfo of buildState.commandInfos) {
|
|
224
|
+
const parent = groupForCommand(root, groups, commandInfo);
|
|
225
|
+
const selectedCommand =
|
|
226
|
+
buildState.selectedCommandPath !== undefined &&
|
|
227
|
+
commandPathMatches(buildState.selectedCommandPath, commandInfo)
|
|
228
|
+
? buildState.selectedCommand
|
|
229
|
+
: undefined;
|
|
230
|
+
const schema = selectedCommand?.schema ?? z.object({});
|
|
231
|
+
const commandOptions = {
|
|
232
|
+
name: cliLeafCommandName(commandInfo),
|
|
233
|
+
description: commandInfo.fullDescription,
|
|
234
|
+
summary: commandInfo.description,
|
|
235
|
+
schema,
|
|
236
|
+
...(selectedCommand?.positionals === undefined
|
|
237
|
+
? {}
|
|
238
|
+
: { positionals: selectedCommand.positionals }),
|
|
239
|
+
...(selectedCommand?.options === undefined ? {} : { options: selectedCommand.options }),
|
|
240
|
+
...(selectedCommand?.completionProvider === undefined
|
|
241
|
+
? {}
|
|
242
|
+
: {
|
|
243
|
+
completionProvider: (ctx: NsCliContext, request: ClinkrDynamicCompletionRequest) =>
|
|
244
|
+
selectedCommand.completionProvider?.(ctx.context, request) ?? [],
|
|
245
|
+
}),
|
|
246
|
+
};
|
|
247
|
+
if (selectedCommand?.resultSchema !== undefined) {
|
|
248
|
+
parent.command({
|
|
249
|
+
...commandOptions,
|
|
250
|
+
resultSchema: selectedCommand.resultSchema,
|
|
251
|
+
...(selectedCommand.renderHuman === undefined
|
|
252
|
+
? {}
|
|
253
|
+
: { renderHuman: selectedCommand.renderHuman }),
|
|
254
|
+
...(selectedCommand.renderMarkdown === undefined
|
|
255
|
+
? {}
|
|
256
|
+
: { renderMarkdown: selectedCommand.renderMarkdown }),
|
|
257
|
+
handler: async (ctx, request) => {
|
|
258
|
+
const result = await selectedCommand.run(ctx.context, request);
|
|
259
|
+
return validateNsClinkrExit(result, selectedCommand.name);
|
|
260
|
+
},
|
|
261
|
+
});
|
|
262
|
+
continue;
|
|
263
|
+
}
|
|
264
|
+
parent.command(
|
|
265
|
+
rawCommand({
|
|
266
|
+
...commandOptions,
|
|
267
|
+
run: async (ctx, request) => {
|
|
268
|
+
const result =
|
|
269
|
+
selectedCommand === undefined
|
|
270
|
+
? {
|
|
271
|
+
ok: false as const,
|
|
272
|
+
exitCode: 2,
|
|
273
|
+
message: `Unknown ns command: ${commandDisplayName(commandInfo)}`,
|
|
274
|
+
}
|
|
275
|
+
: await executeNsCommand(ctx.context, selectedCommand, request);
|
|
276
|
+
writeNsResultOutput(result, ctx);
|
|
277
|
+
return result.ok ? 0 : result.exitCode;
|
|
278
|
+
},
|
|
279
|
+
}),
|
|
280
|
+
);
|
|
281
|
+
}
|
|
282
|
+
root.group(buildNsShellGroup());
|
|
283
|
+
root.group(buildNsCompletionGroup());
|
|
284
|
+
},
|
|
285
|
+
});
|
|
286
|
+
|
|
287
|
+
export function buildCli(options: BuildNsCliOptions = {}): ClinkrGroup<NsCliContext> {
|
|
288
|
+
return entry.buildCli({
|
|
289
|
+
commandInfos: options.commandInfos ?? listStaticNsCommandInfos(),
|
|
290
|
+
...optionalEntries({
|
|
291
|
+
selectedCommand: options.selectedCommand,
|
|
292
|
+
selectedCommandPath: options.selectedCommandPath,
|
|
293
|
+
}),
|
|
294
|
+
});
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
export function listNsCommands(): NsCommandInfo[] {
|
|
298
|
+
return listStaticNsCommandInfos().map(({ group, name, description }) => ({
|
|
299
|
+
...(group === undefined ? {} : { group }),
|
|
300
|
+
name,
|
|
301
|
+
description,
|
|
302
|
+
}));
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
export async function runCli(args: readonly string[], deps: NsCliDeps = {}): Promise<number> {
|
|
306
|
+
return await entry.run(args, deps);
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
async function handleCompletionResolverInvocation(options: {
|
|
310
|
+
args: readonly string[];
|
|
311
|
+
commandCatalog: NsCommandCatalog;
|
|
312
|
+
loadSelectedCommand?: (
|
|
313
|
+
candidate: ExtensionCommandCandidate,
|
|
314
|
+
) => Promise<SelectedNsCommandLoadResult>;
|
|
315
|
+
cwd: string;
|
|
316
|
+
env: NodeJS.ProcessEnv;
|
|
317
|
+
homeDir?: string;
|
|
318
|
+
stdout: (text: string) => void;
|
|
319
|
+
stderr: (text: string) => void;
|
|
320
|
+
injectedContext?: NsExtensionApi;
|
|
321
|
+
onOutput?: (stream: NsOutputStream, text: string) => void;
|
|
322
|
+
onProgress?: NsProgressPhaseListener;
|
|
323
|
+
confirm?: NsConfirmPrompt;
|
|
324
|
+
caps?: Caps;
|
|
325
|
+
}): Promise<{ type: "handled"; exitCode: number }> {
|
|
326
|
+
const words = completionResolverWords(options.args);
|
|
327
|
+
const selectedCommandKey = requestedCommandKey(words, options.commandCatalog.commandInfos);
|
|
328
|
+
const selectedCandidate =
|
|
329
|
+
selectedCommandKey === undefined
|
|
330
|
+
? undefined
|
|
331
|
+
: options.commandCatalog.candidates.get(selectedCommandKey);
|
|
332
|
+
const selectedCommandResolution = await resolveSelectedNsCommand({
|
|
333
|
+
candidate: selectedCandidate,
|
|
334
|
+
commandInfos: options.commandCatalog.commandInfos,
|
|
335
|
+
...optionalEntry("loadSelectedCommand", options.loadSelectedCommand),
|
|
336
|
+
stderr: options.stderr,
|
|
337
|
+
failureExitCode: 0,
|
|
338
|
+
});
|
|
339
|
+
if (!selectedCommandResolution.ok) return selectedCommandResolution.handled;
|
|
340
|
+
const context = await buildNsCliContext({
|
|
341
|
+
args: options.args,
|
|
342
|
+
cwd: options.cwd,
|
|
343
|
+
env: options.env,
|
|
344
|
+
...optionalEntry("homeDir", options.homeDir),
|
|
345
|
+
stdout: options.stdout,
|
|
346
|
+
stderr: options.stderr,
|
|
347
|
+
...optionalEntries({
|
|
348
|
+
injectedContext: options.injectedContext,
|
|
349
|
+
onOutput: options.onOutput,
|
|
350
|
+
onProgress: options.onProgress,
|
|
351
|
+
confirm: options.confirm,
|
|
352
|
+
caps: options.caps,
|
|
353
|
+
}),
|
|
354
|
+
});
|
|
355
|
+
const candidates = await buildCli(selectedCommandResolution.resolution).completeAsync(
|
|
356
|
+
{ words },
|
|
357
|
+
{
|
|
358
|
+
context,
|
|
359
|
+
onDynamicCompletionError: (error) => {
|
|
360
|
+
options.stderr(`completion provider failed: ${formatUnknownError(error)}\n`);
|
|
361
|
+
},
|
|
362
|
+
},
|
|
363
|
+
);
|
|
364
|
+
options.stdout(renderCompletionCandidatesNewline(candidates));
|
|
365
|
+
return { type: "handled", exitCode: 0 };
|
|
366
|
+
}
|
|
367
|
+
|
|
368
|
+
async function resolveSelectedNsCommand(options: {
|
|
369
|
+
candidate: ExtensionCommandCandidate | undefined;
|
|
370
|
+
commandInfos: readonly NsCommandCliInfo[];
|
|
371
|
+
loadSelectedCommand?: (
|
|
372
|
+
candidate: ExtensionCommandCandidate,
|
|
373
|
+
) => Promise<SelectedNsCommandLoadResult>;
|
|
374
|
+
stderr: (text: string) => void;
|
|
375
|
+
failureExitCode: number;
|
|
376
|
+
}): Promise<
|
|
377
|
+
| { ok: true; resolution: NsCliBuildState }
|
|
378
|
+
| { ok: false; handled: { type: "handled"; exitCode: number } }
|
|
379
|
+
> {
|
|
380
|
+
const selectedCommandLoader = options.loadSelectedCommand ?? loadSelectedNsCommand;
|
|
381
|
+
const loadedSelectedCommand =
|
|
382
|
+
options.candidate === undefined ? undefined : await selectedCommandLoader(options.candidate);
|
|
383
|
+
if (loadedSelectedCommand !== undefined && !loadedSelectedCommand.ok) {
|
|
384
|
+
options.stderr(`${formatExtensionErrorDiagnostics([loadedSelectedCommand.diagnostic])}\n`);
|
|
385
|
+
return { ok: false, handled: { type: "handled", exitCode: options.failureExitCode } };
|
|
386
|
+
}
|
|
387
|
+
const selectedCommand = loadedSelectedCommand?.command;
|
|
388
|
+
const selectedSource = loadedSelectedCommand?.source;
|
|
389
|
+
const selectedPath = loadedSelectedCommand?.path;
|
|
390
|
+
const commandInfos = commandInfosForSelectedCommand(
|
|
391
|
+
options.commandInfos,
|
|
392
|
+
selectedCommand === undefined || selectedSource === undefined || selectedPath === undefined
|
|
393
|
+
? undefined
|
|
394
|
+
: { command: selectedCommand, source: selectedSource, path: selectedPath },
|
|
395
|
+
);
|
|
396
|
+
return {
|
|
397
|
+
ok: true,
|
|
398
|
+
resolution: {
|
|
399
|
+
commandInfos,
|
|
400
|
+
...optionalEntries({ selectedCommand, selectedCommandPath: selectedPath }),
|
|
401
|
+
},
|
|
402
|
+
};
|
|
403
|
+
}
|
|
404
|
+
|
|
405
|
+
async function buildNsCliContext(options: {
|
|
406
|
+
args: readonly string[];
|
|
407
|
+
cwd: string;
|
|
408
|
+
env: NodeJS.ProcessEnv;
|
|
409
|
+
homeDir?: string;
|
|
410
|
+
stdout: (text: string) => void;
|
|
411
|
+
stderr: (text: string) => void;
|
|
412
|
+
injectedContext?: NsExtensionApi;
|
|
413
|
+
onOutput?: (stream: NsOutputStream, text: string) => void;
|
|
414
|
+
onProgress?: NsProgressPhaseListener;
|
|
415
|
+
confirm?: NsConfirmPrompt;
|
|
416
|
+
caps?: Caps;
|
|
417
|
+
}): Promise<NsCliContext> {
|
|
418
|
+
const baseContext =
|
|
419
|
+
options.injectedContext ??
|
|
420
|
+
createRealNsCommandContext({
|
|
421
|
+
cwd: options.cwd,
|
|
422
|
+
env: options.env,
|
|
423
|
+
...optionalEntry("homeDir", options.homeDir),
|
|
424
|
+
});
|
|
425
|
+
const onOutput = options.onOutput ?? baseContext.onOutput;
|
|
426
|
+
const confirm = options.confirm ?? baseContext.confirm;
|
|
427
|
+
const stdin = baseContext.stdin ?? readStdin;
|
|
428
|
+
const renderCapabilities: RenderCapabilities = renderCapabilitiesForTerminal(options.caps);
|
|
429
|
+
const contextExtensions = baseContext.extensions;
|
|
430
|
+
const commandIo = createCliCommandIo({
|
|
431
|
+
stdout: options.stdout,
|
|
432
|
+
stderr: options.stderr,
|
|
433
|
+
...optionalEntry("onOutput", onOutput),
|
|
434
|
+
});
|
|
435
|
+
const context: NsExtensionApi = {
|
|
436
|
+
cwd: options.cwd,
|
|
437
|
+
env: options.env,
|
|
438
|
+
...optionalEntry("homeDir", options.homeDir),
|
|
439
|
+
textGenerator: baseContext.textGenerator,
|
|
440
|
+
commandIo,
|
|
441
|
+
progress:
|
|
442
|
+
options.onProgress === undefined
|
|
443
|
+
? noopNsProgress
|
|
444
|
+
: { isLive: true, phase: options.onProgress },
|
|
445
|
+
renderCapabilities,
|
|
446
|
+
outputFormat: clinkrFormatFromArgs(options.args),
|
|
447
|
+
exec: baseContext.exec.bind(baseContext),
|
|
448
|
+
stdout: options.stdout,
|
|
449
|
+
stderr: options.stderr,
|
|
450
|
+
stdin,
|
|
451
|
+
...optionalEntries({ onOutput, confirm, extensions: contextExtensions }),
|
|
452
|
+
};
|
|
453
|
+
return {
|
|
454
|
+
context,
|
|
455
|
+
cwd: options.cwd,
|
|
456
|
+
env: options.env,
|
|
457
|
+
interaction: createNsCliInteraction({ stderr: options.stderr }),
|
|
458
|
+
stdout: options.stdout,
|
|
459
|
+
stderr: options.stderr,
|
|
460
|
+
};
|
|
461
|
+
}
|
|
462
|
+
|
|
463
|
+
function isCompletionResolverInvocation(args: readonly string[]): boolean {
|
|
464
|
+
return args[0] === "completion" && args[1] === NS_EXEC_GROUP_NAME && args[2] === "resolve";
|
|
465
|
+
}
|
|
466
|
+
|
|
467
|
+
function isCompletionScriptInvocation(args: readonly string[]): boolean {
|
|
468
|
+
return args[0] === "completion" && ["bash", "zsh", "fish"].includes(args[1] ?? "");
|
|
469
|
+
}
|
|
470
|
+
|
|
471
|
+
function completionResolverWords(args: readonly string[]): readonly string[] {
|
|
472
|
+
const resolverArgs = args.slice(3);
|
|
473
|
+
if (resolverArgs[0] !== "--") return resolverArgs;
|
|
474
|
+
return resolverArgs.slice(1);
|
|
475
|
+
}
|
|
476
|
+
|
|
477
|
+
function requestedCommandKey(
|
|
478
|
+
args: readonly string[],
|
|
479
|
+
commandInfos: readonly NsCommandCliInfo[],
|
|
480
|
+
): string | undefined {
|
|
481
|
+
const commandArgs = args.filter((arg) => !arg.startsWith("-"));
|
|
482
|
+
if (commandArgs.length === 0) return undefined;
|
|
483
|
+
|
|
484
|
+
const candidates = commandInfos
|
|
485
|
+
.map((commandInfo) => ({
|
|
486
|
+
commandInfo,
|
|
487
|
+
displaySegments: displaySegmentsForCommand(commandInfo),
|
|
488
|
+
}))
|
|
489
|
+
.filter(({ displaySegments }) => pathPrefixMatches(commandArgs, displaySegments))
|
|
490
|
+
.sort((left, right) => right.displaySegments.length - left.displaySegments.length);
|
|
491
|
+
const selected = candidates[0];
|
|
492
|
+
if (selected === undefined) return commandArgs[0];
|
|
493
|
+
if (commandArgs.length < selected.displaySegments.length) return undefined;
|
|
494
|
+
return commandKey(selected.commandInfo);
|
|
495
|
+
}
|
|
496
|
+
|
|
497
|
+
const NS_EXEC_GROUP_NAME = "exec";
|
|
498
|
+
const NS_BUILT_IN_HELP_GROUP = "Built-in Commands:";
|
|
499
|
+
const NS_EXTENSION_HELP_GROUP = "Extensions:";
|
|
500
|
+
// Dynamic ns extensions are one group deep today. A grouped command named
|
|
501
|
+
// `exec-<name>` is mounted as hidden `ns <group> exec <name>` so agent-only
|
|
502
|
+
// operations keep the same nested exec contract as preinstalled Clinkr groups.
|
|
503
|
+
const NS_EXEC_COMMAND_PREFIX = "exec-";
|
|
504
|
+
|
|
505
|
+
function isGroupedExecCommand(commandInfo: NsCommandCliInfo): boolean {
|
|
506
|
+
return commandInfo.group !== undefined && commandInfo.name.startsWith(NS_EXEC_COMMAND_PREFIX);
|
|
507
|
+
}
|
|
508
|
+
|
|
509
|
+
function cliLeafCommandName(commandInfo: NsCommandCliInfo): string {
|
|
510
|
+
if (commandInfo.segments !== undefined) return commandLeafName(commandInfo);
|
|
511
|
+
if (!isGroupedExecCommand(commandInfo)) return commandInfo.name;
|
|
512
|
+
return commandInfo.name.slice(NS_EXEC_COMMAND_PREFIX.length);
|
|
513
|
+
}
|
|
514
|
+
|
|
515
|
+
function buildNsCompletionGroup(): ClinkrGroup<NsCliContext> {
|
|
516
|
+
const completion = new ClinkrGroup<NsCliContext>({
|
|
517
|
+
name: "completion",
|
|
518
|
+
description: "Print shell completion setup scripts.",
|
|
519
|
+
helpGroup: NS_BUILT_IN_HELP_GROUP,
|
|
520
|
+
});
|
|
521
|
+
for (const shell of ["bash", "zsh", "fish"] as const) {
|
|
522
|
+
completion.command({
|
|
523
|
+
name: shell,
|
|
524
|
+
description: `Print ${shell} completion setup for ns.`,
|
|
525
|
+
schema: z.object({}),
|
|
526
|
+
resultSchema: nsCompletionScriptResultSchema,
|
|
527
|
+
handler: async () => ok(buildNsCompletionScript(shell)),
|
|
528
|
+
renderHuman: renderNsCompletionScriptResult,
|
|
529
|
+
});
|
|
530
|
+
}
|
|
531
|
+
const exec = new ClinkrGroup<NsCliContext>({
|
|
532
|
+
name: NS_EXEC_GROUP_NAME,
|
|
533
|
+
description: "Shell completion resolver operations.",
|
|
534
|
+
isHidden: true,
|
|
535
|
+
});
|
|
536
|
+
exec.command(
|
|
537
|
+
rawCommand({
|
|
538
|
+
name: "resolve",
|
|
539
|
+
description: "Resolve newline-delimited shell completion candidates.",
|
|
540
|
+
schema: z.object({ words: z.array(z.string()).default([]) }),
|
|
541
|
+
positionals: { words: { position: 0 } },
|
|
542
|
+
run: async () => 0,
|
|
543
|
+
}),
|
|
544
|
+
);
|
|
545
|
+
completion.group(exec);
|
|
546
|
+
return completion;
|
|
547
|
+
}
|
|
548
|
+
|
|
549
|
+
function displaySegmentsForCommand(commandInfo: NsCommandCliInfo): readonly string[] {
|
|
550
|
+
if (commandInfo.segments !== undefined) return commandInfo.segments;
|
|
551
|
+
if (!isGroupedExecCommand(commandInfo)) return commandSegments(commandInfo);
|
|
552
|
+
return [commandInfo.group ?? "", NS_EXEC_GROUP_NAME, cliLeafCommandName(commandInfo)].filter(
|
|
553
|
+
(segment) => segment !== "",
|
|
554
|
+
);
|
|
555
|
+
}
|
|
556
|
+
|
|
557
|
+
function pathPrefixMatches(args: readonly string[], path: readonly string[]): boolean {
|
|
558
|
+
return path.every((segment, index) => args[index] === segment);
|
|
559
|
+
}
|
|
560
|
+
|
|
561
|
+
type ShellCommandSchema = z.ZodObject<{ shell: z.ZodOptional<z.ZodString> }>;
|
|
562
|
+
|
|
563
|
+
type ShellCommandSpec<T> = Omit<
|
|
564
|
+
ClinkrCommandSpec<NsCliContext, ShellCommandSchema, T>,
|
|
565
|
+
"name" | "description"
|
|
566
|
+
>;
|
|
567
|
+
|
|
568
|
+
// Keep this shell command face kernel-owned instead of delegating parent-shell integration
|
|
569
|
+
// to one extension. The reusable abstraction we expect here is future typed shell
|
|
570
|
+
// contributions rendered inside one managed shell integration, not extension-owned rc-file
|
|
571
|
+
// mutation or command helpers that each install their own wrapper.
|
|
572
|
+
function buildNsShellGroup(): ClinkrGroup<NsCliContext> {
|
|
573
|
+
const shell = new ClinkrGroup<NsCliContext>({
|
|
574
|
+
name: "shell",
|
|
575
|
+
description: "Show or install parent-shell integration.",
|
|
576
|
+
helpGroup: NS_BUILT_IN_HELP_GROUP,
|
|
577
|
+
});
|
|
578
|
+
shell.command({
|
|
579
|
+
name: "show",
|
|
580
|
+
description: "Print the parent-shell wrapper script.",
|
|
581
|
+
...withShellOption({
|
|
582
|
+
schema: nsShellShowRequestSchema,
|
|
583
|
+
resultSchema: nsShellShowResultSchema,
|
|
584
|
+
handler: runNsShellShow,
|
|
585
|
+
renderHuman: renderNsShellShow,
|
|
586
|
+
}),
|
|
587
|
+
});
|
|
588
|
+
shell.command({
|
|
589
|
+
name: "install",
|
|
590
|
+
description: "Install the parent-shell wrapper in the detected or selected rc file.",
|
|
591
|
+
schema: nsShellInstallRequestSchema,
|
|
592
|
+
options: { shell: { short: "-s" }, yes: { short: "-y" } },
|
|
593
|
+
resultSchema: nsShellInstallResultSchema,
|
|
594
|
+
handler: runNsShellInstall,
|
|
595
|
+
renderHuman: renderNsShellInstall,
|
|
596
|
+
});
|
|
597
|
+
return shell;
|
|
598
|
+
}
|
|
599
|
+
|
|
600
|
+
function withShellOption<T>(spec: ShellCommandSpec<T>): ShellCommandSpec<T> {
|
|
601
|
+
return {
|
|
602
|
+
...spec,
|
|
603
|
+
options: { shell: { short: "-s" }, ...(spec.options ?? {}) },
|
|
604
|
+
};
|
|
605
|
+
}
|
|
606
|
+
|
|
607
|
+
function groupForCommand(
|
|
608
|
+
root: ClinkrGroup<NsCliContext>,
|
|
609
|
+
groupCache: Map<string, ClinkrGroup<NsCliContext>>,
|
|
610
|
+
commandInfo: NsCommandCliInfo,
|
|
611
|
+
): ClinkrGroup<NsCliContext> {
|
|
612
|
+
const displaySegments = displaySegmentsForCommand(commandInfo);
|
|
613
|
+
const parentSegments = displaySegments.slice(0, -1);
|
|
614
|
+
let parent = root;
|
|
615
|
+
for (let index = 0; index < parentSegments.length; index += 1) {
|
|
616
|
+
const segment = parentSegments[index];
|
|
617
|
+
if (segment === undefined) continue;
|
|
618
|
+
const groupKey = parentSegments.slice(0, index + 1).join("/");
|
|
619
|
+
const existing = groupCache.get(groupKey);
|
|
620
|
+
if (existing !== undefined) {
|
|
621
|
+
parent = existing;
|
|
622
|
+
continue;
|
|
623
|
+
}
|
|
624
|
+
const group = new ClinkrGroup<NsCliContext>({
|
|
625
|
+
name: segment,
|
|
626
|
+
description: groupDescription(parentSegments.slice(0, index + 1), commandInfo),
|
|
627
|
+
...(index === 0 ? { helpGroup: NS_EXTENSION_HELP_GROUP } : {}),
|
|
628
|
+
...(segment === NS_EXEC_GROUP_NAME && isGroupedExecCommand(commandInfo)
|
|
629
|
+
? { isHidden: true }
|
|
630
|
+
: {}),
|
|
631
|
+
});
|
|
632
|
+
groupCache.set(groupKey, group);
|
|
633
|
+
parent.group(group);
|
|
634
|
+
parent = group;
|
|
635
|
+
}
|
|
636
|
+
return parent;
|
|
637
|
+
}
|
|
638
|
+
|
|
639
|
+
function groupDescription(segments: readonly string[], commandInfo: NsCommandCliInfo): string {
|
|
640
|
+
if (segments.at(-1) === NS_EXEC_GROUP_NAME && isGroupedExecCommand(commandInfo)) {
|
|
641
|
+
return `Skill-invoked NS ${segments[0] ?? "extension"} operations.`;
|
|
642
|
+
}
|
|
643
|
+
if (segments.length === 1 && commandInfo.groupDescription !== undefined) {
|
|
644
|
+
return commandInfo.groupDescription;
|
|
645
|
+
}
|
|
646
|
+
return `NS ${segments.join(" ")} commands.`;
|
|
647
|
+
}
|
|
648
|
+
|
|
649
|
+
function isStaticTopLevelMetadataRequest(args: readonly string[]): boolean {
|
|
650
|
+
return args.includes("--version") || args.includes("--runtime");
|
|
651
|
+
}
|
|
652
|
+
|
|
653
|
+
function writeNsResultOutput(
|
|
654
|
+
result: { ok: true; message: string } | { ok: false; message: string },
|
|
655
|
+
deps: Pick<NsCliContext, "stdout" | "stderr">,
|
|
656
|
+
): void {
|
|
657
|
+
if (result.message === "") return;
|
|
658
|
+
const output = `${result.message}\n`;
|
|
659
|
+
if (result.ok) {
|
|
660
|
+
deps.stdout(output);
|
|
661
|
+
return;
|
|
662
|
+
}
|
|
663
|
+
deps.stderr(output);
|
|
664
|
+
}
|
|
665
|
+
|
|
666
|
+
export const VERSION = entry.version;
|
|
667
|
+
|
|
668
|
+
await entry.runIfMain({ isImportMetaMain: import.meta.main });
|