@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 +28 -0
- package/README.md +36 -0
- package/dist/auth-provider.d.ts +6 -0
- package/dist/auth-provider.js +58 -0
- package/dist/command-utils.d.ts +23 -0
- package/dist/command-utils.js +78 -0
- package/dist/commands/auth.d.ts +2 -0
- package/dist/commands/auth.js +186 -0
- package/dist/commands/env.d.ts +2 -0
- package/dist/commands/env.js +295 -0
- package/dist/commands/typegen.d.ts +2 -0
- package/dist/commands/typegen.js +25 -0
- package/dist/constants.d.ts +4 -0
- package/dist/constants.js +4 -0
- package/dist/credentials-store.d.ts +7 -0
- package/dist/credentials-store.js +199 -0
- package/dist/http.d.ts +22 -0
- package/dist/http.js +66 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.js +17 -0
- package/dist/runtime-config.d.ts +10 -0
- package/dist/runtime-config.js +49 -0
- package/dist/typegen.d.ts +20 -0
- package/dist/typegen.js +14 -0
- package/dist/types.d.ts +13 -0
- package/dist/types.js +1 -0
- package/package.json +37 -0
- package/src/auth-provider.ts +86 -0
- package/src/command-utils.ts +118 -0
- package/src/commands/auth.ts +247 -0
- package/src/commands/env.ts +496 -0
- package/src/commands/typegen.ts +38 -0
- package/src/constants.ts +4 -0
- package/src/credentials-store.ts +243 -0
- package/src/http.ts +86 -0
- package/src/index.ts +21 -0
- package/src/runtime-config.ts +66 -0
- package/src/types.ts +14 -0
- package/tsconfig.json +13 -0
|
@@ -0,0 +1,243 @@
|
|
|
1
|
+
import { mkdir, readFile, rm, writeFile } from "node:fs/promises";
|
|
2
|
+
import { homedir } from "node:os";
|
|
3
|
+
import path from "node:path";
|
|
4
|
+
import { spawn } from "node:child_process";
|
|
5
|
+
|
|
6
|
+
import type { CliConfig, CliCredentials } from "./types.js";
|
|
7
|
+
|
|
8
|
+
const SERVICE_NAME = "barekey-cli";
|
|
9
|
+
const CONFIG_DIR = path.join(homedir(), ".config", "barekey");
|
|
10
|
+
const CONFIG_PATH = path.join(CONFIG_DIR, "config.json");
|
|
11
|
+
const CREDENTIALS_DIR = path.join(CONFIG_DIR, "credentials");
|
|
12
|
+
|
|
13
|
+
type CommandResult = {
|
|
14
|
+
stdout: string;
|
|
15
|
+
stderr: string;
|
|
16
|
+
code: number;
|
|
17
|
+
};
|
|
18
|
+
|
|
19
|
+
function runCommand(command: string, args: Array<string>, input?: string): Promise<CommandResult> {
|
|
20
|
+
return new Promise((resolve) => {
|
|
21
|
+
const child = spawn(command, args, {
|
|
22
|
+
stdio: "pipe",
|
|
23
|
+
});
|
|
24
|
+
|
|
25
|
+
let stdout = "";
|
|
26
|
+
let stderr = "";
|
|
27
|
+
|
|
28
|
+
child.stdout.on("data", (chunk: Buffer) => {
|
|
29
|
+
stdout += chunk.toString("utf8");
|
|
30
|
+
});
|
|
31
|
+
child.stderr.on("data", (chunk: Buffer) => {
|
|
32
|
+
stderr += chunk.toString("utf8");
|
|
33
|
+
});
|
|
34
|
+
|
|
35
|
+
child.on("error", () => {
|
|
36
|
+
resolve({ stdout: "", stderr: "", code: 127 });
|
|
37
|
+
});
|
|
38
|
+
|
|
39
|
+
child.on("close", (code) => {
|
|
40
|
+
resolve({ stdout, stderr, code: code ?? 1 });
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
if (input !== undefined) {
|
|
44
|
+
child.stdin.write(input);
|
|
45
|
+
}
|
|
46
|
+
child.stdin.end();
|
|
47
|
+
});
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
async function ensureConfigDir(): Promise<void> {
|
|
51
|
+
await mkdir(CONFIG_DIR, {
|
|
52
|
+
recursive: true,
|
|
53
|
+
mode: 0o700,
|
|
54
|
+
});
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
async function ensureCredentialsDir(): Promise<void> {
|
|
58
|
+
await ensureConfigDir();
|
|
59
|
+
await mkdir(CREDENTIALS_DIR, {
|
|
60
|
+
recursive: true,
|
|
61
|
+
mode: 0o700,
|
|
62
|
+
});
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
async function readJsonFile<T>(filePath: string): Promise<T | null> {
|
|
66
|
+
try {
|
|
67
|
+
const value = await readFile(filePath, "utf8");
|
|
68
|
+
return JSON.parse(value) as T;
|
|
69
|
+
} catch {
|
|
70
|
+
return null;
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
async function writeJsonFile(filePath: string, value: unknown): Promise<void> {
|
|
75
|
+
await ensureConfigDir();
|
|
76
|
+
await writeFile(filePath, JSON.stringify(value, null, 2), {
|
|
77
|
+
encoding: "utf8",
|
|
78
|
+
mode: 0o600,
|
|
79
|
+
});
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
function credentialsPathForAccount(accountId: string): string {
|
|
83
|
+
return path.join(CREDENTIALS_DIR, `${encodeURIComponent(accountId)}.json`);
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
async function canUseLinuxSecretTool(): Promise<boolean> {
|
|
87
|
+
if (process.platform !== "linux") {
|
|
88
|
+
return false;
|
|
89
|
+
}
|
|
90
|
+
const result = await runCommand("which", ["secret-tool"]);
|
|
91
|
+
return result.code === 0;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
async function canUseMacSecurity(): Promise<boolean> {
|
|
95
|
+
if (process.platform !== "darwin") {
|
|
96
|
+
return false;
|
|
97
|
+
}
|
|
98
|
+
const result = await runCommand("which", ["security"]);
|
|
99
|
+
return result.code === 0;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
async function setInKeychain(account: string, value: string): Promise<boolean> {
|
|
103
|
+
if (await canUseMacSecurity()) {
|
|
104
|
+
const result = await runCommand("security", [
|
|
105
|
+
"add-generic-password",
|
|
106
|
+
"-U",
|
|
107
|
+
"-s",
|
|
108
|
+
SERVICE_NAME,
|
|
109
|
+
"-a",
|
|
110
|
+
account,
|
|
111
|
+
"-w",
|
|
112
|
+
value,
|
|
113
|
+
]);
|
|
114
|
+
return result.code === 0;
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
if (await canUseLinuxSecretTool()) {
|
|
118
|
+
const result = await runCommand(
|
|
119
|
+
"secret-tool",
|
|
120
|
+
["store", `--label=${SERVICE_NAME}`, "service", SERVICE_NAME, "account", account],
|
|
121
|
+
value,
|
|
122
|
+
);
|
|
123
|
+
return result.code === 0;
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
return false;
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
async function getFromKeychain(account: string): Promise<string | null> {
|
|
130
|
+
if (await canUseMacSecurity()) {
|
|
131
|
+
const result = await runCommand("security", [
|
|
132
|
+
"find-generic-password",
|
|
133
|
+
"-s",
|
|
134
|
+
SERVICE_NAME,
|
|
135
|
+
"-a",
|
|
136
|
+
account,
|
|
137
|
+
"-w",
|
|
138
|
+
]);
|
|
139
|
+
if (result.code !== 0) {
|
|
140
|
+
return null;
|
|
141
|
+
}
|
|
142
|
+
const value = result.stdout.trim();
|
|
143
|
+
return value.length > 0 ? value : null;
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
if (await canUseLinuxSecretTool()) {
|
|
147
|
+
const result = await runCommand("secret-tool", [
|
|
148
|
+
"lookup",
|
|
149
|
+
"service",
|
|
150
|
+
SERVICE_NAME,
|
|
151
|
+
"account",
|
|
152
|
+
account,
|
|
153
|
+
]);
|
|
154
|
+
if (result.code !== 0) {
|
|
155
|
+
return null;
|
|
156
|
+
}
|
|
157
|
+
const value = result.stdout.trim();
|
|
158
|
+
return value.length > 0 ? value : null;
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
return null;
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
async function deleteFromKeychain(account: string): Promise<boolean> {
|
|
165
|
+
if (await canUseMacSecurity()) {
|
|
166
|
+
const result = await runCommand("security", [
|
|
167
|
+
"delete-generic-password",
|
|
168
|
+
"-s",
|
|
169
|
+
SERVICE_NAME,
|
|
170
|
+
"-a",
|
|
171
|
+
account,
|
|
172
|
+
]);
|
|
173
|
+
return result.code === 0;
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
if (await canUseLinuxSecretTool()) {
|
|
177
|
+
const result = await runCommand("secret-tool", [
|
|
178
|
+
"clear",
|
|
179
|
+
"service",
|
|
180
|
+
SERVICE_NAME,
|
|
181
|
+
"account",
|
|
182
|
+
account,
|
|
183
|
+
]);
|
|
184
|
+
return result.code === 0;
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
return false;
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
export async function saveConfig(config: CliConfig): Promise<void> {
|
|
191
|
+
await writeJsonFile(CONFIG_PATH, config);
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
export async function loadConfig(): Promise<CliConfig | null> {
|
|
195
|
+
return readJsonFile<CliConfig>(CONFIG_PATH);
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
export async function clearConfig(): Promise<void> {
|
|
199
|
+
await rm(CONFIG_PATH, {
|
|
200
|
+
force: true,
|
|
201
|
+
});
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
export async function saveCredentials(
|
|
205
|
+
accountId: string,
|
|
206
|
+
credentials: CliCredentials,
|
|
207
|
+
): Promise<void> {
|
|
208
|
+
const serialized = JSON.stringify(credentials);
|
|
209
|
+
const storedInKeychain = await setInKeychain(accountId, serialized);
|
|
210
|
+
if (storedInKeychain) {
|
|
211
|
+
await rm(credentialsPathForAccount(accountId), {
|
|
212
|
+
force: true,
|
|
213
|
+
});
|
|
214
|
+
return;
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
await ensureCredentialsDir();
|
|
218
|
+
await writeFile(credentialsPathForAccount(accountId), serialized, {
|
|
219
|
+
encoding: "utf8",
|
|
220
|
+
mode: 0o600,
|
|
221
|
+
});
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
export async function loadCredentials(accountId: string): Promise<CliCredentials | null> {
|
|
225
|
+
const fromKeychain = await getFromKeychain(accountId);
|
|
226
|
+
if (fromKeychain !== null) {
|
|
227
|
+
try {
|
|
228
|
+
return JSON.parse(fromKeychain) as CliCredentials;
|
|
229
|
+
} catch {
|
|
230
|
+
// Fall back to file credentials when keychain data is malformed.
|
|
231
|
+
}
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
const fromFile = await readJsonFile<CliCredentials>(credentialsPathForAccount(accountId));
|
|
235
|
+
return fromFile;
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
export async function deleteCredentials(accountId: string): Promise<void> {
|
|
239
|
+
await deleteFromKeychain(accountId);
|
|
240
|
+
await rm(credentialsPathForAccount(accountId), {
|
|
241
|
+
force: true,
|
|
242
|
+
});
|
|
243
|
+
}
|
package/src/http.ts
ADDED
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
export class CliHttpError extends Error {
|
|
2
|
+
readonly code: string;
|
|
3
|
+
readonly status: number;
|
|
4
|
+
readonly requestId: string | null;
|
|
5
|
+
|
|
6
|
+
constructor(input: { code: string; status: number; message: string; requestId?: string | null }) {
|
|
7
|
+
super(input.message);
|
|
8
|
+
this.name = "CliHttpError";
|
|
9
|
+
this.code = input.code;
|
|
10
|
+
this.status = input.status;
|
|
11
|
+
this.requestId = input.requestId ?? null;
|
|
12
|
+
}
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
async function parseJson(response: Response): Promise<unknown> {
|
|
16
|
+
try {
|
|
17
|
+
return await response.json();
|
|
18
|
+
} catch {
|
|
19
|
+
return null;
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export async function postJson<TResponse>(input: {
|
|
24
|
+
baseUrl: string;
|
|
25
|
+
path: string;
|
|
26
|
+
payload: unknown;
|
|
27
|
+
accessToken?: string | null;
|
|
28
|
+
}): Promise<TResponse> {
|
|
29
|
+
const response = await fetch(`${input.baseUrl.replace(/\/$/, "")}${input.path}`, {
|
|
30
|
+
method: "POST",
|
|
31
|
+
headers: {
|
|
32
|
+
"content-type": "application/json",
|
|
33
|
+
...(input.accessToken
|
|
34
|
+
? {
|
|
35
|
+
authorization: `Bearer ${input.accessToken}`,
|
|
36
|
+
}
|
|
37
|
+
: {}),
|
|
38
|
+
},
|
|
39
|
+
body: JSON.stringify(input.payload),
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
const parsed = await parseJson(response);
|
|
43
|
+
if (!response.ok) {
|
|
44
|
+
const payload = parsed as {
|
|
45
|
+
error?: { code?: string; message?: string; requestId?: string };
|
|
46
|
+
} | null;
|
|
47
|
+
throw new CliHttpError({
|
|
48
|
+
code: payload?.error?.code ?? "HTTP_ERROR",
|
|
49
|
+
status: response.status,
|
|
50
|
+
message: payload?.error?.message ?? `HTTP ${response.status}`,
|
|
51
|
+
requestId: payload?.error?.requestId ?? null,
|
|
52
|
+
});
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
return parsed as TResponse;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
export async function getJson<TResponse>(input: {
|
|
59
|
+
baseUrl: string;
|
|
60
|
+
path: string;
|
|
61
|
+
accessToken?: string | null;
|
|
62
|
+
}): Promise<TResponse> {
|
|
63
|
+
const response = await fetch(`${input.baseUrl.replace(/\/$/, "")}${input.path}`, {
|
|
64
|
+
method: "GET",
|
|
65
|
+
headers: input.accessToken
|
|
66
|
+
? {
|
|
67
|
+
authorization: `Bearer ${input.accessToken}`,
|
|
68
|
+
}
|
|
69
|
+
: undefined,
|
|
70
|
+
});
|
|
71
|
+
|
|
72
|
+
const parsed = await parseJson(response);
|
|
73
|
+
if (!response.ok) {
|
|
74
|
+
const payload = parsed as {
|
|
75
|
+
error?: { code?: string; message?: string; requestId?: string };
|
|
76
|
+
} | null;
|
|
77
|
+
throw new CliHttpError({
|
|
78
|
+
code: payload?.error?.code ?? "HTTP_ERROR",
|
|
79
|
+
status: response.status,
|
|
80
|
+
message: payload?.error?.message ?? `HTTP ${response.status}`,
|
|
81
|
+
requestId: payload?.error?.requestId ?? null,
|
|
82
|
+
});
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
return parsed as TResponse;
|
|
86
|
+
}
|
package/src/index.ts
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { Command } from "commander";
|
|
3
|
+
import pc from "picocolors";
|
|
4
|
+
|
|
5
|
+
import { registerAuthCommands } from "./commands/auth.js";
|
|
6
|
+
import { registerEnvCommands } from "./commands/env.js";
|
|
7
|
+
import { registerTypegenCommand } from "./commands/typegen.js";
|
|
8
|
+
import { CLI_DESCRIPTION, CLI_NAME, CLI_VERSION } from "./constants.js";
|
|
9
|
+
|
|
10
|
+
const program = new Command();
|
|
11
|
+
program.name(CLI_NAME).description(CLI_DESCRIPTION).version(CLI_VERSION);
|
|
12
|
+
|
|
13
|
+
registerAuthCommands(program);
|
|
14
|
+
registerEnvCommands(program);
|
|
15
|
+
registerTypegenCommand(program);
|
|
16
|
+
|
|
17
|
+
program.parseAsync(process.argv).catch((error: unknown) => {
|
|
18
|
+
const message = error instanceof Error ? error.message : "Command failed.";
|
|
19
|
+
console.error(pc.red(message));
|
|
20
|
+
process.exitCode = 1;
|
|
21
|
+
});
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
import { access, readFile } from "node:fs/promises";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
|
|
4
|
+
export type BarekeyRuntimeConfig = {
|
|
5
|
+
org?: string;
|
|
6
|
+
project?: string;
|
|
7
|
+
environment?: string;
|
|
8
|
+
};
|
|
9
|
+
|
|
10
|
+
export type BarekeyRuntimeConfigResult = {
|
|
11
|
+
config: BarekeyRuntimeConfig;
|
|
12
|
+
path: string;
|
|
13
|
+
};
|
|
14
|
+
|
|
15
|
+
async function fileExists(filePath: string): Promise<boolean> {
|
|
16
|
+
try {
|
|
17
|
+
await access(filePath);
|
|
18
|
+
return true;
|
|
19
|
+
} catch {
|
|
20
|
+
return false;
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
async function findBarekeyConfigPath(startDirectory: string): Promise<string | null> {
|
|
25
|
+
let current = path.resolve(startDirectory);
|
|
26
|
+
while (true) {
|
|
27
|
+
const candidate = path.join(current, "barekey.json");
|
|
28
|
+
if (await fileExists(candidate)) {
|
|
29
|
+
return candidate;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
const parent = path.dirname(current);
|
|
33
|
+
if (parent === current) {
|
|
34
|
+
return null;
|
|
35
|
+
}
|
|
36
|
+
current = parent;
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
export async function loadRuntimeConfig(): Promise<BarekeyRuntimeConfigResult | null> {
|
|
41
|
+
const configPath = await findBarekeyConfigPath(process.cwd());
|
|
42
|
+
if (configPath === null) {
|
|
43
|
+
return null;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
const raw = await readFile(configPath, "utf8");
|
|
47
|
+
const parsed = JSON.parse(raw) as Record<string, unknown>;
|
|
48
|
+
return {
|
|
49
|
+
path: configPath,
|
|
50
|
+
config: {
|
|
51
|
+
org:
|
|
52
|
+
typeof parsed.organization === "string"
|
|
53
|
+
? parsed.organization.trim()
|
|
54
|
+
: typeof parsed.org === "string"
|
|
55
|
+
? parsed.org.trim()
|
|
56
|
+
: undefined,
|
|
57
|
+
project: typeof parsed.project === "string" ? parsed.project.trim() : undefined,
|
|
58
|
+
environment:
|
|
59
|
+
typeof parsed.environment === "string"
|
|
60
|
+
? parsed.environment.trim()
|
|
61
|
+
: typeof parsed.stage === "string"
|
|
62
|
+
? parsed.stage.trim()
|
|
63
|
+
: undefined,
|
|
64
|
+
},
|
|
65
|
+
};
|
|
66
|
+
}
|
package/src/types.ts
ADDED
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
export type CliCredentials = {
|
|
2
|
+
accessToken: string;
|
|
3
|
+
refreshToken: string;
|
|
4
|
+
accessTokenExpiresAtMs: number;
|
|
5
|
+
refreshTokenExpiresAtMs: number;
|
|
6
|
+
clerkUserId: string;
|
|
7
|
+
orgId: string;
|
|
8
|
+
orgSlug: string;
|
|
9
|
+
};
|
|
10
|
+
|
|
11
|
+
export type CliConfig = {
|
|
12
|
+
baseUrl: string;
|
|
13
|
+
activeAccountId: string;
|
|
14
|
+
};
|
package/tsconfig.json
ADDED