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.
- package/dist/install-plan.js +20 -0
- package/dist/install.js +29 -12
- package/dist/ports.js +27 -9
- package/package.json +1 -1
package/dist/install-plan.js
CHANGED
|
@@ -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(
|
|
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 ??
|
|
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:
|
|
219
|
-
placeholder:
|
|
220
|
-
defaultValue:
|
|
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 ||
|
|
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
|
|
231
|
-
|
|
232
|
-
|
|
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(
|
|
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
|
|
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
|
-
/**
|
|
6
|
-
|
|
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
|
|
12
|
-
|
|
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
|
-
//
|
|
16
|
-
// so we never suggest a port we can't actually validate.
|
|
17
|
-
resolve(
|
|
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(
|
|
31
|
+
server.close(() => resolve("free"));
|
|
22
32
|
});
|
|
23
|
-
server.listen(port,
|
|
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.
|