#!/usr/bin/env node // ableton-template.mjs — install the stripped "AC Blank" Set into each fleet Mac's Live browser. // // node ableton-template.mjs # build + install as a browser template // node ableton-template.mjs blueberry # just blueberry // node ableton-template.mjs --default # ALSO make it the Set that ⌘N opens // node ableton-template.mjs --name "AC Blank" // // The Set itself is generated by reference/ableton/als-template.mjs — one audio track and the Main // out, no MIDI tracks, no returns. // // TWO PLACES, TWO MEANINGS: // // ~/Music/Ableton/User Library/Templates/.als // What Live's browser lists under Templates. One click to a blank Set. Always installed. // // ~/Library/Preferences/Ableton/Live /BaseFiles/DefaultLiveSet.als (--default) // The Set `New Live Set` (⌘N) opens. The FILENAME is the whole trick — `DefaultLiveSet.als`, // which is the string the Live binary carries. Copies named anything else (Template.als, the // Set's own name) are ignored no matter where you put them, which is what made this so slippery. // Only overwritten with --default, and the previous default is backed up first, because a Mac // may already have a hand-made default worth keeping (neo did). // // UNVERIFIED. The filename was found by spotting an existing DefaultLiveSet.als on neo; installing // one has not yet been confirmed to change ⌘N across a Live restart. Launch Live and press ⌘N once // to check. See reference/ableton/TEMPLATE.md. import { execFile } from "node:child_process"; import { promisify } from "node:util"; import { readFile } from "node:fs/promises"; import { fileURLToPath } from "node:url"; import { dirname, join } from "node:path"; const run = promisify(execFile); const HERE = dirname(fileURLToPath(import.meta.url)); const GEN = join(HERE, "..", "..", "..", "reference", "ableton", "als-template.mjs"); const HOSTS = ["neo", "blueberry"]; const argv = process.argv.slice(2); const opt = (n, d) => (argv.includes(n) ? argv[argv.indexOf(n) + 1] : d); const name = opt("--name", "AC Blank"); const positional = argv.filter((a, i) => !a.startsWith("--") && argv[i - 1] !== "--name"); const hosts = positional.length ? positional : HOSTS; // Build the Set fresh so the installed template always matches the generator in git. const built = join(HERE, "..", "..", "..", "reference", "ableton", "AC-Blank.als"); await run("node", [GEN, "--out", built]); const als = await readFile(built); console.log(`built ${name}.als (${als.length} bytes)`); const asDefault = argv.includes("--default"); const b64 = als.toString("base64"); for (const host of hosts) { try { const tpl = "$HOME/Music/Ableton/User Library/Templates"; await run("ssh", [ "-o", "ConnectTimeout=8", host, `mkdir -p "${tpl}" && printf %s '${b64}' | base64 -d > "${tpl}/${name}.als"`, ], { maxBuffer: 1 << 24 }); console.log(` ✓ ${host}: User Library/Templates/${name}.als`); if (asDefault) { // Live's settings folder is versioned; target the newest one it has actually run. const { stdout: ver } = await run("ssh", [ "-o", "ConnectTimeout=8", host, `ls "$HOME/Library/Preferences/Ableton" | grep '^Live ' | sort -V | tail -1`, ]); const v = ver.trim(); if (!v) throw new Error("no Live settings folder"); const base = `$HOME/Library/Preferences/Ableton/${v}/BaseFiles`; const f = `${base}/DefaultLiveSet.als`; await run("ssh", [ "-o", "ConnectTimeout=8", host, `mkdir -p "${base}"; ` + `[ -f "${f}" ] && cp "${f}" "${f}.bak-$(date +%Y%m%d-%H%M%S)" && echo " (backed up the existing default)"; ` + `printf %s '${b64}' | base64 -d > "${f}"`, ], { maxBuffer: 1 << 24 }); console.log(` ✓ ${host}: ${v}/BaseFiles/DefaultLiveSet.als — ⌘N should now open it`); } } catch (err) { console.log(` ✗ ${host}: ${err.message.split("\n")[0]}`); } } console.log(`\nIn Live: browser > Templates > "${name}".`); if (asDefault) { console.log("Launch Live and press ⌘N to confirm the default took — this path is unverified."); } else { console.log("⌘N still opens Ableton's stock Set. Pass --default to change that."); }