buddymac 0.1.2 → 0.1.4

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 CHANGED
@@ -6,7 +6,7 @@ npx buddymac
6
6
 
7
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
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.
9
+ The installer shows download progress and checks a pinned SHA-256 checksum, Francesco Oddo's Apple Developer signature, and macOS Gatekeeper approval. 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
10
 
11
11
  ```sh
12
12
  npx buddymac --no-open
@@ -14,6 +14,8 @@ npx buddymac --destination /Applications
14
14
  npx buddymac --help
15
15
  ```
16
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.
17
+ The app is signed and notarized by Apple, with the notarization ticket included. The installer preserves macOS download protection.
18
18
 
19
19
  Source and downloads: https://github.com/OxFrancesco/BuddyMac
20
+
21
+ Installer 0.1.4 downloads the same notarized BuddyMac 0.1.3 app using macOS curl with HTTP/2 and a smaller tar.xz archive. The installed app remains in Applications after Terminal closes.
package/cli.mjs CHANGED
@@ -1,26 +1,26 @@
1
1
  #!/usr/bin/env node
2
2
  import { createHash } from 'node:crypto';
3
- import { createReadStream, createWriteStream } from 'node:fs';
3
+ import { createWriteStream } from 'node:fs';
4
4
  import { access, mkdir, mkdtemp, readFile, rename, rm } from 'node:fs/promises';
5
5
  import { homedir } from 'node:os';
6
6
  import { join, resolve } from 'node:path';
7
- import { spawnSync } from 'node:child_process';
8
- import { Readable } from 'node:stream';
7
+ import { spawn, spawnSync } from 'node:child_process';
8
+ import { Transform } from 'node:stream';
9
9
  import { pipeline } from 'node:stream/promises';
10
10
 
11
11
  const release = JSON.parse(await readFile(new URL('./release.json', import.meta.url), 'utf8'));
12
+ const { version: installerVersion } = JSON.parse(await readFile(new URL('./package.json', import.meta.url), 'utf8'));
12
13
  const args = process.argv.slice(2);
13
14
  const help = `Usage: npx buddymac [--destination <folder>] [--no-open]
14
15
 
15
16
  Installs BuddyMac ${release.version} to ~/Applications and opens it.
16
17
  Requires Apple Silicon, macOS 26+, and Node.js 20+.
17
18
  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.
19
+ The download is signed and notarized by Apple. macOS download protection stays enabled.
20
20
 
21
21
  --destination <folder> Install in a different folder
22
22
  --no-open Install without opening the app
23
- --version Print the bundled release version
23
+ --version Print the installer version
24
24
  --help Show this help
25
25
  `;
26
26
 
@@ -32,12 +32,13 @@ function run(command, parameters) {
32
32
  }
33
33
 
34
34
  async function install() {
35
+ const started = performance.now();
35
36
  let destination = join(homedir(), 'Applications');
36
37
  let open = true;
37
38
  for (let i = 0; i < args.length; i++) {
38
39
  switch (args[i]) {
39
40
  case '--help': case '-h': process.stdout.write(help); return;
40
- case '--version': process.stdout.write(`${release.version}\n`); return;
41
+ case '--version': process.stdout.write(`${installerVersion}\n`); return;
41
42
  case '--no-open': open = false; break;
42
43
  case '--destination':
43
44
  if (!args[i + 1] || args[i + 1].startsWith('--')) throw new Error('--destination requires a folder.');
@@ -65,22 +66,58 @@ async function install() {
65
66
  if (error.code !== 'ENOENT') throw error;
66
67
  }
67
68
  stage = await mkdtemp(join(destination, '.buddymac-'));
68
- const archive = join(stage, 'BuddyMac.zip');
69
+ const archive = join(stage, 'BuddyMac.tar.xz');
69
70
  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' }));
71
+ const downloadStarted = performance.now();
72
+ const download = spawn('/usr/bin/curl', [
73
+ '--disable', '--fail', '--location', '--http2', '--proto', '=https', '--proto-redir', '=https',
74
+ '--silent', '--show-error', '--connect-timeout', '20', '--max-time', '300', release.url,
75
+ ], { env: { ...process.env }, stdio: ['ignore', 'pipe', 'pipe'] });
76
+ let downloadError = '';
77
+ download.stderr.on('data', chunk => { downloadError = (downloadError + chunk.toString()).slice(-4096); });
78
+ const finished = new Promise((resolve, reject) => {
79
+ download.once('error', reject);
80
+ download.once('close', code => code === 0 ? resolve() : reject(new Error(downloadError.trim() || `Download failed: curl exited with ${code}.`)));
81
+ });
82
+ const total = release.bytes;
83
+ let received = 0;
73
84
  const hash = createHash('sha256');
74
- for await (const chunk of createReadStream(archive)) hash.update(chunk);
85
+ const meter = new Transform({ transform(chunk, encoding, done) {
86
+ received += chunk.length;
87
+ hash.update(chunk);
88
+ done(null, chunk);
89
+ } });
90
+ const progress = () => {
91
+ const seconds = (performance.now() - downloadStarted) / 1000;
92
+ const mb = (received / 1_000_000).toFixed(1);
93
+ const size = total > 0 ? ` / ${(total / 1_000_000).toFixed(1)} MB (${Math.min(100, Math.floor(received / total * 100))}%)` : ' MB';
94
+ const message = `${mb}${size} · ${(received / 1_000_000 / Math.max(seconds, 0.001)).toFixed(1)} MB/s · ${seconds.toFixed(0)}s`;
95
+ process.stdout.write(process.stdout.isTTY ? `\r\x1b[2K${message}` : `${message}\n`);
96
+ };
97
+ const timer = setInterval(progress, process.stdout.isTTY ? 250 : 5000);
98
+ try {
99
+ await Promise.all([finished, pipeline(download.stdout, meter, createWriteStream(archive, { flags: 'wx' }))]);
100
+ } finally {
101
+ if (download.exitCode === null) download.kill();
102
+ await finished.catch(() => {});
103
+ clearInterval(timer);
104
+ if (process.stdout.isTTY) process.stdout.write('\r\x1b[2K');
105
+ }
75
106
  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]);
107
+ process.stdout.write(`Download verified in ${((performance.now() - downloadStarted) / 1000).toFixed(1)}s. Unpacking…\n`);
108
+ const unpackStarted = performance.now();
109
+ run('/usr/bin/tar', ['-xJf', archive, '-C', stage]);
77
110
  const app = join(stage, 'BuddyMac.app');
111
+ process.stdout.write(`Unpacked in ${((performance.now() - unpackStarted) / 1000).toFixed(1)}s. Checking Apple signature and notarization…\n`);
112
+ const verificationStarted = performance.now();
78
113
  run('/usr/bin/codesign', ['--verify', '--deep', '--strict', '-R', '=anchor apple generic and certificate leaf[subject.OU] = "G2442WAF29"', app]);
79
114
  const identifier = run('/usr/libexec/PlistBuddy', ['-c', 'Print :CFBundleIdentifier', join(app, 'Contents/Info.plist')]);
80
115
  if (identifier !== 'org.buddytools.BuddyMac') throw new Error('Unexpected application identifier.');
81
116
  run('/usr/bin/xattr', ['-w', 'com.apple.quarantine', `0081;${Math.floor(Date.now() / 1000).toString(16)};BuddyMac Installer;`, app]);
117
+ run('/usr/sbin/spctl', ['--assess', '--type', 'execute', app]);
118
+ process.stdout.write(`Apple verification passed in ${((performance.now() - verificationStarted) / 1000).toFixed(1)}s.\n`);
82
119
  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`);
120
+ process.stdout.write(`Installed ${target} in ${((performance.now() - started) / 1000).toFixed(1)}s.\n`);
84
121
  if (open) run('/usr/bin/open', [target]);
85
122
  } finally {
86
123
  if (stage) await rm(stage, { recursive: true, force: true });
package/package.json CHANGED
@@ -1,17 +1,25 @@
1
1
  {
2
2
  "name": "buddymac",
3
- "version": "0.1.2",
3
+ "version": "0.1.4",
4
4
  "description": "Download and install BuddyMac for Apple Silicon Macs",
5
5
  "type": "module",
6
6
  "bin": {
7
7
  "buddymac": "cli.mjs"
8
8
  },
9
- "files": ["cli.mjs", "release.json", "README.md"],
9
+ "files": [
10
+ "cli.mjs",
11
+ "release.json",
12
+ "README.md"
13
+ ],
10
14
  "engines": {
11
15
  "node": ">=20"
12
16
  },
13
- "os": ["darwin"],
14
- "cpu": ["arm64"],
17
+ "os": [
18
+ "darwin"
19
+ ],
20
+ "cpu": [
21
+ "arm64"
22
+ ],
15
23
  "repository": {
16
24
  "type": "git",
17
25
  "url": "git+https://github.com/OxFrancesco/BuddyMac.git",
package/release.json CHANGED
@@ -1,5 +1,6 @@
1
1
  {
2
- "version": "0.1.2",
3
- "url": "https://github.com/OxFrancesco/BuddyMac/releases/download/v0.1.2/BuddyMac-darwin-arm64.zip",
4
- "sha256": "ddf75a0c1bd7d34fad4db252228edf66eaeecfb4162ce0f560f2a4ed43ce44a5"
2
+ "version": "0.1.3",
3
+ "url": "https://github.com/OxFrancesco/BuddyMac/releases/download/v0.1.3/BuddyMac-darwin-arm64.tar.xz",
4
+ "sha256": "5baa5ab5d4c45154c3b63f78e14bd7cbab0dc6886173f5e839feaa995943a206",
5
+ "bytes": 28078884
5
6
  }