repo-nexus 0.7.26 → 0.7.29

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 (31) hide show
  1. package/CHANGELOG.md +21 -0
  2. package/docs/CLI.md +1 -1
  3. package/docs/plugins/karpathy-llm.md +22 -5
  4. package/docs/rnex.example.yaml +4 -2
  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/commands/sync.sh +4 -0
  9. package/lib/hooks.sh +29 -8
  10. package/lib/plugins.sh +263 -15
  11. package/lib/repos.sh +3 -1
  12. package/lib/workspace.sh +94 -0
  13. package/lib/yaml.sh +172 -17
  14. package/package.json +1 -1
  15. package/templates/rnex.example.yaml +4 -2
  16. package/toolkit/plugins/copilot/hooks/after_sync.sh +4 -4
  17. package/toolkit/plugins/copilot/hooks/before_plugin_disable.sh +4 -4
  18. package/toolkit/plugins/karpathy-llm/README.md +7 -13
  19. package/toolkit/plugins/karpathy-llm/agents/rnex-wiki-curator.agent.md +1 -1
  20. package/toolkit/plugins/karpathy-llm/hooks/after_sync.sh +82 -38
  21. package/toolkit/plugins/karpathy-llm/hooks/git_post-commit.sh +20 -0
  22. package/toolkit/plugins/karpathy-llm/hooks/git_post-merge.sh +20 -0
  23. package/toolkit/plugins/karpathy-llm/hooks/git_pre-commit.sh +20 -0
  24. package/toolkit/plugins/karpathy-llm/hooks/git_pre-push.sh +20 -0
  25. package/toolkit/plugins/karpathy-llm/instructions/rnex-scaffolding-meta-prompt.md +5 -5
  26. package/toolkit/plugins/karpathy-llm/instructions/rnex-wiki-architecture.md +6 -6
  27. package/toolkit/plugins/karpathy-llm/plugin.yaml +4 -4
  28. package/toolkit/plugins/karpathy-llm/rules/behavioral.md +1 -1
  29. package/toolkit/plugins/karpathy-llm/skills/wiki-ingest.md +7 -6
  30. package/toolkit/plugins/karpathy-llm/skills/wiki-lint.md +7 -6
  31. 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,13 +93,16 @@ 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
 
105
108
  # Enable or disable
@@ -109,17 +112,31 @@ plugins:
109
112
  | Option | Type | Default | Description |
110
113
  |:---|:---|:---|:---|
111
114
  | `wiki_dir` | string | `.rnex/wiki` | Directory containing curated atomic markdown pages. |
112
- | `raw_dir` | string | `./docs` | Append-only directory for unmodified source intake documents. |
115
+ | `sources` | list | `[docs, "repos/**/README.md"]` | Intake directories and/or glob patterns relative to workspace root. Overridable in `.local.rnex.yaml`. |
113
116
  | `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/`. |
117
+ | `notify_on_changes` | boolean | `true` | Show notification banner when changes are committed to intake sources or `.rnex/wiki/`. |
115
118
  | `enabled` | boolean | `true` | Enable or disable the plugin. Overridable in `.local.rnex.yaml`. |
116
119
 
117
120
  ---
118
121
 
122
+ ### Intake Sources (`sources`)
123
+
124
+ The `sources` setting defines where the Karpathy LLM Wiki discovers raw intake documents for `wiki-ingest` and change verification:
125
+
126
+ - **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.
127
+ - **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.
128
+ - **Single-Level Globs (e.g. `repos/*/README.md` or `docs/*.md`)**: Matches files at a fixed directory level.
129
+ - **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.
130
+ - **Two-Level Configuration Precedence**: If `sources` is declared in `.local.rnex.yaml`, it completely overrides the `sources` list defined in `rnex.yaml`.
131
+ - **Legacy Migration**: Existing configurations with `raw_dir: <dir>` are automatically rewritten to a `sources` list during `rnex sync`.
132
+ - **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.
133
+
134
+ ---
135
+
119
136
  ## Automated Git Hooks Integration
120
137
 
121
138
  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.
139
+ - **`post-merge`**: Detects if incoming commits touched intake sources or `.rnex/wiki/`, warning the developer to review or re-lint.
123
140
  - **`post-commit`**: Reminds the developer to run `wiki-lint` or `wiki-ingest` if wiki files were committed.
124
141
  - **`pre-commit` & `pre-push`**: Emits trigger notices if unvalidated wiki files are staged or about to be pushed.
125
142
 
@@ -101,11 +101,13 @@ 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.
105
108
  # wiki_dir: ".rnex/wiki" # Directory for curated atomic markdown pages (default: .rnex/wiki)
106
109
  # 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
110
  # notify_on_changes: true # Show notification when changes are committed to ./docs or .rnex/wiki
108
- # ingest_member_readmes: true # Automatically ingest member repository READMEs into atomic wiki pages (default: true)
109
111
 
110
112
 
111
113
  # ------------------------------------------------------------------------------
@@ -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 — run "rnex sync" to rewrite to sources%b\n' "$_Y" "$_NC"
232
+ fi
230
233
  fi
231
234
  fi
232
235
  else
@@ -71,6 +71,10 @@ cmd_sync() {
71
71
  [ "$_sync_quiet" -eq 0 ] && log_ok "Upgraded workspace configuration and routing instructions to v$RNEX_VERSION"
72
72
  fi
73
73
 
74
+ # Reconcile legacy plugin settings (e.g. karpathy-llm raw_dir -> sources, strip ingest_member_readmes)
75
+ migrate_karpathy_llm_config "$WORKSPACE_YAML"
76
+ [ -f "$WORKSPACE_LOCAL_YAML" ] && migrate_karpathy_llm_config "$WORKSPACE_LOCAL_YAML"
77
+
74
78
  # --------------------------------------------------------------------------
75
79
  # Step 1: Sync hardcoded files & invoke commands that do not depend on config
76
80
  # --------------------------------------------------------------------------
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,12 +164,17 @@ 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
 
@@ -189,15 +193,15 @@ sync_plugins() {
189
193
  done
190
194
 
191
195
  # 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)"
196
+ _raw_cfg="$(plugin_primary_source_dir "karpathy-llm" 2>/dev/null || true)"
193
197
  [ -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
198
 
196
199
  _wiki_cfg="$(yaml_plugin_get "karpathy-llm" "wiki_dir" 2>/dev/null || true)"
197
200
  [ -z "$_wiki_cfg" ] && _wiki_cfg=".rnex/wiki"
198
201
  _wiki_cfg="$(printf '%s' "$_wiki_cfg" | sed -e 's/^[[:space:]]*//' -e 's/[[:space:]]*$//' | tr -d "'\"" | sed -e 's#^\./##' -e 's#^/##' -e 's#/$##')"
199
202
 
200
- if [ "$_raw_cfg" != "raw" ] && [ -d "$NEXUS_DIR/raw" ]; then
203
+ if [ "$_raw_cfg" != "raw" ] && [ -d "$NEXUS_DIR/raw" ] && \
204
+ ! plugin_source_dirs "karpathy-llm" 2>/dev/null | grep -qx 'raw'; then
201
205
  if ! rmdir "$NEXUS_DIR/raw" 2>/dev/null; then
202
206
  _can_clean=1
203
207
  for _f in "$NEXUS_DIR/raw"/* "$NEXUS_DIR/raw"/.[!.]* "$NEXUS_DIR/raw"/..?*; do
@@ -293,7 +297,9 @@ sync_plugins() {
293
297
  rm -rf "${_scoped_dest:?}/$_sub"
294
298
  mkdir -p "$_scoped_dest/$_sub"
295
299
  cp -R "$_pdir/$_sub"/. "$_scoped_dest/$_sub"/ 2>/dev/null || true
296
- [ "$_sub" = "hooks" ] && chmod +x "$_scoped_dest/hooks"/* 2>/dev/null || true
300
+ if [ "$_sub" = "hooks" ]; then
301
+ chmod +x "$_scoped_dest/hooks"/* 2>/dev/null || true
302
+ fi
297
303
  else
298
304
  rm -rf "${_scoped_dest:?}/$_sub"
299
305
  fi
@@ -311,6 +317,17 @@ sync_plugins() {
311
317
  fi
312
318
  fi
313
319
 
320
+ # Provision explicit directory sources declared under plugins.<name>.sources (e.g. ./docs)
321
+ for _sdir in $(plugin_source_dirs "$_pname" 2>/dev/null); do
322
+ [ -n "$_sdir" ] || continue
323
+ _full_sdir="$NEXUS_DIR/$_sdir"
324
+ if [ ! -d "$_full_sdir" ]; then
325
+ mkdir -p "$_full_sdir"
326
+ [ -f "$_full_sdir/.gitkeep" ] || touch "$_full_sdir/.gitkeep"
327
+ log_ok "Created intake source directory: $_sdir"
328
+ fi
329
+ done
330
+
314
331
  # Initialize templates declared in plugin (functional external dirs like raw/, wiki/)
315
332
  plugin_list_templates "$_pdir" | while read -r _item; do
316
333
  [ -n "$_item" ] || continue
@@ -321,6 +338,11 @@ sync_plugins() {
321
338
 
322
339
  _d="$(plugin_resolve_placeholders "$_pname" "$_pdir" "$_d")"
323
340
 
341
+ # No explicit directory source configured (e.g. glob-only sources): nothing to provision
342
+ case "$_d" in
343
+ *"{raw_dir}"*) continue ;;
344
+ esac
345
+
324
346
  # Defensive guard: never create directories or files if placeholder expansion failed or quotes leaked
325
347
  case "$_d" in
326
348
  *"{"*"}"*|*\"*|*\'*)
@@ -335,9 +357,235 @@ sync_plugins() {
335
357
  if [ -f "$_src_path" ] && [ ! -f "$_dest_path" ]; then
336
358
  mkdir -p "$(dirname "$_dest_path")"
337
359
  plugin_instantiate_template "$_pname" "$_pdir" "$_src_path" "$_dest_path"
338
- [ -x "$_src_path" ] && chmod +x "$_dest_path" 2>/dev/null || true
360
+ if [ -x "$_src_path" ]; then
361
+ chmod +x "$_dest_path" 2>/dev/null || true
362
+ fi
339
363
  log_ok "Initialized template: $_d"
340
364
  fi
341
365
  done
342
366
  done
343
367
  }
368
+
369
+ # ----------------------------------------------------------------------------
370
+ # Plugin source resolution (directories + POSIX glob patterns)
371
+ # ----------------------------------------------------------------------------
372
+
373
+ # True if the argument contains a glob metacharacter (*, ?, [)
374
+ _src_has_glob() {
375
+ case "$1" in
376
+ *'*'*|*'?'*|*'['*) return 0 ;;
377
+ esac
378
+ return 1
379
+ }
380
+
381
+ # Workspace-relative member repositories directory (default: repos)
382
+ _plugin_repos_rel_dir() {
383
+ _prd="${REPOS_DIR:-}"
384
+ if [ -n "$_prd" ]; then
385
+ _prd="${_prd#"${NEXUS_DIR:-.}"/}"
386
+ else
387
+ _prd="repos"
388
+ fi
389
+ while [ "${_prd#./}" != "$_prd" ]; do _prd="${_prd#./}"; done
390
+ printf '%s' "${_prd%/}"
391
+ }
392
+
393
+ # Normalize a source entry: trim whitespace/CR, strip leading ./ and trailing /
394
+ _src_normalize_entry() {
395
+ _sne="$(printf '%s' "$1" | tr -d '\r' | sed -e 's/^[[:space:]]*//' -e 's/[[:space:]]*$//')"
396
+ while [ "${_sne#./}" != "$_sne" ]; do _sne="${_sne#./}"; done
397
+ [ "$_sne" = "." ] || _sne="${_sne%/}"
398
+ printf '%s' "$_sne"
399
+ }
400
+
401
+ # True if a normalized entry names a file rather than a directory
402
+ # (glob pattern, .md/.txt suffix, or an existing regular file)
403
+ _src_is_file_like() {
404
+ _src_has_glob "$1" && return 0
405
+ case "$1" in
406
+ *.md|*.txt) return 0 ;;
407
+ esac
408
+ [ -f "${NEXUS_DIR:-.}/$1" ] && return 0
409
+ return 1
410
+ }
411
+
412
+ # List explicit directory sources of a plugin that rnex may provision (mkdir).
413
+ # Skips glob patterns, file entries, absolute paths, '..' traversal, the workspace
414
+ # root, and paths inside the member repositories directory.
415
+ plugin_source_dirs() {
416
+ _psd_rdir="$(_plugin_repos_rel_dir)"
417
+ get_plugin_config_list "$1" "sources" 2>/dev/null | while IFS= read -r _psd_e || [ -n "$_psd_e" ]; do
418
+ _psd_e="$(_src_normalize_entry "$_psd_e")"
419
+ case "$_psd_e" in
420
+ ''|.|/*|..|../*|*/..|*/../*) continue ;;
421
+ esac
422
+ _src_is_file_like "$_psd_e" && continue
423
+ case "$_psd_e" in
424
+ "$_psd_rdir"|"$_psd_rdir"/*) continue ;;
425
+ esac
426
+ printf '%s\n' "$_psd_e"
427
+ done
428
+ }
429
+
430
+ # First provisionable directory source (legacy {raw_dir} placeholder); may be empty
431
+ plugin_primary_source_dir() {
432
+ plugin_source_dirs "$1" | sed -n '1p'
433
+ }
434
+
435
+ # Human-readable source list for templates, e.g.: `docs/`, `repos/**/README.md`
436
+ plugin_sources_display() {
437
+ get_plugin_config_list "$1" "sources" 2>/dev/null | while IFS= read -r _psy_e || [ -n "$_psy_e" ]; do
438
+ _psy_e="$(_src_normalize_entry "$_psy_e")"
439
+ [ -n "$_psy_e" ] || continue
440
+ if _src_is_file_like "$_psy_e"; then
441
+ printf '\140%s\140\n' "$_psy_e"
442
+ else
443
+ printf '\140%s/\140\n' "$_psy_e"
444
+ fi
445
+ done | awk '{ out = (NR == 1) ? $0 : out ", " $0 } END { if (NR) print out }'
446
+ }
447
+
448
+ # Split a pattern into its leading wildcard-free directory prefix and the rest.
449
+ # The last component always stays in the rest. Sets _sp_base (default ".") and _sp_rest.
450
+ _src_static_prefix() {
451
+ _sp_base=""
452
+ _sp_rest="$1"
453
+ while :; do
454
+ _sp_comp="${_sp_rest%%/*}"
455
+ [ "$_sp_comp" = "$_sp_rest" ] && break
456
+ _src_has_glob "$_sp_comp" && break
457
+ _sp_base="${_sp_base:+$_sp_base/}$_sp_comp"
458
+ _sp_rest="${_sp_rest#*/}"
459
+ done
460
+ [ -z "$_sp_base" ] && _sp_base="."
461
+ return 0
462
+ }
463
+
464
+ # List all files under a directory, pruning hidden directories/files and node_modules
465
+ _src_find_dir() {
466
+ find "$1" ! -path "$1" -type d \( -name '.*' -o -name node_modules \) -prune -o -type f ! -name '.*' -print 2>/dev/null
467
+ }
468
+
469
+ # Resolve a recursive glob: <base>/**/<rest> (base "." for a leading **/)
470
+ _src_find_recursive() {
471
+ _fr_base="$1"; _fr_rest="$2"
472
+ if [ "${_fr_rest#*/}" = "$_fr_rest" ] && ! _src_has_glob "$_fr_base"; then
473
+ # Single trailing component: exact semantics via -name at any depth
474
+ [ -d "$_fr_base" ] || return 0
475
+ find "$_fr_base" ! -path "$_fr_base" -type d \( -name .git -o -name .rnex -o -name node_modules \) -prune \
476
+ -o -type f -name "$_fr_rest" -print 2>/dev/null
477
+ return 0
478
+ fi
479
+ _fr_start="$_fr_base"
480
+ if _src_has_glob "$_fr_base"; then
481
+ _src_static_prefix "$_fr_base/x"
482
+ _fr_start="$_sp_base"
483
+ fi
484
+ [ -d "$_fr_start" ] || return 0
485
+ find "$_fr_start" ! -path "$_fr_start" -type d \( -name .git -o -name .rnex -o -name node_modules \) -prune \
486
+ -o -type f \( -path "$_fr_base/$_fr_rest" -o -path "$_fr_base/*/$_fr_rest" \) -print 2>/dev/null
487
+ }
488
+
489
+ # Resolve a single-level glob (e.g. repos/*/README.md, docs/*.md) with bounded depth
490
+ _src_find_glob() {
491
+ _src_static_prefix "$1"
492
+ [ -d "$_sp_base" ] || return 0
493
+ # Prune directories at (or below) the pattern depth so find never descends further
494
+ _fg_depth="$_sp_base"
495
+ _fg_r="$_sp_rest"
496
+ while :; do
497
+ _fg_depth="$_fg_depth/*"
498
+ case "$_fg_r" in
499
+ */*) _fg_r="${_fg_r#*/}" ;;
500
+ *) break ;;
501
+ esac
502
+ done
503
+ find "$_sp_base" ! -path "$_sp_base" -type d \( -name .git -o -name .rnex -o -name node_modules -o -path "$_fg_depth" \) -prune \
504
+ -o -type f -path "$_sp_base/$_sp_rest" -print 2>/dev/null
505
+ }
506
+
507
+ # Resolve one normalized source entry (run from the workspace root)
508
+ _src_resolve_entry() {
509
+ _re="$1"
510
+ case "$_re" in
511
+ '**')
512
+ _src_find_dir "."
513
+ ;;
514
+ */'**')
515
+ _re="${_re%/\*\*}"
516
+ [ -d "$_re" ] && _src_find_dir "$_re"
517
+ ;;
518
+ '**/'*)
519
+ _src_find_recursive "." "${_re#\*\*/}"
520
+ ;;
521
+ *'/**/'*)
522
+ _src_find_recursive "${_re%%/\*\*/*}" "${_re#*/\*\*/}"
523
+ ;;
524
+ *)
525
+ if [ -d "$_re" ]; then
526
+ _src_find_dir "$_re"
527
+ elif [ -f "$_re" ]; then
528
+ printf '%s\n' "$_re"
529
+ elif _src_has_glob "$_re"; then
530
+ _src_find_glob "$_re"
531
+ fi
532
+ ;;
533
+ esac
534
+ return 0
535
+ }
536
+
537
+ # Resolve plugin source entries (directories, files, glob patterns) into a
538
+ # deduplicated, sorted list of workspace-relative file paths.
539
+ # Input: newline-separated entries as $1 (or stdin), e.g. from get_plugin_config_list.
540
+ # Files inside member repositories disabled via `enabled: false` are excluded.
541
+ resolve_plugin_sources() {
542
+ _rs_root="${NEXUS_DIR:-${RNEX_ROOT:-.}}"
543
+
544
+ _saved_wy="${WORKSPACE_YAML:-}"
545
+ _saved_wly="${WORKSPACE_LOCAL_YAML:-}"
546
+ [ -z "$WORKSPACE_YAML" ] && WORKSPACE_YAML="$_rs_root/${RNEX_CONFIG_FILE:-rnex.yaml}"
547
+ [ -z "$WORKSPACE_LOCAL_YAML" ] && WORKSPACE_LOCAL_YAML="$_rs_root/${RNEX_LOCAL_CONFIG_FILE:-.local.rnex.yaml}"
548
+
549
+ # Workspace-relative member repositories directory (default: repos)
550
+ _rs_rdir="$(_plugin_repos_rel_dir)"
551
+
552
+ # Space-separated list of disabled repositories (local config overrides repo config)
553
+ _rs_disabled="$(yaml_list_repos 2>/dev/null | while IFS= read -r _rs_r; do
554
+ [ -n "$_rs_r" ] || continue
555
+ yaml_repo_enabled "$_rs_r" || printf '%s ' "$_rs_r"
556
+ done)"
557
+
558
+ _rs_out="$(
559
+ if [ "$#" -gt 0 ]; then
560
+ printf '%s\n' "$1"
561
+ else
562
+ cat
563
+ fi | (
564
+ cd "$_rs_root" || exit 0
565
+ while IFS= read -r _rs_e || [ -n "$_rs_e" ]; do
566
+ _rs_e="$(printf '%s' "$_rs_e" | tr -d '\r' | sed -e 's/^[[:space:]]*//' -e 's/[[:space:]]*$//')"
567
+ while [ "${_rs_e#./}" != "$_rs_e" ]; do _rs_e="${_rs_e#./}"; done
568
+ [ "$_rs_e" = "." ] || _rs_e="${_rs_e%/}"
569
+ [ -n "$_rs_e" ] || continue
570
+ _src_resolve_entry "$_rs_e"
571
+ done
572
+ ) | sed -e 's#^\(\./\)*##' | awk -v prefix="$_rs_rdir/" -v disabled="$_rs_disabled" '
573
+ BEGIN {
574
+ n = split(disabled, d, " ")
575
+ for (i = 1; i <= n; i++) off[d[i]] = 1
576
+ }
577
+ length($0) == 0 { next }
578
+ index($0, prefix) == 1 {
579
+ name = substr($0, length(prefix) + 1)
580
+ sub(/\/.*/, "", name)
581
+ if (name in off) next
582
+ }
583
+ { print }
584
+ ' | sort -u
585
+ )"
586
+
587
+ WORKSPACE_YAML="$_saved_wy"
588
+ WORKSPACE_LOCAL_YAML="$_saved_wly"
589
+
590
+ printf '%s\n' "$_rs_out" | awk 'NF'
591
+ }
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