@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.
- package/package.json +2 -1
- package/src/commands/index.js +163 -1
- package/src/doctor.js +155 -17
- package/src/plugins/bundled/coda/auth.js +79 -0
- package/src/plugins/bundled/coda/client.js +187 -0
- package/src/plugins/bundled/coda/fops.plugin.json +7 -0
- package/src/plugins/bundled/coda/index.js +284 -0
- package/src/plugins/bundled/coda/package.json +3 -0
- package/src/plugins/bundled/coda/skills/coda/SKILL.md +82 -0
- package/src/plugins/bundled/cursor/fops.plugin.json +7 -0
- package/src/plugins/bundled/cursor/index.js +432 -0
- package/src/plugins/bundled/cursor/package.json +1 -0
- package/src/plugins/bundled/cursor/skills/cursor/SKILL.md +48 -0
- package/src/plugins/bundled/fops-plugin-1password/fops.plugin.json +7 -0
- package/src/plugins/bundled/fops-plugin-1password/index.js +239 -0
- package/src/plugins/bundled/fops-plugin-1password/lib/env.js +100 -0
- package/src/plugins/bundled/fops-plugin-1password/lib/op.js +111 -0
- package/src/plugins/bundled/fops-plugin-1password/lib/setup.js +235 -0
- package/src/plugins/bundled/fops-plugin-1password/lib/sync.js +61 -0
- package/src/plugins/bundled/fops-plugin-1password/package.json +1 -0
- package/src/plugins/bundled/fops-plugin-1password/skills/1password/SKILL.md +79 -0
- package/src/plugins/bundled/fops-plugin-ecr/fops.plugin.json +7 -0
- package/src/plugins/bundled/fops-plugin-ecr/index.js +302 -0
- package/src/plugins/bundled/fops-plugin-ecr/lib/aws.js +147 -0
- package/src/plugins/bundled/fops-plugin-ecr/lib/images.js +73 -0
- package/src/plugins/bundled/fops-plugin-ecr/lib/setup.js +180 -0
- package/src/plugins/bundled/fops-plugin-ecr/lib/sync.js +74 -0
- package/src/plugins/bundled/fops-plugin-ecr/package.json +1 -0
- package/src/plugins/bundled/fops-plugin-ecr/skills/ecr/SKILL.md +105 -0
- package/src/plugins/bundled/fops-plugin-memory/fops.plugin.json +7 -0
- package/src/plugins/bundled/fops-plugin-memory/index.js +148 -0
- package/src/plugins/bundled/fops-plugin-memory/lib/relevance.js +72 -0
- package/src/plugins/bundled/fops-plugin-memory/lib/store.js +75 -0
- package/src/plugins/bundled/fops-plugin-memory/package.json +1 -0
- package/src/plugins/bundled/fops-plugin-memory/skills/memory/SKILL.md +58 -0
- package/src/plugins/loader.js +40 -0
- package/src/setup/aws.js +51 -38
- package/src/setup/setup.js +2 -0
- 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
|
+
```
|
package/src/plugins/loader.js
CHANGED
|
@@ -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
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
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
|
-
|
|
187
|
-
|
|
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.
|
|
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
|
/**
|
package/src/setup/setup.js
CHANGED
|
@@ -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
|
}
|
package/src/setup/wizard.js
CHANGED
|
@@ -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
|
-
|
|
34
|
-
|
|
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
|
-
|
|
37
|
-
|
|
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
|
-
|
|
40
|
-
|
|
41
|
-
console.log(
|
|
42
|
-
|
|
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
|
-
|
|
55
|
-
|
|
56
|
-
|
|
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("
|
|
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 = [
|