@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.
Files changed (44) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +208 -0
  3. package/bun.lock +35 -0
  4. package/package.json +51 -0
  5. package/src/artifacts.ts +75 -0
  6. package/src/capabilities/nav.ts +734 -0
  7. package/src/capabilities/propose.ts +456 -0
  8. package/src/capabilities/qmd.ts +174 -0
  9. package/src/capabilities/read.ts +1040 -0
  10. package/src/capabilities/remote-client.ts +856 -0
  11. package/src/capabilities/rename-recovery.ts +294 -0
  12. package/src/capabilities/rg.ts +505 -0
  13. package/src/capabilities/search.ts +1105 -0
  14. package/src/capabilities/tls-identity.ts +87 -0
  15. package/src/capabilities/update.ts +385 -0
  16. package/src/cli.ts +425 -0
  17. package/src/commands/diagnose.ts +383 -0
  18. package/src/commands/flags.ts +49 -0
  19. package/src/commands/guide-content/client.txt +36 -0
  20. package/src/commands/guide-content/propose.txt +38 -0
  21. package/src/commands/guide-content/service-qmd.txt +44 -0
  22. package/src/commands/guide-content/service.txt +114 -0
  23. package/src/commands/guide.ts +121 -0
  24. package/src/commands/init.ts +245 -0
  25. package/src/commands/inspect.ts +182 -0
  26. package/src/commands/inventory.ts +570 -0
  27. package/src/commands/kit.ts +269 -0
  28. package/src/commands/nav.ts +280 -0
  29. package/src/commands/propose.ts +257 -0
  30. package/src/commands/read.ts +558 -0
  31. package/src/commands/rg.ts +355 -0
  32. package/src/commands/search.ts +325 -0
  33. package/src/commands/serve.ts +221 -0
  34. package/src/commands/update.ts +99 -0
  35. package/src/config/client.ts +55 -0
  36. package/src/config/external-tool.ts +58 -0
  37. package/src/config/file-native.ts +111 -0
  38. package/src/config/manifest.ts +188 -0
  39. package/src/fslock.ts +95 -0
  40. package/src/path-safety.ts +65 -0
  41. package/src/registry.ts +436 -0
  42. package/src/scope.ts +78 -0
  43. package/src/server.ts +1011 -0
  44. 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
+ }