@meshxdata/fops 0.0.4 → 0.0.6

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 (39) hide show
  1. package/package.json +2 -1
  2. package/src/commands/index.js +163 -1
  3. package/src/doctor.js +155 -17
  4. package/src/plugins/bundled/coda/auth.js +79 -0
  5. package/src/plugins/bundled/coda/client.js +187 -0
  6. package/src/plugins/bundled/coda/fops.plugin.json +7 -0
  7. package/src/plugins/bundled/coda/index.js +284 -0
  8. package/src/plugins/bundled/coda/package.json +3 -0
  9. package/src/plugins/bundled/coda/skills/coda/SKILL.md +82 -0
  10. package/src/plugins/bundled/cursor/fops.plugin.json +7 -0
  11. package/src/plugins/bundled/cursor/index.js +432 -0
  12. package/src/plugins/bundled/cursor/package.json +1 -0
  13. package/src/plugins/bundled/cursor/skills/cursor/SKILL.md +48 -0
  14. package/src/plugins/bundled/fops-plugin-1password/fops.plugin.json +7 -0
  15. package/src/plugins/bundled/fops-plugin-1password/index.js +239 -0
  16. package/src/plugins/bundled/fops-plugin-1password/lib/env.js +100 -0
  17. package/src/plugins/bundled/fops-plugin-1password/lib/op.js +111 -0
  18. package/src/plugins/bundled/fops-plugin-1password/lib/setup.js +235 -0
  19. package/src/plugins/bundled/fops-plugin-1password/lib/sync.js +61 -0
  20. package/src/plugins/bundled/fops-plugin-1password/package.json +1 -0
  21. package/src/plugins/bundled/fops-plugin-1password/skills/1password/SKILL.md +79 -0
  22. package/src/plugins/bundled/fops-plugin-ecr/fops.plugin.json +7 -0
  23. package/src/plugins/bundled/fops-plugin-ecr/index.js +302 -0
  24. package/src/plugins/bundled/fops-plugin-ecr/lib/aws.js +147 -0
  25. package/src/plugins/bundled/fops-plugin-ecr/lib/images.js +73 -0
  26. package/src/plugins/bundled/fops-plugin-ecr/lib/setup.js +180 -0
  27. package/src/plugins/bundled/fops-plugin-ecr/lib/sync.js +74 -0
  28. package/src/plugins/bundled/fops-plugin-ecr/package.json +1 -0
  29. package/src/plugins/bundled/fops-plugin-ecr/skills/ecr/SKILL.md +105 -0
  30. package/src/plugins/bundled/fops-plugin-memory/fops.plugin.json +7 -0
  31. package/src/plugins/bundled/fops-plugin-memory/index.js +148 -0
  32. package/src/plugins/bundled/fops-plugin-memory/lib/relevance.js +72 -0
  33. package/src/plugins/bundled/fops-plugin-memory/lib/store.js +75 -0
  34. package/src/plugins/bundled/fops-plugin-memory/package.json +1 -0
  35. package/src/plugins/bundled/fops-plugin-memory/skills/memory/SKILL.md +58 -0
  36. package/src/plugins/loader.js +40 -0
  37. package/src/setup/aws.js +51 -38
  38. package/src/setup/setup.js +2 -0
  39. package/src/setup/wizard.js +137 -12
@@ -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
+ ```
@@ -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.)
@@ -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);
package/src/setup/aws.js CHANGED
@@ -1,7 +1,6 @@
1
1
  import fs from "node:fs";
2
2
  import os from "node:os";
3
3
  import path from "node:path";
4
- import readline from "node:readline";
5
4
  import chalk from "chalk";
6
5
  import { execa } from "execa";
7
6
  import inquirer from "inquirer";
@@ -138,20 +137,6 @@ export function detectAwsSsoProfiles() {
138
137
  return profiles;
139
138
  }
140
139
 
141
- /**
142
- * Simple readline prompt helper.
143
- */
144
- function ask(question, defaultVal) {
145
- const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
146
- const suffix = defaultVal ? chalk.dim(` (${defaultVal})`) : "";
147
- return new Promise((resolve) => {
148
- rl.question(` ${question}${suffix}: `, (answer) => {
149
- rl.close();
150
- resolve(answer.trim() || defaultVal || "");
151
- });
152
- });
153
- }
154
-
155
140
  /**
156
141
  * Check if ~/.aws/config has an sso-session block with sso_start_url.
157
142
  * If not, prompt user for the values and write them.
@@ -166,38 +151,35 @@ export async function ensureSsoConfig() {
166
151
  console.log(chalk.cyan("\n AWS SSO is not configured. Let's set it up.\n"));
167
152
  console.log(chalk.dim(" You can find these values in your AWS SSO portal.\n"));
168
153
 
169
- const sessionName = await ask("SSO session name", "meshx");
170
- const startUrl = await ask("SSO start URL");
171
- const ssoRegion = await ask("SSO region", "us-east-1");
172
- const accountId = await ask("AWS account ID");
173
- const roleName = await ask("SSO role name", "AdministratorAccess");
174
- const region = await ask("Default region", ssoRegion);
175
- const profileName = await ask("Profile name", "dev");
176
-
177
- if (!startUrl || !accountId) {
178
- throw new Error("SSO start URL and account ID are required");
179
- }
154
+ const answers = await inquirer.prompt([
155
+ { type: "input", name: "sessionName", message: "SSO session name:", default: "me-central-1" },
156
+ { type: "input", name: "startUrl", message: "SSO start URL:", validate: (v) => v?.trim() ? true : "Required." },
157
+ { type: "input", name: "ssoRegion", message: "SSO region:", default: "us-east-1" },
158
+ { type: "input", name: "accountId", message: "AWS account ID:", validate: (v) => /^\d{12}$/.test(v?.trim()) ? true : "Must be 12 digits." },
159
+ { type: "input", name: "roleName", message: "SSO role name:", default: "AdministratorAccess" },
160
+ { type: "input", name: "profileName", message: "Profile name:", default: "dev" },
161
+ { type: "input", name: "region", message: "Default region:", default: (a) => a.ssoRegion },
162
+ ]);
180
163
 
181
164
  // Ensure ~/.aws directory exists
182
165
  const awsDir = path.join(os.homedir(), ".aws");
183
166
  if (!fs.existsSync(awsDir)) fs.mkdirSync(awsDir, { mode: 0o700 });
184
167
 
185
- const block = `
186
- [sso-session ${sessionName}]
187
- sso_start_url = ${startUrl}
188
- sso_region = ${ssoRegion}
168
+ const block = `[sso-session ${answers.sessionName.trim()}]
169
+ sso_start_url = ${answers.startUrl.trim()}
170
+ sso_region = ${answers.ssoRegion.trim()}
189
171
  sso_registration_scopes = sso:account:access
190
172
 
191
- [profile ${profileName}]
192
- sso_session = ${sessionName}
193
- sso_account_id = ${accountId}
194
- sso_role_name = ${roleName}
195
- region = ${region}
173
+ [profile ${answers.profileName.trim()}]
174
+ sso_session = ${answers.sessionName.trim()}
175
+ sso_account_id = ${answers.accountId.trim()}
176
+ sso_role_name = ${answers.roleName.trim()}
177
+ region = ${answers.region.trim()}
196
178
  output = json
197
179
  `;
198
180
 
199
- fs.appendFileSync(configPath, block);
200
- console.log(chalk.green(`\n ✓ Written to ~/.aws/config (profile: ${profileName})`));
181
+ fs.writeFileSync(configPath, block);
182
+ console.log(chalk.green(`\n ✓ Written to ~/.aws/config (profile: ${answers.profileName.trim()})`));
201
183
  }
202
184
 
203
185
  /**
@@ -220,13 +202,44 @@ export async function fixAwsSso() {
220
202
  let ttyFd;
221
203
  try { ttyFd = fs.openSync("/dev/tty", "r"); } catch { ttyFd = null; }
222
204
 
223
- await execa("aws", ["sso", "login", "--profile", profile.name], {
205
+ const { exitCode } = await execa("aws", ["sso", "login", "--profile", profile.name], {
224
206
  stdio: [ttyFd ?? "inherit", "inherit", "inherit"],
225
207
  reject: false,
226
208
  timeout: 120_000,
227
209
  });
228
210
 
229
211
  if (ttyFd !== null) fs.closeSync(ttyFd);
212
+
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();
219
+
220
+ const retryProfiles = detectAwsSsoProfiles();
221
+ if (retryProfiles.length === 0) {
222
+ throw new Error("No SSO profiles found after config — check ~/.aws/config");
223
+ }
224
+
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; }
230
+
231
+ const { exitCode: retryCode } = await execa("aws", ["sso", "login", "--profile", retryProfile.name], {
232
+ stdio: [retryTtyFd ?? "inherit", "inherit", "inherit"],
233
+ reject: false,
234
+ timeout: 120_000,
235
+ });
236
+
237
+ if (retryTtyFd !== null) fs.closeSync(retryTtyFd);
238
+
239
+ if (retryCode !== 0) {
240
+ throw new Error("SSO login failed. Check your SSO start URL and region in ~/.aws/config");
241
+ }
242
+ }
230
243
  }
231
244
 
232
245
  /**
@@ -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
  }
@@ -8,6 +8,60 @@ import { isFoundationRoot, findComposeRootUp } from "../project.js";
8
8
  import { discoverPlugins } from "../plugins/discovery.js";
9
9
  import { validateManifest } from "../plugins/manifest.js";
10
10
  import { runSetup, CLONE_BRANCH } from "./setup.js";
11
+ import { confirm } from "../ui/index.js";
12
+
13
+ /**
14
+ * Ensure Homebrew is available (macOS). Installs if missing.
15
+ */
16
+ async function ensureBrew() {
17
+ try { await execa("brew", ["--version"]); return true; } catch {}
18
+ console.log(chalk.cyan(" ▶ Installing Homebrew…"));
19
+ try {
20
+ await execa("bash", ["-c", 'NONINTERACTIVE=1 /bin/bash -c "$(curl -fsSL https://raw.githubusercontent.com/Homebrew/install/HEAD/install.sh)"'], {
21
+ stdio: "inherit", timeout: 300_000,
22
+ });
23
+ const brewPaths = ["/opt/homebrew/bin/brew", "/usr/local/bin/brew"];
24
+ for (const bp of brewPaths) {
25
+ if (fs.existsSync(bp)) { process.env.PATH = path.dirname(bp) + ":" + process.env.PATH; break; }
26
+ }
27
+ return true;
28
+ } catch { return false; }
29
+ }
30
+
31
+ /**
32
+ * Try to install a missing tool. Returns true if installed successfully.
33
+ */
34
+ async function installTool(name, { brew, brewCask, winget, apt, npm: npmPkg } = {}) {
35
+ const shouldInstall = await confirm(` Install ${name}?`, true);
36
+ if (!shouldInstall) return false;
37
+ try {
38
+ if (process.platform === "darwin" && (brew || brewCask)) {
39
+ if (!(await ensureBrew())) { console.log(chalk.red(" Homebrew required")); return false; }
40
+ const cmd = brewCask ? ["install", "--cask", brewCask] : ["install", brew];
41
+ console.log(chalk.cyan(` ▶ brew ${cmd.join(" ")}`));
42
+ await execa("brew", cmd, { stdio: "inherit", timeout: 300_000 });
43
+ return true;
44
+ }
45
+ if (process.platform === "win32" && winget) {
46
+ console.log(chalk.cyan(` ▶ winget install ${winget}`));
47
+ await execa("winget", ["install", winget, "--accept-source-agreements", "--accept-package-agreements"], { stdio: "inherit", timeout: 300_000 });
48
+ return true;
49
+ }
50
+ if (process.platform === "linux" && apt) {
51
+ console.log(chalk.cyan(` ▶ sudo apt-get install -y ${apt}`));
52
+ await execa("sudo", ["apt-get", "install", "-y", apt], { stdio: "inherit", timeout: 300_000 });
53
+ return true;
54
+ }
55
+ if (npmPkg) {
56
+ console.log(chalk.cyan(` ▶ npm install`));
57
+ await execa("npm", ["install"], { stdio: "inherit", timeout: 300_000 });
58
+ return true;
59
+ }
60
+ } catch (err) {
61
+ console.log(chalk.red(` Failed to install ${name}: ${err.message}`));
62
+ }
63
+ return false;
64
+ }
11
65
 
12
66
  export async function runInitWizard() {
13
67
  const cwd = process.cwd();
@@ -30,19 +84,76 @@ export async function runInitWizard() {
30
84
  projectRoot = foundUp;
31
85
  }
32
86
  if (!projectRoot) {
33
- let hasGit = false, hasDocker = false, hasAws = false, hasClaude = false;
34
- try { await execa("git", ["--version"]); hasGit = true; } catch {}
87
+ // ── Check prerequisites and offer to install missing ones ──
88
+ const getVer = async (cmd, args = ["--version"]) => {
89
+ try { const { stdout } = await execa(cmd, args, { reject: false, timeout: 5000 }); return stdout?.split("\n")[0]?.trim() || null; } catch { return null; }
90
+ };
91
+
92
+ let hasGit = !!(await getVer("git"));
93
+ let hasDocker = false;
35
94
  try { await execa("docker", ["info"], { timeout: 5000 }); hasDocker = true; } catch {}
36
- try { await execa("aws", ["--version"]); hasAws = true; } catch {}
37
- try { await execa("claude", ["--version"]); hasClaude = true; } catch {}
95
+ let hasAws = !!(await getVer("aws"));
96
+ let hasClaude = !!(await getVer("claude"));
97
+ let hasOp = !!(await getVer("op"));
98
+
38
99
  console.log(chalk.cyan(" Prerequisites\n"));
39
- console.log(hasGit ? chalk.green(" ✓ Git") : chalk.red(" ✗ Git — install git first"));
40
- console.log(hasDocker ? chalk.green(" ✓ Docker") : chalk.red(" ✗ Docker — install and start Docker Desktop"));
41
- console.log(hasClaude ? chalk.green(" ✓ Claude CLI") : chalk.red(" ✗ Claude CLI — run: npm install (included as a dependency)"));
42
- console.log(hasAws ? chalk.green(" ✓ AWS CLI") : chalk.yellow(" ⚠ AWS CLI — install for ECR image pulls (brew install awscli)"));
100
+
101
+ // Git
102
+ if (hasGit) console.log(chalk.green(" ✓ Git"));
103
+ else {
104
+ console.log(chalk.red(" ✗ Git"));
105
+ hasGit = await installTool("Git", { brew: "git", winget: "Git.Git", apt: "git" });
106
+ }
107
+
108
+ // Docker
109
+ if (hasDocker) console.log(chalk.green(" ✓ Docker"));
110
+ else {
111
+ console.log(chalk.red(" ✗ Docker"));
112
+ hasDocker = await installTool("Docker Desktop", { brewCask: "docker", winget: "Docker.DockerDesktop" });
113
+ if (hasDocker && process.platform === "darwin") {
114
+ console.log(chalk.cyan(" ▶ open -a Docker"));
115
+ await execa("open", ["-a", "Docker"], { timeout: 10000 }).catch(() => {});
116
+ }
117
+ }
118
+
119
+ // Claude CLI
120
+ if (hasClaude) console.log(chalk.green(" ✓ Claude CLI"));
121
+ else {
122
+ console.log(chalk.red(" ✗ Claude CLI"));
123
+ hasClaude = await installTool("Claude CLI", { npm: true });
124
+ }
125
+
126
+ // AWS CLI (optional)
127
+ if (hasAws) console.log(chalk.green(" ✓ AWS CLI"));
128
+ else {
129
+ console.log(chalk.yellow(" ⚠ AWS CLI") + chalk.dim(" — needed for ECR image pulls"));
130
+ hasAws = await installTool("AWS CLI", { brew: "awscli", winget: "Amazon.AWSCLI" });
131
+ }
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)
146
+ 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
+ }
151
+
152
+ // GitHub credentials
43
153
  const netrcPath = path.join(os.homedir(), ".netrc");
44
154
  const hasNetrc = fs.existsSync(netrcPath) && fs.readFileSync(netrcPath, "utf8").includes("machine github.com");
45
155
  console.log(hasNetrc ? chalk.green(" ✓ GitHub credentials (~/.netrc)") : chalk.yellow(" ⚠ GitHub credentials — add to ~/.netrc (needed for private submodules)"));
156
+
46
157
  // Cursor IDE (only when cursor plugin is installed)
47
158
  const cursorPluginDir = path.join(os.homedir(), ".fops", "plugins", "cursor");
48
159
  if (fs.existsSync(cursorPluginDir)) {
@@ -51,13 +162,27 @@ export async function runInitWizard() {
51
162
  const { stdout } = await execa("cursor", ["--version"]);
52
163
  cursorVer = (stdout || "").split("\n")[0].trim();
53
164
  } catch {}
54
- console.log(cursorVer
55
- ? chalk.green(" ✓ Cursor IDE") + chalk.dim(` — ${cursorVer}`)
56
- : chalk.yellow(" Cursor IDE install from cursor.com, then: Cmd+Shift+P → 'Install cursor command'"));
165
+
166
+ if (cursorVer) {
167
+ console.log(chalk.green(" Cursor IDE") + chalk.dim(` ${cursorVer}`));
168
+ } else {
169
+ // Check if Cursor app is installed even without CLI command
170
+ const appInstalled = process.platform === "darwin"
171
+ ? fs.existsSync("/Applications/Cursor.app")
172
+ : process.platform === "win32"
173
+ ? fs.existsSync(path.join(os.homedir(), "AppData", "Local", "Programs", "Cursor", "Cursor.exe"))
174
+ : false;
175
+ if (appInstalled) {
176
+ console.log(chalk.green(" ✓ Cursor IDE") + chalk.dim(" — app installed (CLI not in PATH — Cmd+Shift+P → 'Install cursor command' to enable)"));
177
+ } else {
178
+ console.log(chalk.yellow(" ⚠ Cursor IDE — install from cursor.com, then: Cmd+Shift+P → 'Install cursor command'"));
179
+ }
180
+ }
57
181
  }
182
+
58
183
  console.log("");
59
184
  if (!hasGit || !hasDocker || !hasClaude) {
60
- console.log(chalk.red("Fix the missing prerequisites above, then run fops init again.\n"));
185
+ console.log(chalk.red(" Required tools are still missing. Install them and run fops init again.\n"));
61
186
  process.exit(1);
62
187
  }
63
188
  const choices = [