@ainova-systems/intelligence 0.11.0-rc.6 → 0.11.0-rc.7
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 +2 -2
- package/cli/commands/add.sh +8 -7
- package/cli/commands/install.sh +8 -1
- package/cli/commands/migrate.sh +49 -19
- package/cli/commands/registry.sh +1 -0
- package/cli/commands/sync.sh +1 -1
- package/cli/commands/update.sh +12 -3
- package/cli/engine-package.yaml +2 -2
- package/cli/intelligence +27 -15
- package/cli/lib/cli-common.sh +34 -7
- package/cli/lib/lockfile.sh +14 -8
- package/cli/lib/manifest.sh +20 -1
- package/cli/lib/registry.sh +25 -11
- package/cli/lib/semver.sh +4 -2
- package/engine/ENGINE_SHA +1 -0
- package/engine/{scripts/adapters → adapters}/agents.sh +10 -15
- package/engine/{scripts/lib → lib}/common.sh +15 -519
- package/engine/lib/contract.sh +120 -0
- package/engine/sync.sh +233 -0
- package/package.json +6 -5
- package/engine/INIT.md +0 -500
- package/engine/docs/CLI.md +0 -90
- package/engine/scripts/ENGINE_SHA +0 -1
- 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/{scripts/VERSION → VERSION} +0 -0
- /package/engine/{scripts/adapters → adapters}/_template.sh +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}/opencode.sh +0 -0
- /package/engine/{scripts/adapters → adapters}/pi.sh +0 -0
- /package/{engine → packages/sync}/agents/intelligence-architect.md +0 -0
- /package/{engine → packages/sync}/agents/intelligence-operator.md +0 -0
- /package/{engine → packages/sync}/docs/ADAPTERS.md +0 -0
- /package/{engine → packages/sync}/docs/CONVENTIONS.md +0 -0
- /package/{engine → packages/sync}/rules/intelligence-authoring.md +0 -0
- /package/{engine → packages/sync}/skills/intelligence-add-agent/SKILL.md +0 -0
- /package/{engine → packages/sync}/skills/intelligence-add-rule/SKILL.md +0 -0
- /package/{engine → packages/sync}/skills/intelligence-add-skill/SKILL.md +0 -0
- /package/{engine → packages/sync}/skills/intelligence-extract-skill/SKILL.md +0 -0
- /package/{engine → packages/sync}/skills/intelligence-install-adapter/SKILL.md +0 -0
- /package/{engine → packages/sync}/skills/intelligence-learn-from-context/SKILL.md +0 -0
- /package/{engine → packages/sync}/skills/intelligence-review-skills/SKILL.md +0 -0
- /package/{engine → packages/sync}/skills/intelligence-sync/SKILL.md +0 -0
- /package/{engine → packages/sync}/skills/intelligence-uninstall-adapter/SKILL.md +0 -0
- /package/{engine → packages/sync}/skills/intelligence-update/SKILL.md +0 -0
package/README.md
CHANGED
|
@@ -38,9 +38,9 @@ intelligence install
|
|
|
38
38
|
| `intelligence update [pkg]` | Re-resolve version ranges, rewrite the lock |
|
|
39
39
|
| `intelligence sync [target]` | Render intelligence to every enabled tool |
|
|
40
40
|
| `intelligence list` / `doctor` / `status` | Inspect and verify |
|
|
41
|
-
| `intelligence registry add
|
|
41
|
+
| `intelligence registry add <repo-url>` | Trust a registry (a git repo with an `index.yaml`) |
|
|
42
42
|
| `intelligence migrate` | Convert a vendored intelligence-sync setup to the CLI |
|
|
43
43
|
|
|
44
|
-
Any git repo with `rules/`, `agents/` or `skills/` at its root is already an Intelligence Package: `intelligence add
|
|
44
|
+
Any git repo with `rules/`, `agents/` or `skills/` at its root is already an Intelligence Package: install it from an explicit source — `intelligence add github:your-org/your-repo` — with versions from its `x.y.z` git tags. Names (`@scope/name`) resolve only through registries this project explicitly trusts; a monorepo of packages needs one line in such an index.
|
|
45
45
|
|
|
46
46
|
Requires `git` (and on Windows the bash it ships with — Git for Windows). Engine and docs: [ainova-systems/intelligence-sync](https://github.com/ainova-systems/intelligence-sync).
|
package/cli/commands/add.sh
CHANGED
|
@@ -2,9 +2,9 @@
|
|
|
2
2
|
# intelligence add <spec> [--name @scope/name] [--no-sync]
|
|
3
3
|
#
|
|
4
4
|
# Specs:
|
|
5
|
-
# @scope/name[@range] resolved
|
|
6
|
-
# github:org/repo[#path] direct from GitHub, no registry involved
|
|
7
|
-
# git+<url>[@ref][#path] direct from any git host
|
|
5
|
+
# @scope/name[@range] resolved ONLY through the manifest's trusted registries
|
|
6
|
+
# github:org/repo[#path] explicit source: direct from GitHub, no registry involved
|
|
7
|
+
# git+<url>[@ref][#path] explicit source: direct from any git host
|
|
8
8
|
set -euo pipefail
|
|
9
9
|
source "$CLI_DIR/lib/cli-common.sh"
|
|
10
10
|
|
|
@@ -43,14 +43,15 @@ case "$spec" in
|
|
|
43
43
|
rest="${spec#git+}"
|
|
44
44
|
case "$rest" in *#*) path="${rest#*#}"; rest="${rest%%#*}" ;; esac
|
|
45
45
|
# ref = the part after the last `@` in the post-scheme URL, unless it
|
|
46
|
-
# contains `/`
|
|
47
|
-
# git
|
|
46
|
+
# contains `/` or `:` — either marks a user@host part (scp-like
|
|
47
|
+
# `git@host:repo.git` included), not a ref. A branch name containing
|
|
48
|
+
# `/` cannot be expressed here; pin it via `ref:` in the manifest.
|
|
48
49
|
base="${rest#*://}"
|
|
49
50
|
case "$base" in
|
|
50
51
|
*@*)
|
|
51
52
|
tail="${base##*@}"
|
|
52
53
|
case "$tail" in
|
|
53
|
-
|
|
54
|
+
*/*|*:*) ;;
|
|
54
55
|
*) ref="$tail"; rest="${rest%@"$tail"}" ;;
|
|
55
56
|
esac
|
|
56
57
|
;;
|
|
@@ -93,7 +94,7 @@ fi
|
|
|
93
94
|
|
|
94
95
|
# The source must exist before anything else is attempted — never die
|
|
95
96
|
# mid-pipeline with no message.
|
|
96
|
-
if ! GIT_TERMINAL_PROMPT=0 git ls-remote "$url" >/dev/null 2>&1; then
|
|
97
|
+
if ! GIT_TERMINAL_PROMPT=0 git ls-remote -- "$url" >/dev/null 2>&1; then
|
|
97
98
|
echo "ERROR: no reachable repository at $url" >&2
|
|
98
99
|
echo " Declared by ${RES_VIA:-the spec} — check the URL and your git access." >&2
|
|
99
100
|
exit 1
|
package/cli/commands/install.sh
CHANGED
|
@@ -63,6 +63,7 @@ fi
|
|
|
63
63
|
missing=""
|
|
64
64
|
while IFS= read -r name; do
|
|
65
65
|
[ -n "$name" ] || continue
|
|
66
|
+
assert_valid_pkg_name "$name"
|
|
66
67
|
if [ ! -f "$lock" ] || [ -z "$(qmap_field "$lock" "packages" "$name" "url")" ]; then
|
|
67
68
|
missing="$missing $name"
|
|
68
69
|
fi
|
|
@@ -101,8 +102,14 @@ fi
|
|
|
101
102
|
if [ -f "$lock" ]; then
|
|
102
103
|
while IFS="$LOCK_SEP" read -r name requested url path resolved sha; do
|
|
103
104
|
[ -n "$name" ] || continue
|
|
105
|
+
# The lock arrives with a cloned repo — its keys are untrusted input
|
|
106
|
+
# on their way into rm -rf paths, and its urls into git argv.
|
|
107
|
+
assert_valid_pkg_name "$name"
|
|
104
108
|
rel=".intelligence/packages/$name"
|
|
105
|
-
|
|
109
|
+
# --frozen never trusts what is already on disk: a pre-existing
|
|
110
|
+
# (possibly substituted) store dir would otherwise be synced with no
|
|
111
|
+
# sha verification at all.
|
|
112
|
+
if [ -d "$IP_ROOT/$rel" ] && [ "$force" -eq 0 ] && [ "$frozen" -eq 0 ]; then
|
|
106
113
|
continue
|
|
107
114
|
fi
|
|
108
115
|
# Fetch into staging and commit to the store only after the integrity
|
package/cli/commands/migrate.sh
CHANGED
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
# to the CLI setup: root intelligence.yaml, .intelligence/ store,
|
|
4
4
|
# intelligence.lock, no vendored engine.
|
|
5
5
|
#
|
|
6
|
-
# Transactional
|
|
6
|
+
# Transactional: STAGE everything next to the
|
|
7
7
|
# project, VERIFY the staged state, then COMMIT in an order where every
|
|
8
8
|
# destructive step happens only after a real sync of the new state reported
|
|
9
9
|
# IS_STATUS=ok. Any earlier failure leaves the project untouched.
|
|
@@ -44,6 +44,12 @@ stamp="$(read_engine_stamp "$config")"
|
|
|
44
44
|
eng="$(bundled_engine_version)"
|
|
45
45
|
[ -n "$stamp" ] && _ver_gt "$stamp" "$eng" && die "project schema $stamp is newer than this CLI's engine $eng — update the CLI first"
|
|
46
46
|
|
|
47
|
+
# The v1 `packs:` block is the one v1 shape migrate still has to read, and the
|
|
48
|
+
# engine no longer knows it — the reader lives here, with its only consumer.
|
|
49
|
+
get_pack_field() {
|
|
50
|
+
get_nested_yaml_value "$1" "packs" "$2" "$3"
|
|
51
|
+
}
|
|
52
|
+
|
|
47
53
|
# ---- Stage ----------------------------------------------------------------
|
|
48
54
|
stage="$root/.intelligence-migrate-stage.$$"
|
|
49
55
|
[ "$dry_run" -eq 1 ] && stage="$(mktemp -d -t intelligence-migrate-XXXXXX 2>/dev/null || mktemp -d)"
|
|
@@ -51,22 +57,18 @@ cleanup_stage() { rm -rf "$stage"; }
|
|
|
51
57
|
trap cleanup_stage EXIT INT TERM
|
|
52
58
|
mkdir -p "$stage/.intelligence/packages"
|
|
53
59
|
|
|
54
|
-
#
|
|
55
|
-
#
|
|
56
|
-
#
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
echo "== applying the engine migration chain ($stamp -> $eng) =="
|
|
67
|
-
run_migrations "$umbrella" "$(basename "$module_dir")" "" || die "engine migration chain failed (IS_STATUS above) — nothing converted"
|
|
68
|
-
stamp_version "$config" "$eng"
|
|
69
|
-
fi
|
|
60
|
+
# v2 carries no v1 migration chain: v1 is archived, and the archived engine is
|
|
61
|
+
# what knows how to walk an old project forward. So migrate accepts only a
|
|
62
|
+
# project already at the final v1 schema and names the exact remedy otherwise —
|
|
63
|
+
# a remedy that keeps working indefinitely, because the archive stays at the
|
|
64
|
+
# URL the project's own update.sh already clones.
|
|
65
|
+
V1_FINAL_SCHEMA="0.10.0"
|
|
66
|
+
if [ -z "$stamp" ] || _ver_gt "$V1_FINAL_SCHEMA" "$stamp"; then
|
|
67
|
+
echo "ERROR: project schema is ${stamp:-absent (pre-0.3.1)}, older than the final v1 schema $V1_FINAL_SCHEMA." >&2
|
|
68
|
+
echo " Bring it forward with its own engine first — that still works:" >&2
|
|
69
|
+
echo " bash $module_rel/scripts/update.sh --yes" >&2
|
|
70
|
+
echo " then re-run: intelligence migrate" >&2
|
|
71
|
+
exit 1
|
|
70
72
|
fi
|
|
71
73
|
|
|
72
74
|
echo "== staging =="
|
|
@@ -78,6 +80,7 @@ sync_pkg_sha="$(fetch_package "$SYNC_PKG_URL" "v$eng" "$SYNC_PKG_PATH" "$stage/$
|
|
|
78
80
|
# per-pack source facts. Recorded as rows for the config rewrite and the lock.
|
|
79
81
|
pack_rows="$stage/packs.rows"
|
|
80
82
|
: > "$pack_rows"
|
|
83
|
+
seen_names=" "
|
|
81
84
|
while IFS= read -r pack; do
|
|
82
85
|
[ -n "$pack" ] || continue
|
|
83
86
|
url="$(get_pack_field "$config" "$pack" "url")"
|
|
@@ -91,6 +94,12 @@ while IFS= read -r pack; do
|
|
|
91
94
|
case "$repo" in ""|*[!A-Za-z0-9._-]*) repo="$(printf '%s' "$repo" | tr -c 'A-Za-z0-9._-' '-')" ;; esac
|
|
92
95
|
name="@$org/$repo"
|
|
93
96
|
assert_valid_pkg_name "$name"
|
|
97
|
+
# Two packs deriving the same @org/repo would overwrite one store dir and
|
|
98
|
+
# duplicate the lock key — fail closed, the user renames a source.
|
|
99
|
+
case "$seen_names" in
|
|
100
|
+
*" $name "*) die "packs '$pack' and another both derive package name '$name' — sources must map to distinct names" ;;
|
|
101
|
+
esac
|
|
102
|
+
seen_names="$seen_names $name "
|
|
94
103
|
printf '%s\n' "$pack$LOCK_SEP$name$LOCK_SEP$url$LOCK_SEP$ref$LOCK_SEP$mirror"
|
|
95
104
|
done < <(read_yaml_keys "$config" "packs") >> "$pack_rows"
|
|
96
105
|
|
|
@@ -164,8 +173,19 @@ while IFS= read -r line || [ -n "$line" ]; do
|
|
|
164
173
|
*"- "*)
|
|
165
174
|
indent="${line%%-*}"
|
|
166
175
|
v="${line#*- }"
|
|
176
|
+
# A trailing inline comment survives the rewrite — split it
|
|
177
|
+
# off before touching quotes, or it would be mangled into the
|
|
178
|
+
# value.
|
|
179
|
+
trail=""
|
|
180
|
+
case "$v" in
|
|
181
|
+
*[[:space:]]"#"*)
|
|
182
|
+
trail=" #${v#*[[:space:]]#}"
|
|
183
|
+
v="${v%%[[:space:]]#*}"
|
|
184
|
+
;;
|
|
185
|
+
esac
|
|
186
|
+
v="${v%"${v##*[![:space:]]}"}"
|
|
167
187
|
v="${v%\"}"; v="${v#\"}"; v="${v%\'}"; v="${v#\'}"
|
|
168
|
-
echo "$indent- \"$(rewrite_source_value "$v")\""
|
|
188
|
+
echo "$indent- \"$(rewrite_source_value "$v")\"$trail"
|
|
169
189
|
continue
|
|
170
190
|
;;
|
|
171
191
|
esac
|
|
@@ -246,6 +266,8 @@ rollback() {
|
|
|
246
266
|
fi
|
|
247
267
|
echo "rolled back — the vendored setup is untouched" >&2
|
|
248
268
|
}
|
|
269
|
+
# From the first moved file until success, an interrupt must roll back too.
|
|
270
|
+
trap 'rollback; exit 130' INT TERM
|
|
249
271
|
mv "$stage/.intelligence" "$root/.intelligence"
|
|
250
272
|
cp "$manifest_stage" "$root/intelligence.yaml"
|
|
251
273
|
lock_write_from_tsv "$root/intelligence.lock" "$lock_rows"
|
|
@@ -260,13 +282,21 @@ echo "$sync_out" | grep -q '^IS_STATUS=ok' || {
|
|
|
260
282
|
rollback
|
|
261
283
|
die "sync of the migrated state did not report ok — rolled back"
|
|
262
284
|
}
|
|
285
|
+
# Point of no return: the new state is verified. From here an interrupt must
|
|
286
|
+
# NOT roll back (the old setup is being removed) — only clean the stage.
|
|
287
|
+
trap cleanup_stage EXIT INT TERM
|
|
263
288
|
|
|
264
289
|
# Only now is anything of the old setup removed — and config.yaml is kept as
|
|
265
290
|
# a reviewable backup inside the store.
|
|
266
291
|
mkdir -p "$root/.intelligence/backup"
|
|
267
292
|
cp "$config" "$root/.intelligence/backup/config.yaml"
|
|
268
293
|
while IFS="$LOCK_SEP" read -r pack name url ref mirror; do
|
|
269
|
-
|
|
294
|
+
# Containment before deletion: a mirror value is config input, and rm -rf
|
|
295
|
+
# must never follow it out of the repository.
|
|
296
|
+
case "$mirror" in
|
|
297
|
+
""|/*|*..*) [ -n "$mirror" ] && echo " WARN: mirror '$mirror' not repo-contained — left in place" >&2; continue ;;
|
|
298
|
+
esac
|
|
299
|
+
if [ -d "$root/$mirror" ]; then
|
|
270
300
|
rm -rf "${root:?}/$mirror"
|
|
271
301
|
# An emptied mirror parent (e.g. <umbrella>/external/) goes with it.
|
|
272
302
|
rmdir "$(dirname "$root/$mirror")" 2>/dev/null || true
|
package/cli/commands/registry.sh
CHANGED
|
@@ -38,6 +38,7 @@ case "$sub" in
|
|
|
38
38
|
case "$url" in
|
|
39
39
|
@*) die "registries are added by URL — names carry their own scope. To install a package: intelligence add $url/<name>" ;;
|
|
40
40
|
esac
|
|
41
|
+
assert_safe_source_url "${url#git+}"
|
|
41
42
|
require_v2
|
|
42
43
|
# Fail closed: a URL that cannot produce an index is not a registry,
|
|
43
44
|
# and recording it would turn a typo here into a confusing failure at
|
package/cli/commands/sync.sh
CHANGED
|
@@ -26,7 +26,7 @@ case "$IP_MODE" in
|
|
|
26
26
|
die "installed content missing on disk:$missing — run 'intelligence install'"
|
|
27
27
|
fi
|
|
28
28
|
export_engine_env "$IP_ROOT"
|
|
29
|
-
exec bash "$IS_ENGINE_DIR/
|
|
29
|
+
exec bash "$IS_ENGINE_DIR/sync.sh" "$@"
|
|
30
30
|
;;
|
|
31
31
|
legacy)
|
|
32
32
|
# A vendored project syncs with ITS engine — its pin is the contract,
|
package/cli/commands/update.sh
CHANGED
|
@@ -24,6 +24,8 @@ lock="$IP_ROOT/intelligence.lock"
|
|
|
24
24
|
moved=0 found=0
|
|
25
25
|
while IFS= read -r name; do
|
|
26
26
|
[ -n "$name" ] || continue
|
|
27
|
+
# Manifest keys are untrusted input on their way into store paths.
|
|
28
|
+
assert_valid_pkg_name "$name"
|
|
27
29
|
[ -n "$only" ] && [ "$name" != "$only" ] && continue
|
|
28
30
|
# A named package that exists is "found" whatever happens next — a skip
|
|
29
31
|
# message followed by "not in the manifest" would contradict itself.
|
|
@@ -60,10 +62,17 @@ while IFS= read -r name; do
|
|
|
60
62
|
continue
|
|
61
63
|
fi
|
|
62
64
|
rel=".intelligence/packages/$name"
|
|
63
|
-
#
|
|
64
|
-
#
|
|
65
|
+
# Fetch into staging FIRST: a failed clone must leave the current install
|
|
66
|
+
# fully wired and intact. Only after success is the old shape unwired
|
|
67
|
+
# (the new version may have dropped a section dir), the store swapped,
|
|
68
|
+
# and the new shape wired.
|
|
69
|
+
staging="$IP_ROOT/.intelligence/.staging-$$"
|
|
70
|
+
rm -rf "$staging"
|
|
71
|
+
sha="$(fetch_package "$url" "$tag" "$path" "$staging")"
|
|
65
72
|
unwire_package_sources "$manifest" "$rel"
|
|
66
|
-
|
|
73
|
+
rm -rf "${IP_ROOT:?}/$rel"
|
|
74
|
+
mkdir -p "$(dirname "$IP_ROOT/$rel")"
|
|
75
|
+
mv "$staging" "$IP_ROOT/$rel"
|
|
67
76
|
wire_package_sources "$manifest" "$name" "$rel" "$IP_ROOT"
|
|
68
77
|
lock_upsert "$lock" "$name" "$range" "$url" "$path" "$tag" "$sha"
|
|
69
78
|
echo " $name: ${current:-<none>} -> $tag"
|
package/cli/engine-package.yaml
CHANGED
|
@@ -9,6 +9,6 @@
|
|
|
9
9
|
# `upgrade` moves it, and at the matching version it materializes from the
|
|
10
10
|
# bundle without network.
|
|
11
11
|
name: "@ainova-systems/sync"
|
|
12
|
-
url: "https://github.com/ainova-systems/intelligence
|
|
13
|
-
path: "
|
|
12
|
+
url: "https://github.com/ainova-systems/intelligence.git"
|
|
13
|
+
path: "packages/sync"
|
|
14
14
|
default_registry: "https://github.com/ainova-systems/intelligence-registry.git"
|
package/cli/intelligence
CHANGED
|
@@ -3,25 +3,20 @@
|
|
|
3
3
|
#
|
|
4
4
|
# Dispatcher only: each command lives in commands/<name>.sh so commands can be
|
|
5
5
|
# ported to Node one at a time without touching the others. The engine is the
|
|
6
|
-
#
|
|
7
|
-
# from the intelligence-sync repo itself, the repo's own module.
|
|
6
|
+
# bash engine shipped next to the CLI.
|
|
8
7
|
|
|
9
8
|
set -euo pipefail
|
|
10
9
|
|
|
11
10
|
CLI_DIR="$(cd "$(dirname "$0")" && pwd)"
|
|
12
11
|
export CLI_DIR
|
|
13
12
|
|
|
14
|
-
# Engine discovery, by role (
|
|
15
|
-
#
|
|
16
|
-
#
|
|
17
|
-
# 2. this repo: <repo>/cli/intelligence + <repo>/intelligence/sync/scripts/
|
|
13
|
+
# Engine discovery, by role (sync.sh + VERSION). The repo and the npm package
|
|
14
|
+
# ship one layout — <root>/{cli,engine,packages/sync} — so there is a single
|
|
15
|
+
# candidate rather than a per-distribution list.
|
|
18
16
|
IS_ENGINE_DIR=""
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
break
|
|
23
|
-
fi
|
|
24
|
-
done
|
|
17
|
+
if [ -f "$CLI_DIR/../engine/sync.sh" ] && [ -f "$CLI_DIR/../engine/VERSION" ]; then
|
|
18
|
+
IS_ENGINE_DIR="$(cd "$CLI_DIR/../engine" && pwd)"
|
|
19
|
+
fi
|
|
25
20
|
if [ -z "$IS_ENGINE_DIR" ]; then
|
|
26
21
|
echo "ERROR: bundled sync engine not found next to the CLI ($CLI_DIR)." >&2
|
|
27
22
|
echo " Reinstall: npm i -g @ainova-systems/intelligence" >&2
|
|
@@ -29,10 +24,19 @@ if [ -z "$IS_ENGINE_DIR" ]; then
|
|
|
29
24
|
fi
|
|
30
25
|
export IS_ENGINE_DIR
|
|
31
26
|
|
|
27
|
+
# The engine's own content, shipped beside the engine as the seed for the
|
|
28
|
+
# @ainova-systems/sync package at exactly this CLI's version. Separate from the
|
|
29
|
+
# engine dir: the store must receive rules/agents/skills, never engine scripts.
|
|
30
|
+
IS_BUNDLED_PKG_DIR=""
|
|
31
|
+
if [ -d "$CLI_DIR/../packages/sync" ]; then
|
|
32
|
+
IS_BUNDLED_PKG_DIR="$(cd "$CLI_DIR/../packages/sync" && pwd)"
|
|
33
|
+
fi
|
|
34
|
+
export IS_BUNDLED_PKG_DIR
|
|
35
|
+
|
|
32
36
|
cmd="${1:-}"
|
|
33
37
|
case "$cmd" in
|
|
34
38
|
""|help|-h|--help)
|
|
35
|
-
engine_ver="$(tr -d ' \t\r\n' < "$IS_ENGINE_DIR/
|
|
39
|
+
engine_ver="$(tr -d ' \t\r\n' < "$IS_ENGINE_DIR/VERSION")"
|
|
36
40
|
cat <<EOF
|
|
37
41
|
intelligence — build, version and distribute AI agent intelligence
|
|
38
42
|
(engine $engine_ver${INTELLIGENCE_NPM_VERSION:+, package $INTELLIGENCE_NPM_VERSION})
|
|
@@ -48,7 +52,7 @@ Usage: intelligence <command> [args]
|
|
|
48
52
|
list Installed packages with requested/locked versions
|
|
49
53
|
search [term] What the registries offer, and what this project has
|
|
50
54
|
sync [target] Render intelligence to every enabled tool (.claude, AGENTS.md, ...)
|
|
51
|
-
registry <list|add|remove>
|
|
55
|
+
registry <list|add|remove> Manage the trusted registry list (add <repo-url>)
|
|
52
56
|
migrate [--dry-run] Convert a vendored (v1) setup to the CLI setup
|
|
53
57
|
doctor Check manifest, lockfile, store and engine consistency
|
|
54
58
|
status Project state at a glance
|
|
@@ -57,13 +61,21 @@ EOF
|
|
|
57
61
|
exit 0
|
|
58
62
|
;;
|
|
59
63
|
version|-v|--version)
|
|
60
|
-
engine_ver="$(tr -d ' \t\r\n' < "$IS_ENGINE_DIR/
|
|
64
|
+
engine_ver="$(tr -d ' \t\r\n' < "$IS_ENGINE_DIR/VERSION")"
|
|
61
65
|
echo "${INTELLIGENCE_NPM_VERSION:-$engine_ver} (engine $engine_ver)"
|
|
62
66
|
exit 0
|
|
63
67
|
;;
|
|
64
68
|
esac
|
|
65
69
|
shift
|
|
66
70
|
|
|
71
|
+
# The command word builds a path — allow exactly lowercase words, so
|
|
72
|
+
# `intelligence ../x` can never exec a neighbouring script.
|
|
73
|
+
case "$cmd" in
|
|
74
|
+
*[!a-z-]*|-*)
|
|
75
|
+
echo "ERROR: unknown command '$cmd'. Run 'intelligence help'." >&2
|
|
76
|
+
exit 1
|
|
77
|
+
;;
|
|
78
|
+
esac
|
|
67
79
|
cmd_file="$CLI_DIR/commands/$cmd.sh"
|
|
68
80
|
if [ ! -f "$cmd_file" ]; then
|
|
69
81
|
echo "ERROR: unknown command '$cmd'. Run 'intelligence help'." >&2
|
package/cli/lib/cli-common.sh
CHANGED
|
@@ -7,18 +7,45 @@
|
|
|
7
7
|
# CLI-owned parsing lives in lib/manifest.sh (quoted-key `packages:` /
|
|
8
8
|
# `registries:` blocks, which the engine deliberately never reads).
|
|
9
9
|
|
|
10
|
-
# Engine libraries (readers, is_status, IS_RC_*, engine_version).
|
|
11
|
-
#
|
|
12
|
-
|
|
13
|
-
source "$IS_ENGINE_DIR/
|
|
14
|
-
source "$IS_ENGINE_DIR/scripts/lib/layout.sh"
|
|
15
|
-
source "$IS_ENGINE_DIR/scripts/lib/migrations.sh"
|
|
10
|
+
# Engine libraries (readers, is_status, IS_RC_*, engine_version). CLI_DIR /
|
|
11
|
+
# IS_ENGINE_DIR come exported from the dispatcher.
|
|
12
|
+
source "$IS_ENGINE_DIR/lib/common.sh"
|
|
13
|
+
source "$IS_ENGINE_DIR/lib/contract.sh"
|
|
16
14
|
|
|
17
15
|
die() {
|
|
18
16
|
echo "ERROR: $*" >&2
|
|
19
17
|
exit 1
|
|
20
18
|
}
|
|
21
19
|
|
|
20
|
+
# --- Untrusted-input guards ----------------------------------------------
|
|
21
|
+
# Manifest, lock and registry indexes arrive with a cloned repo: every value
|
|
22
|
+
# they carry is attacker-adjacent. URLs and refs reach git argv (a leading
|
|
23
|
+
# `-` would be an option — `--upload-pack=<cmd>` is code execution), names
|
|
24
|
+
# become store paths fed to rm -rf. Guard at the choke points, loudly.
|
|
25
|
+
|
|
26
|
+
# assert_safe_source_url <url> — scheme allowlist (the engine's own list) or
|
|
27
|
+
# scp-like user@host:path; never option-shaped, never quote-bearing.
|
|
28
|
+
assert_safe_source_url() {
|
|
29
|
+
local url="$1"
|
|
30
|
+
case "$url" in
|
|
31
|
+
""|-*) die "unsafe source url '$url' — option-shaped or empty" ;;
|
|
32
|
+
*[\"\'\ ]*) die "unsafe source url '$url' — quotes or spaces" ;;
|
|
33
|
+
https://*|http://*|ssh://*|git://*|file://*) ;;
|
|
34
|
+
*@*:*) ;;
|
|
35
|
+
*) die "unsafe source url '$url' — allowed: https, http, ssh, git, file, or user@host:path" ;;
|
|
36
|
+
esac
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
# assert_safe_ref <ref-or-empty> — tags/branches/SHAs; never option-shaped.
|
|
40
|
+
assert_safe_ref() {
|
|
41
|
+
local ref="$1"
|
|
42
|
+
[ -z "$ref" ] && return 0
|
|
43
|
+
case "$ref" in
|
|
44
|
+
-*) die "unsafe git ref '$ref' — option-shaped" ;;
|
|
45
|
+
*[\"\'\ \\]*) die "unsafe git ref '$ref'" ;;
|
|
46
|
+
esac
|
|
47
|
+
}
|
|
48
|
+
|
|
22
49
|
source "$CLI_DIR/lib/manifest.sh"
|
|
23
50
|
source "$CLI_DIR/lib/semver.sh"
|
|
24
51
|
source "$CLI_DIR/lib/registry.sh"
|
|
@@ -100,7 +127,7 @@ manifest_intelligence_dir() {
|
|
|
100
127
|
}
|
|
101
128
|
|
|
102
129
|
bundled_engine_version() {
|
|
103
|
-
tr -d ' \t\r\n' < "$IS_ENGINE_DIR/
|
|
130
|
+
tr -d ' \t\r\n' < "$IS_ENGINE_DIR/VERSION"
|
|
104
131
|
}
|
|
105
132
|
|
|
106
133
|
# --- The sync package's manifest/lock plumbing ---------------------------
|
package/cli/lib/lockfile.sh
CHANGED
|
@@ -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
|
@@ -114,9 +114,19 @@ _qmap_stage() {
|
|
|
114
114
|
|
|
115
115
|
# qmap_set <file> <block> <key> <field> <value> — upsert one field, creating
|
|
116
116
|
# the block and the key as needed.
|
|
117
|
+
# _yq_esc <string> — escape \ and " for a double-quoted YAML scalar.
|
|
118
|
+
_yq_esc() {
|
|
119
|
+
local s="$1"
|
|
120
|
+
s="${s//\\/\\\\}"
|
|
121
|
+
printf '%s' "${s//\"/\\\"}"
|
|
122
|
+
}
|
|
123
|
+
|
|
117
124
|
qmap_set() {
|
|
118
125
|
local file="$1" block="$2" key="$3" field="$4" value="$5"
|
|
119
126
|
[ -f "$file" ] || die "no such file: $file"
|
|
127
|
+
# url/path values can carry a `"`; escape before it reaches the quoted
|
|
128
|
+
# scalar the writer emits.
|
|
129
|
+
value="$(_yq_esc "$value")"
|
|
120
130
|
_qmap_stage "$file" -v block="$block" -v key="$key" -v field="$field" -v value="$value" '
|
|
121
131
|
function keyline() { return " \"" key "\":" }
|
|
122
132
|
function fieldline() { return " " field ": \"" value "\"" }
|
|
@@ -163,6 +173,7 @@ qmap_set() {
|
|
|
163
173
|
qmap_set_value() {
|
|
164
174
|
local file="$1" block="$2" key="$3" value="$4"
|
|
165
175
|
[ -f "$file" ] || die "no such file: $file"
|
|
176
|
+
value="$(_yq_esc "$value")"
|
|
166
177
|
_qmap_stage "$file" -v block="$block" -v key="$key" -v value="$value" '
|
|
167
178
|
function entry() { return " \"" key "\": \"" value "\"" }
|
|
168
179
|
{ sub(/\r$/, "") }
|
|
@@ -282,6 +293,14 @@ registries_remove() {
|
|
|
282
293
|
{ print }
|
|
283
294
|
'
|
|
284
295
|
}
|
|
296
|
+
# True (0) if <config> already lists <entry> under any sources section. Quoted
|
|
297
|
+
# and bare spellings both count - a manifest may hold either.
|
|
298
|
+
_mig_has_source() {
|
|
299
|
+
local config="$1" entry="$2"
|
|
300
|
+
[ -f "$config" ] || return 1
|
|
301
|
+
grep -Fq -- "\"$entry\"" "$config" || grep -Fq -- "- $entry" "$config"
|
|
302
|
+
}
|
|
303
|
+
|
|
285
304
|
|
|
286
305
|
# sources_add_entry_first <file> <section> <entry> — idempotent insert at the
|
|
287
306
|
# TOP of sources.<section> (creating sources:/section as needed). Package
|
|
@@ -313,7 +332,7 @@ sources_add_entry_first() {
|
|
|
313
332
|
}
|
|
314
333
|
|
|
315
334
|
# sources_remove_entry <file> <section> <entry> — remove `- "entry"` from
|
|
316
|
-
# sources.<section>.
|
|
335
|
+
# sources.<section>.
|
|
317
336
|
sources_remove_entry() {
|
|
318
337
|
local file="$1" section="$2" entry="$3"
|
|
319
338
|
[ -f "$file" ] || return 0
|
package/cli/lib/registry.sh
CHANGED
|
@@ -21,16 +21,26 @@
|
|
|
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
|
|
@@ -103,6 +113,8 @@ suggest_similar() {
|
|
|
103
113
|
# store holds the bytes the package published.
|
|
104
114
|
fetch_package() {
|
|
105
115
|
local url="$1" ref="$2" subpath="$3" dest="$4"
|
|
116
|
+
assert_safe_source_url "$url"
|
|
117
|
+
assert_safe_ref "$ref"
|
|
106
118
|
# Bundle seed: the engine-content package at the CLI's own version copies
|
|
107
119
|
# from the npm bundle — no network, which keeps init / fresh-clone install
|
|
108
120
|
# / migrate offline exactly like the staging they replace. Keyed on the
|
|
@@ -110,12 +122,14 @@ fetch_package() {
|
|
|
110
122
|
# to the normal clone.
|
|
111
123
|
if [ "$url" = "$SYNC_PKG_URL" ] && [ "$subpath" = "$SYNC_PKG_PATH" ] \
|
|
112
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"
|
|
113
127
|
rm -rf "$dest"
|
|
114
128
|
mkdir -p "$dest"
|
|
115
|
-
cp -R "$
|
|
129
|
+
cp -R "$IS_BUNDLED_PKG_DIR/." "$dest/"
|
|
116
130
|
rm -rf "$dest/.git"
|
|
117
|
-
if [ -f "$IS_ENGINE_DIR/
|
|
118
|
-
tr -d ' \t\r\n' < "$IS_ENGINE_DIR/
|
|
131
|
+
if [ -f "$IS_ENGINE_DIR/ENGINE_SHA" ]; then
|
|
132
|
+
tr -d ' \t\r\n' < "$IS_ENGINE_DIR/ENGINE_SHA"
|
|
119
133
|
fi
|
|
120
134
|
return 0
|
|
121
135
|
fi
|
|
@@ -124,20 +138,20 @@ fetch_package() {
|
|
|
124
138
|
[ -n "$ref" ] && branch_arg=(--branch "$ref")
|
|
125
139
|
tmp="$(mktemp -d -t intelligence-pkg-XXXXXX 2>/dev/null || mktemp -d)"
|
|
126
140
|
if ! GIT_TERMINAL_PROMPT=0 git -c core.symlinks=false -c core.autocrlf=false -c core.eol=lf \
|
|
127
|
-
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
|
|
128
142
|
# A SHA is not clonable with --branch — fall back to a full clone + checkout.
|
|
129
143
|
rm -rf "$tmp/clone"
|
|
130
144
|
GIT_TERMINAL_PROMPT=0 git -c core.symlinks=false -c core.autocrlf=false -c core.eol=lf \
|
|
131
|
-
clone --quiet "$url" "$tmp/clone" 2>/dev/null \
|
|
145
|
+
clone --quiet -- "$url" "$tmp/clone" 2>/dev/null \
|
|
132
146
|
|| { rm -rf "$tmp"; die "cannot clone $url"; }
|
|
133
|
-
[ -z "$ref" ] || git -C "$tmp/clone" checkout --quiet "$ref" \
|
|
147
|
+
[ -z "$ref" ] || git -C "$tmp/clone" checkout --quiet "$ref" -- \
|
|
134
148
|
|| { rm -rf "$tmp"; die "ref '$ref' not found in $url"; }
|
|
135
149
|
fi
|
|
136
150
|
sha="$(git -C "$tmp/clone" rev-parse HEAD)"
|
|
137
151
|
src="$tmp/clone"
|
|
138
152
|
if [ -n "$subpath" ]; then
|
|
139
153
|
case "$subpath" in
|
|
140
|
-
|
|
154
|
+
*..*|/*|*[\"\']*) rm -rf "$tmp"; die "unsafe path '$subpath'" ;;
|
|
141
155
|
esac
|
|
142
156
|
src="$tmp/clone/$subpath"
|
|
143
157
|
[ -d "$src" ] || { rm -rf "$tmp"; die "path '$subpath' not found in $url${ref:+@$ref}" ; }
|
package/cli/lib/semver.sh
CHANGED
|
@@ -75,10 +75,11 @@ semver_match() {
|
|
|
75
75
|
# onto their tag.
|
|
76
76
|
list_remote_versions() {
|
|
77
77
|
local url="$1"
|
|
78
|
+
assert_safe_source_url "$url"
|
|
78
79
|
# `|| true` inside the pipeline: an unreachable repo must read as "no
|
|
79
80
|
# versions", not kill a pipefail caller mid-command-substitution —
|
|
80
81
|
# reachability is the CALLER's question (add probes it explicitly).
|
|
81
|
-
{ GIT_TERMINAL_PROMPT=0 git ls-remote --tags "$url" 2>/dev/null || true; } \
|
|
82
|
+
{ GIT_TERMINAL_PROMPT=0 git ls-remote --tags -- "$url" 2>/dev/null || true; } \
|
|
82
83
|
| awk '{
|
|
83
84
|
sub(/\r$/, "")
|
|
84
85
|
ref = $2
|
|
@@ -95,7 +96,8 @@ list_remote_versions() {
|
|
|
95
96
|
# prints "<tag> <sha>". Peeled sha (the commit a tag object points at) wins.
|
|
96
97
|
remote_tag_for_version() {
|
|
97
98
|
local url="$1" ver="${2#v}"
|
|
98
|
-
|
|
99
|
+
assert_safe_source_url "$url"
|
|
100
|
+
{ GIT_TERMINAL_PROMPT=0 git ls-remote --tags -- "$url" 2>/dev/null || true; } \
|
|
99
101
|
| awk -v want="$ver" '
|
|
100
102
|
{
|
|
101
103
|
sub(/\r$/, "")
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
557cde6997dce612e0e8a5a11adc2472173dcec0
|