@ainova-systems/intelligence 0.11.0-rc.1 → 0.11.0-rc.11
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/README.md +19 -16
- package/cli/commands/adapter.sh +153 -0
- package/cli/commands/init.sh +171 -34
- package/cli/commands/package.sh +30 -0
- package/cli/commands/registry.sh +65 -30
- package/cli/commands/status.sh +20 -11
- package/cli/commands/sync.sh +9 -4
- package/cli/commands/update.sh +80 -46
- package/cli/engine-package.yaml +14 -0
- package/cli/intelligence +37 -26
- package/cli/internal/align-project.sh +134 -0
- package/cli/internal/check.sh +128 -0
- package/cli/internal/convert-legacy.sh +400 -0
- package/cli/{commands/add.sh → internal/package-add.sh} +37 -19
- package/cli/{commands/list.sh → internal/package-list.sh} +10 -5
- package/cli/{commands/remove.sh → internal/package-remove.sh} +16 -4
- package/cli/internal/package-search.sh +83 -0
- package/cli/internal/package-update.sh +103 -0
- package/cli/internal/restore.sh +130 -0
- package/cli/internal/target-state.sh +57 -0
- package/cli/lib/cli-common.sh +213 -63
- package/cli/lib/lockfile.sh +15 -9
- package/cli/lib/manifest.sh +257 -4
- package/cli/lib/registry.sh +96 -47
- package/cli/lib/semver.sh +12 -4
- package/engine/ENGINE_SHA +1 -0
- package/engine/{scripts/adapters → adapters}/_template.sh +10 -9
- package/engine/{scripts/adapters → adapters}/agents.sh +20 -25
- package/engine/{scripts/adapters → adapters}/opencode.sh +1 -1
- package/engine/{scripts/lib → lib}/common.sh +30 -538
- package/engine/lib/contract.sh +120 -0
- package/engine/sync.sh +238 -0
- package/package.json +5 -5
- package/{engine → packages/sync}/agents/intelligence-architect.md +5 -3
- package/{engine → packages/sync}/agents/intelligence-operator.md +10 -13
- package/packages/sync/references/adapters.md +252 -0
- package/packages/sync/references/conventions.md +385 -0
- package/{engine → packages/sync}/rules/intelligence-authoring.md +7 -7
- package/{engine → packages/sync}/skills/intelligence-add-agent/SKILL.md +7 -7
- package/{engine → packages/sync}/skills/intelligence-add-rule/SKILL.md +5 -5
- package/{engine → packages/sync}/skills/intelligence-add-skill/SKILL.md +5 -5
- package/{engine → packages/sync}/skills/intelligence-extract-skill/SKILL.md +2 -2
- package/packages/sync/skills/intelligence-install-adapter/SKILL.md +38 -0
- package/{engine → packages/sync}/skills/intelligence-learn-from-context/SKILL.md +3 -3
- package/packages/sync/skills/intelligence-learn-from-repository/SKILL.md +55 -0
- package/{engine → packages/sync}/skills/intelligence-review-skills/SKILL.md +7 -7
- package/packages/sync/skills/intelligence-sync/SKILL.md +20 -0
- package/packages/sync/skills/intelligence-uninstall-adapter/SKILL.md +24 -0
- package/packages/sync/skills/intelligence-update/SKILL.md +43 -0
- package/cli/commands/doctor.sh +0 -100
- package/cli/commands/install.sh +0 -84
- package/cli/commands/migrate.sh +0 -251
- package/cli/commands/upgrade.sh +0 -27
- package/engine/INIT.md +0 -498
- package/engine/docs/ADAPTERS.md +0 -212
- package/engine/docs/CLI.md +0 -91
- package/engine/docs/CONVENTIONS.md +0 -440
- package/engine/scripts/lib/layout.sh +0 -51
- package/engine/scripts/lib/migrations.sh +0 -708
- package/engine/scripts/sync.sh +0 -311
- package/engine/scripts/update.sh +0 -237
- package/engine/skills/intelligence-install-adapter/SKILL.md +0 -31
- package/engine/skills/intelligence-sync/SKILL.md +0 -18
- package/engine/skills/intelligence-uninstall-adapter/SKILL.md +0 -42
- package/engine/skills/intelligence-update/SKILL.md +0 -159
- package/registry/index.yaml +0 -15
- /package/engine/{scripts/VERSION → VERSION} +0 -0
- /package/engine/{scripts/adapters → adapters}/claude.sh +0 -0
- /package/engine/{scripts/adapters → adapters}/codex.sh +0 -0
- /package/engine/{scripts/adapters → adapters}/copilot.sh +0 -0
- /package/engine/{scripts/adapters → adapters}/cursor.sh +0 -0
- /package/engine/{scripts/adapters → adapters}/pi.sh +0 -0
package/cli/lib/lockfile.sh
CHANGED
|
@@ -12,7 +12,7 @@ LOCK_SEP=$'\x1f'
|
|
|
12
12
|
|
|
13
13
|
# top_scalar <file> <key> — one top-level scalar (`key: "value"`), quotes and
|
|
14
14
|
# trailing comment stripped. The declared exception to "no new parsing": the
|
|
15
|
-
# engine's
|
|
15
|
+
# engine's read_schema_version is hardwired to its own key.
|
|
16
16
|
top_scalar() {
|
|
17
17
|
[ -f "$1" ] || return 0
|
|
18
18
|
awk -v key="$2" '
|
|
@@ -54,14 +54,18 @@ lock_write_from_tsv() {
|
|
|
54
54
|
echo "lockfile_version: $LOCKFILE_VERSION"
|
|
55
55
|
echo "engine_version: \"$(bundled_engine_version)\""
|
|
56
56
|
echo "packages:"
|
|
57
|
+
# Values reach a double-quoted YAML scalar: a `"` or `\` in any of
|
|
58
|
+
# them (registry-supplied url/path included) must be escaped, or it
|
|
59
|
+
# would break or alias the neighbouring field.
|
|
57
60
|
awk -F"$LOCK_SEP" '
|
|
61
|
+
function esc(s) { gsub(/\\/, "\\\\", s); gsub(/"/, "\\\"", s); return s }
|
|
58
62
|
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 "\""
|
|
63
|
+
print " \"" esc($1) "\":"
|
|
64
|
+
if ($2 != "") print " requested: \"" esc($2) "\""
|
|
65
|
+
print " url: \"" esc($3) "\""
|
|
66
|
+
if ($4 != "") print " path: \"" esc($4) "\""
|
|
67
|
+
if ($5 != "") print " resolved: \"" esc($5) "\""
|
|
68
|
+
print " sha: \"" esc($6) "\""
|
|
65
69
|
}
|
|
66
70
|
' "$tsv"
|
|
67
71
|
} > "$tmp"
|
|
@@ -72,7 +76,8 @@ lock_write_from_tsv() {
|
|
|
72
76
|
# Replace-or-append one entry, keeping the others.
|
|
73
77
|
lock_upsert() {
|
|
74
78
|
local lock="$1" name="$2" requested="$3" url="$4" path="$5" resolved="$6" sha="$7"
|
|
75
|
-
local tsv
|
|
79
|
+
local tsv
|
|
80
|
+
tsv="$(mktemp -t intelligence-lock-XXXXXX 2>/dev/null || mktemp)"
|
|
76
81
|
{
|
|
77
82
|
lock_to_tsv "$lock" | awk -F"$LOCK_SEP" -v n="$name" '$1 != n'
|
|
78
83
|
printf '%s\037%s\037%s\037%s\037%s\037%s\n' \
|
|
@@ -86,7 +91,8 @@ lock_upsert() {
|
|
|
86
91
|
lock_remove() {
|
|
87
92
|
local lock="$1" name="$2"
|
|
88
93
|
[ -f "$lock" ] || return 0
|
|
89
|
-
local tsv
|
|
94
|
+
local tsv
|
|
95
|
+
tsv="$(mktemp -t intelligence-lock-XXXXXX 2>/dev/null || mktemp)"
|
|
90
96
|
lock_to_tsv "$lock" | awk -F"$LOCK_SEP" -v n="$name" '$1 != n' > "$tsv"
|
|
91
97
|
if [ -s "$tsv" ]; then
|
|
92
98
|
lock_write_from_tsv "$lock" "$tsv"
|
package/cli/lib/manifest.sh
CHANGED
|
@@ -24,6 +24,102 @@ assert_valid_pkg_name() {
|
|
|
24
24
|
case "$1" in
|
|
25
25
|
*[!@/A-Za-z0-9._-]*|*/*/*) die "invalid package name '$1' — allowed: letters, digits, . _ -, one '/'" ;;
|
|
26
26
|
esac
|
|
27
|
+
# Both segments become store path components fed to rm -rf: an empty one
|
|
28
|
+
# ('@scope/') resolves to the whole scope dir, and a dot segment walks the
|
|
29
|
+
# tree — either would turn one bad --name into deleting other packages.
|
|
30
|
+
local scope="${1%%/*}" short="${1#*/}"
|
|
31
|
+
case "${scope#@}" in
|
|
32
|
+
""|.|..) die "invalid package name '$1' — empty or dot scope" ;;
|
|
33
|
+
esac
|
|
34
|
+
case "$short" in
|
|
35
|
+
""|.|..) die "invalid package name '$1' — empty or dot name" ;;
|
|
36
|
+
esac
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
# Target names become adapter filenames and shell function suffixes, so keep
|
|
40
|
+
# them inside the portable shell-identifier subset used by built-in adapters.
|
|
41
|
+
assert_valid_target_name() {
|
|
42
|
+
case "$1" in
|
|
43
|
+
""|[!abcdefghijklmnopqrstuvwxyz]*|*[!abcdefghijklmnopqrstuvwxyz0123456789_]*)
|
|
44
|
+
die "invalid target name '$1' — expected [a-z][a-z0-9_]*"
|
|
45
|
+
;;
|
|
46
|
+
esac
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
# target_exists <manifest> <name> — distinguish a disabled target from one
|
|
50
|
+
# that is not declared at all. Both inline and block target shapes count.
|
|
51
|
+
target_exists() {
|
|
52
|
+
local file="$1" target="$2"
|
|
53
|
+
[ -f "$file" ] || return 1
|
|
54
|
+
awk -v target="$target" '
|
|
55
|
+
{ sub(/\r$/, "") }
|
|
56
|
+
/^targets:[[:space:]]*$/ { in_targets = 1; next }
|
|
57
|
+
/^[A-Za-z]/ { in_targets = 0 }
|
|
58
|
+
in_targets && $0 ~ "^ " target ":[[:space:]]*" { found = 1; exit }
|
|
59
|
+
END { exit(found ? 0 : 1) }
|
|
60
|
+
' "$file"
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
# target_set_enabled <manifest> <name> <true|false> <default-output>
|
|
64
|
+
# Transactionally update only `enabled`, preserving every other target field
|
|
65
|
+
# and comment. A missing target is added in the compact form used by init.
|
|
66
|
+
target_set_enabled() {
|
|
67
|
+
local file="$1" target="$2" enabled="$3" output="$4"
|
|
68
|
+
[ -f "$file" ] || die "no such file: $file"
|
|
69
|
+
case "$enabled" in true|false) ;; *) die "internal: invalid target state '$enabled'" ;; esac
|
|
70
|
+
output="$(_yq_esc "$output")"
|
|
71
|
+
_qmap_stage "$file" -v target="$target" -v enabled="$enabled" -v output="$output" '
|
|
72
|
+
function entry() { return " " target ": { enabled: " enabled ", output: \"" output "\" }" }
|
|
73
|
+
function add_enabled_to_inline(line, p) {
|
|
74
|
+
p = index(line, "{")
|
|
75
|
+
if (p > 0) return substr(line, 1, p) " enabled: " enabled "," substr(line, p + 1)
|
|
76
|
+
return line
|
|
77
|
+
}
|
|
78
|
+
function flush_target() {
|
|
79
|
+
if (in_target && !enabled_seen) print " enabled: " enabled
|
|
80
|
+
in_target = 0
|
|
81
|
+
}
|
|
82
|
+
function flush_targets() {
|
|
83
|
+
flush_target()
|
|
84
|
+
if (in_targets && !target_seen) { print entry(); target_seen = 1 }
|
|
85
|
+
in_targets = 0
|
|
86
|
+
}
|
|
87
|
+
{ sub(/\r$/, "") }
|
|
88
|
+
/^targets:[[:space:]]*$/ { targets_seen = 1; in_targets = 1; print; next }
|
|
89
|
+
in_targets && /^[A-Za-z]/ { flush_targets() }
|
|
90
|
+
in_targets && /^ [A-Za-z]/ {
|
|
91
|
+
flush_target()
|
|
92
|
+
if ($0 ~ "^ " target ":[[:space:]]*") {
|
|
93
|
+
target_seen = 1
|
|
94
|
+
if (match($0, /enabled:[[:space:]]*(true|false)/)) {
|
|
95
|
+
print substr($0, 1, RSTART - 1) "enabled: " enabled substr($0, RSTART + RLENGTH)
|
|
96
|
+
enabled_seen = 1
|
|
97
|
+
} else if ($0 ~ /\{/) {
|
|
98
|
+
print add_enabled_to_inline($0)
|
|
99
|
+
enabled_seen = 1
|
|
100
|
+
} else {
|
|
101
|
+
print
|
|
102
|
+
in_target = 1
|
|
103
|
+
enabled_seen = 0
|
|
104
|
+
}
|
|
105
|
+
next
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
in_target && /^ enabled:[[:space:]]*/ {
|
|
109
|
+
print " enabled: " enabled
|
|
110
|
+
enabled_seen = 1
|
|
111
|
+
next
|
|
112
|
+
}
|
|
113
|
+
{ last = $0; print }
|
|
114
|
+
END {
|
|
115
|
+
flush_targets()
|
|
116
|
+
if (!targets_seen) {
|
|
117
|
+
if (last != "") print ""
|
|
118
|
+
print "targets:"
|
|
119
|
+
print entry()
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
'
|
|
27
123
|
}
|
|
28
124
|
|
|
29
125
|
# qmap_keys <file> <block> — quoted keys, one per line.
|
|
@@ -104,9 +200,19 @@ _qmap_stage() {
|
|
|
104
200
|
|
|
105
201
|
# qmap_set <file> <block> <key> <field> <value> — upsert one field, creating
|
|
106
202
|
# the block and the key as needed.
|
|
203
|
+
# _yq_esc <string> — escape \ and " for a double-quoted YAML scalar.
|
|
204
|
+
_yq_esc() {
|
|
205
|
+
local s="$1"
|
|
206
|
+
s="${s//\\/\\\\}"
|
|
207
|
+
printf '%s' "${s//\"/\\\"}"
|
|
208
|
+
}
|
|
209
|
+
|
|
107
210
|
qmap_set() {
|
|
108
211
|
local file="$1" block="$2" key="$3" field="$4" value="$5"
|
|
109
212
|
[ -f "$file" ] || die "no such file: $file"
|
|
213
|
+
# url/path values can carry a `"`; escape before it reaches the quoted
|
|
214
|
+
# scalar the writer emits.
|
|
215
|
+
value="$(_yq_esc "$value")"
|
|
110
216
|
_qmap_stage "$file" -v block="$block" -v key="$key" -v field="$field" -v value="$value" '
|
|
111
217
|
function keyline() { return " \"" key "\":" }
|
|
112
218
|
function fieldline() { return " " field ": \"" value "\"" }
|
|
@@ -134,10 +240,13 @@ qmap_set() {
|
|
|
134
240
|
if (c > 0 && substr(line, 1, c - 1) == field) { print fieldline(); done = 1; next }
|
|
135
241
|
print; next
|
|
136
242
|
}
|
|
137
|
-
{ print }
|
|
243
|
+
{ last = $0; print }
|
|
138
244
|
END {
|
|
139
245
|
flush_key(); flush_block()
|
|
140
246
|
if (!blockseen) {
|
|
247
|
+
# A block appended to a file that does not end blank would
|
|
248
|
+
# otherwise glue itself onto the previous section.
|
|
249
|
+
if (last != "") print ""
|
|
141
250
|
print block ":"
|
|
142
251
|
print keyline()
|
|
143
252
|
print fieldline()
|
|
@@ -150,6 +259,7 @@ qmap_set() {
|
|
|
150
259
|
qmap_set_value() {
|
|
151
260
|
local file="$1" block="$2" key="$3" value="$4"
|
|
152
261
|
[ -f "$file" ] || die "no such file: $file"
|
|
262
|
+
value="$(_yq_esc "$value")"
|
|
153
263
|
_qmap_stage "$file" -v block="$block" -v key="$key" -v value="$value" '
|
|
154
264
|
function entry() { return " \"" key "\": \"" value "\"" }
|
|
155
265
|
{ sub(/\r$/, "") }
|
|
@@ -159,10 +269,14 @@ qmap_set_value() {
|
|
|
159
269
|
s = substr($0, 4); q = index(s, "\"")
|
|
160
270
|
if (q > 0 && substr(s, 1, q - 1) == key) { print entry(); done = 1; next }
|
|
161
271
|
}
|
|
162
|
-
{ print }
|
|
272
|
+
{ last = $0; print }
|
|
163
273
|
END {
|
|
164
274
|
if (inb && !done) { print entry(); done = 1 }
|
|
165
|
-
if (!blockseen) {
|
|
275
|
+
if (!blockseen) {
|
|
276
|
+
if (last != "") print ""
|
|
277
|
+
print block ":"
|
|
278
|
+
print entry()
|
|
279
|
+
}
|
|
166
280
|
}
|
|
167
281
|
'
|
|
168
282
|
}
|
|
@@ -187,8 +301,147 @@ qmap_delete_key() {
|
|
|
187
301
|
'
|
|
188
302
|
}
|
|
189
303
|
|
|
304
|
+
# qmap_delete_field <file> <block> <key> <field> — drop one field from a
|
|
305
|
+
# quoted-key entry without disturbing its other fields or surrounding YAML.
|
|
306
|
+
qmap_delete_field() {
|
|
307
|
+
local file="$1" block="$2" key="$3" field="$4"
|
|
308
|
+
[ -f "$file" ] || return 0
|
|
309
|
+
_qmap_stage "$file" -v block="$block" -v key="$key" -v field="$field" '
|
|
310
|
+
{ sub(/\r$/, "") }
|
|
311
|
+
$0 ~ "^" block ":[ \t]*$" { inb = 1; print; next }
|
|
312
|
+
inb && /^[^ #]/ { inb = 0; ink = 0 }
|
|
313
|
+
inb && /^ "/ {
|
|
314
|
+
s = substr($0, 4); q = index(s, "\"")
|
|
315
|
+
ink = (q > 0 && substr(s, 1, q - 1) == key)
|
|
316
|
+
print; next
|
|
317
|
+
}
|
|
318
|
+
inb && ink && /^ [A-Za-z_]/ {
|
|
319
|
+
line = $0; sub(/^ /, "", line)
|
|
320
|
+
c = index(line, ":")
|
|
321
|
+
if (c > 0 && substr(line, 1, c - 1) == field) next
|
|
322
|
+
}
|
|
323
|
+
{ print }
|
|
324
|
+
'
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
# --- registries: a trust LIST, not a scope map -----------------------------
|
|
328
|
+
# The block holds registry repo URLs in trust order. Two shapes are read so
|
|
329
|
+
# early manifests keep working: the list form (`- "url"`) and the retired
|
|
330
|
+
# flat-map form (`"@scope": "url"` — the scope label is ignored, the URL is
|
|
331
|
+
# simply another registry).
|
|
332
|
+
|
|
333
|
+
# registries_list <file> — registry URLs, one per line, manifest order.
|
|
334
|
+
registries_list() {
|
|
335
|
+
[ -f "$1" ] || return 0
|
|
336
|
+
awk '
|
|
337
|
+
{ sub(/\r$/, "") }
|
|
338
|
+
/^registries:[ \t]*$/ { inb = 1; next }
|
|
339
|
+
inb && /^[^ #]/ { inb = 0 }
|
|
340
|
+
inb {
|
|
341
|
+
line = $0
|
|
342
|
+
if (line ~ /^[ \t]*-[ \t]*/) {
|
|
343
|
+
sub(/^[ \t]*-[ \t]*/, "", line)
|
|
344
|
+
} else if (line ~ /^ "/) {
|
|
345
|
+
c = index(line, ":")
|
|
346
|
+
if (c == 0) next
|
|
347
|
+
line = substr(line, c + 1)
|
|
348
|
+
} else next
|
|
349
|
+
sub(/^[ \t]+/, "", line)
|
|
350
|
+
gsub(/["\x27]/, "", line)
|
|
351
|
+
sub(/[ \t]+#.*$/, "", line)
|
|
352
|
+
sub(/[ \t]+$/, "", line)
|
|
353
|
+
if (line != "") print line
|
|
354
|
+
}
|
|
355
|
+
' "$1"
|
|
356
|
+
}
|
|
357
|
+
|
|
358
|
+
# registries_add <file> <url> — idempotent append in list form.
|
|
359
|
+
registries_add() {
|
|
360
|
+
local file="$1" url="$2" existing
|
|
361
|
+
while IFS= read -r existing; do
|
|
362
|
+
[ "$existing" = "$url" ] && return 0
|
|
363
|
+
done < <(registries_list "$file")
|
|
364
|
+
_qmap_stage "$file" -v url="$url" '
|
|
365
|
+
function entry() { return " - \"" url "\"" }
|
|
366
|
+
{ sub(/\r$/, "") }
|
|
367
|
+
/^registries:[ \t]*$/ { blockseen = 1; inb = 1; print; next }
|
|
368
|
+
inb && /^[^ #]/ { if (!done) { print entry(); done = 1 }; inb = 0 }
|
|
369
|
+
{ last = $0; print }
|
|
370
|
+
END {
|
|
371
|
+
if (inb && !done) { print entry(); done = 1 }
|
|
372
|
+
if (!blockseen) {
|
|
373
|
+
if (last != "") print ""
|
|
374
|
+
print "registries:"
|
|
375
|
+
print entry()
|
|
376
|
+
}
|
|
377
|
+
}
|
|
378
|
+
'
|
|
379
|
+
}
|
|
380
|
+
|
|
381
|
+
# registries_remove <file> <url> — drop the entry, either shape.
|
|
382
|
+
registries_remove() {
|
|
383
|
+
local file="$1" url="$2"
|
|
384
|
+
[ -f "$file" ] || return 0
|
|
385
|
+
_qmap_stage "$file" -v url="$url" '
|
|
386
|
+
{ sub(/\r$/, "") }
|
|
387
|
+
/^registries:[ \t]*$/ { inb = 1; print; next }
|
|
388
|
+
inb && /^[^ #]/ { inb = 0 }
|
|
389
|
+
inb {
|
|
390
|
+
line = $0
|
|
391
|
+
v = ""
|
|
392
|
+
if (line ~ /^[ \t]*-[ \t]*/) { v = line; sub(/^[ \t]*-[ \t]*/, "", v) }
|
|
393
|
+
else if (line ~ /^ "/) { c = index(line, ":"); if (c > 0) v = substr(line, c + 1) }
|
|
394
|
+
if (v != "") {
|
|
395
|
+
sub(/^[ \t]+/, "", v)
|
|
396
|
+
gsub(/["\x27]/, "", v)
|
|
397
|
+
sub(/[ \t]+#.*$/, "", v)
|
|
398
|
+
sub(/[ \t]+$/, "", v)
|
|
399
|
+
if (v == url) next
|
|
400
|
+
}
|
|
401
|
+
}
|
|
402
|
+
{ print }
|
|
403
|
+
'
|
|
404
|
+
}
|
|
405
|
+
# True (0) if <config> already lists <entry> under any sources section. Quoted
|
|
406
|
+
# and bare spellings both count - a manifest may hold either.
|
|
407
|
+
_mig_has_source() {
|
|
408
|
+
local config="$1" entry="$2"
|
|
409
|
+
[ -f "$config" ] || return 1
|
|
410
|
+
grep -Fq -- "\"$entry\"" "$config" || grep -Fq -- "- $entry" "$config"
|
|
411
|
+
}
|
|
412
|
+
|
|
413
|
+
|
|
414
|
+
# sources_add_entry_first <file> <section> <entry> — idempotent insert at the
|
|
415
|
+
# TOP of sources.<section> (creating sources:/section as needed). Package
|
|
416
|
+
# entries go through this so project-owned entries stay later in the list —
|
|
417
|
+
# adapters copy sources in order and the last write wins, which is exactly
|
|
418
|
+
# the documented "your file overrides the package's" behavior.
|
|
419
|
+
sources_add_entry_first() {
|
|
420
|
+
local file="$1" section="$2" entry="$3"
|
|
421
|
+
_mig_has_source "$file" "$entry" && return 0
|
|
422
|
+
_qmap_stage "$file" -v section="$section" -v entry="$entry" '
|
|
423
|
+
function line() { return " - \"" entry "\"" }
|
|
424
|
+
{ sub(/\r$/, "") }
|
|
425
|
+
/^sources:[ \t]*$/ { ins = 1; sourceseen = 1; print; next }
|
|
426
|
+
ins && /^[^ #]/ {
|
|
427
|
+
if (!secseen && !done) { print " " section ":"; print line(); done = 1 }
|
|
428
|
+
ins = 0
|
|
429
|
+
}
|
|
430
|
+
ins && $0 ~ "^ " section ":[ \t]*$" { secseen = 1; print; print line(); done = 1; next }
|
|
431
|
+
{ print }
|
|
432
|
+
END {
|
|
433
|
+
if (ins && !secseen && !done) { print " " section ":"; print line(); done = 1 }
|
|
434
|
+
if (!sourceseen) {
|
|
435
|
+
print "sources:"
|
|
436
|
+
print " " section ":"
|
|
437
|
+
print line()
|
|
438
|
+
}
|
|
439
|
+
}
|
|
440
|
+
'
|
|
441
|
+
}
|
|
442
|
+
|
|
190
443
|
# sources_remove_entry <file> <section> <entry> — remove `- "entry"` from
|
|
191
|
-
# sources.<section>.
|
|
444
|
+
# sources.<section>.
|
|
192
445
|
sources_remove_entry() {
|
|
193
446
|
local file="$1" section="$2" entry="$3"
|
|
194
447
|
[ -f "$file" ] || return 0
|
package/cli/lib/registry.sh
CHANGED
|
@@ -1,36 +1,46 @@
|
|
|
1
1
|
#!/bin/bash
|
|
2
2
|
# Name -> source resolution and package fetching.
|
|
3
3
|
#
|
|
4
|
-
#
|
|
5
|
-
#
|
|
6
|
-
#
|
|
7
|
-
#
|
|
8
|
-
#
|
|
9
|
-
#
|
|
4
|
+
# A name resolves ONLY through the manifest's `registries:` trust list (git
|
|
5
|
+
# repos holding index.yaml, consulted in order; the first to declare the name
|
|
6
|
+
# wins). There is deliberately no built-in catalog and no name->github
|
|
7
|
+
# convention: the CLI core knows no vendor, and a name nobody explicitly
|
|
8
|
+
# trusted must never silently turn into an install from a guessed URL.
|
|
9
|
+
# Sources without a registry are always EXPLICIT: github:org/repo, git+<url>.
|
|
10
|
+
# Vendor defaults exist only as lines `init` writes into the manifest —
|
|
11
|
+
# visible, reviewable, deletable.
|
|
12
|
+
#
|
|
13
|
+
# The NAME is the trust anchor a developer reasons with; source integrity is
|
|
14
|
+
# a separate mechanism: the lock pins url+sha, and `status --check` flags
|
|
15
|
+
# resolution/lock url drift.
|
|
10
16
|
#
|
|
11
17
|
# An index is itself fetched with git (never curl): auth, proxies and private
|
|
12
18
|
# hosting all come for free, and the CLI keeps the engine's bash+awk+git-only
|
|
13
19
|
# dependency footprint.
|
|
14
20
|
|
|
15
|
-
default_index_file() {
|
|
16
|
-
local f="$CLI_DIR/../registry/index.yaml"
|
|
17
|
-
[ -f "$f" ] && printf '%s' "$f"
|
|
18
|
-
return 0
|
|
19
|
-
}
|
|
20
|
-
|
|
21
21
|
# _fetch_index <registry-repo-url> — clones the registry repo (shallow) and
|
|
22
22
|
# prints the path of its index.yaml; empty on failure. Cached per run+url.
|
|
23
23
|
_fetch_index() {
|
|
24
|
-
local url="$1"
|
|
25
|
-
|
|
24
|
+
local url="$1" key tmp
|
|
25
|
+
assert_safe_source_url "$url"
|
|
26
|
+
# Per-run private cache dir under an UNPREDICTABLE mktemp path (a fixed
|
|
27
|
+
# /tmp/...-$$ would be trusted on cache hit and pre-creatable by others).
|
|
28
|
+
# No EXIT trap: _fetch_index is always called inside $( ), so a trap would
|
|
29
|
+
# fire in that subshell and delete the dir before the caller reads it. The
|
|
30
|
+
# dir is small and OS-reaped from the temp root; IS_REMOTE_CACHE-style
|
|
31
|
+
# persistence is exported so repeated calls in one command share it.
|
|
32
|
+
if [ -z "${IS_CLI_INDEX_CACHE:-}" ]; then
|
|
33
|
+
IS_CLI_INDEX_CACHE="$(mktemp -d -t intelligence-cli-index-XXXXXX 2>/dev/null || mktemp -d)"
|
|
34
|
+
export IS_CLI_INDEX_CACHE
|
|
35
|
+
fi
|
|
36
|
+
local cache="$IS_CLI_INDEX_CACHE"
|
|
26
37
|
key="$(printf '%s' "$url" | cksum | awk '{print $1}')"
|
|
27
38
|
if [ -f "$cache/$key/index.yaml" ]; then
|
|
28
39
|
printf '%s' "$cache/$key/index.yaml"
|
|
29
40
|
return 0
|
|
30
41
|
fi
|
|
31
|
-
mkdir -p "$cache"
|
|
32
42
|
tmp="$cache/$key"
|
|
33
|
-
if GIT_TERMINAL_PROMPT=0 git -c core.autocrlf=false clone --depth 1 --quiet "$url" "$tmp" 2>/dev/null \
|
|
43
|
+
if GIT_TERMINAL_PROMPT=0 git -c core.autocrlf=false clone --depth 1 --quiet -- "$url" "$tmp" 2>/dev/null \
|
|
34
44
|
&& [ -f "$tmp/index.yaml" ]; then
|
|
35
45
|
printf '%s' "$tmp/index.yaml"
|
|
36
46
|
fi
|
|
@@ -43,38 +53,57 @@ _fetch_index() {
|
|
|
43
53
|
# shellcheck disable=SC2034
|
|
44
54
|
resolve_package_source() {
|
|
45
55
|
local manifest="$1" name="$2"
|
|
46
|
-
local scope="${name%%/*}" short="${name#*/}"
|
|
47
56
|
RES_URL=""; RES_PATH=""; RES_VIA=""
|
|
48
57
|
|
|
58
|
+
local reg_url index
|
|
49
59
|
if [ -n "$manifest" ] && [ -f "$manifest" ]; then
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
if [ -n "$reg_url" ]; then
|
|
60
|
+
while IFS= read -r reg_url; do
|
|
61
|
+
[ -n "$reg_url" ] || continue
|
|
53
62
|
index="$(_fetch_index "${reg_url#git+}")"
|
|
54
|
-
[ -
|
|
63
|
+
if [ -z "$index" ]; then
|
|
64
|
+
echo " WARN: registry unreachable or missing index.yaml, skipped: $reg_url" >&2
|
|
65
|
+
continue
|
|
66
|
+
fi
|
|
55
67
|
RES_URL="$(qmap_field "$index" "packages" "$name" "url")"
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
68
|
+
if [ -n "$RES_URL" ]; then
|
|
69
|
+
RES_PATH="$(qmap_field "$index" "packages" "$name" "path")"
|
|
70
|
+
RES_VIA="registry:$reg_url"
|
|
71
|
+
return 0
|
|
72
|
+
fi
|
|
73
|
+
done < <(registries_list "$manifest")
|
|
61
74
|
fi
|
|
75
|
+
# No trusted registry declares the name. Deliberately NOT an invented
|
|
76
|
+
# URL: the caller reports it, with suggestions.
|
|
77
|
+
return 0
|
|
78
|
+
}
|
|
62
79
|
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
80
|
+
# suggest_similar <manifest-or-empty> <@scope/name> — "Did you mean" lines to
|
|
81
|
+
# stderr, matched on the short name (singular/plural tolerant) across every
|
|
82
|
+
# trusted registry.
|
|
83
|
+
suggest_similar() {
|
|
84
|
+
local manifest="$1" name="$2"
|
|
85
|
+
local want cand cshort reg_url index seen=" "
|
|
86
|
+
want="${name#*/}"; want="${want%s}"
|
|
87
|
+
_suggest_from() {
|
|
88
|
+
local idx="$1"
|
|
89
|
+
[ -n "$idx" ] && [ -f "$idx" ] || return 0
|
|
90
|
+
while IFS= read -r cand; do
|
|
91
|
+
[ -n "$cand" ] || continue
|
|
92
|
+
case "$seen" in *" $cand "*) continue ;; esac
|
|
93
|
+
cshort="${cand#*/}"; cshort="${cshort%s}"
|
|
94
|
+
if [ "$cshort" = "$want" ] && [ "$cand" != "$name" ]; then
|
|
95
|
+
seen="$seen$cand "
|
|
96
|
+
echo " Did you mean: intelligence package add $cand" >&2
|
|
97
|
+
fi
|
|
98
|
+
done < <(qmap_keys "$idx" "packages")
|
|
99
|
+
}
|
|
100
|
+
if [ -n "$manifest" ] && [ -f "$manifest" ]; then
|
|
101
|
+
while IFS= read -r reg_url; do
|
|
102
|
+
[ -n "$reg_url" ] || continue
|
|
103
|
+
index="$(_fetch_index "${reg_url#git+}")"
|
|
104
|
+
_suggest_from "$index"
|
|
105
|
+
done < <(registries_list "$manifest")
|
|
72
106
|
fi
|
|
73
|
-
|
|
74
|
-
RES_URL="https://github.com/${scope#@}/$short.git"
|
|
75
|
-
RES_PATH=""
|
|
76
|
-
RES_VIA="convention"
|
|
77
|
-
return 0
|
|
78
107
|
}
|
|
79
108
|
|
|
80
109
|
# fetch_package <url> <ref> <subpath> <dest-dir>
|
|
@@ -84,25 +113,45 @@ resolve_package_source() {
|
|
|
84
113
|
# store holds the bytes the package published.
|
|
85
114
|
fetch_package() {
|
|
86
115
|
local url="$1" ref="$2" subpath="$3" dest="$4"
|
|
116
|
+
assert_safe_source_url "$url"
|
|
117
|
+
assert_safe_ref "$ref"
|
|
118
|
+
# Bundle seed: the engine-content package at the CLI's own version copies
|
|
119
|
+
# from the npm bundle — no network, which keeps init, sync restoration and
|
|
120
|
+
# archived-project conversion offline. Keyed on the
|
|
121
|
+
# full (url, path, ref) triple; any other version or source falls through
|
|
122
|
+
# to the normal clone.
|
|
123
|
+
if [ "$url" = "$SYNC_PKG_URL" ] && [ "$subpath" = "$SYNC_PKG_PATH" ] \
|
|
124
|
+
&& [ "${ref#v}" = "$(bundled_engine_version)" ]; then
|
|
125
|
+
[ -n "${IS_BUNDLED_PKG_DIR:-}" ] && [ -d "$IS_BUNDLED_PKG_DIR" ] \
|
|
126
|
+
|| die "bundled engine content not found next to the CLI — reinstall @ainova-systems/intelligence"
|
|
127
|
+
rm -rf "$dest"
|
|
128
|
+
mkdir -p "$dest"
|
|
129
|
+
cp -R "$IS_BUNDLED_PKG_DIR/." "$dest/"
|
|
130
|
+
rm -rf "$dest/.git"
|
|
131
|
+
if [ -f "$IS_ENGINE_DIR/ENGINE_SHA" ]; then
|
|
132
|
+
tr -d ' \t\r\n' < "$IS_ENGINE_DIR/ENGINE_SHA"
|
|
133
|
+
fi
|
|
134
|
+
return 0
|
|
135
|
+
fi
|
|
87
136
|
local tmp sha src
|
|
88
137
|
local -a branch_arg=()
|
|
89
138
|
[ -n "$ref" ] && branch_arg=(--branch "$ref")
|
|
90
139
|
tmp="$(mktemp -d -t intelligence-pkg-XXXXXX 2>/dev/null || mktemp -d)"
|
|
91
140
|
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
|
|
141
|
+
clone --depth 1 "${branch_arg[@]+"${branch_arg[@]}"}" --quiet -- "$url" "$tmp/clone" 2>/dev/null; then
|
|
93
142
|
# A SHA is not clonable with --branch — fall back to a full clone + checkout.
|
|
94
143
|
rm -rf "$tmp/clone"
|
|
95
144
|
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 \
|
|
145
|
+
clone --quiet -- "$url" "$tmp/clone" 2>/dev/null \
|
|
97
146
|
|| { rm -rf "$tmp"; die "cannot clone $url"; }
|
|
98
|
-
[ -z "$ref" ] || git -C "$tmp/clone" checkout --quiet "$ref" \
|
|
147
|
+
[ -z "$ref" ] || git -C "$tmp/clone" checkout --quiet "$ref" -- \
|
|
99
148
|
|| { rm -rf "$tmp"; die "ref '$ref' not found in $url"; }
|
|
100
149
|
fi
|
|
101
150
|
sha="$(git -C "$tmp/clone" rev-parse HEAD)"
|
|
102
151
|
src="$tmp/clone"
|
|
103
152
|
if [ -n "$subpath" ]; then
|
|
104
153
|
case "$subpath" in
|
|
105
|
-
|
|
154
|
+
*..*|/*|*[\"\']*) rm -rf "$tmp"; die "unsafe path '$subpath'" ;;
|
|
106
155
|
esac
|
|
107
156
|
src="$tmp/clone/$subpath"
|
|
108
157
|
[ -d "$src" ] || { rm -rf "$tmp"; die "path '$subpath' not found in $url${ref:+@$ref}" ; }
|
|
@@ -119,14 +168,14 @@ fetch_package() {
|
|
|
119
168
|
|
|
120
169
|
# wire_package_sources <manifest> <@scope/name> <store-rel-dir>
|
|
121
170
|
# The provides convention: whichever of rules/agents/skills the installed
|
|
122
|
-
# package has becomes a sources entry
|
|
123
|
-
#
|
|
171
|
+
# package has becomes a sources entry — inserted FIRST in its section, so
|
|
172
|
+
# project-owned entries come later and win on same-named artifacts.
|
|
124
173
|
wire_package_sources() {
|
|
125
174
|
local manifest="$1" name="$2" rel="$3" root="$4"
|
|
126
175
|
local section
|
|
127
176
|
for section in rules agents skills; do
|
|
128
177
|
if [ -d "$root/$rel/$section" ]; then
|
|
129
|
-
|
|
178
|
+
sources_add_entry_first "$manifest" "$section" "$rel/$section"
|
|
130
179
|
fi
|
|
131
180
|
done
|
|
132
181
|
}
|
package/cli/lib/semver.sh
CHANGED
|
@@ -39,9 +39,12 @@ semver_match() {
|
|
|
39
39
|
""|"*"|latest) return 0 ;;
|
|
40
40
|
esac
|
|
41
41
|
local op="" base="$range"
|
|
42
|
+
# Both strip patterns are quoted: an unquoted `~` in ${range#~} is a tilde
|
|
43
|
+
# EXPANSION (it becomes $HOME), so the prefix never strips and every
|
|
44
|
+
# `~x.y.z` range silently matches nothing.
|
|
42
45
|
case "$range" in
|
|
43
|
-
"^"*) op="^"; base="${range
|
|
44
|
-
"~"*) op="~"; base="${range
|
|
46
|
+
"^"*) op="^"; base="${range#'^'}" ;;
|
|
47
|
+
"~"*) op="~"; base="${range#'~'}" ;;
|
|
45
48
|
esac
|
|
46
49
|
base="${base#v}"
|
|
47
50
|
semver_is_stable "$base" || return 1
|
|
@@ -72,7 +75,11 @@ semver_match() {
|
|
|
72
75
|
# onto their tag.
|
|
73
76
|
list_remote_versions() {
|
|
74
77
|
local url="$1"
|
|
75
|
-
|
|
78
|
+
assert_safe_source_url "$url"
|
|
79
|
+
# `|| true` inside the pipeline: an unreachable repo must read as "no
|
|
80
|
+
# versions", not kill a pipefail caller mid-command-substitution —
|
|
81
|
+
# reachability is the CALLER's question (add probes it explicitly).
|
|
82
|
+
{ GIT_TERMINAL_PROMPT=0 git ls-remote --tags -- "$url" 2>/dev/null || true; } \
|
|
76
83
|
| awk '{
|
|
77
84
|
sub(/\r$/, "")
|
|
78
85
|
ref = $2
|
|
@@ -89,7 +96,8 @@ list_remote_versions() {
|
|
|
89
96
|
# prints "<tag> <sha>". Peeled sha (the commit a tag object points at) wins.
|
|
90
97
|
remote_tag_for_version() {
|
|
91
98
|
local url="$1" ver="${2#v}"
|
|
92
|
-
|
|
99
|
+
assert_safe_source_url "$url"
|
|
100
|
+
{ GIT_TERMINAL_PROMPT=0 git ls-remote --tags -- "$url" 2>/dev/null || true; } \
|
|
93
101
|
| awk -v want="$ver" '
|
|
94
102
|
{
|
|
95
103
|
sub(/\r$/, "")
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
9be4945e8194e82d53d510e72b7a30769ff30e95
|
|
@@ -1,22 +1,21 @@
|
|
|
1
1
|
#!/bin/bash
|
|
2
|
-
#
|
|
3
|
-
#
|
|
2
|
+
# Intelligence CLI: project adapter template
|
|
3
|
+
# Scaffold this file with: intelligence adapter create <name>
|
|
4
4
|
#
|
|
5
5
|
# This file is NOT executable as-is — `<name>` placeholders below would be
|
|
6
6
|
# parsed by bash as input redirection (`<` operator). Replace every
|
|
7
7
|
# occurrence with your adapter name before sourcing.
|
|
8
8
|
#
|
|
9
9
|
# Required:
|
|
10
|
-
# 1.
|
|
11
|
-
# 2.
|
|
12
|
-
#
|
|
13
|
-
# 4. Add target to config.yaml:
|
|
10
|
+
# 1. Implement sync_to_<name>() and its three content transforms
|
|
11
|
+
# 2. Enable it after implementation: intelligence adapter enable <name>
|
|
12
|
+
# This adds the target to intelligence.yaml when it is absent:
|
|
14
13
|
# targets:
|
|
15
14
|
# <name>: { enabled: true, output: ".<name>" }
|
|
16
15
|
#
|
|
17
16
|
# The sync_to_<name>() function receives:
|
|
18
17
|
# $1 = repo_root — absolute path to the project root
|
|
19
|
-
# $2 = config_file — absolute path to
|
|
18
|
+
# $2 = config_file — absolute path to intelligence.yaml
|
|
20
19
|
# $3 = output_dir — absolute path to the output directory (e.g., .myide/)
|
|
21
20
|
#
|
|
22
21
|
# Available library functions (from lib/common.sh):
|
|
@@ -29,10 +28,12 @@
|
|
|
29
28
|
# get_model_default(ide, tier) — hardcoded default for ide:tier
|
|
30
29
|
# map_access_to_claude_tools(access) — full->"" (no tools list; inherits all), readonly->restricted
|
|
31
30
|
# map_access_to_claude_disallowed(access) — readonly->"Write, Edit", full->""
|
|
32
|
-
# read_yaml_list(
|
|
31
|
+
# read_yaml_list(manifest, section) — read a source list from intelligence.yaml
|
|
33
32
|
# get_target_field(config, target, field) — read a target's config field
|
|
34
33
|
|
|
35
|
-
|
|
34
|
+
# Project adapters are sourced by engine/sync.sh after the shared library is
|
|
35
|
+
# loaded. Keep this template position-independent: after scaffolding it lives
|
|
36
|
+
# under the project's content directory, not beside engine/lib/.
|
|
36
37
|
|
|
37
38
|
# Sync rules for <agent-name>
|
|
38
39
|
# Typical transformations:
|