@hackmd/spm 0.1.0

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/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 HackMD Team
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,170 @@
1
+ # SPM — Simpler Process Manager
2
+
3
+ A minimal process manager implementing a lean subset of [pm2](https://pm2.keymetrics.io/) features, with some extensions, designed for better development script setup. Lightweight, zero daemon, and ecosystem config compatible.
4
+
5
+ ## Features
6
+
7
+ - **Start / Stop / Restart** — Manage multiple services from a single config
8
+ - **Multi-instance** — Run multiple instances with port/env auto-increment
9
+ - **Log management** — Tail, filter, flush, and rotate logs
10
+ - **JSON output** — `jlist` for machine-readable status (CI/scripting)
11
+ - **Log rotation** — Size-based rotation and retention with optional background watcher
12
+
13
+ ## Installation
14
+
15
+ ```bash
16
+ npm install -g @hackmd/spm
17
+ ```
18
+
19
+ Or with Bun:
20
+
21
+ ```bash
22
+ bun add -g @hackmd/spm
23
+ ```
24
+
25
+ ## Quick Start
26
+
27
+ 1. Create an ecosystem config file (e.g. `ecosystem.config.js`) in your project:
28
+
29
+ ```javascript
30
+ export default {
31
+ apps: [
32
+ {
33
+ name: 'api',
34
+ script: 'node',
35
+ args: 'server.js',
36
+ instances: 2,
37
+ env: { PORT: '3000' },
38
+ increment_vars: ['PORT'],
39
+ },
40
+ {
41
+ name: 'worker',
42
+ script: 'node',
43
+ args: 'worker.js',
44
+ },
45
+ ],
46
+ }
47
+ ```
48
+
49
+ 2. Run SPM:
50
+
51
+ ```bash
52
+ spm start # Start all services
53
+ spm start api # Start specific service
54
+ spm list # List services and PIDs
55
+ spm stop # Stop all
56
+ spm restart api # Restart specific service
57
+ spm logs api -t # Tail logs
58
+ ```
59
+
60
+ ## Configuration
61
+
62
+ | Option | Description |
63
+ |--------|-------------|
64
+ | `name` | Service identifier |
65
+ | `script` | Command to run (e.g. `node`, `bun`, `python`) |
66
+ | `args` | Arguments passed to the script |
67
+ | `instances` | Number of instances (default: 1) |
68
+ | `env` | Environment variables |
69
+ | `increment_vars` | Env vars to increment per instance (e.g. `PORT`) |
70
+
71
+ Config file is resolved from `./ecosystem.custom.config.js` by default. Override with `--config`:
72
+
73
+ ```bash
74
+ spm --config ./my-ecosystem.config.js start
75
+ ```
76
+
77
+ ## Commands
78
+
79
+ | Command | Description |
80
+ |---------|-------------|
81
+ | `spm start [service]` | Start all or a specific service |
82
+ | `spm stop [service]` | Stop processes (alias: `kill`) |
83
+ | `spm restart [service]` | Restart processes |
84
+ | `spm list` | List services and running PIDs |
85
+ | `spm jlist` | JSON output of service status |
86
+ | `spm logs [service]` | View logs (`-t` tail, `-n` lines, `-f` filter) |
87
+ | `spm flush [service]` | Clear log files |
88
+ | `spm rotate start` | Start log rotation watcher |
89
+ | `spm rotate stop` | Stop rotation watcher |
90
+
91
+ ## Log Rotation
92
+
93
+ Logs are stored in `~/.spm2/logs/`. Rotation:
94
+
95
+ - Rotates when a log exceeds 10MB
96
+ - Removes logs older than 3 days
97
+ - Run `spm rotate start` for a background watcher
98
+
99
+ ## Shell Integration
100
+
101
+ Completions require [jq](https://jqlang.github.io/jq/). App selector also requires [fzf](https://github.com/junegunn/fzf).
102
+
103
+ ### Bash
104
+
105
+ **Completions** — Source the script (requires [bash-completion](https://github.com/scop/bash-completion); on macOS: `brew install bash-completion@2`):
106
+
107
+ ```bash
108
+ # After npm install -g @hackmd/spm:
109
+ source $(npm root -g)/@hackmd/spm/completions/bash/spm.bash
110
+ # Or add to ~/.bashrc:
111
+ # source /path/to/spm/completions/bash/spm.bash
112
+ ```
113
+
114
+ **App selector** — Source the script:
115
+
116
+ ```bash
117
+ source $(npm root -g)/@hackmd/spm/completions/bash/spm_app_selector.sh
118
+ # Then:
119
+ spm_app_selector start
120
+ spm_app_selector --appName=api restart
121
+ ```
122
+
123
+ ### Zsh
124
+
125
+ **Completions** — Add the completion dir to `fpath` and ensure compinit runs:
126
+
127
+ ```bash
128
+ # After npm install -g @hackmd/spm:
129
+ fpath=($(npm root -g)/@hackmd/spm/completions/zsh $fpath)
130
+ compinit
131
+ # Or copy to a dir in $fpath:
132
+ mkdir -p ~/.zsh/completions
133
+ cp $(npm root -g)/@hackmd/spm/completions/zsh/_spm ~/.zsh/completions/
134
+ # Add to ~/.zshrc: fpath=(~/.zsh/completions $fpath)
135
+ ```
136
+
137
+ **App selector** — Source the script:
138
+
139
+ ```bash
140
+ source $(npm root -g)/@hackmd/spm/completions/zsh/spm_app_selector.zsh
141
+ spm_app_selector start
142
+ ```
143
+
144
+ ### Fish
145
+
146
+ **Completions** — Copy to your Fish config:
147
+
148
+ ```bash
149
+ mkdir -p ~/.config/fish/completions
150
+ cp $(npm root -g)/@hackmd/spm/completions/fish/spm.fish ~/.config/fish/completions/
151
+ ```
152
+
153
+ **App selector** — Copy the function:
154
+
155
+ ```bash
156
+ mkdir -p ~/.config/fish/functions
157
+ cp $(npm root -g)/@hackmd/spm/completions/fish/functions/spm_app_selector.fish ~/.config/fish/functions/
158
+ ```
159
+
160
+ Usage:
161
+
162
+ ```fish
163
+ spm_app_selector start
164
+ spm_app_selector --appName=api restart
165
+ spm_app_selector --config=ecosystem.config.js logs
166
+ ```
167
+
168
+ ## License
169
+
170
+ MIT
@@ -0,0 +1,93 @@
1
+ # Bash completion for spm
2
+ # Usage: source this file or copy to /etc/bash_completion.d/spm
3
+ # Requires: bash-completion (Linux) or bash-completion2 (macOS: brew install bash-completion@2)
4
+
5
+ _spm_services() {
6
+ spm jlist 2>/dev/null | jq -r '.[].name'
7
+ }
8
+
9
+ _spm() {
10
+ local cur prev words cword
11
+ if declare -f _init_completion &>/dev/null; then
12
+ _init_completion -s || return
13
+ else
14
+ words=("${COMP_WORDS[@]}")
15
+ cword=$COMP_CWORD
16
+ cur="${words[cword]}"
17
+ prev="${words[cword-1]}"
18
+ fi
19
+
20
+ local subcommands="start stop kill logs list jlist restart flush rotate"
21
+ local rotate_subcommands="start watch stop"
22
+
23
+ case $prev in
24
+ -c|--config)
25
+ if declare -f _filedir &>/dev/null; then
26
+ _filedir
27
+ else
28
+ COMPREPLY=($(compgen -f -X '!*.@(js|mjs|cjs)' -- "$cur"))
29
+ fi
30
+ return
31
+ ;;
32
+ -s|--signal)
33
+ COMPREPLY=($(compgen -W "SIGTERM SIGINT SIGKILL" -- "$cur"))
34
+ return
35
+ ;;
36
+ -n|--lines)
37
+ return
38
+ ;;
39
+ -f|--filter)
40
+ return
41
+ ;;
42
+ --cleanup-interval)
43
+ return
44
+ ;;
45
+ spm)
46
+ COMPREPLY=($(compgen -W "$subcommands -c --config -v --verbose -h --help" -- "$cur"))
47
+ return
48
+ ;;
49
+ rotate)
50
+ COMPREPLY=($(compgen -W "$rotate_subcommands" -- "$cur"))
51
+ return
52
+ ;;
53
+ esac
54
+
55
+ # Check if we're completing after a subcommand that takes a service name
56
+ for ((i = 1; i < cword; i++)); do
57
+ case ${words[i]} in
58
+ start|stop|kill|restart|logs|flush)
59
+ COMPREPLY=($(compgen -W "$(_spm_services)" -- "$cur"))
60
+ return
61
+ ;;
62
+ rotate)
63
+ if [[ $i -eq $((cword - 1)) ]]; then
64
+ COMPREPLY=($(compgen -W "$rotate_subcommands" -- "$cur"))
65
+ fi
66
+ return
67
+ ;;
68
+ esac
69
+ done
70
+
71
+ # Check for logs options
72
+ if [[ " ${words[@]} " =~ " logs " ]]; then
73
+ case $cur in
74
+ -*) COMPREPLY=($(compgen -W "-t --tail -n --lines -f --filter -h --help" -- "$cur")) ;;
75
+ *) COMPREPLY=($(compgen -W "$(_spm_services)" -- "$cur")) ;;
76
+ esac
77
+ return
78
+ fi
79
+
80
+ # Check for stop/kill options
81
+ if [[ " ${words[@]} " =~ " stop " ]] || [[ " ${words[@]} " =~ " kill " ]]; then
82
+ case $cur in
83
+ -*) COMPREPLY=($(compgen -W "-s --signal -h --help" -- "$cur")) ;;
84
+ *) COMPREPLY=($(compgen -W "$(_spm_services)" -- "$cur")) ;;
85
+ esac
86
+ return
87
+ fi
88
+
89
+ # Default: complete subcommands or global options
90
+ COMPREPLY=($(compgen -W "$subcommands -c --config -v --verbose -h --help" -- "$cur"))
91
+ }
92
+
93
+ complete -F _spm spm
@@ -0,0 +1,81 @@
1
+ #!/usr/bin/env bash
2
+ # Interactive service picker for spm. Requires fzf and jq.
3
+ # Usage: source this file, then run spm_app_selector start|restart|logs|stop|flush
4
+
5
+ spm_app_selector() {
6
+ local command=""
7
+ local config=""
8
+ local app_name=""
9
+
10
+ while [[ $# -gt 0 ]]; do
11
+ case $1 in
12
+ --config=*)
13
+ config="${1#*=}"
14
+ shift
15
+ ;;
16
+ --appName=*)
17
+ app_name="${1#*=}"
18
+ shift
19
+ ;;
20
+ -h|--help)
21
+ echo "Usage: spm_app_selector [OPTIONS] COMMAND"
22
+ echo ""
23
+ echo "Options:"
24
+ echo " --appName=APP_NAME Specify the application name directly."
25
+ echo " --config=CONFIG_FILE Specify the ecosystem configuration file."
26
+ echo " -h, --help Display this help message."
27
+ echo ""
28
+ echo "Examples:"
29
+ echo " spm_app_selector start"
30
+ echo " spm_app_selector --appName=api restart"
31
+ echo " spm_app_selector --config=ecosystem.config.js logs"
32
+ return 0
33
+ ;;
34
+ start|stop|kill|restart|logs|flush)
35
+ command="$1"
36
+ shift
37
+ break
38
+ ;;
39
+ *)
40
+ echo "Unknown option or command: $1"
41
+ return 1
42
+ ;;
43
+ esac
44
+ done
45
+
46
+ if [[ -z "$command" ]]; then
47
+ echo "Usage: spm_app_selector [OPTIONS] COMMAND"
48
+ echo "COMMAND must be one of: start stop kill restart logs flush"
49
+ return 1
50
+ fi
51
+
52
+ if [[ -z "$config" ]]; then
53
+ if [[ -f ecosystem.config.js ]]; then
54
+ config="ecosystem.config.js"
55
+ elif [[ -f ecosystem.config.cjs ]]; then
56
+ config="ecosystem.config.cjs"
57
+ elif [[ -f ecosystem.custom.config.js ]]; then
58
+ config="ecosystem.custom.config.js"
59
+ fi
60
+ fi
61
+
62
+ if [[ -z "$config" ]]; then
63
+ echo "No ecosystem configuration file found."
64
+ return 1
65
+ fi
66
+
67
+ if [[ -z "$app_name" ]]; then
68
+ app_name=$(spm ${config:+--config "$config"} jlist 2>/dev/null | jq -r '.[].name' | fzf)
69
+ fi
70
+
71
+ if [[ -n "$app_name" ]]; then
72
+ if [[ -n "$config" ]]; then
73
+ spm --config "$config" "$command" "$app_name"
74
+ else
75
+ spm "$command" "$app_name"
76
+ fi
77
+ else
78
+ echo "No application selected."
79
+ return 1
80
+ fi
81
+ }
@@ -0,0 +1,61 @@
1
+ function spm_app_selector
2
+ argparse 'h/help' 'appName=' 'config=' -- $argv
3
+ or return
4
+
5
+ if set -q _flag_help
6
+ echo "Usage: spm_app_selector [OPTIONS] -- [COMMAND]"
7
+ echo ""
8
+ echo "Options:"
9
+ echo " --appName=APP_NAME Specify the application name directly."
10
+ echo " --config=CONFIG_FILE Specify the ecosystem configuration file (ecosystem.config.js or ecosystem.custom.config.js)."
11
+ echo " -h, --help Display this help message and exit."
12
+ echo ""
13
+ echo "Examples:"
14
+ echo " spm_app_selector start"
15
+ echo " spm_app_selector --appName=api restart"
16
+ echo " spm_app_selector --config=ecosystem.config.js logs"
17
+ return
18
+ end
19
+
20
+ if not set -q _flag_config
21
+ if test -f ecosystem.config.js
22
+ set _flag_config ecosystem.config.js
23
+ else if test -f ecosystem.config.cjs
24
+ set _flag_config ecosystem.config.cjs
25
+ else if test -f ecosystem.custom.config.js
26
+ set _flag_config ecosystem.custom.config.js
27
+ end
28
+ end
29
+
30
+ if not set -q _flag_config
31
+ echo "No ecosystem configuration file found."
32
+ return
33
+ end
34
+
35
+ set -l command $argv[1]
36
+ if test -z "$command"
37
+ echo "Usage: spm_app_selector [OPTIONS] COMMAND"
38
+ echo "COMMAND must be one of: start stop kill restart logs flush"
39
+ return 1
40
+ end
41
+
42
+ if set -q _flag_appName
43
+ set APP_NAME $_flag_appName
44
+ else
45
+ set spm_args
46
+ if set -q _flag_config
47
+ set -a spm_args --config $_flag_config
48
+ end
49
+ set APP_NAME (spm $spm_args jlist 2>/dev/null | jq -r '.[].name' | fzf)
50
+ end
51
+
52
+ if test -n "$APP_NAME"
53
+ set spm_args
54
+ if set -q _flag_config
55
+ set -a spm_args --config $_flag_config
56
+ end
57
+ spm $spm_args $command $APP_NAME
58
+ else
59
+ echo "No application selected."
60
+ end
61
+ end
@@ -0,0 +1,44 @@
1
+ # Disable file completions globally for spm
2
+ complete -c spm -f
3
+
4
+ # Helper function to check if spm has not received a subcommand
5
+ function __fish_spm_no_subcommand --description "Check if spm has not received a subcommand"
6
+ for i in (commandline -opc)
7
+ if contains -- $i start stop kill logs list jlist restart flush rotate
8
+ return 1
9
+ end
10
+ end
11
+ return 0
12
+ end
13
+
14
+ # Dynamic service names completion using spm jlist
15
+ function __fish_spm_process_names --description "Fetch list of spm services"
16
+ spm jlist 2>/dev/null | jq -r '.[].name'
17
+ end
18
+
19
+ # Root-level subcommands with descriptions
20
+ complete -c spm -n '__fish_spm_no_subcommand' -a "start" -d "Start service instance(s)"
21
+ complete -c spm -n '__fish_spm_no_subcommand' -a "stop" -d "Stop service instance(s)"
22
+ complete -c spm -n '__fish_spm_no_subcommand' -a "kill" -d "Alias for 'stop'"
23
+ complete -c spm -n '__fish_spm_no_subcommand' -a "logs" -d "Display logs for service instance(s)"
24
+ complete -c spm -n '__fish_spm_no_subcommand' -a "list" -d "List services with running PIDs"
25
+ complete -c spm -n '__fish_spm_no_subcommand' -a "jlist" -d "List services in JSON format"
26
+ complete -c spm -n '__fish_spm_no_subcommand' -a "restart" -d "Restart service instance(s)"
27
+ complete -c spm -n '__fish_spm_no_subcommand' -a "flush" -d "Clear log file contents"
28
+ complete -c spm -n '__fish_spm_no_subcommand' -a "rotate" -d "Manage log rotation"
29
+
30
+ # Dynamic service name completion for commands that take a service argument
31
+ for cmd in start stop restart logs flush kill
32
+ complete -c spm -n "__fish_seen_subcommand_from $cmd" -a "(__fish_spm_process_names)" -d "Service names"
33
+ end
34
+
35
+ # Subcommand-specific completions for the logs command
36
+ complete -c spm -n '__fish_seen_subcommand_from logs' -a '--tail' -d "Tail log files in real time"
37
+ complete -c spm -n '__fish_seen_subcommand_from logs' -a '-n --lines' -d "Number of lines to show"
38
+ complete -c spm -n '__fish_seen_subcommand_from logs' -a '-f --filter' -d "Filter logs by pattern"
39
+ complete -c spm -n '__fish_seen_subcommand_from logs' -a '-h --help' -d "Show help"
40
+
41
+ # Completions for the rotate subcommand group
42
+ complete -c spm -n '__fish_seen_subcommand_from rotate' -a "start" -d "Perform log rotation and spawn rotate-watch process"
43
+ complete -c spm -n '__fish_seen_subcommand_from rotate' -a "watch" -d "Continuously watch and rotate logs"
44
+ complete -c spm -n '__fish_seen_subcommand_from rotate' -a "stop" -d "Stop the rotate-watch process"
@@ -0,0 +1,57 @@
1
+ #compdef spm
2
+ # Zsh completion for spm
3
+
4
+ _spm_services() {
5
+ spm jlist 2>/dev/null | jq -r '.[].name'
6
+ }
7
+
8
+ _spm() {
9
+ local -a subcommands rotate_subcommands
10
+ subcommands=(start:Start\ service\ instance\(s\) stop:Stop\ service\ instance\(s\) kill:Alias\ for\ stop logs:Display\ logs list:List\ services\ with\ running\ PIDs jlist:List\ services\ in\ JSON\ format restart:Restart\ service\ instance\(s\) flush:Clear\ log\ file\ contents rotate:Manage\ log\ rotation)
11
+ rotate_subcommands=(start:Perform\ log\ rotation\ and\ spawn\ rotate-watch\ process watch:Continuously\ watch\ and\ rotate\ logs stop:Stop\ the\ rotate-watch\ process)
12
+
13
+ local context state line
14
+ typeset -A opt_args
15
+
16
+ _arguments -C \
17
+ '(-c --config)'{-c,--config}'[Specify ecosystem config file]:config file:_files' \
18
+ '(-v --verbose)'{-v,--verbose}'[Enable verbose output]' \
19
+ '(-h --help)'{-h,--help}'[Show help]' \
20
+ '1:subcommand:->subcommand' \
21
+ '*::args:->args'
22
+
23
+ case $state in
24
+ subcommand)
25
+ _describe 'subcommand' subcommands
26
+ ;;
27
+ args)
28
+ case $words[2] in
29
+ start|restart|flush)
30
+ _values 'service' $(_spm_services)
31
+ ;;
32
+ logs)
33
+ _arguments \
34
+ '(-t --tail)'{-t,--tail}'[Tail log files in real time]' \
35
+ '(-n --lines)'{-n,--lines}'[Number of lines to show]:lines' \
36
+ '(-f --filter)'{-f,--filter}'[Filter logs by pattern]:pattern' \
37
+ '(-h --help)'{-h,--help}'[Show help]' \
38
+ '::service:($(_spm_services))'
39
+ ;;
40
+ stop|kill)
41
+ _arguments \
42
+ '(-s --signal)'{-s,--signal}'[Signal to send]:signal:(SIGTERM SIGINT SIGKILL)' \
43
+ '(-h --help)'{-h,--help}'[Show help]' \
44
+ '::service:($(_spm_services))'
45
+ ;;
46
+ rotate)
47
+ _describe 'rotate subcommand' rotate_subcommands
48
+ ;;
49
+ *)
50
+ _normal
51
+ ;;
52
+ esac
53
+ ;;
54
+ esac
55
+ }
56
+
57
+ _spm "$@"
@@ -0,0 +1,80 @@
1
+ # Interactive service picker for spm. Requires fzf and jq.
2
+ # Usage: source this file, then run spm_app_selector start|restart|logs|stop|flush
3
+
4
+ spm_app_selector() {
5
+ local command=""
6
+ local config=""
7
+ local app_name=""
8
+
9
+ while [[ $# -gt 0 ]]; do
10
+ case $1 in
11
+ --config=*)
12
+ config="${1#*=}"
13
+ shift
14
+ ;;
15
+ --appName=*)
16
+ app_name="${1#*=}"
17
+ shift
18
+ ;;
19
+ -h|--help)
20
+ echo "Usage: spm_app_selector [OPTIONS] COMMAND"
21
+ echo ""
22
+ echo "Options:"
23
+ echo " --appName=APP_NAME Specify the application name directly."
24
+ echo " --config=CONFIG_FILE Specify the ecosystem configuration file."
25
+ echo " -h, --help Display this help message."
26
+ echo ""
27
+ echo "Examples:"
28
+ echo " spm_app_selector start"
29
+ echo " spm_app_selector --appName=api restart"
30
+ echo " spm_app_selector --config=ecosystem.config.js logs"
31
+ return 0
32
+ ;;
33
+ start|stop|kill|restart|logs|flush)
34
+ command="$1"
35
+ shift
36
+ break
37
+ ;;
38
+ *)
39
+ echo "Unknown option or command: $1"
40
+ return 1
41
+ ;;
42
+ esac
43
+ done
44
+
45
+ if [[ -z "$command" ]]; then
46
+ echo "Usage: spm_app_selector [OPTIONS] COMMAND"
47
+ echo "COMMAND must be one of: start stop kill restart logs flush"
48
+ return 1
49
+ fi
50
+
51
+ if [[ -z "$config" ]]; then
52
+ if [[ -f ecosystem.config.js ]]; then
53
+ config="ecosystem.config.js"
54
+ elif [[ -f ecosystem.config.cjs ]]; then
55
+ config="ecosystem.config.cjs"
56
+ elif [[ -f ecosystem.custom.config.js ]]; then
57
+ config="ecosystem.custom.config.js"
58
+ fi
59
+ fi
60
+
61
+ if [[ -z "$config" ]]; then
62
+ echo "No ecosystem configuration file found."
63
+ return 1
64
+ fi
65
+
66
+ if [[ -z "$app_name" ]]; then
67
+ app_name=$(spm ${config:+--config "$config"} jlist 2>/dev/null | jq -r '.[].name' | fzf)
68
+ fi
69
+
70
+ if [[ -n "$app_name" ]]; then
71
+ if [[ -n "$config" ]]; then
72
+ spm --config "$config" "$command" "$app_name"
73
+ else
74
+ spm "$command" "$app_name"
75
+ fi
76
+ else
77
+ echo "No application selected."
78
+ return 1
79
+ fi
80
+ }