@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.
- package/package.json +3 -2
- package/src/auth/coda.js +4 -4
- package/src/auth/login.js +9 -6
- package/src/commands/index.js +158 -0
- package/src/doctor.js +340 -71
- package/src/feature-flags.js +3 -3
- package/src/lazy.js +12 -0
- package/src/plugins/bundled/coda/auth.js +85 -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 +433 -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 +241 -0
- package/src/plugins/bundled/fops-plugin-1password/lib/env.js +100 -0
- package/src/plugins/bundled/fops-plugin-1password/lib/op.js +119 -0
- package/src/plugins/bundled/fops-plugin-1password/lib/setup.js +235 -0
- package/src/plugins/bundled/fops-plugin-1password/lib/sync.js +66 -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 +146 -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 +43 -3
- package/src/setup/aws.js +74 -46
- package/src/setup/setup.js +4 -2
- package/src/setup/wizard.js +16 -20
- 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
|
+
```
|
package/src/plugins/loader.js
CHANGED
|
@@ -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
|
|
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
|
|
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
|
-
|
|
105
|
-
|
|
106
|
-
|
|
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
|
|
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
|
-
*
|
|
189
|
+
* Run `aws sso login` for a profile, handling TTY on all platforms.
|
|
190
|
+
* Returns the execa result ({ exitCode, timedOut }).
|
|
187
191
|
*/
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
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
|
|
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
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
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
|
-
|
|
221
|
-
|
|
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
|
|
226
|
-
console.log(chalk.
|
|
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
|
|
232
|
-
stdio:
|
|
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 (
|
|
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 (
|
|
240
|
-
|
|
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
|
|
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
|
|
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
|
|
package/src/setup/setup.js
CHANGED
|
@@ -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
|
|
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
|
}
|
package/src/setup/wizard.js
CHANGED
|
@@ -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
|
|
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
|
|
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
|
|
177
|
-
{
|
|
178
|
-
{
|
|
179
|
-
{
|
|
180
|
-
];
|
|
181
|
-
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
+
}
|