@meshxdata/fops 0.0.6 → 0.0.8

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.
@@ -144,9 +144,7 @@ export function register(api) {
144
144
  } else if (whoami.hasAccount) {
145
145
  ok(`1Password configured`, `${whoami.email} — session will unlock on next use`);
146
146
  } else {
147
- warn("1Password not signed in", "enable CLI integration in 1Password app", async () => {
148
- await opSignin();
149
- });
147
+ warn("1Password not signed in", "optional run: op signin");
150
148
  }
151
149
  },
152
150
  });
@@ -163,7 +161,11 @@ export function register(api) {
163
161
 
164
162
  const whoami = await opWhoami();
165
163
  if (!whoami.authenticated) {
166
- console.log(chalk.yellow(" ⚠ 1Password: not signed in — skipping secret sync"));
164
+ if (whoami.hasAccount) {
165
+ console.log(chalk.dim(" 1Password: session locked — unlock the app to auto-sync secrets"));
166
+ } else {
167
+ console.log(chalk.yellow(" ⚠ 1Password: not signed in — skipping secret sync"));
168
+ }
167
169
  return;
168
170
  }
169
171
 
@@ -90,7 +90,7 @@ export async function opSignin() {
90
90
  if (check.exitCode === 0) return;
91
91
  } catch {}
92
92
 
93
- // Desktop app integration not enabled — open settings and wait
93
+ // Desktop app integration not enabled — platform-specific fallback
94
94
  if (process.platform === "darwin") {
95
95
  console.log(" Opening 1Password Developer settings...");
96
96
  console.log(" Enable \"Integrate with 1Password CLI\", then come back.\n");
@@ -105,7 +105,15 @@ export async function opSignin() {
105
105
  if (exitCode === 0) return;
106
106
  } catch {}
107
107
  }
108
+ } else if (process.platform === "linux") {
109
+ // On Linux/WSL there's no desktop app — use interactive signin
110
+ console.log(" No 1Password desktop app on Linux. Signing in interactively...\n");
111
+ try {
112
+ await execa("op", ["signin"], { stdio: "inherit", timeout: 120_000 });
113
+ const check = await execa("op", ["whoami"], { reject: false, timeout: 5000 });
114
+ if (check.exitCode === 0) return;
115
+ } catch {}
108
116
  }
109
117
 
110
- throw new Error("1Password CLI integration not enabled");
118
+ throw new Error("1Password CLI integration not enabled — run: op signin");
111
119
  }
@@ -2,9 +2,9 @@ import fs from "node:fs";
2
2
  import os from "node:os";
3
3
  import path from "node:path";
4
4
  import chalk from "chalk";
5
- import inquirer from "inquirer";
6
5
  import { opVersion, opWhoami, opSignin, opListVaults } from "./op.js";
7
6
  import { discoverTemplates } from "./env.js";
7
+ import { getInquirer } from "../../../../lazy.js";
8
8
 
9
9
  // Patterns that indicate a key is likely a secret
10
10
  const SECRET_PATTERNS = [
@@ -50,14 +50,14 @@ function saveFopsConfig(updates) {
50
50
  }
51
51
 
52
52
  async function ask(message, defaultValue = true) {
53
- const { answer } = await inquirer.prompt([{
53
+ const { answer } = await (await getInquirer()).prompt([{
54
54
  type: "confirm", name: "answer", message, default: defaultValue,
55
55
  }]);
56
56
  return answer;
57
57
  }
58
58
 
59
59
  async function choose(message, choices) {
60
- const { answer } = await inquirer.prompt([{
60
+ const { answer } = await (await getInquirer()).prompt([{
61
61
  type: "list", name: "answer", message, choices,
62
62
  }]);
63
63
  return answer;
@@ -51,9 +51,14 @@ export async function syncSecrets(root) {
51
51
  console.log(chalk.green(` ✓ ${relDir}/.env — ${secrets.size} secret(s) synced`));
52
52
  synced++;
53
53
  } catch (err) {
54
- const msg = `${relDir}: ${err.message}`;
54
+ // Extract clean error: prefer op's stderr over the generic execa message
55
+ const stderr = err.stderr?.trim();
56
+ const clean = stderr
57
+ ? stderr.replace(/^\[ERROR\]\s*\S+\s*/gm, "").trim()
58
+ : err.message.split("\n")[0];
59
+ const msg = `${relDir}: ${clean}`;
55
60
  errors.push(msg);
56
- console.log(chalk.red(` ✗ ${relDir}/.env.1password — ${err.message}`));
61
+ console.log(chalk.red(` ✗ ${relDir}/.env.1password — ${clean}`));
57
62
  }
58
63
  }
59
64
 
@@ -44,12 +44,11 @@ export async function ssoLogin(profile) {
44
44
  const args = ["sso", "login"];
45
45
  if (profile) args.push("--profile", profile);
46
46
 
47
- // Open /dev/tty so SSO login gets a real terminal even under piped stdio
48
- let ttyFd;
49
- try {
50
- ttyFd = fs.openSync("/dev/tty", "r");
51
- } catch {
52
- ttyFd = null;
47
+ // Open /dev/tty so SSO login gets a real terminal even under piped stdio.
48
+ // On Windows /dev/tty doesn't exist — fall back to inherited stdio.
49
+ let ttyFd = null;
50
+ if (process.platform !== "win32") {
51
+ try { ttyFd = fs.openSync("/dev/tty", "r"); } catch { ttyFd = null; }
53
52
  }
54
53
 
55
54
  await execa("aws", args, {
@@ -2,7 +2,6 @@ import fs from "node:fs";
2
2
  import os from "node:os";
3
3
  import path from "node:path";
4
4
  import chalk from "chalk";
5
- import inquirer from "inquirer";
6
5
  import {
7
6
  awsVersion,
8
7
  detectEcrRegistry,
@@ -11,6 +10,7 @@ import {
11
10
  ssoLogin,
12
11
  ecrLogin,
13
12
  } from "./aws.js";
13
+ import { getInquirer } from "../../../../lazy.js";
14
14
 
15
15
  /**
16
16
  * Read ~/.fops.json config (full file).
@@ -53,7 +53,7 @@ export async function runSetupWizard(root) {
53
53
  let version = await awsVersion();
54
54
  if (!version) {
55
55
  console.log(chalk.red(" ✗ AWS CLI not found."));
56
- const { install } = await inquirer.prompt([{
56
+ const { install } = await (await getInquirer()).prompt([{
57
57
  type: "confirm", name: "install", message: "Install via Homebrew?", default: true,
58
58
  }]);
59
59
  if (install) {
@@ -85,7 +85,7 @@ export async function runSetupWizard(root) {
85
85
  console.log(chalk.dim(` Using profile: ${selectedProfile}`));
86
86
  } else {
87
87
  const choices = profiles.map((p) => ({ name: p.name, value: p.name }));
88
- const { profile } = await inquirer.prompt([{
88
+ const { profile } = await (await getInquirer()).prompt([{
89
89
  type: "list", name: "profile", message: "Select AWS profile:", choices,
90
90
  }]);
91
91
  selectedProfile = profile;
@@ -94,7 +94,7 @@ export async function runSetupWizard(root) {
94
94
  // No profiles — prompt for SSO config
95
95
  console.log(chalk.yellow(" No SSO profiles found. Let's configure one."));
96
96
 
97
- const answers = await inquirer.prompt([
97
+ const answers = await (await getInquirer()).prompt([
98
98
  { type: "input", name: "sessionName", message: "SSO session name:", default: "me-central-1" },
99
99
  { type: "input", name: "ssoStartUrl", message: "SSO start URL:", validate: (v) => v?.trim() ? true : "Required." },
100
100
  { type: "input", name: "ssoRegion", message: "SSO region:", default: "us-east-1" },
@@ -154,7 +154,7 @@ output = json
154
154
  }
155
155
 
156
156
  // Step 5: Auto-login preference
157
- const { autoLogin } = await inquirer.prompt([{
157
+ const { autoLogin } = await (await getInquirer()).prompt([{
158
158
  type: "confirm", name: "autoLogin", message: "Auto-login to ECR before `fops up`?", default: true,
159
159
  }]);
160
160
 
@@ -1,7 +1,7 @@
1
1
  import fs from "node:fs";
2
2
  import os from "node:os";
3
3
  import path from "node:path";
4
- import { fileURLToPath } from "node:url";
4
+ import { fileURLToPath, pathToFileURL } from "node:url";
5
5
  import { createRegistry } from "./registry.js";
6
6
  import { validateManifest } from "./manifest.js";
7
7
  import { discoverPlugins } from "./discovery.js";
@@ -127,7 +127,7 @@ async function loadBuiltinPlugins(registry) {
127
127
  const entries = fs.readdirSync(builtinsDir).filter((f) => f.endsWith(".js"));
128
128
  for (const file of entries) {
129
129
  try {
130
- const mod = await import(path.join(builtinsDir, file));
130
+ const mod = await import(pathToFileURL(path.join(builtinsDir, file)).href);
131
131
  const plugin = mod.default || mod;
132
132
  if (typeof plugin.register === "function") {
133
133
  const pluginId = `builtin:${path.basename(file, ".js")}`;
@@ -192,7 +192,7 @@ export async function loadPlugins() {
192
192
  }
193
193
 
194
194
  try {
195
- const mod = await import(entryPoint);
195
+ const mod = await import(pathToFileURL(entryPoint).href);
196
196
  const plugin = mod.default || mod;
197
197
 
198
198
  if (typeof plugin.register === "function") {
@@ -2,7 +2,6 @@ import fs from "node:fs";
2
2
  import os from "node:os";
3
3
  import path from "node:path";
4
4
  import { fileURLToPath } from "node:url";
5
- import { execa } from "execa";
6
5
 
7
6
  const __dirname = path.dirname(fileURLToPath(import.meta.url));
8
7
 
@@ -27,6 +26,7 @@ function parseFrontmatter(content) {
27
26
  */
28
27
  async function hasBin(name) {
29
28
  try {
29
+ const { execa } = await import("execa");
30
30
  await execa("which", [name], { reject: false, timeout: 2000 });
31
31
  return true;
32
32
  } catch {
package/src/setup/aws.js CHANGED
@@ -3,7 +3,7 @@ import os from "node:os";
3
3
  import path from "node:path";
4
4
  import chalk from "chalk";
5
5
  import { execa } from "execa";
6
- import inquirer from "inquirer";
6
+ import { getInquirer } from "../lazy.js";
7
7
 
8
8
  /**
9
9
  * Read saved FOPS config (~/.fops.json) for AWS SSO settings etc.
@@ -34,7 +34,7 @@ export async function promptAwsSsoConfig() {
34
34
  console.log(chalk.dim(" We'll set up an AWS CLI profile for ECR image pulls."));
35
35
  console.log(chalk.dim(" You can find these values in your AWS SSO portal.\n"));
36
36
 
37
- const answers = await inquirer.prompt([
37
+ const answers = await (await getInquirer()).prompt([
38
38
  {
39
39
  type: "input",
40
40
  name: "profileName",
@@ -100,10 +100,13 @@ export function detectEcrRegistry(dir) {
100
100
  /**
101
101
  * Parse ~/.aws/config and find SSO profiles with their sso_session names.
102
102
  */
103
- export function detectAwsSsoProfiles() {
104
- const configPath = path.join(os.homedir(), ".aws", "config");
105
- if (!fs.existsSync(configPath)) return [];
106
- const content = fs.readFileSync(configPath, "utf8");
103
+ export function detectAwsSsoProfiles(configContent = null) {
104
+ if (configContent === null) {
105
+ const configPath = path.join(os.homedir(), ".aws", "config");
106
+ if (!fs.existsSync(configPath)) return [];
107
+ configContent = fs.readFileSync(configPath, "utf8");
108
+ }
109
+ const content = configContent;
107
110
  const profiles = [];
108
111
  let currentProfile = null;
109
112
  let currentAttrs = {};
@@ -151,7 +154,7 @@ export async function ensureSsoConfig() {
151
154
  console.log(chalk.cyan("\n AWS SSO is not configured. Let's set it up.\n"));
152
155
  console.log(chalk.dim(" You can find these values in your AWS SSO portal.\n"));
153
156
 
154
- const answers = await inquirer.prompt([
157
+ const answers = await (await getInquirer()).prompt([
155
158
  { type: "input", name: "sessionName", message: "SSO session name:", default: "me-central-1" },
156
159
  { type: "input", name: "startUrl", message: "SSO start URL:", validate: (v) => v?.trim() ? true : "Required." },
157
160
  { type: "input", name: "ssoRegion", message: "SSO region:", default: "us-east-1" },
@@ -183,86 +186,111 @@ output = json
183
186
  }
184
187
 
185
188
  /**
186
- * Fix AWS SSO: ensure config exists, detect profile, then login.
189
+ * Run `aws sso login` for a profile, handling TTY on all platforms.
190
+ * Returns the execa result ({ exitCode, timedOut }).
187
191
  */
188
- export async function fixAwsSso() {
189
- await ensureSsoConfig();
190
-
191
- const profiles = detectAwsSsoProfiles();
192
- if (profiles.length === 0) {
193
- throw new Error("No SSO profiles found after config — check ~/.aws/config");
192
+ async function runSsoLogin(profileName) {
193
+ let ttyFd = null;
194
+ if (process.platform !== "win32") {
195
+ try { ttyFd = fs.openSync("/dev/tty", "r"); } catch { ttyFd = null; }
194
196
  }
195
197
 
196
- const profile = profiles[0];
197
- console.log(chalk.dim(` Using AWS profile: ${profile.name}`));
198
- console.log(chalk.cyan(` ▶ aws sso login --profile ${profile.name}`));
199
-
200
- // Open /dev/tty directly so SSO login gets a real terminal even when
201
- // the parent process has piped stdio (e.g. agent → runShellCommand → fops doctor)
202
- let ttyFd;
203
- try { ttyFd = fs.openSync("/dev/tty", "r"); } catch { ttyFd = null; }
204
-
205
- const { exitCode } = await execa("aws", ["sso", "login", "--profile", profile.name], {
198
+ const result = await execa("aws", ["sso", "login", "--profile", profileName], {
206
199
  stdio: [ttyFd ?? "inherit", "inherit", "inherit"],
207
200
  reject: false,
208
201
  timeout: 120_000,
209
202
  });
210
203
 
211
204
  if (ttyFd !== null) fs.closeSync(ttyFd);
205
+ return result;
206
+ }
212
207
 
213
- if (exitCode !== 0) {
214
- // SSO login failed likely bad config. Remove it and re-run setup.
215
- console.log(chalk.yellow(" SSO login failed re-running setup with new values...\n"));
216
- const configPath = path.join(os.homedir(), ".aws", "config");
217
- if (fs.existsSync(configPath)) fs.unlinkSync(configPath);
218
- await ensureSsoConfig();
208
+ /**
209
+ * Fix AWS SSO: ensure config exists, detect profile, then login.
210
+ * Distinguishes timeout/incomplete browser auth from bad config so
211
+ * the user isn't forced to re-enter values when they just ran out of time.
212
+ */
213
+ export async function fixAwsSso(ctx = {}) {
214
+ const exec = ctx.exec || execa;
215
+ const home = ctx.home || os.homedir();
216
+
217
+ await ensureSsoConfig();
219
218
 
220
- const retryProfiles = detectAwsSsoProfiles();
221
- if (retryProfiles.length === 0) {
219
+ const MAX_ATTEMPTS = 3;
220
+
221
+ for (let attempt = 0; attempt < MAX_ATTEMPTS; attempt++) {
222
+ const profiles = detectAwsSsoProfiles();
223
+ if (profiles.length === 0) {
222
224
  throw new Error("No SSO profiles found after config — check ~/.aws/config");
223
225
  }
224
226
 
225
- const retryProfile = retryProfiles[0];
226
- console.log(chalk.cyan(` aws sso login --profile ${retryProfile.name}`));
227
-
228
- let retryTtyFd;
229
- try { retryTtyFd = fs.openSync("/dev/tty", "r"); } catch { retryTtyFd = null; }
227
+ const profile = profiles[0];
228
+ console.log(chalk.dim(` Using AWS profile: ${profile.name}`));
229
+ console.log(chalk.cyan(` ▶ aws sso login --profile ${profile.name}`));
230
230
 
231
- const { exitCode: retryCode } = await execa("aws", ["sso", "login", "--profile", retryProfile.name], {
232
- stdio: [retryTtyFd ?? "inherit", "inherit", "inherit"],
231
+ const result = await exec("aws", ["sso", "login", "--profile", profile.name], {
232
+ stdio: "inherit",
233
233
  reject: false,
234
234
  timeout: 120_000,
235
235
  });
236
236
 
237
- if (retryTtyFd !== null) fs.closeSync(retryTtyFd);
237
+ if (result.exitCode === 0) return;
238
+
239
+ if (result.timedOut) {
240
+ console.log(chalk.yellow("\n SSO login timed out — browser auth was not completed in time."));
241
+ } else {
242
+ console.log(chalk.yellow("\n SSO login did not complete."));
243
+ }
244
+
245
+ const { action } = await (await getInquirer()).prompt([{
246
+ type: "list",
247
+ name: "action",
248
+ message: "What would you like to do?",
249
+ choices: [
250
+ { name: "Retry login (same settings)", value: "retry" },
251
+ { name: "Reconfigure SSO (re-enter values)", value: "reconfig" },
252
+ { name: "Abort", value: "abort" },
253
+ ],
254
+ }]);
255
+
256
+ if (action === "abort") {
257
+ throw new Error("SSO login aborted by user");
258
+ }
238
259
 
239
- if (retryCode !== 0) {
240
- throw new Error("SSO login failed. Check your SSO start URL and region in ~/.aws/config");
260
+ if (action === "reconfig") {
261
+ const configPath = path.join(home, ".aws", "config");
262
+ if (fs.existsSync(configPath)) fs.unlinkSync(configPath);
263
+ await ensureSsoConfig();
241
264
  }
265
+ // "retry" loops again with the same config
242
266
  }
267
+
268
+ throw new Error("SSO login failed after multiple attempts. Check your SSO start URL and region in ~/.aws/config");
243
269
  }
244
270
 
245
271
  /**
246
272
  * Fix ECR: ensure SSO session is valid, then docker login to ECR.
247
273
  */
248
- export async function fixEcr(ecrInfo) {
274
+ export async function fixEcr(ecrInfo, ctx = {}) {
275
+ const exec = ctx.exec || execa;
276
+
249
277
  const profiles = detectAwsSsoProfiles();
250
278
  // Pick the profile whose region matches ECR, or fall back to first
251
279
  const profile = profiles.find((p) => p.region === ecrInfo.region) || profiles[0];
252
280
  const profileArgs = profile ? ["--profile", profile.name] : [];
253
281
 
254
282
  // First make sure SSO works
255
- const { stdout } = await execa("aws", ["sts", "get-caller-identity", "--output", "json", ...profileArgs], {
283
+ const { stdout } = await exec("aws", ["sts", "get-caller-identity", "--output", "json", ...profileArgs], {
256
284
  reject: false, timeout: 10000,
257
285
  });
258
286
  if (!stdout || !stdout.includes("Account")) {
259
- await fixAwsSso();
287
+ await fixAwsSso(ctx);
260
288
  }
261
289
 
262
290
  // Now do ECR docker login
263
291
  const ecrUrl = `${ecrInfo.accountId}.dkr.ecr.${ecrInfo.region}.amazonaws.com`;
264
292
  console.log(chalk.cyan(` ▶ ECR docker login → ${ecrUrl}`));
265
- const { stdout: password } = await execa("aws", [
293
+ const { stdout: password } = await exec("aws", [
266
294
  "ecr", "get-login-password", "--region", ecrInfo.region, ...profileArgs,
267
295
  ], { reject: false, timeout: 15000 });
268
296
 
@@ -3,9 +3,9 @@ import os from "node:os";
3
3
  import path from "node:path";
4
4
  import chalk from "chalk";
5
5
  import { execa } from "execa";
6
- import inquirer from "inquirer";
7
6
  import { make } from "../shell.js";
8
7
  import { readFopsConfig, saveFopsConfig, promptAwsSsoConfig, detectEcrRegistry, checkEcrRepos } from "./aws.js";
8
+ import { getInquirer } from "../lazy.js";
9
9
 
10
10
  // TODO: change back to "main" once stack/api is merged
11
11
  export const CLONE_BRANCH = "stack/api";
@@ -70,7 +70,7 @@ export function runSetup(dir, opts = {}) {
70
70
  // Check if docker-compose references ECR to auto-detect some values
71
71
  const ecrInfo = detectEcrRegistry(dir);
72
72
 
73
- const { setupAws } = await inquirer.prompt([{
73
+ const { setupAws } = await (await getInquirer()).prompt([{
74
74
  type: "confirm",
75
75
  name: "setupAws",
76
76
  message: ecrInfo
@@ -3,12 +3,12 @@ import os from "node:os";
3
3
  import path from "node:path";
4
4
  import chalk from "chalk";
5
5
  import { execa } from "execa";
6
- import inquirer from "inquirer";
7
6
  import { isFoundationRoot, findComposeRootUp } from "../project.js";
8
7
  import { discoverPlugins } from "../plugins/discovery.js";
9
8
  import { validateManifest } from "../plugins/manifest.js";
10
9
  import { runSetup, CLONE_BRANCH } from "./setup.js";
11
- import { confirm } from "../ui/index.js";
10
+ import { confirm, selectOption } from "../ui/index.js";
11
+ import { getInquirer } from "../lazy.js";
12
12
 
13
13
  /**
14
14
  * Ensure Homebrew is available (macOS). Installs if missing.
@@ -76,7 +76,7 @@ export async function runInitWizard() {
76
76
  } else {
77
77
  const foundUp = findComposeRootUp(cwd);
78
78
  if (foundUp && foundUp !== cwd) {
79
- const { useFound } = await inquirer.prompt([
79
+ const { useFound } = await (await getInquirer()).prompt([
80
80
  { type: "confirm", name: "useFound", message: `Found Foundation project at:\n ${foundUp}\n Use it instead of the current directory?`, default: false },
81
81
  ]);
82
82
  if (useFound) projectRoot = foundUp;
@@ -130,24 +130,9 @@ export async function runInitWizard() {
130
130
  hasAws = await installTool("AWS CLI", { brew: "awscli", winget: "Amazon.AWSCLI" });
131
131
  }
132
132
 
133
- // 1Password desktop app (optional — needed for CLI integration)
134
- const has1PwdApp = process.platform === "darwin"
135
- ? fs.existsSync("/Applications/1Password.app")
136
- : process.platform === "win32"
137
- ? fs.existsSync(path.join(process.env.LOCALAPPDATA || "", "1Password", "app", "8", "1Password.exe"))
138
- : false;
139
- if (has1PwdApp) console.log(chalk.green(" ✓ 1Password"));
140
- else {
141
- console.log(chalk.yellow(" ⚠ 1Password") + chalk.dim(" — desktop app needed for CLI integration"));
142
- await installTool("1Password", { brewCask: "1password", winget: "AgileBits.1Password" });
143
- }
144
-
145
- // 1Password CLI (optional)
133
+ // 1Password (optional — status only, no install prompt)
146
134
  if (hasOp) console.log(chalk.green(" ✓ 1Password CLI"));
147
- else {
148
- console.log(chalk.yellow(" ⚠ 1Password CLI") + chalk.dim(" — needed for secret sync"));
149
- hasOp = await installTool("1Password CLI", { brewCask: "1password-cli", winget: "AgileBits.1Password.CLI" });
150
- }
135
+ else console.log(chalk.yellow(" ⚠ 1Password CLI") + chalk.dim(" — optional, run: op signin"));
151
136
 
152
137
  // GitHub credentials
153
138
  const netrcPath = path.join(os.homedir(), ".netrc");
@@ -185,19 +170,18 @@ export async function runInitWizard() {
185
170
  console.log(chalk.red(" Required tools are still missing. Install them and run fops init again.\n"));
186
171
  process.exit(1);
187
172
  }
188
- const choices = [
189
- { name: "Clone foundation-compose into this directory", value: "clone" },
190
- { name: "Enter path to an existing foundation-compose directory", value: "path" },
191
- { name: "Cancel", value: "cancel" },
192
- ];
193
- const { action } = await inquirer.prompt([{ type: "list", name: "action", message: "No Foundation project found. What do you want to do?", choices }]);
194
- if (action === "cancel") process.exit(0);
173
+ const action = await selectOption("No Foundation project found. What do you want to do?", [
174
+ { label: "Clone foundation-compose into this directory", value: "clone" },
175
+ { label: "Enter path to an existing foundation-compose directory", value: "path" },
176
+ { label: "Cancel", value: "cancel" },
177
+ ]);
178
+ if (!action || action === "cancel") process.exit(0);
195
179
  if (action === "clone") {
196
- const { repoUrl } = await inquirer.prompt([
180
+ const { repoUrl } = await (await getInquirer()).prompt([
197
181
  { type: "input", name: "repoUrl", message: "Repository URL:", default: "https://github.com/meshxdata/foundation-compose.git", validate: (v) => (v?.trim() ? true : "Repository URL is required.") },
198
182
  ]);
199
183
  const repoName = repoUrl.trim().replace(/\.git$/, "").split("/").pop() || "foundation-compose";
200
- const { targetDir } = await inquirer.prompt([
184
+ const { targetDir } = await (await getInquirer()).prompt([
201
185
  { type: "input", name: "targetDir", message: "Clone into:", default: path.join(cwd, repoName) },
202
186
  ]);
203
187
  const resolved = path.resolve(targetDir.trim());
@@ -238,7 +222,7 @@ export async function runInitWizard() {
238
222
  }
239
223
  }
240
224
  if (action === "path") {
241
- const { dir } = await inquirer.prompt([
225
+ const { dir } = await (await getInquirer()).prompt([
242
226
  {
243
227
  type: "input", name: "dir", message: "Path to foundation-compose directory:",
244
228
  validate: (value) => {
@@ -253,7 +237,7 @@ export async function runInitWizard() {
253
237
  }
254
238
  }
255
239
  }
256
- const { submodules, env, download } = await inquirer.prompt([
240
+ const { submodules, env, download } = await (await getInquirer()).prompt([
257
241
  { type: "confirm", name: "submodules", message: "Initialize and update git submodules?", default: true },
258
242
  { type: "confirm", name: "env", message: "Create .env from .env.example (if missing)?", default: true },
259
243
  { type: "confirm", name: "download", message: "Download container images now (make download)?", default: false },
@@ -292,7 +276,7 @@ export async function runInitWizard() {
292
276
  };
293
277
  });
294
278
 
295
- const { enabledPlugins } = await inquirer.prompt([{
279
+ const { enabledPlugins } = await (await getInquirer()).prompt([{
296
280
  type: "checkbox",
297
281
  name: "enabledPlugins",
298
282
  message: "Plugins:",
package/src/shell.js CHANGED
@@ -1,9 +1,15 @@
1
- import { execa } from "execa";
1
+ let _execa;
2
+ async function lazyExeca() {
3
+ if (!_execa) _execa = (await import("execa")).execa;
4
+ return _execa;
5
+ }
2
6
 
3
7
  export async function make(root, target, args = []) {
8
+ const execa = await lazyExeca();
4
9
  return execa("make", [target, ...args], { cwd: root, stdio: "inherit", reject: false });
5
10
  }
6
11
 
7
12
  export async function dockerCompose(root, args) {
13
+ const execa = await lazyExeca();
8
14
  return execa("docker", ["compose", ...args], { cwd: root, stdio: "inherit", reject: false });
9
15
  }
package/src/ui/confirm.js CHANGED
@@ -76,7 +76,16 @@ export async function selectOption(message, options) {
76
76
  resolved = true;
77
77
  clear();
78
78
  unmount();
79
- setTimeout(() => resolve(selected ? selected.value : null), 50);
79
+ // Fix for Node 24+: restore stdin state after ink unmounts
80
+ // ink leaves stdin paused/in raw mode which breaks inquirer
81
+ setTimeout(() => {
82
+ if (process.stdin.isTTY) {
83
+ process.stdin.setRawMode(false);
84
+ process.stdin.resume();
85
+ process.stdin.ref();
86
+ }
87
+ resolve(selected ? selected.value : null);
88
+ }, 100);
80
89
  };
81
90
  const { unmount, clear } = render(
82
91
  h(SelectPrompt, { message, options: normalized, onResult })
package/src/wsl.js ADDED
@@ -0,0 +1,82 @@
1
+ import { execa } from "execa";
2
+
3
+ /**
4
+ * Run a command inside the default WSL distro.
5
+ */
6
+ export async function wslExec(cmd, args = [], opts = {}) {
7
+ const { input, timeout, reject, stdio } = opts;
8
+ return execa("wsl", ["-e", cmd, ...args], { input, timeout, reject, stdio });
9
+ }
10
+
11
+ let _cachedHome = null;
12
+
13
+ /**
14
+ * Get the WSL user's home directory (cached after first call).
15
+ */
16
+ export async function wslHomedir() {
17
+ if (_cachedHome) return _cachedHome;
18
+ const { stdout } = await execa("wsl", ["-e", "sh", "-c", "echo $HOME"], {
19
+ timeout: 10000,
20
+ });
21
+ _cachedHome = stdout.trim();
22
+ return _cachedHome;
23
+ }
24
+
25
+ /**
26
+ * Reset the cached WSL home directory (for testing).
27
+ */
28
+ export function _resetHomedirCache() {
29
+ _cachedHome = null;
30
+ }
31
+
32
+ /**
33
+ * Check whether a file exists inside WSL.
34
+ */
35
+ export async function wslFileExists(filepath) {
36
+ try {
37
+ const { exitCode } = await execa("wsl", ["-e", "test", "-f", filepath], {
38
+ reject: false,
39
+ timeout: 5000,
40
+ });
41
+ return exitCode === 0;
42
+ } catch {
43
+ return false;
44
+ }
45
+ }
46
+
47
+ /**
48
+ * Read file content from inside WSL.
49
+ */
50
+ export async function wslReadFile(filepath) {
51
+ const { stdout } = await execa("wsl", ["-e", "cat", filepath], {
52
+ timeout: 5000,
53
+ });
54
+ return stdout;
55
+ }
56
+
57
+ /**
58
+ * Write content to a file inside WSL.
59
+ */
60
+ export async function wslWriteFile(filepath, content) {
61
+ await execa("wsl", ["-e", "tee", filepath], {
62
+ input: content,
63
+ timeout: 5000,
64
+ stdout: "ignore",
65
+ });
66
+ }
67
+
68
+ /**
69
+ * Get the version string for a command inside WSL.
70
+ * Returns the first line of output, or null if the command fails.
71
+ */
72
+ export async function wslCmdVersion(cmd, args = ["--version"]) {
73
+ try {
74
+ const { stdout } = await execa("wsl", ["-e", cmd, ...args], {
75
+ reject: false,
76
+ timeout: 10000,
77
+ });
78
+ return stdout?.split("\n")[0]?.trim() || null;
79
+ } catch {
80
+ return null;
81
+ }
82
+ }