bulgur 0.0.0-stage → 1.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/README.md +76 -2
- package/bin/bulgur.js +59 -0
- package/binaries/darwin-arm64/bulgur +0 -0
- package/binaries/darwin-x64/bulgur +0 -0
- package/binaries/linux-arm64/bulgur +0 -0
- package/binaries/linux-x64/bulgur +0 -0
- package/index.d.ts +75 -0
- package/index.js +191 -0
- package/package.json +48 -4
package/README.md
CHANGED
|
@@ -1,3 +1,77 @@
|
|
|
1
|
-
#
|
|
1
|
+
# bulgur
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
Ultra-simple secrets manager CLI. This npm package is a thin, self-contained
|
|
4
|
+
distribution of the `bulgur` binary — no postinstall downloads, no network
|
|
5
|
+
access required. The correct binary for your platform is bundled inside the
|
|
6
|
+
package and launched by a small Node shim.
|
|
7
|
+
|
|
8
|
+
## Add it to a project
|
|
9
|
+
|
|
10
|
+
```sh
|
|
11
|
+
npx bulgur init
|
|
12
|
+
```
|
|
13
|
+
|
|
14
|
+
`init` links the directory to a bulgur project (writes `bulgur.json`), adds
|
|
15
|
+
`bulgur` as a devDependency and wraps your `dev` script:
|
|
16
|
+
|
|
17
|
+
```json
|
|
18
|
+
"scripts": { "dev": "bulgur run -- next dev" }
|
|
19
|
+
```
|
|
20
|
+
|
|
21
|
+
Commit `bulgur.json` and `package.json`. From then on a teammate only runs:
|
|
22
|
+
|
|
23
|
+
```sh
|
|
24
|
+
git clone … && cd app
|
|
25
|
+
pnpm install
|
|
26
|
+
pnpm dev # asks for their bulgur email/username + password once, then starts with secrets
|
|
27
|
+
```
|
|
28
|
+
|
|
29
|
+
No `.env` files to pass around. In CI, set `BULGUR_TOKEN`, or
|
|
30
|
+
`BULGUR_SKIP=1` to run a wrapped script without secrets.
|
|
31
|
+
|
|
32
|
+
## Install globally
|
|
33
|
+
|
|
34
|
+
```sh
|
|
35
|
+
npm install -g bulgur
|
|
36
|
+
```
|
|
37
|
+
|
|
38
|
+
## Usage
|
|
39
|
+
|
|
40
|
+
```sh
|
|
41
|
+
bulgur login # email/username + password, or --token blg_...
|
|
42
|
+
bulgur init
|
|
43
|
+
bulgur run -- <command> # inject secrets into one command
|
|
44
|
+
bulgur pull # write a .env
|
|
45
|
+
bulgur push
|
|
46
|
+
```
|
|
47
|
+
|
|
48
|
+
## Supported platforms
|
|
49
|
+
|
|
50
|
+
- macOS (Intel `x64`, Apple Silicon `arm64`)
|
|
51
|
+
- Linux (`x64`, `arm64`)
|
|
52
|
+
|
|
53
|
+
Windows is not supported yet.
|
|
54
|
+
|
|
55
|
+
## How it works
|
|
56
|
+
|
|
57
|
+
`bin/bulgur.js` selects `binaries/<platform>-<arch>/bulgur` based on
|
|
58
|
+
`process.platform` + `process.arch`, spawns it with inherited stdio, and
|
|
59
|
+
forwards the exit code and signals. Binaries are stamped into the package at
|
|
60
|
+
publish time from the goreleaser build.
|
|
61
|
+
|
|
62
|
+
## Runtime SDK
|
|
63
|
+
|
|
64
|
+
This package also exports a tiny, dependency-free runtime SDK for loading
|
|
65
|
+
secrets into `process.env` at process startup — no CLI binary involved:
|
|
66
|
+
|
|
67
|
+
```js
|
|
68
|
+
const { load } = require("bulgur");
|
|
69
|
+
// or: import { load } from "bulgur";
|
|
70
|
+
|
|
71
|
+
await load(); // reads BULGUR_DSN, assigns onto process.env
|
|
72
|
+
```
|
|
73
|
+
|
|
74
|
+
Set `BULGUR_DSN` to a **runtime-scoped** token DSN:
|
|
75
|
+
`bulgur://TOKEN@HOST/ORG/PROJECT/ENV`. See the repo root `README.md`
|
|
76
|
+
("Runtime (deploy platforms)") for platform examples. `load()` accepts
|
|
77
|
+
`{ dsn, override, optional }` — see `index.d.ts` for details.
|
package/bin/bulgur.js
ADDED
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
"use strict";
|
|
3
|
+
|
|
4
|
+
// Thin launcher: pick the bundled bulgur binary that matches the host
|
|
5
|
+
// platform/arch and exec it, forwarding stdio, the exit code, and signals.
|
|
6
|
+
// The binaries are fat-bundled into this package at pack time (see the repo
|
|
7
|
+
// Makefile `npm-pack` target), so there is NO network/postinstall step —
|
|
8
|
+
// this works even while the GitHub repo is private.
|
|
9
|
+
|
|
10
|
+
const { spawnSync } = require("child_process");
|
|
11
|
+
const path = require("path");
|
|
12
|
+
const fs = require("fs");
|
|
13
|
+
|
|
14
|
+
// Node reports arch as x64/arm64; we bundle binaries under matching dir names.
|
|
15
|
+
const platform = process.platform; // 'darwin' | 'linux'
|
|
16
|
+
const arch = process.arch; // 'x64' | 'arm64'
|
|
17
|
+
|
|
18
|
+
const supported = new Set([
|
|
19
|
+
"darwin-x64",
|
|
20
|
+
"darwin-arm64",
|
|
21
|
+
"linux-x64",
|
|
22
|
+
"linux-arm64",
|
|
23
|
+
]);
|
|
24
|
+
|
|
25
|
+
const key = `${platform}-${arch}`;
|
|
26
|
+
if (!supported.has(key)) {
|
|
27
|
+
console.error(
|
|
28
|
+
`bulgur: unsupported platform/arch "${key}". ` +
|
|
29
|
+
`Supported: ${[...supported].join(", ")}.`
|
|
30
|
+
);
|
|
31
|
+
process.exit(1);
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
const binPath = path.join(__dirname, "..", "binaries", key, "bulgur");
|
|
35
|
+
if (!fs.existsSync(binPath)) {
|
|
36
|
+
console.error(
|
|
37
|
+
`bulgur: bundled binary not found at ${binPath}. ` +
|
|
38
|
+
`This package may have been built without the ${key} binary.`
|
|
39
|
+
);
|
|
40
|
+
process.exit(1);
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
// spawnSync with stdio: inherit forwards stdin/stdout/stderr transparently and
|
|
44
|
+
// relays terminating signals to the child on POSIX, so the run command's signal
|
|
45
|
+
// handling keeps working.
|
|
46
|
+
const result = spawnSync(binPath, process.argv.slice(2), { stdio: "inherit" });
|
|
47
|
+
|
|
48
|
+
if (result.error) {
|
|
49
|
+
console.error(`bulgur: failed to launch binary: ${result.error.message}`);
|
|
50
|
+
process.exit(1);
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
if (result.signal) {
|
|
54
|
+
// Re-raise the signal so the parent shell sees the correct termination cause.
|
|
55
|
+
process.kill(process.pid, result.signal);
|
|
56
|
+
return;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
process.exit(result.status === null ? 1 : result.status);
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
package/index.d.ts
ADDED
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
// Type definitions for the `bulgur` runtime SDK.
|
|
2
|
+
// Hand-written; no build step. Kept in sync with index.js by hand.
|
|
3
|
+
|
|
4
|
+
export interface LoadOptions {
|
|
5
|
+
/**
|
|
6
|
+
* The DSN to load, of the form `bulgur://TOKEN@HOST/ORG/PROJECT/ENV`.
|
|
7
|
+
* Defaults to `process.env.BULGUR_DSN`.
|
|
8
|
+
*/
|
|
9
|
+
dsn?: string;
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* When true, values fetched from bulgur overwrite any existing
|
|
13
|
+
* `process.env` entry with the same name.
|
|
14
|
+
*
|
|
15
|
+
* Default: false — existing `process.env` values win, and the fetched
|
|
16
|
+
* value is skipped (not counted in the returned `loaded` count).
|
|
17
|
+
*/
|
|
18
|
+
override?: boolean;
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* When true:
|
|
22
|
+
* - a missing/empty DSN resolves `{ loaded: 0 }` instead of throwing.
|
|
23
|
+
* - a network failure or non-2xx HTTP response resolves `{ loaded: 0 }`
|
|
24
|
+
* instead of throwing.
|
|
25
|
+
* A DSN that IS present but malformed (bad scheme, missing token, wrong
|
|
26
|
+
* number of path segments, etc.) always throws regardless of this option
|
|
27
|
+
* — a configuration typo should be loud.
|
|
28
|
+
*
|
|
29
|
+
* Default: false — all of the above throw.
|
|
30
|
+
*/
|
|
31
|
+
optional?: boolean;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
export interface LoadResult {
|
|
35
|
+
/**
|
|
36
|
+
* The number of secrets actually written to `process.env`. Secrets that
|
|
37
|
+
* were fetched but skipped because an existing `process.env` value won
|
|
38
|
+
* (see `override`) are NOT counted.
|
|
39
|
+
*/
|
|
40
|
+
loaded: number;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
export interface ParsedDsn {
|
|
44
|
+
/** The bearer token (userinfo component of the DSN). */
|
|
45
|
+
token: string;
|
|
46
|
+
/** Lowercased `host[:port]`. */
|
|
47
|
+
host: string;
|
|
48
|
+
/** Org slug (first path segment). */
|
|
49
|
+
org: string;
|
|
50
|
+
/** Project slug (second path segment). */
|
|
51
|
+
project: string;
|
|
52
|
+
/** Environment slug (third path segment). */
|
|
53
|
+
env: string;
|
|
54
|
+
/**
|
|
55
|
+
* The resolved API base URL, e.g. `https://api.bulgur.dev` or
|
|
56
|
+
* `http://localhost:5432`. `http` is used only for `localhost` /
|
|
57
|
+
* `127.0.0.1` hosts; every other host uses `https`.
|
|
58
|
+
*/
|
|
59
|
+
baseUrl: string;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
/**
|
|
63
|
+
* Load secret values for the environment identified by `options.dsn` (or
|
|
64
|
+
* `process.env.BULGUR_DSN`) and assign them onto `process.env`.
|
|
65
|
+
*
|
|
66
|
+
* This is a one-shot call: after it resolves, this module has no further
|
|
67
|
+
* runtime effect and holds no open connections.
|
|
68
|
+
*/
|
|
69
|
+
export function load(options?: LoadOptions): Promise<LoadResult>;
|
|
70
|
+
|
|
71
|
+
/**
|
|
72
|
+
* Parse and validate a `bulgur://TOKEN@HOST/ORG/PROJECT/ENV` DSN without
|
|
73
|
+
* making a network call. Thrown error messages never include the token.
|
|
74
|
+
*/
|
|
75
|
+
export function parseDsn(dsn: string): ParsedDsn;
|
package/index.js
ADDED
|
@@ -0,0 +1,191 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
|
|
3
|
+
// bulgur runtime SDK — Node >= 18 (relies on global fetch, no deps).
|
|
4
|
+
//
|
|
5
|
+
// Reads a `bulgur://TOKEN@HOST/ORG/PROJECT/ENV` DSN, fetches the merged
|
|
6
|
+
// secret values for that org/project/environment from the bulgur API, and
|
|
7
|
+
// assigns them onto process.env. This module has zero runtime dependencies
|
|
8
|
+
// and is hand-written CJS (no bundler/build step) so it can ship inside the
|
|
9
|
+
// npm fat-bundle package alongside the CLI binary wrapper.
|
|
10
|
+
//
|
|
11
|
+
// Error hygiene: no error thrown by this module ever includes the TOKEN.
|
|
12
|
+
// Messages may include host/org/project/env and HTTP status for debugging.
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* Parse a bulgur DSN of the form:
|
|
16
|
+
* bulgur://TOKEN@HOST[:PORT]/ORG/PROJECT/ENV
|
|
17
|
+
*
|
|
18
|
+
* Parsed manually (not via WHATWG URL) because the URL parser does not
|
|
19
|
+
* lowercase the host for non-special schemes like "bulgur:", which would
|
|
20
|
+
* silently produce a different (case-sensitive) host than intended.
|
|
21
|
+
*
|
|
22
|
+
* @param {string} dsn
|
|
23
|
+
* @returns {{token: string, host: string, org: string, project: string, env: string, baseUrl: string}}
|
|
24
|
+
*/
|
|
25
|
+
function parseDsn(dsn) {
|
|
26
|
+
if (typeof dsn !== "string" || dsn.length === 0) {
|
|
27
|
+
throw new Error("bulgur: invalid DSN: expected a non-empty string");
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
const SCHEME_SEP = "://";
|
|
31
|
+
const schemeIdx = dsn.indexOf(SCHEME_SEP);
|
|
32
|
+
if (schemeIdx === -1) {
|
|
33
|
+
throw new Error(
|
|
34
|
+
'bulgur: invalid DSN: expected "bulgur://TOKEN@HOST/ORG/PROJECT/ENV"'
|
|
35
|
+
);
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
const scheme = dsn.slice(0, schemeIdx);
|
|
39
|
+
if (scheme !== "bulgur") {
|
|
40
|
+
throw new Error(
|
|
41
|
+
`bulgur: invalid DSN: scheme must be "bulgur", got "${scheme}"`
|
|
42
|
+
);
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
// rest = TOKEN@HOST[:PORT]/ORG/PROJECT/ENV[/]
|
|
46
|
+
const rest = dsn.slice(schemeIdx + SCHEME_SEP.length);
|
|
47
|
+
|
|
48
|
+
const atIdx = rest.indexOf("@");
|
|
49
|
+
if (atIdx === -1) {
|
|
50
|
+
throw new Error(
|
|
51
|
+
"bulgur: invalid DSN: missing token (expected TOKEN@HOST/...)"
|
|
52
|
+
);
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
const token = rest.slice(0, atIdx);
|
|
56
|
+
if (token.length === 0) {
|
|
57
|
+
throw new Error("bulgur: invalid DSN: token must not be empty");
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
const afterAt = rest.slice(atIdx + 1); // HOST[:PORT]/ORG/PROJECT/ENV[/]
|
|
61
|
+
const slashIdx = afterAt.indexOf("/");
|
|
62
|
+
if (slashIdx === -1) {
|
|
63
|
+
throw new Error(
|
|
64
|
+
"bulgur: invalid DSN: missing path (expected /ORG/PROJECT/ENV)"
|
|
65
|
+
);
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
const rawHost = afterAt.slice(0, slashIdx);
|
|
69
|
+
if (rawHost.length === 0) {
|
|
70
|
+
throw new Error("bulgur: invalid DSN: host must not be empty");
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
let pathPart = afterAt.slice(slashIdx + 1); // ORG/PROJECT/ENV possibly with a trailing slash
|
|
74
|
+
if (pathPart.endsWith("/")) {
|
|
75
|
+
pathPart = pathPart.slice(0, -1);
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
const segments = pathPart.length === 0 ? [] : pathPart.split("/");
|
|
79
|
+
if (segments.length !== 3 || segments.some((s) => s.length === 0)) {
|
|
80
|
+
throw new Error(
|
|
81
|
+
"bulgur: invalid DSN: path must have exactly 3 non-empty segments " +
|
|
82
|
+
`(/ORG/PROJECT/ENV), got ${segments.length} segment(s)`
|
|
83
|
+
);
|
|
84
|
+
}
|
|
85
|
+
const [org, project, envSlug] = segments;
|
|
86
|
+
|
|
87
|
+
// Split optional :PORT off the host, then lowercase (hosts are
|
|
88
|
+
// case-insensitive; the WHATWG URL parser does NOT do this for us on a
|
|
89
|
+
// non-special scheme like "bulgur:", so we do it ourselves).
|
|
90
|
+
let hostname = rawHost;
|
|
91
|
+
let port = "";
|
|
92
|
+
const colonIdx = rawHost.lastIndexOf(":");
|
|
93
|
+
if (colonIdx !== -1) {
|
|
94
|
+
hostname = rawHost.slice(0, colonIdx);
|
|
95
|
+
port = rawHost.slice(colonIdx + 1);
|
|
96
|
+
if (!/^\d+$/.test(port) || Number(port) < 1 || Number(port) > 65535) {
|
|
97
|
+
throw new Error("bulgur: invalid DSN: invalid port in host");
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
if (hostname.length === 0) {
|
|
101
|
+
throw new Error("bulgur: invalid DSN: host must not be empty");
|
|
102
|
+
}
|
|
103
|
+
hostname = hostname.toLowerCase();
|
|
104
|
+
const host = port ? `${hostname}:${port}` : hostname;
|
|
105
|
+
|
|
106
|
+
const isLocal = hostname === "localhost" || hostname === "127.0.0.1";
|
|
107
|
+
const scheme2 = isLocal ? "http" : "https";
|
|
108
|
+
const baseUrl = `${scheme2}://${host}`;
|
|
109
|
+
|
|
110
|
+
return { token, host, org, project, env: envSlug, baseUrl };
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
/**
|
|
114
|
+
* @param {{dsn?: string, override?: boolean, optional?: boolean}} [options]
|
|
115
|
+
* @returns {Promise<{loaded: number}>}
|
|
116
|
+
*/
|
|
117
|
+
async function load(options) {
|
|
118
|
+
const opts = options || {};
|
|
119
|
+
const dsn = opts.dsn !== undefined ? opts.dsn : process.env.BULGUR_DSN;
|
|
120
|
+
const override = opts.override === true;
|
|
121
|
+
const optional = opts.optional === true;
|
|
122
|
+
|
|
123
|
+
if (!dsn) {
|
|
124
|
+
if (optional) return { loaded: 0 };
|
|
125
|
+
throw new Error(
|
|
126
|
+
"bulgur: missing DSN (set BULGUR_DSN or pass { dsn } to load())"
|
|
127
|
+
);
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
// Malformed DSN is always loud, even with optional:true.
|
|
131
|
+
const parsed = parseDsn(dsn);
|
|
132
|
+
const { token, host, org, project, env, baseUrl } = parsed;
|
|
133
|
+
|
|
134
|
+
const url =
|
|
135
|
+
`${baseUrl}/v1/orgs/${encodeURIComponent(org)}` +
|
|
136
|
+
`/projects/${encodeURIComponent(project)}` +
|
|
137
|
+
`/environments/${encodeURIComponent(env)}/values`;
|
|
138
|
+
|
|
139
|
+
let res;
|
|
140
|
+
try {
|
|
141
|
+
res = await fetch(url, {
|
|
142
|
+
headers: {
|
|
143
|
+
Authorization: `Bearer ${token}`,
|
|
144
|
+
Accept: "application/json",
|
|
145
|
+
},
|
|
146
|
+
});
|
|
147
|
+
} catch (err) {
|
|
148
|
+
if (optional) return { loaded: 0 };
|
|
149
|
+
throw new Error(
|
|
150
|
+
`bulgur: failed to reach ${host} (org=${org}, project=${project}, ` +
|
|
151
|
+
`env=${env}): ${err && err.message ? err.message : err}`
|
|
152
|
+
);
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
if (!res.ok) {
|
|
156
|
+
if (optional) return { loaded: 0 };
|
|
157
|
+
throw new Error(
|
|
158
|
+
`bulgur: request to ${host} failed with HTTP ${res.status} ` +
|
|
159
|
+
`(org=${org}, project=${project}, env=${env})`
|
|
160
|
+
);
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
let body;
|
|
164
|
+
try {
|
|
165
|
+
body = await res.json();
|
|
166
|
+
} catch (err) {
|
|
167
|
+
if (optional) return { loaded: 0 };
|
|
168
|
+
throw new Error(
|
|
169
|
+
`bulgur: failed to parse response from ${host} as JSON ` +
|
|
170
|
+
`(org=${org}, project=${project}, env=${env})`
|
|
171
|
+
);
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
const secrets = body && Array.isArray(body.secrets) ? body.secrets : [];
|
|
175
|
+
let loaded = 0;
|
|
176
|
+
for (const secret of secrets) {
|
|
177
|
+
if (!secret || typeof secret.name !== "string") continue;
|
|
178
|
+
const key = secret.name;
|
|
179
|
+
const hasExisting = Object.prototype.hasOwnProperty.call(
|
|
180
|
+
process.env,
|
|
181
|
+
key
|
|
182
|
+
);
|
|
183
|
+
if (hasExisting && !override) continue; // existing env vars win by default
|
|
184
|
+
process.env[key] = secret.value == null ? "" : String(secret.value);
|
|
185
|
+
loaded++;
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
return { loaded };
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
module.exports = { load, parseDsn };
|
package/package.json
CHANGED
|
@@ -1,6 +1,50 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "bulgur",
|
|
3
|
-
"version": "
|
|
4
|
-
"
|
|
5
|
-
"
|
|
6
|
-
|
|
3
|
+
"version": "1.1.0",
|
|
4
|
+
"description": "Ultra-simple secrets manager CLI (bulgur), plus a runtime SDK: `import { load } from \"bulgur\"`",
|
|
5
|
+
"bin": {
|
|
6
|
+
"bulgur": "bin/bulgur.js"
|
|
7
|
+
},
|
|
8
|
+
"main": "index.js",
|
|
9
|
+
"types": "index.d.ts",
|
|
10
|
+
"exports": {
|
|
11
|
+
".": {
|
|
12
|
+
"types": "./index.d.ts",
|
|
13
|
+
"import": "./index.js",
|
|
14
|
+
"require": "./index.js"
|
|
15
|
+
},
|
|
16
|
+
"./package.json": "./package.json"
|
|
17
|
+
},
|
|
18
|
+
"files": [
|
|
19
|
+
"bin/",
|
|
20
|
+
"binaries/",
|
|
21
|
+
"index.js",
|
|
22
|
+
"index.d.ts",
|
|
23
|
+
"README.md"
|
|
24
|
+
],
|
|
25
|
+
"scripts": {
|
|
26
|
+
"test": "node --test"
|
|
27
|
+
},
|
|
28
|
+
"engines": {
|
|
29
|
+
"node": ">=18"
|
|
30
|
+
},
|
|
31
|
+
"os": [
|
|
32
|
+
"darwin",
|
|
33
|
+
"linux"
|
|
34
|
+
],
|
|
35
|
+
"cpu": [
|
|
36
|
+
"x64",
|
|
37
|
+
"arm64"
|
|
38
|
+
],
|
|
39
|
+
"homepage": "https://bulgur.dev",
|
|
40
|
+
"license": "UNLICENSED",
|
|
41
|
+
"keywords": [
|
|
42
|
+
"bulgur",
|
|
43
|
+
"secrets",
|
|
44
|
+
"cli",
|
|
45
|
+
"env",
|
|
46
|
+
"dotenv",
|
|
47
|
+
"environment-variables",
|
|
48
|
+
"secrets-manager"
|
|
49
|
+
]
|
|
50
|
+
}
|