@jango-blockchained/hoox-cli 0.3.4 → 0.3.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.
Files changed (48) hide show
  1. package/README.md +2 -0
  2. package/package.json +5 -3
  3. package/src/commands/check/check-command.test.ts +28 -20
  4. package/src/commands/check/prerequisites-command.ts +9 -5
  5. package/src/commands/clone/clone-command.ts +1 -10
  6. package/src/commands/config/config-command.ts +7 -11
  7. package/src/commands/config/env-command.ts +16 -35
  8. package/src/commands/config/kv-command.ts +33 -72
  9. package/src/commands/db/db-command.ts +23 -25
  10. package/src/commands/deploy/deploy-command.ts +133 -52
  11. package/src/commands/deploy/telegram-service.ts +21 -6
  12. package/src/commands/dev/dev-command.ts +7 -16
  13. package/src/commands/infra/infra-command.test.ts +2 -2
  14. package/src/commands/infra/infra-command.ts +45 -11
  15. package/src/commands/init/init-command.test.ts +2 -2
  16. package/src/commands/monitor/monitor-command.test.ts +44 -16
  17. package/src/commands/monitor/monitor-command.ts +45 -21
  18. package/src/commands/monitor/monitor-service.ts +19 -4
  19. package/src/commands/repair/repair-command.test.ts +35 -15
  20. package/src/commands/repair/repair-command.ts +39 -18
  21. package/src/commands/repair/repair-service.ts +48 -12
  22. package/src/commands/test/test-command.ts +4 -10
  23. package/src/commands/tui/index.ts +1 -0
  24. package/src/commands/tui/tui-command.ts +87 -0
  25. package/src/commands/update/index.ts +1 -0
  26. package/src/commands/update/update-command.ts +51 -0
  27. package/src/commands/waf/waf-command.test.ts +1 -1
  28. package/src/commands/waf/waf-command.ts +11 -11
  29. package/src/index.ts +52 -1
  30. package/src/services/cloudflare/cloudflare-service.test.ts +12 -8
  31. package/src/services/cloudflare/cloudflare-service.ts +8 -10
  32. package/src/services/cloudflare/types.ts +6 -5
  33. package/src/services/db/db-service.ts +8 -17
  34. package/src/services/env/env-service.test.ts +41 -15
  35. package/src/services/env/env-service.ts +276 -46
  36. package/src/services/kv/kv-sync-service.ts +117 -34
  37. package/src/services/prerequisites/prerequisites-service.ts +137 -29
  38. package/src/services/secrets/index.ts +0 -1
  39. package/src/services/secrets/secrets-service.test.ts +34 -25
  40. package/src/services/secrets/secrets-service.ts +10 -16
  41. package/src/services/secrets/types.ts +4 -11
  42. package/src/services/update/index.ts +2 -0
  43. package/src/services/update/update-service.test.ts +76 -0
  44. package/src/services/update/update-service.ts +193 -0
  45. package/src/ui/banner.ts +5 -5
  46. package/src/ui/menu.ts +6 -1
  47. package/src/utils/formatters.ts +21 -4
  48. package/src/utils/theme.ts +1 -1
@@ -1,8 +1,8 @@
1
1
  import { parse as parseJsonc } from "jsonc-parser";
2
2
  import type {
3
+ Result,
3
4
  SecretCheckResult,
4
5
  SecretStatus,
5
- WranglerResult,
6
6
  WorkersJsonc,
7
7
  } from "./types.js";
8
8
 
@@ -140,11 +140,11 @@ export class SecretsService {
140
140
  * Creates (or overwrites) a `.dev.vars` template file for a worker
141
141
  * with placeholder values for every secret declared in `wrangler.jsonc`.
142
142
  */
143
- async generateDevVars(workerName: string): Promise<WranglerResult<string>> {
143
+ async generateDevVars(workerName: string): Promise<Result<string>> {
144
144
  const worker = this.config.workers[workerName];
145
145
  if (!worker) {
146
146
  return {
147
- success: false,
147
+ ok: false,
148
148
  error: `Worker "${workerName}" not found in config`,
149
149
  };
150
150
  }
@@ -159,10 +159,10 @@ export class SecretsService {
159
159
 
160
160
  try {
161
161
  await Bun.write(devVarsPath, content);
162
- return { success: true, data: devVarsPath };
162
+ return { ok: true, value: devVarsPath };
163
163
  } catch (err: unknown) {
164
164
  const message = err instanceof Error ? err.message : String(err);
165
- return { success: false, error: `Failed to write .dev.vars: ${message}` };
165
+ return { ok: false, error: `Failed to write .dev.vars: ${message}` };
166
166
  }
167
167
  }
168
168
 
@@ -173,13 +173,11 @@ export class SecretsService {
173
173
  * the worker's `.dev.vars` file. Secrets with placeholder values are
174
174
  * skipped and reported as errors.
175
175
  */
176
- async syncToCloudflare(
177
- workerName: string
178
- ): Promise<WranglerResult<string[]>> {
176
+ async syncToCloudflare(workerName: string): Promise<Result<string[]>> {
179
177
  const worker = this.config.workers[workerName];
180
178
  if (!worker) {
181
179
  return {
182
- success: false,
180
+ ok: false,
183
181
  error: `Worker "${workerName}" not found in config`,
184
182
  };
185
183
  }
@@ -215,15 +213,11 @@ export class SecretsService {
215
213
  }
216
214
  }
217
215
 
218
- if (synced.length === 0 && errors.length > 0) {
219
- return { success: false, error: errors.join("; ") };
216
+ if (errors.length > 0) {
217
+ return { ok: false, error: errors.join("; ") };
220
218
  }
221
219
 
222
- return {
223
- success: errors.length === 0,
224
- data: synced,
225
- error: errors.length > 0 ? errors.join("; ") : undefined,
226
- };
220
+ return { ok: true, value: synced };
227
221
  }
228
222
 
229
223
  // -----------------------------------------------------------------------
@@ -1,22 +1,15 @@
1
1
  /**
2
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
3
  */
8
4
 
9
5
  // ---------------------------------------------------------------------------
10
- // Result / Wrangler
6
+ // Result
11
7
  // ---------------------------------------------------------------------------
12
8
 
13
- export type Result<T> = { ok: true; value: T } | { ok: false; error: string };
9
+ import type { Result } from "@jango-blockchained/hoox-shared";
14
10
 
15
- export interface WranglerResult<T> {
16
- success: boolean;
17
- data?: T;
18
- error?: string;
19
- }
11
+ // Re-export the shared Result<T> for convenience
12
+ export type { Result };
20
13
 
21
14
  // ---------------------------------------------------------------------------
22
15
  // Domain types
@@ -0,0 +1,2 @@
1
+ export { UpdateService } from "./update-service.js";
2
+ export type { UpdateResult } from "./update-service.js";
@@ -0,0 +1,76 @@
1
+ import { describe, expect, it, mock } from "bun:test";
2
+ import { UpdateService } from "./update-service.js";
3
+
4
+ function createMockPrereqs(
5
+ overrides?: Partial<{
6
+ outdated: boolean;
7
+ current: string;
8
+ minimum: string;
9
+ }>
10
+ ) {
11
+ const defaults = { outdated: false, current: "4.0.0", minimum: "3.88.0" };
12
+ const config = { ...defaults, ...overrides };
13
+
14
+ return {
15
+ checkWranglerVersion: mock(() => Promise.resolve(config)),
16
+ checkBun: mock(() => Promise.resolve({})),
17
+ checkGit: mock(() => Promise.resolve({})),
18
+ checkNode: mock(() => Promise.resolve({})),
19
+ checkWrangler: mock(() => Promise.resolve({})),
20
+ checkCloudflareAuth: mock(() => Promise.resolve({})),
21
+ checkDocker: mock(() => Promise.resolve({})),
22
+ checkRepository: mock(() => Promise.resolve({})),
23
+ runAll: mock(() => Promise.resolve({ checks: [], allPassed: true })),
24
+ };
25
+ }
26
+
27
+ describe("UpdateService", () => {
28
+ describe("checkAndPromptUpdate", () => {
29
+ it("returns updated=false when wrangler is up to date", async () => {
30
+ const mockPrereqs = createMockPrereqs({ outdated: false });
31
+ const svc = new UpdateService(undefined, mockPrereqs as any);
32
+
33
+ const result = await svc.checkAndPromptUpdate({ yes: true });
34
+
35
+ expect(result.updated).toBe(false);
36
+ expect(result.error).toBeUndefined();
37
+ });
38
+
39
+ it("auto-updates when wrangler is outdated and --yes is set", async () => {
40
+ const mockPrereqs = createMockPrereqs({
41
+ outdated: true,
42
+ current: "3.87.0",
43
+ });
44
+ const svc = new UpdateService(undefined, mockPrereqs as any);
45
+
46
+ const result = await svc.checkAndPromptUpdate({ yes: true });
47
+
48
+ expect(typeof result.updated).toBe("boolean");
49
+ }, 30000);
50
+ });
51
+
52
+ describe("updateWrangler", () => {
53
+ it("returns result when called (does not throw)", async () => {
54
+ const mockPrereqs = createMockPrereqs({
55
+ outdated: false,
56
+ current: "4.0.0",
57
+ });
58
+ const svc = new UpdateService(undefined, mockPrereqs as any);
59
+
60
+ const result = await svc.updateWrangler();
61
+
62
+ expect(typeof result.updated).toBe("boolean");
63
+ }, 30000);
64
+ });
65
+
66
+ describe("checkLatestVersion", () => {
67
+ it("returns a version string or null", async () => {
68
+ const svc = new UpdateService();
69
+ const version = await svc.checkLatestVersion();
70
+ expect(version === null || typeof version === "string").toBe(true);
71
+ if (typeof version === "string") {
72
+ expect(version).toMatch(/^\d+\.\d+\.\d+$/);
73
+ }
74
+ }, 30000);
75
+ });
76
+ });
@@ -0,0 +1,193 @@
1
+ /**
2
+ * UpdateService — manages wrangler version updates.
3
+ *
4
+ * Checks current wrangler version, compares against minimum, and updates
5
+ * via `bun update wrangler` when needed. Integrates with PrerequisitesService
6
+ * for version checking.
7
+ */
8
+
9
+ import { PrerequisitesService } from "../prerequisites/index.js";
10
+ import { theme } from "../../utils/theme.js";
11
+ import { confirm } from "@clack/prompts";
12
+
13
+ export interface UpdateResult {
14
+ updated: boolean;
15
+ previousVersion?: string;
16
+ newVersion?: string;
17
+ error?: string;
18
+ }
19
+
20
+ export class UpdateService {
21
+ private readonly prereqs: PrerequisitesService;
22
+ private readonly cwd: string;
23
+
24
+ constructor(cwd?: string, prereqs?: PrerequisitesService) {
25
+ this.prereqs = prereqs ?? new PrerequisitesService();
26
+ this.cwd = cwd ?? process.cwd();
27
+ }
28
+
29
+ /**
30
+ * Check if wrangler is outdated. If yes, prompt user (TTY) or
31
+ * auto-update (non-TTY / --yes flag). Never throws — errors are
32
+ * returned in UpdateResult.
33
+ */
34
+ async checkAndPromptUpdate(options?: {
35
+ yes?: boolean;
36
+ }): Promise<UpdateResult> {
37
+ try {
38
+ const versionCheck = await this.prereqs.checkWranglerVersion();
39
+
40
+ if (!versionCheck.outdated) {
41
+ process.stdout.write(
42
+ ` ${theme.success("✓")} Wrangler ${versionCheck.current} is up to date\n`
43
+ );
44
+ return { updated: false };
45
+ }
46
+
47
+ const current = versionCheck.current ?? "unknown";
48
+ const minimum = versionCheck.minimum ?? "unknown";
49
+
50
+ // Fetch latest available version for the prompt
51
+ const latest = await this.checkLatestVersion();
52
+
53
+ // Determine if we should prompt or auto-update
54
+ let shouldUpdate: boolean;
55
+ if (options?.yes ?? !process.stdout.isTTY) {
56
+ shouldUpdate = true;
57
+ } else {
58
+ shouldUpdate = await this.promptUpdate(current, minimum, latest);
59
+ }
60
+
61
+ if (!shouldUpdate) {
62
+ process.stdout.write(
63
+ ` ${theme.warning("!")} Skipping wrangler update (current: ${current})\n`
64
+ );
65
+ return { updated: false };
66
+ }
67
+
68
+ return await this.runUpdate();
69
+ } catch (err) {
70
+ const message = err instanceof Error ? err.message : String(err);
71
+ process.stdout.write(
72
+ ` ${theme.error("!")} Wrangler update check failed: ${message}\n`
73
+ );
74
+ return { updated: false, error: message };
75
+ }
76
+ }
77
+
78
+ /**
79
+ * Force-update wrangler regardless of current version.
80
+ * Used by the standalone `hoox update` command.
81
+ */
82
+ async updateWrangler(): Promise<UpdateResult> {
83
+ process.stdout.write(` ${theme.info("i")} Checking wrangler version...\n`);
84
+
85
+ const before = await this.prereqs.checkWranglerVersion();
86
+ const previousVersion = before.current;
87
+
88
+ if (!previousVersion) {
89
+ process.stdout.write(
90
+ ` ${theme.warning("!")} Wrangler is not installed. Install with: bun add -g wrangler\n`
91
+ );
92
+ return { updated: false, error: "Wrangler not installed" };
93
+ }
94
+
95
+ const result = await this.runUpdate(previousVersion);
96
+ return { ...result, previousVersion };
97
+ }
98
+
99
+ /**
100
+ * Check the latest available wrangler version from the npm registry.
101
+ * Uses the npm registry JSON API directly — no dependency on npm CLI.
102
+ * Returns null if the check fails (network error, etc.).
103
+ */
104
+ async checkLatestVersion(): Promise<string | null> {
105
+ try {
106
+ const res = await fetch("https://registry.npmjs.org/wrangler/latest");
107
+ if (!res.ok) return null;
108
+ const data = (await res.json()) as { version?: string };
109
+ return data.version ?? null;
110
+ } catch {
111
+ return null;
112
+ }
113
+ }
114
+
115
+ // ── Private helpers ──────────────────────────────────────────────
116
+
117
+ /**
118
+ * Prompt the user whether to update wrangler.
119
+ * Returns true if the user wants to update.
120
+ */
121
+ private async promptUpdate(
122
+ current: string,
123
+ minimum: string,
124
+ latest: string | null
125
+ ): Promise<boolean> {
126
+ const latestStr = latest ? `, latest: ${latest}` : "";
127
+ process.stdout.write(
128
+ `\n ${theme.warning("!")} Wrangler ${current} is outdated (minimum: ${minimum}${latestStr})\n`
129
+ );
130
+
131
+ const result = await confirm({
132
+ message: "Update wrangler?",
133
+ initialValue: true,
134
+ });
135
+
136
+ // Confirm returns boolean | symbol (CLACK_CANCEL)
137
+ return result === true;
138
+ }
139
+
140
+ /**
141
+ * Run `bun update wrangler` in the project root and verify the result.
142
+ */
143
+ private async runUpdate(previousVersion?: string): Promise<UpdateResult> {
144
+ process.stdout.write(` ${theme.info("i")} Updating wrangler...\n`);
145
+
146
+ try {
147
+ const proc = Bun.spawn(["bun", "update", "wrangler"], {
148
+ cwd: this.cwd,
149
+ stdout: "ignore",
150
+ stderr: "pipe",
151
+ });
152
+
153
+ const stderr = await new Response(proc.stderr).text();
154
+ const exitCode = await proc.exited;
155
+
156
+ if (exitCode !== 0) {
157
+ const errorMsg = stderr.split("\n")[0] || "bun update wrangler failed";
158
+ process.stdout.write(
159
+ ` ${theme.error("!")} Update failed: ${errorMsg}\n`
160
+ );
161
+ return { updated: false, error: errorMsg };
162
+ }
163
+
164
+ // Verify the new version
165
+ const verify = await this.prereqs.checkWranglerVersion();
166
+ const newVersion = verify.current;
167
+
168
+ const versionChanged = previousVersion
169
+ ? previousVersion !== newVersion
170
+ : true;
171
+
172
+ if (versionChanged) {
173
+ process.stdout.write(
174
+ ` ${theme.success("✓")} Wrangler updated from ${previousVersion ?? "?"} to ${newVersion}\n`
175
+ );
176
+ } else {
177
+ process.stdout.write(
178
+ ` ${theme.warning("!")} Wrangler version unchanged (${newVersion})\n`
179
+ );
180
+ }
181
+
182
+ return {
183
+ updated: versionChanged,
184
+ previousVersion,
185
+ newVersion,
186
+ };
187
+ } catch (err) {
188
+ const message = err instanceof Error ? err.message : String(err);
189
+ process.stdout.write(` ${theme.error("!")} Update failed: ${message}\n`);
190
+ return { updated: false, error: message };
191
+ }
192
+ }
193
+ }
package/src/ui/banner.ts CHANGED
@@ -25,12 +25,12 @@ export function renderBanner(): string {
25
25
  const top = ` ${theme.corner.charAt(0)}${theme.separator.repeat(bannerWidth - 2)}${theme.corner.charAt(2)}`;
26
26
  const bottom = ` ${theme.corner.charAt(3)}${theme.separator.repeat(bannerWidth - 2)}${theme.corner.charAt(1)}`;
27
27
 
28
- const lines = BANNER_LINES.map((line) =>
29
- ` ${theme.heading(line)}`
30
- );
28
+ const lines = BANNER_LINES.map((line) => ` ${theme.heading(line)}`);
31
29
 
32
30
  // Center the tagline
33
- const taglineLeft = Math.floor((bannerWidth - TAGLINE.length - VERSION.length - 2) / 2);
31
+ const taglineLeft = Math.floor(
32
+ (bannerWidth - TAGLINE.length - VERSION.length - 2) / 2
33
+ );
34
34
  const tagline = ` ${" ".repeat(taglineLeft)}${theme.dim(TAGLINE)} ${theme.dim(VERSION)}`;
35
35
 
36
36
  const result = [
@@ -49,4 +49,4 @@ export function renderBanner(): string {
49
49
  */
50
50
  export function renderCompactBanner(): string {
51
51
  return `${theme.heading("Hoox CLI")} ${theme.dim(`${TAGLINE} ${VERSION}`)}`;
52
- }
52
+ }
package/src/ui/menu.ts CHANGED
@@ -422,6 +422,11 @@ async function showToolsMenu(
422
422
  label: "Dashboard UI",
423
423
  hint: "manage dashboard URLs",
424
424
  },
425
+ {
426
+ value: "tui",
427
+ label: "Launch TUI Dashboard",
428
+ hint: "OpenTUI terminal operations center",
429
+ },
425
430
  { value: "__back", label: "◀ Back to main menu" },
426
431
  ],
427
432
  });
@@ -470,4 +475,4 @@ async function runCommand(program: Command, commandStr: string): Promise<void> {
470
475
  : String(err);
471
476
  log.error(`Command failed: ${message}`);
472
477
  }
473
- }
478
+ }
@@ -6,6 +6,7 @@
6
6
 
7
7
  import { CLIError, ExitCode } from "./errors.js";
8
8
  import { theme, icons, stripAnsi, hr } from "./theme.js";
9
+ import { Command } from "commander";
9
10
 
10
11
  export interface FormatOptions {
11
12
  json?: boolean;
@@ -48,7 +49,10 @@ export function renderProgressBar(
48
49
  * Returns lines suitable for overwriting via \r or manual clear.
49
50
  */
50
51
  export function renderStepProgress(
51
- steps: Array<{ name: string; status: "pending" | "running" | "done" | "failed" }>
52
+ steps: Array<{
53
+ name: string;
54
+ status: "pending" | "running" | "done" | "failed";
55
+ }>
52
56
  ): string {
53
57
  const iconMap: Record<string, string> = {
54
58
  pending: theme.dim("○"),
@@ -212,11 +216,15 @@ export function formatKeyValue(
212
216
  return;
213
217
  }
214
218
 
215
- const maxKeyLen = Math.max(...Object.keys(pairs).map((k) => stripAnsi(k).length));
219
+ const maxKeyLen = Math.max(
220
+ ...Object.keys(pairs).map((k) => stripAnsi(k).length)
221
+ );
216
222
 
217
223
  for (const [key, value] of Object.entries(pairs)) {
218
224
  const paddedKey = key.padEnd(maxKeyLen);
219
- process.stdout.write(` ${theme.key(paddedKey)} ${theme.dim(":")} ${value}\n`);
225
+ process.stdout.write(
226
+ ` ${theme.key(paddedKey)} ${theme.dim(":")} ${value}\n`
227
+ );
220
228
  }
221
229
  }
222
230
 
@@ -249,4 +257,13 @@ export function formatList(
249
257
  for (const item of items) {
250
258
  process.stdout.write(` ${icon} ${item}\n`);
251
259
  }
252
- }
260
+ }
261
+
262
+ /**
263
+ * Build the format options for output, reading global --json / --quiet flags.
264
+ * Uses `optsWithGlobals()` to include options inherited from the top-level program.
265
+ */
266
+ export function getFormatOptions(cmd: Command): FormatOptions {
267
+ const opts = cmd.optsWithGlobals();
268
+ return { json: Boolean(opts.json), quiet: Boolean(opts.quiet) };
269
+ }
@@ -91,4 +91,4 @@ export function kv(key: string, value: string): string {
91
91
  /** Format a labeled value. */
92
92
  export function tagged(label: string, value: string): string {
93
93
  return `${theme.key(label)} ${theme.dim(":")} ${theme.value(value)}`;
94
- }
94
+ }