@rubytech/create-maxy 1.0.739 → 1.0.740
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/dist/index.js
CHANGED
|
@@ -138,14 +138,23 @@ function shell(command, args, options) {
|
|
|
138
138
|
stdio: "inherit",
|
|
139
139
|
timeout: options?.timeout ?? 300_000,
|
|
140
140
|
cwd: options?.cwd,
|
|
141
|
+
env: options?.env,
|
|
141
142
|
});
|
|
142
143
|
const dur = ((Date.now() - start) / 1000).toFixed(1);
|
|
144
|
+
// bestEffort (Task 787): tear-down ops on units/state that may or may not
|
|
145
|
+
// exist (stop/disable a system service we may never have started, reset-failed
|
|
146
|
+
// a freshly-created unit) log the non-zero exit but do not throw. Reserved
|
|
147
|
+
// for the "may not exist" pattern only — never use for ops that must succeed.
|
|
143
148
|
if (result.signal) {
|
|
144
149
|
logFile(` KILLED (${result.signal}) after ${dur}s`);
|
|
150
|
+
if (options?.bestEffort)
|
|
151
|
+
return;
|
|
145
152
|
throw new Error(`Command killed (${result.signal}) after ${dur}s: ${cmd} ${cmdArgs.join(" ")}`);
|
|
146
153
|
}
|
|
147
154
|
if (result.status !== 0) {
|
|
148
|
-
logFile(` FAILED (exit ${result.status}) after ${dur}s`);
|
|
155
|
+
logFile(` ${options?.bestEffort ? "best-effort non-zero" : "FAILED"} (exit ${result.status}) after ${dur}s`);
|
|
156
|
+
if (options?.bestEffort)
|
|
157
|
+
return;
|
|
149
158
|
throw new Error(`Command failed (exit ${result.status}) after ${dur}s: ${cmd} ${cmdArgs.join(" ")}`);
|
|
150
159
|
}
|
|
151
160
|
logFile(` OK in ${dur}s`);
|
|
@@ -835,8 +844,11 @@ function installNeo4j() {
|
|
|
835
844
|
/**
|
|
836
845
|
* Create a dedicated Neo4j instance for this brand when NEO4J_DEDICATED is true.
|
|
837
846
|
* Produces: separate config dir, data dir, log dir, systemd service, and password.
|
|
838
|
-
* On upgrade (config already exists), skips
|
|
839
|
-
*
|
|
847
|
+
* On upgrade (config already exists), skips conf creation — but always runs the
|
|
848
|
+
* Task 787 state-remediation block (stop/disable system unit, reset-failed
|
|
849
|
+
* dedicated, start, verify) so a half-installed Pi recovers in-place without
|
|
850
|
+
* manual systemctl. ensureNeo4jPassword() handles password verification on the
|
|
851
|
+
* recovery path.
|
|
840
852
|
*/
|
|
841
853
|
function setupDedicatedNeo4j() {
|
|
842
854
|
if (!NEO4J_DEDICATED)
|
|
@@ -847,49 +859,52 @@ function setupDedicatedNeo4j() {
|
|
|
847
859
|
const logDir = `/var/log/neo4j-${brandSuffix}`;
|
|
848
860
|
const serviceName = `neo4j-${brandSuffix}`;
|
|
849
861
|
const httpPort = NEO4J_PORT - 213; // Preserve standard 7687/7474 offset
|
|
850
|
-
//
|
|
851
|
-
//
|
|
852
|
-
|
|
853
|
-
|
|
862
|
+
// Conf creation is gated on first install; everything below the if/else
|
|
863
|
+
// (state remediation + start + verify) runs on every install — Task 787
|
|
864
|
+
// closes the recovery gap where a half-installed Pi (failed prior install
|
|
865
|
+
// hit start-limit-hit, dedicated unit in failed state) returned here without
|
|
866
|
+
// the fix being applied.
|
|
867
|
+
const confExists = spawnSync("test", ["-f", `${confDir}/neo4j.conf`], { stdio: "pipe" }).status === 0;
|
|
868
|
+
if (confExists) {
|
|
854
869
|
console.log(` Dedicated Neo4j instance for ${BRAND.productName} already configured at ${confDir}`);
|
|
855
|
-
logFile(` Neo4j dedicated: existing config at ${confDir}, skipping
|
|
856
|
-
return;
|
|
857
|
-
}
|
|
858
|
-
console.log(` Setting up dedicated Neo4j instance for ${BRAND.productName} on bolt://localhost:${NEO4J_PORT}...`);
|
|
859
|
-
// Pre-check: neo4j user must exist (created by the apt package)
|
|
860
|
-
const neo4jUserCheck = spawnSync("id", ["neo4j"], { stdio: "pipe" });
|
|
861
|
-
if (neo4jUserCheck.status !== 0) {
|
|
862
|
-
throw new Error("Neo4j system user 'neo4j' not found. Is Neo4j installed via apt?");
|
|
863
|
-
}
|
|
864
|
-
// Pre-check: source config must exist
|
|
865
|
-
if (!existsSync("/etc/neo4j/neo4j.conf")) {
|
|
866
|
-
throw new Error("/etc/neo4j/neo4j.conf not found. Cannot create dedicated instance without base config.");
|
|
870
|
+
logFile(` Neo4j dedicated: existing config at ${confDir}, skipping conf creation`);
|
|
867
871
|
}
|
|
868
|
-
|
|
869
|
-
|
|
870
|
-
|
|
871
|
-
|
|
872
|
-
|
|
873
|
-
|
|
874
|
-
|
|
875
|
-
|
|
876
|
-
|
|
877
|
-
|
|
878
|
-
|
|
879
|
-
|
|
880
|
-
|
|
881
|
-
|
|
882
|
-
|
|
883
|
-
console.
|
|
884
|
-
|
|
885
|
-
|
|
886
|
-
|
|
887
|
-
|
|
888
|
-
|
|
889
|
-
|
|
890
|
-
|
|
891
|
-
|
|
892
|
-
|
|
872
|
+
else {
|
|
873
|
+
console.log(` Setting up dedicated Neo4j instance for ${BRAND.productName} on bolt://localhost:${NEO4J_PORT}...`);
|
|
874
|
+
// Pre-check: neo4j user must exist (created by the apt package)
|
|
875
|
+
const neo4jUserCheck = spawnSync("id", ["neo4j"], { stdio: "pipe" });
|
|
876
|
+
if (neo4jUserCheck.status !== 0) {
|
|
877
|
+
throw new Error("Neo4j system user 'neo4j' not found. Is Neo4j installed via apt?");
|
|
878
|
+
}
|
|
879
|
+
// Pre-check: source config must exist
|
|
880
|
+
if (!existsSync("/etc/neo4j/neo4j.conf")) {
|
|
881
|
+
throw new Error("/etc/neo4j/neo4j.conf not found. Cannot create dedicated instance without base config.");
|
|
882
|
+
}
|
|
883
|
+
// 1. Copy base config
|
|
884
|
+
console.log(" [privileged] cp -r");
|
|
885
|
+
shell("cp", ["-r", "/etc/neo4j", confDir], { sudo: true });
|
|
886
|
+
// 2. Modify config for this instance: bolt port, HTTP port, data/log directories
|
|
887
|
+
console.log(" [privileged] sed -i");
|
|
888
|
+
shell("sed", ["-i", `s/^#\\?server\\.bolt\\.listen_address=.*/server.bolt.listen_address=:${NEO4J_PORT}/`, `${confDir}/neo4j.conf`], { sudo: true });
|
|
889
|
+
console.log(" [privileged] sed -i");
|
|
890
|
+
shell("sed", ["-i", `s/^#\\?server\\.http\\.listen_address=.*/server.http.listen_address=:${httpPort}/`, `${confDir}/neo4j.conf`], { sudo: true });
|
|
891
|
+
console.log(" [privileged] sed -i");
|
|
892
|
+
shell("sed", ["-i", `s|^#\\?server\\.directories\\.data=.*|server.directories.data=${dataDir}/data|`, `${confDir}/neo4j.conf`], { sudo: true });
|
|
893
|
+
console.log(" [privileged] sed -i");
|
|
894
|
+
shell("sed", ["-i", `s|^#\\?server\\.directories\\.logs=.*|server.directories.logs=${logDir}|`, `${confDir}/neo4j.conf`], { sudo: true });
|
|
895
|
+
// Verify config was updated — sed silently no-ops if the key format changed
|
|
896
|
+
const confContent = spawnSync("grep", [`server.bolt.listen_address=:${NEO4J_PORT}`, `${confDir}/neo4j.conf`], { stdio: "pipe" });
|
|
897
|
+
if (confContent.status !== 0) {
|
|
898
|
+
console.error(` WARNING: neo4j.conf may not have been updated correctly — bolt port ${NEO4J_PORT} not found in config`);
|
|
899
|
+
logFile(` WARNING: sed verification failed — bolt port ${NEO4J_PORT} not found in ${confDir}/neo4j.conf`);
|
|
900
|
+
}
|
|
901
|
+
// 3. Create data and log directories
|
|
902
|
+
console.log(" [privileged] mkdir -p");
|
|
903
|
+
shell("mkdir", ["-p", `${dataDir}/data`, logDir], { sudo: true });
|
|
904
|
+
console.log(" [privileged] chown -R");
|
|
905
|
+
shell("chown", ["-R", "neo4j:neo4j", dataDir, logDir, confDir], { sudo: true });
|
|
906
|
+
// 4. Create systemd service
|
|
907
|
+
const serviceContent = `[Unit]
|
|
893
908
|
Description=Neo4j Graph Database (${BRAND.productName})
|
|
894
909
|
After=network-online.target
|
|
895
910
|
Wants=network-online.target
|
|
@@ -905,53 +920,73 @@ LimitNOFILE=60000
|
|
|
905
920
|
[Install]
|
|
906
921
|
WantedBy=multi-user.target
|
|
907
922
|
`;
|
|
908
|
-
|
|
909
|
-
|
|
910
|
-
|
|
911
|
-
|
|
912
|
-
|
|
913
|
-
|
|
914
|
-
|
|
915
|
-
|
|
916
|
-
|
|
917
|
-
|
|
918
|
-
|
|
919
|
-
|
|
920
|
-
|
|
921
|
-
|
|
922
|
-
|
|
923
|
-
|
|
924
|
-
|
|
925
|
-
|
|
923
|
+
const tmpServicePath = `/tmp/${serviceName}.service`;
|
|
924
|
+
writeFileSync(tmpServicePath, serviceContent);
|
|
925
|
+
console.log(" [privileged] cp");
|
|
926
|
+
shell("cp", [tmpServicePath, `/etc/systemd/system/${serviceName}.service`], { sudo: true });
|
|
927
|
+
spawnSync("rm", ["-f", tmpServicePath]);
|
|
928
|
+
// 5. Set initial password before first start
|
|
929
|
+
const password = randomBytes(24).toString("base64url");
|
|
930
|
+
const persistDir = resolve(process.env.HOME ?? "/root", BRAND.configDir);
|
|
931
|
+
mkdirSync(persistDir, { recursive: true });
|
|
932
|
+
writeFileSync(join(persistDir, ".neo4j-password"), password, { mode: 0o600 });
|
|
933
|
+
const configDir = resolve(INSTALL_DIR, "platform/config");
|
|
934
|
+
mkdirSync(configDir, { recursive: true });
|
|
935
|
+
writeFileSync(join(configDir, ".neo4j-password"), password, { mode: 0o600 });
|
|
936
|
+
// sudo env VAR=val passes NEO4J_CONF through sudo's env_reset policy
|
|
937
|
+
spawnSync("sudo", ["env", `NEO4J_CONF=${confDir}`, "neo4j-admin", "dbms", "set-initial-password", "--", password], {
|
|
938
|
+
stdio: "inherit",
|
|
939
|
+
});
|
|
940
|
+
}
|
|
941
|
+
// ============================================================================
|
|
942
|
+
// Task 787 — unified state remediation + start + verify.
|
|
943
|
+
//
|
|
944
|
+
// Runs on both fresh and recovery paths. The dedicated unit and the apt
|
|
945
|
+
// package's system unit both exec /usr/bin/neo4j console without overriding
|
|
946
|
+
// NEO4J_HOME, so server.directories.run resolves to /var/lib/neo4j/run for
|
|
947
|
+
// both — the launcher refuses with "Neo4j is already running (pid:N)" if
|
|
948
|
+
// the system unit holds the PID file. Stopping the system unit first frees
|
|
949
|
+
// the run-state; disabling prevents it returning at next boot. reset-failed
|
|
950
|
+
// clears any prior start-limit-hit from a half-installed Pi.
|
|
951
|
+
// ============================================================================
|
|
926
952
|
spawnSync("sudo", ["systemctl", "daemon-reload"], { stdio: "inherit" });
|
|
927
953
|
console.log(" [privileged] systemctl enable");
|
|
928
954
|
shell("systemctl", ["enable", serviceName], { sudo: true });
|
|
955
|
+
console.log(` [neo4j] disabling system unit (brand-dedicated active on port ${NEO4J_PORT})`);
|
|
956
|
+
logFile(` [neo4j] disabling system unit (brand-dedicated active on port ${NEO4J_PORT})`);
|
|
957
|
+
shell("systemctl", ["stop", "neo4j"], { sudo: true, bestEffort: true });
|
|
958
|
+
shell("systemctl", ["disable", "neo4j"], { sudo: true, bestEffort: true });
|
|
959
|
+
console.log(` [neo4j] reset-failed ${serviceName} before start`);
|
|
960
|
+
logFile(` [neo4j] reset-failed ${serviceName} before start`);
|
|
961
|
+
shell("systemctl", ["reset-failed", serviceName], { sudo: true, bestEffort: true });
|
|
929
962
|
console.log(" [privileged] systemctl start");
|
|
930
963
|
shell("systemctl", ["start", serviceName], { sudo: true });
|
|
931
|
-
//
|
|
964
|
+
// Verify the dedicated unit bound its port. Password verification is
|
|
965
|
+
// ensureNeo4jPassword()'s job (called next in the install pipeline) — that
|
|
966
|
+
// function tests the stored password against this port and resets auth if
|
|
967
|
+
// the password no longer matches the running instance.
|
|
932
968
|
console.log(` Waiting for dedicated Neo4j instance on port ${NEO4J_PORT}...`);
|
|
933
|
-
let
|
|
969
|
+
let listening = false;
|
|
970
|
+
const portMatch = new RegExp(`:${NEO4J_PORT}\\b`);
|
|
934
971
|
for (let i = 0; i < 15; i++) {
|
|
935
|
-
const
|
|
936
|
-
|
|
937
|
-
|
|
938
|
-
"RETURN 1",
|
|
939
|
-
], { stdio: "pipe", timeout: 5000 });
|
|
940
|
-
if (check.status === 0) {
|
|
941
|
-
connected = true;
|
|
972
|
+
const portCheck = spawnSync("ss", ["-tln"], { stdio: "pipe", timeout: 5000 });
|
|
973
|
+
if (portMatch.test(portCheck.stdout?.toString() ?? "")) {
|
|
974
|
+
listening = true;
|
|
942
975
|
break;
|
|
943
976
|
}
|
|
944
977
|
spawnSync("sleep", ["2"]);
|
|
945
978
|
}
|
|
946
|
-
if (!
|
|
947
|
-
//
|
|
979
|
+
if (!listening) {
|
|
980
|
+
// Loud failure — no silent fallback to the system instance (Task 787).
|
|
948
981
|
const portCheck = spawnSync("ss", ["-tlnp"], { stdio: "pipe", timeout: 5000 });
|
|
949
|
-
const portLines = portCheck.stdout?.toString().split("\n").filter((l) => l.includes(String(NEO4J_PORT)))
|
|
982
|
+
const portLines = (portCheck.stdout?.toString() ?? "").split("\n").filter((l) => l.includes(String(NEO4J_PORT)));
|
|
950
983
|
const diagnostic = portLines.length > 0 ? portLines.join("; ") : "nothing listening on port";
|
|
951
|
-
|
|
952
|
-
|
|
953
|
-
logFile(` Neo4j dedicated:
|
|
954
|
-
|
|
984
|
+
const journal = spawnSync("journalctl", ["-u", serviceName, "--since", "5 min ago"], { stdio: "pipe", timeout: 5000 });
|
|
985
|
+
const journalTail = (journal.stdout?.toString() ?? "").split("\n").slice(-20).join("\n");
|
|
986
|
+
logFile(` Neo4j dedicated: failed to bind port ${NEO4J_PORT} — ${diagnostic}`);
|
|
987
|
+
throw new Error(`Dedicated Neo4j instance ${serviceName} did not bind bolt://localhost:${NEO4J_PORT} within 30s.\n` +
|
|
988
|
+
`Port ${NEO4J_PORT}: ${diagnostic}\n` +
|
|
989
|
+
`journalctl -u ${serviceName} --since "5 min ago" | tail -20:\n${journalTail}`);
|
|
955
990
|
}
|
|
956
991
|
logFile(` Neo4j dedicated: config=${confDir} data=${dataDir} service=${serviceName} bolt=:${NEO4J_PORT} http=:${httpPort}`);
|
|
957
992
|
console.log(` Dedicated Neo4j instance ready on bolt://localhost:${NEO4J_PORT}`);
|
|
@@ -1449,7 +1484,23 @@ function setupAccount() {
|
|
|
1449
1484
|
log("10", TOTAL, "Setting up...");
|
|
1450
1485
|
const seedScript = join(INSTALL_DIR, "platform/scripts/seed-neo4j.sh");
|
|
1451
1486
|
if (existsSync(seedScript)) {
|
|
1452
|
-
|
|
1487
|
+
// Task 787 — explicit env to seed. The script no longer falls back to
|
|
1488
|
+
// bolt://localhost:7687, so the installer is responsible for naming the
|
|
1489
|
+
// brand-correct URI and providing the password. Missing password file is
|
|
1490
|
+
// a hard error: ensureNeo4jPassword() ran upstream and would have thrown
|
|
1491
|
+
// already if it couldn't reach the brand's Neo4j.
|
|
1492
|
+
const passwordFile = join(INSTALL_DIR, "platform/config/.neo4j-password");
|
|
1493
|
+
if (!existsSync(passwordFile)) {
|
|
1494
|
+
throw new Error(`Neo4j password file missing at ${passwordFile} — required by seed step.`);
|
|
1495
|
+
}
|
|
1496
|
+
const password = readFileSync(passwordFile, "utf-8").trim();
|
|
1497
|
+
const neo4jUri = `bolt://localhost:${NEO4J_PORT}`;
|
|
1498
|
+
console.log(` [neo4j] passing NEO4J_URI=${neo4jUri} to seed`);
|
|
1499
|
+
logFile(` [neo4j] passing NEO4J_URI=${neo4jUri} to seed`);
|
|
1500
|
+
shell("bash", [seedScript], {
|
|
1501
|
+
cwd: INSTALL_DIR,
|
|
1502
|
+
env: { ...process.env, NEO4J_URI: neo4jUri, NEO4J_PASSWORD: password },
|
|
1503
|
+
});
|
|
1453
1504
|
}
|
|
1454
1505
|
// Task 748 — universal embedding coverage backfill. Run after seed so the
|
|
1455
1506
|
// entity_search index is in place and any pre-Task-748 nodes (e.g. the
|
package/package.json
CHANGED
|
@@ -100,6 +100,7 @@ systemctl --user daemon-reload
|
|
|
100
100
|
A single Pi or laptop can host more than one brand (for example Maxy and Real Agent) side by side. Each brand runs as its own service on its own port, with its own install directory and its own data. Installing one brand does not touch the other.
|
|
101
101
|
|
|
102
102
|
- **Separate:** each brand has its own install folder (`~/maxy/`, `~/realagent/`), its own config folder (`~/.maxy/`, `~/.realagent/`), its own web port, its own Cloudflare tunnel state, its own edge systemd unit (`maxy-edge.service` vs `realagent-edge.service`), and by default its own Neo4j database (Maxy on bolt port 7687, Real Agent on 7688). Action runner units are transient and per-invocation, not per-brand, so no naming conflict is possible.
|
|
103
|
+
- **Brand-isolated Neo4j (Task 787):** when a brand provisions a dedicated Neo4j instance (any port other than 7687), the installer stops and disables the apt-package's system `neo4j.service` after enabling the brand-dedicated unit, so only one Neo4j process holds the shared `/var/lib/neo4j/run/` PID file. The seed step receives the brand-correct `NEO4J_URI` and `NEO4J_PASSWORD` as explicit environment variables — the seed script no longer carries a `bolt://localhost:7687` default. A failed dedicated start aborts the install loudly with a journalctl tail; there is no silent fallback to the system instance. Stop/disable targets the literal `neo4j.service` only, so re-running another brand's installer never touches a peer brand's `neo4j-{brand}.service`.
|
|
103
104
|
- **Shared:** both brands share the system Chromium/VNC stack, the Ollama model server, and the `cloudflared` command itself. Browser automation is serialised — one admin session at a time across both brands.
|
|
104
105
|
|
|
105
106
|
To install a second brand on a device that already runs the first, just run the other installer. No flags needed for isolation:
|
|
@@ -16,7 +16,13 @@ INSTALL_DIR="$(dirname "$PROJECT_DIR")"
|
|
|
16
16
|
ACCOUNTS_DIR="$INSTALL_DIR/data/accounts"
|
|
17
17
|
NEO4J_DIR="$PROJECT_DIR/neo4j"
|
|
18
18
|
|
|
19
|
-
NEO4J_URI
|
|
19
|
+
# NEO4J_URI is hard-required (Task 787). The previous default
|
|
20
|
+
# `bolt://localhost:7687` would silently route the seed to the wrong Neo4j on
|
|
21
|
+
# any brand-dedicated install — masking every other defect downstream.
|
|
22
|
+
if [ -z "${NEO4J_URI:-}" ]; then
|
|
23
|
+
echo "Error: NEO4J_URI required (no default — see Task 787)" >&2
|
|
24
|
+
exit 1
|
|
25
|
+
fi
|
|
20
26
|
NEO4J_USER="${NEO4J_USER:-neo4j}"
|
|
21
27
|
|
|
22
28
|
# Read password from the file created during setup
|