arelos 0.1.0

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.
@@ -0,0 +1,56 @@
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).
5
+ */
6
+ import { chmodSync, existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
7
+ import { join } from "node:path";
8
+ import { bootstrapService, kickstartService } from "./launchd.js";
9
+ import { VAULT_LABEL, WEB_LABEL, launchAgentsDir, plistPath } from "./paths.js";
10
+ import { renderPlistTemplate } from "./plist.js";
11
+ export function renderServicePlists(installDir) {
12
+ const specs = [
13
+ { label: WEB_LABEL, templateFile: "com.arelos.web.plist.tmpl" },
14
+ { label: VAULT_LABEL, templateFile: "com.arelos.vault.plist.tmpl" },
15
+ ];
16
+ return specs.map(({ label, templateFile }) => {
17
+ const templatePath = join(installDir, "scripts", "service", templateFile);
18
+ if (!existsSync(templatePath)) {
19
+ throw new Error(`Missing plist template: ${templatePath}`);
20
+ }
21
+ const template = readFileSync(templatePath, "utf8");
22
+ const xml = renderPlistTemplate(template, installDir);
23
+ return { label, templatePath, targetPath: plistPath(label), xml };
24
+ });
25
+ }
26
+ /** Write rendered plists to ~/Library/LaunchAgents and chmod the run scripts. */
27
+ export function installServiceFiles(installDir) {
28
+ mkdirSync(launchAgentsDir(), { recursive: true });
29
+ const rendered = renderServicePlists(installDir);
30
+ for (const svc of rendered) {
31
+ writeFileSync(svc.targetPath, svc.xml);
32
+ }
33
+ for (const script of ["run-web.sh", "run-vault.sh"]) {
34
+ const p = join(installDir, "scripts", "service", script);
35
+ if (existsSync(p))
36
+ chmodSync(p, 0o755);
37
+ }
38
+ return rendered;
39
+ }
40
+ /** Bootstrap + kickstart both services (idempotent bootout-then-bootstrap). */
41
+ export async function bootstrapAndStart() {
42
+ const errors = [];
43
+ const webRes = await bootstrapService(WEB_LABEL);
44
+ if (!webRes.ok)
45
+ errors.push(`web bootstrap: ${webRes.stderr.trim()}`);
46
+ const vaultRes = await bootstrapService(VAULT_LABEL);
47
+ if (!vaultRes.ok)
48
+ errors.push(`vault bootstrap: ${vaultRes.stderr.trim()}`);
49
+ const webKick = await kickstartService(WEB_LABEL);
50
+ if (!webKick.ok)
51
+ errors.push(`web kickstart: ${webKick.stderr.trim()}`);
52
+ const vaultKick = await kickstartService(VAULT_LABEL);
53
+ if (!vaultKick.ok)
54
+ errors.push(`vault kickstart: ${vaultKick.stderr.trim()}`);
55
+ return { web: webRes.ok && webKick.ok, vault: vaultRes.ok && vaultKick.ok, errors };
56
+ }
package/dist/status.js ADDED
@@ -0,0 +1,42 @@
1
+ /**
2
+ * `rlo status` (spec §2). Config summary + service state + port probes +
3
+ * git revision / behind-origin check.
4
+ */
5
+ import pc from "picocolors";
6
+ import { readConfig } from "./config.js";
7
+ import { checkVaultHealth, checkWebHealth } from "./health.js";
8
+ import { getServiceStatus } from "./launchd.js";
9
+ import { currentRevision, isBehindOrigin } from "./repo.js";
10
+ import { VAULT_LABEL, WEB_LABEL } from "./paths.js";
11
+ export async function statusCommand() {
12
+ const config = readConfig();
13
+ if (!config) {
14
+ console.error("No Arel OS install found. Run `npx arelos` to install.");
15
+ return 1;
16
+ }
17
+ console.log(pc.bold(config.displayName));
18
+ console.log(` Install dir: ${config.installDir}`);
19
+ console.log(` Vault path: ${config.vaultPath}`);
20
+ console.log(` Web port: ${config.webPort}`);
21
+ console.log(` Vault port: ${config.vaultPort}`);
22
+ console.log("");
23
+ const [webSvc, vaultSvc, webHealth, vaultHealth, rev, behind] = await Promise.all([
24
+ getServiceStatus(WEB_LABEL),
25
+ getServiceStatus(VAULT_LABEL),
26
+ checkWebHealth(config.webPort),
27
+ checkVaultHealth(config.vaultPort),
28
+ currentRevision(config.installDir),
29
+ isBehindOrigin(config.installDir),
30
+ ]);
31
+ printService("web", webSvc, webHealth.up, webHealth.status ? `HTTP ${webHealth.status}` : webHealth.error);
32
+ printService("vault", vaultSvc, vaultHealth.up, vaultHealth.up ? `displayName=${vaultHealth.displayName}` : vaultHealth.error);
33
+ console.log("");
34
+ console.log(` Revision: ${rev ?? "unknown"}${behind === true ? pc.yellow(" (behind origin — run arelos update)") : behind === false ? " (up to date)" : ""}`);
35
+ return 0;
36
+ }
37
+ function printService(name, svc, up, detail) {
38
+ const stateLabel = up ? pc.green("up") : pc.red("down");
39
+ const loadedLabel = svc.loaded ? "loaded" : "not loaded";
40
+ const pidLabel = svc.pid ? `pid ${svc.pid}` : "no pid";
41
+ console.log(` ${name.padEnd(6)} ${stateLabel} (${loadedLabel}, ${pidLabel}) ${detail ?? ""}`);
42
+ }
@@ -0,0 +1,84 @@
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.
7
+ */
8
+ import * as p from "@clack/prompts";
9
+ import pc from "picocolors";
10
+ import { existsSync, rmSync, unlinkSync } from "node:fs";
11
+ import { readConfig } from "./config.js";
12
+ import { bootoutService } from "./launchd.js";
13
+ import { VAULT_LABEL, WEB_LABEL, plistPath, configPath } from "./paths.js";
14
+ /**
15
+ * Pure gate: vault is deleted iff confirmed AND the typed word is exactly
16
+ * "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.
19
+ */
20
+ export function shouldDeleteVault(confirmed, typedWord) {
21
+ return confirmed === true && typedWord === "DELETE";
22
+ }
23
+ export async function uninstallCommand() {
24
+ const config = readConfig();
25
+ if (!config) {
26
+ console.error("No Arel OS install found. Nothing to uninstall.");
27
+ return 1;
28
+ }
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]) {
33
+ const path = plistPath(label);
34
+ if (existsSync(path))
35
+ unlinkSync(path);
36
+ }
37
+ p.log.success("Services stopped and unregistered.");
38
+ const removeInstallDir = await p.confirm({
39
+ message: `Remove the install directory ${config.installDir}?`,
40
+ initialValue: false,
41
+ });
42
+ if (p.isCancel(removeInstallDir)) {
43
+ p.cancel("Uninstall stopped after removing services.");
44
+ return 1;
45
+ }
46
+ let deleteVault = false;
47
+ const vaultConfirm = await p.confirm({
48
+ message: `Also delete your vault at ${config.vaultPath}? This erases all your notes.`,
49
+ initialValue: false,
50
+ });
51
+ if (!p.isCancel(vaultConfirm) && vaultConfirm) {
52
+ const typed = await p.text({
53
+ message: 'Type DELETE (all caps) to confirm permanent vault deletion, or anything else to cancel:',
54
+ });
55
+ const typedWord = p.isCancel(typed) ? "" : String(typed);
56
+ deleteVault = shouldDeleteVault(true, typedWord);
57
+ if (!deleteVault) {
58
+ p.log.message("Vault deletion cancelled — vault preserved.");
59
+ }
60
+ }
61
+ const removeConfig = await p.confirm({
62
+ message: "Remove the saved config (~/.arelos/config.json)? Keeping it lets a reinstall remember your settings.",
63
+ initialValue: false,
64
+ });
65
+ performUninstall(config.installDir, config.vaultPath, {
66
+ removeInstallDir: !p.isCancel(removeInstallDir) && removeInstallDir,
67
+ deleteVault,
68
+ removeConfig: !p.isCancel(removeConfig) && removeConfig,
69
+ });
70
+ p.outro(pc.green("Arel OS uninstalled." + (deleteVault ? "" : " Your vault was preserved.")));
71
+ return 0;
72
+ }
73
+ /** Destructive I/O, isolated from prompt flow for testability via direct calls with explicit choices. */
74
+ export function performUninstall(installDir, vaultPath, choices) {
75
+ if (choices.deleteVault && existsSync(vaultPath)) {
76
+ rmSync(vaultPath, { recursive: true, force: true });
77
+ }
78
+ if (choices.removeInstallDir && existsSync(installDir)) {
79
+ rmSync(installDir, { recursive: true, force: true });
80
+ }
81
+ if (choices.removeConfig && existsSync(configPath())) {
82
+ unlinkSync(configPath());
83
+ }
84
+ }
package/dist/update.js ADDED
@@ -0,0 +1,59 @@
1
+ /**
2
+ * `rlo update` (spec §2). git pull --ff-only + bun install/build + restart +
3
+ * re-health-check. Config file untouched.
4
+ */
5
+ import pc from "picocolors";
6
+ import { ensureBun } from "./bun-setup.js";
7
+ import { readConfig } from "./config.js";
8
+ import { runStreaming } from "./exec.js";
9
+ import { waitForHealthy } from "./health.js";
10
+ import { pullLatest } from "./repo.js";
11
+ 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.");
16
+ return 1;
17
+ }
18
+ return runUpdate(config);
19
+ }
20
+ export async function runUpdate(config) {
21
+ console.log(`Updating ${config.installDir}…`);
22
+ const pull = await pullLatest(config.installDir);
23
+ if (pull.dirty) {
24
+ console.error(pc.red("Working tree has uncommitted changes — refusing to pull. Run `git -C " +
25
+ `${config.installDir} stash` +
26
+ "` first."));
27
+ return 1;
28
+ }
29
+ if (pull.code !== 0) {
30
+ console.error(pc.red(`git pull failed: ${pull.stderr}`));
31
+ return 1;
32
+ }
33
+ const bunResult = await ensureBun();
34
+ if ("error" in bunResult) {
35
+ console.error(pc.red(bunResult.error));
36
+ return 1;
37
+ }
38
+ const installRes = await runStreaming(bunResult.bunBin, ["install"], { cwd: config.installDir });
39
+ if (installRes.code !== 0) {
40
+ console.error(pc.red("bun install failed."));
41
+ return 1;
42
+ }
43
+ const buildRes = await runStreaming(bunResult.bunBin, ["run", "build"], { cwd: config.installDir });
44
+ if (buildRes.code !== 0) {
45
+ console.warn(pc.yellow("bun run build failed — keeping old dist/. The web service will keep serving it."));
46
+ }
47
+ installServiceFiles(config.installDir);
48
+ const bootstrap = await bootstrapAndStart();
49
+ for (const e of bootstrap.errors)
50
+ console.error(pc.yellow(e));
51
+ console.log("Restarting services and re-checking health…");
52
+ const health = await waitForHealthy(config.webPort, config.vaultPort);
53
+ if (!health.healthy) {
54
+ console.error(pc.red("Health check timed out after update. Run `arelos logs`."));
55
+ return 1;
56
+ }
57
+ console.log(pc.green("Update complete. Arel OS is healthy."));
58
+ return 0;
59
+ }
package/package.json ADDED
@@ -0,0 +1,42 @@
1
+ {
2
+ "name": "arelos",
3
+ "version": "0.1.0",
4
+ "description": "Installer and service manager for a self-hosted Arel OS on macOS.",
5
+ "type": "module",
6
+ "bin": {
7
+ "arelos": "dist/cli.js",
8
+ "rlo": "dist/cli.js"
9
+ },
10
+ "files": [
11
+ "dist"
12
+ ],
13
+ "engines": {
14
+ "node": ">=18"
15
+ },
16
+ "os": [
17
+ "darwin"
18
+ ],
19
+ "scripts": {
20
+ "build": "tsc -p tsconfig.json",
21
+ "pretest": "tsc -p tsconfig.test.json",
22
+ "test": "node --test dist-test/test/*.test.js"
23
+ },
24
+ "dependencies": {
25
+ "@clack/prompts": "^0.9.0",
26
+ "picocolors": "^1.1.1"
27
+ },
28
+ "devDependencies": {
29
+ "@types/node": "^20.0.0",
30
+ "typescript": "^5.7.0"
31
+ },
32
+ "license": "MIT",
33
+ "repository": {
34
+ "type": "git",
35
+ "url": "git+https://github.com/vishmathpati/arel-os.git",
36
+ "directory": "cli"
37
+ },
38
+ "homepage": "https://github.com/vishmathpati/arel-os#readme",
39
+ "bugs": {
40
+ "url": "https://github.com/vishmathpati/arel-os/issues"
41
+ }
42
+ }