@meshxdata/fops 0.1.36 → 0.1.38

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 (59) hide show
  1. package/CHANGELOG.md +207 -0
  2. package/fops.mjs +37 -14
  3. package/package.json +1 -1
  4. package/src/agent/llm.js +2 -0
  5. package/src/auth/azure.js +92 -0
  6. package/src/auth/cloudflare.js +125 -0
  7. package/src/auth/index.js +2 -0
  8. package/src/commands/index.js +8 -4
  9. package/src/commands/lifecycle.js +31 -10
  10. package/src/plugins/bundled/fops-plugin-azure/index.js +44 -2896
  11. package/src/plugins/bundled/fops-plugin-azure/lib/azure-aks.js +130 -2
  12. package/src/plugins/bundled/fops-plugin-azure/lib/azure-auth.js +497 -0
  13. package/src/plugins/bundled/fops-plugin-azure/lib/azure-helpers.js +51 -13
  14. package/src/plugins/bundled/fops-plugin-azure/lib/azure-ops.js +206 -52
  15. package/src/plugins/bundled/fops-plugin-azure/lib/azure-provision.js +128 -34
  16. package/src/plugins/bundled/fops-plugin-azure/lib/azure-shared-cache.js +1 -1
  17. package/src/plugins/bundled/fops-plugin-azure/lib/azure-sync.js +4 -4
  18. package/src/plugins/bundled/fops-plugin-azure/lib/azure.js +2 -2
  19. package/src/plugins/bundled/fops-plugin-azure/lib/commands/fleet-cmds.js +254 -0
  20. package/src/plugins/bundled/fops-plugin-azure/lib/commands/infra-cmds.js +894 -0
  21. package/src/plugins/bundled/fops-plugin-azure/lib/commands/test-cmds.js +314 -0
  22. package/src/plugins/bundled/fops-plugin-azure/lib/commands/vm-cmds.js +893 -0
  23. package/src/plugins/bundled/fops-plugin-azure/templates/cluster/apps/dai-backend.yaml +13 -0
  24. package/src/plugins/bundled/fops-plugin-azure/templates/cluster/apps/dai-frontend.yaml +13 -0
  25. package/src/plugins/bundled/fops-plugin-azure/templates/cluster/apps/foundation-backend.yaml +13 -0
  26. package/src/plugins/bundled/fops-plugin-azure/templates/cluster/apps/foundation-frontend.yaml +13 -0
  27. package/src/plugins/bundled/fops-plugin-azure/templates/cluster/apps/foundation-hive.yaml +13 -0
  28. package/src/plugins/bundled/fops-plugin-azure/templates/cluster/apps/foundation-kafka.yaml +13 -0
  29. package/src/plugins/bundled/fops-plugin-azure/templates/cluster/apps/foundation-meltano.yaml +13 -0
  30. package/src/plugins/bundled/fops-plugin-azure/templates/cluster/apps/foundation-mlflow.yaml +13 -0
  31. package/src/plugins/bundled/fops-plugin-azure/templates/cluster/apps/foundation-opa.yaml +13 -0
  32. package/src/plugins/bundled/fops-plugin-azure/templates/cluster/apps/foundation-processor.yaml +13 -0
  33. package/src/plugins/bundled/fops-plugin-azure/templates/cluster/apps/foundation-scheduler.yaml +13 -0
  34. package/src/plugins/bundled/fops-plugin-azure/templates/cluster/apps/foundation-storage-engine.yaml +13 -0
  35. package/src/plugins/bundled/fops-plugin-azure/templates/cluster/apps/foundation-trino.yaml +13 -0
  36. package/src/plugins/bundled/fops-plugin-azure/templates/cluster/apps/foundation-watcher.yaml +13 -0
  37. package/src/plugins/bundled/fops-plugin-azure/templates/cluster/config/repository.yaml +66 -0
  38. package/src/plugins/bundled/fops-plugin-azure/templates/cluster/kustomization.yaml +30 -0
  39. package/src/plugins/bundled/fops-plugin-azure/templates/cluster/operator/acr-webhook-controller.yaml +63 -0
  40. package/src/plugins/bundled/fops-plugin-azure/templates/cluster/operator/externalsecrets.yaml +15 -0
  41. package/src/plugins/bundled/fops-plugin-azure/templates/cluster/operator/istio.yaml +42 -0
  42. package/src/plugins/bundled/fops-plugin-azure/templates/cluster/operator/kafka.yaml +15 -0
  43. package/src/plugins/bundled/fops-plugin-azure/templates/cluster/operator/kube-reflector.yaml +33 -0
  44. package/src/plugins/bundled/fops-plugin-azure/templates/cluster/operator/kubecost.yaml +12 -0
  45. package/src/plugins/bundled/fops-plugin-azure/templates/cluster/operator/nats-server.yaml +15 -0
  46. package/src/plugins/bundled/fops-plugin-azure/templates/cluster/operator/prometheus-agent.yaml +34 -0
  47. package/src/plugins/bundled/fops-plugin-azure/templates/cluster/operator/reloader.yaml +12 -0
  48. package/src/plugins/bundled/fops-plugin-azure/templates/cluster/operator/spark.yaml +112 -0
  49. package/src/plugins/bundled/fops-plugin-azure/templates/cluster/operator/tailscale.yaml +67 -0
  50. package/src/plugins/bundled/fops-plugin-azure/templates/cluster/operator/vertical-pod-autoscaler.yaml +15 -0
  51. package/src/plugins/bundled/fops-plugin-file/demo/orders_renamed.aligned.csv +1 -1
  52. package/src/plugins/bundled/fops-plugin-file/index.js +81 -12
  53. package/src/plugins/bundled/fops-plugin-file/lib/match.js +133 -15
  54. package/src/plugins/bundled/fops-plugin-file/lib/report.js +3 -0
  55. package/src/plugins/bundled/fops-plugin-foundation/index.js +26 -6
  56. package/src/plugins/bundled/fops-plugin-foundation/lib/client.js +9 -5
  57. package/src/plugins/bundled/fops-plugin-foundation-graphql/lib/graphql/resolvers/data-product.js +32 -0
  58. package/src/plugins/bundled/fops-plugin-foundation-graphql/lib/graphql/schema.js +20 -1
  59. package/src/plugins/loader.js +23 -6
package/CHANGELOG.md CHANGED
@@ -1,3 +1,210 @@
1
+ ## [0.1.38] - 2026-03-10
2
+
3
+ - callback url for localhost (821fb94)
4
+ - disable 4 scaffolding plugin by default. (bfb2b76)
5
+ - jaccard improvements (b7494a0)
6
+ - refactor azure plugin (68dfef4)
7
+ - refactor azure plugin (b24a008)
8
+ - fix trino catalog missing (4928a55)
9
+ - v36 bump and changelog generation on openai (37a0440)
10
+ - v36 bump and changelog generation on openai (a3b02d9)
11
+ - bump (a990058)
12
+ - status bar fix and new plugin for ttyd (27dde1e)
13
+ - file demo and tray (1a3e704)
14
+ - electron app (59ad0bb)
15
+ - compose and fops file plugin (1cf0e81)
16
+ - bump (346ffc1)
17
+ - localhost replaced by 127.0.0.1 (82b9f30)
18
+ - .29 (587b0e1)
19
+ - improve up down and bootstrap script (b79ebaf)
20
+ - checksum (22c8086)
21
+ - checksum (96b434f)
22
+ - checksum (15ed3c0)
23
+ - checksum (8a6543a)
24
+ - bump embed trino linksg (8440504)
25
+ - bump data (765ffd9)
26
+ - bump (cb8b232)
27
+ - broken tests (c532229)
28
+ - release 0.1.18, preflight checks (d902249)
29
+ - fix compute display bug (d10f5d9)
30
+ - cleanup packer files (6330f18)
31
+ - plan mode (cb36a8a)
32
+ - bump to 0.1.16 - agent ui (41ac1a2)
33
+ - bump to 0.1.15 - agent ui (4ebe2e1)
34
+ - bump to 0.1.14 (6c3a7fa)
35
+ - bump to 0.1.13 (8db570f)
36
+ - release 0.1.12 (c1c79e5)
37
+ - bump (11aa3b0)
38
+ - git keep and bump tui (be1678e)
39
+ - skills, index, rrf, compacted context (100k > 10k) (7b2fffd)
40
+ - cloudflare and token consumption, graphs indexing (0ad9eec)
41
+ - bump storage default (22c83ba)
42
+ - storage fix (68a22a0)
43
+ - skills update (7f56500)
44
+ - v9 bump (3864446)
45
+ - bump (c95eedc)
46
+ - rrf (dbf8c95)
47
+ - feat: warning when running predictions (95e8c52)
48
+ - feat: support for local predictions (45cf26b)
49
+ - feat: wip support for predictions + mlflow (3457052)
50
+ - add Reciprocal Rank Fusion (RRF) to knowledge and skill retrieval (61549bc)
51
+ - validate CSV headers in compute_run readiness check (a8c7a43)
52
+ - fix corrupted Iceberg metadata: probe tables + force cleanup on re-apply (50578af)
53
+ - enforce: never use foundation_apply to fix broken products (2e049bf)
54
+ - update SKILL.md with complete tool reference for knowledge retrieval (30b1924)
55
+ - add storage read, input DP table probe, and compute_run improvements (34e6c4c)
56
+ - skills update (1220385)
57
+ - skills update (bb66958)
58
+ - some tui improvement andd tools apply overwrite (e90c35c)
59
+ - skills update (e9227a1)
60
+ - skills update (669c4b3)
61
+ - fix plugin pre-flight checks (f741743)
62
+ - increase agent context (6479aaa)
63
+ - skills and init sql fixes (5fce35e)
64
+ - checksum (3518b56)
65
+ - penging job limit (a139861)
66
+ - checksum (575d28c)
67
+ - bump (92049ba)
68
+ - fix bug per tab status (0a33657)
69
+ - fix bug per tab status (50457c6)
70
+ - checksumming (0ad842e)
71
+ - shot af mardkwon overlapping (51f63b9)
72
+ - add spark dockerfile for multiarch builds (95abbd1)
73
+ - fix plugin initialization (16b9782)
74
+ - split index.js (50902a2)
75
+ - cloudflare cidr (cc4e021)
76
+ - cloduflare restrictions (2f6ba2d)
77
+ - sequential start (86b496e)
78
+ - sequential start (4930fe1)
79
+ - sequential start (353f014)
80
+ - qa tests (2dc6a1a)
81
+ - bump sha for .85 (dc2edfe)
82
+ - preserve env on sudo (7831227)
83
+ - bump sha for .84 (6c052f9)
84
+ - non interactive for azure vms (0aa8a2f)
85
+ - keep .env if present (d072450)
86
+ - bump (7a8e732)
87
+ - ensure opa is on compose if not set (f4a5228)
88
+ - checksum bump (a2ccc20)
89
+ - netrc defensive checks (a0b0ccc)
90
+ - netrc defensive checks (ae37403)
91
+ - checksum (ec45d11)
92
+ - update sync and fix up (7f9af72)
93
+ - expand test for azure and add new per app tag support (388a168)
94
+ - checksum on update (44005fc)
95
+ - cleanup for later (15e5313)
96
+ - cleanup for later (11c9597)
97
+ - switch branch feature (822fecc)
98
+ - add pull (d1c19ab)
99
+ - Bump hono from 4.11.9 to 4.12.0 in /operator-cli (ad25144)
100
+ - tests (f180a9a)
101
+ - cleanup (39c49a3)
102
+ - registry (7b7126a)
103
+ - reconcile kafka (832d0db)
104
+ - gh login bug (025886c)
105
+ - cleanup (bb96cab)
106
+ - strip envs from process (2421180)
107
+ - force use of gh creds not tokens in envs var (fff7787)
108
+ - resolve import between npm installs and npm link (79522e1)
109
+ - fix gh scope and azure states (afd846c)
110
+ - refactoring (da50352)
111
+ - split fops repo (d447638)
112
+ - aks (b791f8f)
113
+ - refactor azure (67d3bad)
114
+ - wildcard (391f023)
115
+ - azure plugin (c074074)
116
+ - zap (d7e6e7f)
117
+ - fix knock (cf89c05)
118
+ - azure (4adec98)
119
+ - Bump tar from 7.5.7 to 7.5.9 in /operator-cli (e41e98e)
120
+ - azure stack index.js split (de12272)
121
+ - Bump ajv from 8.17.1 to 8.18.0 in /operator-cli (76da21f)
122
+ - packer (9665fbc)
123
+ - remove stack api (db0fd4d)
124
+ - packer cleanup (fe1bf14)
125
+ - force refresh token (3a3d7e2)
126
+ - provision shell (2ad505f)
127
+ - azure vm management (91dcb31)
128
+ - azure specific (2b0cca8)
129
+ - azure packer (12175b8)
130
+ - init hashed pwd (db8523c)
131
+ - packer (5b5c7c4)
132
+ - doctor for azure vm (ed524fa)
133
+ - packer and 1pwd (c6d053e)
134
+ - split big index.js (dc85a1b)
135
+ - kafka volume update (21815ec)
136
+ - fix openai azure tools confirmation and flow (0118cd1)
137
+ - nighly fixx, test fix (5e0d04f)
138
+ - open ai training (cdc494a)
139
+ - openai integration in azure (1ca1475)
140
+ - ci (672cea9)
141
+ - refresh ghcr creds (4220c48)
142
+ - cleaned up version (1a0074f)
143
+ - traefik on ghcr and templates (8e31a05)
144
+ - apply fcl (e78911f)
145
+ - demo landscape (dd205fe)
146
+ - smarter login and schema (1af514f)
147
+ - no down before up unless something broke (56b1132)
148
+ - dai, reconcile failed containers (12907fa)
149
+ - reconcile dead container (7da75e4)
150
+ - defensive around storage buckets dir (b98871d)
151
+ - defensive around storage buckets dir (e86e132)
152
+ - gear in for multiarch (bf3fa3e)
153
+ - up autofix (99c7f89)
154
+ - autofix stale containers on up (43c7d0f)
155
+ - shared sessions fix (5de1359)
156
+ - share sessions between ui and tui (8321391)
157
+ - fix chat view display details (e263996)
158
+ - fix chat view display details (9babdda)
159
+ - tui up fixes (86e9f17)
160
+ - fix commands init (442538b)
161
+ - enable k3s profile (b2dcfc8)
162
+ - test up till job creation (656d388)
163
+ - tui fixes (0599779)
164
+ - cleanup (27731f0)
165
+ - train (90bf559)
166
+ - training (f809bf6)
167
+ - training (ba2b836)
168
+ - training (6fc5267)
169
+ - training (4af8ac9)
170
+ - fix build script (bd82836)
171
+ - infra test (5b79815)
172
+ - infra test (3a0ac05)
173
+ - infra test (e5c67b5)
174
+ - tests (ae7b621)
175
+ - tests (c09ae6a)
176
+ - update tui (4784153)
177
+ - training (0a5a330)
178
+ - tui (df4dd4a)
179
+ - pkg builds (4dc9993)
180
+ - also source env for creds (9a17d8f)
181
+ - fcl support (e8a5743)
182
+ - fcl support (8d6b6cd)
183
+ - fcl support (cb76a4a)
184
+ - bump package (df2ee85)
185
+
186
+ # Changelog
187
+
188
+ All notable changes to @meshxdata/fops (Foundation Operator CLI) are documented here.
189
+
190
+ ## [0.1.37] - 2026-03-06
191
+
192
+ - **Azure plugin & VM management:** Major refactor of the Azure plugin with improved VM lifecycle management, non-interactive VM support, better state handling, and expanded test coverage. Includes Azure-specific fixes, token refresh improvements, and enhanced preflight checks.
193
+
194
+ - **Agent & retrieval improvements:** Added Reciprocal Rank Fusion (RRF) to knowledge and skill retrieval, improved Jaccard scoring, expanded agent context, and reduced context size (100k → 10k) via compaction. Various skills updates and documentation improvements for knowledge tools.
195
+
196
+ - **Predictions & ML:** Introduced support for local predictions, added prediction warnings, and early MLflow integration work.
197
+
198
+ - **CLI & TUI improvements:** Fixed multiple TUI/UI issues (status per tab, chat display, compute display), improved `up/down/bootstrap` flows, added plan mode, preserved environment variables with `sudo`, and enhanced sequential startup and container reconciliation.
199
+
200
+ - **Docker & multi-arch:** Added multi-arch Spark Dockerfile, improved multi-arch support, ensured OPA inclusion in compose, and replaced `localhost` with `127.0.0.1` for better compatibility.
201
+
202
+ - **Storage, compute & data reliability:** Improved `compute_run` readiness checks (CSV header validation, table probes), added storage read enhancements, strengthened checksum handling, fixed corrupted Iceberg metadata cleanup, and improved storage defaults and defensive checks.
203
+
204
+ - **Plugins & integrations:** Added TTYD plugin, improved compose/fops file plugin, GitHub login and credential handling fixes, Cloudflare CIDR updates, Kafka reconciliation improvements, and registry enhancements.
205
+
206
+ - **Maintenance & deps:** Dependency bumps (e.g., tar, ajv, hono), test fixes and expansions, repo splits/refactors, packer cleanup, and general stability improvements.
207
+
1
208
  ## [0.1.36] - 2026-03-05
2
209
 
3
210
  - **Azure & Cloud Enhancements**
package/fops.mjs CHANGED
@@ -37,6 +37,19 @@ function loadProjectEnv() {
37
37
  }
38
38
  process.env[key] = value;
39
39
  }
40
+ // Save the directory of the found .env as projectRoot so future runs from any
41
+ // directory (e.g. ~) can find credentials via findProjectRoot() in client.js.
42
+ try {
43
+ const home = process.env.HOME || process.env.USERPROFILE || "";
44
+ const fopsJsonPath = path.join(home, ".fops.json");
45
+ const foundRoot = path.dirname(envPath);
46
+ let cfg = {};
47
+ try { cfg = JSON.parse(fs.readFileSync(fopsJsonPath, "utf8")); } catch {}
48
+ if (cfg.projectRoot !== foundRoot) {
49
+ cfg.projectRoot = foundRoot;
50
+ fs.writeFileSync(fopsJsonPath, JSON.stringify(cfg, null, 2) + "\n");
51
+ }
52
+ } catch { /* non-fatal */ }
40
53
  break; // use the first .env found
41
54
  } catch { /* ignore read errors */ }
42
55
  }
@@ -48,15 +61,32 @@ loadProjectEnv();
48
61
  // whenever ONNX was loaded; otherwise it uses regular process.exit.
49
62
  const _realExit = process.exit.bind(process);
50
63
  function safeExit(code) {
51
- // Defer hard exit when native addons (ONNX, better-sqlite3) were used so C++ destructors
52
- // don't run during exit and trigger mutex/abort.
53
- if (globalThis.__onnxLoaded || globalThis.__onnxDone || globalThis.__sqliteStoreUsed) {
54
- process.exitCode = code;
64
+ if (globalThis.__exitScheduled) return;
65
+ globalThis.__exitScheduled = true;
66
+
67
+ process.exitCode = code ?? 0;
68
+
69
+ if (globalThis.__onnxLoaded) {
70
+ // ONNX C++ destructors call abort() — bypass them with SIGKILL.
71
+ try { fs.closeSync(2); } catch {}
55
72
  globalThis.__onnxDone = true;
56
- globalThis.__sqliteStoreUsed = false;
73
+ if (code === 0 || code == null) {
74
+ // Unref'd: let event loop drain naturally; SIGKILL only if handles linger.
75
+ setTimeout(() => process.kill(process.pid, "SIGKILL"), 400).unref();
76
+ } else {
77
+ // Ref'd: ensure we exit even if event loop is stuck.
78
+ setTimeout(() => process.kill(process.pid, "SIGKILL"), 100);
79
+ }
57
80
  return;
58
81
  }
59
- _realExit(code);
82
+
83
+ // ONNX was never loaded — safe to exit normally without SIGKILL.
84
+ // Use reallyExit to bypass lingering handles (Ink, MCP) without crashing.
85
+ if (typeof process.reallyExit === "function") {
86
+ process.reallyExit(code ?? 0);
87
+ } else {
88
+ _realExit(code ?? 0);
89
+ }
60
90
  }
61
91
  // Intercept all process.exit() calls so plugin code (which can't import safeExit)
62
92
  // also avoids the ONNX C++ destructor abort.
@@ -141,16 +171,9 @@ program.parseAsync().then(() => {
141
171
  // Without this, commands hang because MCP connections / ink refs keep the event loop alive.
142
172
  const code = typeof process.exitCode === "number" ? process.exitCode : 0;
143
173
  setTimeout(() => {
144
- if (globalThis.__onnxDone) return;
174
+ if (globalThis.__exitScheduled) return;
145
175
  safeExit(code);
146
176
  }, 200);
147
-
148
- // Fallback: if ONNX native handles keep the event loop alive after safeExit
149
- // set the exit code, force-terminate via SIGKILL (avoids atexit / C++ destructor crash).
150
- // The timer is unref'd so it won't itself prevent a clean exit.
151
- if (globalThis.__onnxLoaded || globalThis.__onnxDone) {
152
- setTimeout(() => process.kill(process.pid, "SIGKILL"), 300).unref();
153
- }
154
177
  }).catch((err) => {
155
178
  console.error("Command failed:", err?.message || err);
156
179
  if (err?.stack) console.error(err.stack);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@meshxdata/fops",
3
- "version": "0.1.36",
3
+ "version": "0.1.38",
4
4
  "description": "CLI to install and manage data mesh platforms",
5
5
  "keywords": [
6
6
  "fops",
package/src/agent/llm.js CHANGED
@@ -309,6 +309,8 @@ let _responsesApiFallback = false;
309
309
  function isResponsesApiFormatError(err) {
310
310
  if (!err) return false;
311
311
  const status = err.status ?? err.error?.status ?? err.code;
312
+ // 404 = Responses API route doesn't exist on this Azure endpoint/api-version
313
+ if (status === 404 || status === "404") return true;
312
314
  if (status !== 400 && status !== "400") return false;
313
315
  const msg = [err.message, err.error?.message, err.body?.error?.message].filter(Boolean).join(" ");
314
316
  return /Responses API|moved to ['"]input['"]|unknown parameter|'input' is required|'instructions' is not/i.test(msg);
@@ -0,0 +1,92 @@
1
+ import chalk from "chalk";
2
+ import { getInquirer } from "../lazy.js";
3
+
4
+ async function getExeca() {
5
+ const { execa } = await import("execa");
6
+ return execa;
7
+ }
8
+
9
+ async function checkAzInstalled(execa) {
10
+ try {
11
+ await execa("az", ["--version"], { timeout: 10000, reject: true });
12
+ return true;
13
+ } catch {
14
+ return false;
15
+ }
16
+ }
17
+
18
+ async function getAzAccount(execa) {
19
+ try {
20
+ const { stdout } = await execa("az", ["account", "show", "--output", "json"], { timeout: 15000 });
21
+ return JSON.parse(stdout);
22
+ } catch {
23
+ return null;
24
+ }
25
+ }
26
+
27
+ export async function runAzureLogin() {
28
+ const execa = await getExeca();
29
+
30
+ const installed = await checkAzInstalled(execa);
31
+ if (!installed) {
32
+ console.log(chalk.red("\n Azure CLI (az) is not installed."));
33
+ console.log(chalk.dim(" Install it from: https://docs.microsoft.com/en-us/cli/azure/install-azure-cli"));
34
+ if (process.platform === "darwin") {
35
+ console.log(chalk.dim(" macOS: brew install azure-cli"));
36
+ } else if (process.platform === "linux") {
37
+ console.log(chalk.dim(" Linux: curl -sL https://aka.ms/InstallAzureCLIDeb | sudo bash"));
38
+ }
39
+ console.log("");
40
+ return false;
41
+ }
42
+
43
+ const account = await getAzAccount(execa);
44
+ if (account) {
45
+ console.log("");
46
+ console.log(chalk.bold.cyan(" Azure CLI — Current Session"));
47
+ console.log("");
48
+ console.log(chalk.green(` ✓ Logged in as: ${account.user?.name || account.user?.type || "unknown"}`));
49
+ console.log(chalk.dim(` Subscription: ${account.name} (${account.id})`));
50
+ console.log(chalk.dim(` Tenant: ${account.tenantId}`));
51
+ console.log("");
52
+
53
+ const { relogin } = await (await getInquirer()).prompt([{
54
+ type: "confirm",
55
+ name: "relogin",
56
+ message: "Re-authenticate (switch account or refresh session)?",
57
+ default: false,
58
+ }]);
59
+
60
+ if (!relogin) {
61
+ console.log(chalk.dim("Keeping existing Azure session.\n"));
62
+ return true;
63
+ }
64
+
65
+ await execa("az", ["logout"], { reject: false, timeout: 10000 });
66
+ }
67
+
68
+ console.log("");
69
+ console.log(chalk.bold.cyan(" Azure Login"));
70
+ console.log(chalk.dim(" This will open your browser for interactive sign-in."));
71
+ console.log("");
72
+
73
+ const { exitCode } = await execa("az", ["login"], {
74
+ stdio: "inherit",
75
+ timeout: 300000,
76
+ reject: false,
77
+ });
78
+
79
+ if (exitCode !== 0) {
80
+ console.log(chalk.red("\n Azure login failed or was cancelled.\n"));
81
+ return false;
82
+ }
83
+
84
+ const newAccount = await getAzAccount(execa);
85
+ if (newAccount) {
86
+ console.log(chalk.green(`\n ✓ Azure login successful!`));
87
+ console.log(chalk.dim(` Logged in as: ${newAccount.user?.name || newAccount.user?.type}`));
88
+ console.log(chalk.dim(` Subscription: ${newAccount.name} (${newAccount.id})\n`));
89
+ }
90
+
91
+ return true;
92
+ }
@@ -0,0 +1,125 @@
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 { openBrowser } from "./login.js";
6
+ import { getInquirer } from "../lazy.js";
7
+
8
+ const CF_TOKENS_URL = "https://dash.cloudflare.com/profile/api-tokens";
9
+ const FOPS_CONFIG_PATH = path.join(os.homedir(), ".fops.json");
10
+
11
+ function readFopsConfig() {
12
+ try {
13
+ if (fs.existsSync(FOPS_CONFIG_PATH)) {
14
+ return JSON.parse(fs.readFileSync(FOPS_CONFIG_PATH, "utf8"));
15
+ }
16
+ } catch {}
17
+ return {};
18
+ }
19
+
20
+ function saveFopsConfig(config) {
21
+ fs.writeFileSync(FOPS_CONFIG_PATH, JSON.stringify(config, null, 2) + "\n", { mode: 0o600 });
22
+ }
23
+
24
+ export function resolveCloudflareApiToken() {
25
+ if (process.env.CLOUDFLARE_API_TOKEN?.trim()) return process.env.CLOUDFLARE_API_TOKEN.trim();
26
+ if (process.env.CF_API_TOKEN?.trim()) return process.env.CF_API_TOKEN.trim();
27
+ try {
28
+ const config = readFopsConfig();
29
+ const token = config.cloudflare?.apiToken?.trim();
30
+ if (token) return token;
31
+ } catch {}
32
+ return null;
33
+ }
34
+
35
+ function saveCloudflareToken(token) {
36
+ const config = readFopsConfig();
37
+ config.cloudflare = { ...config.cloudflare, apiToken: token.trim() };
38
+ saveFopsConfig(config);
39
+ }
40
+
41
+ async function validateCloudflareToken(token) {
42
+ try {
43
+ const res = await fetch("https://api.cloudflare.com/client/v4/user/tokens/verify", {
44
+ headers: { Authorization: `Bearer ${token}` },
45
+ });
46
+ const json = await res.json();
47
+ if (json.success) {
48
+ return { valid: true, status: json.result?.status };
49
+ }
50
+ const msg = (json.errors || []).map((e) => e.message).join("; ");
51
+ return { valid: false, error: msg || `Cloudflare API returned ${res.status}.` };
52
+ } catch (err) {
53
+ return { valid: false, error: `Could not reach Cloudflare API: ${err.message}` };
54
+ }
55
+ }
56
+
57
+ export async function runCloudflareLogin() {
58
+ const existing = resolveCloudflareApiToken();
59
+ if (existing) {
60
+ const masked = existing.slice(0, 8) + "..." + existing.slice(-4);
61
+ const { overwrite } = await (await getInquirer()).prompt([{
62
+ type: "confirm",
63
+ name: "overwrite",
64
+ message: `Already have a Cloudflare API token (${masked}). Replace it?`,
65
+ default: false,
66
+ }]);
67
+ if (!overwrite) {
68
+ console.log(chalk.dim("Keeping existing Cloudflare token."));
69
+ return true;
70
+ }
71
+ }
72
+
73
+ console.log("");
74
+ console.log(chalk.bold.cyan(" Cloudflare API Token Setup"));
75
+ console.log("");
76
+ console.log(chalk.white(" To connect Foundation to Cloudflare, you need an API token."));
77
+ console.log(chalk.white(" Here's how to get one:\n"));
78
+ console.log(chalk.dim(" 1. Go to ") + chalk.cyan(CF_TOKENS_URL));
79
+ console.log(chalk.dim(" 2. Click ") + chalk.white('"Create Token"'));
80
+ console.log(chalk.dim(" 3. Use the ") + chalk.white('"Edit zone DNS"') + chalk.dim(" template (or create a custom token)"));
81
+ console.log(chalk.dim(" 4. Copy the token and paste it below"));
82
+ console.log("");
83
+
84
+ const { openIt } = await (await getInquirer()).prompt([{
85
+ type: "confirm",
86
+ name: "openIt",
87
+ message: "Open Cloudflare API tokens page in your browser?",
88
+ default: true,
89
+ }]);
90
+
91
+ if (openIt) {
92
+ const opened = openBrowser(CF_TOKENS_URL);
93
+ if (!opened) {
94
+ console.log(chalk.yellow("\n Could not open browser. Visit: " + CF_TOKENS_URL + "\n"));
95
+ } else {
96
+ console.log("");
97
+ }
98
+ }
99
+
100
+ const { token } = await (await getInquirer()).prompt([{
101
+ type: "password",
102
+ name: "token",
103
+ message: "Paste your Cloudflare API token:",
104
+ mask: "*",
105
+ validate: (v) => {
106
+ if (!v?.trim()) return "API token is required.";
107
+ return true;
108
+ },
109
+ }]);
110
+
111
+ console.log(chalk.dim("\n Validating token..."));
112
+ const result = await validateCloudflareToken(token.trim());
113
+
114
+ if (!result.valid) {
115
+ console.log(chalk.red(`\n ${result.error}`));
116
+ console.log(chalk.dim(" Check the token and try again with: fops login cloudflare\n"));
117
+ return false;
118
+ }
119
+
120
+ saveCloudflareToken(token);
121
+ console.log(chalk.green(`\n Cloudflare login successful!`));
122
+ if (result.status) console.log(chalk.dim(` Token status: ${result.status}`));
123
+ console.log(chalk.dim(" Token saved to ~/.fops.json\n"));
124
+ return true;
125
+ }
package/src/auth/index.js CHANGED
@@ -4,3 +4,5 @@ export { authHelp, offerClaudeLogin, runLogin } from "./login.js";
4
4
  export { runOAuthLogin } from "./oauth.js";
5
5
  export { runCodaLogin, resolveCodaApiToken } from "./coda.js";
6
6
  export { runLinearLogin, resolveLinearApiKey } from "./linear.js";
7
+ export { runCloudflareLogin, resolveCloudflareApiToken } from "./cloudflare.js";
8
+ export { runAzureLogin } from "./azure.js";
@@ -15,20 +15,24 @@ export function registerCommands(program, registry) {
15
15
 
16
16
  program
17
17
  .command("login")
18
- .description("Authenticate with services (Claude, Coda, Linear)")
19
- .argument("[service]", "Service to login to: claude (default), coda, or linear")
18
+ .description("Authenticate with services (Claude, Coda, Linear, Cloudflare, Azure)")
19
+ .argument("[service]", "Service to login to: claude (default), coda, linear, cloudflare, or azure")
20
20
  .option("--no-browser", "Paste API key in terminal instead of OAuth")
21
21
  .action(async (service, opts) => {
22
- const { runLogin, runCodaLogin, runLinearLogin } = await import("../auth/index.js");
22
+ const { runLogin, runCodaLogin, runLinearLogin, runCloudflareLogin, runAzureLogin } = await import("../auth/index.js");
23
23
  const target = (service || "claude").toLowerCase();
24
24
  if (target === "coda") {
25
25
  await runCodaLogin();
26
26
  } else if (target === "linear") {
27
27
  await runLinearLogin();
28
+ } else if (target === "cloudflare" || target === "cf") {
29
+ await runCloudflareLogin();
30
+ } else if (target === "azure" || target === "az") {
31
+ await runAzureLogin();
28
32
  } else if (target === "claude") {
29
33
  await runLogin({ browser: opts.browser });
30
34
  } else {
31
- console.error(chalk.red(`Unknown service "${target}". Use: claude, coda, linear`));
35
+ console.error(chalk.red(`Unknown service "${target}". Use: claude, coda, linear, cloudflare, azure`));
32
36
  process.exit(1);
33
37
  }
34
38
  });
@@ -527,21 +527,21 @@ async function runUp(program, registry, opts) {
527
527
 
528
528
  if (opts.url) {
529
529
  publicUrl = opts.url.replace(/\/+$/, "");
530
- if (/^FOUNDATION_PUBLIC_URL=/m.test(envContent)) {
531
- envContent = envContent.replace(/^FOUNDATION_PUBLIC_URL=.*/m, `FOUNDATION_PUBLIC_URL=${publicUrl}`);
532
- } else {
530
+ // Only write if not already set in .env — leave the repo's value untouched
531
+ if (!/^FOUNDATION_PUBLIC_URL=/m.test(envContent)) {
533
532
  envContent = envContent.trimEnd() + `\nFOUNDATION_PUBLIC_URL=${publicUrl}\n`;
533
+ fs.writeFileSync(envPath, envContent);
534
+ console.log(chalk.dim(` FOUNDATION_PUBLIC_URL=${publicUrl} written to .env`));
534
535
  }
535
- fs.writeFileSync(envPath, envContent);
536
- console.log(chalk.dim(` FOUNDATION_PUBLIC_URL=${publicUrl} written to .env`));
537
536
  } else {
538
- // Local: ensure compose uses localhost and no remote URL / no traefik
539
- if (/^FOUNDATION_PUBLIC_URL=/m.test(envContent)) {
540
- envContent = envContent.replace(/^FOUNDATION_PUBLIC_URL=.*/m, `FOUNDATION_PUBLIC_URL=${localBaseUrl}`);
541
- } else {
537
+ // Local: only inject localhost fallback if not already set
538
+ if (!/^FOUNDATION_PUBLIC_URL=/m.test(envContent)) {
542
539
  envContent = envContent.trimEnd() + `\nFOUNDATION_PUBLIC_URL=${localBaseUrl}\n`;
540
+ fs.writeFileSync(envPath, envContent);
543
541
  }
544
- fs.writeFileSync(envPath, envContent);
542
+ // Read the actual value from .env for profile resolution below
543
+ const existingUrl = envContent.match(/^FOUNDATION_PUBLIC_URL=(.+)$/m)?.[1]?.trim();
544
+ if (existingUrl && existingUrl !== localBaseUrl) publicUrl = existingUrl;
545
545
  }
546
546
 
547
547
  // Resolve profiles: local up = no traefik; --url with 443 = traefik
@@ -1284,7 +1284,28 @@ async function runUp(program, registry, opts) {
1284
1284
  const forwardOut = createBufferWritable("out");
1285
1285
  const forwardErr = createBufferWritable("err");
1286
1286
 
1287
+ // Detect env misalignment: if FOUNDATION_PUBLIC_URL in .env differs from what's
1288
+ // running in containers, force-recreate so services pick up the new value.
1289
+ let forceRecreate = false;
1290
+ try {
1291
+ const envPublicUrl = (envContent.match(/^FOUNDATION_PUBLIC_URL=(.+)$/m)?.[1] || "").trim();
1292
+ if (envPublicUrl) {
1293
+ const { stdout: inspectOut } = await execa("docker", [
1294
+ "inspect", "--format", "{{range .Config.Env}}{{println .}}{{end}}",
1295
+ "foundation-compose-foundation-frontend-1",
1296
+ ], { reject: false, cwd: root });
1297
+ const runningUrl = (inspectOut || "").split("\n")
1298
+ .find(l => l.startsWith("FOUNDATION_PUBLIC_URL="))
1299
+ ?.slice("FOUNDATION_PUBLIC_URL=".length).trim();
1300
+ if (runningUrl && runningUrl !== envPublicUrl) {
1301
+ console.error(chalk.dim(` ↻ FOUNDATION_PUBLIC_URL mismatch (${runningUrl} → ${envPublicUrl}) — recreating affected services`));
1302
+ forceRecreate = true;
1303
+ }
1304
+ }
1305
+ } catch { /* best-effort */ }
1306
+
1287
1307
  const upArgs = ["compose", ...effectiveProfileArgs, "--progress", "plain", "up", "-d", "--remove-orphans"];
1308
+ if (forceRecreate) upArgs.push("--force-recreate");
1288
1309
  if (serviceList.length > 0) upArgs.push(...serviceList);
1289
1310
  const upProc = execa("docker", upArgs, {
1290
1311
  cwd: root,