@ainova-systems/intelligence 0.11.0-rc.1 → 0.11.0-rc.10

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 (72) hide show
  1. package/README.md +19 -16
  2. package/cli/commands/adapter.sh +153 -0
  3. package/cli/commands/init.sh +171 -34
  4. package/cli/commands/package.sh +30 -0
  5. package/cli/commands/registry.sh +65 -30
  6. package/cli/commands/status.sh +20 -11
  7. package/cli/commands/sync.sh +9 -4
  8. package/cli/commands/update.sh +80 -46
  9. package/cli/engine-package.yaml +14 -0
  10. package/cli/intelligence +37 -26
  11. package/cli/internal/align-project.sh +134 -0
  12. package/cli/internal/check.sh +128 -0
  13. package/cli/internal/convert-legacy.sh +400 -0
  14. package/cli/{commands/add.sh → internal/package-add.sh} +37 -19
  15. package/cli/{commands/list.sh → internal/package-list.sh} +10 -5
  16. package/cli/{commands/remove.sh → internal/package-remove.sh} +16 -4
  17. package/cli/internal/package-search.sh +83 -0
  18. package/cli/internal/package-update.sh +103 -0
  19. package/cli/internal/restore.sh +130 -0
  20. package/cli/internal/target-state.sh +57 -0
  21. package/cli/lib/cli-common.sh +213 -63
  22. package/cli/lib/lockfile.sh +15 -9
  23. package/cli/lib/manifest.sh +257 -4
  24. package/cli/lib/registry.sh +96 -47
  25. package/cli/lib/semver.sh +12 -4
  26. package/engine/ENGINE_SHA +1 -0
  27. package/engine/{scripts/adapters → adapters}/_template.sh +10 -9
  28. package/engine/{scripts/adapters → adapters}/agents.sh +20 -25
  29. package/engine/{scripts/adapters → adapters}/opencode.sh +1 -1
  30. package/engine/{scripts/lib → lib}/common.sh +30 -538
  31. package/engine/lib/contract.sh +120 -0
  32. package/engine/sync.sh +238 -0
  33. package/package.json +5 -5
  34. package/{engine → packages/sync}/agents/intelligence-architect.md +5 -3
  35. package/{engine → packages/sync}/agents/intelligence-operator.md +10 -13
  36. package/packages/sync/references/adapters.md +252 -0
  37. package/packages/sync/references/conventions.md +385 -0
  38. package/{engine → packages/sync}/rules/intelligence-authoring.md +7 -7
  39. package/{engine → packages/sync}/skills/intelligence-add-agent/SKILL.md +7 -7
  40. package/{engine → packages/sync}/skills/intelligence-add-rule/SKILL.md +5 -5
  41. package/{engine → packages/sync}/skills/intelligence-add-skill/SKILL.md +5 -5
  42. package/{engine → packages/sync}/skills/intelligence-extract-skill/SKILL.md +2 -2
  43. package/packages/sync/skills/intelligence-install-adapter/SKILL.md +38 -0
  44. package/{engine → packages/sync}/skills/intelligence-learn-from-context/SKILL.md +3 -3
  45. package/packages/sync/skills/intelligence-learn-from-repository/SKILL.md +55 -0
  46. package/{engine → packages/sync}/skills/intelligence-review-skills/SKILL.md +7 -7
  47. package/packages/sync/skills/intelligence-sync/SKILL.md +20 -0
  48. package/packages/sync/skills/intelligence-uninstall-adapter/SKILL.md +24 -0
  49. package/packages/sync/skills/intelligence-update/SKILL.md +43 -0
  50. package/cli/commands/doctor.sh +0 -100
  51. package/cli/commands/install.sh +0 -84
  52. package/cli/commands/migrate.sh +0 -251
  53. package/cli/commands/upgrade.sh +0 -27
  54. package/engine/INIT.md +0 -498
  55. package/engine/docs/ADAPTERS.md +0 -212
  56. package/engine/docs/CLI.md +0 -91
  57. package/engine/docs/CONVENTIONS.md +0 -440
  58. package/engine/scripts/lib/layout.sh +0 -51
  59. package/engine/scripts/lib/migrations.sh +0 -708
  60. package/engine/scripts/sync.sh +0 -311
  61. package/engine/scripts/update.sh +0 -237
  62. package/engine/skills/intelligence-install-adapter/SKILL.md +0 -31
  63. package/engine/skills/intelligence-sync/SKILL.md +0 -18
  64. package/engine/skills/intelligence-uninstall-adapter/SKILL.md +0 -42
  65. package/engine/skills/intelligence-update/SKILL.md +0 -159
  66. package/registry/index.yaml +0 -15
  67. /package/engine/{scripts/VERSION → VERSION} +0 -0
  68. /package/engine/{scripts/adapters → adapters}/claude.sh +0 -0
  69. /package/engine/{scripts/adapters → adapters}/codex.sh +0 -0
  70. /package/engine/{scripts/adapters → adapters}/copilot.sh +0 -0
  71. /package/engine/{scripts/adapters → adapters}/cursor.sh +0 -0
  72. /package/engine/{scripts/adapters → adapters}/pi.sh +0 -0
@@ -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 read_engine_stamp is hardwired to its own key.
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="${TMPDIR:-/tmp}/intelligence-lock-$$.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="${TMPDIR:-/tmp}/intelligence-lock-$$.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"
@@ -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) { print block ":"; print entry() }
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>. Inverse of the engine-side _mig_add_source.
444
+ # sources.<section>.
192
445
  sources_remove_entry() {
193
446
  local file="$1" section="$2" entry="$3"
194
447
  [ -f "$file" ] || return 0
@@ -1,36 +1,46 @@
1
1
  #!/bin/bash
2
2
  # Name -> source resolution and package fetching.
3
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
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" cache key tmp
25
- cache="${TMPDIR:-/tmp}/intelligence-cli-index-$$"
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
- local reg_url index
51
- reg_url="$(qmap_value "$manifest" "registries" "$scope")"
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
- [ -n "$index" ] || die "registry for scope '$scope' is unreachable or has no index.yaml: $reg_url"
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
- 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
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
- 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
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
- *..*) rm -rf "$tmp"; die "unsafe path '$subpath'" ;;
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. Reuses the engine's idempotent,
123
- # comment-preserving _mig_add_source.
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
- _mig_add_source "$manifest" "$section" "$rel/$section"
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
- GIT_TERMINAL_PROMPT=0 git ls-remote --tags "$url" 2>/dev/null \
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
- GIT_TERMINAL_PROMPT=0 git ls-remote --tags "$url" 2>/dev/null \
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
+ 6158bbd8821e2b1c26bf067c5016d9adc1297954
@@ -1,22 +1,21 @@
1
1
  #!/bin/bash
2
- # intelligence-sync: Adapter template
3
- # Copy this file to create a new IDE adapter.
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. Name the file: <ide-name>.sh (e.g., myide.sh)
11
- # 2. Replace every `<name>` placeholder with your adapter name
12
- # 3. Implement sync_to_<name>() function
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 config.yaml
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(config, section) — read list from config.yaml
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
- source "$(dirname "${BASH_SOURCE[0]}")/../lib/common.sh"
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: