arelos 0.2.1 → 0.2.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-args.js +11 -0
- package/dist/cli.js +10 -2
- package/dist/ports-command.js +158 -0
- package/package.json +1 -1
package/dist/cli-args.js
CHANGED
|
@@ -43,6 +43,17 @@ export function parseInstallFlags(argv) {
|
|
|
43
43
|
}
|
|
44
44
|
return flags;
|
|
45
45
|
}
|
|
46
|
+
export function parsePortsFlags(argv) {
|
|
47
|
+
const flags = { webPort: null, vaultPort: null };
|
|
48
|
+
for (let i = 0; i < argv.length; i++) {
|
|
49
|
+
const arg = argv[i];
|
|
50
|
+
if (arg === "--web-port")
|
|
51
|
+
flags.webPort = Number(argv[++i]);
|
|
52
|
+
else if (arg === "--vault-port")
|
|
53
|
+
flags.vaultPort = Number(argv[++i]);
|
|
54
|
+
}
|
|
55
|
+
return flags;
|
|
56
|
+
}
|
|
46
57
|
export function parseLogsFlags(argv) {
|
|
47
58
|
let which = "both";
|
|
48
59
|
let follow = false;
|
package/dist/cli.js
CHANGED
|
@@ -9,10 +9,11 @@ if (process.platform !== "darwin") {
|
|
|
9
9
|
console.error("Arel OS currently supports macOS only.");
|
|
10
10
|
process.exit(1);
|
|
11
11
|
}
|
|
12
|
-
import { parseInstallFlags, parseLogsFlags } from "./cli-args.js";
|
|
12
|
+
import { parseInstallFlags, parseLogsFlags, parsePortsFlags } from "./cli-args.js";
|
|
13
13
|
import { runInstall } from "./install.js";
|
|
14
14
|
import { listCommand } from "./list.js";
|
|
15
15
|
import { logsCommand } from "./logs.js";
|
|
16
|
+
import { portsCommand } from "./ports-command.js";
|
|
16
17
|
import { statusCommand } from "./status.js";
|
|
17
18
|
import { uninstallCommand } from "./uninstall.js";
|
|
18
19
|
import { updateCommand } from "./update.js";
|
|
@@ -26,7 +27,7 @@ async function main() {
|
|
|
26
27
|
// rather than a subcommand name — both mean "install". Anything else in
|
|
27
28
|
// the known set consumes its name as the subcommand; everything after it
|
|
28
29
|
// is passed through as that subcommand's own args.
|
|
29
|
-
const knownSubcommands = new Set(["install", "status", "update", "uninstall", "logs", "list"]);
|
|
30
|
+
const knownSubcommands = new Set(["install", "status", "update", "uninstall", "logs", "list", "ports"]);
|
|
30
31
|
const firstIsFlag = argv.length === 0 || argv[0].startsWith("-");
|
|
31
32
|
const firstIsKnown = argv.length > 0 && knownSubcommands.has(argv[0]);
|
|
32
33
|
if (!firstIsFlag && !firstIsKnown) {
|
|
@@ -50,6 +51,8 @@ async function main() {
|
|
|
50
51
|
return uninstallCommand(nameArgFrom(rest));
|
|
51
52
|
case "logs":
|
|
52
53
|
return logsCommand(parseLogsFlags(rest), logsNameArgFrom(rest));
|
|
54
|
+
case "ports":
|
|
55
|
+
return portsCommand(parsePortsFlags(rest), nameArgFrom(rest));
|
|
53
56
|
default:
|
|
54
57
|
console.error(`Unknown command: ${subcommand}\n`);
|
|
55
58
|
printHelp();
|
|
@@ -89,6 +92,7 @@ Usage:
|
|
|
89
92
|
arelos update [name] git pull + rebuild + restart
|
|
90
93
|
arelos uninstall [name] Stop services, optionally remove install dir / vault
|
|
91
94
|
arelos logs [name] [web|vault] Tail service logs (-f to follow, -n <N> for line count)
|
|
95
|
+
arelos ports [name] Change the web/vault ports (interactive, or via flags below)
|
|
92
96
|
|
|
93
97
|
[name] is only needed when you have more than one install; omit it with a
|
|
94
98
|
single install, or you'll be prompted to choose interactively.
|
|
@@ -102,6 +106,10 @@ Install flags (non-interactive):
|
|
|
102
106
|
--vault-port <port>
|
|
103
107
|
--no-service Skip launchd bootstrap (for dry runs / development)
|
|
104
108
|
--local-repo <path> Use a local path instead of cloning from GitHub
|
|
109
|
+
|
|
110
|
+
Ports flags (non-interactive):
|
|
111
|
+
--web-port <port> New web port (omit to keep current)
|
|
112
|
+
--vault-port <port> New vault port (omit to keep current)
|
|
105
113
|
`);
|
|
106
114
|
}
|
|
107
115
|
main()
|
|
@@ -0,0 +1,158 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `arelos ports [name]`. Change an existing install's web/vault ports:
|
|
3
|
+
* interactive prompt (default = keep current) or non-interactive
|
|
4
|
+
* `--web-port`/`--vault-port` flags. Registry-aware like status/update/logs
|
|
5
|
+
* (see cli-context.ts resolveInstall).
|
|
6
|
+
*
|
|
7
|
+
* On change: rewrite <root>/config.json atomically, then restart both
|
|
8
|
+
* launchd services (kickstart -k — the web service's run-web.sh rebuilds and
|
|
9
|
+
* re-bakes VITE_VAULT_API from config.json on every start, so a kickstart
|
|
10
|
+
* alone is enough to pick up new ports; see scripts/service/run-web.sh), then
|
|
11
|
+
* health-check both new ports with the existing health/diagnostics machinery.
|
|
12
|
+
*
|
|
13
|
+
* The restart+health-check step is expressed as an injectable "effects"
|
|
14
|
+
* object (RestartEffects) so tests can assert the exact call sequence
|
|
15
|
+
* without ever touching real launchctl or the network.
|
|
16
|
+
*/
|
|
17
|
+
import * as p from "@clack/prompts";
|
|
18
|
+
import pc from "picocolors";
|
|
19
|
+
import { resolveRoot, resolveServiceLabels, writeConfig } from "./config.js";
|
|
20
|
+
import { resolveInstall } from "./cli-context.js";
|
|
21
|
+
import { formatHealthTimeoutDiagnostics, waitForHealthy } from "./health.js";
|
|
22
|
+
import { lastLines, logPathFor } from "./logs.js";
|
|
23
|
+
import { installConfigPath } from "./paths.js";
|
|
24
|
+
import { resolvePort } from "./install-plan.js";
|
|
25
|
+
import { bootstrapAndStart } from "./services.js";
|
|
26
|
+
/**
|
|
27
|
+
* Resolve one requested port against its current value: null (or equal to
|
|
28
|
+
* current) means "keep, no validation needed" — re-validating the port
|
|
29
|
+
* you're already bound to would spuriously fail once services are up and
|
|
30
|
+
* holding it. A genuinely different request must resolve to itself exactly
|
|
31
|
+
* (hard-fail on occupied) — `resolvePortFn` is injected so tests can stub it
|
|
32
|
+
* without touching real sockets, and the real caller passes the hardened
|
|
33
|
+
* `resolvePort` from install-plan.ts.
|
|
34
|
+
*/
|
|
35
|
+
async function resolveOneField(field, current, requested, resolvePortFn) {
|
|
36
|
+
if (requested === null || requested === current) {
|
|
37
|
+
return { field, current, requested: null, resolution: null, changed: false };
|
|
38
|
+
}
|
|
39
|
+
const resolution = await resolvePortFn(requested);
|
|
40
|
+
return { field, current, requested, resolution, changed: resolution.resolved === requested };
|
|
41
|
+
}
|
|
42
|
+
/**
|
|
43
|
+
* Build and validate a port-change plan from the install's current config
|
|
44
|
+
* and the caller's requested new ports (null = keep current). Both fields
|
|
45
|
+
* are validated independently; a requested port that's occupied (and thus
|
|
46
|
+
* would resolve to something other than itself) is a hard failure — ports is
|
|
47
|
+
* a deliberate, exact change, unlike install's "pick something nearby".
|
|
48
|
+
*/
|
|
49
|
+
export async function buildPortChangePlan(current, requested, resolvePortFn = resolvePort) {
|
|
50
|
+
if (requested.webPort !== null && requested.vaultPort !== null && requested.webPort === requested.vaultPort) {
|
|
51
|
+
return { ok: false, message: `Web port and vault port must differ (both were ${requested.webPort}).` };
|
|
52
|
+
}
|
|
53
|
+
const web = await resolveOneField("webPort", current.webPort, requested.webPort, resolvePortFn);
|
|
54
|
+
if (web.requested !== null && web.resolution && !web.resolution.wasFree) {
|
|
55
|
+
return {
|
|
56
|
+
ok: false,
|
|
57
|
+
message: `Port ${web.requested} is already in use — choose a free port for the web service.`,
|
|
58
|
+
};
|
|
59
|
+
}
|
|
60
|
+
const vault = await resolveOneField("vaultPort", current.vaultPort, requested.vaultPort, resolvePortFn);
|
|
61
|
+
if (vault.requested !== null && vault.resolution && !vault.resolution.wasFree) {
|
|
62
|
+
return {
|
|
63
|
+
ok: false,
|
|
64
|
+
message: `Port ${vault.requested} is already in use — choose a free port for the vault service.`,
|
|
65
|
+
};
|
|
66
|
+
}
|
|
67
|
+
// Cross-field collision: e.g. new web port == current (unchanged) vault port.
|
|
68
|
+
const finalWeb = web.changed ? web.requested : current.webPort;
|
|
69
|
+
const finalVault = vault.changed ? vault.requested : current.vaultPort;
|
|
70
|
+
if (finalWeb === finalVault) {
|
|
71
|
+
return { ok: false, message: `Web port and vault port must differ (both would be ${finalWeb}).` };
|
|
72
|
+
}
|
|
73
|
+
return { ok: true, plan: { web, vault, anyChange: web.changed || vault.changed } };
|
|
74
|
+
}
|
|
75
|
+
/** Apply a resolved plan to a config object, returning the updated config (pure — no I/O). */
|
|
76
|
+
export function applyPlanToConfig(config, plan) {
|
|
77
|
+
return {
|
|
78
|
+
...config,
|
|
79
|
+
webPort: plan.web.changed ? plan.web.requested : config.webPort,
|
|
80
|
+
vaultPort: plan.vault.changed ? plan.vault.requested : config.vaultPort,
|
|
81
|
+
};
|
|
82
|
+
}
|
|
83
|
+
export const realRestartEffects = {
|
|
84
|
+
restartServices: async (labels) => {
|
|
85
|
+
const result = await bootstrapAndStart(labels);
|
|
86
|
+
return { errors: result.errors };
|
|
87
|
+
},
|
|
88
|
+
waitForHealthy,
|
|
89
|
+
};
|
|
90
|
+
export async function portsCommand(flags, name, effects = realRestartEffects) {
|
|
91
|
+
const result = await resolveInstall({ name, interactive: process.stdout.isTTY === true });
|
|
92
|
+
if (!result.ok) {
|
|
93
|
+
console.error(result.message);
|
|
94
|
+
return 1;
|
|
95
|
+
}
|
|
96
|
+
const { config, root } = result.install;
|
|
97
|
+
const interactive = process.stdout.isTTY === true && flags.webPort === null && flags.vaultPort === null;
|
|
98
|
+
console.log(pc.bold(config.displayName));
|
|
99
|
+
console.log(` Web port: ${config.webPort}`);
|
|
100
|
+
console.log(` Vault port: ${config.vaultPort}`);
|
|
101
|
+
console.log("");
|
|
102
|
+
let requestedWeb = flags.webPort;
|
|
103
|
+
let requestedVault = flags.vaultPort;
|
|
104
|
+
if (interactive) {
|
|
105
|
+
p.intro(pc.bold("Change ports"));
|
|
106
|
+
const webRaw = await p.text({
|
|
107
|
+
message: "New web port (blank = keep current):",
|
|
108
|
+
placeholder: String(config.webPort),
|
|
109
|
+
defaultValue: String(config.webPort),
|
|
110
|
+
});
|
|
111
|
+
if (p.isCancel(webRaw)) {
|
|
112
|
+
p.cancel("Cancelled.");
|
|
113
|
+
return 1;
|
|
114
|
+
}
|
|
115
|
+
const vaultRaw = await p.text({
|
|
116
|
+
message: "New vault port (blank = keep current):",
|
|
117
|
+
placeholder: String(config.vaultPort),
|
|
118
|
+
defaultValue: String(config.vaultPort),
|
|
119
|
+
});
|
|
120
|
+
if (p.isCancel(vaultRaw)) {
|
|
121
|
+
p.cancel("Cancelled.");
|
|
122
|
+
return 1;
|
|
123
|
+
}
|
|
124
|
+
const webNum = Number(webRaw);
|
|
125
|
+
const vaultNum = Number(vaultRaw);
|
|
126
|
+
requestedWeb = Number.isFinite(webNum) ? webNum : null;
|
|
127
|
+
requestedVault = Number.isFinite(vaultNum) ? vaultNum : null;
|
|
128
|
+
}
|
|
129
|
+
const planOutcome = await buildPortChangePlan({ webPort: config.webPort, vaultPort: config.vaultPort }, { webPort: requestedWeb, vaultPort: requestedVault });
|
|
130
|
+
if (!planOutcome.ok) {
|
|
131
|
+
console.error(pc.red(planOutcome.message));
|
|
132
|
+
return 1;
|
|
133
|
+
}
|
|
134
|
+
const { plan } = planOutcome;
|
|
135
|
+
if (!plan.anyChange) {
|
|
136
|
+
console.log("No change — ports are already what was requested.");
|
|
137
|
+
return 0;
|
|
138
|
+
}
|
|
139
|
+
const updatedConfig = applyPlanToConfig(config, plan);
|
|
140
|
+
const cfgRoot = resolveRoot(config);
|
|
141
|
+
const cfgPath = installConfigPath(cfgRoot);
|
|
142
|
+
writeConfig(updatedConfig, cfgPath);
|
|
143
|
+
console.log(pc.green(`Config updated: web ${config.webPort} -> ${updatedConfig.webPort}, vault ${config.vaultPort} -> ${updatedConfig.vaultPort}.`));
|
|
144
|
+
const labels = resolveServiceLabels(updatedConfig);
|
|
145
|
+
console.log("Restarting services…");
|
|
146
|
+
const restart = await effects.restartServices(labels);
|
|
147
|
+
for (const e of restart.errors)
|
|
148
|
+
console.error(pc.yellow(e));
|
|
149
|
+
console.log("Waiting for the app to come back up…");
|
|
150
|
+
const health = await effects.waitForHealthy(updatedConfig.webPort, updatedConfig.vaultPort);
|
|
151
|
+
if (!health.healthy) {
|
|
152
|
+
console.error(pc.red(formatHealthTimeoutDiagnostics(cfgRoot, (path) => lastLines(path, 10), logPathFor)));
|
|
153
|
+
console.error(pc.dim("\nFull logs: arelos logs"));
|
|
154
|
+
return 1;
|
|
155
|
+
}
|
|
156
|
+
console.log(pc.green("Ports changed and Arel OS is healthy again."));
|
|
157
|
+
return 0;
|
|
158
|
+
}
|