@meshxdata/fops 0.1.49 → 0.1.50
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/CHANGELOG.md +182 -0
- package/package.json +1 -1
- package/src/plugins/bundled/fops-plugin-azure/lib/azure-aks-core.js +347 -6
- package/src/plugins/bundled/fops-plugin-azure/lib/azure-aks-data-bootstrap.js +421 -0
- package/src/plugins/bundled/fops-plugin-azure/lib/azure-aks-flux.js +5 -179
- package/src/plugins/bundled/fops-plugin-azure/lib/azure-aks-naming.js +14 -4
- package/src/plugins/bundled/fops-plugin-azure/lib/azure-aks-postgres.js +171 -4
- package/src/plugins/bundled/fops-plugin-azure/lib/azure-aks-storage.js +303 -8
- package/src/plugins/bundled/fops-plugin-azure/lib/azure-aks.js +2 -0
- package/src/plugins/bundled/fops-plugin-azure/lib/azure-auth.js +1 -1
- package/src/plugins/bundled/fops-plugin-azure/lib/azure-fleet-swarm.js +936 -0
- package/src/plugins/bundled/fops-plugin-azure/lib/azure-fleet.js +10 -918
- package/src/plugins/bundled/fops-plugin-azure/lib/azure-helpers.js +5 -0
- package/src/plugins/bundled/fops-plugin-azure/lib/azure-keyvault-keys.js +413 -0
- package/src/plugins/bundled/fops-plugin-azure/lib/azure-keyvault.js +14 -399
- package/src/plugins/bundled/fops-plugin-azure/lib/azure-ops-config.js +754 -0
- package/src/plugins/bundled/fops-plugin-azure/lib/azure-ops-knock.js +527 -0
- package/src/plugins/bundled/fops-plugin-azure/lib/azure-ops-ssh.js +427 -0
- package/src/plugins/bundled/fops-plugin-azure/lib/azure-ops.js +99 -1686
- package/src/plugins/bundled/fops-plugin-azure/lib/azure-provision-health.js +279 -0
- package/src/plugins/bundled/fops-plugin-azure/lib/azure-provision-init.js +186 -0
- package/src/plugins/bundled/fops-plugin-azure/lib/azure-provision.js +66 -444
- package/src/plugins/bundled/fops-plugin-azure/lib/azure-results.js +11 -0
- package/src/plugins/bundled/fops-plugin-azure/lib/azure-vm-lifecycle.js +5 -540
- package/src/plugins/bundled/fops-plugin-azure/lib/azure-vm-terraform.js +544 -0
- package/src/plugins/bundled/fops-plugin-azure/lib/commands/infra-cmds.js +75 -3
- package/src/plugins/bundled/fops-plugin-azure/lib/commands/test-cmds.js +227 -11
- package/src/plugins/bundled/fops-plugin-azure/lib/commands/vm-cmds.js +2 -1
- package/src/plugins/bundled/fops-plugin-azure/lib/pytest-parse.js +21 -0
- package/src/plugins/bundled/fops-plugin-foundation/index.js +309 -44
|
@@ -984,15 +984,50 @@ app.run()
|
|
|
984
984
|
.command("tray")
|
|
985
985
|
.description("Run a macOS menu bar tray for the Foundation stack")
|
|
986
986
|
.option("--url <url>", "Override the backend API URL")
|
|
987
|
+
.option("--force", "Kill existing tray and start a new one")
|
|
987
988
|
.action(async (opts) => {
|
|
988
|
-
const { spawn } = await import("node:child_process");
|
|
989
|
-
const { writeFileSync, existsSync, realpathSync, readFileSync } = await import("node:fs");
|
|
989
|
+
const { spawn, execSync } = await import("node:child_process");
|
|
990
|
+
const { writeFileSync, existsSync, realpathSync, readFileSync, mkdirSync, unlinkSync } = await import("node:fs");
|
|
990
991
|
const { tmpdir, homedir } = await import("node:os");
|
|
991
992
|
const { join, dirname } = await import("node:path");
|
|
992
993
|
const { findComposeRoot } = await import("./lib/tools-write.js");
|
|
993
994
|
|
|
994
995
|
const composeRoot = program._fopsRoot || findComposeRoot() || "";
|
|
995
996
|
|
|
997
|
+
// ── Singleton check ─────────────────────────────────────────────────────
|
|
998
|
+
const fopsDir = join(homedir(), ".fops");
|
|
999
|
+
const pidFile = join(fopsDir, ".tray.pid");
|
|
1000
|
+
|
|
1001
|
+
const isProcessRunning = (pid) => {
|
|
1002
|
+
try {
|
|
1003
|
+
process.kill(pid, 0);
|
|
1004
|
+
return true;
|
|
1005
|
+
} catch {
|
|
1006
|
+
return false;
|
|
1007
|
+
}
|
|
1008
|
+
};
|
|
1009
|
+
|
|
1010
|
+
if (existsSync(pidFile)) {
|
|
1011
|
+
try {
|
|
1012
|
+
const existingPid = parseInt(readFileSync(pidFile, "utf8").trim(), 10);
|
|
1013
|
+
if (existingPid && isProcessRunning(existingPid)) {
|
|
1014
|
+
if (opts.force) {
|
|
1015
|
+
console.log(ACCENT(" Killing existing tray process..."));
|
|
1016
|
+
try { process.kill(existingPid, "SIGTERM"); } catch { /* ignore */ }
|
|
1017
|
+
// Wait a bit for process to terminate
|
|
1018
|
+
await new Promise((r) => setTimeout(r, 500));
|
|
1019
|
+
} else {
|
|
1020
|
+
console.log(ACCENT(" Foundation tray is already running (PID: " + existingPid + ")"));
|
|
1021
|
+
console.log(ACCENT(" Use --force to restart it"));
|
|
1022
|
+
return;
|
|
1023
|
+
}
|
|
1024
|
+
}
|
|
1025
|
+
} catch { /* ignore invalid pid file */ }
|
|
1026
|
+
}
|
|
1027
|
+
|
|
1028
|
+
// Ensure .fops directory exists
|
|
1029
|
+
if (!existsSync(fopsDir)) mkdirSync(fopsDir, { recursive: true });
|
|
1030
|
+
|
|
996
1031
|
let apiUrl = opts.url || process.env.FOPS_API_URL || "http://127.0.0.1:9001";
|
|
997
1032
|
|
|
998
1033
|
// Current fops version — resolve from the running binary's location
|
|
@@ -1021,19 +1056,27 @@ app.run()
|
|
|
1021
1056
|
if (existsSync(src)) iconPath = src;
|
|
1022
1057
|
} catch { /* icon optional */ }
|
|
1023
1058
|
|
|
1024
|
-
//
|
|
1025
|
-
|
|
1059
|
+
// Plugin discovery paths - tray will discover plugins dynamically
|
|
1060
|
+
const globalPluginsDir = join(homedir(), ".fops", "plugins");
|
|
1061
|
+
let npmPluginsDir = "";
|
|
1026
1062
|
try {
|
|
1027
|
-
const
|
|
1028
|
-
const
|
|
1029
|
-
|
|
1030
|
-
const pluginList = candidates.map((c) => {
|
|
1031
|
-
const manifest = validateManifest(c.path);
|
|
1032
|
-
return { id: c.id, name: manifest?.name || c.id, source: c.source };
|
|
1033
|
-
}).filter((p) => p.id);
|
|
1034
|
-
fopsPluginsJson = JSON.stringify(pluginList);
|
|
1063
|
+
const pluginsNodeModules = join(homedir(), ".fops", "plugins", "node_modules");
|
|
1064
|
+
const fopsRoot = dirname(realpathSync(pluginsNodeModules));
|
|
1065
|
+
npmPluginsDir = join(fopsRoot, "node_modules");
|
|
1035
1066
|
} catch { /* optional */ }
|
|
1036
1067
|
|
|
1068
|
+
// Common environment for tray processes
|
|
1069
|
+
const trayEnv = {
|
|
1070
|
+
...process.env,
|
|
1071
|
+
FOUNDATION_API_URL: apiUrl,
|
|
1072
|
+
FOUNDATION_URL: "http://127.0.0.1:3002",
|
|
1073
|
+
FOUNDATION_ICON: iconPath,
|
|
1074
|
+
FOUNDATION_COMPOSE_ROOT: composeRoot,
|
|
1075
|
+
FOPS_VERSION: fopsVersion,
|
|
1076
|
+
FOPS_GLOBAL_PLUGINS_DIR: globalPluginsDir,
|
|
1077
|
+
FOPS_NPM_PLUGINS_DIR: npmPluginsDir,
|
|
1078
|
+
};
|
|
1079
|
+
|
|
1037
1080
|
// ── Windows tray (PowerShell NotifyIcon) ─────────────────────────────
|
|
1038
1081
|
if (process.platform === "win32") {
|
|
1039
1082
|
const ps1Tray = join(tmpdir(), "fops-tray.ps1");
|
|
@@ -1044,8 +1087,49 @@ Add-Type -AssemblyName System.Drawing
|
|
|
1044
1087
|
$script:apiUrl = $env:FOUNDATION_API_URL
|
|
1045
1088
|
$script:composeRoot = $env:FOUNDATION_COMPOSE_ROOT
|
|
1046
1089
|
$script:foundationUrl = if ($env:FOUNDATION_URL) { $env:FOUNDATION_URL } else { "http://127.0.0.1:3002" }
|
|
1047
|
-
$script:
|
|
1048
|
-
|
|
1090
|
+
$script:globalPluginsDir = $env:FOPS_GLOBAL_PLUGINS_DIR
|
|
1091
|
+
$script:npmPluginsDir = $env:FOPS_NPM_PLUGINS_DIR
|
|
1092
|
+
|
|
1093
|
+
function Discover-Plugins {
|
|
1094
|
+
$plugins = @()
|
|
1095
|
+
# Global plugins: ~/.fops/plugins/<name>/
|
|
1096
|
+
if ($script:globalPluginsDir -and (Test-Path $script:globalPluginsDir)) {
|
|
1097
|
+
Get-ChildItem -Path $script:globalPluginsDir -Directory -ErrorAction SilentlyContinue | ForEach-Object {
|
|
1098
|
+
$manifestPath = Join-Path $_.FullName "fops.plugin.json"
|
|
1099
|
+
if (Test-Path $manifestPath) {
|
|
1100
|
+
try {
|
|
1101
|
+
$manifest = Get-Content $manifestPath -Raw | ConvertFrom-Json
|
|
1102
|
+
$plugins += [PSCustomObject]@{
|
|
1103
|
+
id = $_.Name
|
|
1104
|
+
name = if ($manifest.name) { $manifest.name } else { $_.Name }
|
|
1105
|
+
source = "global"
|
|
1106
|
+
}
|
|
1107
|
+
} catch {
|
|
1108
|
+
$plugins += [PSCustomObject]@{ id = $_.Name; name = $_.Name; source = "global" }
|
|
1109
|
+
}
|
|
1110
|
+
}
|
|
1111
|
+
}
|
|
1112
|
+
}
|
|
1113
|
+
# NPM plugins: node_modules/fops-plugin-*
|
|
1114
|
+
if ($script:npmPluginsDir -and (Test-Path $script:npmPluginsDir)) {
|
|
1115
|
+
Get-ChildItem -Path $script:npmPluginsDir -Directory -Filter "fops-plugin-*" -ErrorAction SilentlyContinue | ForEach-Object {
|
|
1116
|
+
$manifestPath = Join-Path $_.FullName "fops.plugin.json"
|
|
1117
|
+
if (Test-Path $manifestPath) {
|
|
1118
|
+
try {
|
|
1119
|
+
$manifest = Get-Content $manifestPath -Raw | ConvertFrom-Json
|
|
1120
|
+
$plugins += [PSCustomObject]@{
|
|
1121
|
+
id = $_.Name
|
|
1122
|
+
name = if ($manifest.name) { $manifest.name } else { $_.Name }
|
|
1123
|
+
source = "npm"
|
|
1124
|
+
}
|
|
1125
|
+
} catch {
|
|
1126
|
+
$plugins += [PSCustomObject]@{ id = $_.Name; name = $_.Name; source = "npm" }
|
|
1127
|
+
}
|
|
1128
|
+
}
|
|
1129
|
+
}
|
|
1130
|
+
}
|
|
1131
|
+
return $plugins
|
|
1132
|
+
}
|
|
1049
1133
|
|
|
1050
1134
|
function Get-Docker {
|
|
1051
1135
|
foreach ($p in @("C:\\Program Files\\Docker\\Docker\\resources\\bin\\docker.exe")) {
|
|
@@ -1156,11 +1240,12 @@ $menu.Items.Add($composeItem) | Out-Null
|
|
|
1156
1240
|
$pluginsItem = [System.Windows.Forms.ToolStripMenuItem]::new("Plugins")
|
|
1157
1241
|
$pluginsItem.add_DropDownOpening({
|
|
1158
1242
|
$pluginsItem.DropDownItems.Clear()
|
|
1159
|
-
|
|
1243
|
+
$currentPlugins = Discover-Plugins
|
|
1244
|
+
if ($currentPlugins.Count -eq 0) {
|
|
1160
1245
|
$n = [System.Windows.Forms.ToolStripMenuItem]::new("No plugins installed")
|
|
1161
1246
|
$n.Enabled = $false; $pluginsItem.DropDownItems.Add($n) | Out-Null; return
|
|
1162
1247
|
}
|
|
1163
|
-
foreach ($plugin in $
|
|
1248
|
+
foreach ($plugin in $currentPlugins) {
|
|
1164
1249
|
$enabled = Read-PluginEnabled $plugin.id
|
|
1165
1250
|
$prefix = if ($enabled) { [char]0x2713 + " " } else { " " }
|
|
1166
1251
|
$pItem = [System.Windows.Forms.ToolStripMenuItem]::new($prefix + $plugin.name)
|
|
@@ -1235,6 +1320,7 @@ $tray.Visible = $false
|
|
|
1235
1320
|
env: trayEnv,
|
|
1236
1321
|
windowsHide: true,
|
|
1237
1322
|
});
|
|
1323
|
+
writeFileSync(pidFile, String(winChild.pid));
|
|
1238
1324
|
winChild.unref();
|
|
1239
1325
|
return;
|
|
1240
1326
|
}
|
|
@@ -1266,6 +1352,8 @@ func git() -> String {
|
|
|
1266
1352
|
let composeRoot = ProcessInfo.processInfo.environment["FOUNDATION_COMPOSE_ROOT"] ?? ""
|
|
1267
1353
|
let iconPath = ProcessInfo.processInfo.environment["FOUNDATION_ICON"] ?? ""
|
|
1268
1354
|
let fopsVersion = ProcessInfo.processInfo.environment["FOPS_VERSION"] ?? "0.0.0"
|
|
1355
|
+
let globalPluginsDir = ProcessInfo.processInfo.environment["FOPS_GLOBAL_PLUGINS_DIR"] ?? ""
|
|
1356
|
+
let npmPluginsDir = ProcessInfo.processInfo.environment["FOPS_NPM_PLUGINS_DIR"] ?? ""
|
|
1269
1357
|
|
|
1270
1358
|
// ── Plugin registry ───────────────────────────────────────────────────────────
|
|
1271
1359
|
|
|
@@ -1275,15 +1363,52 @@ struct PluginEntry {
|
|
|
1275
1363
|
let source: String
|
|
1276
1364
|
}
|
|
1277
1365
|
|
|
1278
|
-
|
|
1279
|
-
|
|
1280
|
-
|
|
1281
|
-
|
|
1282
|
-
|
|
1283
|
-
|
|
1284
|
-
|
|
1366
|
+
func discoverPlugins() -> [PluginEntry] {
|
|
1367
|
+
var plugins: [PluginEntry] = []
|
|
1368
|
+
let fm = FileManager.default
|
|
1369
|
+
|
|
1370
|
+
// Global plugins: ~/.fops/plugins/<name>/
|
|
1371
|
+
if !globalPluginsDir.isEmpty, fm.fileExists(atPath: globalPluginsDir) {
|
|
1372
|
+
if let dirs = try? fm.contentsOfDirectory(atPath: globalPluginsDir) {
|
|
1373
|
+
for dir in dirs {
|
|
1374
|
+
let pluginPath = (globalPluginsDir as NSString).appendingPathComponent(dir)
|
|
1375
|
+
let manifestPath = (pluginPath as NSString).appendingPathComponent("fops.plugin.json")
|
|
1376
|
+
var isDir: ObjCBool = false
|
|
1377
|
+
guard fm.fileExists(atPath: pluginPath, isDirectory: &isDir), isDir.boolValue,
|
|
1378
|
+
fm.fileExists(atPath: manifestPath) else { continue }
|
|
1379
|
+
var name = dir
|
|
1380
|
+
if let data = try? Data(contentsOf: URL(fileURLWithPath: manifestPath)),
|
|
1381
|
+
let json = try? JSONSerialization.jsonObject(with: data) as? [String: Any],
|
|
1382
|
+
let manifestName = json["name"] as? String {
|
|
1383
|
+
name = manifestName
|
|
1384
|
+
}
|
|
1385
|
+
plugins.append(PluginEntry(id: dir, name: name, source: "global"))
|
|
1386
|
+
}
|
|
1387
|
+
}
|
|
1388
|
+
}
|
|
1389
|
+
|
|
1390
|
+
// NPM plugins: node_modules/fops-plugin-*
|
|
1391
|
+
if !npmPluginsDir.isEmpty, fm.fileExists(atPath: npmPluginsDir) {
|
|
1392
|
+
if let dirs = try? fm.contentsOfDirectory(atPath: npmPluginsDir) {
|
|
1393
|
+
for dir in dirs where dir.hasPrefix("fops-plugin-") {
|
|
1394
|
+
let pluginPath = (npmPluginsDir as NSString).appendingPathComponent(dir)
|
|
1395
|
+
let manifestPath = (pluginPath as NSString).appendingPathComponent("fops.plugin.json")
|
|
1396
|
+
var isDir: ObjCBool = false
|
|
1397
|
+
guard fm.fileExists(atPath: pluginPath, isDirectory: &isDir), isDir.boolValue,
|
|
1398
|
+
fm.fileExists(atPath: manifestPath) else { continue }
|
|
1399
|
+
var name = dir
|
|
1400
|
+
if let data = try? Data(contentsOf: URL(fileURLWithPath: manifestPath)),
|
|
1401
|
+
let json = try? JSONSerialization.jsonObject(with: data) as? [String: Any],
|
|
1402
|
+
let manifestName = json["name"] as? String {
|
|
1403
|
+
name = manifestName
|
|
1404
|
+
}
|
|
1405
|
+
plugins.append(PluginEntry(id: dir, name: name, source: "npm"))
|
|
1406
|
+
}
|
|
1407
|
+
}
|
|
1285
1408
|
}
|
|
1286
|
-
|
|
1409
|
+
|
|
1410
|
+
return plugins
|
|
1411
|
+
}
|
|
1287
1412
|
|
|
1288
1413
|
func readPluginEnabled(_ id: String) -> Bool {
|
|
1289
1414
|
let home = ProcessInfo.processInfo.environment["HOME"] ?? ""
|
|
@@ -1316,6 +1441,11 @@ func compose(_ args: [String], capture: Bool = false, done: ((String, Bool) -> V
|
|
|
1316
1441
|
run(docker(), ["compose"] + args, capture: capture, done: done)
|
|
1317
1442
|
}
|
|
1318
1443
|
|
|
1444
|
+
func fops(_ args: [String], capture: Bool = false, done: ((String, Bool) -> Void)? = nil) {
|
|
1445
|
+
let fopsPath = "/opt/homebrew/bin/fops"
|
|
1446
|
+
run(fopsPath, args, cwd: nil, capture: capture, done: done)
|
|
1447
|
+
}
|
|
1448
|
+
|
|
1319
1449
|
// ── Service discovery ─────────────────────────────────────────────────────────
|
|
1320
1450
|
|
|
1321
1451
|
struct Service {
|
|
@@ -1439,6 +1569,12 @@ let pluginsMenu = NSMenu(title: "Plugins")
|
|
|
1439
1569
|
pluginsItem.submenu = pluginsMenu
|
|
1440
1570
|
menu.addItem(pluginsItem)
|
|
1441
1571
|
|
|
1572
|
+
// Azure > VMs / AKS (populated dynamically)
|
|
1573
|
+
let azureItem = NSMenuItem(title: "Azure", action: nil, keyEquivalent: "")
|
|
1574
|
+
let azureMenu = NSMenu(title: "Azure")
|
|
1575
|
+
azureItem.submenu = azureMenu
|
|
1576
|
+
menu.addItem(azureItem)
|
|
1577
|
+
|
|
1442
1578
|
menu.addItem(NSMenuItem.separator())
|
|
1443
1579
|
|
|
1444
1580
|
let openItem = NSMenuItem(title: "Open Foundation…", action: #selector(AppDelegate.openBrowser), keyEquivalent: "o")
|
|
@@ -1474,6 +1610,7 @@ class AppDelegate: NSObject, NSApplicationDelegate, NSMenuDelegate {
|
|
|
1474
1610
|
func applicationDidFinishLaunching(_ n: Notification) {
|
|
1475
1611
|
composeMenu.delegate = self
|
|
1476
1612
|
pluginsMenu.delegate = pluginsMenuDelegate
|
|
1613
|
+
azureMenu.delegate = self
|
|
1477
1614
|
refresh()
|
|
1478
1615
|
let pollTimer = Timer(timeInterval: 8, repeats: true) { _ in self.refresh() }
|
|
1479
1616
|
RunLoop.main.add(pollTimer, forMode: .common)
|
|
@@ -1536,34 +1673,73 @@ class AppDelegate: NSObject, NSApplicationDelegate, NSMenuDelegate {
|
|
|
1536
1673
|
RunLoop.main.add(pulse, forMode: .common)
|
|
1537
1674
|
}
|
|
1538
1675
|
|
|
1676
|
+
var updateSpinnerTimer: Timer?
|
|
1677
|
+
var updateSpinnerFrame = 0
|
|
1678
|
+
|
|
1679
|
+
func startUpdateSpinner(item: NSMenuItem, label: String) {
|
|
1680
|
+
item.isEnabled = false
|
|
1681
|
+
updateSpinnerFrame = 0
|
|
1682
|
+
updateSpinnerTimer?.invalidate()
|
|
1683
|
+
let t = Timer(timeInterval: 0.1, repeats: true) { _ in
|
|
1684
|
+
let frame = self.spinnerFrames[self.updateSpinnerFrame % self.spinnerFrames.count]
|
|
1685
|
+
item.title = "\\(frame) \\(label)"
|
|
1686
|
+
self.updateSpinnerFrame += 1
|
|
1687
|
+
}
|
|
1688
|
+
RunLoop.main.add(t, forMode: .common)
|
|
1689
|
+
updateSpinnerTimer = t
|
|
1690
|
+
}
|
|
1691
|
+
|
|
1692
|
+
func stopUpdateSpinner() {
|
|
1693
|
+
updateSpinnerTimer?.invalidate()
|
|
1694
|
+
updateSpinnerTimer = nil
|
|
1695
|
+
}
|
|
1696
|
+
|
|
1539
1697
|
@objc func checkForUpdateManual() {
|
|
1540
|
-
checkUpdateItem
|
|
1541
|
-
checkUpdateItem.isEnabled = false
|
|
1698
|
+
startUpdateSpinner(item: checkUpdateItem, label: "Checking for updates…")
|
|
1542
1699
|
checkForUpdate()
|
|
1543
1700
|
DispatchQueue.main.asyncAfter(deadline: .now() + 3) {
|
|
1701
|
+
self.stopUpdateSpinner()
|
|
1544
1702
|
checkUpdateItem.title = "Check for updates"
|
|
1545
1703
|
checkUpdateItem.isEnabled = true
|
|
1546
1704
|
}
|
|
1547
1705
|
}
|
|
1548
1706
|
|
|
1549
1707
|
@objc func runUpdate() {
|
|
1550
|
-
updateItem
|
|
1551
|
-
updateItem.isEnabled = false
|
|
1708
|
+
startUpdateSpinner(item: updateItem, label: "Updating fops…")
|
|
1552
1709
|
let npm = "/opt/homebrew/bin/npm"
|
|
1553
1710
|
let p = Process()
|
|
1554
1711
|
p.executableURL = URL(fileURLWithPath: npm)
|
|
1555
1712
|
p.arguments = ["install", "-g", "@meshxdata/fops"]
|
|
1556
|
-
p.terminationHandler = {
|
|
1713
|
+
p.terminationHandler = { proc in
|
|
1557
1714
|
DispatchQueue.main.async {
|
|
1558
|
-
|
|
1559
|
-
|
|
1715
|
+
self.stopUpdateSpinner()
|
|
1716
|
+
if proc.terminationStatus == 0 {
|
|
1717
|
+
updateItem.title = "✓ Updated — restarting tray…"
|
|
1718
|
+
updateItem.isEnabled = false
|
|
1719
|
+
// Relaunch tray after short delay
|
|
1720
|
+
DispatchQueue.main.asyncAfter(deadline: .now() + 1) {
|
|
1721
|
+
let fops = "/opt/homebrew/bin/fops"
|
|
1722
|
+
let relaunch = Process()
|
|
1723
|
+
relaunch.executableURL = URL(fileURLWithPath: fops)
|
|
1724
|
+
relaunch.arguments = ["tray"]
|
|
1725
|
+
try? relaunch.run()
|
|
1726
|
+
NSApp.terminate(nil)
|
|
1727
|
+
}
|
|
1728
|
+
} else {
|
|
1729
|
+
updateItem.title = "✗ Update failed"
|
|
1730
|
+
updateItem.isEnabled = true
|
|
1731
|
+
}
|
|
1560
1732
|
}
|
|
1561
1733
|
}
|
|
1562
1734
|
try? p.run()
|
|
1563
1735
|
}
|
|
1564
1736
|
|
|
1565
|
-
// Rebuild Compose
|
|
1737
|
+
// Rebuild Compose/Azure submenus each time they open
|
|
1566
1738
|
func menuWillOpen(_ m: NSMenu) {
|
|
1739
|
+
if m === azureMenu {
|
|
1740
|
+
populateAzureMenu(m)
|
|
1741
|
+
return
|
|
1742
|
+
}
|
|
1567
1743
|
guard m === composeMenu else { return }
|
|
1568
1744
|
m.removeAllItems()
|
|
1569
1745
|
let loading = NSMenuItem(title: "Loading services…", action: nil, keyEquivalent: "")
|
|
@@ -1639,6 +1815,102 @@ class AppDelegate: NSObject, NSApplicationDelegate, NSMenuDelegate {
|
|
|
1639
1815
|
|
|
1640
1816
|
@objc func noop() {}
|
|
1641
1817
|
|
|
1818
|
+
// ── Azure Menu ───────────────────────────────────────────────────────────────
|
|
1819
|
+
|
|
1820
|
+
var azureCache: [String: Any]? = nil
|
|
1821
|
+
var azureCacheTime: Date? = nil
|
|
1822
|
+
let azureCacheTTL: TimeInterval = 60 // 60 seconds
|
|
1823
|
+
|
|
1824
|
+
func populateAzureMenu(_ m: NSMenu) {
|
|
1825
|
+
// Use cache if fresh
|
|
1826
|
+
if let cache = azureCache,
|
|
1827
|
+
let cacheTime = azureCacheTime,
|
|
1828
|
+
Date().timeIntervalSince(cacheTime) < azureCacheTTL {
|
|
1829
|
+
renderAzureMenu(m, data: cache)
|
|
1830
|
+
return
|
|
1831
|
+
}
|
|
1832
|
+
|
|
1833
|
+
m.removeAllItems()
|
|
1834
|
+
let loading = NSMenuItem(title: "Loading resources…", action: nil, keyEquivalent: "")
|
|
1835
|
+
loading.isEnabled = false
|
|
1836
|
+
m.addItem(loading)
|
|
1837
|
+
|
|
1838
|
+
// Fetch VMs and AKS clusters via fops azure list --json
|
|
1839
|
+
fops(["azure", "list", "--json"], capture: true) { out, success in
|
|
1840
|
+
guard success,
|
|
1841
|
+
let data = out.data(using: .utf8),
|
|
1842
|
+
let json = try? JSONSerialization.jsonObject(with: data) as? [String: Any] else {
|
|
1843
|
+
m.removeAllItems()
|
|
1844
|
+
let err = NSMenuItem(title: "Not authenticated or plugin disabled", action: nil, keyEquivalent: "")
|
|
1845
|
+
err.isEnabled = false
|
|
1846
|
+
m.addItem(err)
|
|
1847
|
+
return
|
|
1848
|
+
}
|
|
1849
|
+
|
|
1850
|
+
// Update cache
|
|
1851
|
+
self.azureCache = json
|
|
1852
|
+
self.azureCacheTime = Date()
|
|
1853
|
+
|
|
1854
|
+
self.renderAzureMenu(m, data: json)
|
|
1855
|
+
}
|
|
1856
|
+
}
|
|
1857
|
+
|
|
1858
|
+
func renderAzureMenu(_ m: NSMenu, data: [String: Any]) {
|
|
1859
|
+
m.removeAllItems()
|
|
1860
|
+
|
|
1861
|
+
let vms = data["vms"] as? [[String: Any]] ?? []
|
|
1862
|
+
let clusters = data["clusters"] as? [[String: Any]] ?? []
|
|
1863
|
+
|
|
1864
|
+
// VMs section
|
|
1865
|
+
if !vms.isEmpty {
|
|
1866
|
+
let vmHeader = NSMenuItem(title: "VMs", action: nil, keyEquivalent: "")
|
|
1867
|
+
vmHeader.isEnabled = false
|
|
1868
|
+
m.addItem(vmHeader)
|
|
1869
|
+
for vm in vms {
|
|
1870
|
+
let name = vm["name"] as? String ?? "unknown"
|
|
1871
|
+
let ip = vm["publicIp"] as? String ?? ""
|
|
1872
|
+
let ipSuffix = ip.isEmpty ? "" : " (\\(ip))"
|
|
1873
|
+
let item = NSMenuItem(title: " " + name + ipSuffix, action: #selector(AppDelegate.openAzureVM(_:)), keyEquivalent: "")
|
|
1874
|
+
item.representedObject = name
|
|
1875
|
+
item.target = self
|
|
1876
|
+
m.addItem(item)
|
|
1877
|
+
}
|
|
1878
|
+
}
|
|
1879
|
+
|
|
1880
|
+
// AKS section
|
|
1881
|
+
if !clusters.isEmpty {
|
|
1882
|
+
if !vms.isEmpty { m.addItem(NSMenuItem.separator()) }
|
|
1883
|
+
let aksHeader = NSMenuItem(title: "AKS Clusters", action: nil, keyEquivalent: "")
|
|
1884
|
+
aksHeader.isEnabled = false
|
|
1885
|
+
m.addItem(aksHeader)
|
|
1886
|
+
for cluster in clusters {
|
|
1887
|
+
let name = cluster["name"] as? String ?? "unknown"
|
|
1888
|
+
let state = cluster["provisioningState"] as? String ?? ""
|
|
1889
|
+
let dot = state.lowercased() == "succeeded" ? "● " : "○ "
|
|
1890
|
+
let item = NSMenuItem(title: " " + dot + name, action: #selector(AppDelegate.openAzureAKS(_:)), keyEquivalent: "")
|
|
1891
|
+
item.representedObject = name
|
|
1892
|
+
item.target = self
|
|
1893
|
+
m.addItem(item)
|
|
1894
|
+
}
|
|
1895
|
+
}
|
|
1896
|
+
|
|
1897
|
+
if vms.isEmpty && clusters.isEmpty {
|
|
1898
|
+
let none = NSMenuItem(title: "No resources found", action: nil, keyEquivalent: "")
|
|
1899
|
+
none.isEnabled = false
|
|
1900
|
+
m.addItem(none)
|
|
1901
|
+
}
|
|
1902
|
+
}
|
|
1903
|
+
|
|
1904
|
+
@objc func openAzureVM(_ sender: NSMenuItem) {
|
|
1905
|
+
guard let name = sender.representedObject as? String else { return }
|
|
1906
|
+
openTerminal(command: "fops azure ssh \\(name)")
|
|
1907
|
+
}
|
|
1908
|
+
|
|
1909
|
+
@objc func openAzureAKS(_ sender: NSMenuItem) {
|
|
1910
|
+
guard let name = sender.representedObject as? String else { return }
|
|
1911
|
+
openTerminal(command: "fops azure aks status \\(name)")
|
|
1912
|
+
}
|
|
1913
|
+
|
|
1642
1914
|
// ── Stack-level actions ───────────────────────────────────────────────────
|
|
1643
1915
|
|
|
1644
1916
|
func refresh() {
|
|
@@ -1946,17 +2218,19 @@ class BranchMenuDelegate: NSObject, NSMenuDelegate {
|
|
|
1946
2218
|
|
|
1947
2219
|
// ── PluginMenuDelegate ────────────────────────────────────────────────────────
|
|
1948
2220
|
// Populates the Plugins submenu lazily; clicking a plugin item toggles it.
|
|
2221
|
+
// Discovers plugins dynamically each time the menu opens.
|
|
1949
2222
|
|
|
1950
2223
|
class PluginMenuDelegate: NSObject, NSMenuDelegate {
|
|
1951
2224
|
func menuWillOpen(_ menu: NSMenu) {
|
|
1952
2225
|
menu.removeAllItems()
|
|
1953
|
-
|
|
2226
|
+
let currentPlugins = discoverPlugins()
|
|
2227
|
+
if currentPlugins.isEmpty {
|
|
1954
2228
|
let none = NSMenuItem(title: "No plugins installed", action: nil, keyEquivalent: "")
|
|
1955
2229
|
none.isEnabled = false
|
|
1956
2230
|
menu.addItem(none)
|
|
1957
2231
|
return
|
|
1958
2232
|
}
|
|
1959
|
-
for plugin in
|
|
2233
|
+
for plugin in currentPlugins {
|
|
1960
2234
|
let enabled = readPluginEnabled(plugin.id)
|
|
1961
2235
|
let prefix = enabled ? "✓ " : " "
|
|
1962
2236
|
let item = NSMenuItem(
|
|
@@ -1978,16 +2252,6 @@ app.delegate = delegate
|
|
|
1978
2252
|
app.run()
|
|
1979
2253
|
`);
|
|
1980
2254
|
|
|
1981
|
-
const trayEnv = {
|
|
1982
|
-
...process.env,
|
|
1983
|
-
FOUNDATION_API_URL: apiUrl,
|
|
1984
|
-
FOUNDATION_URL: "http://127.0.0.1:3002",
|
|
1985
|
-
FOUNDATION_ICON: iconPath,
|
|
1986
|
-
FOUNDATION_COMPOSE_ROOT: composeRoot,
|
|
1987
|
-
FOPS_VERSION: fopsVersion,
|
|
1988
|
-
FOPS_PLUGINS: fopsPluginsJson,
|
|
1989
|
-
};
|
|
1990
|
-
|
|
1991
2255
|
console.log(ACCENT(" Foundation tray started — look for the icon in your menu bar"));
|
|
1992
2256
|
|
|
1993
2257
|
const child = spawn("swift", [swiftTray], {
|
|
@@ -1995,6 +2259,7 @@ app.run()
|
|
|
1995
2259
|
detached: true,
|
|
1996
2260
|
env: trayEnv,
|
|
1997
2261
|
});
|
|
2262
|
+
writeFileSync(pidFile, String(child.pid));
|
|
1998
2263
|
child.unref();
|
|
1999
2264
|
});
|
|
2000
2265
|
});
|