buddymac 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.
- package/README.md +19 -0
- package/cli.mjs +94 -0
- package/package.json +26 -0
- package/release.json +5 -0
package/README.md
ADDED
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
# BuddyMac installer
|
|
2
|
+
|
|
3
|
+
```sh
|
|
4
|
+
npx buddymac
|
|
5
|
+
```
|
|
6
|
+
|
|
7
|
+
Downloads BuddyMac to `~/Applications` and opens it. Requires an Apple Silicon Mac running macOS 26 or later and Node.js 20 or later. No Bun installation is needed.
|
|
8
|
+
|
|
9
|
+
The installer checks a pinned SHA-256 checksum and Francesco Oddo's Apple Developer signature. It does not overwrite existing apps or change your app data. To update, quit BuddyMac and move the previous app out of the destination first.
|
|
10
|
+
|
|
11
|
+
```sh
|
|
12
|
+
npx buddymac --no-open
|
|
13
|
+
npx buddymac --destination /Applications
|
|
14
|
+
npx buddymac --help
|
|
15
|
+
```
|
|
16
|
+
|
|
17
|
+
This early release is signed but not notarized. If macOS blocks the first launch, open System Settings → Privacy & Security → Open Anyway. The installer preserves macOS download protection.
|
|
18
|
+
|
|
19
|
+
Source and downloads: https://github.com/OxFrancesco/BuddyMac
|
package/cli.mjs
ADDED
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { createHash } from 'node:crypto';
|
|
3
|
+
import { createReadStream, createWriteStream } from 'node:fs';
|
|
4
|
+
import { access, mkdir, mkdtemp, readFile, rename, rm } from 'node:fs/promises';
|
|
5
|
+
import { homedir } from 'node:os';
|
|
6
|
+
import { join, resolve } from 'node:path';
|
|
7
|
+
import { spawnSync } from 'node:child_process';
|
|
8
|
+
import { Readable } from 'node:stream';
|
|
9
|
+
import { pipeline } from 'node:stream/promises';
|
|
10
|
+
|
|
11
|
+
const release = JSON.parse(await readFile(new URL('./release.json', import.meta.url), 'utf8'));
|
|
12
|
+
const args = process.argv.slice(2);
|
|
13
|
+
const help = `Usage: npx buddymac [--destination <folder>] [--no-open]
|
|
14
|
+
|
|
15
|
+
Installs BuddyMac ${release.version} to ~/Applications and opens it.
|
|
16
|
+
Requires Apple Silicon, macOS 26+, and Node.js 20+.
|
|
17
|
+
Existing apps are never overwritten. Quit and move an old copy before updating.
|
|
18
|
+
This early build is signed but not notarized. macOS may require Open Anyway
|
|
19
|
+
in System Settings > Privacy & Security after the first launch attempt.
|
|
20
|
+
|
|
21
|
+
--destination <folder> Install in a different folder
|
|
22
|
+
--no-open Install without opening the app
|
|
23
|
+
--version Print the bundled release version
|
|
24
|
+
--help Show this help
|
|
25
|
+
`;
|
|
26
|
+
|
|
27
|
+
function run(command, parameters) {
|
|
28
|
+
const result = spawnSync(command, parameters, { encoding: 'utf8', env: { ...process.env } });
|
|
29
|
+
if (result.error) throw result.error;
|
|
30
|
+
if (result.status !== 0) throw new Error(result.stderr?.trim() || `${command} failed`);
|
|
31
|
+
return result.stdout.trim();
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
async function install() {
|
|
35
|
+
let destination = join(homedir(), 'Applications');
|
|
36
|
+
let open = true;
|
|
37
|
+
for (let i = 0; i < args.length; i++) {
|
|
38
|
+
switch (args[i]) {
|
|
39
|
+
case '--help': case '-h': process.stdout.write(help); return;
|
|
40
|
+
case '--version': process.stdout.write(`${release.version}\n`); return;
|
|
41
|
+
case '--no-open': open = false; break;
|
|
42
|
+
case '--destination':
|
|
43
|
+
if (!args[i + 1] || args[i + 1].startsWith('--')) throw new Error('--destination requires a folder.');
|
|
44
|
+
destination = resolve(args[++i]);
|
|
45
|
+
break;
|
|
46
|
+
default: throw new Error(`Unknown option: ${args[i]}. Use --help.`);
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
if (process.platform !== 'darwin' || process.arch !== 'arm64') throw new Error('BuddyMac requires an Apple Silicon Mac.');
|
|
50
|
+
const macOS = run('/usr/bin/sw_vers', ['-productVersion']);
|
|
51
|
+
if (Number(macOS.split('.')[0]) < 26) throw new Error(`BuddyMac requires macOS 26 or later; found ${macOS}.`);
|
|
52
|
+
await mkdir(destination, { recursive: true });
|
|
53
|
+
const target = join(destination, 'BuddyMac.app');
|
|
54
|
+
const lock = join(destination, '.buddymac-install-lock');
|
|
55
|
+
await mkdir(lock).catch(error => {
|
|
56
|
+
if (error.code === 'EEXIST') throw new Error(`Another install may be running. If it stopped, remove ${lock} and retry.`);
|
|
57
|
+
throw error;
|
|
58
|
+
});
|
|
59
|
+
let stage;
|
|
60
|
+
try {
|
|
61
|
+
try {
|
|
62
|
+
await access(target);
|
|
63
|
+
throw new Error(`${target} already exists. Quit BuddyMac and move that copy before installing, or choose another --destination.`);
|
|
64
|
+
} catch (error) {
|
|
65
|
+
if (error.code !== 'ENOENT') throw error;
|
|
66
|
+
}
|
|
67
|
+
stage = await mkdtemp(join(destination, '.buddymac-'));
|
|
68
|
+
const archive = join(stage, 'BuddyMac.zip');
|
|
69
|
+
process.stdout.write(`Downloading BuddyMac ${release.version}…\n`);
|
|
70
|
+
const response = await fetch(release.url, { signal: AbortSignal.timeout(300_000) });
|
|
71
|
+
if (!response.ok || !response.body) throw new Error(`Download failed: HTTP ${response.status}. ${release.url}`);
|
|
72
|
+
await pipeline(Readable.fromWeb(response.body), createWriteStream(archive, { flags: 'wx' }));
|
|
73
|
+
const hash = createHash('sha256');
|
|
74
|
+
for await (const chunk of createReadStream(archive)) hash.update(chunk);
|
|
75
|
+
if (hash.digest('hex') !== release.sha256) throw new Error('Download checksum mismatch. No app was installed.');
|
|
76
|
+
run('/usr/bin/ditto', ['-x', '-k', archive, stage]);
|
|
77
|
+
const app = join(stage, 'BuddyMac.app');
|
|
78
|
+
run('/usr/bin/codesign', ['--verify', '--deep', '--strict', '-R', '=anchor apple generic and certificate leaf[subject.OU] = "G2442WAF29"', app]);
|
|
79
|
+
const identifier = run('/usr/libexec/PlistBuddy', ['-c', 'Print :CFBundleIdentifier', join(app, 'Contents/Info.plist')]);
|
|
80
|
+
if (identifier !== 'org.buddytools.BuddyMac') throw new Error('Unexpected application identifier.');
|
|
81
|
+
run('/usr/bin/xattr', ['-w', 'com.apple.quarantine', `0081;${Math.floor(Date.now() / 1000).toString(16)};BuddyMac Installer;`, app]);
|
|
82
|
+
await rename(app, target);
|
|
83
|
+
process.stdout.write(`Installed ${target}\nThis build is not notarized. If macOS blocks it, use System Settings > Privacy & Security > Open Anyway.\n`);
|
|
84
|
+
if (open) run('/usr/bin/open', [target]);
|
|
85
|
+
} finally {
|
|
86
|
+
if (stage) await rm(stage, { recursive: true, force: true });
|
|
87
|
+
await rm(lock, { recursive: true, force: true });
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
install().catch(error => {
|
|
92
|
+
process.stderr.write(`BuddyMac: ${error.message}\n`);
|
|
93
|
+
process.exitCode = 1;
|
|
94
|
+
});
|
package/package.json
ADDED
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "buddymac",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "Download and install BuddyMac for Apple Silicon Macs",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"bin": {
|
|
7
|
+
"buddymac": "cli.mjs"
|
|
8
|
+
},
|
|
9
|
+
"files": ["cli.mjs", "release.json", "README.md"],
|
|
10
|
+
"engines": {
|
|
11
|
+
"node": ">=20"
|
|
12
|
+
},
|
|
13
|
+
"os": ["darwin"],
|
|
14
|
+
"cpu": ["arm64"],
|
|
15
|
+
"repository": {
|
|
16
|
+
"type": "git",
|
|
17
|
+
"url": "git+https://github.com/OxFrancesco/BuddyMac.git",
|
|
18
|
+
"directory": "packages/buddymac"
|
|
19
|
+
},
|
|
20
|
+
"homepage": "https://github.com/OxFrancesco/BuddyMac",
|
|
21
|
+
"author": "Francesco Oddo",
|
|
22
|
+
"license": "UNLICENSED",
|
|
23
|
+
"publishConfig": {
|
|
24
|
+
"access": "public"
|
|
25
|
+
}
|
|
26
|
+
}
|