commit 11ed816a57bd47a1d9ae0fa145e556eed4480fa8
parent f02a03e9889f596388c8cb071c280cc3ef1d1f44
Author: SageAzakaela <106701693+SageAzakaela@users.noreply.github.com>
Date: Sat, 21 Feb 2026 13:27:05 -0700
improved instance detection for multi instance update
patched the fleet updater to detect:
compose-only Bzl folders (not just package.json + server.js)
Docker Compose label metadata (working_dir + config_files)
docker-compose.yaml in addition to existing compose filenames
Changes are in:
bzl-instances-update.js (line 5)
bzl-instances-update.js (line 172)
bzl-instances-update.js (line 245)
INSTANCE_FLEET_AUTOMATION.md (line 13)
bzl-instances-update.js (line 5)
INSTANCE_FLEET_AUTOMATION.md (line 13)
Diffstat:
4 files changed, 410 insertions(+), 94 deletions(-)
diff --git a/CLEAN_INSTALL/docs/INSTANCE_FLEET_AUTOMATION.md b/CLEAN_INSTALL/docs/INSTANCE_FLEET_AUTOMATION.md
@@ -10,10 +10,15 @@ List discovered Bzl instances:
npm run instances:scan -- --roots=/ --max-depth=4
```
-By default, detection looks for folders containing:
-- `package.json` with `"name": "bzl"`
-- `server.js`
-- one compose file (`compose.yaml`, `compose.yml`, or `docker-compose.yml`)
+By default, detection combines:
+- filesystem scan for compose projects with Bzl-like signals (`bzl` in path/name or Bzl source markers)
+- Docker label scan (`com.docker.compose.project.working_dir`) so running instances are found even if the folder is compose-only
+
+Supported compose filenames include:
+- `compose.yaml`
+- `compose.yml`
+- `docker-compose.yml`
+- `docker-compose.yaml`
## 2) Update all discovered instances
diff --git a/CLEAN_INSTALL/scripts/bzl-instances-update.js b/CLEAN_INSTALL/scripts/bzl-instances-update.js
@@ -2,8 +2,34 @@ const fs = require("fs");
const path = require("path");
const { spawnSync } = require("child_process");
-const BZL_COMPOSE_FILES = ["compose.yaml", "compose.yml", "docker-compose.yml"];
+const BZL_COMPOSE_FILES = ["compose.yaml", "compose.yml", "docker-compose.yml", "docker-compose.yaml"];
const DEFAULT_MAX_DEPTH = 4;
+const SKIP_DIR_NAMES = new Set([
+ ".git",
+ "node_modules",
+ "data",
+ "dist",
+ "clean_install",
+ "multi_instance",
+ "tmp",
+ "temp",
+ "proc",
+ "sys",
+ "dev",
+ "run",
+ "usr",
+ "lib",
+ "lib64",
+ "bin",
+ "sbin",
+ "etc",
+ "boot",
+ "mnt",
+ "media",
+ "snap",
+ "lost+found",
+ "__pycache__"
+]);
function log(msg) {
console.log(`[instances] ${msg}`);
@@ -59,62 +85,121 @@ function splitList(raw, fallback) {
function shouldSkipDir(name) {
const n = String(name || "").trim().toLowerCase();
if (!n) return true;
- return new Set([
- ".git",
- "node_modules",
- "data",
- "dist",
- "clean_install",
- "multi_instance",
- "tmp",
- "temp",
- "proc",
- "sys",
- "dev",
- "run",
- "usr",
- "lib",
- "lib64",
- "bin",
- "sbin",
- "etc",
- "boot",
- "mnt",
- "media",
- "snap",
- "lost+found",
- "__pycache__"
- ]).has(n);
+ return SKIP_DIR_NAMES.has(n);
}
-function readPackageName(dir) {
+function isExistingFile(filePath) {
+ try {
+ return fs.existsSync(filePath) && fs.statSync(filePath).isFile();
+ } catch {
+ return false;
+ }
+}
+
+function isExistingDir(dirPath) {
+ try {
+ return fs.existsSync(dirPath) && fs.statSync(dirPath).isDirectory();
+ } catch {
+ return false;
+ }
+}
+
+function readPackageJson(dir) {
try {
const pkgPath = path.join(dir, "package.json");
- const raw = JSON.parse(fs.readFileSync(pkgPath, "utf8"));
- return String(raw?.name || "").trim().toLowerCase();
+ return JSON.parse(fs.readFileSync(pkgPath, "utf8"));
} catch {
- return "";
+ return null;
+ }
+}
+
+function looksLikeBzlProject(pkg) {
+ const p = pkg && typeof pkg === "object" ? pkg : null;
+ if (!p) return false;
+ const name = String(p?.name || "").trim().toLowerCase();
+ if (name.includes("bzl")) return true;
+ const scripts = p?.scripts && typeof p.scripts === "object" ? p.scripts : {};
+ const startScript = String(scripts.start || "").trim().toLowerCase();
+ if (startScript.includes("server.js")) return true;
+ if (Object.prototype.hasOwnProperty.call(scripts, "create-user")) return true;
+ const deps = p?.dependencies && typeof p.dependencies === "object" ? p.dependencies : {};
+ if (Object.prototype.hasOwnProperty.call(deps, "ws") && Object.prototype.hasOwnProperty.call(deps, "sanitize-html")) {
+ return true;
+ }
+ return false;
+}
+
+function looksLikeBzlText(value) {
+ const text = String(value || "").trim().toLowerCase();
+ return text.includes("bzl");
+}
+
+function sourcePriority(source) {
+ const s = String(source || "").trim().toLowerCase();
+ if (s === "docker-label") return 2;
+ return 1;
+}
+
+function normalizeDockerValue(value) {
+ const text = String(value || "").trim();
+ if (!text || text === "<no value>" || text === "<nil>") return "";
+ return text;
+}
+
+function composeCandidatesFromLabel(workingDir, rawConfigFiles) {
+ const cfg = String(rawConfigFiles || "").trim();
+ if (!cfg) return [];
+ const candidates = [];
+ const tokens = cfg
+ .split(/[;,]/)
+ .map((x) => normalizeDockerValue(x))
+ .filter(Boolean);
+ for (const token of tokens) {
+ const full = path.isAbsolute(token) ? token : path.resolve(workingDir, token);
+ if (isExistingFile(full) && !candidates.includes(full)) candidates.push(full);
}
+ return candidates;
}
function findComposeFile(dir) {
for (const name of BZL_COMPOSE_FILES) {
const full = path.join(dir, name);
- if (fs.existsSync(full) && fs.statSync(full).isFile()) return full;
+ if (isExistingFile(full)) return full;
}
return "";
}
-function detectBzlInstance(dir) {
+function detectBzlInstance(dir, options = {}) {
try {
- const hasServer = fs.existsSync(path.join(dir, "server.js"));
- const hasPkg = fs.existsSync(path.join(dir, "package.json"));
- if (!hasServer || !hasPkg) return null;
- const pkgName = readPackageName(dir);
- if (pkgName !== "bzl") return null;
- const composeFile = findComposeFile(dir);
+ const absDir = path.resolve(dir);
+ let composeFile = String(options.preferredComposeFile || "").trim();
+ if (composeFile) composeFile = path.resolve(absDir, composeFile);
+ if (!isExistingFile(composeFile)) composeFile = "";
+ if (!composeFile) composeFile = findComposeFile(absDir);
if (!composeFile) return null;
- return { rootDir: dir, composeFile };
+
+ const source = String(options.source || "filesystem").trim() || "filesystem";
+ const allowComposeOnly = options.allowComposeOnly === true;
+ const hints = Array.isArray(options.hints) ? options.hints : [];
+
+ const hasServer = isExistingFile(path.join(absDir, "server.js"));
+ const hasPkg = isExistingFile(path.join(absDir, "package.json"));
+ const pkg = hasPkg ? readPackageJson(absDir) : null;
+
+ const packageHint = hasPkg && looksLikeBzlProject(pkg);
+ const pathHint = looksLikeBzlText(absDir) || looksLikeBzlText(path.basename(absDir));
+ const externalHint = hints.some((hint) => looksLikeBzlText(hint));
+ const hasSource = hasServer && hasPkg;
+ const allowBySource = hasSource && (packageHint || pathHint || externalHint);
+ const allowByComposeOnly = allowComposeOnly && (pathHint || externalHint);
+ if (!allowBySource && !allowByComposeOnly) return null;
+
+ return {
+ rootDir: absDir,
+ composeFile: path.resolve(composeFile),
+ source,
+ hasSource
+ };
} catch {
return null;
}
@@ -130,7 +215,11 @@ function discoverFromRoot(rootDir, maxDepth) {
if (seen.has(abs)) continue;
seen.add(abs);
- const instance = detectBzlInstance(abs);
+ const instance = detectBzlInstance(abs, {
+ source: "filesystem",
+ allowComposeOnly: true,
+ hints: [abs]
+ });
if (instance) {
found.push(instance);
continue;
@@ -153,6 +242,62 @@ function discoverFromRoot(rootDir, maxDepth) {
return found;
}
+function discoverFromDockerLabels() {
+ try {
+ const format = [
+ "{{.Names}}",
+ "{{.Image}}",
+ '{{.Label "com.docker.compose.project"}}',
+ '{{.Label "com.docker.compose.service"}}',
+ '{{.Label "com.docker.compose.project.working_dir"}}',
+ '{{.Label "com.docker.compose.project.config_files"}}'
+ ].join("|");
+ const psRes = spawnSync("docker", ["ps", "-a", "--format", format], {
+ cwd: process.cwd(),
+ stdio: ["ignore", "pipe", "pipe"],
+ encoding: "utf8",
+ shell: false
+ });
+ if (psRes.error || (psRes.status || 0) !== 0) return [];
+
+ const found = [];
+ const lines = String(psRes.stdout || "")
+ .split(/\r?\n/)
+ .map((line) => String(line || "").trim())
+ .filter(Boolean);
+
+ for (const line of lines) {
+ const parts = line.split("|");
+ if (parts.length < 6) continue;
+ const containerName = normalizeDockerValue(parts[0]);
+ const imageName = normalizeDockerValue(parts[1]);
+ const projectName = normalizeDockerValue(parts[2]);
+ const serviceName = normalizeDockerValue(parts[3]);
+ const workingDirRaw = normalizeDockerValue(parts[4]);
+ const configFilesRaw = normalizeDockerValue(parts[5]);
+ if (!workingDirRaw) continue;
+
+ const workingDir = path.resolve(workingDirRaw);
+ if (!isExistingDir(workingDir)) continue;
+
+ const composeCandidates = composeCandidatesFromLabel(workingDir, configFilesRaw);
+ const preferredComposeFile = composeCandidates.length ? composeCandidates[0] : "";
+ const detected = detectBzlInstance(workingDir, {
+ source: "docker-label",
+ allowComposeOnly: true,
+ preferredComposeFile,
+ hints: [containerName, imageName, projectName, serviceName, workingDir]
+ });
+ if (detected) {
+ found.push(detected);
+ }
+ }
+ return found;
+ } catch {
+ return [];
+ }
+}
+
function run(cmd, args, cwd, { dryRun = false, allowFail = false } = {}) {
const printable = `${cmd} ${args.join(" ")}`;
if (dryRun) {
@@ -199,7 +344,10 @@ function printDiscovered(instances) {
}
log(`Discovered ${instances.length} instance(s):`);
for (const [i, inst] of instances.entries()) {
- console.log(` ${i + 1}. ${displayPath(inst.rootDir)} (compose: ${path.basename(inst.composeFile)})`);
+ const source = String(inst?.source || "filesystem");
+ console.log(
+ ` ${i + 1}. ${displayPath(inst.rootDir)} (compose: ${path.basename(inst.composeFile)}, source: ${source})`
+ );
}
}
@@ -209,6 +357,7 @@ function main() {
const skipGit = args["skip-git"] === "1";
const skipBuild = args["skip-build"] === "1";
const dryRun = args["dry-run"] === "1";
+ const noDockerDiscovery = args["no-docker-discovery"] === "1";
const remote = String(args.remote || "origin").trim();
const branch = String(args.branch || "main").trim();
const maxDepth = Number.isInteger(Number(args["max-depth"])) ? Math.max(1, Number(args["max-depth"])) : DEFAULT_MAX_DEPTH;
@@ -222,11 +371,15 @@ function main() {
const foundRaw = [];
for (const root of roots) foundRaw.push(...discoverFromRoot(root, maxDepth));
+ if (!noDockerDiscovery) foundRaw.push(...discoverFromDockerLabels());
const byRoot = new Map();
for (const inst of foundRaw) {
const key = path.resolve(inst.rootDir);
- if (!byRoot.has(key)) byRoot.set(key, inst);
+ const existing = byRoot.get(key);
+ if (!existing || sourcePriority(inst.source) > sourcePriority(existing.source)) {
+ byRoot.set(key, inst);
+ }
}
const instances = Array.from(byRoot.values()).sort((a, b) => a.rootDir.localeCompare(b.rootDir));
printDiscovered(instances);
diff --git a/docs/INSTANCE_FLEET_AUTOMATION.md b/docs/INSTANCE_FLEET_AUTOMATION.md
@@ -10,10 +10,15 @@ List discovered Bzl instances:
npm run instances:scan -- --roots=/ --max-depth=4
```
-By default, detection looks for folders containing:
-- `package.json` with `"name": "bzl"`
-- `server.js`
-- one compose file (`compose.yaml`, `compose.yml`, or `docker-compose.yml`)
+By default, detection combines:
+- filesystem scan for compose projects with Bzl-like signals (`bzl` in path/name or Bzl source markers)
+- Docker label scan (`com.docker.compose.project.working_dir`) so running instances are found even if the folder is compose-only
+
+Supported compose filenames include:
+- `compose.yaml`
+- `compose.yml`
+- `docker-compose.yml`
+- `docker-compose.yaml`
## 2) Update all discovered instances
diff --git a/scripts/bzl-instances-update.js b/scripts/bzl-instances-update.js
@@ -2,8 +2,34 @@ const fs = require("fs");
const path = require("path");
const { spawnSync } = require("child_process");
-const BZL_COMPOSE_FILES = ["compose.yaml", "compose.yml", "docker-compose.yml"];
+const BZL_COMPOSE_FILES = ["compose.yaml", "compose.yml", "docker-compose.yml", "docker-compose.yaml"];
const DEFAULT_MAX_DEPTH = 4;
+const SKIP_DIR_NAMES = new Set([
+ ".git",
+ "node_modules",
+ "data",
+ "dist",
+ "clean_install",
+ "multi_instance",
+ "tmp",
+ "temp",
+ "proc",
+ "sys",
+ "dev",
+ "run",
+ "usr",
+ "lib",
+ "lib64",
+ "bin",
+ "sbin",
+ "etc",
+ "boot",
+ "mnt",
+ "media",
+ "snap",
+ "lost+found",
+ "__pycache__"
+]);
function log(msg) {
console.log(`[instances] ${msg}`);
@@ -59,62 +85,121 @@ function splitList(raw, fallback) {
function shouldSkipDir(name) {
const n = String(name || "").trim().toLowerCase();
if (!n) return true;
- return new Set([
- ".git",
- "node_modules",
- "data",
- "dist",
- "clean_install",
- "multi_instance",
- "tmp",
- "temp",
- "proc",
- "sys",
- "dev",
- "run",
- "usr",
- "lib",
- "lib64",
- "bin",
- "sbin",
- "etc",
- "boot",
- "mnt",
- "media",
- "snap",
- "lost+found",
- "__pycache__"
- ]).has(n);
+ return SKIP_DIR_NAMES.has(n);
}
-function readPackageName(dir) {
+function isExistingFile(filePath) {
+ try {
+ return fs.existsSync(filePath) && fs.statSync(filePath).isFile();
+ } catch {
+ return false;
+ }
+}
+
+function isExistingDir(dirPath) {
+ try {
+ return fs.existsSync(dirPath) && fs.statSync(dirPath).isDirectory();
+ } catch {
+ return false;
+ }
+}
+
+function readPackageJson(dir) {
try {
const pkgPath = path.join(dir, "package.json");
- const raw = JSON.parse(fs.readFileSync(pkgPath, "utf8"));
- return String(raw?.name || "").trim().toLowerCase();
+ return JSON.parse(fs.readFileSync(pkgPath, "utf8"));
} catch {
- return "";
+ return null;
+ }
+}
+
+function looksLikeBzlProject(pkg) {
+ const p = pkg && typeof pkg === "object" ? pkg : null;
+ if (!p) return false;
+ const name = String(p?.name || "").trim().toLowerCase();
+ if (name.includes("bzl")) return true;
+ const scripts = p?.scripts && typeof p.scripts === "object" ? p.scripts : {};
+ const startScript = String(scripts.start || "").trim().toLowerCase();
+ if (startScript.includes("server.js")) return true;
+ if (Object.prototype.hasOwnProperty.call(scripts, "create-user")) return true;
+ const deps = p?.dependencies && typeof p.dependencies === "object" ? p.dependencies : {};
+ if (Object.prototype.hasOwnProperty.call(deps, "ws") && Object.prototype.hasOwnProperty.call(deps, "sanitize-html")) {
+ return true;
+ }
+ return false;
+}
+
+function looksLikeBzlText(value) {
+ const text = String(value || "").trim().toLowerCase();
+ return text.includes("bzl");
+}
+
+function sourcePriority(source) {
+ const s = String(source || "").trim().toLowerCase();
+ if (s === "docker-label") return 2;
+ return 1;
+}
+
+function normalizeDockerValue(value) {
+ const text = String(value || "").trim();
+ if (!text || text === "<no value>" || text === "<nil>") return "";
+ return text;
+}
+
+function composeCandidatesFromLabel(workingDir, rawConfigFiles) {
+ const cfg = String(rawConfigFiles || "").trim();
+ if (!cfg) return [];
+ const candidates = [];
+ const tokens = cfg
+ .split(/[;,]/)
+ .map((x) => normalizeDockerValue(x))
+ .filter(Boolean);
+ for (const token of tokens) {
+ const full = path.isAbsolute(token) ? token : path.resolve(workingDir, token);
+ if (isExistingFile(full) && !candidates.includes(full)) candidates.push(full);
}
+ return candidates;
}
function findComposeFile(dir) {
for (const name of BZL_COMPOSE_FILES) {
const full = path.join(dir, name);
- if (fs.existsSync(full) && fs.statSync(full).isFile()) return full;
+ if (isExistingFile(full)) return full;
}
return "";
}
-function detectBzlInstance(dir) {
+function detectBzlInstance(dir, options = {}) {
try {
- const hasServer = fs.existsSync(path.join(dir, "server.js"));
- const hasPkg = fs.existsSync(path.join(dir, "package.json"));
- if (!hasServer || !hasPkg) return null;
- const pkgName = readPackageName(dir);
- if (pkgName !== "bzl") return null;
- const composeFile = findComposeFile(dir);
+ const absDir = path.resolve(dir);
+ let composeFile = String(options.preferredComposeFile || "").trim();
+ if (composeFile) composeFile = path.resolve(absDir, composeFile);
+ if (!isExistingFile(composeFile)) composeFile = "";
+ if (!composeFile) composeFile = findComposeFile(absDir);
if (!composeFile) return null;
- return { rootDir: dir, composeFile };
+
+ const source = String(options.source || "filesystem").trim() || "filesystem";
+ const allowComposeOnly = options.allowComposeOnly === true;
+ const hints = Array.isArray(options.hints) ? options.hints : [];
+
+ const hasServer = isExistingFile(path.join(absDir, "server.js"));
+ const hasPkg = isExistingFile(path.join(absDir, "package.json"));
+ const pkg = hasPkg ? readPackageJson(absDir) : null;
+
+ const packageHint = hasPkg && looksLikeBzlProject(pkg);
+ const pathHint = looksLikeBzlText(absDir) || looksLikeBzlText(path.basename(absDir));
+ const externalHint = hints.some((hint) => looksLikeBzlText(hint));
+ const hasSource = hasServer && hasPkg;
+ const allowBySource = hasSource && (packageHint || pathHint || externalHint);
+ const allowByComposeOnly = allowComposeOnly && (pathHint || externalHint);
+ if (!allowBySource && !allowByComposeOnly) return null;
+
+ return {
+ rootDir: absDir,
+ composeFile: path.resolve(composeFile),
+ source,
+ hasSource
+ };
} catch {
return null;
}
@@ -130,7 +215,11 @@ function discoverFromRoot(rootDir, maxDepth) {
if (seen.has(abs)) continue;
seen.add(abs);
- const instance = detectBzlInstance(abs);
+ const instance = detectBzlInstance(abs, {
+ source: "filesystem",
+ allowComposeOnly: true,
+ hints: [abs]
+ });
if (instance) {
found.push(instance);
continue;
@@ -153,6 +242,62 @@ function discoverFromRoot(rootDir, maxDepth) {
return found;
}
+function discoverFromDockerLabels() {
+ try {
+ const format = [
+ "{{.Names}}",
+ "{{.Image}}",
+ '{{.Label "com.docker.compose.project"}}',
+ '{{.Label "com.docker.compose.service"}}',
+ '{{.Label "com.docker.compose.project.working_dir"}}',
+ '{{.Label "com.docker.compose.project.config_files"}}'
+ ].join("|");
+ const psRes = spawnSync("docker", ["ps", "-a", "--format", format], {
+ cwd: process.cwd(),
+ stdio: ["ignore", "pipe", "pipe"],
+ encoding: "utf8",
+ shell: false
+ });
+ if (psRes.error || (psRes.status || 0) !== 0) return [];
+
+ const found = [];
+ const lines = String(psRes.stdout || "")
+ .split(/\r?\n/)
+ .map((line) => String(line || "").trim())
+ .filter(Boolean);
+
+ for (const line of lines) {
+ const parts = line.split("|");
+ if (parts.length < 6) continue;
+ const containerName = normalizeDockerValue(parts[0]);
+ const imageName = normalizeDockerValue(parts[1]);
+ const projectName = normalizeDockerValue(parts[2]);
+ const serviceName = normalizeDockerValue(parts[3]);
+ const workingDirRaw = normalizeDockerValue(parts[4]);
+ const configFilesRaw = normalizeDockerValue(parts[5]);
+ if (!workingDirRaw) continue;
+
+ const workingDir = path.resolve(workingDirRaw);
+ if (!isExistingDir(workingDir)) continue;
+
+ const composeCandidates = composeCandidatesFromLabel(workingDir, configFilesRaw);
+ const preferredComposeFile = composeCandidates.length ? composeCandidates[0] : "";
+ const detected = detectBzlInstance(workingDir, {
+ source: "docker-label",
+ allowComposeOnly: true,
+ preferredComposeFile,
+ hints: [containerName, imageName, projectName, serviceName, workingDir]
+ });
+ if (detected) {
+ found.push(detected);
+ }
+ }
+ return found;
+ } catch {
+ return [];
+ }
+}
+
function run(cmd, args, cwd, { dryRun = false, allowFail = false } = {}) {
const printable = `${cmd} ${args.join(" ")}`;
if (dryRun) {
@@ -199,7 +344,10 @@ function printDiscovered(instances) {
}
log(`Discovered ${instances.length} instance(s):`);
for (const [i, inst] of instances.entries()) {
- console.log(` ${i + 1}. ${displayPath(inst.rootDir)} (compose: ${path.basename(inst.composeFile)})`);
+ const source = String(inst?.source || "filesystem");
+ console.log(
+ ` ${i + 1}. ${displayPath(inst.rootDir)} (compose: ${path.basename(inst.composeFile)}, source: ${source})`
+ );
}
}
@@ -209,6 +357,7 @@ function main() {
const skipGit = args["skip-git"] === "1";
const skipBuild = args["skip-build"] === "1";
const dryRun = args["dry-run"] === "1";
+ const noDockerDiscovery = args["no-docker-discovery"] === "1";
const remote = String(args.remote || "origin").trim();
const branch = String(args.branch || "main").trim();
const maxDepth = Number.isInteger(Number(args["max-depth"])) ? Math.max(1, Number(args["max-depth"])) : DEFAULT_MAX_DEPTH;
@@ -222,11 +371,15 @@ function main() {
const foundRaw = [];
for (const root of roots) foundRaw.push(...discoverFromRoot(root, maxDepth));
+ if (!noDockerDiscovery) foundRaw.push(...discoverFromDockerLabels());
const byRoot = new Map();
for (const inst of foundRaw) {
const key = path.resolve(inst.rootDir);
- if (!byRoot.has(key)) byRoot.set(key, inst);
+ const existing = byRoot.get(key);
+ if (!existing || sourcePriority(inst.source) > sourcePriority(existing.source)) {
+ byRoot.set(key, inst);
+ }
}
const instances = Array.from(byRoot.values()).sort((a, b) => a.rootDir.localeCompare(b.rootDir));
printDiscovered(instances);