arelos 0.1.1 → 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.
@@ -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);
package/dist/install.js CHANGED
@@ -9,7 +9,7 @@ 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";
@@ -173,7 +173,7 @@ export async function runInstall(argv, flags) {
173
173
  }
174
174
  }
175
175
  p.outro([
176
- pc.green(`Arel OS is running at ${url}`),
176
+ pc.green(`${answers.displayName} is running at ${url}`),
177
177
  "Runs 24/7 in the background.",
178
178
  "Next: arelos status · arelos logs · arelos update · arelos uninstall",
179
179
  ].join("\n"));
@@ -189,7 +189,7 @@ async function checkGit() {
189
189
  async function collectAnswers(flags) {
190
190
  if (flags.yes) {
191
191
  const displayName = normalizeDisplayName(flags.displayName ?? DEFAULTS.displayName);
192
- const installDir = flags.installDir ?? DEFAULTS.installDir;
192
+ const installDir = flags.installDir ?? defaultInstallDirFor(displayName);
193
193
  const vaultPath = flags.vaultPath ?? defaultVaultPath(installDir);
194
194
  const webPortReq = flags.webPort ?? DEFAULTS.webPort;
195
195
  const vaultPortReq = flags.vaultPort ?? DEFAULTS.vaultPort;
@@ -211,28 +211,45 @@ async function collectAnswers(flags) {
211
211
  });
212
212
  if (p.isCancel(displayNameRaw))
213
213
  return null;
214
+ const displayName = normalizeDisplayName(String(displayNameRaw));
215
+ const installDirDefault = defaultInstallDirFor(displayName);
214
216
  // Step 3 — Install location.
215
217
  let installDir = "";
216
218
  for (;;) {
217
219
  const raw = await p.text({
218
- message: "Where should Arel OS be installed?",
219
- placeholder: DEFAULTS.installDir,
220
- defaultValue: DEFAULTS.installDir,
220
+ message: `Where should ${displayName} be installed?`,
221
+ placeholder: installDirDefault,
222
+ defaultValue: installDirDefault,
221
223
  });
222
224
  if (p.isCancel(raw))
223
225
  return null;
224
- const check = checkInstallDir(String(raw || DEFAULTS.installDir));
226
+ const check = checkInstallDir(String(raw || installDirDefault));
225
227
  if (!check.parentWritable) {
226
228
  p.log.error(`Cannot write to ${check.path} — choose another location.`);
227
229
  continue;
228
230
  }
229
231
  if (check.nonEmpty && !check.isPriorArelosInstall) {
230
- const proceed = await p.confirm({
231
- message: `${check.path} exists and is not empty. Use it anyway?`,
232
- 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",
233
241
  });
234
- if (p.isCancel(proceed) || !proceed)
242
+ if (p.isCancel(choice) || choice === "cancel")
243
+ return null;
244
+ if (choice === "different")
235
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.`);
249
+ continue;
250
+ }
251
+ installDir = subCheck.path;
252
+ break;
236
253
  }
237
254
  installDir = check.path;
238
255
  break;
@@ -253,7 +270,7 @@ async function collectAnswers(flags) {
253
270
  if (vaultPort === null)
254
271
  return null;
255
272
  return {
256
- displayName: normalizeDisplayName(String(displayNameRaw)),
273
+ displayName,
257
274
  installDir,
258
275
  vaultPath: String(vaultRaw || defaultVaultPath(installDir)),
259
276
  webPort,
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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "arelos",
3
- "version": "0.1.1",
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": {