@voxli/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.
- package/dist/cli.js +1 -0
- package/dist/commands/auth.d.ts +1 -0
- package/dist/commands/auth.js +7 -6
- package/dist/commands/listen.js +17 -3
- package/dist/lib/browser-auth.js +1 -0
- package/dist/lib/config.d.ts +17 -1
- package/dist/lib/config.js +78 -15
- package/package.json +1 -1
package/dist/cli.js
CHANGED
|
@@ -14,6 +14,7 @@ program
|
|
|
14
14
|
.command("auth")
|
|
15
15
|
.description("Authenticate with your Voxli API key")
|
|
16
16
|
.option("--manual", "Enter API key manually instead of browser auth")
|
|
17
|
+
.option("--local", "Save credentials in the current directory")
|
|
17
18
|
.action(authCommand);
|
|
18
19
|
program
|
|
19
20
|
.command("listen")
|
package/dist/commands/auth.d.ts
CHANGED
package/dist/commands/auth.js
CHANGED
|
@@ -18,7 +18,7 @@ async function promptForToken() {
|
|
|
18
18
|
rl.close();
|
|
19
19
|
}
|
|
20
20
|
}
|
|
21
|
-
async function validateAndSave(token, extra) {
|
|
21
|
+
async function validateAndSave(token, extra, opts) {
|
|
22
22
|
const label = extra ? "Access token" : "API key";
|
|
23
23
|
console.log("Validating...");
|
|
24
24
|
try {
|
|
@@ -37,12 +37,13 @@ async function validateAndSave(token, extra) {
|
|
|
37
37
|
// Network error or other — warn but still save
|
|
38
38
|
console.warn("Warning: could not validate token (network error). Saving anyway.");
|
|
39
39
|
}
|
|
40
|
-
|
|
40
|
+
const target = opts?.local ? "local" : "global";
|
|
41
|
+
const savedPath = await writeConfig({
|
|
41
42
|
accessToken: token,
|
|
42
43
|
refreshToken: extra?.refreshToken,
|
|
43
44
|
clientId: extra?.clientId,
|
|
44
|
-
});
|
|
45
|
-
console.log(`${label} saved to
|
|
45
|
+
}, { target });
|
|
46
|
+
console.log(`${label} saved to ${savedPath}`);
|
|
46
47
|
}
|
|
47
48
|
export async function authCommand(opts) {
|
|
48
49
|
if (!opts.manual) {
|
|
@@ -51,7 +52,7 @@ export async function authCommand(opts) {
|
|
|
51
52
|
await validateAndSave(result.accessToken, {
|
|
52
53
|
refreshToken: result.refreshToken,
|
|
53
54
|
clientId: result.clientId,
|
|
54
|
-
});
|
|
55
|
+
}, { local: opts.local });
|
|
55
56
|
return;
|
|
56
57
|
}
|
|
57
58
|
catch (err) {
|
|
@@ -61,5 +62,5 @@ export async function authCommand(opts) {
|
|
|
61
62
|
}
|
|
62
63
|
}
|
|
63
64
|
const token = await promptForToken();
|
|
64
|
-
await validateAndSave(token);
|
|
65
|
+
await validateAndSave(token, undefined, { local: opts.local });
|
|
65
66
|
}
|
package/dist/commands/listen.js
CHANGED
|
@@ -1,11 +1,25 @@
|
|
|
1
|
+
import { join } from "node:path";
|
|
1
2
|
import { spawn } from "node:child_process";
|
|
2
|
-
import {
|
|
3
|
+
import { resolveApiKey, resolveConfig, attemptTokenRefresh, } from "../lib/config.js";
|
|
3
4
|
import { getStableHostname } from "../lib/hostname.js";
|
|
4
5
|
import { register, ApiError } from "../lib/api.js";
|
|
5
6
|
const POLL_INTERVAL = 5_000;
|
|
6
7
|
export async function listenCommand(options) {
|
|
7
8
|
const isEnvToken = !!resolveApiKey();
|
|
8
|
-
let apiKey =
|
|
9
|
+
let apiKey = null;
|
|
10
|
+
let credentialSource = null;
|
|
11
|
+
if (isEnvToken) {
|
|
12
|
+
apiKey = resolveApiKey();
|
|
13
|
+
credentialSource = "VOXLI_API_TOKEN";
|
|
14
|
+
}
|
|
15
|
+
else {
|
|
16
|
+
const resolved = await resolveConfig();
|
|
17
|
+
if (resolved) {
|
|
18
|
+
const { config, configDir } = resolved;
|
|
19
|
+
apiKey = config.accessToken ?? config.apiKey ?? null;
|
|
20
|
+
credentialSource = join(configDir, "config.json");
|
|
21
|
+
}
|
|
22
|
+
}
|
|
9
23
|
if (!apiKey) {
|
|
10
24
|
console.error("Error: No API key found. Set VOXLI_API_TOKEN or run `voxli auth`.");
|
|
11
25
|
process.exit(1);
|
|
@@ -22,7 +36,7 @@ export async function listenCommand(options) {
|
|
|
22
36
|
};
|
|
23
37
|
process.on("SIGINT", shutdown);
|
|
24
38
|
process.on("SIGTERM", shutdown);
|
|
25
|
-
console.log(`Listening as ${hostname}
|
|
39
|
+
console.log(`Listening as ${hostname} using credentials from ${credentialSource}`);
|
|
26
40
|
while (true) {
|
|
27
41
|
try {
|
|
28
42
|
const data = await register(apiKey, {
|
package/dist/lib/browser-auth.js
CHANGED
package/dist/lib/config.d.ts
CHANGED
|
@@ -1,10 +1,26 @@
|
|
|
1
1
|
import type { VoxliConfig } from "../types.js";
|
|
2
|
+
/**
|
|
3
|
+
* Walk up from CWD looking for a `.voxli/config.json` file.
|
|
4
|
+
* Returns the `.voxli` directory path if found, or null.
|
|
5
|
+
*/
|
|
6
|
+
export declare function findLocalConfigDir(): Promise<string | null>;
|
|
2
7
|
export declare function readConfig(): Promise<VoxliConfig | null>;
|
|
8
|
+
/**
|
|
9
|
+
* Resolve config checking local first, then global.
|
|
10
|
+
* Returns the config and the directory it was found in.
|
|
11
|
+
*/
|
|
12
|
+
export declare function resolveConfig(): Promise<{
|
|
13
|
+
config: VoxliConfig;
|
|
14
|
+
configDir: string;
|
|
15
|
+
} | null>;
|
|
3
16
|
export declare function writeConfig(config: {
|
|
4
17
|
accessToken: string;
|
|
5
18
|
refreshToken?: string;
|
|
6
19
|
clientId?: string;
|
|
7
|
-
}
|
|
20
|
+
}, opts?: {
|
|
21
|
+
target?: "global" | "local";
|
|
22
|
+
configDir?: string;
|
|
23
|
+
}): Promise<string>;
|
|
8
24
|
export declare function resolveApiKey(): string | null;
|
|
9
25
|
export declare function resolveApiKeyAsync(): Promise<string | null>;
|
|
10
26
|
export declare function attemptTokenRefresh(): Promise<string | null>;
|
package/dist/lib/config.js
CHANGED
|
@@ -1,47 +1,110 @@
|
|
|
1
|
-
import { readFile, writeFile, mkdir, chmod } from "node:fs/promises";
|
|
2
|
-
import { join } from "node:path";
|
|
1
|
+
import { readFile, writeFile, mkdir, chmod, access } from "node:fs/promises";
|
|
2
|
+
import { join, dirname } from "node:path";
|
|
3
3
|
import { homedir } from "node:os";
|
|
4
4
|
import { refreshAccessToken } from "./oauth.js";
|
|
5
5
|
import { getApiBaseUrl } from "./api.js";
|
|
6
|
-
const
|
|
7
|
-
const
|
|
8
|
-
|
|
6
|
+
const GLOBAL_CONFIG_DIR = join(homedir(), ".voxli");
|
|
7
|
+
const GLOBAL_CONFIG_PATH = join(GLOBAL_CONFIG_DIR, "config.json");
|
|
8
|
+
async function fileExists(path) {
|
|
9
|
+
try {
|
|
10
|
+
await access(path);
|
|
11
|
+
return true;
|
|
12
|
+
}
|
|
13
|
+
catch {
|
|
14
|
+
return false;
|
|
15
|
+
}
|
|
16
|
+
}
|
|
17
|
+
/**
|
|
18
|
+
* Walk up from CWD looking for a `.voxli/config.json` file.
|
|
19
|
+
* Returns the `.voxli` directory path if found, or null.
|
|
20
|
+
*/
|
|
21
|
+
export async function findLocalConfigDir() {
|
|
22
|
+
let dir = process.cwd();
|
|
23
|
+
while (true) {
|
|
24
|
+
const candidate = join(dir, ".voxli", "config.json");
|
|
25
|
+
if (await fileExists(candidate)) {
|
|
26
|
+
return join(dir, ".voxli");
|
|
27
|
+
}
|
|
28
|
+
const parent = dirname(dir);
|
|
29
|
+
if (parent === dir)
|
|
30
|
+
break;
|
|
31
|
+
dir = parent;
|
|
32
|
+
}
|
|
33
|
+
return null;
|
|
34
|
+
}
|
|
35
|
+
async function readConfigFrom(configPath) {
|
|
9
36
|
try {
|
|
10
|
-
const raw = await readFile(
|
|
37
|
+
const raw = await readFile(configPath, "utf-8");
|
|
11
38
|
return JSON.parse(raw);
|
|
12
39
|
}
|
|
13
40
|
catch {
|
|
14
41
|
return null;
|
|
15
42
|
}
|
|
16
43
|
}
|
|
17
|
-
export async function
|
|
44
|
+
export async function readConfig() {
|
|
45
|
+
return readConfigFrom(GLOBAL_CONFIG_PATH);
|
|
46
|
+
}
|
|
47
|
+
/**
|
|
48
|
+
* Resolve config checking local first, then global.
|
|
49
|
+
* Returns the config and the directory it was found in.
|
|
50
|
+
*/
|
|
51
|
+
export async function resolveConfig() {
|
|
52
|
+
const localDir = await findLocalConfigDir();
|
|
53
|
+
if (localDir) {
|
|
54
|
+
const config = await readConfigFrom(join(localDir, "config.json"));
|
|
55
|
+
if (config)
|
|
56
|
+
return { config, configDir: localDir };
|
|
57
|
+
}
|
|
58
|
+
const config = await readConfigFrom(GLOBAL_CONFIG_PATH);
|
|
59
|
+
if (config)
|
|
60
|
+
return { config, configDir: GLOBAL_CONFIG_DIR };
|
|
61
|
+
return null;
|
|
62
|
+
}
|
|
63
|
+
export async function writeConfig(config, opts) {
|
|
64
|
+
let targetDir;
|
|
65
|
+
if (opts?.configDir) {
|
|
66
|
+
targetDir = opts.configDir;
|
|
67
|
+
}
|
|
68
|
+
else if (opts?.target === "local") {
|
|
69
|
+
targetDir = join(process.cwd(), ".voxli");
|
|
70
|
+
}
|
|
71
|
+
else {
|
|
72
|
+
targetDir = GLOBAL_CONFIG_DIR;
|
|
73
|
+
}
|
|
74
|
+
const targetPath = join(targetDir, "config.json");
|
|
18
75
|
const data = { accessToken: config.accessToken };
|
|
19
76
|
if (config.refreshToken)
|
|
20
77
|
data.refreshToken = config.refreshToken;
|
|
21
78
|
if (config.clientId)
|
|
22
79
|
data.clientId = config.clientId;
|
|
23
|
-
await mkdir(
|
|
24
|
-
await writeFile(
|
|
25
|
-
|
|
80
|
+
await mkdir(targetDir, { recursive: true });
|
|
81
|
+
await writeFile(targetPath, JSON.stringify(data, null, 2) + "\n", {
|
|
82
|
+
mode: 0o600,
|
|
83
|
+
});
|
|
84
|
+
await chmod(targetPath, 0o600);
|
|
85
|
+
return targetPath;
|
|
26
86
|
}
|
|
27
87
|
export function resolveApiKey() {
|
|
28
88
|
const envKey = process.env.VOXLI_API_TOKEN;
|
|
29
89
|
if (envKey)
|
|
30
90
|
return envKey;
|
|
31
|
-
// Caller should await readConfig() for the file-based key
|
|
32
91
|
return null;
|
|
33
92
|
}
|
|
34
93
|
export async function resolveApiKeyAsync() {
|
|
35
94
|
const envKey = resolveApiKey();
|
|
36
95
|
if (envKey)
|
|
37
96
|
return envKey;
|
|
38
|
-
const
|
|
97
|
+
const resolved = await resolveConfig();
|
|
98
|
+
const config = resolved?.config;
|
|
39
99
|
return config?.accessToken ?? config?.apiKey ?? null;
|
|
40
100
|
}
|
|
41
101
|
export async function attemptTokenRefresh() {
|
|
42
102
|
try {
|
|
43
|
-
const
|
|
44
|
-
if (!
|
|
103
|
+
const resolved = await resolveConfig();
|
|
104
|
+
if (!resolved)
|
|
105
|
+
return null;
|
|
106
|
+
const { config, configDir } = resolved;
|
|
107
|
+
if (!config.refreshToken || !config.clientId)
|
|
45
108
|
return null;
|
|
46
109
|
const baseUrl = getApiBaseUrl();
|
|
47
110
|
const result = await refreshAccessToken(baseUrl, config.refreshToken, config.clientId);
|
|
@@ -49,7 +112,7 @@ export async function attemptTokenRefresh() {
|
|
|
49
112
|
accessToken: result.accessToken,
|
|
50
113
|
refreshToken: result.refreshToken ?? config.refreshToken,
|
|
51
114
|
clientId: config.clientId,
|
|
52
|
-
});
|
|
115
|
+
}, { configDir });
|
|
53
116
|
return result.accessToken;
|
|
54
117
|
}
|
|
55
118
|
catch {
|