@ainova-systems/intelligence 0.11.0-rc.1

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 (54) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +46 -0
  3. package/bin/intelligence.js +59 -0
  4. package/cli/commands/add.sh +133 -0
  5. package/cli/commands/doctor.sh +100 -0
  6. package/cli/commands/init.sh +96 -0
  7. package/cli/commands/install.sh +84 -0
  8. package/cli/commands/list.sh +28 -0
  9. package/cli/commands/migrate.sh +251 -0
  10. package/cli/commands/registry.sh +51 -0
  11. package/cli/commands/remove.sh +39 -0
  12. package/cli/commands/status.sh +34 -0
  13. package/cli/commands/sync.sh +22 -0
  14. package/cli/commands/update.sh +59 -0
  15. package/cli/commands/upgrade.sh +27 -0
  16. package/cli/intelligence +71 -0
  17. package/cli/lib/cli-common.sh +149 -0
  18. package/cli/lib/lockfile.sh +97 -0
  19. package/cli/lib/manifest.sh +211 -0
  20. package/cli/lib/registry.sh +141 -0
  21. package/cli/lib/semver.sh +127 -0
  22. package/engine/INIT.md +498 -0
  23. package/engine/agents/intelligence-architect.md +53 -0
  24. package/engine/agents/intelligence-operator.md +49 -0
  25. package/engine/docs/ADAPTERS.md +212 -0
  26. package/engine/docs/CLI.md +91 -0
  27. package/engine/docs/CONVENTIONS.md +440 -0
  28. package/engine/rules/intelligence-authoring.md +114 -0
  29. package/engine/scripts/VERSION +1 -0
  30. package/engine/scripts/adapters/_template.sh +86 -0
  31. package/engine/scripts/adapters/agents.sh +299 -0
  32. package/engine/scripts/adapters/claude.sh +136 -0
  33. package/engine/scripts/adapters/codex.sh +118 -0
  34. package/engine/scripts/adapters/copilot.sh +193 -0
  35. package/engine/scripts/adapters/cursor.sh +146 -0
  36. package/engine/scripts/adapters/opencode.sh +200 -0
  37. package/engine/scripts/adapters/pi.sh +256 -0
  38. package/engine/scripts/lib/common.sh +1602 -0
  39. package/engine/scripts/lib/layout.sh +51 -0
  40. package/engine/scripts/lib/migrations.sh +708 -0
  41. package/engine/scripts/sync.sh +311 -0
  42. package/engine/scripts/update.sh +237 -0
  43. package/engine/skills/intelligence-add-agent/SKILL.md +62 -0
  44. package/engine/skills/intelligence-add-rule/SKILL.md +54 -0
  45. package/engine/skills/intelligence-add-skill/SKILL.md +53 -0
  46. package/engine/skills/intelligence-extract-skill/SKILL.md +47 -0
  47. package/engine/skills/intelligence-install-adapter/SKILL.md +31 -0
  48. package/engine/skills/intelligence-learn-from-context/SKILL.md +69 -0
  49. package/engine/skills/intelligence-review-skills/SKILL.md +86 -0
  50. package/engine/skills/intelligence-sync/SKILL.md +18 -0
  51. package/engine/skills/intelligence-uninstall-adapter/SKILL.md +42 -0
  52. package/engine/skills/intelligence-update/SKILL.md +159 -0
  53. package/package.json +39 -0
  54. package/registry/index.yaml +15 -0
@@ -0,0 +1,149 @@
1
+ #!/bin/bash
2
+ # Shared plumbing for every CLI command. Commands are separate processes (the
3
+ # dispatcher execs them), so each one sources this file first.
4
+ #
5
+ # All YAML reading goes through the engine's lib/common.sh — the CLI never
6
+ # grows a parallel parser for shapes the engine can already read. The only
7
+ # CLI-owned parsing lives in lib/manifest.sh (quoted-key `packages:` /
8
+ # `registries:` blocks, which the engine deliberately never reads).
9
+
10
+ # Engine libraries (readers, is_status, IS_RC_*, engine_version). Sourced in
11
+ # the same order sync.sh uses. CLI_DIR / IS_ENGINE_DIR come exported from the
12
+ # dispatcher.
13
+ source "$IS_ENGINE_DIR/scripts/lib/common.sh"
14
+ source "$IS_ENGINE_DIR/scripts/lib/layout.sh"
15
+ source "$IS_ENGINE_DIR/scripts/lib/migrations.sh"
16
+
17
+ die() {
18
+ echo "ERROR: $*" >&2
19
+ exit 1
20
+ }
21
+
22
+ source "$CLI_DIR/lib/manifest.sh"
23
+ source "$CLI_DIR/lib/semver.sh"
24
+ source "$CLI_DIR/lib/registry.sh"
25
+ source "$CLI_DIR/lib/lockfile.sh"
26
+
27
+ # Meta-skills that the CLI replaces with first-class commands — staging skips
28
+ # them so a v2 project never carries a skill telling the agent to run a flow
29
+ # the CLI already owns.
30
+ CLI_OBSOLETE_SKILLS="intelligence-update intelligence-sync"
31
+
32
+ # --- Project detection ---------------------------------------------------
33
+ # Sets: IP_MODE (v2|legacy|none), IP_ROOT, IP_UMBRELLA, IP_MODULE_DIR.
34
+ # These ARE this lib's public API — every command reads them after calling
35
+ # detect_project, which per-file shellcheck cannot see.
36
+ # shellcheck disable=SC2034
37
+ # v2 wins: the nearest ancestor holding intelligence.yaml. Legacy is a git
38
+ # repo holding an umbrella (a dir with config.yaml) whose module is identified
39
+ # by role — scripts/sync.sh + scripts/VERSION — never by name.
40
+ detect_project() {
41
+ IP_MODE="none"; IP_ROOT=""; IP_UMBRELLA=""; IP_MODULE_DIR=""
42
+ local dir="$PWD"
43
+ while :; do
44
+ if [ -f "$dir/intelligence.yaml" ]; then
45
+ IP_MODE="v2"
46
+ IP_ROOT="$(cd "$dir" && pwd)"
47
+ return 0
48
+ fi
49
+ [ "$dir" = "$(dirname "$dir")" ] && break
50
+ dir="$(dirname "$dir")"
51
+ done
52
+ local root
53
+ root="$(git rev-parse --show-toplevel 2>/dev/null || true)"
54
+ [ -n "$root" ] || return 0
55
+ root="$(cd "$root" && pwd)"
56
+ local cf umbrella mod
57
+ while IFS= read -r cf; do
58
+ [ -n "$cf" ] || continue
59
+ umbrella="$(cd "$(dirname "$cf")" && pwd)"
60
+ for mod in "$umbrella"/*/; do
61
+ [ -d "$mod" ] || continue
62
+ if [ -f "${mod}scripts/sync.sh" ] && [ -f "${mod}scripts/VERSION" ]; then
63
+ IP_MODE="legacy"; IP_ROOT="$root"; IP_UMBRELLA="$umbrella"
64
+ IP_MODULE_DIR="${mod%/}"
65
+ return 0
66
+ fi
67
+ done
68
+ done < <(find "$root" -maxdepth 2 -name 'config.yaml' -not -path '*/.*' 2>/dev/null)
69
+ return 0
70
+ }
71
+
72
+ require_v2() {
73
+ detect_project
74
+ case "$IP_MODE" in
75
+ v2) ;;
76
+ legacy) die "this is a vendored (v1) setup — run 'intelligence migrate' first, or keep using its own flow" ;;
77
+ *) die "no intelligence project found here — run 'intelligence init'" ;;
78
+ esac
79
+ }
80
+
81
+ # --- Manifest basics (engine-readable shapes) ----------------------------
82
+
83
+ manifest_intelligence_dir() {
84
+ local manifest="$1" v
85
+ v="$(get_yaml_field "$manifest" "project" "intelligence_dir")"
86
+ printf '%s' "${v:-intelligence}"
87
+ }
88
+
89
+ bundled_engine_version() {
90
+ tr -d ' \t\r\n' < "$IS_ENGINE_DIR/scripts/VERSION"
91
+ }
92
+
93
+ # --- Engine content staging ----------------------------------------------
94
+ # The v2 project vendors no engine code; the engine's own rules / agents /
95
+ # meta-skills / docs are staged into the package store so sources can reach
96
+ # them as ordinary repo-relative dirs. Re-staged whenever the bundled engine
97
+ # version differs from the `.version` stamp.
98
+ # stage_engine_content <dest-dir> — unconditional copy of the engine's own
99
+ # content into <dest-dir> plus the `.version` stamp.
100
+ stage_engine_content() {
101
+ local store="$1"
102
+ rm -rf "$store"
103
+ mkdir -p "$store"
104
+ local d s name skip
105
+ for d in rules agents docs; do
106
+ [ -d "$IS_ENGINE_DIR/$d" ] && cp -R "$IS_ENGINE_DIR/$d" "$store/$d"
107
+ done
108
+ if [ -d "$IS_ENGINE_DIR/skills" ]; then
109
+ mkdir -p "$store/skills"
110
+ for s in "$IS_ENGINE_DIR"/skills/*/; do
111
+ [ -d "$s" ] || continue
112
+ name="$(basename "$s")"
113
+ skip=0
114
+ for d in $CLI_OBSOLETE_SKILLS; do
115
+ [ "$name" = "$d" ] && skip=1
116
+ done
117
+ [ "$skip" = "1" ] && continue
118
+ cp -R "$s" "$store/skills/$name"
119
+ done
120
+ fi
121
+ bundled_engine_version > "$store/.version"
122
+ }
123
+
124
+ ensure_engine_staged() {
125
+ local root="$1"
126
+ local store="$root/.intelligence/engine"
127
+ local bundled_ver staged_ver
128
+ bundled_ver="$(bundled_engine_version)"
129
+ staged_ver=""
130
+ [ -f "$store/.version" ] && staged_ver="$(tr -d ' \t\r\n' < "$store/.version")"
131
+ [ "$staged_ver" = "$bundled_ver" ] && return 0
132
+ stage_engine_content "$store"
133
+ echo " engine content staged: .intelligence/engine ($bundled_ver)"
134
+ }
135
+
136
+ # --- The engine env contract ---------------------------------------------
137
+ # Everything the IS_CLI mode of sync.sh needs, derived from the manifest.
138
+ export_engine_env() {
139
+ local root="$1"
140
+ local umbrella
141
+ umbrella="$(manifest_intelligence_dir "$root/intelligence.yaml")"
142
+ export IS_CLI=1
143
+ export CONFIG_FILE="$root/intelligence.yaml"
144
+ export REPO_ROOT="$root"
145
+ export IS_UMBRELLA_REL="$umbrella"
146
+ export IS_MODULE_REL=".intelligence/engine"
147
+ export IS_SYNC_CMD="intelligence sync"
148
+ export IS_PROTECTED_DIRS="$umbrella:.intelligence"
149
+ }
@@ -0,0 +1,97 @@
1
+ #!/bin/bash
2
+ # intelligence.lock — the resolved state, committed with the project.
3
+ #
4
+ # The lock is fully generated (npm's model): readers use the qmap parser from
5
+ # manifest.sh, writers rebuild the whole file from a row stream of entries.
6
+ # Columns: name, requested, url, path, resolved, sha — separated by the ASCII
7
+ # unit separator, NOT a tab: tab is IFS whitespace, so `read` would collapse
8
+ # an empty field (a rootless `path`) and shift every column after it.
9
+
10
+ LOCKFILE_VERSION=1
11
+ LOCK_SEP=$'\x1f'
12
+
13
+ # top_scalar <file> <key> — one top-level scalar (`key: "value"`), quotes and
14
+ # trailing comment stripped. The declared exception to "no new parsing": the
15
+ # engine's read_engine_stamp is hardwired to its own key.
16
+ top_scalar() {
17
+ [ -f "$1" ] || return 0
18
+ awk -v key="$2" '
19
+ { sub(/\r$/, "") }
20
+ $0 ~ "^" key ":" {
21
+ v = substr($0, length(key) + 2)
22
+ sub(/^[ \t]+/, "", v)
23
+ if (v ~ /^"/) { v = substr(v, 2); q = index(v, "\""); if (q > 0) v = substr(v, 1, q - 1) }
24
+ else { sub(/[ \t]+#.*$/, "", v); sub(/[ \t]+$/, "", v) }
25
+ print v
26
+ exit
27
+ }
28
+ ' "$1"
29
+ }
30
+
31
+ # lock_to_tsv <lock> — every locked package as one row.
32
+ lock_to_tsv() {
33
+ local lock="$1" name
34
+ [ -f "$lock" ] || return 0
35
+ while IFS= read -r name; do
36
+ [ -n "$name" ] || continue
37
+ printf '%s\037%s\037%s\037%s\037%s\037%s\n' \
38
+ "$name" \
39
+ "$(qmap_field "$lock" "packages" "$name" "requested")" \
40
+ "$(qmap_field "$lock" "packages" "$name" "url")" \
41
+ "$(qmap_field "$lock" "packages" "$name" "path")" \
42
+ "$(qmap_field "$lock" "packages" "$name" "resolved")" \
43
+ "$(qmap_field "$lock" "packages" "$name" "sha")"
44
+ done < <(qmap_keys "$lock" "packages")
45
+ }
46
+
47
+ # lock_write_from_tsv <lock> <tsv-file> — rebuild the lock. Entries are
48
+ # written in the TSV's order; callers keep it stable (manifest order).
49
+ lock_write_from_tsv() {
50
+ local lock="$1" tsv="$2"
51
+ local tmp="$lock.cli.tmp"
52
+ {
53
+ echo "# Generated by the intelligence CLI. Do not edit."
54
+ echo "lockfile_version: $LOCKFILE_VERSION"
55
+ echo "engine_version: \"$(bundled_engine_version)\""
56
+ echo "packages:"
57
+ awk -F"$LOCK_SEP" '
58
+ NF >= 6 {
59
+ print " \"" $1 "\":"
60
+ if ($2 != "") print " requested: \"" $2 "\""
61
+ print " url: \"" $3 "\""
62
+ if ($4 != "") print " path: \"" $4 "\""
63
+ if ($5 != "") print " resolved: \"" $5 "\""
64
+ print " sha: \"" $6 "\""
65
+ }
66
+ ' "$tsv"
67
+ } > "$tmp"
68
+ mv "$tmp" "$lock"
69
+ }
70
+
71
+ # lock_upsert <lock> <name> <requested> <url> <path> <resolved> <sha>
72
+ # Replace-or-append one entry, keeping the others.
73
+ lock_upsert() {
74
+ local lock="$1" name="$2" requested="$3" url="$4" path="$5" resolved="$6" sha="$7"
75
+ local tsv="${TMPDIR:-/tmp}/intelligence-lock-$$.tsv"
76
+ {
77
+ lock_to_tsv "$lock" | awk -F"$LOCK_SEP" -v n="$name" '$1 != n'
78
+ printf '%s\037%s\037%s\037%s\037%s\037%s\n' \
79
+ "$name" "$requested" "$url" "$path" "$resolved" "$sha"
80
+ } > "$tsv"
81
+ lock_write_from_tsv "$lock" "$tsv"
82
+ rm -f "$tsv"
83
+ }
84
+
85
+ # lock_remove <lock> <name>
86
+ lock_remove() {
87
+ local lock="$1" name="$2"
88
+ [ -f "$lock" ] || return 0
89
+ local tsv="${TMPDIR:-/tmp}/intelligence-lock-$$.tsv"
90
+ lock_to_tsv "$lock" | awk -F"$LOCK_SEP" -v n="$name" '$1 != n' > "$tsv"
91
+ if [ -s "$tsv" ]; then
92
+ lock_write_from_tsv "$lock" "$tsv"
93
+ else
94
+ rm -f "$lock"
95
+ fi
96
+ rm -f "$tsv"
97
+ }
@@ -0,0 +1,211 @@
1
+ #!/bin/bash
2
+ # The CLI-owned YAML shapes: quoted-key maps (`packages:`, `registries:` in
3
+ # the manifest, `packages:` in the lock and in registry indexes).
4
+ #
5
+ # The engine deliberately never reads these blocks, and its readers cannot
6
+ # hold `@scope/name` keys — so this is the CLI's single, deliberate parser,
7
+ # scoped to exactly one shape:
8
+ #
9
+ # block:
10
+ # "@scope/name": # 2-space indent, key always quoted
11
+ # field: "value" # 4-space indent
12
+ # "@scope/other": "value" # flat form (registries)
13
+ #
14
+ # Every editor stages to `<file>.cli.tmp` and `mv`s — the migrations
15
+ # discipline — and preserves unrelated lines and comments byte-for-byte.
16
+
17
+ # Package names are paths (store dirs) and YAML keys, so the charset is
18
+ # closed: scoped, one slash, no spaces / quotes / colons.
19
+ assert_valid_pkg_name() {
20
+ case "$1" in
21
+ @*/*) ;;
22
+ *) die "invalid package name '$1' — expected @scope/name" ;;
23
+ esac
24
+ case "$1" in
25
+ *[!@/A-Za-z0-9._-]*|*/*/*) die "invalid package name '$1' — allowed: letters, digits, . _ -, one '/'" ;;
26
+ esac
27
+ }
28
+
29
+ # qmap_keys <file> <block> — quoted keys, one per line.
30
+ qmap_keys() {
31
+ [ -f "$1" ] || return 0
32
+ awk -v block="$2" '
33
+ { sub(/\r$/, "") }
34
+ $0 ~ "^" block ":[ \t]*$" { inb = 1; next }
35
+ inb && /^[^ #]/ { inb = 0 }
36
+ inb {
37
+ if ($0 ~ /^ "/) {
38
+ s = substr($0, 4)
39
+ q = index(s, "\"")
40
+ if (q > 0) print substr(s, 1, q - 1)
41
+ }
42
+ }
43
+ ' "$1"
44
+ }
45
+
46
+ # qmap_field <file> <block> <key> <field> — 4-indent field value under a
47
+ # quoted key; strips surrounding quotes and a trailing unquoted comment.
48
+ qmap_field() {
49
+ [ -f "$1" ] || return 0
50
+ awk -v block="$2" -v key="$3" -v field="$4" '
51
+ { sub(/\r$/, "") }
52
+ $0 ~ "^" block ":[ \t]*$" { inb = 1; next }
53
+ inb && /^[^ #]/ { inb = 0; ink = 0 }
54
+ inb && /^ "/ {
55
+ s = substr($0, 4); q = index(s, "\"")
56
+ ink = (q > 0 && substr(s, 1, q - 1) == key)
57
+ next
58
+ }
59
+ inb && ink && /^ [A-Za-z_]/ {
60
+ line = $0
61
+ sub(/^ /, "", line)
62
+ c = index(line, ":")
63
+ if (c == 0) next
64
+ if (substr(line, 1, c - 1) != field) next
65
+ v = substr(line, c + 1)
66
+ sub(/^[ \t]+/, "", v)
67
+ if (v ~ /^"/) { v = substr(v, 2); q2 = index(v, "\""); if (q2 > 0) v = substr(v, 1, q2 - 1) }
68
+ else { sub(/[ \t]+#.*$/, "", v); sub(/[ \t]+$/, "", v) }
69
+ print v
70
+ exit
71
+ }
72
+ ' "$1"
73
+ }
74
+
75
+ # qmap_value <file> <block> <key> — flat form: ` "key": "value"`.
76
+ qmap_value() {
77
+ [ -f "$1" ] || return 0
78
+ awk -v block="$2" -v key="$3" '
79
+ { sub(/\r$/, "") }
80
+ $0 ~ "^" block ":[ \t]*$" { inb = 1; next }
81
+ inb && /^[^ #]/ { inb = 0 }
82
+ inb && /^ "/ {
83
+ s = substr($0, 4); q = index(s, "\"")
84
+ if (q == 0 || substr(s, 1, q - 1) != key) next
85
+ v = substr(s, q + 1)
86
+ sub(/^:[ \t]*/, "", v)
87
+ if (v ~ /^"/) { v = substr(v, 2); q2 = index(v, "\""); if (q2 > 0) v = substr(v, 1, q2 - 1) }
88
+ else { sub(/[ \t]+#.*$/, "", v); sub(/[ \t]+$/, "", v) }
89
+ if (v != "") print v
90
+ exit
91
+ }
92
+ ' "$1"
93
+ }
94
+
95
+ # _qmap_stage <file> <awk-program> [awk args…] — run an editing pass, verify
96
+ # it produced output, commit.
97
+ _qmap_stage() {
98
+ local file="$1"; shift
99
+ local tmp="$file.cli.tmp"
100
+ awk "$@" "$file" > "$tmp"
101
+ [ -s "$tmp" ] || { rm -f "$tmp"; die "internal: manifest edit produced an empty file for $file"; }
102
+ mv "$tmp" "$file"
103
+ }
104
+
105
+ # qmap_set <file> <block> <key> <field> <value> — upsert one field, creating
106
+ # the block and the key as needed.
107
+ qmap_set() {
108
+ local file="$1" block="$2" key="$3" field="$4" value="$5"
109
+ [ -f "$file" ] || die "no such file: $file"
110
+ _qmap_stage "$file" -v block="$block" -v key="$key" -v field="$field" -v value="$value" '
111
+ function keyline() { return " \"" key "\":" }
112
+ function fieldline() { return " " field ": \"" value "\"" }
113
+ function flush_key() {
114
+ # leaving the key without having written the field -> append it
115
+ if (ink && !done) { print fieldline(); done = 1 }
116
+ ink = 0
117
+ }
118
+ function flush_block() {
119
+ if (inb && !keyseen && !done) { print keyline(); print fieldline(); keyseen = 1; done = 1 }
120
+ inb = 0
121
+ }
122
+ { sub(/\r$/, "") }
123
+ $0 ~ "^" block ":[ \t]*$" { blockseen = 1; inb = 1; print; next }
124
+ inb && /^[^ #]/ { flush_key(); flush_block() }
125
+ inb && /^ "/ {
126
+ flush_key()
127
+ s = substr($0, 4); q = index(s, "\"")
128
+ if (q > 0 && substr(s, 1, q - 1) == key) { keyseen = 1; ink = 1 }
129
+ print; next
130
+ }
131
+ inb && ink && /^ [A-Za-z_]/ {
132
+ line = $0; sub(/^ /, "", line)
133
+ c = index(line, ":")
134
+ if (c > 0 && substr(line, 1, c - 1) == field) { print fieldline(); done = 1; next }
135
+ print; next
136
+ }
137
+ { print }
138
+ END {
139
+ flush_key(); flush_block()
140
+ if (!blockseen) {
141
+ print block ":"
142
+ print keyline()
143
+ print fieldline()
144
+ }
145
+ }
146
+ '
147
+ }
148
+
149
+ # qmap_set_value <file> <block> <key> <value> — flat-form upsert.
150
+ qmap_set_value() {
151
+ local file="$1" block="$2" key="$3" value="$4"
152
+ [ -f "$file" ] || die "no such file: $file"
153
+ _qmap_stage "$file" -v block="$block" -v key="$key" -v value="$value" '
154
+ function entry() { return " \"" key "\": \"" value "\"" }
155
+ { sub(/\r$/, "") }
156
+ $0 ~ "^" block ":[ \t]*$" { blockseen = 1; inb = 1; print; next }
157
+ inb && /^[^ #]/ { if (!done) { print entry(); done = 1 }; inb = 0 }
158
+ inb && /^ "/ {
159
+ s = substr($0, 4); q = index(s, "\"")
160
+ if (q > 0 && substr(s, 1, q - 1) == key) { print entry(); done = 1; next }
161
+ }
162
+ { print }
163
+ END {
164
+ if (inb && !done) { print entry(); done = 1 }
165
+ if (!blockseen) { print block ":"; print entry() }
166
+ }
167
+ '
168
+ }
169
+
170
+ # qmap_delete_key <file> <block> <key> — drop the key and everything indented
171
+ # under it. Flat-form entries are one line, block-form keys take their fields
172
+ # with them. The block header stays even when it empties.
173
+ qmap_delete_key() {
174
+ local file="$1" block="$2" key="$3"
175
+ [ -f "$file" ] || return 0
176
+ _qmap_stage "$file" -v block="$block" -v key="$key" '
177
+ { sub(/\r$/, "") }
178
+ $0 ~ "^" block ":[ \t]*$" { inb = 1; print; next }
179
+ inb && /^[^ #]/ { inb = 0; drop = 0 }
180
+ inb && /^ "/ {
181
+ s = substr($0, 4); q = index(s, "\"")
182
+ drop = (q > 0 && substr(s, 1, q - 1) == key)
183
+ if (drop) next
184
+ }
185
+ inb && drop && /^ / { next }
186
+ { print }
187
+ '
188
+ }
189
+
190
+ # sources_remove_entry <file> <section> <entry> — remove `- "entry"` from
191
+ # sources.<section>. Inverse of the engine-side _mig_add_source.
192
+ sources_remove_entry() {
193
+ local file="$1" section="$2" entry="$3"
194
+ [ -f "$file" ] || return 0
195
+ _qmap_stage "$file" -v section="$section" -v entry="$entry" '
196
+ { sub(/\r$/, "") }
197
+ /^sources:[ \t]*$/ { ins = 1; print; next }
198
+ ins && /^[^ #]/ { ins = 0; insec = 0 }
199
+ ins && $0 ~ "^ " section ":[ \t]*$" { insec = 1; print; next }
200
+ ins && /^ [A-Za-z_]/ { insec = 0 }
201
+ insec {
202
+ line = $0
203
+ sub(/^[ \t]*-[ \t]*/, "", line)
204
+ gsub(/["\x27]/, "", line)
205
+ sub(/[ \t]+#.*$/, "", line)
206
+ sub(/[ \t]+$/, "", line)
207
+ if (line == entry) next
208
+ }
209
+ { print }
210
+ '
211
+ }
@@ -0,0 +1,141 @@
1
+ #!/bin/bash
2
+ # Name -> source resolution and package fetching.
3
+ #
4
+ # Resolution order (first hit wins):
5
+ # 1. project `registries:` — a scope bound to a registry repo (a git repo
6
+ # holding index.yaml); private orgs override anything shipped
7
+ # 2. the bundled default index (registry/index.yaml next to the CLI)
8
+ # 3. convention: @org/name -> https://github.com/org/name.git, content at
9
+ # the repo root — the zero-infrastructure default
10
+ #
11
+ # An index is itself fetched with git (never curl): auth, proxies and private
12
+ # hosting all come for free, and the CLI keeps the engine's bash+awk+git-only
13
+ # dependency footprint.
14
+
15
+ default_index_file() {
16
+ local f="$CLI_DIR/../registry/index.yaml"
17
+ [ -f "$f" ] && printf '%s' "$f"
18
+ return 0
19
+ }
20
+
21
+ # _fetch_index <registry-repo-url> — clones the registry repo (shallow) and
22
+ # prints the path of its index.yaml; empty on failure. Cached per run+url.
23
+ _fetch_index() {
24
+ local url="$1" cache key tmp
25
+ cache="${TMPDIR:-/tmp}/intelligence-cli-index-$$"
26
+ key="$(printf '%s' "$url" | cksum | awk '{print $1}')"
27
+ if [ -f "$cache/$key/index.yaml" ]; then
28
+ printf '%s' "$cache/$key/index.yaml"
29
+ return 0
30
+ fi
31
+ mkdir -p "$cache"
32
+ tmp="$cache/$key"
33
+ if GIT_TERMINAL_PROMPT=0 git -c core.autocrlf=false clone --depth 1 --quiet "$url" "$tmp" 2>/dev/null \
34
+ && [ -f "$tmp/index.yaml" ]; then
35
+ printf '%s' "$tmp/index.yaml"
36
+ fi
37
+ return 0
38
+ }
39
+
40
+ # resolve_package_source <manifest-or-empty> <@scope/name>
41
+ # Sets RES_URL, RES_PATH (may be empty = repo root), RES_VIA — the caller's
42
+ # result contract, invisible to per-file shellcheck.
43
+ # shellcheck disable=SC2034
44
+ resolve_package_source() {
45
+ local manifest="$1" name="$2"
46
+ local scope="${name%%/*}" short="${name#*/}"
47
+ RES_URL=""; RES_PATH=""; RES_VIA=""
48
+
49
+ if [ -n "$manifest" ] && [ -f "$manifest" ]; then
50
+ local reg_url index
51
+ reg_url="$(qmap_value "$manifest" "registries" "$scope")"
52
+ if [ -n "$reg_url" ]; then
53
+ index="$(_fetch_index "${reg_url#git+}")"
54
+ [ -n "$index" ] || die "registry for scope '$scope' is unreachable or has no index.yaml: $reg_url"
55
+ RES_URL="$(qmap_field "$index" "packages" "$name" "url")"
56
+ RES_PATH="$(qmap_field "$index" "packages" "$name" "path")"
57
+ [ -n "$RES_URL" ] || die "package '$name' not found in the '$scope' registry ($reg_url)"
58
+ RES_VIA="registry:$reg_url"
59
+ return 0
60
+ fi
61
+ fi
62
+
63
+ local bundled
64
+ bundled="$(default_index_file)"
65
+ if [ -n "$bundled" ]; then
66
+ RES_URL="$(qmap_field "$bundled" "packages" "$name" "url")"
67
+ if [ -n "$RES_URL" ]; then
68
+ RES_PATH="$(qmap_field "$bundled" "packages" "$name" "path")"
69
+ RES_VIA="index"
70
+ return 0
71
+ fi
72
+ fi
73
+
74
+ RES_URL="https://github.com/${scope#@}/$short.git"
75
+ RES_PATH=""
76
+ RES_VIA="convention"
77
+ return 0
78
+ }
79
+
80
+ # fetch_package <url> <ref> <subpath> <dest-dir>
81
+ # Shallow-clones url@ref (tag, branch, or SHA fallback), copies <subpath>
82
+ # (or the repo root) into <dest-dir>, prints the resolved commit sha.
83
+ # The clone pins autocrlf/eol/symlinks exactly like the engine does, so the
84
+ # store holds the bytes the package published.
85
+ fetch_package() {
86
+ local url="$1" ref="$2" subpath="$3" dest="$4"
87
+ local tmp sha src
88
+ local -a branch_arg=()
89
+ [ -n "$ref" ] && branch_arg=(--branch "$ref")
90
+ tmp="$(mktemp -d -t intelligence-pkg-XXXXXX 2>/dev/null || mktemp -d)"
91
+ if ! GIT_TERMINAL_PROMPT=0 git -c core.symlinks=false -c core.autocrlf=false -c core.eol=lf \
92
+ clone --depth 1 "${branch_arg[@]+"${branch_arg[@]}"}" --quiet "$url" "$tmp/clone" 2>/dev/null; then
93
+ # A SHA is not clonable with --branch — fall back to a full clone + checkout.
94
+ rm -rf "$tmp/clone"
95
+ GIT_TERMINAL_PROMPT=0 git -c core.symlinks=false -c core.autocrlf=false -c core.eol=lf \
96
+ clone --quiet "$url" "$tmp/clone" 2>/dev/null \
97
+ || { rm -rf "$tmp"; die "cannot clone $url"; }
98
+ [ -z "$ref" ] || git -C "$tmp/clone" checkout --quiet "$ref" \
99
+ || { rm -rf "$tmp"; die "ref '$ref' not found in $url"; }
100
+ fi
101
+ sha="$(git -C "$tmp/clone" rev-parse HEAD)"
102
+ src="$tmp/clone"
103
+ if [ -n "$subpath" ]; then
104
+ case "$subpath" in
105
+ *..*) rm -rf "$tmp"; die "unsafe path '$subpath'" ;;
106
+ esac
107
+ src="$tmp/clone/$subpath"
108
+ [ -d "$src" ] || { rm -rf "$tmp"; die "path '$subpath' not found in $url${ref:+@$ref}" ; }
109
+ fi
110
+ rm -rf "$dest"
111
+ mkdir -p "$dest"
112
+ cp -R "$src/." "$dest/"
113
+ # A nested repository would land as an untracked .git — the store must
114
+ # hold plain files only.
115
+ rm -rf "$dest/.git"
116
+ rm -rf "$tmp"
117
+ printf '%s' "$sha"
118
+ }
119
+
120
+ # wire_package_sources <manifest> <@scope/name> <store-rel-dir>
121
+ # The provides convention: whichever of rules/agents/skills the installed
122
+ # package has becomes a sources entry. Reuses the engine's idempotent,
123
+ # comment-preserving _mig_add_source.
124
+ wire_package_sources() {
125
+ local manifest="$1" name="$2" rel="$3" root="$4"
126
+ local section
127
+ for section in rules agents skills; do
128
+ if [ -d "$root/$rel/$section" ]; then
129
+ _mig_add_source "$manifest" "$section" "$rel/$section"
130
+ fi
131
+ done
132
+ }
133
+
134
+ # unwire_package_sources <manifest> <store-rel-dir>
135
+ unwire_package_sources() {
136
+ local manifest="$1" rel="$2"
137
+ local section
138
+ for section in rules agents skills; do
139
+ sources_remove_entry "$manifest" "$section" "$rel/$section"
140
+ done
141
+ }