@catheadowl/ukp 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +208 -0
- package/bun.lock +35 -0
- package/package.json +51 -0
- package/src/artifacts.ts +75 -0
- package/src/capabilities/nav.ts +734 -0
- package/src/capabilities/propose.ts +456 -0
- package/src/capabilities/qmd.ts +174 -0
- package/src/capabilities/read.ts +1040 -0
- package/src/capabilities/remote-client.ts +856 -0
- package/src/capabilities/rename-recovery.ts +294 -0
- package/src/capabilities/rg.ts +505 -0
- package/src/capabilities/search.ts +1105 -0
- package/src/capabilities/tls-identity.ts +87 -0
- package/src/capabilities/update.ts +385 -0
- package/src/cli.ts +425 -0
- package/src/commands/diagnose.ts +383 -0
- package/src/commands/flags.ts +49 -0
- package/src/commands/guide-content/client.txt +36 -0
- package/src/commands/guide-content/propose.txt +38 -0
- package/src/commands/guide-content/service-qmd.txt +44 -0
- package/src/commands/guide-content/service.txt +114 -0
- package/src/commands/guide.ts +121 -0
- package/src/commands/init.ts +245 -0
- package/src/commands/inspect.ts +182 -0
- package/src/commands/inventory.ts +570 -0
- package/src/commands/kit.ts +269 -0
- package/src/commands/nav.ts +280 -0
- package/src/commands/propose.ts +257 -0
- package/src/commands/read.ts +558 -0
- package/src/commands/rg.ts +355 -0
- package/src/commands/search.ts +325 -0
- package/src/commands/serve.ts +221 -0
- package/src/commands/update.ts +99 -0
- package/src/config/client.ts +55 -0
- package/src/config/external-tool.ts +58 -0
- package/src/config/file-native.ts +111 -0
- package/src/config/manifest.ts +188 -0
- package/src/fslock.ts +95 -0
- package/src/path-safety.ts +65 -0
- package/src/registry.ts +436 -0
- package/src/scope.ts +78 -0
- package/src/server.ts +1011 -0
- package/tsconfig.json +12 -0
|
@@ -0,0 +1,383 @@
|
|
|
1
|
+
import { loadManifest, type LoadedManifest, type ManifestCapability } from "../config/manifest.ts";
|
|
2
|
+
import { FILE_NATIVE_CAPABILITIES, isFileNativeCapability } from "../config/file-native.ts";
|
|
3
|
+
import { EXTERNAL_TOOL_CAPABILITIES, EXTERNAL_PROVIDER, isExternalToolCapability } from "../config/external-tool.ts";
|
|
4
|
+
import { isRemoteBinding, localPathOf, readRegistry, type RegistryBinding } from "../registry.ts";
|
|
5
|
+
import { resolveScope } from "../scope.ts";
|
|
6
|
+
import { KitUsageError, parseKitArgs, renderKitHelp, renderKitUsageError, type UkpCommandSpec } from "./kit.ts";
|
|
7
|
+
import { HelpRequestError, isHelpRequest } from "./flags.ts";
|
|
8
|
+
import { defaultQmdCommand } from "../capabilities/qmd.ts";
|
|
9
|
+
import { resolveProposeFolder } from "../capabilities/propose.ts";
|
|
10
|
+
import { rgExecutableAvailable } from "../capabilities/rg.ts";
|
|
11
|
+
|
|
12
|
+
export interface ProviderCheck {
|
|
13
|
+
supported: boolean;
|
|
14
|
+
reason?: string;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export type ProviderResolver = (provider: string, capability?: string) => ProviderCheck;
|
|
18
|
+
|
|
19
|
+
export interface DiagnoseReport {
|
|
20
|
+
service: LoadedManifest;
|
|
21
|
+
capabilities: Array<{
|
|
22
|
+
name: string;
|
|
23
|
+
provider: string;
|
|
24
|
+
source: "manifest" | "derived-local" | "base-tier";
|
|
25
|
+
status: "ok" | "warning";
|
|
26
|
+
reason?: string;
|
|
27
|
+
}>;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export interface DiagnoseCommandContext {
|
|
31
|
+
currentDirectory: string;
|
|
32
|
+
registryPath: string;
|
|
33
|
+
resolveProvider?: ProviderResolver;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
export interface DiagnoseCommandResult {
|
|
37
|
+
exitCode: number;
|
|
38
|
+
stdout: string;
|
|
39
|
+
stderr: string;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
export interface RenderDiagnoseOptions {
|
|
43
|
+
includeSearchabilityHint?: boolean;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
const SEARCHABILITY_HINT = "hint: diagnose checks wiring, not indexed content; content is searchable after provider setup (see 'ukp guide service qmd').";
|
|
47
|
+
|
|
48
|
+
/** Single-source command spec (ADR 0024): summary feeds the root help via
|
|
49
|
+
* cli.ts; usage feeds the help header and the usage-error line; the scope
|
|
50
|
+
* family, singleton detection, and help-intent triage live in kit.ts. */
|
|
51
|
+
export const DIAGNOSE_SPEC: UkpCommandSpec = {
|
|
52
|
+
name: "diagnose",
|
|
53
|
+
summary: "check a Service or endpoints for wiring problems (manifest, provider setup)",
|
|
54
|
+
group: "operations",
|
|
55
|
+
description: "Validate a Service folder or selected registered Service endpoints.",
|
|
56
|
+
usage: "[--endpoint <name> ... | -g]",
|
|
57
|
+
scope: {
|
|
58
|
+
endpointHelp: "validate one registered endpoint; repeat to validate multiple endpoints",
|
|
59
|
+
globalHelp: "validate every endpoint in the Host Registry; takes no value",
|
|
60
|
+
},
|
|
61
|
+
helpSuffix: [
|
|
62
|
+
"",
|
|
63
|
+
"Scope:",
|
|
64
|
+
" With no --endpoint or -g, diagnose validates the current folder as a",
|
|
65
|
+
" Service (Manifest and provider wiring). -g validates every registered",
|
|
66
|
+
" endpoint; -g cannot be combined with --endpoint.",
|
|
67
|
+
"",
|
|
68
|
+
].join("\n"),
|
|
69
|
+
};
|
|
70
|
+
|
|
71
|
+
// Provider registration point for this UKP build: every provider that can
|
|
72
|
+
// back a capability must have its availability check here. Adding a provider
|
|
73
|
+
// means (1) a branch in this resolver and (2) a capability adapter under
|
|
74
|
+
// capabilities/ that the command layer dispatches to.
|
|
75
|
+
export function defaultProviderResolver(provider: string, capability = "search"): ProviderCheck {
|
|
76
|
+
if (isFileNativeCapability(capability)) {
|
|
77
|
+
// Only the UKP-native file provider exists for the file-native set;
|
|
78
|
+
// Manifest load already defaults bare declarations to "file".
|
|
79
|
+
return provider === "file"
|
|
80
|
+
? { supported: true }
|
|
81
|
+
: {
|
|
82
|
+
supported: false,
|
|
83
|
+
reason: `provider '${provider}' is not supported for capability '${capability}' by this UKP build`,
|
|
84
|
+
};
|
|
85
|
+
}
|
|
86
|
+
if (isExternalToolCapability(capability)) {
|
|
87
|
+
// External-tool base tier (ADR-RG-003): the provider marker is fixed;
|
|
88
|
+
// availability is the base tool itself, degrading to a warning row.
|
|
89
|
+
if (provider !== EXTERNAL_PROVIDER) {
|
|
90
|
+
return { supported: false, reason: `provider '${provider}' is not supported for capability '${capability}' by this UKP build` };
|
|
91
|
+
}
|
|
92
|
+
return rgExecutableAvailable()
|
|
93
|
+
? { supported: true }
|
|
94
|
+
: { supported: false, reason: "rg executable is not available" };
|
|
95
|
+
}
|
|
96
|
+
if (capability === "update") {
|
|
97
|
+
if (provider !== "qmd") {
|
|
98
|
+
return { supported: false, reason: `provider '${provider}' is not supported for capability 'update' by this UKP build` };
|
|
99
|
+
}
|
|
100
|
+
return defaultQmdCommand()
|
|
101
|
+
? { supported: true }
|
|
102
|
+
: { supported: false, reason: "qmd executable is not available" };
|
|
103
|
+
}
|
|
104
|
+
if (capability !== "search") {
|
|
105
|
+
return { supported: false, reason: `capability '${capability}' is not implemented by this UKP build` };
|
|
106
|
+
}
|
|
107
|
+
if (provider !== "qmd") {
|
|
108
|
+
return { supported: false, reason: `provider '${provider}' is not supported by this UKP build` };
|
|
109
|
+
}
|
|
110
|
+
return defaultQmdCommand()
|
|
111
|
+
? { supported: true }
|
|
112
|
+
: { supported: false, reason: "qmd executable is not available" };
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
function proposeFolderError(serviceFolder: string, declaration: ManifestCapability): string | undefined {
|
|
116
|
+
try {
|
|
117
|
+
resolveProposeFolder(serviceFolder, declaration);
|
|
118
|
+
return undefined;
|
|
119
|
+
} catch (error) {
|
|
120
|
+
return error instanceof Error ? error.message : String(error);
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
export function evaluateServiceCapabilities(
|
|
125
|
+
service: LoadedManifest,
|
|
126
|
+
resolveProvider: ProviderResolver = defaultProviderResolver,
|
|
127
|
+
): DiagnoseReport["capabilities"] {
|
|
128
|
+
const manifestCapabilities = Object.entries(service.manifest.capabilities).map(([name, declaration]) => {
|
|
129
|
+
// Provider is guaranteed post-load for valid Manifests (file-native bare
|
|
130
|
+
// declarations default to "file"; others fail at load). The "(none)"
|
|
131
|
+
// arm is defensive depth for direct API callers.
|
|
132
|
+
const provider = declaration.provider ?? "(none)";
|
|
133
|
+
const check = resolveProvider(provider, name);
|
|
134
|
+
// Front-load propose folder validation so config typos surface in the
|
|
135
|
+
// Service-side self-check instead of the first `ukp propose` run.
|
|
136
|
+
const folderError = name === "propose"
|
|
137
|
+
? proposeFolderError(service.folder, declaration)
|
|
138
|
+
: undefined;
|
|
139
|
+
const supported = check.supported && folderError === undefined;
|
|
140
|
+
const reason = !check.supported ? check.reason : folderError;
|
|
141
|
+
return {
|
|
142
|
+
name,
|
|
143
|
+
provider,
|
|
144
|
+
source: "manifest" as const,
|
|
145
|
+
status: supported ? "ok" as const : "warning" as const,
|
|
146
|
+
...(reason ? { reason } : {}),
|
|
147
|
+
};
|
|
148
|
+
});
|
|
149
|
+
|
|
150
|
+
// Derived file-native defaults (ADR 0016 rule 2): every read-side
|
|
151
|
+
// file-native capability not declared by the Manifest is still effectively
|
|
152
|
+
// present. The table is the single source — no per-capability rows here.
|
|
153
|
+
const derived = Object.entries(FILE_NATIVE_CAPABILITIES)
|
|
154
|
+
.filter(([name, spec]) => spec.derived && !Object.hasOwn(service.manifest.capabilities, name))
|
|
155
|
+
.map(([name]) => {
|
|
156
|
+
const check = resolveProvider("file", name);
|
|
157
|
+
return {
|
|
158
|
+
name,
|
|
159
|
+
provider: "file",
|
|
160
|
+
source: "derived-local" as const,
|
|
161
|
+
status: check.supported ? "ok" as const : "warning" as const,
|
|
162
|
+
...(check.reason ? { reason: check.reason } : {}),
|
|
163
|
+
};
|
|
164
|
+
});
|
|
165
|
+
|
|
166
|
+
// External-tool base tier (ADR-RG-003): base-tier members are effectively
|
|
167
|
+
// present on every registered local Service without declaration; a missing
|
|
168
|
+
// base tool degrades the row to a warning, never hides it.
|
|
169
|
+
const baseTier = Object.entries(EXTERNAL_TOOL_CAPABILITIES)
|
|
170
|
+
.filter(([name, spec]) => spec.baseTier && !Object.hasOwn(service.manifest.capabilities, name))
|
|
171
|
+
.map(([name]) => {
|
|
172
|
+
const check = resolveProvider(EXTERNAL_PROVIDER, name);
|
|
173
|
+
return {
|
|
174
|
+
name,
|
|
175
|
+
provider: EXTERNAL_PROVIDER,
|
|
176
|
+
source: "base-tier" as const,
|
|
177
|
+
status: check.supported ? "ok" as const : "warning" as const,
|
|
178
|
+
...(check.reason ? { reason: check.reason } : {}),
|
|
179
|
+
};
|
|
180
|
+
});
|
|
181
|
+
return [...manifestCapabilities, ...derived, ...baseTier];
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
export function diagnoseService(
|
|
185
|
+
serviceFolder: string,
|
|
186
|
+
resolveProvider: ProviderResolver = defaultProviderResolver,
|
|
187
|
+
): DiagnoseReport {
|
|
188
|
+
const service = loadManifest(serviceFolder);
|
|
189
|
+
const capabilities = evaluateServiceCapabilities(service, resolveProvider);
|
|
190
|
+
|
|
191
|
+
if (!capabilities.some((capability) => capability.status === "ok")) {
|
|
192
|
+
throw new Error("NO_SUPPORTED_CAPABILITY");
|
|
193
|
+
}
|
|
194
|
+
return { service, capabilities };
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
export function renderDependencyRegistryWarnings(
|
|
198
|
+
service: LoadedManifest,
|
|
199
|
+
registry: readonly RegistryBinding[],
|
|
200
|
+
): string[] {
|
|
201
|
+
if (!service.manifest.dependencies || service.manifest.dependencies.length === 0) return [];
|
|
202
|
+
const registeredEndpoints = new Set(registry.map((binding) => binding.name));
|
|
203
|
+
return service.manifest.dependencies
|
|
204
|
+
.filter((dependency) => !registeredEndpoints.has(dependency.endpoint))
|
|
205
|
+
.map((dependency) =>
|
|
206
|
+
`dependency target '${dependency.endpoint}' is not registered (declared by ${service.effectiveName})`
|
|
207
|
+
);
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
function parseDiagnoseArgs(args: readonly string[]): {
|
|
211
|
+
explicitEndpoints?: string[];
|
|
212
|
+
global: boolean;
|
|
213
|
+
warnings: string[];
|
|
214
|
+
} {
|
|
215
|
+
const parsed = parseKitArgs(DIAGNOSE_SPEC, args);
|
|
216
|
+
return {
|
|
217
|
+
explicitEndpoints: parsed.scope.explicitEndpoints,
|
|
218
|
+
global: parsed.scope.global,
|
|
219
|
+
warnings: parsed.scope.warnings,
|
|
220
|
+
};
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
function diagnoseBinding(
|
|
224
|
+
binding: RegistryBinding,
|
|
225
|
+
resolveProvider: ProviderResolver | undefined,
|
|
226
|
+
): { status: "ok"; report: DiagnoseReport } | { status: "failed"; message: string } {
|
|
227
|
+
if (isRemoteBinding(binding)) {
|
|
228
|
+
return {
|
|
229
|
+
status: "failed",
|
|
230
|
+
message: `remote endpoint '${binding.name}' (${binding.url}): diagnose is local-only in ukp_remote W2; reachability shows in 'ukp list'`,
|
|
231
|
+
};
|
|
232
|
+
}
|
|
233
|
+
try {
|
|
234
|
+
const report = diagnoseService(localPathOf(binding), resolveProvider);
|
|
235
|
+
if (report.service.effectiveName !== binding.name) {
|
|
236
|
+
return {
|
|
237
|
+
status: "failed",
|
|
238
|
+
message: `endpoint '${binding.name}' no longer matches Service effective name '${report.service.effectiveName}'`,
|
|
239
|
+
};
|
|
240
|
+
}
|
|
241
|
+
return { status: "ok", report };
|
|
242
|
+
} catch (error) {
|
|
243
|
+
return { status: "failed", message: error instanceof Error ? error.message : String(error) };
|
|
244
|
+
}
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
export function executeDiagnoseCommand(
|
|
248
|
+
args: readonly string[],
|
|
249
|
+
context: DiagnoseCommandContext,
|
|
250
|
+
): DiagnoseCommandResult {
|
|
251
|
+
if (isHelpRequest(args)) {
|
|
252
|
+
return { exitCode: 0, stdout: renderDiagnoseHelp(), stderr: "" };
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
try {
|
|
256
|
+
const parsed = parseDiagnoseArgs(args);
|
|
257
|
+
if (!parsed.explicitEndpoints && !parsed.global) {
|
|
258
|
+
try {
|
|
259
|
+
const report = diagnoseService(context.currentDirectory, context.resolveProvider);
|
|
260
|
+
let dependencyWarnings: string[] = [];
|
|
261
|
+
try {
|
|
262
|
+
dependencyWarnings = renderDependencyRegistryWarnings(report.service, readRegistry(context.registryPath));
|
|
263
|
+
} catch (error) {
|
|
264
|
+
if (report.service.manifest.dependencies && report.service.manifest.dependencies.length > 0) {
|
|
265
|
+
dependencyWarnings = [
|
|
266
|
+
`dependency registry check skipped: ${error instanceof Error ? error.message : String(error)}`,
|
|
267
|
+
];
|
|
268
|
+
}
|
|
269
|
+
}
|
|
270
|
+
const warnings = [...parsed.warnings, ...dependencyWarnings];
|
|
271
|
+
return {
|
|
272
|
+
exitCode: 0,
|
|
273
|
+
stdout: renderDiagnose(report, {
|
|
274
|
+
includeSearchabilityHint: true,
|
|
275
|
+
}),
|
|
276
|
+
stderr: warnings.length > 0 ? `${warnings.join("\n")}\n` : "",
|
|
277
|
+
};
|
|
278
|
+
} catch (error) {
|
|
279
|
+
return {
|
|
280
|
+
exitCode: 1,
|
|
281
|
+
stdout: "",
|
|
282
|
+
stderr: renderLocalDiagnoseError(error instanceof Error ? error.message : String(error)),
|
|
283
|
+
};
|
|
284
|
+
}
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
const registry = readRegistry(context.registryPath);
|
|
288
|
+
const scope = resolveScope({
|
|
289
|
+
currentDirectory: context.currentDirectory,
|
|
290
|
+
registry,
|
|
291
|
+
explicitEndpoints: parsed.explicitEndpoints,
|
|
292
|
+
global: parsed.global,
|
|
293
|
+
});
|
|
294
|
+
const warnings = [...parsed.warnings, ...scope.warnings];
|
|
295
|
+
const output: string[] = [];
|
|
296
|
+
let failed = false;
|
|
297
|
+
|
|
298
|
+
for (const binding of scope.bindings) {
|
|
299
|
+
output.push(`== ${binding.name} ==`);
|
|
300
|
+
const result = diagnoseBinding(binding, context.resolveProvider);
|
|
301
|
+
if (result.status === "ok") {
|
|
302
|
+
output.push(renderDiagnose(result.report, { includeSearchabilityHint: true }).trimEnd());
|
|
303
|
+
warnings.push(...renderDependencyRegistryWarnings(result.report.service, registry));
|
|
304
|
+
} else {
|
|
305
|
+
failed = true;
|
|
306
|
+
output.push("status: failed");
|
|
307
|
+
output.push(`error: ${result.message}`);
|
|
308
|
+
}
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
if (scope.bindings.length === 0) {
|
|
312
|
+
failed = true;
|
|
313
|
+
warnings.push("no endpoints selected: the Host Registry is empty; run 'ukp register' from a Service folder, then retry");
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
return {
|
|
317
|
+
exitCode: failed ? 1 : 0,
|
|
318
|
+
stdout: output.length > 0 ? `${output.join("\n")}\n` : "",
|
|
319
|
+
stderr: warnings.length > 0 ? `${warnings.join("\n")}\n` : "",
|
|
320
|
+
};
|
|
321
|
+
} catch (error) {
|
|
322
|
+
if (error instanceof HelpRequestError) {
|
|
323
|
+
return { exitCode: 0, stdout: renderDiagnoseHelp(), stderr: "" };
|
|
324
|
+
}
|
|
325
|
+
if (error instanceof KitUsageError) {
|
|
326
|
+
return { exitCode: 2, stdout: "", stderr: renderDiagnoseUsageError(error.message) };
|
|
327
|
+
}
|
|
328
|
+
return {
|
|
329
|
+
exitCode: 1,
|
|
330
|
+
stdout: "",
|
|
331
|
+
stderr: `ukp diagnose: ${error instanceof Error ? error.message : String(error)}\n`,
|
|
332
|
+
};
|
|
333
|
+
}
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
export function renderLocalDiagnoseError(message: string): string {
|
|
337
|
+
return [
|
|
338
|
+
`ukp diagnose: ${message}`,
|
|
339
|
+
"Hint: 'ukp diagnose' checks the current folder as a Service.",
|
|
340
|
+
"Run 'ukp guide service' for the short onboarding guide.",
|
|
341
|
+
"Use 'ukp diagnose -g' to validate every registered endpoint, or 'ukp diagnose --endpoint <name>' for one endpoint.",
|
|
342
|
+
].join("\n") + "\n";
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
export function renderDiagnose(report: DiagnoseReport, options: RenderDiagnoseOptions = {}): string {
|
|
346
|
+
const lines = [
|
|
347
|
+
`endpoint: ${report.service.effectiveName} (source: ${report.service.nameSource})`,
|
|
348
|
+
];
|
|
349
|
+
if (report.service.manifest.description) {
|
|
350
|
+
lines.push(`description: ${report.service.manifest.description}`);
|
|
351
|
+
}
|
|
352
|
+
if (report.service.manifest.dependencies && report.service.manifest.dependencies.length > 0) {
|
|
353
|
+
for (const dependency of report.service.manifest.dependencies) {
|
|
354
|
+
lines.push(`dependency: depends_on -> ${dependency.endpoint} (kind: ${dependency.kind})`);
|
|
355
|
+
if (dependency.reason) {
|
|
356
|
+
lines.push(`dependency_reason: ${dependency.reason}`);
|
|
357
|
+
}
|
|
358
|
+
}
|
|
359
|
+
}
|
|
360
|
+
lines.push(`location: ${report.service.folder}`);
|
|
361
|
+
const showSearchabilityHint = options.includeSearchabilityHint
|
|
362
|
+
&& report.capabilities.some((capability) => capability.name === "search" && capability.status === "ok");
|
|
363
|
+
for (const capability of report.capabilities) {
|
|
364
|
+
lines.push(capability.source === "derived-local"
|
|
365
|
+
? `capability: ${capability.name} (derived local baseline)`
|
|
366
|
+
: capability.source === "base-tier"
|
|
367
|
+
? `capability: ${capability.name} (external-tool base tier)`
|
|
368
|
+
: `capability: ${capability.name}`);
|
|
369
|
+
lines.push(`provider: ${capability.provider}`);
|
|
370
|
+
lines.push(`status: ${capability.status}`);
|
|
371
|
+
if (capability.reason) lines.push(`warning: ${capability.reason}`);
|
|
372
|
+
}
|
|
373
|
+
if (showSearchabilityHint) lines.push(SEARCHABILITY_HINT);
|
|
374
|
+
return `${lines.join("\n")}\n`;
|
|
375
|
+
}
|
|
376
|
+
|
|
377
|
+
export function renderDiagnoseHelp(): string {
|
|
378
|
+
return renderKitHelp(DIAGNOSE_SPEC);
|
|
379
|
+
}
|
|
380
|
+
|
|
381
|
+
export function renderDiagnoseUsageError(message: string): string {
|
|
382
|
+
return renderKitUsageError(DIAGNOSE_SPEC, message);
|
|
383
|
+
}
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
import { CommanderError } from "commander";
|
|
2
|
+
|
|
3
|
+
export function countFlagOccurrences(args: readonly string[], flag: string): number {
|
|
4
|
+
if (flag.startsWith("--")) {
|
|
5
|
+
return args.filter((argument) => argument === flag || argument.startsWith(`${flag}=`)).length;
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
if (!flag.startsWith("-") || flag.length !== 2) {
|
|
9
|
+
return args.filter((argument) => argument === flag).length;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
const shortFlag = flag[1];
|
|
13
|
+
return args.reduce((count, argument) => {
|
|
14
|
+
if (argument === flag) return count + 1;
|
|
15
|
+
if (!argument.startsWith("-") || argument.length <= 2) return count;
|
|
16
|
+
|
|
17
|
+
const bundle = argument.slice(1);
|
|
18
|
+
if (!/^[A-Za-z]+$/.test(bundle)) return count;
|
|
19
|
+
if (!bundle.split("").every((character) => character === shortFlag)) return count;
|
|
20
|
+
return count + bundle.length;
|
|
21
|
+
}, 0);
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export function isHelpRequest(args: readonly string[]): boolean {
|
|
25
|
+
return args.length === 1 && (args[0] === "-h" || args[0] === "--help");
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
/** commander reports help/version intent through exitOverride's
|
|
29
|
+
* CommanderError (codes `commander.help` / `commander.helpDisplayed` /
|
|
30
|
+
* `commander.version`); the `(outputHelp)` message is an internal sentinel,
|
|
31
|
+
* not user-facing text. Options are order-independent, so `--help` must win
|
|
32
|
+
* wherever it appears, and the parse layer must not translate these into
|
|
33
|
+
* usage errors. Call inside an `instanceof CommanderError` guard — a type
|
|
34
|
+
* predicate here would narrow the sibling branch to `never`. */
|
|
35
|
+
export function isCommanderHelpIntent(error: CommanderError): boolean {
|
|
36
|
+
return error.code === "commander.help"
|
|
37
|
+
|| error.code === "commander.helpDisplayed"
|
|
38
|
+
|| error.code === "commander.version";
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/** Flows from the parse layer (whose configureOutput silencing swallowed
|
|
42
|
+
* commander's own help rendering) to the execute layer, which re-renders
|
|
43
|
+
* help and returns exit 0. */
|
|
44
|
+
export class HelpRequestError extends Error {
|
|
45
|
+
constructor() {
|
|
46
|
+
super("help requested");
|
|
47
|
+
this.name = "HelpRequestError";
|
|
48
|
+
}
|
|
49
|
+
}
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
UKP Client quickstart
|
|
2
|
+
|
|
3
|
+
Goal: use registered Services from a workspace by default, without naming an endpoint on every call.
|
|
4
|
+
|
|
5
|
+
Model:
|
|
6
|
+
- Client path is separate from the provider path: it uses Services; it does not declare one.
|
|
7
|
+
- A folder may hold both roles at once (dual-role): a Service folder may also carry its own .ukp/client.toml to set its default scope. Each file is read independently.
|
|
8
|
+
- A workspace .ukp/client.toml default_endpoints list selects which registered endpoints you mean when no selector is given.
|
|
9
|
+
- UKP walks up from the current directory to find the nearest .ukp/client.toml.
|
|
10
|
+
- Client Config stores endpoint names only; bindings live in the Host Registry.
|
|
11
|
+
- Explicit --endpoint <name> or -g overrides the client defaults. With no Client Config, most commands fall back to every registered endpoint.
|
|
12
|
+
- ukp update is stricter: it does not fall back to the Registry. It uses your Client Config defaults, or requires --endpoint <name> / -g.
|
|
13
|
+
- ukp register does not edit .ukp/client.toml.
|
|
14
|
+
|
|
15
|
+
1. Create a workspace Client Config
|
|
16
|
+
.ukp/client.toml at the workspace root:
|
|
17
|
+
default_endpoints = ["endpoint-a", "endpoint-b"]
|
|
18
|
+
Each name must match a registered endpoint; run 'ukp list' to see what is registered.
|
|
19
|
+
|
|
20
|
+
2. Use Services without a selector
|
|
21
|
+
ukp search "keyword"
|
|
22
|
+
ukp inspect
|
|
23
|
+
ukp read --endpoint endpoint-a docs/example.md (read still names a path, and --endpoint here wins)
|
|
24
|
+
|
|
25
|
+
3. Check which scope is active
|
|
26
|
+
ukp inspect
|
|
27
|
+
It prints scope: explicit / global / client-config / registry-fallback and the source.
|
|
28
|
+
|
|
29
|
+
4. Dangling defaults warn instead of failing silently
|
|
30
|
+
A default_endpoints entry not registered yet is reported as a warning by inspect, and commands still run on the resolvable ones.
|
|
31
|
+
|
|
32
|
+
Remember:
|
|
33
|
+
- Client path consumes Services; provider path (ukp guide service) creates them.
|
|
34
|
+
- Client Config lists names, not paths or provider details.
|
|
35
|
+
- Explicit selectors always beat Client Config defaults.
|
|
36
|
+
- Register does not edit Client Config, and Client Config does not register endpoints.
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
UKP propose quickstart
|
|
2
|
+
|
|
3
|
+
Goal: submit a change proposal to a Service endpoint as an idempotent, adjudicable document — not a file write.
|
|
4
|
+
|
|
5
|
+
Model:
|
|
6
|
+
- A proposal is a pending change the Service owner will adjudicate; the submitter proposes, the owner decides.
|
|
7
|
+
- Identity is client-owned: you pick the id (a slug), and resubmitting the same id updates the same proposal.
|
|
8
|
+
- Submission is an upsert with three outcomes: created (new, revision 1), unchanged (identical content, no write), updated (replaced, revision +1).
|
|
9
|
+
- revision is an audit counter and the history of your edits; it never resets.
|
|
10
|
+
- ukp propose is the only documented write path. Never edit Service-side proposal files directly: a later submission overwrites them and your edits bypass revision tracking.
|
|
11
|
+
|
|
12
|
+
1. Declare the capability (Service side)
|
|
13
|
+
.ukp/service.toml:
|
|
14
|
+
[capabilities.propose]
|
|
15
|
+
provider = "file"
|
|
16
|
+
The provider line is optional (a bare [capabilities.propose] means the file provider).
|
|
17
|
+
Optional [capabilities.propose.config] folder = "inbox" (default) sets where proposals land.
|
|
18
|
+
|
|
19
|
+
2. Submit a proposal
|
|
20
|
+
ukp propose --endpoint <name> --file draft.md
|
|
21
|
+
--id defaults to the draft basename (extension stripped); the basename becomes the proposal's persistent identity.
|
|
22
|
+
Renaming the file and resubmitting creates a new proposal, not a revision.
|
|
23
|
+
Pass --id <slug> to pin the identity explicitly.
|
|
24
|
+
Add --json for a machine-readable envelope (id, status, revision).
|
|
25
|
+
|
|
26
|
+
3. Revise a proposal
|
|
27
|
+
Edit your local draft, then run the same command with the same id.
|
|
28
|
+
The Service replaces the stored proposal, keeps created, bumps revision, refreshes updated.
|
|
29
|
+
Identical resubmission is a no-op (exit 0, revision unchanged).
|
|
30
|
+
|
|
31
|
+
4. Read proposals back
|
|
32
|
+
Reading is owned by the Service: if it indexes its proposal folder into its search surface,
|
|
33
|
+
ukp search / ukp read reach proposals like any other content.
|
|
34
|
+
|
|
35
|
+
Remember:
|
|
36
|
+
- Same id, same proposal: do not fork ids (add-topic, add-topic-v2) for revisions of one idea.
|
|
37
|
+
- Frontmatter service fields (id, status, revision, created, updated) are Service-maintained and stripped from submissions; your subject/kind display fields pass through.
|
|
38
|
+
- status stays "proposed" until adjudication exists; there is no accept/reject command yet.
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
UKP Service quickstart - QMD provider setup
|
|
2
|
+
|
|
3
|
+
Goal: make a registered Service's content searchable through the default QMD provider.
|
|
4
|
+
Run these from the Service folder root.
|
|
5
|
+
|
|
6
|
+
Boundary:
|
|
7
|
+
- These are provider-owned steps. QMD owns collection, index, ranking, and config.
|
|
8
|
+
- UKP only routes capability requests. This topic carries the journey; exact
|
|
9
|
+
flags and arguments belong to the relevant QMD subcommand help.
|
|
10
|
+
|
|
11
|
+
Fast path for a simple knowledge folder:
|
|
12
|
+
1. qmd init
|
|
13
|
+
2. qmd collection add .
|
|
14
|
+
3. qmd update
|
|
15
|
+
4. qmd search "keyword"
|
|
16
|
+
5. qmd get <reference> --no-line-numbers
|
|
17
|
+
|
|
18
|
+
1. Initialize QMD (qmd init)
|
|
19
|
+
2. Add the searchable collections (qmd collection add)
|
|
20
|
+
For a pure knowledge base, add the folder root; otherwise add the folder(s)
|
|
21
|
+
whose content should be searchable.
|
|
22
|
+
3. Use a stable short collection name (qmd collection rename)
|
|
23
|
+
QMD names a collection from the indexed folder; rename it to a short
|
|
24
|
+
stable name so provider-native artifact URIs stay clean.
|
|
25
|
+
4. Update the index (qmd update, qmd status)
|
|
26
|
+
5. Verify the provider before returning to UKP (qmd search / qmd get)
|
|
27
|
+
If a search returns matches, the provider is ready; return to
|
|
28
|
+
'ukp guide service' step 4.
|
|
29
|
+
A read smoke ('qmd get <reference> --no-line-numbers') is required for
|
|
30
|
+
observe mode (external or nested collection root): it confirms the provider
|
|
31
|
+
can resolve the provider references that ukp read will delegate to it
|
|
32
|
+
(bare docid handoff keys and qmd:// references).
|
|
33
|
+
|
|
34
|
+
Exact flags and arguments come from the relevant QMD subcommand help, for example:
|
|
35
|
+
qmd init --help
|
|
36
|
+
qmd collection add --help
|
|
37
|
+
qmd update --help
|
|
38
|
+
qmd search --help
|
|
39
|
+
qmd get --help
|
|
40
|
+
|
|
41
|
+
Remember:
|
|
42
|
+
- Provider config is not stored in the UKP Registry.
|
|
43
|
+
- A registered Service is addressable; it is searchable only after provider setup.
|
|
44
|
+
- Keep the local index out of version control: ignore .qmd/*.sqlite and .qmd/*.sqlite-*.
|
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
UKP Service quickstart
|
|
2
|
+
|
|
3
|
+
Goal: make a folder a named Knowledge Service, register it, inspect the route, search it, and read an endpoint-scoped reference.
|
|
4
|
+
Run the setup commands from the Service folder root.
|
|
5
|
+
|
|
6
|
+
Fast path:
|
|
7
|
+
1. Create the UKP Service Manifest
|
|
8
|
+
ukp init service --name your-endpoint-name
|
|
9
|
+
Optional: add --description "Short boundary note"
|
|
10
|
+
|
|
11
|
+
2. Make content searchable through the provider
|
|
12
|
+
ukp guide service qmd
|
|
13
|
+
|
|
14
|
+
3. Validate and register the Service
|
|
15
|
+
ukp diagnose
|
|
16
|
+
ukp register
|
|
17
|
+
ukp list
|
|
18
|
+
|
|
19
|
+
4. Inspect, search, then read
|
|
20
|
+
ukp inspect --endpoint your-endpoint-name
|
|
21
|
+
ukp search "keyword" --endpoint your-endpoint-name --limit 3
|
|
22
|
+
ukp read --endpoint your-endpoint-name <reference>
|
|
23
|
+
|
|
24
|
+
Key boundary:
|
|
25
|
+
- init/register makes the folder addressable as a Service.
|
|
26
|
+
- provider setup makes the Service's content searchable.
|
|
27
|
+
- Both are needed for an end-to-end search path.
|
|
28
|
+
|
|
29
|
+
Model:
|
|
30
|
+
- A UKP Service is a knowledge endpoint that declares capabilities.
|
|
31
|
+
- Four separate journeys:
|
|
32
|
+
- provider path: init service + register make a folder an addressable Service;
|
|
33
|
+
- content-searchable: provider setup decides what content is indexed (provider-owned; see 'ukp guide service qmd');
|
|
34
|
+
- declared dependencies: a Service Manifest can declare [[dependencies]] entries with endpoint + kind; this is declarative only;
|
|
35
|
+
- client path: a workspace .ukp/client.toml default scope lets you use Services by default instead of naming one each call.
|
|
36
|
+
- Registered as a Service does not mean its content is searchable; both steps are needed.
|
|
37
|
+
- QMD is the current default search/update provider, not the definition of a Service.
|
|
38
|
+
- Provider setup is provider-owned: the provider owns collection, index, ranking, and config.
|
|
39
|
+
|
|
40
|
+
Details:
|
|
41
|
+
|
|
42
|
+
1. Choose a stable Service folder
|
|
43
|
+
Use the project or knowledge-domain root. Limit searchable subfolders in the provider, not in the UKP Registry.
|
|
44
|
+
|
|
45
|
+
2. Create the UKP Service Manifest
|
|
46
|
+
ukp init service --name your-endpoint-name
|
|
47
|
+
Optional: add --description "Short boundary note"
|
|
48
|
+
Name must be a lowercase endpoint slug, for example: cad-notes
|
|
49
|
+
This writes .ukp/service.toml with the current minimal provider declaration:
|
|
50
|
+
[capabilities.search]
|
|
51
|
+
provider = "qmd"
|
|
52
|
+
|
|
53
|
+
3. Make content searchable through the provider (provider-owned)
|
|
54
|
+
Provider setup is not a UKP step. For the default QMD provider:
|
|
55
|
+
ukp guide service qmd
|
|
56
|
+
That topic points exact flags and arguments to the relevant QMD subcommand help.
|
|
57
|
+
|
|
58
|
+
If this Service depends on other externally consumed knowledge endpoints,
|
|
59
|
+
declare them in the Manifest with repeated --dependency <name> entries;
|
|
60
|
+
each entry becomes [[dependencies]] with kind = "context" and can be edited later.
|
|
61
|
+
|
|
62
|
+
4. Validate and register
|
|
63
|
+
ukp diagnose
|
|
64
|
+
ukp register
|
|
65
|
+
ukp list
|
|
66
|
+
At this point the folder is a registered Service: addressable and callable.
|
|
67
|
+
Its content is searchable only because step 3 configured the provider.
|
|
68
|
+
|
|
69
|
+
5. Inspect the effective route
|
|
70
|
+
ukp inspect --endpoint your-endpoint-name
|
|
71
|
+
Use inspect when you need to explain scope, Registry binding, Manifest capability, or provider availability.
|
|
72
|
+
|
|
73
|
+
6. Search
|
|
74
|
+
ukp search "keyword" --endpoint your-endpoint-name --limit 3
|
|
75
|
+
ukp search "keyword" --endpoint your-endpoint-name --limit 3 --json
|
|
76
|
+
ukp search "keyword" --endpoint your-endpoint-name --recursive
|
|
77
|
+
In a workspace with .ukp/client.toml default_endpoints, you can omit --endpoint for search.
|
|
78
|
+
|
|
79
|
+
7. Read an endpoint-scoped reference
|
|
80
|
+
Every registered local Service has the derived read/file baseline; no Manifest get entry is needed.
|
|
81
|
+
ukp read --endpoint your-endpoint-name docs/example.md
|
|
82
|
+
ukp read --endpoint your-endpoint-name docs/example.md --lines 10:20
|
|
83
|
+
Search results carry copyable handoff lines you can paste straight into read:
|
|
84
|
+
- a ukp://<endpoint>/<path> URI is the durable, endpoint-scoped reference;
|
|
85
|
+
- a bare docid is the provider's short-lived document id (QMD handoff key).
|
|
86
|
+
On a QMD-backed Service (including observe mode), provider content is read with
|
|
87
|
+
'ukp read --endpoint <name> <docid handoff key or qmd:// reference>'; do not
|
|
88
|
+
manually convert qmd:// provider locations into Service-folder paths.
|
|
89
|
+
|
|
90
|
+
8. Update provider-owned state
|
|
91
|
+
This is optional for the first search path. Add this capability to .ukp/service.toml when provider maintenance should be exposed:
|
|
92
|
+
[capabilities.update]
|
|
93
|
+
provider = "qmd"
|
|
94
|
+
ukp update --endpoint your-endpoint-name
|
|
95
|
+
In a workspace with .ukp/client.toml default_endpoints, you can omit --endpoint for update too.
|
|
96
|
+
Use ukp update -g only when you explicitly want to update every registered endpoint.
|
|
97
|
+
|
|
98
|
+
Advanced:
|
|
99
|
+
- Declared dependencies belong in the Service Manifest as [[dependencies]] entries.
|
|
100
|
+
- ukp init service --dependency <name> emits a context dependency; edit the Manifest later for authority, implementation, evidence, or a reason.
|
|
101
|
+
- Dependencies do not change ordinary search/read. Only explicit search --recursive traverses direct authority/context dependencies.
|
|
102
|
+
- Workspace defaults belong to the client path; run 'ukp guide client' from the consuming workspace.
|
|
103
|
+
- update is a provider-owned maintenance route, not part of the first search smoke.
|
|
104
|
+
|
|
105
|
+
Remember:
|
|
106
|
+
- Endpoint name identifies the Service.
|
|
107
|
+
- Host Registry stores endpoint name -> Service folder path.
|
|
108
|
+
- Provider collection decides what content is indexed; folder-external authoritative knowledge should be modeled as its own endpoint when UKP ownership matters.
|
|
109
|
+
- Register makes the folder addressable; it does not make content searchable (provider indexing does).
|
|
110
|
+
- Register does not edit .ukp/client.toml or provider configuration.
|
|
111
|
+
- Declarative dependencies live in the Service Manifest as [[dependencies]] entries; they do not change ordinary search/read, while explicit search --recursive traverses direct authority/context dependencies.
|
|
112
|
+
- Future providers should add provider adapters instead of turning provider internals into UKP rules.
|
|
113
|
+
- To register an already-used name at a new location, unregister the old binding first:
|
|
114
|
+
ukp unregister --endpoint <name>
|