@jango-blockchained/hoox-cli 0.5.2 → 0.5.7
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 +32989 -0
- package/package.json +10 -15
- package/src/commands/check/check-command.test.ts +0 -476
- package/src/commands/check/check-command.ts +0 -1087
- 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 -97
- package/src/commands/check/types.ts +0 -103
- package/src/commands/clone/clone-command.test.ts +0 -440
- 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 -896
- package/src/commands/config/env-command.test.ts +0 -43
- package/src/commands/config/env-command.ts +0 -292
- 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 -130
- 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 -311
- package/src/commands/db/index.ts +0 -1
- package/src/commands/deploy/deploy-command.test.ts +0 -324
- package/src/commands/deploy/deploy-command.ts +0 -1054
- 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 -405
- package/src/commands/dev/dev-command.ts +0 -398
- package/src/commands/dev/index.ts +0 -1
- package/src/commands/disclaimer/disclaimer-command.ts +0 -21
- package/src/commands/disclaimer/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 -854
- package/src/commands/init/init-command.ts +0 -597
- 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 -293
- 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 -271
- 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 -243
- package/src/commands/repair/repair-command.ts +0 -220
- package/src/commands/repair/repair-service.ts +0 -164
- package/src/commands/repair/types.ts +0 -13
- package/src/commands/schema/index.ts +0 -1
- package/src/commands/schema/schema-command.ts +0 -137
- package/src/commands/test/index.ts +0 -2
- package/src/commands/test/test-command.test.ts +0 -318
- 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 -109
- 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 -545
- package/src/index.ts +0 -261
- 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 -202
- 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 -63
- 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 -259
- package/src/services/env/env-service.ts +0 -372
- 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 -243
- 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/schema/index.ts +0 -1
- package/src/services/schema/schema-service.ts +0 -99
- package/src/services/secrets/index.ts +0 -11
- package/src/services/secrets/secrets-service.test.ts +0 -502
- 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/error-handler.ts +0 -62
- package/src/utils/errors.test.ts +0 -69
- package/src/utils/errors.ts +0 -24
- package/src/utils/formatters.test.ts +0 -180
- package/src/utils/formatters.ts +0 -269
- package/src/utils/git.ts +0 -134
- package/src/utils/theme.ts +0 -92
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,24 +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
|
-
CommandFailed = -1,
|
|
12
|
-
}
|
|
13
|
-
|
|
14
|
-
export class CLIError extends Error {
|
|
15
|
-
constructor(
|
|
16
|
-
message: string,
|
|
17
|
-
public readonly code: ExitCode = ExitCode.ERROR,
|
|
18
|
-
public readonly details?: string,
|
|
19
|
-
public readonly recoverable = false
|
|
20
|
-
) {
|
|
21
|
-
super(message);
|
|
22
|
-
this.name = "CLIError";
|
|
23
|
-
}
|
|
24
|
-
}
|
|
@@ -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/git.ts
DELETED
|
@@ -1,134 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Shared git utilities for Hoox CLI.
|
|
3
|
-
*
|
|
4
|
-
* All functions use Bun.spawn for asynchronous git operations.
|
|
5
|
-
* Bun is a global in the Bun runtime — no import needed.
|
|
6
|
-
*/
|
|
7
|
-
|
|
8
|
-
// ---------------------------------------------------------------------------
|
|
9
|
-
// Repository and working tree checks
|
|
10
|
-
// ---------------------------------------------------------------------------
|
|
11
|
-
|
|
12
|
-
/**
|
|
13
|
-
* Check if `cwd` is inside a git work-tree.
|
|
14
|
-
*/
|
|
15
|
-
export async function isGitRepo(cwd: string): Promise<boolean> {
|
|
16
|
-
const proc = Bun.spawn(["git", "rev-parse", "--is-inside-work-tree"], {
|
|
17
|
-
cwd,
|
|
18
|
-
stdout: "pipe",
|
|
19
|
-
stderr: "pipe",
|
|
20
|
-
});
|
|
21
|
-
const exitCode = await proc.exited;
|
|
22
|
-
return exitCode === 0;
|
|
23
|
-
}
|
|
24
|
-
|
|
25
|
-
/**
|
|
26
|
-
* Check whether a path inside `cwd` is a registered git submodule.
|
|
27
|
-
*/
|
|
28
|
-
export async function isSubmodule(
|
|
29
|
-
cwd: string,
|
|
30
|
-
submodulePath: string
|
|
31
|
-
): Promise<boolean> {
|
|
32
|
-
const proc = Bun.spawn(["git", "submodule", "status", "--", submodulePath], {
|
|
33
|
-
cwd,
|
|
34
|
-
stdout: "pipe",
|
|
35
|
-
stderr: "pipe",
|
|
36
|
-
});
|
|
37
|
-
const exitCode = await proc.exited;
|
|
38
|
-
if (exitCode !== 0) return false;
|
|
39
|
-
const out = (await new Response(proc.stdout).text()).trim();
|
|
40
|
-
return out.length > 0;
|
|
41
|
-
}
|
|
42
|
-
|
|
43
|
-
/**
|
|
44
|
-
* Check if a specific file inside `dir` is tracked by git.
|
|
45
|
-
*/
|
|
46
|
-
export async function isGitTracked(
|
|
47
|
-
dir: string,
|
|
48
|
-
filename: string
|
|
49
|
-
): Promise<boolean> {
|
|
50
|
-
try {
|
|
51
|
-
const proc = Bun.spawn(["git", "ls-files", "--error-unmatch", filename], {
|
|
52
|
-
cwd: dir,
|
|
53
|
-
stdout: "pipe",
|
|
54
|
-
stderr: "pipe",
|
|
55
|
-
});
|
|
56
|
-
const exitCode = await proc.exited;
|
|
57
|
-
return exitCode === 0;
|
|
58
|
-
} catch {
|
|
59
|
-
return false;
|
|
60
|
-
}
|
|
61
|
-
}
|
|
62
|
-
|
|
63
|
-
// ---------------------------------------------------------------------------
|
|
64
|
-
// Git operations
|
|
65
|
-
// ---------------------------------------------------------------------------
|
|
66
|
-
|
|
67
|
-
/**
|
|
68
|
-
* Run `git pull --ff-only` inside `cwd`.
|
|
69
|
-
*/
|
|
70
|
-
export async function gitPull(cwd: string): Promise<string> {
|
|
71
|
-
const proc = Bun.spawn(["git", "pull", "--ff-only"], {
|
|
72
|
-
cwd,
|
|
73
|
-
stdout: "pipe",
|
|
74
|
-
stderr: "pipe",
|
|
75
|
-
});
|
|
76
|
-
const exitCode = await proc.exited;
|
|
77
|
-
const stdout = await new Response(proc.stdout).text();
|
|
78
|
-
const stderr = await new Response(proc.stderr).text();
|
|
79
|
-
|
|
80
|
-
if (exitCode !== 0) {
|
|
81
|
-
throw new Error(stderr.trim() || `git pull failed (exit ${exitCode})`);
|
|
82
|
-
}
|
|
83
|
-
return stdout.trim();
|
|
84
|
-
}
|
|
85
|
-
|
|
86
|
-
/**
|
|
87
|
-
* Run `git submodule update --remote --init` for a specific submodule path.
|
|
88
|
-
*/
|
|
89
|
-
export async function gitSubmoduleUpdate(
|
|
90
|
-
cwd: string,
|
|
91
|
-
submodulePath: string
|
|
92
|
-
): Promise<string> {
|
|
93
|
-
const proc = Bun.spawn(
|
|
94
|
-
["git", "submodule", "update", "--remote", "--init", "--", submodulePath],
|
|
95
|
-
{
|
|
96
|
-
cwd,
|
|
97
|
-
stdout: "pipe",
|
|
98
|
-
stderr: "pipe",
|
|
99
|
-
}
|
|
100
|
-
);
|
|
101
|
-
const exitCode = await proc.exited;
|
|
102
|
-
const stdout = await new Response(proc.stdout).text();
|
|
103
|
-
const stderr = await new Response(proc.stderr).text();
|
|
104
|
-
|
|
105
|
-
if (exitCode !== 0) {
|
|
106
|
-
throw new Error(
|
|
107
|
-
stderr.trim() || `git submodule update failed (exit ${exitCode})`
|
|
108
|
-
);
|
|
109
|
-
}
|
|
110
|
-
return stdout.trim();
|
|
111
|
-
}
|
|
112
|
-
|
|
113
|
-
/**
|
|
114
|
-
* Run `git rm --cached` to remove a file from git tracking while keeping it
|
|
115
|
-
* on disk.
|
|
116
|
-
*/
|
|
117
|
-
export async function gitUntrackFile(
|
|
118
|
-
dir: string,
|
|
119
|
-
filename: string
|
|
120
|
-
): Promise<void> {
|
|
121
|
-
await new Promise<void>((resolve, reject) => {
|
|
122
|
-
const proc = Bun.spawn(["git", "rm", "--cached", filename], {
|
|
123
|
-
cwd: dir,
|
|
124
|
-
stdout: "pipe",
|
|
125
|
-
stderr: "pipe",
|
|
126
|
-
});
|
|
127
|
-
proc.exited
|
|
128
|
-
.then((code: number) => {
|
|
129
|
-
if (code === 0) resolve();
|
|
130
|
-
else reject(new Error(`git rm --cached failed with code ${code}`));
|
|
131
|
-
})
|
|
132
|
-
.catch(reject);
|
|
133
|
-
});
|
|
134
|
-
}
|