@barekey/cli 0.1.0 → 0.2.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.
@@ -0,0 +1,59 @@
1
+ name: Publish package
2
+
3
+ on:
4
+ push:
5
+ branches:
6
+ - master
7
+ workflow_dispatch:
8
+
9
+ permissions:
10
+ contents: read
11
+ id-token: write
12
+
13
+ concurrency:
14
+ group: publish-${{ github.ref }}
15
+ cancel-in-progress: false
16
+
17
+ jobs:
18
+ publish:
19
+ runs-on: ubuntu-latest
20
+ steps:
21
+ - name: Check out repository
22
+ uses: actions/checkout@v5
23
+
24
+ - name: Set up Bun
25
+ uses: oven-sh/setup-bun@v2
26
+ with:
27
+ bun-version: 1.2.22
28
+
29
+ - name: Set up Node.js
30
+ uses: actions/setup-node@v4
31
+ with:
32
+ node-version: 24
33
+ registry-url: https://registry.npmjs.org
34
+
35
+ - name: Install dependencies
36
+ run: bun install --frozen-lockfile
37
+
38
+ - name: Build package
39
+ run: bun run build
40
+
41
+ - name: Check if version is already published
42
+ id: published
43
+ shell: bash
44
+ run: |
45
+ set -euo pipefail
46
+ package_name="$(node -p "require('./package.json').name")"
47
+ package_version="$(node -p "require('./package.json').version")"
48
+ version_exists="$(node -e "const https = require('node:https'); const url = 'https://registry.npmjs.org/' + encodeURIComponent(require('./package.json').name); https.get(url, (response) => { if (response.statusCode === 404) { console.log('false'); response.resume(); return; } if (response.statusCode !== 200) { console.error('Unexpected registry status:', response.statusCode); process.exit(1); } let data = ''; response.on('data', (chunk) => data += chunk); response.on('end', () => { const metadata = JSON.parse(data); console.log(Boolean(metadata.versions && metadata.versions[require('./package.json').version])); }); }).on('error', (error) => { console.error(error); process.exit(1); });")"
49
+ echo "package_name=${package_name}" >> "$GITHUB_OUTPUT"
50
+ echo "package_version=${package_version}" >> "$GITHUB_OUTPUT"
51
+ echo "version_exists=${version_exists}" >> "$GITHUB_OUTPUT"
52
+
53
+ - name: Publish to npm
54
+ if: steps.published.outputs.version_exists != 'true'
55
+ run: npm publish --access public --provenance
56
+
57
+ - name: Report skipped publish
58
+ if: steps.published.outputs.version_exists == 'true'
59
+ run: echo "${{ steps.published.outputs.package_name }}@${{ steps.published.outputs.package_version }} is already published; skipping."
package/AGENTS.md ADDED
@@ -0,0 +1,14 @@
1
+ # AGENTS.md — @barekey/cli
2
+
3
+ ## Package Manager
4
+
5
+ - This repo uses Bun.
6
+ - Use `bun install` for dependency changes.
7
+ - Use `bun run <script>` for project scripts.
8
+ - Use `bun test` for tests.
9
+ - Do not use `npm` or commit `package-lock.json`.
10
+
11
+ ## Releases
12
+
13
+ - If work includes merging changes to `/home/sander/barekey/sdk` on `master`, make sure the SDK `package.json` version is bumped before or with that merge whenever the SDK surface, generated types, runtime behavior, or published artifacts changed.
14
+ - Use semantic versioning for SDK version bumps: `patch` for backward-compatible fixes, `minor` for backward-compatible features, `major` for breaking changes.
package/README.md CHANGED
@@ -5,7 +5,7 @@ CLI for logging into Barekey, managing environment variables, and pulling resolv
5
5
  ## Install
6
6
 
7
7
  ```bash
8
- npm install -g @barekey/cli
8
+ bun add -g @barekey/cli
9
9
  ```
10
10
 
11
11
  ## Quickstart
@@ -30,7 +30,8 @@ barekey env get-many --names DATABASE_URL,FEATURE_FLAG --org acme --project api
30
30
  ## Development
31
31
 
32
32
  ```bash
33
- npm install
34
- npm run build
35
- npm run typecheck
33
+ bun install
34
+ bun run build
35
+ bun run typecheck
36
+ bun test
36
37
  ```
package/bun.lock ADDED
@@ -0,0 +1,59 @@
1
+ {
2
+ "lockfileVersion": 1,
3
+ "configVersion": 1,
4
+ "workspaces": {
5
+ "": {
6
+ "name": "@barekey/cli",
7
+ "dependencies": {
8
+ "@barekey/sdk": "^0.2.0",
9
+ "@clack/prompts": "^0.11.0",
10
+ "commander": "^14.0.1",
11
+ "open": "^10.2.0",
12
+ "picocolors": "^1.1.1",
13
+ },
14
+ "devDependencies": {
15
+ "@types/node": "^24.10.1",
16
+ "typescript": "^5.9.3",
17
+ },
18
+ },
19
+ },
20
+ "packages": {
21
+ "@barekey/sdk": ["@barekey/sdk@0.2.0", "", {}, "sha512-vSbxzJ6ZqR8ThBqe6DIweoQliF6nCfuDvUnuKHFFqBVagkcT26goZekD9NXfJBb/Obip9rdJvKG9zfFTV1UBgQ=="],
22
+
23
+ "@clack/core": ["@clack/core@0.5.0", "", { "dependencies": { "picocolors": "^1.0.0", "sisteransi": "^1.0.5" } }, "sha512-p3y0FIOwaYRUPRcMO7+dlmLh8PSRcrjuTndsiA0WAFbWES0mLZlrjVoBRZ9DzkPFJZG6KGkJmoEAY0ZcVWTkow=="],
24
+
25
+ "@clack/prompts": ["@clack/prompts@0.11.0", "", { "dependencies": { "@clack/core": "0.5.0", "picocolors": "^1.0.0", "sisteransi": "^1.0.5" } }, "sha512-pMN5FcrEw9hUkZA4f+zLlzivQSeQf5dRGJjSUbvVYDLvpKCdQx5OaknvKzgbtXOizhP+SJJJjqEbOe55uKKfAw=="],
26
+
27
+ "@types/node": ["@types/node@24.12.0", "", { "dependencies": { "undici-types": "~7.16.0" } }, "sha512-GYDxsZi3ChgmckRT9HPU0WEhKLP08ev/Yfcq2AstjrDASOYCSXeyjDsHg4v5t4jOj7cyDX3vmprafKlWIG9MXQ=="],
28
+
29
+ "bundle-name": ["bundle-name@4.1.0", "", { "dependencies": { "run-applescript": "^7.0.0" } }, "sha512-tjwM5exMg6BGRI+kNmTntNsvdZS1X8BFYS6tnJ2hdH0kVxM6/eVZ2xy+FqStSWvYmtfFMDLIxurorHwDKfDz5Q=="],
30
+
31
+ "commander": ["commander@14.0.3", "", {}, "sha512-H+y0Jo/T1RZ9qPP4Eh1pkcQcLRglraJaSLoyOtHxu6AapkjWVCy2Sit1QQ4x3Dng8qDlSsZEet7g5Pq06MvTgw=="],
32
+
33
+ "default-browser": ["default-browser@5.5.0", "", { "dependencies": { "bundle-name": "^4.1.0", "default-browser-id": "^5.0.0" } }, "sha512-H9LMLr5zwIbSxrmvikGuI/5KGhZ8E2zH3stkMgM5LpOWDutGM2JZaj460Udnf1a+946zc7YBgrqEWwbk7zHvGw=="],
34
+
35
+ "default-browser-id": ["default-browser-id@5.0.1", "", {}, "sha512-x1VCxdX4t+8wVfd1so/9w+vQ4vx7lKd2Qp5tDRutErwmR85OgmfX7RlLRMWafRMY7hbEiXIbudNrjOAPa/hL8Q=="],
36
+
37
+ "define-lazy-prop": ["define-lazy-prop@3.0.0", "", {}, "sha512-N+MeXYoqr3pOgn8xfyRPREN7gHakLYjhsHhWGT3fWAiL4IkAt0iDw14QiiEm2bE30c5XX5q0FtAA3CK5f9/BUg=="],
38
+
39
+ "is-docker": ["is-docker@3.0.0", "", { "bin": { "is-docker": "cli.js" } }, "sha512-eljcgEDlEns/7AXFosB5K/2nCM4P7FQPkGc/DWLy5rmFEWvZayGrik1d9/QIY5nJ4f9YsVvBkA6kJpHn9rISdQ=="],
40
+
41
+ "is-inside-container": ["is-inside-container@1.0.0", "", { "dependencies": { "is-docker": "^3.0.0" }, "bin": { "is-inside-container": "cli.js" } }, "sha512-KIYLCCJghfHZxqjYBE7rEy0OBuTd5xCHS7tHVgvCLkx7StIoaxwNW3hCALgEUjFfeRk+MG/Qxmp/vtETEF3tRA=="],
42
+
43
+ "is-wsl": ["is-wsl@3.1.1", "", { "dependencies": { "is-inside-container": "^1.0.0" } }, "sha512-e6rvdUCiQCAuumZslxRJWR/Doq4VpPR82kqclvcS0efgt430SlGIk05vdCN58+VrzgtIcfNODjozVielycD4Sw=="],
44
+
45
+ "open": ["open@10.2.0", "", { "dependencies": { "default-browser": "^5.2.1", "define-lazy-prop": "^3.0.0", "is-inside-container": "^1.0.0", "wsl-utils": "^0.1.0" } }, "sha512-YgBpdJHPyQ2UE5x+hlSXcnejzAvD0b22U2OuAP+8OnlJT+PjWPxtgmGqKKc+RgTM63U9gN0YzrYc71R2WT/hTA=="],
46
+
47
+ "picocolors": ["picocolors@1.1.1", "", {}, "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA=="],
48
+
49
+ "run-applescript": ["run-applescript@7.1.0", "", {}, "sha512-DPe5pVFaAsinSaV6QjQ6gdiedWDcRCbUuiQfQa2wmWV7+xC9bGulGI8+TdRmoFkAPaBXk8CrAbnlY2ISniJ47Q=="],
50
+
51
+ "sisteransi": ["sisteransi@1.0.5", "", {}, "sha512-bLGGlR1QxBcynn2d5YmDX4MGjlZvy2MRBDRNHLJ8VI6l6+9FUiyTFNJ0IveOSP0bcXgVDPRcfGqA0pjaqUpfVg=="],
52
+
53
+ "typescript": ["typescript@5.9.3", "", { "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" } }, "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw=="],
54
+
55
+ "undici-types": ["undici-types@7.16.0", "", {}, "sha512-Zz+aZWSj8LE6zoxD+xrjh4VfkIG8Ya6LvYkZqtUQGJPZjYl53ypCaUwWqo7eI0x66KBGeRo+mlBEkMSeSZ38Nw=="],
56
+
57
+ "wsl-utils": ["wsl-utils@0.1.0", "", { "dependencies": { "is-wsl": "^3.1.0" } }, "sha512-h3Fbisa2nKGPxCpm89Hk33lBLsnaGBvctQopaBSOW/uIs6FTe1ATyAnKFJrzVs9vpGdsTe73WF3V4lIsk4Gacw=="],
58
+ }
59
+ }
@@ -0,0 +1,10 @@
1
+ export type CliRolloutFunction = "linear" | "step" | "ease_in_out";
2
+ export type CliVisibility = "private" | "public";
3
+ export type CliRolloutMilestone = {
4
+ at: string;
5
+ percentage: number;
6
+ };
7
+ export declare function collectOptionValues(value: string, previous?: Array<string>): Array<string>;
8
+ export declare function parseRolloutFunction(value: string | undefined): CliRolloutFunction;
9
+ export declare function parseRolloutMilestones(values: Array<string> | undefined): Array<CliRolloutMilestone>;
10
+ export declare function parseVisibility(value: string | undefined): CliVisibility;
@@ -0,0 +1,61 @@
1
+ export function collectOptionValues(value, previous = []) {
2
+ return [...previous, value];
3
+ }
4
+ export function parseRolloutFunction(value) {
5
+ if (value === undefined) {
6
+ return "linear";
7
+ }
8
+ if (value === "linear" || value === "step" || value === "ease_in_out") {
9
+ return value;
10
+ }
11
+ throw new Error("--function must be one of: linear, step, ease_in_out.");
12
+ }
13
+ export function parseRolloutMilestones(values) {
14
+ if (values === undefined || values.length === 0) {
15
+ return [
16
+ {
17
+ at: new Date().toISOString(),
18
+ percentage: 0,
19
+ },
20
+ ];
21
+ }
22
+ const milestones = values.map((rawValue) => {
23
+ const separatorIndex = rawValue.lastIndexOf("=");
24
+ if (separatorIndex <= 0 || separatorIndex >= rawValue.length - 1) {
25
+ throw new Error(`Invalid --point value "${rawValue}". Use ISO_TIMESTAMP=PERCENTAGE, for example 2026-03-12T18:00:00Z=50.`);
26
+ }
27
+ const at = rawValue.slice(0, separatorIndex).trim();
28
+ const percentageValue = rawValue.slice(separatorIndex + 1).trim();
29
+ const atMs = Date.parse(at);
30
+ const percentage = Number(percentageValue);
31
+ if (!Number.isFinite(atMs)) {
32
+ throw new Error(`Invalid rollout point time "${at}". Use an ISO 8601 timestamp.`);
33
+ }
34
+ if (!Number.isFinite(percentage) || percentage < 0 || percentage > 100) {
35
+ throw new Error(`Invalid rollout point percentage "${percentageValue}". Use a number between 0 and 100.`);
36
+ }
37
+ return {
38
+ atMs,
39
+ milestone: {
40
+ at: new Date(atMs).toISOString(),
41
+ percentage,
42
+ },
43
+ };
44
+ });
45
+ milestones.sort((left, right) => left.atMs - right.atMs);
46
+ for (let index = 1; index < milestones.length; index += 1) {
47
+ if ((milestones[index]?.atMs ?? 0) <= (milestones[index - 1]?.atMs ?? 0)) {
48
+ throw new Error("Rollout points must be strictly increasing by time.");
49
+ }
50
+ }
51
+ return milestones.map((entry) => entry.milestone);
52
+ }
53
+ export function parseVisibility(value) {
54
+ if (value === undefined || value === "private") {
55
+ return "private";
56
+ }
57
+ if (value === "public") {
58
+ return "public";
59
+ }
60
+ throw new Error("--visibility must be one of: private, public.");
61
+ }
@@ -1,10 +1,11 @@
1
1
  import { writeFile } from "node:fs/promises";
2
2
  import { cancel, confirm, isCancel } from "@clack/prompts";
3
- import { BarekeyClient } from "@barekey/sdk";
3
+ import { BarekeyClient } from "@barekey/sdk/server";
4
4
  import pc from "picocolors";
5
5
  import { createCliAuthProvider } from "../auth-provider.js";
6
6
  import { addTargetOptions, dotenvEscape, parseChance, requireLocalSession, resolveTarget, toJsonOutput, } from "../command-utils.js";
7
7
  import { postJson } from "../http.js";
8
+ import { collectOptionValues, parseRolloutFunction, parseRolloutMilestones, parseVisibility, } from "./env-helpers.js";
8
9
  function createEnvClient(input) {
9
10
  const organization = input.organization?.trim();
10
11
  if (!organization) {
@@ -94,7 +95,7 @@ async function runEnvList(options) {
94
95
  const rolloutSuffix = row.kind === "rollout"
95
96
  ? ` ${pc.dim(`${row.rolloutFunction ?? "linear"}(${row.rolloutMilestones?.length ?? 0} milestones)`)}`
96
97
  : "";
97
- console.log(`${row.name} ${pc.dim(row.kind)} ${pc.dim(row.declaredType)}${chanceSuffix}${rolloutSuffix}`);
98
+ console.log(`${row.name} ${pc.dim(row.visibility)} ${pc.dim(row.kind)} ${pc.dim(row.declaredType)}${chanceSuffix}${rolloutSuffix}`);
98
99
  }
99
100
  }
100
101
  async function runEnvWrite(operation, name, value, options) {
@@ -102,21 +103,47 @@ async function runEnvWrite(operation, name, value, options) {
102
103
  const target = await resolveTarget(options, local);
103
104
  const authProvider = createCliAuthProvider();
104
105
  const accessToken = await authProvider.getAccessToken();
105
- const entry = options.ab !== undefined
106
+ if (options.ab !== undefined && options.rollout !== undefined) {
107
+ throw new Error("Use either --ab or --rollout, not both.");
108
+ }
109
+ const hasRolloutPoints = (options.point?.length ?? 0) > 0;
110
+ if (options.rollout === undefined && (options.function !== undefined || hasRolloutPoints)) {
111
+ throw new Error("--function and --point can only be used together with --rollout.");
112
+ }
113
+ if (options.ab !== undefined && (options.function !== undefined || hasRolloutPoints)) {
114
+ throw new Error("--function and --point are only supported for --rollout, not --ab.");
115
+ }
116
+ if (options.rollout !== undefined && options.chance !== undefined) {
117
+ throw new Error("--chance only applies to --ab.");
118
+ }
119
+ const entry = options.rollout !== undefined
106
120
  ? {
107
121
  name,
108
- kind: "ab_roll",
122
+ visibility: parseVisibility(options.visibility),
123
+ kind: "rollout",
109
124
  declaredType: options.type ?? "string",
110
125
  valueA: value,
111
- valueB: options.ab,
112
- chance: parseChance(options.chance),
126
+ valueB: options.rollout,
127
+ rolloutFunction: parseRolloutFunction(options.function),
128
+ rolloutMilestones: parseRolloutMilestones(options.point),
113
129
  }
114
- : {
115
- name,
116
- kind: "secret",
117
- declaredType: options.type ?? "string",
118
- value,
119
- };
130
+ : options.ab !== undefined
131
+ ? {
132
+ name,
133
+ visibility: parseVisibility(options.visibility),
134
+ kind: "ab_roll",
135
+ declaredType: options.type ?? "string",
136
+ valueA: value,
137
+ valueB: options.ab,
138
+ chance: parseChance(options.chance),
139
+ }
140
+ : {
141
+ name,
142
+ visibility: parseVisibility(options.visibility),
143
+ kind: "secret",
144
+ declaredType: options.type ?? "string",
145
+ value,
146
+ };
120
147
  const result = await postJson({
121
148
  baseUrl: local.baseUrl,
122
149
  path: "/v1/env/write",
@@ -257,7 +284,11 @@ export function registerEnvCommands(program) {
257
284
  .argument("<name>", "Variable name")
258
285
  .argument("<value>", "Variable value")
259
286
  .option("--ab <value-b>", "Second value for ab_roll")
287
+ .option("--rollout <value-b>", "Second value for rollout")
260
288
  .option("--chance <number>", "A-branch probability between 0 and 1")
289
+ .option("--function <name>", "Rollout interpolation function (linear, step, ease_in_out)")
290
+ .option("--point <at=percentage>", "Rollout milestone, repeatable. Example: 2026-03-12T18:00:00Z=50", collectOptionValues, [])
291
+ .option("--visibility <visibility>", "Variable visibility: private|public", "private")
261
292
  .option("--type <type>", "Declared value type", "string")
262
293
  .option("--json", "Machine-readable output", false)).action(async (name, value, options) => {
263
294
  await runEnvWrite("create_only", name, value, options);
@@ -268,7 +299,11 @@ export function registerEnvCommands(program) {
268
299
  .argument("<name>", "Variable name")
269
300
  .argument("<value>", "Variable value")
270
301
  .option("--ab <value-b>", "Second value for ab_roll")
302
+ .option("--rollout <value-b>", "Second value for rollout")
271
303
  .option("--chance <number>", "A-branch probability between 0 and 1")
304
+ .option("--function <name>", "Rollout interpolation function (linear, step, ease_in_out)")
305
+ .option("--point <at=percentage>", "Rollout milestone, repeatable. Example: 2026-03-12T18:00:00Z=50", collectOptionValues, [])
306
+ .option("--visibility <visibility>", "Variable visibility: private|public", "private")
272
307
  .option("--type <type>", "Declared value type", "string")
273
308
  .option("--json", "Machine-readable output", false)).action(async (name, value, options) => {
274
309
  await runEnvWrite("upsert", name, value, options);
@@ -1,2 +1,4 @@
1
+ import type { BarekeyTypegenResult } from "@barekey/sdk/server";
1
2
  import { Command } from "commander";
3
+ export declare function formatTypegenResultMessage(result: BarekeyTypegenResult): string;
2
4
  export declare function registerTypegenCommand(program: Command): void;
@@ -1,5 +1,20 @@
1
- import { BarekeyClient } from "@barekey/sdk";
1
+ import { BarekeyClient } from "@barekey/sdk/server";
2
+ import pc from "picocolors";
2
3
  import { addTargetOptions, requireLocalSession, resolveTarget, } from "../command-utils.js";
4
+ export function formatTypegenResultMessage(result) {
5
+ const title = result.written
6
+ ? pc.green(pc.bold("Typegen complete"))
7
+ : pc.cyan(pc.bold("Typegen already up to date"));
8
+ const detail = result.written
9
+ ? "Fresh SDK types are ready in your installed @barekey/sdk package."
10
+ : "Your installed @barekey/sdk package already has the latest generated types.";
11
+ return [
12
+ title,
13
+ detail,
14
+ `${pc.bold("Server types")}: ${result.serverPath}`,
15
+ `${pc.bold("Public types")}: ${result.publicPath}`,
16
+ ].join("\n");
17
+ }
3
18
  async function runTypegen(options) {
4
19
  const local = await requireLocalSession();
5
20
  const target = await resolveTarget(options, local);
@@ -14,7 +29,7 @@ async function runTypegen(options) {
14
29
  typegen: false,
15
30
  });
16
31
  const result = await client.typegen();
17
- console.log(`${result.written ? "Updated" : "Unchanged"} ${result.path}`);
32
+ console.log(formatTypegenResultMessage(result));
18
33
  }
19
34
  export function registerTypegenCommand(program) {
20
35
  addTargetOptions(program
package/dist/index.js CHANGED
File without changes
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@barekey/cli",
3
- "version": "0.1.0",
3
+ "version": "0.2.0",
4
4
  "description": "Barekey command line interface",
5
5
  "type": "module",
6
6
  "bin": {
@@ -9,11 +9,13 @@
9
9
  "scripts": {
10
10
  "build": "tsc -p tsconfig.json",
11
11
  "typecheck": "tsc -p tsconfig.json --noEmit",
12
+ "test": "bun test test",
12
13
  "start": "node dist/index.js"
13
14
  },
15
+ "packageManager": "bun@1.2.22",
14
16
  "dependencies": {
15
17
  "@clack/prompts": "^0.11.0",
16
- "@barekey/sdk": "^0.1.0",
18
+ "@barekey/sdk": "^0.2.0",
17
19
  "commander": "^14.0.1",
18
20
  "open": "^10.2.0",
19
21
  "picocolors": "^1.1.1"
@@ -0,0 +1,87 @@
1
+ export type CliRolloutFunction = "linear" | "step" | "ease_in_out";
2
+
3
+ export type CliVisibility = "private" | "public";
4
+
5
+ export type CliRolloutMilestone = {
6
+ at: string;
7
+ percentage: number;
8
+ };
9
+
10
+ export function collectOptionValues(
11
+ value: string,
12
+ previous: Array<string> = [],
13
+ ): Array<string> {
14
+ return [...previous, value];
15
+ }
16
+
17
+ export function parseRolloutFunction(value: string | undefined): CliRolloutFunction {
18
+ if (value === undefined) {
19
+ return "linear";
20
+ }
21
+ if (value === "linear" || value === "step" || value === "ease_in_out") {
22
+ return value;
23
+ }
24
+ throw new Error("--function must be one of: linear, step, ease_in_out.");
25
+ }
26
+
27
+ export function parseRolloutMilestones(
28
+ values: Array<string> | undefined,
29
+ ): Array<CliRolloutMilestone> {
30
+ if (values === undefined || values.length === 0) {
31
+ return [
32
+ {
33
+ at: new Date().toISOString(),
34
+ percentage: 0,
35
+ },
36
+ ];
37
+ }
38
+
39
+ const milestones = values.map((rawValue) => {
40
+ const separatorIndex = rawValue.lastIndexOf("=");
41
+ if (separatorIndex <= 0 || separatorIndex >= rawValue.length - 1) {
42
+ throw new Error(
43
+ `Invalid --point value "${rawValue}". Use ISO_TIMESTAMP=PERCENTAGE, for example 2026-03-12T18:00:00Z=50.`,
44
+ );
45
+ }
46
+
47
+ const at = rawValue.slice(0, separatorIndex).trim();
48
+ const percentageValue = rawValue.slice(separatorIndex + 1).trim();
49
+ const atMs = Date.parse(at);
50
+ const percentage = Number(percentageValue);
51
+ if (!Number.isFinite(atMs)) {
52
+ throw new Error(`Invalid rollout point time "${at}". Use an ISO 8601 timestamp.`);
53
+ }
54
+ if (!Number.isFinite(percentage) || percentage < 0 || percentage > 100) {
55
+ throw new Error(
56
+ `Invalid rollout point percentage "${percentageValue}". Use a number between 0 and 100.`,
57
+ );
58
+ }
59
+
60
+ return {
61
+ atMs,
62
+ milestone: {
63
+ at: new Date(atMs).toISOString(),
64
+ percentage,
65
+ },
66
+ };
67
+ });
68
+
69
+ milestones.sort((left, right) => left.atMs - right.atMs);
70
+ for (let index = 1; index < milestones.length; index += 1) {
71
+ if ((milestones[index]?.atMs ?? 0) <= (milestones[index - 1]?.atMs ?? 0)) {
72
+ throw new Error("Rollout points must be strictly increasing by time.");
73
+ }
74
+ }
75
+
76
+ return milestones.map((entry) => entry.milestone);
77
+ }
78
+
79
+ export function parseVisibility(value: string | undefined): CliVisibility {
80
+ if (value === undefined || value === "private") {
81
+ return "private";
82
+ }
83
+ if (value === "public") {
84
+ return "public";
85
+ }
86
+ throw new Error("--visibility must be one of: private, public.");
87
+ }
@@ -1,7 +1,7 @@
1
1
  import { writeFile } from "node:fs/promises";
2
2
 
3
3
  import { cancel, confirm, isCancel } from "@clack/prompts";
4
- import { BarekeyClient } from "@barekey/sdk";
4
+ import { BarekeyClient } from "@barekey/sdk/server";
5
5
  import { Command } from "commander";
6
6
  import pc from "picocolors";
7
7
 
@@ -16,6 +16,14 @@ import {
16
16
  type EnvTargetOptions,
17
17
  } from "../command-utils.js";
18
18
  import { postJson } from "../http.js";
19
+ import {
20
+ collectOptionValues,
21
+ parseRolloutFunction,
22
+ parseRolloutMilestones,
23
+ parseVisibility,
24
+ type CliRolloutFunction,
25
+ type CliVisibility,
26
+ } from "./env-helpers.js";
19
27
 
20
28
  function createEnvClient(input: {
21
29
  organization: string | undefined;
@@ -117,12 +125,13 @@ async function runEnvList(options: EnvTargetOptions & { json?: boolean }): Promi
117
125
  const response = await postJson<{
118
126
  variables: Array<{
119
127
  name: string;
128
+ visibility: CliVisibility;
120
129
  kind: "secret" | "ab_roll" | "rollout";
121
130
  declaredType: "string" | "boolean" | "int64" | "float" | "date" | "json";
122
131
  createdAtMs: number;
123
132
  updatedAtMs: number;
124
133
  chance: number | null;
125
- rolloutFunction: "linear" | null;
134
+ rolloutFunction: CliRolloutFunction | null;
126
135
  rolloutMilestones: Array<{ at: string; percentage: number }> | null;
127
136
  }>;
128
137
  }>({
@@ -155,7 +164,7 @@ async function runEnvList(options: EnvTargetOptions & { json?: boolean }): Promi
155
164
  )}`
156
165
  : "";
157
166
  console.log(
158
- `${row.name} ${pc.dim(row.kind)} ${pc.dim(row.declaredType)}${chanceSuffix}${rolloutSuffix}`,
167
+ `${row.name} ${pc.dim(row.visibility)} ${pc.dim(row.kind)} ${pc.dim(row.declaredType)}${chanceSuffix}${rolloutSuffix}`,
159
168
  );
160
169
  }
161
170
  }
@@ -166,7 +175,11 @@ async function runEnvWrite(
166
175
  value: string,
167
176
  options: EnvTargetOptions & {
168
177
  ab?: string;
178
+ rollout?: string;
179
+ function?: string;
180
+ point?: Array<string>;
169
181
  chance?: string;
182
+ visibility?: CliVisibility;
170
183
  type?: "string" | "boolean" | "int64" | "float" | "date" | "json";
171
184
  json?: boolean;
172
185
  },
@@ -176,22 +189,49 @@ async function runEnvWrite(
176
189
  const authProvider = createCliAuthProvider();
177
190
  const accessToken = await authProvider.getAccessToken();
178
191
 
192
+ if (options.ab !== undefined && options.rollout !== undefined) {
193
+ throw new Error("Use either --ab or --rollout, not both.");
194
+ }
195
+ const hasRolloutPoints = (options.point?.length ?? 0) > 0;
196
+ if (options.rollout === undefined && (options.function !== undefined || hasRolloutPoints)) {
197
+ throw new Error("--function and --point can only be used together with --rollout.");
198
+ }
199
+ if (options.ab !== undefined && (options.function !== undefined || hasRolloutPoints)) {
200
+ throw new Error("--function and --point are only supported for --rollout, not --ab.");
201
+ }
202
+ if (options.rollout !== undefined && options.chance !== undefined) {
203
+ throw new Error("--chance only applies to --ab.");
204
+ }
205
+
179
206
  const entry =
180
- options.ab !== undefined
207
+ options.rollout !== undefined
181
208
  ? {
182
209
  name,
183
- kind: "ab_roll" as const,
210
+ visibility: parseVisibility(options.visibility),
211
+ kind: "rollout" as const,
184
212
  declaredType: options.type ?? "string",
185
213
  valueA: value,
186
- valueB: options.ab,
187
- chance: parseChance(options.chance),
214
+ valueB: options.rollout,
215
+ rolloutFunction: parseRolloutFunction(options.function),
216
+ rolloutMilestones: parseRolloutMilestones(options.point),
188
217
  }
189
- : {
190
- name,
191
- kind: "secret" as const,
192
- declaredType: options.type ?? "string",
193
- value,
194
- };
218
+ : options.ab !== undefined
219
+ ? {
220
+ name,
221
+ visibility: parseVisibility(options.visibility),
222
+ kind: "ab_roll" as const,
223
+ declaredType: options.type ?? "string",
224
+ valueA: value,
225
+ valueB: options.ab,
226
+ chance: parseChance(options.chance),
227
+ }
228
+ : {
229
+ name,
230
+ visibility: parseVisibility(options.visibility),
231
+ kind: "secret" as const,
232
+ declaredType: options.type ?? "string",
233
+ value,
234
+ };
195
235
 
196
236
  const result = await postJson<{
197
237
  createdCount: number;
@@ -315,7 +355,7 @@ async function runEnvPull(
315
355
  const response = await postJson<{
316
356
  values: Array<{
317
357
  name: string;
318
- kind: "secret" | "ab_roll";
358
+ kind: "secret" | "ab_roll" | "rollout";
319
359
  declaredType: "string" | "boolean" | "int64" | "float" | "date" | "json";
320
360
  value: string;
321
361
  }>;
@@ -411,7 +451,19 @@ export function registerEnvCommands(program: Command): void {
411
451
  .argument("<name>", "Variable name")
412
452
  .argument("<value>", "Variable value")
413
453
  .option("--ab <value-b>", "Second value for ab_roll")
454
+ .option("--rollout <value-b>", "Second value for rollout")
414
455
  .option("--chance <number>", "A-branch probability between 0 and 1")
456
+ .option(
457
+ "--function <name>",
458
+ "Rollout interpolation function (linear, step, ease_in_out)",
459
+ )
460
+ .option(
461
+ "--point <at=percentage>",
462
+ "Rollout milestone, repeatable. Example: 2026-03-12T18:00:00Z=50",
463
+ collectOptionValues,
464
+ [],
465
+ )
466
+ .option("--visibility <visibility>", "Variable visibility: private|public", "private")
415
467
  .option("--type <type>", "Declared value type", "string")
416
468
  .option("--json", "Machine-readable output", false),
417
469
  ).action(
@@ -420,7 +472,11 @@ export function registerEnvCommands(program: Command): void {
420
472
  value: string,
421
473
  options: EnvTargetOptions & {
422
474
  ab?: string;
475
+ rollout?: string;
476
+ function?: string;
477
+ point?: Array<string>;
423
478
  chance?: string;
479
+ visibility?: CliVisibility;
424
480
  type?: "string" | "boolean" | "int64" | "float" | "date" | "json";
425
481
  json?: boolean;
426
482
  },
@@ -436,7 +492,19 @@ export function registerEnvCommands(program: Command): void {
436
492
  .argument("<name>", "Variable name")
437
493
  .argument("<value>", "Variable value")
438
494
  .option("--ab <value-b>", "Second value for ab_roll")
495
+ .option("--rollout <value-b>", "Second value for rollout")
439
496
  .option("--chance <number>", "A-branch probability between 0 and 1")
497
+ .option(
498
+ "--function <name>",
499
+ "Rollout interpolation function (linear, step, ease_in_out)",
500
+ )
501
+ .option(
502
+ "--point <at=percentage>",
503
+ "Rollout milestone, repeatable. Example: 2026-03-12T18:00:00Z=50",
504
+ collectOptionValues,
505
+ [],
506
+ )
507
+ .option("--visibility <visibility>", "Variable visibility: private|public", "private")
440
508
  .option("--type <type>", "Declared value type", "string")
441
509
  .option("--json", "Machine-readable output", false),
442
510
  ).action(
@@ -445,7 +513,11 @@ export function registerEnvCommands(program: Command): void {
445
513
  value: string,
446
514
  options: EnvTargetOptions & {
447
515
  ab?: string;
516
+ rollout?: string;
517
+ function?: string;
518
+ point?: Array<string>;
448
519
  chance?: string;
520
+ visibility?: CliVisibility;
449
521
  type?: "string" | "boolean" | "int64" | "float" | "date" | "json";
450
522
  json?: boolean;
451
523
  },
@@ -1,5 +1,7 @@
1
- import { BarekeyClient } from "@barekey/sdk";
1
+ import { BarekeyClient } from "@barekey/sdk/server";
2
+ import type { BarekeyTypegenResult } from "@barekey/sdk/server";
2
3
  import { Command } from "commander";
4
+ import pc from "picocolors";
3
5
 
4
6
  import {
5
7
  addTargetOptions,
@@ -8,6 +10,22 @@ import {
8
10
  type EnvTargetOptions,
9
11
  } from "../command-utils.js";
10
12
 
13
+ export function formatTypegenResultMessage(result: BarekeyTypegenResult): string {
14
+ const title = result.written
15
+ ? pc.green(pc.bold("Typegen complete"))
16
+ : pc.cyan(pc.bold("Typegen already up to date"));
17
+ const detail = result.written
18
+ ? "Fresh SDK types are ready in your installed @barekey/sdk package."
19
+ : "Your installed @barekey/sdk package already has the latest generated types.";
20
+
21
+ return [
22
+ title,
23
+ detail,
24
+ `${pc.bold("Server types")}: ${result.serverPath}`,
25
+ `${pc.bold("Public types")}: ${result.publicPath}`,
26
+ ].join("\n");
27
+ }
28
+
11
29
  async function runTypegen(options: EnvTargetOptions): Promise<void> {
12
30
  const local = await requireLocalSession();
13
31
  const target = await resolveTarget(options, local);
@@ -24,7 +42,7 @@ async function runTypegen(options: EnvTargetOptions): Promise<void> {
24
42
  });
25
43
  const result = await client.typegen();
26
44
 
27
- console.log(`${result.written ? "Updated" : "Unchanged"} ${result.path}`);
45
+ console.log(formatTypegenResultMessage(result));
28
46
  }
29
47
 
30
48
  export function registerTypegenCommand(program: Command): void {
@@ -0,0 +1,64 @@
1
+ import { describe, expect, test } from "bun:test";
2
+
3
+ import {
4
+ parseRolloutFunction,
5
+ parseRolloutMilestones,
6
+ parseVisibility,
7
+ } from "../src/commands/env-helpers";
8
+
9
+ describe("parseVisibility", () => {
10
+ test("defaults to private when missing", () => {
11
+ expect(parseVisibility(undefined)).toBe("private");
12
+ });
13
+
14
+ test("accepts public visibility", () => {
15
+ expect(parseVisibility("public")).toBe("public");
16
+ });
17
+
18
+ test("rejects unsupported visibility values", () => {
19
+ expect(() => parseVisibility("internal")).toThrow(
20
+ "--visibility must be one of: private, public.",
21
+ );
22
+ });
23
+ });
24
+
25
+ describe("parseRolloutFunction", () => {
26
+ test("defaults to linear", () => {
27
+ expect(parseRolloutFunction(undefined)).toBe("linear");
28
+ });
29
+
30
+ test("rejects unsupported rollout functions", () => {
31
+ expect(() => parseRolloutFunction("curve")).toThrow(
32
+ "--function must be one of: linear, step, ease_in_out.",
33
+ );
34
+ });
35
+ });
36
+
37
+ describe("parseRolloutMilestones", () => {
38
+ test("sorts valid rollout milestones chronologically", () => {
39
+ expect(
40
+ parseRolloutMilestones([
41
+ "2026-03-12T19:00:00Z=100",
42
+ "2026-03-12T18:00:00Z=50",
43
+ ]),
44
+ ).toEqual([
45
+ {
46
+ at: "2026-03-12T18:00:00.000Z",
47
+ percentage: 50,
48
+ },
49
+ {
50
+ at: "2026-03-12T19:00:00.000Z",
51
+ percentage: 100,
52
+ },
53
+ ]);
54
+ });
55
+
56
+ test("rejects duplicate rollout milestone times", () => {
57
+ expect(() =>
58
+ parseRolloutMilestones([
59
+ "2026-03-12T18:00:00Z=25",
60
+ "2026-03-12T18:00:00Z=50",
61
+ ]),
62
+ ).toThrow("Rollout points must be strictly increasing by time.");
63
+ });
64
+ });
@@ -0,0 +1,37 @@
1
+ import { describe, expect, test } from "bun:test";
2
+
3
+ import { formatTypegenResultMessage } from "../src/commands/typegen";
4
+
5
+ describe("formatTypegenResultMessage", () => {
6
+ test("renders a helpful success message when files were updated", () => {
7
+ const message = formatTypegenResultMessage({
8
+ written: true,
9
+ path: "/tmp/generated.server.d.ts",
10
+ serverPath: "/tmp/generated.server.d.ts",
11
+ publicPath: "/tmp/generated.public.d.ts",
12
+ manifestVersion: "manifest-1",
13
+ });
14
+
15
+ expect(message).toContain("Typegen complete");
16
+ expect(message).toContain("Fresh SDK types are ready");
17
+ expect(message).toContain("Server types");
18
+ expect(message).toContain("/tmp/generated.server.d.ts");
19
+ expect(message).toContain("Public types");
20
+ expect(message).toContain("/tmp/generated.public.d.ts");
21
+ });
22
+
23
+ test("renders a calm message when nothing changed", () => {
24
+ const message = formatTypegenResultMessage({
25
+ written: false,
26
+ path: "/tmp/generated.server.d.ts",
27
+ serverPath: "/tmp/generated.server.d.ts",
28
+ publicPath: "/tmp/generated.public.d.ts",
29
+ manifestVersion: "manifest-1",
30
+ });
31
+
32
+ expect(message).toContain("Typegen already up to date");
33
+ expect(message).toContain("already has the latest generated types");
34
+ expect(message).toContain("/tmp/generated.server.d.ts");
35
+ expect(message).toContain("/tmp/generated.public.d.ts");
36
+ });
37
+ });
package/dist/typegen.d.ts DELETED
@@ -1,20 +0,0 @@
1
- export type TypegenManifest = {
2
- orgId: string;
3
- orgSlug: string;
4
- projectSlug: string;
5
- stageSlug: string;
6
- generatedAtMs: number;
7
- manifestVersion: string;
8
- variables: Array<{
9
- name: string;
10
- kind: "secret" | "ab_roll" | "rollout";
11
- declaredType: "string" | "boolean" | "int64" | "float" | "date" | "json";
12
- required: boolean;
13
- updatedAtMs: number;
14
- typeScriptType: string;
15
- }>;
16
- };
17
- export declare function writeTypegenFile(input: {
18
- manifest: TypegenManifest;
19
- outPath: string;
20
- }): Promise<void>;
package/dist/typegen.js DELETED
@@ -1,14 +0,0 @@
1
- import { writeFile } from "node:fs/promises";
2
- export async function writeTypegenFile(input) {
3
- const keys = input.manifest.variables
4
- .map((row) => row.name)
5
- .sort((left, right) => left.localeCompare(right));
6
- const unionLines = keys.map((key) => ` | ${JSON.stringify(key)}`).join("\n");
7
- const mapLines = input.manifest.variables
8
- .slice()
9
- .sort((left, right) => left.name.localeCompare(right.name))
10
- .map((row) => ` ${JSON.stringify(row.name)}: ${row.typeScriptType};`)
11
- .join("\n");
12
- const contents = `/* eslint-disable */\n/* This file is generated by barekey typegen. */\n\nimport type { BarekeyTemporalInstant } from "@barekey/sdk";\nimport "@barekey/sdk";\n\ndeclare module "@barekey/sdk" {\n interface BarekeyGeneratedTypeMap {\n${mapLines}\n }\n}\n\nexport type BarekeyKnownKey =\n${unionLines.length > 0 ? unionLines : " never"};\n\nexport const barekeyManifestVersion = ${JSON.stringify(input.manifest.manifestVersion)};\n`;
13
- await writeFile(input.outPath, contents, "utf8");
14
- }