@barekey/cli 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/LICENSE ADDED
@@ -0,0 +1,28 @@
1
+ BSD 3-Clause License
2
+
3
+ Copyright (c) 2026, Barekey Inc.
4
+
5
+ Redistribution and use in source and binary forms, with or without
6
+ modification, are permitted provided that the following conditions are met:
7
+
8
+ 1. Redistributions of source code must retain the above copyright notice, this
9
+ list of conditions and the following disclaimer.
10
+
11
+ 2. Redistributions in binary form must reproduce the above copyright notice,
12
+ this list of conditions and the following disclaimer in the documentation
13
+ and/or other materials provided with the distribution.
14
+
15
+ 3. Neither the name of the copyright holder nor the names of its
16
+ contributors may be used to endorse or promote products derived from
17
+ this software without specific prior written permission.
18
+
19
+ THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
20
+ AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
21
+ IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
22
+ DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
23
+ FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
24
+ DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
25
+ SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
26
+ CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
27
+ OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
28
+ OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
package/README.md ADDED
@@ -0,0 +1,36 @@
1
+ # @barekey/cli
2
+
3
+ CLI for logging into Barekey, managing environment variables, and pulling resolved values into local workflows.
4
+
5
+ ## Install
6
+
7
+ ```bash
8
+ npm install -g @barekey/cli
9
+ ```
10
+
11
+ ## Quickstart
12
+
13
+ ```bash
14
+ barekey auth login
15
+ barekey env list --org acme --project api --stage development
16
+ barekey env get DATABASE_URL --org acme --project api --stage development
17
+ barekey env pull --org acme --project api --stage development --out .env
18
+ ```
19
+
20
+ ## Common commands
21
+
22
+ ```bash
23
+ barekey auth whoami
24
+ barekey env new FEATURE_FLAG true --type boolean --org acme --project api --stage development
25
+ barekey env set CHECKOUT_COPY original --ab redesign --chance 0.25 --org acme --project api --stage development
26
+ barekey env delete FEATURE_FLAG --yes --org acme --project api --stage development
27
+ barekey env get-many --names DATABASE_URL,FEATURE_FLAG --org acme --project api --stage development
28
+ ```
29
+
30
+ ## Development
31
+
32
+ ```bash
33
+ npm install
34
+ npm run build
35
+ npm run typecheck
36
+ ```
@@ -0,0 +1,6 @@
1
+ type CliAuthProvider = {
2
+ getAccessToken(): Promise<string>;
3
+ onAuthError?(): Promise<void>;
4
+ };
5
+ export declare function createCliAuthProvider(): CliAuthProvider;
6
+ export {};
@@ -0,0 +1,58 @@
1
+ import { postJson } from "./http.js";
2
+ import { loadConfig, loadCredentials, saveCredentials } from "./credentials-store.js";
3
+ export function createCliAuthProvider() {
4
+ let cachedCredentials = null;
5
+ let forceRefresh = false;
6
+ async function readCurrentCredentials() {
7
+ const config = await loadConfig();
8
+ if (config === null) {
9
+ throw new Error("Not logged in. Run barekey login first.");
10
+ }
11
+ const credentials = await loadCredentials(config.activeAccountId);
12
+ if (credentials === null) {
13
+ throw new Error("CLI credentials are missing. Run barekey login again.");
14
+ }
15
+ cachedCredentials = credentials;
16
+ return {
17
+ baseUrl: config.baseUrl,
18
+ accountId: config.activeAccountId,
19
+ credentials,
20
+ };
21
+ }
22
+ async function refreshIfNeeded() {
23
+ const { baseUrl, accountId, credentials } = await readCurrentCredentials();
24
+ const now = Date.now();
25
+ if (!forceRefresh && credentials.accessTokenExpiresAtMs > now + 10_000) {
26
+ return credentials;
27
+ }
28
+ const refreshed = await postJson({
29
+ baseUrl,
30
+ path: "/v1/cli/token/refresh",
31
+ payload: {
32
+ refreshToken: credentials.refreshToken,
33
+ },
34
+ });
35
+ const nextCredentials = {
36
+ accessToken: refreshed.accessToken,
37
+ refreshToken: refreshed.refreshToken,
38
+ accessTokenExpiresAtMs: refreshed.accessTokenExpiresAtMs,
39
+ refreshTokenExpiresAtMs: refreshed.refreshTokenExpiresAtMs,
40
+ clerkUserId: refreshed.clerkUserId,
41
+ orgId: refreshed.orgId,
42
+ orgSlug: refreshed.orgSlug,
43
+ };
44
+ await saveCredentials(accountId, nextCredentials);
45
+ cachedCredentials = nextCredentials;
46
+ forceRefresh = false;
47
+ return nextCredentials;
48
+ }
49
+ return {
50
+ async getAccessToken() {
51
+ const credentials = await refreshIfNeeded();
52
+ return credentials.accessToken;
53
+ },
54
+ async onAuthError() {
55
+ forceRefresh = true;
56
+ },
57
+ };
58
+ }
@@ -0,0 +1,23 @@
1
+ import { Command } from "commander";
2
+ import type { CliCredentials } from "./types.js";
3
+ export type LocalSession = {
4
+ baseUrl: string;
5
+ accountId: string;
6
+ credentials: CliCredentials;
7
+ };
8
+ export type EnvTargetOptions = {
9
+ project?: string;
10
+ stage?: string;
11
+ org?: string;
12
+ };
13
+ export declare function toJsonOutput(enabled: boolean, value: unknown): void;
14
+ export declare function resolveBaseUrl(explicit: string | undefined): Promise<string>;
15
+ export declare function requireLocalSession(): Promise<LocalSession>;
16
+ export declare function resolveTarget(options: EnvTargetOptions, local: LocalSession): Promise<{
17
+ projectSlug: string;
18
+ stageSlug: string;
19
+ orgSlug?: string;
20
+ }>;
21
+ export declare function parseChance(value: string | undefined): number;
22
+ export declare function dotenvEscape(value: string): string;
23
+ export declare function addTargetOptions(command: Command): Command;
@@ -0,0 +1,78 @@
1
+ import { loadConfig, loadCredentials } from "./credentials-store.js";
2
+ import { DEFAULT_BAREKEY_API_URL } from "./constants.js";
3
+ import { loadRuntimeConfig } from "./runtime-config.js";
4
+ export function toJsonOutput(enabled, value) {
5
+ if (!enabled) {
6
+ return;
7
+ }
8
+ console.log(JSON.stringify(value, null, 2));
9
+ }
10
+ export async function resolveBaseUrl(explicit) {
11
+ const explicitUrl = explicit?.trim();
12
+ if (explicitUrl && explicitUrl.length > 0) {
13
+ return explicitUrl.replace(/\/$/, "");
14
+ }
15
+ const envUrl = process.env.BAREKEY_API_URL?.trim();
16
+ if (envUrl && envUrl.length > 0) {
17
+ return envUrl.replace(/\/$/, "");
18
+ }
19
+ const config = await loadConfig();
20
+ if (config && config.baseUrl.length > 0) {
21
+ return config.baseUrl.replace(/\/$/, "");
22
+ }
23
+ return DEFAULT_BAREKEY_API_URL;
24
+ }
25
+ export async function requireLocalSession() {
26
+ const config = await loadConfig();
27
+ if (config === null) {
28
+ throw new Error("Not logged in. Run barekey auth login first.");
29
+ }
30
+ const credentials = await loadCredentials(config.activeAccountId);
31
+ if (credentials === null) {
32
+ throw new Error("Saved credentials not found. Run barekey auth login again.");
33
+ }
34
+ return {
35
+ baseUrl: config.baseUrl,
36
+ accountId: config.activeAccountId,
37
+ credentials,
38
+ };
39
+ }
40
+ export async function resolveTarget(options, local) {
41
+ const runtime = await loadRuntimeConfig();
42
+ const projectSlug = options.project?.trim() || runtime?.config.project || "";
43
+ const stageSlug = options.stage?.trim() || runtime?.config.environment || "";
44
+ const orgSlug = options.org?.trim() || runtime?.config.org || local.credentials.orgSlug;
45
+ if (projectSlug.length === 0 || stageSlug.length === 0) {
46
+ const hint = runtime
47
+ ? `Found ${runtime.path} but project/environment is incomplete.`
48
+ : "No barekey.json found in current directory tree.";
49
+ throw new Error(`${hint} Pass --project/--stage, or create barekey.json with {"organization":"...","project":"...","environment":"..."}.`);
50
+ }
51
+ return {
52
+ projectSlug,
53
+ stageSlug,
54
+ orgSlug: orgSlug.length > 0 ? orgSlug : undefined,
55
+ };
56
+ }
57
+ export function parseChance(value) {
58
+ if (value === undefined) {
59
+ throw new Error("--chance is required when using --ab.");
60
+ }
61
+ const parsed = Number(value);
62
+ if (!Number.isFinite(parsed) || parsed < 0 || parsed > 1) {
63
+ throw new Error("--chance must be a number between 0 and 1.");
64
+ }
65
+ return parsed;
66
+ }
67
+ export function dotenvEscape(value) {
68
+ if (/^[A-Za-z0-9_./:-]+$/.test(value)) {
69
+ return value;
70
+ }
71
+ return JSON.stringify(value);
72
+ }
73
+ export function addTargetOptions(command) {
74
+ return command
75
+ .option("--project <slug>", "Project slug")
76
+ .option("--stage <slug>", "Stage slug")
77
+ .option("--org <slug>", "Organization slug");
78
+ }
@@ -0,0 +1,2 @@
1
+ import { Command } from "commander";
2
+ export declare function registerAuthCommands(program: Command): void;
@@ -0,0 +1,186 @@
1
+ import os from "node:os";
2
+ import { setTimeout as wait } from "node:timers/promises";
3
+ import { intro, outro, spinner } from "@clack/prompts";
4
+ import pc from "picocolors";
5
+ import open from "open";
6
+ import { createCliAuthProvider } from "../auth-provider.js";
7
+ import { clearConfig, deleteCredentials, saveConfig, saveCredentials, } from "../credentials-store.js";
8
+ import { getJson, postJson } from "../http.js";
9
+ import { requireLocalSession, resolveBaseUrl, toJsonOutput } from "../command-utils.js";
10
+ function resolveClientName() {
11
+ const configured = process.env.BAREKEY_CLIENT_NAME?.trim() ||
12
+ process.env.COMPUTERNAME?.trim() ||
13
+ process.env.HOSTNAME?.trim() ||
14
+ os.hostname().trim();
15
+ if (configured.length === 0) {
16
+ return undefined;
17
+ }
18
+ return configured.slice(0, 120);
19
+ }
20
+ function resolveVerificationUri(baseUrl, verificationUri) {
21
+ try {
22
+ const resolvedBaseUrl = new URL(baseUrl);
23
+ const resolvedVerificationUrl = new URL(verificationUri);
24
+ const isConvexHost = resolvedVerificationUrl.host.endsWith(".convex.site") ||
25
+ resolvedVerificationUrl.host.endsWith(".convex.cloud");
26
+ const usesPublicApiHost = resolvedBaseUrl.host.startsWith("api.");
27
+ if (isConvexHost && usesPublicApiHost) {
28
+ return new URL(`${resolvedVerificationUrl.pathname}${resolvedVerificationUrl.search}`, `${resolvedBaseUrl.protocol}//${resolvedBaseUrl.host.replace(/^api\./, "")}`).toString();
29
+ }
30
+ }
31
+ catch {
32
+ return verificationUri;
33
+ }
34
+ return verificationUri;
35
+ }
36
+ async function runLogin(options) {
37
+ const baseUrl = await resolveBaseUrl(options.baseUrl);
38
+ intro("Barekey CLI login");
39
+ const loading = spinner();
40
+ loading.start("Starting device authorization");
41
+ let loadingActive = true;
42
+ let pollSpinner = null;
43
+ let pollActive = false;
44
+ try {
45
+ const started = await postJson({
46
+ baseUrl,
47
+ path: "/v1/cli/device/start",
48
+ payload: {
49
+ clientName: resolveClientName(),
50
+ },
51
+ });
52
+ const verificationUri = resolveVerificationUri(baseUrl, started.verificationUri);
53
+ loading.stop("Authorization initialized");
54
+ loadingActive = false;
55
+ console.log(`${pc.bold("Open")}: ${verificationUri}`);
56
+ console.log(`${pc.bold("Code")}: ${started.userCode}`);
57
+ try {
58
+ await open(verificationUri);
59
+ }
60
+ catch {
61
+ // Browser-open can fail in headless environments.
62
+ }
63
+ const startedAtMs = Date.now();
64
+ const expiresAtMs = startedAtMs + started.expiresInSec * 1000;
65
+ pollSpinner = spinner();
66
+ pollSpinner.start("Waiting for approval in browser");
67
+ pollActive = true;
68
+ while (Date.now() < expiresAtMs) {
69
+ const poll = await postJson({
70
+ baseUrl,
71
+ path: "/v1/cli/device/poll",
72
+ payload: {
73
+ deviceCode: started.deviceCode,
74
+ },
75
+ });
76
+ if (poll.status === "pending") {
77
+ await wait(Math.max(1, poll.intervalSec) * 1000);
78
+ continue;
79
+ }
80
+ const accountId = `${poll.orgSlug}:${poll.clerkUserId}`;
81
+ await saveCredentials(accountId, {
82
+ accessToken: poll.accessToken,
83
+ refreshToken: poll.refreshToken,
84
+ accessTokenExpiresAtMs: poll.accessTokenExpiresAtMs,
85
+ refreshTokenExpiresAtMs: poll.refreshTokenExpiresAtMs,
86
+ clerkUserId: poll.clerkUserId,
87
+ orgId: poll.orgId,
88
+ orgSlug: poll.orgSlug,
89
+ });
90
+ await saveConfig({
91
+ baseUrl,
92
+ activeAccountId: accountId,
93
+ });
94
+ pollSpinner.stop("Login approved");
95
+ pollActive = false;
96
+ outro(`Logged in as ${pc.bold(poll.clerkUserId)} in ${pc.bold(poll.orgSlug)}.`);
97
+ return;
98
+ }
99
+ pollSpinner.stop("Timed out");
100
+ pollActive = false;
101
+ throw new Error("Login timed out before device approval completed.");
102
+ }
103
+ catch (error) {
104
+ if (loadingActive) {
105
+ loading.stop("Authorization failed");
106
+ }
107
+ if (pollSpinner && pollActive) {
108
+ pollSpinner.stop("Login failed");
109
+ }
110
+ throw error;
111
+ }
112
+ }
113
+ async function runLogout() {
114
+ const local = await requireLocalSession();
115
+ await postJson({
116
+ baseUrl: local.baseUrl,
117
+ path: "/v1/cli/logout",
118
+ payload: {
119
+ refreshToken: local.credentials.refreshToken,
120
+ },
121
+ });
122
+ await deleteCredentials(local.accountId);
123
+ await clearConfig();
124
+ console.log("Logged out.");
125
+ }
126
+ async function runWhoami(options) {
127
+ const local = await requireLocalSession();
128
+ const authProvider = createCliAuthProvider();
129
+ const accessToken = await authProvider.getAccessToken();
130
+ const session = await getJson({
131
+ baseUrl: local.baseUrl,
132
+ path: "/v1/cli/session",
133
+ accessToken,
134
+ });
135
+ if (options.json) {
136
+ toJsonOutput(true, session);
137
+ return;
138
+ }
139
+ console.log(`${pc.bold("User")}: ${session.clerkUserId}`);
140
+ console.log(`${pc.bold("Org")}: ${session.orgSlug}`);
141
+ console.log(`${pc.bold("Source")}: ${session.source}`);
142
+ }
143
+ export function registerAuthCommands(program) {
144
+ const auth = program.command("auth").description("Authentication commands");
145
+ auth
146
+ .command("login")
147
+ .description("Authenticate this machine using browser device flow")
148
+ .option("--base-url <url>", "Barekey API base URL")
149
+ .action(async (options) => {
150
+ await runLogin(options);
151
+ });
152
+ auth
153
+ .command("logout")
154
+ .description("Revoke local CLI session")
155
+ .action(async () => {
156
+ await runLogout();
157
+ });
158
+ auth
159
+ .command("whoami")
160
+ .description("Show active CLI auth context")
161
+ .option("--json", "Machine-readable output", false)
162
+ .action(async (options) => {
163
+ await runWhoami(options);
164
+ });
165
+ // Backward-compatible top-level auth aliases.
166
+ program
167
+ .command("login")
168
+ .description("Alias for barekey auth login")
169
+ .option("--base-url <url>", "Barekey API base URL")
170
+ .action(async (options) => {
171
+ await runLogin(options);
172
+ });
173
+ program
174
+ .command("logout")
175
+ .description("Alias for barekey auth logout")
176
+ .action(async () => {
177
+ await runLogout();
178
+ });
179
+ program
180
+ .command("whoami")
181
+ .description("Alias for barekey auth whoami")
182
+ .option("--json", "Machine-readable output", false)
183
+ .action(async (options) => {
184
+ await runWhoami(options);
185
+ });
186
+ }
@@ -0,0 +1,2 @@
1
+ import { Command } from "commander";
2
+ export declare function registerEnvCommands(program: Command): void;