@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.
Files changed (44) hide show
  1. package/package.json +3 -2
  2. package/src/auth/coda.js +4 -4
  3. package/src/auth/login.js +9 -6
  4. package/src/commands/index.js +158 -0
  5. package/src/doctor.js +340 -71
  6. package/src/feature-flags.js +3 -3
  7. package/src/lazy.js +12 -0
  8. package/src/plugins/bundled/coda/auth.js +85 -0
  9. package/src/plugins/bundled/coda/client.js +187 -0
  10. package/src/plugins/bundled/coda/fops.plugin.json +7 -0
  11. package/src/plugins/bundled/coda/index.js +284 -0
  12. package/src/plugins/bundled/coda/package.json +3 -0
  13. package/src/plugins/bundled/coda/skills/coda/SKILL.md +82 -0
  14. package/src/plugins/bundled/cursor/fops.plugin.json +7 -0
  15. package/src/plugins/bundled/cursor/index.js +433 -0
  16. package/src/plugins/bundled/cursor/package.json +1 -0
  17. package/src/plugins/bundled/cursor/skills/cursor/SKILL.md +48 -0
  18. package/src/plugins/bundled/fops-plugin-1password/fops.plugin.json +7 -0
  19. package/src/plugins/bundled/fops-plugin-1password/index.js +241 -0
  20. package/src/plugins/bundled/fops-plugin-1password/lib/env.js +100 -0
  21. package/src/plugins/bundled/fops-plugin-1password/lib/op.js +119 -0
  22. package/src/plugins/bundled/fops-plugin-1password/lib/setup.js +235 -0
  23. package/src/plugins/bundled/fops-plugin-1password/lib/sync.js +66 -0
  24. package/src/plugins/bundled/fops-plugin-1password/package.json +1 -0
  25. package/src/plugins/bundled/fops-plugin-1password/skills/1password/SKILL.md +79 -0
  26. package/src/plugins/bundled/fops-plugin-ecr/fops.plugin.json +7 -0
  27. package/src/plugins/bundled/fops-plugin-ecr/index.js +302 -0
  28. package/src/plugins/bundled/fops-plugin-ecr/lib/aws.js +146 -0
  29. package/src/plugins/bundled/fops-plugin-ecr/lib/images.js +73 -0
  30. package/src/plugins/bundled/fops-plugin-ecr/lib/setup.js +180 -0
  31. package/src/plugins/bundled/fops-plugin-ecr/lib/sync.js +74 -0
  32. package/src/plugins/bundled/fops-plugin-ecr/package.json +1 -0
  33. package/src/plugins/bundled/fops-plugin-ecr/skills/ecr/SKILL.md +105 -0
  34. package/src/plugins/bundled/fops-plugin-memory/fops.plugin.json +7 -0
  35. package/src/plugins/bundled/fops-plugin-memory/index.js +148 -0
  36. package/src/plugins/bundled/fops-plugin-memory/lib/relevance.js +72 -0
  37. package/src/plugins/bundled/fops-plugin-memory/lib/store.js +75 -0
  38. package/src/plugins/bundled/fops-plugin-memory/package.json +1 -0
  39. package/src/plugins/bundled/fops-plugin-memory/skills/memory/SKILL.md +58 -0
  40. package/src/plugins/loader.js +43 -3
  41. package/src/setup/aws.js +74 -46
  42. package/src/setup/setup.js +4 -2
  43. package/src/setup/wizard.js +16 -20
  44. package/src/wsl.js +82 -0
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@meshxdata/fops",
3
- "version": "0.0.5",
3
+ "version": "0.0.7",
4
4
  "description": "CLI to install and manage Foundation data mesh platforms",
5
5
  "keywords": [
6
6
  "foundation",
@@ -25,8 +25,9 @@
25
25
  "test:watch": "vitest"
26
26
  },
27
27
  "dependencies": {
28
+ "24": "^0.0.0",
28
29
  "@anthropic-ai/claude-code": "^1.0.0",
29
- "@meshxdata/fops": "^0.0.4",
30
+ "@meshxdata/fops": "0.0.6",
30
31
  "boxen": "^8.0.1",
31
32
  "chalk": "^5.3.0",
32
33
  "commander": "^12.0.0",
package/src/auth/coda.js CHANGED
@@ -2,8 +2,8 @@ import fs from "node:fs";
2
2
  import os from "node:os";
3
3
  import path from "node:path";
4
4
  import chalk from "chalk";
5
- import inquirer from "inquirer";
6
5
  import { openBrowser } from "./login.js";
6
+ import { getInquirer } from "../lazy.js";
7
7
 
8
8
  const CODA_ACCOUNT_URL = "https://coda.io/account";
9
9
  const FOPS_CONFIG_PATH = path.join(os.homedir(), ".fops.json");
@@ -60,7 +60,7 @@ export async function runCodaLogin() {
60
60
  const existing = resolveCodaApiToken();
61
61
  if (existing) {
62
62
  const masked = existing.slice(0, 8) + "..." + existing.slice(-4);
63
- const { overwrite } = await inquirer.prompt([{
63
+ const { overwrite } = await (await getInquirer()).prompt([{
64
64
  type: "confirm",
65
65
  name: "overwrite",
66
66
  message: `Already have a Coda API token (${masked}). Replace it?`,
@@ -84,7 +84,7 @@ export async function runCodaLogin() {
84
84
  console.log(chalk.dim(" 5. Copy the token and paste it below"));
85
85
  console.log("");
86
86
 
87
- const { openIt } = await inquirer.prompt([{
87
+ const { openIt } = await (await getInquirer()).prompt([{
88
88
  type: "confirm",
89
89
  name: "openIt",
90
90
  message: "Open Coda account page in your browser?",
@@ -100,7 +100,7 @@ export async function runCodaLogin() {
100
100
  }
101
101
  }
102
102
 
103
- const { token } = await inquirer.prompt([{
103
+ const { token } = await (await getInquirer()).prompt([{
104
104
  type: "password",
105
105
  name: "token",
106
106
  message: "Paste your Coda API token:",
package/src/auth/login.js CHANGED
@@ -2,8 +2,8 @@ import fs from "node:fs";
2
2
  import http from "node:http";
3
3
  import chalk from "chalk";
4
4
  import { execaSync } from "execa";
5
- import inquirer from "inquirer";
6
5
  import { CLAUDE_DIR, CLAUDE_CREDENTIALS, resolveAnthropicApiKey } from "./resolve.js";
6
+ import { getInquirer } from "../lazy.js";
7
7
 
8
8
  const ANTHROPIC_KEYS_URL = "https://console.anthropic.com/settings/keys";
9
9
 
@@ -17,9 +17,12 @@ export function authHelp() {
17
17
 
18
18
  export function openBrowser(url) {
19
19
  const platform = process.platform;
20
- const cmd = platform === "darwin" ? "open" : platform === "win32" ? "start" : "xdg-open";
21
20
  try {
22
- execaSync(cmd, [url], { reject: false });
21
+ if (platform === "win32") {
22
+ execaSync("cmd", ["/c", "start", "", url], { reject: false });
23
+ } else {
24
+ execaSync(platform === "darwin" ? "open" : "xdg-open", [url], { reject: false });
25
+ }
23
26
  return true;
24
27
  } catch {
25
28
  return false;
@@ -51,7 +54,7 @@ export function saveApiKey(apiKey) {
51
54
  }
52
55
 
53
56
  export async function offerClaudeLogin() {
54
- const { startLogin } = await inquirer.prompt([
57
+ const { startLogin } = await (await getInquirer()).prompt([
55
58
  {
56
59
  type: "confirm",
57
60
  name: "startLogin",
@@ -301,7 +304,7 @@ export async function runLogin(options = {}) {
301
304
  const existingKey = resolveAnthropicApiKey();
302
305
  if (existingKey) {
303
306
  const masked = existingKey.slice(0, 15) + "..." + existingKey.slice(-4);
304
- const { overwrite } = await inquirer.prompt([
307
+ const { overwrite } = await (await getInquirer()).prompt([
305
308
  {
306
309
  type: "confirm",
307
310
  name: "overwrite",
@@ -323,7 +326,7 @@ export async function runLogin(options = {}) {
323
326
  // Terminal-only flow
324
327
  console.log(chalk.blue("\nGet an API key from: " + ANTHROPIC_KEYS_URL + "\n"));
325
328
 
326
- const { apiKey } = await inquirer.prompt([
329
+ const { apiKey } = await (await getInquirer()).prompt([
327
330
  {
328
331
  type: "password",
329
332
  name: "apiKey",
@@ -1,6 +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 https from "node:https";
4
5
  import chalk from "chalk";
5
6
  import { Command } from "commander";
6
7
  import { PKG } from "../config.js";
@@ -229,6 +230,49 @@ export function registerCommands(program, registry) {
229
230
  await make(root, "download");
230
231
  });
231
232
 
233
+ program
234
+ .command("update")
235
+ .description("Update fops CLI to the latest version")
236
+ .option("--check", "Only check for updates, don't install")
237
+ .action(async (opts) => {
238
+ const currentVersion = PKG.version;
239
+ console.log(chalk.dim(` Current version: ${currentVersion}`));
240
+
241
+ // Fetch latest version from npm registry
242
+ console.log(chalk.dim(" Checking for updates..."));
243
+ let latestVersion;
244
+ try {
245
+ const { stdout } = await execa("npm", ["view", PKG.name, "version"], { timeout: 15000 });
246
+ latestVersion = stdout.trim();
247
+ } catch (err) {
248
+ console.error(chalk.red(` Failed to check npm registry: ${err.message}`));
249
+ process.exit(1);
250
+ }
251
+
252
+ if (latestVersion === currentVersion) {
253
+ console.log(chalk.green(` ✓ Already on the latest version (${currentVersion})`));
254
+ return;
255
+ }
256
+
257
+ console.log(chalk.yellow(` Update available: ${currentVersion} → ${latestVersion}`));
258
+
259
+ if (opts.check) {
260
+ console.log(chalk.dim(` Run 'fops update' to install the update.`));
261
+ return;
262
+ }
263
+
264
+ // Install the latest version globally
265
+ console.log(chalk.cyan(` ▶ npm install -g ${PKG.name}@latest`));
266
+ try {
267
+ await execa("npm", ["install", "-g", `${PKG.name}@latest`], { stdio: "inherit", timeout: 300_000 });
268
+ console.log(chalk.green(` ✓ Updated to ${latestVersion}`));
269
+ } catch (err) {
270
+ console.error(chalk.red(` Update failed: ${err.message}`));
271
+ console.log(chalk.dim(" Try running with sudo: sudo fops update"));
272
+ process.exit(1);
273
+ }
274
+ });
275
+
232
276
  program
233
277
  .command("bootstrap")
234
278
  .description("Create demo data mesh (make bootstrap)")
@@ -385,4 +429,118 @@ export function registerCommands(program, registry) {
385
429
  setPluginEnabled(id, false);
386
430
  console.log(chalk.yellow(` ○ Disabled plugin "${id}". Restart fops to apply.`));
387
431
  });
432
+
433
+ // ── Plugin marketplace ──────────────────────────────
434
+ const marketplaceCmd = pluginCmd
435
+ .command("marketplace")
436
+ .description("Browse and install plugins from GitHub");
437
+
438
+ marketplaceCmd
439
+ .command("add <repo>")
440
+ .description("Install a plugin from GitHub (owner/repo)")
441
+ .action(async (repo) => {
442
+ const parts = repo.split("/");
443
+ if (parts.length !== 2 || !parts[0] || !parts[1]) {
444
+ console.error(chalk.red(' Usage: fops plugin marketplace add <owner/repo>'));
445
+ process.exit(1);
446
+ }
447
+ const [owner, repoName] = parts;
448
+
449
+ // Download tarball from GitHub
450
+ const tarballUrl = `https://api.github.com/repos/${owner}/${repoName}/tarball/main`;
451
+ const tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), "fops-marketplace-"));
452
+ const tarPath = path.join(tmpDir, "plugin.tar.gz");
453
+
454
+ console.log(chalk.blue(` Downloading ${owner}/${repoName}...`));
455
+
456
+ try {
457
+ await new Promise((resolve, reject) => {
458
+ const follow = (url) => {
459
+ https.get(url, { headers: { "User-Agent": "fops-cli", Accept: "application/vnd.github+json" } }, (res) => {
460
+ if (res.statusCode >= 300 && res.statusCode < 400 && res.headers.location) {
461
+ follow(res.headers.location);
462
+ return;
463
+ }
464
+ if (res.statusCode !== 200) {
465
+ reject(new Error(`GitHub returned ${res.statusCode}. Check that ${owner}/${repoName} exists and is public.`));
466
+ res.resume();
467
+ return;
468
+ }
469
+ const ws = fs.createWriteStream(tarPath);
470
+ res.pipe(ws);
471
+ ws.on("finish", resolve);
472
+ ws.on("error", reject);
473
+ }).on("error", reject);
474
+ };
475
+ follow(tarballUrl);
476
+ });
477
+ } catch (err) {
478
+ console.error(chalk.red(` Download failed: ${err.message}`));
479
+ fs.rmSync(tmpDir, { recursive: true, force: true });
480
+ process.exit(1);
481
+ }
482
+
483
+ // Extract tarball
484
+ const extractDir = path.join(tmpDir, "extracted");
485
+ fs.mkdirSync(extractDir, { recursive: true });
486
+ try {
487
+ await execa("tar", ["xzf", tarPath, "-C", extractDir]);
488
+ } catch (err) {
489
+ console.error(chalk.red(` Failed to extract archive: ${err.message}`));
490
+ fs.rmSync(tmpDir, { recursive: true, force: true });
491
+ process.exit(1);
492
+ }
493
+
494
+ // GitHub tarballs extract into a single directory like owner-repo-sha/
495
+ const extracted = fs.readdirSync(extractDir);
496
+ if (extracted.length === 0) {
497
+ console.error(chalk.red(" Archive was empty."));
498
+ fs.rmSync(tmpDir, { recursive: true, force: true });
499
+ process.exit(1);
500
+ }
501
+ const pluginSrc = path.join(extractDir, extracted[0]);
502
+
503
+ // Validate manifest
504
+ const manifestPath = path.join(pluginSrc, "fops.plugin.json");
505
+ if (!fs.existsSync(manifestPath)) {
506
+ console.error(chalk.red(` No fops.plugin.json found in ${owner}/${repoName}. Not a valid fops plugin.`));
507
+ fs.rmSync(tmpDir, { recursive: true, force: true });
508
+ process.exit(1);
509
+ }
510
+
511
+ let manifest;
512
+ try {
513
+ manifest = JSON.parse(fs.readFileSync(manifestPath, "utf8"));
514
+ } catch {
515
+ console.error(chalk.red(" Invalid fops.plugin.json"));
516
+ fs.rmSync(tmpDir, { recursive: true, force: true });
517
+ process.exit(1);
518
+ }
519
+
520
+ if (!manifest.id) {
521
+ console.error(chalk.red(' fops.plugin.json missing required "id" field.'));
522
+ fs.rmSync(tmpDir, { recursive: true, force: true });
523
+ process.exit(1);
524
+ }
525
+
526
+ // Copy to ~/.fops/plugins/<id>/
527
+ const dest = path.join(os.homedir(), ".fops", "plugins", manifest.id);
528
+ fs.mkdirSync(dest, { recursive: true });
529
+
530
+ const entries = fs.readdirSync(pluginSrc, { withFileTypes: true });
531
+ for (const entry of entries) {
532
+ const srcFile = path.join(pluginSrc, entry.name);
533
+ const destFile = path.join(dest, entry.name);
534
+ if (entry.isFile()) {
535
+ fs.copyFileSync(srcFile, destFile);
536
+ } else if (entry.isDirectory()) {
537
+ fs.cpSync(srcFile, destFile, { recursive: true });
538
+ }
539
+ }
540
+
541
+ // Cleanup temp dir
542
+ fs.rmSync(tmpDir, { recursive: true, force: true });
543
+
544
+ console.log(chalk.green(` ✓ Installed plugin "${manifest.id}" from ${owner}/${repoName} to ${dest}`));
545
+ });
388
546
  }