@jango-blockchained/hoox-cli 0.5.1 → 0.5.6
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/bin/hoox.js +3 -4
- package/dist/index.js +32545 -0
- package/package.json +10 -15
- package/src/commands/check/check-command.test.ts +0 -476
- package/src/commands/check/check-command.ts +0 -1144
- package/src/commands/check/index.ts +0 -10
- package/src/commands/check/prerequisites-command.test.ts +0 -19
- package/src/commands/check/prerequisites-command.ts +0 -96
- package/src/commands/check/types.ts +0 -103
- package/src/commands/clone/clone-command.test.ts +0 -442
- package/src/commands/clone/clone-command.ts +0 -431
- package/src/commands/clone/index.ts +0 -1
- package/src/commands/config/config-command.test.ts +0 -583
- package/src/commands/config/config-command.ts +0 -897
- package/src/commands/config/env-command.test.ts +0 -43
- package/src/commands/config/env-command.ts +0 -295
- package/src/commands/config/index.ts +0 -3
- package/src/commands/config/kv-command.test.ts +0 -14
- package/src/commands/config/kv-command.ts +0 -290
- package/src/commands/dashboard/dashboard-command.test.ts +0 -47
- package/src/commands/dashboard/dashboard-command.ts +0 -128
- package/src/commands/dashboard/index.ts +0 -1
- package/src/commands/db/db-command.test.ts +0 -21
- package/src/commands/db/db-command.ts +0 -312
- package/src/commands/db/index.ts +0 -1
- package/src/commands/deploy/deploy-command.test.ts +0 -304
- package/src/commands/deploy/deploy-command.ts +0 -1135
- package/src/commands/deploy/index.ts +0 -2
- package/src/commands/deploy/telegram-service.ts +0 -76
- package/src/commands/deploy/types.ts +0 -34
- package/src/commands/dev/dev-command.test.ts +0 -383
- package/src/commands/dev/dev-command.ts +0 -398
- package/src/commands/dev/index.ts +0 -1
- package/src/commands/infra/index.ts +0 -5
- package/src/commands/infra/infra-command.test.ts +0 -719
- package/src/commands/infra/infra-command.ts +0 -974
- package/src/commands/infra/types.ts +0 -23
- package/src/commands/init/__tests__/cli-provisioner.test.ts +0 -17
- package/src/commands/init/cli-provisioner.ts +0 -71
- package/src/commands/init/index.ts +0 -1
- package/src/commands/init/init-command.test.ts +0 -876
- package/src/commands/init/init-command.ts +0 -593
- package/src/commands/init/types.ts +0 -23
- package/src/commands/logs/index.ts +0 -1
- package/src/commands/logs/logs-command.test.ts +0 -294
- package/src/commands/logs/logs-command.ts +0 -452
- package/src/commands/monitor/index.ts +0 -2
- package/src/commands/monitor/monitor-command.test.ts +0 -263
- package/src/commands/monitor/monitor-command.ts +0 -269
- package/src/commands/monitor/monitor-service.ts +0 -65
- package/src/commands/monitor/types.ts +0 -13
- package/src/commands/repair/index.ts +0 -2
- package/src/commands/repair/repair-command.test.ts +0 -224
- package/src/commands/repair/repair-command.ts +0 -220
- package/src/commands/repair/repair-service.ts +0 -138
- package/src/commands/repair/types.ts +0 -13
- package/src/commands/test/index.ts +0 -2
- package/src/commands/test/test-command.test.ts +0 -319
- package/src/commands/test/test-command.ts +0 -406
- package/src/commands/tui/index.ts +0 -1
- package/src/commands/tui/tui-command.ts +0 -87
- package/src/commands/update/index.ts +0 -1
- package/src/commands/update/update-command.ts +0 -168
- package/src/commands/waf/index.ts +0 -2
- package/src/commands/waf/types.ts +0 -48
- package/src/commands/waf/waf-command.test.ts +0 -506
- package/src/commands/waf/waf-command.ts +0 -548
- package/src/index.ts +0 -269
- package/src/services/cloudflare/cloudflare-service.test.ts +0 -658
- package/src/services/cloudflare/cloudflare-service.ts +0 -433
- package/src/services/cloudflare/index.ts +0 -2
- package/src/services/cloudflare/types.ts +0 -30
- package/src/services/config/config-service.test.ts +0 -395
- package/src/services/config/config-service.ts +0 -207
- package/src/services/config/index.ts +0 -9
- package/src/services/config/types.ts +0 -66
- package/src/services/db/db-service.test.ts +0 -51
- package/src/services/db/db-service.ts +0 -131
- package/src/services/db/index.ts +0 -1
- package/src/services/docker/docker-service.ts +0 -155
- package/src/services/docker/index.ts +0 -1
- package/src/services/env/env-service.test.ts +0 -236
- package/src/services/env/env-service.ts +0 -386
- package/src/services/env/index.ts +0 -1
- package/src/services/kv/index.ts +0 -1
- package/src/services/kv/kv-sync-service.test.ts +0 -38
- package/src/services/kv/kv-sync-service.ts +0 -234
- package/src/services/prerequisites/index.ts +0 -1
- package/src/services/prerequisites/prerequisites-service.test.ts +0 -89
- package/src/services/prerequisites/prerequisites-service.ts +0 -377
- package/src/services/prerequisites/types.ts +0 -48
- package/src/services/secrets/index.ts +0 -11
- package/src/services/secrets/secrets-service.test.ts +0 -495
- package/src/services/secrets/secrets-service.ts +0 -287
- package/src/services/secrets/types.ts +0 -50
- package/src/services/update/index.ts +0 -2
- package/src/services/update/update-service.test.ts +0 -76
- package/src/services/update/update-service.ts +0 -193
- package/src/ui/banner.ts +0 -141
- package/src/ui/index.ts +0 -8
- package/src/ui/menu.ts +0 -490
- package/src/utils/errors.test.ts +0 -69
- package/src/utils/errors.ts +0 -23
- package/src/utils/formatters.test.ts +0 -180
- package/src/utils/formatters.ts +0 -269
- package/src/utils/theme.ts +0 -94
package/src/utils/errors.test.ts
DELETED
|
@@ -1,69 +0,0 @@
|
|
|
1
|
-
import { describe, it, expect } from "bun:test";
|
|
2
|
-
import { CLIError, ExitCode } from "./errors.js";
|
|
3
|
-
|
|
4
|
-
// CommandItem is defined in errors.js and re-exported; avoid redeclare
|
|
5
|
-
|
|
6
|
-
describe("ExitCode", () => {
|
|
7
|
-
it("has correct numeric values", () => {
|
|
8
|
-
expect(ExitCode.SUCCESS).toBe(0);
|
|
9
|
-
expect(ExitCode.ERROR).toBe(1);
|
|
10
|
-
expect(ExitCode.INVALID_USAGE).toBe(2);
|
|
11
|
-
expect(ExitCode.INFRA_UNAVAILABLE).toBe(3);
|
|
12
|
-
});
|
|
13
|
-
|
|
14
|
-
it("is a numeric enum with expected values", () => {
|
|
15
|
-
expect(ExitCode.SUCCESS).toBe(0);
|
|
16
|
-
expect(ExitCode.ERROR).toBe(1);
|
|
17
|
-
expect(ExitCode.INVALID_USAGE).toBe(2);
|
|
18
|
-
expect(ExitCode.INFRA_UNAVAILABLE).toBe(3);
|
|
19
|
-
// All values are distinct
|
|
20
|
-
const values = [
|
|
21
|
-
ExitCode.SUCCESS,
|
|
22
|
-
ExitCode.ERROR,
|
|
23
|
-
ExitCode.INVALID_USAGE,
|
|
24
|
-
ExitCode.INFRA_UNAVAILABLE,
|
|
25
|
-
];
|
|
26
|
-
expect(new Set(values).size).toBe(4);
|
|
27
|
-
});
|
|
28
|
-
});
|
|
29
|
-
|
|
30
|
-
describe("CLIError", () => {
|
|
31
|
-
it("constructs with message and defaults", () => {
|
|
32
|
-
const err = new CLIError("something went wrong");
|
|
33
|
-
expect(err).toBeInstanceOf(Error);
|
|
34
|
-
expect(err).toBeInstanceOf(CLIError);
|
|
35
|
-
expect(err.name).toBe("CLIError");
|
|
36
|
-
expect(err.message).toBe("something went wrong");
|
|
37
|
-
expect(err.code).toBe(ExitCode.ERROR);
|
|
38
|
-
expect(err.details).toBeUndefined();
|
|
39
|
-
expect(err.recoverable).toBe(false);
|
|
40
|
-
});
|
|
41
|
-
|
|
42
|
-
it("constructs with all fields", () => {
|
|
43
|
-
const err = new CLIError(
|
|
44
|
-
"invalid input",
|
|
45
|
-
ExitCode.INVALID_USAGE,
|
|
46
|
-
"Expected --name to be a non-empty string",
|
|
47
|
-
true
|
|
48
|
-
);
|
|
49
|
-
expect(err.message).toBe("invalid input");
|
|
50
|
-
expect(err.code).toBe(ExitCode.INVALID_USAGE);
|
|
51
|
-
expect(err.details).toBe("Expected --name to be a non-empty string");
|
|
52
|
-
expect(err.recoverable).toBe(true);
|
|
53
|
-
});
|
|
54
|
-
|
|
55
|
-
it("supports INFRA_UNAVAILABLE code", () => {
|
|
56
|
-
const err = new CLIError(
|
|
57
|
-
"Cloudflare API unreachable",
|
|
58
|
-
ExitCode.INFRA_UNAVAILABLE,
|
|
59
|
-
"Check your network or try again later"
|
|
60
|
-
);
|
|
61
|
-
expect(err.code).toBe(3);
|
|
62
|
-
expect(err.recoverable).toBe(false);
|
|
63
|
-
});
|
|
64
|
-
|
|
65
|
-
it("has a string representation that includes the name", () => {
|
|
66
|
-
const err = new CLIError("fail");
|
|
67
|
-
expect(err.toString()).toBe("CLIError: fail");
|
|
68
|
-
});
|
|
69
|
-
});
|
package/src/utils/errors.ts
DELETED
|
@@ -1,23 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* CLIError — structured error class for the Hoox CLI.
|
|
3
|
-
* Provides exit codes, optional details, and recoverable flag.
|
|
4
|
-
*/
|
|
5
|
-
|
|
6
|
-
export const enum ExitCode {
|
|
7
|
-
SUCCESS = 0,
|
|
8
|
-
ERROR = 1,
|
|
9
|
-
INVALID_USAGE = 2,
|
|
10
|
-
INFRA_UNAVAILABLE = 3,
|
|
11
|
-
}
|
|
12
|
-
|
|
13
|
-
export class CLIError extends Error {
|
|
14
|
-
constructor(
|
|
15
|
-
message: string,
|
|
16
|
-
public readonly code: ExitCode = ExitCode.ERROR,
|
|
17
|
-
public readonly details?: string,
|
|
18
|
-
public readonly recoverable = false
|
|
19
|
-
) {
|
|
20
|
-
super(message);
|
|
21
|
-
this.name = "CLIError";
|
|
22
|
-
}
|
|
23
|
-
}
|
|
@@ -1,180 +0,0 @@
|
|
|
1
|
-
import { describe, it, expect, beforeEach, afterEach, mock } from "bun:test";
|
|
2
|
-
import {
|
|
3
|
-
formatSuccess,
|
|
4
|
-
formatError,
|
|
5
|
-
formatTable,
|
|
6
|
-
formatJson,
|
|
7
|
-
formatKeyValue,
|
|
8
|
-
} from "./formatters.js";
|
|
9
|
-
import { CLIError, ExitCode } from "./errors.js";
|
|
10
|
-
|
|
11
|
-
/**
|
|
12
|
-
* Helper: captures everything written to process.stdout.write into a string.
|
|
13
|
-
* Returns a cleanup function that restores the original write.
|
|
14
|
-
*/
|
|
15
|
-
function captureStdout(): { output: () => string; restore: () => void } {
|
|
16
|
-
const chunks: string[] = [];
|
|
17
|
-
const originalWrite = process.stdout.write.bind(process.stdout);
|
|
18
|
-
const writeMock = mock((chunk: string | Buffer) => {
|
|
19
|
-
chunks.push(typeof chunk === "string" ? chunk : chunk.toString());
|
|
20
|
-
return true;
|
|
21
|
-
});
|
|
22
|
-
process.stdout.write = writeMock as unknown as typeof process.stdout.write;
|
|
23
|
-
|
|
24
|
-
return {
|
|
25
|
-
output: () => chunks.join(""),
|
|
26
|
-
restore: () => {
|
|
27
|
-
process.stdout.write = originalWrite;
|
|
28
|
-
},
|
|
29
|
-
};
|
|
30
|
-
}
|
|
31
|
-
|
|
32
|
-
describe("formatSuccess", () => {
|
|
33
|
-
let capture: ReturnType<typeof captureStdout>;
|
|
34
|
-
|
|
35
|
-
beforeEach(() => {
|
|
36
|
-
capture = captureStdout();
|
|
37
|
-
});
|
|
38
|
-
|
|
39
|
-
afterEach(() => {
|
|
40
|
-
capture.restore();
|
|
41
|
-
});
|
|
42
|
-
|
|
43
|
-
it("outputs human-formatted success", () => {
|
|
44
|
-
formatSuccess("Deploy complete");
|
|
45
|
-
const out = capture.output();
|
|
46
|
-
expect(out).toContain("✓");
|
|
47
|
-
|
|
48
|
-
// Check status indicator is present (just the checkmark)
|
|
49
|
-
expect(out).toContain("✓");
|
|
50
|
-
expect(out).not.toContain("✗");
|
|
51
|
-
});
|
|
52
|
-
});
|
|
53
|
-
|
|
54
|
-
describe("formatTable", () => {
|
|
55
|
-
let capture: ReturnType<typeof captureStdout>;
|
|
56
|
-
|
|
57
|
-
beforeEach(() => {
|
|
58
|
-
capture = captureStdout();
|
|
59
|
-
});
|
|
60
|
-
|
|
61
|
-
afterEach(() => {
|
|
62
|
-
capture.restore();
|
|
63
|
-
});
|
|
64
|
-
|
|
65
|
-
const sampleRows = [
|
|
66
|
-
{ name: "alpha", status: "ok" },
|
|
67
|
-
{ name: "beta-long", status: "error" },
|
|
68
|
-
];
|
|
69
|
-
|
|
70
|
-
it("outputs human-formatted table with box drawing", () => {
|
|
71
|
-
formatTable(sampleRows);
|
|
72
|
-
const out = capture.output();
|
|
73
|
-
expect(out).toContain("┌");
|
|
74
|
-
expect(out).toContain("┬");
|
|
75
|
-
expect(out).toContain("┐");
|
|
76
|
-
expect(out).toContain("├");
|
|
77
|
-
expect(out).toContain("┼");
|
|
78
|
-
expect(out).toContain("┤");
|
|
79
|
-
expect(out).toContain("└");
|
|
80
|
-
expect(out).toContain("┴");
|
|
81
|
-
expect(out).toContain("┘");
|
|
82
|
-
expect(out).toContain("alpha");
|
|
83
|
-
expect(out).toContain("beta-long");
|
|
84
|
-
expect(out).toContain("ok");
|
|
85
|
-
expect(out).toContain("error");
|
|
86
|
-
});
|
|
87
|
-
|
|
88
|
-
it("outputs JSON array when json=true", () => {
|
|
89
|
-
formatTable(sampleRows, { json: true });
|
|
90
|
-
const out = capture.output();
|
|
91
|
-
const parsed = JSON.parse(out);
|
|
92
|
-
expect(parsed).toEqual(sampleRows);
|
|
93
|
-
});
|
|
94
|
-
|
|
95
|
-
it("outputs nothing when quiet=true", () => {
|
|
96
|
-
formatTable(sampleRows, { quiet: true });
|
|
97
|
-
expect(capture.output()).toBe("");
|
|
98
|
-
});
|
|
99
|
-
|
|
100
|
-
it("outputs (empty) for empty rows", () => {
|
|
101
|
-
formatTable([]);
|
|
102
|
-
const out = capture.output();
|
|
103
|
-
expect(out).toContain("(empty)");
|
|
104
|
-
expect(out).not.toContain("┌");
|
|
105
|
-
});
|
|
106
|
-
});
|
|
107
|
-
|
|
108
|
-
describe("formatJson", () => {
|
|
109
|
-
let capture: ReturnType<typeof captureStdout>;
|
|
110
|
-
|
|
111
|
-
beforeEach(() => {
|
|
112
|
-
capture = captureStdout();
|
|
113
|
-
});
|
|
114
|
-
|
|
115
|
-
afterEach(() => {
|
|
116
|
-
capture.restore();
|
|
117
|
-
});
|
|
118
|
-
|
|
119
|
-
it("outputs pretty JSON by default", () => {
|
|
120
|
-
formatJson({ key: "value" });
|
|
121
|
-
const out = capture.output();
|
|
122
|
-
const parsed = JSON.parse(out);
|
|
123
|
-
expect(parsed).toEqual({ key: "value" });
|
|
124
|
-
// Pretty-printed: contains newlines and indentation
|
|
125
|
-
expect(out).toContain("\n");
|
|
126
|
-
expect(out).toContain('"key"');
|
|
127
|
-
});
|
|
128
|
-
|
|
129
|
-
it("outputs compact JSON when quiet=true", () => {
|
|
130
|
-
formatJson({ key: "value" }, { quiet: true });
|
|
131
|
-
const out = capture.output();
|
|
132
|
-
const parsed = JSON.parse(out);
|
|
133
|
-
expect(parsed).toEqual({ key: "value" });
|
|
134
|
-
// Compact: single line of content (console.log appends newline)
|
|
135
|
-
expect(out.trim()).not.toContain("\n");
|
|
136
|
-
});
|
|
137
|
-
|
|
138
|
-
it("outputs pretty JSON when json=true", () => {
|
|
139
|
-
formatJson({ key: "value" }, { json: true });
|
|
140
|
-
const out = capture.output();
|
|
141
|
-
expect(out).toContain("\n");
|
|
142
|
-
});
|
|
143
|
-
});
|
|
144
|
-
|
|
145
|
-
describe("formatKeyValue", () => {
|
|
146
|
-
let capture: ReturnType<typeof captureStdout>;
|
|
147
|
-
|
|
148
|
-
beforeEach(() => {
|
|
149
|
-
capture = captureStdout();
|
|
150
|
-
});
|
|
151
|
-
|
|
152
|
-
afterEach(() => {
|
|
153
|
-
capture.restore();
|
|
154
|
-
});
|
|
155
|
-
|
|
156
|
-
const pairs = { Name: "alpha", Status: "running", Region: "us-east" };
|
|
157
|
-
|
|
158
|
-
it("outputs human-formatted key-value pairs", () => {
|
|
159
|
-
formatKeyValue(pairs);
|
|
160
|
-
const out = capture.output();
|
|
161
|
-
expect(out).toContain("Name");
|
|
162
|
-
expect(out).toContain("alpha");
|
|
163
|
-
expect(out).toContain("Status");
|
|
164
|
-
expect(out).toContain("running");
|
|
165
|
-
expect(out).toContain("Region");
|
|
166
|
-
expect(out).toContain("us-east");
|
|
167
|
-
});
|
|
168
|
-
|
|
169
|
-
it("outputs JSON object when json=true", () => {
|
|
170
|
-
formatKeyValue(pairs, { json: true });
|
|
171
|
-
const out = capture.output();
|
|
172
|
-
const parsed = JSON.parse(out);
|
|
173
|
-
expect(parsed).toEqual(pairs);
|
|
174
|
-
});
|
|
175
|
-
|
|
176
|
-
it("outputs nothing when quiet=true", () => {
|
|
177
|
-
formatKeyValue(pairs, { quiet: true });
|
|
178
|
-
expect(capture.output()).toBe("");
|
|
179
|
-
});
|
|
180
|
-
});
|
package/src/utils/formatters.ts
DELETED
|
@@ -1,269 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Output formatters for the Hoox CLI.
|
|
3
|
-
* Each function respects --json / --quiet flags passed via FormatOptions.
|
|
4
|
-
* Human-readable mode uses ansis styling from the theme.
|
|
5
|
-
*/
|
|
6
|
-
|
|
7
|
-
import { CLIError, ExitCode } from "./errors.js";
|
|
8
|
-
import { theme, icons, stripAnsi, hr } from "./theme.js";
|
|
9
|
-
import { Command } from "commander";
|
|
10
|
-
|
|
11
|
-
export interface FormatOptions {
|
|
12
|
-
json?: boolean;
|
|
13
|
-
quiet?: boolean;
|
|
14
|
-
}
|
|
15
|
-
|
|
16
|
-
// ── Progress bar ──────────────────────────────────────────────────
|
|
17
|
-
|
|
18
|
-
const PROGRESS_BAR_WIDTH = 30;
|
|
19
|
-
|
|
20
|
-
/**
|
|
21
|
-
* Render an inline progress bar string.
|
|
22
|
-
* @param current Current step (0-based)
|
|
23
|
-
* @param total Total steps
|
|
24
|
-
* @param opts Width override
|
|
25
|
-
*/
|
|
26
|
-
export function renderProgressBar(
|
|
27
|
-
current: number,
|
|
28
|
-
total: number,
|
|
29
|
-
opts?: { width?: number }
|
|
30
|
-
): string {
|
|
31
|
-
const width = opts?.width ?? PROGRESS_BAR_WIDTH;
|
|
32
|
-
const ratio = total > 0 ? Math.min(current / total, 1) : 0;
|
|
33
|
-
const filled = Math.round(width * ratio);
|
|
34
|
-
const empty = width - filled;
|
|
35
|
-
const pct = Math.round(ratio * 100);
|
|
36
|
-
|
|
37
|
-
const bar =
|
|
38
|
-
theme.accent("[") +
|
|
39
|
-
theme.success("█".repeat(filled)) +
|
|
40
|
-
theme.dim("░".repeat(empty)) +
|
|
41
|
-
theme.accent("]") +
|
|
42
|
-
` ${pct}%`;
|
|
43
|
-
|
|
44
|
-
return bar;
|
|
45
|
-
}
|
|
46
|
-
|
|
47
|
-
/**
|
|
48
|
-
* Render a multi-line progress display for sequential steps.
|
|
49
|
-
* Returns lines suitable for overwriting via \r or manual clear.
|
|
50
|
-
*/
|
|
51
|
-
export function renderStepProgress(
|
|
52
|
-
steps: Array<{
|
|
53
|
-
name: string;
|
|
54
|
-
status: "pending" | "running" | "done" | "failed";
|
|
55
|
-
}>
|
|
56
|
-
): string {
|
|
57
|
-
const iconMap: Record<string, string> = {
|
|
58
|
-
pending: theme.dim("○"),
|
|
59
|
-
running: theme.info("◌"),
|
|
60
|
-
done: theme.success("●"),
|
|
61
|
-
failed: theme.error("✗"),
|
|
62
|
-
};
|
|
63
|
-
|
|
64
|
-
return steps
|
|
65
|
-
.map((s) => ` ${iconMap[s.status]} ${theme.bold(s.name)}`)
|
|
66
|
-
.join("\n");
|
|
67
|
-
}
|
|
68
|
-
|
|
69
|
-
// ── Basic output ──────────────────────────────────────────────────
|
|
70
|
-
|
|
71
|
-
/**
|
|
72
|
-
* Output a success message.
|
|
73
|
-
* - JSON mode: {"success":true,"message":"..."}
|
|
74
|
-
* - Quiet mode: prints nothing
|
|
75
|
-
* - Human mode: green "✓ message"
|
|
76
|
-
*/
|
|
77
|
-
export function formatSuccess(message: string, opts?: FormatOptions): void {
|
|
78
|
-
if (opts?.quiet) return;
|
|
79
|
-
|
|
80
|
-
if (opts?.json) {
|
|
81
|
-
process.stdout.write(JSON.stringify({ success: true, message }) + "\n");
|
|
82
|
-
return;
|
|
83
|
-
}
|
|
84
|
-
|
|
85
|
-
process.stdout.write(`${theme.success(icons.success)} ${message}\n`);
|
|
86
|
-
}
|
|
87
|
-
|
|
88
|
-
/**
|
|
89
|
-
* Output an error message.
|
|
90
|
-
* - JSON mode: {"success":false,"error":"...","code":1,"details":"..."}
|
|
91
|
-
* - Quiet mode: prints only the error message (no icon)
|
|
92
|
-
* - Human mode: red "✗ message" + optional details indented
|
|
93
|
-
*/
|
|
94
|
-
export function formatError(error: Error | string, opts?: FormatOptions): void {
|
|
95
|
-
const message = typeof error === "string" ? error : error.message;
|
|
96
|
-
const cliError = error instanceof CLIError ? error : null;
|
|
97
|
-
|
|
98
|
-
if (opts?.json) {
|
|
99
|
-
const output: Record<string, unknown> = {
|
|
100
|
-
success: false,
|
|
101
|
-
error: message,
|
|
102
|
-
code: cliError?.code ?? ExitCode.ERROR,
|
|
103
|
-
};
|
|
104
|
-
if (cliError?.details) {
|
|
105
|
-
output.details = cliError.details;
|
|
106
|
-
}
|
|
107
|
-
process.stdout.write(JSON.stringify(output) + "\n");
|
|
108
|
-
return;
|
|
109
|
-
}
|
|
110
|
-
|
|
111
|
-
if (opts?.quiet) {
|
|
112
|
-
process.stdout.write(`${message}\n`);
|
|
113
|
-
return;
|
|
114
|
-
}
|
|
115
|
-
|
|
116
|
-
process.stdout.write(`${theme.error(icons.error)} ${message}\n`);
|
|
117
|
-
|
|
118
|
-
if (cliError?.details) {
|
|
119
|
-
process.stdout.write(` ${theme.dim(cliError.details)}\n`);
|
|
120
|
-
}
|
|
121
|
-
}
|
|
122
|
-
|
|
123
|
-
/**
|
|
124
|
-
* Output tabular data with enhanced box-drawing borders.
|
|
125
|
-
* - JSON mode: JSON array of objects
|
|
126
|
-
* - Quiet mode: prints nothing
|
|
127
|
-
* - Human mode: themed box-drawn table with column headers
|
|
128
|
-
*/
|
|
129
|
-
export function formatTable(
|
|
130
|
-
rows: Record<string, string>[],
|
|
131
|
-
opts?: FormatOptions
|
|
132
|
-
): void {
|
|
133
|
-
if (opts?.quiet) return;
|
|
134
|
-
|
|
135
|
-
if (opts?.json) {
|
|
136
|
-
process.stdout.write(JSON.stringify(rows, null, 2) + "\n");
|
|
137
|
-
return;
|
|
138
|
-
}
|
|
139
|
-
|
|
140
|
-
if (rows.length === 0) {
|
|
141
|
-
process.stdout.write(`${theme.dim("(empty)")}\n`);
|
|
142
|
-
return;
|
|
143
|
-
}
|
|
144
|
-
|
|
145
|
-
// Collect all keys in display order (first row defines column order)
|
|
146
|
-
const keys = Object.keys(rows[0]);
|
|
147
|
-
|
|
148
|
-
// Calculate column widths
|
|
149
|
-
const widths: Record<string, number> = {};
|
|
150
|
-
for (const key of keys) {
|
|
151
|
-
widths[key] = stripAnsi(key).length;
|
|
152
|
-
for (const row of rows) {
|
|
153
|
-
const value = row[key] ?? "";
|
|
154
|
-
const stripped = stripAnsi(value).length;
|
|
155
|
-
widths[key] = Math.max(widths[key], stripped);
|
|
156
|
-
}
|
|
157
|
-
}
|
|
158
|
-
|
|
159
|
-
// Build border segments
|
|
160
|
-
const topParts = keys.map((k) => "─".repeat(widths[k] + 2));
|
|
161
|
-
const topBorder = `┌${topParts.join("┬")}┐`;
|
|
162
|
-
const sepBorder = `├${topParts.join("┼")}┤`;
|
|
163
|
-
const botBorder = `└${topParts.join("┴")}┘`;
|
|
164
|
-
|
|
165
|
-
// Header row with themed styling
|
|
166
|
-
const headerCells = keys.map((k) =>
|
|
167
|
-
theme.bold(k.padEnd(widths[k])).toString()
|
|
168
|
-
);
|
|
169
|
-
const headerRow = `│ ${headerCells.join(" │ ")} │`;
|
|
170
|
-
|
|
171
|
-
// Data rows
|
|
172
|
-
const dataRows = rows.map((row) => {
|
|
173
|
-
const cells = keys.map((k) => {
|
|
174
|
-
const value = row[k] ?? "";
|
|
175
|
-
return value.padEnd(widths[k]);
|
|
176
|
-
});
|
|
177
|
-
return `│ ${cells.join(" │ ")} │`;
|
|
178
|
-
});
|
|
179
|
-
|
|
180
|
-
process.stdout.write(
|
|
181
|
-
`${theme.dim(topBorder)}\n` +
|
|
182
|
-
`${headerRow}\n` +
|
|
183
|
-
`${theme.dim(sepBorder)}\n` +
|
|
184
|
-
`${dataRows.join("\n")}\n` +
|
|
185
|
-
`${theme.dim(botBorder)}\n`
|
|
186
|
-
);
|
|
187
|
-
}
|
|
188
|
-
|
|
189
|
-
/**
|
|
190
|
-
* Output arbitrary data as JSON.
|
|
191
|
-
* - JSON / Quiet mode: JSON stringified (compact or pretty)
|
|
192
|
-
* - Human mode: JSON with 2-space indent
|
|
193
|
-
*/
|
|
194
|
-
export function formatJson(data: unknown, opts?: FormatOptions): void {
|
|
195
|
-
const json = opts?.quiet
|
|
196
|
-
? JSON.stringify(data)
|
|
197
|
-
: JSON.stringify(data, null, 2);
|
|
198
|
-
|
|
199
|
-
process.stdout.write(`${json}\n`);
|
|
200
|
-
}
|
|
201
|
-
|
|
202
|
-
/**
|
|
203
|
-
* Output key-value pairs.
|
|
204
|
-
* - JSON mode: JSON object
|
|
205
|
-
* - Quiet mode: prints nothing
|
|
206
|
-
* - Human mode: "label: value" with themed labels
|
|
207
|
-
*/
|
|
208
|
-
export function formatKeyValue(
|
|
209
|
-
pairs: Record<string, string>,
|
|
210
|
-
opts?: FormatOptions
|
|
211
|
-
): void {
|
|
212
|
-
if (opts?.quiet) return;
|
|
213
|
-
|
|
214
|
-
if (opts?.json) {
|
|
215
|
-
process.stdout.write(JSON.stringify(pairs, null, 2) + "\n");
|
|
216
|
-
return;
|
|
217
|
-
}
|
|
218
|
-
|
|
219
|
-
const maxKeyLen = Math.max(
|
|
220
|
-
...Object.keys(pairs).map((k) => stripAnsi(k).length)
|
|
221
|
-
);
|
|
222
|
-
|
|
223
|
-
for (const [key, value] of Object.entries(pairs)) {
|
|
224
|
-
const paddedKey = key.padEnd(maxKeyLen);
|
|
225
|
-
process.stdout.write(
|
|
226
|
-
` ${theme.key(paddedKey)} ${theme.dim(":")} ${value}\n`
|
|
227
|
-
);
|
|
228
|
-
}
|
|
229
|
-
}
|
|
230
|
-
|
|
231
|
-
/**
|
|
232
|
-
* Output a section header with decorative separator.
|
|
233
|
-
*/
|
|
234
|
-
export function formatHeader(text: string, opts?: FormatOptions): void {
|
|
235
|
-
if (opts?.quiet) return;
|
|
236
|
-
if (opts?.json) return;
|
|
237
|
-
|
|
238
|
-
process.stdout.write(`\n${theme.heading(text)}\n`);
|
|
239
|
-
process.stdout.write(`${hr()}\n`);
|
|
240
|
-
}
|
|
241
|
-
|
|
242
|
-
/**
|
|
243
|
-
* Output a bullet-point list with optional icon per item.
|
|
244
|
-
*/
|
|
245
|
-
export function formatList(
|
|
246
|
-
items: string[],
|
|
247
|
-
opts?: { icon?: string; json?: boolean; quiet?: boolean }
|
|
248
|
-
): void {
|
|
249
|
-
if (opts?.quiet) return;
|
|
250
|
-
|
|
251
|
-
if (opts?.json) {
|
|
252
|
-
process.stdout.write(JSON.stringify(items, null, 2) + "\n");
|
|
253
|
-
return;
|
|
254
|
-
}
|
|
255
|
-
|
|
256
|
-
const icon = opts?.icon ?? icons.arrow;
|
|
257
|
-
for (const item of items) {
|
|
258
|
-
process.stdout.write(` ${icon} ${item}\n`);
|
|
259
|
-
}
|
|
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
|
-
}
|
package/src/utils/theme.ts
DELETED
|
@@ -1,94 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Hoox CLI theme — ansis color palette, icons, and visual primitives.
|
|
3
|
-
* Used by formatters and commands for consistent terminal output.
|
|
4
|
-
*/
|
|
5
|
-
|
|
6
|
-
import ansis from "ansis";
|
|
7
|
-
|
|
8
|
-
// ── Color primitives ──────────────────────────────────────────────
|
|
9
|
-
const green = ansis.green;
|
|
10
|
-
const red = ansis.red;
|
|
11
|
-
const yellow = ansis.yellow;
|
|
12
|
-
const blue = ansis.blue;
|
|
13
|
-
const cyan = ansis.cyan;
|
|
14
|
-
const magenta = ansis.magenta;
|
|
15
|
-
const dim = ansis.dim;
|
|
16
|
-
const bold = ansis.bold;
|
|
17
|
-
const italic = ansis.italic;
|
|
18
|
-
const underline = ansis.underline;
|
|
19
|
-
const gray = ansis.gray;
|
|
20
|
-
const white = ansis.white;
|
|
21
|
-
const black = ansis.black;
|
|
22
|
-
const bgGreen = ansis.bgGreen;
|
|
23
|
-
const bgRed = ansis.bgRed;
|
|
24
|
-
const bgYellow = ansis.bgYellow;
|
|
25
|
-
const bgBlue = ansis.bgBlue;
|
|
26
|
-
|
|
27
|
-
// ── Semantic theme ────────────────────────────────────────────────
|
|
28
|
-
export const theme = {
|
|
29
|
-
success: green,
|
|
30
|
-
error: red,
|
|
31
|
-
warning: yellow,
|
|
32
|
-
info: blue,
|
|
33
|
-
accent: cyan,
|
|
34
|
-
highlight: magenta,
|
|
35
|
-
dim,
|
|
36
|
-
bold,
|
|
37
|
-
italic,
|
|
38
|
-
underline,
|
|
39
|
-
gray,
|
|
40
|
-
white,
|
|
41
|
-
black,
|
|
42
|
-
heading: bold.cyan,
|
|
43
|
-
label: dim,
|
|
44
|
-
subtle: gray,
|
|
45
|
-
muted: dim.gray,
|
|
46
|
-
value: bold.white,
|
|
47
|
-
key: dim.yellow,
|
|
48
|
-
|
|
49
|
-
// Status-specific
|
|
50
|
-
statusOk: bgGreen.black(" OK "),
|
|
51
|
-
statusWarn: bgYellow.black(" WARN "),
|
|
52
|
-
statusError: bgRed.white(" FAIL "),
|
|
53
|
-
statusInfo: bgBlue.white(" INFO "),
|
|
54
|
-
|
|
55
|
-
// Decorations
|
|
56
|
-
separator: dim("─"),
|
|
57
|
-
pipe: dim("│"),
|
|
58
|
-
corner: dim("┌┐└┘"),
|
|
59
|
-
};
|
|
60
|
-
|
|
61
|
-
// ── Icons ─────────────────────────────────────────────────────────
|
|
62
|
-
export const icons = {
|
|
63
|
-
success: "✓",
|
|
64
|
-
error: "✗",
|
|
65
|
-
warning: "!",
|
|
66
|
-
info: "i",
|
|
67
|
-
arrow: "->",
|
|
68
|
-
spinner: ["-", "\\", "|", "/"],
|
|
69
|
-
dot: "●",
|
|
70
|
-
check: "v",
|
|
71
|
-
cross: "x",
|
|
72
|
-
};
|
|
73
|
-
|
|
74
|
-
// ── Display helpers ───────────────────────────────────────────────
|
|
75
|
-
|
|
76
|
-
/** Strip ANSI escape codes from a string. */
|
|
77
|
-
export function stripAnsi(str: string): string {
|
|
78
|
-
return ansis.strip(str);
|
|
79
|
-
}
|
|
80
|
-
|
|
81
|
-
/** Render a horizontal rule using the separator character. */
|
|
82
|
-
export function hr(width: number = 60): string {
|
|
83
|
-
return theme.separator.repeat(width);
|
|
84
|
-
}
|
|
85
|
-
|
|
86
|
-
/** Format a key-value pair for aligned output. */
|
|
87
|
-
export function kv(key: string, value: string): string {
|
|
88
|
-
return `${theme.key(key)} ${theme.dim(":")} ${theme.value(value)}`;
|
|
89
|
-
}
|
|
90
|
-
|
|
91
|
-
/** Format a labeled value. */
|
|
92
|
-
export function tagged(label: string, value: string): string {
|
|
93
|
-
return `${theme.key(label)} ${theme.dim(":")} ${theme.value(value)}`;
|
|
94
|
-
}
|