envhub-cli 0.3.1 → 0.4.0

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 (61) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +96 -34
  3. package/dist/cli.d.ts.map +1 -1
  4. package/dist/cli.js +19 -6
  5. package/dist/cli.js.map +1 -1
  6. package/dist/commands/cat.d.ts +5 -1
  7. package/dist/commands/cat.d.ts.map +1 -1
  8. package/dist/commands/cat.js +27 -6
  9. package/dist/commands/cat.js.map +1 -1
  10. package/dist/commands/delete.d.ts.map +1 -1
  11. package/dist/commands/delete.js +5 -2
  12. package/dist/commands/delete.js.map +1 -1
  13. package/dist/commands/doctor.d.ts +39 -0
  14. package/dist/commands/doctor.d.ts.map +1 -0
  15. package/dist/commands/doctor.js +946 -0
  16. package/dist/commands/doctor.js.map +1 -0
  17. package/dist/commands/init.d.ts.map +1 -1
  18. package/dist/commands/init.js +89 -16
  19. package/dist/commands/init.js.map +1 -1
  20. package/dist/commands/list.d.ts +5 -1
  21. package/dist/commands/list.d.ts.map +1 -1
  22. package/dist/commands/list.js +13 -3
  23. package/dist/commands/list.js.map +1 -1
  24. package/dist/commands/pull.d.ts +6 -1
  25. package/dist/commands/pull.d.ts.map +1 -1
  26. package/dist/commands/pull.js +75 -12
  27. package/dist/commands/pull.js.map +1 -1
  28. package/dist/commands/push.d.ts.map +1 -1
  29. package/dist/commands/push.js +111 -33
  30. package/dist/commands/push.js.map +1 -1
  31. package/dist/config/config.d.ts.map +1 -1
  32. package/dist/config/config.js +7 -2
  33. package/dist/config/config.js.map +1 -1
  34. package/dist/config/config.schema.d.ts.map +1 -1
  35. package/dist/config/config.schema.js +6 -0
  36. package/dist/config/config.schema.js.map +1 -1
  37. package/dist/utils/diff.d.ts +9 -1
  38. package/dist/utils/diff.d.ts.map +1 -1
  39. package/dist/utils/diff.js +22 -8
  40. package/dist/utils/diff.js.map +1 -1
  41. package/dist/utils/env-parser.d.ts +9 -1
  42. package/dist/utils/env-parser.d.ts.map +1 -1
  43. package/dist/utils/env-parser.js +35 -4
  44. package/dist/utils/env-parser.js.map +1 -1
  45. package/dist/utils/envhub-header.d.ts +4 -0
  46. package/dist/utils/envhub-header.d.ts.map +1 -1
  47. package/dist/utils/envhub-header.js +18 -0
  48. package/dist/utils/envhub-header.js.map +1 -1
  49. package/dist/utils/logger.d.ts +44 -39
  50. package/dist/utils/logger.d.ts.map +1 -1
  51. package/dist/utils/logger.js +172 -46
  52. package/dist/utils/logger.js.map +1 -1
  53. package/dist/utils/pull-dry-run-ui.d.ts +11 -0
  54. package/dist/utils/pull-dry-run-ui.d.ts.map +1 -0
  55. package/dist/utils/pull-dry-run-ui.js +157 -0
  56. package/dist/utils/pull-dry-run-ui.js.map +1 -0
  57. package/dist/utils/push-preview-ui.d.ts +10 -0
  58. package/dist/utils/push-preview-ui.d.ts.map +1 -0
  59. package/dist/utils/push-preview-ui.js +134 -0
  60. package/dist/utils/push-preview-ui.js.map +1 -0
  61. package/package.json +4 -4
@@ -0,0 +1,946 @@
1
+ import { configManager } from "../config/config.js";
2
+ import { ProviderFactory } from "../providers/provider.factory.js";
3
+ import { execFile } from "node:child_process";
4
+ import { createRequire } from "node:module";
5
+ import { promisify } from "node:util";
6
+ import { STSClient, GetCallerIdentityCommand } from "@aws-sdk/client-sts";
7
+ import { fromIni } from "@aws-sdk/credential-providers";
8
+ import chalk from "chalk";
9
+ import { note, spinner as clackSpinner, log as clackLog } from "@clack/prompts";
10
+ import { logger } from "../utils/logger.js";
11
+ const require = createRequire(import.meta.url);
12
+ const pkg = require("../../package.json");
13
+ const CHECK_TIMEOUT_MS = 10_000;
14
+ const execFileAsync = promisify(execFile);
15
+ class OperationTimeoutError extends Error {
16
+ constructor(operation, timeoutMs) {
17
+ super(`${operation} timed out after ${Math.floor(timeoutMs / 1000)}s.`);
18
+ this.name = "OperationTimeoutError";
19
+ }
20
+ }
21
+ function isTimeoutError(error) {
22
+ if (!(error instanceof Error))
23
+ return false;
24
+ const message = error.message.toLowerCase();
25
+ return (error.name === "OperationTimeoutError" ||
26
+ message.includes("timed out") ||
27
+ message.includes("etimedout"));
28
+ }
29
+ async function withTimeout(operation, task, timeoutMs = CHECK_TIMEOUT_MS) {
30
+ let timer = null;
31
+ const timeoutPromise = new Promise((_, reject) => {
32
+ timer = setTimeout(() => {
33
+ reject(new OperationTimeoutError(operation, timeoutMs));
34
+ }, timeoutMs);
35
+ });
36
+ try {
37
+ return await Promise.race([task, timeoutPromise]);
38
+ }
39
+ finally {
40
+ if (timer) {
41
+ clearTimeout(timer);
42
+ }
43
+ }
44
+ }
45
+ function iconForStatus(status) {
46
+ if (status === "pass")
47
+ return chalk.green("✔");
48
+ if (status === "warn")
49
+ return chalk.yellow("⚠");
50
+ return chalk.red("✖");
51
+ }
52
+ function groupForCheck(id) {
53
+ if (id === "version.check")
54
+ return "Version";
55
+ if (id === "config.load" || id === "prefix")
56
+ return "Configuration";
57
+ if (id === "provider.init" ||
58
+ id === "provider.identity" ||
59
+ id === "provider.identity_verified" ||
60
+ id === "provider.reachability_and_auth") {
61
+ return "Provider";
62
+ }
63
+ return "Permissions";
64
+ }
65
+ function renderGroupHeader(group) {
66
+ if (process.stdout.isTTY && process.stderr.isTTY) {
67
+ clackLog.step(chalk.bold.cyan(group), { spacing: 0 });
68
+ return;
69
+ }
70
+ logger.log(chalk.bold.cyan(`◇ ${group}`));
71
+ }
72
+ function summarizeChecks(checks) {
73
+ return checks.reduce((summary, check) => {
74
+ summary[check.status] += 1;
75
+ return summary;
76
+ }, { pass: 0, warn: 0, fail: 0 });
77
+ }
78
+ function classifyProviderFailure(providerName, errorMessage) {
79
+ const normalized = errorMessage.toLowerCase();
80
+ if (normalized.includes("access denied") ||
81
+ normalized.includes("unauthorized") ||
82
+ normalized.includes("forbidden") ||
83
+ normalized.includes("permission") ||
84
+ normalized.includes("not authorized") ||
85
+ normalized.includes("rbac")) {
86
+ return {
87
+ message: `${providerName} is reachable, but access is denied.`,
88
+ details: "Verify your cloud role/policy has permission to list secrets for this project/account.",
89
+ };
90
+ }
91
+ if (normalized.includes("credential") ||
92
+ normalized.includes("token") ||
93
+ normalized.includes("profile") ||
94
+ normalized.includes("az login") ||
95
+ normalized.includes("gcloud auth") ||
96
+ normalized.includes("expired")) {
97
+ return {
98
+ message: `${providerName} credentials are missing or invalid.`,
99
+ details: "Re-authenticate with your provider CLI and confirm the configured profile/project/account.",
100
+ };
101
+ }
102
+ if (normalized.includes("timeout") ||
103
+ normalized.includes("enotfound") ||
104
+ normalized.includes("econnrefused") ||
105
+ normalized.includes("eai_again") ||
106
+ normalized.includes("network") ||
107
+ normalized.includes("dns")) {
108
+ return {
109
+ message: `Could not reach ${providerName}.`,
110
+ details: "Check internet connectivity, DNS, firewall/proxy settings, and provider endpoint availability.",
111
+ };
112
+ }
113
+ return {
114
+ message: `Failed to verify ${providerName} connectivity and access.`,
115
+ details: "Check provider credentials, account permissions, and network connectivity.",
116
+ };
117
+ }
118
+ async function resolveGcpProjectName(projectId) {
119
+ try {
120
+ const { stdout } = await execFileAsync("gcloud", [
121
+ "projects",
122
+ "describe",
123
+ projectId,
124
+ "--format=value(name)",
125
+ ], {
126
+ timeout: CHECK_TIMEOUT_MS,
127
+ });
128
+ const name = stdout.trim();
129
+ return name.length > 0 ? name : null;
130
+ }
131
+ catch {
132
+ return null;
133
+ }
134
+ }
135
+ async function getGcloudActiveAccount() {
136
+ try {
137
+ const { stdout } = await execFileAsync("gcloud", [
138
+ "auth",
139
+ "list",
140
+ "--filter=status:ACTIVE",
141
+ "--format=value(account)",
142
+ ], { timeout: CHECK_TIMEOUT_MS });
143
+ const account = stdout.trim();
144
+ return account.length > 0 ? account : null;
145
+ }
146
+ catch {
147
+ return null;
148
+ }
149
+ }
150
+ async function getGcloudActiveProject() {
151
+ try {
152
+ const { stdout } = await execFileAsync("gcloud", [
153
+ "config",
154
+ "get-value",
155
+ "project",
156
+ ], { timeout: CHECK_TIMEOUT_MS });
157
+ const project = stdout.trim();
158
+ if (!project || project === "(unset)")
159
+ return null;
160
+ return project;
161
+ }
162
+ catch {
163
+ return null;
164
+ }
165
+ }
166
+ async function verifyAwsIdentity(config) {
167
+ if (!config.aws) {
168
+ return {
169
+ id: "provider.identity_verified",
170
+ title: "Provider identity verified",
171
+ status: "warn",
172
+ message: "Skipped because AWS configuration is incomplete.",
173
+ details: "Ensure 'aws.profile' and 'aws.region' are configured.",
174
+ };
175
+ }
176
+ try {
177
+ const client = new STSClient({
178
+ region: config.aws.region,
179
+ credentials: fromIni({ profile: config.aws.profile }),
180
+ });
181
+ const result = await withTimeout("AWS identity verification", client.send(new GetCallerIdentityCommand({})));
182
+ if (!result.Account || !result.Arn) {
183
+ return {
184
+ id: "provider.identity_verified",
185
+ title: "Provider identity verified",
186
+ status: "warn",
187
+ message: "Could not fully verify AWS identity.",
188
+ details: "Run 'aws sts get-caller-identity' and verify your profile credentials.",
189
+ };
190
+ }
191
+ return {
192
+ id: "provider.identity_verified",
193
+ title: "Provider identity verified",
194
+ status: "pass",
195
+ message: `Verified AWS identity: ${result.Arn} (account ${result.Account}).`,
196
+ };
197
+ }
198
+ catch (error) {
199
+ const message = error instanceof Error ? error.message : "Unknown AWS identity error.";
200
+ if (isTimeoutError(error)) {
201
+ return {
202
+ id: "provider.identity_verified",
203
+ title: "Provider identity verified",
204
+ status: "warn",
205
+ message: "Could not verify AWS identity (timed out after 10s).",
206
+ details: "Timeout while verifying AWS identity. Check network/proxy/VPN and AWS endpoint reachability.",
207
+ };
208
+ }
209
+ return {
210
+ id: "provider.identity_verified",
211
+ title: "Provider identity verified",
212
+ status: "warn",
213
+ message: "Could not verify AWS identity.",
214
+ details: `${message} Re-authenticate or verify profile '${config.aws.profile}' ` +
215
+ `(e.g. 'aws sts get-caller-identity --profile ${config.aws.profile}').`,
216
+ };
217
+ }
218
+ }
219
+ async function verifyGcpIdentity(config) {
220
+ if (!config.gcp) {
221
+ return {
222
+ id: "provider.identity_verified",
223
+ title: "Provider identity verified",
224
+ status: "warn",
225
+ message: "Skipped because GCP configuration is incomplete.",
226
+ details: "Ensure 'gcp.projectId' is configured.",
227
+ };
228
+ }
229
+ const [activeAccount, activeProject] = await Promise.all([
230
+ getGcloudActiveAccount(),
231
+ getGcloudActiveProject(),
232
+ ]);
233
+ if (!activeAccount || !activeProject) {
234
+ return {
235
+ id: "provider.identity_verified",
236
+ title: "Provider identity verified",
237
+ status: "warn",
238
+ message: "Could not verify GCP identity.",
239
+ details: "Run 'gcloud auth login' and ensure an active project is set " +
240
+ "('gcloud config set project <PROJECT_ID>').",
241
+ };
242
+ }
243
+ if (activeProject !== config.gcp.projectId) {
244
+ return {
245
+ id: "provider.identity_verified",
246
+ title: "Provider identity verified",
247
+ status: "fail",
248
+ message: `GCP context mismatch: active project '${activeProject}' does not match configured project '${config.gcp.projectId}'.`,
249
+ details: "Switch gcloud project or update '.envhubrc.json' to the intended project.",
250
+ };
251
+ }
252
+ return {
253
+ id: "provider.identity_verified",
254
+ title: "Provider identity verified",
255
+ status: "pass",
256
+ message: `Verified GCP identity: ${activeAccount} (project ${activeProject}).`,
257
+ };
258
+ }
259
+ async function verifyAzureIdentity() {
260
+ try {
261
+ const { stdout } = await execFileAsync("az", ["account", "show", "--output", "json"], {
262
+ timeout: CHECK_TIMEOUT_MS,
263
+ });
264
+ const payload = JSON.parse(stdout);
265
+ if (!payload.id || !payload.tenantId) {
266
+ return {
267
+ id: "provider.identity_verified",
268
+ title: "Provider identity verified",
269
+ status: "warn",
270
+ message: "Could not fully verify Azure identity.",
271
+ details: "Run 'az account show' and verify account context.",
272
+ };
273
+ }
274
+ const userName = payload.user?.name ?? "unknown-user";
275
+ return {
276
+ id: "provider.identity_verified",
277
+ title: "Provider identity verified",
278
+ status: "pass",
279
+ message: `Verified Azure identity: ${userName} (tenant ${payload.tenantId}, subscription ${payload.id}).`,
280
+ };
281
+ }
282
+ catch {
283
+ return {
284
+ id: "provider.identity_verified",
285
+ title: "Provider identity verified",
286
+ status: "warn",
287
+ message: "Could not verify Azure identity (timed out or unavailable).",
288
+ details: "Run 'az login' and verify tenant/subscription context with 'az account show'.",
289
+ };
290
+ }
291
+ }
292
+ async function verifyProviderIdentity(config) {
293
+ if (config.provider === "aws") {
294
+ return verifyAwsIdentity(config);
295
+ }
296
+ if (config.provider === "gcp") {
297
+ return verifyGcpIdentity(config);
298
+ }
299
+ if (config.provider === "azure") {
300
+ return verifyAzureIdentity();
301
+ }
302
+ return {
303
+ id: "provider.identity_verified",
304
+ title: "Provider identity verified",
305
+ status: "warn",
306
+ message: "Provider identity verification is not available for this provider.",
307
+ };
308
+ }
309
+ function parseSemver(version) {
310
+ const core = version.trim().replace(/^v/, "").split("-")[0];
311
+ const parts = core.split(".");
312
+ if (parts.length < 3)
313
+ return null;
314
+ const major = Number(parts[0]);
315
+ const minor = Number(parts[1]);
316
+ const patch = Number(parts[2]);
317
+ if ([major, minor, patch].some((n) => Number.isNaN(n))) {
318
+ return null;
319
+ }
320
+ return [major, minor, patch];
321
+ }
322
+ function isVersionOlder(localVersion, latestVersion) {
323
+ const local = parseSemver(localVersion);
324
+ const latest = parseSemver(latestVersion);
325
+ if (!local || !latest) {
326
+ return localVersion !== latestVersion;
327
+ }
328
+ if (local[0] !== latest[0])
329
+ return local[0] < latest[0];
330
+ if (local[1] !== latest[1])
331
+ return local[1] < latest[1];
332
+ return local[2] < latest[2];
333
+ }
334
+ async function runVersionCheck() {
335
+ const localVersion = pkg.version;
336
+ const packageName = pkg.name;
337
+ try {
338
+ const controller = new AbortController();
339
+ const timer = setTimeout(() => controller.abort(), CHECK_TIMEOUT_MS);
340
+ let response;
341
+ try {
342
+ response = await fetch(`https://registry.npmjs.org/${packageName}/latest`, {
343
+ signal: controller.signal,
344
+ });
345
+ }
346
+ finally {
347
+ clearTimeout(timer);
348
+ }
349
+ if (!response.ok) {
350
+ return {
351
+ id: "version.check",
352
+ title: "Version check",
353
+ status: "warn",
354
+ message: `Installed envhub version: ${localVersion} (latest version could not be verified).`,
355
+ };
356
+ }
357
+ const data = (await response.json());
358
+ const latestVersion = data.version;
359
+ if (!latestVersion) {
360
+ return {
361
+ id: "version.check",
362
+ title: "Version check",
363
+ status: "warn",
364
+ message: `Installed envhub version: ${localVersion} (latest version could not be verified).`,
365
+ };
366
+ }
367
+ if (isVersionOlder(localVersion, latestVersion)) {
368
+ return {
369
+ id: "version.check",
370
+ title: "Version check",
371
+ status: "warn",
372
+ message: `Update available: ${localVersion} --> ${latestVersion}\n` +
373
+ " Update (project): npm install --save-dev envhub-cli@latest\n" +
374
+ " Update (global): npm install -g envhub-cli@latest\n" +
375
+ " Stay current: run 'npx envhub doctor' regularly."
376
+ };
377
+ }
378
+ return {
379
+ id: "version.check",
380
+ title: "Version check",
381
+ status: "pass",
382
+ message: `Version is up to date (${localVersion}).`,
383
+ };
384
+ }
385
+ catch (error) {
386
+ if (isTimeoutError(error) || (error instanceof Error && error.name === "AbortError")) {
387
+ return {
388
+ id: "version.check",
389
+ title: "Version check",
390
+ status: "warn",
391
+ message: `Installed envhub version: ${localVersion} ` +
392
+ "(latest version check timed out after 10s).",
393
+ };
394
+ }
395
+ return {
396
+ id: "version.check",
397
+ title: "Version check",
398
+ status: "warn",
399
+ message: `Installed envhub version: ${localVersion} (latest version check skipped: network unavailable).`,
400
+ };
401
+ }
402
+ }
403
+ function formatCheckMessage(check) {
404
+ let message = check.message;
405
+ if (check.id === "version.check") {
406
+ const [firstLine, ...restLines] = message.split("\n");
407
+ const match = firstLine.match(/^Update available: (.+) --> (.+)$/);
408
+ if (match) {
409
+ const localVersion = chalk.yellow(match[1]);
410
+ const latestVersion = chalk.green(match[2]);
411
+ const coloredFirstLine = `Update available: ${localVersion} --> ${latestVersion}`;
412
+ message = [coloredFirstLine, ...restLines].join("\n");
413
+ }
414
+ }
415
+ if (check.id === "provider.read_rights") {
416
+ const lines = message.split("\n");
417
+ const firstLine = lines[0] ?? "";
418
+ const match = firstLine.match(/^(.+?)(\d+\/\d+)(.*)$/);
419
+ if (match) {
420
+ const [, before, ratio, after] = match;
421
+ const coloredRatio = check.status === "pass"
422
+ ? chalk.green(ratio)
423
+ : check.status === "warn"
424
+ ? chalk.yellow(ratio)
425
+ : ratio;
426
+ lines[0] = `${before}${coloredRatio}${after}`;
427
+ }
428
+ for (let i = 1; i < lines.length; i++) {
429
+ const line = lines[i];
430
+ const passMatch = line.match(/^(\s*)✔\s(.+)$/);
431
+ if (passMatch) {
432
+ lines[i] = `${passMatch[1]}${chalk.green("✔")} ${chalk.green(passMatch[2])}`;
433
+ continue;
434
+ }
435
+ const failMatch = line.match(/^(\s*)✖\s(.+)$/);
436
+ if (failMatch) {
437
+ lines[i] = `${failMatch[1]}${chalk.red("✖")} ${chalk.red(failMatch[2])}`;
438
+ }
439
+ }
440
+ message = lines.join("\n");
441
+ }
442
+ return message;
443
+ }
444
+ function logCheckLine(check) {
445
+ const lines = formatCheckLines(check);
446
+ for (const line of lines) {
447
+ logger.log(` ${line}`);
448
+ }
449
+ }
450
+ function formatCheckLines(check) {
451
+ const formatted = formatCheckMessage(check);
452
+ const splitLines = formatted.split("\n");
453
+ const firstLine = splitLines[0] ?? "";
454
+ const lines = [`${iconForStatus(check.status)} ${check.id}: ${firstLine}`];
455
+ for (let i = 1; i < splitLines.length; i++) {
456
+ lines.push(splitLines[i] ?? "");
457
+ }
458
+ return lines;
459
+ }
460
+ function renderHumanHeader() {
461
+ const title = chalk.bgCyan.black(" envhub doctor ");
462
+ const subtitle = "Quick health check for version, config, provider identity/access,\n" +
463
+ "and tracked secret readability.";
464
+ logger.newline();
465
+ if (process.stdout.isTTY && process.stderr.isTTY) {
466
+ note(subtitle, title);
467
+ }
468
+ else {
469
+ logger.log(title);
470
+ logger.dim(subtitle);
471
+ }
472
+ }
473
+ function renderHumanSummary(report) {
474
+ logger.newline();
475
+ logger.newline();
476
+ logger.newline();
477
+ logger.log(`${chalk.green(`${report.summary.pass} passed`)}, ` +
478
+ `${chalk.yellow(`${report.summary.warn} warning(s)`)}, ` +
479
+ `${chalk.red(`${report.summary.fail} failed`)}`);
480
+ const hintedChecks = report.checks.filter((check) => !!check.details && check.status !== "pass");
481
+ if (hintedChecks.length > 0) {
482
+ logger.newline();
483
+ logger.log(chalk.bold.yellow("Hints"));
484
+ const groupedHints = new Map();
485
+ for (const check of hintedChecks) {
486
+ const detail = check.details;
487
+ const existing = groupedHints.get(detail) ?? [];
488
+ existing.push(check.id);
489
+ groupedHints.set(detail, existing);
490
+ }
491
+ for (const [detail, checkIds] of groupedHints) {
492
+ logger.log(`${chalk.yellow("⚠")} (${checkIds.join(", ")}): ${detail}`);
493
+ }
494
+ }
495
+ logger.newline();
496
+ }
497
+ function validatePrefix(config) {
498
+ const prefix = config.prefix;
499
+ const trimmed = prefix.trim();
500
+ if (!trimmed) {
501
+ return {
502
+ id: "prefix",
503
+ title: "Prefix validation",
504
+ status: "fail",
505
+ message: "Configured prefix is empty or whitespace only.",
506
+ details: "Set a non-empty 'prefix' in .envhubrc.json (for example: 'envhub-').",
507
+ };
508
+ }
509
+ if (prefix !== trimmed) {
510
+ return {
511
+ id: "prefix",
512
+ title: "Prefix validation",
513
+ status: "warn",
514
+ message: `Configured prefix has surrounding whitespace: '${prefix}'.`,
515
+ details: "Trim the prefix in .envhubrc.json to avoid naming surprises.",
516
+ };
517
+ }
518
+ return {
519
+ id: "prefix",
520
+ title: "Prefix validation",
521
+ status: "pass",
522
+ message: `Prefix is valid ('${prefix}').`,
523
+ };
524
+ }
525
+ async function runDoctorChecks(progress) {
526
+ const checks = [];
527
+ const addCheck = (check) => {
528
+ checks.push(check);
529
+ progress?.({ phase: "end", check });
530
+ };
531
+ const startCheck = (id, title) => {
532
+ progress?.({ phase: "start", id, title });
533
+ };
534
+ let config = null;
535
+ startCheck("version.check", "Version check");
536
+ const versionCheck = await runVersionCheck();
537
+ addCheck(versionCheck);
538
+ try {
539
+ startCheck("config.load", "Config loading");
540
+ config = await configManager.load();
541
+ const configCheck = {
542
+ id: "config.load",
543
+ title: "Config loading",
544
+ status: "pass",
545
+ message: `Configuration loaded from ${configManager.getConfigPath()}.`,
546
+ };
547
+ addCheck(configCheck);
548
+ }
549
+ catch (error) {
550
+ const message = error instanceof Error ? error.message : "Unknown configuration error.";
551
+ const configCheck = {
552
+ id: "config.load",
553
+ title: "Config loading",
554
+ status: "fail",
555
+ message: "Failed to load envhub configuration.",
556
+ details: `${message} Run 'envhub init' to create or repair configuration.`,
557
+ };
558
+ addCheck(configCheck);
559
+ startCheck("prefix", "Prefix validation");
560
+ const prefixCheck = {
561
+ id: "prefix",
562
+ title: "Prefix validation",
563
+ status: "warn",
564
+ message: "Skipped because configuration could not be loaded.",
565
+ };
566
+ addCheck(prefixCheck);
567
+ startCheck("provider.init", "Provider initialization");
568
+ const providerInitCheck = {
569
+ id: "provider.init",
570
+ title: "Provider initialization",
571
+ status: "warn",
572
+ message: "Skipped because configuration could not be loaded.",
573
+ };
574
+ addCheck(providerInitCheck);
575
+ startCheck("provider.identity", "Provider identity");
576
+ const providerIdentityCheck = {
577
+ id: "provider.identity",
578
+ title: "Provider identity",
579
+ status: "warn",
580
+ message: "Skipped because provider could not be initialized.",
581
+ };
582
+ addCheck(providerIdentityCheck);
583
+ startCheck("provider.identity_verified", "Provider identity verified");
584
+ const providerIdentityVerifiedCheck = {
585
+ id: "provider.identity_verified",
586
+ title: "Provider identity verified",
587
+ status: "warn",
588
+ message: "Skipped because provider could not be initialized.",
589
+ };
590
+ addCheck(providerIdentityVerifiedCheck);
591
+ startCheck("provider.reachability_and_auth", "Provider reachability/auth");
592
+ const reachabilityCheck = {
593
+ id: "provider.reachability_and_auth",
594
+ title: "Provider reachability/auth",
595
+ status: "warn",
596
+ message: "Skipped because provider could not be initialized.",
597
+ };
598
+ addCheck(reachabilityCheck);
599
+ startCheck("provider.list_rights", "Provider list rights");
600
+ const listRightsCheck = {
601
+ id: "provider.list_rights",
602
+ title: "Provider list rights",
603
+ status: "warn",
604
+ message: "Skipped because provider could not be initialized.",
605
+ };
606
+ addCheck(listRightsCheck);
607
+ startCheck("provider.read_rights", "Provider read rights");
608
+ const readRightsCheck = {
609
+ id: "provider.read_rights",
610
+ title: "Provider read rights",
611
+ status: "warn",
612
+ message: "Skipped because provider could not be initialized.",
613
+ };
614
+ addCheck(readRightsCheck);
615
+ return {
616
+ checks,
617
+ summary: summarizeChecks(checks),
618
+ };
619
+ }
620
+ startCheck("prefix", "Prefix validation");
621
+ const prefixCheck = validatePrefix(config);
622
+ addCheck(prefixCheck);
623
+ let provider = null;
624
+ try {
625
+ startCheck("provider.init", "Provider initialization");
626
+ provider = ProviderFactory.createProvider(config);
627
+ const initCheck = {
628
+ id: "provider.init",
629
+ title: "Provider initialization",
630
+ status: "pass",
631
+ message: `Provider '${provider.name}' initialized successfully.`,
632
+ };
633
+ addCheck(initCheck);
634
+ }
635
+ catch (error) {
636
+ const message = error instanceof Error ? error.message : "Unknown provider error.";
637
+ const initCheck = {
638
+ id: "provider.init",
639
+ title: "Provider initialization",
640
+ status: "fail",
641
+ message: "Failed to initialize provider from configuration.",
642
+ details: `${message} Check provider-specific fields in .envhubrc.json.`,
643
+ };
644
+ addCheck(initCheck);
645
+ startCheck("provider.identity", "Provider identity");
646
+ const providerIdentityCheck = {
647
+ id: "provider.identity",
648
+ title: "Provider identity",
649
+ status: "warn",
650
+ message: "Skipped because provider could not be initialized.",
651
+ };
652
+ addCheck(providerIdentityCheck);
653
+ startCheck("provider.identity_verified", "Provider identity verified");
654
+ const providerIdentityVerifiedCheck = {
655
+ id: "provider.identity_verified",
656
+ title: "Provider identity verified",
657
+ status: "warn",
658
+ message: "Skipped because provider could not be initialized.",
659
+ };
660
+ addCheck(providerIdentityVerifiedCheck);
661
+ startCheck("provider.reachability_and_auth", "Provider reachability/auth");
662
+ const reachabilityCheck = {
663
+ id: "provider.reachability_and_auth",
664
+ title: "Provider reachability/auth",
665
+ status: "warn",
666
+ message: "Skipped because provider could not be initialized.",
667
+ };
668
+ addCheck(reachabilityCheck);
669
+ startCheck("provider.list_rights", "Provider list rights");
670
+ const listRightsCheck = {
671
+ id: "provider.list_rights",
672
+ title: "Provider list rights",
673
+ status: "warn",
674
+ message: "Skipped because provider could not be initialized.",
675
+ };
676
+ addCheck(listRightsCheck);
677
+ startCheck("provider.read_rights", "Provider read rights");
678
+ const readRightsCheck = {
679
+ id: "provider.read_rights",
680
+ title: "Provider read rights",
681
+ status: "warn",
682
+ message: "Skipped because provider could not be initialized.",
683
+ };
684
+ addCheck(readRightsCheck);
685
+ return {
686
+ checks,
687
+ summary: summarizeChecks(checks),
688
+ };
689
+ }
690
+ startCheck("provider.identity", "Provider identity");
691
+ if (config.provider === "aws" && config.aws) {
692
+ const identityCheck = {
693
+ id: "provider.identity",
694
+ title: "Provider identity",
695
+ status: "pass",
696
+ message: `AWS context: profile '${config.aws.profile}', region '${config.aws.region}'.`,
697
+ };
698
+ addCheck(identityCheck);
699
+ }
700
+ else if (config.provider === "gcp" && config.gcp) {
701
+ const projectName = await resolveGcpProjectName(config.gcp.projectId);
702
+ const identityCheck = {
703
+ id: "provider.identity",
704
+ title: "Provider identity",
705
+ status: "pass",
706
+ message: projectName
707
+ ? `GCP context: project '${config.gcp.projectId}' (${projectName}).`
708
+ : `GCP context: project '${config.gcp.projectId}'.`,
709
+ };
710
+ addCheck(identityCheck);
711
+ }
712
+ else if (config.provider === "azure" && config.azure) {
713
+ const identityCheck = {
714
+ id: "provider.identity",
715
+ title: "Provider identity",
716
+ status: "pass",
717
+ message: `Azure context: vault '${config.azure.vaultUrl}'.`,
718
+ };
719
+ addCheck(identityCheck);
720
+ }
721
+ else {
722
+ const identityCheck = {
723
+ id: "provider.identity",
724
+ title: "Provider identity",
725
+ status: "warn",
726
+ message: "Provider identity context is not available in configuration.",
727
+ };
728
+ addCheck(identityCheck);
729
+ }
730
+ startCheck("provider.identity_verified", "Provider identity verified");
731
+ const identityVerifiedCheck = await verifyProviderIdentity(config);
732
+ addCheck(identityVerifiedCheck);
733
+ try {
734
+ startCheck("provider.reachability_and_auth", "Provider reachability/auth");
735
+ await withTimeout("Provider list check", provider.list());
736
+ const reachabilityCheck = {
737
+ id: "provider.reachability_and_auth",
738
+ title: "Provider reachability/auth",
739
+ status: "pass",
740
+ message: `Connected to ${provider.name} and authenticated successfully.`,
741
+ };
742
+ addCheck(reachabilityCheck);
743
+ startCheck("provider.list_rights", "Provider list rights");
744
+ const listRightsCheck = {
745
+ id: "provider.list_rights",
746
+ title: "Provider list rights",
747
+ status: "pass",
748
+ message: "Current identity can list envhub-managed secrets.",
749
+ };
750
+ addCheck(listRightsCheck);
751
+ const trackedSecretNames = Object.keys(config.secrets).sort((a, b) => a.localeCompare(b));
752
+ startCheck("provider.read_rights", "Provider read rights");
753
+ if (trackedSecretNames.length === 0) {
754
+ const readCheck = {
755
+ id: "provider.read_rights",
756
+ title: "Provider read rights",
757
+ status: "warn",
758
+ message: "Skipped because no tracked secrets are configured. Add secrets via push/pull first.",
759
+ };
760
+ addCheck(readCheck);
761
+ }
762
+ else {
763
+ const passedSecrets = [];
764
+ const failedSecrets = [];
765
+ let firstFailureMessage = "";
766
+ for (const secretName of trackedSecretNames) {
767
+ try {
768
+ await withTimeout(`Provider read check for '${secretName}'`, provider.cat(secretName));
769
+ passedSecrets.push(secretName);
770
+ }
771
+ catch (error) {
772
+ failedSecrets.push(secretName);
773
+ if (!firstFailureMessage) {
774
+ firstFailureMessage =
775
+ error instanceof Error ? error.message : "Unknown provider error.";
776
+ }
777
+ }
778
+ }
779
+ if (failedSecrets.length === 0) {
780
+ const passedLines = trackedSecretNames.map((name) => ` ✔ ${name}`);
781
+ const readCheck = {
782
+ id: "provider.read_rights",
783
+ title: "Provider read rights",
784
+ status: "pass",
785
+ message: `Read checks passed for all tracked secrets (${trackedSecretNames.length}/${trackedSecretNames.length}).\n` +
786
+ passedLines.join("\n"),
787
+ };
788
+ addCheck(readCheck);
789
+ }
790
+ else {
791
+ const classification = classifyProviderFailure(provider.name, firstFailureMessage);
792
+ if (failedSecrets.length === trackedSecretNames.length) {
793
+ const failedLines = failedSecrets.map((name) => ` ✖ ${name}`);
794
+ const allFailedDueToTimeout = isTimeoutError(firstFailureMessage ? new Error(firstFailureMessage) : null);
795
+ const readCheck = {
796
+ id: "provider.read_rights",
797
+ title: "Provider read rights",
798
+ status: allFailedDueToTimeout ? "warn" : "fail",
799
+ message: `${allFailedDueToTimeout ? "Read checks timed out for all" : "Read checks failed for all"} ` +
800
+ `${trackedSecretNames.length} tracked secrets.\n` +
801
+ failedLines.join("\n"),
802
+ details: classification.details,
803
+ };
804
+ addCheck(readCheck);
805
+ }
806
+ else {
807
+ const passedLines = passedSecrets.map((name) => ` ✔ ${name}`);
808
+ const failedLines = failedSecrets.map((name) => ` ✖ ${name}`);
809
+ const readCheck = {
810
+ id: "provider.read_rights",
811
+ title: "Provider read rights",
812
+ status: "warn",
813
+ message: `Read checks partially passed (${passedSecrets.length}/${trackedSecretNames.length}).\n` +
814
+ passedLines.join("\n") +
815
+ "\n" +
816
+ failedLines.join("\n"),
817
+ details: classification.details,
818
+ };
819
+ addCheck(readCheck);
820
+ }
821
+ }
822
+ }
823
+ }
824
+ catch (error) {
825
+ const rawMessage = error instanceof Error ? error.message : "Unknown provider error.";
826
+ if (isTimeoutError(error)) {
827
+ const reachabilityTimeoutCheck = {
828
+ id: "provider.reachability_and_auth",
829
+ title: "Provider reachability/auth",
830
+ status: "warn",
831
+ message: "Provider reachability/auth check timed out after 10s.",
832
+ details: "Timeout while contacting provider. Check network/proxy/VPN and retry.",
833
+ };
834
+ addCheck(reachabilityTimeoutCheck);
835
+ startCheck("provider.list_rights", "Provider list rights");
836
+ const listTimeoutCheck = {
837
+ id: "provider.list_rights",
838
+ title: "Provider list rights",
839
+ status: "warn",
840
+ message: "Skipped because list check timed out after 10s.",
841
+ details: "Retry when provider connectivity is stable.",
842
+ };
843
+ addCheck(listTimeoutCheck);
844
+ startCheck("provider.read_rights", "Provider read rights");
845
+ const readTimeoutCheck = {
846
+ id: "provider.read_rights",
847
+ title: "Provider read rights",
848
+ status: "warn",
849
+ message: "Skipped because provider list check timed out after 10s.",
850
+ details: "Retry when provider connectivity is stable.",
851
+ };
852
+ addCheck(readTimeoutCheck);
853
+ return {
854
+ checks,
855
+ summary: summarizeChecks(checks),
856
+ };
857
+ }
858
+ const classification = classifyProviderFailure(provider.name, rawMessage);
859
+ const reachabilityCheck = {
860
+ id: "provider.reachability_and_auth",
861
+ title: "Provider reachability/auth",
862
+ status: "fail",
863
+ message: classification.message,
864
+ details: classification.details,
865
+ };
866
+ addCheck(reachabilityCheck);
867
+ startCheck("provider.list_rights", "Provider list rights");
868
+ const listRightsCheck = {
869
+ id: "provider.list_rights",
870
+ title: "Provider list rights",
871
+ status: "fail",
872
+ message: "Could not verify permission to list envhub-managed secrets.",
873
+ details: classification.details,
874
+ };
875
+ addCheck(listRightsCheck);
876
+ startCheck("provider.read_rights", "Provider read rights");
877
+ const readCheck = {
878
+ id: "provider.read_rights",
879
+ title: "Provider read rights",
880
+ status: "warn",
881
+ message: "Skipped because list permission check failed.",
882
+ };
883
+ addCheck(readCheck);
884
+ }
885
+ return {
886
+ checks,
887
+ summary: summarizeChecks(checks),
888
+ };
889
+ }
890
+ /**
891
+ * The `envhub doctor` command.
892
+ * Runs read-only health checks for local config and provider connectivity.
893
+ */
894
+ export async function doctorCommand(options) {
895
+ if (!options.json) {
896
+ renderHumanHeader();
897
+ }
898
+ const isInteractiveTty = Boolean(process.stdout.isTTY && process.stderr.isTTY);
899
+ const spinnerEnabled = !options.json && isInteractiveTty && process.env.VITEST !== "true";
900
+ const checkSpinner = spinnerEnabled ? clackSpinner() : null;
901
+ let spinnerStarted = false;
902
+ const progress = options.json
903
+ ? undefined
904
+ : (event) => {
905
+ if (event.phase === "start") {
906
+ if (!checkSpinner || checkSpinner.isCancelled) {
907
+ return;
908
+ }
909
+ if (!spinnerStarted) {
910
+ checkSpinner.start(`Checking ${event.id}...`);
911
+ spinnerStarted = true;
912
+ }
913
+ else {
914
+ checkSpinner.message(`Checking ${event.id}...`);
915
+ }
916
+ return;
917
+ }
918
+ };
919
+ const report = await runDoctorChecks(progress);
920
+ if (checkSpinner && spinnerStarted) {
921
+ checkSpinner.clear();
922
+ }
923
+ if (options.json) {
924
+ console.log(JSON.stringify(report, null, 2));
925
+ }
926
+ else {
927
+ let currentGroup = null;
928
+ for (const check of report.checks) {
929
+ const group = groupForCheck(check.id);
930
+ if (group !== currentGroup) {
931
+ if (currentGroup !== null) {
932
+ logger.newline();
933
+ }
934
+ renderGroupHeader(group);
935
+ currentGroup = group;
936
+ }
937
+ logCheckLine(check);
938
+ }
939
+ renderHumanSummary(report);
940
+ }
941
+ if (report.summary.fail > 0) {
942
+ process.exit(1);
943
+ }
944
+ }
945
+ export { runDoctorChecks };
946
+ //# sourceMappingURL=doctor.js.map