@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.
Files changed (49) hide show
  1. package/README.md +21 -0
  2. package/dist/api-client.d.ts +274 -106
  3. package/dist/api-client.js +192 -167
  4. package/dist/api-types.d.ts +301 -163
  5. package/dist/artifact-api-client.d.ts +28 -1
  6. package/dist/artifact-api-client.js +33 -0
  7. package/dist/artifact-commands.d.ts +5 -0
  8. package/dist/artifact-commands.js +177 -4
  9. package/dist/audit-commands.d.ts +12 -0
  10. package/dist/audit-commands.js +74 -0
  11. package/dist/auth-commands.d.ts +1 -0
  12. package/dist/auth-commands.js +195 -32
  13. package/dist/cli-errors.d.ts +7 -1
  14. package/dist/cli-errors.js +12 -1
  15. package/dist/command-builders.d.ts +1 -0
  16. package/dist/command-builders.js +1 -0
  17. package/dist/command-flags.d.ts +1 -0
  18. package/dist/command-flags.js +7 -0
  19. package/dist/command-groups.js +10 -12
  20. package/dist/command-registry.js +595 -277
  21. package/dist/commands.js +728 -636
  22. package/dist/environment-context.d.ts +9 -0
  23. package/dist/environment-context.js +32 -0
  24. package/dist/error-code.d.ts +2 -0
  25. package/dist/error-code.js +9 -0
  26. package/dist/{integration-commands.d.ts → extension-commands.d.ts} +3 -2
  27. package/dist/extension-commands.js +446 -0
  28. package/dist/files.d.ts +44 -4
  29. package/dist/files.js +349 -26
  30. package/dist/format.d.ts +6 -0
  31. package/dist/format.js +83 -1
  32. package/dist/runner.js +28 -10
  33. package/dist/schema-registry-data.d.ts +318 -571
  34. package/dist/schema-registry-data.js +356 -698
  35. package/dist/session-store.js +80 -0
  36. package/dist/session.d.ts +1 -0
  37. package/dist/transport-refresh.d.ts +10 -0
  38. package/dist/transport-refresh.js +51 -0
  39. package/dist/transport.d.ts +31 -0
  40. package/dist/transport.js +102 -36
  41. package/dist/types.d.ts +2 -1
  42. package/dist/workspace-source.d.ts +1 -0
  43. package/dist/workspace-source.js +13 -0
  44. package/package.json +2 -1
  45. package/dist/dotenv.d.ts +0 -1
  46. package/dist/dotenv.js +0 -26
  47. package/dist/integration-api-client.d.ts +0 -29
  48. package/dist/integration-api-client.js +0 -50
  49. package/dist/integration-commands.js +0 -208
@@ -1,14 +1,17 @@
1
- import { readFile, writeFile } from "node:fs/promises";
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 = resolve(pathValue);
11
- const fileBytes = await readFile(filePath);
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
+ }
@@ -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>;
@@ -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 persistedGlobalConfig = await readCliConfig(context.configPath);
15
- const repoConfigPath = await findRepoConfigPath(context.cwd);
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
- `Open this URL in a browser: ${oidcLoginUrl}`,
32
- "The CLI does not complete browser SSO login in this release. API keys remain the supported non-browser CLI auth path."
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 organizations = (await api.listOrganizations(session)).organizations;
49
- const activeOrg = organizations.length === 1
50
- ? organizations[0] ?? null
51
- : organizations.length > 1
52
- ? await chooseOrganization(organizations, prompts, parsed)
53
- : null;
54
- const latestSession = await readLatestSessionForWrite(context.sessionStore, session);
55
- const sessionForWrite = isSameSessionIdentity(latestSession, session) ? latestSession : session;
56
- const nextSession = {
57
- ...sessionForWrite,
58
- activeOrg: activeOrg
59
- ? {
60
- id: activeOrg.id,
61
- name: activeOrg.name,
62
- role: activeOrg.role,
63
- slug: activeOrg.slug,
64
- status: activeOrg.status
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
- : null
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
- await context.sessionStore.write(nextSession);
69
- if (!envBaseUrl && !repoConfig.baseUrl) {
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);
@@ -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 {};
@@ -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,