@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.
- package/README.md +2 -0
- package/package.json +5 -3
- package/src/commands/check/check-command.test.ts +28 -20
- package/src/commands/check/prerequisites-command.ts +9 -5
- package/src/commands/clone/clone-command.ts +1 -10
- package/src/commands/config/config-command.ts +7 -11
- package/src/commands/config/env-command.ts +16 -35
- package/src/commands/config/kv-command.ts +33 -72
- package/src/commands/db/db-command.ts +23 -25
- package/src/commands/deploy/deploy-command.ts +133 -52
- package/src/commands/deploy/telegram-service.ts +21 -6
- package/src/commands/dev/dev-command.ts +7 -16
- package/src/commands/infra/infra-command.test.ts +2 -2
- package/src/commands/infra/infra-command.ts +45 -11
- package/src/commands/init/init-command.test.ts +2 -2
- package/src/commands/monitor/monitor-command.test.ts +44 -16
- package/src/commands/monitor/monitor-command.ts +45 -21
- package/src/commands/monitor/monitor-service.ts +19 -4
- package/src/commands/repair/repair-command.test.ts +35 -15
- package/src/commands/repair/repair-command.ts +39 -18
- package/src/commands/repair/repair-service.ts +48 -12
- package/src/commands/test/test-command.ts +4 -10
- package/src/commands/tui/index.ts +1 -0
- package/src/commands/tui/tui-command.ts +87 -0
- package/src/commands/update/index.ts +1 -0
- package/src/commands/update/update-command.ts +51 -0
- package/src/commands/waf/waf-command.test.ts +1 -1
- package/src/commands/waf/waf-command.ts +11 -11
- package/src/index.ts +52 -1
- package/src/services/cloudflare/cloudflare-service.test.ts +12 -8
- package/src/services/cloudflare/cloudflare-service.ts +8 -10
- package/src/services/cloudflare/types.ts +6 -5
- package/src/services/db/db-service.ts +8 -17
- package/src/services/env/env-service.test.ts +41 -15
- package/src/services/env/env-service.ts +276 -46
- package/src/services/kv/kv-sync-service.ts +117 -34
- package/src/services/prerequisites/prerequisites-service.ts +137 -29
- package/src/services/secrets/index.ts +0 -1
- package/src/services/secrets/secrets-service.test.ts +34 -25
- package/src/services/secrets/secrets-service.ts +10 -16
- package/src/services/secrets/types.ts +4 -11
- package/src/services/update/index.ts +2 -0
- package/src/services/update/update-service.test.ts +76 -0
- package/src/services/update/update-service.ts +193 -0
- package/src/ui/banner.ts +5 -5
- package/src/ui/menu.ts +6 -1
- package/src/utils/formatters.ts +21 -4
- 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<
|
|
143
|
+
async generateDevVars(workerName: string): Promise<Result<string>> {
|
|
144
144
|
const worker = this.config.workers[workerName];
|
|
145
145
|
if (!worker) {
|
|
146
146
|
return {
|
|
147
|
-
|
|
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 {
|
|
162
|
+
return { ok: true, value: devVarsPath };
|
|
163
163
|
} catch (err: unknown) {
|
|
164
164
|
const message = err instanceof Error ? err.message : String(err);
|
|
165
|
-
return {
|
|
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
|
-
|
|
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 (
|
|
219
|
-
return {
|
|
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
|
|
6
|
+
// Result
|
|
11
7
|
// ---------------------------------------------------------------------------
|
|
12
8
|
|
|
13
|
-
|
|
9
|
+
import type { Result } from "@jango-blockchained/hoox-shared";
|
|
14
10
|
|
|
15
|
-
export
|
|
16
|
-
|
|
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,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(
|
|
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
|
+
}
|
package/src/utils/formatters.ts
CHANGED
|
@@ -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<{
|
|
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(
|
|
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(
|
|
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
|
+
}
|
package/src/utils/theme.ts
CHANGED