@ainova-systems/intelligence 0.17.1 → 0.17.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/cli/commands/adapter.sh +4 -0
- package/cli/commands/sync.sh +3 -0
- package/cli/commands/update.sh +3 -0
- package/cli/intelligence +10 -4
- package/cli/internal/align-project.sh +1 -0
- package/cli/internal/check.sh +14 -1
- package/cli/internal/convert-legacy.sh +52 -1
- package/cli/internal/package-add.sh +6 -0
- package/cli/internal/package-remove.sh +2 -0
- package/cli/internal/package-update.sh +3 -0
- package/cli/internal/restore.sh +11 -5
- package/cli/internal/target-state.sh +1 -0
- package/cli/lib/cli-common.sh +128 -23
- package/cli/lib/lockfile.sh +2 -1
- package/cli/lib/qmap.awk +16 -1
- package/cli/lib/sync-cache.sh +20 -7
- package/cli/lib/sync-lock.sh +148 -0
- package/engine/ENGINE_SHA +1 -1
- package/engine/VERSION +1 -1
- package/engine/adapters/claude.sh +3 -5
- package/engine/adapters/cursor.sh +3 -5
- package/engine/lib/adapter-contract.sh +1 -5
- package/engine/lib/common.sh +228 -59
- package/engine/lib/contract.sh +57 -17
- package/engine/sync.sh +315 -29
- package/package.json +1 -1
- package/packages/sync/references/adapters.md +7 -0
- package/packages/sync/references/conventions.md +2 -2
package/cli/commands/adapter.sh
CHANGED
|
@@ -118,6 +118,8 @@ adapter_remove() {
|
|
|
118
118
|
read -r answer
|
|
119
119
|
case "$answer" in y|Y|yes|YES) ;; *) echo "removal cancelled"; exit 0 ;; esac
|
|
120
120
|
fi
|
|
121
|
+
# A render sources project adapters: never remove one under it.
|
|
122
|
+
project_lock_hold "$IP_ROOT"
|
|
121
123
|
rm -f "$file"
|
|
122
124
|
echo "removed: $content_dir/adapters/$name.sh"
|
|
123
125
|
echo " generated output was kept"
|
|
@@ -136,6 +138,8 @@ case "$action" in
|
|
|
136
138
|
[ -n "$name" ] && [ $# -eq 2 ] || die "usage: intelligence adapter <enable|disable> <name>"
|
|
137
139
|
assert_valid_target_name "$name"
|
|
138
140
|
require_cli_project
|
|
141
|
+
# Held through the final render: target-state and sync join it.
|
|
142
|
+
project_lock_hold "$IP_ROOT"
|
|
139
143
|
ensure_project_current "$IP_ROOT"
|
|
140
144
|
if [ "$action" = "enable" ]; then
|
|
141
145
|
state_stage="$(mktemp -d -t intelligence-adapter-state-XXXXXX)"
|
package/cli/commands/sync.sh
CHANGED
|
@@ -29,6 +29,9 @@ run_sync() {
|
|
|
29
29
|
detect_project || return $?
|
|
30
30
|
case "$IP_MODE" in
|
|
31
31
|
cli)
|
|
32
|
+
# One writer per project at a time: the store restore and the
|
|
33
|
+
# render below both rewrite shared state (sync-lock.sh).
|
|
34
|
+
project_lock_hold "$IP_ROOT"
|
|
32
35
|
# `sync` is the normal fresh-clone command. A newer globally installed
|
|
33
36
|
# CLI aligns the current Intelligence project first (except in CI, where a
|
|
34
37
|
# tracked migration must be reviewed and committed locally), and a
|
package/cli/commands/update.sh
CHANGED
|
@@ -105,6 +105,9 @@ if [ "$mode" = "ask" ]; then
|
|
|
105
105
|
esac
|
|
106
106
|
fi
|
|
107
107
|
|
|
108
|
+
# Held from alignment through the final render: the package pass and the sync
|
|
109
|
+
# it execs join this lock instead of taking their own.
|
|
110
|
+
project_lock_hold "$IP_ROOT"
|
|
108
111
|
ensure_project_current "$IP_ROOT"
|
|
109
112
|
apply_args=(--no-sync)
|
|
110
113
|
[ -z "$only" ] || apply_args+=("$only")
|
package/cli/intelligence
CHANGED
|
@@ -7,15 +7,21 @@
|
|
|
7
7
|
|
|
8
8
|
set -euo pipefail
|
|
9
9
|
|
|
10
|
-
|
|
10
|
+
case "$0" in
|
|
11
|
+
*/*) CLI_DIR="${0%/*}"; [ -n "$CLI_DIR" ] || CLI_DIR="/" ;;
|
|
12
|
+
*) CLI_DIR="." ;;
|
|
13
|
+
esac
|
|
14
|
+
CLI_DIR="$(cd "$CLI_DIR" && pwd)"
|
|
11
15
|
export CLI_DIR
|
|
12
16
|
|
|
13
17
|
# Engine discovery, by role (sync.sh + VERSION). The repo and the npm package
|
|
14
18
|
# ship one layout — <root>/{cli,engine,packages/sync} — so there is a single
|
|
15
|
-
# candidate rather than a per-distribution list.
|
|
19
|
+
# candidate rather than a per-distribution list. CLI_DIR is already the logical
|
|
20
|
+
# `pwd` spelling, so its textual parent is what `cd "$CLI_DIR/.."` would print.
|
|
21
|
+
CLI_ROOT="${CLI_DIR%/*}"
|
|
16
22
|
IS_ENGINE_DIR=""
|
|
17
23
|
if [ -f "$CLI_DIR/../engine/sync.sh" ] && [ -f "$CLI_DIR/../engine/VERSION" ]; then
|
|
18
|
-
IS_ENGINE_DIR="$
|
|
24
|
+
IS_ENGINE_DIR="$CLI_ROOT/engine"
|
|
19
25
|
fi
|
|
20
26
|
if [ -z "$IS_ENGINE_DIR" ]; then
|
|
21
27
|
echo "ERROR: bundled sync engine not found next to the CLI ($CLI_DIR)." >&2
|
|
@@ -29,7 +35,7 @@ export IS_ENGINE_DIR
|
|
|
29
35
|
# engine dir: the store must receive rules/agents/skills, never engine scripts.
|
|
30
36
|
IS_BUNDLED_PKG_DIR=""
|
|
31
37
|
if [ -d "$CLI_DIR/../packages/sync" ]; then
|
|
32
|
-
IS_BUNDLED_PKG_DIR="$
|
|
38
|
+
IS_BUNDLED_PKG_DIR="$CLI_ROOT/packages/sync"
|
|
33
39
|
fi
|
|
34
40
|
export IS_BUNDLED_PKG_DIR
|
|
35
41
|
|
package/cli/internal/check.sh
CHANGED
|
@@ -102,7 +102,20 @@ while IFS= read -r name; do
|
|
|
102
102
|
warn "$name is locked but not installed — run 'intelligence sync'"
|
|
103
103
|
continue
|
|
104
104
|
fi
|
|
105
|
-
|
|
105
|
+
# Report what the store holds, never what the lock pins: they differ
|
|
106
|
+
# after a pull that moved the lock and left the ignored store behind.
|
|
107
|
+
l_url="$(qmap_field "$lock" "packages" "$name" "url")"
|
|
108
|
+
l_path="$(qmap_field "$lock" "packages" "$name" "path")"
|
|
109
|
+
l_sha="$(qmap_field "$lock" "packages" "$name" "sha")"
|
|
110
|
+
if ! store_record_get "$IP_ROOT" "$name"; then
|
|
111
|
+
warn "$name is installed, but the store does not record which commit it holds; the lock pins $(pin_label "$m_ref" "$l_res" "$l_sha") — run 'intelligence sync'"
|
|
112
|
+
elif [ "$IS_REC_URL" != "$l_url" ] || [ "$IS_REC_PATH" != "$l_path" ]; then
|
|
113
|
+
warn "$name was installed from ${IS_REC_URL}${IS_REC_PATH:+ (path $IS_REC_PATH)} but the lock names ${l_url}${l_path:+ (path $l_path)} — run 'intelligence sync'"
|
|
114
|
+
elif [ "$IS_REC_RESOLVED" != "$l_res" ] || [ "$IS_REC_SHA" != "$l_sha" ]; then
|
|
115
|
+
warn "$name holds $IS_REC_RESOLVED@$(short_sha "$IS_REC_SHA") but the lock pins $l_res@$(short_sha "$l_sha") — run 'intelligence sync'"
|
|
116
|
+
else
|
|
117
|
+
ok "$name @ $(pin_label "$m_ref" "$IS_REC_RESOLVED" "$IS_REC_SHA")"
|
|
118
|
+
fi
|
|
106
119
|
fi
|
|
107
120
|
done < <(qmap_keys "$manifest" "packages")
|
|
108
121
|
|
|
@@ -227,7 +227,38 @@ rewrite_source_value() {
|
|
|
227
227
|
esac
|
|
228
228
|
printf '%s' "$v"
|
|
229
229
|
}
|
|
230
|
-
|
|
230
|
+
# The project's own content stays where the legacy layout kept it. When that
|
|
231
|
+
# directory is not `intelligence/`, the manifest names it: the content directory
|
|
232
|
+
# is where project adapters live, what no output may overwrite, and the source of
|
|
233
|
+
# truth AGENTS.md points readers at. Left at the default, all three name a
|
|
234
|
+
# directory that only a case-insensitive filesystem confuses with the real one.
|
|
235
|
+
legacy_content_dir=""
|
|
236
|
+
case "$umbrella_rel" in
|
|
237
|
+
"$umbrella"|intelligence|*/*|.intelligence*|*[\"\'\\]*|*[[:cntrl:]]*) ;;
|
|
238
|
+
*)
|
|
239
|
+
# Only content that stays counts: the vendored module and pack mirrors
|
|
240
|
+
# move into the store and leave, so a config-only umbrella is not one.
|
|
241
|
+
if [ -z "$(get_yaml_field "$config" "project" "intelligence_dir")" ]; then
|
|
242
|
+
for section in rules agents skills; do
|
|
243
|
+
while IFS= read -r src; do
|
|
244
|
+
case "$src" in ""|git+*|@*) continue ;; esac
|
|
245
|
+
src_canon="$(normalize_path "$root/$src")"
|
|
246
|
+
case "$src_canon" in "$root"/*) src_rel="${src_canon#"$root"/}" ;; *) continue ;; esac
|
|
247
|
+
case "$src_rel" in "$module_rel"|"$module_rel"/*) continue ;; esac
|
|
248
|
+
leaving=0
|
|
249
|
+
while IFS="$LOCK_SEP" read -r _ _ _ _ mirror; do
|
|
250
|
+
[ -n "$mirror" ] || continue
|
|
251
|
+
mirror="$(normalize_path "$root/$mirror")"
|
|
252
|
+
case "$src_canon" in "$mirror"|"$mirror"/*) leaving=1 ;; esac
|
|
253
|
+
done < "$pack_rows"
|
|
254
|
+
[ "$leaving" -eq 0 ] || continue
|
|
255
|
+
case "$src_rel" in "$umbrella_rel"|"$umbrella_rel"/*) legacy_content_dir="$umbrella_rel" ;; esac
|
|
256
|
+
done < <(read_yaml_list "$config" "$section")
|
|
257
|
+
done
|
|
258
|
+
fi
|
|
259
|
+
;;
|
|
260
|
+
esac
|
|
261
|
+
in_packs=0 in_sources=0 project_key=0 content_dir_written=0
|
|
231
262
|
while IFS= read -r line || [ -n "$line" ]; do
|
|
232
263
|
line="${line%$'\r'}"
|
|
233
264
|
case "$line" in
|
|
@@ -236,6 +267,18 @@ while IFS= read -r line || [ -n "$line" ]; do
|
|
|
236
267
|
sources:*) in_sources=1; in_packs=0; echo "$line"; continue ;;
|
|
237
268
|
[!\ \#]*) in_packs=0; in_sources=0 ;;
|
|
238
269
|
esac
|
|
270
|
+
case "$line" in project:*) project_key=1 ;; esac
|
|
271
|
+
if [ -n "$legacy_content_dir" ] && [ "$content_dir_written" -eq 0 ]; then
|
|
272
|
+
# Only the block form takes a child line; `project: {...}` does not.
|
|
273
|
+
key_part="${line%%#*}"
|
|
274
|
+
key_part="${key_part%"${key_part##*[![:space:]]}"}"
|
|
275
|
+
if [ "$key_part" = "project:" ]; then
|
|
276
|
+
echo "$line"
|
|
277
|
+
echo " intelligence_dir: \"$legacy_content_dir\""
|
|
278
|
+
content_dir_written=1
|
|
279
|
+
continue
|
|
280
|
+
fi
|
|
281
|
+
fi
|
|
239
282
|
[ "$in_packs" -eq 1 ] && continue
|
|
240
283
|
if [ "$in_sources" -eq 1 ]; then
|
|
241
284
|
case "$line" in
|
|
@@ -261,6 +304,9 @@ while IFS= read -r line || [ -n "$line" ]; do
|
|
|
261
304
|
fi
|
|
262
305
|
echo "$line"
|
|
263
306
|
done < "$config" > "$manifest_stage"
|
|
307
|
+
if [ -n "$legacy_content_dir" ] && [ "$content_dir_written" -eq 0 ] && [ "$project_key" -eq 0 ]; then
|
|
308
|
+
printf 'project:\n intelligence_dir: "%s"\n' "$legacy_content_dir" >> "$manifest_stage"
|
|
309
|
+
fi
|
|
264
310
|
stamp_schema_version "$manifest_stage" "$eng"
|
|
265
311
|
|
|
266
312
|
# packages: requested pins only; resolved source details live in the lock.
|
|
@@ -360,6 +406,11 @@ trap 'rollback; exit 130' INT TERM
|
|
|
360
406
|
mv "$stage/.intelligence" "$root/.intelligence"
|
|
361
407
|
cp "$manifest_stage" "$root/intelligence.yaml"
|
|
362
408
|
lock_write_from_tsv "$root/intelligence.lock" "$lock_rows"
|
|
409
|
+
# The staged store holds exactly these rows; record them so sync trusts it.
|
|
410
|
+
while IFS="$LOCK_SEP" read -r rec_name _ rec_url rec_path rec_resolved rec_sha; do
|
|
411
|
+
[ -n "$rec_name" ] || continue
|
|
412
|
+
store_record_set "$root" "$rec_name" "$rec_url" "$rec_path" "$rec_resolved" "$rec_sha"
|
|
413
|
+
done < "$lock_rows"
|
|
363
414
|
ensure_manifest_gitignore "$root" "$root/intelligence.yaml"
|
|
364
415
|
ensure_manifest_publisher_ignores "$root" "$root/intelligence.yaml"
|
|
365
416
|
|
|
@@ -24,6 +24,7 @@ while [ $# -gt 0 ]; do
|
|
|
24
24
|
done
|
|
25
25
|
|
|
26
26
|
require_cli_project
|
|
27
|
+
project_lock_hold "$IP_ROOT"
|
|
27
28
|
manifest="$IP_ROOT/intelligence.yaml"
|
|
28
29
|
lock="$IP_ROOT/intelligence.lock"
|
|
29
30
|
|
|
@@ -122,6 +123,10 @@ if [ -z "$ref" ]; then
|
|
|
122
123
|
fi
|
|
123
124
|
|
|
124
125
|
rel=".intelligence/packages/$name"
|
|
126
|
+
# Forget what the directory held before replacing it: an interruption then
|
|
127
|
+
# leaves the package unrecorded, which the next sync repairs, never a stale
|
|
128
|
+
# record vouching for new bytes.
|
|
129
|
+
store_record_remove "$IP_ROOT" "$name"
|
|
125
130
|
sha="$(fetch_package "$url" "${ref:-$resolved_tag}" "$path" "$IP_ROOT/$rel")"
|
|
126
131
|
|
|
127
132
|
wire_package_sources "$manifest" "$name" "$rel" "$IP_ROOT"
|
|
@@ -139,6 +144,7 @@ else
|
|
|
139
144
|
fi
|
|
140
145
|
|
|
141
146
|
lock_upsert "$lock" "$name" "$requested" "$url" "$path" "${ref:-$resolved_tag}" "$sha"
|
|
147
|
+
store_record_set "$IP_ROOT" "$name" "$url" "$path" "${ref:-$resolved_tag}" "$sha"
|
|
142
148
|
|
|
143
149
|
sections=""
|
|
144
150
|
for s in rules agents skills; do
|
|
@@ -16,6 +16,7 @@ for a in "$@"; do
|
|
|
16
16
|
done
|
|
17
17
|
|
|
18
18
|
require_cli_project
|
|
19
|
+
project_lock_hold "$IP_ROOT"
|
|
19
20
|
|
|
20
21
|
# Removing the engine-content package guts the outputs (meta-skills, the
|
|
21
22
|
# authoring rule, both engine agents disappear) while everything still
|
|
@@ -42,6 +43,7 @@ unwire_package_sources "$manifest" "$rel"
|
|
|
42
43
|
qmap_delete_key "$manifest" "packages" "$name"
|
|
43
44
|
lock_remove "$lock" "$name"
|
|
44
45
|
rm -rf "${IP_ROOT:?}/$rel"
|
|
46
|
+
store_record_remove "$IP_ROOT" "$name"
|
|
45
47
|
# Drop the now-empty scope dir so the store stays tidy.
|
|
46
48
|
rmdir "$IP_ROOT/.intelligence/packages/${name%%/*}" 2>/dev/null || true
|
|
47
49
|
echo "- $name"
|
|
@@ -25,6 +25,7 @@ done
|
|
|
25
25
|
|| die "--latest needs the package to move: intelligence update @scope/name --latest"
|
|
26
26
|
|
|
27
27
|
require_cli_project
|
|
28
|
+
[ "$preview" -eq 1 ] || project_lock_hold "$IP_ROOT"
|
|
28
29
|
manifest="$IP_ROOT/intelligence.yaml"
|
|
29
30
|
lock="$IP_ROOT/intelligence.lock"
|
|
30
31
|
|
|
@@ -173,11 +174,13 @@ while IFS= read -r name; do
|
|
|
173
174
|
rm -rf "$staging"
|
|
174
175
|
sha="$(fetch_package "$url" "$tag" "$path" "$staging")"
|
|
175
176
|
unwire_package_sources "$manifest" "$rel"
|
|
177
|
+
store_record_remove "$IP_ROOT" "$name"
|
|
176
178
|
rm -rf "${IP_ROOT:?}/$rel"
|
|
177
179
|
mkdir -p "$(dirname "$IP_ROOT/$rel")"
|
|
178
180
|
mv "$staging" "$IP_ROOT/$rel"
|
|
179
181
|
wire_package_sources "$manifest" "$name" "$rel" "$IP_ROOT"
|
|
180
182
|
lock_upsert "$lock" "$name" "$requested" "$url" "$path" "$tag" "$sha"
|
|
183
|
+
store_record_set "$IP_ROOT" "$name" "$url" "$path" "$tag" "$sha"
|
|
181
184
|
# The widened intent is recorded only once its content is installed and
|
|
182
185
|
# wired: a manifest saying ^0.6.1 over a failed fetch would describe a
|
|
183
186
|
# state the project never reached.
|
package/cli/internal/restore.sh
CHANGED
|
@@ -19,6 +19,7 @@ while [ $# -gt 0 ]; do
|
|
|
19
19
|
done
|
|
20
20
|
|
|
21
21
|
require_cli_project
|
|
22
|
+
project_lock_hold "$IP_ROOT"
|
|
22
23
|
manifest="$IP_ROOT/intelligence.yaml"
|
|
23
24
|
lock="$IP_ROOT/intelligence.lock"
|
|
24
25
|
validate_project_lock "$IP_ROOT" --restore
|
|
@@ -81,14 +82,16 @@ if [ -n "$missing" ]; then
|
|
|
81
82
|
read -r resolved_tag _ <<< "$(remote_tag_for_version "$url" "$picked")"
|
|
82
83
|
fi
|
|
83
84
|
rel=".intelligence/packages/$name"
|
|
85
|
+
store_record_remove "$IP_ROOT" "$name"
|
|
84
86
|
sha="$(fetch_package "$url" "${ref:-$resolved_tag}" "$path" "$IP_ROOT/$rel")"
|
|
85
87
|
wire_package_sources "$manifest" "$name" "$rel" "$IP_ROOT"
|
|
86
88
|
lock_upsert "$lock" "$name" "$range" "$url" "$path" "${ref:-$resolved_tag}" "$sha"
|
|
89
|
+
store_record_set "$IP_ROOT" "$name" "$url" "$path" "${ref:-$resolved_tag}" "$sha"
|
|
87
90
|
echo "+ $name@${resolved_tag:-${ref:-HEAD}} (resolved)"
|
|
88
91
|
done
|
|
89
92
|
fi
|
|
90
93
|
|
|
91
|
-
# Restore every locked package
|
|
94
|
+
# Restore every locked package the store does not hold at its locked commit.
|
|
92
95
|
if [ -f "$lock" ]; then
|
|
93
96
|
while IFS="$LOCK_SEP" read -r name _requested url path resolved sha; do
|
|
94
97
|
[ -n "$name" ] || continue
|
|
@@ -96,10 +99,11 @@ if [ -f "$lock" ]; then
|
|
|
96
99
|
# on their way into rm -rf paths, and its urls into git argv.
|
|
97
100
|
assert_valid_pkg_name "$name"
|
|
98
101
|
rel=".intelligence/packages/$name"
|
|
99
|
-
#
|
|
100
|
-
#
|
|
101
|
-
#
|
|
102
|
-
|
|
102
|
+
# A directory counts only with a record of the lock row it satisfies:
|
|
103
|
+
# one present without it may hold any commit. --frozen never trusts
|
|
104
|
+
# what is already on disk: a pre-existing (possibly substituted) store
|
|
105
|
+
# dir would otherwise be synced with no sha verification at all.
|
|
106
|
+
if [ -d "$IP_ROOT/$rel" ] && [ "$force" -eq 0 ] && [ "$frozen" -eq 0 ] && store_record_matches "$IP_ROOT" "$name" "$url" "$path" "$resolved" "$sha"; then
|
|
103
107
|
continue
|
|
104
108
|
fi
|
|
105
109
|
# Fetch into staging and commit to the store only after the integrity
|
|
@@ -108,9 +112,11 @@ if [ -f "$lock" ]; then
|
|
|
108
112
|
staging="$IP_ROOT/.intelligence/.staging-$$"
|
|
109
113
|
rm -rf "$staging"
|
|
110
114
|
fetch_package "$url" "$resolved" "$path" "$staging" "$sha" >/dev/null
|
|
115
|
+
store_record_remove "$IP_ROOT" "$name"
|
|
111
116
|
rm -rf "${IP_ROOT:?}/$rel"
|
|
112
117
|
mkdir -p "$(dirname "$IP_ROOT/$rel")"
|
|
113
118
|
mv "$staging" "$IP_ROOT/$rel"
|
|
119
|
+
store_record_set "$IP_ROOT" "$name" "$url" "$path" "$resolved" "$sha"
|
|
114
120
|
wire_package_sources "$manifest" "$name" "$rel" "$IP_ROOT"
|
|
115
121
|
echo "= $name@${resolved:-HEAD}"
|
|
116
122
|
done < <(lock_to_tsv "$lock")
|
|
@@ -15,6 +15,7 @@ case "$action" in enable|disable) ;; *) die "internal target state: expected <en
|
|
|
15
15
|
assert_valid_target_name "$name"
|
|
16
16
|
|
|
17
17
|
require_cli_project
|
|
18
|
+
project_lock_hold "$IP_ROOT"
|
|
18
19
|
manifest="$IP_ROOT/intelligence.yaml"
|
|
19
20
|
content_dir="$(manifest_intelligence_dir "$manifest")"
|
|
20
21
|
|
package/cli/lib/cli-common.sh
CHANGED
|
@@ -87,6 +87,7 @@ source "$CLI_DIR/lib/adapter-lifecycle.sh"
|
|
|
87
87
|
source "$CLI_DIR/lib/gitignore.sh"
|
|
88
88
|
source "$CLI_DIR/lib/onboarding.sh"
|
|
89
89
|
source "$CLI_DIR/lib/sync-cache.sh"
|
|
90
|
+
source "$CLI_DIR/lib/sync-lock.sh"
|
|
90
91
|
|
|
91
92
|
# The engine-content package: OPTIONAL but auto-selected at init. Package by
|
|
92
93
|
# UX (manifest entry, lockfile row, list/search/remove), bundle by mechanics —
|
|
@@ -97,12 +98,21 @@ source "$CLI_DIR/lib/sync-cache.sh"
|
|
|
97
98
|
# Its identity is DATA shipped with the distribution (cli/engine-package.yaml),
|
|
98
99
|
# never a name compiled into cli code — a fork edits the file.
|
|
99
100
|
_EPKG="$CLI_DIR/engine-package.yaml"
|
|
100
|
-
SYNC_PKG_NAME="
|
|
101
|
-
SYNC_PKG_URL="$(top_scalar "$_EPKG" "url")"
|
|
102
|
-
SYNC_PKG_PATH="$(top_scalar "$_EPKG" "path")"
|
|
101
|
+
SYNC_PKG_NAME="" SYNC_PKG_URL="" SYNC_PKG_PATH=""
|
|
103
102
|
# Read by commands (init seeds it) — per-file shellcheck cannot see that.
|
|
104
103
|
# shellcheck disable=SC2034
|
|
105
|
-
DEFAULT_REGISTRY_URL="
|
|
104
|
+
DEFAULT_REGISTRY_URL=""
|
|
105
|
+
# Every command loads this file first: read the four fields in one pass.
|
|
106
|
+
# shellcheck disable=SC2034 # DEFAULT_REGISTRY_URL is read by commands, as above
|
|
107
|
+
while IFS="$LOCK_SEP" read -r _epkg_key _epkg_value; do
|
|
108
|
+
case "$_epkg_key" in
|
|
109
|
+
name) SYNC_PKG_NAME="$_epkg_value" ;;
|
|
110
|
+
url) SYNC_PKG_URL="$_epkg_value" ;;
|
|
111
|
+
path) SYNC_PKG_PATH="$_epkg_value" ;;
|
|
112
|
+
default_registry) DEFAULT_REGISTRY_URL="$_epkg_value" ;;
|
|
113
|
+
esac
|
|
114
|
+
done <<< "$(_qmap_read tops "$_EPKG" '' 'name url path default_registry')"
|
|
115
|
+
unset _epkg_key _epkg_value
|
|
106
116
|
[ -n "$SYNC_PKG_NAME" ] || die "corrupt CLI installation: $_EPKG is missing or has no 'name'"
|
|
107
117
|
SYNC_PKG_STORE=".intelligence/packages/$SYNC_PKG_NAME"
|
|
108
118
|
|
|
@@ -116,15 +126,18 @@ SYNC_PKG_STORE=".intelligence/packages/$SYNC_PKG_NAME"
|
|
|
116
126
|
# by role — scripts/sync.sh + scripts/VERSION — never by name.
|
|
117
127
|
detect_project() {
|
|
118
128
|
IP_MODE="none"; IP_ROOT=""; IP_UMBRELLA=""; IP_MODULE_DIR=""
|
|
119
|
-
|
|
129
|
+
# PWD is already the logical `pwd` spelling, and so is each textual parent.
|
|
130
|
+
local dir="$PWD" parent
|
|
120
131
|
while :; do
|
|
121
132
|
if [ -f "$dir/intelligence.yaml" ]; then
|
|
122
133
|
IP_MODE="cli"
|
|
123
|
-
IP_ROOT="$
|
|
134
|
+
IP_ROOT="$dir"
|
|
124
135
|
return 0
|
|
125
136
|
fi
|
|
126
|
-
|
|
127
|
-
|
|
137
|
+
parent="${dir%/*}"
|
|
138
|
+
[ -n "$parent" ] || parent="/"
|
|
139
|
+
[ "$dir" = "$parent" ] && break
|
|
140
|
+
dir="$parent"
|
|
128
141
|
done
|
|
129
142
|
local root
|
|
130
143
|
root="$(git rev-parse --show-toplevel 2>/dev/null || true)"
|
|
@@ -257,10 +270,23 @@ default_target_output() {
|
|
|
257
270
|
esac
|
|
258
271
|
}
|
|
259
272
|
|
|
273
|
+
# bundled_engine_version_var sets IS_BUNDLED_ENGINE_VERSION, reading VERSION
|
|
274
|
+
# once per process; lifecycle preflight compares against it several times.
|
|
275
|
+
bundled_engine_version_var() {
|
|
276
|
+
[ -z "${IS_BUNDLED_ENGINE_VERSION_READ:-}" ] || return 0
|
|
277
|
+
local version=""
|
|
278
|
+
[ -r "$IS_ENGINE_DIR/VERSION" ] || {
|
|
279
|
+
echo "ERROR: cannot read $IS_ENGINE_DIR/VERSION" >&2
|
|
280
|
+
return 1
|
|
281
|
+
}
|
|
282
|
+
IFS= read -r -d '' version < "$IS_ENGINE_DIR/VERSION" || true
|
|
283
|
+
IS_BUNDLED_ENGINE_VERSION="${version//[$' \t\r\n']/}"
|
|
284
|
+
IS_BUNDLED_ENGINE_VERSION_READ=1
|
|
285
|
+
}
|
|
286
|
+
|
|
260
287
|
bundled_engine_version() {
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
printf '%s' "${version//[$' \t\r\n']/}"
|
|
288
|
+
bundled_engine_version_var
|
|
289
|
+
printf '%s' "$IS_BUNDLED_ENGINE_VERSION"
|
|
264
290
|
}
|
|
265
291
|
|
|
266
292
|
# --- The sync package's manifest/lock plumbing ---------------------------
|
|
@@ -277,9 +303,11 @@ sync_pkg_entry() {
|
|
|
277
303
|
sync_pkg_install() {
|
|
278
304
|
local root="$1" ver sha
|
|
279
305
|
ver="$(bundled_engine_version)"
|
|
306
|
+
store_record_remove "$root" "$SYNC_PKG_NAME"
|
|
280
307
|
sha="$(fetch_package "$SYNC_PKG_URL" "v$ver" "$SYNC_PKG_PATH" "$root/$SYNC_PKG_STORE")"
|
|
281
308
|
wire_package_sources "$root/intelligence.yaml" "$SYNC_PKG_NAME" "$SYNC_PKG_STORE" "$root"
|
|
282
309
|
lock_upsert "$root/intelligence.lock" "$SYNC_PKG_NAME" "$ver" "$SYNC_PKG_URL" "$SYNC_PKG_PATH" "v$ver" "$sha"
|
|
310
|
+
store_record_set "$root" "$SYNC_PKG_NAME" "$SYNC_PKG_URL" "$SYNC_PKG_PATH" "v$ver" "$sha"
|
|
283
311
|
echo " engine content installed: $SYNC_PKG_STORE (v$ver)"
|
|
284
312
|
}
|
|
285
313
|
|
|
@@ -345,15 +373,19 @@ assert_safe_content_dir() {
|
|
|
345
373
|
# the admitted minor/patch gap; the predicate itself does not re-judge the gap.
|
|
346
374
|
project_stamped_ahead() {
|
|
347
375
|
local stamp
|
|
348
|
-
|
|
349
|
-
|
|
376
|
+
read_schema_version_var "$1/intelligence.yaml"
|
|
377
|
+
stamp="$IS_SCHEMA_VERSION"
|
|
378
|
+
bundled_engine_version_var
|
|
379
|
+
[ -n "$stamp" ] && _ver_gt "$stamp" "$IS_BUNDLED_ENGINE_VERSION"
|
|
350
380
|
}
|
|
351
381
|
|
|
352
382
|
project_needs_upgrade() {
|
|
353
|
-
local root="$1" manifest="$1/intelligence.yaml" stamp eng pinned locked name
|
|
383
|
+
local root="$1" manifest="$1/intelligence.yaml" stamp eng pinned locked name url path rows
|
|
354
384
|
[ -f "$manifest" ] || return 1
|
|
355
|
-
|
|
356
|
-
|
|
385
|
+
read_schema_version_var "$manifest"
|
|
386
|
+
stamp="$IS_SCHEMA_VERSION"
|
|
387
|
+
bundled_engine_version_var
|
|
388
|
+
eng="$IS_BUNDLED_ENGINE_VERSION"
|
|
357
389
|
[ -z "$stamp" ] && return 0
|
|
358
390
|
# A project stamped ahead belongs to a newer CLI. Aligning it here would
|
|
359
391
|
# restamp the schema and re-pin the engine content DOWNWARD, and the next
|
|
@@ -363,14 +395,15 @@ project_needs_upgrade() {
|
|
|
363
395
|
_ver_gt "$eng" "$stamp" && return 0
|
|
364
396
|
[ -d "$root/.intelligence/engine" ] && return 0
|
|
365
397
|
|
|
366
|
-
|
|
398
|
+
# One manifest pass for every package's fields, not a reader per field.
|
|
399
|
+
rows="$(_qmap_read fieldrows "$manifest" packages '' 'url path version')"
|
|
400
|
+
while IFS="$LOCK_SEP" read -r name url path pinned; do
|
|
367
401
|
[ -n "$name" ] || continue
|
|
368
402
|
# Early RC manifests mixed requested intent with resolved source
|
|
369
403
|
# details. Source URL/path now live only in the required lockfile.
|
|
370
|
-
[ -n "$
|
|
371
|
-
[ -n "$
|
|
404
|
+
[ -n "$url" ] && return 0
|
|
405
|
+
[ -n "$path" ] && return 0
|
|
372
406
|
if [ "$name" = "$SYNC_PKG_NAME" ]; then
|
|
373
|
-
pinned="$(qmap_field "$manifest" "packages" "$name" "version")"
|
|
374
407
|
locked="$(qmap_field "$root/intelligence.lock" "packages" "$name" "resolved")"
|
|
375
408
|
[ "$pinned" = "$eng" ] || return 0
|
|
376
409
|
if [ -f "$root/intelligence.lock" ]; then
|
|
@@ -378,7 +411,7 @@ project_needs_upgrade() {
|
|
|
378
411
|
[ "${locked#v}" = "$eng" ] || return 0
|
|
379
412
|
fi
|
|
380
413
|
fi
|
|
381
|
-
done
|
|
414
|
+
done <<< "$rows"
|
|
382
415
|
return 1
|
|
383
416
|
}
|
|
384
417
|
|
|
@@ -402,8 +435,10 @@ ensure_project_current() {
|
|
|
402
435
|
die "manifest declares packages but intelligence.lock is absent — restore the committed lock before running project lifecycle commands"
|
|
403
436
|
fi
|
|
404
437
|
project_needs_upgrade "$root" || return 0
|
|
405
|
-
|
|
406
|
-
|
|
438
|
+
read_schema_version_var "$manifest"
|
|
439
|
+
stamp="$IS_SCHEMA_VERSION"
|
|
440
|
+
bundled_engine_version_var
|
|
441
|
+
eng="$IS_BUNDLED_ENGINE_VERSION"
|
|
407
442
|
if is_ci_environment && [ "$explicit" != "--explicit" ]; then
|
|
408
443
|
die "project lifecycle requires alignment (stamp ${stamp:-unstamped}, engine $eng) — run 'intelligence init --apply' locally, review and commit the diff"
|
|
409
444
|
fi
|
|
@@ -444,6 +479,65 @@ check_project_lock() {
|
|
|
444
479
|
validate_project_lock "$1" "$mode"
|
|
445
480
|
}
|
|
446
481
|
|
|
482
|
+
# --- What the store holds (decision 0012) ---------------------------------
|
|
483
|
+
# The store keeps plain files only, so which commit a package directory holds is
|
|
484
|
+
# recorded beside the packages, never inside one (the engine would render it):
|
|
485
|
+
# .intelligence/packages/.installed, one row per package — name, url, path,
|
|
486
|
+
# resolved, sha, separated like the lock's rows — written whenever a package is
|
|
487
|
+
# installed from the lock row it now satisfies. A directory without a row, or
|
|
488
|
+
# with one that differs from the lock, is not what the lock pins.
|
|
489
|
+
STORE_RECORD=".intelligence/packages/.installed"
|
|
490
|
+
|
|
491
|
+
# store_record_get <root> <name> — set IS_REC_URL / _PATH / _RESOLVED / _SHA;
|
|
492
|
+
# returns 1 when the store records nothing for <name>.
|
|
493
|
+
store_record_get() {
|
|
494
|
+
local file="$1/$STORE_RECORD" name url path resolved sha
|
|
495
|
+
IS_REC_URL="" IS_REC_PATH="" IS_REC_RESOLVED="" IS_REC_SHA=""
|
|
496
|
+
[ -f "$file" ] || return 1
|
|
497
|
+
while IFS="$LOCK_SEP" read -r name url path resolved sha; do
|
|
498
|
+
[ "$name" = "$2" ] || continue
|
|
499
|
+
IS_REC_URL="$url" IS_REC_PATH="$path" IS_REC_RESOLVED="$resolved" IS_REC_SHA="$sha"
|
|
500
|
+
return 0
|
|
501
|
+
done < "$file"
|
|
502
|
+
return 1
|
|
503
|
+
}
|
|
504
|
+
|
|
505
|
+
# store_record_matches <root> <name> <url> <path> <resolved> <sha>
|
|
506
|
+
store_record_matches() {
|
|
507
|
+
store_record_get "$1" "$2" || return 1
|
|
508
|
+
[ "$IS_REC_URL" = "$3" ] && [ "$IS_REC_PATH" = "$4" ] \
|
|
509
|
+
&& [ "$IS_REC_RESOLVED" = "$5" ] && [ "$IS_REC_SHA" = "$6" ]
|
|
510
|
+
}
|
|
511
|
+
|
|
512
|
+
# store_record_set <root> <name> <url> <path> <resolved> <sha> — record what
|
|
513
|
+
# <name> now holds; store_record_remove <root> <name> forgets it. Both rewrite
|
|
514
|
+
# the file through a temp file, so a reader never sees half a row.
|
|
515
|
+
store_record_set() {
|
|
516
|
+
_store_record_write "$@"
|
|
517
|
+
}
|
|
518
|
+
|
|
519
|
+
store_record_remove() {
|
|
520
|
+
_store_record_write "$1" "$2"
|
|
521
|
+
}
|
|
522
|
+
|
|
523
|
+
_store_record_write() {
|
|
524
|
+
local root="$1" target="$2" file="$1/$STORE_RECORD" tmp name rest
|
|
525
|
+
mkdir -p "$root/.intelligence/packages" || die "cannot create $root/.intelligence/packages"
|
|
526
|
+
tmp="$file.tmp.$$"
|
|
527
|
+
{
|
|
528
|
+
if [ -f "$file" ]; then
|
|
529
|
+
while IFS="$LOCK_SEP" read -r name rest; do
|
|
530
|
+
[ -n "$name" ] && [ "$name" != "$target" ] || continue
|
|
531
|
+
printf '%s%s%s\n' "$name" "$LOCK_SEP" "$rest"
|
|
532
|
+
done < "$file"
|
|
533
|
+
fi
|
|
534
|
+
[ "$#" -lt 6 ] || printf '%s%s%s%s%s%s%s%s%s\n' "$target" "$LOCK_SEP" "$3" "$LOCK_SEP" "$4" "$LOCK_SEP" "$5" "$LOCK_SEP" "$6"
|
|
535
|
+
} > "$tmp" || { rm -f "$tmp"; die "cannot write $file"; }
|
|
536
|
+
mv -f "$tmp" "$file" || { rm -f "$tmp"; die "cannot write $file"; }
|
|
537
|
+
}
|
|
538
|
+
|
|
539
|
+
# project_store_missing <root> — a package the manifest or lock names is absent
|
|
540
|
+
# from the store, or holds something other than its lock row pins.
|
|
447
541
|
project_store_missing() {
|
|
448
542
|
local root="$1" manifest="$1/intelligence.yaml" name src
|
|
449
543
|
while IFS= read -r name; do
|
|
@@ -460,6 +554,17 @@ project_store_missing() {
|
|
|
460
554
|
esac
|
|
461
555
|
done < <(read_yaml_list "$manifest" "$section")
|
|
462
556
|
done
|
|
557
|
+
# Present is not enough: a `git pull` moves the lock and leaves the ignored
|
|
558
|
+
# store on the commit it held before.
|
|
559
|
+
local rows _requested url path resolved sha
|
|
560
|
+
[ -f "$root/intelligence.lock" ] || return 1
|
|
561
|
+
rows="$(lock_to_tsv "$root/intelligence.lock")"
|
|
562
|
+
while IFS="$LOCK_SEP" read -r name _requested url path resolved sha; do
|
|
563
|
+
[ -n "$name" ] || continue
|
|
564
|
+
assert_valid_pkg_name "$name"
|
|
565
|
+
[ -d "$root/.intelligence/packages/$name" ] || return 0
|
|
566
|
+
store_record_matches "$root" "$name" "$url" "$path" "$resolved" "$sha" || return 0
|
|
567
|
+
done <<< "$rows"
|
|
463
568
|
return 1
|
|
464
569
|
}
|
|
465
570
|
|
package/cli/lib/lockfile.sh
CHANGED
|
@@ -24,7 +24,8 @@ lock_validate() (
|
|
|
24
24
|
# The same tokenizer supplies ordinary lock rows and strict metadata. Buffer
|
|
25
25
|
# before consumption: a failed parser must never yield a partial package set.
|
|
26
26
|
rows="$(_qmap_read lock "$lock" packages '' '' "$LOCKFILE_VERSION")" || return $?
|
|
27
|
-
|
|
27
|
+
bundled_engine_version_var
|
|
28
|
+
current="$IS_BUNDLED_ENGINE_VERSION"
|
|
28
29
|
while IFS="$LOCK_SEP" read -r kind name requested url path resolved sha line; do
|
|
29
30
|
case "$kind" in
|
|
30
31
|
V)
|
package/cli/lib/qmap.awk
CHANGED
|
@@ -177,7 +177,17 @@ END {
|
|
|
177
177
|
exit 1
|
|
178
178
|
}
|
|
179
179
|
if (mode == "lock") print "V" sep top_value["lockfile_version"] sep top_value["engine_version"]
|
|
180
|
-
if (mode == "
|
|
180
|
+
if (mode == "fieldrows") {
|
|
181
|
+
# One row per package key, as `keys` lists them: the name, then each
|
|
182
|
+
# field QMAP_FIELD names (space separated), as qmap_field decodes it.
|
|
183
|
+
count = split(wanted_field, fields, " ")
|
|
184
|
+
for (i = 1; i <= row_count; i++) {
|
|
185
|
+
name = row_name[i]
|
|
186
|
+
row = name
|
|
187
|
+
for (f = 1; f <= count; f++) row = row sep field_value[name SUBSEP fields[f]]
|
|
188
|
+
print row
|
|
189
|
+
}
|
|
190
|
+
} else if (mode == "rows" || mode == "lock" || mode == "keys") {
|
|
181
191
|
for (i = 1; i <= row_count; i++) {
|
|
182
192
|
name = row_name[i]
|
|
183
193
|
if (mode == "keys") print name
|
|
@@ -187,5 +197,10 @@ END {
|
|
|
187
197
|
else print row
|
|
188
198
|
}
|
|
189
199
|
}
|
|
200
|
+
} else if (mode == "tops") {
|
|
201
|
+
# Several top-level scalars in one pass: QMAP_KEY lists them, space
|
|
202
|
+
# separated; each present one prints as key<US>value.
|
|
203
|
+
count = split(wanted_key, tops, " ")
|
|
204
|
+
for (i = 1; i <= count; i++) if (tops[i] in top_value) print tops[i] sep top_value[tops[i]]
|
|
190
205
|
} else if (found) print result
|
|
191
206
|
}
|