midi-shell-commands 1.0.0 → 1.1.1

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/.nvmrc ADDED
@@ -0,0 +1 @@
1
+ 22
package/README.md CHANGED
@@ -46,6 +46,9 @@ The application periodically checks for new MIDI input devices and will start li
46
46
 
47
47
  chmod +x ./scripts/*.sh
48
48
 
49
+ On macOS, a LaunchAgent will be installed on npm install that automatically runs this tool in the background watching
50
+ `~/Documents/MidiShellCommands` at login. See Daemon mode below.
51
+
49
52
  ## Usage
50
53
 
51
54
  You can run the tool in a few ways:
@@ -64,7 +67,7 @@ You can run the tool in a few ways:
64
67
 
65
68
  - Or directly with Node, specifying the path to your scripts directory:
66
69
 
67
- node index.js ./scripts
70
+ node midi-shell-commands.js ./scripts
68
71
 
69
72
  Replace `./scripts` with the path to your own directory containing executable scripts.
70
73
 
@@ -95,6 +98,21 @@ Make it executable:
95
98
  chmod +x scripts/noteon.60.sh
96
99
  ```
97
100
 
101
+ ## Daemon mode (macOS)
102
+
103
+ - On macOS, when you run `npm install` for this package, it will:
104
+ - Create the directory `~/Documents/MidiShellCommands` if it does not exist.
105
+ - Install a LaunchAgent at `~/Library/LaunchAgents/com.midi-shell-commands.plist` that starts `midi-shell-commands` at login, watching
106
+ that directory.
107
+ - Attempt to load the LaunchAgent immediately so it begins running right away.
108
+ - Logs can be found at `~/Library/Logs/midi-shell-commands.stdout.log` and `~/Library/Logs/midi-shell-commands.stderr.log`.
109
+ - To manage the LaunchAgent manually:
110
+ - Reload:
111
+ `launchctl unload ~/Library/LaunchAgents/com.midi-shell-commands.plist && launchctl load -w ~/Library/LaunchAgents/com.midi-shell-commands.plist`
112
+ - Disable autostart: `launchctl unload -w ~/Library/LaunchAgents/com.midi-shell-commands.plist`
113
+ - Remove: delete the plist file and unload it.
114
+ - If you run `midi-shell-commands` with no arguments, it will also default to watching `~/Documents/MidiShellCommands`.
115
+
98
116
  ## Notes and behavior
99
117
 
100
118
  - Only non-hidden files in the scripts directory are considered (files not starting with a dot).
@@ -118,7 +136,7 @@ chmod +x scripts/noteon.60.sh
118
136
 
119
137
  ## Development
120
138
 
121
- - Main entry: `index.js`
139
+ - Main entry: `midi-shell-commands.js`
122
140
  - Dependencies: `easymidi`
123
141
  - Helpful npm scripts:
124
142
  - `npm start` — starts the watcher using `./scripts` as the scripts directory.
@@ -0,0 +1,142 @@
1
+ #!/usr/local/bin/node
2
+ /**
3
+ * Postinstall script to set up a macOS LaunchAgent which runs midi-shell-commands
4
+ * pointing at ~/Documents/MidiShellCommands.
5
+ */
6
+ const fs = require('fs');
7
+ const os = require('os');
8
+ const path = require('path');
9
+ const childProcess = require('child_process');
10
+
11
+ function log(msg) {
12
+ try {
13
+ // Best-effort: npm may suppress some outputs; still try.
14
+ console.log(`[midi-shell-commands] ${msg}`);
15
+ }
16
+ catch {}
17
+ }
18
+
19
+ (function main() {
20
+ const platform = process.platform;
21
+ if (platform !== 'darwin') {
22
+ log('Postinstall daemon setup skipped (not macOS).');
23
+ return;
24
+ }
25
+
26
+ const home = os.homedir();
27
+ const scriptsDir = path.join(home, 'Documents', 'MidiShellCommands');
28
+ try {
29
+ fs.mkdirSync(scriptsDir, { recursive: true });
30
+ // Add a sample README the first time, without overwriting user files.
31
+ const readmePath = path.join(scriptsDir, 'README.txt');
32
+ if (!fs.existsSync(readmePath)) {
33
+ fs.writeFileSync(readmePath, 'Place executable scripts here. Names should match patterns like:\nnoteon.<note>\nnoteon.<note>.<channel>\nnoteon.<note>.<channel>.<velocity>\nMake files executable (chmod +x).');
34
+ }
35
+ }
36
+ catch (e) {
37
+ log(`Failed to create scripts directory at ${scriptsDir}: ${e.message}`);
38
+ // Continue; LaunchAgent will still start and the app will create it on run.
39
+ }
40
+
41
+ // Determine bin path where npm linked the CLI
42
+ const npmPrefix = process.env.npm_config_prefix || '';
43
+ let binPath = '';
44
+ if (npmPrefix) {
45
+ // Typical global install
46
+ binPath = path.join(npmPrefix, 'bin', 'midi-shell-commands');
47
+ }
48
+ if (!binPath || !fs.existsSync(binPath)) {
49
+ // Fallback to local project binary path
50
+ binPath = path.join(process.cwd(), 'midi-shell-commands.js');
51
+ }
52
+
53
+ const launchAgentsDir = path.join(home, 'Library', 'LaunchAgents');
54
+ const label = 'com.midi-shell-commands';
55
+ const plistPath = path.join(launchAgentsDir, `${label}.plist`);
56
+
57
+ try {
58
+ fs.mkdirSync(launchAgentsDir, { recursive: true });
59
+ }
60
+ catch (e) {
61
+ log(`Failed to ensure LaunchAgents directory: ${e.message}`);
62
+ return;
63
+ }
64
+
65
+ const stdoutPath = path.join(home, 'Library', 'Logs', 'midi-shell-commands.stdout.log');
66
+ const stderrPath = path.join(home, 'Library', 'Logs', 'midi-shell-commands.stderr.log');
67
+
68
+ const plist = `<?xml version="1.0" encoding="UTF-8"?>
69
+ <!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
70
+ <plist version="1.0">
71
+ <dict>
72
+ <key>Label</key>
73
+ <string>${label}</string>
74
+ <key>ProgramArguments</key>
75
+ <array>
76
+ <string>${binPath}</string>
77
+ <string>${scriptsDir}</string>
78
+ </array>
79
+ <key>RunAtLoad</key>
80
+ <true/>
81
+ <key>KeepAlive</key>
82
+ <true/>
83
+ <key>StandardOutPath</key>
84
+ <string>${stdoutPath}</string>
85
+ <key>StandardErrorPath</key>
86
+ <string>${stderrPath}</string>
87
+ <key>EnvironmentVariables</key>
88
+ <key>PATH</key>
89
+ <string>/usr/local/bin:/usr/local/sbin:/usr/bin:/bin:/usr/sbin:/sbin</string>
90
+ </dict>
91
+ </plist>`;
92
+
93
+ try {
94
+ fs.writeFileSync(plistPath, plist, { encoding: 'utf8' });
95
+ log(`Installed LaunchAgent at ${plistPath}`);
96
+ }
97
+ catch (e) {
98
+ log(`Failed to write LaunchAgent plist: ${e.message}`);
99
+ return;
100
+ }
101
+
102
+ // Try to (re)load the LaunchAgent so it starts now
103
+ try {
104
+ if (process.env.SUDO_UID) {
105
+ // Avoid trying to load as root for a user agent
106
+ log('Skipping launchctl load because running under sudo. You can load it later with your user session.');
107
+ return;
108
+ }
109
+
110
+ // Unload if already loaded to pick up updates
111
+ try {
112
+ childProcess.execSync(`launchctl unload ${escapePath(plistPath)}`, { stdio: 'ignore' });
113
+ }
114
+ catch {}
115
+
116
+ // Preferred modern approach: bootstrap into the current user session
117
+ const uid = process.getuid && process.getuid();
118
+ if (uid) {
119
+ try {
120
+ childProcess.execSync(`launchctl bootstrap gui/${uid} ${escapePath(plistPath)}`, { stdio: 'ignore' });
121
+ log('LaunchAgent bootstrapped.');
122
+ return;
123
+ }
124
+ catch {}
125
+ }
126
+
127
+ // Fallback to legacy load
128
+ childProcess.execSync(`launchctl load -w ${escapePath(plistPath)}`, { stdio: 'ignore' });
129
+ log('LaunchAgent loaded.');
130
+ }
131
+ catch (e) {
132
+ log(`Could not load LaunchAgent automatically: ${e.message}\nYou can load it manually with:\n launchctl load -w ${plistPath}`);
133
+ }
134
+ })();
135
+
136
+ function escapePath(p) {
137
+ // Simple shell escaping for spaces
138
+ if (p.includes(' ')) {
139
+ return `'${p.replace(/'/g, '\'\\\'\'')}'`;
140
+ }
141
+ return p;
142
+ }
@@ -3,21 +3,57 @@ const childProcess = require('child_process');
3
3
  const easyMIDI = require('easymidi');
4
4
  const fs = require('fs');
5
5
  const path = require('path');
6
+ const os = require('os');
6
7
 
7
- if (process.argv.length === 2) {
8
- console.error('Please pass the path to the directory containing your scripts.');
8
+ console.log('Midi Shell Commands starting up!');
9
+
10
+ // Default to ~/Documents/MidiShellCommands if no directory is provided
11
+ let targetDirArg = process.argv[process.argv.length - 1];
12
+ if (process.argv.length === 2 || targetDirArg === '--daemon') {
13
+ targetDirArg = path.join(os.homedir(), 'Documents', 'MidiShellCommands');
14
+ }
15
+ console.log(`Watching ${targetDirArg}`);
16
+
17
+ const watchDir = path.resolve(targetDirArg);
18
+
19
+ // Ensure the directory exists
20
+ try {
21
+ fs.mkdirSync(watchDir, { recursive: true });
22
+ }
23
+ catch (e) {
24
+ console.error('Failed to create or access scripts directory:', watchDir);
25
+ console.error(e);
9
26
  process.exit(1);
10
27
  }
11
28
 
12
- const watchDir = path.resolve(process.argv[process.argv.length - 1]);
13
- const scripts = fs.readdirSync(watchDir).filter(f => f[0] !== '.');
14
- const scriptsWithoutExtension = scripts.map(script => script.split('.').slice(0, -1).join('.'));
15
- const checkDelay = 60 * 1000 + (Math.random() * 3000) | 0;
29
+ let scripts = [];
30
+ let scriptsWithoutExtension = [];
16
31
 
32
+ process.on('exit', cleanUpInputs);
33
+ refreshScripts();
34
+ try {
35
+ fs.watch(watchDir, { persistent: true }, refreshScripts);
36
+ }
37
+ catch (e) {
38
+ // Non-fatal on some platforms
39
+ }
40
+
41
+ const checkDelay = 60 * 1000 + (Math.random() * 3000) | 0;
17
42
  const watchedInputs = {};
18
43
  checkInputs();
19
44
  setInterval(checkInputs, checkDelay);
20
45
 
46
+ function refreshScripts() {
47
+ try {
48
+ scripts = fs.readdirSync(watchDir).filter(f => f[0] !== '.');
49
+ scriptsWithoutExtension = scripts.map(script => script.split('.').slice(0, -1).join('.'));
50
+ }
51
+ catch (e) {
52
+ console.error('Failed to read scripts directory:', watchDir);
53
+ console.error(e);
54
+ }
55
+ }
56
+
21
57
  function checkInputs() {
22
58
  const inputNames = easyMIDI.getInputs();
23
59
  for (const inputName of inputNames) {
@@ -36,6 +72,7 @@ function listenToInput(inputName) {
36
72
 
37
73
  function invokeScripts(msg) {
38
74
  const possibleFileNames = mapMessageToFileNames(msg);
75
+ console.log(possibleFileNames[0]);
39
76
  for (const possibleFileName of possibleFileNames) {
40
77
  for (let i = 0; i < scriptsWithoutExtension.length; i++) {
41
78
  if (scriptsWithoutExtension[i] === possibleFileName) {
@@ -60,8 +97,8 @@ function reportErrors(err) {
60
97
  }
61
98
  }
62
99
 
63
- process.on('exit', () => {
100
+ function cleanUpInputs() {
64
101
  for (const input of Object.values(watchedInputs)) {
65
102
  input.close();
66
103
  }
67
- });
104
+ }
package/package.json CHANGED
@@ -1,18 +1,19 @@
1
1
  {
2
2
  "name": "midi-shell-commands",
3
- "version": "1.0.0",
4
- "main": "index.js",
3
+ "version": "1.1.1",
4
+ "main": "midi-shell-commands.js",
5
5
  "bin": {
6
- "midi-shell-commands": "index.js"
6
+ "midi-shell-commands": "midi-shell-commands.js"
7
7
  },
8
8
  "scripts": {
9
- "start": "node index.js ./scripts",
9
+ "start": "node midi-shell-commands.js ./scripts",
10
+ "postinstall": "node lib/postinstall.js",
10
11
  "test": "echo \"Error: no test specified\" && exit 1"
11
12
  },
12
13
  "keywords": [],
13
14
  "author": "",
14
15
  "license": "ISC",
15
- "description": "",
16
+ "description": "Execute shell scripts on MIDI note events; supports macOS daemon mode via LaunchAgent.",
16
17
  "dependencies": {
17
18
  "easymidi": "^3.1.0"
18
19
  }