aias 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.
Files changed (71) hide show
  1. checksums.yaml +7 -0
  2. data/.envrc +1 -0
  3. data/.github/workflows/deploy-github-pages.yml +52 -0
  4. data/.ruby-version +1 -0
  5. data/CHANGELOG.md +140 -0
  6. data/COMMITS.md +196 -0
  7. data/LICENSE.txt +21 -0
  8. data/README.md +249 -0
  9. data/Rakefile +27 -0
  10. data/aia_schedule_idea.md +256 -0
  11. data/docs/assets/images/logo.jpg +0 -0
  12. data/docs/cli/add.md +101 -0
  13. data/docs/cli/check.md +70 -0
  14. data/docs/cli/clear.md +45 -0
  15. data/docs/cli/dry-run.md +57 -0
  16. data/docs/cli/index.md +51 -0
  17. data/docs/cli/install.md +198 -0
  18. data/docs/cli/last.md +49 -0
  19. data/docs/cli/list.md +40 -0
  20. data/docs/cli/next.md +49 -0
  21. data/docs/cli/remove.md +87 -0
  22. data/docs/cli/show.md +54 -0
  23. data/docs/cli/uninstall.md +29 -0
  24. data/docs/cli/update.md +75 -0
  25. data/docs/getting-started/installation.md +69 -0
  26. data/docs/getting-started/quick-start.md +105 -0
  27. data/docs/guides/configuration-layering.md +168 -0
  28. data/docs/guides/cron-environment.md +112 -0
  29. data/docs/guides/scheduling-prompts.md +319 -0
  30. data/docs/guides/understanding-cron.md +134 -0
  31. data/docs/guides/validation.md +114 -0
  32. data/docs/index.md +100 -0
  33. data/docs/reference/api.md +409 -0
  34. data/docs/reference/architecture.md +122 -0
  35. data/docs/reference/environment.md +67 -0
  36. data/docs/reference/logging.md +73 -0
  37. data/example_prompts/code_health_check.md +51 -0
  38. data/example_prompts/daily_digest.md +19 -0
  39. data/example_prompts/morning_standup.md +19 -0
  40. data/example_prompts/reports/monthly_review.md +44 -0
  41. data/example_prompts/reports/weekly_summary.md +22 -0
  42. data/example_prompts/you_are_good.md +22 -0
  43. data/exe/aias +5 -0
  44. data/lib/aias/block_parser.rb +42 -0
  45. data/lib/aias/cli/add.rb +30 -0
  46. data/lib/aias/cli/check.rb +50 -0
  47. data/lib/aias/cli/clear.rb +11 -0
  48. data/lib/aias/cli/dry_run.rb +24 -0
  49. data/lib/aias/cli/install.rb +57 -0
  50. data/lib/aias/cli/last.rb +26 -0
  51. data/lib/aias/cli/list.rb +20 -0
  52. data/lib/aias/cli/next.rb +28 -0
  53. data/lib/aias/cli/remove.rb +14 -0
  54. data/lib/aias/cli/show.rb +18 -0
  55. data/lib/aias/cli/uninstall.rb +15 -0
  56. data/lib/aias/cli/update.rb +30 -0
  57. data/lib/aias/cli/version.rb +10 -0
  58. data/lib/aias/cli.rb +91 -0
  59. data/lib/aias/cron_describer.rb +142 -0
  60. data/lib/aias/crontab_manager.rb +140 -0
  61. data/lib/aias/env_file.rb +75 -0
  62. data/lib/aias/job_builder.rb +56 -0
  63. data/lib/aias/paths.rb +14 -0
  64. data/lib/aias/prompt_scanner.rb +111 -0
  65. data/lib/aias/schedule_config.rb +31 -0
  66. data/lib/aias/validator.rb +101 -0
  67. data/lib/aias/version.rb +5 -0
  68. data/lib/aias.rb +17 -0
  69. data/mkdocs.yml +137 -0
  70. data/sig/aias.rbs +4 -0
  71. metadata +191 -0
@@ -0,0 +1,51 @@
1
+ ---
2
+ description: Weekly code health review — runs checks and reports findings
3
+ schedule: "every friday at 9am"
4
+ ---
5
+ You are a pragmatic software quality engineer. Do not generate checklists. Run the
6
+ actual commands, read the actual files, and report what you find. Flag problems
7
+ clearly. Confirm what is healthy. Be specific — file names, line numbers, method
8
+ names, exact gem versions.
9
+
10
+ **Repository:** /Users/dewayne/sandbox/git_repos/madbomber/aias
11
+ **Coverage target:** 95%
12
+
13
+ Work through each section below. For each one, execute the relevant commands or
14
+ read the relevant files, then write a short findings paragraph. Use "OK" when
15
+ things are fine and "PROBLEM" when they are not.
16
+
17
+ ---
18
+
19
+ ## 1. Test Coverage
20
+
21
+ Run `bundle exec rake test` from the repository root. Report:
22
+ - The actual coverage percentage produced
23
+ - Whether it meets the 95% target
24
+ - Any test files that contain `skip` calls and why they are skipped
25
+
26
+ ## 2. Code Quality
27
+
28
+ Scan the `lib/` directory. Report:
29
+ - Every TODO or FIXME comment — quote it, give the file and line number
30
+ - Every public method longer than 15 lines — name it, measure it, say whether it
31
+ is a candidate for extraction
32
+ - Any method that takes more than 3 parameters (a sign of unclear interface)
33
+
34
+ ## 3. Dependencies
35
+
36
+ Run `bundle outdated`. Report:
37
+ - Every gem that is more than one minor version behind — current vs latest
38
+ - Any gem with a known security advisory (check the output of `bundle audit` if
39
+ available, otherwise note that it was not checked)
40
+
41
+ ## 4. Documentation
42
+
43
+ Read CHANGELOG.md and the git log for the current week. Report:
44
+ - Whether CHANGELOG.md has been updated to reflect commits made since last Friday
45
+ - Any public API change in `lib/` that is not reflected in README.md examples
46
+
47
+ ## 5. Summary
48
+
49
+ End with a one-paragraph plain-English summary: overall health, the single most
50
+ urgent problem (if any), and the one thing that would most improve the codebase
51
+ this week.
@@ -0,0 +1,19 @@
1
+ ---
2
+ description: Morning digest of Ruby and AI ecosystem news
3
+ schedule: "every day at 7am"
4
+ ---
5
+ You are a concise technical news editor. Today is <%= Time.now.strftime("%A, %B %-d, %Y") %>.
6
+
7
+ Summarize the most significant developments from the past 24 hours in these areas:
8
+
9
+ 1. **Ruby ecosystem** — new gem releases, RubyGems stats, notable blog posts, Ruby core changes
10
+ 2. **AI/LLM landscape** — model releases, API changes, benchmark results, research papers worth noting
11
+ 3. **Developer tools** — anything relevant to CLI tooling, code generation, or developer productivity
12
+
13
+ Format:
14
+ - 3–5 bullet points per section
15
+ - Each bullet under 25 words
16
+ - Flag anything that is a breaking change or security issue with [!]
17
+ - Total response under 400 words
18
+
19
+ Skip anything that is purely speculative or marketing-focused.
@@ -0,0 +1,19 @@
1
+ ---
2
+ description: Weekday morning standup prep — what to focus on today
3
+ schedule: "every weekday at 8am"
4
+ ---
5
+ You are a focused engineering coach helping me start the day clearly.
6
+
7
+ **Project:** aias gem
8
+ **Current priorities:** shipping v1, writing tests, documentation
9
+ **Today is:** <%= Time.now.strftime("%A, %B %-d") %>
10
+
11
+ Give me a crisp standup-style summary:
12
+
13
+ **Yesterday:** Suggest 2–3 likely accomplishments based on the current priorities (I will edit as needed).
14
+
15
+ **Today:** Recommend the 3 highest-leverage tasks I should tackle given the priorities above. Order them by impact. Each task should be specific and completable in a single work session.
16
+
17
+ **Blockers:** List any common blockers or risks for a project at this stage that I should watch for today.
18
+
19
+ Keep the entire response under 200 words. Use plain text, no markdown headers — this gets read aloud.
@@ -0,0 +1,44 @@
1
+ ---
2
+ description: First-of-month retrospective and planning prompt
3
+ schedule: "every month on the 1st at 8am"
4
+ ---
5
+ You are a reflective technical mentor helping me run a monthly review on the first of each month.
6
+
7
+ **Review month:** <%= Date.today.prev_month.strftime("%B %Y") %>
8
+ **Focus area:** gem development and open source contributions
9
+ **Rating scale:** 1 to 5
10
+
11
+ Structure the review as follows:
12
+
13
+ ---
14
+
15
+ ### Retrospective — <%= Date.today.prev_month.strftime("%B") %>
16
+
17
+ **Momentum** (rate 1 to 5)
18
+ Prompt me to score the overall momentum of gem development and open source contributions last month and briefly explain why.
19
+
20
+ **Highlight**
21
+ Ask me: what is the single most satisfying thing completed or learned last month?
22
+
23
+ **What slipped**
24
+ Ask me: what did I plan to do that did not happen, and why?
25
+
26
+ **Surprise**
27
+ Ask me: what was unexpected — good or bad?
28
+
29
+ ---
30
+
31
+ ### Planning — <%= Date.today.strftime("%B %Y") %>
32
+
33
+ **Top 3 outcomes for this month**
34
+ Help me define 3 specific, measurable outcomes for gem development and open source contributions this month. Make each outcome completable within 30 days.
35
+
36
+ **Risk**
37
+ Name the single most likely thing that will derail this month's plan. Suggest one mitigation.
38
+
39
+ **First step**
40
+ What is the smallest concrete action I can take today to start the month with momentum?
41
+
42
+ ---
43
+
44
+ Keep questions direct and brief. Leave space for my answers after each prompt.
@@ -0,0 +1,22 @@
1
+ ---
2
+ description: Monday morning weekly summary of the previous week's work
3
+ schedule: "every monday at 7:30am"
4
+ ---
5
+ You are a thoughtful technical writer composing a personal weekly review.
6
+
7
+ **Week ending:** <%= (Date.today - 3).strftime("%B %-d, %Y") %>
8
+ **Focus areas:** Ruby gems, AI tooling, open source
9
+ **Output format:** brief narrative
10
+
11
+ Write a concise weekly summary covering:
12
+
13
+ **What moved forward**
14
+ Describe 3–4 meaningful things that likely progressed in the focus areas this past week. Base this on known release cadences, typical open-source activity patterns, and industry trends.
15
+
16
+ **What to carry into next week**
17
+ Identify 2–3 themes or threads that deserve continued attention in the coming week.
18
+
19
+ **One insight**
20
+ Offer a single, specific observation or pattern that is worth sitting with — something non-obvious that connects two or more of the focus areas.
21
+
22
+ Keep it under 300 words. Write in first-person as if I authored it. Avoid bullet points — this is a narrative format meant to be read, not scanned.
@@ -0,0 +1,22 @@
1
+ ---
2
+ flags:
3
+ debug: true
4
+ verbose: true
5
+ provider: ollama
6
+ models:
7
+ - name: gpt-oss:latest
8
+ role:
9
+ schedule: every 2 minutes
10
+ required: ['shared_tools']
11
+ tools:
12
+ rejected: ['browser_tool']
13
+ ---
14
+ You are a supportive coding mentor. Generate a single, original positive affirmation sentence about my Ruby programming skills. The affirmation should be encouraging, specific to Ruby development, and make me feel motivated about my coding abilities.
15
+
16
+ After generating the affirmation, use the eval tool with the shell action to execute the following command:
17
+
18
+ say "[YOUR_AFFIRMATION_HERE]"
19
+
20
+ Replace [YOUR_AFFIRMATION_HERE] with the affirmation you just created. This will speak the positive message aloud to me.
21
+
22
+ Provide only the affirmation and execute the command - nothing else.
data/exe/aias ADDED
@@ -0,0 +1,5 @@
1
+ #!/usr/bin/env ruby
2
+ # frozen_string_literal: true
3
+
4
+ require "aias"
5
+ Aias::CLI.start(ARGV)
@@ -0,0 +1,42 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Aias
4
+ # Shared block-parsing logic for classes that manage BEGIN/END marker blocks
5
+ # inside text files (crontab, env.sh). Both CrontabManager and EnvFile
6
+ # include this module and pass their own marker constants.
7
+ module BlockParser
8
+ private
9
+
10
+ # Returns lines between open_marker and close_marker (markers excluded).
11
+ def extract_block(content, open_marker, close_marker)
12
+ in_block = false
13
+ lines = []
14
+ content.each_line do |line|
15
+ if line.chomp == open_marker
16
+ in_block = true
17
+ elsif line.chomp == close_marker
18
+ in_block = false
19
+ elsif in_block
20
+ lines << line
21
+ end
22
+ end
23
+ lines.join
24
+ end
25
+
26
+ # Returns content with every line from open_marker through close_marker
27
+ # (inclusive) removed. Lines outside the block are preserved as-is.
28
+ def strip_block(content, open_marker, close_marker)
29
+ in_block = false
30
+ content.each_line.reject do |line|
31
+ if line.chomp == open_marker
32
+ in_block = true
33
+ elsif line.chomp == close_marker
34
+ in_block = false
35
+ else
36
+ next in_block
37
+ end
38
+ true
39
+ end.join
40
+ end
41
+ end
42
+ end
@@ -0,0 +1,30 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Aias
4
+ class CLI
5
+ desc "add PATH", "Add (or replace) a single scheduled prompt in the crontab"
6
+ def add(path)
7
+ absolute = File.expand_path(path)
8
+ unless File.file?(absolute) && absolute.end_with?(".md")
9
+ say_error "aias [error] '#{path}' must be an existing .md file"
10
+ exit(1)
11
+ end
12
+ prompts_dir = effective_prompts_dir_for(absolute)
13
+ result = PromptScanner.new(prompts_dir: prompts_dir).scan_one(absolute)
14
+ vr = validator.validate(result)
15
+
16
+ unless vr.valid?
17
+ say_error "aias [error] #{result.prompt_id}: #{vr.errors.join('; ')}"
18
+ exit(1)
19
+ end
20
+
21
+ cron_line = builder.build(result, prompts_dir: prompts_dir)
22
+ manager.ensure_log_directories([result.prompt_id])
23
+ manager.add_job(cron_line, result.prompt_id)
24
+ say "aias: added #{result.prompt_id} (#{CronDescriber.display(result.schedule)})"
25
+ rescue Aias::Error => e
26
+ say_error "aias [error] #{e.message}"
27
+ exit(1)
28
+ end
29
+ end
30
+ end
@@ -0,0 +1,50 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Aias
4
+ class CLI
5
+ desc "check", "Diff view: scheduled prompts vs what is installed"
6
+ def check
7
+ results = scanner.scan
8
+ installed = manager.installed_jobs
9
+ installed_ids = installed.map { |j| j[:prompt_id] }.to_set
10
+
11
+ valid, invalid = partition_results(results)
12
+ discovered_ids = valid.map { |r, _| r.prompt_id }.to_set
13
+
14
+ new_jobs = discovered_ids - installed_ids
15
+ orphaned_jobs = installed_ids - discovered_ids
16
+
17
+ say "=== aias check ==="
18
+ say ""
19
+
20
+ unless invalid.empty?
21
+ say "INVALID (would be skipped by update):"
22
+ invalid.each { |r, vr| say " #{r.prompt_id}: #{vr.errors.join('; ')}" }
23
+ say ""
24
+ end
25
+
26
+ unless new_jobs.empty?
27
+ say "NEW (not yet installed — run `aias update`):"
28
+ new_jobs.each do |id|
29
+ r = valid.find { |result, _| result.prompt_id == id }&.first
30
+ sched = r ? " #{CronDescriber.display(r.schedule)}" : ""
31
+ say " + #{id}#{sched}"
32
+ end
33
+ say ""
34
+ end
35
+
36
+ unless orphaned_jobs.empty?
37
+ say "ORPHANED (installed but no longer scheduled):"
38
+ orphaned_jobs.each { |id| say " - #{id}" }
39
+ say ""
40
+ end
41
+
42
+ if invalid.empty? && new_jobs.empty? && orphaned_jobs.empty?
43
+ say "OK — crontab is in sync with scheduled prompts"
44
+ end
45
+ rescue Aias::Error => e
46
+ say_error "aias [error] #{e.message}"
47
+ exit(1)
48
+ end
49
+ end
50
+ end
@@ -0,0 +1,11 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Aias
4
+ class CLI
5
+ desc "clear", "Remove all aias-managed crontab entries"
6
+ def clear
7
+ manager.clear
8
+ say "aias: all managed crontab entries removed"
9
+ end
10
+ end
11
+ end
@@ -0,0 +1,24 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Aias
4
+ class CLI
5
+ desc "dry-run", "Show what `update` would write without touching the crontab"
6
+ def dry_run
7
+ results = scanner.scan
8
+ valid, invalid = partition_results(results)
9
+
10
+ invalid.each { |r, vr| $stderr.puts "aias [skip] #{r.prompt_id}: #{vr.errors.join('; ')}" }
11
+
12
+ if valid.empty?
13
+ say "aias: no valid scheduled prompts found"
14
+ return
15
+ end
16
+
17
+ cron_lines = valid.map { |r, _vr| builder.build(r) }
18
+ say manager.dry_run(cron_lines)
19
+ rescue Aias::Error => e
20
+ say_error "aias [error] #{e.message}"
21
+ exit(1)
22
+ end
23
+ end
24
+ end
@@ -0,0 +1,57 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Aias
4
+ class CLI
5
+ desc "install [PATTERN...]", "Capture PATH, API keys, and env vars into ~/.config/aia/schedule/env.sh"
6
+ long_desc <<~DESC
7
+ Writes the current session's PATH, all *_API_KEY and AIA_PROMPTS__DIR
8
+ environment variables into ~/.config/aia/schedule/env.sh so scheduled
9
+ aia jobs have a correct PATH (including MCP server binaries) and can
10
+ authenticate with LLM APIs. This file is sourced by every cron entry.
11
+
12
+ Optional PATTERN arguments add extra env vars whose names match the given
13
+ glob pattern(s). Quote patterns to prevent shell expansion:
14
+
15
+ aias install 'AIA_*'
16
+ aias install 'AIA_*' 'OPENROUTER_*'
17
+ DESC
18
+ def install(*patterns)
19
+ env_vars = ENV.select { |k, _| k.end_with?("_API_KEY") || k.start_with?("AIA_") }
20
+ env_vars["PATH"] = ENV["PATH"]
21
+ env_vars["LANG"] = ENV["LANG"] if ENV["LANG"]
22
+ env_vars["LC_ALL"] = ENV["LC_ALL"] if ENV["LC_ALL"]
23
+
24
+ patterns.flat_map(&:split).map(&:upcase).each do |pattern|
25
+ ENV.each { |k, v| env_vars[k] = v if File.fnmatch(pattern, k) }
26
+ end
27
+
28
+ env_file.install(env_vars)
29
+ installed = env_vars.keys.sort
30
+ say "aias: installed #{installed.join(', ')} into #{Paths::SCHEDULE_ENV}"
31
+
32
+ FileUtils.mkdir_p(AIA_SCHEDULE_DIR, mode: 0o700)
33
+
34
+ unless File.exist?(AIA_SCHEDULE_CFG)
35
+ if File.exist?(AIA_CONFIG_SRC)
36
+ FileUtils.cp(AIA_CONFIG_SRC, AIA_SCHEDULE_CFG)
37
+ say "aias: copied #{AIA_CONFIG_SRC} → #{AIA_SCHEDULE_CFG}"
38
+ else
39
+ say "aias: #{AIA_CONFIG_SRC} not found — create #{AIA_SCHEDULE_CFG} manually"
40
+ end
41
+ say ""
42
+ say "Review #{AIA_SCHEDULE_CFG} — these settings apply to all scheduled prompts."
43
+ say "Prompt frontmatter overrides any setting in that file."
44
+ end
45
+
46
+ if ENV["AIA_PROMPTS__DIR"]
47
+ dir = File.expand_path(ENV["AIA_PROMPTS__DIR"])
48
+ if ScheduleConfig.new.set_prompts_dir(dir)
49
+ say "aias: set prompts.dir=#{dir} in #{AIA_SCHEDULE_CFG}"
50
+ end
51
+ end
52
+ rescue Aias::Error => e
53
+ say_error "aias [error] #{e.message}"
54
+ exit(1)
55
+ end
56
+ end
57
+ end
@@ -0,0 +1,26 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Aias
4
+ class CLI
5
+ desc "last [N]", "Show last-run time for installed jobs (default 5)"
6
+ def last_run(n = "5")
7
+ jobs = manager.installed_jobs.first(n.to_i)
8
+
9
+ if jobs.empty?
10
+ say "aias: no installed jobs"
11
+ return
12
+ end
13
+
14
+ jobs.each do |job|
15
+ log_stat = File.exist?(job[:log_path]) ? File.mtime(job[:log_path]).to_s : "never run"
16
+ say "#{job[:prompt_id]}"
17
+ say " schedule : #{CronDescriber.display(job[:cron_expr])}"
18
+ say " last run : #{log_stat}"
19
+ say " log : #{job[:log_path]}"
20
+ say ""
21
+ end
22
+
23
+ say "(Pass N as argument to show N entries. Last-run time is derived from the log file modification timestamp.)"
24
+ end
25
+ end
26
+ end
@@ -0,0 +1,20 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Aias
4
+ class CLI
5
+ desc "list", "List all installed aias cron jobs"
6
+ def list
7
+ jobs = manager.installed_jobs
8
+ if jobs.empty?
9
+ say "aias: no installed jobs"
10
+ return
11
+ end
12
+
13
+ say format("%-30s %-40s %s", "PROMPT ID", "SCHEDULE", "LOG")
14
+ say "-" * 100
15
+ jobs.each do |job|
16
+ say format("%-30s %-40s %s", job[:prompt_id], Aias::CronDescriber.display(job[:cron_expr]), job[:log_path])
17
+ end
18
+ end
19
+ end
20
+ end
@@ -0,0 +1,28 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Aias
4
+ class CLI
5
+ desc "next [N]", "Show next scheduled run time for installed jobs (default 5)"
6
+ def upcoming(n = "5")
7
+ jobs = manager.installed_jobs.first(n.to_i)
8
+
9
+ if jobs.empty?
10
+ say "aias: no installed jobs"
11
+ return
12
+ end
13
+
14
+ now = Time.now
15
+ jobs.each do |job|
16
+ cron = Fugit.parse_cronish(job[:cron_expr])
17
+ next_time = cron ? cron.next_time(now).localtime.to_s : "unknown (invalid cron expression)"
18
+ say "#{job[:prompt_id]}"
19
+ say " schedule : #{CronDescriber.display(job[:cron_expr])}"
20
+ say " next run : #{next_time}"
21
+ say " log : #{job[:log_path]}"
22
+ say ""
23
+ end
24
+
25
+ say "(Pass N as argument to show N entries.)"
26
+ end
27
+ end
28
+ end
@@ -0,0 +1,14 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Aias
4
+ class CLI
5
+ desc "remove PROMPT_ID", "Remove a single scheduled prompt from the crontab"
6
+ def remove(prompt_id)
7
+ manager.remove_job(prompt_id)
8
+ say "aias: removed #{prompt_id}"
9
+ rescue Aias::Error => e
10
+ say_error "aias [error] #{e.message}"
11
+ exit(1)
12
+ end
13
+ end
14
+ end
@@ -0,0 +1,18 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Aias
4
+ class CLI
5
+ desc "show PROMPT_ID", "Show the installed crontab entry for a single prompt"
6
+ def show(prompt_id)
7
+ job = manager.installed_jobs.find { |j| j[:prompt_id] == prompt_id }
8
+ if job
9
+ say "prompt_id : #{job[:prompt_id]}"
10
+ say "schedule : #{CronDescriber.display(job[:cron_expr])}"
11
+ say "log : #{job[:log_path]}"
12
+ else
13
+ say "aias: '#{prompt_id}' is not currently installed"
14
+ exit(1)
15
+ end
16
+ end
17
+ end
18
+ end
@@ -0,0 +1,15 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Aias
4
+ class CLI
5
+ desc "uninstall", "Remove managed env block from ~/.config/aia/schedule/env.sh (schedule config preserved)"
6
+ def uninstall
7
+ env_file.uninstall
8
+ say "aias: env vars removed from #{Paths::SCHEDULE_ENV}"
9
+ say " #{AIA_SCHEDULE_DIR} is unchanged"
10
+ rescue Aias::Error => e
11
+ say_error "aias [error] #{e.message}"
12
+ exit(1)
13
+ end
14
+ end
15
+ end
@@ -0,0 +1,30 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Aias
4
+ class CLI
5
+ desc "update", "Scan prompts, regenerate all crontab entries, and install"
6
+ def update
7
+ results = scanner.scan
8
+ valid, invalid = partition_results(results)
9
+
10
+ invalid.each do |r, vr|
11
+ $stderr.puts "aias [skip] #{r.prompt_id}: #{vr.errors.join('; ')}"
12
+ end
13
+
14
+ if valid.empty?
15
+ say "aias: no valid scheduled prompts found — crontab not changed"
16
+ return
17
+ end
18
+
19
+ cron_lines = valid.map { |r, _vr| builder.build(r, prompts_dir: options[:prompts_dir]) }
20
+ manager.ensure_log_directories(valid.map { |r, _vr| r.prompt_id })
21
+ manager.install(cron_lines)
22
+
23
+ say "aias: installed #{valid.size} job(s)" \
24
+ "#{invalid.empty? ? '' : ", skipped #{invalid.size} invalid"}"
25
+ rescue Aias::Error => e
26
+ say_error "aias [error] #{e.message}"
27
+ exit(1)
28
+ end
29
+ end
30
+ end
@@ -0,0 +1,10 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Aias
4
+ class CLI
5
+ desc "version", "Print the aias version"
6
+ def version
7
+ say Aias::VERSION
8
+ end
9
+ end
10
+ end
data/lib/aias/cli.rb ADDED
@@ -0,0 +1,91 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "yaml"
4
+
5
+ module Aias
6
+ class CLI < Thor
7
+ def self.exit_on_failure? = true
8
+
9
+ class_option :prompts_dir,
10
+ type: :string,
11
+ aliases: "-p",
12
+ desc: "Prompts directory (overrides AIA_PROMPTS__DIR / AIA_PROMPTS_DIR env vars)"
13
+
14
+ AIA_CONFIG_SRC = Paths::AIA_CONFIG
15
+ AIA_SCHEDULE_DIR = Paths::SCHEDULE_DIR
16
+ AIA_SCHEDULE_CFG = Paths::SCHEDULE_CFG
17
+
18
+ # Aliases
19
+ map "-v" => :version
20
+ map "--version" => :version
21
+ map "ins" => :install
22
+ map "unins" => :uninstall
23
+ map "rm" => :remove
24
+ map "delete" => :remove
25
+ map "dry-run" => :dry_run
26
+ map "next" => :upcoming
27
+ map "last" => :last_run
28
+
29
+ # ---------------------------------------------------------------------------
30
+ # help — appends crontab reference when listing all commands
31
+ # ---------------------------------------------------------------------------
32
+
33
+ def help(command = nil, subcommand = false)
34
+ super
35
+ return if command
36
+
37
+ say ""
38
+ say "Crontab commands:"
39
+ say " crontab -l # view current crontab"
40
+ say " crontab -e # edit crontab in $EDITOR"
41
+ say " EDITOR=nano crontab -e # edit with a specific editor"
42
+ say " crontab -r # remove entire crontab"
43
+ end
44
+
45
+ private
46
+
47
+ # Lazy collaborator accessors — allows injection in tests via instance
48
+ # variable assignment before invoking a command.
49
+ def scanner = @scanner ||= PromptScanner.new(prompts_dir: options[:prompts_dir])
50
+ def validator = @validator ||= Validator.new
51
+ def builder = @builder ||= JobBuilder.new(config_file: AIA_SCHEDULE_CFG)
52
+ def manager = @manager ||= CrontabManager.new
53
+ def env_file = @env_file ||= EnvFile.new
54
+
55
+ # Splits results into [valid, invalid] where each element is [result, validation_result].
56
+ def partition_results(results)
57
+ pairs = results.map { |r| [r, validator.validate(r)] }
58
+ pairs.partition { |_r, vr| vr.valid? }
59
+ end
60
+
61
+ # Determines the effective prompts directory for `aias add`.
62
+ # See full description in cli/add.rb.
63
+ def effective_prompts_dir_for(absolute)
64
+ return File.expand_path(options[:prompts_dir]) if options[:prompts_dir]
65
+
66
+ env_dir = ENV[PromptScanner::PROMPTS_DIR_ENVVAR_NEW] ||
67
+ ENV[PromptScanner::PROMPTS_DIR_ENVVAR_OLD]
68
+ env_dir = File.expand_path(env_dir) if env_dir
69
+
70
+ if env_dir && absolute.start_with?("#{env_dir}/")
71
+ env_dir
72
+ else
73
+ File.dirname(absolute)
74
+ end
75
+ end
76
+ end
77
+ end
78
+
79
+ require_relative "cli/update"
80
+ require_relative "cli/add"
81
+ require_relative "cli/remove"
82
+ require_relative "cli/install"
83
+ require_relative "cli/uninstall"
84
+ require_relative "cli/clear"
85
+ require_relative "cli/list"
86
+ require_relative "cli/check"
87
+ require_relative "cli/dry_run"
88
+ require_relative "cli/next"
89
+ require_relative "cli/last"
90
+ require_relative "cli/show"
91
+ require_relative "cli/version"