@catheadowl/ukp 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +208 -0
- package/bun.lock +35 -0
- package/package.json +51 -0
- package/src/artifacts.ts +75 -0
- package/src/capabilities/nav.ts +734 -0
- package/src/capabilities/propose.ts +456 -0
- package/src/capabilities/qmd.ts +174 -0
- package/src/capabilities/read.ts +1040 -0
- package/src/capabilities/remote-client.ts +856 -0
- package/src/capabilities/rename-recovery.ts +294 -0
- package/src/capabilities/rg.ts +505 -0
- package/src/capabilities/search.ts +1105 -0
- package/src/capabilities/tls-identity.ts +87 -0
- package/src/capabilities/update.ts +385 -0
- package/src/cli.ts +425 -0
- package/src/commands/diagnose.ts +383 -0
- package/src/commands/flags.ts +49 -0
- package/src/commands/guide-content/client.txt +36 -0
- package/src/commands/guide-content/propose.txt +38 -0
- package/src/commands/guide-content/service-qmd.txt +44 -0
- package/src/commands/guide-content/service.txt +114 -0
- package/src/commands/guide.ts +121 -0
- package/src/commands/init.ts +245 -0
- package/src/commands/inspect.ts +182 -0
- package/src/commands/inventory.ts +570 -0
- package/src/commands/kit.ts +269 -0
- package/src/commands/nav.ts +280 -0
- package/src/commands/propose.ts +257 -0
- package/src/commands/read.ts +558 -0
- package/src/commands/rg.ts +355 -0
- package/src/commands/search.ts +325 -0
- package/src/commands/serve.ts +221 -0
- package/src/commands/update.ts +99 -0
- package/src/config/client.ts +55 -0
- package/src/config/external-tool.ts +58 -0
- package/src/config/file-native.ts +111 -0
- package/src/config/manifest.ts +188 -0
- package/src/fslock.ts +95 -0
- package/src/path-safety.ts +65 -0
- package/src/registry.ts +436 -0
- package/src/scope.ts +78 -0
- package/src/server.ts +1011 -0
- package/tsconfig.json +12 -0
|
@@ -0,0 +1,269 @@
|
|
|
1
|
+
import { Command, CommanderError } from "commander";
|
|
2
|
+
import { countFlagOccurrences, HelpRequestError, isCommanderHelpIntent, isHelpRequest } from "./flags.ts";
|
|
3
|
+
import { ScopeError } from "../scope.ts";
|
|
4
|
+
|
|
5
|
+
/** CLI-owned command result shape (ADR 0021; same fields every command
|
|
6
|
+
* result already carries). */
|
|
7
|
+
export interface KitCommandResult {
|
|
8
|
+
exitCode: number;
|
|
9
|
+
stdout: string;
|
|
10
|
+
stderr: string;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
/** Usage error surfaced by kit parsing (commander translation, generated
|
|
14
|
+
* singleton detection, scope family rules). Exit 2 at the adapter. */
|
|
15
|
+
export class KitUsageError extends Error {
|
|
16
|
+
constructor(message: string) {
|
|
17
|
+
super(message);
|
|
18
|
+
this.name = "KitUsageError";
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
/** Option declaration. Non-multi options get generated singleton duplicate
|
|
23
|
+
* detection (replaces per-command countFlagOccurrences if-chains).
|
|
24
|
+
* `teachingFlag` marks a presence-only option (the `-g` "not supported"
|
|
25
|
+
* flags) whose any occurrence triggers its own dedicated error — it skips
|
|
26
|
+
* singleton detection so `-g -g` reports the teaching message, not a
|
|
27
|
+
* duplicate-count message. */
|
|
28
|
+
export interface KitOptionSpec {
|
|
29
|
+
flags: string;
|
|
30
|
+
help: string;
|
|
31
|
+
/** repeat-to-collect (append semantics); default singleton */
|
|
32
|
+
multi?: boolean;
|
|
33
|
+
/** presence-only option; excluded from singleton duplicate detection */
|
|
34
|
+
teachingFlag?: boolean;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/** Command spec — the single source for everything the family rules render
|
|
38
|
+
* (ADR 0024). `usage` omits the leading "ukp <name> ": the help header and
|
|
39
|
+
* the usage-error line both prepend it, from this one string. */
|
|
40
|
+
export interface UkpCommandSpec {
|
|
41
|
+
name: string;
|
|
42
|
+
/** root-help one-liner; feeds COMMAND_DESCRIPTIONS once cli.ts is wired */
|
|
43
|
+
summary: string;
|
|
44
|
+
group: "endpoint" | "registry" | "operations" | "help";
|
|
45
|
+
description: string;
|
|
46
|
+
usage: string;
|
|
47
|
+
arguments?: Array<{ name: string; help: string; required?: boolean }>;
|
|
48
|
+
options?: KitOptionSpec[];
|
|
49
|
+
/** -c/--endpoint (repeat-multi) + -g family: mutual exclusion, duplicate
|
|
50
|
+
* endpoint warnings, generated -g singleton detection. Commands with
|
|
51
|
+
* `scope` and no `arguments` reject positionals with the family message. */
|
|
52
|
+
scope?: {
|
|
53
|
+
endpointHelp: string;
|
|
54
|
+
globalHelp: string;
|
|
55
|
+
};
|
|
56
|
+
/** Single-endpoint family: `-c, --endpoint <name>` rendered before the
|
|
57
|
+
* declared options, the `-g` teaching flag after them; parse rejects -g
|
|
58
|
+
* ("requires --endpoint <name> and does not support -g") and a missing
|
|
59
|
+
* --endpoint ("requires --endpoint <name>"). Positionals and per-command
|
|
60
|
+
* semantics stay command-side. */
|
|
61
|
+
singleEndpoint?: {
|
|
62
|
+
endpointHelp: string;
|
|
63
|
+
unsupportedHelp: string;
|
|
64
|
+
};
|
|
65
|
+
/** pre-wrapped extra section appended verbatim to --help */
|
|
66
|
+
helpSuffix?: string;
|
|
67
|
+
/** Reject excess positionals with commander's own message ("too many
|
|
68
|
+
* arguments. Expected N ..."). Default false — selector-family commands
|
|
69
|
+
* reject positionals with the family message instead. */
|
|
70
|
+
strictArguments?: boolean;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
function collectValues(value: string, previous: string[] = []): string[] {
|
|
74
|
+
return [...previous, value];
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
function flagSpellings(flags: string): { long?: string; short?: string } {
|
|
78
|
+
let long: string | undefined;
|
|
79
|
+
let short: string | undefined;
|
|
80
|
+
for (const token of flags.split(",").map((part) => part.trim())) {
|
|
81
|
+
if (token.startsWith("--")) long = token.split(/\s+/)[0];
|
|
82
|
+
else if (token.startsWith("-")) short = token.split(/\s+/)[0];
|
|
83
|
+
}
|
|
84
|
+
return { long, short };
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
/** Generated singleton detection: every declared non-multi option (plus the
|
|
88
|
+
* multi scope family's -g, plus the single-endpoint family's -c) may appear
|
|
89
|
+
* exactly once, counting short/long spellings and bundled short forms
|
|
90
|
+
* together. The error names the canonical spelling (long preferred),
|
|
91
|
+
* matching the pre-kit messages. The single-endpoint teaching -g is NOT
|
|
92
|
+
* singleton-checked (pre-kit commands never rejected a repeated -g there —
|
|
93
|
+
* any occurrence is already a usage error). */
|
|
94
|
+
function assertSingletonFlags(spec: UkpCommandSpec, args: readonly string[]): void {
|
|
95
|
+
const declarations: string[] = [
|
|
96
|
+
...(spec.options ?? [])
|
|
97
|
+
.filter((option) => !option.multi && !option.teachingFlag)
|
|
98
|
+
.map((option) => option.flags),
|
|
99
|
+
...(spec.scope ? ["-g"] : []),
|
|
100
|
+
...(spec.singleEndpoint ? ["-c, --endpoint <name>"] : []),
|
|
101
|
+
];
|
|
102
|
+
for (const flags of declarations) {
|
|
103
|
+
const { long, short } = flagSpellings(flags);
|
|
104
|
+
const occurrences = (long === undefined ? 0 : countFlagOccurrences(args, long))
|
|
105
|
+
+ (short === undefined ? 0 : countFlagOccurrences(args, short));
|
|
106
|
+
if (occurrences > 1) {
|
|
107
|
+
throw new KitUsageError(`${long ?? short} may only be specified once`);
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
export function createKitCommand(spec: UkpCommandSpec): Command {
|
|
113
|
+
const command = new Command(`ukp ${spec.name}`)
|
|
114
|
+
.exitOverride()
|
|
115
|
+
.allowUnknownOption(false)
|
|
116
|
+
.allowExcessArguments(spec.strictArguments !== true)
|
|
117
|
+
.helpOption("-h, --help", "show this help")
|
|
118
|
+
.usage(spec.usage)
|
|
119
|
+
.description(spec.description);
|
|
120
|
+
if (spec.singleEndpoint) {
|
|
121
|
+
command.option("-c, --endpoint <name>", spec.singleEndpoint.endpointHelp);
|
|
122
|
+
}
|
|
123
|
+
for (const argument of spec.arguments ?? []) {
|
|
124
|
+
command.argument(argument.required ? `<${argument.name}>` : `[${argument.name}]`, argument.help);
|
|
125
|
+
}
|
|
126
|
+
for (const option of spec.options ?? []) {
|
|
127
|
+
if (option.multi) command.option(option.flags, option.help, collectValues);
|
|
128
|
+
else command.option(option.flags, option.help);
|
|
129
|
+
}
|
|
130
|
+
if (spec.scope) {
|
|
131
|
+
command.option("-c, --endpoint <name>", spec.scope.endpointHelp, collectValues);
|
|
132
|
+
command.option("-g", spec.scope.globalHelp);
|
|
133
|
+
}
|
|
134
|
+
if (spec.singleEndpoint) {
|
|
135
|
+
command.option("-g", spec.singleEndpoint.unsupportedHelp);
|
|
136
|
+
}
|
|
137
|
+
return command;
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
export interface KitScopeSelection {
|
|
141
|
+
explicitEndpoints?: string[];
|
|
142
|
+
global: boolean;
|
|
143
|
+
warnings: string[];
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
export interface KitParsed<Options extends Record<string, unknown> = Record<string, unknown>> {
|
|
147
|
+
positionals: string[];
|
|
148
|
+
options: Options;
|
|
149
|
+
scope: KitScopeSelection;
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
/** Family parse pipeline, order-matched to the pre-kit commands: commander
|
|
153
|
+
* errors first (unknown option…), then generated singleton detection, then
|
|
154
|
+
* positional rejection, then duplicate-endpoint warnings, then the
|
|
155
|
+
* `--endpoint`/`-g` conflict. Help/version intent surfaces as
|
|
156
|
+
* HelpRequestError for the execute layer to re-render. */
|
|
157
|
+
export function parseKitArgs<Options extends Record<string, unknown> = Record<string, unknown>>(
|
|
158
|
+
spec: UkpCommandSpec,
|
|
159
|
+
args: readonly string[],
|
|
160
|
+
): KitParsed<Options> {
|
|
161
|
+
const command = createKitCommand(spec)
|
|
162
|
+
.configureOutput({ writeOut: () => undefined, writeErr: () => undefined });
|
|
163
|
+
|
|
164
|
+
try {
|
|
165
|
+
command.parse(args, { from: "user" });
|
|
166
|
+
} catch (error) {
|
|
167
|
+
if (error instanceof CommanderError) {
|
|
168
|
+
if (isCommanderHelpIntent(error)) throw new HelpRequestError();
|
|
169
|
+
throw new KitUsageError(error.message.replace(/^error: /, ""));
|
|
170
|
+
}
|
|
171
|
+
throw error;
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
assertSingletonFlags(spec, args);
|
|
175
|
+
|
|
176
|
+
const options = command.opts<Options>();
|
|
177
|
+
const positionals = command.args;
|
|
178
|
+
if (spec.singleEndpoint) {
|
|
179
|
+
if ((options as { g?: boolean }).g) {
|
|
180
|
+
throw new KitUsageError(`${spec.name} requires --endpoint <name> and does not support -g`);
|
|
181
|
+
}
|
|
182
|
+
const endpoint = (options as { endpoint?: string }).endpoint;
|
|
183
|
+
if (endpoint === undefined || endpoint.length === 0) {
|
|
184
|
+
throw new KitUsageError(`${spec.name} requires --endpoint <name>`);
|
|
185
|
+
}
|
|
186
|
+
return {
|
|
187
|
+
positionals,
|
|
188
|
+
options,
|
|
189
|
+
scope: { explicitEndpoints: [endpoint], global: false, warnings: [] },
|
|
190
|
+
};
|
|
191
|
+
}
|
|
192
|
+
if (!spec.scope) {
|
|
193
|
+
return { positionals, options, scope: { global: false, warnings: [] } };
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
if (spec.arguments === undefined && positionals.length > 0) {
|
|
197
|
+
throw new KitUsageError(
|
|
198
|
+
`unexpected argument '${positionals[0]}'. Use '--endpoint <name>' to select an endpoint; '-g' takes no value.`,
|
|
199
|
+
);
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
const explicit: string[] = [];
|
|
203
|
+
const warnings: string[] = [];
|
|
204
|
+
for (const endpoint of (options as { endpoint?: string[] }).endpoint ?? []) {
|
|
205
|
+
if (explicit.includes(endpoint)) warnings.push(`duplicate endpoint '${endpoint}' ignored`);
|
|
206
|
+
else explicit.push(endpoint);
|
|
207
|
+
}
|
|
208
|
+
const global = (options as { g?: boolean }).g === true;
|
|
209
|
+
if (global && explicit.length > 0) {
|
|
210
|
+
throw new KitUsageError("--endpoint and -g cannot be used together");
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
return {
|
|
214
|
+
positionals,
|
|
215
|
+
options,
|
|
216
|
+
scope: {
|
|
217
|
+
explicitEndpoints: explicit.length > 0 ? explicit : undefined,
|
|
218
|
+
global,
|
|
219
|
+
warnings,
|
|
220
|
+
},
|
|
221
|
+
};
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
export function renderKitHelp(spec: UkpCommandSpec): string {
|
|
225
|
+
const base = createKitCommand(spec).helpInformation();
|
|
226
|
+
return spec.helpSuffix === undefined ? base : base + spec.helpSuffix;
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
export function renderKitUsageError(spec: UkpCommandSpec, message: string): string {
|
|
230
|
+
return [
|
|
231
|
+
`ukp ${spec.name}: ${message}`,
|
|
232
|
+
`Usage: ukp ${spec.name} ${spec.usage}`,
|
|
233
|
+
`Run 'ukp ${spec.name} --help' for details.`,
|
|
234
|
+
].join("\n");
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
/** Family execute pipeline: sole-arg help, HelpRequestError re-render,
|
|
238
|
+
* KitUsageError → exit 2 with the single-source usage line, ScopeError →
|
|
239
|
+
* exit 1 with the command's recovery hint, fallback → exit 1. */
|
|
240
|
+
export function executeKitCommand(
|
|
241
|
+
spec: UkpCommandSpec,
|
|
242
|
+
args: readonly string[],
|
|
243
|
+
run: (parsed: KitParsed) => KitCommandResult,
|
|
244
|
+
recovery: { scopeErrorHint?: string } = {},
|
|
245
|
+
): KitCommandResult {
|
|
246
|
+
if (isHelpRequest(args)) {
|
|
247
|
+
return { exitCode: 0, stdout: renderKitHelp(spec), stderr: "" };
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
try {
|
|
251
|
+
return run(parseKitArgs(spec, args));
|
|
252
|
+
} catch (error) {
|
|
253
|
+
if (error instanceof HelpRequestError) {
|
|
254
|
+
return { exitCode: 0, stdout: renderKitHelp(spec), stderr: "" };
|
|
255
|
+
}
|
|
256
|
+
if (error instanceof KitUsageError) {
|
|
257
|
+
return { exitCode: 2, stdout: "", stderr: renderKitUsageError(spec, error.message) };
|
|
258
|
+
}
|
|
259
|
+
if (error instanceof ScopeError) {
|
|
260
|
+
const hint = recovery.scopeErrorHint === undefined ? "" : `\n${recovery.scopeErrorHint}`;
|
|
261
|
+
return { exitCode: 1, stdout: "", stderr: `ukp ${spec.name}: ${error.message}${hint}\n` };
|
|
262
|
+
}
|
|
263
|
+
return {
|
|
264
|
+
exitCode: 1,
|
|
265
|
+
stdout: "",
|
|
266
|
+
stderr: `ukp ${spec.name}: ${error instanceof Error ? error.message : String(error)}\n`,
|
|
267
|
+
};
|
|
268
|
+
}
|
|
269
|
+
}
|
|
@@ -0,0 +1,280 @@
|
|
|
1
|
+
import {
|
|
2
|
+
runNav,
|
|
3
|
+
projectNavEnvelope,
|
|
4
|
+
renderNavHuman,
|
|
5
|
+
renderNavJson,
|
|
6
|
+
NAV_MAX_DEPTH,
|
|
7
|
+
NavProviderError,
|
|
8
|
+
NavUsageError,
|
|
9
|
+
validateNavRoutePath,
|
|
10
|
+
type NavContext,
|
|
11
|
+
type NavErrorClass,
|
|
12
|
+
type NavRequest,
|
|
13
|
+
} from "../capabilities/nav.ts";
|
|
14
|
+
import {
|
|
15
|
+
fetchDiscoveryDocument,
|
|
16
|
+
openRemoteTransport,
|
|
17
|
+
remoteNav,
|
|
18
|
+
resolveRemoteToken,
|
|
19
|
+
type RemoteTransportHandle,
|
|
20
|
+
} from "../capabilities/remote-client.ts";
|
|
21
|
+
import { isRemoteBinding, readRegistry, type RegistryBinding } from "../registry.ts";
|
|
22
|
+
import { ScopeError } from "../scope.ts";
|
|
23
|
+
import { ManifestError } from "../config/manifest.ts";
|
|
24
|
+
import { KitUsageError, parseKitArgs, renderKitHelp, renderKitUsageError, type UkpCommandSpec } from "./kit.ts";
|
|
25
|
+
import { HelpRequestError, isHelpRequest } from "./flags.ts";
|
|
26
|
+
|
|
27
|
+
/** CLI-owned command result shape (ADR 0021: exit codes and channel text
|
|
28
|
+
* belong to the surface adapter, not the capability). */
|
|
29
|
+
export interface NavCommandResult {
|
|
30
|
+
exitCode: number;
|
|
31
|
+
stdout: string;
|
|
32
|
+
stderr: string;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/** Error-class → exit-code mapping (ADR 0021: owned by the CLI adapter per
|
|
36
|
+
* the Capability Contract; every nav failure class is a hard stop today). */
|
|
37
|
+
const NAV_EXIT_BY_ERROR_CLASS: Record<NavErrorClass, number> = {
|
|
38
|
+
"no-endpoint": 1,
|
|
39
|
+
"endpoint-name-mismatch": 1,
|
|
40
|
+
"provider-unsupported": 1,
|
|
41
|
+
"route-root-not-found": 1,
|
|
42
|
+
"route-root-not-directory": 1,
|
|
43
|
+
};
|
|
44
|
+
|
|
45
|
+
/** Single-source command spec (ADR 0024): summary feeds the root help via
|
|
46
|
+
* cli.ts; usage feeds the help header and the usage-error line; the
|
|
47
|
+
* single-endpoint family (-c singleton, -g teaching flag, required
|
|
48
|
+
* --endpoint) and help-intent triage live in kit.ts. */
|
|
49
|
+
export const NAV_SPEC: UkpCommandSpec = {
|
|
50
|
+
name: "nav",
|
|
51
|
+
summary: "navigate the Markdown structure of an endpoint",
|
|
52
|
+
group: "endpoint",
|
|
53
|
+
description: "Navigate the Markdown structure of one Service endpoint (endpoint names come from 'ukp list').",
|
|
54
|
+
usage: "--endpoint <name> [path] [--depth <n>] [--json]",
|
|
55
|
+
arguments: [
|
|
56
|
+
{ name: "path", help: "endpoint-relative route to expand (default: the endpoint root)" },
|
|
57
|
+
],
|
|
58
|
+
options: [
|
|
59
|
+
{
|
|
60
|
+
flags: "--depth <n>",
|
|
61
|
+
help: "how many directory levels to expand from the route root (0-10, default 0; deeper folders appear as [truncated: N], where N is that folder's total recursive .md count)",
|
|
62
|
+
},
|
|
63
|
+
{ flags: "--json", help: "emit the structured response envelope" },
|
|
64
|
+
],
|
|
65
|
+
singleEndpoint: {
|
|
66
|
+
endpointHelp: "select the endpoint to navigate",
|
|
67
|
+
unsupportedHelp: "not supported by nav; use --endpoint <name>",
|
|
68
|
+
},
|
|
69
|
+
};
|
|
70
|
+
|
|
71
|
+
interface NavCommandOptions extends Record<string, unknown> {
|
|
72
|
+
endpoint?: string;
|
|
73
|
+
depth?: string;
|
|
74
|
+
json?: boolean;
|
|
75
|
+
g?: boolean;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
export function parseNavArgs(args: readonly string[]): NavRequest {
|
|
79
|
+
const parsed = parseKitArgs<NavCommandOptions>(NAV_SPEC, args);
|
|
80
|
+
|
|
81
|
+
// Command-side semantics, pre-kit order: excess positionals, then depth
|
|
82
|
+
// form and range, then route-path lexical validation.
|
|
83
|
+
if (parsed.positionals.length > 1) {
|
|
84
|
+
throw new KitUsageError(
|
|
85
|
+
`unexpected argument '${parsed.positionals[1]}'; nav takes at most one [path] argument`,
|
|
86
|
+
);
|
|
87
|
+
}
|
|
88
|
+
let depth: number | undefined;
|
|
89
|
+
if (parsed.options.depth !== undefined) {
|
|
90
|
+
// Strict decimal form only: Number() would accept "1e1"/"0x2" variants
|
|
91
|
+
// that are almost certainly typos on a 0-10 flag.
|
|
92
|
+
if (!/^\d+$/.test(parsed.options.depth.trim())) {
|
|
93
|
+
throw new KitUsageError("--depth must be a non-negative integer");
|
|
94
|
+
}
|
|
95
|
+
const candidate = Number(parsed.options.depth.trim());
|
|
96
|
+
if (candidate < 0 || candidate > NAV_MAX_DEPTH) {
|
|
97
|
+
throw new KitUsageError(`--depth must be an integer between 0 and ${NAV_MAX_DEPTH}`);
|
|
98
|
+
}
|
|
99
|
+
depth = candidate;
|
|
100
|
+
}
|
|
101
|
+
if (parsed.positionals[0] !== undefined) {
|
|
102
|
+
validateNavRoutePath(parsed.positionals[0]);
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
return {
|
|
106
|
+
endpoint: parsed.scope.explicitEndpoints![0],
|
|
107
|
+
...(parsed.positionals[0] === undefined ? {} : { path: parsed.positionals[0] }),
|
|
108
|
+
...(depth === undefined ? {} : { depth }),
|
|
109
|
+
json: parsed.options.json === true,
|
|
110
|
+
};
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
export function executeNavCommand(
|
|
114
|
+
args: readonly string[],
|
|
115
|
+
context: NavContext,
|
|
116
|
+
): NavCommandResult | Promise<NavCommandResult> {
|
|
117
|
+
if (isHelpRequest(args)) {
|
|
118
|
+
return { exitCode: 0, stdout: renderNavHelp(), stderr: "" };
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
let request: NavRequest;
|
|
122
|
+
try {
|
|
123
|
+
request = parseNavArgs(args);
|
|
124
|
+
} catch (error) {
|
|
125
|
+
if (error instanceof HelpRequestError) {
|
|
126
|
+
return { exitCode: 0, stdout: renderNavHelp(), stderr: "" };
|
|
127
|
+
}
|
|
128
|
+
if (error instanceof KitUsageError) {
|
|
129
|
+
return { exitCode: 2, stdout: "", stderr: renderNavUsageError(error.message) };
|
|
130
|
+
}
|
|
131
|
+
throw error;
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
// Remote branch (ukp_remote W6): a remote binding routes through the
|
|
135
|
+
// remote transport; the sync local contract is unchanged
|
|
136
|
+
// (conditional-async seam, same as search/read).
|
|
137
|
+
const binding = readRegistry(context.registryPath).find((entry) => entry.name === request.endpoint);
|
|
138
|
+
if (binding !== undefined && isRemoteBinding(binding)) {
|
|
139
|
+
return executeRemoteNav(request, binding, context);
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
try {
|
|
143
|
+
const outcome = runNav(request, context);
|
|
144
|
+
if (!outcome.ok) {
|
|
145
|
+
// Single render source for failures too: the factual message is
|
|
146
|
+
// capability data; only the prefix, exit code, and hint wording are
|
|
147
|
+
// CLI renderings.
|
|
148
|
+
return {
|
|
149
|
+
exitCode: NAV_EXIT_BY_ERROR_CLASS[outcome.failure.errorClass],
|
|
150
|
+
stdout: "",
|
|
151
|
+
stderr: `ukp nav: ${outcome.failure.message}\n`,
|
|
152
|
+
};
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
const envelope = projectNavEnvelope(outcome.result);
|
|
156
|
+
// Diagnostics go to stderr in BOTH render modes (read's channel
|
|
157
|
+
// discipline: stdout is the payload, stderr is the operational
|
|
158
|
+
// channel). Without this, a Human-mode budget hit would be silent —
|
|
159
|
+
// violating ADR 0018's loud contract. JSON keeps the diagnostics in
|
|
160
|
+
// the envelope too.
|
|
161
|
+
const diagnosticLines = envelope.diagnostics
|
|
162
|
+
.map((diagnostic) => `ukp nav: ${diagnostic.code}: ${diagnostic.message}`)
|
|
163
|
+
.join("\n");
|
|
164
|
+
return {
|
|
165
|
+
exitCode: 0,
|
|
166
|
+
stdout: request.json ? renderNavJson(envelope) : renderNavHuman(envelope),
|
|
167
|
+
stderr: diagnosticLines.length > 0 ? `${diagnosticLines}\n` : "",
|
|
168
|
+
};
|
|
169
|
+
} catch (error) {
|
|
170
|
+
if (error instanceof NavUsageError) {
|
|
171
|
+
return { exitCode: 2, stdout: "", stderr: renderNavUsageError(error.message) };
|
|
172
|
+
}
|
|
173
|
+
if (error instanceof ScopeError) {
|
|
174
|
+
return {
|
|
175
|
+
exitCode: 1,
|
|
176
|
+
stdout: "",
|
|
177
|
+
stderr: `ukp nav: ${error.message}\nRun 'ukp list' to inspect registrations and 'ukp register' from a Service folder, then retry.\n`,
|
|
178
|
+
};
|
|
179
|
+
}
|
|
180
|
+
if (error instanceof ManifestError) {
|
|
181
|
+
return { exitCode: 1, stdout: "", stderr: `ukp nav: ${error.message}\n` };
|
|
182
|
+
}
|
|
183
|
+
if (error instanceof NavProviderError) {
|
|
184
|
+
return { exitCode: 1, stdout: "", stderr: `ukp nav: ${error.message}\n` };
|
|
185
|
+
}
|
|
186
|
+
return {
|
|
187
|
+
exitCode: 1,
|
|
188
|
+
stdout: "",
|
|
189
|
+
stderr: `ukp nav: ${error instanceof Error ? error.message : String(error)}\n`,
|
|
190
|
+
};
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
export function renderNavHelp(): string {
|
|
195
|
+
return renderKitHelp(NAV_SPEC);
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
export function renderNavUsageError(message: string): string {
|
|
199
|
+
return renderKitUsageError(NAV_SPEC, message);
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
function remoteTokenHint(endpointName: string): string {
|
|
203
|
+
return `endpoint '${endpointName}' requires a bearer token; set UKP_ENDPOINT_${endpointName.toUpperCase().replace(/-/g, "_")}_TOKEN`;
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
/** Wire error class → NavErrorClass. Nav's word-level route classes pass
|
|
207
|
+
* through; everything transport-shaped (auth, identity, route miss on an
|
|
208
|
+
* older serve, unreachable) folds into provider-unsupported with the
|
|
209
|
+
* specifics preserved in the message (W6 mapping, mirroring read's). */
|
|
210
|
+
function mapRemoteNavErrorClass(errorClass: string | undefined): NavErrorClass {
|
|
211
|
+
if (errorClass === "route-root-not-found") return "route-root-not-found";
|
|
212
|
+
if (errorClass === "route-root-not-directory") return "route-root-not-directory";
|
|
213
|
+
return "provider-unsupported";
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
/** Failure rendering identical to the sync path's outcome branch, with the
|
|
217
|
+
* remote-phase warnings appended to stderr. */
|
|
218
|
+
function remoteNavFailure(errorClass: NavErrorClass, message: string, warnings: string[]): NavCommandResult {
|
|
219
|
+
const stderr = [`ukp nav: ${message}`, ...warnings];
|
|
220
|
+
return { exitCode: NAV_EXIT_BY_ERROR_CLASS[errorClass], stdout: "", stderr: `${stderr.join("\n")}\n` };
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
/** GET /v1/nav over the wire; the wire envelope IS the local envelope, so
|
|
224
|
+
* the shared renderers finish the job with zero output divergence. */
|
|
225
|
+
async function executeRemoteNav(
|
|
226
|
+
request: NavRequest,
|
|
227
|
+
binding: RegistryBinding,
|
|
228
|
+
context: NavContext,
|
|
229
|
+
): Promise<NavCommandResult> {
|
|
230
|
+
const token = resolveRemoteToken(binding);
|
|
231
|
+
const warnings: string[] = [];
|
|
232
|
+
let transport: RemoteTransportHandle | undefined;
|
|
233
|
+
try {
|
|
234
|
+
transport = await openRemoteTransport(binding, { registryPath: context.registryPath });
|
|
235
|
+
const discovery = await fetchDiscoveryDocument(binding, transport, token);
|
|
236
|
+
warnings.push(...discovery.warnings);
|
|
237
|
+
if (discovery.bearerRequired && token === undefined) warnings.push(remoteTokenHint(binding.name));
|
|
238
|
+
} catch (error) {
|
|
239
|
+
transport?.close();
|
|
240
|
+
return remoteNavFailure(
|
|
241
|
+
"provider-unsupported",
|
|
242
|
+
error instanceof Error ? error.message : String(error),
|
|
243
|
+
warnings,
|
|
244
|
+
);
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
let result;
|
|
248
|
+
try {
|
|
249
|
+
result = await remoteNav(transport, token, {
|
|
250
|
+
...(request.path !== undefined ? { path: request.path } : {}),
|
|
251
|
+
...(request.depth !== undefined ? { depth: request.depth } : {}),
|
|
252
|
+
});
|
|
253
|
+
} catch (error) {
|
|
254
|
+
return remoteNavFailure(
|
|
255
|
+
"provider-unsupported",
|
|
256
|
+
error instanceof Error ? error.message : String(error),
|
|
257
|
+
warnings,
|
|
258
|
+
);
|
|
259
|
+
} finally {
|
|
260
|
+
transport.close();
|
|
261
|
+
}
|
|
262
|
+
if (result.ok && result.envelope !== undefined) {
|
|
263
|
+
// Diagnostics channel discipline matches the sync path (stdout is the
|
|
264
|
+
// payload, stderr is operational — in BOTH render modes).
|
|
265
|
+
const diagnosticLines = result.envelope.diagnostics
|
|
266
|
+
.map((diagnostic) => `ukp nav: ${diagnostic.code}: ${diagnostic.message}`)
|
|
267
|
+
.join("\n");
|
|
268
|
+
const operational = [...warnings, ...(diagnosticLines.length > 0 ? [diagnosticLines] : [])];
|
|
269
|
+
return {
|
|
270
|
+
exitCode: 0,
|
|
271
|
+
stdout: request.json ? renderNavJson(result.envelope) : renderNavHuman(result.envelope),
|
|
272
|
+
stderr: operational.length > 0 ? `${operational.join("\n")}\n` : "",
|
|
273
|
+
};
|
|
274
|
+
}
|
|
275
|
+
return remoteNavFailure(
|
|
276
|
+
mapRemoteNavErrorClass(result.errorClass),
|
|
277
|
+
result.errorMessage ?? `remote nav failed (status ${result.status})`,
|
|
278
|
+
warnings,
|
|
279
|
+
);
|
|
280
|
+
}
|