@rubytech/create-maxy 1.0.792 → 1.0.793
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/__tests__/brew-install.test.js +141 -0
- package/dist/__tests__/brew-resolve.test.js +103 -0
- package/dist/__tests__/launchd-plist.test.js +149 -0
- package/dist/__tests__/macos-version.test.js +96 -0
- package/dist/__tests__/platform-detect.test.js +50 -0
- package/dist/brew-install.js +175 -0
- package/dist/brew-resolve.js +68 -0
- package/dist/index.js +305 -27
- package/dist/launchd-plist.js +68 -0
- package/dist/macos-version.js +53 -0
- package/dist/platform-detect.js +36 -0
- package/dist/uninstall.js +47 -0
- package/package.json +1 -1
- package/payload/platform/plugins/docs/references/deployment.md +15 -3
- package/payload/server/chunk-ZVUVUP6R.js +9892 -0
- package/payload/server/maxy-edge.js +1 -1
- package/payload/server/server.js +2 -2
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
// Task 838 — pure plist XML rendering for the macOS LaunchAgent supervisor.
|
|
2
|
+
// Mirrors the apt-resolve.ts / port-resolution.ts pattern: inputs in, decision
|
|
3
|
+
// out, no I/O. The installer wraps this with writeFileSync + spawnSync(
|
|
4
|
+
// "launchctl", ["bootstrap", `gui/${uid}`, plistPath]) — all spawn/fs lives in
|
|
5
|
+
// index.ts so this module can be unit-tested with concrete fixtures and no
|
|
6
|
+
// side effects.
|
|
7
|
+
//
|
|
8
|
+
// The Linux supervisor (systemd-user persistent unit at
|
|
9
|
+
// `~/.config/systemd/user/<service>.service`) has no analogue here: launchd's
|
|
10
|
+
// LaunchAgent format is a property-list XML document at
|
|
11
|
+
// `~/Library/LaunchAgents/<label>.plist`, and the bootstrap-into-domain flow
|
|
12
|
+
// uses launchctl(1) instead of systemctl. Both supervisors achieve the same
|
|
13
|
+
// success-criteria set: process registered with the user's session manager,
|
|
14
|
+
// auto-restart on crash, restart on logout/login, RunAtLoad on reboot.
|
|
15
|
+
/**
|
|
16
|
+
* Escape the five XML predefined entities, in order. Order matters: replace
|
|
17
|
+
* `&` first so an input of `<` doesn't double-escape via the `&` produced by
|
|
18
|
+
* the `<` rule. The output is safe to embed inside an XML <string> element.
|
|
19
|
+
*/
|
|
20
|
+
function xmlEscape(s) {
|
|
21
|
+
return s
|
|
22
|
+
.replace(/&/g, "&")
|
|
23
|
+
.replace(/</g, "<")
|
|
24
|
+
.replace(/>/g, ">")
|
|
25
|
+
.replace(/"/g, """)
|
|
26
|
+
.replace(/'/g, "'");
|
|
27
|
+
}
|
|
28
|
+
/**
|
|
29
|
+
* Render a launchd LaunchAgent plist as XML. Pure: same input → same output
|
|
30
|
+
* forever. The caller writes this to `~/Library/LaunchAgents/<label>.plist`
|
|
31
|
+
* with mode 0644 and then runs `launchctl bootstrap gui/$UID <path>`.
|
|
32
|
+
*
|
|
33
|
+
* The output is the canonical plist 1.0 form launchd accepts — XML preamble,
|
|
34
|
+
* PLIST PUBLIC DOCTYPE, <plist version="1.0"> wrapping a single <dict>. Key
|
|
35
|
+
* order inside the dict mirrors Apple's LaunchAgent examples for grep-ability:
|
|
36
|
+
* Label, ProgramArguments, StandardOutPath, StandardErrorPath, [WorkingDirectory,]
|
|
37
|
+
* KeepAlive, RunAtLoad.
|
|
38
|
+
*/
|
|
39
|
+
export function renderPlist(spec) {
|
|
40
|
+
const lines = [];
|
|
41
|
+
lines.push(`<?xml version="1.0" encoding="UTF-8"?>`);
|
|
42
|
+
lines.push(`<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">`);
|
|
43
|
+
lines.push(`<plist version="1.0">`);
|
|
44
|
+
lines.push(`<dict>`);
|
|
45
|
+
lines.push(` <key>Label</key>`);
|
|
46
|
+
lines.push(` <string>${xmlEscape(spec.label)}</string>`);
|
|
47
|
+
lines.push(` <key>ProgramArguments</key>`);
|
|
48
|
+
lines.push(` <array>`);
|
|
49
|
+
for (const arg of spec.programArguments) {
|
|
50
|
+
lines.push(` <string>${xmlEscape(arg)}</string>`);
|
|
51
|
+
}
|
|
52
|
+
lines.push(` </array>`);
|
|
53
|
+
lines.push(` <key>StandardOutPath</key>`);
|
|
54
|
+
lines.push(` <string>${xmlEscape(spec.stdoutPath)}</string>`);
|
|
55
|
+
lines.push(` <key>StandardErrorPath</key>`);
|
|
56
|
+
lines.push(` <string>${xmlEscape(spec.stderrPath)}</string>`);
|
|
57
|
+
if (spec.workingDirectory !== undefined) {
|
|
58
|
+
lines.push(` <key>WorkingDirectory</key>`);
|
|
59
|
+
lines.push(` <string>${xmlEscape(spec.workingDirectory)}</string>`);
|
|
60
|
+
}
|
|
61
|
+
lines.push(` <key>KeepAlive</key>`);
|
|
62
|
+
lines.push(` ${spec.keepAlive ? "<true/>" : "<false/>"}`);
|
|
63
|
+
lines.push(` <key>RunAtLoad</key>`);
|
|
64
|
+
lines.push(` ${spec.runAtLoad ? "<true/>" : "<false/>"}`);
|
|
65
|
+
lines.push(`</dict>`);
|
|
66
|
+
lines.push(`</plist>`);
|
|
67
|
+
return lines.join("\n") + "\n";
|
|
68
|
+
}
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
// Task 840 — pure macOS-version classification for the installer.
|
|
2
|
+
//
|
|
3
|
+
// The installer needs two darwin facts at pre-flight: the macOS version (from
|
|
4
|
+
// `sw_vers`) and whether that version meets the floor required by Tasks 838
|
|
5
|
+
// (modern `launchctl bootstrap gui/$UID …`) and 839 (Apple-Silicon Homebrew).
|
|
6
|
+
// Older macOS partially succeeds, then breaks at the supervisor or brew-cellar
|
|
7
|
+
// layer with cryptic errors — refusing loudly at pre-flight is the contract.
|
|
8
|
+
//
|
|
9
|
+
// Pure logic with no I/O — caller spawns `sw_vers`, captures stdout, and feeds
|
|
10
|
+
// it through parseSwVers + isSupportedMacosVersion. Mirrors the apt-resolve.ts
|
|
11
|
+
// pattern (Task 638): inputs in, decision out, every branch testable with
|
|
12
|
+
// real captured fixtures.
|
|
13
|
+
/**
|
|
14
|
+
* Parse the stdout of macOS's `sw_vers` command. Real output has the shape:
|
|
15
|
+
*
|
|
16
|
+
* ProductName: macOS
|
|
17
|
+
* ProductVersion: 14.4.1
|
|
18
|
+
* BuildVersion: 23E224
|
|
19
|
+
*
|
|
20
|
+
* Returns null when either ProductName or ProductVersion is absent — the
|
|
21
|
+
* caller treats null as "refuse with malformed-stdout diagnostic" rather than
|
|
22
|
+
* silently coercing to undefined and producing a confusing version check.
|
|
23
|
+
*/
|
|
24
|
+
export function parseSwVers(stdout) {
|
|
25
|
+
const productMatch = stdout.match(/^ProductName:\s*(.+)$/m);
|
|
26
|
+
const versionMatch = stdout.match(/^ProductVersion:\s*(.+)$/m);
|
|
27
|
+
if (!productMatch || !versionMatch)
|
|
28
|
+
return null;
|
|
29
|
+
return {
|
|
30
|
+
product: productMatch[1].trim(),
|
|
31
|
+
version: versionMatch[1].trim(),
|
|
32
|
+
};
|
|
33
|
+
}
|
|
34
|
+
/**
|
|
35
|
+
* True when `version`'s major component is ≥ 14 (the floor required by Tasks
|
|
36
|
+
* 838 + 839). Returns false for empty strings, non-numeric leading components,
|
|
37
|
+
* and any major < 14. A numeric-only check is sufficient — Apple's published
|
|
38
|
+
* macOS versions use a strict `MAJOR.MINOR[.PATCH]` integer scheme.
|
|
39
|
+
*/
|
|
40
|
+
export function isSupportedMacosVersion(version) {
|
|
41
|
+
const m = version.match(/^(\d+)\./);
|
|
42
|
+
if (!m) {
|
|
43
|
+
// Fall back to whole-version-as-integer for the macOS 14.0 / 15 form
|
|
44
|
+
// where the leading number isn't followed by a dot. Defence in depth —
|
|
45
|
+
// the regex above already handles "14.0", but a bare "14" should also
|
|
46
|
+
// pass.
|
|
47
|
+
const bare = version.match(/^(\d+)$/);
|
|
48
|
+
if (!bare)
|
|
49
|
+
return false;
|
|
50
|
+
return parseInt(bare[1], 10) >= 14;
|
|
51
|
+
}
|
|
52
|
+
return parseInt(m[1], 10) >= 14;
|
|
53
|
+
}
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
// Task 836 wedge — pure platform classification for the installer.
|
|
2
|
+
//
|
|
3
|
+
// The installer's existing `isLinux()` checks (26 sites in index.ts) collapse
|
|
4
|
+
// process.platform into a boolean and treat every non-Linux value as "skip
|
|
5
|
+
// silently." Follow-up tasks 838/839/840 widen that to an explicit ternary
|
|
6
|
+
// (linux | darwin | unsupported) so unsupported targets refuse loudly with a
|
|
7
|
+
// log line operators can grep, instead of completing in a non-functional state.
|
|
8
|
+
//
|
|
9
|
+
// This module ships dead until those follow-ups land. Pure logic with no I/O —
|
|
10
|
+
// caller injects process.platform so tests pass arbitrary fixtures without
|
|
11
|
+
// touching the host's actual platform.
|
|
12
|
+
/**
|
|
13
|
+
* Classify a Node process.platform value into the ternary the installer cares
|
|
14
|
+
* about. Anything not in { linux, darwin } returns "unsupported"; the caller
|
|
15
|
+
* decides whether to refuse the install or no-op.
|
|
16
|
+
*/
|
|
17
|
+
export function detectPlatform(nodePlatform) {
|
|
18
|
+
if (nodePlatform === "linux")
|
|
19
|
+
return "linux";
|
|
20
|
+
if (nodePlatform === "darwin")
|
|
21
|
+
return "darwin";
|
|
22
|
+
return "unsupported";
|
|
23
|
+
}
|
|
24
|
+
/**
|
|
25
|
+
* Narrow a Node process.platform to a SupportedPlatform or throw with the
|
|
26
|
+
* literal refusal message follow-up tasks log + grep on. Use this at install
|
|
27
|
+
* pre-flight where continuing on an unsupported OS would silently complete a
|
|
28
|
+
* broken install.
|
|
29
|
+
*/
|
|
30
|
+
export function requireSupportedPlatform(nodePlatform) {
|
|
31
|
+
const detected = detectPlatform(nodePlatform);
|
|
32
|
+
if (detected === "unsupported") {
|
|
33
|
+
throw new Error(`[create-maxy] platform=${nodePlatform} — refusing: only linux and darwin are supported`);
|
|
34
|
+
}
|
|
35
|
+
return detected;
|
|
36
|
+
}
|
package/dist/uninstall.js
CHANGED
|
@@ -64,6 +64,23 @@ function shell(command, args, options) {
|
|
|
64
64
|
function isLinux() {
|
|
65
65
|
return process.platform === "linux";
|
|
66
66
|
}
|
|
67
|
+
function isDarwin() {
|
|
68
|
+
return process.platform === "darwin";
|
|
69
|
+
}
|
|
70
|
+
// Task 838 — mirror the launchd identifiers from index.ts so the uninstall
|
|
71
|
+
// can locate the LaunchAgent without reaching across modules. Per the
|
|
72
|
+
// `uninstall.ts:` "Shell helpers (duplicated from index.ts ...)" policy,
|
|
73
|
+
// these are intentionally duplicated rather than shared.
|
|
74
|
+
function launchdLabel() {
|
|
75
|
+
return `com.rubytech.${BRAND.hostname}`;
|
|
76
|
+
}
|
|
77
|
+
function plistPath() {
|
|
78
|
+
return resolve(HOME, "Library/LaunchAgents", `${launchdLabel()}.plist`);
|
|
79
|
+
}
|
|
80
|
+
function gui() {
|
|
81
|
+
const uid = typeof process.getuid === "function" ? process.getuid() : 0;
|
|
82
|
+
return `gui/${uid}`;
|
|
83
|
+
}
|
|
67
84
|
function commandExists(cmd) {
|
|
68
85
|
try {
|
|
69
86
|
execFileSync("which", [cmd], { stdio: "pipe" });
|
|
@@ -120,6 +137,15 @@ function peerBrandPresent() {
|
|
|
120
137
|
// ---------------------------------------------------------------------------
|
|
121
138
|
function stopServices() {
|
|
122
139
|
log("1", "Stopping services...");
|
|
140
|
+
// Task 838 — darwin: bootout the LaunchAgent. bootout exits 0 on success,
|
|
141
|
+
// 113 ("Unknown service") when the agent isn't loaded — both are
|
|
142
|
+
// acceptable end-states for "service stopped". The plist removal in
|
|
143
|
+
// removeSystemdService() finishes the cleanup.
|
|
144
|
+
if (isDarwin()) {
|
|
145
|
+
spawnSync("launchctl", ["bootout", `${gui()}/${launchdLabel()}`], { stdio: "pipe", timeout: 15_000 });
|
|
146
|
+
console.log(` Booted out ${launchdLabel()}`);
|
|
147
|
+
return;
|
|
148
|
+
}
|
|
123
149
|
// Stop platform user service
|
|
124
150
|
try {
|
|
125
151
|
spawnSync("systemctl", ["--user", "stop", BRAND.serviceName.replace(".service", "")], { stdio: "pipe", timeout: 15_000 });
|
|
@@ -563,6 +589,27 @@ function removeSystemConfig() {
|
|
|
563
589
|
// ---------------------------------------------------------------------------
|
|
564
590
|
function removeSystemdService() {
|
|
565
591
|
log("8", "Removing systemd service...");
|
|
592
|
+
// Task 838 — darwin: bootout (idempotent — already done in stopServices,
|
|
593
|
+
// re-running is a no-op exit 113) and delete the plist. The wrapper
|
|
594
|
+
// shell script lives in CONFIG_DIR which step 4 (removeAppDirs) wipes,
|
|
595
|
+
// so no separate cleanup is needed.
|
|
596
|
+
if (isDarwin()) {
|
|
597
|
+
spawnSync("launchctl", ["bootout", `${gui()}/${launchdLabel()}`], { stdio: "pipe", timeout: 15_000 });
|
|
598
|
+
const plist = plistPath();
|
|
599
|
+
if (existsSync(plist)) {
|
|
600
|
+
try {
|
|
601
|
+
rmSync(plist);
|
|
602
|
+
console.log(` Removed ${plist}`);
|
|
603
|
+
}
|
|
604
|
+
catch (err) {
|
|
605
|
+
console.log(` Failed to remove ${plist}: ${err instanceof Error ? err.message : String(err)}`);
|
|
606
|
+
}
|
|
607
|
+
}
|
|
608
|
+
else {
|
|
609
|
+
console.log(` ${plist} not found — skipping`);
|
|
610
|
+
}
|
|
611
|
+
return;
|
|
612
|
+
}
|
|
566
613
|
// Disable the service
|
|
567
614
|
const svcUnit = BRAND.serviceName.replace(".service", "");
|
|
568
615
|
try {
|
package/package.json
CHANGED
|
@@ -2,11 +2,23 @@
|
|
|
2
2
|
|
|
3
3
|
## Hardware Requirements
|
|
4
4
|
|
|
5
|
-
- Raspberry Pi 5 (16GB RAM minimum)
|
|
6
|
-
-
|
|
7
|
-
-
|
|
5
|
+
- Raspberry Pi 5 (16GB RAM minimum) with Raspberry Pi OS, **or**
|
|
6
|
+
- Mac with macOS 14 (Sonoma) or newer — both Apple Silicon and Intel
|
|
7
|
+
- 256GB storage minimum
|
|
8
8
|
- Always-on power and network connection
|
|
9
9
|
|
|
10
|
+
## macOS install
|
|
11
|
+
|
|
12
|
+
On macOS the installer uses Homebrew + launchd instead of apt + systemd, but the operator command is the same:
|
|
13
|
+
|
|
14
|
+
```bash
|
|
15
|
+
npx -y @rubytech/create-maxy
|
|
16
|
+
```
|
|
17
|
+
|
|
18
|
+
**Prerequisite:** Homebrew. If `brew` is missing, the installer refuses with `Homebrew not found. Install from https://brew.sh and re-run.` Install Homebrew once via the official one-liner, then re-run.
|
|
19
|
+
|
|
20
|
+
The installer registers {{productName}} as a launchd LaunchAgent at `~/Library/LaunchAgents/com.rubytech.maxy.plist` — survives logout/login and reboot. Use `launchctl print gui/$UID/com.rubytech.maxy` for service state. `--hostname <h>` sets HostName / LocalHostName / ComputerName via `sudo scutil`. macOS < 14 is refused at pre-flight.
|
|
21
|
+
|
|
10
22
|
## Initial Setup
|
|
11
23
|
|
|
12
24
|
The {{productName}} installer handles the full setup. Run it on your Pi:
|