| 1 | |
| 2 | var promise = require('promise'); |
| 3 | |
| 4 | function apps() { |
| 5 | var ret = new promise(function (res, rej) { this._res = res; this._rej = rej; }); |
| 6 | |
| 7 | function runSh(script) { |
| 8 | try { |
| 9 | var child = require('child_process').execFile('/bin/sh', ['sh']); |
| 10 | child.stdout.str = ''; |
| 11 | child.stdout.on('data', function (d) { child.stdout.str += d; }); |
| 12 | child.stdin.write(script + '\nexit\n'); |
| 13 | child.waitExit(); |
| 14 | return child.stdout.str.trim(); |
| 15 | } catch (ex) { return ''; } |
| 16 | } |
| 17 | |
| 18 | // Single shell process: PlistBuddy handles both XML and binary plists. |
| 19 | // Outputs tab-separated: name, version, publisher, date, scope — one app per line. |
| 20 | var script = [ |
| 21 | 'PB=/usr/libexec/PlistBuddy', |
| 22 | 'process_app() {', |
| 23 | ' app="$1" scope="$2"', |
| 24 | ' plist="$app/Contents/Info.plist"', |
| 25 | ' [ -f "$plist" ] || return', |
| 26 | ' name=$(basename "$app" .app)', |
| 27 | ' version=$("$PB" -c "Print :CFBundleShortVersionString" "$plist" 2>/dev/null)', |
| 28 | ' publisher=$("$PB" -c "Print :NSHumanReadableCopyright" "$plist" 2>/dev/null)', |
| 29 | ' if [ -z "$publisher" ]; then', |
| 30 | ' bid=$("$PB" -c "Print :CFBundleIdentifier" "$plist" 2>/dev/null)', |
| 31 | ' publisher=$(echo "$bid" | awk -F. \'{if(NF>=2){s=$2; print toupper(substr(s,1,1)) substr(s,2)}}\')', |
| 32 | ' fi', |
| 33 | ' date=$(stat -f "%Sm" -t "%Y-%m-%d" "$app" 2>/dev/null)', |
| 34 | ' arch=""', |
| 35 | ' exe=$("$PB" -c "Print :CFBundleExecutable" "$plist" 2>/dev/null)', |
| 36 | ' if [ -n "$exe" ]; then', |
| 37 | ' lipoout=$(lipo -info "$app/Contents/MacOS/$exe" 2>/dev/null)', |
| 38 | ' if echo "$lipoout" | grep -q "x86_64" && echo "$lipoout" | grep -q "arm64"; then', |
| 39 | ' arch="universal"', |
| 40 | ' elif echo "$lipoout" | grep -q "arm64"; then', |
| 41 | ' arch="arm64"', |
| 42 | ' elif echo "$lipoout" | grep -q "x86_64"; then', |
| 43 | ' arch="x86_64"', |
| 44 | ' fi', |
| 45 | ' fi', |
| 46 | ' printf "%s\\t%s\\t%s\\t%s\\t%s\\t%s\\n" \\', |
| 47 | ' "$(printf "%s" "$name" | tr "\\t\\n" " ")" \\', |
| 48 | ' "$(printf "%s" "$version" | tr "\\t\\n" " ")" \\', |
| 49 | ' "$(printf "%s" "$publisher" | tr "\\t\\n" " ")" \\', |
| 50 | ' "$date" "$arch" "$scope"', |
| 51 | '}', |
| 52 | 'for sysdir in /Applications /System/Applications /System/Applications/Utilities; do', |
| 53 | ' [ -d "$sysdir" ] || continue', |
| 54 | ' find "$sysdir" -maxdepth 1 -name "*.app" -type d 2>/dev/null | while IFS= read -r app; do', |
| 55 | ' process_app "$app" system', |
| 56 | ' done', |
| 57 | 'done', |
| 58 | 'for userdir in /Users/*/; do', |
| 59 | ' user=$(basename "$userdir")', |
| 60 | ' [ "$user" = Shared ] && continue', |
| 61 | ' case "$user" in .*) continue;; esac', |
| 62 | ' appsdir="${userdir}Applications"', |
| 63 | ' [ -d "$appsdir" ] || continue', |
| 64 | ' find "$appsdir" -maxdepth 1 -name "*.app" -type d 2>/dev/null | while IFS= read -r app; do', |
| 65 | ' process_app "$app" user', |
| 66 | ' done', |
| 67 | 'done' |
| 68 | ].join('\n'); |
| 69 | |
| 70 | try { |
| 71 | var out = runSh(script); |
| 72 | var results = []; |
| 73 | if (out) { |
| 74 | out.split('\n').forEach(function (line) { |
| 75 | var p = line.split('\t'); |
| 76 | if (p.length < 6 || !p[0]) return; |
| 77 | results.push({ name: p[0], version: p[1] || '', publisher: p[2] || '', date: p[3] || '', arch: p[4] || '', location: p[5] || 'system' }); |
| 78 | }); |
| 79 | } |
| 80 | ret._res(results); |
| 81 | } catch (e) { |
| 82 | ret._rej(e); |
| 83 | } |
| 84 | return (ret); |
| 85 | } |
| 86 | |
| 87 | if (process.platform == 'darwin') { |
| 88 | module.exports = { apps: apps }; |
| 89 | } else { |
| 90 | var not_supported = function () { throw (process.platform + ' not supported'); }; |
| 91 | module.exports = { apps: not_supported }; |
| 92 | } |