remodex-cli 1.0.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 (99) hide show
  1. package/LICENSE +12 -0
  2. package/README.md +105 -0
  3. package/dist/archive-store.d.ts +28 -0
  4. package/dist/archive-store.js +68 -0
  5. package/dist/archive-store.js.map +1 -0
  6. package/dist/cli.d.ts +2 -0
  7. package/dist/cli.js +88 -0
  8. package/dist/cli.js.map +1 -0
  9. package/dist/codex-process.d.ts +186 -0
  10. package/dist/codex-process.js +2111 -0
  11. package/dist/codex-process.js.map +1 -0
  12. package/dist/debug-trace-store.d.ts +15 -0
  13. package/dist/debug-trace-store.js +78 -0
  14. package/dist/debug-trace-store.js.map +1 -0
  15. package/dist/doctor.d.ts +58 -0
  16. package/dist/doctor.js +670 -0
  17. package/dist/doctor.js.map +1 -0
  18. package/dist/firebase-auth.d.ts +35 -0
  19. package/dist/firebase-auth.js +132 -0
  20. package/dist/firebase-auth.js.map +1 -0
  21. package/dist/gallery-store.d.ts +67 -0
  22. package/dist/gallery-store.js +333 -0
  23. package/dist/gallery-store.js.map +1 -0
  24. package/dist/git-assist.d.ts +7 -0
  25. package/dist/git-assist.js +51 -0
  26. package/dist/git-assist.js.map +1 -0
  27. package/dist/git-operations.d.ts +63 -0
  28. package/dist/git-operations.js +292 -0
  29. package/dist/git-operations.js.map +1 -0
  30. package/dist/image-store.d.ts +23 -0
  31. package/dist/image-store.js +142 -0
  32. package/dist/image-store.js.map +1 -0
  33. package/dist/index.d.ts +1 -0
  34. package/dist/index.js +198 -0
  35. package/dist/index.js.map +1 -0
  36. package/dist/mdns.d.ts +7 -0
  37. package/dist/mdns.js +49 -0
  38. package/dist/mdns.js.map +1 -0
  39. package/dist/parser.d.ts +620 -0
  40. package/dist/parser.js +423 -0
  41. package/dist/parser.js.map +1 -0
  42. package/dist/path-utils.d.ts +4 -0
  43. package/dist/path-utils.js +34 -0
  44. package/dist/path-utils.js.map +1 -0
  45. package/dist/project-history.d.ts +10 -0
  46. package/dist/project-history.js +73 -0
  47. package/dist/project-history.js.map +1 -0
  48. package/dist/prompt-history-backup.d.ts +15 -0
  49. package/dist/prompt-history-backup.js +46 -0
  50. package/dist/prompt-history-backup.js.map +1 -0
  51. package/dist/proxy.d.ts +15 -0
  52. package/dist/proxy.js +95 -0
  53. package/dist/proxy.js.map +1 -0
  54. package/dist/push-i18n.d.ts +7 -0
  55. package/dist/push-i18n.js +75 -0
  56. package/dist/push-i18n.js.map +1 -0
  57. package/dist/push-relay.d.ts +29 -0
  58. package/dist/push-relay.js +70 -0
  59. package/dist/push-relay.js.map +1 -0
  60. package/dist/recording-store.d.ts +51 -0
  61. package/dist/recording-store.js +158 -0
  62. package/dist/recording-store.js.map +1 -0
  63. package/dist/screenshot.d.ts +28 -0
  64. package/dist/screenshot.js +98 -0
  65. package/dist/screenshot.js.map +1 -0
  66. package/dist/sdk-process.d.ts +180 -0
  67. package/dist/sdk-process.js +960 -0
  68. package/dist/sdk-process.js.map +1 -0
  69. package/dist/session.d.ts +144 -0
  70. package/dist/session.js +687 -0
  71. package/dist/session.js.map +1 -0
  72. package/dist/sessions-index.d.ts +130 -0
  73. package/dist/sessions-index.js +1817 -0
  74. package/dist/sessions-index.js.map +1 -0
  75. package/dist/setup-launchd.d.ts +9 -0
  76. package/dist/setup-launchd.js +115 -0
  77. package/dist/setup-launchd.js.map +1 -0
  78. package/dist/setup-systemd.d.ts +9 -0
  79. package/dist/setup-systemd.js +122 -0
  80. package/dist/setup-systemd.js.map +1 -0
  81. package/dist/startup-info.d.ts +9 -0
  82. package/dist/startup-info.js +116 -0
  83. package/dist/startup-info.js.map +1 -0
  84. package/dist/usage.d.ts +69 -0
  85. package/dist/usage.js +545 -0
  86. package/dist/usage.js.map +1 -0
  87. package/dist/version.d.ts +13 -0
  88. package/dist/version.js +43 -0
  89. package/dist/version.js.map +1 -0
  90. package/dist/websocket.d.ts +132 -0
  91. package/dist/websocket.js +3551 -0
  92. package/dist/websocket.js.map +1 -0
  93. package/dist/worktree-store.d.ts +26 -0
  94. package/dist/worktree-store.js +61 -0
  95. package/dist/worktree-store.js.map +1 -0
  96. package/dist/worktree.d.ts +47 -0
  97. package/dist/worktree.js +330 -0
  98. package/dist/worktree.js.map +1 -0
  99. package/package.json +62 -0
package/dist/doctor.js ADDED
@@ -0,0 +1,670 @@
1
+ /**
2
+ * Bridge Server doctor command.
3
+ *
4
+ * Checks the health of all dependencies and provides actionable guidance
5
+ * when issues are found — similar to `flutter doctor`.
6
+ */
7
+ import { execFile, execSync } from "node:child_process";
8
+ import { accessSync, constants as fsConstants, existsSync, } from "node:fs";
9
+ import net from "node:net";
10
+ import { homedir } from "node:os";
11
+ import { join } from "node:path";
12
+ // ---------------------------------------------------------------------------
13
+ // Helper
14
+ // ---------------------------------------------------------------------------
15
+ function execQuiet(cmd) {
16
+ return execSync(cmd, { encoding: "utf-8", stdio: ["pipe", "pipe", "pipe"] }).trim();
17
+ }
18
+ // ---------------------------------------------------------------------------
19
+ // Individual checks
20
+ // ---------------------------------------------------------------------------
21
+ export async function checkNodeVersion() {
22
+ const version = process.version; // e.g. "v22.5.0"
23
+ const major = parseInt(version.slice(1), 10);
24
+ if (major >= 18) {
25
+ return { name: "Node.js", status: "pass", message: version };
26
+ }
27
+ return {
28
+ name: "Node.js",
29
+ status: "fail",
30
+ message: `${version} (requires >=18.0.0)`,
31
+ remediation: "Install Node.js >=18.0.0: https://nodejs.org/",
32
+ };
33
+ }
34
+ export async function checkGit() {
35
+ try {
36
+ const out = execQuiet("git --version"); // "git version 2.44.0"
37
+ const version = out.replace("git version ", "");
38
+ return { name: "Git", status: "pass", message: `v${version}` };
39
+ }
40
+ catch {
41
+ return {
42
+ name: "Git",
43
+ status: "fail",
44
+ message: "Not installed",
45
+ remediation: "Install Git: https://git-scm.com/downloads",
46
+ };
47
+ }
48
+ }
49
+ /** Check both Claude Code CLI and Codex CLI. At least one must be installed. */
50
+ export async function checkCliProviders() {
51
+ const providers = [];
52
+ // --- Claude Code CLI ---
53
+ {
54
+ let installed = false;
55
+ let version;
56
+ let authenticated = false;
57
+ let authMessage;
58
+ let remediation;
59
+ try {
60
+ const out = execQuiet("claude --version");
61
+ installed = true;
62
+ version = out.trim().split("\n")[0];
63
+ // Check auth
64
+ try {
65
+ const authOut = execQuiet("claude auth status");
66
+ // If exit code 0, authenticated
67
+ if (authOut.toLowerCase().includes("not logged in") || authOut.toLowerCase().includes("unauthenticated")) {
68
+ authenticated = false;
69
+ authMessage = "Not authenticated";
70
+ remediation = "Run: claude auth login";
71
+ }
72
+ else {
73
+ authenticated = true;
74
+ }
75
+ }
76
+ catch {
77
+ // auth command failed — treat as unauthenticated
78
+ authenticated = false;
79
+ authMessage = "Not authenticated";
80
+ remediation = "Run: claude auth login";
81
+ }
82
+ }
83
+ catch {
84
+ remediation = "Install Claude Code: https://docs.anthropic.com/en/docs/claude-code/getting-started";
85
+ }
86
+ providers.push({
87
+ name: "Claude Code CLI",
88
+ installed,
89
+ version,
90
+ authenticated,
91
+ authMessage,
92
+ remediation,
93
+ });
94
+ }
95
+ // --- Codex CLI ---
96
+ {
97
+ let installed = false;
98
+ let version;
99
+ let authenticated = false;
100
+ let authMessage;
101
+ let remediation;
102
+ try {
103
+ const out = execQuiet("codex --version");
104
+ installed = true;
105
+ version = out.trim().split("\n")[0];
106
+ // Codex authenticates via OPENAI_API_KEY env var or ~/.codex/auth.json
107
+ if (process.env.OPENAI_API_KEY) {
108
+ authenticated = true;
109
+ }
110
+ else {
111
+ const authFile = join(homedir(), ".codex", "auth.json");
112
+ if (existsSync(authFile)) {
113
+ authenticated = true;
114
+ }
115
+ else {
116
+ authenticated = false;
117
+ authMessage = "Not authenticated";
118
+ remediation = "Run: codex login";
119
+ }
120
+ }
121
+ }
122
+ catch {
123
+ remediation = "Install Codex CLI: https://github.com/openai/codex";
124
+ }
125
+ providers.push({
126
+ name: "Codex CLI",
127
+ installed,
128
+ version,
129
+ authenticated,
130
+ authMessage,
131
+ remediation,
132
+ });
133
+ }
134
+ const installedCount = providers.filter((p) => p.installed).length;
135
+ const total = providers.length;
136
+ if (installedCount === 0) {
137
+ return {
138
+ name: "CLI providers",
139
+ status: "fail",
140
+ message: "No CLI providers installed",
141
+ remediation: "Install at least one: https://docs.anthropic.com/en/docs/claude-code/getting-started OR https://github.com/openai/codex",
142
+ providers,
143
+ };
144
+ }
145
+ // At least one installed — check if any auth warnings
146
+ const hasAuthWarn = providers.some((p) => p.installed && !p.authenticated);
147
+ return {
148
+ name: "CLI providers",
149
+ status: hasAuthWarn ? "warn" : "pass",
150
+ message: `${installedCount} of ${total} available`,
151
+ providers,
152
+ };
153
+ }
154
+ export async function checkDependencies() {
155
+ // In monorepo setups, node_modules may be hoisted to the workspace root.
156
+ // Use import.meta.resolve() to check if packages are resolvable.
157
+ const requiredPackages = [
158
+ "ws",
159
+ "@anthropic-ai/claude-agent-sdk",
160
+ "bonjour-service",
161
+ ];
162
+ const missing = [];
163
+ for (const pkg of requiredPackages) {
164
+ try {
165
+ import.meta.resolve(pkg);
166
+ }
167
+ catch {
168
+ missing.push(pkg);
169
+ }
170
+ }
171
+ if (missing.length > 0) {
172
+ return {
173
+ name: "npm dependencies",
174
+ status: "fail",
175
+ message: `Missing: ${missing.join(", ")}`,
176
+ remediation: "Run: npm install",
177
+ };
178
+ }
179
+ return { name: "npm dependencies", status: "pass", message: "All packages available" };
180
+ }
181
+ export async function checkPortAvailable(port) {
182
+ if (port === 0) {
183
+ return {
184
+ name: "Port availability",
185
+ status: "pass",
186
+ message: "An available ephemeral port can be allocated",
187
+ };
188
+ }
189
+ return new Promise((resolve) => {
190
+ let resolved = false;
191
+ const timeout = setTimeout(() => {
192
+ try {
193
+ server.close();
194
+ }
195
+ catch { /* ignore */ }
196
+ done({
197
+ name: "Port availability",
198
+ status: "warn",
199
+ message: `Port ${port} check timed out`,
200
+ });
201
+ }, 3000);
202
+ const done = (result) => {
203
+ if (resolved)
204
+ return;
205
+ resolved = true;
206
+ clearTimeout(timeout);
207
+ resolve(result);
208
+ };
209
+ const server = net.createServer();
210
+ server.once("error", (err) => {
211
+ if (err.code === "EADDRINUSE") {
212
+ done({
213
+ name: "Port availability",
214
+ status: "warn",
215
+ message: `Port ${port} is in use`,
216
+ remediation: `Another Bridge may be running, or set BRIDGE_PORT to a different port`,
217
+ });
218
+ }
219
+ else {
220
+ done({
221
+ name: "Port availability",
222
+ status: "warn",
223
+ message: `Port ${port} check failed: ${err.code}`,
224
+ });
225
+ }
226
+ });
227
+ server.listen(port, "127.0.0.1", () => {
228
+ server.close(() => {
229
+ done({
230
+ name: "Port availability",
231
+ status: "pass",
232
+ message: `Port ${port} is available`,
233
+ });
234
+ });
235
+ });
236
+ });
237
+ }
238
+ /** Resolve the tailscale CLI binary path (may be inside macOS .app bundle). */
239
+ function tailscaleCmd() {
240
+ // Try bare command first (Linux, Homebrew install, etc.)
241
+ try {
242
+ execQuiet("tailscale version");
243
+ return "tailscale";
244
+ }
245
+ catch { /* not in PATH */ }
246
+ // macOS: Tailscale.app bundles the CLI inside the app
247
+ const macPath = "/Applications/Tailscale.app/Contents/MacOS/Tailscale";
248
+ if (existsSync(macPath))
249
+ return macPath;
250
+ throw new Error("tailscale not found");
251
+ }
252
+ export async function checkTailscale() {
253
+ let cmd;
254
+ try {
255
+ cmd = tailscaleCmd();
256
+ }
257
+ catch {
258
+ return {
259
+ name: "Tailscale",
260
+ status: "skip",
261
+ message: "Not installed (optional for remote access)",
262
+ remediation: "Install: https://tailscale.com/download",
263
+ };
264
+ }
265
+ try {
266
+ const out = execQuiet(`${cmd} status`);
267
+ // Extract the Tailscale IP (first IPv4 in output)
268
+ const ipMatch = out.match(/(\d+\.\d+\.\d+\.\d+)/);
269
+ const ip = ipMatch ? ipMatch[1] : "";
270
+ return {
271
+ name: "Tailscale",
272
+ status: "pass",
273
+ message: ip ? `Connected (${ip})` : "Connected",
274
+ };
275
+ }
276
+ catch {
277
+ return {
278
+ name: "Tailscale",
279
+ status: "warn",
280
+ message: "Installed but not connected",
281
+ remediation: "Run: tailscale up",
282
+ };
283
+ }
284
+ }
285
+ export async function checkFirebaseConnectivity() {
286
+ // Use a read-only endpoint to avoid creating anonymous accounts as a side effect
287
+ const FIREBASE_API_KEY = "AIzaSyAptNnokWPqJIgv2Lr3I8ETN6bqZb5BGvc";
288
+ const url = `https://identitytoolkit.googleapis.com/v1/accounts:lookup?key=${FIREBASE_API_KEY}`;
289
+ try {
290
+ const response = await fetch(url, {
291
+ method: "POST",
292
+ headers: { "Content-Type": "application/json" },
293
+ body: JSON.stringify({}),
294
+ signal: AbortSignal.timeout(5000),
295
+ });
296
+ // Any response (even 400) means the API is reachable
297
+ if (response.status < 500) {
298
+ return {
299
+ name: "Firebase connectivity",
300
+ status: "pass",
301
+ message: "Firebase Auth API reachable",
302
+ };
303
+ }
304
+ return {
305
+ name: "Firebase connectivity",
306
+ status: "warn",
307
+ message: `Firebase Auth API returned ${response.status}`,
308
+ remediation: "Push notifications may not work. Check network connectivity.",
309
+ };
310
+ }
311
+ catch {
312
+ return {
313
+ name: "Firebase connectivity",
314
+ status: "warn",
315
+ message: "Unreachable",
316
+ remediation: "Push notifications will be disabled. Check network connectivity.",
317
+ };
318
+ }
319
+ }
320
+ export async function checkDataDirectory() {
321
+ const dir = join(homedir(), ".remodex");
322
+ if (!existsSync(dir)) {
323
+ return {
324
+ name: "Data directory",
325
+ status: "pass",
326
+ message: "~/.remodex/ will be created on first run",
327
+ };
328
+ }
329
+ try {
330
+ accessSync(dir, fsConstants.R_OK | fsConstants.W_OK);
331
+ return {
332
+ name: "Data directory",
333
+ status: "pass",
334
+ message: "~/.remodex/ exists",
335
+ };
336
+ }
337
+ catch {
338
+ return {
339
+ name: "Data directory",
340
+ status: "warn",
341
+ message: "~/.remodex/ is not writable",
342
+ remediation: "Fix permissions: chmod u+rw ~/.remodex",
343
+ };
344
+ }
345
+ }
346
+ export async function checkLaunchdService() {
347
+ if (process.platform !== "darwin") {
348
+ return {
349
+ name: "launchd service",
350
+ status: "skip",
351
+ message: "macOS only",
352
+ };
353
+ }
354
+ try {
355
+ const out = execSync("launchctl list", {
356
+ encoding: "utf-8",
357
+ stdio: ["pipe", "pipe", "pipe"],
358
+ });
359
+ if (out.includes("com.remodex.bridge")) {
360
+ return {
361
+ name: "launchd service",
362
+ status: "pass",
363
+ message: "Registered",
364
+ };
365
+ }
366
+ return {
367
+ name: "launchd service",
368
+ status: "skip",
369
+ message: "Not registered",
370
+ remediation: "Register with: remodex-cli setup",
371
+ };
372
+ }
373
+ catch {
374
+ return {
375
+ name: "launchd service",
376
+ status: "skip",
377
+ message: "Unable to check",
378
+ };
379
+ }
380
+ }
381
+ export async function checkSystemdService() {
382
+ if (process.platform !== "linux") {
383
+ return {
384
+ name: "systemd service",
385
+ status: "skip",
386
+ message: "Linux only",
387
+ };
388
+ }
389
+ try {
390
+ const out = execSync("systemctl --user is-active remodex-cli.service", {
391
+ encoding: "utf-8",
392
+ stdio: ["pipe", "pipe", "pipe"],
393
+ });
394
+ if (out.trim() === "active") {
395
+ return {
396
+ name: "systemd service",
397
+ status: "pass",
398
+ message: "Active",
399
+ };
400
+ }
401
+ return {
402
+ name: "systemd service",
403
+ status: "skip",
404
+ message: `Status: ${out.trim()}`,
405
+ remediation: "Register with: remodex-cli setup",
406
+ };
407
+ }
408
+ catch {
409
+ return {
410
+ name: "systemd service",
411
+ status: "skip",
412
+ message: "Not registered",
413
+ remediation: "Register with: remodex-cli setup",
414
+ };
415
+ }
416
+ }
417
+ // ---------------------------------------------------------------------------
418
+ // macOS permission checks
419
+ // ---------------------------------------------------------------------------
420
+ /**
421
+ * Swift inline script to check Screen Recording permission.
422
+ * CGWindowListCopyWindowInfo returns window names only when the process has
423
+ * Screen Recording permission. Without it, kCGWindowName is always empty.
424
+ * We check if *any* on-screen window has a non-empty name.
425
+ */
426
+ const CHECK_SCREEN_RECORDING_SWIFT = `
427
+ import CoreGraphics
428
+
429
+ let windowList = CGWindowListCopyWindowInfo(
430
+ [.optionOnScreenOnly, .excludeDesktopElements],
431
+ kCGNullWindowID
432
+ ) as? [[String: Any]] ?? []
433
+
434
+ var hasName = false
435
+ for w in windowList {
436
+ guard let layer = w[kCGWindowLayer as String] as? Int, layer == 0 else { continue }
437
+ if let name = w[kCGWindowName as String] as? String, !name.isEmpty {
438
+ hasName = true
439
+ break
440
+ }
441
+ }
442
+ print(hasName ? "granted" : "denied")
443
+ `;
444
+ export async function checkScreenRecording() {
445
+ if (process.platform !== "darwin") {
446
+ return { name: "Screen Recording", status: "skip", message: "macOS only" };
447
+ }
448
+ return new Promise((resolve) => {
449
+ execFile("swift", ["-e", CHECK_SCREEN_RECORDING_SWIFT], { timeout: 15_000 }, (err, stdout) => {
450
+ if (err) {
451
+ resolve({
452
+ name: "Screen Recording",
453
+ status: "warn",
454
+ message: "Unable to check (swift not available)",
455
+ remediation: "Install Xcode Command Line Tools: xcode-select --install",
456
+ });
457
+ return;
458
+ }
459
+ const result = stdout.trim();
460
+ if (result === "granted") {
461
+ resolve({
462
+ name: "Screen Recording",
463
+ status: "pass",
464
+ message: "Permission granted",
465
+ });
466
+ }
467
+ else {
468
+ resolve({
469
+ name: "Screen Recording",
470
+ status: "warn",
471
+ message: "Permission not granted (screenshots will fail)",
472
+ remediation: "System Settings > Privacy & Security > Screen Recording > enable your terminal app",
473
+ });
474
+ }
475
+ });
476
+ });
477
+ }
478
+ /**
479
+ * Check if Claude Code credentials are available.
480
+ *
481
+ * Checks ~/.claude/.credentials.json first, then falls back to
482
+ * macOS Keychain ("Claude Code-credentials" service).
483
+ */
484
+ export async function checkKeychainAccess() {
485
+ const credPath = join(homedir(), ".claude", ".credentials.json");
486
+ if (existsSync(credPath)) {
487
+ return {
488
+ name: "Keychain access",
489
+ status: "pass",
490
+ message: "Claude Code credentials found (~/.claude/.credentials.json)",
491
+ };
492
+ }
493
+ // Fallback: check macOS Keychain
494
+ if (process.platform === "darwin") {
495
+ try {
496
+ const { execFileSync } = await import("node:child_process");
497
+ execFileSync("security", ["find-generic-password", "-s", "Claude Code-credentials"], { stdio: "ignore" });
498
+ return {
499
+ name: "Keychain access",
500
+ status: "pass",
501
+ message: "Claude Code credentials found (macOS Keychain)",
502
+ };
503
+ }
504
+ catch {
505
+ // Not in Keychain either
506
+ }
507
+ }
508
+ return {
509
+ name: "Keychain access",
510
+ status: "skip",
511
+ message: "No Claude Code credentials stored",
512
+ remediation: "Run: claude auth login (if using Claude Code)",
513
+ };
514
+ }
515
+ // ---------------------------------------------------------------------------
516
+ // Runner
517
+ // ---------------------------------------------------------------------------
518
+ function getAllChecks() {
519
+ const port = parseInt(process.env.BRIDGE_PORT ?? "8765", 10);
520
+ return [
521
+ // Required
522
+ { name: "Node.js", category: "required", run: checkNodeVersion },
523
+ { name: "Git", category: "required", run: checkGit },
524
+ { name: "CLI providers", category: "required", run: checkCliProviders },
525
+ { name: "npm dependencies", category: "required", run: checkDependencies },
526
+ {
527
+ name: "Port availability",
528
+ category: "required",
529
+ run: () => checkPortAvailable(port),
530
+ },
531
+ // Optional — macOS permissions
532
+ {
533
+ name: "Screen Recording",
534
+ category: "optional",
535
+ run: checkScreenRecording,
536
+ },
537
+ {
538
+ name: "Keychain access",
539
+ category: "optional",
540
+ run: checkKeychainAccess,
541
+ },
542
+ // Optional — connectivity & services
543
+ { name: "Tailscale", category: "optional", run: checkTailscale },
544
+ {
545
+ name: "Firebase connectivity",
546
+ category: "optional",
547
+ run: checkFirebaseConnectivity,
548
+ },
549
+ { name: "Data directory", category: "optional", run: checkDataDirectory },
550
+ // Platform-specific service checks
551
+ ...(process.platform === "darwin"
552
+ ? [{ name: "launchd service", category: "optional", run: checkLaunchdService }]
553
+ : []),
554
+ ...(process.platform === "linux"
555
+ ? [{ name: "systemd service", category: "optional", run: checkSystemdService }]
556
+ : []),
557
+ ];
558
+ }
559
+ export async function runDoctor() {
560
+ const checks = getAllChecks();
561
+ const results = [];
562
+ for (const check of checks) {
563
+ const result = await check.run();
564
+ results.push({ ...result, category: check.category });
565
+ }
566
+ const allRequiredPassed = results
567
+ .filter((r) => r.category === "required")
568
+ .every((r) => r.status === "pass" || r.status === "warn");
569
+ return { results, allRequiredPassed };
570
+ }
571
+ // ---------------------------------------------------------------------------
572
+ // Output formatting
573
+ // ---------------------------------------------------------------------------
574
+ const SYMBOLS_TTY = {
575
+ pass: "\x1b[32m✓\x1b[0m",
576
+ fail: "\x1b[31m✗\x1b[0m",
577
+ warn: "\x1b[33m!\x1b[0m",
578
+ skip: "\x1b[90m-\x1b[0m",
579
+ };
580
+ const SYMBOLS_PLAIN = {
581
+ pass: "[OK]",
582
+ fail: "[FAIL]",
583
+ warn: "[WARN]",
584
+ skip: "[SKIP]",
585
+ };
586
+ function providerStatusIcon(p, sym) {
587
+ if (!p.installed)
588
+ return sym.skip;
589
+ if (!p.authenticated)
590
+ return sym.warn;
591
+ return sym.pass;
592
+ }
593
+ function providerStatusMessage(p) {
594
+ if (!p.installed)
595
+ return "Not installed";
596
+ const parts = [];
597
+ if (p.version)
598
+ parts.push(p.version);
599
+ if (p.authenticated) {
600
+ parts.push("(authenticated)");
601
+ }
602
+ else if (p.authMessage) {
603
+ parts.push(`(${p.authMessage})`);
604
+ }
605
+ return parts.join(" ") || "Installed";
606
+ }
607
+ export function printReport(report) {
608
+ const isTTY = process.stdout.isTTY ?? false;
609
+ const sym = isTTY ? SYMBOLS_TTY : SYMBOLS_PLAIN;
610
+ const NAME_WIDTH = 22;
611
+ console.log("");
612
+ console.log("remodex-cli doctor");
613
+ console.log("======================");
614
+ // Required checks
615
+ const required = report.results.filter((r) => r.category === "required");
616
+ if (required.length > 0) {
617
+ console.log("");
618
+ console.log("Required:");
619
+ for (const r of required) {
620
+ const icon = sym[r.status];
621
+ const nameCol = r.name.padEnd(NAME_WIDTH);
622
+ console.log(` ${icon} ${nameCol} ${r.message}`);
623
+ // Print provider sub-items for CLI providers check
624
+ if (r.providers) {
625
+ for (const p of r.providers) {
626
+ const pIcon = providerStatusIcon(p, sym);
627
+ const pName = p.name.padEnd(NAME_WIDTH);
628
+ console.log(` ${pIcon} ${pName} ${providerStatusMessage(p)}`);
629
+ if (p.remediation) {
630
+ console.log(` → ${p.remediation}`);
631
+ }
632
+ }
633
+ }
634
+ else if (r.remediation && (r.status === "fail" || r.status === "warn")) {
635
+ console.log(` → ${r.remediation}`);
636
+ }
637
+ }
638
+ }
639
+ // Optional checks
640
+ const optional = report.results.filter((r) => r.category === "optional");
641
+ if (optional.length > 0) {
642
+ console.log("");
643
+ console.log("Optional:");
644
+ for (const r of optional) {
645
+ const icon = sym[r.status];
646
+ const nameCol = r.name.padEnd(NAME_WIDTH);
647
+ console.log(` ${icon} ${nameCol} ${r.message}`);
648
+ if (r.remediation && (r.status === "fail" || r.status === "warn" || r.status === "skip")) {
649
+ console.log(` → ${r.remediation}`);
650
+ }
651
+ }
652
+ }
653
+ // Summary
654
+ console.log("");
655
+ const failCount = report.results.filter((r) => r.status === "fail").length;
656
+ const warnCount = report.results.filter((r) => r.status === "warn").length;
657
+ if (report.allRequiredPassed) {
658
+ const msg = "All required checks passed.";
659
+ console.log(isTTY ? `\x1b[32m${msg}\x1b[0m` : msg);
660
+ }
661
+ else {
662
+ const msg = `${failCount} required check(s) failed.`;
663
+ console.log(isTTY ? `\x1b[31m${msg}\x1b[0m` : msg);
664
+ }
665
+ if (warnCount > 0) {
666
+ console.log(`${warnCount} warning(s).`);
667
+ }
668
+ console.log("");
669
+ }
670
+ //# sourceMappingURL=doctor.js.map