@rubytech/create-maxy 1.0.738 → 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 setupensureNeo4jPassword() handles
839
- * password verification for existing instances.
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
- // Upgrade detection: if dedicated config already exists, skip setup entirely.
851
- // ensureNeo4jPassword() will verify the password is correct for this instance.
852
- const confCheck = spawnSync("test", ["-f", `${confDir}/neo4j.conf`], { stdio: "pipe" });
853
- if (confCheck.status === 0) {
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 setup`);
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
- // 1. Copy base config
869
- console.log(" [privileged] cp -r");
870
- shell("cp", ["-r", "/etc/neo4j", confDir], { sudo: true });
871
- // 2. Modify config for this instance: bolt port, HTTP port, data/log directories
872
- console.log(" [privileged] sed -i");
873
- shell("sed", ["-i", `s/^#\\?server\\.bolt\\.listen_address=.*/server.bolt.listen_address=:${NEO4J_PORT}/`, `${confDir}/neo4j.conf`], { sudo: true });
874
- console.log(" [privileged] sed -i");
875
- shell("sed", ["-i", `s/^#\\?server\\.http\\.listen_address=.*/server.http.listen_address=:${httpPort}/`, `${confDir}/neo4j.conf`], { sudo: true });
876
- console.log(" [privileged] sed -i");
877
- shell("sed", ["-i", `s|^#\\?server\\.directories\\.data=.*|server.directories.data=${dataDir}/data|`, `${confDir}/neo4j.conf`], { sudo: true });
878
- console.log(" [privileged] sed -i");
879
- shell("sed", ["-i", `s|^#\\?server\\.directories\\.logs=.*|server.directories.logs=${logDir}|`, `${confDir}/neo4j.conf`], { sudo: true });
880
- // Verify config was updated — sed silently no-ops if the key format changed
881
- const confContent = spawnSync("grep", [`server.bolt.listen_address=:${NEO4J_PORT}`, `${confDir}/neo4j.conf`], { stdio: "pipe" });
882
- if (confContent.status !== 0) {
883
- console.error(` WARNING: neo4j.conf may not have been updated correctly — bolt port ${NEO4J_PORT} not found in config`);
884
- logFile(` WARNING: sed verification failed — bolt port ${NEO4J_PORT} not found in ${confDir}/neo4j.conf`);
885
- }
886
- // 3. Create data and log directories
887
- console.log(" [privileged] mkdir -p");
888
- shell("mkdir", ["-p", `${dataDir}/data`, logDir], { sudo: true });
889
- console.log(" [privileged] chown -R");
890
- shell("chown", ["-R", "neo4j:neo4j", dataDir, logDir, confDir], { sudo: true });
891
- // 4. Create systemd service
892
- const serviceContent = `[Unit]
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
- const tmpServicePath = `/tmp/${serviceName}.service`;
909
- writeFileSync(tmpServicePath, serviceContent);
910
- console.log(" [privileged] cp");
911
- shell("cp", [tmpServicePath, `/etc/systemd/system/${serviceName}.service`], { sudo: true });
912
- spawnSync("rm", ["-f", tmpServicePath]);
913
- // 5. Set initial password before first start
914
- const password = randomBytes(24).toString("base64url");
915
- const persistDir = resolve(process.env.HOME ?? "/root", BRAND.configDir);
916
- mkdirSync(persistDir, { recursive: true });
917
- writeFileSync(join(persistDir, ".neo4j-password"), password, { mode: 0o600 });
918
- const configDir = resolve(INSTALL_DIR, "platform/config");
919
- mkdirSync(configDir, { recursive: true });
920
- writeFileSync(join(configDir, ".neo4j-password"), password, { mode: 0o600 });
921
- // sudo env VAR=val passes NEO4J_CONF through sudo's env_reset policy
922
- spawnSync("sudo", ["env", `NEO4J_CONF=${confDir}`, "neo4j-admin", "dbms", "set-initial-password", "--", password], {
923
- stdio: "inherit",
924
- });
925
- // 6. Enable and start the dedicated service
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
- // 7. Verify connectivity poll until cypher-shell can connect
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 connected = false;
969
+ let listening = false;
970
+ const portMatch = new RegExp(`:${NEO4J_PORT}\\b`);
934
971
  for (let i = 0; i < 15; i++) {
935
- const check = spawnSync("cypher-shell", [
936
- "-u", "neo4j", "-p", password,
937
- "-a", `bolt://localhost:${NEO4J_PORT}`,
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 (!connected) {
947
- // Diagnostic: check what's on this port
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
- console.error(` WARNING: Dedicated Neo4j instance not responding on bolt://localhost:${NEO4J_PORT}`);
952
- console.error(` Port ${NEO4J_PORT} status: ${diagnostic}`);
953
- logFile(` Neo4j dedicated: connectivity check FAILED on port ${NEO4J_PORT} — ${diagnostic}`);
954
- // Don't throw ensureNeo4jPassword() will retry
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
- shell("bash", [seedScript], { cwd: INSTALL_DIR });
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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@rubytech/create-maxy",
3
- "version": "1.0.738",
3
+ "version": "1.0.740",
4
4
  "description": "Install Maxy — AI for Productive People",
5
5
  "bin": {
6
6
  "create-maxy": "./dist/index.js"
@@ -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="${NEO4J_URI:-bolt://localhost:7687}"
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