arelos 0.1.0 → 0.1.2

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/cli.js CHANGED
File without changes
package/dist/config.js CHANGED
@@ -5,7 +5,7 @@
5
5
  */
6
6
  import { existsSync, mkdirSync, readFileSync, renameSync, writeFileSync } from "node:fs";
7
7
  import { dirname } from "node:path";
8
- import { configPath } from "./paths.js";
8
+ import { configPath, LEGACY_VAULT_LABEL, LEGACY_WEB_LABEL } from "./paths.js";
9
9
  export function readConfig() {
10
10
  const p = configPath();
11
11
  if (!existsSync(p))
@@ -28,3 +28,11 @@ export function writeConfig(config) {
28
28
  writeFileSync(tmp, `${JSON.stringify(config, null, 2)}\n`, { mode: 0o600 });
29
29
  renameSync(tmp, p);
30
30
  }
31
+ /**
32
+ * The labels to actually operate on for an existing install: config.serviceLabels
33
+ * when present (installs made after this fix), else the legacy fixed labels
34
+ * (installs made before it — backward compat per spec).
35
+ */
36
+ export function resolveServiceLabels(config) {
37
+ return config.serviceLabels ?? { web: LEGACY_WEB_LABEL, vault: LEGACY_VAULT_LABEL };
38
+ }
@@ -6,7 +6,7 @@
6
6
  import { existsSync, readdirSync, statSync } from "node:fs";
7
7
  import { accessSync, constants } from "node:fs";
8
8
  import { dirname } from "node:path";
9
- import { expandHome } from "./paths.js";
9
+ import { deriveServiceLabels, expandHome } from "./paths.js";
10
10
  import { findFreePort, isValidPort } from "./ports.js";
11
11
  export const DEFAULTS = {
12
12
  displayName: "Arel OS",
@@ -19,6 +19,26 @@ export function normalizeDisplayName(input) {
19
19
  const trimmed = input.trim();
20
20
  return trimmed.length > 0 ? trimmed : DEFAULTS.displayName;
21
21
  }
22
+ /**
23
+ * Slugify a chosen display name into a safe directory-name fragment:
24
+ * lowercase, spaces/unsafe chars -> dashes, collapsed and trimmed. Falls back
25
+ * to the fixed default install dir's basename when the slug would be empty
26
+ * (e.g. a name made entirely of emoji/punctuation).
27
+ */
28
+ export function slugifyName(input) {
29
+ return input
30
+ .toLowerCase()
31
+ .trim()
32
+ .replace(/[^a-z0-9]+/g, "-")
33
+ .replace(/^-+|-+$/g, "");
34
+ }
35
+ /** Default install dir derived from the chosen display name, e.g. "My Brain" -> "~/my-brain". */
36
+ export function defaultInstallDirFor(displayName) {
37
+ const slug = slugifyName(displayName);
38
+ if (!slug)
39
+ return DEFAULTS.installDir;
40
+ return `~/${slug}`;
41
+ }
22
42
  export function checkInstallDir(rawPath) {
23
43
  const path = expandHome(rawPath);
24
44
  const parent = dirname(path);
@@ -55,12 +75,14 @@ export async function resolvePort(requested) {
55
75
  return { requested, resolved: suggestion, wasFree: false };
56
76
  }
57
77
  export function toArelConfig(answers) {
78
+ const installDir = expandHome(answers.installDir);
58
79
  return {
59
80
  version: 1,
60
81
  displayName: answers.displayName,
61
- installDir: expandHome(answers.installDir),
82
+ installDir,
62
83
  vaultPath: expandHome(answers.vaultPath),
63
84
  webPort: answers.webPort,
64
85
  vaultPort: answers.vaultPort,
86
+ serviceLabels: deriveServiceLabels(installDir),
65
87
  };
66
88
  }
package/dist/install.js CHANGED
@@ -9,12 +9,13 @@ import { ensureBun } from "./bun-setup.js";
9
9
  import { readConfig, writeConfig } from "./config.js";
10
10
  import { runStreaming } from "./exec.js";
11
11
  import { waitForHealthy } from "./health.js";
12
- import { checkInstallDir, defaultVaultPath, DEFAULTS, normalizeDisplayName, resolvePort, toArelConfig, } from "./install-plan.js";
12
+ import { checkInstallDir, defaultInstallDirFor, defaultVaultPath, DEFAULTS, normalizeDisplayName, resolvePort, slugifyName, toArelConfig, } from "./install-plan.js";
13
13
  import { bootstrapAndStart, installServiceFiles } from "./services.js";
14
14
  import { cloneRepo, isGitCheckout } from "./repo.js";
15
15
  import { ensureEnvFile, ensureLogsDir, scaffoldVault, TemplateVaultMissingError } from "./scaffold.js";
16
16
  import { runRepairMenu } from "./repair.js";
17
- import { configPath } from "./paths.js";
17
+ import { configPath, deriveServiceLabels } from "./paths.js";
18
+ import { listLoadedArelosLabels } from "./launchd.js";
18
19
  export async function runInstall(argv, flags) {
19
20
  // Step 0 — Preflight & existing-install detection.
20
21
  if (process.platform !== "darwin") {
@@ -125,11 +126,24 @@ export async function runInstall(argv, flags) {
125
126
  p.outro(pc.green("Dry-run install complete (no services started)."));
126
127
  return 0;
127
128
  }
129
+ // Step 10.5 — Preflight: check for a same-slug reinstall vs. an unrelated
130
+ // Arel OS install already holding other com.arelos.* labels.
131
+ const labels = config.serviceLabels ?? deriveServiceLabels(installDir);
132
+ const loadedLabels = await listLoadedArelosLabels();
133
+ const oursAlreadyLoaded = loadedLabels.filter((l) => l === labels.web || l === labels.vault);
134
+ const othersLoaded = loadedLabels.filter((l) => l !== labels.web && l !== labels.vault);
135
+ if (oursAlreadyLoaded.length > 0) {
136
+ log(flags.yes, `Services for this install dir are already loaded (${oursAlreadyLoaded.join(", ")}) — will bootout and re-bootstrap.`);
137
+ }
138
+ if (othersLoaded.length > 0) {
139
+ console.error(pc.yellow(`Another Arel OS install is already running with its own services (${othersLoaded.join(", ")}). ` +
140
+ "This install uses its own unique labels, so it will not disturb that install."));
141
+ }
128
142
  // Step 11 — Generate + bootstrap launchd services.
129
143
  const svcSpin = spinner(flags.yes);
130
144
  svcSpin.start("Registering background services…");
131
- installServiceFiles(installDir);
132
- const bootstrapResult = await bootstrapAndStart();
145
+ installServiceFiles(installDir, labels);
146
+ const bootstrapResult = await bootstrapAndStart(labels);
133
147
  if (bootstrapResult.errors.length > 0) {
134
148
  svcSpin.stop("Service registration had errors.");
135
149
  for (const e of bootstrapResult.errors)
@@ -159,7 +173,7 @@ export async function runInstall(argv, flags) {
159
173
  }
160
174
  }
161
175
  p.outro([
162
- pc.green(`Arel OS is running at ${url}`),
176
+ pc.green(`${answers.displayName} is running at ${url}`),
163
177
  "Runs 24/7 in the background.",
164
178
  "Next: arelos status · arelos logs · arelos update · arelos uninstall",
165
179
  ].join("\n"));
@@ -175,7 +189,7 @@ async function checkGit() {
175
189
  async function collectAnswers(flags) {
176
190
  if (flags.yes) {
177
191
  const displayName = normalizeDisplayName(flags.displayName ?? DEFAULTS.displayName);
178
- const installDir = flags.installDir ?? DEFAULTS.installDir;
192
+ const installDir = flags.installDir ?? defaultInstallDirFor(displayName);
179
193
  const vaultPath = flags.vaultPath ?? defaultVaultPath(installDir);
180
194
  const webPortReq = flags.webPort ?? DEFAULTS.webPort;
181
195
  const vaultPortReq = flags.vaultPort ?? DEFAULTS.vaultPort;
@@ -197,28 +211,45 @@ async function collectAnswers(flags) {
197
211
  });
198
212
  if (p.isCancel(displayNameRaw))
199
213
  return null;
214
+ const displayName = normalizeDisplayName(String(displayNameRaw));
215
+ const installDirDefault = defaultInstallDirFor(displayName);
200
216
  // Step 3 — Install location.
201
217
  let installDir = "";
202
218
  for (;;) {
203
219
  const raw = await p.text({
204
- message: "Where should Arel OS be installed?",
205
- placeholder: DEFAULTS.installDir,
206
- defaultValue: DEFAULTS.installDir,
220
+ message: `Where should ${displayName} be installed?`,
221
+ placeholder: installDirDefault,
222
+ defaultValue: installDirDefault,
207
223
  });
208
224
  if (p.isCancel(raw))
209
225
  return null;
210
- const check = checkInstallDir(String(raw || DEFAULTS.installDir));
226
+ const check = checkInstallDir(String(raw || installDirDefault));
211
227
  if (!check.parentWritable) {
212
228
  p.log.error(`Cannot write to ${check.path} — choose another location.`);
213
229
  continue;
214
230
  }
215
231
  if (check.nonEmpty && !check.isPriorArelosInstall) {
216
- const proceed = await p.confirm({
217
- message: `${check.path} exists and is not empty. Use it anyway?`,
218
- initialValue: false,
232
+ const subfolder = `${check.path}/${slugifyName(displayName) || "arelos"}`;
233
+ const choice = await p.select({
234
+ message: `${check.path} already has files in it, so installing there would fail.`,
235
+ options: [
236
+ { value: "subfolder", label: `Install into ${subfolder} instead`, hint: "recommended" },
237
+ { value: "different", label: "Choose a different location" },
238
+ { value: "cancel", label: "Cancel install" },
239
+ ],
240
+ initialValue: "subfolder",
219
241
  });
220
- if (p.isCancel(proceed) || !proceed)
242
+ if (p.isCancel(choice) || choice === "cancel")
243
+ return null;
244
+ if (choice === "different")
245
+ continue;
246
+ const subCheck = checkInstallDir(subfolder);
247
+ if (subCheck.nonEmpty && !subCheck.isPriorArelosInstall) {
248
+ p.log.error(`${subCheck.path} also already has files in it — choose another location.`);
221
249
  continue;
250
+ }
251
+ installDir = subCheck.path;
252
+ break;
222
253
  }
223
254
  installDir = check.path;
224
255
  break;
@@ -239,7 +270,7 @@ async function collectAnswers(flags) {
239
270
  if (vaultPort === null)
240
271
  return null;
241
272
  return {
242
- displayName: normalizeDisplayName(String(displayNameRaw)),
273
+ displayName,
243
274
  installDir,
244
275
  vaultPath: String(vaultRaw || defaultVaultPath(installDir)),
245
276
  webPort,
package/dist/launchd.js CHANGED
@@ -2,6 +2,29 @@
2
2
  * launchctl wrapper (modern per-GUI-domain bootstrap/bootout/kickstart API).
3
3
  * Every op is idempotent per spec §3.2: bootout-then-bootstrap never errors
4
4
  * on "already loaded"; a --no-service dry run skips all of this.
5
+ *
6
+ * Field report conclusion — "Bootstrap failed: 5: Input/output error":
7
+ * launchd's errno 5 (EIO) here is a catch-all it surfaces for several distinct
8
+ * setup failures, not just "job already loaded". Two concrete causes were
9
+ * found and fixed in this codebase:
10
+ * 1. Label collision (the actual reported bug) — a second install on the
11
+ * same Mac reused the fixed labels com.arelos.web/.vault. Bootstrapping
12
+ * a plist under a label already claimed by a *different* plist path
13
+ * (the first install's) is exactly the kind of state launchd reports as
14
+ * an I/O error rather than a clean "already exists". Fixed by deriving a
15
+ * per-installDir label (paths.ts deriveServiceLabels) so two installs
16
+ * never share a label.
17
+ * 2. Missing log directory — the plists' StandardOutPath/StandardErrorPath
18
+ * point at <installDir>/logs/service/*.log. If that directory doesn't
19
+ * exist yet, launchd can fail to set up the job's stdio redirection at
20
+ * bootstrap/first-spawn time. install.ts already called ensureLogsDir
21
+ * before registering services, but repair.ts/update.ts funneled straight
22
+ * into installServiceFiles without it — a real gap for any repair/update
23
+ * run where logs/service/ had been deleted or never existed. Fixed by
24
+ * making installServiceFiles itself create the directory unconditionally
25
+ * (see services.ts), so all three callers get it for free.
26
+ * The domain string ("gui/<uid>") and the bootout-before-bootstrap idempotency
27
+ * pattern were checked and are correct as written — not a contributor here.
5
28
  */
6
29
  import { userInfo } from "node:os";
7
30
  import { runCapture } from "./exec.js";
@@ -44,3 +67,15 @@ export async function getServiceStatus(label) {
44
67
  export function guiDomainForTest() {
45
68
  return guiDomain();
46
69
  }
70
+ /**
71
+ * All currently-loaded launchd labels starting with "com.arelos." — used by
72
+ * install preflight to detect a same-slug reinstall vs. an unrelated,
73
+ * still-running Arel OS install that grabbed different labels first.
74
+ */
75
+ export async function listLoadedArelosLabels() {
76
+ const res = await runCapture("launchctl", ["list"]);
77
+ return res.stdout
78
+ .split("\n")
79
+ .map((l) => l.trim().split(/\s+/).pop())
80
+ .filter((label) => !!label && label.startsWith("com.arelos."));
81
+ }
package/dist/paths.js CHANGED
@@ -3,6 +3,7 @@
3
3
  * Everything here honors ARELOS_CONFIG_PATH so tests/dry-runs never touch
4
4
  * the real ~/.arelos.
5
5
  */
6
+ import { createHash } from "node:crypto";
6
7
  import { homedir } from "node:os";
7
8
  import { join } from "node:path";
8
9
  export function configPath() {
@@ -14,8 +15,30 @@ export function configDir() {
14
15
  export function launchAgentsDir() {
15
16
  return join(homedir(), "Library", "LaunchAgents");
16
17
  }
17
- export const WEB_LABEL = "com.arelos.web";
18
- export const VAULT_LABEL = "com.arelos.vault";
18
+ /**
19
+ * Pre-per-install-label launchd labels. A second install on the same Mac used
20
+ * to collide on these fixed labels — see serviceLabels below for the fix.
21
+ * Kept as the fallback for configs written before this fix (spec: legacy
22
+ * compat) and as the thing preflight checks for "another install exists".
23
+ */
24
+ export const LEGACY_WEB_LABEL = "com.arelos.web";
25
+ export const LEGACY_VAULT_LABEL = "com.arelos.vault";
26
+ /**
27
+ * Short stable hash of the resolved installDir: same dir -> same slug across
28
+ * reinstalls/repairs, different dirs -> different slugs, so two installs on
29
+ * one Mac never fight over the same launchd label.
30
+ */
31
+ export function installSlug(installDir) {
32
+ return createHash("sha256").update(installDir).digest("hex").slice(0, 8);
33
+ }
34
+ /** Derive the unique per-install label pair from the resolved installDir. */
35
+ export function deriveServiceLabels(installDir) {
36
+ const slug = installSlug(installDir);
37
+ return {
38
+ web: `com.arelos.${slug}.web`,
39
+ vault: `com.arelos.${slug}.vault`,
40
+ };
41
+ }
19
42
  export function plistPath(label) {
20
43
  return join(launchAgentsDir(), `${label}.plist`);
21
44
  }
package/dist/plist.js CHANGED
@@ -1,11 +1,12 @@
1
1
  /**
2
2
  * Plist template rendering (portability-contract.md §3.1). Templates ship at
3
- * `scripts/service/com.arelos.{web,vault}.plist.tmpl` in the app repo and
4
- * take a single {{INSTALL_DIR}} token ports/vaultPath are read from config
5
- * at process start by the run-*.sh scripts / server, never baked into the plist.
3
+ * `scripts/service/{web,vault}.plist.tmpl` in the app repo and take two
4
+ * tokens {{INSTALL_DIR}} and {{LABEL}} (the per-install launchd label from
5
+ * paths.ts deriveServiceLabels). Ports/vaultPath are read from config at
6
+ * process start by the run-*.sh scripts / server, never baked into the plist.
6
7
  */
7
- export function renderPlistTemplate(template, installDir) {
8
- return template.split("{{INSTALL_DIR}}").join(installDir);
8
+ export function renderPlistTemplate(template, installDir, label) {
9
+ return template.split("{{INSTALL_DIR}}").join(installDir).split("{{LABEL}}").join(label);
9
10
  }
10
11
  /** Basic structural sanity check without shelling out to `plutil` (used in unit tests; `plutil -lint` is used in the dry-run for the real macOS check). */
11
12
  export function looksLikeValidPlist(xml) {
package/dist/ports.js CHANGED
@@ -1,28 +1,46 @@
1
1
  /**
2
2
  * Zero-dependency free-port detection via node:net (spec §1 Step 5).
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.
3
10
  */
4
11
  import { createServer } from "node:net";
5
- /** Resolve true if `port` is free to bind on 127.0.0.1, false if taken. */
6
- export function isPortFree(port) {
12
+ /** Error codes meaning "this address family isn't available on this machine". */
13
+ const FAMILY_UNAVAILABLE = new Set(["EADDRNOTAVAIL", "EAFNOSUPPORT", "EINVAL"]);
14
+ /** Try to bind `port` on one loopback host and report what happened. */
15
+ function probeBind(port, host) {
7
16
  return new Promise((resolve) => {
8
17
  const server = createServer();
9
18
  server.once("error", (err) => {
10
19
  server.close();
11
- if (err.code === "EADDRINUSE" || err.code === "EACCES") {
12
- resolve(false);
20
+ if (err.code && FAMILY_UNAVAILABLE.has(err.code)) {
21
+ // No IPv6 (or IPv4) loopback on this machine — nothing to conflict with.
22
+ resolve("family-unavailable");
13
23
  }
14
24
  else {
15
- // Unexpected error probing the port — treat conservatively as taken
16
- // so we never suggest a port we can't actually validate.
17
- resolve(false);
25
+ // EADDRINUSE/EACCES, or anything unexpected — treat conservatively as
26
+ // taken so we never suggest a port we can't actually validate.
27
+ resolve("taken");
18
28
  }
19
29
  });
20
30
  server.once("listening", () => {
21
- server.close(() => resolve(true));
31
+ server.close(() => resolve("free"));
22
32
  });
23
- server.listen(port, "127.0.0.1");
33
+ server.listen(port, host);
24
34
  });
25
35
  }
36
+ /**
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.
39
+ */
40
+ 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";
43
+ }
26
44
  /**
27
45
  * Find the first free port at or above `start`, scanning upward. Stops at
28
46
  * `start + maxScan` to avoid an unbounded loop.
package/dist/repair.js CHANGED
@@ -5,6 +5,7 @@
5
5
  */
6
6
  import * as p from "@clack/prompts";
7
7
  import pc from "picocolors";
8
+ import { resolveServiceLabels } from "./config.js";
8
9
  import { runUpdate } from "./update.js";
9
10
  import { waitForHealthy } from "./health.js";
10
11
  import { bootstrapAndStart, installServiceFiles } from "./services.js";
@@ -67,8 +68,9 @@ async function runRepair(existing) {
67
68
  }
68
69
  s.stop("Rebuilt.");
69
70
  s.start("Re-rendering and re-bootstrapping services…");
70
- installServiceFiles(existing.installDir);
71
- const bootstrap = await bootstrapAndStart();
71
+ const labels = resolveServiceLabels(existing);
72
+ installServiceFiles(existing.installDir, labels);
73
+ const bootstrap = await bootstrapAndStart(labels);
72
74
  s.stop(bootstrap.errors.length ? "Services re-registered with warnings." : "Services re-registered.");
73
75
  for (const e of bootstrap.errors)
74
76
  console.error(pc.yellow(e));
package/dist/services.js CHANGED
@@ -6,12 +6,12 @@
6
6
  import { chmodSync, existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
7
7
  import { join } from "node:path";
8
8
  import { bootstrapService, kickstartService } from "./launchd.js";
9
- import { VAULT_LABEL, WEB_LABEL, launchAgentsDir, plistPath } from "./paths.js";
9
+ import { deriveServiceLabels, launchAgentsDir, plistPath } from "./paths.js";
10
10
  import { renderPlistTemplate } from "./plist.js";
11
- export function renderServicePlists(installDir) {
11
+ export function renderServicePlists(installDir, labels = deriveServiceLabels(installDir)) {
12
12
  const specs = [
13
- { label: WEB_LABEL, templateFile: "com.arelos.web.plist.tmpl" },
14
- { label: VAULT_LABEL, templateFile: "com.arelos.vault.plist.tmpl" },
13
+ { label: labels.web, templateFile: "web.plist.tmpl" },
14
+ { label: labels.vault, templateFile: "vault.plist.tmpl" },
15
15
  ];
16
16
  return specs.map(({ label, templateFile }) => {
17
17
  const templatePath = join(installDir, "scripts", "service", templateFile);
@@ -19,14 +19,22 @@ export function renderServicePlists(installDir) {
19
19
  throw new Error(`Missing plist template: ${templatePath}`);
20
20
  }
21
21
  const template = readFileSync(templatePath, "utf8");
22
- const xml = renderPlistTemplate(template, installDir);
22
+ const xml = renderPlistTemplate(template, installDir, label);
23
23
  return { label, templatePath, targetPath: plistPath(label), xml };
24
24
  });
25
25
  }
26
26
  /** Write rendered plists to ~/Library/LaunchAgents and chmod the run scripts. */
27
- export function installServiceFiles(installDir) {
27
+ export function installServiceFiles(installDir, labels = deriveServiceLabels(installDir)) {
28
28
  mkdirSync(launchAgentsDir(), { recursive: true });
29
- const rendered = renderServicePlists(installDir);
29
+ // The plists' StandardOutPath/StandardErrorPath point at <installDir>/logs/service/*.log;
30
+ // launchd needs that directory to exist before it can bootstrap the job (a
31
+ // missing log dir is one confirmed cause of the "Bootstrap failed: 5:
32
+ // Input/output error" field report — see launchd.ts docstring). install.ts
33
+ // already calls ensureLogsDir before this, but repair/update funnel through
34
+ // here too, so guarantee it unconditionally rather than relying on callers
35
+ // to remember.
36
+ mkdirSync(join(installDir, "logs", "service"), { recursive: true });
37
+ const rendered = renderServicePlists(installDir, labels);
30
38
  for (const svc of rendered) {
31
39
  writeFileSync(svc.targetPath, svc.xml);
32
40
  }
@@ -38,18 +46,18 @@ export function installServiceFiles(installDir) {
38
46
  return rendered;
39
47
  }
40
48
  /** Bootstrap + kickstart both services (idempotent bootout-then-bootstrap). */
41
- export async function bootstrapAndStart() {
49
+ export async function bootstrapAndStart(labels) {
42
50
  const errors = [];
43
- const webRes = await bootstrapService(WEB_LABEL);
51
+ const webRes = await bootstrapService(labels.web);
44
52
  if (!webRes.ok)
45
53
  errors.push(`web bootstrap: ${webRes.stderr.trim()}`);
46
- const vaultRes = await bootstrapService(VAULT_LABEL);
54
+ const vaultRes = await bootstrapService(labels.vault);
47
55
  if (!vaultRes.ok)
48
56
  errors.push(`vault bootstrap: ${vaultRes.stderr.trim()}`);
49
- const webKick = await kickstartService(WEB_LABEL);
57
+ const webKick = await kickstartService(labels.web);
50
58
  if (!webKick.ok)
51
59
  errors.push(`web kickstart: ${webKick.stderr.trim()}`);
52
- const vaultKick = await kickstartService(VAULT_LABEL);
60
+ const vaultKick = await kickstartService(labels.vault);
53
61
  if (!vaultKick.ok)
54
62
  errors.push(`vault kickstart: ${vaultKick.stderr.trim()}`);
55
63
  return { web: webRes.ok && webKick.ok, vault: vaultRes.ok && vaultKick.ok, errors };
package/dist/status.js CHANGED
@@ -3,11 +3,10 @@
3
3
  * git revision / behind-origin check.
4
4
  */
5
5
  import pc from "picocolors";
6
- import { readConfig } from "./config.js";
6
+ import { readConfig, resolveServiceLabels } from "./config.js";
7
7
  import { checkVaultHealth, checkWebHealth } from "./health.js";
8
8
  import { getServiceStatus } from "./launchd.js";
9
9
  import { currentRevision, isBehindOrigin } from "./repo.js";
10
- import { VAULT_LABEL, WEB_LABEL } from "./paths.js";
11
10
  export async function statusCommand() {
12
11
  const config = readConfig();
13
12
  if (!config) {
@@ -20,9 +19,10 @@ export async function statusCommand() {
20
19
  console.log(` Web port: ${config.webPort}`);
21
20
  console.log(` Vault port: ${config.vaultPort}`);
22
21
  console.log("");
22
+ const labels = resolveServiceLabels(config);
23
23
  const [webSvc, vaultSvc, webHealth, vaultHealth, rev, behind] = await Promise.all([
24
- getServiceStatus(WEB_LABEL),
25
- getServiceStatus(VAULT_LABEL),
24
+ getServiceStatus(labels.web),
25
+ getServiceStatus(labels.vault),
26
26
  checkWebHealth(config.webPort),
27
27
  checkVaultHealth(config.vaultPort),
28
28
  currentRevision(config.installDir),
package/dist/uninstall.js CHANGED
@@ -8,9 +8,9 @@
8
8
  import * as p from "@clack/prompts";
9
9
  import pc from "picocolors";
10
10
  import { existsSync, rmSync, unlinkSync } from "node:fs";
11
- import { readConfig } from "./config.js";
11
+ import { readConfig, resolveServiceLabels } from "./config.js";
12
12
  import { bootoutService } from "./launchd.js";
13
- import { VAULT_LABEL, WEB_LABEL, plistPath, configPath } from "./paths.js";
13
+ import { plistPath, configPath } from "./paths.js";
14
14
  /**
15
15
  * Pure gate: vault is deleted iff confirmed AND the typed word is exactly
16
16
  * "DELETE". Any other input (including case variants, whitespace, empty)
@@ -27,9 +27,10 @@ export async function uninstallCommand() {
27
27
  return 1;
28
28
  }
29
29
  p.intro(pc.bold("Uninstall Arel OS"));
30
- await bootoutService(WEB_LABEL);
31
- await bootoutService(VAULT_LABEL);
32
- for (const label of [WEB_LABEL, VAULT_LABEL]) {
30
+ const labels = resolveServiceLabels(config);
31
+ await bootoutService(labels.web);
32
+ await bootoutService(labels.vault);
33
+ for (const label of [labels.web, labels.vault]) {
33
34
  const path = plistPath(label);
34
35
  if (existsSync(path))
35
36
  unlinkSync(path);
package/dist/update.js CHANGED
@@ -4,7 +4,7 @@
4
4
  */
5
5
  import pc from "picocolors";
6
6
  import { ensureBun } from "./bun-setup.js";
7
- import { readConfig } from "./config.js";
7
+ import { readConfig, resolveServiceLabels } from "./config.js";
8
8
  import { runStreaming } from "./exec.js";
9
9
  import { waitForHealthy } from "./health.js";
10
10
  import { pullLatest } from "./repo.js";
@@ -44,8 +44,9 @@ export async function runUpdate(config) {
44
44
  if (buildRes.code !== 0) {
45
45
  console.warn(pc.yellow("bun run build failed — keeping old dist/. The web service will keep serving it."));
46
46
  }
47
- installServiceFiles(config.installDir);
48
- const bootstrap = await bootstrapAndStart();
47
+ const labels = resolveServiceLabels(config);
48
+ installServiceFiles(config.installDir, labels);
49
+ const bootstrap = await bootstrapAndStart(labels);
49
50
  for (const e of bootstrap.errors)
50
51
  console.error(pc.yellow(e));
51
52
  console.log("Restarting services and re-checking health…");
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "arelos",
3
- "version": "0.1.0",
3
+ "version": "0.1.2",
4
4
  "description": "Installer and service manager for a self-hosted Arel OS on macOS.",
5
5
  "type": "module",
6
6
  "bin": {