@ainova-systems/intelligence 0.19.0 → 0.20.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.
- package/cli/commands/registry.sh +1 -1
- package/cli/commands/source.sh +1 -1
- package/cli/commands/sync.sh +78 -6
- package/cli/intelligence +15 -2
- package/cli/internal/convert-legacy.sh +1 -1
- package/cli/internal/package-search.sh +1 -1
- package/cli/internal/package-update.sh +2 -2
- package/cli/internal/restore.sh +3 -2
- package/cli/lib/cli-common.sh +145 -33
- package/cli/lib/lockfile.sh +14 -4
- package/cli/lib/manifest.sh +147 -1
- package/cli/lib/qmap.awk +97 -32
- package/cli/lib/registry.sh +1 -1
- package/cli/lib/sync-cache.sh +596 -142
- package/cli/lib/sync-lock.sh +19 -2
- package/engine/ENGINE_SHA +1 -1
- package/engine/VERSION +1 -1
- package/engine/adapters/agents.sh +5 -2
- package/engine/adapters/antigravity.sh +3 -1
- package/engine/adapters/claude.sh +8 -2
- package/engine/adapters/codex.sh +3 -1
- package/engine/adapters/copilot.sh +8 -2
- package/engine/adapters/cursor.sh +7 -1
- package/engine/adapters/opencode.sh +7 -1
- package/engine/adapters/pi.sh +3 -1
- package/engine/lib/adapter-contract.sh +36 -4
- package/engine/lib/common.sh +665 -121
- package/engine/lib/contract.sh +5 -0
- package/engine/sync.sh +119 -28
- package/package.json +1 -1
- package/packages/sync/references/adapters.md +6 -1
- package/packages/sync/references/conventions.md +4 -1
package/engine/lib/common.sh
CHANGED
|
@@ -452,8 +452,11 @@ copy_md_with_quoted_frontmatter() {
|
|
|
452
452
|
# ship support files beside SKILL.md, and SKILL.md bodies point at them by
|
|
453
453
|
# relative path, so dropping them breaks the skill at runtime). Markdown is
|
|
454
454
|
# normalized to LF; every other file is copied byte-for-byte so potentially
|
|
455
|
-
# binary assets survive.
|
|
456
|
-
#
|
|
455
|
+
# binary assets survive. A symlink never reaches the output: the plan
|
|
456
|
+
# skill_source_inventory made turns a link inside its source into the regular
|
|
457
|
+
# file or directory it points at and leaves any other out, and the flush
|
|
458
|
+
# removes a link no plan covered (a copy from outside every skills source), so
|
|
459
|
+
# no host file outside the source is ever copied in (decision 0017).
|
|
457
460
|
#
|
|
458
461
|
# SKILL.md frontmatter is quoted for EVERY consumer, not only the strict-YAML
|
|
459
462
|
# ones. `argument-hint: [pr-number]` is a YAML *flow sequence*, so an unquoted
|
|
@@ -470,7 +473,7 @@ copy_md_with_quoted_frontmatter() {
|
|
|
470
473
|
# Usage: copy_skill_bundle "src/skill/dir" "dest/skill/dir"
|
|
471
474
|
copy_skill_bundle() {
|
|
472
475
|
_skill_bundles_reset
|
|
473
|
-
_skill_bundle_stage "$1" "$2"
|
|
476
|
+
_skill_bundle_stage "$1" "$2" || return 1
|
|
474
477
|
_skill_bundles_flush open
|
|
475
478
|
}
|
|
476
479
|
|
|
@@ -478,7 +481,8 @@ copy_skill_bundle() {
|
|
|
478
481
|
# copies every source skill directory into <dest_root>/<skill-name>, then one
|
|
479
482
|
# awk pass quotes and finalizes every bundled markdown file across all
|
|
480
483
|
# bundles. A later source with the same skill name overwrites file-by-file in
|
|
481
|
-
# order, exactly like the sequential per-bundle copies did.
|
|
484
|
+
# order, exactly like the sequential per-bundle copies did. A skill with links
|
|
485
|
+
# to resolve is copied from its materialized stage, in the same order.
|
|
482
486
|
copy_skill_bundle_dirs() {
|
|
483
487
|
copy_skill_bundle_dirs_for open "$@"
|
|
484
488
|
}
|
|
@@ -489,13 +493,27 @@ copy_skill_bundle_dirs_for() {
|
|
|
489
493
|
local effort_tool="$1" dest_root="$2"
|
|
490
494
|
shift 2
|
|
491
495
|
[ "$#" -gt 0 ] || return 0
|
|
492
|
-
local src dest
|
|
496
|
+
local src dest linked=0
|
|
493
497
|
local -a srcs=()
|
|
494
498
|
for src in "$@"; do
|
|
495
|
-
|
|
499
|
+
src="${src%/}"
|
|
500
|
+
srcs+=("$src")
|
|
501
|
+
! _ski_planned "$src" || linked=1
|
|
496
502
|
done
|
|
497
503
|
mkdir -p "$dest_root"
|
|
498
|
-
|
|
504
|
+
if [ "$linked" = 0 ]; then
|
|
505
|
+
cp -R "${srcs[@]}" "$dest_root/"
|
|
506
|
+
else
|
|
507
|
+
for src in "${srcs[@]}"; do
|
|
508
|
+
if _ski_planned "$src"; then
|
|
509
|
+
_skill_bundle_materialize "$src" || return 1
|
|
510
|
+
cp -R "$IS_SKI_STAGED" "$dest_root/" || return 1
|
|
511
|
+
rm -rf "${IS_SKI_STAGED%/*}"
|
|
512
|
+
else
|
|
513
|
+
cp -R "$src" "$dest_root/" || return 1
|
|
514
|
+
fi
|
|
515
|
+
done
|
|
516
|
+
fi
|
|
499
517
|
_skill_bundles_reset
|
|
500
518
|
for src in "${srcs[@]}"; do
|
|
501
519
|
dest="$dest_root/${src##*/}"
|
|
@@ -513,24 +531,63 @@ _skill_bundles_reset() {
|
|
|
513
531
|
_skill_bundle_stage() {
|
|
514
532
|
local src="${1%/}" dest="$2"
|
|
515
533
|
mkdir -p "$dest"
|
|
516
|
-
|
|
534
|
+
if _ski_planned "$src"; then
|
|
535
|
+
_skill_bundle_materialize "$src" || return 1
|
|
536
|
+
cp -R "$IS_SKI_STAGED/." "$dest/" || return 1
|
|
537
|
+
rm -rf "${IS_SKI_STAGED%/*}"
|
|
538
|
+
else
|
|
539
|
+
cp -R "$src/." "$dest/"
|
|
540
|
+
fi
|
|
517
541
|
_skill_bundle_note "$dest"
|
|
518
542
|
}
|
|
519
543
|
|
|
544
|
+
# _skill_bundle_materialize <skill_dir> — copy a skill whose plan holds links
|
|
545
|
+
# into a fresh stage and apply the plan there, parents first: each planned link
|
|
546
|
+
# is replaced by a copy of the file or directory it resolves to inside its
|
|
547
|
+
# source, or removed. Sets IS_SKI_STAGED to <stage>/<name>; the caller copies
|
|
548
|
+
# it on and removes the stage. Spawns per link, never per file — a source
|
|
549
|
+
# without links never comes here.
|
|
550
|
+
_skill_bundle_materialize() {
|
|
551
|
+
local src="$1" key stage out rec rel act phys first=1
|
|
552
|
+
_ski_key_var "$src"
|
|
553
|
+
key="$_SKI_KEY"
|
|
554
|
+
stage="$(mktemp -d -t intelligence-skill-XXXXXX)" || return 1
|
|
555
|
+
out="$stage/${key##*/}"
|
|
556
|
+
while IFS= read -r rec; do
|
|
557
|
+
case "$rec" in
|
|
558
|
+
"$key$IS_SKI_US"*) ;;
|
|
559
|
+
*) continue ;;
|
|
560
|
+
esac
|
|
561
|
+
rec="${rec#"$key$IS_SKI_US"}"
|
|
562
|
+
rel="${rec%%"$IS_SKI_US"*}"
|
|
563
|
+
rec="${rec#*"$IS_SKI_US"}"
|
|
564
|
+
act="${rec%%"$IS_SKI_US"*}"
|
|
565
|
+
phys="${rec#*"$IS_SKI_US"}"
|
|
566
|
+
if [ "$first" = 1 ]; then
|
|
567
|
+
first=0
|
|
568
|
+
if [ -z "$rel" ]; then
|
|
569
|
+
# The skill directory itself is the link.
|
|
570
|
+
mkdir "$out" && cp -R "$phys/." "$out/" || return 1
|
|
571
|
+
continue
|
|
572
|
+
fi
|
|
573
|
+
cp -R "$src" "$stage/" || return 1
|
|
574
|
+
fi
|
|
575
|
+
# The copy holds the link itself here; `rm -f` never follows it and
|
|
576
|
+
# refuses a directory, so a plan that no longer matches stops the run.
|
|
577
|
+
rm -f "$out/$rel" || return 1
|
|
578
|
+
case "$act" in
|
|
579
|
+
f) cp "$phys" "$out/$rel" || return 1 ;;
|
|
580
|
+
d) mkdir "$out/$rel" && cp -R "$phys/." "$out/$rel/" || return 1 ;;
|
|
581
|
+
esac
|
|
582
|
+
done <<< "$IS_SKI_PLAN"
|
|
583
|
+
IS_SKI_STAGED="$out"
|
|
584
|
+
}
|
|
585
|
+
|
|
520
586
|
# Record a staged bundle for the flush pass: mark its SKILL.md for
|
|
521
587
|
# frontmatter quoting and deduplicate the destination.
|
|
522
588
|
_skill_bundle_note() {
|
|
523
589
|
local dest="$1"
|
|
524
|
-
|
|
525
|
-
# symlink. `[ -f ]` follows links, so quoting it would read the link's
|
|
526
|
-
# TARGET and write that content into a real file, turning
|
|
527
|
-
# `skills/x/SKILL.md -> /etc/…` into a copy of a host file inside the
|
|
528
|
-
# output. That is the leak the symlink-preserving copy exists to
|
|
529
|
-
# prevent, so skip the rewrite and say so (the same reason
|
|
530
|
-
# `find -type f` in the flush never matches a symlink).
|
|
531
|
-
if [ -L "$dest/SKILL.md" ]; then
|
|
532
|
-
echo " WARN: ${dest##*/}/SKILL.md is a symlink — emitted as-is (frontmatter not quoted, tokens not expanded)" >&2
|
|
533
|
-
elif [ -f "$dest/SKILL.md" ]; then
|
|
590
|
+
if [ -f "$dest/SKILL.md" ]; then
|
|
534
591
|
_SB_QUOTE_LIST="$_SB_QUOTE_LIST$dest/SKILL.md"$'\n'
|
|
535
592
|
fi
|
|
536
593
|
# The same skill name from a later source overwrites the earlier copy;
|
|
@@ -547,11 +604,25 @@ _skill_bundle_note() {
|
|
|
547
604
|
# _skill_bundles_flush <effort-tool>
|
|
548
605
|
_skill_bundles_flush() {
|
|
549
606
|
[ "${#_SB_DESTS[@]}" -gt 0 ] || return 0
|
|
550
|
-
local -a mds=()
|
|
551
|
-
local f quote_list="$_SB_QUOTE_LIST"
|
|
607
|
+
local -a mds=() links=()
|
|
608
|
+
local f listing quote_list="$_SB_QUOTE_LIST"
|
|
609
|
+
# Captured, not streamed: a listing cut short could leave a link behind.
|
|
610
|
+
listing="$(find "${_SB_DESTS[@]}" \( -type l -o -type f -name '*.md' \) -print)" || {
|
|
611
|
+
echo "ERROR: could not list the skill copies under ${_SB_DESTS[0]%/*}" >&2
|
|
612
|
+
return 1
|
|
613
|
+
}
|
|
552
614
|
while IFS= read -r f; do
|
|
553
|
-
[ -n "$f" ]
|
|
554
|
-
|
|
615
|
+
[ -n "$f" ] || continue
|
|
616
|
+
if [ -L "$f" ]; then links+=("$f"); else mds+=("$f"); fi
|
|
617
|
+
done <<< "$listing"
|
|
618
|
+
# A link only a copy from outside every skills source can bring: no plan
|
|
619
|
+
# resolved it, so it is not followed but removed.
|
|
620
|
+
if [ "${#links[@]}" -gt 0 ]; then
|
|
621
|
+
rm -f "${links[@]}" || return 1
|
|
622
|
+
for f in "${links[@]}"; do
|
|
623
|
+
echo "WARNING: ${f#"${REPO_ROOT:-}"/} was copied as a symlink from outside every skills source — removed; generated output never holds one" >&2
|
|
624
|
+
done
|
|
625
|
+
fi
|
|
555
626
|
_skill_bundles_reset
|
|
556
627
|
[ "${#mds[@]}" -gt 0 ] || return 0
|
|
557
628
|
effort_map_var "$1"
|
|
@@ -711,19 +782,15 @@ sync_open_skill_dirs() {
|
|
|
711
782
|
# when the author's file sets none, and a file whose policy says otherwise, or
|
|
712
783
|
# that sync cannot read as saying it, refuses the render — an owner-only skill
|
|
713
784
|
# Codex can still select is the exact failure the field exists to prevent, so
|
|
714
|
-
# it never passes as a success.
|
|
785
|
+
# it never passes as a success. The tree holds no symlink — copy_skill_bundle_dirs
|
|
786
|
+
# renders a link inside its source as a regular file and leaves any other out —
|
|
787
|
+
# so a skill whose SKILL.md or agents/openai.yaml is a link in its source is
|
|
788
|
+
# read and enforced like any other.
|
|
715
789
|
open_skill_invocation_policies() {
|
|
716
790
|
local output_dir="$1" path dir flag rows refusals
|
|
717
791
|
local -a skill_mds=() policy_dirs=() policies=() authored=()
|
|
718
792
|
for path in "$output_dir"/*/SKILL.md; do
|
|
719
|
-
|
|
720
|
-
# A link is emitted as-is (see _skill_bundle_note): reading through it
|
|
721
|
-
# or writing beside it would reach outside the output tree.
|
|
722
|
-
if [ -L "$dir" ] || [ -L "$path" ]; then
|
|
723
|
-
echo " WARN: ${dir##*/}/SKILL.md is reached through a symlink — Codex gets no invocation policy derived for it" >&2
|
|
724
|
-
elif [ -f "$path" ]; then
|
|
725
|
-
skill_mds+=("$path")
|
|
726
|
-
fi
|
|
793
|
+
[ ! -f "$path" ] || skill_mds+=("$path")
|
|
727
794
|
done
|
|
728
795
|
[ "${#skill_mds[@]}" -gt 0 ] || return 0
|
|
729
796
|
# Captured, not streamed: a reader cut short must fail the render rather
|
|
@@ -736,10 +803,6 @@ open_skill_invocation_policies() {
|
|
|
736
803
|
[ "$flag" = "true" ] || continue
|
|
737
804
|
dir="${path%/SKILL.md}"
|
|
738
805
|
path="$dir/agents"
|
|
739
|
-
if [ -L "$path" ] || [ -L "$path/openai.yaml" ]; then
|
|
740
|
-
echo " WARN: ${dir##*/}/agents/openai.yaml is a symlink — left as-is, its invocation policy is not enforced" >&2
|
|
741
|
-
continue
|
|
742
|
-
fi
|
|
743
806
|
if [ -s "$path/openai.yaml" ]; then
|
|
744
807
|
authored+=("$path/openai.yaml")
|
|
745
808
|
continue
|
|
@@ -860,6 +923,8 @@ enforce_authored_invocation_policies() {
|
|
|
860
923
|
# Lint YAML frontmatter for common pitfalls (unquoted colons, leading tabs).
|
|
861
924
|
# Print warnings to stderr; do not fail. Strict consumers (Codex CLI) reject
|
|
862
925
|
# these files with cryptic messages — catching them in sync gives better DX.
|
|
926
|
+
# Each finding is a `WARNING:` line: the author has to edit the source, so
|
|
927
|
+
# `sync --compact` (the first sync `init` runs) must keep it.
|
|
863
928
|
# Batched: one awk process lints every file passed.
|
|
864
929
|
# The engine lints every source once, so this is also where an `effort:` off
|
|
865
930
|
# IS_EFFORT_LEVELS is reported — read with frontmatter_index's semantics,
|
|
@@ -892,7 +957,7 @@ lint_frontmatter_files() {
|
|
|
892
957
|
}
|
|
893
958
|
}
|
|
894
959
|
in_fm && /^\t/ {
|
|
895
|
-
printf "
|
|
960
|
+
printf "WARNING: %s:%d leading tab in frontmatter (use spaces)\n", FILENAME, FNR > "/dev/stderr"
|
|
896
961
|
}
|
|
897
962
|
in_fm && /^[a-zA-Z0-9_-]+:[[:space:]]+[^"\047|>[{]/ {
|
|
898
963
|
value_start = index($0, ":") + 1
|
|
@@ -900,10 +965,10 @@ lint_frontmatter_files() {
|
|
|
900
965
|
sub(/^[[:space:]]+/, "", value)
|
|
901
966
|
if (value ~ /:[[:space:]]/ || value ~ /:$/) {
|
|
902
967
|
col = index(value, ":") + value_start
|
|
903
|
-
printf "
|
|
968
|
+
printf "WARNING: %s:%d unquoted colon in value at column %d — wrap value in quotes\n", FILENAME, FNR, col > "/dev/stderr"
|
|
904
969
|
}
|
|
905
970
|
if (value ~ /"/) {
|
|
906
|
-
printf "
|
|
971
|
+
printf "WARNING: %s:%d literal double quote in unquoted value — wrap value in single quotes or escape as \\\" so strict-YAML targets accept it\n", FILENAME, FNR > "/dev/stderr"
|
|
907
972
|
}
|
|
908
973
|
}
|
|
909
974
|
# Field-length limits. Both Claude Code and the Agent Skills standard
|
|
@@ -922,7 +987,7 @@ lint_frontmatter_files() {
|
|
|
922
987
|
}
|
|
923
988
|
limit = (key == "name") ? 64 : 1024
|
|
924
989
|
if (length(val) > limit) {
|
|
925
|
-
printf "
|
|
990
|
+
printf "WARNING: %s:%d %s is %d chars — over the %d-char limit; the skill/agent will be REJECTED at load time\n", FILENAME, FNR, key, length(val), limit > "/dev/stderr"
|
|
926
991
|
}
|
|
927
992
|
}
|
|
928
993
|
' "$@"
|
|
@@ -1561,7 +1626,8 @@ warn_unsynced() {
|
|
|
1561
1626
|
load_yaml_list "$config_file" "$section"
|
|
1562
1627
|
while IFS= read -r src; do
|
|
1563
1628
|
[ -z "$src" ] && continue
|
|
1564
|
-
|
|
1629
|
+
normalize_path_var "$repo_root/$src"
|
|
1630
|
+
all_sources+=("${IS_NORM_PATH#"$repo_root/"}")
|
|
1565
1631
|
done <<< "$IS_YAML_LIST"
|
|
1566
1632
|
done
|
|
1567
1633
|
|
|
@@ -1655,11 +1721,7 @@ warn_unsynced() {
|
|
|
1655
1721
|
done
|
|
1656
1722
|
|
|
1657
1723
|
if [ "$matched" = false ]; then
|
|
1658
|
-
|
|
1659
|
-
echo ""
|
|
1660
|
-
echo "=== WARNING: Unsynced directories ==="
|
|
1661
|
-
fi
|
|
1662
|
-
echo " NOT SYNCED: $rel_dir"
|
|
1724
|
+
echo "WARNING: NOT SYNCED: $rel_dir" >&2
|
|
1663
1725
|
warnings=$((warnings + 1))
|
|
1664
1726
|
fi
|
|
1665
1727
|
done < <(find "$repo_root" \( -name ".git" -o -name "node_modules" -o -name "vendor" -o -name "dist" -o -name ".claude" -o -name ".cursor" -o -name ".github" -o -name ".codex" -o -name ".agents" -o -name ".intelligence" \) -prune -o -type d \( -name "rules" -o -name "agents" -o -name "skills" -o -name "Rules" -o -name "Agents" -o -name "Skills" \) -print 2>/dev/null)
|
|
@@ -1670,13 +1732,366 @@ warn_unsynced() {
|
|
|
1670
1732
|
# and hand-placing an entry in an ordered list decides which artifact
|
|
1671
1733
|
# wins. The engine may also run without the CLI around it.
|
|
1672
1734
|
if [ "${IS_CLI:-0}" = "1" ]; then
|
|
1673
|
-
echo "
|
|
1735
|
+
echo "WARNING: Wire one in: intelligence source add <rules|agents|skills> <path>" >&2
|
|
1674
1736
|
else
|
|
1675
|
-
echo "
|
|
1737
|
+
echo "WARNING: Add these paths to sources: in ${config_file##*/}" >&2
|
|
1676
1738
|
fi
|
|
1677
1739
|
fi
|
|
1678
1740
|
}
|
|
1679
1741
|
|
|
1742
|
+
# --- Skill sources and the symlinks inside them (decision 0017) ---
|
|
1743
|
+
#
|
|
1744
|
+
# A symlink in a skills source renders as the regular file or directory it
|
|
1745
|
+
# resolves to while that stays inside the same source, and is left out with a
|
|
1746
|
+
# WARNING: line when it resolves outside it, nowhere, or to a directory that
|
|
1747
|
+
# encloses it. Generated output never holds a link, and nothing is ever copied
|
|
1748
|
+
# from outside the source (decision 0006). Every consumer takes that decision
|
|
1749
|
+
# from here, once per run: the skill list read_source_artifact_files returns,
|
|
1750
|
+
# the membership the glob-listing adapters check (skill_source_rendered), and
|
|
1751
|
+
# the plan copy_skill_bundle_dirs applies to its copy. A skill rendered from a
|
|
1752
|
+
# link is an ordinary skill from then on — quoting, token expansion, the
|
|
1753
|
+
# effort rendering and the Codex invocation policy all run on the output copy.
|
|
1754
|
+
#
|
|
1755
|
+
# skill_source_inventory <repo_root> <config_file> fills, in the caller's
|
|
1756
|
+
# shell and fail-closed like read_source_artifact_files:
|
|
1757
|
+
# IS_SKI_LIST <source>/<skill>/SKILL.md of every rendered skill, one a line,
|
|
1758
|
+
# sources in manifest order, names in byte order;
|
|
1759
|
+
# IS_SKI_KEYS "\n<source>/<skill>\n…", the same skills as a membership set;
|
|
1760
|
+
# IS_SKI_PLAN "\n" + one record a line for every link in a rendered skill,
|
|
1761
|
+
# parents first: <source>/<skill> US <path in the skill copy>
|
|
1762
|
+
# US f|d|x US <physical target>. f and d materialize the file or
|
|
1763
|
+
# directory at that path, x leaves the link out; an empty path
|
|
1764
|
+
# is the skill directory itself.
|
|
1765
|
+
# The resolution spawns processes only for a source that holds a link, and
|
|
1766
|
+
# then per source and per link-chain hop, never per file.
|
|
1767
|
+
IS_SKI_US=$'\x1f'
|
|
1768
|
+
|
|
1769
|
+
skill_source_inventory() {
|
|
1770
|
+
local repo_root="$1" config_file="$2" memo_key src dir listing
|
|
1771
|
+
memo_key="$repo_root"$'\n'"$config_file"
|
|
1772
|
+
if [ "${IS_SOURCE_FILES_MEMO:-0}" = 1 ] && [ "${IS_SKI_KEY:-}" = "$memo_key" ]; then
|
|
1773
|
+
return 0
|
|
1774
|
+
fi
|
|
1775
|
+
IS_SKI_KEY=""
|
|
1776
|
+
IS_SKI_LIST=""
|
|
1777
|
+
IS_SKI_KEYS=$'\n'
|
|
1778
|
+
IS_SKI_PLAN=$'\n'
|
|
1779
|
+
load_yaml_list "$config_file" "skills"
|
|
1780
|
+
while IFS= read -r src; do
|
|
1781
|
+
[ -n "$src" ] || continue
|
|
1782
|
+
dir="$repo_root/$src"
|
|
1783
|
+
while [ "${dir%/}" != "$dir" ]; do dir="${dir%/}"; done
|
|
1784
|
+
[ -d "$dir" ] || continue
|
|
1785
|
+
# Every directory and link at any depth: the top level holds the
|
|
1786
|
+
# skills, and a link anywhere below one is part of its bundle. -H
|
|
1787
|
+
# lists a source that is itself a link through it, as a glob does.
|
|
1788
|
+
# Command substitution, so find or sort failing stops the run here
|
|
1789
|
+
# instead of yielding a short list.
|
|
1790
|
+
listing="$(find -H "$dir" -mindepth 1 \( -type d -o -type l \) -print | LC_ALL=C sort)" || {
|
|
1791
|
+
echo "ERROR: cannot enumerate skills under '$dir' — refusing to render a partial list." >&2
|
|
1792
|
+
exit 1
|
|
1793
|
+
}
|
|
1794
|
+
[ -n "$listing" ] || continue
|
|
1795
|
+
_ski_source "$repo_root" "$dir" "$listing"
|
|
1796
|
+
done <<< "$IS_YAML_LIST"
|
|
1797
|
+
[ "${IS_SOURCE_FILES_MEMO:-0}" != 1 ] || IS_SKI_KEY="$memo_key"
|
|
1798
|
+
}
|
|
1799
|
+
|
|
1800
|
+
# skill_source_rendered <skill_dir> — the inventory renders this skill. For
|
|
1801
|
+
# adapters that list a skills source with their own glob.
|
|
1802
|
+
skill_source_rendered() {
|
|
1803
|
+
_ski_key_var "$1"
|
|
1804
|
+
case "${IS_SKI_KEYS:-}" in
|
|
1805
|
+
*$'\n'"$_SKI_KEY"$'\n'*) return 0 ;;
|
|
1806
|
+
esac
|
|
1807
|
+
return 1
|
|
1808
|
+
}
|
|
1809
|
+
|
|
1810
|
+
# _ski_key_var <skill_dir> — the key a skill is listed and planned under, its
|
|
1811
|
+
# source and name joined by one slash however the caller spelled a trailing
|
|
1812
|
+
# slash on either.
|
|
1813
|
+
_ski_key_var() {
|
|
1814
|
+
local path="$1" parent
|
|
1815
|
+
while [ "${path%/}" != "$path" ]; do path="${path%/}"; done
|
|
1816
|
+
parent="${path%/*}"
|
|
1817
|
+
while [ -n "$parent" ] && [ "${parent%/}" != "$parent" ]; do parent="${parent%/}"; done
|
|
1818
|
+
_SKI_KEY="$parent/${path##*/}"
|
|
1819
|
+
}
|
|
1820
|
+
|
|
1821
|
+
# _ski_planned <skill_dir> — the skill has links the copy has to resolve.
|
|
1822
|
+
_ski_planned() {
|
|
1823
|
+
_ski_key_var "$1"
|
|
1824
|
+
case "${IS_SKI_PLAN:-}" in
|
|
1825
|
+
*$'\n'"$_SKI_KEY$IS_SKI_US"*) return 0 ;;
|
|
1826
|
+
esac
|
|
1827
|
+
return 1
|
|
1828
|
+
}
|
|
1829
|
+
|
|
1830
|
+
# _ski_source <repo_root> <source_dir> <listing> — inventory one skills source.
|
|
1831
|
+
_ski_source() {
|
|
1832
|
+
local repo_root="$1" f rel
|
|
1833
|
+
local -a tops=() links=()
|
|
1834
|
+
_SKI_DIR="$2"
|
|
1835
|
+
_SKI_ROOT_REL="${2#"$repo_root"/}"
|
|
1836
|
+
_SKI_REPO="$repo_root"
|
|
1837
|
+
while IFS= read -r f; do
|
|
1838
|
+
[ -n "$f" ] || continue
|
|
1839
|
+
rel="${f#"$_SKI_DIR"/}"
|
|
1840
|
+
case "$rel" in
|
|
1841
|
+
*/*) [ ! -L "$f" ] || links+=("$f") ;;
|
|
1842
|
+
*)
|
|
1843
|
+
tops+=("$f")
|
|
1844
|
+
[ ! -L "$f" ] || links+=("$f")
|
|
1845
|
+
;;
|
|
1846
|
+
esac
|
|
1847
|
+
done <<< "$3"
|
|
1848
|
+
_SKI_LREL=(); _SKI_LSTAT=(); _SKI_LKIND=(); _SKI_LTREL=(); _SKI_LPHYS=()
|
|
1849
|
+
[ "${#links[@]}" -eq 0 ] || _ski_classify_links "${links[@]}"
|
|
1850
|
+
for f in "${tops[@]+"${tops[@]}"}"; do
|
|
1851
|
+
_ski_skill "${f##*/}"
|
|
1852
|
+
done
|
|
1853
|
+
}
|
|
1854
|
+
|
|
1855
|
+
# _ski_classify_links <link>... — resolve every link of the current source and
|
|
1856
|
+
# record where it leads: ok (inside the source), outside, dangling, or other
|
|
1857
|
+
# (neither a file nor a directory). A link that cannot be resolved stops the
|
|
1858
|
+
# run, like any enumeration that cannot answer.
|
|
1859
|
+
_ski_classify_links() {
|
|
1860
|
+
local out root_phys rec kind phys i=0 link
|
|
1861
|
+
local -a recs=()
|
|
1862
|
+
out="$(_ski_physical "$_SKI_DIR" "$@")" || {
|
|
1863
|
+
echo "ERROR: cannot resolve the symlinks under '$_SKI_DIR' — refusing to render a partial list." >&2
|
|
1864
|
+
exit 1
|
|
1865
|
+
}
|
|
1866
|
+
while IFS= read -r rec; do recs+=("$rec"); done <<< "$out"
|
|
1867
|
+
if [ "${#recs[@]}" -ne $(($# + 1)) ]; then
|
|
1868
|
+
echo "ERROR: cannot resolve the symlinks under '$_SKI_DIR' — refusing to render a partial list." >&2
|
|
1869
|
+
exit 1
|
|
1870
|
+
fi
|
|
1871
|
+
root_phys="${recs[0]%/}"
|
|
1872
|
+
for link in "$@"; do
|
|
1873
|
+
rec="${recs[i + 1]}"
|
|
1874
|
+
kind="${rec%%"$IS_SKI_US"*}"
|
|
1875
|
+
phys="${rec#*"$IS_SKI_US"}"
|
|
1876
|
+
_SKI_LREL[i]="${link#"$_SKI_DIR"/}"
|
|
1877
|
+
_SKI_LKIND[i]="$kind"
|
|
1878
|
+
_SKI_LPHYS[i]="$phys"
|
|
1879
|
+
_SKI_LTREL[i]=""
|
|
1880
|
+
case "$kind" in
|
|
1881
|
+
d|f)
|
|
1882
|
+
case "$phys" in
|
|
1883
|
+
"$root_phys"/*) _SKI_LSTAT[i]=ok; _SKI_LTREL[i]="${phys#"$root_phys"/}" ;;
|
|
1884
|
+
"${root_phys:-/}") _SKI_LSTAT[i]=ok ;;
|
|
1885
|
+
*) _SKI_LSTAT[i]=outside ;;
|
|
1886
|
+
esac
|
|
1887
|
+
;;
|
|
1888
|
+
o) _SKI_LSTAT[i]=other ;;
|
|
1889
|
+
*) _SKI_LSTAT[i]=dangling ;;
|
|
1890
|
+
esac
|
|
1891
|
+
i=$((i + 1))
|
|
1892
|
+
done
|
|
1893
|
+
}
|
|
1894
|
+
|
|
1895
|
+
# _ski_physical <source_dir> <link>... — print the source's physical path,
|
|
1896
|
+
# then one "<kind> US <physical target>" line per link: d (directory), f
|
|
1897
|
+
# (regular file), o (anything else) or x (dangling, or a link loop). Runs in a
|
|
1898
|
+
# subshell so it can change directory: `cd -P` resolves a directory, and a
|
|
1899
|
+
# file's chain is read with one readlink per hop for all links at once (no
|
|
1900
|
+
# readlink -f on macOS).
|
|
1901
|
+
_ski_physical() (
|
|
1902
|
+
local dir="$1" link target targets i k n hops=0 parent
|
|
1903
|
+
local -a kind=() cur=() phys=() pend=() next=() paths=()
|
|
1904
|
+
shift
|
|
1905
|
+
n=$#
|
|
1906
|
+
cd -P -- "$dir" 2>/dev/null || exit 1
|
|
1907
|
+
printf '%s\n' "$PWD"
|
|
1908
|
+
i=0
|
|
1909
|
+
for link in "$@"; do
|
|
1910
|
+
kind[i]=x; phys[i]=""; cur[i]="$link"
|
|
1911
|
+
if [ -d "$link" ]; then
|
|
1912
|
+
cd -P -- "$link" 2>/dev/null || exit 1
|
|
1913
|
+
kind[i]=d; phys[i]="$PWD"
|
|
1914
|
+
elif [ -f "$link" ]; then
|
|
1915
|
+
kind[i]=f; pend+=("$i")
|
|
1916
|
+
elif [ -e "$link" ]; then
|
|
1917
|
+
kind[i]=o
|
|
1918
|
+
fi
|
|
1919
|
+
i=$((i + 1))
|
|
1920
|
+
done
|
|
1921
|
+
while [ "${#pend[@]}" -gt 0 ]; do
|
|
1922
|
+
hops=$((hops + 1))
|
|
1923
|
+
[ "$hops" -le 40 ] || exit 1
|
|
1924
|
+
paths=()
|
|
1925
|
+
for i in "${pend[@]}"; do paths+=("${cur[i]}"); done
|
|
1926
|
+
targets="$(readlink "${paths[@]}")" || exit 1
|
|
1927
|
+
next=(); k=0
|
|
1928
|
+
while IFS= read -r target; do
|
|
1929
|
+
# More lines than links: a target held a newline.
|
|
1930
|
+
[ "$k" -lt "${#pend[@]}" ] || exit 1
|
|
1931
|
+
i="${pend[k]}"
|
|
1932
|
+
case "$target" in
|
|
1933
|
+
/*) cur[i]="$target" ;;
|
|
1934
|
+
*) cur[i]="${cur[i]%/*}/$target" ;;
|
|
1935
|
+
esac
|
|
1936
|
+
[ ! -L "${cur[i]}" ] || next+=("$i")
|
|
1937
|
+
k=$((k + 1))
|
|
1938
|
+
done <<< "$targets"
|
|
1939
|
+
[ "$k" -eq "${#pend[@]}" ] || exit 1
|
|
1940
|
+
pend=("${next[@]+"${next[@]}"}")
|
|
1941
|
+
done
|
|
1942
|
+
for ((i = 0; i < n; i++)); do
|
|
1943
|
+
if [ "${kind[i]}" = f ]; then
|
|
1944
|
+
parent="${cur[i]%/*}"
|
|
1945
|
+
cd -P -- "${parent:-/}" 2>/dev/null || exit 1
|
|
1946
|
+
phys[i]="${PWD%/}/${cur[i]##*/}"
|
|
1947
|
+
fi
|
|
1948
|
+
printf '%s%s%s\n' "${kind[i]}" "$IS_SKI_US" "${phys[i]}"
|
|
1949
|
+
done
|
|
1950
|
+
)
|
|
1951
|
+
|
|
1952
|
+
# _ski_link_var <path in source> — set _SKI_K to the index of the link at that
|
|
1953
|
+
# path, or -1.
|
|
1954
|
+
_ski_link_var() {
|
|
1955
|
+
local k=0
|
|
1956
|
+
_SKI_K=-1
|
|
1957
|
+
while [ "$k" -lt "${#_SKI_LREL[@]}" ]; do
|
|
1958
|
+
if [ "${_SKI_LREL[k]}" = "$1" ]; then
|
|
1959
|
+
_SKI_K="$k"
|
|
1960
|
+
return 0
|
|
1961
|
+
fi
|
|
1962
|
+
k=$((k + 1))
|
|
1963
|
+
done
|
|
1964
|
+
}
|
|
1965
|
+
|
|
1966
|
+
# _ski_loops <target> <open> — materializing the directory at <target> would
|
|
1967
|
+
# copy one of the <open> paths (newline-separated) being materialized, the
|
|
1968
|
+
# link itself included, so it would never end.
|
|
1969
|
+
_ski_loops() {
|
|
1970
|
+
local target="$1" open
|
|
1971
|
+
[ -n "$target" ] || return 0
|
|
1972
|
+
while IFS= read -r open; do
|
|
1973
|
+
case "$open" in "$target"|"$target"/*) return 0 ;; esac
|
|
1974
|
+
done <<< "$2"
|
|
1975
|
+
return 1
|
|
1976
|
+
}
|
|
1977
|
+
|
|
1978
|
+
# _ski_skill <name> — decide one top-level entry of the current source: a
|
|
1979
|
+
# rendered skill (with its plan and warnings), a skill left out, or no skill.
|
|
1980
|
+
_ski_skill() {
|
|
1981
|
+
local name="$1" key content open="" state rel link reason nl=$'\n'
|
|
1982
|
+
key="$_SKI_DIR/$name"
|
|
1983
|
+
_SKI_ENTRIES="$nl"
|
|
1984
|
+
_SKI_DROPS=""
|
|
1985
|
+
_ski_link_var "$name"
|
|
1986
|
+
if [ "$_SKI_K" -ge 0 ]; then
|
|
1987
|
+
# The skill directory is itself a link. A test through one that
|
|
1988
|
+
# leaves the source only stats it; nothing is read from outside.
|
|
1989
|
+
case "${_SKI_LSTAT[_SKI_K]}:${_SKI_LKIND[_SKI_K]}" in
|
|
1990
|
+
ok:d) ;;
|
|
1991
|
+
outside:d)
|
|
1992
|
+
_ski_warn "$name" "" "$name" outside
|
|
1993
|
+
return 0
|
|
1994
|
+
;;
|
|
1995
|
+
dangling:*)
|
|
1996
|
+
_ski_warn "$name" "" "$name" dangling
|
|
1997
|
+
return 0
|
|
1998
|
+
;;
|
|
1999
|
+
*) return 0 ;;
|
|
2000
|
+
esac
|
|
2001
|
+
content="${_SKI_LTREL[_SKI_K]}"
|
|
2002
|
+
if _ski_loops "$content" ""; then
|
|
2003
|
+
[ ! -f "$_SKI_DIR/$name/SKILL.md" ] || _ski_warn "$name" "" "$name" loop
|
|
2004
|
+
return 0
|
|
2005
|
+
fi
|
|
2006
|
+
_SKI_ENTRIES="$_SKI_ENTRIES$key$IS_SKI_US${IS_SKI_US}d$IS_SKI_US${_SKI_LPHYS[_SKI_K]}$nl"
|
|
2007
|
+
open="$nl"
|
|
2008
|
+
else
|
|
2009
|
+
content="$name"
|
|
2010
|
+
fi
|
|
2011
|
+
_ski_expand "$key" "$content" "" "$open"
|
|
2012
|
+
# A skill is a directory whose SKILL.md renders as a regular file.
|
|
2013
|
+
case "$_SKI_ENTRIES" in
|
|
2014
|
+
*"$nl$key${IS_SKI_US}SKILL.md${IS_SKI_US}f$IS_SKI_US"*) state=ok ;;
|
|
2015
|
+
*"$nl$key${IS_SKI_US}SKILL.md${IS_SKI_US}x$IS_SKI_US"*) state=dropped ;;
|
|
2016
|
+
*"$nl$key${IS_SKI_US}SKILL.md$IS_SKI_US"*) state=none ;;
|
|
2017
|
+
*)
|
|
2018
|
+
state=none
|
|
2019
|
+
[ ! -f "$_SKI_DIR/$content/SKILL.md" ] || state=ok
|
|
2020
|
+
;;
|
|
2021
|
+
esac
|
|
2022
|
+
case "$state" in
|
|
2023
|
+
none) return 0 ;;
|
|
2024
|
+
dropped)
|
|
2025
|
+
while IFS="$IS_SKI_US" read -r rel link reason; do
|
|
2026
|
+
[ "$rel" != "SKILL.md" ] || _ski_warn "$name" "" "$link" "$reason"
|
|
2027
|
+
done <<< "$_SKI_DROPS"
|
|
2028
|
+
return 0
|
|
2029
|
+
;;
|
|
2030
|
+
esac
|
|
2031
|
+
IS_SKI_LIST="$IS_SKI_LIST$_SKI_DIR/$name/SKILL.md$nl"
|
|
2032
|
+
IS_SKI_KEYS="$IS_SKI_KEYS$key$nl"
|
|
2033
|
+
IS_SKI_PLAN="$IS_SKI_PLAN${_SKI_ENTRIES#"$nl"}"
|
|
2034
|
+
while IFS="$IS_SKI_US" read -r rel link reason; do
|
|
2035
|
+
[ -z "$rel" ] || _ski_warn "$name" "$rel" "$link" "$reason"
|
|
2036
|
+
done <<< "$_SKI_DROPS"
|
|
2037
|
+
}
|
|
2038
|
+
|
|
2039
|
+
# _ski_expand <key> <content> <prefix> <open> — plan every link inside the
|
|
2040
|
+
# source directory <content> as it lands at <prefix> in the skill copy,
|
|
2041
|
+
# recursing into each directory a link materializes. <open> holds the
|
|
2042
|
+
# locations of the links already being materialized, for _ski_loops.
|
|
2043
|
+
_ski_expand() {
|
|
2044
|
+
local key="$1" content="$2" prefix="$3" open="$4" k=0 rel out location
|
|
2045
|
+
while [ "$k" -lt "${#_SKI_LREL[@]}" ]; do
|
|
2046
|
+
rel="${_SKI_LREL[k]}"
|
|
2047
|
+
case "$rel" in
|
|
2048
|
+
"$content"/*) ;;
|
|
2049
|
+
*) k=$((k + 1)); continue ;;
|
|
2050
|
+
esac
|
|
2051
|
+
out="${prefix:+$prefix/}${rel#"$content"/}"
|
|
2052
|
+
case "${_SKI_LSTAT[k]}:${_SKI_LKIND[k]}" in
|
|
2053
|
+
ok:f)
|
|
2054
|
+
_SKI_ENTRIES="$_SKI_ENTRIES$key$IS_SKI_US$out${IS_SKI_US}f$IS_SKI_US${_SKI_LPHYS[k]}"$'\n'
|
|
2055
|
+
;;
|
|
2056
|
+
ok:d)
|
|
2057
|
+
location="${rel%/*}"
|
|
2058
|
+
if _ski_loops "${_SKI_LTREL[k]}" "$open$location"; then
|
|
2059
|
+
_ski_drop "$key" "$out" "$rel" loop
|
|
2060
|
+
else
|
|
2061
|
+
_SKI_ENTRIES="$_SKI_ENTRIES$key$IS_SKI_US$out${IS_SKI_US}d$IS_SKI_US${_SKI_LPHYS[k]}"$'\n'
|
|
2062
|
+
_ski_expand "$key" "${_SKI_LTREL[k]}" "$out" "$open$location"$'\n'
|
|
2063
|
+
fi
|
|
2064
|
+
;;
|
|
2065
|
+
*) _ski_drop "$key" "$out" "$rel" "${_SKI_LSTAT[k]}" ;;
|
|
2066
|
+
esac
|
|
2067
|
+
k=$((k + 1))
|
|
2068
|
+
done
|
|
2069
|
+
}
|
|
2070
|
+
|
|
2071
|
+
# _ski_drop <key> <path in copy> <path in source> <reason> — leave a link out.
|
|
2072
|
+
_ski_drop() {
|
|
2073
|
+
_SKI_ENTRIES="$_SKI_ENTRIES$1$IS_SKI_US$2${IS_SKI_US}x$IS_SKI_US"$'\n'
|
|
2074
|
+
_SKI_DROPS="$_SKI_DROPS$2$IS_SKI_US$3$IS_SKI_US$4"$'\n'
|
|
2075
|
+
}
|
|
2076
|
+
|
|
2077
|
+
# _ski_warn <skill> <path in copy> <path in source> <reason> — one WARNING:
|
|
2078
|
+
# line naming the skill and the link by its own path. An empty <path in copy>
|
|
2079
|
+
# means the whole skill is left out.
|
|
2080
|
+
_ski_warn() {
|
|
2081
|
+
local link="$_SKI_ROOT_REL/$3" what
|
|
2082
|
+
case "$4" in
|
|
2083
|
+
outside) what="is a symlink that resolves outside $_SKI_ROOT_REL" ;;
|
|
2084
|
+
dangling) what="is a dangling symlink" ;;
|
|
2085
|
+
loop) what="is a symlink to a directory that encloses it" ;;
|
|
2086
|
+
*) what="is a symlink to neither a file nor a directory" ;;
|
|
2087
|
+
esac
|
|
2088
|
+
if [ -n "$2" ]; then
|
|
2089
|
+
echo "WARNING: skill '$1' renders without $2: $link $what" >&2
|
|
2090
|
+
else
|
|
2091
|
+
echo "WARNING: skill '$1' is left out of every output: $link $what" >&2
|
|
2092
|
+
fi
|
|
2093
|
+
}
|
|
2094
|
+
|
|
1680
2095
|
# Enumerate one manifest source kind using the same depth and ordering
|
|
1681
2096
|
# everywhere. Source-list order is significant; entries inside each directory
|
|
1682
2097
|
# use byte-order sorting for cross-platform determinism. The result lands in
|
|
@@ -1696,6 +2111,9 @@ warn_unsynced() {
|
|
|
1696
2111
|
# replay that answer. Only the engine sets it: a sync never writes into a
|
|
1697
2112
|
# source (validate_output_path refuses), so each caller would otherwise re-run
|
|
1698
2113
|
# the same find and sort for every source directory.
|
|
2114
|
+
#
|
|
2115
|
+
# Skills come from skill_source_inventory, which also decides what a symlink
|
|
2116
|
+
# inside a skills source renders as.
|
|
1699
2117
|
read_source_artifact_files() {
|
|
1700
2118
|
local repo_root="$1"
|
|
1701
2119
|
local config_file="$2"
|
|
@@ -1703,8 +2121,15 @@ read_source_artifact_files() {
|
|
|
1703
2121
|
local src f dir listing memo_key="" key_var="" list_var="" joined=""
|
|
1704
2122
|
|
|
1705
2123
|
IS_SOURCE_FILES=()
|
|
2124
|
+
if [ "$section" = "skills" ]; then
|
|
2125
|
+
skill_source_inventory "$repo_root" "$config_file"
|
|
2126
|
+
while IFS= read -r f; do
|
|
2127
|
+
[ -n "$f" ] && IS_SOURCE_FILES+=("$f")
|
|
2128
|
+
done <<< "$IS_SKI_LIST"
|
|
2129
|
+
return 0
|
|
2130
|
+
fi
|
|
1706
2131
|
case "${IS_SOURCE_FILES_MEMO:-0}:$section" in
|
|
1707
|
-
1:rules|1:agents
|
|
2132
|
+
1:rules|1:agents)
|
|
1708
2133
|
key_var="IS_SF_${section}_KEY"
|
|
1709
2134
|
list_var="IS_SF_${section}_LIST"
|
|
1710
2135
|
memo_key="$repo_root"$'\n'"$config_file"
|
|
@@ -1731,12 +2156,6 @@ read_source_artifact_files() {
|
|
|
1731
2156
|
exit 1
|
|
1732
2157
|
}
|
|
1733
2158
|
;;
|
|
1734
|
-
skills)
|
|
1735
|
-
listing="$(find "$dir" -mindepth 1 -maxdepth 1 -type d -print | LC_ALL=C sort)" || {
|
|
1736
|
-
echo "ERROR: cannot enumerate $section under '$dir' — refusing to render a partial list." >&2
|
|
1737
|
-
exit 1
|
|
1738
|
-
}
|
|
1739
|
-
;;
|
|
1740
2159
|
*) continue ;;
|
|
1741
2160
|
esac
|
|
1742
2161
|
[ -n "$listing" ] || continue
|
|
@@ -1744,10 +2163,6 @@ read_source_artifact_files() {
|
|
|
1744
2163
|
# array it fills survives and any failure inside it is this shell's.
|
|
1745
2164
|
while IFS= read -r f; do
|
|
1746
2165
|
[ -n "$f" ] || continue
|
|
1747
|
-
if [ "$section" = "skills" ]; then
|
|
1748
|
-
[ -f "$f/SKILL.md" ] || continue
|
|
1749
|
-
f="$f/SKILL.md"
|
|
1750
|
-
fi
|
|
1751
2166
|
IS_SOURCE_FILES+=("$f")
|
|
1752
2167
|
joined="$joined$f"$'\n'
|
|
1753
2168
|
done <<< "$listing"
|
|
@@ -2106,8 +2521,9 @@ IS_YAML_LIST_AWK='
|
|
|
2106
2521
|
# Consults the load_yaml_list cache first: sync reads the same sections from
|
|
2107
2522
|
# the same manifest dozens of times, and each awk spawn costs tens of
|
|
2108
2523
|
# milliseconds on Windows. The cache is only ever populated by
|
|
2109
|
-
# load_yaml_list
|
|
2110
|
-
#
|
|
2524
|
+
# load_yaml_list and load_yaml_lists. The engine never mutates its manifest;
|
|
2525
|
+
# CLI writers invalidate it through qmap_memo_reset; load_yaml_lists and the
|
|
2526
|
+
# project preloader also populate it.
|
|
2111
2527
|
read_yaml_list() {
|
|
2112
2528
|
local file="$1"
|
|
2113
2529
|
local section="$2"
|
|
@@ -2215,22 +2631,17 @@ load_yaml_list() {
|
|
|
2215
2631
|
esac
|
|
2216
2632
|
}
|
|
2217
2633
|
|
|
2218
|
-
#
|
|
2219
|
-
#
|
|
2220
|
-
#
|
|
2221
|
-
#
|
|
2222
|
-
#
|
|
2223
|
-
#
|
|
2224
|
-
|
|
2225
|
-
|
|
2226
|
-
|
|
2227
|
-
|
|
2228
|
-
|
|
2229
|
-
rules|agents|skills|ignore|submodules) ;;
|
|
2230
|
-
*) return 1 ;;
|
|
2231
|
-
esac
|
|
2232
|
-
done
|
|
2233
|
-
out="$(awk -v sections="$*" "$IS_PKG_REF_AWK$IS_YAML_ITEM_AWK"'
|
|
2634
|
+
# The manifest readers below share their awk programs, so the combined pass in
|
|
2635
|
+
# load_manifest_view reads each shape exactly as its single-purpose reader does.
|
|
2636
|
+
# Rule order matters when they are joined: the targets program ends its rules
|
|
2637
|
+
# with `next`, so it runs last and never hides a line from the others.
|
|
2638
|
+
#
|
|
2639
|
+
# The lists program carries the package-reference parser with it: it sees every
|
|
2640
|
+
# line, so `packages:` is collected wherever it stands, and each source list is
|
|
2641
|
+
# expanded exactly as read_yaml_list expands it. A reference that resolves to
|
|
2642
|
+
# nothing comes out as a `!` row, which _yaml_lists_store keeps in
|
|
2643
|
+
# IS_YL_UNRESOLVED.
|
|
2644
|
+
_IS_AWK_LISTS="$IS_PKG_REF_AWK$IS_YAML_ITEM_AWK"'
|
|
2234
2645
|
BEGIN { n = split(sections, want, " ") }
|
|
2235
2646
|
{ sub(/\r$/, ""); pkr_collect($0) }
|
|
2236
2647
|
{
|
|
@@ -2261,42 +2672,33 @@ load_yaml_lists() {
|
|
|
2261
2672
|
else print "!\037" s "\037" val "\037" PKR_STATE "\037" dir "\037" PKR_ALIAS "\037" PKR_HOLDERS
|
|
2262
2673
|
}
|
|
2263
2674
|
}
|
|
2264
|
-
|
|
2265
|
-
for s in "$@"; do
|
|
2266
|
-
j=""
|
|
2267
|
-
while IFS= read -r line; do
|
|
2268
|
-
[ "${line%%$'\037'*}" = "$s" ] || continue
|
|
2269
|
-
v="${line#*$'\037'}"
|
|
2270
|
-
j="$j$v"$'\n'
|
|
2271
|
-
done <<< "$out"
|
|
2272
|
-
while [[ "$j" == *$'\n' ]]; do j="${j%$'\n'}"; done
|
|
2273
|
-
printf -v "IS_YL_${s}_FILE" '%s' "$file"
|
|
2274
|
-
printf -v "IS_YL_${s}_VAL" '%s' "$j"
|
|
2275
|
-
done
|
|
2276
|
-
IS_YL_UNRESOLVED=""
|
|
2277
|
-
while IFS= read -r line; do
|
|
2278
|
-
case "$line" in
|
|
2279
|
-
"!"$'\037'*) IS_YL_UNRESOLVED="$IS_YL_UNRESOLVED${line#"!"$'\037'}"$'\n' ;;
|
|
2280
|
-
esac
|
|
2281
|
-
done <<< "$out"
|
|
2282
|
-
}
|
|
2675
|
+
'
|
|
2283
2676
|
|
|
2284
|
-
#
|
|
2285
|
-
#
|
|
2286
|
-
|
|
2287
|
-
|
|
2288
|
-
|
|
2289
|
-
|
|
2290
|
-
|
|
2291
|
-
|
|
2292
|
-
|
|
2293
|
-
|
|
2294
|
-
|
|
2295
|
-
|
|
2677
|
+
# get_yaml_field: the first ` fkey:` inside the first `fsection:` block. A
|
|
2678
|
+
# top-level key closes the block; ftag prefixes the printed value.
|
|
2679
|
+
_IS_AWK_FIELD='
|
|
2680
|
+
{ sub(/\r$/, "") }
|
|
2681
|
+
!fdone {
|
|
2682
|
+
if ($0 ~ "^" fsection ":") fin = 1
|
|
2683
|
+
else if (fin && /^[a-zA-Z]/) fdone = 1
|
|
2684
|
+
else if (fin && $0 ~ "^ " fkey ":") {
|
|
2685
|
+
fval = $0
|
|
2686
|
+
sub(/.*:[[:space:]]*["\047]?/, "", fval)
|
|
2687
|
+
sub(/["\047]?[[:space:]]*$/, "", fval)
|
|
2688
|
+
ffound = 1
|
|
2689
|
+
fdone = 1
|
|
2690
|
+
}
|
|
2691
|
+
}
|
|
2692
|
+
END { if (ffound) print ftag fval }
|
|
2693
|
+
'
|
|
2694
|
+
|
|
2695
|
+
# load_targets_cache: one `name<TAB>enabled<TAB>output` row per target, each
|
|
2696
|
+
# prefixed with ttag.
|
|
2697
|
+
_IS_AWK_TARGETS='
|
|
2296
2698
|
function flush_target(at_sibling) {
|
|
2297
2699
|
if (name == "") return
|
|
2298
2700
|
if (enabled == "" && at_sibling) enabled = 0
|
|
2299
|
-
printf "%s\t%s\t%s\n", name, enabled, output
|
|
2701
|
+
printf "%s%s\t%s\t%s\n", ttag, name, enabled, output
|
|
2300
2702
|
name = ""
|
|
2301
2703
|
}
|
|
2302
2704
|
{ sub(/\r$/, "") }
|
|
@@ -2332,10 +2734,147 @@ load_targets_cache() {
|
|
|
2332
2734
|
output = val
|
|
2333
2735
|
}
|
|
2334
2736
|
END { flush_target(0) }
|
|
2335
|
-
|
|
2737
|
+
'
|
|
2738
|
+
|
|
2739
|
+
# _yaml_lists_store <file> <awk-output> <section>... — fill the load_yaml_list
|
|
2740
|
+
# cache for each section from `<section><US><value>` rows, and IS_YL_UNRESOLVED
|
|
2741
|
+
# from the `!` rows: one read_source_entries-shaped line each, the section in
|
|
2742
|
+
# front.
|
|
2743
|
+
_yaml_lists_store() {
|
|
2744
|
+
local file="$1" out="$2" line v s k saved_ifs="$IFS"
|
|
2745
|
+
local -a lines=() names=() vals=()
|
|
2746
|
+
shift 2
|
|
2747
|
+
names=("$@")
|
|
2748
|
+
IFS=$'\n'
|
|
2749
|
+
set -f
|
|
2750
|
+
# shellcheck disable=SC2206 # rows are `<section><US><value>`, never empty
|
|
2751
|
+
lines=($out)
|
|
2752
|
+
set +f
|
|
2753
|
+
IFS="$saved_ifs"
|
|
2754
|
+
IS_YL_UNRESOLVED=""
|
|
2755
|
+
# One pass over the rows in this shell: reading a here-string again per
|
|
2756
|
+
# section costs a system call per byte on Git Bash.
|
|
2757
|
+
for line in "${lines[@]+"${lines[@]}"}"; do
|
|
2758
|
+
s="${line%%$'\037'*}"
|
|
2759
|
+
v="${line#*$'\037'}"
|
|
2760
|
+
if [ "$s" = "!" ]; then
|
|
2761
|
+
IS_YL_UNRESOLVED="$IS_YL_UNRESOLVED$v"$'\n'
|
|
2762
|
+
continue
|
|
2763
|
+
fi
|
|
2764
|
+
for ((k = 0; k < ${#names[@]}; k++)); do
|
|
2765
|
+
[ "${names[k]}" = "$s" ] || continue
|
|
2766
|
+
vals[k]="${vals[k]-}$v"$'\n'
|
|
2767
|
+
done
|
|
2768
|
+
done
|
|
2769
|
+
for ((k = 0; k < ${#names[@]}; k++)); do
|
|
2770
|
+
v="${vals[k]-}"
|
|
2771
|
+
while [[ "$v" == *$'\n' ]]; do v="${v%$'\n'}"; done
|
|
2772
|
+
printf -v "IS_YL_${names[k]}_FILE" '%s' "$file"
|
|
2773
|
+
printf -v "IS_YL_${names[k]}_VAL" '%s' "$v"
|
|
2774
|
+
done
|
|
2775
|
+
}
|
|
2776
|
+
|
|
2777
|
+
# load_yaml_lists <file> <section>... — warm the load_yaml_list cache for several
|
|
2778
|
+
# sections in one manifest pass. Each section runs read_yaml_list's own state
|
|
2779
|
+
# machine and its reference expansion, so every cached value is what
|
|
2780
|
+
# load_yaml_list would have stored. The same pass leaves the references that
|
|
2781
|
+
# resolve to nothing in IS_YL_UNRESOLVED, one read_source_entries-shaped line
|
|
2782
|
+
# each with the section in front, for the WARNING: lines `sync` prints.
|
|
2783
|
+
load_yaml_lists() {
|
|
2784
|
+
local file="$1" out s
|
|
2785
|
+
shift
|
|
2786
|
+
for s in "$@"; do
|
|
2787
|
+
case "$s" in
|
|
2788
|
+
rules|agents|skills|ignore|submodules) ;;
|
|
2789
|
+
*) return 1 ;;
|
|
2790
|
+
esac
|
|
2791
|
+
done
|
|
2792
|
+
out="$(awk -v sections="$*" "$_IS_AWK_LISTS" "$file")"
|
|
2793
|
+
_yaml_lists_store "$file" "$out" "$@"
|
|
2794
|
+
}
|
|
2795
|
+
|
|
2796
|
+
# load_targets_cache <file> — parse the whole targets: section once into the
|
|
2797
|
+
# global IS_TGT_TSV (one `name<TAB>enabled<TAB>output` row per target),
|
|
2798
|
+
# matching is_target_enabled and get_target_output semantics: inline and block
|
|
2799
|
+
# forms, first occurrence wins, `enabled` defaults to 0 when the block ends
|
|
2800
|
+
# without one and to empty at end of file. The engine warms this once;
|
|
2801
|
+
# both readers consult it before spawning awk.
|
|
2802
|
+
load_targets_cache() {
|
|
2803
|
+
local file="$1"
|
|
2804
|
+
if [ "${IS_TGT_FILE:-}" = "$file" ]; then
|
|
2805
|
+
return 0
|
|
2806
|
+
fi
|
|
2807
|
+
IS_TGT_TSV="$(awk "$_IS_AWK_TARGETS" "$file")"
|
|
2336
2808
|
IS_TGT_FILE="$file"
|
|
2337
2809
|
}
|
|
2338
2810
|
|
|
2811
|
+
# load_manifest_view <file> <field-section> <field-key> <section>... — one
|
|
2812
|
+
# awk pass for what the CLI's preflight reads of a manifest: the
|
|
2813
|
+
# load_yaml_lists cache for each <section>, the targets cache, and the one
|
|
2814
|
+
# get_yaml_field answer that get_yaml_field_var then returns without a process.
|
|
2815
|
+
# Each reader's program is the one it runs alone, so the answers cannot differ.
|
|
2816
|
+
# A caller that overlaps the pass with other work runs manifest_view_run (it
|
|
2817
|
+
# prints) and hands the output to manifest_view_store itself.
|
|
2818
|
+
load_manifest_view() {
|
|
2819
|
+
local out
|
|
2820
|
+
out="$(manifest_view_run "$@")" || return 1
|
|
2821
|
+
manifest_view_store "$out" "$@"
|
|
2822
|
+
}
|
|
2823
|
+
|
|
2824
|
+
manifest_view_run() {
|
|
2825
|
+
local file="$1" fsection="$2" fkey="$3" s
|
|
2826
|
+
shift 3
|
|
2827
|
+
for s in "$@"; do
|
|
2828
|
+
case "$s" in
|
|
2829
|
+
rules|agents|skills|ignore|submodules) ;;
|
|
2830
|
+
*) return 1 ;;
|
|
2831
|
+
esac
|
|
2832
|
+
done
|
|
2833
|
+
awk -v sections="$*" -v fsection="$fsection" -v fkey="$fkey" \
|
|
2834
|
+
-v ftag=$'\036F' -v ttag=$'\036T' \
|
|
2835
|
+
"$_IS_AWK_LISTS$_IS_AWK_FIELD$_IS_AWK_TARGETS" "$file"
|
|
2836
|
+
}
|
|
2837
|
+
|
|
2838
|
+
# manifest_view_store <output> <file> <field-section> <field-key> <section>...
|
|
2839
|
+
manifest_view_store() {
|
|
2840
|
+
local out="$1" file="$2" fsection="$3" fkey="$4" line lists="" targets="" field="" found=0
|
|
2841
|
+
local saved_ifs="$IFS"
|
|
2842
|
+
local -a lines=()
|
|
2843
|
+
shift 4
|
|
2844
|
+
IFS=$'\n'
|
|
2845
|
+
set -f
|
|
2846
|
+
# shellcheck disable=SC2206 # every row is tagged or `<section><US>`, never empty
|
|
2847
|
+
lines=($out)
|
|
2848
|
+
set +f
|
|
2849
|
+
IFS="$saved_ifs"
|
|
2850
|
+
for line in "${lines[@]+"${lines[@]}"}"; do
|
|
2851
|
+
case "$line" in
|
|
2852
|
+
$'\036T'*) targets="$targets${line#??}"$'\n' ;;
|
|
2853
|
+
$'\036F'*) field="${line#??}"; found=1 ;;
|
|
2854
|
+
*) lists="$lists$line"$'\n' ;;
|
|
2855
|
+
esac
|
|
2856
|
+
done
|
|
2857
|
+
while [[ "$targets" == *$'\n' ]]; do targets="${targets%$'\n'}"; done
|
|
2858
|
+
_yaml_lists_store "$file" "$lists" "$@"
|
|
2859
|
+
IS_TGT_TSV="$targets"
|
|
2860
|
+
IS_TGT_FILE="$file"
|
|
2861
|
+
IS_YF_KEY="$file"$'\037'"$fsection"$'\037'"$fkey"
|
|
2862
|
+
IS_YF_VALUE="$field"
|
|
2863
|
+
IS_YF_FOUND="$found"
|
|
2864
|
+
}
|
|
2865
|
+
|
|
2866
|
+
# manifest_view_reset — forget every manifest answer cached in this shell. A
|
|
2867
|
+
# process that has the manifest rewritten (by a child it ran) calls it before
|
|
2868
|
+
# reading again.
|
|
2869
|
+
manifest_view_reset() {
|
|
2870
|
+
local s
|
|
2871
|
+
for s in rules agents skills ignore submodules; do
|
|
2872
|
+
unset "IS_YL_${s}_FILE" "IS_YL_${s}_VAL"
|
|
2873
|
+
done
|
|
2874
|
+
unset IS_TGT_FILE IS_TGT_TSV IS_YF_KEY IS_YF_VALUE IS_YF_FOUND IS_YL_UNRESOLVED
|
|
2875
|
+
}
|
|
2876
|
+
|
|
2877
|
+
|
|
2339
2878
|
# _targets_cache_row <target> — scan the cached TSV; sets IS_TGT_ROW_ENABLED /
|
|
2340
2879
|
# IS_TGT_ROW_OUTPUT. Returns 1 when the target has no row (reader falls back
|
|
2341
2880
|
# to empty, same as the awk scan finding nothing).
|
|
@@ -2557,18 +3096,23 @@ get_yaml_field() {
|
|
|
2557
3096
|
local file="$1"
|
|
2558
3097
|
local section="$2"
|
|
2559
3098
|
local key="$3"
|
|
2560
|
-
|
|
2561
|
-
|
|
2562
|
-
|
|
2563
|
-
|
|
2564
|
-
|
|
2565
|
-
|
|
2566
|
-
|
|
2567
|
-
|
|
2568
|
-
|
|
2569
|
-
|
|
2570
|
-
|
|
2571
|
-
|
|
3099
|
+
if [ "${IS_YF_KEY:-}" = "$file"$'\037'"$section"$'\037'"$key" ]; then
|
|
3100
|
+
[ "$IS_YF_FOUND" != 1 ] || printf '%s\n' "$IS_YF_VALUE"
|
|
3101
|
+
return 0
|
|
3102
|
+
fi
|
|
3103
|
+
awk -v fsection="$section" -v fkey="$key" "$_IS_AWK_FIELD" "$file"
|
|
3104
|
+
}
|
|
3105
|
+
|
|
3106
|
+
# get_yaml_field_var <file> <section> <key> — get_yaml_field into
|
|
3107
|
+
# IS_YAML_FIELD, without a process when load_manifest_view read it.
|
|
3108
|
+
# shellcheck disable=SC2034 # IS_YAML_FIELD is the return channel
|
|
3109
|
+
get_yaml_field_var() {
|
|
3110
|
+
if [ "${IS_YF_KEY:-}" = "$1"$'\037'"$2"$'\037'"$3" ]; then
|
|
3111
|
+
IS_YAML_FIELD=""
|
|
3112
|
+
[ "$IS_YF_FOUND" != 1 ] || IS_YAML_FIELD="$IS_YF_VALUE"
|
|
3113
|
+
return 0
|
|
3114
|
+
fi
|
|
3115
|
+
IS_YAML_FIELD="$(get_yaml_field "$@")"
|
|
2572
3116
|
}
|
|
2573
3117
|
|
|
2574
3118
|
# Get project name from config.yaml (project.name)
|