@catheadowl/ukp 0.2.6 → 0.3.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/README.md +83 -16
- package/SECURITY.md +69 -0
- package/bin/ukp.js +31 -7
- package/docs/remote-deployment.md +86 -8
- package/package.json +11 -6
- package/skills/ukp/SKILL.md +32 -0
- package/src/capabilities/nav.ts +209 -444
- package/src/capabilities/qmd.ts +19 -0
- package/src/capabilities/read.ts +18 -2
- package/src/capabilities/remote-client.ts +4 -2
- package/src/capabilities/search.ts +23 -7
- package/src/capabilities/session.ts +455 -0
- package/src/capabilities/tls-identity.ts +54 -3
- package/src/cli.ts +32 -64
- package/src/commands/guide-content/client.txt +19 -6
- package/src/commands/guide-content/remote.txt +21 -0
- package/src/commands/guide-content/rg.txt +2 -2
- package/src/commands/init.ts +224 -9
- package/src/commands/inventory.ts +277 -17
- package/src/commands/nav.ts +17 -12
- package/src/commands/read.ts +1 -1
- package/src/commands/rg.ts +2 -2
- package/src/commands/search.ts +19 -3
- package/src/commands/serve-task.ts +5 -5
- package/src/commands/serve.ts +313 -29
- package/src/commands/session.ts +541 -0
- package/src/commands/use.ts +178 -0
- package/src/config/agent-backed.ts +136 -0
- package/src/config/manifest.ts +39 -3
- package/src/manifest.yml +15 -0
- package/src/path-safety.ts +9 -4
- package/src/plumbing/token-file.ts +368 -0
- package/src/registry.ts +78 -1
- package/src/server.ts +228 -45
- package/src/spawn-relay.ts +21 -3
|
@@ -1,16 +1,26 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { join } from "node:path";
|
|
2
|
+
import { ENDPOINT_NAME, loadManifest, ServiceFolderInaccessibleError } from "../config/manifest.ts";
|
|
2
3
|
import { FILE_NATIVE_CAPABILITIES, isFileNativeCapability } from "../config/file-native.ts";
|
|
4
|
+
import { EXTERNAL_TOOL_CAPABILITIES } from "../config/external-tool.ts";
|
|
3
5
|
import { diagnoseService, type ProviderResolver } from "./diagnose.ts";
|
|
6
|
+
import {
|
|
7
|
+
assertUkpResidents,
|
|
8
|
+
ensureVersionVisibleUkpIgnore,
|
|
9
|
+
renderUkpIgnorePostureLine,
|
|
10
|
+
ukpIgnoreStarWarning,
|
|
11
|
+
} from "./init.ts";
|
|
4
12
|
import {
|
|
5
13
|
assertRegistrableRemoteUrl,
|
|
6
14
|
isRemoteBinding,
|
|
7
15
|
parseRemoteUrl,
|
|
8
16
|
readRegistry,
|
|
17
|
+
rebindAt,
|
|
9
18
|
registerAt,
|
|
10
19
|
registerRemoteAt,
|
|
11
20
|
unregisterAt,
|
|
12
21
|
type RegistryBinding,
|
|
13
22
|
} from "../registry.ts";
|
|
23
|
+
import { resolveScope, type ResolvedScope } from "../scope.ts";
|
|
14
24
|
import {
|
|
15
25
|
createTransportPool,
|
|
16
26
|
fetchDiscoveryDocument,
|
|
@@ -73,6 +83,7 @@ export const REGISTER_SPEC: UkpCommandSpec = {
|
|
|
73
83
|
strictArguments: true,
|
|
74
84
|
options: [
|
|
75
85
|
{ flags: "--url <url>", help: "register a remote ukp-serve endpoint (https://host[:port], ssh://host[/endpoint] - ssh takes no port, the woken door picks its own - or loopback http); a host door url (its document declares scope:\"host\") imports every endpoint behind the door - binding urls gain the endpoint-name path (ssh://ali/notes)" },
|
|
86
|
+
{ flags: "--rebind", help: "local only: relocate this endpoint name to the current Service folder in one step (the folder moved); the name must already be bound to a local folder - registering a name bound elsewhere without this flag is rejected with the recovery path" },
|
|
76
87
|
{ flags: "--endpoint <name>", help: "assert the remote-declared name of the endpoint being registered (expected-name assertion; with a host door this imports only that endpoint)" },
|
|
77
88
|
{ flags: "--name <handle>", help: "remote only: register under this local handle - the registry key and ukp:// authority - instead of the declared name; use it when the declared name is already taken" },
|
|
78
89
|
{ flags: "--select <names>", help: "with a host door url: import only the named endpoints (comma-separated); a name not on the door is a usage error" },
|
|
@@ -82,12 +93,19 @@ export const REGISTER_SPEC: UkpCommandSpec = {
|
|
|
82
93
|
"",
|
|
83
94
|
"Recovery:",
|
|
84
95
|
" Registering a name already bound to a different location is rejected.",
|
|
85
|
-
"
|
|
96
|
+
" If the Service folder moved, re-run register from the new folder with",
|
|
97
|
+
" --rebind to relocate the name in one step (local bindings only):",
|
|
98
|
+
" ukp register --rebind",
|
|
99
|
+
" Without the flag the two-step path stays available:",
|
|
86
100
|
" ukp unregister --endpoint <name>",
|
|
87
101
|
"",
|
|
88
102
|
"Binding:",
|
|
89
103
|
" The Host Registry binds the Service Manifest's effective name to this",
|
|
90
|
-
" folder's location; 'ukp list' shows the resulting bindings.",
|
|
104
|
+
" folder's location; 'ukp list' shows the resulting bindings. A local",
|
|
105
|
+
" register (plain or --rebind) also brings .ukp/.gitignore to the",
|
|
106
|
+
" precise-ignore posture: created when absent, a legacy '*' template",
|
|
107
|
+
" migrated so the declarations can be committed, an operator-owned",
|
|
108
|
+
" file left untouched.",
|
|
91
109
|
" With --url, the declared name and instance identity come from the",
|
|
92
110
|
" remote discovery document (/.well-known/ukp.json) and are pinned",
|
|
93
111
|
" TOFU-style as provenance; your registry binds them under a local",
|
|
@@ -111,11 +129,15 @@ export const REGISTER_SPEC: UkpCommandSpec = {
|
|
|
111
129
|
|
|
112
130
|
export const LIST_SPEC: UkpCommandSpec = {
|
|
113
131
|
name: "list",
|
|
114
|
-
summary: "list
|
|
132
|
+
summary: "list endpoint bindings - workspace scope by default, every registration with -g",
|
|
115
133
|
group: "registry",
|
|
116
|
-
description: "List
|
|
117
|
-
usage: "[
|
|
134
|
+
description: "List endpoint bindings and capabilities: the workspace scope by default (nearest .ukp/client.toml; every registered endpoint when there is none), or the full Host Registry with -g.",
|
|
135
|
+
usage: "[-g] [--json]",
|
|
118
136
|
strictArguments: true,
|
|
137
|
+
options: [
|
|
138
|
+
{ flags: "-g, --global", help: "list every registered endpoint (the full Host Registry), ignoring the workspace scope and without reading .ukp/client.toml" },
|
|
139
|
+
{ flags: "--json", help: "emit the offline machine-readable endpoint snapshot (schema ukp.list.v1; registry facts only - remote bindings are not probed, their capability fields are null); rides the same scope as the text listing" },
|
|
140
|
+
],
|
|
119
141
|
};
|
|
120
142
|
|
|
121
143
|
export const UNREGISTER_SPEC: UkpCommandSpec = {
|
|
@@ -142,6 +164,59 @@ const DEFAULT_CAPABILITIES = (Object.keys(FILE_NATIVE_CAPABILITIES) as Array<key
|
|
|
142
164
|
.filter((name) => FILE_NATIVE_CAPABILITIES[name].derived)
|
|
143
165
|
.sort();
|
|
144
166
|
|
|
167
|
+
/** Base-tier membership for the machine snapshot (ukp.list.v1): the
|
|
168
|
+
* declaration-free baseline every healthy local endpoint carries — the
|
|
169
|
+
* derived file-native set (nav, read; ADR 0016 rule 2, the header's own
|
|
170
|
+
* statement) plus the external-tool base tier (rg; ADR-RG-003). Membership
|
|
171
|
+
* only: rg's runtime availability still degrades by tool presence (tier
|
|
172
|
+
* semantics unchanged — `base_tier` is not a liveness claim). */
|
|
173
|
+
const BASE_TIER_CAPABILITIES = [
|
|
174
|
+
...DEFAULT_CAPABILITIES,
|
|
175
|
+
...(Object.keys(EXTERNAL_TOOL_CAPABILITIES) as Array<keyof typeof EXTERNAL_TOOL_CAPABILITIES>)
|
|
176
|
+
.filter((name) => EXTERNAL_TOOL_CAPABILITIES[name].baseTier),
|
|
177
|
+
].sort();
|
|
178
|
+
|
|
179
|
+
/** One endpoint as offline machine facts — the `ukp.list.v1` row (TODO
|
|
180
|
+
* 20261028-0053). The machine contract is a registry-snapshot projection:
|
|
181
|
+
* local rows read the Service Manifest (a local file read, never the
|
|
182
|
+
* network); remote rows carry registry facts only, because the registry
|
|
183
|
+
* pins no capability or liveness claims for remote bindings and the
|
|
184
|
+
* contract never probes — the text face's fresh discovery fetch is a
|
|
185
|
+
* per-invocation human affordance, not snapshot truth. Nullability is the
|
|
186
|
+
* contract's one rule: `null` = "not asserted in this snapshot". */
|
|
187
|
+
export interface ListEndpointSnapshot {
|
|
188
|
+
/** Registry handle — the ukp:// authority and the `--endpoint` key. */
|
|
189
|
+
name: string;
|
|
190
|
+
/** Local/remote discriminator (registry `kind`; absence = local). */
|
|
191
|
+
kind: "local" | "remote";
|
|
192
|
+
/** Binding location: the local canonical absolute path, or the
|
|
193
|
+
* registered remote url verbatim. Exactly one of path/url is non-null. */
|
|
194
|
+
location: { path: string | null; url: string | null };
|
|
195
|
+
/** Remote-declared name snapshot (ADR-REM-007 provenance); null for
|
|
196
|
+
* local bindings and legacy remote bindings that predate the field. */
|
|
197
|
+
declared_name: string | null;
|
|
198
|
+
/** Sorted declared capability extras (the text capabilities cell as an
|
|
199
|
+
* array); null on degraded local rows and on every remote row. */
|
|
200
|
+
capabilities: string[] | null;
|
|
201
|
+
/** Declaration-free baseline members in effect (nav/read/rg today);
|
|
202
|
+
* null on degraded local rows and on every remote row. */
|
|
203
|
+
base_tier: string[] | null;
|
|
204
|
+
/** Local rows: the Service Manifest loaded (true) or the row degraded
|
|
205
|
+
* (false). Remote rows: null — remote liveness is not an offline fact. */
|
|
206
|
+
available: boolean | null;
|
|
207
|
+
/** The same one-line degradation headline the text face emits on
|
|
208
|
+
* stderr; null when the row is not degraded. */
|
|
209
|
+
warning: string | null;
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
/** Envelope of the `--json` snapshot — the repo machine-face convention
|
|
213
|
+
* (versioned schema id + pretty-printed single document, as nav/rg). */
|
|
214
|
+
export interface ListSnapshotEnvelope {
|
|
215
|
+
schema: "ukp.list.v1";
|
|
216
|
+
command: "list";
|
|
217
|
+
endpoints: ListEndpointSnapshot[];
|
|
218
|
+
}
|
|
219
|
+
|
|
145
220
|
/** One list row as structured cells — column geometry is decided once, in
|
|
146
221
|
* renderListOutput, from every row's actual widths (raw tab stops made the
|
|
147
222
|
* columns land on a different multiple-of-8 per row once names, paths and
|
|
@@ -158,7 +233,23 @@ interface InventoryListRow {
|
|
|
158
233
|
warning?: string;
|
|
159
234
|
}
|
|
160
235
|
|
|
161
|
-
|
|
236
|
+
/** Offline facts for one local binding — the single producer BOTH faces
|
|
237
|
+
* consume (the same-truth seam of the machine contract: the text row and
|
|
238
|
+
* the ukp.list.v1 row are projections of this, never parallel assemblies).
|
|
239
|
+
* Manifest load failures degrade exactly like the text face: no capability
|
|
240
|
+
* assertions plus the same one-line warning. */
|
|
241
|
+
interface LocalListFacts {
|
|
242
|
+
name: string;
|
|
243
|
+
path: string;
|
|
244
|
+
/** Sorted declared extras; null on a degraded row. */
|
|
245
|
+
capabilities: string[] | null;
|
|
246
|
+
/** Baseline members; null on a degraded row. */
|
|
247
|
+
baseTier: string[] | null;
|
|
248
|
+
available: boolean;
|
|
249
|
+
warning: string | null;
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
function localListFacts(endpoint: { name: string; path: string }): LocalListFacts {
|
|
162
253
|
let service;
|
|
163
254
|
try {
|
|
164
255
|
service = loadManifest(endpoint.path);
|
|
@@ -166,17 +257,100 @@ function renderLocalListRow(endpoint: { name: string; path: string }): Inventory
|
|
|
166
257
|
// One-line headline only: an inventory warning must stay scannable even
|
|
167
258
|
// when the underlying ManifestError carries a full zod schema dump.
|
|
168
259
|
const headline = (error instanceof Error ? error.message : String(error)).split("\n")[0];
|
|
260
|
+
// A dead binding (folder unreachable) is a binding-level failure, not a
|
|
261
|
+
// capability degradation: the binding itself routes nowhere. It gets its
|
|
262
|
+
// own frame plus the one-step rebind remedy (dead-binding item 1; the
|
|
263
|
+
// `declared capabilities` qualifier stays reserved for manifest-level
|
|
264
|
+
// failures where the folder IS reachable — probe-anchored wording).
|
|
265
|
+
if (error instanceof ServiceFolderInaccessibleError) {
|
|
266
|
+
return {
|
|
267
|
+
name: endpoint.name,
|
|
268
|
+
path: endpoint.path,
|
|
269
|
+
capabilities: null,
|
|
270
|
+
baseTier: null,
|
|
271
|
+
available: false,
|
|
272
|
+
warning: `endpoint '${endpoint.name}' binding is dead: ${headline}; `
|
|
273
|
+
+ `if the folder moved, run 'ukp register --rebind' from the new folder `
|
|
274
|
+
+ `(or 'ukp unregister --endpoint ${endpoint.name}')`,
|
|
275
|
+
};
|
|
276
|
+
}
|
|
169
277
|
return {
|
|
170
278
|
name: endpoint.name,
|
|
171
|
-
|
|
172
|
-
capabilities:
|
|
279
|
+
path: endpoint.path,
|
|
280
|
+
capabilities: null,
|
|
281
|
+
baseTier: null,
|
|
282
|
+
available: false,
|
|
173
283
|
warning: `endpoint '${endpoint.name}' declared capabilities unavailable: ${headline}`,
|
|
174
284
|
};
|
|
175
285
|
}
|
|
176
286
|
const extras = Object.keys(service.manifest.capabilities)
|
|
177
287
|
.filter((name) => !(isFileNativeCapability(name) && FILE_NATIVE_CAPABILITIES[name].derived))
|
|
178
288
|
.sort();
|
|
179
|
-
return { name: endpoint.name,
|
|
289
|
+
return { name: endpoint.name, path: endpoint.path, capabilities: extras, baseTier: [...BASE_TIER_CAPABILITIES], available: true, warning: null };
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
function renderLocalListRow(endpoint: { name: string; path: string }): InventoryListRow {
|
|
293
|
+
const facts = localListFacts(endpoint);
|
|
294
|
+
return {
|
|
295
|
+
name: facts.name,
|
|
296
|
+
location: facts.path,
|
|
297
|
+
capabilities: facts.available
|
|
298
|
+
? (facts.capabilities!.length > 0 ? facts.capabilities!.join(",") : "-")
|
|
299
|
+
: "(unavailable)",
|
|
300
|
+
...(facts.warning !== null ? { warning: facts.warning } : {}),
|
|
301
|
+
};
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
/** ukp.list.v1 projection of one local binding — the same localListFacts
|
|
305
|
+
* the text row renders from, shaped as snapshot fields. */
|
|
306
|
+
function localSnapshotRow(endpoint: { name: string; path: string }): ListEndpointSnapshot {
|
|
307
|
+
const facts = localListFacts(endpoint);
|
|
308
|
+
return {
|
|
309
|
+
name: facts.name,
|
|
310
|
+
kind: "local",
|
|
311
|
+
location: { path: facts.path, url: null },
|
|
312
|
+
declared_name: null,
|
|
313
|
+
capabilities: facts.capabilities,
|
|
314
|
+
base_tier: facts.baseTier,
|
|
315
|
+
available: facts.available,
|
|
316
|
+
warning: facts.warning,
|
|
317
|
+
};
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
/** ukp.list.v1 row for one remote binding: pure registry projection, no
|
|
321
|
+
* transport. The handle stays the bare `name` — the text face's
|
|
322
|
+
* `name (declares X)` cell is a rendering; here the declaration rides in
|
|
323
|
+
* `declared_name` as structure. Capability and liveness fields are null
|
|
324
|
+
* per the offline contract (the registry pins none for remotes). */
|
|
325
|
+
function remoteSnapshotRow(endpoint: RegistryBinding): ListEndpointSnapshot {
|
|
326
|
+
return {
|
|
327
|
+
name: endpoint.name,
|
|
328
|
+
kind: "remote",
|
|
329
|
+
location: { path: null, url: endpoint.url! },
|
|
330
|
+
declared_name: endpoint.declared_name ?? null,
|
|
331
|
+
capabilities: null,
|
|
332
|
+
base_tier: null,
|
|
333
|
+
available: null,
|
|
334
|
+
warning: null,
|
|
335
|
+
};
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
/** `--json` emission (TODO 20261028-0053 machine contract): one
|
|
339
|
+
* pretty-printed document in the repo machine-face convention, rows in the
|
|
340
|
+
* text face's order (locals first, then remotes, each in registry order).
|
|
341
|
+
* Offline and synchronous by construction — no transport pool is ever
|
|
342
|
+
* created for this path, so a registry full of remote bindings costs zero
|
|
343
|
+
* network. stderr stays empty in this mode: degradation truth rides in the
|
|
344
|
+
* rows' `warning` fields. */
|
|
345
|
+
function renderListJsonSnapshot(endpoints: readonly RegistryBinding[]): InventoryCommandResult {
|
|
346
|
+
const rows = [
|
|
347
|
+
...endpoints
|
|
348
|
+
.filter((endpoint) => !isRemoteBinding(endpoint))
|
|
349
|
+
.map((endpoint) => localSnapshotRow({ name: endpoint.name, path: endpoint.path! })),
|
|
350
|
+
...endpoints.filter(isRemoteBinding).map(remoteSnapshotRow),
|
|
351
|
+
];
|
|
352
|
+
const envelope: ListSnapshotEnvelope = { schema: "ukp.list.v1", command: "list", endpoints: rows };
|
|
353
|
+
return { exitCode: 0, stdout: `${JSON.stringify(envelope, null, 2)}\n`, stderr: "" };
|
|
180
354
|
}
|
|
181
355
|
|
|
182
356
|
export function executeRegisterCommand(
|
|
@@ -188,7 +362,7 @@ export function executeRegisterCommand(
|
|
|
188
362
|
}
|
|
189
363
|
|
|
190
364
|
try {
|
|
191
|
-
const parsed = parseKitArgs<{ url?: string; token?: string; select?: string; endpoint?: string; name?: string }>(REGISTER_SPEC, args);
|
|
365
|
+
const parsed = parseKitArgs<{ url?: string; token?: string; select?: string; endpoint?: string; name?: string; rebind?: boolean }>(REGISTER_SPEC, args);
|
|
192
366
|
const expectedName = parsed.options.endpoint;
|
|
193
367
|
if (expectedName !== undefined && !ENDPOINT_NAME.test(expectedName)) {
|
|
194
368
|
throw new KitUsageError("--endpoint requires a valid endpoint name");
|
|
@@ -202,6 +376,11 @@ export function executeRegisterCommand(
|
|
|
202
376
|
// folder owner controls; the registry naming slot is remote-only.
|
|
203
377
|
throw new KitUsageError("--name chooses a remote registration handle and requires --url");
|
|
204
378
|
}
|
|
379
|
+
if (parsed.options.rebind === true && parsed.options.url !== undefined) {
|
|
380
|
+
// --rebind is the narrow local relocation exit; remote handles keep
|
|
381
|
+
// the unregister path (registry-commands rebind-one-step, 2026-10-24).
|
|
382
|
+
throw new KitUsageError("--rebind relocates a local binding and cannot be combined with --url");
|
|
383
|
+
}
|
|
205
384
|
if (parsed.options.url !== undefined) {
|
|
206
385
|
return executeRegisterRemote(parsed.options.url, parsed.options.token, parsed.options.select, expectedName, handleName, context);
|
|
207
386
|
}
|
|
@@ -212,11 +391,45 @@ export function executeRegisterCommand(
|
|
|
212
391
|
if (expectedName !== undefined && expectedName !== report.service.effectiveName) {
|
|
213
392
|
throw new Error(`expected endpoint '${expectedName}', but this Service effective name is '${report.service.effectiveName}'`);
|
|
214
393
|
}
|
|
394
|
+
// INV-21 admission gate BEFORE the binding write: a foreign resident in
|
|
395
|
+
// .ukp/ refuses the whole gesture — registry untouched, guard not
|
|
396
|
+
// rewritten over material it no longer covers (the 2026-11-02 KB shape:
|
|
397
|
+
// a '*' guard swapped onto the current template above a legacy tls/).
|
|
398
|
+
const ukpDirectory = join(report.service.folder, ".ukp");
|
|
399
|
+
assertUkpResidents(ukpDirectory, { endpointName: report.service.effectiveName });
|
|
400
|
+
if (parsed.options.rebind === true) {
|
|
401
|
+
// Diagnosis already validated the new Service folder — a failed rebind
|
|
402
|
+
// (unknown name, remote handle, taken location) leaves the old binding
|
|
403
|
+
// untouched; the swap itself is one atomic registry transition.
|
|
404
|
+
const previous = readRegistry(context.registryPath).find(
|
|
405
|
+
(endpoint) => endpoint.name === report.service.effectiveName,
|
|
406
|
+
);
|
|
407
|
+
rebindAt(context.registryPath, report.service.effectiveName, report.service.folder);
|
|
408
|
+
// The service-side version-control step rides the same binding
|
|
409
|
+
// gestures (use is the client-face carrier; a pure Service folder —
|
|
410
|
+
// the fresh-clone revival path this command IS — has no reason to
|
|
411
|
+
// run use, and re-running init is refused once the manifest exists).
|
|
412
|
+
const posture = ensureVersionVisibleUkpIgnore(ukpDirectory);
|
|
413
|
+
const lines = [
|
|
414
|
+
`rebound: ${report.service.effectiveName}`,
|
|
415
|
+
`from: ${previous?.path ?? "(unknown)"}`,
|
|
416
|
+
...(report.service.manifest.description ? [`description: ${report.service.manifest.description}`] : []),
|
|
417
|
+
`location: ${report.service.folder}`,
|
|
418
|
+
renderUkpIgnorePostureLine(posture, ukpDirectory),
|
|
419
|
+
];
|
|
420
|
+
return {
|
|
421
|
+
exitCode: 0,
|
|
422
|
+
stdout: lines.join("\n"),
|
|
423
|
+
stderr: posture === "operator-owned-star" ? ukpIgnoreStarWarning(ukpDirectory) : "",
|
|
424
|
+
};
|
|
425
|
+
}
|
|
215
426
|
registerAt(context.registryPath, report.service.effectiveName, report.service.folder);
|
|
427
|
+
const posture = ensureVersionVisibleUkpIgnore(ukpDirectory);
|
|
216
428
|
const lines = [
|
|
217
429
|
`registered: ${report.service.effectiveName}`,
|
|
218
430
|
...(report.service.manifest.description ? [`description: ${report.service.manifest.description}`] : []),
|
|
219
431
|
`location: ${report.service.folder}`,
|
|
432
|
+
renderUkpIgnorePostureLine(posture, ukpDirectory),
|
|
220
433
|
// Register never edits Client Config (guide client owns that); this is
|
|
221
434
|
// a pointer for the cross-workspace consumption case, which is the
|
|
222
435
|
// standard follow-up after registering a Service others will reference.
|
|
@@ -225,7 +438,7 @@ export function executeRegisterCommand(
|
|
|
225
438
|
return {
|
|
226
439
|
exitCode: 0,
|
|
227
440
|
stdout: lines.join("\n"),
|
|
228
|
-
stderr: "",
|
|
441
|
+
stderr: posture === "operator-owned-star" ? ukpIgnoreStarWarning(ukpDirectory) : "",
|
|
229
442
|
};
|
|
230
443
|
} catch (error) {
|
|
231
444
|
if (error instanceof HelpRequestError) {
|
|
@@ -541,14 +754,58 @@ export function executeListCommand(
|
|
|
541
754
|
}
|
|
542
755
|
|
|
543
756
|
try {
|
|
544
|
-
parseKitArgs(LIST_SPEC, args);
|
|
545
|
-
|
|
757
|
+
const parsed = parseKitArgs<{ json?: boolean; global?: boolean }>(LIST_SPEC, args);
|
|
758
|
+
// Scope semantics (owner ruling 2026-10-28 / ADR 0031, restoring the
|
|
759
|
+
// 13ae382 clause): list eats the workspace scope like every consumer
|
|
760
|
+
// command — the nearest client.toml's default_endpoints by default,
|
|
761
|
+
// full-registry fallback when there is none (bare machines unchanged) —
|
|
762
|
+
// and -g lists the full Host Registry without reading the config. No
|
|
763
|
+
// machine-face exemption: --json rides the same resolution (the
|
|
764
|
+
// exemption pattern is exactly what let the 09-21 contract ratify the
|
|
765
|
+
// implementation drift); ukp.list.v1's fields are unchanged, only the
|
|
766
|
+
// row set is selected.
|
|
767
|
+
const registry = readRegistry(context.registryPath);
|
|
768
|
+
let endpoints: readonly RegistryBinding[];
|
|
769
|
+
let scopeWarnings: string[] = [];
|
|
770
|
+
if (parsed.options.global === true) {
|
|
771
|
+
endpoints = registry;
|
|
772
|
+
} else {
|
|
773
|
+
let scope: ResolvedScope;
|
|
774
|
+
try {
|
|
775
|
+
scope = resolveScope({ currentDirectory: context.currentDirectory, registry });
|
|
776
|
+
} catch (error) {
|
|
777
|
+
// Same law as the scope family, no list exemption: an invalid
|
|
778
|
+
// client.toml or one that resolves to nothing fails the listing —
|
|
779
|
+
// with the -g escape taught in place (it never reads the config).
|
|
780
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
781
|
+
return {
|
|
782
|
+
exitCode: 1,
|
|
783
|
+
stdout: "",
|
|
784
|
+
stderr: `ukp list: ${message}; use 'ukp list -g' to list every registered endpoint\n`,
|
|
785
|
+
};
|
|
786
|
+
}
|
|
787
|
+
endpoints = scope.bindings;
|
|
788
|
+
scopeWarnings = scope.warnings;
|
|
789
|
+
}
|
|
790
|
+
if (parsed.options.json === true) {
|
|
791
|
+
// Machine contract (ukp.list.v1 / TODO 20261028-0053): the offline
|
|
792
|
+
// snapshot of the SELECTED endpoints. Returns before any transport
|
|
793
|
+
// pool exists — see renderListJsonSnapshot; the sync return with
|
|
794
|
+
// remote bindings present is the structural pin of the no-network
|
|
795
|
+
// promise. Scope-level dangling warnings ride stderr (same strings
|
|
796
|
+
// as the text face); row-level degradation truth stays in rows.
|
|
797
|
+
const snapshot = renderListJsonSnapshot(endpoints);
|
|
798
|
+
return {
|
|
799
|
+
...snapshot,
|
|
800
|
+
stderr: scopeWarnings.length > 0 ? `${scopeWarnings.join("\n")}\n` : "",
|
|
801
|
+
};
|
|
802
|
+
}
|
|
546
803
|
if (endpoints.length === 0) {
|
|
547
804
|
return { exitCode: 0, stdout: "No endpoints registered.", stderr: "" };
|
|
548
805
|
}
|
|
549
806
|
const remotes = endpoints.filter(isRemoteBinding);
|
|
550
807
|
if (remotes.length === 0) {
|
|
551
|
-
return renderListOutput(endpoints.map((endpoint) => renderLocalListRow({ name: endpoint.name, path: endpoint.path! })));
|
|
808
|
+
return renderListOutput(endpoints.map((endpoint) => renderLocalListRow({ name: endpoint.name, path: endpoint.path! })), scopeWarnings);
|
|
552
809
|
}
|
|
553
810
|
// Remote rows fetch the discovery document (fresh per invocation); the
|
|
554
811
|
// same conditional-async seam as search/read keeps all-local listings
|
|
@@ -640,7 +897,9 @@ export function executeListCommand(
|
|
|
640
897
|
// drift fetch may be the slowest leg and a spinner under a finished
|
|
641
898
|
// table would be noise.
|
|
642
899
|
clearProgress();
|
|
643
|
-
|
|
900
|
+
// Scope dangling warnings lead (they qualify the selection itself),
|
|
901
|
+
// then row degradation, then the same-instance/drift notes.
|
|
902
|
+
const stderrLines = [...scopeWarnings, ...warnings, ...collectSameInstanceNotes(endpoints), ...(await drift)];
|
|
644
903
|
return {
|
|
645
904
|
exitCode: 0,
|
|
646
905
|
stdout: context.emitStdout === undefined ? lines.join("\n") : "",
|
|
@@ -663,9 +922,10 @@ export function executeListCommand(
|
|
|
663
922
|
|
|
664
923
|
function renderListOutput(
|
|
665
924
|
rows: ReadonlyArray<InventoryListRow>,
|
|
925
|
+
leadingNotes: readonly string[] = [],
|
|
666
926
|
extraNotes: readonly string[] = [],
|
|
667
927
|
): InventoryCommandResult {
|
|
668
|
-
const warnings = [...rows.flatMap((row) => (row.warning !== undefined ? [row.warning] : [])), ...extraNotes];
|
|
928
|
+
const warnings = [...leadingNotes, ...rows.flatMap((row) => (row.warning !== undefined ? [row.warning] : [])), ...extraNotes];
|
|
669
929
|
const stdout = [listHeaderLine(), ...rows.map((row) => renderListLine(row, listRowGeometry(rows)))].join("\n");
|
|
670
930
|
return {
|
|
671
931
|
exitCode: 0,
|
package/src/commands/nav.ts
CHANGED
|
@@ -58,7 +58,7 @@ export const NAV_SPEC: UkpCommandSpec = {
|
|
|
58
58
|
options: [
|
|
59
59
|
{
|
|
60
60
|
flags: "--depth <n>",
|
|
61
|
-
help: "how many directory levels to expand from the route root (0-10, default 0; deeper folders appear as
|
|
61
|
+
help: "how many directory levels to expand from the route root (0-10, default 0; deeper folders appear as folder/ (+N .md), where N is that folder's total recursive .md count)",
|
|
62
62
|
},
|
|
63
63
|
{ flags: "--json", help: "emit the structured response envelope" },
|
|
64
64
|
],
|
|
@@ -110,10 +110,10 @@ export function parseNavArgs(args: readonly string[]): NavRequest {
|
|
|
110
110
|
};
|
|
111
111
|
}
|
|
112
112
|
|
|
113
|
-
export function executeNavCommand(
|
|
113
|
+
export async function executeNavCommand(
|
|
114
114
|
args: readonly string[],
|
|
115
115
|
context: NavContext,
|
|
116
|
-
):
|
|
116
|
+
): Promise<NavCommandResult> {
|
|
117
117
|
if (isHelpRequest(args)) {
|
|
118
118
|
return { exitCode: 0, stdout: renderNavHelp(), stderr: "" };
|
|
119
119
|
}
|
|
@@ -132,15 +132,15 @@ export function executeNavCommand(
|
|
|
132
132
|
}
|
|
133
133
|
|
|
134
134
|
// Remote branch (ukp_remote W6): a remote binding routes through the
|
|
135
|
-
// remote transport;
|
|
136
|
-
//
|
|
135
|
+
// remote transport; local and remote now share one async contract (the
|
|
136
|
+
// scan-engine delegation made the local branch async, 2026-10-22).
|
|
137
137
|
const binding = readRegistry(context.registryPath).find((entry) => entry.name === request.endpoint);
|
|
138
138
|
if (binding !== undefined && isRemoteBinding(binding)) {
|
|
139
139
|
return executeRemoteNav(request, binding, context);
|
|
140
140
|
}
|
|
141
141
|
|
|
142
142
|
try {
|
|
143
|
-
const outcome = runNav(request, context);
|
|
143
|
+
const outcome = await runNav(request, context);
|
|
144
144
|
if (!outcome.ok) {
|
|
145
145
|
// Single render source for failures too: the factual message is
|
|
146
146
|
// capability data; only the prefix, exit code, and hint wording are
|
|
@@ -153,18 +153,23 @@ export function executeNavCommand(
|
|
|
153
153
|
}
|
|
154
154
|
|
|
155
155
|
const envelope = projectNavEnvelope(outcome.result);
|
|
156
|
-
//
|
|
157
|
-
// discipline: stdout is the payload, stderr is the operational
|
|
158
|
-
//
|
|
159
|
-
//
|
|
160
|
-
// the envelope too.
|
|
156
|
+
// Consumer diagnostics go to stderr in BOTH render modes (read's channel
|
|
157
|
+
// discipline: stdout is the payload, stderr is the operational channel);
|
|
158
|
+
// JSON keeps them in the envelope too. Maintainer findings (E1,
|
|
159
|
+
// 2026-10-22) are human-face stderr only — the JSON face stays silent.
|
|
161
160
|
const diagnosticLines = envelope.diagnostics
|
|
162
161
|
.map((diagnostic) => `ukp nav: ${diagnostic.code}: ${diagnostic.message}`)
|
|
163
162
|
.join("\n");
|
|
163
|
+
const maintainerLines = request.json
|
|
164
|
+
? ""
|
|
165
|
+
: outcome.result.maintainerDiagnostics
|
|
166
|
+
.map((diagnostic) => `ukp nav: ${diagnostic.code}: ${diagnostic.message}`)
|
|
167
|
+
.join("\n");
|
|
168
|
+
const operational = [diagnosticLines, maintainerLines].filter((block) => block.length > 0).join("\n");
|
|
164
169
|
return {
|
|
165
170
|
exitCode: 0,
|
|
166
171
|
stdout: request.json ? renderNavJson(envelope) : renderNavHuman(envelope),
|
|
167
|
-
stderr:
|
|
172
|
+
stderr: operational.length > 0 ? `${operational}\n` : "",
|
|
168
173
|
};
|
|
169
174
|
} catch (error) {
|
|
170
175
|
if (error instanceof NavUsageError) {
|
package/src/commands/read.ts
CHANGED
|
@@ -66,7 +66,7 @@ const READ_EXIT_BY_ERROR_CLASS: Record<ReadErrorClass, number> = {
|
|
|
66
66
|
* dispatch and its precedence stay command-side. */
|
|
67
67
|
export const READ_SPEC: UkpCommandSpec = {
|
|
68
68
|
name: "read",
|
|
69
|
-
summary: "read an endpoint-scoped resource (exact path, ukp:// URI,
|
|
69
|
+
summary: "read an endpoint-scoped resource (exact path, ukp:// URI, a docid from search results, or document-relative via --from)",
|
|
70
70
|
group: "endpoint",
|
|
71
71
|
description:
|
|
72
72
|
"Read an endpoint-scoped resource reference from one Service endpoint (endpoint names come from 'ukp list'). "
|
package/src/commands/rg.ts
CHANGED
|
@@ -91,8 +91,8 @@ export const RG_SPEC: UkpCommandSpec = {
|
|
|
91
91
|
"",
|
|
92
92
|
"Visibility:",
|
|
93
93
|
" Every rg mode skips hidden files (dot-prefixed, including the .ukp/",
|
|
94
|
-
" wiring) by default; pass -- --hidden after '--' to include them",
|
|
95
|
-
"
|
|
94
|
+
" wiring) by default; pass -- --hidden after '--' to include them, and",
|
|
95
|
+
" -- --no-ignore to also lift .gitignore rules. --files lists every",
|
|
96
96
|
" non-hidden file, including empty and binary ones.",
|
|
97
97
|
"",
|
|
98
98
|
].join("\n"),
|
package/src/commands/search.ts
CHANGED
|
@@ -51,14 +51,15 @@ export const SEARCH_SPEC: UkpCommandSpec = {
|
|
|
51
51
|
name: "search",
|
|
52
52
|
summary: "search a Service's indexed content (provider-backed; use 'ukp rg' to grep raw files)",
|
|
53
53
|
group: "endpoint",
|
|
54
|
-
description: "Run
|
|
55
|
-
usage: "<query> [--limit <1-1000>] [--endpoint <name> ... | -g] [--recursive] [--json]",
|
|
54
|
+
description: "Run a lexical (text) search across the selected Service endpoints through each endpoint's configured provider.",
|
|
55
|
+
usage: "<query> [--limit <1-1000>] [--endpoint <name> ... | -g] [--recursive] [--timeout <seconds>] [--json]",
|
|
56
56
|
arguments: [{ name: "query", help: "one non-empty search query; quote multi-word queries" }],
|
|
57
57
|
options: [
|
|
58
58
|
{ flags: "--limit <1-1000>", help: "maximum results requested from each endpoint (default: 20)" },
|
|
59
59
|
{ flags: "-c, --endpoint <name>", help: "select one endpoint; repeat to select multiple endpoints", multi: true },
|
|
60
60
|
{ flags: "-g", help: "search every endpoint in the Host Registry; takes no value" },
|
|
61
61
|
{ flags: "--recursive", help: "include direct authority and context dependencies" },
|
|
62
|
+
{ flags: "--timeout <seconds>", help: "provider spawn timeout for this search in whole seconds; 0 disables the guard (wait indefinitely - interrupt is then the only cancel); default 60 (UKP_PROVIDER_TIMEOUT_MS applies when the flag is absent)" },
|
|
62
63
|
{ flags: "--json", help: "write provider-native results to artifacts and print an envelope" },
|
|
63
64
|
],
|
|
64
65
|
helpSuffix: [
|
|
@@ -86,6 +87,7 @@ interface SearchCommandOptions extends Record<string, unknown> {
|
|
|
86
87
|
endpoint?: string[];
|
|
87
88
|
g?: boolean;
|
|
88
89
|
recursive?: boolean;
|
|
90
|
+
timeout?: string;
|
|
89
91
|
json?: boolean;
|
|
90
92
|
}
|
|
91
93
|
|
|
@@ -106,6 +108,20 @@ export function parseSearchArgs(args: readonly string[]): ParsedSearch {
|
|
|
106
108
|
}
|
|
107
109
|
}
|
|
108
110
|
|
|
111
|
+
// Owner ruling 2026-10-28 (TODO 20261028-1113 option 3): whole seconds,
|
|
112
|
+
// 0 = disable the guard (the classic-interactive "zero disables"
|
|
113
|
+
// grammar), positive wins over the env override.
|
|
114
|
+
let timeoutSeconds: number | undefined;
|
|
115
|
+
if (parsed.options.timeout !== undefined) {
|
|
116
|
+
if (!/^[0-9]+$/.test(parsed.options.timeout)) {
|
|
117
|
+
throw new KitUsageError("--timeout must be a non-negative integer number of seconds (0 disables the guard)");
|
|
118
|
+
}
|
|
119
|
+
timeoutSeconds = Number(parsed.options.timeout);
|
|
120
|
+
if (!Number.isSafeInteger(timeoutSeconds)) {
|
|
121
|
+
throw new KitUsageError("--timeout is out of range");
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
|
|
109
125
|
for (const endpoint of parsed.options.endpoint ?? []) {
|
|
110
126
|
if (explicit.includes(endpoint)) warnings.push(`duplicate endpoint '${endpoint}' ignored`);
|
|
111
127
|
else explicit.push(endpoint);
|
|
@@ -123,7 +139,7 @@ export function parseSearchArgs(args: readonly string[]): ParsedSearch {
|
|
|
123
139
|
throw new KitUsageError("--endpoint and -g cannot be used together");
|
|
124
140
|
}
|
|
125
141
|
return {
|
|
126
|
-
request: { query, limit },
|
|
142
|
+
request: { query, limit, ...(timeoutSeconds !== undefined ? { timeoutSeconds } : {}) },
|
|
127
143
|
options: {
|
|
128
144
|
explicitEndpoints: explicit.length > 0 ? explicit : undefined,
|
|
129
145
|
global: parsed.options.g ?? false,
|
|
@@ -301,11 +301,11 @@ export function renderServeTaskArtifacts(input: ServeTaskInput): string {
|
|
|
301
301
|
: []),
|
|
302
302
|
...(input.tls?.mode === "self-signed" && input.endpoint !== undefined
|
|
303
303
|
? [
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
"
|
|
307
|
-
"
|
|
308
|
-
"
|
|
304
|
+
` The --tls identity is host-local: %USERPROFILE%\\.ukp\\tls\\`,
|
|
305
|
+
` ${input.endpoint}\\ - beside the host registry, never inside`,
|
|
306
|
+
" the knowledge folder (git or cloud sync cannot carry it out;",
|
|
307
|
+
" a pre-2026-11-01 .ukp\\tls\\ moved there on first serve).",
|
|
308
|
+
" Delete that folder with the endpoint if you are done.",
|
|
309
309
|
]
|
|
310
310
|
: []),
|
|
311
311
|
...(input.tls?.mode === "self-signed" && input.endpoint === undefined
|