kanseishitsu 0.1.1

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA256:
3
+ metadata.gz: 25050a62712f4707b8f1b388647b618d7d722fc15f9a7713a6a2b06d504236d6
4
+ data.tar.gz: adf5c5eba9308caeb08cec65eefa966caf9f9ab6784eeef243d50030be5a0f79
5
+ SHA512:
6
+ metadata.gz: 1ec5b00e46746dbf649876030ca6b7825471c2e4d22ea1f8ba5af2ca7adbdc03328ae61be34b74f81537c99aa4cc75be9dcd60425f44a60584ec433121c6f8a2
7
+ data.tar.gz: 6f89365ab7c0d2cc4d724ec71fae360cc66d70bb94492b209b66dba2cb23f470d22fcb634b7eb1776190774c8aa36ea0131304c9060c35a7e569a69a54f80eda
data/LICENSE.md ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2024 Nels Nelson
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.
data/README.md ADDED
@@ -0,0 +1,141 @@
1
+ # kanseishitsu
2
+
3
+ A user who wishes to define their own cron jobs and file monitors in macOS faces some obstacles.
4
+
5
+ As of 2024, the current state of the art relies on XML plist files to define jobs in the `Library/LaunchAgents` directory in a user's home directory.
6
+
7
+ XML is not a very simple format for a human being to read or write.
8
+
9
+ If you already have the skills to read and write macOS LaunchAgent XML documents with ease, then the authors commend you for your determination and studiousness.
10
+
11
+ Otherwise, for all of you lazy and ignorant people like the authors, this gem is for you!
12
+
13
+ The `kanseishitsu` gem provides some little ruby scripts which will support simpler management of such launch agents.
14
+
15
+
16
+ ## Usage
17
+
18
+ Here is some usage and examples for each script.
19
+
20
+
21
+ ### cron.rb
22
+
23
+ The usage for `cron.rb`.
24
+
25
+ The `cron.rb` executable uses something similar to a `crontab` entry in GNU/Linux.
26
+
27
+ ```sh
28
+ $ cron.rb --help
29
+ Usage: cron.rb <crontab>|<options>
30
+
31
+ Options:
32
+ --show-all Show all cron jobs
33
+ -l, --list List launch agent labels
34
+ -r, --remove=<label> Remove a cron job by label
35
+ ```
36
+
37
+
38
+ #### Example
39
+
40
+ For example:
41
+
42
+ ```sh
43
+ cron.rb "0 1 * * * archive.rb ~/Downloads/archive"
44
+ ```
45
+
46
+
47
+ ### watch.rb
48
+
49
+ The usage for `watch.rb`.
50
+
51
+ ```sh
52
+ $ watch.rb --help
53
+ Usage: watch.rb <watch_path> <handler_program_path>
54
+
55
+ Options:
56
+ --show-all Show watchers
57
+ -l, --list List launch agent labels
58
+ -r, --remove=<label> Remove a watcher by label
59
+ ```
60
+
61
+
62
+ #### Example
63
+
64
+ For example:
65
+
66
+ ```sh
67
+ watch.rb ~/Desktop ~/.local/usr/bin/desktop.rb
68
+ ```
69
+
70
+
71
+ ## Docker
72
+
73
+ Build the docker image to support CI/CD containers.
74
+
75
+ ```sh
76
+ docker buildx build --tag="$(basename $(pwd))" .
77
+ docker run --interactive --tty --rm --name "$(basename $(pwd))" "$(basename $(pwd))"
78
+ ```
79
+
80
+
81
+ ## Name meaning that nobody should really care about
82
+
83
+ The Japanese word "管制室" (Kanseishitsu) translates to "control room" in English. It refers to a room from which operations are directed or monitored, typically involving equipment and personnel to oversee and manage various functions such as in factories, airports, or television studios.
84
+
85
+ Pronounced: Kahn-say-she-tsu
86
+
87
+ Here's how to say each part:
88
+
89
+ - Kahn: like the beginning of "con" in "convenient," but with a more open 'a' sound.
90
+ - Say: just like the English word "say."
91
+ - She: as in "she."
92
+ - Tsu: this part can be a bit tricky because it's not a sound typically found in English. It's similar to saying "sue," but start it with a soft 't' sound placed right before the 's'.
93
+
94
+
95
+ ## Project file tree
96
+
97
+ Here is a bird's-eye view of the project layout.
98
+
99
+ ```sh
100
+ date && tree -A -I "Gemfile.lock|*.gem|tmp|vendor"
101
+ Wed May 15 23:07:43 CDT 2024
102
+ .
103
+ ├── Dockerfile
104
+ ├── Gemfile
105
+ ├── LICENSE.md
106
+ ├── README.md
107
+ ├── Rakefile
108
+ ├── bin
109
+ │ ├── cron.rb
110
+ │ └── watch.rb
111
+ ├── kanseishitsu.gemspec
112
+ ├── kanseishitsu.png
113
+ ├── lib
114
+ │ ├── kanseishitsu
115
+ │ │ ├── cron
116
+ │ │ │ ├── argument_parser.rb
117
+ │ │ │ └── launch_agent_manager_rexml.rb
118
+ │ │ ├── cron.rb
119
+ │ │ ├── launch_agent_constants.rb
120
+ │ │ ├── launch_agent_manager.rb
121
+ │ │ ├── logging.rb
122
+ │ │ ├── plist_parser.rb
123
+ │ │ ├── version.rb
124
+ │ │ ├── watch
125
+ │ │ │ └── argument_parser.rb
126
+ │ │ ├── watch.rb
127
+ │ │ ├── which.rb
128
+ │ │ ├── xml_helper.rb
129
+ │ │ └── xml_helper_rexml.rb
130
+ │ └── kanseishitsu.rb
131
+ ├── scripts
132
+ │ └── version_bump.sh
133
+ └── spec
134
+ ├── spec_helper.rb
135
+ ├── test_spec.rb
136
+ └── verify
137
+ ├── launchctl.rb
138
+ └── verify_spec.rb
139
+
140
+ 8 directories, 28 files
141
+ ```
data/bin/cron.rb ADDED
@@ -0,0 +1,27 @@
1
+ #! /usr/bin/env ruby
2
+
3
+ # encoding: utf-8
4
+ # frozen_string_literal: false
5
+
6
+ # -*- mode: ruby -*-
7
+ # vi: set ft=ruby :
8
+
9
+ # Copyright Nels Nelson 2024 but freely usable (see license)
10
+
11
+ # Usage: cron.rb <crontab>|<options>
12
+ #
13
+ # Options:
14
+ # --show-all Show cron jobs
15
+ # -l, --list List cron jobs labels
16
+ # -r, --remove=<label> Remove a cron job by label
17
+ #
18
+ # Examples:
19
+ #
20
+ # cron.rb "0 1 * * * archive.rb ${HOME}/Downloads/archive"
21
+ # cron.rb --show-all
22
+ # cron.rb --list
23
+ # cron.rb --remove=com.local.archive
24
+
25
+ require_relative '../lib/kanseishitsu'
26
+
27
+ Object.new.extend(Cron).main
data/bin/watch.rb ADDED
@@ -0,0 +1,27 @@
1
+ #! /usr/bin/env ruby
2
+
3
+ # encoding: utf-8
4
+ # frozen_string_literal: false
5
+
6
+ # -*- mode: ruby -*-
7
+ # vi: set ft=ruby :
8
+
9
+ # Copyright Nels Nelson 2024 but freely usable (see license)
10
+
11
+ # Usage: watch.rb <path> [handler_program_path]
12
+ #
13
+ # Options:
14
+ # --show-all Show watchers
15
+ # -l, --list List launch agent labels
16
+ # -r, --remove=<label> Remove a watcher by label
17
+ #
18
+ # Examples:
19
+ #
20
+ # watch.rb ~/Desktop ~/.local/usr/bin/desktop.rb
21
+ # watch.rb --show-all
22
+ # watch.rb --list
23
+ # watch.rb --remove=com.local.desktop
24
+
25
+ require_relative '../lib/kanseishitsu'
26
+
27
+ Object.new.extend(Watcher).main
@@ -0,0 +1,120 @@
1
+ # encoding: utf-8
2
+ # frozen_string_literal: false
3
+
4
+ # vi: set ft=ruby :
5
+ # -*- mode: ruby -*-
6
+
7
+ # Copyright Nels Nelson 2024 but freely usable (see license)
8
+
9
+ require 'logger'
10
+ require 'optparse'
11
+
12
+ require_relative '../version'
13
+
14
+ # Define module Cron
15
+ module Cron
16
+ # Define the ArgumentsParser class
17
+ class ArgumentsParser
18
+ FLAGS = %i[banner show_all list remove verbose version].freeze
19
+ POSITIONAL = %i[crontab].freeze
20
+ attr_reader :parser, :options
21
+
22
+ def initialize(option_parser = OptionParser.new)
23
+ @parser = option_parser
24
+ @options = {}
25
+ FLAGS.each { |method_name| self.method(method_name).call }
26
+ end
27
+
28
+ def banner
29
+ @parser.banner = "Usage: #{File.basename($PROGRAM_NAME)} <crontab>|<options>"
30
+ @parser.separator ''
31
+ @parser.separator 'Options:'
32
+ end
33
+
34
+ def show_all
35
+ @parser.on('--show-all', 'Show cron jobs') do
36
+ @options[:show_all] = true
37
+ end
38
+ end
39
+
40
+ def list
41
+ @parser.on('-l', '--list', 'List cron jobs labels') do
42
+ @options[:list] = true
43
+ end
44
+ end
45
+
46
+ def remove
47
+ @parser.on('-r', '--remove=<label>', 'Remove a cron job by label') do |label|
48
+ @options[:remove] = label
49
+ end
50
+ end
51
+
52
+ def verbose
53
+ @options[:log_level] ||= Logger::INFO
54
+ @parser.on_tail('-v', '--verbose', 'Increase verbosity') do
55
+ @options[:log_level] -= 1
56
+ end
57
+ end
58
+
59
+ def version
60
+ @parser.on_tail('--version', 'Show version') do
61
+ puts "#{File.basename($PROGRAM_NAME)} version #{Kanseishitsu::VERSION}"
62
+ exit
63
+ end
64
+ end
65
+
66
+ def crontab(args)
67
+ crontab = args.shift.gsub(/\A['"]|['"]\z/, '').split
68
+ @options[:crontab] = crontab
69
+ @options[:cron_schedule] =
70
+ crontab.take(LaunchAgentManager::SCHEDULE_PARTS_COUNT).join(' ')
71
+ @options[:executable_path_with_args] =
72
+ crontab.drop(LaunchAgentManager::SCHEDULE_PARTS_COUNT).join(' ')
73
+ end
74
+
75
+ def demand(arg, positional: false)
76
+ return @options[arg] unless @options[arg].nil?
77
+
78
+ required_arg = if positional then "<#{arg}>"
79
+ else "--#{arg.to_s.gsub(UNDERSCORE_PATTERN, HYPHEN_STRING)}"
80
+ end
81
+ raise OptionParser::MissingArgument, "Required argument: #{required_arg}"
82
+ end
83
+
84
+ def positional!(args)
85
+ POSITIONAL.each do |opt|
86
+ self.method(opt).call(args)
87
+ self.demand(opt, positional: true)
88
+ end
89
+ end
90
+
91
+ def options?
92
+ ArgumentsParser::FLAGS.any? { |flag| @options.include?(flag) }
93
+ end
94
+
95
+ def usage!
96
+ puts @parser
97
+ exit
98
+ end
99
+
100
+ # rubocop: disable Metrics/MethodLength
101
+ def self.parse(args = ARGV, _file_path = ARGF, arguments_parser = ArgumentsParser.new)
102
+ arguments_parser.parser.parse!(args)
103
+ if !arguments_parser.options? && ARGV.length != 1
104
+ message = 'A crontab definition is required'
105
+ raise OptionParser::MissingArgument, message
106
+ elsif !args.empty?
107
+ arguments_parser.positional!(args)
108
+ end
109
+ arguments_parser.options
110
+ rescue OptionParser::AmbiguousOption => e
111
+ abort e.message
112
+ rescue OptionParser::ParseError => e
113
+ puts e.message
114
+ arguments_parser.usage!
115
+ end
116
+ # rubocop: enable Metrics/MethodLength
117
+ end
118
+ # class ArgumentsParser
119
+ end
120
+ # module Cron
@@ -0,0 +1,183 @@
1
+ # encoding: utf-8
2
+ # frozen_string_literal: false
3
+
4
+ # vi: set ft=ruby :
5
+ # -*- mode: ruby -*-
6
+
7
+ # Copyright Nels Nelson 2024 but freely usable (see license)
8
+
9
+ require 'rexml/formatters/pretty'
10
+
11
+ require 'fileutils'
12
+
13
+ require_relative '../launch_agent_plist'
14
+
15
+ # Define module LaunchAgentManagerConstants
16
+ module LaunchAgentManagerConstants
17
+ VERSION = '0.1.0'.freeze unless defined?(VERSION)
18
+ LABEL_NAMESPACE = %w[com local].freeze
19
+ INTERVALS = %w[Minute Hour Day Month Weekday].freeze
20
+ SCHEDULE_PARTS_COUNT = INTERVALS.length
21
+ LAUNCHCTL_TEMPLATE = 'launchctl bootstrap gui/%<uid>s %<plist>s'.freeze
22
+ SUCCESS_MESSAGE = 'Created and enabled launchd job: %<label>s'.freeze
23
+ LAUNCH_AGENTS_DIR_PATH = File.expand_path(File.join(Dir.home, 'Library', 'LaunchAgents'))
24
+ BOOTOUT_TEMPLATE = 'launchctl bootout gui/%<uid>s/%<label>s'.freeze
25
+ REMOVE_TEMPLATE = 'launchctl remove gui/%<uid>s/%<label>s'.freeze
26
+ end
27
+
28
+ # Define module LaunchAgentManagerInstanceMethods
29
+ module LaunchAgentManagerInstanceMethods
30
+ include LaunchAgentManagerConstants
31
+
32
+ def find_executable_in_path(cmd)
33
+ directory = ENV['PATH'].split(File::PATH_SEPARATOR).find do |dir|
34
+ File.executable?(File.join(dir, cmd))
35
+ end
36
+ directory.nil? ? nil : File.join(directory, cmd)
37
+ end
38
+
39
+ def executable?(exe)
40
+ !exe.empty? && File.executable?(exe) && !File.directory?(exe)
41
+ end
42
+
43
+ def explicit_which(cmd)
44
+ exe = `which #{cmd}`.chomp
45
+ executable?(exe) ? exe : nil
46
+ rescue Errno::ENOENT => _e
47
+ nil
48
+ end
49
+
50
+ def portable_which(cmd)
51
+ explicit_which(cmd) || find_executable_in_path(cmd)
52
+ end
53
+
54
+ # Extract program path and arguments, verifying executable
55
+ # rubocop: disable Metrics/MethodLength
56
+ def extract_program_path(executable_with_args)
57
+ *args = executable_with_args.split
58
+ exe = args.shift
59
+ exe_path = File.exist?(exe) ? exe : portable_which(exe)
60
+ if exe_path.nil? || !File.exist?(exe_path)
61
+ abort "Cannot find executable in path: #{exe}"
62
+ end
63
+ if exe_path.nil? || !File.executable?(exe_path) || File.directory?(exe_path)
64
+ abort "Given file is not executable: #{exe}"
65
+ end
66
+ args.unshift(exe_path)
67
+ args
68
+ end
69
+ # rubocop: enable Metrics/MethodLength
70
+
71
+ # Create the job name based on the given executable file path
72
+ def derive_job_label_from_executable(exe_path)
73
+ label = LABEL_NAMESPACE.dup
74
+ label << File.basename(exe_path, '.*')
75
+ label.join('.')
76
+ end
77
+
78
+ # Define cron settings for plist
79
+ def derive_calendar_interval(cron_schedule)
80
+ cron_fields = cron_schedule.split
81
+ calendar_interval = {}
82
+ INTERVALS.each do |key|
83
+ value = cron_fields.shift
84
+ raise "Invalid cron string: #{cron_schedule}" if value.nil? || value.empty?
85
+
86
+ calendar_interval[key] = value
87
+ end
88
+ calendar_interval
89
+ end
90
+
91
+ # Save plist file to LaunchAgents directory; do not overwrite an
92
+ # already existing file with the same name at the generated path
93
+ # rubocop: disable Metrics/MethodLength
94
+ def save_plist(label, doc)
95
+ FileUtils.mkdir_p(LAUNCH_AGENTS_DIR_PATH)
96
+ plist_path = File.join(LAUNCH_AGENTS_DIR_PATH, "#{label}.plist")
97
+ puts "plist_path: #{plist_path}"
98
+ return plist_path if File.exist?(plist_path)
99
+
100
+ document = ''
101
+ formatter = REXML::Formatters::Pretty.new(4)
102
+ formatter.write(doc, document)
103
+ puts '[DEBUG] Contents of plist xml document:'
104
+ puts document
105
+ File.write(plist_path, document)
106
+ plist_path
107
+ end
108
+ # rubocop: enable Metrics/MethodLength
109
+
110
+ def execute(command)
111
+ puts "[DEBUG] Executing command: #{command}"
112
+ system(command)
113
+ end
114
+
115
+ # Load the launchd job
116
+ def load_launchd_job(plist_path)
117
+ execute(format(LAUNCHCTL_TEMPLATE, uid: Process.uid, plist: plist_path))
118
+ end
119
+
120
+ # Show the launchd agents as crontabs
121
+ def show_all_cron_jobs
122
+ Dir.glob(File.join(LAUNCH_AGENTS_DIR_PATH, '*.plist')).map do |plist_path|
123
+ job = parse_plist(plist_path)
124
+ puts "#{job[:schedule]} #{job[:command]}"
125
+ end
126
+ end
127
+
128
+ # Return a list of all user launchd agent labels
129
+ def launch_agent_labels
130
+ plist_file_paths = Dir.glob(File.join(LAUNCH_AGENTS_DIR_PATH, '*.plist'))
131
+ plist_file_paths.each_with_object([]) do |file_path, labels|
132
+ label = LaunchAgentPlist.parse(file_path)[:label]
133
+ labels << label unless label.nil?
134
+ end
135
+ end
136
+
137
+ # List labels of launchd agents
138
+ def list_launch_agent_labels
139
+ launch_agent_labels.each do |label|
140
+ puts label
141
+ end
142
+ end
143
+
144
+ # Remove plist Launch Agents by label
145
+ def remove_cron_job(label)
146
+ # warn "[DEBUG] Removing launch agent: #{label}"
147
+ plist_path = File.expand_path(File.join(LAUNCH_AGENTS_DIR_PATH, "#{label}.plist"))
148
+ # warn "[DEBUG] Removing launch agent plist definition file: #{plist_path}"
149
+ if File.exist?(plist_path)
150
+ execute(format(BOOTOUT_TEMPLATE, uid: Process.uid, label: label))
151
+ execute(format(REMOVE_TEMPLATE, uid: Process.uid, label: label))
152
+ FileUtils.rm(plist_path)
153
+ puts "Removed launch agent: #{label}"
154
+ else
155
+ warn "Not found; crontab or launch agent: #{label}"
156
+ end
157
+ end
158
+
159
+ # Create the launchd job
160
+ def create_launchd_job(options)
161
+ args = extract_program_path(options[:executable_path_with_args])
162
+ label = derive_job_label_from_executable(args.first)
163
+ calendar_interval = derive_calendar_interval(options[:cron_schedule])
164
+ plist_doc = LaunchAgentPlist.new(label, args, calendar_interval).to_doc
165
+ plist_path = save_plist(label, plist_doc)
166
+ puts "Executing: cat #{plist_path}"
167
+ puts `cat #{plist_path}`.strip
168
+ load_launchd_job(plist_path)
169
+ puts format(SUCCESS_MESSAGE, label: label)
170
+ end
171
+ end
172
+ # module LaunchAgentManagerInstanceMethods
173
+
174
+ # Define the LaunchAgentManager class
175
+ class LaunchAgentManager
176
+ include LaunchAgentManagerInstanceMethods
177
+
178
+ attr_reader :options
179
+
180
+ def initialize(options)
181
+ @options = options
182
+ end
183
+ end
@@ -0,0 +1,53 @@
1
+ #! /usr/bin/env ruby
2
+ # encoding: utf-8
3
+ # frozen_string_literal: false
4
+
5
+ # vi: set ft=ruby :
6
+ # -*- mode: ruby -*-
7
+
8
+ # Copyright Nels Nelson 2024 but freely usable (see license)
9
+
10
+ # Usage: cron.rb <crontab>|<options>
11
+ #
12
+ # Options:
13
+ # --show-all Show cron jobs
14
+ # -l, --list List cron jobs labels
15
+ # -r, --remove=<label> Remove a cron job by label
16
+ #
17
+ # Examples:
18
+ #
19
+ # cron.rb "0 1 * * * archive.rb ${HOME}/Downloads/archive"
20
+ # cron.rb --show-all
21
+ # cron.rb --list
22
+ # cron.rb --remove=com.local.archive
23
+
24
+ require_relative 'launch_agent_manager'
25
+ require_relative 'cron/argument_parser'
26
+
27
+ # Define module Cron
28
+ module Cron
29
+ LaunchAgentManager = ::LaunchAgentManager
30
+
31
+ # rubocop: disable Metrics/MethodLength
32
+ def main(args = Cron::ArgumentsParser.parse)
33
+ log.log_level = args[:log_level]
34
+ manager = Cron::LaunchAgentManager.new(args)
35
+ if args[:show_all]
36
+ manager.show_all_launch_agents
37
+ elsif args[:list]
38
+ manager.list_launch_agent_labels
39
+ elsif args[:remove]
40
+ manager.remove_launch_agent(args[:remove])
41
+ else
42
+ manager.create_launch_agent(
43
+ args[:executable_path_with_args],
44
+ nil,
45
+ args[:cron_schedule]
46
+ )
47
+ end
48
+ end
49
+ # rubocop: enable Metrics/MethodLength
50
+ end
51
+ # module Cron
52
+
53
+ Object.new.extend(Cron).main if $PROGRAM_NAME == __FILE__
@@ -0,0 +1,41 @@
1
+ # encoding: utf-8
2
+ # frozen_string_literal: false
3
+
4
+ # -*- mode: ruby -*-
5
+ # vi: set ft=ruby :
6
+
7
+ # Copyright Nels Nelson 2024 but freely usable (see license)
8
+
9
+ # Define the LaunchAgentConstants module
10
+ module LaunchAgentConstants
11
+ DEFAULT_ENCODING = 'UTF-8'.freeze
12
+ DOCTYPE_ELEMENT = '<!%<doctypes>s>'.freeze
13
+ DOCTYPES = [
14
+ 'DOCTYPE',
15
+ 'plist',
16
+ 'PUBLIC',
17
+ '"-//Apple//DTD PLIST 1.0//EN"',
18
+ '"http://www.apple.com/DTDs/PropertyList-1.0.dtd"'
19
+ ].freeze
20
+ LABEL_NAMESPACE = %w[com local].freeze
21
+ INTERVALS = %w[Minute Hour Day Month Weekday].freeze
22
+ SCHEDULE_PARTS_COUNT = INTERVALS.length
23
+ CDATA_PATTERN = %r{[<>&'"\n]+}
24
+ SCHEDULE_XPATH_TEMPLATE = './/key[text()="%<key>s"]/following-sibling::integer'.freeze
25
+ KEYS_REQUIRING_CDATA = ['PATH'].freeze
26
+ LAUNCHCTL_TEMPLATE = 'launchctl bootstrap gui/%<uid>s %<plist>s'.freeze
27
+ SUCCESS_MESSAGE = 'Created and enabled launchd job: %<label>s'.freeze
28
+ LAUNCH_AGENTS_DIR_PATH = File.expand_path(File.join(Dir.home, 'Library', 'LaunchAgents'))
29
+ LABEL_XPATH = '//key[text()="Label"]/following-sibling::*[1]'.freeze
30
+ START_CALENDAR_INTERVAL_XPATH =
31
+ '//key[text()="StartCalendarInterval"]/following-sibling::*[1]'.freeze
32
+ WATCH_PATHS_XPATH = '//key[text()="WatchPaths"]/following-sibling::*[1]'.freeze
33
+ LOCAL_STRING_XPATH = './string'.freeze
34
+ PROGRAM_ARGUMENTS_XPATH =
35
+ '//key[text()="ProgramArguments"]/following-sibling::array[1]'.freeze
36
+ INTERVAL_XPATH_TEMPLATE = './key[text()="%<key>s"]/following-sibling::*[1]'.freeze
37
+ BOOTOUT_TEMPLATE = 'launchctl bootout gui/%<uid>s/%<label>s'.freeze
38
+ REMOVE_TEMPLATE = 'launchctl remove gui/%<uid>s/%<label>s'.freeze
39
+ ON_LOGIN = '@login'.freeze
40
+ end
41
+ # module LaunchAgentConstants