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/cli-args.js +6 -6
- package/dist/cli-context.js +80 -0
- package/dist/cli.js +45 -13
- package/dist/config.js +40 -11
- package/dist/health.js +62 -1
- package/dist/install-plan.js +58 -17
- package/dist/install.js +139 -96
- package/dist/list.js +57 -0
- package/dist/logs.js +14 -9
- package/dist/paths.js +78 -6
- package/dist/plist.js +19 -7
- package/dist/ports.js +81 -13
- package/dist/registry.js +41 -0
- package/dist/repair.js +11 -7
- package/dist/repo.js +4 -4
- package/dist/scaffold.js +8 -8
- package/dist/services.js +13 -9
- package/dist/status.js +12 -7
- package/dist/uninstall.js +40 -19
- package/dist/update.js +11 -9
- package/package.json +2 -3
package/dist/cli-args.js
CHANGED
|
@@ -4,8 +4,8 @@ export function parseInstallFlags(argv) {
|
|
|
4
4
|
noService: false,
|
|
5
5
|
localRepo: null,
|
|
6
6
|
displayName: null,
|
|
7
|
-
|
|
8
|
-
|
|
7
|
+
root: null,
|
|
8
|
+
parentDir: null,
|
|
9
9
|
webPort: null,
|
|
10
10
|
vaultPort: null,
|
|
11
11
|
};
|
|
@@ -25,11 +25,11 @@ export function parseInstallFlags(argv) {
|
|
|
25
25
|
case "--display-name":
|
|
26
26
|
flags.displayName = argv[++i] ?? null;
|
|
27
27
|
break;
|
|
28
|
-
case "--
|
|
29
|
-
flags.
|
|
28
|
+
case "--root":
|
|
29
|
+
flags.root = argv[++i] ?? null;
|
|
30
30
|
break;
|
|
31
|
-
case "--
|
|
32
|
-
flags.
|
|
31
|
+
case "--parent-dir":
|
|
32
|
+
flags.parentDir = argv[++i] ?? null;
|
|
33
33
|
break;
|
|
34
34
|
case "--web-port":
|
|
35
35
|
flags.webPort = Number(argv[++i]);
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Shared "which install does this command target" resolution for
|
|
3
|
+
* status/update/logs/uninstall (0.2.0 multi-install support).
|
|
4
|
+
*
|
|
5
|
+
* Rule: no name arg + exactly one known install -> that one. No name arg +
|
|
6
|
+
* multiple installs -> interactive select (or, non-interactively, an error
|
|
7
|
+
* listing the options). A name arg always resolves by exact registry
|
|
8
|
+
* name/slug match. "Known installs" = registry entries, plus the legacy
|
|
9
|
+
* fixed ~/.arelos/config.json as an "(unnamed)" install when present and not
|
|
10
|
+
* already superseded by a registry entry at the same root.
|
|
11
|
+
*/
|
|
12
|
+
import * as p from "@clack/prompts";
|
|
13
|
+
import { readConfigAt, readConfig } from "./config.js";
|
|
14
|
+
import { legacyConfigPath } from "./paths.js";
|
|
15
|
+
import { existsSync } from "node:fs";
|
|
16
|
+
import { readRegistry } from "./registry.js";
|
|
17
|
+
function listCandidates() {
|
|
18
|
+
const entries = readRegistry();
|
|
19
|
+
const candidates = entries.map((e) => ({ name: e.name, root: e.root }));
|
|
20
|
+
if (existsSync(legacyConfigPath())) {
|
|
21
|
+
candidates.push({ name: "(unnamed — legacy install)", root: null });
|
|
22
|
+
}
|
|
23
|
+
return candidates;
|
|
24
|
+
}
|
|
25
|
+
function loadCandidate(candidate) {
|
|
26
|
+
const config = candidate.root ? readConfigAt(candidate.root) : readConfig();
|
|
27
|
+
if (!config)
|
|
28
|
+
return null;
|
|
29
|
+
return { name: candidate.name, root: candidate.root, config };
|
|
30
|
+
}
|
|
31
|
+
/** Resolve which install a status/update/logs/uninstall invocation targets. */
|
|
32
|
+
export async function resolveInstall(opts) {
|
|
33
|
+
const candidates = listCandidates();
|
|
34
|
+
if (opts.name) {
|
|
35
|
+
const match = candidates.find((c) => c.name === opts.name);
|
|
36
|
+
if (!match) {
|
|
37
|
+
const known = candidates.map((c) => c.name).join(", ") || "(none)";
|
|
38
|
+
return { ok: false, message: `No install named "${opts.name}" found. Known installs: ${known}` };
|
|
39
|
+
}
|
|
40
|
+
const resolved = loadCandidate(match);
|
|
41
|
+
if (!resolved) {
|
|
42
|
+
return { ok: false, message: `Install "${opts.name}" is registered but its config could not be read.` };
|
|
43
|
+
}
|
|
44
|
+
return { ok: true, install: resolved };
|
|
45
|
+
}
|
|
46
|
+
if (candidates.length === 0) {
|
|
47
|
+
return { ok: false, message: "No Arel OS install found. Run `npx arelos` to install." };
|
|
48
|
+
}
|
|
49
|
+
if (candidates.length === 1) {
|
|
50
|
+
const resolved = loadCandidate(candidates[0]);
|
|
51
|
+
if (!resolved) {
|
|
52
|
+
return { ok: false, message: "Install is registered but its config could not be read." };
|
|
53
|
+
}
|
|
54
|
+
return { ok: true, install: resolved };
|
|
55
|
+
}
|
|
56
|
+
// Multiple installs, no name given.
|
|
57
|
+
if (!opts.interactive) {
|
|
58
|
+
const known = candidates.map((c) => c.name).join(", ");
|
|
59
|
+
return {
|
|
60
|
+
ok: false,
|
|
61
|
+
message: `Multiple Arel OS installs found — pass a name. Known installs: ${known}`,
|
|
62
|
+
};
|
|
63
|
+
}
|
|
64
|
+
const choice = await p.select({
|
|
65
|
+
message: "Which install?",
|
|
66
|
+
options: candidates.map((c) => ({ value: c.name, label: c.name })),
|
|
67
|
+
});
|
|
68
|
+
if (p.isCancel(choice)) {
|
|
69
|
+
return { ok: false, message: "Cancelled." };
|
|
70
|
+
}
|
|
71
|
+
const match = candidates.find((c) => c.name === choice);
|
|
72
|
+
const resolved = match ? loadCandidate(match) : null;
|
|
73
|
+
if (!resolved) {
|
|
74
|
+
return { ok: false, message: "Selected install's config could not be read." };
|
|
75
|
+
}
|
|
76
|
+
return { ok: true, install: resolved };
|
|
77
|
+
}
|
|
78
|
+
export function listInstallNames() {
|
|
79
|
+
return listCandidates().map((c) => c.name);
|
|
80
|
+
}
|
package/dist/cli.js
CHANGED
|
@@ -1,7 +1,9 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
/**
|
|
3
3
|
* arelos — the Arel OS installer/service manager. `npx arelos` with no subcommand
|
|
4
|
-
* runs the interactive install flow
|
|
4
|
+
* runs the interactive install flow. 0.2.0 supports multiple named,
|
|
5
|
+
* self-contained installs on one Mac — status/update/logs/uninstall accept an
|
|
6
|
+
* optional install name/slug (see cli-context.ts for the resolution rule).
|
|
5
7
|
*/
|
|
6
8
|
if (process.platform !== "darwin") {
|
|
7
9
|
console.error("Arel OS currently supports macOS only.");
|
|
@@ -9,6 +11,7 @@ if (process.platform !== "darwin") {
|
|
|
9
11
|
}
|
|
10
12
|
import { parseInstallFlags, parseLogsFlags } from "./cli-args.js";
|
|
11
13
|
import { runInstall } from "./install.js";
|
|
14
|
+
import { listCommand } from "./list.js";
|
|
12
15
|
import { logsCommand } from "./logs.js";
|
|
13
16
|
import { statusCommand } from "./status.js";
|
|
14
17
|
import { uninstallCommand } from "./uninstall.js";
|
|
@@ -23,7 +26,7 @@ async function main() {
|
|
|
23
26
|
// rather than a subcommand name — both mean "install". Anything else in
|
|
24
27
|
// the known set consumes its name as the subcommand; everything after it
|
|
25
28
|
// is passed through as that subcommand's own args.
|
|
26
|
-
const knownSubcommands = new Set(["install", "status", "update", "uninstall", "logs"]);
|
|
29
|
+
const knownSubcommands = new Set(["install", "status", "update", "uninstall", "logs", "list"]);
|
|
27
30
|
const firstIsFlag = argv.length === 0 || argv[0].startsWith("-");
|
|
28
31
|
const firstIsKnown = argv.length > 0 && knownSubcommands.has(argv[0]);
|
|
29
32
|
if (!firstIsFlag && !firstIsKnown) {
|
|
@@ -37,35 +40,64 @@ async function main() {
|
|
|
37
40
|
switch (subcommand) {
|
|
38
41
|
case "install":
|
|
39
42
|
return runInstall(rest, parseInstallFlags(rest));
|
|
43
|
+
case "list":
|
|
44
|
+
return listCommand();
|
|
40
45
|
case "status":
|
|
41
|
-
return statusCommand();
|
|
46
|
+
return statusCommand(nameArgFrom(rest));
|
|
42
47
|
case "update":
|
|
43
|
-
return updateCommand();
|
|
48
|
+
return updateCommand(nameArgFrom(rest));
|
|
44
49
|
case "uninstall":
|
|
45
|
-
return uninstallCommand();
|
|
50
|
+
return uninstallCommand(nameArgFrom(rest));
|
|
46
51
|
case "logs":
|
|
47
|
-
return logsCommand(parseLogsFlags(rest));
|
|
52
|
+
return logsCommand(parseLogsFlags(rest), logsNameArgFrom(rest));
|
|
48
53
|
default:
|
|
49
54
|
console.error(`Unknown command: ${subcommand}\n`);
|
|
50
55
|
printHelp();
|
|
51
56
|
return 1;
|
|
52
57
|
}
|
|
53
58
|
}
|
|
59
|
+
/** The first non-flag positional arg, if any — the optional install name/slug. */
|
|
60
|
+
function nameArgFrom(args) {
|
|
61
|
+
return args.find((a) => !a.startsWith("-")) ?? null;
|
|
62
|
+
}
|
|
63
|
+
/**
|
|
64
|
+
* `logs` has two possible positionals — the install name and the web/vault
|
|
65
|
+
* target — in either order. The target is a fixed keyword, so anything else
|
|
66
|
+
* non-flag (and not `-n`'s own value) is the name.
|
|
67
|
+
*/
|
|
68
|
+
function logsNameArgFrom(args) {
|
|
69
|
+
for (let i = 0; i < args.length; i++) {
|
|
70
|
+
const arg = args[i];
|
|
71
|
+
if (arg.startsWith("-")) {
|
|
72
|
+
if (arg === "-n")
|
|
73
|
+
i++; // skip -n's value
|
|
74
|
+
continue;
|
|
75
|
+
}
|
|
76
|
+
if (arg === "web" || arg === "vault")
|
|
77
|
+
continue;
|
|
78
|
+
return arg;
|
|
79
|
+
}
|
|
80
|
+
return null;
|
|
81
|
+
}
|
|
54
82
|
function printHelp() {
|
|
55
83
|
console.log(`arelos — install and manage a self-hosted Arel OS
|
|
56
84
|
|
|
57
85
|
Usage:
|
|
58
|
-
npx arelos
|
|
59
|
-
arelos
|
|
60
|
-
arelos
|
|
61
|
-
arelos
|
|
62
|
-
arelos
|
|
86
|
+
npx arelos Install (interactive)
|
|
87
|
+
arelos list List every install (name, root, ports, status)
|
|
88
|
+
arelos status [name] Show install + service status
|
|
89
|
+
arelos update [name] git pull + rebuild + restart
|
|
90
|
+
arelos uninstall [name] Stop services, optionally remove install dir / vault
|
|
91
|
+
arelos logs [name] [web|vault] Tail service logs (-f to follow, -n <N> for line count)
|
|
92
|
+
|
|
93
|
+
[name] is only needed when you have more than one install; omit it with a
|
|
94
|
+
single install, or you'll be prompted to choose interactively.
|
|
63
95
|
|
|
64
96
|
Install flags (non-interactive):
|
|
65
97
|
--yes, --defaults Skip prompts, use defaults/flags below
|
|
66
98
|
--display-name <name>
|
|
67
|
-
--
|
|
68
|
-
--
|
|
99
|
+
--root <path> Full override of the resolved install root
|
|
100
|
+
--parent-dir <path> Override the parent dir the app's folder is created in
|
|
69
101
|
--web-port <port>
|
|
70
102
|
--vault-port <port>
|
|
71
103
|
--no-service Skip launchd bootstrap (for dry runs / development)
|
package/dist/config.js
CHANGED
|
@@ -1,13 +1,16 @@
|
|
|
1
1
|
/**
|
|
2
|
-
*
|
|
3
|
-
* server/config.ts
|
|
4
|
-
*
|
|
2
|
+
* arelos's own config read/write helpers — mirrors the shape read by
|
|
3
|
+
* server/config.ts. arelos is the writer, the app is the reader; this is the
|
|
4
|
+
* one contract between them.
|
|
5
|
+
*
|
|
6
|
+
* 0.2.0: config is per-install, at <root>/config.json (see paths.ts
|
|
7
|
+
* installConfigPath). The old single fixed ~/.arelos/config.json is kept as
|
|
8
|
+
* a read-only legacy fallback ("unnamed install") for 0.1.x installs.
|
|
5
9
|
*/
|
|
6
10
|
import { existsSync, mkdirSync, readFileSync, renameSync, writeFileSync } from "node:fs";
|
|
7
11
|
import { dirname } from "node:path";
|
|
8
|
-
import {
|
|
9
|
-
|
|
10
|
-
const p = configPath();
|
|
12
|
+
import { configPathOverride, installConfigPath, legacyConfigPath, LEGACY_VAULT_LABEL, LEGACY_WEB_LABEL, } from "./paths.js";
|
|
13
|
+
function parseConfigFile(p) {
|
|
11
14
|
if (!existsSync(p))
|
|
12
15
|
return null;
|
|
13
16
|
const raw = JSON.parse(readFileSync(p, "utf8"));
|
|
@@ -16,13 +19,30 @@ export function readConfig() {
|
|
|
16
19
|
}
|
|
17
20
|
return raw;
|
|
18
21
|
}
|
|
22
|
+
/** Read the config at a specific known root's config.json. */
|
|
23
|
+
export function readConfigAt(root) {
|
|
24
|
+
return parseConfigFile(installConfigPath(root));
|
|
25
|
+
}
|
|
26
|
+
/**
|
|
27
|
+
* Read a single config with no registry context: honors ARELOS_CONFIG_PATH
|
|
28
|
+
* if set (tests / pointing at one install directly), else falls back to the
|
|
29
|
+
* legacy fixed ~/.arelos/config.json (pre-0.2.0 "unnamed install"). Used by
|
|
30
|
+
* callers that haven't gone through the registry (e.g. a bare `arelos status`
|
|
31
|
+
* when there's exactly one legacy install and no registry entries).
|
|
32
|
+
*/
|
|
33
|
+
export function readConfig() {
|
|
34
|
+
const override = configPathOverride();
|
|
35
|
+
if (override)
|
|
36
|
+
return parseConfigFile(override);
|
|
37
|
+
return parseConfigFile(legacyConfigPath());
|
|
38
|
+
}
|
|
19
39
|
/**
|
|
20
40
|
* Write config atomically: write to a .tmp sibling then rename over the
|
|
21
|
-
* target. Guarantees a reader never observes a partially written file
|
|
22
|
-
*
|
|
41
|
+
* target. Guarantees a reader never observes a partially written file —
|
|
42
|
+
* config write is last-writer-wins; never partially written.
|
|
23
43
|
*/
|
|
24
|
-
export function writeConfig(config) {
|
|
25
|
-
const p =
|
|
44
|
+
export function writeConfig(config, targetPath) {
|
|
45
|
+
const p = targetPath ?? configPathOverride() ?? (config.root ? installConfigPath(config.root) : legacyConfigPath());
|
|
26
46
|
mkdirSync(dirname(p), { recursive: true });
|
|
27
47
|
const tmp = `${p}.tmp`;
|
|
28
48
|
writeFileSync(tmp, `${JSON.stringify(config, null, 2)}\n`, { mode: 0o600 });
|
|
@@ -31,8 +51,17 @@ export function writeConfig(config) {
|
|
|
31
51
|
/**
|
|
32
52
|
* The labels to actually operate on for an existing install: config.serviceLabels
|
|
33
53
|
* when present (installs made after this fix), else the legacy fixed labels
|
|
34
|
-
* (installs made before it — backward compat
|
|
54
|
+
* (installs made before it — backward compat).
|
|
35
55
|
*/
|
|
36
56
|
export function resolveServiceLabels(config) {
|
|
37
57
|
return config.serviceLabels ?? { web: LEGACY_WEB_LABEL, vault: LEGACY_VAULT_LABEL };
|
|
38
58
|
}
|
|
59
|
+
/**
|
|
60
|
+
* The self-contained root that owns this install's logs/service/ and
|
|
61
|
+
* config.json (0.2.0 layout). Legacy (pre-0.2.0) configs have no `root` field
|
|
62
|
+
* — for those, installDir *was* the root (logs lived at installDir/logs),
|
|
63
|
+
* so fall back to it.
|
|
64
|
+
*/
|
|
65
|
+
export function resolveRoot(config) {
|
|
66
|
+
return config.root ?? config.installDir;
|
|
67
|
+
}
|
package/dist/health.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* HTTP health probes for the vault (`/config`) and web (`/`) services
|
|
3
|
-
* (
|
|
3
|
+
* (used by `arelos install` and `arelos status`). Pure fetch-based, no deps.
|
|
4
4
|
*/
|
|
5
5
|
async function fetchWithTimeout(url, timeoutMs) {
|
|
6
6
|
const controller = new AbortController();
|
|
@@ -56,3 +56,64 @@ export async function waitForHealthy(webPort, vaultPort, opts = {}) {
|
|
|
56
56
|
}
|
|
57
57
|
return { healthy: false, vault, web };
|
|
58
58
|
}
|
|
59
|
+
export const LOG_SIGNATURE_HINTS = [
|
|
60
|
+
{
|
|
61
|
+
pattern: "operation not permitted",
|
|
62
|
+
hint: "This looks like macOS's TCC privacy protection: background services can't run from " +
|
|
63
|
+
"Desktop, Documents, Downloads, or iCloud Drive. Move the install to a folder in your home " +
|
|
64
|
+
"directory (e.g. ~/arelos) and reinstall.",
|
|
65
|
+
},
|
|
66
|
+
{
|
|
67
|
+
pattern: "eaddrinuse",
|
|
68
|
+
hint: "This looks like a port conflict: another process is already using the configured port. " +
|
|
69
|
+
"Free the port or re-run install with a different --web-port/--vault-port.",
|
|
70
|
+
},
|
|
71
|
+
{
|
|
72
|
+
pattern: "address already in use",
|
|
73
|
+
hint: "This looks like a port conflict: another process is already using the configured port. " +
|
|
74
|
+
"Free the port or re-run install with a different --web-port/--vault-port.",
|
|
75
|
+
},
|
|
76
|
+
{
|
|
77
|
+
pattern: "command not found",
|
|
78
|
+
hint: "This looks like a missing dependency on PATH inside the launchd environment. Check the service's shebang/interpreter is installed and resolvable without a login shell.",
|
|
79
|
+
},
|
|
80
|
+
];
|
|
81
|
+
/**
|
|
82
|
+
* Match a log tail against known failure signatures. Pure string matching,
|
|
83
|
+
* no I/O — unit-testable without touching disk. Returns null when nothing
|
|
84
|
+
* recognized matches, so callers can fall back to a generic message.
|
|
85
|
+
*/
|
|
86
|
+
export function matchLogSignature(logTail) {
|
|
87
|
+
const lower = logTail.toLowerCase();
|
|
88
|
+
for (const entry of LOG_SIGNATURE_HINTS) {
|
|
89
|
+
if (lower.includes(entry.pattern))
|
|
90
|
+
return entry;
|
|
91
|
+
}
|
|
92
|
+
return null;
|
|
93
|
+
}
|
|
94
|
+
/**
|
|
95
|
+
* Build the full health-timeout diagnostic message: last N lines of both
|
|
96
|
+
* service logs, plus a targeted hint when a known failure signature is
|
|
97
|
+
* found in either. `readTail` is injected so this stays pure/testable
|
|
98
|
+
* (no direct fs access) while install.ts/repair.ts wire in the real reader.
|
|
99
|
+
*/
|
|
100
|
+
export function formatHealthTimeoutDiagnostics(logsRoot, readTail, logPathFor) {
|
|
101
|
+
const webLogPath = logPathFor(logsRoot, "web");
|
|
102
|
+
const vaultLogPath = logPathFor(logsRoot, "vault");
|
|
103
|
+
const webTail = readTail(webLogPath);
|
|
104
|
+
const vaultTail = readTail(vaultLogPath);
|
|
105
|
+
const hint = matchLogSignature(webTail) ?? matchLogSignature(vaultTail);
|
|
106
|
+
const lines = [
|
|
107
|
+
"App did not come up in time.",
|
|
108
|
+
"",
|
|
109
|
+
`-- last lines of ${webLogPath} --`,
|
|
110
|
+
webTail.trim() || "(empty)",
|
|
111
|
+
"",
|
|
112
|
+
`-- last lines of ${vaultLogPath} --`,
|
|
113
|
+
vaultTail.trim() || "(empty)",
|
|
114
|
+
];
|
|
115
|
+
if (hint) {
|
|
116
|
+
lines.push("", hint.hint);
|
|
117
|
+
}
|
|
118
|
+
return lines.join("\n");
|
|
119
|
+
}
|
package/dist/install-plan.js
CHANGED
|
@@ -1,17 +1,23 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Pure planning/validation logic for the install flow, factored out of the
|
|
3
3
|
* interactive prompt wiring in install.ts so it can be unit tested without
|
|
4
|
-
* a TTY. Each function here validates one prompt's answer
|
|
4
|
+
* a TTY. Each function here validates one prompt's answer.
|
|
5
|
+
*
|
|
6
|
+
* 0.2.0 self-contained layout: everything for one install lives under a
|
|
7
|
+
* single `root` folder (`<parent>/<slug>`) — `root/app` (the git checkout),
|
|
8
|
+
* `root/vault`, `root/logs/service`, and `root/config.json`. `installDir` in
|
|
9
|
+
* this module and in ArelConfig now specifically means the app checkout
|
|
10
|
+
* (`root/app`), not the root — see toArelConfig.
|
|
5
11
|
*/
|
|
6
12
|
import { existsSync, readdirSync, statSync } from "node:fs";
|
|
7
13
|
import { accessSync, constants } from "node:fs";
|
|
8
|
-
import { dirname } from "node:path";
|
|
9
|
-
import { deriveServiceLabels, expandHome } from "./paths.js";
|
|
14
|
+
import { dirname, join } from "node:path";
|
|
15
|
+
import { deriveServiceLabels, expandHome, isTccProtectedPath } from "./paths.js";
|
|
10
16
|
import { findFreePort, isValidPort } from "./ports.js";
|
|
11
17
|
export const DEFAULTS = {
|
|
12
18
|
displayName: "Arel OS",
|
|
13
|
-
|
|
14
|
-
|
|
19
|
+
parentDir: "~",
|
|
20
|
+
slugFallback: "arelos",
|
|
15
21
|
webPort: 1347,
|
|
16
22
|
vaultPort: 5274,
|
|
17
23
|
};
|
|
@@ -32,14 +38,45 @@ export function slugifyName(input) {
|
|
|
32
38
|
.replace(/[^a-z0-9]+/g, "-")
|
|
33
39
|
.replace(/^-+|-+$/g, "");
|
|
34
40
|
}
|
|
35
|
-
/**
|
|
36
|
-
export function
|
|
41
|
+
/** Slug fallback when the chosen name slugifies to empty (e.g. all emoji/punctuation). */
|
|
42
|
+
export function slugOrFallback(displayName) {
|
|
37
43
|
const slug = slugifyName(displayName);
|
|
38
|
-
|
|
39
|
-
return DEFAULTS.installDir;
|
|
40
|
-
return `~/${slug}`;
|
|
44
|
+
return slug || DEFAULTS.slugFallback;
|
|
41
45
|
}
|
|
42
|
-
|
|
46
|
+
/** Default parent dir offered for "change location?" — always the home directory. */
|
|
47
|
+
export function defaultParentDir() {
|
|
48
|
+
return DEFAULTS.parentDir;
|
|
49
|
+
}
|
|
50
|
+
/**
|
|
51
|
+
* The self-contained root for this install: always `<parent>/<slug>` — we
|
|
52
|
+
* never install loose into an existing folder, so the slug subfolder is
|
|
53
|
+
* appended unconditionally, even when the user changes the parent.
|
|
54
|
+
*/
|
|
55
|
+
export function rootFor(parentDir, displayName) {
|
|
56
|
+
return join(expandHome(parentDir), slugOrFallback(displayName));
|
|
57
|
+
}
|
|
58
|
+
/** installDir (the app checkout) and vaultPath are fixed children of root. */
|
|
59
|
+
export function appDirFor(root) {
|
|
60
|
+
return join(root, "app");
|
|
61
|
+
}
|
|
62
|
+
export function vaultPathFor(root) {
|
|
63
|
+
return join(root, "vault");
|
|
64
|
+
}
|
|
65
|
+
/**
|
|
66
|
+
* Plain-English explanation shown (and re-prompted with) when the chosen
|
|
67
|
+
* root or vault path resolves to inside a macOS TCC-protected folder
|
|
68
|
+
* (field bug: launchd-spawned services get `Operation not permitted`,
|
|
69
|
+
* exit 126, and crash-loop forever from Desktop/Documents/Downloads/iCloud
|
|
70
|
+
* Drive — see paths.ts isTccProtectedPath).
|
|
71
|
+
*/
|
|
72
|
+
export const TCC_PROTECTED_PATH_MESSAGE = "macOS blocks background services from running in Desktop, Documents, Downloads, or iCloud Drive. Pick a folder in your home directory instead.";
|
|
73
|
+
/**
|
|
74
|
+
* Validate a candidate self-contained root folder (`<parent>/<slug>`). A
|
|
75
|
+
* "prior arelos install of the same name" is recognized by the self-contained
|
|
76
|
+
* layout's own marker — root/app is a git checkout — so re-running the
|
|
77
|
+
* installer against the same root is a repair, not a collision.
|
|
78
|
+
*/
|
|
79
|
+
export function checkRootDir(rawPath) {
|
|
43
80
|
const path = expandHome(rawPath);
|
|
44
81
|
const parent = dirname(path);
|
|
45
82
|
let parentWritable = true;
|
|
@@ -55,12 +92,11 @@ export function checkInstallDir(rawPath) {
|
|
|
55
92
|
if (exists && statSync(path).isDirectory()) {
|
|
56
93
|
const entries = readdirSync(path);
|
|
57
94
|
nonEmpty = entries.length > 0;
|
|
58
|
-
|
|
95
|
+
const appDir = join(path, "app");
|
|
96
|
+
isPriorArelosInstall =
|
|
97
|
+
existsSync(join(appDir, ".git")) && existsSync(join(appDir, "package.json"));
|
|
59
98
|
}
|
|
60
|
-
return { path, parentWritable, exists, nonEmpty, isPriorArelosInstall };
|
|
61
|
-
}
|
|
62
|
-
export function defaultVaultPath(installDir) {
|
|
63
|
-
return `${expandHome(installDir)}/${DEFAULTS.vaultPathSuffix}`;
|
|
99
|
+
return { path, parentWritable, exists, nonEmpty, isPriorArelosInstall, isTccProtected: isTccProtectedPath(path) };
|
|
64
100
|
}
|
|
65
101
|
/** Validate a chosen port and, if it's taken, propose the next free one. */
|
|
66
102
|
export async function resolvePort(requested) {
|
|
@@ -75,14 +111,19 @@ export async function resolvePort(requested) {
|
|
|
75
111
|
return { requested, resolved: suggestion, wasFree: false };
|
|
76
112
|
}
|
|
77
113
|
export function toArelConfig(answers) {
|
|
114
|
+
const root = expandHome(answers.root);
|
|
78
115
|
const installDir = expandHome(answers.installDir);
|
|
79
116
|
return {
|
|
80
117
|
version: 1,
|
|
81
118
|
displayName: answers.displayName,
|
|
119
|
+
root,
|
|
82
120
|
installDir,
|
|
83
121
|
vaultPath: expandHome(answers.vaultPath),
|
|
84
122
|
webPort: answers.webPort,
|
|
85
123
|
vaultPort: answers.vaultPort,
|
|
86
|
-
|
|
124
|
+
// Labels are derived from root, not installDir: root is what's unique
|
|
125
|
+
// per named install; installDir (root/app) would collide in derivation
|
|
126
|
+
// only coincidentally, but keying off root is the more direct contract.
|
|
127
|
+
serviceLabels: deriveServiceLabels(root),
|
|
87
128
|
};
|
|
88
129
|
}
|