howzit 2.1.29 → 2.1.30

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.
@@ -0,0 +1,479 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'fileutils'
4
+ require 'shellwords'
5
+
6
+ module Howzit
7
+ # Script Support module
8
+ # Handles helper script installation and injection for run blocks
9
+ # rubocop:disable Metrics/ModuleLength
10
+ module ScriptSupport
11
+ SUPPORT_DIR = '~/.local/share/howzit/support'
12
+
13
+ class << self
14
+ ##
15
+ ## Get the support directory path
16
+ ##
17
+ ## @return [String] expanded path to support directory
18
+ ##
19
+ def support_dir
20
+ File.expand_path(SUPPORT_DIR)
21
+ end
22
+
23
+ ##
24
+ ## Ensure support directory exists and is populated
25
+ ##
26
+ def ensure_support_dir
27
+ dir = support_dir
28
+ FileUtils.mkdir_p(dir) unless File.directory?(dir)
29
+ install_helper_scripts
30
+ dir
31
+ end
32
+
33
+ ##
34
+ ## Detect interpreter from hashbang line
35
+ ##
36
+ ## @param script_content [String] The script content
37
+ ##
38
+ ## @return [Symbol, nil] Language identifier (:bash, :zsh, :fish, :ruby, :python, etc.)
39
+ ##
40
+ def detect_interpreter(script_content)
41
+ first_line = script_content.lines.first&.strip
42
+ return nil unless first_line&.start_with?('#!')
43
+
44
+ shebang = first_line.sub(/^#!/, '').strip
45
+
46
+ case shebang
47
+ when %r{/bin/bash}, %r{/usr/bin/env bash}
48
+ :bash
49
+ when %r{/bin/zsh}, %r{/usr/bin/env zsh}
50
+ :zsh
51
+ when %r{/bin/fish}, %r{/usr/bin/env fish}
52
+ :fish
53
+ when %r{/usr/bin/env ruby}, %r{/usr/bin/ruby}, %r{/usr/local/bin/ruby}
54
+ :ruby
55
+ when %r{/usr/bin/env python3?}, %r{/usr/bin/python3?}, %r{/usr/local/bin/python3?}
56
+ :python
57
+ when %r{/usr/bin/env perl}, %r{/usr/bin/perl}
58
+ :perl
59
+ when %r{/usr/bin/env node}, %r{/usr/bin/node}
60
+ :node
61
+ end
62
+ end
63
+
64
+ ##
65
+ ## Get the injection line for a given interpreter
66
+ ##
67
+ ## @param interpreter [Symbol] The interpreter type
68
+ ##
69
+ ## @return [String, nil] The injection line to add
70
+ ##
71
+ def injection_line_for(interpreter)
72
+ support_path = support_dir
73
+ case interpreter
74
+ when :bash, :zsh
75
+ "source \"#{support_path}/howzit.sh\""
76
+ when :fish
77
+ "source \"#{support_path}/howzit.fish\""
78
+ when :ruby
79
+ "require '#{support_path}/howzit.rb'"
80
+ when :python
81
+ "import sys\nsys.path.insert(0, '#{support_path}')\nimport howzit"
82
+ when :perl
83
+ "require '#{support_path}/howzit.pl'"
84
+ when :node
85
+ "require('#{support_path}/howzit.js')"
86
+ end
87
+ end
88
+
89
+ ##
90
+ ## Inject helper script loading into script content
91
+ ##
92
+ ## @param script_content [String] The original script content
93
+ ##
94
+ ## @return [Array] [modified_content, interpreter] Script content with injection added and interpreter
95
+ ##
96
+ def inject_helper(script_content)
97
+ interpreter = detect_interpreter(script_content)
98
+ return [script_content, nil] unless interpreter
99
+
100
+ injection = injection_line_for(interpreter)
101
+ return [script_content, interpreter] unless injection
102
+
103
+ lines = script_content.lines
104
+ injection_lines = injection.split("\n").map { |l| "#{l}\n" }
105
+ # Find the hashbang line
106
+ if lines.first&.strip&.start_with?('#!')
107
+ # Insert after hashbang
108
+ injection_lines.each_with_index do |line, idx|
109
+ lines.insert(1 + idx, line)
110
+ end
111
+ else
112
+ # No hashbang, prepend
113
+ lines = injection_lines + lines
114
+ end
115
+
116
+ [lines.join, interpreter]
117
+ end
118
+
119
+ ##
120
+ ## Get the command to execute a script based on interpreter
121
+ ##
122
+ ## @param script_path [String] Path to the script file
123
+ ## @param interpreter [Symbol, nil] The interpreter type
124
+ ##
125
+ ## @return [String] Command to execute the script
126
+ ##
127
+ def execution_command_for(script_path, interpreter)
128
+ cmd = case interpreter
129
+ when :bash
130
+ "/bin/bash #{Shellwords.escape(script_path)}"
131
+ when :zsh
132
+ "/bin/zsh #{Shellwords.escape(script_path)}"
133
+ when :fish
134
+ "/usr/bin/env fish #{Shellwords.escape(script_path)}"
135
+ when :ruby
136
+ "/usr/bin/env ruby #{Shellwords.escape(script_path)}"
137
+ when :python
138
+ "/usr/bin/env python3 #{Shellwords.escape(script_path)}"
139
+ when :perl
140
+ "/usr/bin/env perl #{Shellwords.escape(script_path)}"
141
+ when :node
142
+ "/usr/bin/env node #{Shellwords.escape(script_path)}"
143
+ end
144
+ # Fallback to direct execution if interpreter not recognized
145
+ cmd || script_path
146
+ end
147
+
148
+ ##
149
+ ## Install all helper scripts
150
+ ##
151
+ def install_helper_scripts
152
+ dir = support_dir
153
+ FileUtils.mkdir_p(dir)
154
+
155
+ install_bash_helper(dir)
156
+ install_fish_helper(dir)
157
+ install_ruby_helper(dir)
158
+ install_python_helper(dir)
159
+ install_perl_helper(dir)
160
+ install_node_helper(dir)
161
+ end
162
+
163
+ private
164
+
165
+ ##
166
+ ## Install bash/zsh helper script
167
+ ##
168
+ def install_bash_helper(dir)
169
+ file = File.join(dir, 'howzit.sh')
170
+ return if File.exist?(file) && !file_stale?(file)
171
+
172
+ content = <<~BASH
173
+ #!/bin/bash
174
+ # Howzit helper functions for bash/zsh
175
+
176
+ # Log functions
177
+ log() {
178
+ local level="$1"
179
+ shift
180
+ local message="$*"
181
+ if [ -n "$HOWZIT_COMM_FILE" ]; then
182
+ echo "LOG:$level:$message" >> "$HOWZIT_COMM_FILE"
183
+ fi
184
+ }
185
+
186
+ log_info() { log info "$@"; }
187
+ log_warn() { log warn "$@"; }
188
+ log_error() { log error "$@"; }
189
+ log_debug() { log debug "$@"; }
190
+
191
+ # Set variable function
192
+ set_var() {
193
+ local var_name="$1"
194
+ local var_value="$2"
195
+ if [ -n "$HOWZIT_COMM_FILE" ]; then
196
+ echo "VAR:$var_name=$var_value" >> "$HOWZIT_COMM_FILE"
197
+ fi
198
+ }
199
+ BASH
200
+
201
+ File.write(file, content)
202
+ File.chmod(0o644, file)
203
+ end
204
+
205
+ ##
206
+ ## Install fish helper script
207
+ ##
208
+ def install_fish_helper(dir)
209
+ file = File.join(dir, 'howzit.fish')
210
+ return if File.exist?(file) && !file_stale?(file)
211
+
212
+ content = <<~FISH
213
+ #!/usr/bin/env fish
214
+ # Howzit helper functions for fish
215
+
216
+ function log -d "Log a message at the specified level"
217
+ set level $argv[1]
218
+ set -e argv[1]
219
+ set message (string join " " $argv)
220
+ if test -n "$HOWZIT_COMM_FILE"
221
+ echo "LOG:$level:$message" >> "$HOWZIT_COMM_FILE"
222
+ end
223
+ end
224
+
225
+ function log_info -d "Log an info message"
226
+ log info $argv
227
+ end
228
+
229
+ function log_warn -d "Log a warning message"
230
+ log warn $argv
231
+ end
232
+
233
+ function log_error -d "Log an error message"
234
+ log error $argv
235
+ end
236
+
237
+ function log_debug -d "Log a debug message"
238
+ log debug $argv
239
+ end
240
+
241
+ function set_var -d "Set a variable for howzit"
242
+ set var_name $argv[1]
243
+ set var_value $argv[2]
244
+ if test -n "$HOWZIT_COMM_FILE"
245
+ echo "VAR:$var_name=$var_value" >> "$HOWZIT_COMM_FILE"
246
+ end
247
+ end
248
+ FISH
249
+
250
+ File.write(file, content)
251
+ File.chmod(0o644, file)
252
+ end
253
+
254
+ ##
255
+ ## Install Ruby helper script
256
+ ##
257
+ def install_ruby_helper(dir)
258
+ file = File.join(dir, 'howzit.rb')
259
+ return if File.exist?(file) && !file_stale?(file)
260
+
261
+ content = <<~'RUBY'
262
+ # frozen_string_literal: true
263
+
264
+ # Howzit helper module for Ruby
265
+ module Howzit
266
+ class << self
267
+ # Log methods
268
+ def logger
269
+ @logger ||= Logger.new
270
+ end
271
+
272
+ class Logger
273
+ def info(message)
274
+ log(:info, message)
275
+ end
276
+
277
+ def warn(message)
278
+ log(:warn, message)
279
+ end
280
+
281
+ def error(message)
282
+ log(:error, message)
283
+ end
284
+
285
+ def debug(message)
286
+ log(:debug, message)
287
+ end
288
+
289
+ private
290
+
291
+ def log(level, message)
292
+ comm_file = ENV['HOWZIT_COMM_FILE']
293
+ return unless comm_file
294
+
295
+ File.open(comm_file, 'a') do |f|
296
+ f.puts "LOG:#{level}:#{message}"
297
+ end
298
+ end
299
+ end
300
+
301
+ # Set variable method
302
+ def set_var(name, value)
303
+ comm_file = ENV['HOWZIT_COMM_FILE']
304
+ return unless comm_file
305
+
306
+ File.open(comm_file, 'a') do |f|
307
+ f.puts "VAR:#{name}=#{value}"
308
+ end
309
+ end
310
+ end
311
+ end
312
+ RUBY
313
+
314
+ File.write(file, content)
315
+ File.chmod(0o644, file)
316
+ end
317
+
318
+ ##
319
+ ## Install Python helper script
320
+ ##
321
+ def install_python_helper(dir)
322
+ file = File.join(dir, 'howzit.py')
323
+ return if File.exist?(file) && !file_stale?(file)
324
+
325
+ content = <<~PYTHON
326
+ #!/usr/bin/env python3
327
+ # Howzit helper module for Python
328
+
329
+ import os
330
+
331
+ class _Logger:
332
+ def _log(self, level, message):
333
+ comm_file = os.environ.get('HOWZIT_COMM_FILE')
334
+ if comm_file:
335
+ with open(comm_file, 'a') as f:
336
+ f.write(f"LOG:{level}:{message}\\n")
337
+
338
+ def info(self, message):
339
+ self._log('info', message)
340
+
341
+ def warn(self, message):
342
+ self._log('warn', message)
343
+
344
+ def error(self, message):
345
+ self._log('error', message)
346
+
347
+ def debug(self, message):
348
+ self._log('debug', message)
349
+
350
+ class Howzit:
351
+ logger = _Logger()
352
+
353
+ @staticmethod
354
+ def set_var(name, value):
355
+ comm_file = os.environ.get('HOWZIT_COMM_FILE')
356
+ if comm_file:
357
+ with open(comm_file, 'a') as f:
358
+ f.write(f"VAR:{name}={value}\\n")
359
+ PYTHON
360
+
361
+ File.write(file, content)
362
+ File.chmod(0o644, file)
363
+ end
364
+
365
+ ##
366
+ ## Install Perl helper script
367
+ ##
368
+ def install_perl_helper(dir)
369
+ file = File.join(dir, 'howzit.pl')
370
+ return if File.exist?(file) && !file_stale?(file)
371
+
372
+ content = <<~PERL
373
+ #!/usr/bin/env perl
374
+ # Howzit helper module for Perl
375
+
376
+ package Howzit;
377
+
378
+ use strict;
379
+ use warnings;
380
+
381
+ sub log {
382
+ my ($level, $message) = @_;
383
+ my $comm_file = $ENV{'HOWZIT_COMM_FILE'};
384
+ return unless $comm_file;
385
+
386
+ open(my $fh, '>>', $comm_file) or return;
387
+ print $fh "LOG:$level:$message\\n";
388
+ close($fh);
389
+ }
390
+
391
+ sub log_info { log('info', $_[0]); }
392
+ sub log_warn { log('warn', $_[0]); }
393
+ sub log_error { log('error', $_[0]); }
394
+ sub log_debug { log('debug', $_[0]); }
395
+
396
+ sub set_var {
397
+ my ($name, $value) = @_;
398
+ my $comm_file = $ENV{'HOWZIT_COMM_FILE'};
399
+ return unless $comm_file;
400
+
401
+ open(my $fh, '>>', $comm_file) or return;
402
+ print $fh "VAR:$name=$value\\n";
403
+ close($fh);
404
+ }
405
+
406
+ 1;
407
+ PERL
408
+
409
+ File.write(file, content)
410
+ File.chmod(0o644, file)
411
+ end
412
+
413
+ ##
414
+ ## Install Node.js helper script
415
+ ##
416
+ def install_node_helper(dir)
417
+ file = File.join(dir, 'howzit.js')
418
+ return if File.exist?(file) && !file_stale?(file)
419
+
420
+ content = <<~JAVASCRIPT
421
+ // Howzit helper module for Node.js
422
+
423
+ const fs = require('fs');
424
+ const path = require('path');
425
+
426
+ class Logger {
427
+ _log(level, message) {
428
+ const commFile = process.env.HOWZIT_COMM_FILE;
429
+ if (commFile) {
430
+ fs.appendFileSync(commFile, `LOG:${level}:${message}\\n`);
431
+ }
432
+ }
433
+
434
+ info(message) {
435
+ this._log('info', message);
436
+ }
437
+
438
+ warn(message) {
439
+ this._log('warn', message);
440
+ }
441
+
442
+ error(message) {
443
+ this._log('error', message);
444
+ }
445
+
446
+ debug(message) {
447
+ this._log('debug', message);
448
+ }
449
+ }
450
+
451
+ class Howzit {
452
+ static logger = new Logger();
453
+
454
+ static setVar(name, value) {
455
+ const commFile = process.env.HOWZIT_COMM_FILE;
456
+ if (commFile) {
457
+ fs.appendFileSync(commFile, `VAR:${name}=${value}\\n`);
458
+ }
459
+ }
460
+ }
461
+
462
+ module.exports = { Howzit, Logger };
463
+ JAVASCRIPT
464
+
465
+ File.write(file, content)
466
+ File.chmod(0o644, file)
467
+ end
468
+
469
+ ##
470
+ ## Check if a file is stale and needs updating
471
+ ## For now, always update to ensure latest version
472
+ ##
473
+ def file_stale?(_file)
474
+ true
475
+ end
476
+ end
477
+ # rubocop:enable Metrics/ModuleLength
478
+ end
479
+ end
data/lib/howzit/task.rb CHANGED
@@ -5,7 +5,7 @@ require 'English'
5
5
  module Howzit
6
6
  # Task object
7
7
  class Task
8
- attr_reader :type, :title, :action, :arguments, :parent, :optional, :default, :last_status
8
+ attr_reader :type, :title, :action, :arguments, :parent, :optional, :default, :last_status, :log_level
9
9
 
10
10
  ##
11
11
  ## Initialize a Task object
@@ -20,6 +20,7 @@ module Howzit
20
20
  ## @option attributes :title [String] task title
21
21
  ## @option attributes :action [String] task action
22
22
  ## @option attributes :parent [String] title of nested (included) topic origin
23
+ ## @option attributes :log_level [String] log level for this task (debug, info, warn, error)
23
24
  def initialize(attributes, optional: false, default: true)
24
25
  @prefix = "{bw}\u{25B7}\u{25B7} {x}"
25
26
  # arrow = "{bw}\u{279F}{x}"
@@ -30,6 +31,7 @@ module Howzit
30
31
  @parent = attributes[:parent] || nil
31
32
 
32
33
  @action = attributes[:action].render_arguments || nil
34
+ @log_level = attributes[:log_level]
33
35
 
34
36
  @optional = optional
35
37
  @default = default
@@ -62,12 +64,29 @@ module Howzit
62
64
  block = @action
63
65
  script = Tempfile.new('howzit_script')
64
66
  comm_file = ScriptComm.setup
67
+ old_log_level = apply_log_level
65
68
  begin
66
- script.write(block)
69
+ # Ensure support directory exists and install helpers
70
+ ScriptSupport.ensure_support_dir
71
+ ENV['HOWZIT_SUPPORT_DIR'] = ScriptSupport.support_dir
72
+
73
+ # Inject helper script loading
74
+ modified_block, interpreter = ScriptSupport.inject_helper(block)
75
+
76
+ script.write(modified_block)
67
77
  script.close
68
- File.chmod(0o777, script.path)
69
- res = system(%(/bin/sh -c "#{script.path}"))
78
+ File.chmod(0o755, script.path)
79
+
80
+ # Use appropriate interpreter command
81
+ cmd = ScriptSupport.execution_command_for(script.path, interpreter)
82
+ # If interpreter is nil, execute directly (will respect hashbang)
83
+ res = if interpreter.nil?
84
+ system(script.path)
85
+ else
86
+ system(cmd)
87
+ end
70
88
  ensure
89
+ restore_log_level(old_log_level) if old_log_level
71
90
  script.close
72
91
  script.unlink
73
92
  # Process script communication
@@ -97,6 +116,38 @@ module Howzit
97
116
  [output, matches[0].tasks.count]
98
117
  end
99
118
 
119
+ ##
120
+ ## Apply log level for this task
121
+ ##
122
+ def apply_log_level
123
+ return unless @log_level
124
+
125
+ level_map = {
126
+ 'debug' => 0,
127
+ 'info' => 1,
128
+ 'warn' => 2,
129
+ 'warning' => 2,
130
+ 'error' => 3
131
+ }
132
+ level_value = level_map[@log_level.downcase] || @log_level.to_i
133
+ old_level = Howzit.options[:log_level]
134
+ Howzit.options[:log_level] = level_value
135
+ Howzit.console.log_level = level_value
136
+ ENV['HOWZIT_LOG_LEVEL'] = @log_level.downcase
137
+ old_level
138
+ end
139
+
140
+ ##
141
+ ## Restore log level after task execution
142
+ ##
143
+ def restore_log_level(old_level)
144
+ return unless @log_level
145
+
146
+ Howzit.options[:log_level] = old_level
147
+ Howzit.console.log_level = old_level
148
+ ENV.delete('HOWZIT_LOG_LEVEL')
149
+ end
150
+
100
151
  ##
101
152
  ## Execute a run task
102
153
  ##
@@ -116,9 +167,11 @@ module Howzit
116
167
  Howzit.console.info("#{@prefix}{bg}Running {bw}#{display_title}{x}".c)
117
168
  ENV['HOWZIT_SCRIPTS'] = File.expand_path('~/.config/howzit/scripts')
118
169
  comm_file = ScriptComm.setup
170
+ old_log_level = apply_log_level
119
171
  begin
120
172
  res = system(@action)
121
173
  ensure
174
+ restore_log_level(old_log_level) if old_log_level
122
175
  # Process script communication
123
176
  ScriptComm.apply(comm_file) if comm_file
124
177
  end