@meshxdata/fops 0.0.1
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.
Potentially problematic release.
This version of @meshxdata/fops might be problematic. Click here for more details.
- package/README.md +98 -0
- package/STRUCTURE.md +43 -0
- package/foundation.mjs +16 -0
- package/package.json +52 -0
- package/src/agent/agent.js +367 -0
- package/src/agent/agent.test.js +233 -0
- package/src/agent/context.js +143 -0
- package/src/agent/context.test.js +81 -0
- package/src/agent/index.js +2 -0
- package/src/agent/llm.js +127 -0
- package/src/agent/llm.test.js +139 -0
- package/src/auth/index.js +4 -0
- package/src/auth/keychain.js +58 -0
- package/src/auth/keychain.test.js +185 -0
- package/src/auth/login.js +421 -0
- package/src/auth/login.test.js +192 -0
- package/src/auth/oauth.js +203 -0
- package/src/auth/oauth.test.js +118 -0
- package/src/auth/resolve.js +78 -0
- package/src/auth/resolve.test.js +153 -0
- package/src/commands/index.js +268 -0
- package/src/config.js +24 -0
- package/src/config.test.js +70 -0
- package/src/doctor.js +487 -0
- package/src/doctor.test.js +134 -0
- package/src/plugins/api.js +37 -0
- package/src/plugins/api.test.js +95 -0
- package/src/plugins/discovery.js +78 -0
- package/src/plugins/discovery.test.js +92 -0
- package/src/plugins/hooks.js +13 -0
- package/src/plugins/hooks.test.js +118 -0
- package/src/plugins/index.js +3 -0
- package/src/plugins/loader.js +110 -0
- package/src/plugins/manifest.js +26 -0
- package/src/plugins/manifest.test.js +106 -0
- package/src/plugins/registry.js +14 -0
- package/src/plugins/registry.test.js +43 -0
- package/src/plugins/skills.js +126 -0
- package/src/plugins/skills.test.js +173 -0
- package/src/project.js +61 -0
- package/src/project.test.js +196 -0
- package/src/setup/aws.js +369 -0
- package/src/setup/aws.test.js +280 -0
- package/src/setup/index.js +3 -0
- package/src/setup/setup.js +161 -0
- package/src/setup/wizard.js +119 -0
- package/src/shell.js +9 -0
- package/src/shell.test.js +72 -0
- package/src/skills/foundation/SKILL.md +107 -0
- package/src/ui/banner.js +56 -0
- package/src/ui/banner.test.js +97 -0
- package/src/ui/confirm.js +97 -0
- package/src/ui/index.js +5 -0
- package/src/ui/input.js +199 -0
- package/src/ui/spinner.js +170 -0
- package/src/ui/spinner.test.js +29 -0
- package/src/ui/streaming.js +106 -0
|
@@ -0,0 +1,268 @@
|
|
|
1
|
+
import fs from "node:fs";
|
|
2
|
+
import os from "node:os";
|
|
3
|
+
import path from "node:path";
|
|
4
|
+
import chalk from "chalk";
|
|
5
|
+
import { Command } from "commander";
|
|
6
|
+
import { PKG } from "../config.js";
|
|
7
|
+
import { rootDir, requireRoot, hasComposeInDir, isFoundationRoot, findComposeRootUp } from "../project.js";
|
|
8
|
+
import { make } from "../shell.js";
|
|
9
|
+
import { runSetup, runInitWizard } from "../setup/index.js";
|
|
10
|
+
import { ensureEcrAuth } from "../setup/aws.js";
|
|
11
|
+
import { runAgentSingleTurn, runAgentInteractive } from "../agent/index.js";
|
|
12
|
+
import { runDoctor } from "../doctor.js";
|
|
13
|
+
import { runLogin } from "../auth/index.js";
|
|
14
|
+
import { runHook, loadSkills } from "../plugins/index.js";
|
|
15
|
+
|
|
16
|
+
export function registerCommands(program, registry) {
|
|
17
|
+
program.name(PKG.name).description("Install and manage Foundation data mesh platforms").version(PKG.version);
|
|
18
|
+
|
|
19
|
+
program
|
|
20
|
+
.command("login")
|
|
21
|
+
.description("Authenticate with Claude (OAuth login via browser)")
|
|
22
|
+
.option("--no-browser", "Paste API key in terminal instead of OAuth")
|
|
23
|
+
.action(async (opts) => {
|
|
24
|
+
await runLogin({ browser: opts.browser });
|
|
25
|
+
});
|
|
26
|
+
|
|
27
|
+
program
|
|
28
|
+
.command("setup")
|
|
29
|
+
.description("Complete automated setup (env, submodules, optional download). Replaces setup.sh.")
|
|
30
|
+
.option("-d, --dir <path>", "Project directory (default: cwd or FOUNDATION_ROOT)")
|
|
31
|
+
.option("--no-submodules", "Skip git submodule init/update")
|
|
32
|
+
.option("--no-env", "Skip .env creation from .env.example")
|
|
33
|
+
.option("--no-netrc-check", "Skip GitHub .netrc reminder")
|
|
34
|
+
.option("--download", "Run make download after submodules", false)
|
|
35
|
+
.option("--yes", "Use defaults without prompting")
|
|
36
|
+
.action(async (opts) => {
|
|
37
|
+
const dir = opts.dir ? path.resolve(opts.dir) : (rootDir() || process.cwd());
|
|
38
|
+
if (!fs.existsSync(path.join(dir, "docker-compose.yaml")) && !fs.existsSync(path.join(dir, "docker-compose.yml"))) {
|
|
39
|
+
console.error(chalk.red("No docker-compose in %s. Run from foundation-compose root."), dir);
|
|
40
|
+
process.exit(1);
|
|
41
|
+
}
|
|
42
|
+
await runHook(registry, "before:setup", { root: dir });
|
|
43
|
+
await runSetup(dir, {
|
|
44
|
+
submodules: opts.submodules !== false,
|
|
45
|
+
env: opts.env !== false,
|
|
46
|
+
netrcCheck: opts.netrcCheck !== false,
|
|
47
|
+
download: opts.download === true,
|
|
48
|
+
});
|
|
49
|
+
await runHook(registry, "after:setup", { root: dir });
|
|
50
|
+
});
|
|
51
|
+
|
|
52
|
+
program
|
|
53
|
+
.command("init")
|
|
54
|
+
.description("Initialize a Foundation project. Runs an interactive wizard when not in a project root.")
|
|
55
|
+
.option("-d, --dir <path>", "Project directory (default: cwd or wizard)")
|
|
56
|
+
.option("--no-submodules", "Skip git submodule init/update")
|
|
57
|
+
.option("--yes", "Use defaults without prompting; fail if no docker-compose in cwd")
|
|
58
|
+
.action(async (opts) => {
|
|
59
|
+
const dir = opts.dir ? path.resolve(opts.dir) : process.cwd();
|
|
60
|
+
const hasCompose = hasComposeInDir(dir);
|
|
61
|
+
if (opts.yes) {
|
|
62
|
+
if (!hasCompose || !fs.existsSync(path.join(dir, "Makefile"))) {
|
|
63
|
+
console.error(chalk.red("No docker-compose + Makefile in %s. Clone foundation-compose first or run without --yes for the wizard."), dir);
|
|
64
|
+
process.exit(1);
|
|
65
|
+
}
|
|
66
|
+
await runSetup(dir, { submodules: opts.submodules !== false, env: true, netrcCheck: true });
|
|
67
|
+
return;
|
|
68
|
+
}
|
|
69
|
+
if (hasCompose && fs.existsSync(path.join(dir, "Makefile"))) {
|
|
70
|
+
await runSetup(dir, { submodules: opts.submodules !== false });
|
|
71
|
+
return;
|
|
72
|
+
}
|
|
73
|
+
await runInitWizard();
|
|
74
|
+
});
|
|
75
|
+
|
|
76
|
+
program
|
|
77
|
+
.command("agent")
|
|
78
|
+
.description("AI assistant for managing the Foundation stack. Interactive TUI or single-turn with -m.")
|
|
79
|
+
.option("-m, --message <text>", "Single turn: send message, get reply (uses ANTHROPIC_API_KEY or OPENAI_API_KEY)")
|
|
80
|
+
.option("--no-run", "Do not offer to run suggested commands (single-turn only)", false)
|
|
81
|
+
.option("--model <id>", "Model override (e.g. claude-sonnet-4-20250514, gpt-4o-mini)")
|
|
82
|
+
.action(async (opts) => {
|
|
83
|
+
const root = requireRoot(program);
|
|
84
|
+
if (opts.message) {
|
|
85
|
+
await runAgentSingleTurn(root, opts.message, { runSuggestions: opts.run !== false, model: opts.model });
|
|
86
|
+
} else {
|
|
87
|
+
await runAgentInteractive(root);
|
|
88
|
+
}
|
|
89
|
+
});
|
|
90
|
+
|
|
91
|
+
program
|
|
92
|
+
.command("up")
|
|
93
|
+
.description("Start all Foundation services (docker compose up)")
|
|
94
|
+
.option("-d, --detach", "Run in background", true)
|
|
95
|
+
.option("--no-chat", "Skip interactive AI assistant after startup")
|
|
96
|
+
.action(async (opts) => {
|
|
97
|
+
const root = requireRoot(program);
|
|
98
|
+
await ensureEcrAuth(root);
|
|
99
|
+
await runHook(registry, "before:up", { root });
|
|
100
|
+
await make(root, "start");
|
|
101
|
+
await runHook(registry, "after:up", { root });
|
|
102
|
+
if (opts.chat !== false) await runAgentInteractive(root);
|
|
103
|
+
});
|
|
104
|
+
|
|
105
|
+
program
|
|
106
|
+
.command("chat")
|
|
107
|
+
.description("Interactive AI assistant (same as foundation agent with no -m)")
|
|
108
|
+
.action(async () => {
|
|
109
|
+
const root = requireRoot(program);
|
|
110
|
+
await runAgentInteractive(root);
|
|
111
|
+
});
|
|
112
|
+
|
|
113
|
+
program
|
|
114
|
+
.command("down")
|
|
115
|
+
.description("Stop all Foundation services")
|
|
116
|
+
.option("--clean", "Remove volumes and orphans (make clean)")
|
|
117
|
+
.action(async (opts) => {
|
|
118
|
+
const root = requireRoot(program);
|
|
119
|
+
await runHook(registry, "before:down", { root });
|
|
120
|
+
await make(root, opts.clean ? "clean" : "stop");
|
|
121
|
+
await runHook(registry, "after:down", { root });
|
|
122
|
+
});
|
|
123
|
+
|
|
124
|
+
program
|
|
125
|
+
.command("status")
|
|
126
|
+
.description("Show status of Foundation services")
|
|
127
|
+
.action(async () => {
|
|
128
|
+
const root = requireRoot(program);
|
|
129
|
+
await make(root, "status");
|
|
130
|
+
});
|
|
131
|
+
|
|
132
|
+
program
|
|
133
|
+
.command("logs")
|
|
134
|
+
.description("Stream logs for all services or a specific service")
|
|
135
|
+
.argument("[service]", "Service name (e.g. backend, frontend)")
|
|
136
|
+
.option("-f, --follow", "Follow log output", true)
|
|
137
|
+
.action(async (service, opts) => {
|
|
138
|
+
const root = requireRoot(program);
|
|
139
|
+
if (service) await make(root, `logs-${service}`);
|
|
140
|
+
else await make(root, "logs");
|
|
141
|
+
});
|
|
142
|
+
|
|
143
|
+
program
|
|
144
|
+
.command("doctor")
|
|
145
|
+
.description("Check environment and Foundation setup (Docker, git, .env, submodules)")
|
|
146
|
+
.option("--fix", "Apply suggested fixes where possible", false)
|
|
147
|
+
.action(async (opts) => {
|
|
148
|
+
await runDoctor(opts, registry);
|
|
149
|
+
});
|
|
150
|
+
|
|
151
|
+
program
|
|
152
|
+
.command("config")
|
|
153
|
+
.description("Launch interactive configuration (make config)")
|
|
154
|
+
.action(async () => {
|
|
155
|
+
const root = requireRoot(program);
|
|
156
|
+
await make(root, "config");
|
|
157
|
+
});
|
|
158
|
+
|
|
159
|
+
program
|
|
160
|
+
.command("bootstrap")
|
|
161
|
+
.description("Create demo data mesh (make bootstrap)")
|
|
162
|
+
.action(async () => {
|
|
163
|
+
const root = requireRoot(program);
|
|
164
|
+
await make(root, "bootstrap");
|
|
165
|
+
});
|
|
166
|
+
|
|
167
|
+
program
|
|
168
|
+
.command("test")
|
|
169
|
+
.description("Run health checks (make test)")
|
|
170
|
+
.action(async () => {
|
|
171
|
+
const root = requireRoot(program);
|
|
172
|
+
await make(root, "test");
|
|
173
|
+
});
|
|
174
|
+
|
|
175
|
+
// ── Skill management commands ──────────────────────
|
|
176
|
+
const skillCmd = program
|
|
177
|
+
.command("skill")
|
|
178
|
+
.description("Manage agent skills");
|
|
179
|
+
|
|
180
|
+
skillCmd
|
|
181
|
+
.command("list")
|
|
182
|
+
.description("List available agent skills")
|
|
183
|
+
.action(async () => {
|
|
184
|
+
const skills = await loadSkills(registry);
|
|
185
|
+
if (skills.length === 0) {
|
|
186
|
+
console.log(chalk.gray(" No skills available."));
|
|
187
|
+
return;
|
|
188
|
+
}
|
|
189
|
+
console.log(chalk.bold.cyan("\n Agent Skills\n"));
|
|
190
|
+
for (const s of skills) {
|
|
191
|
+
const source = s.pluginId ? chalk.gray(`(plugin: ${s.pluginId})`) : chalk.gray("(built-in)");
|
|
192
|
+
console.log(` ${chalk.green("●")} ${chalk.bold(s.name)} ${source}`);
|
|
193
|
+
if (s.description) console.log(chalk.gray(` ${s.description}`));
|
|
194
|
+
}
|
|
195
|
+
console.log("");
|
|
196
|
+
});
|
|
197
|
+
|
|
198
|
+
// ── Plugin management commands ─────────────────────
|
|
199
|
+
const pluginCmd = program
|
|
200
|
+
.command("plugin")
|
|
201
|
+
.description("Manage fops plugins");
|
|
202
|
+
|
|
203
|
+
pluginCmd
|
|
204
|
+
.command("list")
|
|
205
|
+
.description("List installed plugins with status")
|
|
206
|
+
.action(async () => {
|
|
207
|
+
if (registry.plugins.length === 0) {
|
|
208
|
+
console.log(chalk.gray(" No plugins installed."));
|
|
209
|
+
console.log(chalk.gray(" Install plugins to ~/.fops/plugins/ or via npm (fops-plugin-*)."));
|
|
210
|
+
return;
|
|
211
|
+
}
|
|
212
|
+
console.log(chalk.bold.cyan("\n Installed Plugins\n"));
|
|
213
|
+
for (const p of registry.plugins) {
|
|
214
|
+
const source = chalk.gray(`(${p.source})`);
|
|
215
|
+
console.log(` ${chalk.green("●")} ${chalk.bold(p.name)} ${chalk.gray("v" + p.version)} ${source}`);
|
|
216
|
+
console.log(chalk.gray(` id: ${p.id} path: ${p.path}`));
|
|
217
|
+
}
|
|
218
|
+
console.log("");
|
|
219
|
+
});
|
|
220
|
+
|
|
221
|
+
pluginCmd
|
|
222
|
+
.command("install <source>")
|
|
223
|
+
.description("Install a plugin from a local path")
|
|
224
|
+
.action(async (source) => {
|
|
225
|
+
const srcPath = path.resolve(source);
|
|
226
|
+
if (!fs.existsSync(srcPath) || !fs.existsSync(path.join(srcPath, "fops.plugin.json"))) {
|
|
227
|
+
console.error(chalk.red("Source must be a directory with fops.plugin.json"));
|
|
228
|
+
process.exit(1);
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
let manifest;
|
|
232
|
+
try {
|
|
233
|
+
manifest = JSON.parse(fs.readFileSync(path.join(srcPath, "fops.plugin.json"), "utf8"));
|
|
234
|
+
} catch {
|
|
235
|
+
console.error(chalk.red("Invalid fops.plugin.json"));
|
|
236
|
+
process.exit(1);
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
const dest = path.join(os.homedir(), ".fops", "plugins", manifest.id);
|
|
240
|
+
fs.mkdirSync(dest, { recursive: true });
|
|
241
|
+
|
|
242
|
+
const entries = fs.readdirSync(srcPath, { withFileTypes: true });
|
|
243
|
+
for (const entry of entries) {
|
|
244
|
+
const srcFile = path.join(srcPath, entry.name);
|
|
245
|
+
const destFile = path.join(dest, entry.name);
|
|
246
|
+
if (entry.isFile()) {
|
|
247
|
+
fs.copyFileSync(srcFile, destFile);
|
|
248
|
+
} else if (entry.isDirectory()) {
|
|
249
|
+
fs.cpSync(srcFile, destFile, { recursive: true });
|
|
250
|
+
}
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
console.log(chalk.green(` ✓ Installed plugin "${manifest.id}" to ${dest}`));
|
|
254
|
+
});
|
|
255
|
+
|
|
256
|
+
pluginCmd
|
|
257
|
+
.command("remove <id>")
|
|
258
|
+
.description("Remove a plugin from ~/.fops/plugins/")
|
|
259
|
+
.action(async (id) => {
|
|
260
|
+
const pluginDir = path.join(os.homedir(), ".fops", "plugins", id);
|
|
261
|
+
if (!fs.existsSync(pluginDir)) {
|
|
262
|
+
console.error(chalk.red(`Plugin "${id}" not found in ~/.fops/plugins/`));
|
|
263
|
+
process.exit(1);
|
|
264
|
+
}
|
|
265
|
+
fs.rmSync(pluginDir, { recursive: true, force: true });
|
|
266
|
+
console.log(chalk.green(` ✓ Removed plugin "${id}"`));
|
|
267
|
+
});
|
|
268
|
+
}
|
package/src/config.js
ADDED
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import os from "node:os";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
import { createRequire } from "node:module";
|
|
4
|
+
import { fileURLToPath } from "node:url";
|
|
5
|
+
import chalk from "chalk";
|
|
6
|
+
|
|
7
|
+
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
|
8
|
+
const require = createRequire(import.meta.url);
|
|
9
|
+
const pkg = require("../package.json");
|
|
10
|
+
|
|
11
|
+
export const PKG = { name: pkg.name, version: pkg.version };
|
|
12
|
+
export const CLI_BRAND = {
|
|
13
|
+
title: "Foundation Operator CLI",
|
|
14
|
+
version: `v${PKG.version}`,
|
|
15
|
+
byline: "Foundation Team · meshx",
|
|
16
|
+
};
|
|
17
|
+
|
|
18
|
+
export function printFoundationBanner(cwd) {
|
|
19
|
+
const cwdShort = cwd.replace(os.homedir(), "~");
|
|
20
|
+
console.log(chalk.cyan(` ${CLI_BRAND.title} ${CLI_BRAND.version}`));
|
|
21
|
+
console.log(chalk.gray(` ${CLI_BRAND.byline}`));
|
|
22
|
+
console.log(chalk.gray(` ${cwdShort}`));
|
|
23
|
+
console.log("");
|
|
24
|
+
}
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
import { describe, it, expect, vi } from "vitest";
|
|
2
|
+
import os from "node:os";
|
|
3
|
+
import { PKG, CLI_BRAND, printFoundationBanner } from "./config.js";
|
|
4
|
+
|
|
5
|
+
describe("config", () => {
|
|
6
|
+
describe("PKG", () => {
|
|
7
|
+
it("exports name and version", () => {
|
|
8
|
+
expect(PKG.name).toBe("@meshxdata/fops");
|
|
9
|
+
expect(typeof PKG.version).toBe("string");
|
|
10
|
+
expect(PKG.version).toMatch(/^\d+\.\d+\.\d+/);
|
|
11
|
+
});
|
|
12
|
+
|
|
13
|
+
it("only exposes name and version (no extra fields)", () => {
|
|
14
|
+
expect(Object.keys(PKG).sort()).toEqual(["name", "version"]);
|
|
15
|
+
});
|
|
16
|
+
});
|
|
17
|
+
|
|
18
|
+
describe("CLI_BRAND", () => {
|
|
19
|
+
it("has the expected shape", () => {
|
|
20
|
+
expect(CLI_BRAND.title).toBe("Foundation Operator CLI");
|
|
21
|
+
expect(CLI_BRAND.version).toMatch(/^v\d+/);
|
|
22
|
+
expect(CLI_BRAND.byline).toContain("meshx");
|
|
23
|
+
});
|
|
24
|
+
|
|
25
|
+
it("version matches PKG.version", () => {
|
|
26
|
+
expect(CLI_BRAND.version).toBe(`v${PKG.version}`);
|
|
27
|
+
});
|
|
28
|
+
});
|
|
29
|
+
|
|
30
|
+
describe("printFoundationBanner", () => {
|
|
31
|
+
it("prints CLI info to stdout", () => {
|
|
32
|
+
const spy = vi.spyOn(console, "log").mockImplementation(() => {});
|
|
33
|
+
printFoundationBanner("/tmp/test-project");
|
|
34
|
+
expect(spy).toHaveBeenCalled();
|
|
35
|
+
const output = spy.mock.calls.map((c) => c[0]).join("\n");
|
|
36
|
+
expect(output).toContain("Foundation Operator CLI");
|
|
37
|
+
});
|
|
38
|
+
|
|
39
|
+
it("shortens home dir to ~", () => {
|
|
40
|
+
const spy = vi.spyOn(console, "log").mockImplementation(() => {});
|
|
41
|
+
const home = os.homedir();
|
|
42
|
+
printFoundationBanner(home + "/projects/test");
|
|
43
|
+
const output = spy.mock.calls.map((c) => c[0]).join("\n");
|
|
44
|
+
expect(output).toContain("~/projects/test");
|
|
45
|
+
});
|
|
46
|
+
|
|
47
|
+
it("does not replace ~ when path is not under homedir", () => {
|
|
48
|
+
const spy = vi.spyOn(console, "log").mockImplementation(() => {});
|
|
49
|
+
printFoundationBanner("/opt/data/project");
|
|
50
|
+
const output = spy.mock.calls.map((c) => c[0]).join("\n");
|
|
51
|
+
expect(output).toContain("/opt/data/project");
|
|
52
|
+
expect(output).not.toContain("~");
|
|
53
|
+
});
|
|
54
|
+
|
|
55
|
+
it("prints version and byline", () => {
|
|
56
|
+
const spy = vi.spyOn(console, "log").mockImplementation(() => {});
|
|
57
|
+
printFoundationBanner("/tmp");
|
|
58
|
+
const output = spy.mock.calls.map((c) => c[0]).join("\n");
|
|
59
|
+
expect(output).toContain(CLI_BRAND.version);
|
|
60
|
+
expect(output).toContain("meshx");
|
|
61
|
+
});
|
|
62
|
+
|
|
63
|
+
it("prints a trailing blank line", () => {
|
|
64
|
+
const spy = vi.spyOn(console, "log").mockImplementation(() => {});
|
|
65
|
+
printFoundationBanner("/tmp");
|
|
66
|
+
// Last call should be empty string (blank line)
|
|
67
|
+
expect(spy.mock.calls[spy.mock.calls.length - 1][0]).toBe("");
|
|
68
|
+
});
|
|
69
|
+
});
|
|
70
|
+
});
|