Monorepo for Aesthetic.Computer aesthetic.computer
4
fork

Configure Feed

Select the types of activity you want to include in your feed.

notepat.com distribution: versioned build + permalink + ableton redesign

Full-scope shift of AC's M4L story around notepat.com as the primary
device. Older plugins stay downloadable for anyone who already
grabbed them, but the front door is now https://notepat.com/amxd.

Infra
─────
• Caddyfile: notepat.com/amxd serves /m4l/notepat.com.amxd as a
direct download (Content-Disposition), so a single-tap install works
from any AC surface.
• ac-m4l/build-notepat.mjs: canonical build script. Writes
\`system/public/m4l/notepat.com/<git-hash>.amxd\` (immutable),
\`system/public/m4l/notepat.com.amxd\` (current alias), and
\`system/public/m4l/notepat.com/latest.json\` with sha256, size,
piece_git, built timestamp, permalink. npm scripts notepat:build /
notepat:build:desktop.
• .gitignore excludes versioned amxd binaries (kept locally, not in
the repo), keeps latest.json + alias.

Staleness check
───────────────
• bios env-info fetches /m4l/notepat.com/latest.json instead of
/.commit-ref, so "UPDATE AVAILABLE" only fires on real amxd
revisions — not incidental lith commits.

Deprecations
────────────
• ac-m4l/devices.json: notepat.com (notepat-remote) promoted, flagged
\`featured: true\` + permalink. kidlisp.com/device, notepat,
metronome, prompt, pedal, spreadnob-clean, spreadnob all marked
\`deprecated: true\`.
• disks/ableton.mjs: FEATURED_DOWNLOADS replaced with notepat.com
hero card (badge "PRIMARY"); featuredAssetUrl prefers downloadUrl
(permalink) over S3 path; DEPRECATED_PIECES set filters the main
plugin list; deprecated entries still exist in MongoDB for anyone
hitting the direct URLs.
• disks/notepat.mjs download button now jumps to
https://notepat.com/amxd.

+223 -37
+4
.gitignore
··· 340 340 ac-vst/vst3sdk/ 341 341 *.amxd 342 342 !system/public/m4l/*.amxd 343 + # Versioned build-stream amxds regenerate per commit — keep latest.json 344 + # + the root alias (notepat.com.amxd above) but don't bloat the repo 345 + # with per-build binaries. 346 + system/public/m4l/notepat.com/*.amxd 343 347 344 348 # Emacs performance logs (keep directory, ignore log files) 345 349 .emacs-logs/*.log
+128
ac-m4l/build-notepat.mjs
··· 1 + #!/usr/bin/env node 2 + // ac-m4l/build-notepat.mjs 3 + // 4 + // Canonical build script for the notepat.com Max for Live device. 5 + // Produces a versioned offline-chunked amxd + a latest.json manifest. 6 + // 7 + // Layout: 8 + // system/public/m4l/notepat.com.amxd ← always-current alias 9 + // system/public/m4l/notepat.com/<git-hash>.amxd ← immutable versioned 10 + // system/public/m4l/notepat.com/latest.json ← manifest 11 + // 12 + // The manifest carries: piece git hash, SHA-256, filesize, build time, 13 + // and the download permalink (https://notepat.com/amxd). The piece's 14 + // env-info bridge in bios.mjs reads this to decide whether the user's 15 + // installed amxd is out of date. 16 + // 17 + // Usage: 18 + // node ac-m4l/build-notepat.mjs # build to repo paths 19 + // node ac-m4l/build-notepat.mjs --desktop # also drop on ~/Desktop 20 + // node ac-m4l/build-notepat.mjs --log # stream progress events 21 + 22 + import { promises as fs } from "node:fs"; 23 + import path from "node:path"; 24 + import os from "node:os"; 25 + import { createHash } from "node:crypto"; 26 + import { execSync } from "node:child_process"; 27 + import { fileURLToPath } from "node:url"; 28 + 29 + const __dirname = path.dirname(fileURLToPath(import.meta.url)); 30 + const REPO_ROOT = path.resolve(__dirname, ".."); 31 + const AC_SOURCE_DIR = path.join(REPO_ROOT, "system/public/aesthetic.computer"); 32 + const M4L_DIR = path.join(REPO_ROOT, "system/public/m4l"); 33 + const DEVICE_DIR = path.join(M4L_DIR, "notepat.com"); 34 + 35 + // Pass AC_SOURCE_DIR through so the bundler finds disks/ + lib/. 36 + process.env.AC_SOURCE_DIR = process.env.AC_SOURCE_DIR || AC_SOURCE_DIR; 37 + 38 + const args = process.argv.slice(2); 39 + const WITH_DESKTOP = args.includes("--desktop"); 40 + const VERBOSE = args.includes("--log"); 41 + 42 + function gitHash() { 43 + try { 44 + return execSync("git rev-parse HEAD", { cwd: REPO_ROOT }).toString().trim(); 45 + } catch { 46 + return "unversioned"; 47 + } 48 + } 49 + 50 + function gitDirty() { 51 + try { 52 + const out = execSync("git status --porcelain", { cwd: REPO_ROOT }) 53 + .toString() 54 + .trim(); 55 + return out.length > 0; 56 + } catch { 57 + return false; 58 + } 59 + } 60 + 61 + async function main() { 62 + const bundlerPath = path.join(REPO_ROOT, "oven/bundler.mjs"); 63 + const { createM4DBundle } = await import(bundlerPath); 64 + 65 + const hash = gitHash(); 66 + const dirty = gitDirty(); 67 + const version = dirty ? `${hash}-dirty` : hash; 68 + 69 + const onProgress = VERBOSE 70 + ? (p) => console.log(`[${p.stage}] ${p.message}`) 71 + : () => {}; 72 + 73 + console.log(`→ Building notepat.com.amxd @ ${version}…`); 74 + const { binary, filename, sizeKB } = await createM4DBundle( 75 + "notepat-remote", 76 + true, 77 + onProgress, 78 + ); 79 + 80 + // Compute SHA-256 for the manifest. 81 + const sha256 = createHash("sha256").update(binary).digest("hex"); 82 + const sizeBytes = binary.length; 83 + 84 + await fs.mkdir(DEVICE_DIR, { recursive: true }); 85 + 86 + // Versioned file — immutable, named by git hash. 87 + const versionedName = `${version}.amxd`; 88 + const versionedPath = path.join(DEVICE_DIR, versionedName); 89 + await fs.writeFile(versionedPath, binary); 90 + 91 + // Current alias at the root — download link target. 92 + const aliasPath = path.join(M4L_DIR, "notepat.com.amxd"); 93 + await fs.writeFile(aliasPath, binary); 94 + 95 + // Manifest. 96 + const manifest = { 97 + name: "notepat.com", 98 + version, 99 + piece_git: hash, 100 + dirty, 101 + built: new Date().toISOString(), 102 + amxd: { 103 + filename: "notepat.com.amxd", 104 + versionedPath: `/m4l/notepat.com/${versionedName}`, 105 + aliasPath: "/m4l/notepat.com.amxd", 106 + permalink: "https://notepat.com/amxd", 107 + sizeBytes, 108 + sha256, 109 + }, 110 + }; 111 + const manifestPath = path.join(DEVICE_DIR, "latest.json"); 112 + await fs.writeFile(manifestPath, JSON.stringify(manifest, null, 2) + "\n"); 113 + 114 + console.log(` ✓ ${versionedName} (${sizeKB} KB, sha256 ${sha256.slice(0, 12)}…)`); 115 + console.log(` ✓ ${path.relative(REPO_ROOT, aliasPath)} (alias)`); 116 + console.log(` ✓ ${path.relative(REPO_ROOT, manifestPath)}`); 117 + 118 + if (WITH_DESKTOP) { 119 + const desktopPath = path.join(os.homedir(), "Desktop", filename); 120 + await fs.writeFile(desktopPath, binary); 121 + console.log(` ✓ ${desktopPath} (Desktop)`); 122 + } 123 + } 124 + 125 + main().catch((err) => { 126 + console.error("✗ Build failed:", err); 127 + process.exit(1); 128 + });
+26 -17
ac-m4l/devices.json
··· 1 1 { 2 2 "devices": [ 3 3 { 4 + "name": "notepat.com", 5 + "piece": "notepat-remote", 6 + "description": "Play the notepat keyboard in any Ableton MIDI track — stacked octaves, qwerty pads, live track-color theming. Primary distribution @ https://notepat.com/amxd.", 7 + "width": 150, 8 + "height": 169, 9 + "type": "midi", 10 + "source": "AC-NotepatRemote.amxd.json", 11 + "version": "1.0.0", 12 + "featured": true, 13 + "permalink": "https://notepat.com/amxd" 14 + }, 15 + { 4 16 "name": "AC KidLisp.com", 5 17 "piece": "kidlisp.com/device", 6 18 "description": "KidLisp.com Device View - receives code from kidlisp.com IDE", 7 19 "width": 400, 8 20 "height": 300, 9 21 "devUrl": "https://localhost:8888/kidlisp.com/device", 10 - "prodUrl": "https://kidlisp.com/device" 22 + "prodUrl": "https://kidlisp.com/device", 23 + "deprecated": true 11 24 }, 12 25 { 13 26 "name": "AC 🟪 notepat (https://aesthetic.computer/notepat)", 14 27 "piece": "notepat", 15 28 "description": "Aesthetic Computer Notepat with Ableton Sync", 16 29 "width": 500, 17 - "height": 169 30 + "height": 169, 31 + "deprecated": true 18 32 }, 19 33 { 20 34 "name": "AC 🟪 metronome (https://aesthetic.computer/metronome)", 21 35 "piece": "metronome", 22 36 "description": "Aesthetic Computer Metronome with Ableton Sync", 23 37 "width": 200, 24 - "height": 169 38 + "height": 169, 39 + "deprecated": true 25 40 }, 26 41 { 27 42 "name": "AC 🟪 prompt (https://aesthetic.computer/prompt)", 28 43 "piece": "prompt", 29 44 "description": "Aesthetic Computer Prompt with Ableton Sync", 30 45 "width": 200, 31 - "height": 200 46 + "height": 200, 47 + "deprecated": true 32 48 }, 33 49 { 34 50 "name": "AC 🎸 pedal (https://aesthetic.computer/pedal)", ··· 36 52 "description": "Aesthetic Computer Audio Effect Pedal - filter-style plugin with audio input", 37 53 "width": 400, 38 54 "height": 250, 39 - "type": "effect" 55 + "type": "effect", 56 + "deprecated": true 40 57 }, 41 58 { 42 59 "name": "AC 🎹 spreadnob-clean (aesthetic.computer)", ··· 46 63 "height": 110, 47 64 "type": "midi", 48 65 "source": "AC-SpreadnobClean.amxd.json", 49 - "version": "2.0.0" 66 + "version": "2.0.0", 67 + "deprecated": true 50 68 }, 51 69 { 52 70 "name": "AC 🎹 spreadnob (aesthetic.computer)", ··· 56 74 "height": 170, 57 75 "type": "midi", 58 76 "source": "AC-KnobMap.amxd.json", 59 - "version": "1.0.5" 60 - }, 61 - { 62 - "name": "AC 🎹 notepat-remote (aesthetic.computer)", 63 - "piece": "notepat-remote", 64 - "description": "Relay MIDI from ac-native notepat (ThinkPad) to this track via session-server", 65 - "width": 360, 66 - "height": 169, 67 - "type": "midi", 68 - "source": "AC-NotepatRemote.amxd.json", 69 - "version": "0.1.0" 77 + "version": "1.0.5", 78 + "deprecated": true 70 79 } 71 80 ], 72 81 "defaults": {
+9
lith/Caddyfile
··· 432 432 handle @notepatindex { 433 433 rewrite * /notepat 434 434 } 435 + # Permalink: notepat.com/amxd → current notepat.com.amxd as a 436 + # direct download. Clients get the Content-Disposition so it 437 + # lands in ~/Downloads instead of opening in-browser. 438 + handle /amxd { 439 + rewrite * /m4l/notepat.com.amxd 440 + header Content-Disposition "attachment; filename=\"notepat.com.amxd\"" 441 + root * /opt/ac/system/public 442 + file_server 443 + } 435 444 reverse_proxy localhost:8888 436 445 } 437 446 @mainspa host aesthetic.computer www.aesthetic.computer lith.aesthetic.computer notepat.com www.notepat.com p5.aesthetic.computer sitemap.aesthetic.computer
+2
package.json
··· 78 78 "site:debug": "cd system; npm run codespaces-dev-debug", 79 79 "site:live": "cd system; npm run codespaces-dev-public", 80 80 "new": "node utilities/generate-new-piece.mjs", 81 + "notepat:build": "node ac-m4l/build-notepat.mjs", 82 + "notepat:build:desktop": "node ac-m4l/build-notepat.mjs --desktop --log", 81 83 "reload-piece": "curl -X POST -H \"Content-Type: application/json\" -d '{\"piece\": \"@digitpain/hello\"}' http://localhost:8082/reload", 82 84 "server:socket": "cd socket-server; npm run server", 83 85 "assets:sync:down": "aws s3 sync s3://assets-aesthetic-computer system/public/assets --endpoint-url https://sfo3.digitaloceanspaces.com --exclude 'false.work/spiderlily-*.zip*' || echo 'Sync completed with some directory conflicts (safe to ignore)'",
+10 -4
system/public/aesthetic.computer/bios.mjs
··· 4648 4648 packMode: !!window.acPACK_MODE, 4649 4649 packGit: window.acPACK_GIT || null, 4650 4650 packDate: window.acPACK_DATE || null, 4651 - latestCommit: null, 4651 + latestCommit: null, // piece_git from notepat.com/latest.json 4652 + latestAmxd: null, // { sizeBytes, sha256, versionedPath, built } 4652 4653 }; 4653 4654 let envLatestFetched = false; 4654 4655 async function _dawFetchLatestCommit() { 4655 4656 if (envLatestFetched) return; 4656 4657 envLatestFetched = true; 4658 + // Amxd-specific manifest — the piece_git here is the commit the 4659 + // *amxd* was built from, not whichever commit lith happens to be 4660 + // serving. Avoids false "UPDATE AVAILABLE" on unrelated repo 4661 + // changes (docs, other pieces, infra). 4657 4662 try { 4658 4663 const r = await fetch( 4659 - "https://aesthetic.computer/.commit-ref", 4664 + "https://aesthetic.computer/m4l/notepat.com/latest.json", 4660 4665 { cache: "no-cache", mode: "cors" }, 4661 4666 ); 4662 4667 if (!r.ok) return; 4663 - const hash = (await r.text()).trim(); 4664 - if (hash) envInfo.latestCommit = hash; 4668 + const manifest = await r.json(); 4669 + if (manifest?.piece_git) envInfo.latestCommit = manifest.piece_git; 4670 + if (manifest?.amxd) envInfo.latestAmxd = manifest.amxd; 4665 4671 } catch (_e) { /* offline */ } 4666 4672 } 4667 4673 function _dawSendEnvInfo() {
+28 -15
system/public/aesthetic.computer/disks/ableton.mjs
··· 37 37 38 38 const FEATURED_DOWNLOADS = [ 39 39 { 40 - id: "featured-spreadnob-clean", 41 - label: "spreadnob clean", 42 - badge: "FOR TOM", 43 - piece: "spreadnob-clean", 44 - fileName: "AC 🎹 spreadnob-clean (aesthetic.computer).amxd", 45 - downloadLabel: "FOR TOM", 46 - blurb: "main version - compact, octave-aware, and the one to grab", 47 - }, 48 - { 49 - id: "featured-spreadnob", 50 - label: "spreadnob rack", 51 - piece: "spreadnob", 52 - fileName: "AC 🎹 spreadnob (aesthetic.computer).amxd", 53 - blurb: "expanded rack view with the full module layout", 40 + id: "featured-notepat-com", 41 + label: "notepat.com", 42 + badge: "PRIMARY", 43 + piece: "notepat-remote", 44 + fileName: "notepat.com.amxd", 45 + downloadUrl: "https://notepat.com/amxd", 46 + downloadLabel: "download", 47 + blurb: "piano pads + qwerty keyboard in any MIDI track — live track-color theming, case-door focus animation", 54 48 }, 55 49 ]; 50 + 51 + // Older devices kept on S3 for anyone who already had them; hidden 52 + // from the main list so notepat.com is the front door. 53 + const DEPRECATED_PIECES = new Set([ 54 + "kidlisp.com/device", 55 + "notepat", 56 + "metronome", 57 + "prompt", 58 + "pedal", 59 + "spreadnob-clean", 60 + "spreadnob", 61 + ]); 56 62 57 63 const { sin, cos, floor, abs, max } = Math; 58 64 ··· 395 401 } 396 402 397 403 function featuredAssetUrl(featured) { 404 + // Prefer an explicit permalink (e.g. https://notepat.com/amxd) when 405 + // provided; fall back to the legacy S3 path. 406 + if (featured.downloadUrl) return featured.downloadUrl; 398 407 return `https://assets.aesthetic.computer/m4l/${encodeURIComponent(featured.fileName)}`; 399 408 } 400 409 ··· 414 423 return res.json(); 415 424 }) 416 425 .then((data) => { 417 - plugins = Array.isArray(data) ? data : []; 426 + const raw = Array.isArray(data) ? data : []; 427 + // Hide deprecated plugins — they stay in MongoDB for historical 428 + // downloads but drop off the primary list so notepat.com is the 429 + // hero here. 430 + plugins = raw.filter((p) => !DEPRECATED_PIECES.has(p?.metadata?.piece)); 418 431 syncCustomPieceOptions(); 419 432 loading = false; 420 433 needsPaintRef?.();
+1 -1
system/public/aesthetic.computer/disks/notepat.mjs
··· 7124 7124 down: () => api.beep(400), 7125 7125 push: () => { 7126 7126 api.beep(); 7127 - jump("out:https://aesthetic.computer/m4l/notepat.com.amxd"); 7127 + jump("out:https://notepat.com/amxd"); 7128 7128 }, 7129 7129 }); 7130 7130
system/public/m4l/notepat.com.amxd

This is a binary file and will not be displayed.

+15
system/public/m4l/notepat.com/latest.json
··· 1 + { 2 + "name": "notepat.com", 3 + "version": "110c359993afbf1f17cda2fd07a9b241373c0a2a-dirty", 4 + "piece_git": "110c359993afbf1f17cda2fd07a9b241373c0a2a", 5 + "dirty": true, 6 + "built": "2026-04-24T20:07:01.429Z", 7 + "amxd": { 8 + "filename": "notepat.com.amxd", 9 + "versionedPath": "/m4l/notepat.com/110c359993afbf1f17cda2fd07a9b241373c0a2a-dirty.amxd", 10 + "aliasPath": "/m4l/notepat.com.amxd", 11 + "permalink": "https://notepat.com/amxd", 12 + "sizeBytes": 1167417, 13 + "sha256": "c9b5c5d707f6209f323c670ebc1d99f2fb70c82c07b511fdce48ef3827ade597" 14 + } 15 + }