@jango-blockchained/hoox-cli 0.3.4
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 +403 -0
- package/bin/hoox.js +12 -0
- package/package.json +60 -0
- package/src/commands/check/check-command.test.ts +468 -0
- package/src/commands/check/check-command.ts +1144 -0
- package/src/commands/check/index.ts +10 -0
- package/src/commands/check/prerequisites-command.test.ts +19 -0
- package/src/commands/check/prerequisites-command.ts +92 -0
- package/src/commands/check/types.ts +103 -0
- package/src/commands/clone/clone-command.test.ts +442 -0
- package/src/commands/clone/clone-command.ts +440 -0
- package/src/commands/clone/index.ts +1 -0
- package/src/commands/config/config-command.test.ts +583 -0
- package/src/commands/config/config-command.ts +901 -0
- package/src/commands/config/env-command.test.ts +43 -0
- package/src/commands/config/env-command.ts +314 -0
- package/src/commands/config/index.ts +3 -0
- package/src/commands/config/kv-command.test.ts +14 -0
- package/src/commands/config/kv-command.ts +329 -0
- package/src/commands/dashboard/dashboard-command.test.ts +47 -0
- package/src/commands/dashboard/dashboard-command.ts +127 -0
- package/src/commands/dashboard/index.ts +1 -0
- package/src/commands/db/db-command.test.ts +21 -0
- package/src/commands/db/db-command.ts +314 -0
- package/src/commands/db/index.ts +1 -0
- package/src/commands/deploy/deploy-command.test.ts +304 -0
- package/src/commands/deploy/deploy-command.ts +1053 -0
- package/src/commands/deploy/index.ts +2 -0
- package/src/commands/deploy/telegram-service.ts +61 -0
- package/src/commands/deploy/types.ts +34 -0
- package/src/commands/dev/dev-command.test.ts +383 -0
- package/src/commands/dev/dev-command.ts +407 -0
- package/src/commands/dev/index.ts +1 -0
- package/src/commands/infra/index.ts +5 -0
- package/src/commands/infra/infra-command.test.ts +719 -0
- package/src/commands/infra/infra-command.ts +940 -0
- package/src/commands/infra/types.ts +23 -0
- package/src/commands/init/index.ts +1 -0
- package/src/commands/init/init-command.test.ts +827 -0
- package/src/commands/init/init-command.ts +627 -0
- package/src/commands/init/types.ts +185 -0
- package/src/commands/monitor/index.ts +2 -0
- package/src/commands/monitor/monitor-command.test.ts +235 -0
- package/src/commands/monitor/monitor-command.ts +245 -0
- package/src/commands/monitor/monitor-service.ts +50 -0
- package/src/commands/monitor/types.ts +13 -0
- package/src/commands/repair/index.ts +2 -0
- package/src/commands/repair/repair-command.test.ts +204 -0
- package/src/commands/repair/repair-command.ts +199 -0
- package/src/commands/repair/repair-service.ts +102 -0
- package/src/commands/repair/types.ts +13 -0
- package/src/commands/test/index.ts +2 -0
- package/src/commands/test/test-command.test.ts +319 -0
- package/src/commands/test/test-command.ts +412 -0
- package/src/commands/waf/index.ts +2 -0
- package/src/commands/waf/types.ts +48 -0
- package/src/commands/waf/waf-command.test.ts +506 -0
- package/src/commands/waf/waf-command.ts +548 -0
- package/src/index.ts +198 -0
- package/src/services/cloudflare/cloudflare-service.test.ts +654 -0
- package/src/services/cloudflare/cloudflare-service.ts +435 -0
- package/src/services/cloudflare/index.ts +2 -0
- package/src/services/cloudflare/types.ts +29 -0
- package/src/services/config/config-service.test.ts +395 -0
- package/src/services/config/config-service.ts +207 -0
- package/src/services/config/index.ts +9 -0
- package/src/services/config/types.ts +66 -0
- package/src/services/db/db-service.test.ts +51 -0
- package/src/services/db/db-service.ts +140 -0
- package/src/services/db/index.ts +1 -0
- package/src/services/docker/docker-service.ts +155 -0
- package/src/services/docker/index.ts +1 -0
- package/src/services/env/env-service.test.ts +210 -0
- package/src/services/env/env-service.ts +156 -0
- package/src/services/env/index.ts +1 -0
- package/src/services/kv/index.ts +1 -0
- package/src/services/kv/kv-sync-service.test.ts +38 -0
- package/src/services/kv/kv-sync-service.ts +151 -0
- package/src/services/prerequisites/index.ts +1 -0
- package/src/services/prerequisites/prerequisites-service.test.ts +89 -0
- package/src/services/prerequisites/prerequisites-service.ts +269 -0
- package/src/services/prerequisites/types.ts +48 -0
- package/src/services/secrets/index.ts +12 -0
- package/src/services/secrets/secrets-service.test.ts +486 -0
- package/src/services/secrets/secrets-service.ts +293 -0
- package/src/services/secrets/types.ts +57 -0
- package/src/ui/banner.ts +52 -0
- package/src/ui/index.ts +8 -0
- package/src/ui/menu.ts +473 -0
- package/src/utils/errors.test.ts +69 -0
- package/src/utils/errors.ts +23 -0
- package/src/utils/formatters.test.ts +180 -0
- package/src/utils/formatters.ts +252 -0
- package/src/utils/theme.ts +94 -0
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
import { describe, expect, it } from "bun:test";
|
|
2
|
+
import { DbService } from "./db-service.js";
|
|
3
|
+
|
|
4
|
+
describe("DbService", () => {
|
|
5
|
+
describe("resolveDbName", () => {
|
|
6
|
+
it("uses explicit name when provided", async () => {
|
|
7
|
+
const svc = new DbService();
|
|
8
|
+
const name = await svc.resolveDbName("my-explicit-db");
|
|
9
|
+
expect(name).toBe("my-explicit-db");
|
|
10
|
+
});
|
|
11
|
+
|
|
12
|
+
it("falls back to default when no config exists", async () => {
|
|
13
|
+
const svc = new DbService();
|
|
14
|
+
const name = await svc.resolveDbName();
|
|
15
|
+
expect(name).toBe("my-database");
|
|
16
|
+
});
|
|
17
|
+
});
|
|
18
|
+
|
|
19
|
+
describe("parseTableNames", () => {
|
|
20
|
+
it("extracts table names from wrangler JSON output", () => {
|
|
21
|
+
const output = JSON.stringify([
|
|
22
|
+
{
|
|
23
|
+
results: [
|
|
24
|
+
{ name: "trade_signals" },
|
|
25
|
+
{ name: "trades" },
|
|
26
|
+
{ name: "positions" },
|
|
27
|
+
],
|
|
28
|
+
},
|
|
29
|
+
]);
|
|
30
|
+
const tables = DbService.parseTableNames(output);
|
|
31
|
+
expect(tables).toContain("trade_signals");
|
|
32
|
+
expect(tables).toContain("trades");
|
|
33
|
+
expect(tables).toContain("positions");
|
|
34
|
+
expect(tables.length).toBe(3);
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
it("handles empty results", () => {
|
|
38
|
+
const output = JSON.stringify([{ results: [] }]);
|
|
39
|
+
const tables = DbService.parseTableNames(output);
|
|
40
|
+
expect(tables.length).toBe(0);
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
it("handles non-JSON output as text lines", () => {
|
|
44
|
+
const output = "table1\ntable2\ntable3\n";
|
|
45
|
+
const tables = DbService.parseTableNames(output);
|
|
46
|
+
expect(tables).toContain("table1");
|
|
47
|
+
expect(tables).toContain("table2");
|
|
48
|
+
expect(tables).toContain("table3");
|
|
49
|
+
});
|
|
50
|
+
});
|
|
51
|
+
});
|
|
@@ -0,0 +1,140 @@
|
|
|
1
|
+
import { ConfigService } from "../config/index.js";
|
|
2
|
+
|
|
3
|
+
export interface QueryResult {
|
|
4
|
+
columns: string[];
|
|
5
|
+
rows: Record<string, unknown>[];
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
export class DbService {
|
|
9
|
+
private configService: ConfigService;
|
|
10
|
+
|
|
11
|
+
constructor(configService?: ConfigService) {
|
|
12
|
+
this.configService = configService ?? new ConfigService();
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
async resolveDbName(dbName?: string): Promise<string> {
|
|
16
|
+
if (dbName) return dbName;
|
|
17
|
+
try {
|
|
18
|
+
await this.configService.load();
|
|
19
|
+
const d1Worker = this.configService.getWorker("d1-worker");
|
|
20
|
+
const vars = d1Worker?.vars as Record<string, string> | undefined;
|
|
21
|
+
if (vars?.database_name) return vars.database_name;
|
|
22
|
+
} catch {
|
|
23
|
+
// Config not found — use default
|
|
24
|
+
}
|
|
25
|
+
return "my-database";
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
async apply(
|
|
29
|
+
dbName: string,
|
|
30
|
+
remote: boolean,
|
|
31
|
+
schemaPath?: string,
|
|
32
|
+
): Promise<string> {
|
|
33
|
+
const path = schemaPath ?? "workers/trade-worker/schema.sql";
|
|
34
|
+
const args = ["d1", "execute", dbName, "--file", path];
|
|
35
|
+
if (remote) args.push("--remote");
|
|
36
|
+
return await this.runWrangler(args);
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
async migrate(dbName: string, remote: boolean): Promise<string> {
|
|
40
|
+
const migrationSql = await this.readMigrationSql();
|
|
41
|
+
const args = ["d1", "execute", dbName, "--command", migrationSql];
|
|
42
|
+
if (remote) args.push("--remote");
|
|
43
|
+
return await this.runWrangler(args);
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
async listTables(dbName: string, remote: boolean): Promise<string[]> {
|
|
47
|
+
const args = [
|
|
48
|
+
"d1",
|
|
49
|
+
"execute",
|
|
50
|
+
dbName,
|
|
51
|
+
"--command",
|
|
52
|
+
"SELECT name FROM sqlite_master WHERE type='table' ORDER BY name",
|
|
53
|
+
"--json",
|
|
54
|
+
];
|
|
55
|
+
if (remote) args.push("--remote");
|
|
56
|
+
|
|
57
|
+
const output = await this.runWrangler(args);
|
|
58
|
+
return DbService.parseTableNames(output);
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
async query(
|
|
62
|
+
dbName: string,
|
|
63
|
+
sql: string,
|
|
64
|
+
remote: boolean,
|
|
65
|
+
): Promise<string> {
|
|
66
|
+
const args = ["d1", "execute", dbName, "--command", sql, "--json"];
|
|
67
|
+
if (remote) args.push("--remote");
|
|
68
|
+
return await this.runWrangler(args);
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
async export(
|
|
72
|
+
dbName: string,
|
|
73
|
+
outputPath?: string,
|
|
74
|
+
): Promise<string> {
|
|
75
|
+
const outPath =
|
|
76
|
+
outputPath ?? `backup-${new Date().toISOString().slice(0, 10)}.sql`;
|
|
77
|
+
const args = ["d1", "export", dbName, "--output", outPath, "--remote"];
|
|
78
|
+
await this.runWrangler(args);
|
|
79
|
+
return outPath;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
async reset(dbName: string): Promise<string> {
|
|
83
|
+
await this.runWrangler(["d1", "delete", dbName, "-y"]);
|
|
84
|
+
return await this.runWrangler(["d1", "create", dbName]);
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
static parseTableNames(output: string): string[] {
|
|
88
|
+
try {
|
|
89
|
+
const parsed = JSON.parse(output);
|
|
90
|
+
if (Array.isArray(parsed) && parsed.length > 0) {
|
|
91
|
+
const first = parsed[0];
|
|
92
|
+
if (first.results && Array.isArray(first.results)) {
|
|
93
|
+
return first.results.map(
|
|
94
|
+
(r: Record<string, unknown>) => String(r.name ?? ""),
|
|
95
|
+
).filter(Boolean);
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
} catch {
|
|
99
|
+
// Not JSON — try text parse
|
|
100
|
+
}
|
|
101
|
+
return output
|
|
102
|
+
.split("\n")
|
|
103
|
+
.map((l) => l.trim())
|
|
104
|
+
.filter(Boolean);
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
private async runWrangler(args: string[]): Promise<string> {
|
|
108
|
+
const proc = Bun.spawn(["wrangler", ...args], {
|
|
109
|
+
stdout: "pipe",
|
|
110
|
+
stderr: "pipe",
|
|
111
|
+
});
|
|
112
|
+
const stdout = await new Response(proc.stdout).text();
|
|
113
|
+
const stderr = await new Response(proc.stderr).text();
|
|
114
|
+
const exitCode = await proc.exited;
|
|
115
|
+
|
|
116
|
+
if (exitCode !== 0) {
|
|
117
|
+
throw new Error(
|
|
118
|
+
stderr.trim() || `wrangler exited with code ${exitCode}`,
|
|
119
|
+
);
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
return stdout.trim();
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
private async readMigrationSql(): Promise<string> {
|
|
126
|
+
try {
|
|
127
|
+
const file = Bun.file("scripts/migrate-tracking.sh");
|
|
128
|
+
if (await file.exists()) {
|
|
129
|
+
const content = await file.text();
|
|
130
|
+
const match = content.match(
|
|
131
|
+
/d1\s+execute\s+\S+\s+--command=["'](.+?)["']/s,
|
|
132
|
+
);
|
|
133
|
+
if (match) return match[1];
|
|
134
|
+
}
|
|
135
|
+
} catch {
|
|
136
|
+
// Fall through
|
|
137
|
+
}
|
|
138
|
+
return "SELECT 1";
|
|
139
|
+
}
|
|
140
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { DbService } from "./db-service.js";
|
|
@@ -0,0 +1,155 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* DockerService wraps Docker Compose operations for local development.
|
|
3
|
+
*
|
|
4
|
+
* Provides availability checks, compose file detection, and compose up/down.
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
interface DockerAvailability {
|
|
8
|
+
docker: boolean;
|
|
9
|
+
compose: boolean;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
interface ComposeResult {
|
|
13
|
+
ok: boolean;
|
|
14
|
+
error?: string;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* Resolved path to docker-compose.yml in the project root.
|
|
19
|
+
*/
|
|
20
|
+
const COMPOSE_FILE = "docker-compose.yml";
|
|
21
|
+
|
|
22
|
+
export class DockerService {
|
|
23
|
+
private readonly cwd: string;
|
|
24
|
+
|
|
25
|
+
constructor(cwd?: string) {
|
|
26
|
+
this.cwd = cwd ?? process.cwd();
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* Check if Docker and docker-compose are available in PATH.
|
|
31
|
+
*/
|
|
32
|
+
async checkAvailability(): Promise<DockerAvailability> {
|
|
33
|
+
const [dockerOk, composeOk] = await Promise.all([
|
|
34
|
+
this.isCommandAvailable("docker"),
|
|
35
|
+
this.isCommandAvailable("docker compose"),
|
|
36
|
+
]);
|
|
37
|
+
|
|
38
|
+
return { docker: dockerOk, compose: composeOk };
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* Check if docker-compose.yml exists in the project root.
|
|
43
|
+
*/
|
|
44
|
+
async composeFileExists(): Promise<boolean> {
|
|
45
|
+
const file = Bun.file(
|
|
46
|
+
this.cwd.endsWith("/")
|
|
47
|
+
? `${this.cwd}${COMPOSE_FILE}`
|
|
48
|
+
: `${this.cwd}/${COMPOSE_FILE}`
|
|
49
|
+
);
|
|
50
|
+
return file.exists();
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/**
|
|
54
|
+
* Run `docker compose up` for the given profiles.
|
|
55
|
+
*
|
|
56
|
+
* @param profiles - Compose profiles to activate (e.g. ["workers", "dashboard"]).
|
|
57
|
+
* @param detached - Run in detached mode (default false, inherits stdout/stderr).
|
|
58
|
+
* @returns ComposeResult with ok=true on success.
|
|
59
|
+
*/
|
|
60
|
+
async composeUp(
|
|
61
|
+
profiles: string[],
|
|
62
|
+
detached = false
|
|
63
|
+
): Promise<ComposeResult> {
|
|
64
|
+
const args = ["compose", "up", "--profile", ...profiles];
|
|
65
|
+
|
|
66
|
+
if (detached) {
|
|
67
|
+
args.push("-d");
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
try {
|
|
71
|
+
const proc = Bun.spawn(["docker", ...args], {
|
|
72
|
+
cwd: this.cwd,
|
|
73
|
+
stdout: detached ? "pipe" : "inherit",
|
|
74
|
+
stderr: "inherit",
|
|
75
|
+
stdin: "pipe",
|
|
76
|
+
});
|
|
77
|
+
|
|
78
|
+
if (!detached) {
|
|
79
|
+
// For non-detached, inherit stdout/stderr — user sees real-time output.
|
|
80
|
+
// Wait for the process to exit naturally (Ctrl+C or completion).
|
|
81
|
+
const exitCode = await proc.exited;
|
|
82
|
+
|
|
83
|
+
if (exitCode === 0) {
|
|
84
|
+
return { ok: true };
|
|
85
|
+
}
|
|
86
|
+
return {
|
|
87
|
+
ok: false,
|
|
88
|
+
error: `docker compose exited with code ${exitCode}`,
|
|
89
|
+
};
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
// Detached mode: capture output
|
|
93
|
+
const stdout = await new Response(proc.stdout).text();
|
|
94
|
+
const exitCode = await proc.exited;
|
|
95
|
+
|
|
96
|
+
if (exitCode === 0) {
|
|
97
|
+
return { ok: true };
|
|
98
|
+
}
|
|
99
|
+
return {
|
|
100
|
+
ok: false,
|
|
101
|
+
error: stdout.trim() || `docker compose exited with code ${exitCode}`,
|
|
102
|
+
};
|
|
103
|
+
} catch (err) {
|
|
104
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
105
|
+
return { ok: false, error: `Failed to run docker compose: ${message}` };
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
/**
|
|
110
|
+
* Run `docker compose down` to stop running containers.
|
|
111
|
+
*/
|
|
112
|
+
async composeDown(): Promise<ComposeResult> {
|
|
113
|
+
try {
|
|
114
|
+
const proc = Bun.spawn(["docker", "compose", "down"], {
|
|
115
|
+
cwd: this.cwd,
|
|
116
|
+
stdout: "pipe",
|
|
117
|
+
stderr: "pipe",
|
|
118
|
+
});
|
|
119
|
+
|
|
120
|
+
const stderr = await new Response(proc.stderr).text();
|
|
121
|
+
const exitCode = await proc.exited;
|
|
122
|
+
|
|
123
|
+
if (exitCode === 0) {
|
|
124
|
+
return { ok: true };
|
|
125
|
+
}
|
|
126
|
+
return {
|
|
127
|
+
ok: false,
|
|
128
|
+
error:
|
|
129
|
+
stderr.trim() || `docker compose down exited with code ${exitCode}`,
|
|
130
|
+
};
|
|
131
|
+
} catch (err) {
|
|
132
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
133
|
+
return {
|
|
134
|
+
ok: false,
|
|
135
|
+
error: `Failed to run docker compose down: ${message}`,
|
|
136
|
+
};
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
/**
|
|
141
|
+
* Probe PATH for a command's availability without throwing.
|
|
142
|
+
*/
|
|
143
|
+
private isCommandAvailable(cmd: string): Promise<boolean> {
|
|
144
|
+
return new Promise((resolve) => {
|
|
145
|
+
const proc = Bun.spawn(["which", cmd.split(" ")[0]!], {
|
|
146
|
+
stdout: "pipe",
|
|
147
|
+
stderr: "pipe",
|
|
148
|
+
});
|
|
149
|
+
|
|
150
|
+
proc.exited
|
|
151
|
+
.then((code) => resolve(code === 0))
|
|
152
|
+
.catch(() => resolve(false));
|
|
153
|
+
});
|
|
154
|
+
}
|
|
155
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { DockerService } from "./docker-service.js";
|
|
@@ -0,0 +1,210 @@
|
|
|
1
|
+
import { describe, expect, it } from "bun:test";
|
|
2
|
+
import { EnvService } from "./env-service.js";
|
|
3
|
+
|
|
4
|
+
describe("EnvService", () => {
|
|
5
|
+
describe("getDefinitions", () => {
|
|
6
|
+
it("returns all known env var definitions", () => {
|
|
7
|
+
const defs = EnvService.getDefinitions();
|
|
8
|
+
expect(defs.length).toBe(31);
|
|
9
|
+
expect(defs.some(d => d.name === "CLOUDFLARE_API_TOKEN")).toBe(true);
|
|
10
|
+
});
|
|
11
|
+
|
|
12
|
+
it("each definition has required fields", () => {
|
|
13
|
+
for (const def of EnvService.getDefinitions()) {
|
|
14
|
+
expect(def.name).toBeTruthy();
|
|
15
|
+
expect(typeof def.required).toBe("boolean");
|
|
16
|
+
expect(typeof def.secret).toBe("boolean");
|
|
17
|
+
expect(def.section).toBeTruthy();
|
|
18
|
+
}
|
|
19
|
+
});
|
|
20
|
+
});
|
|
21
|
+
|
|
22
|
+
describe("getSections", () => {
|
|
23
|
+
it("returns unique sections in order", () => {
|
|
24
|
+
const sections = EnvService.getSections();
|
|
25
|
+
expect(sections.length).toBeGreaterThanOrEqual(7);
|
|
26
|
+
expect(sections[0]).toBe("Cloudflare Account");
|
|
27
|
+
});
|
|
28
|
+
});
|
|
29
|
+
|
|
30
|
+
describe("generateEnvLocal", () => {
|
|
31
|
+
it("generates template with all vars", () => {
|
|
32
|
+
const content = EnvService.generateEnvLocal();
|
|
33
|
+
expect(content).toContain("CLOUDFLARE_API_TOKEN");
|
|
34
|
+
expect(content).toContain("NEVER commit this file");
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
it("includes provided values", () => {
|
|
38
|
+
const content = EnvService.generateEnvLocal({ SUBDOMAIN_PREFIX: "myapp" });
|
|
39
|
+
expect(content).toContain('SUBDOMAIN_PREFIX="myapp"');
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
it("uses defaults for missing vars", () => {
|
|
43
|
+
const content = EnvService.generateEnvLocal({});
|
|
44
|
+
expect(content).toContain('SUBDOMAIN_PREFIX="cryptolinx"');
|
|
45
|
+
});
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
describe("getWorkerDevVars", () => {
|
|
49
|
+
it("maps vars to correct workers", () => {
|
|
50
|
+
const vars = {
|
|
51
|
+
AGENT_OPENAI_KEY: "sk-123",
|
|
52
|
+
TELEGRAM_BOT_TOKEN: "tg-456",
|
|
53
|
+
D1_INTERNAL_KEY: "d1-789",
|
|
54
|
+
};
|
|
55
|
+
const result = EnvService.getWorkerDevVars(vars);
|
|
56
|
+
expect(result["workers/agent-worker"]).toBeDefined();
|
|
57
|
+
expect(result["workers/agent-worker"].AGENT_OPENAI_KEY).toBe("sk-123");
|
|
58
|
+
expect(result["workers/telegram-worker"]).toBeDefined();
|
|
59
|
+
expect(result["workers/telegram-worker"].TELEGRAM_BOT_TOKEN).toBe("tg-456");
|
|
60
|
+
});
|
|
61
|
+
|
|
62
|
+
it("omits workers with no matching vars", () => {
|
|
63
|
+
const result = EnvService.getWorkerDevVars({});
|
|
64
|
+
expect(Object.keys(result).length).toBe(0);
|
|
65
|
+
});
|
|
66
|
+
|
|
67
|
+
it("omits empty-string vars", () => {
|
|
68
|
+
const result = EnvService.getWorkerDevVars({ D1_INTERNAL_KEY: "" });
|
|
69
|
+
expect(Object.keys(result).length).toBe(0);
|
|
70
|
+
});
|
|
71
|
+
|
|
72
|
+
it("maps all newly added vars to correct workers", () => {
|
|
73
|
+
const vars = {
|
|
74
|
+
WEBHOOK_API_KEY_BINDING: "webhook-key",
|
|
75
|
+
HA_TOKEN_BINDING: "ha-token",
|
|
76
|
+
API_SERVICE_KEY: "api-key",
|
|
77
|
+
TELEGRAM_SECRET_TOKEN: "tg-secret",
|
|
78
|
+
WALLET_MNEMONIC_SECRET: "mnemonic",
|
|
79
|
+
WALLET_PK_SECRET: "pk",
|
|
80
|
+
EMAIL_HOST: "imap.example.com",
|
|
81
|
+
EMAIL_USER: "user",
|
|
82
|
+
EMAIL_PASS: "pass",
|
|
83
|
+
INTERNAL_KEY: "int-key",
|
|
84
|
+
};
|
|
85
|
+
const result = EnvService.getWorkerDevVars(vars);
|
|
86
|
+
expect(result["workers/hoox"]).toBeDefined();
|
|
87
|
+
expect(result["workers/hoox"].WEBHOOK_API_KEY_BINDING).toBe("webhook-key");
|
|
88
|
+
expect(result["workers/hoox"].HA_TOKEN_BINDING).toBe("ha-token");
|
|
89
|
+
expect(result["workers/trade-worker"].API_SERVICE_KEY).toBe("api-key");
|
|
90
|
+
expect(result["workers/telegram-worker"].TELEGRAM_SECRET_TOKEN).toBe("tg-secret");
|
|
91
|
+
expect(result["workers/web3-wallet-worker"]).toBeDefined();
|
|
92
|
+
expect(result["workers/web3-wallet-worker"].WALLET_MNEMONIC_SECRET).toBe("mnemonic");
|
|
93
|
+
expect(result["workers/email-worker"]).toBeDefined();
|
|
94
|
+
expect(result["workers/email-worker"].EMAIL_HOST).toBe("imap.example.com");
|
|
95
|
+
});
|
|
96
|
+
});
|
|
97
|
+
|
|
98
|
+
describe("validate", () => {
|
|
99
|
+
it("flags missing required vars", () => {
|
|
100
|
+
const result = EnvService.validate({});
|
|
101
|
+
expect(result.missing.length).toBeGreaterThan(0);
|
|
102
|
+
expect(result.missing).toContain("CLOUDFLARE_API_TOKEN");
|
|
103
|
+
});
|
|
104
|
+
|
|
105
|
+
it("flags 'your_' placeholder values as missing", () => {
|
|
106
|
+
const result = EnvService.validate({ CLOUDFLARE_API_TOKEN: "your_cloudflare_api_token" });
|
|
107
|
+
expect(result.missing).toContain("CLOUDFLARE_API_TOKEN");
|
|
108
|
+
});
|
|
109
|
+
|
|
110
|
+
it("flags 'generate_' placeholder values as missing", () => {
|
|
111
|
+
const result = EnvService.validate({ SESSION_SECRET: "generate_a_32_character_secure_random_string" });
|
|
112
|
+
expect(result.missing).toContain("SESSION_SECRET");
|
|
113
|
+
});
|
|
114
|
+
|
|
115
|
+
it("passes when all required vars are set with real values", () => {
|
|
116
|
+
const vars: Record<string, string> = {
|
|
117
|
+
CLOUDFLARE_API_TOKEN: "cfut_xxx",
|
|
118
|
+
CLOUDFLARE_ACCOUNT_ID: "abc123",
|
|
119
|
+
SUBDOMAIN_PREFIX: "myapp",
|
|
120
|
+
D1_INTERNAL_KEY: "d1-key",
|
|
121
|
+
TRADE_INTERNAL_KEY: "trade-key",
|
|
122
|
+
AGENT_INTERNAL_KEY: "agent-key",
|
|
123
|
+
WEBHOOK_API_KEY_BINDING: "webhook-key",
|
|
124
|
+
INTERNAL_KEY_BINDING: "int-key",
|
|
125
|
+
API_SERVICE_KEY: "api-key",
|
|
126
|
+
DASHBOARD_USER: "admin",
|
|
127
|
+
DASHBOARD_PASS: "pass123",
|
|
128
|
+
SESSION_SECRET: "a".repeat(32),
|
|
129
|
+
};
|
|
130
|
+
const result = EnvService.validate(vars);
|
|
131
|
+
expect(result.missing.length).toBe(0);
|
|
132
|
+
});
|
|
133
|
+
|
|
134
|
+
it("warns on short session secret", () => {
|
|
135
|
+
const vars: Record<string, string> = {
|
|
136
|
+
CLOUDFLARE_API_TOKEN: "tok",
|
|
137
|
+
CLOUDFLARE_ACCOUNT_ID: "id",
|
|
138
|
+
SUBDOMAIN_PREFIX: "p",
|
|
139
|
+
D1_INTERNAL_KEY: "k",
|
|
140
|
+
TRADE_INTERNAL_KEY: "k",
|
|
141
|
+
AGENT_INTERNAL_KEY: "k",
|
|
142
|
+
WEBHOOK_API_KEY_BINDING: "wk",
|
|
143
|
+
INTERNAL_KEY_BINDING: "ik",
|
|
144
|
+
API_SERVICE_KEY: "ak",
|
|
145
|
+
DASHBOARD_USER: "u",
|
|
146
|
+
DASHBOARD_PASS: "p",
|
|
147
|
+
SESSION_SECRET: "short",
|
|
148
|
+
};
|
|
149
|
+
const result = EnvService.validate(vars);
|
|
150
|
+
expect(result.warnings).toContain("SESSION_SECRET should be at least 32 characters");
|
|
151
|
+
});
|
|
152
|
+
});
|
|
153
|
+
|
|
154
|
+
describe("show", () => {
|
|
155
|
+
it("redacts secrets", () => {
|
|
156
|
+
const output = EnvService.show({ CLOUDFLARE_API_TOKEN: "secret123" });
|
|
157
|
+
expect(output).toContain("********");
|
|
158
|
+
expect(output).not.toContain("secret123");
|
|
159
|
+
});
|
|
160
|
+
|
|
161
|
+
it("shows non-secrets in plain text", () => {
|
|
162
|
+
const output = EnvService.show({ SUBDOMAIN_PREFIX: "myapp" });
|
|
163
|
+
expect(output).toContain("myapp");
|
|
164
|
+
});
|
|
165
|
+
});
|
|
166
|
+
|
|
167
|
+
describe("loadDotEnvAsync", () => {
|
|
168
|
+
it("returns empty object for missing file", async () => {
|
|
169
|
+
const result = await EnvService.loadDotEnvAsync("/tmp/nonexistent-file-12345.env");
|
|
170
|
+
expect(Object.keys(result).length).toBe(0);
|
|
171
|
+
});
|
|
172
|
+
|
|
173
|
+
it("parses simple key=value lines", async () => {
|
|
174
|
+
const filePath = "/tmp/test-simple-12345.env";
|
|
175
|
+
await Bun.write(filePath, 'KEY=value\nFOO=bar\n');
|
|
176
|
+
const result = await EnvService.loadDotEnvAsync(filePath);
|
|
177
|
+
expect(result.KEY).toBe("value");
|
|
178
|
+
expect(result.FOO).toBe("bar");
|
|
179
|
+
await Bun.write(filePath, ""); // cleanup
|
|
180
|
+
});
|
|
181
|
+
|
|
182
|
+
it("strips double quotes from values", async () => {
|
|
183
|
+
const filePath = "/tmp/test-quotes-12345.env";
|
|
184
|
+
await Bun.write(filePath, 'KEY="quoted value"\nNESTED="val with \\"quote\\""\n');
|
|
185
|
+
const result = await EnvService.loadDotEnvAsync(filePath);
|
|
186
|
+
// First quote-stripped value
|
|
187
|
+
expect(result.KEY).toBe("quoted value");
|
|
188
|
+
await Bun.write(filePath, "");
|
|
189
|
+
});
|
|
190
|
+
|
|
191
|
+
it("skips comments and blank lines", async () => {
|
|
192
|
+
const filePath = "/tmp/test-comments-12345.env";
|
|
193
|
+
await Bun.write(filePath, '# this is a comment\n\nKEY=val\n# another comment\nFOO=bar\n');
|
|
194
|
+
const result = await EnvService.loadDotEnvAsync(filePath);
|
|
195
|
+
expect(result.KEY).toBe("val");
|
|
196
|
+
expect(result.FOO).toBe("bar");
|
|
197
|
+
expect(Object.keys(result).length).toBe(2);
|
|
198
|
+
await Bun.write(filePath, "");
|
|
199
|
+
});
|
|
200
|
+
|
|
201
|
+
it("handles empty values", async () => {
|
|
202
|
+
const filePath = "/tmp/test-empty-12345.env";
|
|
203
|
+
await Bun.write(filePath, 'EMPTY=\nKEY=val\n');
|
|
204
|
+
const result = await EnvService.loadDotEnvAsync(filePath);
|
|
205
|
+
expect(result.EMPTY).toBe("");
|
|
206
|
+
expect(result.KEY).toBe("val");
|
|
207
|
+
await Bun.write(filePath, "");
|
|
208
|
+
});
|
|
209
|
+
});
|
|
210
|
+
});
|