@meshxdata/fops 0.0.5 → 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.
Files changed (44) hide show
  1. package/package.json +3 -2
  2. package/src/auth/coda.js +4 -4
  3. package/src/auth/login.js +9 -6
  4. package/src/commands/index.js +158 -0
  5. package/src/doctor.js +340 -71
  6. package/src/feature-flags.js +3 -3
  7. package/src/lazy.js +12 -0
  8. package/src/plugins/bundled/coda/auth.js +85 -0
  9. package/src/plugins/bundled/coda/client.js +187 -0
  10. package/src/plugins/bundled/coda/fops.plugin.json +7 -0
  11. package/src/plugins/bundled/coda/index.js +284 -0
  12. package/src/plugins/bundled/coda/package.json +3 -0
  13. package/src/plugins/bundled/coda/skills/coda/SKILL.md +82 -0
  14. package/src/plugins/bundled/cursor/fops.plugin.json +7 -0
  15. package/src/plugins/bundled/cursor/index.js +433 -0
  16. package/src/plugins/bundled/cursor/package.json +1 -0
  17. package/src/plugins/bundled/cursor/skills/cursor/SKILL.md +48 -0
  18. package/src/plugins/bundled/fops-plugin-1password/fops.plugin.json +7 -0
  19. package/src/plugins/bundled/fops-plugin-1password/index.js +241 -0
  20. package/src/plugins/bundled/fops-plugin-1password/lib/env.js +100 -0
  21. package/src/plugins/bundled/fops-plugin-1password/lib/op.js +119 -0
  22. package/src/plugins/bundled/fops-plugin-1password/lib/setup.js +235 -0
  23. package/src/plugins/bundled/fops-plugin-1password/lib/sync.js +66 -0
  24. package/src/plugins/bundled/fops-plugin-1password/package.json +1 -0
  25. package/src/plugins/bundled/fops-plugin-1password/skills/1password/SKILL.md +79 -0
  26. package/src/plugins/bundled/fops-plugin-ecr/fops.plugin.json +7 -0
  27. package/src/plugins/bundled/fops-plugin-ecr/index.js +302 -0
  28. package/src/plugins/bundled/fops-plugin-ecr/lib/aws.js +146 -0
  29. package/src/plugins/bundled/fops-plugin-ecr/lib/images.js +73 -0
  30. package/src/plugins/bundled/fops-plugin-ecr/lib/setup.js +180 -0
  31. package/src/plugins/bundled/fops-plugin-ecr/lib/sync.js +74 -0
  32. package/src/plugins/bundled/fops-plugin-ecr/package.json +1 -0
  33. package/src/plugins/bundled/fops-plugin-ecr/skills/ecr/SKILL.md +105 -0
  34. package/src/plugins/bundled/fops-plugin-memory/fops.plugin.json +7 -0
  35. package/src/plugins/bundled/fops-plugin-memory/index.js +148 -0
  36. package/src/plugins/bundled/fops-plugin-memory/lib/relevance.js +72 -0
  37. package/src/plugins/bundled/fops-plugin-memory/lib/store.js +75 -0
  38. package/src/plugins/bundled/fops-plugin-memory/package.json +1 -0
  39. package/src/plugins/bundled/fops-plugin-memory/skills/memory/SKILL.md +58 -0
  40. package/src/plugins/loader.js +43 -3
  41. package/src/setup/aws.js +74 -46
  42. package/src/setup/setup.js +4 -2
  43. package/src/setup/wizard.js +16 -20
  44. package/src/wsl.js +82 -0
@@ -0,0 +1,58 @@
1
+ ---
2
+ name: memory
3
+ description: Persistent memory across agent sessions
4
+ ---
5
+
6
+ ## Agent Memory
7
+
8
+ You have persistent memory across sessions. Relevant memories are automatically recalled into your context when they match the user's query. You can also save new memories during conversations.
9
+
10
+ ## When to Save Memories
11
+
12
+ Save a memory when you learn something that would be useful in **future sessions**:
13
+
14
+ - **Resolved issues**: "postgres init fails if vault isn't started first — start vault before running migrations"
15
+ - **User preferences**: "user prefers to rebuild images rather than pull from ECR"
16
+ - **Stack quirks**: "trino takes 30-45 seconds to become healthy after container starts"
17
+ - **Configuration discoveries**: "frontend needs NEXT_PUBLIC_API_URL set to http://localhost:9001 in .env"
18
+ - **Debugging insights**: "backend OOM usually means too many Kafka consumers — reduce KAFKA_MAX_POLL_RECORDS"
19
+ - **Workflow patterns**: "user runs fops down && fops up to reset state, not just restart"
20
+
21
+ ## How to Save
22
+
23
+ ```bash
24
+ fops memory save "description of what you learned"
25
+ fops memory save "postgres needs vault running first" --tag postgres --tag startup
26
+ ```
27
+
28
+ Tags help with recall but are optional. The text itself is searched.
29
+
30
+ ## When NOT to Save
31
+
32
+ - Transient state (container is currently down, an image was just pulled)
33
+ - Information already in the stack context (service ports, container health)
34
+ - Generic knowledge (Docker commands, AWS docs)
35
+ - Anything the user explicitly asks you not to remember
36
+
37
+ ## Commands
38
+
39
+ ```bash
40
+ fops memory save "text" [--tag tag1 --tag tag2] # Save a memory
41
+ fops memory list # List all memories
42
+ fops memory search "query" # Search by relevance
43
+ fops memory forget <id> # Remove a memory
44
+ fops memory clear # Clear all memories
45
+ fops mem ... # Shorthand alias
46
+ ```
47
+
48
+ ## Recall
49
+
50
+ Memories are automatically recalled via the knowledge system. When the user's message matches stored memories, they appear in your context under "Agent Memory". You don't need to explicitly search — relevant memories are injected per turn.
51
+
52
+ ## Housekeeping
53
+
54
+ If you notice a recalled memory is outdated or incorrect, suggest removing it:
55
+
56
+ ```bash
57
+ fops memory forget abc123
58
+ ```
@@ -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";
@@ -10,6 +10,45 @@ import { loadBuiltinAgents } from "../agent/agents.js";
10
10
 
11
11
  const __dirname = path.dirname(fileURLToPath(import.meta.url));
12
12
 
13
+ /**
14
+ * Sync bundled plugins from src/plugins/bundled/ to ~/.fops/plugins/.
15
+ * Installs missing plugins and updates existing ones when the bundled version is newer.
16
+ */
17
+ function syncBundledPlugins() {
18
+ const bundledDir = path.join(__dirname, "bundled");
19
+ if (!fs.existsSync(bundledDir)) return;
20
+
21
+ const globalDir = path.join(os.homedir(), ".fops", "plugins");
22
+ fs.mkdirSync(globalDir, { recursive: true });
23
+
24
+ const entries = fs.readdirSync(bundledDir, { withFileTypes: true });
25
+ for (const entry of entries) {
26
+ if (!entry.isDirectory()) continue;
27
+ const srcDir = path.join(bundledDir, entry.name);
28
+ const manifestPath = path.join(srcDir, "fops.plugin.json");
29
+ if (!fs.existsSync(manifestPath)) continue;
30
+
31
+ let manifest;
32
+ try { manifest = JSON.parse(fs.readFileSync(manifestPath, "utf8")); } catch { continue; }
33
+
34
+ const destDir = path.join(globalDir, entry.name);
35
+ const destManifest = path.join(destDir, "fops.plugin.json");
36
+
37
+ // Skip if installed version is same or newer
38
+ if (fs.existsSync(destManifest)) {
39
+ try {
40
+ const existing = JSON.parse(fs.readFileSync(destManifest, "utf8"));
41
+ if (existing.version >= manifest.version) continue;
42
+ } catch {
43
+ // corrupt manifest — overwrite
44
+ }
45
+ }
46
+
47
+ // Copy plugin to ~/.fops/plugins/<name>/
48
+ fs.cpSync(srcDir, destDir, { recursive: true });
49
+ }
50
+ }
51
+
13
52
  /**
14
53
  * Ensure ~/.fops/plugins/node_modules symlinks to the CLI's node_modules.
15
54
  * This lets global plugins resolve bare imports (chalk, execa, inquirer, etc.)
@@ -88,7 +127,7 @@ async function loadBuiltinPlugins(registry) {
88
127
  const entries = fs.readdirSync(builtinsDir).filter((f) => f.endsWith(".js"));
89
128
  for (const file of entries) {
90
129
  try {
91
- const mod = await import(path.join(builtinsDir, file));
130
+ const mod = await import(pathToFileURL(path.join(builtinsDir, file)).href);
92
131
  const plugin = mod.default || mod;
93
132
  if (typeof plugin.register === "function") {
94
133
  const pluginId = `builtin:${path.basename(file, ".js")}`;
@@ -106,6 +145,7 @@ async function loadBuiltinPlugins(registry) {
106
145
  * Returns a populated PluginRegistry.
107
146
  */
108
147
  export async function loadPlugins() {
148
+ syncBundledPlugins();
109
149
  ensurePluginNodeModules();
110
150
  const registry = createRegistry();
111
151
  loadBuiltinAgents(registry);
@@ -152,7 +192,7 @@ export async function loadPlugins() {
152
192
  }
153
193
 
154
194
  try {
155
- const mod = await import(entryPoint);
195
+ const mod = await import(pathToFileURL(entryPoint).href);
156
196
  const plugin = mod.default || mod;
157
197
 
158
198
  if (typeof plugin.register === "function") {
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
@@ -109,6 +109,7 @@ export function runSetup(dir, opts = {}) {
109
109
  } catch {
110
110
  console.log(chalk.yellow("\n⚠ Some images failed to download."));
111
111
  }
112
+ console.log("");
112
113
  console.log(chalk.green("Setup complete. Run: fops up"));
113
114
  return;
114
115
  }
@@ -162,6 +163,7 @@ export function runSetup(dir, opts = {}) {
162
163
  console.log(chalk.dim(" Then re-run: fops init --download\n"));
163
164
  }
164
165
  }
166
+ console.log("");
165
167
  console.log(chalk.green("Setup complete. Run: fops up"));
166
168
  })();
167
169
  }
@@ -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,12 +130,9 @@ export async function runInitWizard() {
130
130
  hasAws = await installTool("AWS CLI", { brew: "awscli", winget: "Amazon.AWSCLI" });
131
131
  }
132
132
 
133
- // 1Password CLI (optional)
133
+ // 1Password (optional — status only, no install prompt)
134
134
  if (hasOp) console.log(chalk.green(" ✓ 1Password CLI"));
135
- else {
136
- console.log(chalk.yellow(" ⚠ 1Password CLI") + chalk.dim(" — needed for secret sync"));
137
- hasOp = await installTool("1Password CLI", { brewCask: "1password-cli", winget: "AgileBits.1Password.CLI" });
138
- }
135
+ else console.log(chalk.yellow(" ⚠ 1Password CLI") + chalk.dim(" — optional, run: op signin"));
139
136
 
140
137
  // GitHub credentials
141
138
  const netrcPath = path.join(os.homedir(), ".netrc");
@@ -173,19 +170,18 @@ export async function runInitWizard() {
173
170
  console.log(chalk.red(" Required tools are still missing. Install them and run fops init again.\n"));
174
171
  process.exit(1);
175
172
  }
176
- const choices = [
177
- { name: "Clone foundation-compose into this directory", value: "clone" },
178
- { name: "Enter path to an existing foundation-compose directory", value: "path" },
179
- { name: "Cancel", value: "cancel" },
180
- ];
181
- const { action } = await inquirer.prompt([{ type: "list", name: "action", message: "No Foundation project found. What do you want to do?", choices }]);
182
- 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);
183
179
  if (action === "clone") {
184
- const { repoUrl } = await inquirer.prompt([
180
+ const { repoUrl } = await (await getInquirer()).prompt([
185
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.") },
186
182
  ]);
187
183
  const repoName = repoUrl.trim().replace(/\.git$/, "").split("/").pop() || "foundation-compose";
188
- const { targetDir } = await inquirer.prompt([
184
+ const { targetDir } = await (await getInquirer()).prompt([
189
185
  { type: "input", name: "targetDir", message: "Clone into:", default: path.join(cwd, repoName) },
190
186
  ]);
191
187
  const resolved = path.resolve(targetDir.trim());
@@ -226,7 +222,7 @@ export async function runInitWizard() {
226
222
  }
227
223
  }
228
224
  if (action === "path") {
229
- const { dir } = await inquirer.prompt([
225
+ const { dir } = await (await getInquirer()).prompt([
230
226
  {
231
227
  type: "input", name: "dir", message: "Path to foundation-compose directory:",
232
228
  validate: (value) => {
@@ -241,7 +237,7 @@ export async function runInitWizard() {
241
237
  }
242
238
  }
243
239
  }
244
- const { submodules, env, download } = await inquirer.prompt([
240
+ const { submodules, env, download } = await (await getInquirer()).prompt([
245
241
  { type: "confirm", name: "submodules", message: "Initialize and update git submodules?", default: true },
246
242
  { type: "confirm", name: "env", message: "Create .env from .env.example (if missing)?", default: true },
247
243
  { type: "confirm", name: "download", message: "Download container images now (make download)?", default: false },
@@ -280,7 +276,7 @@ export async function runInitWizard() {
280
276
  };
281
277
  });
282
278
 
283
- const { enabledPlugins } = await inquirer.prompt([{
279
+ const { enabledPlugins } = await (await getInquirer()).prompt([{
284
280
  type: "checkbox",
285
281
  name: "enabledPlugins",
286
282
  message: "Plugins:",
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
+ }