@barekey/cli 0.4.0 → 0.5.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.
Files changed (60) hide show
  1. package/README.md +53 -12
  2. package/bun.lock +9 -3
  3. package/dist/auth-provider.js +7 -4
  4. package/dist/command-utils.js +6 -6
  5. package/dist/commands/audit.d.ts +2 -0
  6. package/dist/commands/audit.js +47 -0
  7. package/dist/commands/auth.js +22 -7
  8. package/dist/commands/billing.d.ts +2 -0
  9. package/dist/commands/billing.js +62 -0
  10. package/dist/commands/env.js +157 -125
  11. package/dist/commands/init.d.ts +2 -0
  12. package/dist/commands/init.js +32 -0
  13. package/dist/commands/org.d.ts +2 -0
  14. package/dist/commands/org.js +85 -0
  15. package/dist/commands/project.d.ts +2 -0
  16. package/dist/commands/project.js +99 -0
  17. package/dist/commands/stage.d.ts +2 -0
  18. package/dist/commands/stage.js +125 -0
  19. package/dist/commands/target-prompts.d.ts +184 -0
  20. package/dist/commands/target-prompts.js +312 -0
  21. package/dist/commands/typegen.d.ts +2 -2
  22. package/dist/commands/typegen.js +57 -32
  23. package/dist/constants.d.ts +1 -1
  24. package/dist/constants.js +1 -1
  25. package/dist/context/session-id.d.ts +11 -0
  26. package/dist/context/session-id.js +14 -0
  27. package/dist/contracts/index.d.ts +491 -0
  28. package/dist/contracts/index.js +307 -0
  29. package/dist/credentials-store.js +70 -11
  30. package/dist/http.d.ts +34 -0
  31. package/dist/http.js +56 -2
  32. package/dist/index.js +12 -0
  33. package/dist/runtime-config.js +14 -26
  34. package/dist/typegen/core.d.ts +45 -0
  35. package/dist/typegen/core.js +219 -0
  36. package/dist/types.d.ts +5 -3
  37. package/package.json +2 -2
  38. package/src/auth-provider.ts +8 -5
  39. package/src/command-utils.ts +6 -6
  40. package/src/commands/audit.ts +63 -0
  41. package/src/commands/auth.ts +32 -37
  42. package/src/commands/billing.ts +73 -0
  43. package/src/commands/env.ts +211 -218
  44. package/src/commands/init.ts +47 -0
  45. package/src/commands/org.ts +104 -0
  46. package/src/commands/project.ts +130 -0
  47. package/src/commands/stage.ts +167 -0
  48. package/src/commands/target-prompts.ts +357 -0
  49. package/src/commands/typegen.ts +71 -45
  50. package/src/constants.ts +1 -1
  51. package/src/context/session-id.ts +14 -0
  52. package/src/contracts/index.ts +370 -0
  53. package/src/credentials-store.ts +86 -12
  54. package/src/http.ts +78 -2
  55. package/src/index.ts +12 -0
  56. package/src/runtime-config.ts +19 -32
  57. package/src/typegen/core.ts +311 -0
  58. package/src/types.ts +5 -3
  59. package/test/command-utils.test.ts +47 -0
  60. package/test/credentials-store.test.ts +40 -0
@@ -0,0 +1,45 @@
1
+ import { type TypegenManifest } from "../contracts/index.js";
2
+ export type CliTypegenMode = "semantic" | "minimal";
3
+ export type CliRuntimeMode = "centralized" | "standalone";
4
+ export type CliTypegenResult = {
5
+ written: boolean;
6
+ path: string;
7
+ serverPath: string;
8
+ publicPath: string;
9
+ manifestVersion: string;
10
+ };
11
+ type TypegenIdentity = {
12
+ baseUrl: string | null;
13
+ orgSlug: string;
14
+ projectSlug: string;
15
+ stageSlug: string;
16
+ typegenMode: CliTypegenMode;
17
+ runtimeMode: CliRuntimeMode;
18
+ localEnvRoot: string | null;
19
+ };
20
+ /**
21
+ * Renders the generated SDK declaration files for one manifest.
22
+ *
23
+ * @param manifest The decoded typegen manifest.
24
+ * @param mode The requested typegen mode.
25
+ * @returns The rendered server and public declaration contents.
26
+ * @remarks The CLI owns this rendering so it no longer depends on SDK runtime helpers.
27
+ * @lastModified 2026-03-19
28
+ * @author GPT-5.4
29
+ */
30
+ export declare function renderGeneratedTypesForManifest(manifest: TypegenManifest, mode: CliTypegenMode): {
31
+ serverContents: string;
32
+ publicContents: string;
33
+ };
34
+ /**
35
+ * Writes generated SDK declaration files into the installed `@barekey/sdk` package.
36
+ *
37
+ * @param manifest The decoded typegen manifest.
38
+ * @param identity The manifest identity used for metadata freshness.
39
+ * @returns The typegen write result.
40
+ * @remarks This preserves the existing generated file locations without importing the SDK package at runtime.
41
+ * @lastModified 2026-03-19
42
+ * @author GPT-5.4
43
+ */
44
+ export declare function writeInstalledSdkGeneratedTypes(manifest: TypegenManifest, identity: TypegenIdentity): Promise<CliTypegenResult>;
45
+ export {};
@@ -0,0 +1,219 @@
1
+ import { createRequire } from "node:module";
2
+ import { access, readFile, rename, unlink, writeFile } from "node:fs/promises";
3
+ import path from "node:path";
4
+ import { Schema } from "effect";
5
+ import { TypegenManifestSchema } from "../contracts/index.js";
6
+ const MANIFEST_VERSION_PATTERN = /\/\* barekey-manifest-version: ([^\n]+) \*\//;
7
+ const TypegenMetadataSchema = Schema.Struct({
8
+ version: Schema.Number,
9
+ last: Schema.String,
10
+ baseUrl: Schema.NullOr(Schema.String),
11
+ orgSlug: Schema.NullOr(Schema.String),
12
+ projectSlug: Schema.NullOr(Schema.String),
13
+ stageSlug: Schema.NullOr(Schema.String),
14
+ typegenMode: Schema.NullOr(Schema.Literal("semantic", "minimal")),
15
+ runtimeMode: Schema.NullOr(Schema.Literal("centralized", "standalone")),
16
+ localEnvRoot: Schema.NullOr(Schema.String),
17
+ });
18
+ function renderLinearMilestones(milestones) {
19
+ if (milestones.length === 0) {
20
+ return "readonly []";
21
+ }
22
+ return `readonly [${milestones
23
+ .map((milestone) => `readonly [${JSON.stringify(milestone.at)}, ${String(milestone.percentage)}]`)
24
+ .join(", ")}]`;
25
+ }
26
+ function renderRolloutMetadataType(row) {
27
+ const renderedMilestones = renderLinearMilestones(row.rolloutMilestones ?? []);
28
+ if (row.rolloutFunction === "step") {
29
+ return `Step<${renderedMilestones}>`;
30
+ }
31
+ if (row.rolloutFunction === "ease_in_out") {
32
+ return `EaseInOut<${renderedMilestones}>`;
33
+ }
34
+ return `Linear<${renderedMilestones}>`;
35
+ }
36
+ function renderVariableType(row, mode) {
37
+ if (mode === "minimal") {
38
+ return row.typeScriptType;
39
+ }
40
+ const descriptor = `{ Type: ${row.typeScriptType}; Kind: ${JSON.stringify(row.kind)}; Visibility: ${JSON.stringify(row.visibility)}; Rollout: ${row.kind === "rollout" ? renderRolloutMetadataType(row) : "never"} }`;
41
+ return `Env<${descriptor}>`;
42
+ }
43
+ function buildGeneratedTypesContents(manifest, input) {
44
+ const mapLines = manifest.variables
45
+ .slice()
46
+ .filter(input.include)
47
+ .sort((left, right) => left.name.localeCompare(right.name))
48
+ .map((row) => ` ${JSON.stringify(row.name)}: ${renderVariableType(row, input.mode)};`)
49
+ .join("\n");
50
+ const importLine = input.mode === "semantic"
51
+ ? `import type { EaseInOut, Env, Linear, Step } from "${input.typeModulePath}";\n\n`
52
+ : "";
53
+ return `/* eslint-disable */\n/* This file is generated by barekey typegen. */\n/* barekey-manifest-version: ${manifest.manifestVersion} */\n\n${importLine}declare module "${input.declaredModulePath}" {\n interface ${input.interfaceName} {\n${mapLines.length > 0 ? mapLines : ""}\n }\n}\n\nexport {};\n`;
54
+ }
55
+ function readManifestVersion(contents) {
56
+ return contents?.match(MANIFEST_VERSION_PATTERN)?.[1]?.trim() ?? null;
57
+ }
58
+ async function readTextFile(filePath) {
59
+ try {
60
+ return await readFile(filePath, "utf8");
61
+ }
62
+ catch (error) {
63
+ const nodeError = error;
64
+ if (nodeError?.code === "ENOENT") {
65
+ return null;
66
+ }
67
+ throw error;
68
+ }
69
+ }
70
+ async function writeTextFileAtomic(filePath, value) {
71
+ const temporaryPath = `${filePath}.${process.pid}.${Date.now()}.tmp`;
72
+ try {
73
+ await writeFile(temporaryPath, value, "utf8");
74
+ await rename(temporaryPath, filePath);
75
+ }
76
+ catch (error) {
77
+ try {
78
+ await unlink(temporaryPath);
79
+ }
80
+ catch {
81
+ // Best-effort cleanup only.
82
+ }
83
+ throw error;
84
+ }
85
+ }
86
+ async function resolveInstalledSdkTypegenTarget() {
87
+ const require = createRequire(path.join(process.cwd(), "__barekey_typegen__.cjs"));
88
+ let resolvedEntrypoint;
89
+ try {
90
+ resolvedEntrypoint = require.resolve("@barekey/sdk/package.json");
91
+ }
92
+ catch {
93
+ return null;
94
+ }
95
+ let current = path.dirname(resolvedEntrypoint);
96
+ while (true) {
97
+ const candidatePackageJson = path.join(current, "package.json");
98
+ const rawPackageJson = await readTextFile(candidatePackageJson);
99
+ if (rawPackageJson !== null) {
100
+ try {
101
+ const parsed = JSON.parse(rawPackageJson);
102
+ if (parsed.name === "@barekey/sdk") {
103
+ const serverGeneratedTypesPath = path.join(current, "generated.server.d.ts");
104
+ const publicGeneratedTypesPath = path.join(current, "generated.public.d.ts");
105
+ await access(serverGeneratedTypesPath);
106
+ await access(publicGeneratedTypesPath);
107
+ return {
108
+ packageRoot: current,
109
+ serverGeneratedTypesPath,
110
+ publicGeneratedTypesPath,
111
+ typegenMetadataPath: path.join(current, "typegen.json"),
112
+ };
113
+ }
114
+ }
115
+ catch {
116
+ // Keep walking upward.
117
+ }
118
+ }
119
+ const parent = path.dirname(current);
120
+ if (parent === current) {
121
+ return null;
122
+ }
123
+ current = parent;
124
+ }
125
+ }
126
+ function buildTypegenMetadataContents(lastGeneratedAt, identity) {
127
+ return `${JSON.stringify({
128
+ version: 1,
129
+ last: lastGeneratedAt.toISOString(),
130
+ baseUrl: identity.baseUrl,
131
+ orgSlug: identity.orgSlug,
132
+ projectSlug: identity.projectSlug,
133
+ stageSlug: identity.stageSlug,
134
+ typegenMode: identity.typegenMode,
135
+ runtimeMode: identity.runtimeMode,
136
+ localEnvRoot: identity.localEnvRoot,
137
+ }, null, 2)}\n`;
138
+ }
139
+ /**
140
+ * Renders the generated SDK declaration files for one manifest.
141
+ *
142
+ * @param manifest The decoded typegen manifest.
143
+ * @param mode The requested typegen mode.
144
+ * @returns The rendered server and public declaration contents.
145
+ * @remarks The CLI owns this rendering so it no longer depends on SDK runtime helpers.
146
+ * @lastModified 2026-03-19
147
+ * @author GPT-5.4
148
+ */
149
+ export function renderGeneratedTypesForManifest(manifest, mode) {
150
+ const decodedManifest = Schema.decodeUnknownSync(TypegenManifestSchema)(manifest);
151
+ return {
152
+ serverContents: buildGeneratedTypesContents(decodedManifest, {
153
+ mode,
154
+ typeModulePath: "./dist/types.js",
155
+ declaredModulePath: "./dist/types.js",
156
+ interfaceName: "BarekeyGeneratedTypeMap",
157
+ include: () => true,
158
+ }),
159
+ publicContents: buildGeneratedTypesContents(decodedManifest, {
160
+ mode,
161
+ typeModulePath: "./dist/public-types.js",
162
+ declaredModulePath: "./dist/public-types.js",
163
+ interfaceName: "BarekeyPublicGeneratedTypeMap",
164
+ include: (row) => row.visibility === "public",
165
+ }),
166
+ };
167
+ }
168
+ /**
169
+ * Writes generated SDK declaration files into the installed `@barekey/sdk` package.
170
+ *
171
+ * @param manifest The decoded typegen manifest.
172
+ * @param identity The manifest identity used for metadata freshness.
173
+ * @returns The typegen write result.
174
+ * @remarks This preserves the existing generated file locations without importing the SDK package at runtime.
175
+ * @lastModified 2026-03-19
176
+ * @author GPT-5.4
177
+ */
178
+ export async function writeInstalledSdkGeneratedTypes(manifest, identity) {
179
+ const target = await resolveInstalledSdkTypegenTarget();
180
+ if (target === null) {
181
+ return {
182
+ written: false,
183
+ path: "",
184
+ serverPath: "",
185
+ publicPath: "",
186
+ manifestVersion: manifest.manifestVersion,
187
+ };
188
+ }
189
+ const [existingServerContents, existingPublicContents] = await Promise.all([
190
+ readTextFile(target.serverGeneratedTypesPath),
191
+ readTextFile(target.publicGeneratedTypesPath),
192
+ ]);
193
+ const rendered = renderGeneratedTypesForManifest(manifest, identity.typegenMode);
194
+ if (existingServerContents === rendered.serverContents &&
195
+ existingPublicContents === rendered.publicContents &&
196
+ readManifestVersion(existingServerContents) === manifest.manifestVersion &&
197
+ readManifestVersion(existingPublicContents) === manifest.manifestVersion) {
198
+ await writeTextFileAtomic(target.typegenMetadataPath, buildTypegenMetadataContents(new Date(), identity));
199
+ return {
200
+ written: false,
201
+ path: target.serverGeneratedTypesPath,
202
+ serverPath: target.serverGeneratedTypesPath,
203
+ publicPath: target.publicGeneratedTypesPath,
204
+ manifestVersion: manifest.manifestVersion,
205
+ };
206
+ }
207
+ await Promise.all([
208
+ writeTextFileAtomic(target.serverGeneratedTypesPath, rendered.serverContents),
209
+ writeTextFileAtomic(target.publicGeneratedTypesPath, rendered.publicContents),
210
+ ]);
211
+ await writeTextFileAtomic(target.typegenMetadataPath, buildTypegenMetadataContents(new Date(), identity));
212
+ return {
213
+ written: true,
214
+ path: target.serverGeneratedTypesPath,
215
+ serverPath: target.serverGeneratedTypesPath,
216
+ publicPath: target.publicGeneratedTypesPath,
217
+ manifestVersion: manifest.manifestVersion,
218
+ };
219
+ }
package/dist/types.d.ts CHANGED
@@ -4,10 +4,12 @@ export type CliCredentials = {
4
4
  accessTokenExpiresAtMs: number;
5
5
  refreshTokenExpiresAtMs: number;
6
6
  clerkUserId: string;
7
- orgId: string;
8
- orgSlug: string;
7
+ orgId: string | null;
8
+ orgSlug: string | null;
9
+ lastOrgId?: string | null;
10
+ lastOrgSlug?: string | null;
9
11
  };
10
12
  export type CliConfig = {
11
13
  baseUrl: string;
12
- activeAccountId: string;
14
+ activeSessionId: string;
13
15
  };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@barekey/cli",
3
- "version": "0.4.0",
3
+ "version": "0.5.0",
4
4
  "description": "Barekey command line interface",
5
5
  "type": "module",
6
6
  "bin": {
@@ -15,8 +15,8 @@
15
15
  "packageManager": "bun@1.2.22",
16
16
  "dependencies": {
17
17
  "@clack/prompts": "^0.11.0",
18
- "@barekey/sdk": "^0.5.0",
19
18
  "commander": "^14.0.1",
19
+ "effect": "3.19.19",
20
20
  "open": "^10.2.0",
21
21
  "picocolors": "^1.1.1"
22
22
  },
@@ -1,6 +1,7 @@
1
1
  import { postJson } from "./http.js";
2
2
  import type { CliCredentials } from "./types.js";
3
3
  import { loadConfig, loadCredentials, saveCredentials } from "./credentials-store.js";
4
+ import { buildSessionId } from "./context/session-id.js";
4
5
 
5
6
  type CliAuthProvider = {
6
7
  getAccessToken(): Promise<string>;
@@ -13,7 +14,7 @@ export function createCliAuthProvider(): CliAuthProvider {
13
14
 
14
15
  async function readCurrentCredentials(): Promise<{
15
16
  baseUrl: string;
16
- accountId: string;
17
+ sessionId: string;
17
18
  credentials: CliCredentials;
18
19
  }> {
19
20
  const config = await loadConfig();
@@ -21,7 +22,7 @@ export function createCliAuthProvider(): CliAuthProvider {
21
22
  throw new Error("Not logged in. Run barekey login first.");
22
23
  }
23
24
 
24
- const credentials = await loadCredentials(config.activeAccountId);
25
+ const credentials = await loadCredentials(config.activeSessionId);
25
26
  if (credentials === null) {
26
27
  throw new Error("CLI credentials are missing. Run barekey login again.");
27
28
  }
@@ -30,13 +31,13 @@ export function createCliAuthProvider(): CliAuthProvider {
30
31
 
31
32
  return {
32
33
  baseUrl: config.baseUrl,
33
- accountId: config.activeAccountId,
34
+ sessionId: config.activeSessionId,
34
35
  credentials,
35
36
  };
36
37
  }
37
38
 
38
39
  async function refreshIfNeeded(): Promise<CliCredentials> {
39
- const { baseUrl, accountId, credentials } = await readCurrentCredentials();
40
+ const { baseUrl, credentials } = await readCurrentCredentials();
40
41
  const now = Date.now();
41
42
  if (!forceRefresh && credentials.accessTokenExpiresAtMs > now + 10_000) {
42
43
  return credentials;
@@ -66,9 +67,11 @@ export function createCliAuthProvider(): CliAuthProvider {
66
67
  clerkUserId: refreshed.clerkUserId,
67
68
  orgId: refreshed.orgId,
68
69
  orgSlug: refreshed.orgSlug,
70
+ lastOrgId: refreshed.orgId,
71
+ lastOrgSlug: refreshed.orgSlug,
69
72
  };
70
73
 
71
- await saveCredentials(accountId, nextCredentials);
74
+ await saveCredentials(buildSessionId(baseUrl, refreshed.clerkUserId), nextCredentials);
72
75
  cachedCredentials = nextCredentials;
73
76
  forceRefresh = false;
74
77
  return nextCredentials;
@@ -49,14 +49,14 @@ export async function requireLocalSession(): Promise<LocalSession> {
49
49
  throw new Error("Not logged in. Run barekey auth login first.");
50
50
  }
51
51
 
52
- const credentials = await loadCredentials(config.activeAccountId);
52
+ const credentials = await loadCredentials(config.activeSessionId);
53
53
  if (credentials === null) {
54
54
  throw new Error("Saved credentials not found. Run barekey auth login again.");
55
55
  }
56
56
 
57
57
  return {
58
58
  baseUrl: config.baseUrl,
59
- accountId: config.activeAccountId,
59
+ accountId: config.activeSessionId,
60
60
  credentials,
61
61
  };
62
62
  }
@@ -74,14 +74,14 @@ export async function resolveTarget(
74
74
 
75
75
  const projectSlug = options.project?.trim() || runtime?.config.project || "";
76
76
  const stageSlug = options.stage?.trim() || runtime?.config.environment || "";
77
- const orgSlug = options.org?.trim() || runtime?.config.org || local?.credentials.orgSlug || "";
77
+ const orgSlug = options.org?.trim() || runtime?.config.org || "";
78
78
 
79
- if (!isStandalone && (projectSlug.length === 0 || stageSlug.length === 0)) {
79
+ if (!isStandalone && (orgSlug.length === 0 || projectSlug.length === 0 || stageSlug.length === 0)) {
80
80
  const hint = runtime
81
- ? `Found ${runtime.path} but project/environment is incomplete.`
81
+ ? `Found ${runtime.path} but organization/project/environment is incomplete.`
82
82
  : "No barekey.json found in current directory tree.";
83
83
  throw new Error(
84
- `${hint} Pass --project/--stage, or create barekey.json with {"organization":"...","project":"...","environment":"..."}.`,
84
+ `${hint} Run barekey init or pass --org/--project/--stage.`,
85
85
  );
86
86
  }
87
87
 
@@ -0,0 +1,63 @@
1
+ import { Command } from "commander";
2
+
3
+ import { createCliAuthProvider } from "../auth-provider.js";
4
+ import { requireLocalSession, toJsonOutput, type EnvTargetOptions } from "../command-utils.js";
5
+ import { AuditListResponseSchema } from "../contracts/index.js";
6
+ import { postJson } from "../http.js";
7
+ import { promptForOrganizationSlug } from "./target-prompts.js";
8
+
9
+ async function runAuditList(
10
+ options: EnvTargetOptions & {
11
+ limit?: string;
12
+ json?: boolean;
13
+ },
14
+ ) {
15
+ const local = await requireLocalSession();
16
+ const authProvider = createCliAuthProvider();
17
+ const accessToken = await authProvider.getAccessToken();
18
+ const orgSlug = await promptForOrganizationSlug(options.org);
19
+ const limit = options.limit ? Number.parseInt(options.limit, 10) : 25;
20
+
21
+ const response = await postJson({
22
+ baseUrl: local.baseUrl,
23
+ path: "/v1/cli/audit/list",
24
+ accessToken,
25
+ payload: {
26
+ orgSlug,
27
+ projectSlug: options.project?.trim() || null,
28
+ limit: Number.isFinite(limit) ? limit : 25,
29
+ },
30
+ schema: AuditListResponseSchema,
31
+ });
32
+
33
+ if (options.json) {
34
+ toJsonOutput(true, response);
35
+ return;
36
+ }
37
+
38
+ if (response.items.length === 0) {
39
+ console.log("No audit events found.");
40
+ return;
41
+ }
42
+
43
+ for (const item of response.items) {
44
+ console.log(`${new Date(item.occurredAtMs).toISOString()} ${item.category} ${item.title}`);
45
+ }
46
+ }
47
+
48
+ export function registerAuditCommands(program: Command): void {
49
+ const audit = program.command("audit").description("Audit history");
50
+
51
+ audit
52
+ .command("list")
53
+ .description("List recent audit events")
54
+ .option("--org <slug>", "Organization slug")
55
+ .option("--project <slug>", "Project slug")
56
+ .option("--limit <count>", "Maximum number of events", "25")
57
+ .option("--json", "Machine-readable output", false)
58
+ .action(
59
+ async (options: EnvTargetOptions & { limit?: string; json?: boolean }) => {
60
+ await runAuditList(options);
61
+ },
62
+ );
63
+ }
@@ -7,12 +7,19 @@ import pc from "picocolors";
7
7
  import open from "open";
8
8
 
9
9
  import { createCliAuthProvider } from "../auth-provider.js";
10
+ import { buildSessionId } from "../context/session-id.js";
10
11
  import {
11
12
  clearConfig,
12
13
  deleteCredentials,
13
14
  saveConfig,
14
15
  saveCredentials,
15
16
  } from "../credentials-store.js";
17
+ import {
18
+ CliSessionResponseSchema,
19
+ DevicePollResponseSchema,
20
+ DeviceStartResponseSchema,
21
+ RefreshResponseSchema,
22
+ } from "../contracts/index.js";
16
23
  import { getJson, postJson } from "../http.js";
17
24
  import { requireLocalSession, resolveBaseUrl, toJsonOutput } from "../command-utils.js";
18
25
 
@@ -60,18 +67,13 @@ async function runLogin(options: { baseUrl?: string }): Promise<void> {
60
67
  let pollActive = false;
61
68
 
62
69
  try {
63
- const started = await postJson<{
64
- deviceCode: string;
65
- userCode: string;
66
- verificationUri: string;
67
- intervalSec: number;
68
- expiresInSec: number;
69
- }>({
70
+ const started = await postJson({
70
71
  baseUrl,
71
72
  path: "/v1/cli/device/start",
72
73
  payload: {
73
74
  clientName: resolveClientName(),
74
75
  },
76
+ schema: DeviceStartResponseSchema,
75
77
  });
76
78
  const verificationUri = resolveVerificationUri(baseUrl, started.verificationUri);
77
79
 
@@ -93,27 +95,13 @@ async function runLogin(options: { baseUrl?: string }): Promise<void> {
93
95
  pollActive = true;
94
96
 
95
97
  while (Date.now() < expiresAtMs) {
96
- const poll = await postJson<
97
- | {
98
- status: "pending";
99
- intervalSec: number;
100
- }
101
- | {
102
- status: "approved";
103
- accessToken: string;
104
- refreshToken: string;
105
- accessTokenExpiresAtMs: number;
106
- refreshTokenExpiresAtMs: number;
107
- orgId: string;
108
- orgSlug: string;
109
- clerkUserId: string;
110
- }
111
- >({
98
+ const poll = await postJson({
112
99
  baseUrl,
113
100
  path: "/v1/cli/device/poll",
114
101
  payload: {
115
102
  deviceCode: started.deviceCode,
116
103
  },
104
+ schema: DevicePollResponseSchema,
117
105
  });
118
106
 
119
107
  if (poll.status === "pending") {
@@ -121,24 +109,36 @@ async function runLogin(options: { baseUrl?: string }): Promise<void> {
121
109
  continue;
122
110
  }
123
111
 
124
- const accountId = `${poll.orgSlug}:${poll.clerkUserId}`;
125
- await saveCredentials(accountId, {
112
+ const refreshed = RefreshResponseSchema.make({
126
113
  accessToken: poll.accessToken,
127
114
  refreshToken: poll.refreshToken,
128
115
  accessTokenExpiresAtMs: poll.accessTokenExpiresAtMs,
129
116
  refreshTokenExpiresAtMs: poll.refreshTokenExpiresAtMs,
130
- clerkUserId: poll.clerkUserId,
131
117
  orgId: poll.orgId,
132
118
  orgSlug: poll.orgSlug,
119
+ clerkUserId: poll.clerkUserId,
120
+ });
121
+ const sessionId = buildSessionId(baseUrl, refreshed.clerkUserId);
122
+
123
+ await saveCredentials(sessionId, {
124
+ accessToken: refreshed.accessToken,
125
+ refreshToken: refreshed.refreshToken,
126
+ accessTokenExpiresAtMs: refreshed.accessTokenExpiresAtMs,
127
+ refreshTokenExpiresAtMs: refreshed.refreshTokenExpiresAtMs,
128
+ clerkUserId: refreshed.clerkUserId,
129
+ orgId: refreshed.orgId,
130
+ orgSlug: refreshed.orgSlug,
131
+ lastOrgId: refreshed.orgId,
132
+ lastOrgSlug: refreshed.orgSlug,
133
133
  });
134
134
  await saveConfig({
135
135
  baseUrl,
136
- activeAccountId: accountId,
136
+ activeSessionId: sessionId,
137
137
  });
138
138
 
139
139
  pollSpinner.stop("Login approved");
140
140
  pollActive = false;
141
- outro(`Logged in as ${pc.bold(poll.clerkUserId)} in ${pc.bold(poll.orgSlug)}.`);
141
+ outro(`Logged in as ${pc.bold(refreshed.clerkUserId)}.`);
142
142
  return;
143
143
  }
144
144
 
@@ -158,7 +158,7 @@ async function runLogin(options: { baseUrl?: string }): Promise<void> {
158
158
 
159
159
  async function runLogout(): Promise<void> {
160
160
  const local = await requireLocalSession();
161
- await postJson<{ revoked: boolean }>({
161
+ await postJson({
162
162
  baseUrl: local.baseUrl,
163
163
  path: "/v1/cli/logout",
164
164
  payload: {
@@ -174,15 +174,11 @@ async function runWhoami(options: { json?: boolean }): Promise<void> {
174
174
  const local = await requireLocalSession();
175
175
  const authProvider = createCliAuthProvider();
176
176
  const accessToken = await authProvider.getAccessToken();
177
- const session = await getJson<{
178
- clerkUserId: string;
179
- orgId: string;
180
- orgSlug: string;
181
- source: "clerk" | "cli";
182
- }>({
177
+ const session = await getJson({
183
178
  baseUrl: local.baseUrl,
184
179
  path: "/v1/cli/session",
185
180
  accessToken,
181
+ schema: CliSessionResponseSchema,
186
182
  });
187
183
 
188
184
  if (options.json) {
@@ -191,7 +187,7 @@ async function runWhoami(options: { json?: boolean }): Promise<void> {
191
187
  }
192
188
 
193
189
  console.log(`${pc.bold("User")}: ${session.clerkUserId}`);
194
- console.log(`${pc.bold("Org")}: ${session.orgSlug}`);
190
+ console.log(`${pc.bold("Current org")}: ${session.orgSlug}`);
195
191
  console.log(`${pc.bold("Source")}: ${session.source}`);
196
192
  }
197
193
 
@@ -221,7 +217,6 @@ export function registerAuthCommands(program: Command): void {
221
217
  await runWhoami(options);
222
218
  });
223
219
 
224
- // Backward-compatible top-level auth aliases.
225
220
  program
226
221
  .command("login")
227
222
  .description("Alias for barekey auth login")
@@ -0,0 +1,73 @@
1
+ import { Command } from "commander";
2
+
3
+ import { createCliAuthProvider } from "../auth-provider.js";
4
+ import { requireLocalSession, toJsonOutput, type EnvTargetOptions } from "../command-utils.js";
5
+ import { BillingCatalogResponseSchema, BillingStatusResponseSchema } from "../contracts/index.js";
6
+ import { getJson, postJson } from "../http.js";
7
+ import { promptForOrganizationSlug } from "./target-prompts.js";
8
+
9
+ async function runBillingCatalog(options: { json?: boolean }) {
10
+ const local = await requireLocalSession();
11
+ const authProvider = createCliAuthProvider();
12
+ const accessToken = await authProvider.getAccessToken();
13
+ const response = await getJson({
14
+ baseUrl: local.baseUrl,
15
+ path: "/v1/cli/billing/catalog",
16
+ accessToken,
17
+ schema: BillingCatalogResponseSchema,
18
+ });
19
+
20
+ if (options.json) {
21
+ toJsonOutput(true, response);
22
+ return;
23
+ }
24
+
25
+ console.log(`Plans: ${response.variants.length}`);
26
+ console.log(`Metered features: ${Object.values(response.featureIds).join(", ")}`);
27
+ }
28
+
29
+ async function runBillingStatus(options: EnvTargetOptions & { json?: boolean }) {
30
+ const local = await requireLocalSession();
31
+ const authProvider = createCliAuthProvider();
32
+ const accessToken = await authProvider.getAccessToken();
33
+ const orgSlug = await promptForOrganizationSlug(options.org);
34
+ const response = await postJson({
35
+ baseUrl: local.baseUrl,
36
+ path: "/v1/cli/billing/status",
37
+ accessToken,
38
+ payload: {
39
+ orgSlug,
40
+ },
41
+ schema: BillingStatusResponseSchema,
42
+ });
43
+
44
+ if (options.json) {
45
+ toJsonOutput(true, response);
46
+ return;
47
+ }
48
+
49
+ console.log(`Tier: ${response.currentTier ?? "none"}`);
50
+ console.log(`Product: ${response.currentProductId ?? "none"}`);
51
+ console.log(`Can manage billing: ${response.canManageBilling ? "yes" : "no"}`);
52
+ }
53
+
54
+ export function registerBillingCommands(program: Command): void {
55
+ const billing = program.command("billing").description("Billing information");
56
+
57
+ billing
58
+ .command("catalog")
59
+ .description("Show the public billing catalog")
60
+ .option("--json", "Machine-readable output", false)
61
+ .action(async (options: { json?: boolean }) => {
62
+ await runBillingCatalog(options);
63
+ });
64
+
65
+ billing
66
+ .command("status")
67
+ .description("Show billing status for an organization")
68
+ .option("--org <slug>", "Organization slug")
69
+ .option("--json", "Machine-readable output", false)
70
+ .action(async (options: EnvTargetOptions & { json?: boolean }) => {
71
+ await runBillingStatus(options);
72
+ });
73
+ }