@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,257 @@
|
|
|
1
|
+
import {
|
|
2
|
+
runPropose,
|
|
3
|
+
renderProposeHuman,
|
|
4
|
+
renderProposeJson,
|
|
5
|
+
proposalIdOf,
|
|
6
|
+
readSubmissionFile,
|
|
7
|
+
ProposeProviderError,
|
|
8
|
+
ProposeUsageError,
|
|
9
|
+
type ProposeContext,
|
|
10
|
+
type ProposeErrorClass,
|
|
11
|
+
type ProposeFailure,
|
|
12
|
+
type ProposeRequest,
|
|
13
|
+
} from "../capabilities/propose.ts";
|
|
14
|
+
import {
|
|
15
|
+
fetchDiscoveryDocument,
|
|
16
|
+
openRemoteTransport,
|
|
17
|
+
remotePropose,
|
|
18
|
+
resolveRemoteToken,
|
|
19
|
+
type RemoteTransportHandle,
|
|
20
|
+
} from "../capabilities/remote-client.ts";
|
|
21
|
+
import { isRemoteBinding, readRegistry, type RegistryBinding } from "../registry.ts";
|
|
22
|
+
import { ScopeError } from "../scope.ts";
|
|
23
|
+
import { ManifestError } from "../config/manifest.ts";
|
|
24
|
+
import { KitUsageError, parseKitArgs, renderKitHelp, renderKitUsageError, type UkpCommandSpec } from "./kit.ts";
|
|
25
|
+
import { HelpRequestError, isHelpRequest } from "./flags.ts";
|
|
26
|
+
|
|
27
|
+
/** CLI-owned command result shape (ADR 0021). */
|
|
28
|
+
export interface ProposeCommandResult {
|
|
29
|
+
exitCode: number;
|
|
30
|
+
stdout: string;
|
|
31
|
+
stderr: string;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/** Error-class → exit-code mapping (ADR 0021). */
|
|
35
|
+
const PROPOSE_EXIT_BY_ERROR_CLASS: Record<ProposeErrorClass, number> = {
|
|
36
|
+
"no-endpoint": 1,
|
|
37
|
+
"endpoint-name-mismatch": 1,
|
|
38
|
+
"capability-undeclared": 1,
|
|
39
|
+
"submission-file-unreadable": 1,
|
|
40
|
+
"provider-unsupported": 1,
|
|
41
|
+
};
|
|
42
|
+
|
|
43
|
+
/** CLI composition of the structured outcome (ADR 0021); kept as the test
|
|
44
|
+
* entry so suites exercise the exact adapter path the bin takes. */
|
|
45
|
+
export function executePropose(request: ProposeRequest, context: ProposeContext): ProposeCommandResult {
|
|
46
|
+
const outcome = runPropose(request, context);
|
|
47
|
+
if (!outcome.ok) {
|
|
48
|
+
return {
|
|
49
|
+
exitCode: PROPOSE_EXIT_BY_ERROR_CLASS[outcome.failure.errorClass],
|
|
50
|
+
stdout: "",
|
|
51
|
+
stderr: `ukp propose: ${outcome.failure.message}\n`,
|
|
52
|
+
};
|
|
53
|
+
}
|
|
54
|
+
return {
|
|
55
|
+
exitCode: 0,
|
|
56
|
+
stdout: request.json
|
|
57
|
+
? renderProposeJson(request.endpoint, outcome.result)
|
|
58
|
+
: renderProposeHuman(outcome.result),
|
|
59
|
+
stderr: "",
|
|
60
|
+
};
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
/** Single-source command spec (ADR 0024): summary feeds the root help via
|
|
64
|
+
* cli.ts; usage feeds the help header and the usage-error line; the
|
|
65
|
+
* single-endpoint family (-c singleton, -g teaching flag, required
|
|
66
|
+
* --endpoint) and help-intent triage live in kit.ts. */
|
|
67
|
+
export const PROPOSE_SPEC: UkpCommandSpec = {
|
|
68
|
+
name: "propose",
|
|
69
|
+
summary: "submit an idempotent change proposal (the write path into a Service)",
|
|
70
|
+
group: "endpoint",
|
|
71
|
+
description: "Submit an idempotent change proposal to one Service endpoint (endpoint names come from 'ukp list').",
|
|
72
|
+
usage: "--endpoint <name> [--id <slug>] --file <path>",
|
|
73
|
+
options: [
|
|
74
|
+
{
|
|
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 — renaming the file creates a new proposal",
|
|
77
|
+
},
|
|
78
|
+
{ flags: "--file <path>", help: "read the proposal content from a file (required)" },
|
|
79
|
+
{ flags: "--json", help: "emit the structured response envelope" },
|
|
80
|
+
],
|
|
81
|
+
singleEndpoint: {
|
|
82
|
+
endpointHelp: "select the endpoint that receives the proposal",
|
|
83
|
+
unsupportedHelp: "not supported by propose; use --endpoint <name>",
|
|
84
|
+
},
|
|
85
|
+
helpSuffix: [
|
|
86
|
+
"",
|
|
87
|
+
"Lifecycle:",
|
|
88
|
+
" Submitted proposals land in the Service's proposal folder (default",
|
|
89
|
+
" 'inbox') for the Service owner to adjudicate; there is no",
|
|
90
|
+
" accept/reject command yet. Resubmitting the same id revises the same",
|
|
91
|
+
" proposal ('ukp guide propose' covers the model).",
|
|
92
|
+
"",
|
|
93
|
+
].join("\n"),
|
|
94
|
+
};
|
|
95
|
+
|
|
96
|
+
interface ProposeCommandOptions extends Record<string, unknown> {
|
|
97
|
+
endpoint?: string;
|
|
98
|
+
id?: string;
|
|
99
|
+
file?: string;
|
|
100
|
+
json?: boolean;
|
|
101
|
+
g?: boolean;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
export function parseProposeArgs(args: readonly string[]): ProposeRequest {
|
|
105
|
+
const parsed = parseKitArgs<ProposeCommandOptions>(PROPOSE_SPEC, args);
|
|
106
|
+
const [unexpected] = parsed.positionals;
|
|
107
|
+
if (unexpected !== undefined) {
|
|
108
|
+
throw new KitUsageError(
|
|
109
|
+
`unexpected argument '${unexpected}'; propose reads content from --file <path>, not from an inline argument`,
|
|
110
|
+
);
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
return {
|
|
114
|
+
endpoint: parsed.scope.explicitEndpoints![0],
|
|
115
|
+
...(parsed.options.id === undefined ? {} : { id: parsed.options.id }),
|
|
116
|
+
...(parsed.options.file === undefined ? {} : { file: parsed.options.file }),
|
|
117
|
+
json: parsed.options.json === true,
|
|
118
|
+
};
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
export function executeProposeCommand(
|
|
122
|
+
args: readonly string[],
|
|
123
|
+
context: ProposeContext,
|
|
124
|
+
): ProposeCommandResult | Promise<ProposeCommandResult> {
|
|
125
|
+
if (isHelpRequest(args)) {
|
|
126
|
+
return { exitCode: 0, stdout: renderProposeHelp(), stderr: "" };
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
try {
|
|
130
|
+
const request = parseProposeArgs(args);
|
|
131
|
+
// Remote branch (ukp_remote W8): a remote binding routes through the
|
|
132
|
+
// remote transport; the sync local contract is unchanged
|
|
133
|
+
// (conditional-async seam, same as nav). The id resolves here so usage
|
|
134
|
+
// violations classify identically to the local path.
|
|
135
|
+
const id = proposalIdOf(request);
|
|
136
|
+
const binding = readRegistry(context.registryPath).find((entry) => entry.name === request.endpoint);
|
|
137
|
+
if (binding !== undefined && isRemoteBinding(binding)) {
|
|
138
|
+
return executeRemotePropose(request, id, binding, context);
|
|
139
|
+
}
|
|
140
|
+
return executePropose(request, context);
|
|
141
|
+
} catch (error) {
|
|
142
|
+
if (error instanceof HelpRequestError) {
|
|
143
|
+
return { exitCode: 0, stdout: renderProposeHelp(), stderr: "" };
|
|
144
|
+
}
|
|
145
|
+
if (error instanceof KitUsageError || error instanceof ProposeUsageError) {
|
|
146
|
+
// ProposeUsageError: capability-side validation (slug form, missing
|
|
147
|
+
// --file) classifies as usage too — same rendering, exit 2.
|
|
148
|
+
return { exitCode: 2, stdout: "", stderr: renderProposeUsageError(error.message) };
|
|
149
|
+
}
|
|
150
|
+
if (error instanceof ScopeError) {
|
|
151
|
+
return {
|
|
152
|
+
exitCode: 1,
|
|
153
|
+
stdout: "",
|
|
154
|
+
stderr: `ukp propose: ${error.message}\nRun 'ukp list' to inspect registrations and 'ukp register' from a Service folder, then retry.\n`,
|
|
155
|
+
};
|
|
156
|
+
}
|
|
157
|
+
if (error instanceof ManifestError) {
|
|
158
|
+
return { exitCode: 1, stdout: "", stderr: `ukp propose: ${error.message}\n` };
|
|
159
|
+
}
|
|
160
|
+
if (error instanceof ProposeProviderError) {
|
|
161
|
+
return { exitCode: 1, stdout: "", stderr: `ukp propose: ${error.message}\n` };
|
|
162
|
+
}
|
|
163
|
+
return {
|
|
164
|
+
exitCode: 1,
|
|
165
|
+
stdout: "",
|
|
166
|
+
stderr: `ukp propose: ${error instanceof Error ? error.message : String(error)}\n`,
|
|
167
|
+
};
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
function remoteTokenHint(endpointName: string): string {
|
|
172
|
+
return `endpoint '${endpointName}' requires a bearer token; set UKP_ENDPOINT_${endpointName.toUpperCase().replace(/-/g, "_")}_TOKEN`;
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
/** Failure rendering identical to the sync path's outcome branch, with the
|
|
176
|
+
* remote-phase warnings appended to stderr. */
|
|
177
|
+
function proposeFailureResult(failure: ProposeFailure, warnings: readonly string[]): ProposeCommandResult {
|
|
178
|
+
const stderr = [`ukp propose: ${failure.message}`, ...warnings];
|
|
179
|
+
return { exitCode: PROPOSE_EXIT_BY_ERROR_CLASS[failure.errorClass], stdout: "", stderr: `${stderr.join("\n")}\n` };
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
/** Wire error class → ProposeErrorClass. capability-undeclared is the same
|
|
183
|
+
* vocabulary on both sides and passes through; everything transport-shaped
|
|
184
|
+
* (auth 401, identity, route miss on an older serve, unreachable, wire
|
|
185
|
+
* guards) folds into provider-unsupported with the specifics preserved in
|
|
186
|
+
* the message (W8 mapping, mirroring nav's W6). */
|
|
187
|
+
function mapRemoteProposeErrorClass(errorClass: string | undefined): ProposeErrorClass {
|
|
188
|
+
if (errorClass === "capability-undeclared") return "capability-undeclared";
|
|
189
|
+
return "provider-unsupported";
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
/** PUT /v1/propose/{id} over the wire (ADR-REM-005); the wire envelope IS
|
|
193
|
+
* the local envelope, so the shared renderers finish the job with zero
|
|
194
|
+
* output divergence. */
|
|
195
|
+
async function executeRemotePropose(
|
|
196
|
+
request: ProposeRequest,
|
|
197
|
+
id: string,
|
|
198
|
+
binding: RegistryBinding,
|
|
199
|
+
context: ProposeContext,
|
|
200
|
+
): Promise<ProposeCommandResult> {
|
|
201
|
+
const submission = readSubmissionFile(request.file!, context.currentDirectory);
|
|
202
|
+
if ("failure" in submission) {
|
|
203
|
+
return proposeFailureResult(submission.failure, []);
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
const token = resolveRemoteToken(binding);
|
|
207
|
+
const warnings: string[] = [];
|
|
208
|
+
let transport: RemoteTransportHandle | undefined;
|
|
209
|
+
try {
|
|
210
|
+
transport = await openRemoteTransport(binding, { registryPath: context.registryPath });
|
|
211
|
+
const discovery = await fetchDiscoveryDocument(binding, transport, token);
|
|
212
|
+
warnings.push(...discovery.warnings);
|
|
213
|
+
if (discovery.bearerRequired && token === undefined) warnings.push(remoteTokenHint(binding.name));
|
|
214
|
+
} catch (error) {
|
|
215
|
+
transport?.close();
|
|
216
|
+
return proposeFailureResult(
|
|
217
|
+
{ errorClass: "provider-unsupported", message: error instanceof Error ? error.message : String(error) },
|
|
218
|
+
warnings,
|
|
219
|
+
);
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
let result;
|
|
223
|
+
try {
|
|
224
|
+
result = await remotePropose(transport, token, id, submission.content);
|
|
225
|
+
} catch (error) {
|
|
226
|
+
return proposeFailureResult(
|
|
227
|
+
{ errorClass: "provider-unsupported", message: error instanceof Error ? error.message : String(error) },
|
|
228
|
+
warnings,
|
|
229
|
+
);
|
|
230
|
+
} finally {
|
|
231
|
+
transport.close();
|
|
232
|
+
}
|
|
233
|
+
if (result.ok && result.result !== undefined) {
|
|
234
|
+
return {
|
|
235
|
+
exitCode: 0,
|
|
236
|
+
stdout: request.json
|
|
237
|
+
? renderProposeJson(request.endpoint, result.result)
|
|
238
|
+
: renderProposeHuman(result.result),
|
|
239
|
+
stderr: warnings.length > 0 ? `${warnings.join("\n")}\n` : "",
|
|
240
|
+
};
|
|
241
|
+
}
|
|
242
|
+
return proposeFailureResult(
|
|
243
|
+
{
|
|
244
|
+
errorClass: mapRemoteProposeErrorClass(result.errorClass),
|
|
245
|
+
message: result.errorMessage ?? `remote propose failed (status ${result.status})`,
|
|
246
|
+
},
|
|
247
|
+
warnings,
|
|
248
|
+
);
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
export function renderProposeHelp(): string {
|
|
252
|
+
return renderKitHelp(PROPOSE_SPEC);
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
export function renderProposeUsageError(message: string): string {
|
|
256
|
+
return renderKitUsageError(PROPOSE_SPEC, message);
|
|
257
|
+
}
|