@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.

Files changed (57) hide show
  1. package/README.md +98 -0
  2. package/STRUCTURE.md +43 -0
  3. package/foundation.mjs +16 -0
  4. package/package.json +52 -0
  5. package/src/agent/agent.js +367 -0
  6. package/src/agent/agent.test.js +233 -0
  7. package/src/agent/context.js +143 -0
  8. package/src/agent/context.test.js +81 -0
  9. package/src/agent/index.js +2 -0
  10. package/src/agent/llm.js +127 -0
  11. package/src/agent/llm.test.js +139 -0
  12. package/src/auth/index.js +4 -0
  13. package/src/auth/keychain.js +58 -0
  14. package/src/auth/keychain.test.js +185 -0
  15. package/src/auth/login.js +421 -0
  16. package/src/auth/login.test.js +192 -0
  17. package/src/auth/oauth.js +203 -0
  18. package/src/auth/oauth.test.js +118 -0
  19. package/src/auth/resolve.js +78 -0
  20. package/src/auth/resolve.test.js +153 -0
  21. package/src/commands/index.js +268 -0
  22. package/src/config.js +24 -0
  23. package/src/config.test.js +70 -0
  24. package/src/doctor.js +487 -0
  25. package/src/doctor.test.js +134 -0
  26. package/src/plugins/api.js +37 -0
  27. package/src/plugins/api.test.js +95 -0
  28. package/src/plugins/discovery.js +78 -0
  29. package/src/plugins/discovery.test.js +92 -0
  30. package/src/plugins/hooks.js +13 -0
  31. package/src/plugins/hooks.test.js +118 -0
  32. package/src/plugins/index.js +3 -0
  33. package/src/plugins/loader.js +110 -0
  34. package/src/plugins/manifest.js +26 -0
  35. package/src/plugins/manifest.test.js +106 -0
  36. package/src/plugins/registry.js +14 -0
  37. package/src/plugins/registry.test.js +43 -0
  38. package/src/plugins/skills.js +126 -0
  39. package/src/plugins/skills.test.js +173 -0
  40. package/src/project.js +61 -0
  41. package/src/project.test.js +196 -0
  42. package/src/setup/aws.js +369 -0
  43. package/src/setup/aws.test.js +280 -0
  44. package/src/setup/index.js +3 -0
  45. package/src/setup/setup.js +161 -0
  46. package/src/setup/wizard.js +119 -0
  47. package/src/shell.js +9 -0
  48. package/src/shell.test.js +72 -0
  49. package/src/skills/foundation/SKILL.md +107 -0
  50. package/src/ui/banner.js +56 -0
  51. package/src/ui/banner.test.js +97 -0
  52. package/src/ui/confirm.js +97 -0
  53. package/src/ui/index.js +5 -0
  54. package/src/ui/input.js +199 -0
  55. package/src/ui/spinner.js +170 -0
  56. package/src/ui/spinner.test.js +29 -0
  57. 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
+ });