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 +1 -0
- package/README.md +20 -2
- package/lib/postinstall.js +142 -0
- package/{index.js → midi-shell-commands.js} +45 -8
- package/package.json +6 -5
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
|
|
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: `
|
|
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
|
-
|
|
8
|
-
|
|
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
|
-
|
|
13
|
-
|
|
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
|
-
|
|
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.
|
|
4
|
-
"main": "
|
|
3
|
+
"version": "1.1.1",
|
|
4
|
+
"main": "midi-shell-commands.js",
|
|
5
5
|
"bin": {
|
|
6
|
-
"midi-shell-commands": "
|
|
6
|
+
"midi-shell-commands": "midi-shell-commands.js"
|
|
7
7
|
},
|
|
8
8
|
"scripts": {
|
|
9
|
-
"start": "node
|
|
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
|
}
|