arelos 0.1.2 → 0.2.1

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/ports.js CHANGED
@@ -1,24 +1,47 @@
1
1
  /**
2
2
  * Zero-dependency free-port detection via node:net (spec §1 Step 5).
3
3
  *
4
- * A port must be probed on BOTH loopback families: a server listening only on
5
- * [::1] (IPv6) leaves 127.0.0.1 bindable, so an IPv4-only probe reports the
6
- * port "free" while browsers (which resolve localhost to ::1 first) hit the
7
- * other process. Field-tested: a Node dev server on [::1]:1347 made the
8
- * installer pre-fill an occupied port. A port counts as free only if every
9
- * loopback family available on this machine can bind it.
4
+ * A port must be probed on every listener shape a process can occupy it
5
+ * with, not just the specific loopback addresses:
6
+ *
7
+ * - A server listening only on [::1] (IPv6) leaves 127.0.0.1 bindable, so an
8
+ * IPv4-only probe reports the port "free" while browsers (which resolve
9
+ * localhost to ::1 first) hit the other process. Field-tested: a Node dev
10
+ * server on [::1]:1347 made the installer pre-fill an occupied port.
11
+ * - A server listening on the WILDCARD address (e.g. `Bun.serve()` with no
12
+ * `hostname`, or `net.createServer().listen(port)` with no host) binds
13
+ * every local address in that family, including both loopback addresses,
14
+ * yet a bind-probe of 127.0.0.1/::1 alone can still report "free" on
15
+ * macOS: binding a specific address with SO_REUSEADDR (Node's default) can
16
+ * succeed even though a wildcard listener already holds the port. Real
17
+ * incident: `isPortFree(5274)` reported free while a Bun server held
18
+ * `*:5274`, and the installer wrote that occupied port into config.
19
+ *
20
+ * The fix probes both shapes with two independent techniques:
21
+ * 1. Bind-probes on the specific loopback addresses AND the wildcard
22
+ * addresses for each family. A wildcard bind-probe fails with
23
+ * EADDRINUSE whenever *any* listener (wildcard or specific) holds the
24
+ * port in that family — it is the strongest bind test available.
25
+ * 2. A short-timeout TCP connect-probe to 127.0.0.1 and ::1. A successful
26
+ * connection proves the port is occupied even in edge cases the bind
27
+ * probes can't reach; ECONNREFUSED proves free for that family; a
28
+ * timeout or other inconclusive result is never treated as occupied on
29
+ * its own.
30
+ *
31
+ * A port counts as free only if every applicable bind probe succeeds AND no
32
+ * connect probe manages to connect.
10
33
  */
11
- import { createServer } from "node:net";
34
+ import { createServer, connect as netConnect } from "node:net";
12
35
  /** Error codes meaning "this address family isn't available on this machine". */
13
36
  const FAMILY_UNAVAILABLE = new Set(["EADDRNOTAVAIL", "EAFNOSUPPORT", "EINVAL"]);
14
- /** Try to bind `port` on one loopback host and report what happened. */
37
+ /** Try to bind `port` on one host (specific address or wildcard) and report what happened. */
15
38
  function probeBind(port, host) {
16
39
  return new Promise((resolve) => {
17
40
  const server = createServer();
18
41
  server.once("error", (err) => {
19
42
  server.close();
20
43
  if (err.code && FAMILY_UNAVAILABLE.has(err.code)) {
21
- // No IPv6 (or IPv4) loopback on this machine — nothing to conflict with.
44
+ // No IPv6 (or IPv4) support on this machine — nothing to conflict with.
22
45
  resolve("family-unavailable");
23
46
  }
24
47
  else {
@@ -33,13 +56,58 @@ function probeBind(port, host) {
33
56
  server.listen(port, host);
34
57
  });
35
58
  }
59
+ const CONNECT_PROBE_TIMEOUT_MS = 250;
60
+ /**
61
+ * Try to TCP-connect to `host:port` with a short timeout. A successful
62
+ * connection means something is definitely listening there. ECONNREFUSED
63
+ * means nothing is listening (free, for that family). Anything else
64
+ * (timeout, unreachable, family unavailable) is inconclusive and must not be
65
+ * treated as proof of occupation by itself.
66
+ */
67
+ function probeConnect(port, host) {
68
+ return new Promise((resolve) => {
69
+ let settled = false;
70
+ let socket;
71
+ const finish = (result) => {
72
+ if (settled)
73
+ return;
74
+ settled = true;
75
+ socket.destroy();
76
+ resolve(result);
77
+ };
78
+ socket = netConnect({ port, host });
79
+ socket.setTimeout(CONNECT_PROBE_TIMEOUT_MS);
80
+ socket.once("connect", () => finish("occupied"));
81
+ socket.once("timeout", () => finish("inconclusive"));
82
+ socket.once("error", (err) => {
83
+ if (err.code === "ECONNREFUSED") {
84
+ finish("free");
85
+ }
86
+ else {
87
+ // EHOSTUNREACH, ENETUNREACH, family unavailable, etc. — inconclusive.
88
+ finish("inconclusive");
89
+ }
90
+ });
91
+ });
92
+ }
36
93
  /**
37
- * Resolve true if `port` is free to bind on ALL applicable loopback families
38
- * (127.0.0.1 and ::1), false if any listener holds it on either family.
94
+ * Resolve true if `port` is free on every probe: bind-probes on the specific
95
+ * loopback addresses and the wildcard addresses for both families, plus
96
+ * connect-probes to the loopback addresses. False if any bind probe reports
97
+ * "taken" or any connect probe successfully connects.
39
98
  */
40
99
  export async function isPortFree(port) {
41
- const [v4, v6] = await Promise.all([probeBind(port, "127.0.0.1"), probeBind(port, "::1")]);
42
- return v4 !== "taken" && v6 !== "taken";
100
+ const [v4Loopback, v6Loopback, v4Wildcard, v6Wildcard, v4Connect, v6Connect] = await Promise.all([
101
+ probeBind(port, "127.0.0.1"),
102
+ probeBind(port, "::1"),
103
+ probeBind(port, "0.0.0.0"),
104
+ probeBind(port, "::"),
105
+ probeConnect(port, "127.0.0.1"),
106
+ probeConnect(port, "::1"),
107
+ ]);
108
+ const bindsOk = [v4Loopback, v6Loopback, v4Wildcard, v6Wildcard].every((result) => result !== "taken");
109
+ const noConnectSucceeded = v4Connect !== "occupied" && v6Connect !== "occupied";
110
+ return bindsOk && noConnectSucceeded;
43
111
  }
44
112
  /**
45
113
  * Find the first free port at or above `start`, scanning upward. Stops at
@@ -0,0 +1,41 @@
1
+ /**
2
+ * Global multi-install registry (~/.arelos/installs.json, or
3
+ * ARELOS_REGISTRY_PATH override). Each entry records one named, self-contained
4
+ * install so `arelos` can find/list/select among several installs on one Mac
5
+ * without any of them clobbering a shared config file (0.2.0 mission).
6
+ */
7
+ import { existsSync, mkdirSync, readFileSync, renameSync, writeFileSync } from "node:fs";
8
+ import { dirname } from "node:path";
9
+ import { registryPath } from "./paths.js";
10
+ export function readRegistry() {
11
+ const p = registryPath();
12
+ if (!existsSync(p))
13
+ return [];
14
+ const raw = JSON.parse(readFileSync(p, "utf8"));
15
+ if (!Array.isArray(raw)) {
16
+ throw new Error(`Invalid registry at ${p}: expected an array`);
17
+ }
18
+ return raw;
19
+ }
20
+ /** Atomic write: tmp file + rename, so a reader never sees a partial file. */
21
+ function writeRegistry(entries) {
22
+ const p = registryPath();
23
+ mkdirSync(dirname(p), { recursive: true });
24
+ const tmp = `${p}.tmp`;
25
+ writeFileSync(tmp, `${JSON.stringify(entries, null, 2)}\n`, { mode: 0o600 });
26
+ renameSync(tmp, p);
27
+ }
28
+ /** Append (or replace, by matching root) an entry — a reinstall at the same root updates it in place. */
29
+ export function addRegistryEntry(entry) {
30
+ const entries = readRegistry().filter((e) => e.root !== entry.root);
31
+ entries.push(entry);
32
+ writeRegistry(entries);
33
+ }
34
+ /** Remove the entry for a given root (uninstall). No-op if absent. */
35
+ export function removeRegistryEntry(root) {
36
+ const entries = readRegistry().filter((e) => e.root !== root);
37
+ writeRegistry(entries);
38
+ }
39
+ export function findRegistryEntryByName(name) {
40
+ return readRegistry().find((e) => e.name === name || e.slug === name) ?? null;
41
+ }
package/dist/repair.js CHANGED
@@ -5,9 +5,10 @@
5
5
  */
6
6
  import * as p from "@clack/prompts";
7
7
  import pc from "picocolors";
8
- import { resolveServiceLabels } from "./config.js";
8
+ import { resolveRoot, resolveServiceLabels } from "./config.js";
9
9
  import { runUpdate } from "./update.js";
10
- import { waitForHealthy } from "./health.js";
10
+ import { formatHealthTimeoutDiagnostics, waitForHealthy } from "./health.js";
11
+ import { lastLines, logPathFor } from "./logs.js";
11
12
  import { bootstrapAndStart, installServiceFiles } from "./services.js";
12
13
  import { runStreaming } from "./exec.js";
13
14
  import { ensureBun } from "./bun-setup.js";
@@ -40,9 +41,8 @@ export async function runRepairMenu(existing) {
40
41
  return runRepair(existing);
41
42
  }
42
43
  if (choice === "reinstall") {
43
- p.log.message("Reinstall elsewhere is not automatic yet run `arelos uninstall` first if you want to reuse this " +
44
- "install dir/ports, or re-run `npx arelos --install-dir <new path> --web-port <n> --vault-port <n>` " +
45
- "for a side-by-side install with a different ARELOS_CONFIG_PATH.");
44
+ p.log.message("Just re-run `npx arelos` with a different name every install is self-contained, so a new name " +
45
+ "creates a fresh, independent install side-by-side with this one (see `arelos list`).");
46
46
  return 0;
47
47
  }
48
48
  return 1;
@@ -69,13 +69,17 @@ async function runRepair(existing) {
69
69
  s.stop("Rebuilt.");
70
70
  s.start("Re-rendering and re-bootstrapping services…");
71
71
  const labels = resolveServiceLabels(existing);
72
- installServiceFiles(existing.installDir, labels);
72
+ installServiceFiles(existing.installDir, resolveRoot(existing), labels);
73
73
  const bootstrap = await bootstrapAndStart(labels);
74
74
  s.stop(bootstrap.errors.length ? "Services re-registered with warnings." : "Services re-registered.");
75
75
  for (const e of bootstrap.errors)
76
76
  console.error(pc.yellow(e));
77
77
  s.start("Health check…");
78
78
  const health = await waitForHealthy(existing.webPort, existing.vaultPort);
79
- s.stop(health.healthy ? "Healthy." : "Health check timed out — see arelos logs.");
79
+ s.stop(health.healthy ? "Healthy." : "Health check timed out.");
80
+ if (!health.healthy) {
81
+ console.error(pc.red(formatHealthTimeoutDiagnostics(resolveRoot(existing), (p) => lastLines(p, 10), logPathFor)));
82
+ console.error(pc.dim("\nFull logs: arelos logs"));
83
+ }
80
84
  return health.healthy ? 0 : 1;
81
85
  }
package/dist/repo.js CHANGED
@@ -1,7 +1,7 @@
1
1
  /**
2
- * Getting the app source onto disk (spec §1 Step 8) and keeping it updated
3
- * (spec §2 rlo update). Supports a `--local-repo <path>` override for
4
- * development/dry-run testing so we never have to hit GitHub in tests.
2
+ * Getting the app source onto disk and keeping it updated (`arelos update`).
3
+ * Supports a `--local-repo <path>` override for development/dry-run testing
4
+ * so we never have to hit GitHub in tests.
5
5
  */
6
6
  import { existsSync } from "node:fs";
7
7
  import { join } from "node:path";
@@ -40,7 +40,7 @@ export async function currentRevision(installDir) {
40
40
  const res = await runCapture("git", ["-C", installDir, "rev-parse", "--short", "HEAD"]);
41
41
  return res.code === 0 ? res.stdout.trim() : null;
42
42
  }
43
- /** Best-effort "is the local branch behind origin" check (spec §2 rlo status). */
43
+ /** Best-effort "is the local branch behind origin" check (`arelos status`). */
44
44
  export async function isBehindOrigin(installDir) {
45
45
  const fetchRes = await runCapture("git", ["-C", installDir, "fetch", "--dry-run"]);
46
46
  if (fetchRes.code !== 0)
package/dist/scaffold.js CHANGED
@@ -1,14 +1,13 @@
1
1
  /**
2
- * Vault scaffolding + supporting install-dir setup (spec §1 Step 9).
3
- * Only copies the template vault when the destination is empty — never
4
- * overwrites user data (spec §3.2 idempotency rule).
2
+ * Vault scaffolding + supporting install-dir setup. Only copies the template
3
+ * vault when the destination is empty — never overwrites user data.
5
4
  */
6
5
  import { cpSync, existsSync, mkdirSync, readdirSync } from "node:fs";
7
6
  import { join } from "node:path";
8
7
  export class TemplateVaultMissingError extends Error {
9
8
  constructor(templateDir) {
10
9
  super(`templates/vault/ is missing at ${templateDir}. This is a repo gap — ` +
11
- `the app repo must ship a seed vault at templates/vault/ (see portability-contract.md / rlo-cli-spec.md §1 Step 9). Cannot scaffold a new vault without it.`);
10
+ `the app repo must ship a seed vault at templates/vault/. Cannot scaffold a new vault without it.`);
12
11
  this.name = "TemplateVaultMissingError";
13
12
  }
14
13
  }
@@ -34,10 +33,11 @@ export function scaffoldVault(installDir, vaultPath) {
34
33
  cpSync(templateDir, vaultPath, { recursive: true });
35
34
  return { copied: true };
36
35
  }
37
- /** Create <installDir>/logs/service/ — must exist before launchd bootstraps
38
- * the plists, since they reference it for stdout/stderr (spec, implementer notes). */
39
- export function ensureLogsDir(installDir) {
40
- const dir = join(installDir, "logs", "service");
36
+ /** Create <root>/logs/service/ — must exist before launchd bootstraps the
37
+ * plists, since they reference it for stdout/stderr. Logs live at the
38
+ * self-contained root, not inside the app checkout (0.2.0 layout). */
39
+ export function ensureLogsDir(root) {
40
+ const dir = join(root, "logs", "service");
41
41
  mkdirSync(dir, { recursive: true });
42
42
  return dir;
43
43
  }
package/dist/services.js CHANGED
@@ -1,14 +1,18 @@
1
1
  /**
2
- * Ties together plist rendering + writing + launchd bootstrap (spec §1 Step 11,
3
- * §3.1 Repair). Also used by `--no-service` dry runs, which stop before the
4
- * launchctl calls (see install.ts).
2
+ * Ties together plist rendering + writing + launchd bootstrap. Also used by
3
+ * `--no-service` dry runs, which stop before the launchctl calls (see
4
+ * install.ts).
5
+ *
6
+ * `installDir` is the app checkout (root/app); `root` is the self-contained
7
+ * install root that owns logs/ and config.json (0.2.0 layout). Both are
8
+ * needed because the plist bakes in both {{INSTALL_DIR}} and {{ROOT_DIR}}.
5
9
  */
6
10
  import { chmodSync, existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
7
11
  import { join } from "node:path";
8
12
  import { bootstrapService, kickstartService } from "./launchd.js";
9
13
  import { deriveServiceLabels, launchAgentsDir, plistPath } from "./paths.js";
10
14
  import { renderPlistTemplate } from "./plist.js";
11
- export function renderServicePlists(installDir, labels = deriveServiceLabels(installDir)) {
15
+ export function renderServicePlists(installDir, root, labels = deriveServiceLabels(root)) {
12
16
  const specs = [
13
17
  { label: labels.web, templateFile: "web.plist.tmpl" },
14
18
  { label: labels.vault, templateFile: "vault.plist.tmpl" },
@@ -19,22 +23,22 @@ export function renderServicePlists(installDir, labels = deriveServiceLabels(ins
19
23
  throw new Error(`Missing plist template: ${templatePath}`);
20
24
  }
21
25
  const template = readFileSync(templatePath, "utf8");
22
- const xml = renderPlistTemplate(template, installDir, label);
26
+ const xml = renderPlistTemplate(template, installDir, root, label);
23
27
  return { label, templatePath, targetPath: plistPath(label), xml };
24
28
  });
25
29
  }
26
30
  /** Write rendered plists to ~/Library/LaunchAgents and chmod the run scripts. */
27
- export function installServiceFiles(installDir, labels = deriveServiceLabels(installDir)) {
31
+ export function installServiceFiles(installDir, root, labels = deriveServiceLabels(root)) {
28
32
  mkdirSync(launchAgentsDir(), { recursive: true });
29
- // The plists' StandardOutPath/StandardErrorPath point at <installDir>/logs/service/*.log;
33
+ // The plists' StandardOutPath/StandardErrorPath point at <root>/logs/service/*.log;
30
34
  // launchd needs that directory to exist before it can bootstrap the job (a
31
35
  // missing log dir is one confirmed cause of the "Bootstrap failed: 5:
32
36
  // Input/output error" field report — see launchd.ts docstring). install.ts
33
37
  // already calls ensureLogsDir before this, but repair/update funnel through
34
38
  // here too, so guarantee it unconditionally rather than relying on callers
35
39
  // to remember.
36
- mkdirSync(join(installDir, "logs", "service"), { recursive: true });
37
- const rendered = renderServicePlists(installDir, labels);
40
+ mkdirSync(join(root, "logs", "service"), { recursive: true });
41
+ const rendered = renderServicePlists(installDir, root, labels);
38
42
  for (const svc of rendered) {
39
43
  writeFileSync(svc.targetPath, svc.xml);
40
44
  }
package/dist/status.js CHANGED
@@ -1,19 +1,24 @@
1
1
  /**
2
- * `rlo status` (spec §2). Config summary + service state + port probes +
3
- * git revision / behind-origin check.
2
+ * `arelos status`. Config summary + service state + port probes + git revision /
3
+ * behind-origin check. With multiple installs registered, resolves by name
4
+ * (or prompts/lists per resolveInstall's rule).
4
5
  */
5
6
  import pc from "picocolors";
6
- import { readConfig, resolveServiceLabels } from "./config.js";
7
+ import { resolveServiceLabels } from "./config.js";
8
+ import { resolveInstall } from "./cli-context.js";
7
9
  import { checkVaultHealth, checkWebHealth } from "./health.js";
8
10
  import { getServiceStatus } from "./launchd.js";
9
11
  import { currentRevision, isBehindOrigin } from "./repo.js";
10
- export async function statusCommand() {
11
- const config = readConfig();
12
- if (!config) {
13
- console.error("No Arel OS install found. Run `npx arelos` to install.");
12
+ export async function statusCommand(name) {
13
+ const result = await resolveInstall({ name, interactive: process.stdout.isTTY === true });
14
+ if (!result.ok) {
15
+ console.error(result.message);
14
16
  return 1;
15
17
  }
18
+ const config = result.install.config;
16
19
  console.log(pc.bold(config.displayName));
20
+ if (config.root)
21
+ console.log(` Root: ${config.root}`);
17
22
  console.log(` Install dir: ${config.installDir}`);
18
23
  console.log(` Vault path: ${config.vaultPath}`);
19
24
  console.log(` Web port: ${config.webPort}`);
package/dist/uninstall.js CHANGED
@@ -1,31 +1,41 @@
1
1
  /**
2
- * `rlo uninstall` (spec §2). Vault deletion is gated behind confirm + a
3
- * literal typed "DELETE" — a single yes can never destroy notes (spec §3.2,
4
- * acceptance criterion 5). This module separates the pure decision logic
5
- * (shouldDeleteVault) from the destructive I/O (performUninstall) so the
6
- * gate can be unit tested without touching a filesystem.
2
+ * `arelos uninstall`. Vault deletion is gated behind confirm + a literal typed
3
+ * "DELETE" — a single yes can never destroy notes. This module separates the
4
+ * pure decision logic (shouldDeleteVault) from the destructive I/O
5
+ * (performUninstall) so the gate can be unit tested without touching a
6
+ * filesystem.
7
+ *
8
+ * 0.2.0 self-contained layout: installDir (root/app) and vaultPath (root/vault)
9
+ * are siblings under root, so removing installDir never touches the vault —
10
+ * the pre-0.2.0 "install dir vs vault" gate semantics carry over unchanged.
11
+ * Removing the registry entry is separate from folder/vault deletion (always
12
+ * done, since a stale registry entry pointing at a gone or kept-around
13
+ * install is never useful).
7
14
  */
8
15
  import * as p from "@clack/prompts";
9
16
  import pc from "picocolors";
10
17
  import { existsSync, rmSync, unlinkSync } from "node:fs";
11
- import { readConfig, resolveServiceLabels } from "./config.js";
18
+ import { resolveRoot, resolveServiceLabels } from "./config.js";
19
+ import { resolveInstall } from "./cli-context.js";
12
20
  import { bootoutService } from "./launchd.js";
13
- import { plistPath, configPath } from "./paths.js";
21
+ import { plistPath, installConfigPath, legacyConfigPath } from "./paths.js";
22
+ import { removeRegistryEntry } from "./registry.js";
14
23
  /**
15
24
  * Pure gate: vault is deleted iff confirmed AND the typed word is exactly
16
25
  * "DELETE". Any other input (including case variants, whitespace, empty)
17
- * preserves the vault. This is the load-bearing safety rule from the spec
18
- * kept as an isolated pure function so it's trivially unit-testable.
26
+ * preserves the vault. This is the load-bearing safety rule kept as an
27
+ * isolated pure function so it's trivially unit-testable.
19
28
  */
20
29
  export function shouldDeleteVault(confirmed, typedWord) {
21
30
  return confirmed === true && typedWord === "DELETE";
22
31
  }
23
- export async function uninstallCommand() {
24
- const config = readConfig();
25
- if (!config) {
26
- console.error("No Arel OS install found. Nothing to uninstall.");
32
+ export async function uninstallCommand(name) {
33
+ const result = await resolveInstall({ name, interactive: process.stdout.isTTY === true });
34
+ if (!result.ok) {
35
+ console.error(result.message);
27
36
  return 1;
28
37
  }
38
+ const { config, root } = result.install;
29
39
  p.intro(pc.bold("Uninstall Arel OS"));
30
40
  const labels = resolveServiceLabels(config);
31
41
  await bootoutService(labels.web);
@@ -60,26 +70,37 @@ export async function uninstallCommand() {
60
70
  }
61
71
  }
62
72
  const removeConfig = await p.confirm({
63
- message: "Remove the saved config (~/.arelos/config.json)? Keeping it lets a reinstall remember your settings.",
73
+ message: "Remove the saved config? Keeping it lets a reinstall remember your settings.",
64
74
  initialValue: false,
65
75
  });
66
- performUninstall(config.installDir, config.vaultPath, {
76
+ performUninstall(config.installDir, config.vaultPath, resolveRoot(config), {
67
77
  removeInstallDir: !p.isCancel(removeInstallDir) && removeInstallDir,
68
78
  deleteVault,
69
79
  removeConfig: !p.isCancel(removeConfig) && removeConfig,
70
80
  });
81
+ if (root)
82
+ removeRegistryEntry(root);
71
83
  p.outro(pc.green("Arel OS uninstalled." + (deleteVault ? "" : " Your vault was preserved.")));
72
84
  return 0;
73
85
  }
74
- /** Destructive I/O, isolated from prompt flow for testability via direct calls with explicit choices. */
75
- export function performUninstall(installDir, vaultPath, choices) {
86
+ /**
87
+ * Destructive I/O, isolated from prompt flow for testability via direct
88
+ * calls with explicit choices. `root` is passed separately from `installDir`
89
+ * so config.json (which lives at root, a parent of installDir) is removed
90
+ * correctly rather than assumed to live inside installDir.
91
+ */
92
+ export function performUninstall(installDir, vaultPath, root, choices) {
76
93
  if (choices.deleteVault && existsSync(vaultPath)) {
77
94
  rmSync(vaultPath, { recursive: true, force: true });
78
95
  }
79
96
  if (choices.removeInstallDir && existsSync(installDir)) {
80
97
  rmSync(installDir, { recursive: true, force: true });
81
98
  }
82
- if (choices.removeConfig && existsSync(configPath())) {
83
- unlinkSync(configPath());
99
+ if (choices.removeConfig) {
100
+ const cfgPath = installConfigPath(root);
101
+ if (existsSync(cfgPath))
102
+ unlinkSync(cfgPath);
103
+ if (existsSync(legacyConfigPath()))
104
+ unlinkSync(legacyConfigPath());
84
105
  }
85
106
  }
package/dist/update.js CHANGED
@@ -1,21 +1,23 @@
1
1
  /**
2
- * `rlo update` (spec §2). git pull --ff-only + bun install/build + restart +
3
- * re-health-check. Config file untouched.
2
+ * `arelos update`. git pull --ff-only + bun install/build + restart +
3
+ * re-health-check. Config file untouched. With multiple installs registered,
4
+ * resolves by name (or prompts/lists per resolveInstall's rule).
4
5
  */
5
6
  import pc from "picocolors";
6
7
  import { ensureBun } from "./bun-setup.js";
7
- import { readConfig, resolveServiceLabels } from "./config.js";
8
+ import { resolveRoot, resolveServiceLabels } from "./config.js";
9
+ import { resolveInstall } from "./cli-context.js";
8
10
  import { runStreaming } from "./exec.js";
9
11
  import { waitForHealthy } from "./health.js";
10
12
  import { pullLatest } from "./repo.js";
11
13
  import { bootstrapAndStart, installServiceFiles } from "./services.js";
12
- export async function updateCommand() {
13
- const config = readConfig();
14
- if (!config) {
15
- console.error("No Arel OS install found. Run `npx arelos` to install.");
14
+ export async function updateCommand(name) {
15
+ const result = await resolveInstall({ name, interactive: process.stdout.isTTY === true });
16
+ if (!result.ok) {
17
+ console.error(result.message);
16
18
  return 1;
17
19
  }
18
- return runUpdate(config);
20
+ return runUpdate(result.install.config);
19
21
  }
20
22
  export async function runUpdate(config) {
21
23
  console.log(`Updating ${config.installDir}…`);
@@ -45,7 +47,7 @@ export async function runUpdate(config) {
45
47
  console.warn(pc.yellow("bun run build failed — keeping old dist/. The web service will keep serving it."));
46
48
  }
47
49
  const labels = resolveServiceLabels(config);
48
- installServiceFiles(config.installDir, labels);
50
+ installServiceFiles(config.installDir, resolveRoot(config), labels);
49
51
  const bootstrap = await bootstrapAndStart(labels);
50
52
  for (const e of bootstrap.errors)
51
53
  console.error(pc.yellow(e));
package/package.json CHANGED
@@ -1,11 +1,10 @@
1
1
  {
2
2
  "name": "arelos",
3
- "version": "0.1.2",
3
+ "version": "0.2.1",
4
4
  "description": "Installer and service manager for a self-hosted Arel OS on macOS.",
5
5
  "type": "module",
6
6
  "bin": {
7
- "arelos": "dist/cli.js",
8
- "rlo": "dist/cli.js"
7
+ "arelos": "dist/cli.js"
9
8
  },
10
9
  "files": [
11
10
  "dist"