@catheadowl/ukp 0.2.3 → 0.2.5
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 +5 -2
- package/bin/ukp.js +31 -11
- package/docs/remote-deployment.md +7 -0
- package/package.json +1 -1
- package/src/capabilities/propose.ts +1 -1
- package/src/capabilities/read.ts +1 -1
- package/src/capabilities/remote-client.ts +361 -70
- package/src/capabilities/rg.ts +148 -21
- package/src/capabilities/search.ts +2 -2
- package/src/cli.ts +73 -4
- package/src/commands/guide-content/propose.txt +1 -1
- package/src/commands/guide-content/remote.txt +9 -7
- package/src/commands/guide-content/rg.txt +35 -0
- package/src/commands/guide-content/service-qmd.txt +1 -1
- package/src/commands/guide.ts +11 -4
- package/src/commands/inventory.ts +182 -36
- package/src/commands/propose.ts +1 -1
- package/src/commands/read.ts +6 -6
- package/src/commands/rg.ts +45 -14
- package/src/commands/serve-task.ts +17 -17
- package/src/commands/serve.ts +22 -22
- package/src/registry.ts +2 -2
- package/src/server.ts +41 -26
- package/src/spawn-relay.ts +57 -0
|
@@ -36,6 +36,22 @@ export interface InventoryCommandContext {
|
|
|
36
36
|
resolveProvider?: ProviderResolver;
|
|
37
37
|
/** Test injection point for the ssh binary behind ssh:// transports. */
|
|
38
38
|
sshCommand?: readonly string[];
|
|
39
|
+
/** Streaming seam (ukp_list W2 / ADR 0026 rule 3): when present, the
|
|
40
|
+
* remote-mixed branch delivers stdout line-by-line through it — the header
|
|
41
|
+
* and every local row flush synchronously before any network work starts,
|
|
42
|
+
* remote rows append in registry order as they resolve — and the returned
|
|
43
|
+
* result carries an empty stdout (the output already went out on this
|
|
44
|
+
* channel; writeCommandResult's falsy check skips the final print).
|
|
45
|
+
* Absent, the full text returns in result.stdout unchanged. The all-local
|
|
46
|
+
* path never streams: it stays synchronous and byte-identical either
|
|
47
|
+
* way. */
|
|
48
|
+
emitStdout?: (line: string) => void;
|
|
49
|
+
/** Progress seam (ukp_list W2 / ADR 0026 rule 3): self-contained stderr
|
|
50
|
+
* frames for the remote fetch — the first frame is plain text, updates
|
|
51
|
+
* carry a leading \r, the final frame clears the line. The wiring layer
|
|
52
|
+
* injects it only when stderr is a TTY, so piped runs stay silent and
|
|
53
|
+
* stdout never gains a control character. */
|
|
54
|
+
emitProgress?: (frame: string) => void;
|
|
39
55
|
}
|
|
40
56
|
|
|
41
57
|
export interface InventoryCommandResult {
|
|
@@ -52,13 +68,13 @@ export const REGISTER_SPEC: UkpCommandSpec = {
|
|
|
52
68
|
summary: "register a Service endpoint",
|
|
53
69
|
group: "registry",
|
|
54
70
|
description:
|
|
55
|
-
"Register the current Service folder, a remote ukp-serve endpoint via --url, or
|
|
71
|
+
"Register the current Service folder, a remote ukp-serve endpoint via --url, or - when that url is a host door - every endpoint behind it in one gesture.",
|
|
56
72
|
usage: "[options]",
|
|
57
73
|
strictArguments: true,
|
|
58
74
|
options: [
|
|
59
|
-
{ flags: "--url <url>", help: "register a remote ukp-serve endpoint (https://host[:port], ssh://host[/endpoint]
|
|
75
|
+
{ 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)" },
|
|
60
76
|
{ 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)" },
|
|
61
|
-
{ flags: "--name <handle>", help: "remote only: register under this local handle
|
|
77
|
+
{ 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" },
|
|
62
78
|
{ 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" },
|
|
63
79
|
{ flags: "--token <token>", help: "store the bearer token in the binding (plaintext, 0600 registry file); with a host door it is copied into every imported binding; UKP_ENDPOINT_<NAME>_TOKEN overrides it at call time" },
|
|
64
80
|
],
|
|
@@ -75,7 +91,7 @@ export const REGISTER_SPEC: UkpCommandSpec = {
|
|
|
75
91
|
" With --url, the declared name and instance identity come from the",
|
|
76
92
|
" remote discovery document (/.well-known/ukp.json) and are pinned",
|
|
77
93
|
" TOFU-style as provenance; your registry binds them under a local",
|
|
78
|
-
" handle
|
|
94
|
+
" handle - the declared name by default, or --name <handle> when that",
|
|
79
95
|
" name is already taken. Use --endpoint <name> as an expected-name",
|
|
80
96
|
" assertion on the declared name; bearer-token services read",
|
|
81
97
|
" UKP_ENDPOINT_<NAME>_TOKEN (keyed by the handle) at call time.",
|
|
@@ -85,7 +101,7 @@ export const REGISTER_SPEC: UkpCommandSpec = {
|
|
|
85
101
|
" and imports every endpoint behind it (a door url may also name one",
|
|
86
102
|
" endpoint: ssh://ali/notes). Import is explicit and idempotent:",
|
|
87
103
|
" re-running refreshes TOFU pins and tokens. Name conflicts are skipped",
|
|
88
|
-
" with a visible reason (never silently renamed)
|
|
104
|
+
" with a visible reason (never silently renamed) - re-register that one",
|
|
89
105
|
" endpoint with --name to land it under another handle; --select narrows",
|
|
90
106
|
" the import; door growth shows in 'ukp list' as drift notes until",
|
|
91
107
|
" imported.",
|
|
@@ -126,7 +142,23 @@ const DEFAULT_CAPABILITIES = (Object.keys(FILE_NATIVE_CAPABILITIES) as Array<key
|
|
|
126
142
|
.filter((name) => FILE_NATIVE_CAPABILITIES[name].derived)
|
|
127
143
|
.sort();
|
|
128
144
|
|
|
129
|
-
|
|
145
|
+
/** One list row as structured cells — column geometry is decided once, in
|
|
146
|
+
* renderListOutput, from every row's actual widths (raw tab stops made the
|
|
147
|
+
* columns land on a different multiple-of-8 per row once names, paths and
|
|
148
|
+
* urls of very different widths mixed). */
|
|
149
|
+
interface InventoryListRow {
|
|
150
|
+
/** Endpoint handle; a remote handle that differs from its declared name
|
|
151
|
+
* carries the declaration inline (`name (declares X)`). */
|
|
152
|
+
name: string;
|
|
153
|
+
/** Local folder path or remote binding url. */
|
|
154
|
+
location: string;
|
|
155
|
+
/** Comma-separated declared extras, `-` for none, `(unavailable)` on
|
|
156
|
+
* degraded rows. */
|
|
157
|
+
capabilities: string;
|
|
158
|
+
warning?: string;
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
function renderLocalListRow(endpoint: { name: string; path: string }): InventoryListRow {
|
|
130
162
|
let service;
|
|
131
163
|
try {
|
|
132
164
|
service = loadManifest(endpoint.path);
|
|
@@ -135,14 +167,16 @@ function renderLocalListRow(endpoint: { name: string; path: string }): { line: s
|
|
|
135
167
|
// when the underlying ManifestError carries a full zod schema dump.
|
|
136
168
|
const headline = (error instanceof Error ? error.message : String(error)).split("\n")[0];
|
|
137
169
|
return {
|
|
138
|
-
|
|
139
|
-
|
|
170
|
+
name: endpoint.name,
|
|
171
|
+
location: endpoint.path,
|
|
172
|
+
capabilities: "(unavailable)",
|
|
173
|
+
warning: `endpoint '${endpoint.name}' declared capabilities unavailable: ${headline}`,
|
|
140
174
|
};
|
|
141
175
|
}
|
|
142
176
|
const extras = Object.keys(service.manifest.capabilities)
|
|
143
177
|
.filter((name) => !(isFileNativeCapability(name) && FILE_NATIVE_CAPABILITIES[name].derived))
|
|
144
178
|
.sort();
|
|
145
|
-
return {
|
|
179
|
+
return { name: endpoint.name, location: endpoint.path, capabilities: extras.length > 0 ? extras.join(",") : "-" };
|
|
146
180
|
}
|
|
147
181
|
|
|
148
182
|
export function executeRegisterCommand(
|
|
@@ -351,6 +385,7 @@ async function executeRegisterRemote(
|
|
|
351
385
|
const wellKnown = await fetchRegistrationDocument(transport.base, {
|
|
352
386
|
...(token !== undefined ? { token } : {}),
|
|
353
387
|
...(pinSelfSigned && tlsProbe !== undefined ? { tlsAnchor: { ca: tlsProbe.certPem } } : {}),
|
|
388
|
+
displayUrl: parts.origin,
|
|
354
389
|
});
|
|
355
390
|
|
|
356
391
|
if (wellKnown.kind === "door") {
|
|
@@ -520,22 +555,97 @@ export function executeListCommand(
|
|
|
520
555
|
// byte-identical and synchronous. One transport pool serves the whole
|
|
521
556
|
// invocation (O-5): same-origin door bindings — and their drift check —
|
|
522
557
|
// share a single ssh tunnel.
|
|
558
|
+
//
|
|
559
|
+
// Fan-out + streaming (ukp_list W1/W2 / ADR 0026 rules 1 and 3): local
|
|
560
|
+
// rows flush first — they are registry data in memory, so first output
|
|
561
|
+
// needs no network at all — then every remote fetch and the drift check
|
|
562
|
+
// start together (wall clock = max over origins, not the sum;
|
|
563
|
+
// same-origin acquires converge on the pool's single open promise), and
|
|
564
|
+
// remote rows append in registry order as they resolve, never rewriting
|
|
565
|
+
// an already-emitted line. Row order is W2's one agent-visible change:
|
|
566
|
+
// locals first, then remotes, each group in registry (name-sorted)
|
|
567
|
+
// order.
|
|
523
568
|
return (async () => {
|
|
524
569
|
const pool = createTransportPool({
|
|
525
570
|
registryPath: context.registryPath,
|
|
526
571
|
...(context.sshCommand === undefined ? {} : { sshCommand: context.sshCommand }),
|
|
527
572
|
});
|
|
528
573
|
try {
|
|
529
|
-
const
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
574
|
+
const localRows = endpoints
|
|
575
|
+
.filter((endpoint) => !isRemoteBinding(endpoint))
|
|
576
|
+
.map((endpoint) => renderLocalListRow({ name: endpoint.name, path: endpoint.path! }));
|
|
577
|
+
// Column geometry is decided once, before the first flush, from
|
|
578
|
+
// every row's registry-known leading cells (ADR 0026 rule 4): only
|
|
579
|
+
// the last cell depends on the network, so streaming never shifts a
|
|
580
|
+
// column width.
|
|
581
|
+
const geometry = listRowGeometry([
|
|
582
|
+
...localRows,
|
|
583
|
+
...remotes.map((endpoint) => ({ name: remoteRowName(endpoint), location: endpoint.url! })),
|
|
584
|
+
]);
|
|
585
|
+
const lines: string[] = [];
|
|
586
|
+
// The progress line and the streamed rows share one terminal: a
|
|
587
|
+
// frame never ends with \n, so the cursor parks at its end — the
|
|
588
|
+
// next row would print BESIDE the frame and the following \r would
|
|
589
|
+
// overwrite the row's head (Windows Terminal dogfood finding). The
|
|
590
|
+
// interlock: every row flush clears the frame first, owns its line,
|
|
591
|
+
// and lets the next drawProgress redraw on the fresh bottom row.
|
|
592
|
+
// With no progress channel (piped runs) the clear is a no-op and
|
|
593
|
+
// the stream is untouched.
|
|
594
|
+
const progress = context.emitProgress;
|
|
595
|
+
let progressWidth = 0;
|
|
596
|
+
const drawProgress = (frame: string) => {
|
|
597
|
+
if (progress === undefined) return;
|
|
598
|
+
progress(frame);
|
|
599
|
+
progressWidth = frame.length;
|
|
600
|
+
};
|
|
601
|
+
const clearProgress = () => {
|
|
602
|
+
if (progress === undefined || progressWidth === 0) return;
|
|
603
|
+
progress(`\r${" ".repeat(progressWidth)}\r`);
|
|
604
|
+
progressWidth = 0;
|
|
605
|
+
};
|
|
606
|
+
const flush = (line: string) => {
|
|
607
|
+
clearProgress();
|
|
608
|
+
lines.push(line);
|
|
609
|
+
context.emitStdout?.(line);
|
|
610
|
+
};
|
|
611
|
+
|
|
612
|
+
flush(listHeaderLine());
|
|
613
|
+
for (const row of localRows) flush(renderListLine(row, geometry));
|
|
614
|
+
|
|
615
|
+
// Everything network-facing starts only after the local flush — and
|
|
616
|
+
// the progress line goes out first, so the user never watches a
|
|
617
|
+
// spinner-less wait. Frames must stay ASCII/narrow and each update
|
|
618
|
+
// at least as wide as its predecessor: the clear blanks
|
|
619
|
+
// progressWidth columns, so a wider glyph than code units (or a
|
|
620
|
+
// shorter update) would leave stale tail cells on a real terminal.
|
|
621
|
+
drawProgress(`fetching ${remotes.length} remote endpoint(s)...`);
|
|
622
|
+
const remoteRows = remotes.map((endpoint) => renderRemoteListRow(endpoint, pool));
|
|
623
|
+
const drift = collectDoorDriftNotes(endpoints, pool);
|
|
624
|
+
|
|
625
|
+
const warnings = localRows.flatMap((row) => (row.warning !== undefined ? [row.warning] : []));
|
|
626
|
+
let done = 0;
|
|
627
|
+
// Head-of-line emission: the fetches already run concurrently; rows
|
|
628
|
+
// print in registry order as their predecessors resolve. Each flush
|
|
629
|
+
// cleared the frame; the update redraws it on the fresh bottom row.
|
|
630
|
+
for (const row of remoteRows) {
|
|
631
|
+
const resolved = await row;
|
|
632
|
+
flush(renderListLine(resolved, geometry));
|
|
633
|
+
if (resolved.warning !== undefined) warnings.push(resolved.warning);
|
|
634
|
+
done += 1;
|
|
635
|
+
if (done < remotes.length) drawProgress(`\rfetching remote endpoint(s): ${done}/${remotes.length}`);
|
|
536
636
|
}
|
|
537
|
-
|
|
538
|
-
|
|
637
|
+
// Cleared before the drift await (the last row's flush already
|
|
638
|
+
// blanked the frame): every row is on screen, so the table reads
|
|
639
|
+
// complete while the stderr footnotes gather — an https door's
|
|
640
|
+
// drift fetch may be the slowest leg and a spinner under a finished
|
|
641
|
+
// table would be noise.
|
|
642
|
+
clearProgress();
|
|
643
|
+
const stderrLines = [...warnings, ...collectSameInstanceNotes(endpoints), ...(await drift)];
|
|
644
|
+
return {
|
|
645
|
+
exitCode: 0,
|
|
646
|
+
stdout: context.emitStdout === undefined ? lines.join("\n") : "",
|
|
647
|
+
stderr: stderrLines.length > 0 ? `${stderrLines.join("\n")}\n` : "",
|
|
648
|
+
};
|
|
539
649
|
} finally {
|
|
540
650
|
pool.close();
|
|
541
651
|
}
|
|
@@ -552,14 +662,11 @@ export function executeListCommand(
|
|
|
552
662
|
}
|
|
553
663
|
|
|
554
664
|
function renderListOutput(
|
|
555
|
-
rows: ReadonlyArray<
|
|
665
|
+
rows: ReadonlyArray<InventoryListRow>,
|
|
556
666
|
extraNotes: readonly string[] = [],
|
|
557
667
|
): InventoryCommandResult {
|
|
558
668
|
const warnings = [...rows.flatMap((row) => (row.warning !== undefined ? [row.warning] : [])), ...extraNotes];
|
|
559
|
-
const stdout = [
|
|
560
|
-
`capabilities on every endpoint: ${DEFAULT_CAPABILITIES.join(", ")} (derived file-native); additional declared capabilities per endpoint:`,
|
|
561
|
-
...rows.map((row) => row.line),
|
|
562
|
-
].join("\n");
|
|
669
|
+
const stdout = [listHeaderLine(), ...rows.map((row) => renderListLine(row, listRowGeometry(rows)))].join("\n");
|
|
563
670
|
return {
|
|
564
671
|
exitCode: 0,
|
|
565
672
|
stdout,
|
|
@@ -567,20 +674,57 @@ function renderListOutput(
|
|
|
567
674
|
};
|
|
568
675
|
}
|
|
569
676
|
|
|
677
|
+
function listHeaderLine(): string {
|
|
678
|
+
// The parenthetical is plain-language by probe verdict (20260921-list-wording):
|
|
679
|
+
// blank readers parsed "(built-in, from the folder itself)" directly, while
|
|
680
|
+
// "(derived file-native)" read as undefined jargon.
|
|
681
|
+
return `capabilities on every endpoint: ${DEFAULT_CAPABILITIES.join(", ")} (built-in, from the folder itself); additional declared capabilities per endpoint:`;
|
|
682
|
+
}
|
|
683
|
+
|
|
684
|
+
/** Space-padded columns: each cell pads to the widest same-column cell plus
|
|
685
|
+
* a two-space gutter (the guide topic list's idiom), last column unpadded so
|
|
686
|
+
* no line carries trailing whitespace. Tab-separated rows rendered ragged
|
|
687
|
+
* whenever name/location widths straddled the terminal's tab stops. The
|
|
688
|
+
* padded columns are registry-known strings (ADR 0026 rule 4): only the
|
|
689
|
+
* last, unpadded cell depends on the remote fetch — so geometry can be
|
|
690
|
+
* computed before the first flush and streaming never shifts a width. */
|
|
691
|
+
interface ListRowGeometry {
|
|
692
|
+
nameWidth: number;
|
|
693
|
+
locationWidth: number;
|
|
694
|
+
}
|
|
695
|
+
|
|
696
|
+
function listRowGeometry(rows: ReadonlyArray<Pick<InventoryListRow, "name" | "location">>): ListRowGeometry {
|
|
697
|
+
return {
|
|
698
|
+
nameWidth: Math.max(...rows.map((row) => row.name.length)),
|
|
699
|
+
locationWidth: Math.max(...rows.map((row) => row.location.length)),
|
|
700
|
+
};
|
|
701
|
+
}
|
|
702
|
+
|
|
703
|
+
function renderListLine(row: InventoryListRow, geometry: ListRowGeometry): string {
|
|
704
|
+
return `${row.name.padEnd(geometry.nameWidth)} ${row.location.padEnd(geometry.locationWidth)} ${row.capabilities}`;
|
|
705
|
+
}
|
|
706
|
+
|
|
707
|
+
/** The name cell of a remote row — registry-known without any network, so
|
|
708
|
+
* column geometry and the streamed local flush can lay out before any
|
|
709
|
+
* fetch resolves. A handle that differs from the declared name
|
|
710
|
+
* (ADR-REM-007) carries the declaration inline for audit. */
|
|
711
|
+
function remoteRowName(endpoint: RegistryBinding): string {
|
|
712
|
+
return endpoint.declared_name !== undefined && endpoint.declared_name !== endpoint.name
|
|
713
|
+
? `${endpoint.name} (declares ${endpoint.declared_name})`
|
|
714
|
+
: endpoint.name;
|
|
715
|
+
}
|
|
716
|
+
|
|
570
717
|
/** Remote row: capabilities from the discovery document; unreachable
|
|
571
718
|
* endpoints degrade to `(unavailable)` + one stderr warning — the same
|
|
572
719
|
* inventory semantics as an unreadable local manifest. Door-form bindings
|
|
573
720
|
* (`ssh://ali/notes`) fetch their per-endpoint document through the shared
|
|
574
|
-
* pool's tunnel.
|
|
575
|
-
* carries the declaration inline for audit. */
|
|
721
|
+
* pool's tunnel. */
|
|
576
722
|
async function renderRemoteListRow(
|
|
577
723
|
endpoint: RegistryBinding,
|
|
578
724
|
pool: RemoteTransportPool,
|
|
579
|
-
): Promise<
|
|
725
|
+
): Promise<InventoryListRow> {
|
|
580
726
|
const url = endpoint.url!;
|
|
581
|
-
const
|
|
582
|
-
? ` (declares ${endpoint.declared_name})`
|
|
583
|
-
: "";
|
|
727
|
+
const name = remoteRowName(endpoint);
|
|
584
728
|
try {
|
|
585
729
|
const transport = await pool.acquire(endpoint);
|
|
586
730
|
const discovery = await fetchDiscoveryDocument(endpoint, transport, resolveRemoteToken(endpoint));
|
|
@@ -589,12 +733,14 @@ async function renderRemoteListRow(
|
|
|
589
733
|
.map(([name]) => name)
|
|
590
734
|
.sort();
|
|
591
735
|
const warnings = discovery.warnings.length > 0 ? { warning: discovery.warnings.join("; ") } : {};
|
|
592
|
-
return {
|
|
736
|
+
return { name, location: url, capabilities: extras.length > 0 ? extras.join(",") : "-", ...warnings };
|
|
593
737
|
} catch (error) {
|
|
594
738
|
const headline = (error instanceof Error ? error.message : String(error)).split("\n")[0];
|
|
595
739
|
return {
|
|
596
|
-
|
|
597
|
-
|
|
740
|
+
name,
|
|
741
|
+
location: url,
|
|
742
|
+
capabilities: "(unavailable)",
|
|
743
|
+
warning: `endpoint '${endpoint.name}' declared capabilities unavailable: ${headline}`,
|
|
598
744
|
};
|
|
599
745
|
}
|
|
600
746
|
}
|
|
@@ -616,7 +762,7 @@ function collectSameInstanceNotes(endpoints: readonly RegistryBinding[]): string
|
|
|
616
762
|
for (const [uid, handles] of byUid) {
|
|
617
763
|
if (handles.length > 1) {
|
|
618
764
|
notes.push(
|
|
619
|
-
`note: ${[...handles].sort().map((handle) => `'${handle}'`).join(" and ")} pin the same instance_uid ${uid}
|
|
765
|
+
`note: ${[...handles].sort().map((handle) => `'${handle}'`).join(" and ")} pin the same instance_uid ${uid} - one service under two handles`,
|
|
620
766
|
);
|
|
621
767
|
}
|
|
622
768
|
}
|
|
@@ -661,7 +807,7 @@ async function collectDoorDriftNotes(
|
|
|
661
807
|
url: origin,
|
|
662
808
|
...(anchor.tls_cert !== undefined ? { tls_cert: anchor.tls_cert, tls_pin: anchor.tls_pin } : {}),
|
|
663
809
|
});
|
|
664
|
-
const door = await fetchDoorDocument(transport.base, {});
|
|
810
|
+
const door = await fetchDoorDocument(transport.base, { displayUrl: transport.displayUrl });
|
|
665
811
|
const unimported = door.doc.endpoints
|
|
666
812
|
.map((endpoint) => endpoint.name)
|
|
667
813
|
.filter((name) => !group.imported.has(name));
|
|
@@ -674,12 +820,12 @@ async function collectDoorDriftNotes(
|
|
|
674
820
|
const taken = unimported.filter((name) => endpoints.some((binding) => binding.name === name));
|
|
675
821
|
if (free.length > 0) {
|
|
676
822
|
notes.push(
|
|
677
|
-
`door ${origin}: ${free.length} unimported endpoint(s): ${free.join(", ")}
|
|
823
|
+
`door ${origin}: ${free.length} unimported endpoint(s): ${free.join(", ")} - run 'ukp register --url ${origin}' to import`,
|
|
678
824
|
);
|
|
679
825
|
}
|
|
680
826
|
if (taken.length > 0) {
|
|
681
827
|
notes.push(
|
|
682
|
-
`door ${origin}: name(s) taken: ${taken.join(", ")}
|
|
828
|
+
`door ${origin}: name(s) taken: ${taken.join(", ")} - import under another handle: 'ukp register --url ${origin}/<name> --name <handle>'`,
|
|
683
829
|
);
|
|
684
830
|
}
|
|
685
831
|
} catch {
|
package/src/commands/propose.ts
CHANGED
|
@@ -73,7 +73,7 @@ export const PROPOSE_SPEC: UkpCommandSpec = {
|
|
|
73
73
|
options: [
|
|
74
74
|
{
|
|
75
75
|
flags: "--id <slug>",
|
|
76
|
-
help: "revision key: resubmitting the same id updates the same proposal (revision +1); 1-63 lowercase ASCII slug (enforced for explicit ids and the default alike); defaults to the --file basename, which then becomes the proposal's persistent id
|
|
76
|
+
help: "revision key: resubmitting the same id updates the same proposal (revision +1); 1-63 lowercase ASCII slug (enforced for explicit ids and the default alike); defaults to the --file basename, which then becomes the proposal's persistent id - renaming the file creates a new proposal",
|
|
77
77
|
},
|
|
78
78
|
{ flags: "--file <path>", help: "read the proposal content from a file (required)" },
|
|
79
79
|
{ flags: "--json", help: "emit the structured response envelope" },
|
package/src/commands/read.ts
CHANGED
|
@@ -96,18 +96,18 @@ export const READ_SPEC: UkpCommandSpec = {
|
|
|
96
96
|
},
|
|
97
97
|
{
|
|
98
98
|
flags: "--show-pin",
|
|
99
|
-
help: "emit the ukp-pin for the read resource on stderr (<!-- ukp-pin: sha256
|
|
99
|
+
help: "emit the ukp-pin for the read resource on stderr (<!-- ukp-pin: sha256-... -->, whole-file LF-normalized sha256; embed next to a ukp:// reference to power rename recovery). Local reads allow a line window - the pin still covers the whole file; remote reads need the whole file, so drop the window there",
|
|
100
100
|
},
|
|
101
101
|
{ flags: "--format <mode>", help: "output mode: 'json' emits a structured failure envelope (body still goes to stdout); default is human output" },
|
|
102
102
|
],
|
|
103
103
|
helpSuffix: [
|
|
104
104
|
"",
|
|
105
105
|
"Reference forms:",
|
|
106
|
-
" endpoint-local path
|
|
107
|
-
" ukp://<endpoint>/<rel-path>[#L<line>]
|
|
108
|
-
" docid[:line] / qmd://<reference>
|
|
106
|
+
" endpoint-local path - requires --endpoint <name>",
|
|
107
|
+
" ukp://<endpoint>/<rel-path>[#L<line>] - carries its own endpoint",
|
|
108
|
+
" docid[:line] / qmd://<reference> - provider-owned references;",
|
|
109
109
|
" require --endpoint <name> and are resolved by the Service's provider",
|
|
110
|
-
" absolute filesystem path
|
|
110
|
+
" absolute filesystem path - the owning endpoint is matched from the",
|
|
111
111
|
" Host Registry (mapping echoed on stderr)",
|
|
112
112
|
"",
|
|
113
113
|
].join("\n"),
|
|
@@ -481,7 +481,7 @@ async function executeRemoteRead(
|
|
|
481
481
|
exitCode: 2,
|
|
482
482
|
stdout: "",
|
|
483
483
|
stderr: renderReadUsageError(
|
|
484
|
-
"--show-pin needs whole-file content; drop the line window (--lines / #L)
|
|
484
|
+
"--show-pin needs whole-file content; drop the line window (--lines / #L) - the pin always covers the whole file",
|
|
485
485
|
),
|
|
486
486
|
};
|
|
487
487
|
}
|
package/src/commands/rg.ts
CHANGED
|
@@ -3,6 +3,7 @@ import {
|
|
|
3
3
|
projectRgEnvelope,
|
|
4
4
|
renderRgHuman,
|
|
5
5
|
RG_DEFAULT_LIMIT,
|
|
6
|
+
RG_FILES_DEFAULT_LIMIT,
|
|
6
7
|
RG_MAX_LIMIT,
|
|
7
8
|
RgPlanningError,
|
|
8
9
|
RgUsageError,
|
|
@@ -11,6 +12,7 @@ import {
|
|
|
11
12
|
type RgAggregateStatus,
|
|
12
13
|
type RgContext,
|
|
13
14
|
type RgEndpointOutcome,
|
|
15
|
+
type RgMode,
|
|
14
16
|
type RgResult,
|
|
15
17
|
} from "../capabilities/rg.ts";
|
|
16
18
|
import { EXTERNAL_PROVIDER } from "../config/external-tool.ts";
|
|
@@ -50,17 +52,19 @@ export const RG_SPEC: UkpCommandSpec = {
|
|
|
50
52
|
summary: "grep raw endpoint files with ripgrep (no index or declaration needed; results as ukp:// references)",
|
|
51
53
|
group: "endpoint",
|
|
52
54
|
description:
|
|
53
|
-
"Run base lexical search (ripgrep) across one or more Service endpoints
|
|
55
|
+
"Run base lexical search (ripgrep) across one or more Service endpoints, "
|
|
56
|
+
+ "or enumerate their files with --files. "
|
|
54
57
|
+ "Available on every registered endpoint by default; results are shaped into ukp:// references that 'ukp read' consumes directly. "
|
|
55
58
|
+ "For indexed/semantic search use 'ukp search'.",
|
|
56
|
-
usage: "[--endpoint <name> ... | -g] <pattern> [--limit <1-1000>] [--count] [--glob <glob>] [--type <type>] [-i] [-- <rg flags>]",
|
|
57
|
-
arguments: [{ name: "pattern", help: "one non-empty regex pattern; quote to escape the shell" }],
|
|
59
|
+
usage: "[--endpoint <name> ... | -g] (<pattern> | --files) [--limit <1-1000>] [--count] [--glob <glob> ...] [--type <type>] [-i] [-- <rg flags>]",
|
|
60
|
+
arguments: [{ name: "pattern", help: "one non-empty regex pattern; quote to escape the shell (omit with --files)" }],
|
|
58
61
|
options: [
|
|
59
62
|
{ flags: "-c, --endpoint <name>", help: "select one endpoint; repeat to select multiple endpoints", multi: true },
|
|
60
63
|
{ flags: "-g", help: "search every endpoint in the Host Registry; takes no value" },
|
|
61
|
-
{ flags: "--limit <1-1000>", help: `maximum
|
|
64
|
+
{ flags: "--limit <1-1000>", help: `maximum results per run (default: ${RG_DEFAULT_LIMIT}; ${RG_FILES_DEFAULT_LIMIT} with --files)` },
|
|
62
65
|
{ flags: "--count", help: "count mode: per-file match counts instead of matches" },
|
|
63
|
-
{ flags: "--
|
|
66
|
+
{ flags: "--files", help: "files mode: enumerate files by tree walk with no pattern (empty and binary files included; combine with --glob/--type)" },
|
|
67
|
+
{ flags: "--glob <glob>", help: "glob filter passed to rg (e.g. \"*.md\"); repeat to combine - a later glob may negate with !", multi: true },
|
|
64
68
|
{ flags: "--type <type>", help: "file type filter passed to rg (e.g. md, py)" },
|
|
65
69
|
{ flags: "-i", help: "case-insensitive search" },
|
|
66
70
|
{ flags: "--json", help: "emit the structured response envelope" },
|
|
@@ -69,7 +73,8 @@ export const RG_SPEC: UkpCommandSpec = {
|
|
|
69
73
|
"",
|
|
70
74
|
"Passthrough:",
|
|
71
75
|
" After '--', rg native flags are passed through on an allowlist",
|
|
72
|
-
" (-A/-B/-C/-m/--glob/--type/--max-filesize and common boolean
|
|
76
|
+
" (-A/-B/-C/-m/--glob/--iglob/--type/--max-filesize and common boolean",
|
|
77
|
+
" flags).",
|
|
73
78
|
" Path operands and output-changing flags (--json, -r, --pre, --config)",
|
|
74
79
|
" are rejected: the endpoint selector owns scope, UKP owns the output.",
|
|
75
80
|
"",
|
|
@@ -84,6 +89,12 @@ export const RG_SPEC: UkpCommandSpec = {
|
|
|
84
89
|
" 'ukp search' covers indexed search where the Service declares a search",
|
|
85
90
|
" capability provider.",
|
|
86
91
|
"",
|
|
92
|
+
"Visibility:",
|
|
93
|
+
" Every rg mode skips hidden files (dot-prefixed, including the .ukp/",
|
|
94
|
+
" wiring) by default; pass -- --hidden after '--' to include them",
|
|
95
|
+
" (-uu additionally lifts .gitignore rules). --files lists every",
|
|
96
|
+
" non-hidden file, including empty and binary ones.",
|
|
97
|
+
"",
|
|
87
98
|
].join("\n"),
|
|
88
99
|
};
|
|
89
100
|
|
|
@@ -92,7 +103,8 @@ interface RgCommandOptions extends Record<string, unknown> {
|
|
|
92
103
|
g?: boolean;
|
|
93
104
|
limit?: string;
|
|
94
105
|
count?: boolean;
|
|
95
|
-
|
|
106
|
+
files?: boolean;
|
|
107
|
+
glob?: string[];
|
|
96
108
|
type?: string;
|
|
97
109
|
i?: boolean;
|
|
98
110
|
json?: boolean;
|
|
@@ -102,10 +114,13 @@ export function parseRgArgs(args: readonly string[]): ParsedRg {
|
|
|
102
114
|
const parsed = parseKitArgs<RgCommandOptions>(RG_SPEC, args);
|
|
103
115
|
const explicit: string[] = [];
|
|
104
116
|
const warnings: string[] = [];
|
|
117
|
+
const files = parsed.options.files === true;
|
|
105
118
|
|
|
106
119
|
// Command-side semantic checks, in the pre-kit order: limit validation,
|
|
107
|
-
// duplicate warnings, excess positional,
|
|
108
|
-
|
|
120
|
+
// duplicate warnings, excess positional, mode conflicts, pattern rule,
|
|
121
|
+
// scope conflict. Files mode (ADR-RG-005) relaxes the pattern requirement
|
|
122
|
+
// and rejects combinations that would silently no-op.
|
|
123
|
+
let limit = files ? RG_FILES_DEFAULT_LIMIT : RG_DEFAULT_LIMIT;
|
|
109
124
|
if (parsed.options.limit !== undefined) {
|
|
110
125
|
if (!/^[0-9]+$/.test(parsed.options.limit)) throw new KitUsageError("--limit must be a decimal integer");
|
|
111
126
|
limit = Number(parsed.options.limit);
|
|
@@ -125,20 +140,34 @@ export function parseRgArgs(args: readonly string[]): ParsedRg {
|
|
|
125
140
|
`unexpected argument '${unexpected}'; rg accepts exactly one pattern. Use '--endpoint <name>' to select an endpoint; '-g' takes no value.`,
|
|
126
141
|
);
|
|
127
142
|
}
|
|
128
|
-
if (
|
|
143
|
+
if (files && parsed.options.count === true) {
|
|
144
|
+
throw new KitUsageError("--files and --count cannot be used together (pick one output mode)");
|
|
145
|
+
}
|
|
146
|
+
if (files && parsed.options.i === true) {
|
|
147
|
+
throw new KitUsageError("--files cannot be combined with -i: there is no pattern to match; pass '-- --iglob <glob>' for a case-insensitive glob filter");
|
|
148
|
+
}
|
|
149
|
+
if (files) {
|
|
150
|
+
if (pattern !== undefined) {
|
|
151
|
+
throw new KitUsageError(`unexpected argument '${pattern}'; --files takes no pattern - use --glob to filter by name`);
|
|
152
|
+
}
|
|
153
|
+
} else if (pattern === undefined || pattern.length === 0) {
|
|
154
|
+
throw new KitUsageError("rg pattern must be non-empty (or pass --files to enumerate files without a pattern)");
|
|
155
|
+
}
|
|
129
156
|
if (parsed.options.g && explicit.length > 0) {
|
|
130
157
|
throw new KitUsageError("--endpoint and -g cannot be used together");
|
|
131
158
|
}
|
|
132
159
|
|
|
160
|
+
const globs = parsed.options.glob ?? [];
|
|
133
161
|
return {
|
|
134
|
-
request: { query: pattern
|
|
162
|
+
request: { query: files ? "" : pattern!, limit },
|
|
135
163
|
options: {
|
|
136
164
|
explicitEndpoints: explicit.length > 0 ? explicit : undefined,
|
|
137
165
|
global: parsed.options.g ?? false,
|
|
138
|
-
...(
|
|
166
|
+
...(globs.length > 0 ? { globs } : {}),
|
|
139
167
|
...(parsed.options.type === undefined ? {} : { type: parsed.options.type }),
|
|
140
168
|
...(parsed.options.i === true ? { ignoreCase: true } : {}),
|
|
141
169
|
...(parsed.options.count === true ? { count: true } : {}),
|
|
170
|
+
...(files ? { files: true } : {}),
|
|
142
171
|
...(parsed.options.json === true ? { json: true } : {}),
|
|
143
172
|
passthrough: [],
|
|
144
173
|
},
|
|
@@ -238,7 +267,7 @@ function renderRgCommandResult(parsed: ParsedRg, result: RgResult): RgCommandRes
|
|
|
238
267
|
if (parsed.options.json === true) {
|
|
239
268
|
return {
|
|
240
269
|
exitCode: RG_EXIT_BY_AGGREGATE[result.aggregate],
|
|
241
|
-
stdout: `${JSON.stringify(projectRgEnvelope(result
|
|
270
|
+
stdout: `${JSON.stringify(projectRgEnvelope(result), null, 2)}\n`,
|
|
242
271
|
stderr: view.diagnostics,
|
|
243
272
|
};
|
|
244
273
|
}
|
|
@@ -305,10 +334,11 @@ async function executeMixedRg(
|
|
|
305
334
|
const execution = await remoteRg(binding, transport, token, {
|
|
306
335
|
query: parsed.request.query,
|
|
307
336
|
limit: parsed.request.limit,
|
|
308
|
-
...(parsed.options.
|
|
337
|
+
...(parsed.options.globs !== undefined ? { globs: parsed.options.globs } : {}),
|
|
309
338
|
...(parsed.options.type !== undefined ? { type: parsed.options.type } : {}),
|
|
310
339
|
...(parsed.options.ignoreCase === true ? { ignoreCase: true } : {}),
|
|
311
340
|
...(parsed.options.count === true ? { count: true } : {}),
|
|
341
|
+
...(parsed.options.files === true ? { files: true } : {}),
|
|
312
342
|
passthrough: parsed.options.passthrough,
|
|
313
343
|
});
|
|
314
344
|
remoteOutcomes.set(binding.name, execution.outcome);
|
|
@@ -343,6 +373,7 @@ async function executeMixedRg(
|
|
|
343
373
|
return renderRgCommandResult(parsed, {
|
|
344
374
|
query: parsed.request.query,
|
|
345
375
|
limit: parsed.request.limit,
|
|
376
|
+
mode: (parsed.options.files === true ? "files" : parsed.options.count === true ? "count" : "match") as RgMode,
|
|
346
377
|
endpoints,
|
|
347
378
|
warnings,
|
|
348
379
|
aggregate,
|