jekyll-theme-zer0 0.22.20 โ 0.22.22
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.
- checksums.yaml +4 -4
- data/CHANGELOG.md +74 -4
- data/README.md +325 -40
- data/_data/README.md +1 -0
- data/_data/roadmap.yml +215 -0
- data/scripts/bin/install +717 -0
- data/scripts/bin/test +45 -2
- data/scripts/generate-roadmap.rb +200 -0
- data/scripts/generate-roadmap.sh +21 -0
- data/scripts/lib/install/README.md +63 -0
- data/scripts/lib/install/agents.sh +166 -0
- data/scripts/lib/install/ai/diagnose.sh +199 -0
- data/scripts/lib/install/ai/openai.sh +233 -0
- data/scripts/lib/install/ai/suggest.sh +182 -0
- data/scripts/lib/install/ai/wizard.sh +160 -0
- data/scripts/lib/install/config.sh +56 -0
- data/scripts/lib/install/deploy/README.md +52 -0
- data/scripts/lib/install/deploy/azure-swa.sh +50 -0
- data/scripts/lib/install/deploy/docker-prod.sh +71 -0
- data/scripts/lib/install/deploy/github-pages.sh +44 -0
- data/scripts/lib/install/deploy/registry.sh +190 -0
- data/scripts/lib/install/doctor.sh +301 -0
- data/scripts/lib/install/fs.sh +52 -0
- data/scripts/lib/install/logging.sh +33 -0
- data/scripts/lib/install/pages.sh +255 -0
- data/scripts/lib/install/platform.sh +71 -0
- data/scripts/lib/install/profile.sh +113 -0
- data/scripts/lib/install/template.sh +137 -0
- data/scripts/lib/install/upgrade.sh +184 -0
- data/scripts/lib/install/wizard_interactive.sh +189 -0
- metadata +27 -2
data/scripts/bin/test
CHANGED
|
@@ -30,11 +30,12 @@ show_usage() {
|
|
|
30
30
|
USAGE:
|
|
31
31
|
./scripts/bin/test [OPTIONS] [TEST_SUITE]
|
|
32
32
|
|
|
33
|
-
|
|
33
|
+
TEST_SUITES:
|
|
34
34
|
all Run all tests (default)
|
|
35
35
|
lib Run library unit tests only
|
|
36
36
|
theme Run theme validation tests only
|
|
37
37
|
integration Run integration tests only
|
|
38
|
+
install Run installer e2e suites only (test/test_install_*.sh)
|
|
38
39
|
|
|
39
40
|
OPTIONS:
|
|
40
41
|
--verbose, -v Show detailed test output
|
|
@@ -67,7 +68,7 @@ for arg in "$@"; do
|
|
|
67
68
|
--dry-run)
|
|
68
69
|
export DRY_RUN=true
|
|
69
70
|
;;
|
|
70
|
-
all|lib|theme|integration)
|
|
71
|
+
all|lib|theme|integration|install)
|
|
71
72
|
TEST_SUITE="$arg"
|
|
72
73
|
;;
|
|
73
74
|
*)
|
|
@@ -161,6 +162,43 @@ run_integration_tests() {
|
|
|
161
162
|
fi
|
|
162
163
|
}
|
|
163
164
|
|
|
165
|
+
# Run installer e2e tests (test/test_install_*.sh in repo root)
|
|
166
|
+
run_install_tests() {
|
|
167
|
+
log_info "Running installer e2e tests..."
|
|
168
|
+
|
|
169
|
+
# Repo root is two levels up from scripts/bin/
|
|
170
|
+
local repo_root
|
|
171
|
+
repo_root="$(cd "$SCRIPTS_ROOT/.." && pwd)"
|
|
172
|
+
local install_test_dir="$repo_root/test"
|
|
173
|
+
|
|
174
|
+
if [[ ! -d "$install_test_dir" ]]; then
|
|
175
|
+
log_warning "Installer test dir not found: $install_test_dir"
|
|
176
|
+
return
|
|
177
|
+
fi
|
|
178
|
+
|
|
179
|
+
local found=false
|
|
180
|
+
for test_file in "$install_test_dir"/test_install_*.sh; do
|
|
181
|
+
[[ -f "$test_file" ]] || continue
|
|
182
|
+
found=true
|
|
183
|
+
local test_name
|
|
184
|
+
test_name=$(basename "$test_file")
|
|
185
|
+
log_info "Running installer suite: $test_name"
|
|
186
|
+
|
|
187
|
+
if bash "$test_file"; then
|
|
188
|
+
log_success "$test_name passed"
|
|
189
|
+
((TOTAL_PASSED++)) || true
|
|
190
|
+
else
|
|
191
|
+
log_error "$test_name failed"
|
|
192
|
+
((TOTAL_FAILED++)) || true
|
|
193
|
+
fi
|
|
194
|
+
((TOTAL_TESTS++)) || true
|
|
195
|
+
done
|
|
196
|
+
|
|
197
|
+
if [[ "$found" == "false" ]]; then
|
|
198
|
+
log_warning "No installer test suites found (test_install_*.sh)"
|
|
199
|
+
fi
|
|
200
|
+
}
|
|
201
|
+
|
|
164
202
|
# Main execution
|
|
165
203
|
log_info "๐งช Starting test suite: $TEST_SUITE"
|
|
166
204
|
echo ""
|
|
@@ -172,6 +210,8 @@ case $TEST_SUITE in
|
|
|
172
210
|
run_theme_tests
|
|
173
211
|
echo ""
|
|
174
212
|
run_integration_tests
|
|
213
|
+
echo ""
|
|
214
|
+
run_install_tests
|
|
175
215
|
;;
|
|
176
216
|
lib)
|
|
177
217
|
run_lib_tests
|
|
@@ -182,6 +222,9 @@ case $TEST_SUITE in
|
|
|
182
222
|
integration)
|
|
183
223
|
run_integration_tests
|
|
184
224
|
;;
|
|
225
|
+
install)
|
|
226
|
+
run_install_tests
|
|
227
|
+
;;
|
|
185
228
|
esac
|
|
186
229
|
|
|
187
230
|
# Summary
|
|
@@ -0,0 +1,200 @@
|
|
|
1
|
+
#!/usr/bin/env ruby
|
|
2
|
+
# frozen_string_literal: true
|
|
3
|
+
|
|
4
|
+
# =============================================================================
|
|
5
|
+
# generate-roadmap.rb
|
|
6
|
+
# =============================================================================
|
|
7
|
+
#
|
|
8
|
+
# Reads `_data/roadmap.yml` and updates the README.md roadmap section in-place.
|
|
9
|
+
#
|
|
10
|
+
# It rewrites two regions delimited by HTML comment markers:
|
|
11
|
+
#
|
|
12
|
+
# <!-- ROADMAP_MERMAID:START --> ... <!-- ROADMAP_MERMAID:END -->
|
|
13
|
+
# <!-- ROADMAP_TABLE:START --> ... <!-- ROADMAP_TABLE:END -->
|
|
14
|
+
#
|
|
15
|
+
# Usage:
|
|
16
|
+
# ruby scripts/generate-roadmap.rb # update README.md in place
|
|
17
|
+
# ruby scripts/generate-roadmap.rb --check # exit non-zero if README is stale
|
|
18
|
+
# ruby scripts/generate-roadmap.rb --stdout # print regenerated sections only
|
|
19
|
+
#
|
|
20
|
+
# This script has no gem dependencies beyond the Ruby stdlib.
|
|
21
|
+
# =============================================================================
|
|
22
|
+
|
|
23
|
+
require 'yaml'
|
|
24
|
+
require 'date'
|
|
25
|
+
require 'optparse'
|
|
26
|
+
|
|
27
|
+
ROOT = File.expand_path('..', __dir__)
|
|
28
|
+
DATA_FILE = File.join(ROOT, '_data', 'roadmap.yml')
|
|
29
|
+
README = File.join(ROOT, 'README.md')
|
|
30
|
+
|
|
31
|
+
MERMAID_START = '<!-- ROADMAP_MERMAID:START -->'
|
|
32
|
+
MERMAID_END = '<!-- ROADMAP_MERMAID:END -->'
|
|
33
|
+
TABLE_START = '<!-- ROADMAP_TABLE:START -->'
|
|
34
|
+
TABLE_END = '<!-- ROADMAP_TABLE:END -->'
|
|
35
|
+
|
|
36
|
+
# Width used to right-pad the gantt label column so the `:status, start, end`
|
|
37
|
+
# tail aligns vertically. Adjust if very long version/title combinations show up.
|
|
38
|
+
GANTT_LABEL_WIDTH = 28
|
|
39
|
+
|
|
40
|
+
# ---------------------------------------------------------------------------
|
|
41
|
+
# Rendering helpers
|
|
42
|
+
# ---------------------------------------------------------------------------
|
|
43
|
+
|
|
44
|
+
# Mermaid gantt task line for a single milestone.
|
|
45
|
+
#
|
|
46
|
+
# v0.22 AIEO Optimization :active, 2026-03, 2026-04
|
|
47
|
+
# v1.0 Stable Release :milestone, 2027-01, 1d
|
|
48
|
+
#
|
|
49
|
+
def gantt_task(milestone)
|
|
50
|
+
label = "v#{milestone['version']} #{milestone['title']}"
|
|
51
|
+
status = milestone['status'].to_s
|
|
52
|
+
start = milestone['start']
|
|
53
|
+
finish = milestone['end'] || milestone['start']
|
|
54
|
+
|
|
55
|
+
prefix =
|
|
56
|
+
case status
|
|
57
|
+
when 'completed' then 'done, '
|
|
58
|
+
when 'active' then 'active, '
|
|
59
|
+
when 'milestone' then 'milestone, '
|
|
60
|
+
else ''
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
range = status == 'milestone' ? "#{start}, 1d" : "#{start}, #{finish}"
|
|
64
|
+
" #{label.ljust(GANTT_LABEL_WIDTH)} :#{prefix}#{range}"
|
|
65
|
+
end
|
|
66
|
+
|
|
67
|
+
def render_mermaid(data)
|
|
68
|
+
title = data.dig('meta', 'title') || 'zer0-mistakes Roadmap'
|
|
69
|
+
milestones = data['milestones'] || []
|
|
70
|
+
|
|
71
|
+
# Group by section while preserving the order in which sections first appear.
|
|
72
|
+
sections = milestones.group_by { |m| m['section'] || 'Roadmap' }
|
|
73
|
+
ordered = milestones.map { |m| m['section'] }.uniq
|
|
74
|
+
|
|
75
|
+
lines = []
|
|
76
|
+
lines << '```mermaid'
|
|
77
|
+
lines << 'gantt'
|
|
78
|
+
lines << " title #{title}"
|
|
79
|
+
lines << ' dateFormat YYYY-MM'
|
|
80
|
+
ordered.each do |section|
|
|
81
|
+
lines << " section #{section}"
|
|
82
|
+
sections[section].each { |m| lines << gantt_task(m) }
|
|
83
|
+
end
|
|
84
|
+
lines << '```'
|
|
85
|
+
lines.join("\n")
|
|
86
|
+
end
|
|
87
|
+
|
|
88
|
+
# Status โ human-readable target column for the summary table.
|
|
89
|
+
def target_label(milestone)
|
|
90
|
+
case milestone['status']
|
|
91
|
+
when 'completed'
|
|
92
|
+
if (released = milestone['released'])
|
|
93
|
+
Date.parse(released.to_s).strftime('%b %Y')
|
|
94
|
+
else
|
|
95
|
+
'Completed'
|
|
96
|
+
end
|
|
97
|
+
when 'active'
|
|
98
|
+
milestone['target'] || 'In progress'
|
|
99
|
+
when 'milestone', 'planned'
|
|
100
|
+
milestone['target'] || milestone['start']
|
|
101
|
+
else
|
|
102
|
+
milestone['target'] || ''
|
|
103
|
+
end
|
|
104
|
+
end
|
|
105
|
+
|
|
106
|
+
def render_table(data)
|
|
107
|
+
rows = (data['milestones'] || []).map do |m|
|
|
108
|
+
version = "**v#{m['version']}**"
|
|
109
|
+
target = target_label(m)
|
|
110
|
+
summary = m['summary'] || ''
|
|
111
|
+
status_emoji =
|
|
112
|
+
case m['status']
|
|
113
|
+
when 'completed' then 'โ
Completed'
|
|
114
|
+
when 'active' then '๐ง In Progress'
|
|
115
|
+
when 'milestone' then '๐ฏ Milestone'
|
|
116
|
+
else '๐ Planned'
|
|
117
|
+
end
|
|
118
|
+
"| #{version} | #{status_emoji} | #{target} | #{summary} |"
|
|
119
|
+
end
|
|
120
|
+
|
|
121
|
+
header = [
|
|
122
|
+
'| Version | Status | Target | Highlights |',
|
|
123
|
+
'|---------|--------|--------|------------|'
|
|
124
|
+
]
|
|
125
|
+
|
|
126
|
+
(header + rows).join("\n")
|
|
127
|
+
end
|
|
128
|
+
|
|
129
|
+
def replace_block(content, marker_start, marker_end, replacement)
|
|
130
|
+
pattern = /(#{Regexp.escape(marker_start)})(.*?)(#{Regexp.escape(marker_end)})/m
|
|
131
|
+
unless content.match?(pattern)
|
|
132
|
+
raise "Markers not found in README: #{marker_start} ... #{marker_end}"
|
|
133
|
+
end
|
|
134
|
+
|
|
135
|
+
# Surround the replacement with blank lines so kramdown parses the following
|
|
136
|
+
# markdown (especially tables) instead of treating it as part of the HTML
|
|
137
|
+
# comment block. Without the blank line after `<!-- ... -->`, GFM tables
|
|
138
|
+
# collapse into a single paragraph.
|
|
139
|
+
content.sub(pattern, "\\1\n\n#{replacement}\n\n\\3")
|
|
140
|
+
end
|
|
141
|
+
|
|
142
|
+
# ---------------------------------------------------------------------------
|
|
143
|
+
# Main
|
|
144
|
+
# ---------------------------------------------------------------------------
|
|
145
|
+
|
|
146
|
+
def main
|
|
147
|
+
options = { mode: :write }
|
|
148
|
+
OptionParser.new do |opts|
|
|
149
|
+
opts.banner = 'Usage: generate-roadmap.rb [--check|--stdout]'
|
|
150
|
+
opts.on('--check', 'Exit non-zero if README would change') { options[:mode] = :check }
|
|
151
|
+
opts.on('--stdout', 'Print regenerated sections to stdout') { options[:mode] = :stdout }
|
|
152
|
+
end.parse!
|
|
153
|
+
|
|
154
|
+
# The roadmap data only contains scalars and Date values; Symbol is not used,
|
|
155
|
+
# but Date/Time must be permitted because YAML's safe loader rejects them by default.
|
|
156
|
+
# Ruby >= 3.1 supports `permitted_classes:` on `YAML.load_file`. On older Rubies
|
|
157
|
+
# (e.g. macOS system Ruby 2.6), fall back to `safe_load` which accepted the
|
|
158
|
+
# keyword earlier, so the generator works for contributors without rbenv/rvm.
|
|
159
|
+
data =
|
|
160
|
+
begin
|
|
161
|
+
YAML.load_file(DATA_FILE, permitted_classes: [Date, Time])
|
|
162
|
+
rescue ArgumentError
|
|
163
|
+
YAML.safe_load(File.read(DATA_FILE), permitted_classes: [Date, Time], aliases: false)
|
|
164
|
+
end
|
|
165
|
+
mermaid = render_mermaid(data)
|
|
166
|
+
table = render_table(data)
|
|
167
|
+
|
|
168
|
+
if options[:mode] == :stdout
|
|
169
|
+
puts mermaid
|
|
170
|
+
puts
|
|
171
|
+
puts table
|
|
172
|
+
return 0
|
|
173
|
+
end
|
|
174
|
+
|
|
175
|
+
original = File.read(README)
|
|
176
|
+
updated = original.dup
|
|
177
|
+
updated = replace_block(updated, MERMAID_START, MERMAID_END, mermaid)
|
|
178
|
+
updated = replace_block(updated, TABLE_START, TABLE_END, table)
|
|
179
|
+
|
|
180
|
+
if options[:mode] == :check
|
|
181
|
+
if original == updated
|
|
182
|
+
puts 'โ README.md roadmap section is up to date with _data/roadmap.yml'
|
|
183
|
+
return 0
|
|
184
|
+
else
|
|
185
|
+
warn 'โ README.md roadmap section is out of date with _data/roadmap.yml'
|
|
186
|
+
warn ' Run: ./scripts/generate-roadmap.sh'
|
|
187
|
+
return 1
|
|
188
|
+
end
|
|
189
|
+
end
|
|
190
|
+
|
|
191
|
+
if original == updated
|
|
192
|
+
puts 'README.md roadmap section already up to date.'
|
|
193
|
+
else
|
|
194
|
+
File.write(README, updated)
|
|
195
|
+
puts "Updated README.md roadmap section from #{File.basename(DATA_FILE)}."
|
|
196
|
+
end
|
|
197
|
+
0
|
|
198
|
+
end
|
|
199
|
+
|
|
200
|
+
exit main if $PROGRAM_NAME == __FILE__
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
# =============================================================================
|
|
3
|
+
# generate-roadmap.sh
|
|
4
|
+
# =============================================================================
|
|
5
|
+
#
|
|
6
|
+
# Thin wrapper around scripts/generate-roadmap.rb.
|
|
7
|
+
#
|
|
8
|
+
# Reads `_data/roadmap.yml` and rewrites the auto-generated roadmap regions
|
|
9
|
+
# of README.md (Mermaid gantt diagram + summary table) in place.
|
|
10
|
+
#
|
|
11
|
+
# Usage:
|
|
12
|
+
# ./scripts/generate-roadmap.sh # update README.md
|
|
13
|
+
# ./scripts/generate-roadmap.sh --check # CI-friendly drift detection
|
|
14
|
+
# ./scripts/generate-roadmap.sh --stdout # print regenerated sections only
|
|
15
|
+
#
|
|
16
|
+
# =============================================================================
|
|
17
|
+
|
|
18
|
+
set -euo pipefail
|
|
19
|
+
|
|
20
|
+
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
|
21
|
+
exec ruby "${SCRIPT_DIR}/generate-roadmap.rb" "$@"
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
# `scripts/lib/install/` โ Installer Library Modules
|
|
2
|
+
|
|
3
|
+
Focused modules sourced by [`install.sh`](../../../install.sh) at the repository root. Each module is self-contained and โค 200 lines for readability and reuse.
|
|
4
|
+
|
|
5
|
+
## Modules
|
|
6
|
+
|
|
7
|
+
| File | Purpose | Key Functions |
|
|
8
|
+
|------|---------|----------------|
|
|
9
|
+
| [`logging.sh`](logging.sh) | `log_info` / `log_success` / `log_warning` / `log_error` shim used throughout `install.sh`. | `log_info`, `log_success`, `log_warning`, `log_error` |
|
|
10
|
+
| [`platform.sh`](platform.sh) | OS, Ruby version, and platform detection (bash 3.2-compatible). | `detect_os`, `detect_ruby_version`, `ruby_version_lt_27`, `needs_macos_gemfile`, `detect_platform` |
|
|
11
|
+
| [`fs.sh`](fs.sh) | Idempotent file/directory copy with timestamped backups. | `copy_file_with_backup`, `copy_directory_with_backup` |
|
|
12
|
+
| [`template.sh`](template.sh) | `{{VAR}}` placeholder substitution + local/remote/fallback resolution. | `render_template`, `create_from_template`, `templates_available` |
|
|
13
|
+
| [`config.sh`](config.sh) | Loads `templates/config/install.conf` with hard-coded defaults as fallback. | `load_install_config` |
|
|
14
|
+
| [`pages.sh`](pages.sh) | Manifest-driven starter-page renderer. Replaces 8 legacy `create_*_page` heredoc functions with one driver. | `render_starter_pages`, `render_admin_settings_pages` (+ `create_starter_pages`/`create_admin_pages` aliases) |
|
|
15
|
+
| [`profile.sh`](profile.sh) | Pure-bash YAML reader for [`templates/profiles/*.yml`](../../../templates/profiles/). Bash 3.2 compatible (no yq/python). | `list_profile_names`, `profile_path`, `profile_get_scalar`, `profile_get_list`, `profile_print_summary` |
|
|
16
|
+
| [`deploy/`](deploy/) | Pluggable deployment-target modules (`github-pages`, `azure-swa`, `docker-prod`) with a uniform `check_prereqs`/`install`/`verify`/`doc_url` contract. | `deploy_run_target`, `deploy_print_summary`, `deploy_render`, `deploy_copy` (see [`deploy/README.md`](deploy/README.md)) |
|
|
17
|
+
|
|
18
|
+
## Loading
|
|
19
|
+
|
|
20
|
+
`install.sh` sources these modules when `scripts/lib/install/` is present. When it isn't (a stripped distribution or a `curl | bash` one-liner that didn't bundle the libraries), `install.sh` falls back to inlined copies of the same functions defined at the top of the script.
|
|
21
|
+
|
|
22
|
+
## CLI Entrypoint
|
|
23
|
+
|
|
24
|
+
For day-to-day use prefer the canonical dispatcher:
|
|
25
|
+
|
|
26
|
+
```bash
|
|
27
|
+
./scripts/bin/install help # subcommand index
|
|
28
|
+
./scripts/bin/install init # full install in CWD
|
|
29
|
+
./scripts/bin/install init --profile minimal /tmp/demo
|
|
30
|
+
./scripts/bin/install list-profiles # available profiles
|
|
31
|
+
./scripts/bin/install list-targets # available deploy targets
|
|
32
|
+
./scripts/bin/install deploy github-pages /tmp/demo
|
|
33
|
+
./scripts/bin/install deploy azure-swa,docker-prod /tmp/demo
|
|
34
|
+
./scripts/bin/install agents # AGENTS.md / instructions index
|
|
35
|
+
./scripts/bin/install version # theme version from version.rb
|
|
36
|
+
```
|
|
37
|
+
|
|
38
|
+
`init` translates `--profile` into the appropriate legacy flag and execs `install.sh`. `deploy` dispatches to the modules under `deploy/` (Phase 4). The remaining subcommands (`wizard`, `diagnose`, `doctor`, `upgrade`) are stubs that print a clear notice until their backing modules land in Phases 5-6.
|
|
39
|
+
|
|
40
|
+
## Roadmap
|
|
41
|
+
|
|
42
|
+
Phases 1, 1.5, 2, 3 (declarative profiles), and 4 (deploy modules) are complete. Future phases will add:
|
|
43
|
+
|
|
44
|
+
- `bootstrap.sh` โ remote install (`download_theme_files`, `cleanup_temp_dir`)
|
|
45
|
+
- `wizard.sh` โ interactive prompts (`gather_user_input`, `prompt_with_default`)
|
|
46
|
+
- `ai/{wizard,diagnose,suggest}.sh` โ opt-in AI integration
|
|
47
|
+
- `doctor.sh` โ pre-flight environment + site health checks
|
|
48
|
+
|
|
49
|
+
See the session refactor plan for the full sequence.
|
|
50
|
+
|
|
51
|
+
## Compatibility
|
|
52
|
+
|
|
53
|
+
All modules target **bash 3.2** (the macOS default `/bin/bash`). No `declare -A`, no `=~` capture groups, no `mapfile`/`readarray`.
|
|
54
|
+
|
|
55
|
+
## Conventions
|
|
56
|
+
|
|
57
|
+
- Every function documents its required globals at the top of the file
|
|
58
|
+
- No module calls `exit` โ caller decides; modules return non-zero on recoverable failure
|
|
59
|
+
- Modules don't `set -euo pipefail` themselves โ they inherit from the caller (`install.sh` already sets it)
|
|
60
|
+
|
|
61
|
+
---
|
|
62
|
+
|
|
63
|
+
**Last updated**: 2026-04-20 โ Phase 4 (`deploy/registry.sh` + `deploy/{github-pages,azure-swa,docker-prod}.sh` + `templates/deploy/`).
|
|
@@ -0,0 +1,166 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
# scripts/lib/install/agents.sh
|
|
3
|
+
#
|
|
4
|
+
# `install agents` โ copy AI agent guidance files into a target site.
|
|
5
|
+
#
|
|
6
|
+
# Pure file copy. No network. No AI calls. Source of truth is the theme
|
|
7
|
+
# repo's own .github/ + AGENTS.md, so the theme is self-canonical:
|
|
8
|
+
# whatever the theme dogfoods is what consumers receive.
|
|
9
|
+
#
|
|
10
|
+
# Public API:
|
|
11
|
+
# agents_install <target_dir> <repo_root> [--cursor] [--claude] [--aider] [--force]
|
|
12
|
+
#
|
|
13
|
+
# Always installs the core set (AGENTS.md + .github/copilot-instructions.md
|
|
14
|
+
# + .github/instructions/ + .github/prompts/). Optional flags add:
|
|
15
|
+
# --cursor .cursor/commands/*.md (mirrors prompts as slash commands)
|
|
16
|
+
# --claude CLAUDE.md stub pointing to AGENTS.md
|
|
17
|
+
# --aider .aider.conf.yml referencing AGENTS.md as read-only context
|
|
18
|
+
#
|
|
19
|
+
# Idempotent: skips files that already exist unless --force.
|
|
20
|
+
|
|
21
|
+
# shellcheck disable=SC2034 # script intended to be sourced
|
|
22
|
+
AGENTS_LIB_VERSION="1.0.0"
|
|
23
|
+
|
|
24
|
+
# Copy a single file with skip/force semantics. Returns 0 on copy, 1 on skip.
|
|
25
|
+
_agents_copy_file() {
|
|
26
|
+
local src="$1" dst="$2" force="$3"
|
|
27
|
+
if [[ ! -f "$src" ]]; then
|
|
28
|
+
log_warning "Source not found, skipping: ${src#$REPO_ROOT/}"
|
|
29
|
+
return 1
|
|
30
|
+
fi
|
|
31
|
+
if [[ -f "$dst" ]] && [[ "$force" != "1" ]]; then
|
|
32
|
+
log_warning "Exists, skipping: ${dst#$PWD/} (use --force to overwrite)"
|
|
33
|
+
return 1
|
|
34
|
+
fi
|
|
35
|
+
mkdir -p "$(dirname "$dst")"
|
|
36
|
+
cp "$src" "$dst"
|
|
37
|
+
log_success "Wrote ${dst#$PWD/}"
|
|
38
|
+
return 0
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
# Copy every file in a source dir matching a glob into a dest dir.
|
|
42
|
+
_agents_copy_glob() {
|
|
43
|
+
local src_dir="$1" pattern="$2" dst_dir="$3" force="$4"
|
|
44
|
+
local copied=0 skipped=0
|
|
45
|
+
if [[ ! -d "$src_dir" ]]; then
|
|
46
|
+
log_warning "Source dir not found: ${src_dir#$REPO_ROOT/}"
|
|
47
|
+
return 0
|
|
48
|
+
fi
|
|
49
|
+
local f base
|
|
50
|
+
for f in "$src_dir"/$pattern; do
|
|
51
|
+
[[ -f "$f" ]] || continue
|
|
52
|
+
base="$(basename "$f")"
|
|
53
|
+
if _agents_copy_file "$f" "$dst_dir/$base" "$force"; then
|
|
54
|
+
copied=$((copied+1))
|
|
55
|
+
else
|
|
56
|
+
skipped=$((skipped+1))
|
|
57
|
+
fi
|
|
58
|
+
done
|
|
59
|
+
log_info " โ $copied copied, $skipped skipped in ${dst_dir#$PWD/}"
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
agents_install() {
|
|
63
|
+
local target_dir="$1" repo_root="$2"
|
|
64
|
+
shift 2 || true
|
|
65
|
+
|
|
66
|
+
local with_cursor=0 with_claude=0 with_aider=0 force=0
|
|
67
|
+
while [[ $# -gt 0 ]]; do
|
|
68
|
+
case "$1" in
|
|
69
|
+
--cursor) with_cursor=1 ;;
|
|
70
|
+
--claude) with_claude=1 ;;
|
|
71
|
+
--aider) with_aider=1 ;;
|
|
72
|
+
--all) with_cursor=1; with_claude=1; with_aider=1 ;;
|
|
73
|
+
-f|--force) force=1 ;;
|
|
74
|
+
*) log_warning "agents_install: ignoring unknown flag: $1" ;;
|
|
75
|
+
esac
|
|
76
|
+
shift
|
|
77
|
+
done
|
|
78
|
+
|
|
79
|
+
if [[ ! -d "$target_dir" ]]; then
|
|
80
|
+
log_error "Target directory does not exist: $target_dir"
|
|
81
|
+
return 1
|
|
82
|
+
fi
|
|
83
|
+
|
|
84
|
+
log_info "Installing AI agent guidance into: $target_dir"
|
|
85
|
+
[[ "$force" = "1" ]] && log_info " (--force: overwrite enabled)"
|
|
86
|
+
|
|
87
|
+
# 1. Core: AGENTS.md (cross-tool entry point)
|
|
88
|
+
_agents_copy_file "$repo_root/AGENTS.md" "$target_dir/AGENTS.md" "$force" || true
|
|
89
|
+
|
|
90
|
+
# 2. Copilot main instructions
|
|
91
|
+
_agents_copy_file \
|
|
92
|
+
"$repo_root/.github/copilot-instructions.md" \
|
|
93
|
+
"$target_dir/.github/copilot-instructions.md" \
|
|
94
|
+
"$force" || true
|
|
95
|
+
|
|
96
|
+
# 3. File-scoped instructions
|
|
97
|
+
log_info "Copying .github/instructions/*.md ..."
|
|
98
|
+
_agents_copy_glob \
|
|
99
|
+
"$repo_root/.github/instructions" "*.md" \
|
|
100
|
+
"$target_dir/.github/instructions" "$force"
|
|
101
|
+
|
|
102
|
+
# 4. Reusable prompts
|
|
103
|
+
log_info "Copying .github/prompts/*.md ..."
|
|
104
|
+
_agents_copy_glob \
|
|
105
|
+
"$repo_root/.github/prompts" "*.md" \
|
|
106
|
+
"$target_dir/.github/prompts" "$force"
|
|
107
|
+
|
|
108
|
+
# 5. Optional: Cursor slash-commands
|
|
109
|
+
if [[ "$with_cursor" = "1" ]]; then
|
|
110
|
+
log_info "Copying .cursor/commands/*.md ..."
|
|
111
|
+
_agents_copy_glob \
|
|
112
|
+
"$repo_root/.cursor/commands" "*.md" \
|
|
113
|
+
"$target_dir/.cursor/commands" "$force"
|
|
114
|
+
fi
|
|
115
|
+
|
|
116
|
+
# 6. Optional: Claude stub
|
|
117
|
+
if [[ "$with_claude" = "1" ]]; then
|
|
118
|
+
local claude_tpl="$repo_root/templates/agents/CLAUDE.md.template"
|
|
119
|
+
if [[ -f "$claude_tpl" ]]; then
|
|
120
|
+
_agents_copy_file "$claude_tpl" "$target_dir/CLAUDE.md" "$force" || true
|
|
121
|
+
else
|
|
122
|
+
cat > "$target_dir/CLAUDE.md.tmp" <<'EOF'
|
|
123
|
+
# Claude Code Instructions
|
|
124
|
+
|
|
125
|
+
This project uses [`AGENTS.md`](./AGENTS.md) as the single source of truth for
|
|
126
|
+
AI agent guidance. Please read it first.
|
|
127
|
+
|
|
128
|
+
For detailed conventions, see:
|
|
129
|
+
- `.github/copilot-instructions.md`
|
|
130
|
+
- `.github/instructions/*.instructions.md`
|
|
131
|
+
- `.github/prompts/*.prompt.md`
|
|
132
|
+
EOF
|
|
133
|
+
if [[ -f "$target_dir/CLAUDE.md" ]] && [[ "$force" != "1" ]]; then
|
|
134
|
+
rm -f "$target_dir/CLAUDE.md.tmp"
|
|
135
|
+
log_warning "Exists, skipping: CLAUDE.md (use --force to overwrite)"
|
|
136
|
+
else
|
|
137
|
+
mv "$target_dir/CLAUDE.md.tmp" "$target_dir/CLAUDE.md"
|
|
138
|
+
log_success "Wrote CLAUDE.md"
|
|
139
|
+
fi
|
|
140
|
+
fi
|
|
141
|
+
fi
|
|
142
|
+
|
|
143
|
+
# 7. Optional: Aider config
|
|
144
|
+
if [[ "$with_aider" = "1" ]]; then
|
|
145
|
+
local aider_tpl="$repo_root/templates/agents/aider.conf.yml.template"
|
|
146
|
+
if [[ -f "$aider_tpl" ]]; then
|
|
147
|
+
_agents_copy_file "$aider_tpl" "$target_dir/.aider.conf.yml" "$force" || true
|
|
148
|
+
else
|
|
149
|
+
if [[ -f "$target_dir/.aider.conf.yml" ]] && [[ "$force" != "1" ]]; then
|
|
150
|
+
log_warning "Exists, skipping: .aider.conf.yml (use --force to overwrite)"
|
|
151
|
+
else
|
|
152
|
+
cat > "$target_dir/.aider.conf.yml" <<'EOF'
|
|
153
|
+
# Aider configuration โ see https://aider.chat
|
|
154
|
+
# Loads project agent guidance as read-only context for every session.
|
|
155
|
+
read:
|
|
156
|
+
- AGENTS.md
|
|
157
|
+
- .github/copilot-instructions.md
|
|
158
|
+
EOF
|
|
159
|
+
log_success "Wrote .aider.conf.yml"
|
|
160
|
+
fi
|
|
161
|
+
fi
|
|
162
|
+
fi
|
|
163
|
+
|
|
164
|
+
log_success "Agent guidance installation complete."
|
|
165
|
+
return 0
|
|
166
|
+
}
|