@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.
- package/package.json +2 -1
- package/src/agent/agent.js +139 -38
- package/src/agent/agents.js +224 -0
- package/src/agent/context.js +146 -12
- package/src/agent/index.js +1 -0
- package/src/agent/llm.js +84 -13
- package/src/auth/coda.js +10 -10
- package/src/auth/login.js +13 -13
- package/src/auth/oauth.js +4 -4
- package/src/commands/index.js +121 -15
- package/src/config.js +2 -2
- package/src/doctor.js +215 -26
- package/src/feature-flags.js +197 -0
- package/src/plugins/api.js +14 -0
- package/src/plugins/builtins/stack-api.js +36 -0
- package/src/plugins/loader.js +67 -0
- package/src/plugins/registry.js +2 -0
- package/src/project.js +20 -1
- package/src/setup/aws.js +58 -45
- package/src/setup/setup.js +10 -9
- package/src/setup/wizard.js +195 -15
- package/src/ui/confirm.js +3 -2
- package/src/ui/input.js +2 -2
- package/src/ui/spinner.js +4 -4
- package/src/ui/streaming.js +2 -2
package/src/setup/wizard.js
CHANGED
|
@@ -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.
|
|
72
|
+
console.log(chalk.dim(`Using FOUNDATION_ROOT: ${projectRoot}\n`));
|
|
17
73
|
} else if (isFoundationRoot(cwd)) {
|
|
18
74
|
projectRoot = cwd;
|
|
19
|
-
console.log(chalk.
|
|
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
|
-
|
|
32
|
-
|
|
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
|
-
|
|
35
|
-
|
|
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
|
-
|
|
38
|
-
|
|
39
|
-
console.log(
|
|
40
|
-
|
|
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("
|
|
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
|
|
214
|
+
console.log(chalk.yellow(`\n ⚠ Some submodules had issues. Attempting to recover...\n`));
|
|
88
215
|
try {
|
|
89
|
-
await execa("git", ["submodule", "
|
|
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.
|
|
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 ? "
|
|
54
|
-
|
|
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, {
|
|
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, {
|
|
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.
|
|
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, {
|
|
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, {
|
|
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, {
|
|
137
|
+
h(Text, { dimColor: true }, contentPreview)
|
|
138
138
|
)
|
|
139
139
|
);
|
|
140
140
|
}
|
package/src/ui/streaming.js
CHANGED
|
@@ -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: "
|
|
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: "
|
|
67
|
+
borderColor: "white",
|
|
68
68
|
paddingX: 1,
|
|
69
69
|
marginTop: 1,
|
|
70
70
|
},
|