@meshxdata/fops 0.0.6 → 0.0.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/package.json +3 -2
- package/src/auth/coda.js +4 -4
- package/src/auth/login.js +9 -6
- package/src/commands/index.js +43 -0
- package/src/doctor.js +333 -71
- package/src/feature-flags.js +3 -3
- package/src/lazy.js +12 -0
- package/src/plugins/bundled/coda/auth.js +8 -2
- package/src/plugins/bundled/cursor/index.js +3 -2
- package/src/plugins/bundled/fops-plugin-1password/index.js +6 -4
- package/src/plugins/bundled/fops-plugin-1password/lib/op.js +10 -2
- package/src/plugins/bundled/fops-plugin-1password/lib/setup.js +3 -3
- package/src/plugins/bundled/fops-plugin-1password/lib/sync.js +7 -2
- package/src/plugins/bundled/fops-plugin-ecr/lib/aws.js +5 -6
- package/src/plugins/bundled/fops-plugin-ecr/lib/setup.js +5 -5
- package/src/plugins/loader.js +3 -3
- package/src/setup/aws.js +74 -46
- package/src/setup/setup.js +2 -2
- package/src/setup/wizard.js +16 -32
- package/src/wsl.js +82 -0
package/src/doctor.js
CHANGED
|
@@ -6,8 +6,9 @@ import path from "node:path";
|
|
|
6
6
|
import chalk from "chalk";
|
|
7
7
|
import { execa } from "execa";
|
|
8
8
|
import { rootDir } from "./project.js";
|
|
9
|
-
import inquirer from "inquirer";
|
|
10
9
|
import { detectEcrRegistry, detectAwsSsoProfiles, fixAwsSso, fixEcr } from "./setup/aws.js";
|
|
10
|
+
import { wslExec, wslHomedir, wslFileExists, wslReadFile, wslCmdVersion } from "./wsl.js";
|
|
11
|
+
import { getInquirer } from "./lazy.js";
|
|
11
12
|
|
|
12
13
|
const KEY_PORTS = {
|
|
13
14
|
5432: "Postgres",
|
|
@@ -62,6 +63,41 @@ async function cmdVersion(cmd, args = ["--version"]) {
|
|
|
62
63
|
}
|
|
63
64
|
}
|
|
64
65
|
|
|
66
|
+
/**
|
|
67
|
+
* Find the Claude CLI binary — checks system PATH first, then node_modules/.bin
|
|
68
|
+
* Returns the path to the binary or null if not found.
|
|
69
|
+
*/
|
|
70
|
+
async function findClaudeBinary() {
|
|
71
|
+
// Check system PATH via execa
|
|
72
|
+
try {
|
|
73
|
+
const cmd = process.platform === "win32" ? "where" : "which";
|
|
74
|
+
const { stdout, exitCode } = await execa(cmd, ["claude"], { reject: false, timeout: 3000 });
|
|
75
|
+
if (exitCode === 0 && stdout?.trim()) return stdout.trim().split("\n")[0];
|
|
76
|
+
} catch {}
|
|
77
|
+
|
|
78
|
+
// Check node_modules/.bin relative to this CLI's installation
|
|
79
|
+
const thisDir = path.dirname(new URL(import.meta.url).pathname);
|
|
80
|
+
const candidates = [
|
|
81
|
+
path.join(thisDir, "..", "node_modules", ".bin", "claude"),
|
|
82
|
+
path.join(thisDir, "..", "..", ".bin", "claude"), // when installed as dependency
|
|
83
|
+
];
|
|
84
|
+
for (const candidate of candidates) {
|
|
85
|
+
if (fs.existsSync(candidate)) return candidate;
|
|
86
|
+
}
|
|
87
|
+
return null;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
async function getClaudeVersion() {
|
|
91
|
+
const claudePath = await findClaudeBinary();
|
|
92
|
+
if (!claudePath) return null;
|
|
93
|
+
try {
|
|
94
|
+
const { stdout } = await execa(claudePath, ["--version"], { reject: false, timeout: 5000 });
|
|
95
|
+
return stdout?.split("\n")[0]?.trim() || null;
|
|
96
|
+
} catch {
|
|
97
|
+
return null;
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
|
|
65
101
|
/**
|
|
66
102
|
* Parse ~/.netrc and return the password/token for a given machine, or null.
|
|
67
103
|
*/
|
|
@@ -160,6 +196,63 @@ export async function runDoctor(opts = {}, registry = null) {
|
|
|
160
196
|
}
|
|
161
197
|
}
|
|
162
198
|
|
|
199
|
+
// WSL (Windows only — Docker Desktop uses WSL 2 backend)
|
|
200
|
+
let useWsl = false;
|
|
201
|
+
let wslHome = null;
|
|
202
|
+
if (process.platform === "win32") {
|
|
203
|
+
try {
|
|
204
|
+
const { stdout: wslList, exitCode: wslExit } = await execa("wsl", ["-l", "-v"], {
|
|
205
|
+
timeout: 10000, reject: false,
|
|
206
|
+
});
|
|
207
|
+
if (wslExit === 0 && wslList?.trim()) {
|
|
208
|
+
// Parse distro list — look for at least one WSL 2 distro
|
|
209
|
+
const hasWsl2 = /\s+2\s*$/m.test(wslList);
|
|
210
|
+
if (hasWsl2) {
|
|
211
|
+
ok("WSL 2 enabled", wslList.split("\n").find((l) => /\s+2\s*$/.test(l))?.trim().replace(/\s+/g, " ") || "active");
|
|
212
|
+
useWsl = true;
|
|
213
|
+
try { wslHome = await wslHomedir(); } catch {}
|
|
214
|
+
} else {
|
|
215
|
+
warn("WSL installed but no WSL 2 distro found", "Docker Desktop requires WSL 2", async () => {
|
|
216
|
+
console.log(chalk.cyan(" ▶ wsl --install"));
|
|
217
|
+
await execa("wsl", ["--install"], { stdio: "inherit", timeout: 300_000 });
|
|
218
|
+
console.log(chalk.yellow(" ⚠ A reboot may be required to complete WSL installation."));
|
|
219
|
+
});
|
|
220
|
+
}
|
|
221
|
+
} else {
|
|
222
|
+
fail("WSL not installed", "required for Docker Desktop", async () => {
|
|
223
|
+
console.log(chalk.cyan(" ▶ wsl --install"));
|
|
224
|
+
await execa("wsl", ["--install"], { stdio: "inherit", timeout: 300_000 });
|
|
225
|
+
console.log(chalk.yellow(" ⚠ A reboot may be required to complete WSL installation."));
|
|
226
|
+
});
|
|
227
|
+
}
|
|
228
|
+
} catch {
|
|
229
|
+
fail("WSL not available", "required for Docker Desktop", async () => {
|
|
230
|
+
console.log(chalk.cyan(" ▶ wsl --install"));
|
|
231
|
+
await execa("wsl", ["--install"], { stdio: "inherit", timeout: 300_000 });
|
|
232
|
+
console.log(chalk.yellow(" ⚠ A reboot may be required to complete WSL installation."));
|
|
233
|
+
});
|
|
234
|
+
}
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
// WSL-aware wrappers — when useWsl is true, delegate tool/config checks into WSL
|
|
238
|
+
const run = useWsl
|
|
239
|
+
? (cmd, args, opts) => wslExec(cmd, args, opts)
|
|
240
|
+
: (cmd, args, opts) => execa(cmd, args, opts);
|
|
241
|
+
|
|
242
|
+
const ver = useWsl
|
|
243
|
+
? (cmd, args) => wslCmdVersion(cmd, args)
|
|
244
|
+
: (cmd, args) => cmdVersion(cmd, args);
|
|
245
|
+
|
|
246
|
+
const home = useWsl ? wslHome : os.homedir();
|
|
247
|
+
|
|
248
|
+
const fileExists = useWsl
|
|
249
|
+
? (p) => wslFileExists(p)
|
|
250
|
+
: (p) => fs.existsSync(p);
|
|
251
|
+
|
|
252
|
+
const readFile = useWsl
|
|
253
|
+
? (p) => wslReadFile(p)
|
|
254
|
+
: (p) => fs.readFileSync(p, "utf8");
|
|
255
|
+
|
|
163
256
|
// Docker
|
|
164
257
|
const dockerVer = await cmdVersion("docker");
|
|
165
258
|
if (dockerVer) {
|
|
@@ -216,6 +309,21 @@ export async function runDoctor(opts = {}, registry = null) {
|
|
|
216
309
|
}
|
|
217
310
|
hasBrew = true;
|
|
218
311
|
}
|
|
312
|
+
// Clean up stale Docker cask artifacts that block reinstall
|
|
313
|
+
try {
|
|
314
|
+
await execa("brew", ["uninstall", "--cask", "--force", "docker-desktop"], { stdio: "ignore", timeout: 30_000 });
|
|
315
|
+
console.log(chalk.dim(" Removed leftover docker-desktop cask"));
|
|
316
|
+
} catch {}
|
|
317
|
+
try {
|
|
318
|
+
await execa("brew", ["uninstall", "--cask", "--force", "docker"], { stdio: "ignore", timeout: 30_000 });
|
|
319
|
+
console.log(chalk.dim(" Removed leftover docker cask"));
|
|
320
|
+
} catch {}
|
|
321
|
+
// Remove known conflicting binaries left behind by previous installs
|
|
322
|
+
for (const bin of ["/usr/local/bin/hub-tool"]) {
|
|
323
|
+
if (fs.existsSync(bin)) {
|
|
324
|
+
try { fs.unlinkSync(bin); console.log(chalk.dim(` Removed stale ${bin}`)); } catch {}
|
|
325
|
+
}
|
|
326
|
+
}
|
|
219
327
|
console.log(chalk.cyan(" ▶ brew install --cask docker"));
|
|
220
328
|
await execa("brew", ["install", "--cask", "docker"], { stdio: "inherit", timeout: 300_000 });
|
|
221
329
|
console.log(chalk.cyan(" ▶ open -a Docker"));
|
|
@@ -254,7 +362,7 @@ export async function runDoctor(opts = {}, registry = null) {
|
|
|
254
362
|
else fail("Docker Compose not found", "included with Docker Desktop — restart Docker or install the compose plugin");
|
|
255
363
|
|
|
256
364
|
// Git
|
|
257
|
-
const gitVer = await
|
|
365
|
+
const gitVer = await ver("git");
|
|
258
366
|
if (gitVer) ok("Git available", gitVer);
|
|
259
367
|
else fail("Git not found", "install git", async () => {
|
|
260
368
|
if (process.platform === "darwin") {
|
|
@@ -262,9 +370,14 @@ export async function runDoctor(opts = {}, registry = null) {
|
|
|
262
370
|
console.log(chalk.cyan(" ▶ brew install git"));
|
|
263
371
|
await execa("brew", ["install", "git"], { stdio: "inherit", timeout: 300_000 });
|
|
264
372
|
} else if (process.platform === "win32") {
|
|
265
|
-
if (
|
|
266
|
-
|
|
267
|
-
|
|
373
|
+
if (useWsl) {
|
|
374
|
+
console.log(chalk.cyan(" ▶ [WSL] sudo apt-get install -y git"));
|
|
375
|
+
await run("sudo", ["apt-get", "install", "-y", "git"], { stdio: "inherit", timeout: 300_000 });
|
|
376
|
+
} else {
|
|
377
|
+
if (!hasWinget) throw new Error("winget required");
|
|
378
|
+
console.log(chalk.cyan(" ▶ winget install Git.Git"));
|
|
379
|
+
await execa("winget", ["install", "Git.Git", "--accept-source-agreements", "--accept-package-agreements"], { stdio: "inherit", timeout: 300_000 });
|
|
380
|
+
}
|
|
268
381
|
} else {
|
|
269
382
|
console.log(chalk.cyan(" ▶ sudo apt-get install -y git"));
|
|
270
383
|
await execa("sudo", ["apt-get", "install", "-y", "git"], { stdio: "inherit", timeout: 300_000 });
|
|
@@ -272,21 +385,65 @@ export async function runDoctor(opts = {}, registry = null) {
|
|
|
272
385
|
});
|
|
273
386
|
|
|
274
387
|
// Node.js version
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
388
|
+
if (useWsl) {
|
|
389
|
+
const wslNodeVer = await ver("node", ["-v"]);
|
|
390
|
+
if (wslNodeVer) {
|
|
391
|
+
const wslNodeMajor = parseInt(wslNodeVer.replace(/^v/, "").split(".")[0], 10);
|
|
392
|
+
if (wslNodeMajor >= 18) ok(`Node.js ${wslNodeVer} (WSL)`, ">=18 required");
|
|
393
|
+
else fail(`Node.js ${wslNodeVer} (WSL)`, "upgrade to >=18");
|
|
394
|
+
} else {
|
|
395
|
+
fail("Node.js not found in WSL", "install Node.js >=18 inside WSL");
|
|
396
|
+
}
|
|
397
|
+
} else {
|
|
398
|
+
const nodeVer = process.versions.node;
|
|
399
|
+
const nodeMajor = parseInt(nodeVer.split(".")[0], 10);
|
|
400
|
+
if (nodeMajor >= 18) ok(`Node.js v${nodeVer}`, ">=18 required");
|
|
401
|
+
else fail(`Node.js v${nodeVer}`, "upgrade to >=18", async () => {
|
|
402
|
+
if (process.platform === "darwin") {
|
|
403
|
+
if (!(await ensureBrew())) throw new Error("Homebrew required");
|
|
404
|
+
console.log(chalk.cyan(" ▶ brew install node@22"));
|
|
405
|
+
await execa("brew", ["install", "node@22"], { stdio: "inherit", timeout: 300_000 });
|
|
406
|
+
} else if (process.platform === "win32") {
|
|
407
|
+
if (!hasWinget) throw new Error("winget required");
|
|
408
|
+
console.log(chalk.cyan(" ▶ winget install OpenJS.NodeJS.LTS"));
|
|
409
|
+
await execa("winget", ["install", "OpenJS.NodeJS.LTS", "--accept-source-agreements", "--accept-package-agreements"], { stdio: "inherit", timeout: 300_000 });
|
|
410
|
+
} else {
|
|
411
|
+
console.log(chalk.dim(" Install via: https://nodejs.org/ or use nvm"));
|
|
412
|
+
}
|
|
413
|
+
});
|
|
414
|
+
}
|
|
415
|
+
|
|
416
|
+
// npm (should come with Node.js but verify)
|
|
417
|
+
const npmVer = await cmdVersion("npm");
|
|
418
|
+
if (npmVer) ok("npm", npmVer);
|
|
419
|
+
else fail("npm not found", "usually bundled with Node.js", async () => {
|
|
420
|
+
if (process.platform === "darwin") {
|
|
421
|
+
if (!(await ensureBrew())) throw new Error("Homebrew required");
|
|
422
|
+
console.log(chalk.cyan(" ▶ brew install npm"));
|
|
423
|
+
await execa("brew", ["install", "npm"], { stdio: "inherit", timeout: 300_000 });
|
|
424
|
+
} else {
|
|
425
|
+
console.log(chalk.dim(" npm is bundled with Node.js — reinstall Node.js"));
|
|
426
|
+
}
|
|
286
427
|
});
|
|
287
428
|
|
|
429
|
+
// Claude CLI (bundled as a dependency or installed globally)
|
|
430
|
+
const claudeVer = await getClaudeVersion();
|
|
431
|
+
if (claudeVer) {
|
|
432
|
+
ok("Claude CLI", claudeVer);
|
|
433
|
+
} else {
|
|
434
|
+
const claudePath = await findClaudeBinary();
|
|
435
|
+
if (claudePath) {
|
|
436
|
+
warn("Claude CLI found but not responding", claudePath);
|
|
437
|
+
} else {
|
|
438
|
+
fail("Claude CLI not found", "install globally: npm install -g @anthropic-ai/claude-code", async () => {
|
|
439
|
+
console.log(chalk.cyan(" ▶ npm install -g @anthropic-ai/claude-code"));
|
|
440
|
+
await execa("npm", ["install", "-g", "@anthropic-ai/claude-code"], { stdio: "inherit", timeout: 300_000 });
|
|
441
|
+
});
|
|
442
|
+
}
|
|
443
|
+
}
|
|
444
|
+
|
|
288
445
|
// AWS CLI (optional)
|
|
289
|
-
const awsVer = await
|
|
446
|
+
const awsVer = await ver("aws");
|
|
290
447
|
if (awsVer) ok("AWS CLI", awsVer);
|
|
291
448
|
else warn("AWS CLI not found", "needed for ECR login", async () => {
|
|
292
449
|
if (process.platform === "darwin") {
|
|
@@ -294,9 +451,16 @@ export async function runDoctor(opts = {}, registry = null) {
|
|
|
294
451
|
console.log(chalk.cyan(" ▶ brew install awscli"));
|
|
295
452
|
await execa("brew", ["install", "awscli"], { stdio: "inherit", timeout: 300_000 });
|
|
296
453
|
} else if (process.platform === "win32") {
|
|
297
|
-
if (
|
|
298
|
-
|
|
299
|
-
|
|
454
|
+
if (useWsl) {
|
|
455
|
+
console.log(chalk.cyan(" ▶ [WSL] curl + unzip install"));
|
|
456
|
+
await run("sh", ["-c", 'curl "https://awscli.amazonaws.com/awscli-exe-linux-x86_64.zip" -o /tmp/awscliv2.zip && unzip -qo /tmp/awscliv2.zip -d /tmp && sudo /tmp/aws/install'], {
|
|
457
|
+
stdio: "inherit", timeout: 300_000,
|
|
458
|
+
});
|
|
459
|
+
} else {
|
|
460
|
+
if (!hasWinget) throw new Error("winget required");
|
|
461
|
+
console.log(chalk.cyan(" ▶ winget install Amazon.AWSCLI"));
|
|
462
|
+
await execa("winget", ["install", "Amazon.AWSCLI", "--accept-source-agreements", "--accept-package-agreements"], { stdio: "inherit", timeout: 300_000 });
|
|
463
|
+
}
|
|
300
464
|
} else {
|
|
301
465
|
console.log(chalk.cyan(" ▶ curl + unzip install"));
|
|
302
466
|
await execa("sh", ["-c", 'curl "https://awscli.amazonaws.com/awscli-exe-linux-x86_64.zip" -o /tmp/awscliv2.zip && unzip -qo /tmp/awscliv2.zip -d /tmp && sudo /tmp/aws/install'], {
|
|
@@ -306,7 +470,7 @@ export async function runDoctor(opts = {}, registry = null) {
|
|
|
306
470
|
});
|
|
307
471
|
|
|
308
472
|
// 1Password CLI (optional — needed for secret sync)
|
|
309
|
-
const opVer = await
|
|
473
|
+
const opVer = await ver("op");
|
|
310
474
|
if (opVer) ok("1Password CLI (op)", opVer);
|
|
311
475
|
else warn("1Password CLI (op) not installed", "needed for secret sync", async () => {
|
|
312
476
|
if (process.platform === "darwin") {
|
|
@@ -314,16 +478,20 @@ export async function runDoctor(opts = {}, registry = null) {
|
|
|
314
478
|
console.log(chalk.cyan(" ▶ brew install --cask 1password-cli"));
|
|
315
479
|
await execa("brew", ["install", "--cask", "1password-cli"], { stdio: "inherit", timeout: 300_000 });
|
|
316
480
|
} else if (process.platform === "win32") {
|
|
317
|
-
if (
|
|
318
|
-
|
|
319
|
-
|
|
481
|
+
if (useWsl) {
|
|
482
|
+
console.log(chalk.dim(" Install manually inside WSL: https://developer.1password.com/docs/cli/get-started/#install"));
|
|
483
|
+
} else {
|
|
484
|
+
if (!hasWinget) throw new Error("winget required");
|
|
485
|
+
console.log(chalk.cyan(" ▶ winget install AgileBits.1Password.CLI"));
|
|
486
|
+
await execa("winget", ["install", "AgileBits.1Password.CLI", "--accept-source-agreements", "--accept-package-agreements"], { stdio: "inherit", timeout: 300_000 });
|
|
487
|
+
}
|
|
320
488
|
} else {
|
|
321
489
|
console.log(chalk.dim(" Install manually: https://developer.1password.com/docs/cli/get-started/#install"));
|
|
322
490
|
}
|
|
323
491
|
});
|
|
324
492
|
|
|
325
493
|
// GitHub CLI
|
|
326
|
-
const ghVer = await
|
|
494
|
+
const ghVer = await ver("gh");
|
|
327
495
|
if (ghVer) ok("GitHub CLI (gh)", ghVer);
|
|
328
496
|
else warn("GitHub CLI (gh) not installed", "needed for auth", async () => {
|
|
329
497
|
if (process.platform === "darwin") {
|
|
@@ -331,20 +499,81 @@ export async function runDoctor(opts = {}, registry = null) {
|
|
|
331
499
|
console.log(chalk.cyan(" ▶ brew install gh"));
|
|
332
500
|
await execa("brew", ["install", "gh"], { stdio: "inherit", timeout: 300_000 });
|
|
333
501
|
} else if (process.platform === "win32") {
|
|
334
|
-
if (
|
|
335
|
-
|
|
336
|
-
|
|
502
|
+
if (useWsl) {
|
|
503
|
+
console.log(chalk.cyan(" ▶ [WSL] Install gh via apt"));
|
|
504
|
+
await run("sh", ["-c", "type -p curl >/dev/null || (sudo apt update && sudo apt install curl -y) && curl -fsSL https://cli.github.com/packages/githubcli-archive-keyring.gpg | sudo dd of=/usr/share/keyrings/githubcli-archive-keyring.gpg && sudo chmod go+r /usr/share/keyrings/githubcli-archive-keyring.gpg && echo 'deb [arch=$(dpkg --print-architecture) signed-by=/usr/share/keyrings/githubcli-archive-keyring.gpg] https://cli.github.com/packages stable main' | sudo tee /etc/apt/sources.list.d/github-cli.list > /dev/null && sudo apt update && sudo apt install gh -y"], {
|
|
505
|
+
stdio: "inherit", timeout: 300_000,
|
|
506
|
+
});
|
|
507
|
+
} else {
|
|
508
|
+
if (!hasWinget) throw new Error("winget required");
|
|
509
|
+
console.log(chalk.cyan(" ▶ winget install GitHub.cli"));
|
|
510
|
+
await execa("winget", ["install", "GitHub.cli", "--accept-source-agreements", "--accept-package-agreements"], { stdio: "inherit", timeout: 300_000 });
|
|
511
|
+
}
|
|
337
512
|
} else {
|
|
338
513
|
console.log(chalk.dim(" Install: https://cli.github.com/"));
|
|
339
514
|
}
|
|
340
515
|
});
|
|
341
516
|
|
|
342
517
|
// ~/.netrc GitHub credentials (required for private repo access)
|
|
343
|
-
const netrcPath =
|
|
518
|
+
const netrcPath = useWsl
|
|
519
|
+
? path.posix.join(home, ".netrc")
|
|
520
|
+
: path.join(os.homedir(), ".netrc");
|
|
521
|
+
|
|
522
|
+
/**
|
|
523
|
+
* Write GitHub credentials to ~/.netrc from gh CLI auth.
|
|
524
|
+
* gh auth login stores tokens in its own keychain but doesn't create netrc.
|
|
525
|
+
*/
|
|
526
|
+
const writeNetrcFromGh = async () => {
|
|
527
|
+
const { stdout: ghToken, exitCode: tokenExit } = await run("gh", ["auth", "token"], { timeout: 5000, reject: false });
|
|
528
|
+
if (tokenExit !== 0 || !ghToken?.trim()) {
|
|
529
|
+
throw new Error("gh auth token failed — run 'gh auth login' first");
|
|
530
|
+
}
|
|
531
|
+
const { stdout: ghUser, exitCode: userExit } = await run("gh", ["api", "/user", "--jq", ".login"], { timeout: 10000, reject: false });
|
|
532
|
+
if (userExit !== 0 || !ghUser?.trim()) {
|
|
533
|
+
throw new Error("gh api /user failed — token may lack read:user scope");
|
|
534
|
+
}
|
|
535
|
+
const entry = `machine github.com\nlogin ${ghUser.trim()}\npassword ${ghToken.trim()}\n`;
|
|
536
|
+
if (useWsl) {
|
|
537
|
+
const exists = await fileExists(netrcPath);
|
|
538
|
+
if (exists) {
|
|
539
|
+
const content = await readFile(netrcPath);
|
|
540
|
+
if (!content.includes("github.com")) {
|
|
541
|
+
await wslExec("sh", ["-c", `echo '\n${entry}' >> ${netrcPath}`]);
|
|
542
|
+
} else {
|
|
543
|
+
// Replace existing github.com entry via WSL
|
|
544
|
+
const newContent = content.replace(
|
|
545
|
+
/machine github\.com\s+login\s+\S+\s+password\s+\S+\n?/g,
|
|
546
|
+
entry
|
|
547
|
+
);
|
|
548
|
+
await wslExec("sh", ["-c", `cat > ${netrcPath} << 'NETRCEOF'\n${newContent}\nNETRCEOF`]);
|
|
549
|
+
await wslExec("chmod", ["600", netrcPath]);
|
|
550
|
+
}
|
|
551
|
+
} else {
|
|
552
|
+
await wslExec("sh", ["-c", `echo '${entry}' > ${netrcPath} && chmod 600 ${netrcPath}`]);
|
|
553
|
+
}
|
|
554
|
+
} else {
|
|
555
|
+
if (fs.existsSync(netrcPath)) {
|
|
556
|
+
const content = fs.readFileSync(netrcPath, "utf8");
|
|
557
|
+
if (!content.includes("github.com")) {
|
|
558
|
+
fs.appendFileSync(netrcPath, "\n" + entry);
|
|
559
|
+
} else {
|
|
560
|
+
// Replace existing github.com entry
|
|
561
|
+
const newContent = content.replace(
|
|
562
|
+
/machine github\.com\s+login\s+\S+\s+password\s+\S+\n?/g,
|
|
563
|
+
entry
|
|
564
|
+
);
|
|
565
|
+
fs.writeFileSync(netrcPath, newContent, { mode: 0o600 });
|
|
566
|
+
}
|
|
567
|
+
} else {
|
|
568
|
+
fs.writeFileSync(netrcPath, entry, { mode: 0o600 });
|
|
569
|
+
}
|
|
570
|
+
}
|
|
571
|
+
console.log(chalk.green(" ✓ ~/.netrc updated with GitHub credentials"));
|
|
572
|
+
};
|
|
344
573
|
const netrcFixFn = async () => {
|
|
345
574
|
// Install gh if missing
|
|
346
575
|
let hasGh = false;
|
|
347
|
-
try { await
|
|
576
|
+
try { await run("gh", ["--version"]); hasGh = true; } catch {}
|
|
348
577
|
if (!hasGh) {
|
|
349
578
|
if (process.platform === "darwin") {
|
|
350
579
|
if (!(await ensureBrew())) throw new Error("Homebrew required to install gh");
|
|
@@ -352,39 +581,45 @@ export async function runDoctor(opts = {}, registry = null) {
|
|
|
352
581
|
await execa("brew", ["install", "gh"], { stdio: "inherit", timeout: 300_000 });
|
|
353
582
|
hasGh = true;
|
|
354
583
|
} else if (process.platform === "win32") {
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
584
|
+
if (useWsl) {
|
|
585
|
+
console.log(chalk.cyan(" ▶ [WSL] Install gh via apt"));
|
|
586
|
+
await run("sh", ["-c", "type -p curl >/dev/null || (sudo apt update && sudo apt install curl -y) && curl -fsSL https://cli.github.com/packages/githubcli-archive-keyring.gpg | sudo dd of=/usr/share/keyrings/githubcli-archive-keyring.gpg && sudo chmod go+r /usr/share/keyrings/githubcli-archive-keyring.gpg && echo 'deb [arch=$(dpkg --print-architecture) signed-by=/usr/share/keyrings/githubcli-archive-keyring.gpg] https://cli.github.com/packages stable main' | sudo tee /etc/apt/sources.list.d/github-cli.list > /dev/null && sudo apt update && sudo apt install gh -y"], {
|
|
587
|
+
stdio: "inherit", timeout: 300_000,
|
|
588
|
+
});
|
|
589
|
+
hasGh = true;
|
|
590
|
+
} else {
|
|
591
|
+
console.log(chalk.cyan(" ▶ winget install GitHub.cli"));
|
|
592
|
+
await execa("winget", ["install", "GitHub.cli", "--accept-source-agreements", "--accept-package-agreements"], { stdio: "inherit", timeout: 300_000 });
|
|
593
|
+
hasGh = true;
|
|
594
|
+
}
|
|
358
595
|
}
|
|
359
596
|
}
|
|
597
|
+
|
|
598
|
+
// Check if already authenticated
|
|
599
|
+
const { exitCode: authStatus } = await execa("gh", ["auth", "status"], { timeout: 5000, reject: false });
|
|
600
|
+
if (authStatus === 0) {
|
|
601
|
+
// Already authenticated — just sync to netrc
|
|
602
|
+
console.log(chalk.dim(" gh already authenticated, syncing to ~/.netrc…"));
|
|
603
|
+
await writeNetrcFromGh();
|
|
604
|
+
return;
|
|
605
|
+
}
|
|
606
|
+
|
|
360
607
|
// Authenticate via gh
|
|
361
608
|
console.log(chalk.cyan("\n ▶ gh auth login -p https -h github.com -w"));
|
|
362
|
-
await
|
|
609
|
+
await run("gh", ["auth", "login", "-p", "https", "-h", "github.com", "-w"], {
|
|
363
610
|
stdio: "inherit", timeout: 120_000,
|
|
364
611
|
});
|
|
365
612
|
console.log(chalk.cyan(" ▶ gh auth setup-git"));
|
|
366
|
-
await
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
const { stdout: ghUser } = await execa("gh", ["api", "/user", "--jq", ".login"], { timeout: 10000 });
|
|
371
|
-
if (ghToken?.trim() && ghUser?.trim()) {
|
|
372
|
-
const entry = `machine github.com\nlogin ${ghUser.trim()}\npassword ${ghToken.trim()}\n`;
|
|
373
|
-
if (fs.existsSync(netrcPath)) {
|
|
374
|
-
const content = fs.readFileSync(netrcPath, "utf8");
|
|
375
|
-
if (!content.includes("github.com")) {
|
|
376
|
-
fs.appendFileSync(netrcPath, "\n" + entry);
|
|
377
|
-
}
|
|
378
|
-
} else {
|
|
379
|
-
fs.writeFileSync(netrcPath, entry, { mode: 0o600 });
|
|
380
|
-
}
|
|
381
|
-
console.log(chalk.green(" ✓ ~/.netrc updated with GitHub credentials"));
|
|
382
|
-
}
|
|
383
|
-
} catch {}
|
|
613
|
+
await run("gh", ["auth", "setup-git"], { stdio: "inherit", timeout: 10_000 }).catch(() => {});
|
|
614
|
+
|
|
615
|
+
// Extract token and write to .netrc
|
|
616
|
+
await writeNetrcFromGh();
|
|
384
617
|
};
|
|
385
|
-
|
|
618
|
+
|
|
619
|
+
const netrcExists = await fileExists(netrcPath);
|
|
620
|
+
if (netrcExists) {
|
|
386
621
|
try {
|
|
387
|
-
const content =
|
|
622
|
+
const content = await readFile(netrcPath);
|
|
388
623
|
if (!content.includes("github.com")) {
|
|
389
624
|
fail("~/.netrc exists but no github.com entry", "needed for private repos", netrcFixFn);
|
|
390
625
|
} else {
|
|
@@ -417,41 +652,55 @@ export async function runDoctor(opts = {}, registry = null) {
|
|
|
417
652
|
}
|
|
418
653
|
|
|
419
654
|
// ~/.fops.json config (optional)
|
|
420
|
-
const fopsConfig =
|
|
421
|
-
|
|
655
|
+
const fopsConfig = useWsl
|
|
656
|
+
? path.posix.join(home, ".fops.json")
|
|
657
|
+
: path.join(os.homedir(), ".fops.json");
|
|
658
|
+
const fopsExists = await fileExists(fopsConfig);
|
|
659
|
+
if (fopsExists) ok("~/.fops.json config");
|
|
422
660
|
else warn("~/.fops.json not found", "optional — run fops init to create");
|
|
423
661
|
|
|
424
662
|
// ── AWS / ECR ──────────────────────────────────────
|
|
425
663
|
header("AWS / ECR");
|
|
426
664
|
|
|
427
|
-
const awsConfigPath =
|
|
665
|
+
const awsConfigPath = useWsl
|
|
666
|
+
? path.posix.join(home, ".aws", "config")
|
|
667
|
+
: path.join(os.homedir(), ".aws", "config");
|
|
428
668
|
let awsSessionValid = false;
|
|
429
669
|
|
|
430
|
-
|
|
670
|
+
// Context object for WSL-delegated AWS commands
|
|
671
|
+
const awsCtx = useWsl ? { exec: run, home } : {};
|
|
672
|
+
const boundFixAwsSso = () => fixAwsSso(awsCtx);
|
|
673
|
+
|
|
674
|
+
const awsConfigExists = await fileExists(awsConfigPath);
|
|
675
|
+
if (awsConfigExists) {
|
|
431
676
|
ok("~/.aws/config exists");
|
|
432
677
|
|
|
433
678
|
// Check for SSO session — use detected profile
|
|
434
|
-
|
|
679
|
+
let awsConfigContent = null;
|
|
680
|
+
if (useWsl) {
|
|
681
|
+
try { awsConfigContent = await readFile(awsConfigPath); } catch {}
|
|
682
|
+
}
|
|
683
|
+
const ssoProfiles = awsVer ? detectAwsSsoProfiles(awsConfigContent) : [];
|
|
435
684
|
const defaultProfile = ssoProfiles[0];
|
|
436
685
|
|
|
437
686
|
if (awsVer) {
|
|
438
687
|
const profileArgs = defaultProfile ? ["--profile", defaultProfile.name] : [];
|
|
439
688
|
try {
|
|
440
|
-
const { stdout } = await
|
|
689
|
+
const { stdout } = await run("aws", ["sts", "get-caller-identity", "--output", "json", ...profileArgs], {
|
|
441
690
|
timeout: 10000, reject: false,
|
|
442
691
|
});
|
|
443
692
|
if (stdout && stdout.includes("Account")) {
|
|
444
693
|
ok("AWS SSO session valid");
|
|
445
694
|
awsSessionValid = true;
|
|
446
695
|
} else {
|
|
447
|
-
fail("AWS SSO session expired or invalid", "run: aws sso login",
|
|
696
|
+
fail("AWS SSO session expired or invalid", "run: aws sso login", boundFixAwsSso);
|
|
448
697
|
}
|
|
449
698
|
} catch {
|
|
450
|
-
fail("AWS SSO session check failed", "run: aws sso login",
|
|
699
|
+
fail("AWS SSO session check failed", "run: aws sso login", boundFixAwsSso);
|
|
451
700
|
}
|
|
452
701
|
}
|
|
453
702
|
} else {
|
|
454
|
-
warn("~/.aws/config not found", "needed for ECR",
|
|
703
|
+
warn("~/.aws/config not found", "needed for ECR", boundFixAwsSso);
|
|
455
704
|
}
|
|
456
705
|
|
|
457
706
|
// Validate ECR access if project references ECR images
|
|
@@ -462,20 +711,24 @@ export async function runDoctor(opts = {}, registry = null) {
|
|
|
462
711
|
fail(`ECR registry ${ecrUrl}`, "fix AWS session first");
|
|
463
712
|
} else {
|
|
464
713
|
// Check we can get an ECR login password (same call the actual login uses)
|
|
465
|
-
|
|
714
|
+
let awsConfigContent = null;
|
|
715
|
+
if (useWsl) {
|
|
716
|
+
try { awsConfigContent = await readFile(awsConfigPath); } catch {}
|
|
717
|
+
}
|
|
718
|
+
const ssoProfiles = detectAwsSsoProfiles(awsConfigContent);
|
|
466
719
|
const ecrProfile = ssoProfiles.find((p) => p.region === ecrInfo.region) || ssoProfiles[0];
|
|
467
720
|
const ecrProfileArgs = ecrProfile ? ["--profile", ecrProfile.name] : [];
|
|
468
721
|
try {
|
|
469
|
-
const { stdout, exitCode } = await
|
|
722
|
+
const { stdout, exitCode } = await run("aws", [
|
|
470
723
|
"ecr", "get-login-password", "--region", ecrInfo.region, ...ecrProfileArgs,
|
|
471
724
|
], { timeout: 10000, reject: false });
|
|
472
725
|
if (exitCode === 0 && stdout?.trim()) {
|
|
473
726
|
ok(`ECR registry accessible`, ecrUrl);
|
|
474
727
|
} else {
|
|
475
|
-
fail(`ECR registry not accessible`, `${ecrUrl} — run: fops doctor --fix`, () => fixEcr(ecrInfo));
|
|
728
|
+
fail(`ECR registry not accessible`, `${ecrUrl} — run: fops doctor --fix`, () => fixEcr(ecrInfo, awsCtx));
|
|
476
729
|
}
|
|
477
730
|
} catch {
|
|
478
|
-
fail(`ECR registry check failed`, ecrUrl, () => fixEcr(ecrInfo));
|
|
731
|
+
fail(`ECR registry check failed`, ecrUrl, () => fixEcr(ecrInfo, awsCtx));
|
|
479
732
|
}
|
|
480
733
|
}
|
|
481
734
|
} else if (dir) {
|
|
@@ -582,9 +835,18 @@ export async function runDoctor(opts = {}, registry = null) {
|
|
|
582
835
|
}
|
|
583
836
|
}
|
|
584
837
|
if (size && avail && pct != null) {
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
|
|
838
|
+
const cleanupFix = async () => {
|
|
839
|
+
console.log(chalk.cyan(" ▶ docker system prune -af --volumes"));
|
|
840
|
+
await execa("docker", ["system", "prune", "-af", "--volumes"], { stdio: "inherit", timeout: 300_000 });
|
|
841
|
+
console.log(chalk.dim(" Tip: Also run 'docker builder prune -af' to clear build cache"));
|
|
842
|
+
};
|
|
843
|
+
if (pct >= 90) {
|
|
844
|
+
fail(`Host disk: ${avail} free of ${size}`, "critically low — Docker needs room", cleanupFix);
|
|
845
|
+
} else if (pct >= 80) {
|
|
846
|
+
warn(`Host disk: ${avail} free of ${size}`, "consider running: docker system prune", cleanupFix);
|
|
847
|
+
} else {
|
|
848
|
+
ok(`Host disk: ${avail} free of ${size}`);
|
|
849
|
+
}
|
|
588
850
|
}
|
|
589
851
|
} catch {}
|
|
590
852
|
|
|
@@ -781,7 +1043,7 @@ export async function runDoctor(opts = {}, registry = null) {
|
|
|
781
1043
|
if (fixes.length > 0) {
|
|
782
1044
|
let shouldFix = opts.fix;
|
|
783
1045
|
if (!shouldFix) {
|
|
784
|
-
const { ans } = await
|
|
1046
|
+
const { ans } = await (await getInquirer()).prompt([{ type: "confirm", name: "ans", message: `Fix ${fixes.length} issue(s) automatically?`, default: true }]);
|
|
785
1047
|
shouldFix = ans;
|
|
786
1048
|
}
|
|
787
1049
|
if (shouldFix) {
|
package/src/feature-flags.js
CHANGED
|
@@ -2,7 +2,7 @@ import fs from "node:fs";
|
|
|
2
2
|
import path from "node:path";
|
|
3
3
|
import chalk from "chalk";
|
|
4
4
|
import { execa } from "execa";
|
|
5
|
-
import
|
|
5
|
+
import { getInquirer } from "./lazy.js";
|
|
6
6
|
|
|
7
7
|
/**
|
|
8
8
|
* Canonical feature flags — the complete set known to the platform.
|
|
@@ -130,7 +130,7 @@ export async function runFeatureFlags(root) {
|
|
|
130
130
|
checked: allFlags[name].value,
|
|
131
131
|
}));
|
|
132
132
|
|
|
133
|
-
const { enabled } = await
|
|
133
|
+
const { enabled } = await (await getInquirer()).prompt([{
|
|
134
134
|
type: "checkbox",
|
|
135
135
|
name: "enabled",
|
|
136
136
|
message: "Toggle feature flags:",
|
|
@@ -175,7 +175,7 @@ export async function runFeatureFlags(root) {
|
|
|
175
175
|
const serviceList = [...affectedServices];
|
|
176
176
|
console.log(chalk.dim(` Affected: ${serviceList.join(", ")}`));
|
|
177
177
|
|
|
178
|
-
const { restart } = await
|
|
178
|
+
const { restart } = await (await getInquirer()).prompt([{
|
|
179
179
|
type: "confirm",
|
|
180
180
|
name: "restart",
|
|
181
181
|
message: `Restart ${serviceList.length} service(s)?`,
|
package/src/lazy.js
ADDED
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Lazy-load inquirer to avoid ~20s import penalty on WSL /mnt/c/ filesystem.
|
|
3
|
+
* Only loads when a command actually needs interactive prompts.
|
|
4
|
+
*/
|
|
5
|
+
let _inquirer;
|
|
6
|
+
export async function getInquirer() {
|
|
7
|
+
if (!_inquirer) {
|
|
8
|
+
const mod = await import("inquirer");
|
|
9
|
+
_inquirer = mod.default;
|
|
10
|
+
}
|
|
11
|
+
return _inquirer;
|
|
12
|
+
}
|
|
@@ -49,8 +49,14 @@ function saveCredentials(creds) {
|
|
|
49
49
|
* Open a URL in the user's default browser.
|
|
50
50
|
*/
|
|
51
51
|
function openBrowser(url) {
|
|
52
|
-
|
|
53
|
-
|
|
52
|
+
try {
|
|
53
|
+
if (process.platform === "win32") {
|
|
54
|
+
execSync(`cmd /c start "" "${url}"`, { stdio: "ignore" });
|
|
55
|
+
} else {
|
|
56
|
+
const cmd = process.platform === "darwin" ? "open" : "xdg-open";
|
|
57
|
+
execSync(`${cmd} '${url}'`, { stdio: "ignore" });
|
|
58
|
+
}
|
|
59
|
+
} catch {}
|
|
54
60
|
}
|
|
55
61
|
|
|
56
62
|
/**
|
|
@@ -247,8 +247,9 @@ export function register(api) {
|
|
|
247
247
|
fn: async (ok, warn) => {
|
|
248
248
|
const bin = cursorBin();
|
|
249
249
|
if (bin) {
|
|
250
|
-
const
|
|
251
|
-
|
|
250
|
+
const raw = run(bin, ["--version"]);
|
|
251
|
+
const version = raw ? raw.split("\n")[0].trim() : bin;
|
|
252
|
+
ok("Cursor IDE", version);
|
|
252
253
|
} else {
|
|
253
254
|
warn("Cursor IDE", "not found — install from cursor.com, then: Cmd+Shift+P → Shell Command: Install 'cursor'");
|
|
254
255
|
}
|