@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.
Files changed (94) hide show
  1. package/README.md +403 -0
  2. package/bin/hoox.js +12 -0
  3. package/package.json +60 -0
  4. package/src/commands/check/check-command.test.ts +468 -0
  5. package/src/commands/check/check-command.ts +1144 -0
  6. package/src/commands/check/index.ts +10 -0
  7. package/src/commands/check/prerequisites-command.test.ts +19 -0
  8. package/src/commands/check/prerequisites-command.ts +92 -0
  9. package/src/commands/check/types.ts +103 -0
  10. package/src/commands/clone/clone-command.test.ts +442 -0
  11. package/src/commands/clone/clone-command.ts +440 -0
  12. package/src/commands/clone/index.ts +1 -0
  13. package/src/commands/config/config-command.test.ts +583 -0
  14. package/src/commands/config/config-command.ts +901 -0
  15. package/src/commands/config/env-command.test.ts +43 -0
  16. package/src/commands/config/env-command.ts +314 -0
  17. package/src/commands/config/index.ts +3 -0
  18. package/src/commands/config/kv-command.test.ts +14 -0
  19. package/src/commands/config/kv-command.ts +329 -0
  20. package/src/commands/dashboard/dashboard-command.test.ts +47 -0
  21. package/src/commands/dashboard/dashboard-command.ts +127 -0
  22. package/src/commands/dashboard/index.ts +1 -0
  23. package/src/commands/db/db-command.test.ts +21 -0
  24. package/src/commands/db/db-command.ts +314 -0
  25. package/src/commands/db/index.ts +1 -0
  26. package/src/commands/deploy/deploy-command.test.ts +304 -0
  27. package/src/commands/deploy/deploy-command.ts +1053 -0
  28. package/src/commands/deploy/index.ts +2 -0
  29. package/src/commands/deploy/telegram-service.ts +61 -0
  30. package/src/commands/deploy/types.ts +34 -0
  31. package/src/commands/dev/dev-command.test.ts +383 -0
  32. package/src/commands/dev/dev-command.ts +407 -0
  33. package/src/commands/dev/index.ts +1 -0
  34. package/src/commands/infra/index.ts +5 -0
  35. package/src/commands/infra/infra-command.test.ts +719 -0
  36. package/src/commands/infra/infra-command.ts +940 -0
  37. package/src/commands/infra/types.ts +23 -0
  38. package/src/commands/init/index.ts +1 -0
  39. package/src/commands/init/init-command.test.ts +827 -0
  40. package/src/commands/init/init-command.ts +627 -0
  41. package/src/commands/init/types.ts +185 -0
  42. package/src/commands/monitor/index.ts +2 -0
  43. package/src/commands/monitor/monitor-command.test.ts +235 -0
  44. package/src/commands/monitor/monitor-command.ts +245 -0
  45. package/src/commands/monitor/monitor-service.ts +50 -0
  46. package/src/commands/monitor/types.ts +13 -0
  47. package/src/commands/repair/index.ts +2 -0
  48. package/src/commands/repair/repair-command.test.ts +204 -0
  49. package/src/commands/repair/repair-command.ts +199 -0
  50. package/src/commands/repair/repair-service.ts +102 -0
  51. package/src/commands/repair/types.ts +13 -0
  52. package/src/commands/test/index.ts +2 -0
  53. package/src/commands/test/test-command.test.ts +319 -0
  54. package/src/commands/test/test-command.ts +412 -0
  55. package/src/commands/waf/index.ts +2 -0
  56. package/src/commands/waf/types.ts +48 -0
  57. package/src/commands/waf/waf-command.test.ts +506 -0
  58. package/src/commands/waf/waf-command.ts +548 -0
  59. package/src/index.ts +198 -0
  60. package/src/services/cloudflare/cloudflare-service.test.ts +654 -0
  61. package/src/services/cloudflare/cloudflare-service.ts +435 -0
  62. package/src/services/cloudflare/index.ts +2 -0
  63. package/src/services/cloudflare/types.ts +29 -0
  64. package/src/services/config/config-service.test.ts +395 -0
  65. package/src/services/config/config-service.ts +207 -0
  66. package/src/services/config/index.ts +9 -0
  67. package/src/services/config/types.ts +66 -0
  68. package/src/services/db/db-service.test.ts +51 -0
  69. package/src/services/db/db-service.ts +140 -0
  70. package/src/services/db/index.ts +1 -0
  71. package/src/services/docker/docker-service.ts +155 -0
  72. package/src/services/docker/index.ts +1 -0
  73. package/src/services/env/env-service.test.ts +210 -0
  74. package/src/services/env/env-service.ts +156 -0
  75. package/src/services/env/index.ts +1 -0
  76. package/src/services/kv/index.ts +1 -0
  77. package/src/services/kv/kv-sync-service.test.ts +38 -0
  78. package/src/services/kv/kv-sync-service.ts +151 -0
  79. package/src/services/prerequisites/index.ts +1 -0
  80. package/src/services/prerequisites/prerequisites-service.test.ts +89 -0
  81. package/src/services/prerequisites/prerequisites-service.ts +269 -0
  82. package/src/services/prerequisites/types.ts +48 -0
  83. package/src/services/secrets/index.ts +12 -0
  84. package/src/services/secrets/secrets-service.test.ts +486 -0
  85. package/src/services/secrets/secrets-service.ts +293 -0
  86. package/src/services/secrets/types.ts +57 -0
  87. package/src/ui/banner.ts +52 -0
  88. package/src/ui/index.ts +8 -0
  89. package/src/ui/menu.ts +473 -0
  90. package/src/utils/errors.test.ts +69 -0
  91. package/src/utils/errors.ts +23 -0
  92. package/src/utils/formatters.test.ts +180 -0
  93. package/src/utils/formatters.ts +252 -0
  94. package/src/utils/theme.ts +94 -0
@@ -0,0 +1,293 @@
1
+ import { parse as parseJsonc } from "jsonc-parser";
2
+ import type {
3
+ SecretCheckResult,
4
+ SecretStatus,
5
+ WranglerResult,
6
+ WorkersJsonc,
7
+ } from "./types.js";
8
+
9
+ /**
10
+ * Manages Cloudflare Worker secrets defined in `wrangler.jsonc`.
11
+ *
12
+ * Reads worker-level `secrets` arrays, checks local `.dev.vars` files,
13
+ * syncs secrets to Cloudflare via `wrangler secret put`, and generates
14
+ * `.dev.vars` templates.
15
+ *
16
+ * Use the static `create()` factory to instantiate — the constructor
17
+ * is private so the config is parsed once and the sync methods remain
18
+ * synchronous.
19
+ *
20
+ * @example
21
+ * ```ts
22
+ * const svc = await SecretsService.create("wrangler.jsonc");
23
+ * const names = svc.listSecrets("trade-worker");
24
+ * const check = await svc.checkLocalSecrets("trade-worker");
25
+ * ```
26
+ */
27
+ export class SecretsService {
28
+ private config: WorkersJsonc;
29
+ private configPath: string;
30
+
31
+ // -----------------------------------------------------------------------
32
+ // Construction
33
+ // -----------------------------------------------------------------------
34
+
35
+ /** Use {@link create} instead — private so config is always loaded. */
36
+ private constructor(config: WorkersJsonc, configPath: string) {
37
+ this.config = config;
38
+ this.configPath = configPath;
39
+ }
40
+
41
+ /**
42
+ * Factory that reads and parses the wrangler.jsonc config file.
43
+ * Throws if the file doesn't exist or can't be parsed.
44
+ */
45
+ static async create(configPath?: string): Promise<SecretsService> {
46
+ const path = configPath ?? "wrangler.jsonc";
47
+ const file = Bun.file(path);
48
+ if (!(await file.exists())) {
49
+ throw new Error(`Config file not found: ${path}`);
50
+ }
51
+ const text = await file.text();
52
+ const config = parseJsonc(text) as WorkersJsonc;
53
+ return new SecretsService(config, path);
54
+ }
55
+
56
+ // -----------------------------------------------------------------------
57
+ // Public API
58
+ // -----------------------------------------------------------------------
59
+
60
+ /**
61
+ * Returns secret names declared for a worker in `wrangler.jsonc`.
62
+ * Synchronous because the config was loaded during construction.
63
+ */
64
+ listSecrets(workerName: string): string[] {
65
+ const worker = this.config.workers[workerName];
66
+ return worker?.secrets ?? [];
67
+ }
68
+
69
+ /**
70
+ * Returns a map of every worker → its declared secret names.
71
+ * Workers that declare no secrets are omitted from the result.
72
+ */
73
+ listAllSecrets(): Record<string, string[]> {
74
+ const result: Record<string, string[]> = {};
75
+ for (const [name, worker] of Object.entries(this.config.workers)) {
76
+ if (worker.secrets && worker.secrets.length > 0) {
77
+ result[name] = worker.secrets;
78
+ }
79
+ }
80
+ return result;
81
+ }
82
+
83
+ /**
84
+ * Checks a worker's local `.dev.vars` file and reports which secrets
85
+ * are set (with real, non-placeholder values) and which are missing.
86
+ */
87
+ async checkLocalSecrets(workerName: string): Promise<SecretCheckResult> {
88
+ const worker = this.config.workers[workerName];
89
+ if (!worker) {
90
+ return {
91
+ worker: workerName,
92
+ secrets: [],
93
+ allSet: false,
94
+ missing: [],
95
+ };
96
+ }
97
+
98
+ const requiredSecrets = worker.secrets ?? [];
99
+ const devVarsPath = `${worker.path}/.dev.vars`;
100
+ const file = Bun.file(devVarsPath);
101
+
102
+ if (!(await file.exists())) {
103
+ return {
104
+ worker: workerName,
105
+ secrets: requiredSecrets.map((s) => ({ name: s, set: false })),
106
+ allSet: requiredSecrets.length === 0,
107
+ missing: [...requiredSecrets],
108
+ };
109
+ }
110
+
111
+ const content = await file.text();
112
+ const secretMap = this.parseDotEnv(content);
113
+
114
+ const secrets: SecretStatus[] = [];
115
+ const missing: string[] = [];
116
+
117
+ for (const name of requiredSecrets) {
118
+ const value = secretMap.get(name);
119
+ if (value !== undefined && !this.isPlaceholder(value)) {
120
+ secrets.push({ name, set: true, source: devVarsPath });
121
+ } else {
122
+ secrets.push({
123
+ name,
124
+ set: false,
125
+ source: value !== undefined ? devVarsPath : undefined,
126
+ });
127
+ missing.push(name);
128
+ }
129
+ }
130
+
131
+ return {
132
+ worker: workerName,
133
+ secrets,
134
+ allSet: missing.length === 0,
135
+ missing,
136
+ };
137
+ }
138
+
139
+ /**
140
+ * Creates (or overwrites) a `.dev.vars` template file for a worker
141
+ * with placeholder values for every secret declared in `wrangler.jsonc`.
142
+ */
143
+ async generateDevVars(workerName: string): Promise<WranglerResult<string>> {
144
+ const worker = this.config.workers[workerName];
145
+ if (!worker) {
146
+ return {
147
+ success: false,
148
+ error: `Worker "${workerName}" not found in config`,
149
+ };
150
+ }
151
+
152
+ const secrets = worker.secrets ?? [];
153
+ const content =
154
+ secrets.length > 0
155
+ ? secrets.map((s) => `${s}=placeholder_${s.toLowerCase()}`).join("\n") +
156
+ "\n"
157
+ : "";
158
+ const devVarsPath = `${worker.path}/.dev.vars`;
159
+
160
+ try {
161
+ await Bun.write(devVarsPath, content);
162
+ return { success: true, data: devVarsPath };
163
+ } catch (err: unknown) {
164
+ const message = err instanceof Error ? err.message : String(err);
165
+ return { success: false, error: `Failed to write .dev.vars: ${message}` };
166
+ }
167
+ }
168
+
169
+ /**
170
+ * Syncs a worker's secrets to Cloudflare via `wrangler secret put`.
171
+ *
172
+ * For each required secret the service first looks for a real value in
173
+ * the worker's `.dev.vars` file. Secrets with placeholder values are
174
+ * skipped and reported as errors.
175
+ */
176
+ async syncToCloudflare(
177
+ workerName: string
178
+ ): Promise<WranglerResult<string[]>> {
179
+ const worker = this.config.workers[workerName];
180
+ if (!worker) {
181
+ return {
182
+ success: false,
183
+ error: `Worker "${workerName}" not found in config`,
184
+ };
185
+ }
186
+
187
+ const secrets = worker.secrets ?? [];
188
+ const synced: string[] = [];
189
+ const errors: string[] = [];
190
+
191
+ // Pre-load existing .dev.vars to avoid prompting in a service.
192
+ const devVarsPath = `${worker.path}/.dev.vars`;
193
+ const devVarsFile = Bun.file(devVarsPath);
194
+ let existingValues: Map<string, string> = new Map();
195
+
196
+ if (await devVarsFile.exists()) {
197
+ const content = await devVarsFile.text();
198
+ existingValues = this.parseDotEnv(content);
199
+ }
200
+
201
+ for (const secret of secrets) {
202
+ try {
203
+ const value = existingValues.get(secret);
204
+ if (value !== undefined && !this.isPlaceholder(value)) {
205
+ await this.execWranglerSecretPut(worker.path, secret, value);
206
+ synced.push(secret);
207
+ } else {
208
+ errors.push(
209
+ `Secret "${secret}": no valid value in .dev.vars (run generateDevVars then edit)`
210
+ );
211
+ }
212
+ } catch (err: unknown) {
213
+ const message = err instanceof Error ? err.message : String(err);
214
+ errors.push(`Secret "${secret}": ${message}`);
215
+ }
216
+ }
217
+
218
+ if (synced.length === 0 && errors.length > 0) {
219
+ return { success: false, error: errors.join("; ") };
220
+ }
221
+
222
+ return {
223
+ success: errors.length === 0,
224
+ data: synced,
225
+ error: errors.length > 0 ? errors.join("; ") : undefined,
226
+ };
227
+ }
228
+
229
+ // -----------------------------------------------------------------------
230
+ // Private helpers
231
+ // -----------------------------------------------------------------------
232
+
233
+ /**
234
+ * Parses a `.env` / `.dev.vars` style file into a `Map<key, value>`.
235
+ * Skips empty lines and comments (lines starting with `#`).
236
+ */
237
+ private parseDotEnv(content: string): Map<string, string> {
238
+ const map = new Map<string, string>();
239
+ for (const line of content.split("\n")) {
240
+ const trimmed = line.trim();
241
+ if (trimmed === "" || trimmed.startsWith("#")) continue;
242
+
243
+ const eqIdx = trimmed.indexOf("=");
244
+ if (eqIdx < 1) continue;
245
+
246
+ const key = trimmed.substring(0, eqIdx).trim();
247
+ const value = trimmed.substring(eqIdx + 1).trim();
248
+ map.set(key, value);
249
+ }
250
+ return map;
251
+ }
252
+
253
+ /** Returns `true` when a value looks like an unfilled template. */
254
+ private isPlaceholder(value: string): boolean {
255
+ return (
256
+ value.startsWith("placeholder_") ||
257
+ value.startsWith("your_") ||
258
+ value.startsWith("generate_") ||
259
+ value === ""
260
+ );
261
+ }
262
+
263
+ /**
264
+ * Runs `wrangler secret put <name>` inside the worker's directory.
265
+ * Marked `protected` so unit tests can stub it without touching the
266
+ * real `Bun.spawn`.
267
+ */
268
+ protected async execWranglerSecretPut(
269
+ workerPath: string,
270
+ name: string,
271
+ value: string
272
+ ): Promise<void> {
273
+ const proc = Bun.spawn(["wrangler", "secret", "put", name], {
274
+ cwd: workerPath,
275
+ stdin: "pipe",
276
+ stdout: "pipe",
277
+ stderr: "pipe",
278
+ });
279
+
280
+ proc.stdin.write(value + "\n");
281
+ proc.stdin.end();
282
+
283
+ const exitCode = await proc.exited;
284
+ if (exitCode !== 0) {
285
+ const stderrText = await new Response(proc.stderr).text();
286
+ throw new Error(
287
+ `wrangler exited with code ${exitCode}: ${stderrText.trim()}`
288
+ );
289
+ }
290
+ }
291
+ }
292
+
293
+ export default SecretsService;
@@ -0,0 +1,57 @@
1
+ /**
2
+ * SecretsService types — secret management for Cloudflare Workers.
3
+ *
4
+ * The `Result<T>` discriminated-union mirrors the pattern used in
5
+ * @jango-blockchained/hoox-shared (see packages/shared/src/types.ts:92) but is defined
6
+ * locally to avoid adding a public export to the shared package.
7
+ */
8
+
9
+ // ---------------------------------------------------------------------------
10
+ // Result / Wrangler
11
+ // ---------------------------------------------------------------------------
12
+
13
+ export type Result<T> = { ok: true; value: T } | { ok: false; error: string };
14
+
15
+ export interface WranglerResult<T> {
16
+ success: boolean;
17
+ data?: T;
18
+ error?: string;
19
+ }
20
+
21
+ // ---------------------------------------------------------------------------
22
+ // Domain types
23
+ // ---------------------------------------------------------------------------
24
+
25
+ export interface SecretStatus {
26
+ /** Name of the secret (e.g. "TELEGRAM_BOT_TOKEN"). */
27
+ name: string;
28
+ /** Whether the secret has a real (non-placeholder) value set. */
29
+ set: boolean;
30
+ /** Where the value was found ("workers/<name>/.dev.vars" or undefined). */
31
+ source?: string;
32
+ }
33
+
34
+ export interface SecretCheckResult {
35
+ /** Worker name as it appears in wrangler.jsonc. */
36
+ worker: string;
37
+ /** Per-secret status entries. */
38
+ secrets: SecretStatus[];
39
+ /** True when *every* required secret is present with a real value. */
40
+ allSet: boolean;
41
+ /** Secret names that are either missing or still placeholder. */
42
+ missing: string[];
43
+ }
44
+
45
+ // ---------------------------------------------------------------------------
46
+ // Internal config shape (subset of wrangler.jsonc used by SecretsService)
47
+ // ---------------------------------------------------------------------------
48
+
49
+ export interface WorkerSecretConfig {
50
+ enabled: boolean;
51
+ path: string;
52
+ secrets?: string[];
53
+ }
54
+
55
+ export interface WorkersJsonc {
56
+ workers: Record<string, WorkerSecretConfig>;
57
+ }
@@ -0,0 +1,52 @@
1
+ /**
2
+ * Hoox ASCII banner — big block letter style with box framing.
3
+ * Rendered with theme colors for consistent terminal output.
4
+ */
5
+
6
+ import { theme } from "../utils/theme.js";
7
+
8
+ const BANNER_LINES = [
9
+ "██╗ ██╗ ██████╗ ██████╗ ██╗ ██╗",
10
+ "██║ ██║██╔═══██╗██╔═══██╗╚██╗██╔╝",
11
+ "███████║██║ ██║██║ ██║ ╚███╔╝ ",
12
+ "██╔══██║██║ ██║██║ ██║ ██╔██╗ ",
13
+ "██║ ██║╚██████╔╝╚██████╔╝██╔╝ ██╗",
14
+ "╚═╝ ╚═╝ ╚═════╝ ╚═════╝ ╚═╝ ╚═╝",
15
+ ];
16
+
17
+ const TAGLINE = "Cloudflare Workers Platform";
18
+ const VERSION = "v0.3.0";
19
+
20
+ /**
21
+ * Render the Hoox ASCII banner with theme coloring and box framing.
22
+ */
23
+ export function renderBanner(): string {
24
+ const bannerWidth = 52;
25
+ const top = ` ${theme.corner.charAt(0)}${theme.separator.repeat(bannerWidth - 2)}${theme.corner.charAt(2)}`;
26
+ const bottom = ` ${theme.corner.charAt(3)}${theme.separator.repeat(bannerWidth - 2)}${theme.corner.charAt(1)}`;
27
+
28
+ const lines = BANNER_LINES.map((line) =>
29
+ ` ${theme.heading(line)}`
30
+ );
31
+
32
+ // Center the tagline
33
+ const taglineLeft = Math.floor((bannerWidth - TAGLINE.length - VERSION.length - 2) / 2);
34
+ const tagline = ` ${" ".repeat(taglineLeft)}${theme.dim(TAGLINE)} ${theme.dim(VERSION)}`;
35
+
36
+ const result = [
37
+ top,
38
+ ...lines,
39
+ ` ${theme.separator.repeat(bannerWidth - 2)}`,
40
+ tagline,
41
+ bottom,
42
+ ].join("\n");
43
+
44
+ return result;
45
+ }
46
+
47
+ /**
48
+ * Render a compact one-line banner for inline display.
49
+ */
50
+ export function renderCompactBanner(): string {
51
+ return `${theme.heading("Hoox CLI")} ${theme.dim(`${TAGLINE} ${VERSION}`)}`;
52
+ }
@@ -0,0 +1,8 @@
1
+ /**
2
+ * Hoox CLI UI module — interactive TUI components.
3
+ *
4
+ * @module
5
+ */
6
+
7
+ export { renderBanner } from "./banner.js";
8
+ export { runInteractiveTUI } from "./menu.js";