@delega-dev/cli 1.0.4 → 1.0.5
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/.github/workflows/ci.yml +2 -2
- package/.github/workflows/publish.yml +2 -2
- package/README.md +6 -0
- package/dist/api.js +10 -1
- package/dist/commands/login.js +27 -11
- package/dist/config.d.ts +1 -0
- package/dist/config.js +19 -3
- package/package.json +1 -1
- package/src/api.ts +10 -1
- package/src/commands/login.ts +32 -11
- package/src/config.ts +22 -3
package/.github/workflows/ci.yml
CHANGED
|
@@ -10,9 +10,9 @@ jobs:
|
|
|
10
10
|
build:
|
|
11
11
|
runs-on: ubuntu-latest
|
|
12
12
|
steps:
|
|
13
|
-
- uses: actions/checkout@v4
|
|
13
|
+
- uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4
|
|
14
14
|
|
|
15
|
-
- uses: actions/setup-node@v4
|
|
15
|
+
- uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4
|
|
16
16
|
with:
|
|
17
17
|
node-version: 22
|
|
18
18
|
cache: npm
|
|
@@ -12,11 +12,11 @@ jobs:
|
|
|
12
12
|
publish:
|
|
13
13
|
runs-on: ubuntu-latest
|
|
14
14
|
steps:
|
|
15
|
-
- uses: actions/checkout@v4
|
|
15
|
+
- uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4
|
|
16
16
|
with:
|
|
17
17
|
fetch-depth: 0
|
|
18
18
|
|
|
19
|
-
- uses: actions/setup-node@v4
|
|
19
|
+
- uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4
|
|
20
20
|
with:
|
|
21
21
|
node-version: 22
|
|
22
22
|
cache: npm
|
package/README.md
CHANGED
|
@@ -104,6 +104,12 @@ Config is stored in `~/.delega/config.json`:
|
|
|
104
104
|
|
|
105
105
|
Environment variables take precedence over the config file.
|
|
106
106
|
|
|
107
|
+
## Security Notes
|
|
108
|
+
|
|
109
|
+
- `delega login` now hides API key input instead of echoing it back to the terminal.
|
|
110
|
+
- `~/.delega/config.json` is written with owner-only permissions (`0600`), and the config directory is locked to `0700`.
|
|
111
|
+
- Remote API URLs must use `https://`; plain `http://` is only accepted for `localhost` / `127.0.0.1`.
|
|
112
|
+
|
|
107
113
|
## License
|
|
108
114
|
|
|
109
115
|
MIT
|
package/dist/api.js
CHANGED
|
@@ -5,7 +5,16 @@ export async function apiCall(method, path, body) {
|
|
|
5
5
|
console.error("Not authenticated. Run: delega login");
|
|
6
6
|
process.exit(1);
|
|
7
7
|
}
|
|
8
|
-
|
|
8
|
+
let apiBase;
|
|
9
|
+
try {
|
|
10
|
+
apiBase = getApiUrl();
|
|
11
|
+
}
|
|
12
|
+
catch (err) {
|
|
13
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
14
|
+
console.error(`Configuration error: ${msg}`);
|
|
15
|
+
process.exit(1);
|
|
16
|
+
}
|
|
17
|
+
const url = apiBase + path;
|
|
9
18
|
const headers = {
|
|
10
19
|
"X-Agent-Key": apiKey,
|
|
11
20
|
"Content-Type": "application/json",
|
package/dist/commands/login.js
CHANGED
|
@@ -1,15 +1,26 @@
|
|
|
1
1
|
import { Command } from "commander";
|
|
2
2
|
import node_readline from "node:readline";
|
|
3
|
-
import { saveConfig, loadConfig } from "../config.js";
|
|
3
|
+
import { saveConfig, loadConfig, normalizeApiUrl } from "../config.js";
|
|
4
4
|
import { printBanner } from "../ui.js";
|
|
5
|
-
async function
|
|
5
|
+
async function promptSecret(question) {
|
|
6
|
+
const mutedOutput = {
|
|
7
|
+
muted: false,
|
|
8
|
+
write(chunk) {
|
|
9
|
+
if (!this.muted || chunk.includes(question)) {
|
|
10
|
+
process.stdout.write(chunk);
|
|
11
|
+
}
|
|
12
|
+
},
|
|
13
|
+
};
|
|
6
14
|
const rl = node_readline.createInterface({
|
|
7
15
|
input: process.stdin,
|
|
8
|
-
output:
|
|
16
|
+
output: mutedOutput,
|
|
17
|
+
terminal: true,
|
|
9
18
|
});
|
|
19
|
+
mutedOutput.muted = true;
|
|
10
20
|
return new Promise((resolve) => {
|
|
11
21
|
rl.question(question, (answer) => {
|
|
12
22
|
rl.close();
|
|
23
|
+
process.stdout.write("\n");
|
|
13
24
|
resolve(answer.trim());
|
|
14
25
|
});
|
|
15
26
|
});
|
|
@@ -18,7 +29,7 @@ export const loginCommand = new Command("login")
|
|
|
18
29
|
.description("Authenticate with the Delega API")
|
|
19
30
|
.action(async () => {
|
|
20
31
|
printBanner();
|
|
21
|
-
const key = await
|
|
32
|
+
const key = await promptSecret("Enter your API key (starts with dlg_): ");
|
|
22
33
|
if (!key) {
|
|
23
34
|
console.error("No key provided.");
|
|
24
35
|
process.exit(1);
|
|
@@ -29,10 +40,18 @@ export const loginCommand = new Command("login")
|
|
|
29
40
|
}
|
|
30
41
|
// Validate by calling the API
|
|
31
42
|
const config = loadConfig();
|
|
32
|
-
|
|
43
|
+
let apiUrl;
|
|
44
|
+
try {
|
|
45
|
+
apiUrl = normalizeApiUrl(config.api_url || process.env.DELEGA_API_URL || "https://api.delega.dev");
|
|
46
|
+
}
|
|
47
|
+
catch (err) {
|
|
48
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
49
|
+
console.error(`Configuration error: ${msg}`);
|
|
50
|
+
process.exit(1);
|
|
51
|
+
}
|
|
33
52
|
let res;
|
|
34
53
|
try {
|
|
35
|
-
res = await fetch(`${apiUrl}/v1/
|
|
54
|
+
res = await fetch(`${apiUrl}/v1/agent/me`, {
|
|
36
55
|
headers: {
|
|
37
56
|
"X-Agent-Key": key,
|
|
38
57
|
"Content-Type": "application/json",
|
|
@@ -51,11 +70,8 @@ export const loginCommand = new Command("login")
|
|
|
51
70
|
let agentName = "agent";
|
|
52
71
|
try {
|
|
53
72
|
const data = (await res.json());
|
|
54
|
-
if (
|
|
55
|
-
agentName = data
|
|
56
|
-
}
|
|
57
|
-
else if (!Array.isArray(data) && data.name) {
|
|
58
|
-
agentName = data.display_name || data.name;
|
|
73
|
+
if (data.agent?.name) {
|
|
74
|
+
agentName = data.agent.display_name || data.agent.name;
|
|
59
75
|
}
|
|
60
76
|
}
|
|
61
77
|
catch {
|
package/dist/config.d.ts
CHANGED
|
@@ -4,5 +4,6 @@ export interface DelegaConfig {
|
|
|
4
4
|
}
|
|
5
5
|
export declare function loadConfig(): DelegaConfig;
|
|
6
6
|
export declare function saveConfig(config: DelegaConfig): void;
|
|
7
|
+
export declare function normalizeApiUrl(rawUrl: string): string;
|
|
7
8
|
export declare function getApiKey(): string | undefined;
|
|
8
9
|
export declare function getApiUrl(): string;
|
package/dist/config.js
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import node_fs from "node:fs";
|
|
2
2
|
import node_path from "node:path";
|
|
3
3
|
import node_os from "node:os";
|
|
4
|
+
const LOCAL_API_HOSTS = new Set(["localhost", "127.0.0.1"]);
|
|
4
5
|
function getConfigDir() {
|
|
5
6
|
return node_path.join(node_os.homedir(), ".delega");
|
|
6
7
|
}
|
|
@@ -23,15 +24,30 @@ export function loadConfig() {
|
|
|
23
24
|
export function saveConfig(config) {
|
|
24
25
|
const configDir = getConfigDir();
|
|
25
26
|
if (!node_fs.existsSync(configDir)) {
|
|
26
|
-
node_fs.mkdirSync(configDir, { recursive: true });
|
|
27
|
+
node_fs.mkdirSync(configDir, { recursive: true, mode: 0o700 });
|
|
27
28
|
}
|
|
28
|
-
node_fs.
|
|
29
|
+
node_fs.chmodSync(configDir, 0o700);
|
|
30
|
+
node_fs.writeFileSync(getConfigPath(), JSON.stringify(config, null, 2) + "\n", { encoding: "utf-8", mode: 0o600 });
|
|
31
|
+
node_fs.chmodSync(getConfigPath(), 0o600);
|
|
32
|
+
}
|
|
33
|
+
export function normalizeApiUrl(rawUrl) {
|
|
34
|
+
let parsed;
|
|
35
|
+
try {
|
|
36
|
+
parsed = new URL(rawUrl);
|
|
37
|
+
}
|
|
38
|
+
catch {
|
|
39
|
+
throw new Error("Invalid Delega API URL");
|
|
40
|
+
}
|
|
41
|
+
if (parsed.protocol !== "https:" && !LOCAL_API_HOSTS.has(parsed.hostname)) {
|
|
42
|
+
throw new Error("Delega API URL must use HTTPS unless it points to localhost");
|
|
43
|
+
}
|
|
44
|
+
return rawUrl.replace(/\/+$/, "");
|
|
29
45
|
}
|
|
30
46
|
export function getApiKey() {
|
|
31
47
|
return process.env.DELEGA_API_KEY || loadConfig().api_key;
|
|
32
48
|
}
|
|
33
49
|
export function getApiUrl() {
|
|
34
|
-
return (process.env.DELEGA_API_URL ||
|
|
50
|
+
return normalizeApiUrl(process.env.DELEGA_API_URL ||
|
|
35
51
|
loadConfig().api_url ||
|
|
36
52
|
"https://api.delega.dev");
|
|
37
53
|
}
|
package/package.json
CHANGED
package/src/api.ts
CHANGED
|
@@ -16,7 +16,16 @@ export async function apiCall<T = unknown>(
|
|
|
16
16
|
process.exit(1);
|
|
17
17
|
}
|
|
18
18
|
|
|
19
|
-
|
|
19
|
+
let apiBase: string;
|
|
20
|
+
try {
|
|
21
|
+
apiBase = getApiUrl();
|
|
22
|
+
} catch (err) {
|
|
23
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
24
|
+
console.error(`Configuration error: ${msg}`);
|
|
25
|
+
process.exit(1);
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
const url = apiBase + path;
|
|
20
29
|
|
|
21
30
|
const headers: Record<string, string> = {
|
|
22
31
|
"X-Agent-Key": apiKey,
|
package/src/commands/login.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { Command } from "commander";
|
|
2
2
|
import node_readline from "node:readline";
|
|
3
|
-
import { saveConfig, loadConfig } from "../config.js";
|
|
3
|
+
import { saveConfig, loadConfig, normalizeApiUrl } from "../config.js";
|
|
4
4
|
import { printBanner } from "../ui.js";
|
|
5
5
|
|
|
6
6
|
interface Agent {
|
|
@@ -9,14 +9,28 @@ interface Agent {
|
|
|
9
9
|
display_name?: string;
|
|
10
10
|
}
|
|
11
11
|
|
|
12
|
-
async function
|
|
12
|
+
async function promptSecret(question: string): Promise<string> {
|
|
13
|
+
const mutedOutput = {
|
|
14
|
+
muted: false,
|
|
15
|
+
write(chunk: string) {
|
|
16
|
+
if (!this.muted || chunk.includes(question)) {
|
|
17
|
+
process.stdout.write(chunk);
|
|
18
|
+
}
|
|
19
|
+
},
|
|
20
|
+
};
|
|
21
|
+
|
|
13
22
|
const rl = node_readline.createInterface({
|
|
14
23
|
input: process.stdin,
|
|
15
|
-
output:
|
|
24
|
+
output: mutedOutput as unknown as NodeJS.WritableStream,
|
|
25
|
+
terminal: true,
|
|
16
26
|
});
|
|
27
|
+
|
|
28
|
+
mutedOutput.muted = true;
|
|
29
|
+
|
|
17
30
|
return new Promise((resolve) => {
|
|
18
31
|
rl.question(question, (answer) => {
|
|
19
32
|
rl.close();
|
|
33
|
+
process.stdout.write("\n");
|
|
20
34
|
resolve(answer.trim());
|
|
21
35
|
});
|
|
22
36
|
});
|
|
@@ -27,7 +41,7 @@ export const loginCommand = new Command("login")
|
|
|
27
41
|
.action(async () => {
|
|
28
42
|
printBanner();
|
|
29
43
|
|
|
30
|
-
const key = await
|
|
44
|
+
const key = await promptSecret("Enter your API key (starts with dlg_): ");
|
|
31
45
|
|
|
32
46
|
if (!key) {
|
|
33
47
|
console.error("No key provided.");
|
|
@@ -41,11 +55,20 @@ export const loginCommand = new Command("login")
|
|
|
41
55
|
|
|
42
56
|
// Validate by calling the API
|
|
43
57
|
const config = loadConfig();
|
|
44
|
-
|
|
58
|
+
let apiUrl: string;
|
|
59
|
+
try {
|
|
60
|
+
apiUrl = normalizeApiUrl(
|
|
61
|
+
config.api_url || process.env.DELEGA_API_URL || "https://api.delega.dev",
|
|
62
|
+
);
|
|
63
|
+
} catch (err) {
|
|
64
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
65
|
+
console.error(`Configuration error: ${msg}`);
|
|
66
|
+
process.exit(1);
|
|
67
|
+
}
|
|
45
68
|
|
|
46
69
|
let res: Response;
|
|
47
70
|
try {
|
|
48
|
-
res = await fetch(`${apiUrl}/v1/
|
|
71
|
+
res = await fetch(`${apiUrl}/v1/agent/me`, {
|
|
49
72
|
headers: {
|
|
50
73
|
"X-Agent-Key": key,
|
|
51
74
|
"Content-Type": "application/json",
|
|
@@ -64,11 +87,9 @@ export const loginCommand = new Command("login")
|
|
|
64
87
|
|
|
65
88
|
let agentName = "agent";
|
|
66
89
|
try {
|
|
67
|
-
const data = (await res.json()) as
|
|
68
|
-
if (
|
|
69
|
-
agentName = data
|
|
70
|
-
} else if (!Array.isArray(data) && data.name) {
|
|
71
|
-
agentName = data.display_name || data.name;
|
|
90
|
+
const data = (await res.json()) as { agent?: Agent };
|
|
91
|
+
if (data.agent?.name) {
|
|
92
|
+
agentName = data.agent.display_name || data.agent.name;
|
|
72
93
|
}
|
|
73
94
|
} catch {
|
|
74
95
|
// Proceed with default name
|
package/src/config.ts
CHANGED
|
@@ -7,6 +7,8 @@ export interface DelegaConfig {
|
|
|
7
7
|
api_url?: string;
|
|
8
8
|
}
|
|
9
9
|
|
|
10
|
+
const LOCAL_API_HOSTS = new Set(["localhost", "127.0.0.1"]);
|
|
11
|
+
|
|
10
12
|
function getConfigDir(): string {
|
|
11
13
|
return node_path.join(node_os.homedir(), ".delega");
|
|
12
14
|
}
|
|
@@ -31,13 +33,30 @@ export function loadConfig(): DelegaConfig {
|
|
|
31
33
|
export function saveConfig(config: DelegaConfig): void {
|
|
32
34
|
const configDir = getConfigDir();
|
|
33
35
|
if (!node_fs.existsSync(configDir)) {
|
|
34
|
-
node_fs.mkdirSync(configDir, { recursive: true });
|
|
36
|
+
node_fs.mkdirSync(configDir, { recursive: true, mode: 0o700 });
|
|
35
37
|
}
|
|
38
|
+
node_fs.chmodSync(configDir, 0o700);
|
|
36
39
|
node_fs.writeFileSync(
|
|
37
40
|
getConfigPath(),
|
|
38
41
|
JSON.stringify(config, null, 2) + "\n",
|
|
39
|
-
"utf-8",
|
|
42
|
+
{ encoding: "utf-8", mode: 0o600 },
|
|
40
43
|
);
|
|
44
|
+
node_fs.chmodSync(getConfigPath(), 0o600);
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
export function normalizeApiUrl(rawUrl: string): string {
|
|
48
|
+
let parsed: URL;
|
|
49
|
+
try {
|
|
50
|
+
parsed = new URL(rawUrl);
|
|
51
|
+
} catch {
|
|
52
|
+
throw new Error("Invalid Delega API URL");
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
if (parsed.protocol !== "https:" && !LOCAL_API_HOSTS.has(parsed.hostname)) {
|
|
56
|
+
throw new Error("Delega API URL must use HTTPS unless it points to localhost");
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
return rawUrl.replace(/\/+$/, "");
|
|
41
60
|
}
|
|
42
61
|
|
|
43
62
|
export function getApiKey(): string | undefined {
|
|
@@ -45,7 +64,7 @@ export function getApiKey(): string | undefined {
|
|
|
45
64
|
}
|
|
46
65
|
|
|
47
66
|
export function getApiUrl(): string {
|
|
48
|
-
return (
|
|
67
|
+
return normalizeApiUrl(
|
|
49
68
|
process.env.DELEGA_API_URL ||
|
|
50
69
|
loadConfig().api_url ||
|
|
51
70
|
"https://api.delega.dev"
|