@meshxdata/fops 0.0.3 → 0.0.5

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.
@@ -5,7 +5,63 @@ import chalk from "chalk";
5
5
  import { execa } from "execa";
6
6
  import inquirer from "inquirer";
7
7
  import { isFoundationRoot, findComposeRootUp } from "../project.js";
8
+ import { discoverPlugins } from "../plugins/discovery.js";
9
+ import { validateManifest } from "../plugins/manifest.js";
8
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
+ }
9
65
 
10
66
  export async function runInitWizard() {
11
67
  const cwd = process.cwd();
@@ -13,10 +69,10 @@ export async function runInitWizard() {
13
69
  let projectRoot = null;
14
70
  if (envRoot && fs.existsSync(envRoot) && isFoundationRoot(envRoot)) {
15
71
  projectRoot = path.resolve(envRoot);
16
- console.log(chalk.gray(`Using FOUNDATION_ROOT: ${projectRoot}\n`));
72
+ console.log(chalk.dim(`Using FOUNDATION_ROOT: ${projectRoot}\n`));
17
73
  } else if (isFoundationRoot(cwd)) {
18
74
  projectRoot = cwd;
19
- console.log(chalk.gray("Using current directory as project root.\n"));
75
+ console.log(chalk.dim("Using current directory as project root.\n"));
20
76
  } else {
21
77
  const foundUp = findComposeRootUp(cwd);
22
78
  if (foundUp && foundUp !== cwd) {
@@ -28,22 +84,93 @@ export async function runInitWizard() {
28
84
  projectRoot = foundUp;
29
85
  }
30
86
  if (!projectRoot) {
31
- let hasGit = false, hasDocker = false, hasAws = false, hasClaude = false;
32
- 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;
33
94
  try { await execa("docker", ["info"], { timeout: 5000 }); hasDocker = true; } catch {}
34
- try { await execa("aws", ["--version"]); hasAws = true; } catch {}
35
- 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
+
36
99
  console.log(chalk.cyan(" Prerequisites\n"));
37
- console.log(hasGit ? chalk.green(" ✓ Git") : chalk.red(" ✗ Git — install git first"));
38
- console.log(hasDocker ? chalk.green(" ✓ Docker") : chalk.red(" ✗ Docker — install and start Docker Desktop"));
39
- console.log(hasClaude ? chalk.green(" ✓ Claude CLI") : chalk.red(" ✗ Claude CLI — run: npm install (included as a dependency)"));
40
- 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 CLI (optional)
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
+ }
139
+
140
+ // GitHub credentials
41
141
  const netrcPath = path.join(os.homedir(), ".netrc");
42
142
  const hasNetrc = fs.existsSync(netrcPath) && fs.readFileSync(netrcPath, "utf8").includes("machine github.com");
43
143
  console.log(hasNetrc ? chalk.green(" ✓ GitHub credentials (~/.netrc)") : chalk.yellow(" ⚠ GitHub credentials — add to ~/.netrc (needed for private submodules)"));
144
+
145
+ // Cursor IDE (only when cursor plugin is installed)
146
+ const cursorPluginDir = path.join(os.homedir(), ".fops", "plugins", "cursor");
147
+ if (fs.existsSync(cursorPluginDir)) {
148
+ let cursorVer = null;
149
+ try {
150
+ const { stdout } = await execa("cursor", ["--version"]);
151
+ cursorVer = (stdout || "").split("\n")[0].trim();
152
+ } catch {}
153
+
154
+ if (cursorVer) {
155
+ console.log(chalk.green(" ✓ Cursor IDE") + chalk.dim(` — ${cursorVer}`));
156
+ } else {
157
+ // Check if Cursor app is installed even without CLI command
158
+ const appInstalled = process.platform === "darwin"
159
+ ? fs.existsSync("/Applications/Cursor.app")
160
+ : process.platform === "win32"
161
+ ? fs.existsSync(path.join(os.homedir(), "AppData", "Local", "Programs", "Cursor", "Cursor.exe"))
162
+ : false;
163
+ if (appInstalled) {
164
+ console.log(chalk.green(" ✓ Cursor IDE") + chalk.dim(" — app installed (CLI not in PATH — Cmd+Shift+P → 'Install cursor command' to enable)"));
165
+ } else {
166
+ console.log(chalk.yellow(" ⚠ Cursor IDE — install from cursor.com, then: Cmd+Shift+P → 'Install cursor command'"));
167
+ }
168
+ }
169
+ }
170
+
44
171
  console.log("");
45
172
  if (!hasGit || !hasDocker || !hasClaude) {
46
- console.log(chalk.red("Fix the missing prerequisites above, then run fops init again.\n"));
173
+ console.log(chalk.red(" Required tools are still missing. Install them and run fops init again.\n"));
47
174
  process.exit(1);
48
175
  }
49
176
  const choices = [
@@ -80,18 +207,19 @@ export async function runInitWizard() {
80
207
  }
81
208
  console.log(chalk.blue(`\nInitializing submodules (checking out ${CLONE_BRANCH})...\n`));
82
209
  try {
83
- await execa("git", ["submodule", "update", "--init", "--remote", "--recursive"], { cwd: resolved, stdio: "inherit" });
210
+ await execa("git", ["submodule", "update", "--init", "--force", "--remote", "--recursive"], { cwd: resolved, stdio: "inherit" });
84
211
  await execa("git", ["submodule", "foreach", `git fetch origin && git checkout origin/${CLONE_BRANCH} 2>/dev/null || git checkout origin/main`], { cwd: resolved, stdio: "inherit" });
85
212
  console.log(chalk.green(`\n Cloned successfully — submodules on ${CLONE_BRANCH} (falling back to main).\n`));
86
213
  } catch {
87
- console.log(chalk.yellow(`\n ⚠ Some submodules had issues. Attempting to check out ${CLONE_BRANCH} individually...\n`));
214
+ console.log(chalk.yellow(`\n ⚠ Some submodules had issues. Attempting to recover...\n`));
88
215
  try {
89
- await execa("git", ["submodule", "init"], { cwd: resolved, stdio: "inherit" });
216
+ await execa("git", ["submodule", "absorbgitdirs"], { cwd: resolved, stdio: "inherit" });
217
+ await execa("git", ["submodule", "update", "--init", "--force", "--recursive"], { cwd: resolved, stdio: "inherit" });
90
218
  await execa("git", ["submodule", "foreach", `git fetch origin && git checkout origin/${CLONE_BRANCH} 2>/dev/null || git checkout origin/main`], { cwd: resolved, stdio: "inherit" });
91
219
  console.log(chalk.green(" Submodules recovered.\n"));
92
220
  } catch {
93
221
  console.log(chalk.yellow(" Some submodules still failed. Fix manually with:"));
94
- console.log(chalk.gray(` cd ${resolved} && git submodule foreach 'git checkout ${CLONE_BRANCH} || git checkout main && git pull'\n`));
222
+ console.log(chalk.dim(` cd ${resolved} && git submodule foreach 'git checkout ${CLONE_BRANCH} || git checkout main && git pull'\n`));
95
223
  }
96
224
  }
97
225
  projectRoot = resolved;
@@ -118,6 +246,58 @@ export async function runInitWizard() {
118
246
  { type: "confirm", name: "env", message: "Create .env from .env.example (if missing)?", default: true },
119
247
  { type: "confirm", name: "download", message: "Download container images now (make download)?", default: false },
120
248
  ]);
249
+
250
+ // ── Plugin selection ───────────────────────────────
251
+ const candidates = discoverPlugins();
252
+ const plugins = candidates
253
+ .map((c) => {
254
+ const manifest = validateManifest(c.path);
255
+ if (!manifest) return null;
256
+ return { id: manifest.id, name: manifest.name, description: manifest.description || "", path: c.path };
257
+ })
258
+ .filter(Boolean);
259
+
260
+ if (plugins.length > 0) {
261
+ console.log(chalk.cyan("\n Plugins\n"));
262
+ console.log(chalk.dim(" Select which plugins to enable:\n"));
263
+
264
+ // Read existing config to preserve current enabled state
265
+ const fopsConfigPath = path.join(os.homedir(), ".fops.json");
266
+ let fopsConfig = {};
267
+ try {
268
+ if (fs.existsSync(fopsConfigPath)) {
269
+ fopsConfig = JSON.parse(fs.readFileSync(fopsConfigPath, "utf8"));
270
+ }
271
+ } catch {}
272
+
273
+ const currentEntries = fopsConfig?.plugins?.entries || {};
274
+ const choices = plugins.map((p) => {
275
+ const isEnabled = currentEntries[p.id]?.enabled !== false;
276
+ return {
277
+ name: `${p.name}${p.description ? chalk.dim(` — ${p.description}`) : ""}`,
278
+ value: p.id,
279
+ checked: isEnabled,
280
+ };
281
+ });
282
+
283
+ const { enabledPlugins } = await inquirer.prompt([{
284
+ type: "checkbox",
285
+ name: "enabledPlugins",
286
+ message: "Plugins:",
287
+ choices,
288
+ }]);
289
+
290
+ // Save enabled/disabled state
291
+ if (!fopsConfig.plugins) fopsConfig.plugins = {};
292
+ if (!fopsConfig.plugins.entries) fopsConfig.plugins.entries = {};
293
+ for (const p of plugins) {
294
+ if (!fopsConfig.plugins.entries[p.id]) fopsConfig.plugins.entries[p.id] = {};
295
+ fopsConfig.plugins.entries[p.id].enabled = enabledPlugins.includes(p.id);
296
+ }
297
+ fs.writeFileSync(fopsConfigPath, JSON.stringify(fopsConfig, null, 2) + "\n");
298
+ console.log(chalk.green(` ✓ ${enabledPlugins.length}/${plugins.length} plugin(s) enabled`));
299
+ }
300
+
121
301
  console.log("");
122
302
  await runSetup(projectRoot, { submodules, env, download, netrcCheck: true });
123
303
  }
package/src/ui/confirm.js CHANGED
@@ -50,8 +50,9 @@ export function SelectPrompt({ message, options, onResult }) {
50
50
  ),
51
51
  ...options.map((opt, i) =>
52
52
  h(Box, { key: i },
53
- h(Text, { color: i === cursor ? "cyan" : "gray" },
54
- ` ${i === cursor ? "" : " "} ${opt.label}`
53
+ h(Text, { color: "cyan" }, i === cursor ? "" : " "),
54
+ h(Text, { color: i === cursor ? "white" : undefined, dimColor: i !== cursor },
55
+ opt.label
55
56
  )
56
57
  )
57
58
  )
package/src/ui/input.js CHANGED
@@ -84,7 +84,7 @@ export function InputBox({ onSubmit, onExit, history = [], statusText }) {
84
84
  h(Text, { color: "cyan" }, cursor)
85
85
  ),
86
86
  h(Separator),
87
- statusText && h(Text, { dimColor: true }, " " + statusText)
87
+ statusText && h(Text, { color: "#888888" }, " " + statusText)
88
88
  );
89
89
  }
90
90
 
@@ -161,7 +161,7 @@ export function StandaloneInput({ onResult, statusText }) {
161
161
  h(Text, { color: "cyan" }, cursor)
162
162
  ),
163
163
  h(Separator),
164
- statusText && h(Text, { dimColor: true }, " " + statusText)
164
+ statusText && h(Text, { color: "#888888" }, " " + statusText)
165
165
  );
166
166
  }
167
167
 
package/src/ui/spinner.js CHANGED
@@ -6,7 +6,7 @@ const h = React.createElement;
6
6
 
7
7
  const SPARKLE_FRAMES = ["✻", "✼", "✻", "✦"];
8
8
  const SPARKLE_INTERVAL = 120;
9
- const INTENT_LINE = chalk.cyan("⏺") + chalk.gray(" Thinking...");
9
+ const INTENT_LINE = chalk.cyan("⏺") + chalk.dim(" Thinking...");
10
10
 
11
11
  // Claude-style verbs for the spinner
12
12
  export const VERBS = [
@@ -55,11 +55,11 @@ export function ThinkingSpinner({ message }) {
55
55
  return h(Box, { flexDirection: "column" },
56
56
  h(Box, null,
57
57
  h(Text, { color: "cyan" }, "⏺"),
58
- h(Text, { color: "gray" }, " Thinking...")
58
+ h(Text, { dimColor: true }, " Thinking...")
59
59
  ),
60
60
  h(Box, null,
61
61
  h(Text, { color: "magenta" }, SPARKLE_FRAMES[frame]),
62
- h(Text, { color: "gray" }, ` ${message || `${verb}…`} `),
62
+ h(Text, { dimColor: true }, ` ${message || `${verb}…`} `),
63
63
  h(Text, { dimColor: true }, "(esc to interrupt)")
64
64
  )
65
65
  );
@@ -134,7 +134,7 @@ function ThinkingDisplay({ status, detail, thinking, content }) {
134
134
  ),
135
135
  // Response content preview
136
136
  contentPreview && h(Box, { marginLeft: 2 },
137
- h(Text, { color: "gray" }, contentPreview)
137
+ h(Text, { dimColor: true }, contentPreview)
138
138
  )
139
139
  );
140
140
  }
@@ -11,7 +11,7 @@ export function ResponseBox({ content, title = "Claude" }) {
11
11
  return h(Box, {
12
12
  flexDirection: "column",
13
13
  borderStyle: "round",
14
- borderColor: "gray",
14
+ borderColor: "white",
15
15
  paddingX: 1,
16
16
  marginY: 1,
17
17
  },
@@ -64,7 +64,7 @@ export function StreamingResponse({ title = "Claude" }) {
64
64
  return h(Box, {
65
65
  flexDirection: "column",
66
66
  borderStyle: "round",
67
- borderColor: "gray",
67
+ borderColor: "white",
68
68
  paddingX: 1,
69
69
  marginTop: 1,
70
70
  },