@kora-platform/cli 0.7.0-rc1 → 0.8.0-rc10
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 +21 -0
- package/dist/api-client.d.ts +274 -106
- package/dist/api-client.js +192 -167
- package/dist/api-types.d.ts +301 -163
- package/dist/artifact-api-client.d.ts +28 -1
- package/dist/artifact-api-client.js +33 -0
- package/dist/artifact-commands.d.ts +5 -0
- package/dist/artifact-commands.js +177 -4
- package/dist/audit-commands.d.ts +12 -0
- package/dist/audit-commands.js +74 -0
- package/dist/auth-commands.d.ts +1 -0
- package/dist/auth-commands.js +195 -32
- package/dist/cli-errors.d.ts +7 -1
- package/dist/cli-errors.js +12 -1
- package/dist/command-builders.d.ts +1 -0
- package/dist/command-builders.js +1 -0
- package/dist/command-flags.d.ts +1 -0
- package/dist/command-flags.js +7 -0
- package/dist/command-groups.js +10 -12
- package/dist/command-registry.js +595 -277
- package/dist/commands.js +728 -636
- package/dist/environment-context.d.ts +9 -0
- package/dist/environment-context.js +32 -0
- package/dist/error-code.d.ts +2 -0
- package/dist/error-code.js +9 -0
- package/dist/{integration-commands.d.ts → extension-commands.d.ts} +3 -2
- package/dist/extension-commands.js +446 -0
- package/dist/files.d.ts +44 -4
- package/dist/files.js +349 -26
- package/dist/format.d.ts +6 -0
- package/dist/format.js +83 -1
- package/dist/runner.js +28 -10
- package/dist/schema-registry-data.d.ts +318 -571
- package/dist/schema-registry-data.js +356 -698
- package/dist/session-store.js +80 -0
- package/dist/session.d.ts +1 -0
- package/dist/transport-refresh.d.ts +10 -0
- package/dist/transport-refresh.js +51 -0
- package/dist/transport.d.ts +31 -0
- package/dist/transport.js +102 -36
- package/dist/types.d.ts +2 -1
- package/dist/workspace-source.d.ts +1 -0
- package/dist/workspace-source.js +13 -0
- package/package.json +2 -1
- package/dist/dotenv.d.ts +0 -1
- package/dist/dotenv.js +0 -26
- package/dist/integration-api-client.d.ts +0 -29
- package/dist/integration-api-client.js +0 -50
- package/dist/integration-commands.js +0 -208
|
@@ -1,14 +1,17 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { writeFile } from "node:fs/promises";
|
|
2
2
|
import { basename, resolve } from "node:path";
|
|
3
3
|
import { TextDecoder } from "node:util";
|
|
4
4
|
import { genericProblem, usageProblem } from "./cli-errors.js";
|
|
5
5
|
import { readOptionalNumberFlag, readOptionalStringFlag, readRequiredStringFlag } from "./command-flags.js";
|
|
6
|
-
import { renderSuccess, renderTable } from "./format.js";
|
|
6
|
+
import { renderKeyValue, renderSuccess, renderTable } from "./format.js";
|
|
7
|
+
import { readLocalFileBytes } from "./files.js";
|
|
8
|
+
import { confirmDestructive } from "./interaction.js";
|
|
7
9
|
export async function executeArtifactUpload(parsed, context, api, resolveOrgScope) {
|
|
8
10
|
const { org, session } = await resolveOrgScope(parsed, context, api);
|
|
9
11
|
const pathValue = readRequiredArg(parsed, "path");
|
|
10
|
-
const filePath =
|
|
11
|
-
|
|
12
|
+
const { absolutePath: filePath, bytes: fileBytes } = await readLocalFileBytes(pathValue, parsed.definition.id, {
|
|
13
|
+
regularFileMessage: "Artifact upload path must be a regular file, not a directory or symbolic link."
|
|
14
|
+
});
|
|
12
15
|
const data = await api.uploadArtifact(session, org.id, {
|
|
13
16
|
body: new Uint8Array(fileBytes),
|
|
14
17
|
...(readOptionalStringFlag(parsed, "sha256") ? { expectedSha256: readOptionalStringFlag(parsed, "sha256") } : {}),
|
|
@@ -40,6 +43,27 @@ export async function executeArtifactList(parsed, context, api, resolveOrgScope)
|
|
|
40
43
|
meta: { command: "artifact list", orgId: org.id }
|
|
41
44
|
};
|
|
42
45
|
}
|
|
46
|
+
export async function executeArtifactInventory(parsed, context, api, resolveOrgScope) {
|
|
47
|
+
const { org, session } = await resolveOrgScope(parsed, context, api);
|
|
48
|
+
const data = await api.listArtifacts(session, org.id, readArtifactInventoryFilters(parsed));
|
|
49
|
+
return {
|
|
50
|
+
data,
|
|
51
|
+
human: renderArtifactInventory(data.artifacts),
|
|
52
|
+
kind: "runtime_artifact_inventory",
|
|
53
|
+
meta: { command: "artifact inventory", orgId: org.id }
|
|
54
|
+
};
|
|
55
|
+
}
|
|
56
|
+
export async function executeArtifactInspect(parsed, context, api, resolveOrgScope) {
|
|
57
|
+
const { org, session } = await resolveOrgScope(parsed, context, api);
|
|
58
|
+
const artifactId = readRequiredArg(parsed, "artifact-id");
|
|
59
|
+
const data = await api.getArtifactDetail(session, org.id, artifactId);
|
|
60
|
+
return {
|
|
61
|
+
data,
|
|
62
|
+
human: renderArtifactDetail(data),
|
|
63
|
+
kind: "runtime_artifact_inspect",
|
|
64
|
+
meta: { command: "artifact inspect", orgId: org.id }
|
|
65
|
+
};
|
|
66
|
+
}
|
|
43
67
|
export async function executeArtifactDownload(parsed, context, api, resolveOrgScope) {
|
|
44
68
|
const { org, session } = await resolveOrgScope(parsed, context, api);
|
|
45
69
|
const artifactId = readRequiredArg(parsed, "artifact-id");
|
|
@@ -94,6 +118,40 @@ export async function executeArtifactRead(parsed, context, api, resolveOrgScope)
|
|
|
94
118
|
meta: { command: "artifact read", orgId: org.id }
|
|
95
119
|
};
|
|
96
120
|
}
|
|
121
|
+
export async function executeArtifactArchive(parsed, context, api, resolveOrgScope) {
|
|
122
|
+
const { org, session } = await resolveOrgScope(parsed, context, api);
|
|
123
|
+
const artifactId = readRequiredArg(parsed, "artifact-id");
|
|
124
|
+
const data = await api.archiveArtifact(session, org.id, artifactId);
|
|
125
|
+
return {
|
|
126
|
+
data,
|
|
127
|
+
human: renderSuccess(`Archived artifact ${data.artifact.name} (${data.artifact.artifactId}).`),
|
|
128
|
+
kind: "runtime_artifact_archive",
|
|
129
|
+
meta: { command: "artifact archive", orgId: org.id }
|
|
130
|
+
};
|
|
131
|
+
}
|
|
132
|
+
export async function executeArtifactRestore(parsed, context, api, resolveOrgScope) {
|
|
133
|
+
const { org, session } = await resolveOrgScope(parsed, context, api);
|
|
134
|
+
const artifactId = readRequiredArg(parsed, "artifact-id");
|
|
135
|
+
const data = await api.restoreArtifact(session, org.id, artifactId);
|
|
136
|
+
return {
|
|
137
|
+
data,
|
|
138
|
+
human: renderSuccess(`Restored artifact ${data.artifact.name} (${data.artifact.artifactId}).`),
|
|
139
|
+
kind: "runtime_artifact_restore",
|
|
140
|
+
meta: { command: "artifact restore", orgId: org.id }
|
|
141
|
+
};
|
|
142
|
+
}
|
|
143
|
+
export async function executeArtifactPurge(parsed, context, api, resolveOrgScope) {
|
|
144
|
+
const { org, session } = await resolveOrgScope(parsed, context, api);
|
|
145
|
+
const artifactId = readRequiredArg(parsed, "artifact-id");
|
|
146
|
+
await confirmDestructive(parsed, context, `Purge bytes for artifact ${artifactId}?`);
|
|
147
|
+
const data = await api.purgeArtifact(session, org.id, artifactId);
|
|
148
|
+
return {
|
|
149
|
+
data,
|
|
150
|
+
human: renderSuccess(`Purged artifact ${data.artifact.name} (${data.artifact.artifactId}); byte deletion is ${data.artifact.byteDeletionStatus}.`),
|
|
151
|
+
kind: "runtime_artifact_purge",
|
|
152
|
+
meta: { command: "artifact purge", orgId: org.id }
|
|
153
|
+
};
|
|
154
|
+
}
|
|
97
155
|
function readRequiredArg(parsed, name) {
|
|
98
156
|
const value = parsed.args[name];
|
|
99
157
|
if (!value) {
|
|
@@ -108,6 +166,108 @@ function readArtifactReadMaxBytes(parsed) {
|
|
|
108
166
|
}
|
|
109
167
|
return maxBytes;
|
|
110
168
|
}
|
|
169
|
+
function readArtifactInventoryLimit(parsed) {
|
|
170
|
+
const limit = readOptionalNumberFlag(parsed, "limit");
|
|
171
|
+
if (limit === undefined) {
|
|
172
|
+
return undefined;
|
|
173
|
+
}
|
|
174
|
+
if (!Number.isInteger(limit) || limit <= 0 || limit > 500) {
|
|
175
|
+
throw usageProblem("--limit must be an integer between 1 and 500.", parsed.definition.id);
|
|
176
|
+
}
|
|
177
|
+
return limit;
|
|
178
|
+
}
|
|
179
|
+
function readArtifactInventoryFilters(parsed) {
|
|
180
|
+
const producerType = readOptionalStringFlag(parsed, "producer-type");
|
|
181
|
+
const associationRole = readOptionalStringFlag(parsed, "association-role");
|
|
182
|
+
const associationKind = readOptionalStringFlag(parsed, "association-kind");
|
|
183
|
+
const runId = readOptionalStringFlag(parsed, "run-id");
|
|
184
|
+
const workflowId = readOptionalStringFlag(parsed, "workflow-id");
|
|
185
|
+
const workflowNodeTestId = readOptionalStringFlag(parsed, "workflow-node-test-id");
|
|
186
|
+
const nodeId = readOptionalStringFlag(parsed, "node-id");
|
|
187
|
+
const mediaType = readOptionalStringFlag(parsed, "media-type");
|
|
188
|
+
const lifecycleStatus = readOptionalArtifactLifecycleFilter(parsed);
|
|
189
|
+
const createdAfter = readOptionalStringFlag(parsed, "created-after");
|
|
190
|
+
const createdBefore = readOptionalStringFlag(parsed, "created-before");
|
|
191
|
+
const limit = readArtifactInventoryLimit(parsed);
|
|
192
|
+
return {
|
|
193
|
+
...(producerType ? { producerType } : {}),
|
|
194
|
+
...(associationRole ? { associationRole } : {}),
|
|
195
|
+
...(associationKind ? { associationKind } : {}),
|
|
196
|
+
...(runId ? { runId } : {}),
|
|
197
|
+
...(workflowId ? { workflowId } : {}),
|
|
198
|
+
...(workflowNodeTestId ? { workflowNodeTestId } : {}),
|
|
199
|
+
...(nodeId ? { nodeId } : {}),
|
|
200
|
+
...(mediaType ? { mediaType } : {}),
|
|
201
|
+
...(lifecycleStatus ? { lifecycleStatus } : {}),
|
|
202
|
+
...(createdAfter ? { createdAfter } : {}),
|
|
203
|
+
...(createdBefore ? { createdBefore } : {}),
|
|
204
|
+
...(limit !== undefined ? { limit } : {})
|
|
205
|
+
};
|
|
206
|
+
}
|
|
207
|
+
function renderArtifactInventory(records) {
|
|
208
|
+
return renderTable(records.map((record) => ({
|
|
209
|
+
artifactId: record.artifact.artifactId,
|
|
210
|
+
byteDeletionStatus: record.artifact.byteDeletionStatus,
|
|
211
|
+
inputUsageCount: record.associationSummary.inputUsageCount,
|
|
212
|
+
lifecycleStatus: record.artifact.lifecycleStatus,
|
|
213
|
+
name: record.artifact.name,
|
|
214
|
+
origin: formatArtifactAssociationOrigin(record.originAssociation),
|
|
215
|
+
outputOriginCount: record.associationSummary.outputOriginCount,
|
|
216
|
+
producerType: record.artifact.producerType,
|
|
217
|
+
reviewUsageCount: record.associationSummary.reviewUsageCount
|
|
218
|
+
})), [
|
|
219
|
+
{ key: "artifactId", label: "Artifact" },
|
|
220
|
+
{ key: "name", label: "Name" },
|
|
221
|
+
{ key: "lifecycleStatus", label: "Lifecycle" },
|
|
222
|
+
{ key: "byteDeletionStatus", label: "Bytes" },
|
|
223
|
+
{ key: "producerType", label: "Producer" },
|
|
224
|
+
{ key: "origin", label: "Origin" },
|
|
225
|
+
{ key: "inputUsageCount", label: "Inputs" },
|
|
226
|
+
{ key: "outputOriginCount", label: "Outputs" },
|
|
227
|
+
{ key: "reviewUsageCount", label: "Reviews" }
|
|
228
|
+
]);
|
|
229
|
+
}
|
|
230
|
+
function renderArtifactDetail(input) {
|
|
231
|
+
const summary = renderKeyValue([
|
|
232
|
+
{ label: "Artifact", value: input.artifact.artifactId },
|
|
233
|
+
{ label: "Name", value: input.artifact.name },
|
|
234
|
+
{ label: "Lifecycle", value: input.artifact.lifecycleStatus },
|
|
235
|
+
{ label: "Bytes", value: input.artifact.byteDeletionStatus },
|
|
236
|
+
{ label: "Producer", value: input.artifact.producerType },
|
|
237
|
+
{ label: "Media type", value: input.artifact.mediaType ?? "" },
|
|
238
|
+
{ label: "Size bytes", value: input.artifact.sizeBytes },
|
|
239
|
+
{ label: "Scan", value: input.artifact.scanStatus },
|
|
240
|
+
{ label: "Created", value: input.artifact.createdAt },
|
|
241
|
+
{ label: "Archived", value: input.artifact.archivedAt ?? "" },
|
|
242
|
+
{ label: "Purged", value: input.artifact.purgedAt ?? "" },
|
|
243
|
+
{ label: "Bytes deleted", value: input.artifact.bytesDeletedAt ?? "" }
|
|
244
|
+
]);
|
|
245
|
+
const associations = renderTable(input.associations.map((association) => ({
|
|
246
|
+
associatedAt: association.associatedAt,
|
|
247
|
+
associationKind: association.associationKind,
|
|
248
|
+
nodeId: association.nodeId ?? "",
|
|
249
|
+
origin: formatArtifactAssociationOrigin(association),
|
|
250
|
+
role: association.role,
|
|
251
|
+
})), [
|
|
252
|
+
{ key: "associationKind", label: "Kind" },
|
|
253
|
+
{ key: "role", label: "Role" },
|
|
254
|
+
{ key: "origin", label: "Origin" },
|
|
255
|
+
{ key: "nodeId", label: "Node" },
|
|
256
|
+
{ key: "associatedAt", label: "Associated" }
|
|
257
|
+
]);
|
|
258
|
+
return `${summary}\n\nAssociations:\n${associations}`;
|
|
259
|
+
}
|
|
260
|
+
function formatArtifactAssociationOrigin(association) {
|
|
261
|
+
if (!association) {
|
|
262
|
+
return "";
|
|
263
|
+
}
|
|
264
|
+
if (association.workflowNodeTestId) {
|
|
265
|
+
return association.workflowName
|
|
266
|
+
? `${association.workflowName}/${association.workflowNodeTestId}`
|
|
267
|
+
: association.workflowNodeTestId;
|
|
268
|
+
}
|
|
269
|
+
return association.workflowRunId ?? association.workflowId ?? "";
|
|
270
|
+
}
|
|
111
271
|
function isInlineArtifactMediaType(contentType) {
|
|
112
272
|
const normalized = contentType.split(";", 1)[0]?.trim().toLowerCase() ?? "";
|
|
113
273
|
if (normalized === "text/html" ||
|
|
@@ -124,3 +284,16 @@ function isInlineArtifactMediaType(contentType) {
|
|
|
124
284
|
function readArtifactContentType(headers) {
|
|
125
285
|
return headers.get("content-type") ?? "application/octet-stream";
|
|
126
286
|
}
|
|
287
|
+
function readOptionalArtifactLifecycleFilter(parsed) {
|
|
288
|
+
const lifecycleStatus = readOptionalStringFlag(parsed, "lifecycle-status");
|
|
289
|
+
if (!lifecycleStatus) {
|
|
290
|
+
return undefined;
|
|
291
|
+
}
|
|
292
|
+
if (lifecycleStatus !== "active" &&
|
|
293
|
+
lifecycleStatus !== "archived" &&
|
|
294
|
+
lifecycleStatus !== "purged" &&
|
|
295
|
+
lifecycleStatus !== "all") {
|
|
296
|
+
throw usageProblem("--lifecycle-status must be one of active, archived, purged, or all.", parsed.definition.id);
|
|
297
|
+
}
|
|
298
|
+
return lifecycleStatus;
|
|
299
|
+
}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import type { createPlatformApiClient } from "./api-client.js";
|
|
2
|
+
import type { CommandExecutionContext, ExecutedCommand } from "./command-types.js";
|
|
3
|
+
import type { ParsedCommand } from "./runner.js";
|
|
4
|
+
import type { CliSessionState } from "./session.js";
|
|
5
|
+
type ResolveOrgScope = (parsed: ParsedCommand, context: CommandExecutionContext, api: ReturnType<typeof createPlatformApiClient>) => Promise<{
|
|
6
|
+
org: {
|
|
7
|
+
id: string;
|
|
8
|
+
};
|
|
9
|
+
session: CliSessionState;
|
|
10
|
+
}>;
|
|
11
|
+
export declare function executeAudit(parsed: ParsedCommand, context: CommandExecutionContext, api: ReturnType<typeof createPlatformApiClient>, resolveOrgScope: ResolveOrgScope): Promise<ExecutedCommand>;
|
|
12
|
+
export {};
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
import { usageProblem, genericProblem } from "./cli-errors.js";
|
|
2
|
+
import { readOptionalNumberFlag, readOptionalStringFlag } from "./command-flags.js";
|
|
3
|
+
import { renderPrettyJson, renderTable } from "./format.js";
|
|
4
|
+
export async function executeAudit(parsed, context, api, resolveOrgScope) {
|
|
5
|
+
const { org, session } = await resolveOrgScope(parsed, context, api);
|
|
6
|
+
switch (parsed.definition.id) {
|
|
7
|
+
case "audit.list": {
|
|
8
|
+
const action = readOptionalStringFlag(parsed, "action");
|
|
9
|
+
const actorId = readOptionalStringFlag(parsed, "actor");
|
|
10
|
+
const category = readOptionalStringFlag(parsed, "category");
|
|
11
|
+
const from = readOptionalStringFlag(parsed, "from");
|
|
12
|
+
const limit = readOptionalNumberFlag(parsed, "limit");
|
|
13
|
+
const outcome = readOptionalStringFlag(parsed, "outcome");
|
|
14
|
+
const resourceId = readOptionalStringFlag(parsed, "resource-id");
|
|
15
|
+
const resourceType = readOptionalStringFlag(parsed, "resource-type");
|
|
16
|
+
const to = readOptionalStringFlag(parsed, "to");
|
|
17
|
+
const data = await api.listAuditEvents(session, org.id, {
|
|
18
|
+
...(action ? { action } : {}),
|
|
19
|
+
...(actorId ? { actorId } : {}),
|
|
20
|
+
...(category ? { category } : {}),
|
|
21
|
+
...(from ? { from } : {}),
|
|
22
|
+
...(limit !== undefined ? { limit } : {}),
|
|
23
|
+
...(outcome ? { outcome } : {}),
|
|
24
|
+
...(resourceId ? { resourceId } : {}),
|
|
25
|
+
...(resourceType ? { resourceType } : {}),
|
|
26
|
+
...(to ? { to } : {})
|
|
27
|
+
});
|
|
28
|
+
return {
|
|
29
|
+
data,
|
|
30
|
+
human: renderTable(data.events.map(formatAuditEventRow), [
|
|
31
|
+
{ key: "occurredAt", label: "Occurred" },
|
|
32
|
+
{ key: "outcome", label: "Outcome" },
|
|
33
|
+
{ key: "category", label: "Category" },
|
|
34
|
+
{ key: "action", label: "Action" },
|
|
35
|
+
{ key: "actorId", label: "Actor" },
|
|
36
|
+
{ key: "resource", label: "Resource" },
|
|
37
|
+
{ key: "id", label: "Event" }
|
|
38
|
+
]),
|
|
39
|
+
kind: "audit_event_list",
|
|
40
|
+
meta: { command: parsed.definition.path.join(" "), orgId: org.id }
|
|
41
|
+
};
|
|
42
|
+
}
|
|
43
|
+
case "audit.get": {
|
|
44
|
+
const data = await api.getAuditEvent(session, org.id, readRequiredArg(parsed, "event-id"));
|
|
45
|
+
return {
|
|
46
|
+
data,
|
|
47
|
+
human: renderPrettyJson(data.event),
|
|
48
|
+
kind: "audit_event_get",
|
|
49
|
+
meta: { command: parsed.definition.path.join(" "), orgId: org.id }
|
|
50
|
+
};
|
|
51
|
+
}
|
|
52
|
+
default:
|
|
53
|
+
throw genericProblem(`Unhandled audit command ${parsed.definition.id}.`, parsed.definition.id);
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
function formatAuditEventRow(event) {
|
|
57
|
+
return {
|
|
58
|
+
...event,
|
|
59
|
+
resource: formatAuditResource(event)
|
|
60
|
+
};
|
|
61
|
+
}
|
|
62
|
+
function formatAuditResource(event) {
|
|
63
|
+
if (event.resourceType && event.resourceId) {
|
|
64
|
+
return `${event.resourceType}:${event.resourceId}`;
|
|
65
|
+
}
|
|
66
|
+
return event.resourceType ?? event.resourceId ?? "";
|
|
67
|
+
}
|
|
68
|
+
function readRequiredArg(parsed, name) {
|
|
69
|
+
const value = parsed.args[name];
|
|
70
|
+
if (!value) {
|
|
71
|
+
throw usageProblem(`Missing required argument <${name}>.`, parsed.definition.id);
|
|
72
|
+
}
|
|
73
|
+
return value;
|
|
74
|
+
}
|
package/dist/auth-commands.d.ts
CHANGED
|
@@ -2,5 +2,6 @@ import type { createPlatformApiClient } from "./api-client.js";
|
|
|
2
2
|
import type { CommandExecutionContext, ExecutedCommand } from "./command-types.js";
|
|
3
3
|
import type { ParsedCommand } from "./runner.js";
|
|
4
4
|
export declare function executeAuthLogin(parsed: ParsedCommand, context: CommandExecutionContext, api: ReturnType<typeof createPlatformApiClient>): Promise<ExecutedCommand>;
|
|
5
|
+
export declare function executeAuthSignup(parsed: ParsedCommand, context: CommandExecutionContext, api: ReturnType<typeof createPlatformApiClient>): Promise<ExecutedCommand>;
|
|
5
6
|
export declare function executeAuthLogout(context: CommandExecutionContext, api: ReturnType<typeof createPlatformApiClient>): Promise<ExecutedCommand>;
|
|
6
7
|
export declare function executeAuthWhoami(context: CommandExecutionContext, api: ReturnType<typeof createPlatformApiClient>): Promise<ExecutedCommand>;
|
package/dist/auth-commands.js
CHANGED
|
@@ -1,8 +1,9 @@
|
|
|
1
|
-
import { authProblem, toProblem } from "./cli-errors.js";
|
|
1
|
+
import { authProblem, toProblem, usageProblem } from "./cli-errors.js";
|
|
2
|
+
import { readOptionalStringFlag } from "./command-flags.js";
|
|
2
3
|
import { writeCliConfig, readCliConfig, findRepoConfigPath } from "./config.js";
|
|
3
4
|
import { renderKeyValue, renderSuccess } from "./format.js";
|
|
4
5
|
import { chooseOrganization, ensureInteractive } from "./interaction.js";
|
|
5
|
-
import { promptPassword, promptText } from "./prompts.js";
|
|
6
|
+
import { isInteractive, promptPassword, promptText } from "./prompts.js";
|
|
6
7
|
export async function executeAuthLogin(parsed, context, api) {
|
|
7
8
|
const prompts = parsed.json
|
|
8
9
|
? {
|
|
@@ -10,13 +11,12 @@ export async function executeAuthLogin(parsed, context, api) {
|
|
|
10
11
|
stdout: context.stderr
|
|
11
12
|
}
|
|
12
13
|
: context;
|
|
14
|
+
if (parsed.flags.device === true || !isInteractive(prompts)) {
|
|
15
|
+
return executeDeviceLogin(parsed, context, api, prompts);
|
|
16
|
+
}
|
|
13
17
|
ensureInteractive(prompts, "auth login");
|
|
14
|
-
const
|
|
15
|
-
const
|
|
16
|
-
const repoConfig = repoConfigPath ? await readCliConfig(repoConfigPath) : {};
|
|
17
|
-
const envBaseUrl = context.env.KORA_BASE_URL;
|
|
18
|
-
const configuredBaseUrl = envBaseUrl ?? repoConfig.baseUrl ?? persistedGlobalConfig.baseUrl;
|
|
19
|
-
const baseUrl = configuredBaseUrl ?? await promptText("Platform base URL: ", prompts);
|
|
18
|
+
const resolvedBaseUrl = await resolveAuthBaseUrl(parsed, context, prompts);
|
|
19
|
+
const { baseUrl } = resolvedBaseUrl;
|
|
20
20
|
const authSettings = await readAuthSettingsOrDefault(api, baseUrl);
|
|
21
21
|
const oidcLoginUrl = buildOidcStartUrl(baseUrl);
|
|
22
22
|
if (authSettings.oidcEnabled && !authSettings.localEnabled) {
|
|
@@ -28,8 +28,8 @@ export async function executeAuthLogin(parsed, context, api) {
|
|
|
28
28
|
},
|
|
29
29
|
human: [
|
|
30
30
|
"SSO is required for this deployment.",
|
|
31
|
-
`
|
|
32
|
-
|
|
31
|
+
"Run `kora auth login --device` to approve this login in the browser instead.",
|
|
32
|
+
`SSO sign-in URL: ${oidcLoginUrl}`
|
|
33
33
|
].join("\n"),
|
|
34
34
|
kind: "access_auth_login_sso_required",
|
|
35
35
|
meta: {
|
|
@@ -45,41 +45,153 @@ export async function executeAuthLogin(parsed, context, api) {
|
|
|
45
45
|
const email = await promptText("Email: ", prompts);
|
|
46
46
|
const password = await promptPassword("Password: ", prompts);
|
|
47
47
|
const session = await api.login({ baseUrl, email, password });
|
|
48
|
-
const
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
48
|
+
const nextSession = await writeSessionWithSelectedOrg({
|
|
49
|
+
api,
|
|
50
|
+
context,
|
|
51
|
+
parsed,
|
|
52
|
+
prompts,
|
|
53
|
+
session
|
|
54
|
+
});
|
|
55
|
+
if (resolvedBaseUrl.shouldPersistGlobalBaseUrl) {
|
|
56
|
+
await writeCliConfig(context.configPath, {
|
|
57
|
+
...resolvedBaseUrl.persistedGlobalConfig,
|
|
58
|
+
baseUrl
|
|
59
|
+
});
|
|
60
|
+
}
|
|
61
|
+
return {
|
|
62
|
+
data: {
|
|
63
|
+
activeOrg: nextSession.activeOrg,
|
|
64
|
+
user: nextSession.user
|
|
65
|
+
},
|
|
66
|
+
human: renderSuccess(nextSession.activeOrg
|
|
67
|
+
? `Logged in as ${nextSession.user.email}. Active org: ${nextSession.activeOrg.slug}.`
|
|
68
|
+
: `Logged in as ${nextSession.user.email}.`),
|
|
69
|
+
kind: "access_auth_login",
|
|
70
|
+
meta: {
|
|
71
|
+
command: "auth login",
|
|
72
|
+
orgId: nextSession.activeOrg?.id ?? null
|
|
73
|
+
}
|
|
74
|
+
};
|
|
75
|
+
}
|
|
76
|
+
export async function executeAuthSignup(parsed, context, api) {
|
|
77
|
+
const prompts = parsed.json
|
|
78
|
+
? {
|
|
79
|
+
...context,
|
|
80
|
+
stdout: context.stderr
|
|
81
|
+
}
|
|
82
|
+
: context;
|
|
83
|
+
ensureInteractive(prompts, "auth signup");
|
|
84
|
+
const resolvedBaseUrl = await resolveAuthBaseUrl(parsed, context, prompts);
|
|
85
|
+
const { baseUrl } = resolvedBaseUrl;
|
|
86
|
+
const authSettings = await readAuthSettingsOrDefault(api, baseUrl);
|
|
87
|
+
const oidcLoginUrl = buildOidcStartUrl(baseUrl);
|
|
88
|
+
if (!authSettings.localEnabled) {
|
|
89
|
+
return {
|
|
90
|
+
data: {
|
|
91
|
+
loginUrl: oidcLoginUrl,
|
|
92
|
+
mode: authSettings.mode
|
|
93
|
+
},
|
|
94
|
+
human: [
|
|
95
|
+
"Local account signup is disabled for this deployment.",
|
|
96
|
+
`Account creation is managed by SSO. Sign in once in a browser: ${oidcLoginUrl}`,
|
|
97
|
+
"Then run `kora auth login --device` to approve a CLI login in the browser."
|
|
98
|
+
].join("\n"),
|
|
99
|
+
kind: "access_auth_signup_sso_required",
|
|
100
|
+
meta: {
|
|
101
|
+
command: "auth signup",
|
|
102
|
+
orgId: null
|
|
65
103
|
}
|
|
66
|
-
|
|
104
|
+
};
|
|
105
|
+
}
|
|
106
|
+
if (authSettings.oidcEnabled) {
|
|
107
|
+
prompts.stdout.write(`SSO is also available at: ${oidcLoginUrl}\n`);
|
|
108
|
+
prompts.stdout.write("Continuing with local account signup.\n");
|
|
109
|
+
}
|
|
110
|
+
const name = await promptText("Name: ", prompts);
|
|
111
|
+
const email = await promptText("Email: ", prompts);
|
|
112
|
+
const password = await promptPassword("Password: ", prompts);
|
|
113
|
+
const session = await api.signup({ baseUrl, email, name, password });
|
|
114
|
+
const nextSession = await writeSessionWithSelectedOrg({
|
|
115
|
+
api,
|
|
116
|
+
context,
|
|
117
|
+
parsed,
|
|
118
|
+
prompts,
|
|
119
|
+
session
|
|
120
|
+
});
|
|
121
|
+
if (resolvedBaseUrl.shouldPersistGlobalBaseUrl) {
|
|
122
|
+
await writeCliConfig(context.configPath, {
|
|
123
|
+
...resolvedBaseUrl.persistedGlobalConfig,
|
|
124
|
+
baseUrl
|
|
125
|
+
});
|
|
126
|
+
}
|
|
127
|
+
return {
|
|
128
|
+
data: {
|
|
129
|
+
activeOrg: nextSession.activeOrg,
|
|
130
|
+
user: nextSession.user
|
|
131
|
+
},
|
|
132
|
+
human: renderSuccess(nextSession.activeOrg
|
|
133
|
+
? `Signed up as ${nextSession.user.email}. Active org: ${nextSession.activeOrg.slug}.`
|
|
134
|
+
: `Signed up as ${nextSession.user.email}.`),
|
|
135
|
+
kind: "access_auth_signup",
|
|
136
|
+
meta: {
|
|
137
|
+
command: "auth signup",
|
|
138
|
+
orgId: nextSession.activeOrg?.id ?? null
|
|
139
|
+
}
|
|
67
140
|
};
|
|
68
|
-
|
|
69
|
-
|
|
141
|
+
}
|
|
142
|
+
async function executeDeviceLogin(parsed, context, api, prompts) {
|
|
143
|
+
const resolvedBaseUrl = await resolveAuthBaseUrl(parsed, context, prompts, {
|
|
144
|
+
allowPrompt: isInteractive(prompts)
|
|
145
|
+
});
|
|
146
|
+
const { baseUrl } = resolvedBaseUrl;
|
|
147
|
+
const started = await api.startDeviceLogin(baseUrl);
|
|
148
|
+
const verificationUrl = buildDeviceVerificationUrl(baseUrl, started.verificationPath, started.userCode);
|
|
149
|
+
prompts.stdout.write(`Open this URL in a browser to approve the login:\n\n ${verificationUrl}\n\n`);
|
|
150
|
+
prompts.stdout.write(`Confirmation code: ${started.userCode}\n`);
|
|
151
|
+
prompts.stdout.write("Waiting for browser approval...\n");
|
|
152
|
+
const expiresAtMs = Date.parse(started.expiresAt);
|
|
153
|
+
const pollIntervalMs = Math.max(0, started.pollIntervalSeconds * 1000);
|
|
154
|
+
let session = null;
|
|
155
|
+
while (Date.now() < expiresAtMs) {
|
|
156
|
+
const claim = await api.claimDeviceLogin({ baseUrl, deviceCode: started.deviceCode });
|
|
157
|
+
if (claim.status === "approved") {
|
|
158
|
+
session = claim.session;
|
|
159
|
+
break;
|
|
160
|
+
}
|
|
161
|
+
if (claim.status === "denied") {
|
|
162
|
+
throw authProblem("Device login was denied in the browser.", "auth login");
|
|
163
|
+
}
|
|
164
|
+
if (claim.status === "expired") {
|
|
165
|
+
break;
|
|
166
|
+
}
|
|
167
|
+
await sleep(pollIntervalMs);
|
|
168
|
+
}
|
|
169
|
+
if (!session) {
|
|
170
|
+
throw authProblem("Device login expired before it was approved. Run `kora auth login` again.", "auth login");
|
|
171
|
+
}
|
|
172
|
+
const nextSession = await writeSessionWithSelectedOrg({
|
|
173
|
+
allowInteractiveOrgChoice: isInteractive(prompts),
|
|
174
|
+
api,
|
|
175
|
+
context,
|
|
176
|
+
parsed,
|
|
177
|
+
prompts,
|
|
178
|
+
session
|
|
179
|
+
});
|
|
180
|
+
if (resolvedBaseUrl.shouldPersistGlobalBaseUrl) {
|
|
70
181
|
await writeCliConfig(context.configPath, {
|
|
71
|
-
...persistedGlobalConfig,
|
|
182
|
+
...resolvedBaseUrl.persistedGlobalConfig,
|
|
72
183
|
baseUrl
|
|
73
184
|
});
|
|
74
185
|
}
|
|
75
186
|
return {
|
|
76
187
|
data: {
|
|
77
188
|
activeOrg: nextSession.activeOrg,
|
|
189
|
+
method: "device",
|
|
78
190
|
user: nextSession.user
|
|
79
191
|
},
|
|
80
192
|
human: renderSuccess(nextSession.activeOrg
|
|
81
193
|
? `Logged in as ${nextSession.user.email}. Active org: ${nextSession.activeOrg.slug}.`
|
|
82
|
-
: `Logged in as ${nextSession.user.email}
|
|
194
|
+
: `Logged in as ${nextSession.user.email}. No active organization selected; use \`kora org select <org>\` or \`kora org create\`.`),
|
|
83
195
|
kind: "access_auth_login",
|
|
84
196
|
meta: {
|
|
85
197
|
command: "auth login",
|
|
@@ -87,6 +199,57 @@ export async function executeAuthLogin(parsed, context, api) {
|
|
|
87
199
|
}
|
|
88
200
|
};
|
|
89
201
|
}
|
|
202
|
+
function buildDeviceVerificationUrl(baseUrl, verificationPath, userCode) {
|
|
203
|
+
const normalizedBaseUrl = baseUrl.replace(/\/+$/u, "");
|
|
204
|
+
const normalizedPath = verificationPath.startsWith("/") ? verificationPath : `/${verificationPath}`;
|
|
205
|
+
return `${normalizedBaseUrl}${normalizedPath}?code=${encodeURIComponent(userCode)}`;
|
|
206
|
+
}
|
|
207
|
+
function sleep(ms) {
|
|
208
|
+
return new Promise((resolve) => {
|
|
209
|
+
setTimeout(resolve, ms);
|
|
210
|
+
});
|
|
211
|
+
}
|
|
212
|
+
async function resolveAuthBaseUrl(parsed, context, prompts, options = { allowPrompt: true }) {
|
|
213
|
+
const persistedGlobalConfig = await readCliConfig(context.configPath);
|
|
214
|
+
const repoConfigPath = await findRepoConfigPath(context.cwd);
|
|
215
|
+
const repoConfig = repoConfigPath ? await readCliConfig(repoConfigPath) : {};
|
|
216
|
+
const flagBaseUrl = readOptionalStringFlag(parsed, "base-url");
|
|
217
|
+
const envBaseUrl = context.env.KORA_BASE_URL;
|
|
218
|
+
const configuredBaseUrl = flagBaseUrl ?? envBaseUrl ?? repoConfig.baseUrl ?? persistedGlobalConfig.baseUrl;
|
|
219
|
+
if (!configuredBaseUrl && !options.allowPrompt) {
|
|
220
|
+
throw usageProblem("Pass --base-url <url> or set KORA_BASE_URL to log in from a non-interactive shell.", "auth login");
|
|
221
|
+
}
|
|
222
|
+
const baseUrl = configuredBaseUrl ?? await promptText("Platform base URL: ", prompts);
|
|
223
|
+
return {
|
|
224
|
+
baseUrl,
|
|
225
|
+
persistedGlobalConfig,
|
|
226
|
+
shouldPersistGlobalBaseUrl: !flagBaseUrl && !envBaseUrl && !repoConfig.baseUrl
|
|
227
|
+
};
|
|
228
|
+
}
|
|
229
|
+
async function writeSessionWithSelectedOrg(input) {
|
|
230
|
+
const organizations = (await input.api.listOrganizations(input.session)).organizations;
|
|
231
|
+
const activeOrg = organizations.length === 1
|
|
232
|
+
? organizations[0] ?? null
|
|
233
|
+
: organizations.length > 1 && (input.allowInteractiveOrgChoice ?? true)
|
|
234
|
+
? await chooseOrganization(organizations, input.prompts, input.parsed)
|
|
235
|
+
: null;
|
|
236
|
+
const latestSession = await readLatestSessionForWrite(input.context.sessionStore, input.session);
|
|
237
|
+
const sessionForWrite = isSameSessionIdentity(latestSession, input.session) ? latestSession : input.session;
|
|
238
|
+
const nextSession = {
|
|
239
|
+
...sessionForWrite,
|
|
240
|
+
activeOrg: activeOrg
|
|
241
|
+
? {
|
|
242
|
+
id: activeOrg.id,
|
|
243
|
+
name: activeOrg.name,
|
|
244
|
+
role: activeOrg.role,
|
|
245
|
+
slug: activeOrg.slug,
|
|
246
|
+
status: activeOrg.status
|
|
247
|
+
}
|
|
248
|
+
: null
|
|
249
|
+
};
|
|
250
|
+
await input.context.sessionStore.write(nextSession);
|
|
251
|
+
return nextSession;
|
|
252
|
+
}
|
|
90
253
|
async function readAuthSettingsOrDefault(api, baseUrl) {
|
|
91
254
|
try {
|
|
92
255
|
return await api.getAuthSettings(baseUrl);
|
package/dist/cli-errors.d.ts
CHANGED
|
@@ -1,13 +1,18 @@
|
|
|
1
1
|
import { ApiError } from "./transport.js";
|
|
2
|
+
type CliProblemDetails = Record<string, unknown>;
|
|
2
3
|
export declare class CliProblem extends Error {
|
|
4
|
+
readonly code: string;
|
|
3
5
|
readonly detail: string;
|
|
6
|
+
readonly details?: CliProblemDetails;
|
|
4
7
|
readonly exitCode: number;
|
|
5
8
|
readonly instance: string;
|
|
6
9
|
readonly status: number;
|
|
7
10
|
readonly title: string;
|
|
8
11
|
readonly type: string;
|
|
9
12
|
constructor(input: {
|
|
13
|
+
code: string;
|
|
10
14
|
detail: string;
|
|
15
|
+
details?: CliProblemDetails;
|
|
11
16
|
exitCode: number;
|
|
12
17
|
instance: string;
|
|
13
18
|
status: number;
|
|
@@ -15,9 +20,10 @@ export declare class CliProblem extends Error {
|
|
|
15
20
|
type: string;
|
|
16
21
|
});
|
|
17
22
|
}
|
|
18
|
-
export declare function usageProblem(detail: string, instance: string): CliProblem;
|
|
23
|
+
export declare function usageProblem(detail: string, instance: string, details?: CliProblemDetails): CliProblem;
|
|
19
24
|
export declare function authProblem(detail: string, instance: string): CliProblem;
|
|
20
25
|
export declare function notFoundProblem(detail: string, instance: string): CliProblem;
|
|
21
26
|
export declare function genericProblem(detail: string, instance: string): CliProblem;
|
|
22
27
|
export declare function toProblem(error: unknown, instance: string): CliProblem | ApiError;
|
|
23
28
|
export declare function exitCodeFromError(error: CliProblem | ApiError): number;
|
|
29
|
+
export {};
|
package/dist/cli-errors.js
CHANGED
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
import { ApiError } from "./transport.js";
|
|
2
2
|
export class CliProblem extends Error {
|
|
3
|
+
code;
|
|
3
4
|
detail;
|
|
5
|
+
details;
|
|
4
6
|
exitCode;
|
|
5
7
|
instance;
|
|
6
8
|
status;
|
|
@@ -9,7 +11,11 @@ export class CliProblem extends Error {
|
|
|
9
11
|
constructor(input) {
|
|
10
12
|
super(input.detail);
|
|
11
13
|
this.name = "CliProblem";
|
|
14
|
+
this.code = input.code;
|
|
12
15
|
this.detail = input.detail;
|
|
16
|
+
if (input.details) {
|
|
17
|
+
this.details = input.details;
|
|
18
|
+
}
|
|
13
19
|
this.exitCode = input.exitCode;
|
|
14
20
|
this.instance = input.instance;
|
|
15
21
|
this.status = input.status;
|
|
@@ -17,9 +23,11 @@ export class CliProblem extends Error {
|
|
|
17
23
|
this.type = input.type;
|
|
18
24
|
}
|
|
19
25
|
}
|
|
20
|
-
export function usageProblem(detail, instance) {
|
|
26
|
+
export function usageProblem(detail, instance, details) {
|
|
21
27
|
return new CliProblem({
|
|
28
|
+
code: "cli/usage",
|
|
22
29
|
detail,
|
|
30
|
+
...(details ? { details } : {}),
|
|
23
31
|
exitCode: 2,
|
|
24
32
|
instance,
|
|
25
33
|
status: 400,
|
|
@@ -29,6 +37,7 @@ export function usageProblem(detail, instance) {
|
|
|
29
37
|
}
|
|
30
38
|
export function authProblem(detail, instance) {
|
|
31
39
|
return new CliProblem({
|
|
40
|
+
code: "cli/auth",
|
|
32
41
|
detail,
|
|
33
42
|
exitCode: 3,
|
|
34
43
|
instance,
|
|
@@ -39,6 +48,7 @@ export function authProblem(detail, instance) {
|
|
|
39
48
|
}
|
|
40
49
|
export function notFoundProblem(detail, instance) {
|
|
41
50
|
return new CliProblem({
|
|
51
|
+
code: "cli/not_found",
|
|
42
52
|
detail,
|
|
43
53
|
exitCode: 4,
|
|
44
54
|
instance,
|
|
@@ -49,6 +59,7 @@ export function notFoundProblem(detail, instance) {
|
|
|
49
59
|
}
|
|
50
60
|
export function genericProblem(detail, instance) {
|
|
51
61
|
return new CliProblem({
|
|
62
|
+
code: "cli/error",
|
|
52
63
|
detail,
|
|
53
64
|
exitCode: 1,
|
|
54
65
|
instance,
|