repo-nexus 0.7.26 → 0.7.28

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 (29) hide show
  1. package/CHANGELOG.md +21 -0
  2. package/docs/CLI.md +1 -1
  3. package/docs/plugins/karpathy-llm.md +27 -5
  4. package/docs/rnex.example.yaml +5 -1
  5. package/lib/commands/init.sh +1 -9
  6. package/lib/commands/plugin.sh +4 -0
  7. package/lib/commands/status.sh +3 -0
  8. package/lib/hooks.sh +29 -8
  9. package/lib/plugins.sh +268 -15
  10. package/lib/repos.sh +3 -1
  11. package/lib/yaml.sh +172 -17
  12. package/package.json +1 -1
  13. package/templates/rnex.example.yaml +5 -1
  14. package/toolkit/plugins/copilot/hooks/after_sync.sh +4 -4
  15. package/toolkit/plugins/copilot/hooks/before_plugin_disable.sh +4 -4
  16. package/toolkit/plugins/karpathy-llm/README.md +8 -3
  17. package/toolkit/plugins/karpathy-llm/agents/rnex-wiki-curator.agent.md +1 -1
  18. package/toolkit/plugins/karpathy-llm/hooks/after_sync.sh +63 -8
  19. package/toolkit/plugins/karpathy-llm/hooks/git_post-commit.sh +20 -0
  20. package/toolkit/plugins/karpathy-llm/hooks/git_post-merge.sh +20 -0
  21. package/toolkit/plugins/karpathy-llm/hooks/git_pre-commit.sh +20 -0
  22. package/toolkit/plugins/karpathy-llm/hooks/git_pre-push.sh +20 -0
  23. package/toolkit/plugins/karpathy-llm/instructions/rnex-scaffolding-meta-prompt.md +5 -5
  24. package/toolkit/plugins/karpathy-llm/instructions/rnex-wiki-architecture.md +6 -6
  25. package/toolkit/plugins/karpathy-llm/plugin.yaml +5 -2
  26. package/toolkit/plugins/karpathy-llm/rules/behavioral.md +1 -1
  27. package/toolkit/plugins/karpathy-llm/skills/wiki-ingest.md +7 -6
  28. package/toolkit/plugins/karpathy-llm/skills/wiki-lint.md +7 -6
  29. package/toolkit/plugins/karpathy-llm/templates/rnex-wiki-page.sample.md +2 -2
package/CHANGELOG.md CHANGED
@@ -5,6 +5,27 @@ All notable changes to Repo Nexus (`rnex`) will be documented in this file.
5
5
  The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
6
6
  and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
7
7
 
8
+ ## [0.7.26] - 2026-10-21
9
+
10
+ ### Added
11
+ - **Multi-Source Intake Configuration for Karpathy LLM Wiki (`sources`)**:
12
+ - Replaced single `raw_dir` scalar configuration with multi-value `sources` list under `plugins.karpathy-llm.sources`.
13
+ - Added pure POSIX `/bin/sh` glob and directory resolver (`resolve_plugin_sources`) supporting both explicit directory paths (e.g. `docs`) and recursive/single-level glob patterns (e.g. `"repos/**/README.md"`, `repos/*/README.md`).
14
+ - Implemented automatic exclusion of member repositories disabled locally or globally (`enabled: false`) from resolved intake paths.
15
+ - Enforced two-level configuration precedence: `.local.rnex.yaml` takes full precedence over `rnex.yaml` when defining `sources`.
16
+ - Added automatic directory pruning for `.git`, `.rnex`, and `node_modules` during recursive file traversal.
17
+ - Added safe directory provisioning in `rnex sync` that creates explicit directory sources (such as `docs`) while skipping wildcard patterns and file targets.
18
+
19
+ ### Changed
20
+ - **Default Intake Sources**:
21
+ - Default sources for `karpathy-llm` plugin are now `docs` and `"repos/**/README.md"`.
22
+ - Updated `rnex init` templates, `rnex status`, and plugin scaffolding to render and configure multi-line `sources`.
23
+ - Updated plugin lifecycle hooks (`after_sync.sh`, `git_*.sh`) and agentic skills (`wiki-ingest.md`, `wiki-lint.md`) to resolve documentation from `sources`.
24
+
25
+ ### Deprecated
26
+ - **Legacy `raw_dir` Configuration**:
27
+ - `plugins.karpathy-llm.raw_dir` is deprecated in favor of `sources`. Workspaces configuring `raw_dir` are automatically read via transparent fallback with a non-fatal deprecation notice advising migration to `sources`.
28
+
8
29
  ## [0.7.7] - 2026-10-15
9
30
 
10
31
  ### Added
package/docs/CLI.md CHANGED
@@ -93,7 +93,7 @@ Hooks have access to POSIX helper functions for managing User Skill pointers:
93
93
  ### Karpathy LLM Wiki Plugin
94
94
  The `karpathy-llm` plugin uses lifecycle hooks to manage its domain directories:
95
95
  - **Default curated knowledge base (`wiki_dir`)**: `.rnex/wiki`
96
- - **Default append-only raw intake (`raw_dir`)**: `./docs`
96
+ - **Default intake sources (`sources`)**: `docs` directory and `"repos/**/README.md"` glob pattern (legacy scalar `raw_dir` supported with deprecation notice)
97
97
  - **Master navigation index**: `.rnex/wiki/index.md`
98
98
  - **Rolling orientation cache**: `.rnex/wiki/hot.md`
99
99
  - **Operation log**: `.rnex/wiki/_log.md`
@@ -93,15 +93,21 @@ plugins:
93
93
  karpathy-llm:
94
94
  # Directory paths relative to workspace root (defaults shown)
95
95
  wiki_dir: .rnex/wiki # Directory for curated atomic markdown pages
96
- raw_dir: ./docs # Append-only directory for unmodified intake documents
96
+ sources: # Intake documents: directories and/or glob patterns
97
+ - docs # directory: all files (hidden dirs and node_modules pruned)
98
+ - "repos/**/README.md" # recursive glob (quote it); disabled repos are excluded
99
+ # A sources list in .local.rnex.yaml replaces this list entirely.
97
100
 
98
101
  # Automated Git hook trigger for wiki verification
99
102
  # Options: git-post-merge, git-post-commit, git-pre-commit, git-pre-push, manual
100
103
  lint_trigger: manual
101
104
 
102
- # Notify developer when changes are committed to ./docs/ or .rnex/wiki/
105
+ # Notify developer when changes are committed to intake sources or .rnex/wiki/
103
106
  notify_on_changes: true
104
107
 
108
+ # Ingest member repository READMEs into atomic wiki pages
109
+ ingest_member_readmes: true
110
+
105
111
  # Enable or disable
106
112
  enabled: true
107
113
  ```
@@ -109,17 +115,33 @@ plugins:
109
115
  | Option | Type | Default | Description |
110
116
  |:---|:---|:---|:---|
111
117
  | `wiki_dir` | string | `.rnex/wiki` | Directory containing curated atomic markdown pages. |
112
- | `raw_dir` | string | `./docs` | Append-only directory for unmodified source intake documents. |
118
+ | `sources` | list | `[docs, "repos/**/README.md"]` | Intake directories and/or glob patterns relative to workspace root. Overridable in `.local.rnex.yaml`. |
119
+ | `raw_dir` | string | *(deprecated)* | Deprecated legacy intake directory. Transparently treated as a single `sources` entry with a migration warning. |
113
120
  | `lint_trigger` | string | `manual` | Git hook event that triggers wiki change detection (`git-post-merge`, `git-post-commit`, `git-pre-commit`, `git-pre-push`, `manual`). |
114
- | `notify_on_changes` | boolean | `true` | Show notification banner when changes are committed to `./docs/` or `.rnex/wiki/`. |
121
+ | `notify_on_changes` | boolean | `true` | Show notification banner when changes are committed to intake sources or `.rnex/wiki/`. |
122
+ | `ingest_member_readmes` | boolean | `true` | Automatically ingest member repository READMEs into atomic wiki pages. |
115
123
  | `enabled` | boolean | `true` | Enable or disable the plugin. Overridable in `.local.rnex.yaml`. |
116
124
 
117
125
  ---
118
126
 
127
+ ### Intake Sources (`sources`)
128
+
129
+ The `sources` setting defines where the Karpathy LLM Wiki discovers raw intake documents for `wiki-ingest` and change verification:
130
+
131
+ - **Directory Paths (e.g. `docs`, `notes`)**: Repo Nexus recursively scans the directory for all files. Hidden directories (starting with `.`) and `node_modules` are automatically pruned.
132
+ - **Recursive Globs (e.g. `"repos/**/README.md"`)**: Pure POSIX `/bin/sh` pattern resolution matches files at arbitrary directory depths. Always quote globs containing `*` in YAML. Directories named `.git`, `.rnex`, and `node_modules` are pruned during traversal.
133
+ - **Single-Level Globs (e.g. `repos/*/README.md` or `docs/*.md`)**: Matches files at a fixed directory level.
134
+ - **Active Repository Filtering**: Any resolved path located within `repos/<name>/` is checked against member repository status. If a repository is disabled (`enabled: false`) in `rnex.yaml` or `.local.rnex.yaml`, its files are automatically excluded.
135
+ - **Two-Level Configuration Precedence**: If `sources` is declared in `.local.rnex.yaml`, it completely overrides the `sources` list defined in `rnex.yaml`.
136
+ - **Legacy Fallback & Migration**: Existing configurations with `raw_dir: ./path` continue to work without disruption. Repo Nexus reads `raw_dir` as a single-element sources list and emits a non-fatal deprecation notice advising you to migrate to `sources`.
137
+ - **Directory Provisioning**: When running `rnex sync`, explicit directory entries (such as `docs`) are created automatically if missing. Glob patterns (containing `*`, `?`, or `[`) and file targets are never converted into directories.
138
+
139
+ ---
140
+
119
141
  ## Automated Git Hooks Integration
120
142
 
121
143
  When workspace Git hooks are installed (`rnex hooks install`), the `karpathy-llm` plugin links directly into Git events:
122
- - **`post-merge`**: Detects if incoming commits touched `./docs/` or `.rnex/wiki/`, warning the developer to review or re-lint.
144
+ - **`post-merge`**: Detects if incoming commits touched intake sources or `.rnex/wiki/`, warning the developer to review or re-lint.
123
145
  - **`post-commit`**: Reminds the developer to run `wiki-lint` or `wiki-ingest` if wiki files were committed.
124
146
  - **`pre-commit` & `pre-push`**: Emits trigger notices if unvalidated wiki files are staged or about to be pushed.
125
147
 
@@ -101,7 +101,11 @@ plugins:
101
101
  # windsurf:
102
102
  # skills: true # generate user skills in .windsurf/rules/ (default: true)
103
103
  # karpathy-llm:
104
- # raw_dir: "./docs" # Append-only directory for unmodified intake documents (default: ./docs)
104
+ # sources: # Intake documents: directories and/or glob patterns (default below)
105
+ # - docs # directory: all files (hidden dirs and node_modules pruned)
106
+ # - "repos/**/README.md" # recursive glob (quote it); disabled repos are excluded
107
+ # # A sources list in .local.rnex.yaml replaces this list entirely.
108
+ # # Legacy 'raw_dir' is still read as a single source (deprecated).
105
109
  # wiki_dir: ".rnex/wiki" # Directory for curated atomic markdown pages (default: .rnex/wiki)
106
110
  # lint_trigger: "manual" # Runs manually via 'rnex sync' or validation checklists (git-post-merge, git-post-commit, git-pre-commit, git-pre-push, manual)
107
111
  # notify_on_changes: true # Show notification when changes are committed to ./docs or .rnex/wiki
@@ -107,15 +107,7 @@ cmd_init() {
107
107
  _pdir="$(find_plugin_dir "$_p" 2>/dev/null || true)"
108
108
  _cfg_lines=""
109
109
  if [ -n "$_pdir" ] && [ -f "$_pdir/plugin.yaml" ]; then
110
- _cfg_lines="$(awk '
111
- /^config_defaults:[ ]*$/ || /^config_defaults:[ ]*#/ { in_c=1; next }
112
- in_c && /^[^ #]/ { exit }
113
- in_c && /^[ ]*[a-zA-Z0-9_.-]+:[ ]*/ {
114
- line = $0
115
- sub(/^[ ]*/, " ", line)
116
- print line
117
- }
118
- ' "$_pdir/plugin.yaml")"
110
+ _cfg_lines="$(plugin_config_default_lines "$_pdir/plugin.yaml")"
119
111
  fi
120
112
  if [ -n "$_cfg_lines" ]; then
121
113
  _init_plugins="${_init_plugins} ${_p}:\n${_cfg_lines}\n"
@@ -99,6 +99,10 @@ cmd_plugin_info() {
99
99
  [ -n "$_skill_target" ] && _desc="$(printf '%s' "$_desc" | sed -e "s|{skill_dir}|$_skill_target|g")"
100
100
 
101
101
  _cur="$(yaml_plugin_get "$_name" "$_opt" 2>/dev/null || true)"
102
+ if [ -z "$_cur" ]; then
103
+ # List-valued options (e.g. sources): show the effective list, comma-separated
104
+ _cur="$(get_plugin_config_list "$_name" "$_opt" 2>/dev/null | awk '{ out = (NR == 1) ? $0 : out ", " $0 } END { if (NR) print out }')"
105
+ fi
102
106
  if [ -z "$_cur" ]; then
103
107
  _cur="$(awk -v k="$_opt" '
104
108
  /^config_defaults:[ ]*$/ || /^config_defaults:[ ]*#/ { in_c=1; next }
@@ -227,6 +227,9 @@ cmd_status() {
227
227
  if [ -n "$_opts" ]; then
228
228
  printf ' %b↳ Options: %s%b\n' "$_DIM" "$_opts" "$_NC"
229
229
  fi
230
+ if [ "$_pname" = "karpathy-llm" ] && [ -n "$(plugin_legacy_raw_dir "$_pname" 2>/dev/null)" ]; then
231
+ printf ' %b↳ [!] Deprecated: raw_dir — replace it with a sources list and run rnex sync%b\n' "$_Y" "$_NC"
232
+ fi
230
233
  fi
231
234
  fi
232
235
  else
package/lib/hooks.sh CHANGED
@@ -146,14 +146,35 @@ run_plugin_git_hooks() {
146
146
 
147
147
  if [ -n "$_h_cand" ] && [ -f "$_h_cand" ]; then
148
148
  chmod +x "$_h_cand" 2>/dev/null || true
149
- (
150
- export RNEX_ROOT RNEX_CONFIG_MERGED RNEX_PLUGIN_DIR RNEX_CMD RNEX_ARGS RNEX_QUIET
151
- if [ -n "$_stdin_data" ]; then
152
- printf '%s\n' "$_stdin_data" | "$_h_cand" "$@"
153
- else
154
- "$_h_cand" "$@"
155
- fi
156
- ) || true
149
+ if [ -n "$_stdin_data" ]; then
150
+ printf '%s\n' "$_stdin_data" | env \
151
+ NEXUS_DIR="$RNEX_ROOT" \
152
+ WORKSPACE_YAML="$RNEX_ROOT/${RNEX_CONFIG_FILE:-rnex.yaml}" \
153
+ WORKSPACE_LOCAL_YAML="$RNEX_ROOT/${RNEX_LOCAL_CONFIG_FILE:-.local.rnex.yaml}" \
154
+ RNEX_ROOT="$RNEX_ROOT" \
155
+ RNEX_CONFIG_MERGED="$RNEX_CONFIG_MERGED" \
156
+ RNEX_PLUGIN_DIR="$RNEX_PLUGIN_DIR" \
157
+ RNEX_CMD="$RNEX_CMD" \
158
+ RNEX_ARGS="$RNEX_ARGS" \
159
+ RNEX_QUIET="$RNEX_QUIET" \
160
+ RNEX_CONFIG_FILE="${RNEX_CONFIG_FILE:-rnex.yaml}" \
161
+ RNEX_LOCAL_CONFIG_FILE="${RNEX_LOCAL_CONFIG_FILE:-.local.rnex.yaml}" \
162
+ "$_h_cand" "$@" || true
163
+ else
164
+ env \
165
+ NEXUS_DIR="$RNEX_ROOT" \
166
+ WORKSPACE_YAML="$RNEX_ROOT/${RNEX_CONFIG_FILE:-rnex.yaml}" \
167
+ WORKSPACE_LOCAL_YAML="$RNEX_ROOT/${RNEX_LOCAL_CONFIG_FILE:-.local.rnex.yaml}" \
168
+ RNEX_ROOT="$RNEX_ROOT" \
169
+ RNEX_CONFIG_MERGED="$RNEX_CONFIG_MERGED" \
170
+ RNEX_PLUGIN_DIR="$RNEX_PLUGIN_DIR" \
171
+ RNEX_CMD="$RNEX_CMD" \
172
+ RNEX_ARGS="$RNEX_ARGS" \
173
+ RNEX_QUIET="$RNEX_QUIET" \
174
+ RNEX_CONFIG_FILE="${RNEX_CONFIG_FILE:-rnex.yaml}" \
175
+ RNEX_LOCAL_CONFIG_FILE="${RNEX_LOCAL_CONFIG_FILE:-.local.rnex.yaml}" \
176
+ "$_h_cand" "$@" || true
177
+ fi
157
178
  fi
158
179
  done
159
180
  }
package/lib/plugins.sh CHANGED
@@ -143,15 +143,14 @@ plugin_resolve_placeholders() {
143
143
  [ -z "$_w_val" ] && _w_val=".rnex/wiki"
144
144
  _w_val="$(printf '%s' "$_w_val" | sed -e 's/^[[:space:]]*//' -e 's/[[:space:]]*$//' | tr -d "'\"" | sed -e 's#^\./##' -e 's#^/##' -e 's#/$##')"
145
145
 
146
- _r_val="$(yaml_plugin_get "$_p" "raw_dir" 2>/dev/null || true)"
147
- [ -z "$_r_val" ] && _r_val="docs"
148
- _r_val="$(printf '%s' "$_r_val" | sed -e 's/^[[:space:]]*//' -e 's/[[:space:]]*$//' | tr -d "'\"" | sed -e 's#^\./##' -e 's#^/##' -e 's#/$##')"
146
+ # {raw_dir} (legacy alias) resolves to the first explicit directory source; when no
147
+ # directory source exists it stays unresolved so callers can skip the destination.
148
+ _r_val="$(plugin_primary_source_dir "$_p" 2>/dev/null || true)"
149
149
 
150
- _res="$(printf '%s' "$_res" | sed \
151
- -e "s|{wiki_dir}|${_w_val}|g" \
152
- -e "s|{raw_dir}|${_r_val}|g" \
153
- -e "s|^\.\{0,1\}\/\{0,1\}wiki/|${_w_val}/|" \
154
- -e "s|^\.\{0,1\}\/\{0,1\}raw/|${_r_val}/|")"
150
+ _res="$(printf '%s' "$_res" | sed -e "s|{wiki_dir}|${_w_val}|g" -e "s|^\.\{0,1\}\/\{0,1\}wiki/|${_w_val}/|")"
151
+ if [ -n "$_r_val" ]; then
152
+ _res="$(printf '%s' "$_res" | sed -e "s|{raw_dir}|${_r_val}|g" -e "s|^\.\{0,1\}\/\{0,1\}raw/|${_r_val}/|")"
153
+ fi
155
154
 
156
155
  _res="$(printf '%s' "$_res" | sed -e 's/^[[:space:]]*//' -e 's/[[:space:]]*$//' | tr -d "'\"" | sed -e 's#^\./##')"
157
156
 
@@ -165,17 +164,27 @@ plugin_instantiate_template() {
165
164
  [ -z "$_w_val" ] && _w_val=".rnex/wiki"
166
165
  _w_val="$(printf '%s' "$_w_val" | sed -e 's/^[[:space:]]*//' -e 's/[[:space:]]*$//' | tr -d "'\"" | sed -e 's#^/##' -e 's#/$##')"
167
166
 
168
- _r_val="$(yaml_plugin_get "$_p" "raw_dir" 2>/dev/null || true)"
167
+ _r_val="$(plugin_primary_source_dir "$_p" 2>/dev/null || true)"
169
168
  [ -z "$_r_val" ] && _r_val="docs"
170
- _r_val="$(printf '%s' "$_r_val" | sed -e 's/^[[:space:]]*//' -e 's/[[:space:]]*$//' | tr -d "'\"" | sed -e 's#^\./##' -e 's#^/##' -e 's#/$##')"
169
+
170
+ # {sources}: human-readable list of configured intake sources (sed-escaped)
171
+ _s_val="$(plugin_sources_display "$_p" 2>/dev/null || true)"
172
+ [ -z "$_s_val" ] && _s_val="$(printf '\140%s/\140' "$_r_val")"
173
+ _s_val="$(printf '%s' "$_s_val" | sed -e 's/[\\&|]/\\&/g')"
171
174
 
172
175
  sed -e "s|{wiki_dir}|$_w_val|g" \
173
176
  -e "s|{raw_dir}|$_r_val|g" \
177
+ -e "s|{sources}|$_s_val|g" \
174
178
  "$_src" > "$_dest"
175
179
  }
176
180
 
177
181
  # Sync plugins into scoped directories: .rnex/plugins/<plugin-name>/
178
182
  sync_plugins() {
183
+ # Emit the legacy raw_dir deprecation notice once per sync (stdout discarded)
184
+ if yaml_plugin_enabled "karpathy-llm" 2>/dev/null; then
185
+ get_plugin_config_list "karpathy-llm" "sources" >/dev/null
186
+ fi
187
+
179
188
  # Clean up any bogus placeholder directories accidentally created in workspace root
180
189
  for _bogus in \
181
190
  "$NEXUS_DIR/\"{wiki_dir}" \
@@ -189,15 +198,15 @@ sync_plugins() {
189
198
  done
190
199
 
191
200
  # Clean up obsolete leaked root ./raw and ./wiki directories if non-default paths are configured
192
- _raw_cfg="$(yaml_plugin_get "karpathy-llm" "raw_dir" 2>/dev/null || true)"
201
+ _raw_cfg="$(plugin_primary_source_dir "karpathy-llm" 2>/dev/null || true)"
193
202
  [ -z "$_raw_cfg" ] && _raw_cfg="docs"
194
- _raw_cfg="$(printf '%s' "$_raw_cfg" | sed -e 's/^[[:space:]]*//' -e 's/[[:space:]]*$//' | tr -d "'\"" | sed -e 's#^\./##' -e 's#^/##' -e 's#/$##')"
195
203
 
196
204
  _wiki_cfg="$(yaml_plugin_get "karpathy-llm" "wiki_dir" 2>/dev/null || true)"
197
205
  [ -z "$_wiki_cfg" ] && _wiki_cfg=".rnex/wiki"
198
206
  _wiki_cfg="$(printf '%s' "$_wiki_cfg" | sed -e 's/^[[:space:]]*//' -e 's/[[:space:]]*$//' | tr -d "'\"" | sed -e 's#^\./##' -e 's#^/##' -e 's#/$##')"
199
207
 
200
- if [ "$_raw_cfg" != "raw" ] && [ -d "$NEXUS_DIR/raw" ]; then
208
+ if [ "$_raw_cfg" != "raw" ] && [ -d "$NEXUS_DIR/raw" ] && \
209
+ ! plugin_source_dirs "karpathy-llm" 2>/dev/null | grep -qx 'raw'; then
201
210
  if ! rmdir "$NEXUS_DIR/raw" 2>/dev/null; then
202
211
  _can_clean=1
203
212
  for _f in "$NEXUS_DIR/raw"/* "$NEXUS_DIR/raw"/.[!.]* "$NEXUS_DIR/raw"/..?*; do
@@ -293,7 +302,9 @@ sync_plugins() {
293
302
  rm -rf "${_scoped_dest:?}/$_sub"
294
303
  mkdir -p "$_scoped_dest/$_sub"
295
304
  cp -R "$_pdir/$_sub"/. "$_scoped_dest/$_sub"/ 2>/dev/null || true
296
- [ "$_sub" = "hooks" ] && chmod +x "$_scoped_dest/hooks"/* 2>/dev/null || true
305
+ if [ "$_sub" = "hooks" ]; then
306
+ chmod +x "$_scoped_dest/hooks"/* 2>/dev/null || true
307
+ fi
297
308
  else
298
309
  rm -rf "${_scoped_dest:?}/$_sub"
299
310
  fi
@@ -311,6 +322,17 @@ sync_plugins() {
311
322
  fi
312
323
  fi
313
324
 
325
+ # Provision explicit directory sources declared under plugins.<name>.sources (e.g. ./docs)
326
+ for _sdir in $(plugin_source_dirs "$_pname" 2>/dev/null); do
327
+ [ -n "$_sdir" ] || continue
328
+ _full_sdir="$NEXUS_DIR/$_sdir"
329
+ if [ ! -d "$_full_sdir" ]; then
330
+ mkdir -p "$_full_sdir"
331
+ [ -f "$_full_sdir/.gitkeep" ] || touch "$_full_sdir/.gitkeep"
332
+ log_ok "Created intake source directory: $_sdir"
333
+ fi
334
+ done
335
+
314
336
  # Initialize templates declared in plugin (functional external dirs like raw/, wiki/)
315
337
  plugin_list_templates "$_pdir" | while read -r _item; do
316
338
  [ -n "$_item" ] || continue
@@ -321,6 +343,11 @@ sync_plugins() {
321
343
 
322
344
  _d="$(plugin_resolve_placeholders "$_pname" "$_pdir" "$_d")"
323
345
 
346
+ # No explicit directory source configured (e.g. glob-only sources): nothing to provision
347
+ case "$_d" in
348
+ *"{raw_dir}"*) continue ;;
349
+ esac
350
+
324
351
  # Defensive guard: never create directories or files if placeholder expansion failed or quotes leaked
325
352
  case "$_d" in
326
353
  *"{"*"}"*|*\"*|*\'*)
@@ -335,9 +362,235 @@ sync_plugins() {
335
362
  if [ -f "$_src_path" ] && [ ! -f "$_dest_path" ]; then
336
363
  mkdir -p "$(dirname "$_dest_path")"
337
364
  plugin_instantiate_template "$_pname" "$_pdir" "$_src_path" "$_dest_path"
338
- [ -x "$_src_path" ] && chmod +x "$_dest_path" 2>/dev/null || true
365
+ if [ -x "$_src_path" ]; then
366
+ chmod +x "$_dest_path" 2>/dev/null || true
367
+ fi
339
368
  log_ok "Initialized template: $_d"
340
369
  fi
341
370
  done
342
371
  done
343
372
  }
373
+
374
+ # ----------------------------------------------------------------------------
375
+ # Plugin source resolution (directories + POSIX glob patterns)
376
+ # ----------------------------------------------------------------------------
377
+
378
+ # True if the argument contains a glob metacharacter (*, ?, [)
379
+ _src_has_glob() {
380
+ case "$1" in
381
+ *'*'*|*'?'*|*'['*) return 0 ;;
382
+ esac
383
+ return 1
384
+ }
385
+
386
+ # Workspace-relative member repositories directory (default: repos)
387
+ _plugin_repos_rel_dir() {
388
+ _prd="${REPOS_DIR:-}"
389
+ if [ -n "$_prd" ]; then
390
+ _prd="${_prd#"${NEXUS_DIR:-.}"/}"
391
+ else
392
+ _prd="repos"
393
+ fi
394
+ while [ "${_prd#./}" != "$_prd" ]; do _prd="${_prd#./}"; done
395
+ printf '%s' "${_prd%/}"
396
+ }
397
+
398
+ # Normalize a source entry: trim whitespace/CR, strip leading ./ and trailing /
399
+ _src_normalize_entry() {
400
+ _sne="$(printf '%s' "$1" | tr -d '\r' | sed -e 's/^[[:space:]]*//' -e 's/[[:space:]]*$//')"
401
+ while [ "${_sne#./}" != "$_sne" ]; do _sne="${_sne#./}"; done
402
+ [ "$_sne" = "." ] || _sne="${_sne%/}"
403
+ printf '%s' "$_sne"
404
+ }
405
+
406
+ # True if a normalized entry names a file rather than a directory
407
+ # (glob pattern, .md/.txt suffix, or an existing regular file)
408
+ _src_is_file_like() {
409
+ _src_has_glob "$1" && return 0
410
+ case "$1" in
411
+ *.md|*.txt) return 0 ;;
412
+ esac
413
+ [ -f "${NEXUS_DIR:-.}/$1" ] && return 0
414
+ return 1
415
+ }
416
+
417
+ # List explicit directory sources of a plugin that rnex may provision (mkdir).
418
+ # Skips glob patterns, file entries, absolute paths, '..' traversal, the workspace
419
+ # root, and paths inside the member repositories directory.
420
+ plugin_source_dirs() {
421
+ _psd_rdir="$(_plugin_repos_rel_dir)"
422
+ get_plugin_config_list "$1" "sources" 2>/dev/null | while IFS= read -r _psd_e || [ -n "$_psd_e" ]; do
423
+ _psd_e="$(_src_normalize_entry "$_psd_e")"
424
+ case "$_psd_e" in
425
+ ''|.|/*|..|../*|*/..|*/../*) continue ;;
426
+ esac
427
+ _src_is_file_like "$_psd_e" && continue
428
+ case "$_psd_e" in
429
+ "$_psd_rdir"|"$_psd_rdir"/*) continue ;;
430
+ esac
431
+ printf '%s\n' "$_psd_e"
432
+ done
433
+ }
434
+
435
+ # First provisionable directory source (legacy {raw_dir} placeholder); may be empty
436
+ plugin_primary_source_dir() {
437
+ plugin_source_dirs "$1" | sed -n '1p'
438
+ }
439
+
440
+ # Human-readable source list for templates, e.g.: `docs/`, `repos/**/README.md`
441
+ plugin_sources_display() {
442
+ get_plugin_config_list "$1" "sources" 2>/dev/null | while IFS= read -r _psy_e || [ -n "$_psy_e" ]; do
443
+ _psy_e="$(_src_normalize_entry "$_psy_e")"
444
+ [ -n "$_psy_e" ] || continue
445
+ if _src_is_file_like "$_psy_e"; then
446
+ printf '\140%s\140\n' "$_psy_e"
447
+ else
448
+ printf '\140%s/\140\n' "$_psy_e"
449
+ fi
450
+ done | awk '{ out = (NR == 1) ? $0 : out ", " $0 } END { if (NR) print out }'
451
+ }
452
+
453
+ # Split a pattern into its leading wildcard-free directory prefix and the rest.
454
+ # The last component always stays in the rest. Sets _sp_base (default ".") and _sp_rest.
455
+ _src_static_prefix() {
456
+ _sp_base=""
457
+ _sp_rest="$1"
458
+ while :; do
459
+ _sp_comp="${_sp_rest%%/*}"
460
+ [ "$_sp_comp" = "$_sp_rest" ] && break
461
+ _src_has_glob "$_sp_comp" && break
462
+ _sp_base="${_sp_base:+$_sp_base/}$_sp_comp"
463
+ _sp_rest="${_sp_rest#*/}"
464
+ done
465
+ [ -z "$_sp_base" ] && _sp_base="."
466
+ return 0
467
+ }
468
+
469
+ # List all files under a directory, pruning hidden directories/files and node_modules
470
+ _src_find_dir() {
471
+ find "$1" ! -path "$1" -type d \( -name '.*' -o -name node_modules \) -prune -o -type f ! -name '.*' -print 2>/dev/null
472
+ }
473
+
474
+ # Resolve a recursive glob: <base>/**/<rest> (base "." for a leading **/)
475
+ _src_find_recursive() {
476
+ _fr_base="$1"; _fr_rest="$2"
477
+ if [ "${_fr_rest#*/}" = "$_fr_rest" ] && ! _src_has_glob "$_fr_base"; then
478
+ # Single trailing component: exact semantics via -name at any depth
479
+ [ -d "$_fr_base" ] || return 0
480
+ find "$_fr_base" ! -path "$_fr_base" -type d \( -name .git -o -name .rnex -o -name node_modules \) -prune \
481
+ -o -type f -name "$_fr_rest" -print 2>/dev/null
482
+ return 0
483
+ fi
484
+ _fr_start="$_fr_base"
485
+ if _src_has_glob "$_fr_base"; then
486
+ _src_static_prefix "$_fr_base/x"
487
+ _fr_start="$_sp_base"
488
+ fi
489
+ [ -d "$_fr_start" ] || return 0
490
+ find "$_fr_start" ! -path "$_fr_start" -type d \( -name .git -o -name .rnex -o -name node_modules \) -prune \
491
+ -o -type f \( -path "$_fr_base/$_fr_rest" -o -path "$_fr_base/*/$_fr_rest" \) -print 2>/dev/null
492
+ }
493
+
494
+ # Resolve a single-level glob (e.g. repos/*/README.md, docs/*.md) with bounded depth
495
+ _src_find_glob() {
496
+ _src_static_prefix "$1"
497
+ [ -d "$_sp_base" ] || return 0
498
+ # Prune directories at (or below) the pattern depth so find never descends further
499
+ _fg_depth="$_sp_base"
500
+ _fg_r="$_sp_rest"
501
+ while :; do
502
+ _fg_depth="$_fg_depth/*"
503
+ case "$_fg_r" in
504
+ */*) _fg_r="${_fg_r#*/}" ;;
505
+ *) break ;;
506
+ esac
507
+ done
508
+ find "$_sp_base" ! -path "$_sp_base" -type d \( -name .git -o -name .rnex -o -name node_modules -o -path "$_fg_depth" \) -prune \
509
+ -o -type f -path "$_sp_base/$_sp_rest" -print 2>/dev/null
510
+ }
511
+
512
+ # Resolve one normalized source entry (run from the workspace root)
513
+ _src_resolve_entry() {
514
+ _re="$1"
515
+ case "$_re" in
516
+ '**')
517
+ _src_find_dir "."
518
+ ;;
519
+ */'**')
520
+ _re="${_re%/\*\*}"
521
+ [ -d "$_re" ] && _src_find_dir "$_re"
522
+ ;;
523
+ '**/'*)
524
+ _src_find_recursive "." "${_re#\*\*/}"
525
+ ;;
526
+ *'/**/'*)
527
+ _src_find_recursive "${_re%%/\*\*/*}" "${_re#*/\*\*/}"
528
+ ;;
529
+ *)
530
+ if [ -d "$_re" ]; then
531
+ _src_find_dir "$_re"
532
+ elif [ -f "$_re" ]; then
533
+ printf '%s\n' "$_re"
534
+ elif _src_has_glob "$_re"; then
535
+ _src_find_glob "$_re"
536
+ fi
537
+ ;;
538
+ esac
539
+ return 0
540
+ }
541
+
542
+ # Resolve plugin source entries (directories, files, glob patterns) into a
543
+ # deduplicated, sorted list of workspace-relative file paths.
544
+ # Input: newline-separated entries as $1 (or stdin), e.g. from get_plugin_config_list.
545
+ # Files inside member repositories disabled via `enabled: false` are excluded.
546
+ resolve_plugin_sources() {
547
+ _rs_root="${NEXUS_DIR:-${RNEX_ROOT:-.}}"
548
+
549
+ _saved_wy="${WORKSPACE_YAML:-}"
550
+ _saved_wly="${WORKSPACE_LOCAL_YAML:-}"
551
+ [ -z "$WORKSPACE_YAML" ] && WORKSPACE_YAML="$_rs_root/${RNEX_CONFIG_FILE:-rnex.yaml}"
552
+ [ -z "$WORKSPACE_LOCAL_YAML" ] && WORKSPACE_LOCAL_YAML="$_rs_root/${RNEX_LOCAL_CONFIG_FILE:-.local.rnex.yaml}"
553
+
554
+ # Workspace-relative member repositories directory (default: repos)
555
+ _rs_rdir="$(_plugin_repos_rel_dir)"
556
+
557
+ # Space-separated list of disabled repositories (local config overrides repo config)
558
+ _rs_disabled="$(yaml_list_repos 2>/dev/null | while IFS= read -r _rs_r; do
559
+ [ -n "$_rs_r" ] || continue
560
+ yaml_repo_enabled "$_rs_r" || printf '%s ' "$_rs_r"
561
+ done)"
562
+
563
+ _rs_out="$(
564
+ if [ "$#" -gt 0 ]; then
565
+ printf '%s\n' "$1"
566
+ else
567
+ cat
568
+ fi | (
569
+ cd "$_rs_root" || exit 0
570
+ while IFS= read -r _rs_e || [ -n "$_rs_e" ]; do
571
+ _rs_e="$(printf '%s' "$_rs_e" | tr -d '\r' | sed -e 's/^[[:space:]]*//' -e 's/[[:space:]]*$//')"
572
+ while [ "${_rs_e#./}" != "$_rs_e" ]; do _rs_e="${_rs_e#./}"; done
573
+ [ "$_rs_e" = "." ] || _rs_e="${_rs_e%/}"
574
+ [ -n "$_rs_e" ] || continue
575
+ _src_resolve_entry "$_rs_e"
576
+ done
577
+ ) | sed -e 's#^\(\./\)*##' | awk -v prefix="$_rs_rdir/" -v disabled="$_rs_disabled" '
578
+ BEGIN {
579
+ n = split(disabled, d, " ")
580
+ for (i = 1; i <= n; i++) off[d[i]] = 1
581
+ }
582
+ length($0) == 0 { next }
583
+ index($0, prefix) == 1 {
584
+ name = substr($0, length(prefix) + 1)
585
+ sub(/\/.*/, "", name)
586
+ if (name in off) next
587
+ }
588
+ { print }
589
+ ' | sort -u
590
+ )"
591
+
592
+ WORKSPACE_YAML="$_saved_wy"
593
+ WORKSPACE_LOCAL_YAML="$_saved_wly"
594
+
595
+ printf '%s\n' "$_rs_out" | awk 'NF'
596
+ }
package/lib/repos.sh CHANGED
@@ -115,7 +115,9 @@ EOF
115
115
  rm -rf "${_dest_pdir:?}/$_sub"
116
116
  mkdir -p "$_dest_pdir/$_sub"
117
117
  cp -R "$_pdir/$_sub"/. "$_dest_pdir/$_sub"/ 2>/dev/null || true
118
- [ "$_sub" = "hooks" ] && chmod +x "$_dest_pdir/hooks"/* 2>/dev/null || true
118
+ if [ "$_sub" = "hooks" ]; then
119
+ chmod +x "$_dest_pdir/hooks"/* 2>/dev/null || true
120
+ fi
119
121
  else
120
122
  rm -rf "${_dest_pdir:?}/$_sub"
121
123
  fi