@meshxdata/fops 0.0.1 → 0.0.3
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/README.md +62 -40
- package/package.json +4 -3
- package/src/agent/agent.js +26 -34
- package/src/agent/context.js +155 -98
- package/src/agent/llm.js +54 -11
- package/src/auth/coda.js +128 -0
- package/src/auth/index.js +1 -0
- package/src/commands/index.js +23 -9
- package/src/doctor.js +143 -15
- package/src/plugins/api.js +9 -0
- package/src/plugins/index.js +1 -0
- package/src/plugins/knowledge.js +124 -0
- package/src/plugins/registry.js +1 -0
- package/src/setup/setup.js +10 -5
- package/src/setup/wizard.js +15 -11
- package/src/shell.js +2 -2
- package/src/skills/foundation/SKILL.md +200 -66
- package/src/ui/input.js +31 -34
- package/src/ui/spinner.js +36 -10
- package/STRUCTURE.md +0 -43
- package/src/agent/agent.test.js +0 -233
- package/src/agent/context.test.js +0 -81
- package/src/agent/llm.test.js +0 -139
- package/src/auth/keychain.test.js +0 -185
- package/src/auth/login.test.js +0 -192
- package/src/auth/oauth.test.js +0 -118
- package/src/auth/resolve.test.js +0 -153
- package/src/config.test.js +0 -70
- package/src/doctor.test.js +0 -134
- package/src/plugins/api.test.js +0 -95
- package/src/plugins/discovery.test.js +0 -92
- package/src/plugins/hooks.test.js +0 -118
- package/src/plugins/manifest.test.js +0 -106
- package/src/plugins/registry.test.js +0 -43
- package/src/plugins/skills.test.js +0 -173
- package/src/project.test.js +0 -196
- package/src/setup/aws.test.js +0 -280
- package/src/shell.test.js +0 -72
- package/src/ui/banner.test.js +0 -97
- package/src/ui/spinner.test.js +0 -29
package/README.md
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
# Foundation CLI
|
|
1
|
+
# Foundation Operator CLI (`fops`)
|
|
2
2
|
|
|
3
3
|
Install and manage **Foundation** data mesh platforms from the terminal.
|
|
4
4
|
|
|
@@ -8,90 +8,112 @@ Install and manage **Foundation** data mesh platforms from the terminal.
|
|
|
8
8
|
|
|
9
9
|
```bash
|
|
10
10
|
# Run without installing
|
|
11
|
-
npx
|
|
11
|
+
npx @meshxdata/fops doctor
|
|
12
12
|
|
|
13
13
|
# Install globally
|
|
14
|
-
npm install -g
|
|
15
|
-
# or
|
|
16
|
-
pnpm add -g foundation
|
|
14
|
+
npm install -g @meshxdata/fops
|
|
17
15
|
```
|
|
18
16
|
|
|
19
17
|
**From repo (development)**
|
|
20
18
|
|
|
21
19
|
```bash
|
|
22
|
-
cd foundation-compose/
|
|
20
|
+
cd foundation-compose/operator-cli
|
|
23
21
|
npm install
|
|
24
|
-
npm link # optional: use `
|
|
22
|
+
npm link # optional: use `fops` from anywhere
|
|
25
23
|
```
|
|
26
24
|
|
|
27
25
|
**Shell installer (optional)**
|
|
28
26
|
|
|
29
27
|
```bash
|
|
30
|
-
curl -fsSL https://raw.githubusercontent.com/meshxdata/foundation-compose/main/
|
|
28
|
+
curl -fsSL https://raw.githubusercontent.com/meshxdata/foundation-compose/main/operator-cli/install.sh | bash
|
|
31
29
|
```
|
|
32
30
|
|
|
33
31
|
## Commands
|
|
34
32
|
|
|
35
33
|
| Command | Description |
|
|
36
34
|
|---------|-------------|
|
|
37
|
-
| `
|
|
38
|
-
| `
|
|
39
|
-
| `
|
|
40
|
-
| `
|
|
41
|
-
| `
|
|
42
|
-
| `
|
|
43
|
-
| `
|
|
44
|
-
| `
|
|
45
|
-
| `
|
|
46
|
-
| `
|
|
47
|
-
| `
|
|
48
|
-
| `
|
|
35
|
+
| `fops login` | Authenticate with Claude (default) or other services |
|
|
36
|
+
| `fops login coda` | Authenticate with Coda (guided API token setup) |
|
|
37
|
+
| `fops setup` | Full automated setup (`.env`, netrc reminder, submodules; use `--download` to pull images) |
|
|
38
|
+
| `fops init` | Initialize a Foundation project. Interactive wizard when not in a project root. |
|
|
39
|
+
| `fops up` | Start all services (`make start`); launches interactive AI assistant after startup |
|
|
40
|
+
| `fops down` | Stop services; use `--clean` to remove volumes |
|
|
41
|
+
| `fops status` | Show service status |
|
|
42
|
+
| `fops logs [service]` | Stream logs (all or one service) |
|
|
43
|
+
| `fops doctor` | Check Docker, git, `.env`, AWS/ECR, ports, services; use `--fix` to apply fixes |
|
|
44
|
+
| `fops config` | Launch interactive configuration |
|
|
45
|
+
| `fops bootstrap` | Create demo data mesh |
|
|
46
|
+
| `fops test` | Run health checks |
|
|
47
|
+
| `fops agent` | AI assistant for the stack. Interactive or single-turn with `-m "message"`. |
|
|
48
|
+
| `fops chat` | Interactive AI assistant (same as `fops agent`) |
|
|
49
|
+
| `fops skill list` | List available agent skills (built-in and plugin-provided) |
|
|
50
|
+
| `fops plugin list` | List installed plugins |
|
|
51
|
+
| `fops plugin install <source>` | Install a plugin from a local directory |
|
|
52
|
+
| `fops plugin remove <id>` | Remove an installed plugin |
|
|
49
53
|
|
|
50
54
|
### AI Assistant (agent / chat)
|
|
51
55
|
|
|
52
|
-
- **Interactive:** `
|
|
53
|
-
- **Single-turn:** `
|
|
56
|
+
- **Interactive:** `fops agent` or `fops chat` — in-CLI chat with streaming responses and command execution.
|
|
57
|
+
- **Single-turn:** `fops agent -m "why are my containers failing?"` — one question, one reply; optional "run suggested command?" prompt.
|
|
58
|
+
- **Model override:** `fops agent --model claude-sonnet-4-20250514`
|
|
54
59
|
|
|
55
60
|
**Auth** (checked in order):
|
|
56
61
|
|
|
57
|
-
1.
|
|
58
|
-
2.
|
|
59
|
-
3. `~/.claude
|
|
60
|
-
4. `~/.claude.json` → `anthropic_api_key`
|
|
62
|
+
1. Claude Code CLI OAuth (if `claude` is installed and authenticated)
|
|
63
|
+
2. `ANTHROPIC_API_KEY` environment variable
|
|
64
|
+
3. `~/.claude/settings.json` → `apiKeyHelper` (script that prints the API key)
|
|
65
|
+
4. `~/.claude/.credentials.json` or `~/.claude.json` → `anthropic_api_key` / `apiKey`
|
|
66
|
+
5. `OPENAI_API_KEY` environment variable (fallback; uses OpenAI-compatible models)
|
|
61
67
|
|
|
62
|
-
|
|
68
|
+
Run `fops login` to authenticate via OAuth browser flow, or `fops login --no-browser` to paste an API key directly.
|
|
63
69
|
|
|
64
|
-
|
|
70
|
+
**Coda** — Run `fops login coda` to set up your Coda API token. The wizard opens your browser to coda.io/account and guides you through generating and pasting the token. Token is validated via the Coda API and saved to `~/.fops.json`. You can also set `CODA_API_TOKEN` as an env var.
|
|
71
|
+
|
|
72
|
+
### Doctor
|
|
73
|
+
|
|
74
|
+
Checks prerequisites (Docker, Git, Node.js, AWS CLI), AWS/ECR access, project structure, Docker resources, port conflicts, running services, and image freshness. Plugins can register additional checks.
|
|
75
|
+
|
|
76
|
+
On `--fix`, doctor can automatically:
|
|
77
|
+
- Install Docker (macOS via Homebrew, Windows via winget, Linux via get.docker.com)
|
|
78
|
+
- Start the Docker daemon
|
|
79
|
+
- Fix AWS SSO sessions and ECR authentication
|
|
80
|
+
- Create missing `.env` from `.env.example`
|
|
81
|
+
- Run `fops init` for missing projects
|
|
82
|
+
- Rebuild stale images
|
|
83
|
+
|
|
84
|
+
### Plugins
|
|
85
|
+
|
|
86
|
+
Plugins extend the CLI with custom commands, doctor checks, lifecycle hooks, and agent skills.
|
|
87
|
+
|
|
88
|
+
- **Global plugins:** `~/.fops/plugins/<id>/` (each with `fops.plugin.json`)
|
|
89
|
+
- **NPM plugins:** packages named `fops-plugin-*` or `@scope/fops-plugin-*`
|
|
65
90
|
|
|
66
91
|
## Usage
|
|
67
92
|
|
|
68
|
-
Run from a **foundation-compose** clone (or any directory
|
|
93
|
+
Run from a **foundation-compose** clone (or any directory containing `docker-compose.yaml` and `Makefile`). You can also set `FOUNDATION_ROOT` to point to your repo root.
|
|
69
94
|
|
|
70
95
|
```bash
|
|
71
96
|
cd /path/to/foundation-compose
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
97
|
+
fops doctor
|
|
98
|
+
fops up
|
|
99
|
+
fops status
|
|
75
100
|
```
|
|
76
101
|
|
|
77
102
|
First-time setup:
|
|
78
103
|
|
|
79
104
|
```bash
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
npx foundation init
|
|
83
|
-
npx foundation up
|
|
105
|
+
fops init
|
|
106
|
+
fops up
|
|
84
107
|
```
|
|
85
108
|
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
The CLI is split into modules under `src/` for long-term maintainability. See [STRUCTURE.md](STRUCTURE.md) for layout and how to add commands.
|
|
109
|
+
The init wizard will prompt to clone the repository if no project is found.
|
|
89
110
|
|
|
90
111
|
## Requirements
|
|
91
112
|
|
|
92
113
|
- Node.js >= 18
|
|
93
114
|
- Docker (Docker Compose)
|
|
94
|
-
- Git (for
|
|
115
|
+
- Git (for submodules)
|
|
116
|
+
- Claude CLI (bundled — installed automatically via `npm install`)
|
|
95
117
|
|
|
96
118
|
## License
|
|
97
119
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@meshxdata/fops",
|
|
3
|
-
"version": "0.0.
|
|
3
|
+
"version": "0.0.3",
|
|
4
4
|
"description": "CLI to install and manage Foundation data mesh platforms",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"foundation",
|
|
@@ -16,8 +16,8 @@
|
|
|
16
16
|
"files": [
|
|
17
17
|
"foundation.mjs",
|
|
18
18
|
"src/",
|
|
19
|
-
"
|
|
20
|
-
"
|
|
19
|
+
"!src/**/*.test.js",
|
|
20
|
+
"README.md"
|
|
21
21
|
],
|
|
22
22
|
"scripts": {
|
|
23
23
|
"start": "node foundation.mjs",
|
|
@@ -25,6 +25,7 @@
|
|
|
25
25
|
"test:watch": "vitest"
|
|
26
26
|
},
|
|
27
27
|
"dependencies": {
|
|
28
|
+
"@anthropic-ai/claude-code": "^1.0.0",
|
|
28
29
|
"boxen": "^8.0.1",
|
|
29
30
|
"chalk": "^5.3.0",
|
|
30
31
|
"commander": "^12.0.0",
|
package/src/agent/agent.js
CHANGED
|
@@ -1,9 +1,10 @@
|
|
|
1
1
|
import chalk from "chalk";
|
|
2
2
|
import { execa } from "execa";
|
|
3
3
|
import { resolveAnthropicApiKey, resolveOpenAiApiKey, authHelp, offerClaudeLogin } from "../auth/index.js";
|
|
4
|
-
import {
|
|
4
|
+
import { PKG } from "../config.js";
|
|
5
|
+
import { renderBanner, promptInput, selectOption } from "../ui/index.js";
|
|
5
6
|
import { FOUNDATION_SYSTEM_PROMPT, gatherStackContext } from "./context.js";
|
|
6
|
-
import { hasClaudeCode, hasClaudeCodeAuth,
|
|
7
|
+
import { hasClaudeCode, hasClaudeCodeAuth, streamAssistantReply } from "./llm.js";
|
|
7
8
|
|
|
8
9
|
/**
|
|
9
10
|
* Render inline markdown: **bold**, *italic*, `code`
|
|
@@ -209,6 +210,7 @@ export async function askToRunCommand(root, replyText) {
|
|
|
209
210
|
|
|
210
211
|
export async function runAgentSingleTurn(root, message, opts = {}) {
|
|
211
212
|
const runSuggestions = opts.runSuggestions !== false;
|
|
213
|
+
const registry = opts.registry;
|
|
212
214
|
const useClaudeCode = hasClaudeCode() && hasClaudeCodeAuth();
|
|
213
215
|
const anthropicKey = resolveAnthropicApiKey();
|
|
214
216
|
const openaiKey = resolveOpenAiApiKey();
|
|
@@ -218,36 +220,18 @@ export async function runAgentSingleTurn(root, message, opts = {}) {
|
|
|
218
220
|
process.exit(1);
|
|
219
221
|
}
|
|
220
222
|
|
|
221
|
-
const context = await gatherStackContext(root);
|
|
223
|
+
const context = await gatherStackContext(root, { registry, message });
|
|
222
224
|
const systemContent = FOUNDATION_SYSTEM_PROMPT + "\n\n" + context;
|
|
223
|
-
let replyText = "";
|
|
224
|
-
|
|
225
|
-
const spinner = renderSpinner();
|
|
226
225
|
|
|
226
|
+
let replyText = "";
|
|
227
227
|
try {
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
const response = await client.messages.create({
|
|
235
|
-
model: opts.model || "claude-sonnet-4-20250514",
|
|
236
|
-
max_tokens: 2048, system: systemContent,
|
|
237
|
-
messages: [{ role: "user", content: message }],
|
|
238
|
-
});
|
|
239
|
-
replyText = response.content.filter((b) => b.type === "text").map((b) => b.text).join("\n");
|
|
240
|
-
} else if (openaiKey) {
|
|
241
|
-
const OpenAI = (await import("openai")).default;
|
|
242
|
-
const client = new OpenAI({ apiKey: openaiKey });
|
|
243
|
-
const response = await client.chat.completions.create({
|
|
244
|
-
model: opts.model || "gpt-4o-mini", max_tokens: 2048,
|
|
245
|
-
messages: [{ role: "system", content: systemContent }, { role: "user", content: message }],
|
|
246
|
-
});
|
|
247
|
-
replyText = response.choices?.[0]?.message?.content ?? "";
|
|
248
|
-
}
|
|
228
|
+
replyText = await streamAssistantReply(
|
|
229
|
+
root,
|
|
230
|
+
[{ role: "user", content: message }],
|
|
231
|
+
systemContent,
|
|
232
|
+
opts,
|
|
233
|
+
);
|
|
249
234
|
} catch (err) {
|
|
250
|
-
spinner.stop();
|
|
251
235
|
if (err.code === "MODULE_NOT_FOUND" || err.message?.includes("Cannot find module")) {
|
|
252
236
|
console.log(chalk.yellow("Install optional deps for agent: npm install @anthropic-ai/sdk openai"));
|
|
253
237
|
process.exit(1);
|
|
@@ -266,7 +250,6 @@ export async function runAgentSingleTurn(root, message, opts = {}) {
|
|
|
266
250
|
throw err;
|
|
267
251
|
}
|
|
268
252
|
|
|
269
|
-
spinner.stop();
|
|
270
253
|
console.log("");
|
|
271
254
|
console.log(formatResponse(replyText));
|
|
272
255
|
console.log("");
|
|
@@ -279,7 +262,8 @@ export async function runAgentSingleTurn(root, message, opts = {}) {
|
|
|
279
262
|
}
|
|
280
263
|
}
|
|
281
264
|
|
|
282
|
-
export async function runAgentInteractive(root) {
|
|
265
|
+
export async function runAgentInteractive(root, opts = {}) {
|
|
266
|
+
const registry = opts.registry;
|
|
283
267
|
const useClaudeCode = hasClaudeCode() && hasClaudeCodeAuth();
|
|
284
268
|
const anthropicKey = resolveAnthropicApiKey();
|
|
285
269
|
const openaiKey = resolveOpenAiApiKey();
|
|
@@ -301,8 +285,8 @@ export async function runAgentInteractive(root) {
|
|
|
301
285
|
}
|
|
302
286
|
}
|
|
303
287
|
|
|
304
|
-
|
|
305
|
-
const
|
|
288
|
+
// Gather base context (without message-specific RAG — that happens per-turn)
|
|
289
|
+
const baseContext = await gatherStackContext(root, { registry });
|
|
306
290
|
const messages = [];
|
|
307
291
|
|
|
308
292
|
// Show banner
|
|
@@ -313,7 +297,7 @@ export async function runAgentInteractive(root) {
|
|
|
313
297
|
|
|
314
298
|
// Main chat loop
|
|
315
299
|
while (true) {
|
|
316
|
-
const input = await promptInput();
|
|
300
|
+
const input = await promptInput(`fops v${PKG.version} \u00b7 /exit to quit \u00b7 \u2191\u2193 history`);
|
|
317
301
|
|
|
318
302
|
// Exit on null (esc/ctrl+c) or exit command
|
|
319
303
|
if (input === null || /^\/(exit|quit|q)$/i.test(input)) {
|
|
@@ -326,6 +310,15 @@ export async function runAgentInteractive(root) {
|
|
|
326
310
|
messages.push({ role: "user", content: input });
|
|
327
311
|
|
|
328
312
|
try {
|
|
313
|
+
// Re-gather knowledge per turn so RAG is query-specific
|
|
314
|
+
let ragBlock = null;
|
|
315
|
+
if (registry) {
|
|
316
|
+
const { searchKnowledge } = await import("../plugins/knowledge.js");
|
|
317
|
+
ragBlock = await searchKnowledge(registry, input);
|
|
318
|
+
}
|
|
319
|
+
const systemContent = FOUNDATION_SYSTEM_PROMPT + "\n\n" + baseContext
|
|
320
|
+
+ (ragBlock ? "\n\n" + ragBlock : "");
|
|
321
|
+
|
|
329
322
|
const replyText = await streamAssistantReply(root, messages, systemContent, {});
|
|
330
323
|
console.log("");
|
|
331
324
|
console.log(formatResponse(replyText));
|
|
@@ -342,7 +335,6 @@ export async function runAgentInteractive(root) {
|
|
|
342
335
|
|
|
343
336
|
// Auto-follow-up if any command failed
|
|
344
337
|
if (cmdResults.some((r) => !r.ok)) {
|
|
345
|
-
console.log("");
|
|
346
338
|
const followUp = await streamAssistantReply(root, messages, systemContent, {});
|
|
347
339
|
console.log("");
|
|
348
340
|
console.log(formatResponse(followUp));
|
package/src/agent/context.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import fs from "node:fs";
|
|
2
2
|
import path from "node:path";
|
|
3
3
|
import { execa } from "execa";
|
|
4
|
-
import { loadSkills } from "../plugins/index.js";
|
|
4
|
+
import { loadSkills, searchKnowledge } from "../plugins/index.js";
|
|
5
5
|
|
|
6
6
|
export const FOUNDATION_SYSTEM_PROMPT = `You are FOPS — the Foundation Operator. Think of yourself as the system admin who actually knows what they're doing. You're direct, no-BS, slightly irreverent. You don't sugarcoat problems — you diagnose and fix them. Channel the energy of someone who lives in the terminal and sees the matrix in docker logs.
|
|
7
7
|
|
|
@@ -11,44 +11,14 @@ export const FOUNDATION_SYSTEM_PROMPT = `You are FOPS — the Foundation Operato
|
|
|
11
11
|
- When something is broken, say what's broken and how to fix it. No preambles.
|
|
12
12
|
- Treat the user like a peer, not a customer.
|
|
13
13
|
|
|
14
|
-
## Capabilities
|
|
15
|
-
- **Setup & Init**: Prerequisites, environment config, first-run setup
|
|
16
|
-
- **Operations**: Start/stop services, status, logs, diagnostics
|
|
17
|
-
- **Debugging**: Troubleshoot issues, analyze logs, suggest fixes
|
|
18
|
-
- **Security**: Validate configs, check credentials safely (never log secrets)
|
|
19
|
-
|
|
20
14
|
## Commands
|
|
21
15
|
When suggesting commands, ALWAYS use \`fops\` commands, not raw \`make\` or \`git clone\`. Output each in its own fenced block.
|
|
22
16
|
|
|
23
|
-
**Always suggest 2–3 commands** so the user can pick.
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
fops doctor
|
|
29
|
-
\`\`\`
|
|
30
|
-
|
|
31
|
-
If a single action is needed, pair it with a follow-up (e.g. restart + logs, doctor + up).
|
|
32
|
-
|
|
33
|
-
## Available fops Commands
|
|
34
|
-
- fops init — clone repos, bootstrap environment
|
|
35
|
-
- fops up / fops down — start/stop the stack
|
|
36
|
-
- fops restart — restart all or specific services
|
|
37
|
-
- fops status — show running containers
|
|
38
|
-
- fops logs [service] — tail logs
|
|
39
|
-
- fops doctor — run diagnostics
|
|
40
|
-
- fops login — authenticate with AWS/ECR
|
|
41
|
-
- fops agent / fops chat — talk to me
|
|
42
|
-
|
|
43
|
-
## Services & Ports
|
|
44
|
-
Backend:9001, Frontend:3002, Storage:9002, Trino:8081, OPA:8181, Kafka:9092, Postgres:5432, Hive:9083, Vault:18201
|
|
45
|
-
|
|
46
|
-
## Setup Checklist (for new users)
|
|
47
|
-
1. Install prerequisites: git, docker, node >= 18, aws cli (optional)
|
|
48
|
-
2. \`npm install -g @meshxdata/fops\`
|
|
49
|
-
3. \`fops init\` — clones repos, sets up .env
|
|
50
|
-
4. \`fops up\` — boots the stack
|
|
51
|
-
5. \`fops doctor\` — verifies everything is healthy
|
|
17
|
+
**Always suggest 2–3 commands** so the user can pick. Pair a primary action with a follow-up (e.g. restart + logs, doctor + up).
|
|
18
|
+
|
|
19
|
+
## Accuracy Rules
|
|
20
|
+
- ALWAYS check the container status context carefully. If ANY container is exited, unhealthy, or failed, report it — never claim "all healthy" when failures exist.
|
|
21
|
+
- When containers have failed, lead with the failures and suggest diagnostics.
|
|
52
22
|
|
|
53
23
|
## Security Rules
|
|
54
24
|
- Never output API keys, passwords, or tokens in responses
|
|
@@ -60,84 +30,171 @@ export function getFoundationContextBlock(root) {
|
|
|
60
30
|
return `Project root: ${root}. Commands run in this directory.`;
|
|
61
31
|
}
|
|
62
32
|
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
33
|
+
async function gatherDockerStatus(root) {
|
|
34
|
+
try {
|
|
35
|
+
const { stdout: psOut } = await execa("docker", ["compose", "ps", "--format", "json"], { cwd: root, reject: false, timeout: 5000 });
|
|
36
|
+
if (psOut && psOut.trim()) {
|
|
37
|
+
const lines = psOut.trim().split("\n").filter(Boolean);
|
|
38
|
+
const parsed = lines.map((line) => {
|
|
39
|
+
try { return JSON.parse(line); } catch { return null; }
|
|
40
|
+
}).filter(Boolean);
|
|
41
|
+
|
|
42
|
+
if (!parsed.length) return "No containers running.";
|
|
43
|
+
|
|
44
|
+
let healthy = 0, unhealthy = 0, exited = 0, running = 0;
|
|
45
|
+
const services = parsed.map((o) => {
|
|
46
|
+
const name = o.Name || o.name || o.Service || "?";
|
|
47
|
+
const state = (o.State || "").toLowerCase();
|
|
48
|
+
const status = o.Status || "";
|
|
49
|
+
const health = (o.Health || "").toLowerCase();
|
|
50
|
+
const exitCode = o.ExitCode ?? "";
|
|
51
|
+
|
|
52
|
+
if (state === "exited" || state === "dead") {
|
|
53
|
+
exited++;
|
|
54
|
+
return `${name}: EXITED (code ${exitCode}) — ${status}`;
|
|
55
|
+
}
|
|
56
|
+
if (health === "unhealthy") {
|
|
57
|
+
unhealthy++;
|
|
58
|
+
return `${name}: UNHEALTHY — ${status}`;
|
|
59
|
+
}
|
|
60
|
+
if (state === "running" && (health === "healthy" || !health)) {
|
|
61
|
+
healthy++;
|
|
62
|
+
running++;
|
|
63
|
+
return `${name}: running ${health ? "(healthy)" : ""} — ${status}`;
|
|
64
|
+
}
|
|
65
|
+
running++;
|
|
66
|
+
return `${name}: ${state} ${health ? `(${health})` : ""} — ${status}`;
|
|
67
|
+
});
|
|
68
|
+
|
|
69
|
+
const summary = [];
|
|
70
|
+
if (running) summary.push(`${running} running`);
|
|
71
|
+
if (healthy) summary.push(`${healthy} healthy`);
|
|
72
|
+
if (unhealthy) summary.push(`${unhealthy} UNHEALTHY`);
|
|
73
|
+
if (exited) summary.push(`${exited} EXITED/FAILED`);
|
|
74
|
+
|
|
75
|
+
let header = `Container summary: ${parsed.length} total — ${summary.join(", ")}`;
|
|
76
|
+
if (unhealthy || exited) {
|
|
77
|
+
header += "\n⚠ ATTENTION: Some containers are failing. Diagnose and report the failures.";
|
|
82
78
|
}
|
|
83
|
-
|
|
84
|
-
|
|
79
|
+
|
|
80
|
+
return header + "\n\nContainer details:\n" + services.join("\n");
|
|
85
81
|
}
|
|
82
|
+
} catch {
|
|
83
|
+
return "Docker: not available or not running.";
|
|
86
84
|
}
|
|
85
|
+
return null;
|
|
86
|
+
}
|
|
87
87
|
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
}
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
}
|
|
88
|
+
async function gatherImageAges(root) {
|
|
89
|
+
try {
|
|
90
|
+
const { stdout: imgOut } = await execa("docker", ["compose", "images", "--format", "json"], { cwd: root, reject: false, timeout: 5000 });
|
|
91
|
+
if (imgOut?.trim()) {
|
|
92
|
+
const images = imgOut.trim().split("\n").filter(Boolean).map((line) => {
|
|
93
|
+
try { return JSON.parse(line); } catch { return null; }
|
|
94
|
+
}).filter(Boolean);
|
|
95
|
+
|
|
96
|
+
// Inspect all images in parallel
|
|
97
|
+
const ageResults = await Promise.all(images.map(async (img) => {
|
|
98
|
+
try {
|
|
99
|
+
const id = img.ID || img.id || "";
|
|
100
|
+
const repo = img.Repository || img.repository || "";
|
|
101
|
+
const tag = img.Tag || img.tag || "";
|
|
102
|
+
if (!id) return null;
|
|
103
|
+
const { stdout: created } = await execa("docker", ["image", "inspect", id, "--format", "{{.Created}}"], { reject: false, timeout: 3000 });
|
|
104
|
+
if (created?.trim()) {
|
|
105
|
+
const days = Math.floor((Date.now() - new Date(created.trim()).getTime()) / 86400000);
|
|
106
|
+
const name = `${repo}:${tag}`.replace(/^:|:$/g, "") || id.slice(0, 12);
|
|
107
|
+
return `${name}: ${days}d old`;
|
|
108
|
+
}
|
|
109
|
+
} catch {}
|
|
110
|
+
return null;
|
|
111
|
+
}));
|
|
113
112
|
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
113
|
+
const ages = ageResults.filter(Boolean);
|
|
114
|
+
if (ages.length) return "Image ages:\n" + ages.join("\n");
|
|
115
|
+
}
|
|
116
|
+
} catch {}
|
|
117
|
+
return null;
|
|
118
|
+
}
|
|
120
119
|
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
120
|
+
async function gatherPrereqs() {
|
|
121
|
+
const checks = await Promise.all([
|
|
122
|
+
execa("git", ["--version"], { reject: false, timeout: 3000 }).then(() => "git: ✓").catch(() => "git: ✗ (REQUIRED)"),
|
|
123
|
+
execa("docker", ["info"], { reject: false, timeout: 5000 }).then(() => "docker: ✓").catch(() => "docker: ✗ (REQUIRED)"),
|
|
124
|
+
execa("aws", ["--version"], { reject: false, timeout: 3000 }).then(() => "aws-cli: ✓").catch(() => "aws-cli: ✗ (REQUIRED)"),
|
|
125
|
+
]);
|
|
126
|
+
|
|
127
|
+
// Check git netrc for GitHub auth
|
|
128
|
+
const homedir = (await import("node:os")).default.homedir();
|
|
129
|
+
const netrcPath = (await import("node:path")).default.join(homedir, ".netrc");
|
|
130
|
+
let netrc = "git-netrc: ✗ (REQUIRED — needed for private repo access)";
|
|
131
|
+
try {
|
|
132
|
+
const netrcContent = (await import("node:fs")).default.readFileSync(netrcPath, "utf8");
|
|
133
|
+
if (netrcContent.includes("github.com")) {
|
|
134
|
+
netrc = "git-netrc: ✓ (github.com configured)";
|
|
135
|
+
} else {
|
|
136
|
+
netrc = "git-netrc: ✗ (no github.com entry — REQUIRED)";
|
|
129
137
|
}
|
|
138
|
+
} catch {}
|
|
139
|
+
|
|
140
|
+
checks.push(netrc);
|
|
141
|
+
return "Prerequisites: " + checks.join(", ");
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
function checkEnvFile(root) {
|
|
145
|
+
const envPath = path.join(root, ".env");
|
|
146
|
+
const envExamplePath = path.join(root, ".env.example");
|
|
147
|
+
if (fs.existsSync(envPath)) {
|
|
148
|
+
return ".env: configured";
|
|
149
|
+
} else if (fs.existsSync(envExamplePath)) {
|
|
150
|
+
return ".env: not configured (run 'fops setup' or 'cp .env.example .env')";
|
|
130
151
|
}
|
|
152
|
+
return null;
|
|
153
|
+
}
|
|
131
154
|
|
|
132
|
-
|
|
155
|
+
async function gatherSkills() {
|
|
133
156
|
try {
|
|
134
157
|
const skills = await loadSkills();
|
|
135
158
|
if (skills.length) {
|
|
136
|
-
|
|
159
|
+
return "## Additional Skills\n" + skills.map((s) => s.content).join("\n\n");
|
|
137
160
|
}
|
|
138
161
|
} catch {
|
|
139
162
|
// skip if skill loading fails
|
|
140
163
|
}
|
|
164
|
+
return null;
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
export async function gatherStackContext(root, { registry, message } = {}) {
|
|
168
|
+
const parts = [getFoundationContextBlock(root)];
|
|
169
|
+
|
|
170
|
+
if (root) {
|
|
171
|
+
// Run all independent checks in parallel
|
|
172
|
+
const [dockerStatus, imageAges, prereqs, envInfo, skills, knowledge] = await Promise.all([
|
|
173
|
+
gatherDockerStatus(root),
|
|
174
|
+
gatherImageAges(root),
|
|
175
|
+
gatherPrereqs(),
|
|
176
|
+
Promise.resolve(checkEnvFile(root)),
|
|
177
|
+
gatherSkills(),
|
|
178
|
+
registry && message ? searchKnowledge(registry, message) : Promise.resolve(null),
|
|
179
|
+
]);
|
|
180
|
+
|
|
181
|
+
if (dockerStatus) parts.push(dockerStatus);
|
|
182
|
+
if (imageAges) parts.push(imageAges);
|
|
183
|
+
parts.push(prereqs);
|
|
184
|
+
if (envInfo) parts.push(envInfo);
|
|
185
|
+
if (skills) parts.push(skills);
|
|
186
|
+
if (knowledge) parts.push(knowledge);
|
|
187
|
+
} else {
|
|
188
|
+
// No root — still check prereqs, skills, and knowledge
|
|
189
|
+
const [prereqs, skills, knowledge] = await Promise.all([
|
|
190
|
+
gatherPrereqs(),
|
|
191
|
+
gatherSkills(),
|
|
192
|
+
registry && message ? searchKnowledge(registry, message) : Promise.resolve(null),
|
|
193
|
+
]);
|
|
194
|
+
parts.push(prereqs);
|
|
195
|
+
if (skills) parts.push(skills);
|
|
196
|
+
if (knowledge) parts.push(knowledge);
|
|
197
|
+
}
|
|
141
198
|
|
|
142
199
|
return parts.join("\n\n");
|
|
143
200
|
}
|