@ainova-systems/intelligence 0.11.7 → 0.11.9
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/init.sh +1 -0
- package/cli/commands/status.sh +13 -3
- package/cli/commands/update.sh +1 -0
- package/cli/internal/check.sh +22 -3
- package/cli/internal/convert-legacy.sh +5 -4
- package/cli/internal/package-list.sh +14 -4
- package/cli/internal/restore.sh +5 -14
- package/cli/lib/cli-common.sh +50 -4
- package/cli/lib/lockfile.sh +72 -28
- package/cli/lib/manifest.sh +30 -65
- package/cli/lib/qmap.awk +191 -0
- package/cli/lib/registry.sh +45 -11
- package/engine/ENGINE_SHA +1 -1
- package/engine/VERSION +1 -1
- package/package.json +1 -1
- package/packages/sync/skills/intelligence-sync/SKILL.md +3 -3
package/cli/commands/init.sh
CHANGED
|
@@ -123,6 +123,7 @@ case "$IP_MODE" in
|
|
|
123
123
|
[ "$force" -eq 0 ] || die "--force applies only when converting a legacy Intelligence Sync project"
|
|
124
124
|
if [ "$preview" -eq 1 ]; then
|
|
125
125
|
check_version_compat "$IP_ROOT/intelligence.yaml"
|
|
126
|
+
validate_project_lock "$IP_ROOT"
|
|
126
127
|
echo "project: Intelligence CLI at $IP_ROOT"
|
|
127
128
|
if project_stamped_ahead "$IP_ROOT"; then
|
|
128
129
|
echo " project schema $(read_schema_version "$IP_ROOT/intelligence.yaml") is ahead of engine $(bundled_engine_version) — left as is (update the CLI)"
|
package/cli/commands/status.sh
CHANGED
|
@@ -12,22 +12,32 @@ esac
|
|
|
12
12
|
detect_project
|
|
13
13
|
case "$IP_MODE" in
|
|
14
14
|
cli)
|
|
15
|
+
lock_valid=1
|
|
15
16
|
echo "Project: $IP_ROOT (Intelligence project)"
|
|
16
17
|
echo "Manifest: intelligence.yaml (schema_version $(read_schema_version "$IP_ROOT/intelligence.yaml"))"
|
|
17
|
-
if
|
|
18
|
+
if ! check_project_lock "$IP_ROOT"; then
|
|
19
|
+
lock_valid=0
|
|
20
|
+
echo "Lockfile: INVALID (locked state unchecked)"
|
|
21
|
+
elif [ -f "$IP_ROOT/intelligence.lock" ]; then
|
|
18
22
|
echo "Lockfile: intelligence.lock"
|
|
19
23
|
elif [ -n "$(qmap_keys "$IP_ROOT/intelligence.yaml" "packages")" ]; then
|
|
20
24
|
echo "Lockfile: MISSING (manifest declares packages)"
|
|
21
25
|
else
|
|
22
26
|
echo "Lockfile: none (no packages added yet)"
|
|
23
27
|
fi
|
|
24
|
-
sync_locked="
|
|
25
|
-
if [
|
|
28
|
+
sync_locked=""
|
|
29
|
+
if [ "$lock_valid" -eq 1 ]; then
|
|
30
|
+
sync_locked="$(qmap_field "$IP_ROOT/intelligence.lock" "packages" "$SYNC_PKG_NAME" "resolved")"
|
|
31
|
+
fi
|
|
32
|
+
if [ "$lock_valid" -eq 0 ]; then
|
|
33
|
+
echo "Content: unchecked"
|
|
34
|
+
elif [ -n "$sync_locked" ]; then
|
|
26
35
|
echo "Content: $SYNC_PKG_NAME at $sync_locked"
|
|
27
36
|
else
|
|
28
37
|
echo "Content: no $SYNC_PKG_NAME (bare setup)"
|
|
29
38
|
fi
|
|
30
39
|
echo "Engine: $(bundled_engine_version) (bundled with the CLI)"
|
|
40
|
+
[ "$lock_valid" -eq 1 ] || exit 1
|
|
31
41
|
;;
|
|
32
42
|
legacy)
|
|
33
43
|
echo "Project: $IP_ROOT (legacy Intelligence Sync)"
|
package/cli/commands/update.sh
CHANGED
package/cli/internal/check.sh
CHANGED
|
@@ -31,6 +31,12 @@ esac
|
|
|
31
31
|
echo "project: $IP_ROOT"
|
|
32
32
|
manifest="$IP_ROOT/intelligence.yaml"
|
|
33
33
|
lock="$IP_ROOT/intelligence.lock"
|
|
34
|
+
lock_valid=1
|
|
35
|
+
if ! check_project_lock "$IP_ROOT"; then
|
|
36
|
+
problems=$((problems + 1))
|
|
37
|
+
lock_valid=0
|
|
38
|
+
note "locked package identities cannot be inspected; continuing independent project checks"
|
|
39
|
+
fi
|
|
34
40
|
|
|
35
41
|
stamp="$(read_schema_version "$manifest")"
|
|
36
42
|
eng="$(bundled_engine_version)"
|
|
@@ -50,7 +56,10 @@ fi
|
|
|
50
56
|
|
|
51
57
|
# The engine-content package must sit at the bundled engine version — the
|
|
52
58
|
# content documents exactly the scripts the CLI executes.
|
|
53
|
-
sync_locked="
|
|
59
|
+
sync_locked=""
|
|
60
|
+
if [ "$lock_valid" -eq 1 ]; then
|
|
61
|
+
sync_locked="$(qmap_field "$lock" "packages" "$SYNC_PKG_NAME" "resolved")"
|
|
62
|
+
fi
|
|
54
63
|
if [ -n "$sync_locked" ]; then
|
|
55
64
|
if [ "${sync_locked#v}" = "$eng" ]; then
|
|
56
65
|
ok "$SYNC_PKG_NAME at $sync_locked (matches the engine)"
|
|
@@ -70,6 +79,10 @@ fi
|
|
|
70
79
|
while IFS= read -r name; do
|
|
71
80
|
[ -n "$name" ] || continue
|
|
72
81
|
assert_valid_pkg_name "$name"
|
|
82
|
+
if [ "$lock_valid" -eq 0 ]; then
|
|
83
|
+
note "$name: locked state unchecked"
|
|
84
|
+
continue
|
|
85
|
+
fi
|
|
73
86
|
if [ -z "$(qmap_field "$lock" "packages" "$name" "url")" ]; then
|
|
74
87
|
warn "$name is in the manifest but not in intelligence.lock — run 'intelligence init'"
|
|
75
88
|
else
|
|
@@ -143,7 +156,7 @@ while IFS= read -r target; do
|
|
|
143
156
|
done < <(target_names "$manifest")
|
|
144
157
|
|
|
145
158
|
# Lock orphans.
|
|
146
|
-
if [ -f "$lock" ]; then
|
|
159
|
+
if [ "$lock_valid" -eq 1 ] && [ -f "$lock" ]; then
|
|
147
160
|
while IFS= read -r name; do
|
|
148
161
|
[ -n "$name" ] || continue
|
|
149
162
|
found=0
|
|
@@ -165,7 +178,13 @@ for section in rules agents skills; do
|
|
|
165
178
|
if [ ! -d "$IP_ROOT/$src" ]; then
|
|
166
179
|
case "$src" in
|
|
167
180
|
# Store content is restorable state: absent means un-installed.
|
|
168
|
-
.intelligence/*)
|
|
181
|
+
.intelligence/*)
|
|
182
|
+
if [ "$lock_valid" -eq 1 ]; then
|
|
183
|
+
warn "sources.$section '$src' missing — run 'intelligence sync'"
|
|
184
|
+
else
|
|
185
|
+
warn "sources.$section '$src' missing — resolve the lock problem first"
|
|
186
|
+
fi
|
|
187
|
+
;;
|
|
169
188
|
# A project-owned dir that does not exist yet is a legitimate
|
|
170
189
|
# state — a project consuming only packages authors nothing of
|
|
171
190
|
# its own — and the engine simply skips it. Declared up front so
|
|
@@ -183,7 +183,9 @@ while IFS="$LOCK_SEP" read -r pack name url ref mirror; do
|
|
|
183
183
|
mkdir -p "$dest"
|
|
184
184
|
cp -R "$root/$mirror/." "$dest/"
|
|
185
185
|
if [ -f "$dest/.pack" ]; then
|
|
186
|
-
sha="$(awk -F= '$1 == "sha" {print $2; exit}' "$dest/.pack")"
|
|
186
|
+
sha="$(awk -F= '{ sub(/\r$/, "") } $1 == "sha" {print $2; exit}' "$dest/.pack")"
|
|
187
|
+
[[ "$sha" =~ ^[0-9a-f]{40}$ || "$sha" =~ ^[0-9a-f]{64}$ ]] \
|
|
188
|
+
|| die "pack '$pack' mirror '$mirror' has a missing or invalid recorded commit SHA in .pack — legacy project unchanged; restore its trusted ownership stamp from history or refresh the mirror with its archived engine, then rerun 'intelligence init'"
|
|
187
189
|
rm -f "$dest/.pack"
|
|
188
190
|
fi
|
|
189
191
|
echo " pack '$pack' -> $name (from mirror $mirror)"
|
|
@@ -191,9 +193,8 @@ while IFS="$LOCK_SEP" read -r pack name url ref mirror; do
|
|
|
191
193
|
sha="$(fetch_package "$url" "$ref" "" "$dest")"
|
|
192
194
|
echo " pack '$pack' -> $name (fetched)"
|
|
193
195
|
fi
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
fi
|
|
196
|
+
# A ref currently advertised by the remote cannot prove which commit
|
|
197
|
+
# produced an existing mirror. Only its recorded identity can do that.
|
|
197
198
|
[ -n "$ref" ] || ref="HEAD"
|
|
198
199
|
printf '%s\037%s\037%s\037%s\037%s\037%s\n' \
|
|
199
200
|
"$name" "" "$url" "" "$ref" "$sha" >> "$lock_rows"
|
|
@@ -6,6 +6,8 @@ source "$CLI_DIR/lib/cli-common.sh"
|
|
|
6
6
|
require_cli_project
|
|
7
7
|
manifest="$IP_ROOT/intelligence.yaml"
|
|
8
8
|
lock="$IP_ROOT/intelligence.lock"
|
|
9
|
+
lock_valid=1
|
|
10
|
+
if ! check_project_lock "$IP_ROOT"; then lock_valid=0; fi
|
|
9
11
|
|
|
10
12
|
count=0
|
|
11
13
|
while IFS= read -r name; do
|
|
@@ -13,9 +15,14 @@ while IFS= read -r name; do
|
|
|
13
15
|
count=$((count + 1))
|
|
14
16
|
range="$(qmap_field "$manifest" "packages" "$name" "version")"
|
|
15
17
|
ref="$(qmap_field "$manifest" "packages" "$name" "ref")"
|
|
16
|
-
resolved="
|
|
17
|
-
|
|
18
|
-
|
|
18
|
+
resolved="" sha=""
|
|
19
|
+
if [ "$lock_valid" -eq 1 ]; then
|
|
20
|
+
resolved="$(qmap_field "$lock" "packages" "$name" "resolved")"
|
|
21
|
+
sha="$(qmap_field "$lock" "packages" "$name" "sha")"
|
|
22
|
+
fi
|
|
23
|
+
if [ "$lock_valid" -eq 0 ]; then
|
|
24
|
+
state="invalid lock — locked state unchecked"
|
|
25
|
+
elif [ ! -f "$lock" ]; then
|
|
19
26
|
state="lock missing — restore committed intelligence.lock"
|
|
20
27
|
elif [ -d "$IP_ROOT/.intelligence/packages/$name" ]; then
|
|
21
28
|
state="installed"
|
|
@@ -30,5 +37,8 @@ while IFS= read -r name; do
|
|
|
30
37
|
done < <(qmap_keys "$manifest" "packages")
|
|
31
38
|
|
|
32
39
|
if [ "$count" -eq 0 ]; then
|
|
33
|
-
|
|
40
|
+
if [ "$lock_valid" -eq 1 ]; then
|
|
41
|
+
echo "No packages. Add one: intelligence package add @ainova-systems/core"
|
|
42
|
+
fi
|
|
34
43
|
fi
|
|
44
|
+
[ "$lock_valid" -eq 1 ] || exit 1
|
package/cli/internal/restore.sh
CHANGED
|
@@ -2,8 +2,8 @@
|
|
|
2
2
|
# Internal locked-store restore operation.
|
|
3
3
|
#
|
|
4
4
|
# Restore .intelligence/ from intelligence.lock — the fresh-clone command.
|
|
5
|
-
# --frozen additionally refuses
|
|
6
|
-
#
|
|
5
|
+
# --frozen additionally refuses manifest/lock divergence (CI mode).
|
|
6
|
+
# Locked packages always restore their recorded commit, never the ref's new HEAD.
|
|
7
7
|
set -euo pipefail
|
|
8
8
|
source "$CLI_DIR/lib/cli-common.sh"
|
|
9
9
|
|
|
@@ -21,6 +21,7 @@ done
|
|
|
21
21
|
require_cli_project
|
|
22
22
|
manifest="$IP_ROOT/intelligence.yaml"
|
|
23
23
|
lock="$IP_ROOT/intelligence.lock"
|
|
24
|
+
validate_project_lock "$IP_ROOT" --restore
|
|
24
25
|
|
|
25
26
|
# --frozen is the reproducibility contract: the lock must agree with the
|
|
26
27
|
# manifest on the package SET and on every requested-intent field before a
|
|
@@ -89,7 +90,7 @@ fi
|
|
|
89
90
|
|
|
90
91
|
# Restore every locked package that is not already in the store.
|
|
91
92
|
if [ -f "$lock" ]; then
|
|
92
|
-
while IFS="$LOCK_SEP" read -r name
|
|
93
|
+
while IFS="$LOCK_SEP" read -r name _requested url path resolved sha; do
|
|
93
94
|
[ -n "$name" ] || continue
|
|
94
95
|
# The lock arrives with a cloned repo — its keys are untrusted input
|
|
95
96
|
# on their way into rm -rf paths, and its urls into git argv.
|
|
@@ -106,17 +107,7 @@ if [ -f "$lock" ]; then
|
|
|
106
107
|
# re-run (whose dir-exists check skips verification) would sync it.
|
|
107
108
|
staging="$IP_ROOT/.intelligence/.staging-$$"
|
|
108
109
|
rm -rf "$staging"
|
|
109
|
-
|
|
110
|
-
# An empty sha (bundle-seeded content on a dev build) means unknown,
|
|
111
|
-
# not moved — only two known-and-different shas are a finding.
|
|
112
|
-
if [ -n "$sha" ] && [ -n "$got" ] && [ "$got" != "$sha" ]; then
|
|
113
|
-
if [ "$frozen" -eq 1 ]; then
|
|
114
|
-
rm -rf "$staging"
|
|
115
|
-
die "$name: fetched $got but the lock pins $sha — the ref moved; refusing under --frozen"
|
|
116
|
-
fi
|
|
117
|
-
echo " WARN: $name resolved to $got, lock had $sha — updating the lock" >&2
|
|
118
|
-
lock_upsert "$lock" "$name" "$requested" "$url" "$path" "$resolved" "$got"
|
|
119
|
-
fi
|
|
110
|
+
fetch_package "$url" "$resolved" "$path" "$staging" "$sha" >/dev/null
|
|
120
111
|
rm -rf "${IP_ROOT:?}/$rel"
|
|
121
112
|
mkdir -p "$(dirname "$IP_ROOT/$rel")"
|
|
122
113
|
mv "$staging" "$IP_ROOT/$rel"
|
package/cli/lib/cli-common.sh
CHANGED
|
@@ -14,7 +14,7 @@ source "$IS_ENGINE_DIR/lib/contract.sh"
|
|
|
14
14
|
source "$IS_ENGINE_DIR/lib/adapter-contract.sh"
|
|
15
15
|
|
|
16
16
|
die() {
|
|
17
|
-
echo "ERROR: $*" >&2
|
|
17
|
+
echo "ERROR: ${CLI_INPUT_CONTEXT:-}$*" >&2
|
|
18
18
|
exit 1
|
|
19
19
|
}
|
|
20
20
|
|
|
@@ -30,7 +30,7 @@ assert_safe_source_url() {
|
|
|
30
30
|
local url="$1"
|
|
31
31
|
case "$url" in
|
|
32
32
|
""|-*) die "unsafe source url '$url' — option-shaped or empty" ;;
|
|
33
|
-
*[\"\'\ ]*) die "unsafe source url '$url' — quotes or
|
|
33
|
+
*[\"\'\ ]*|*[[:cntrl:]]*) die "unsafe source url '$url' — quotes or whitespace" ;;
|
|
34
34
|
https://*|http://*|ssh://*|git://*|file://*) ;;
|
|
35
35
|
*@*:*) ;;
|
|
36
36
|
*) die "unsafe source url '$url' — allowed: https, http, ssh, git, file, or user@host:path" ;;
|
|
@@ -43,7 +43,17 @@ assert_safe_ref() {
|
|
|
43
43
|
[ -z "$ref" ] && return 0
|
|
44
44
|
case "$ref" in
|
|
45
45
|
-*) die "unsafe git ref '$ref' — option-shaped" ;;
|
|
46
|
-
*[\"\'\ \\]*) die "unsafe git ref '$ref'" ;;
|
|
46
|
+
*[\"\'\ \\]*|*[[:cntrl:]]*) die "unsafe git ref '$ref'" ;;
|
|
47
|
+
esac
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
# Lexical package-subdirectory validation shared by acquisition and lock
|
|
51
|
+
# preflight. Physical containment of acquired content is a separate boundary.
|
|
52
|
+
assert_safe_package_path() {
|
|
53
|
+
# Preserve literal Git paths, but also guard Windows separator semantics.
|
|
54
|
+
local normalized="${1//\\//}"
|
|
55
|
+
case "$normalized" in
|
|
56
|
+
*..*|/*|[A-Za-z]:*|*[\"\']*|*[[:cntrl:]]*) die "unsafe path '$1'" ;;
|
|
47
57
|
esac
|
|
48
58
|
}
|
|
49
59
|
|
|
@@ -160,7 +170,9 @@ default_target_output() {
|
|
|
160
170
|
}
|
|
161
171
|
|
|
162
172
|
bundled_engine_version() {
|
|
163
|
-
|
|
173
|
+
local version
|
|
174
|
+
version="$(< "$IS_ENGINE_DIR/VERSION")"
|
|
175
|
+
printf '%s' "${version//[$' \t\r\n']/}"
|
|
164
176
|
}
|
|
165
177
|
|
|
166
178
|
# --- The sync package's manifest/lock plumbing ---------------------------
|
|
@@ -297,6 +309,7 @@ project_has_packages() {
|
|
|
297
309
|
ensure_project_current() {
|
|
298
310
|
local root="$1" explicit="${2:-}" manifest="$1/intelligence.yaml" stamp eng
|
|
299
311
|
check_version_compat "$manifest" || return $?
|
|
312
|
+
validate_project_lock "$root" || return $?
|
|
300
313
|
if project_has_packages "$root" && [ ! -f "$root/intelligence.lock" ]; then
|
|
301
314
|
die "manifest declares packages but intelligence.lock is absent — restore the committed lock before running project lifecycle commands"
|
|
302
315
|
fi
|
|
@@ -310,6 +323,39 @@ ensure_project_current() {
|
|
|
310
323
|
bash "$CLI_DIR/internal/align-project.sh" --no-sync
|
|
311
324
|
}
|
|
312
325
|
|
|
326
|
+
# A present lock is validated even when there is no missing store to restore.
|
|
327
|
+
# Keep this before alignment: repairing the sync pin must not rewrite bad input.
|
|
328
|
+
validate_project_lock() {
|
|
329
|
+
local lock="$1/intelligence.lock" output line rc=0
|
|
330
|
+
if [ -e "$lock" ] || [ -L "$lock" ]; then
|
|
331
|
+
output="$(lock_validate "$lock" "${2---metadata}" "$1/intelligence.yaml" 2>&1)" || rc=$?
|
|
332
|
+
while IFS= read -r line; do
|
|
333
|
+
[ -n "$line" ] || continue
|
|
334
|
+
case "$line" in
|
|
335
|
+
WARNING:*)
|
|
336
|
+
case "${IS_LOCK_WARNINGS:-}" in *"$line"*) continue ;; esac
|
|
337
|
+
# Suppress only duplicate messages across lifecycle children;
|
|
338
|
+
# the lock itself is always parsed and validated again.
|
|
339
|
+
export IS_LOCK_WARNINGS="${IS_LOCK_WARNINGS:-}$line"$'\n'
|
|
340
|
+
;;
|
|
341
|
+
esac
|
|
342
|
+
echo "$line" >&2
|
|
343
|
+
done <<< "$output"
|
|
344
|
+
if [ "$rc" -ne 0 ]; then
|
|
345
|
+
echo "ERROR: invalid intelligence.lock — recovery: https://github.com/ainova-systems/intelligence/blob/main/docs/cli.md#recovering-a-lock" >&2
|
|
346
|
+
return 1
|
|
347
|
+
fi
|
|
348
|
+
fi
|
|
349
|
+
}
|
|
350
|
+
|
|
351
|
+
# Read-only diagnosis must distinguish usable installed metadata from metadata
|
|
352
|
+
# which could actually restore a missing store.
|
|
353
|
+
check_project_lock() {
|
|
354
|
+
local mode=--metadata
|
|
355
|
+
if project_store_missing "$1"; then mode=--restore; fi
|
|
356
|
+
validate_project_lock "$1" "$mode"
|
|
357
|
+
}
|
|
358
|
+
|
|
313
359
|
project_store_missing() {
|
|
314
360
|
local root="$1" manifest="$1/intelligence.yaml" name src
|
|
315
361
|
while IFS= read -r name; do
|
package/cli/lib/lockfile.sh
CHANGED
|
@@ -10,38 +10,74 @@
|
|
|
10
10
|
LOCKFILE_VERSION=1
|
|
11
11
|
LOCK_SEP=$'\x1f'
|
|
12
12
|
|
|
13
|
-
#
|
|
14
|
-
#
|
|
15
|
-
|
|
13
|
+
# lock_validate <lock> [--metadata|--restore] [manifest] — read-only preflight. A subshell
|
|
14
|
+
# contains the fail-fast input guards so status can report a failed verdict too.
|
|
15
|
+
lock_validate() (
|
|
16
|
+
local lock="$1" mode="${2---metadata}" manifest="${3:-}" count=0
|
|
17
|
+
local kind name requested url path resolved sha line rows current
|
|
18
|
+
local -x CLI_INPUT_CONTEXT=""
|
|
19
|
+
[ "$#" -le 3 ] || die "internal: too many lock validation arguments"
|
|
20
|
+
case "$mode" in
|
|
21
|
+
--metadata|--restore) ;;
|
|
22
|
+
*) die "internal: unknown lock validation mode '$mode'" ;;
|
|
23
|
+
esac
|
|
24
|
+
# The same tokenizer supplies ordinary lock rows and strict metadata. Buffer
|
|
25
|
+
# before consumption: a failed parser must never yield a partial package set.
|
|
26
|
+
rows="$(_qmap_read lock "$lock" packages '' '' "$LOCKFILE_VERSION")" || return $?
|
|
27
|
+
current="$(bundled_engine_version)"
|
|
28
|
+
while IFS="$LOCK_SEP" read -r kind name requested url path resolved sha line; do
|
|
29
|
+
case "$kind" in
|
|
30
|
+
V)
|
|
31
|
+
[[ "$requested" =~ ^[0-9]+\.[0-9]+\.[0-9]+$ ]] \
|
|
32
|
+
|| die "$lock has missing or invalid engine_version"
|
|
33
|
+
continue
|
|
34
|
+
;;
|
|
35
|
+
R) ;;
|
|
36
|
+
*) die "internal: unexpected lock record '$kind'" ;;
|
|
37
|
+
esac
|
|
38
|
+
count=$((count + 1))
|
|
39
|
+
CLI_INPUT_CONTEXT="$lock:$line: package $name, field name: "
|
|
40
|
+
assert_valid_pkg_name "$name"
|
|
41
|
+
CLI_INPUT_CONTEXT="$lock:$line: package $name, field url: "
|
|
42
|
+
assert_safe_source_url "$url"
|
|
43
|
+
CLI_INPUT_CONTEXT="$lock:$line: package $name, field resolved: "
|
|
44
|
+
[ -n "$resolved" ] || die "locked restore: $name has a missing resolved ref"
|
|
45
|
+
assert_safe_ref "$resolved"
|
|
46
|
+
CLI_INPUT_CONTEXT="$lock:$line: package $name, field path: "
|
|
47
|
+
assert_safe_package_path "$path"
|
|
48
|
+
CLI_INPUT_CONTEXT="$lock:$line: package $name, field sha: "
|
|
49
|
+
if [ -z "$sha" ]; then
|
|
50
|
+
if [ "$url" = "$SYNC_PKG_URL" ] && [ "$path" = "$SYNC_PKG_PATH" ] \
|
|
51
|
+
&& [ "$resolved" = "v$current" ]; then
|
|
52
|
+
continue
|
|
53
|
+
fi
|
|
54
|
+
# engine_version records the writer CLI, which can differ from the
|
|
55
|
+
# preserved bundle pin after a same-major older CLI adds a package.
|
|
56
|
+
# Versioned bundle metadata admits alignment/installed content only;
|
|
57
|
+
# it must never authorize an unpinned cross-version acquisition.
|
|
58
|
+
if [ "$mode" = "--metadata" ] && [ "$url" = "$SYNC_PKG_URL" ] \
|
|
59
|
+
&& [ "$path" = "$SYNC_PKG_PATH" ] && [[ "$resolved" =~ ^v[0-9]+\.[0-9]+\.[0-9]+$ ]] \
|
|
60
|
+
&& [ "${resolved%%.*}" = "v${current%%.*}" ]; then
|
|
61
|
+
echo "WARNING: $name at $resolved has no locked commit SHA; commit verification is unavailable for this development-bundle metadata" >&2
|
|
62
|
+
continue
|
|
63
|
+
fi
|
|
64
|
+
fi
|
|
65
|
+
[[ "$sha" =~ ^[0-9a-f]{40}$ || "$sha" =~ ^[0-9a-f]{64}$ ]] \
|
|
66
|
+
|| die "locked restore: $name has a missing or invalid commit SHA — restore a valid intelligence.lock"
|
|
67
|
+
done <<< "$rows"
|
|
68
|
+
if [ "$count" -eq 0 ] && [ -n "$manifest" ] && [ -n "$(qmap_keys "$manifest" packages)" ]; then
|
|
69
|
+
die "$lock: packages is empty but the manifest declares packages — restore their recorded identities"
|
|
70
|
+
fi
|
|
71
|
+
)
|
|
72
|
+
|
|
73
|
+
# top_scalar <file> <key> — one top-level scalar through the shared tokenizer.
|
|
16
74
|
top_scalar() {
|
|
17
|
-
|
|
18
|
-
awk -v key="$2" '
|
|
19
|
-
{ sub(/\r$/, "") }
|
|
20
|
-
$0 ~ "^" key ":" {
|
|
21
|
-
v = substr($0, length(key) + 2)
|
|
22
|
-
sub(/^[ \t]+/, "", v)
|
|
23
|
-
if (v ~ /^"/) { v = substr(v, 2); q = index(v, "\""); if (q > 0) v = substr(v, 1, q - 1) }
|
|
24
|
-
else { sub(/[ \t]+#.*$/, "", v); sub(/[ \t]+$/, "", v) }
|
|
25
|
-
print v
|
|
26
|
-
exit
|
|
27
|
-
}
|
|
28
|
-
' "$1"
|
|
75
|
+
_qmap_read top "$1" '' "$2"
|
|
29
76
|
}
|
|
30
77
|
|
|
31
78
|
# lock_to_tsv <lock> — every locked package as one row.
|
|
32
79
|
lock_to_tsv() {
|
|
33
|
-
|
|
34
|
-
[ -f "$lock" ] || return 0
|
|
35
|
-
while IFS= read -r name; do
|
|
36
|
-
[ -n "$name" ] || continue
|
|
37
|
-
printf '%s\037%s\037%s\037%s\037%s\037%s\n' \
|
|
38
|
-
"$name" \
|
|
39
|
-
"$(qmap_field "$lock" "packages" "$name" "requested")" \
|
|
40
|
-
"$(qmap_field "$lock" "packages" "$name" "url")" \
|
|
41
|
-
"$(qmap_field "$lock" "packages" "$name" "path")" \
|
|
42
|
-
"$(qmap_field "$lock" "packages" "$name" "resolved")" \
|
|
43
|
-
"$(qmap_field "$lock" "packages" "$name" "sha")"
|
|
44
|
-
done < <(qmap_keys "$lock" "packages")
|
|
80
|
+
_qmap_read rows "$1" packages
|
|
45
81
|
}
|
|
46
82
|
|
|
47
83
|
# lock_write_from_tsv <lock> <tsv-file> — rebuild the lock. Entries are
|
|
@@ -58,7 +94,15 @@ lock_write_from_tsv() {
|
|
|
58
94
|
# them (registry-supplied url/path included) must be escaped, or it
|
|
59
95
|
# would break or alias the neighbouring field.
|
|
60
96
|
awk -F"$LOCK_SEP" '
|
|
61
|
-
function esc(s
|
|
97
|
+
function esc(s, i, c, out) {
|
|
98
|
+
out = ""
|
|
99
|
+
for (i = 1; i <= length(s); i++) {
|
|
100
|
+
c = substr(s, i, 1)
|
|
101
|
+
if (c == "\\" || c == "\"") out = out "\\"
|
|
102
|
+
out = out c
|
|
103
|
+
}
|
|
104
|
+
return out
|
|
105
|
+
}
|
|
62
106
|
NF >= 6 {
|
|
63
107
|
print " \"" esc($1) "\":"
|
|
64
108
|
if ($2 != "") print " requested: \"" esc($2) "\""
|
package/cli/lib/manifest.sh
CHANGED
|
@@ -87,7 +87,8 @@ target_set_enabled() {
|
|
|
87
87
|
case "$enabled" in true|false) ;; *) die "internal: invalid target state '$enabled'" ;; esac
|
|
88
88
|
target_exists "$file" "$target" && target_missing=0
|
|
89
89
|
output="$(_yq_esc "$output")"
|
|
90
|
-
_qmap_stage "$file" -v target="$target" -v enabled="$enabled" -v
|
|
90
|
+
QMAP_EDIT_VALUE="$output" _qmap_stage "$file" -v target="$target" -v enabled="$enabled" -v target_missing="$target_missing" '
|
|
91
|
+
BEGIN { output = ENVIRON["QMAP_EDIT_VALUE"] }
|
|
91
92
|
function entry() { return " " target ": { enabled: " enabled ", output: \"" output "\" }" }
|
|
92
93
|
function insert_missing_target() {
|
|
93
94
|
if (in_targets && target_missing && !target_seen) { print entry(); target_seen = 1 }
|
|
@@ -154,71 +155,33 @@ target_set_enabled() {
|
|
|
154
155
|
'
|
|
155
156
|
}
|
|
156
157
|
|
|
157
|
-
#
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
}
|
|
171
|
-
' "$1"
|
|
158
|
+
# _qmap_read <mode> <file> <block> [key] [field] [expected-lock-version]
|
|
159
|
+
# All scalar reads and strict lock records share one tokenizer. Environment
|
|
160
|
+
# transport preserves literal backslashes that awk -v would interpret again.
|
|
161
|
+
_qmap_read() {
|
|
162
|
+
local mode="$1" file="$2"
|
|
163
|
+
if [ ! -f "$file" ] || [ ! -r "$file" ]; then
|
|
164
|
+
case "$mode" in
|
|
165
|
+
lock|validate) echo "cannot read $file" >&2; return 1 ;;
|
|
166
|
+
*) return 0 ;;
|
|
167
|
+
esac
|
|
168
|
+
fi
|
|
169
|
+
QMAP_MODE="$mode" QMAP_FILE="$file" QMAP_BLOCK="${3:-}" \
|
|
170
|
+
QMAP_KEY="${4:-}" QMAP_FIELD="${5:-}" QMAP_EXPECTED_VERSION="${6:-}" \
|
|
171
|
+
LC_ALL=C awk -f "${BASH_SOURCE[0]%/*}/qmap.awk" < "$file"
|
|
172
172
|
}
|
|
173
173
|
|
|
174
|
-
#
|
|
175
|
-
|
|
176
|
-
qmap_field() {
|
|
177
|
-
[ -f "$1" ] || return 0
|
|
178
|
-
awk -v block="$2" -v key="$3" -v field="$4" '
|
|
179
|
-
{ sub(/\r$/, "") }
|
|
180
|
-
$0 ~ "^" block ":[ \t]*$" { inb = 1; next }
|
|
181
|
-
inb && /^[^ #]/ { inb = 0; ink = 0 }
|
|
182
|
-
inb && /^ "/ {
|
|
183
|
-
s = substr($0, 4); q = index(s, "\"")
|
|
184
|
-
ink = (q > 0 && substr(s, 1, q - 1) == key)
|
|
185
|
-
next
|
|
186
|
-
}
|
|
187
|
-
inb && ink && /^ [A-Za-z_]/ {
|
|
188
|
-
line = $0
|
|
189
|
-
sub(/^ /, "", line)
|
|
190
|
-
c = index(line, ":")
|
|
191
|
-
if (c == 0) next
|
|
192
|
-
if (substr(line, 1, c - 1) != field) next
|
|
193
|
-
v = substr(line, c + 1)
|
|
194
|
-
sub(/^[ \t]+/, "", v)
|
|
195
|
-
if (v ~ /^"/) { v = substr(v, 2); q2 = index(v, "\""); if (q2 > 0) v = substr(v, 1, q2 - 1) }
|
|
196
|
-
else { sub(/[ \t]+#.*$/, "", v); sub(/[ \t]+$/, "", v) }
|
|
197
|
-
print v
|
|
198
|
-
exit
|
|
199
|
-
}
|
|
200
|
-
' "$1"
|
|
201
|
-
}
|
|
174
|
+
# qmap_validate_document <file> <block> — structural validation only.
|
|
175
|
+
qmap_validate_document() { _qmap_read validate "$1" "$2"; }
|
|
202
176
|
|
|
203
|
-
#
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
s = substr($0, 4); q = index(s, "\"")
|
|
212
|
-
if (q == 0 || substr(s, 1, q - 1) != key) next
|
|
213
|
-
v = substr(s, q + 1)
|
|
214
|
-
sub(/^:[ \t]*/, "", v)
|
|
215
|
-
if (v ~ /^"/) { v = substr(v, 2); q2 = index(v, "\""); if (q2 > 0) v = substr(v, 1, q2 - 1) }
|
|
216
|
-
else { sub(/[ \t]+#.*$/, "", v); sub(/[ \t]+$/, "", v) }
|
|
217
|
-
if (v != "") print v
|
|
218
|
-
exit
|
|
219
|
-
}
|
|
220
|
-
' "$1"
|
|
221
|
-
}
|
|
177
|
+
# qmap_keys <file> <block> — quoted keys in document order.
|
|
178
|
+
qmap_keys() { _qmap_read keys "$1" "$2"; }
|
|
179
|
+
|
|
180
|
+
# qmap_field <file> <block> <key> <field> — decoded nested scalar.
|
|
181
|
+
qmap_field() { _qmap_read field "$1" "$2" "$3" "$4"; }
|
|
182
|
+
|
|
183
|
+
# qmap_value <file> <block> <key> — decoded flat scalar.
|
|
184
|
+
qmap_value() { _qmap_read value "$1" "$2" "$3"; }
|
|
222
185
|
|
|
223
186
|
# _qmap_stage <file> <awk-program> [awk args…] — run an editing pass, verify
|
|
224
187
|
# it produced output, commit.
|
|
@@ -245,7 +208,8 @@ qmap_set() {
|
|
|
245
208
|
# url/path values can carry a `"`; escape before it reaches the quoted
|
|
246
209
|
# scalar the writer emits.
|
|
247
210
|
value="$(_yq_esc "$value")"
|
|
248
|
-
_qmap_stage "$file" -v block="$block" -v key="$key" -v field="$field"
|
|
211
|
+
QMAP_EDIT_VALUE="$value" _qmap_stage "$file" -v block="$block" -v key="$key" -v field="$field" '
|
|
212
|
+
BEGIN { value = ENVIRON["QMAP_EDIT_VALUE"] }
|
|
249
213
|
function keyline() { return " \"" key "\":" }
|
|
250
214
|
function fieldline() { return " " field ": \"" value "\"" }
|
|
251
215
|
function flush_key() {
|
|
@@ -292,7 +256,8 @@ qmap_set_value() {
|
|
|
292
256
|
local file="$1" block="$2" key="$3" value="$4"
|
|
293
257
|
[ -f "$file" ] || die "no such file: $file"
|
|
294
258
|
value="$(_yq_esc "$value")"
|
|
295
|
-
_qmap_stage "$file" -v block="$block" -v key="$key"
|
|
259
|
+
QMAP_EDIT_VALUE="$value" _qmap_stage "$file" -v block="$block" -v key="$key" '
|
|
260
|
+
BEGIN { value = ENVIRON["QMAP_EDIT_VALUE"] }
|
|
296
261
|
function entry() { return " \"" key "\": \"" value "\"" }
|
|
297
262
|
{ sub(/\r$/, "") }
|
|
298
263
|
$0 ~ "^" block ":[ \t]*$" { blockseen = 1; inb = 1; print; next }
|
package/cli/lib/qmap.awk
ADDED
|
@@ -0,0 +1,191 @@
|
|
|
1
|
+
# Shared tokenizer for the CLI-owned quoted-key map and scalar document shape.
|
|
2
|
+
# Reader modes ignore unrelated YAML. Lock mode validates the whole document
|
|
3
|
+
# before publishing rows, so a bad later entry never yields a partial result.
|
|
4
|
+
BEGIN {
|
|
5
|
+
mode = ENVIRON["QMAP_MODE"]
|
|
6
|
+
block = ENVIRON["QMAP_BLOCK"]
|
|
7
|
+
wanted_key = ENVIRON["QMAP_KEY"]
|
|
8
|
+
wanted_field = ENVIRON["QMAP_FIELD"]
|
|
9
|
+
expected_version = ENVIRON["QMAP_EXPECTED_VERSION"]
|
|
10
|
+
source_file = ENVIRON["QMAP_FILE"]
|
|
11
|
+
strict = (mode == "lock" || mode == "validate")
|
|
12
|
+
sep = sprintf("%c", 31)
|
|
13
|
+
single_quote = sprintf("%c", 39)
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
function error_at(reason, line) {
|
|
17
|
+
if (problem == "") {
|
|
18
|
+
problem = reason
|
|
19
|
+
problem_line = line
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
# quoted() also parses keys. quote_end leaves the caller at the delimiter;
|
|
24
|
+
# decoded contains literal bytes, with the writer's two escapes decoded once.
|
|
25
|
+
function quoted(s, i, c, escaped) {
|
|
26
|
+
decoded = ""
|
|
27
|
+
quote_end = 0
|
|
28
|
+
for (i = 2; i <= length(s); i++) {
|
|
29
|
+
c = substr(s, i, 1)
|
|
30
|
+
if (c == "\"") {
|
|
31
|
+
quote_end = i
|
|
32
|
+
return 1
|
|
33
|
+
}
|
|
34
|
+
if (c == "\\") {
|
|
35
|
+
if (++i > length(s)) return 0
|
|
36
|
+
escaped = substr(s, i, 1)
|
|
37
|
+
if (escaped == "\\" || escaped == "\"") c = escaped
|
|
38
|
+
else {
|
|
39
|
+
if (strict) return 0
|
|
40
|
+
c = "\\" escaped
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
if (strict && c ~ /[[:cntrl:]]/) return 0
|
|
44
|
+
decoded = decoded c
|
|
45
|
+
}
|
|
46
|
+
return 0
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
function scalar(s, tail) {
|
|
50
|
+
decoded = ""
|
|
51
|
+
empty_scalar = 0
|
|
52
|
+
sub(/^[ \t]+/, "", s)
|
|
53
|
+
if (s == "" || s ~ /^#/) {
|
|
54
|
+
empty_scalar = 1
|
|
55
|
+
return 1
|
|
56
|
+
}
|
|
57
|
+
if (s ~ /^"/) {
|
|
58
|
+
if (!quoted(s)) return 0
|
|
59
|
+
tail = substr(s, quote_end + 1)
|
|
60
|
+
return !strict || tail ~ /^([ \t]*|[ \t]+#.*)$/
|
|
61
|
+
}
|
|
62
|
+
sub(/[ \t]+#.*$/, "", s)
|
|
63
|
+
sub(/[ \t]+$/, "", s)
|
|
64
|
+
if (strict) {
|
|
65
|
+
if (s ~ /[[:cntrl:]]/ || substr(s, 1, 1) == single_quote) return 0
|
|
66
|
+
if (s ~ /^[!&*\[\]{}>|%@`]/ || s ~ /^[?:-]([ \t]|$)/ || s ~ /"|:[ \t]/) return 0
|
|
67
|
+
if (s ~ /^(null|Null|NULL|~|true|True|TRUE|false|False|FALSE)$/) return 0
|
|
68
|
+
}
|
|
69
|
+
decoded = s
|
|
70
|
+
return 1
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
# Tokenization is shared by permissive reads and strict lock validation.
|
|
74
|
+
function token(line, text, colon, tail) {
|
|
75
|
+
kind = ""
|
|
76
|
+
key = ""
|
|
77
|
+
value = ""
|
|
78
|
+
empty = 0
|
|
79
|
+
if (line ~ /^[A-Za-z_]/) {
|
|
80
|
+
kind = "top"
|
|
81
|
+
text = line
|
|
82
|
+
} else if (line ~ /^ [A-Za-z_]/) {
|
|
83
|
+
kind = "field"
|
|
84
|
+
text = substr(line, 5)
|
|
85
|
+
} else if (line ~ /^ "/) {
|
|
86
|
+
kind = "package"
|
|
87
|
+
text = substr(line, 3)
|
|
88
|
+
if (!quoted(text)) return 0
|
|
89
|
+
key = decoded
|
|
90
|
+
tail = substr(text, quote_end + 1)
|
|
91
|
+
if (substr(tail, 1, 1) != ":") return 0
|
|
92
|
+
if (!scalar(substr(tail, 2))) return 0
|
|
93
|
+
value = decoded
|
|
94
|
+
empty = empty_scalar
|
|
95
|
+
return 1
|
|
96
|
+
} else return 0
|
|
97
|
+
colon = index(text, ":")
|
|
98
|
+
if (!colon) return 0
|
|
99
|
+
key = substr(text, 1, colon - 1)
|
|
100
|
+
if (strict && key !~ /^[A-Za-z_][A-Za-z0-9_]*$/) return 0
|
|
101
|
+
if (!scalar(substr(text, colon + 1))) return 0
|
|
102
|
+
value = decoded
|
|
103
|
+
empty = empty_scalar
|
|
104
|
+
return 1
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
{
|
|
108
|
+
sub(/\r$/, "")
|
|
109
|
+
if ($0 ~ /^[ \t]*(#.*)?$/) next
|
|
110
|
+
if (!token($0)) {
|
|
111
|
+
if (strict) {
|
|
112
|
+
context = ""
|
|
113
|
+
if (key ~ /^[A-Za-z_][A-Za-z0-9_]*$/) {
|
|
114
|
+
if (kind == "field" && package != "") context = "package " package ", field " key ": "
|
|
115
|
+
else if (kind == "top") context = "field " key ": "
|
|
116
|
+
}
|
|
117
|
+
error_at(context "malformed lock structure or unsupported scalar", NR)
|
|
118
|
+
}
|
|
119
|
+
if ($0 ~ /^[^ #\t]/) { in_block = 0; package = "" }
|
|
120
|
+
next
|
|
121
|
+
}
|
|
122
|
+
if (kind == "top") {
|
|
123
|
+
in_block = (key == block && empty)
|
|
124
|
+
package = ""
|
|
125
|
+
if (strict && top_seen[key]++) error_at("duplicate top-level field " key, NR)
|
|
126
|
+
if (!(key in top_value)) top_value[key] = value
|
|
127
|
+
if (mode == "top" && key == wanted_key && !found) { result = value; found = 1 }
|
|
128
|
+
if (strict) {
|
|
129
|
+
if (key == block) {
|
|
130
|
+
block_seen = 1
|
|
131
|
+
if (!empty) error_at(block " must be a block map", NR)
|
|
132
|
+
} else if (empty) error_at("expected scalar for " key, NR)
|
|
133
|
+
}
|
|
134
|
+
next
|
|
135
|
+
}
|
|
136
|
+
if (kind == "package" && in_block) {
|
|
137
|
+
package = key
|
|
138
|
+
row_count++
|
|
139
|
+
row_name[row_count] = key
|
|
140
|
+
row_line[row_count] = NR
|
|
141
|
+
if (strict) {
|
|
142
|
+
if (key == "") error_at("empty package key", NR)
|
|
143
|
+
if (package_seen[key]++) error_at("duplicate package " key, NR)
|
|
144
|
+
if (!empty) error_at("expected field map for " key, NR)
|
|
145
|
+
}
|
|
146
|
+
if (mode == "value" && key == wanted_key && !found) { result = value; found = 1 }
|
|
147
|
+
next
|
|
148
|
+
}
|
|
149
|
+
if (kind == "field" && in_block && package != "") {
|
|
150
|
+
identity = package SUBSEP key
|
|
151
|
+
if (strict && field_seen[identity]++) error_at("duplicate field " package "." key, NR)
|
|
152
|
+
if (!(identity in field_value)) field_value[identity] = value
|
|
153
|
+
if (strict && empty) error_at("expected scalar for " package "." key, NR)
|
|
154
|
+
if (mode == "field" && package == wanted_key && key == wanted_field && !found) {
|
|
155
|
+
result = value
|
|
156
|
+
found = 1
|
|
157
|
+
}
|
|
158
|
+
next
|
|
159
|
+
}
|
|
160
|
+
if (strict) error_at("malformed lock structure: expected top-level scalar or quoted package field", NR)
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
END {
|
|
164
|
+
# Read the format header before interpreting a future body's shape. Errors
|
|
165
|
+
# were retained during the single pass so header placement does not matter.
|
|
166
|
+
if (mode == "lock" && top_value["lockfile_version"] != expected_version) {
|
|
167
|
+
version = top_value["lockfile_version"]
|
|
168
|
+
print source_file " has unsupported or missing lockfile_version '" (version == "" ? "<missing>" : version) "' — expected " expected_version > "/dev/stderr"
|
|
169
|
+
exit 1
|
|
170
|
+
}
|
|
171
|
+
if (strict && problem != "") {
|
|
172
|
+
print source_file ":" problem_line ": " problem > "/dev/stderr"
|
|
173
|
+
exit 1
|
|
174
|
+
}
|
|
175
|
+
if (strict && !block_seen) {
|
|
176
|
+
print source_file ": missing " block " block" > "/dev/stderr"
|
|
177
|
+
exit 1
|
|
178
|
+
}
|
|
179
|
+
if (mode == "lock") print "V" sep top_value["lockfile_version"] sep top_value["engine_version"]
|
|
180
|
+
if (mode == "rows" || mode == "lock" || mode == "keys") {
|
|
181
|
+
for (i = 1; i <= row_count; i++) {
|
|
182
|
+
name = row_name[i]
|
|
183
|
+
if (mode == "keys") print name
|
|
184
|
+
else {
|
|
185
|
+
row = name sep field_value[name SUBSEP "requested"] sep field_value[name SUBSEP "url"] sep field_value[name SUBSEP "path"] sep field_value[name SUBSEP "resolved"] sep field_value[name SUBSEP "sha"]
|
|
186
|
+
if (mode == "lock") print "R" sep row sep row_line[i]
|
|
187
|
+
else print row
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
} else if (found) print result
|
|
191
|
+
}
|
package/cli/lib/registry.sh
CHANGED
|
@@ -106,53 +106,87 @@ suggest_similar() {
|
|
|
106
106
|
fi
|
|
107
107
|
}
|
|
108
108
|
|
|
109
|
-
#
|
|
109
|
+
# is_bundle_source <url> <ref> <subpath>
|
|
110
|
+
is_bundle_source() {
|
|
111
|
+
[ "$1" = "$SYNC_PKG_URL" ] && [ "$3" = "$SYNC_PKG_PATH" ] \
|
|
112
|
+
&& [ "${2#v}" = "$(bundled_engine_version)" ]
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
# fetch_package <url> <ref> <subpath> <dest-dir> [locked-sha]
|
|
110
116
|
# Shallow-clones url@ref (tag, branch, or SHA fallback), copies <subpath>
|
|
111
117
|
# (or the repo root) into <dest-dir>, prints the resolved commit sha.
|
|
118
|
+
# A locked SHA selects and verifies the commit independently of the ref name.
|
|
112
119
|
# The clone pins autocrlf/eol/symlinks exactly like the engine does, so the
|
|
113
120
|
# store holds the bytes the package published.
|
|
114
121
|
fetch_package() {
|
|
115
|
-
local url="$1" ref="$2" subpath="$3" dest="$4"
|
|
122
|
+
local url="$1" ref="$2" subpath="$3" dest="$4" locked_sha="${5:-}" bundle_sha=""
|
|
116
123
|
assert_safe_source_url "$url"
|
|
117
124
|
assert_safe_ref "$ref"
|
|
125
|
+
assert_safe_package_path "$subpath"
|
|
118
126
|
# Bundle seed: the engine-content package at the CLI's own version copies
|
|
119
127
|
# from the npm bundle — no network, which keeps init, sync restoration and
|
|
120
128
|
# archived-project conversion offline. Keyed on the
|
|
121
129
|
# full (url, path, ref) triple; any other version or source falls through
|
|
122
130
|
# to the normal clone.
|
|
123
|
-
if
|
|
124
|
-
|
|
131
|
+
if is_bundle_source "$url" "$ref" "$subpath"; then
|
|
132
|
+
if [ -f "$IS_ENGINE_DIR/ENGINE_SHA" ]; then
|
|
133
|
+
bundle_sha="$(tr -d ' \t\r\n' < "$IS_ENGINE_DIR/ENGINE_SHA")"
|
|
134
|
+
fi
|
|
135
|
+
fi
|
|
136
|
+
if is_bundle_source "$url" "$ref" "$subpath" \
|
|
137
|
+
&& { [ -z "$locked_sha" ] || [ -z "$bundle_sha" ] || [ "$locked_sha" = "$bundle_sha" ]; }; then
|
|
125
138
|
[ -n "${IS_BUNDLED_PKG_DIR:-}" ] && [ -d "$IS_BUNDLED_PKG_DIR" ] \
|
|
126
139
|
|| die "bundled engine content not found next to the CLI — reinstall @ainova-systems/intelligence"
|
|
127
140
|
rm -rf "$dest"
|
|
128
141
|
mkdir -p "$dest"
|
|
129
142
|
cp -R "$IS_BUNDLED_PKG_DIR/." "$dest/"
|
|
130
143
|
rm -rf "$dest/.git"
|
|
131
|
-
if [ -
|
|
132
|
-
|
|
144
|
+
if [ "$#" -ge 5 ] && { [ -z "$locked_sha" ] || [ -z "$bundle_sha" ]; }; then
|
|
145
|
+
echo "WARNING: bundled sync content restored without commit verification (development bundle or lock has no SHA)" >&2
|
|
133
146
|
fi
|
|
147
|
+
printf '%s' "$bundle_sha"
|
|
134
148
|
return 0
|
|
135
149
|
fi
|
|
136
150
|
local tmp sha src
|
|
137
151
|
local -a branch_arg=()
|
|
138
152
|
[ -n "$ref" ] && branch_arg=(--branch "$ref")
|
|
139
153
|
tmp="$(mktemp -d -t intelligence-pkg-XXXXXX 2>/dev/null || mktemp -d)"
|
|
140
|
-
if
|
|
154
|
+
if [ -n "$locked_sha" ]; then
|
|
155
|
+
local -a format_arg=()
|
|
156
|
+
[ "${#locked_sha}" -ne 64 ] || format_arg=(--object-format=sha256)
|
|
157
|
+
git init --quiet "${format_arg[@]+"${format_arg[@]}"}" "$tmp/clone" \
|
|
158
|
+
|| { rm -rf "$tmp"; die "cannot stage locked commit $locked_sha"; }
|
|
159
|
+
# Request the object directly, including retained commits no longer named
|
|
160
|
+
# by a branch/tag. Servers that forbid SHA wants may still advertise a
|
|
161
|
+
# descendant; fetch their history before declaring the commit unavailable.
|
|
162
|
+
if ! GIT_TERMINAL_PROMPT=0 git -C "$tmp/clone" fetch --quiet --no-tags --depth 1 -- "$url" "$locked_sha" 2>/dev/null; then
|
|
163
|
+
GIT_TERMINAL_PROMPT=0 git -C "$tmp/clone" fetch --quiet --tags -- "$url" \
|
|
164
|
+
'+refs/heads/*:refs/remotes/locked/*' 2>/dev/null \
|
|
165
|
+
|| { rm -rf "$tmp"; die "cannot fetch locked commit $locked_sha from $url — check source access and commit availability"; }
|
|
166
|
+
fi
|
|
167
|
+
[ "$(git -C "$tmp/clone" cat-file -t "$locked_sha" 2>/dev/null || true)" = commit ] \
|
|
168
|
+
|| { rm -rf "$tmp"; die "locked commit $locked_sha unavailable from $url — refusing to substitute '$ref' or HEAD"; }
|
|
169
|
+
git -c core.symlinks=false -c core.autocrlf=false -c core.eol=lf \
|
|
170
|
+
-C "$tmp/clone" checkout --quiet --detach "$locked_sha" -- \
|
|
171
|
+
|| { rm -rf "$tmp"; die "cannot check out locked commit $locked_sha from $url"; }
|
|
172
|
+
elif ! GIT_TERMINAL_PROMPT=0 git -c core.symlinks=false -c core.autocrlf=false -c core.eol=lf \
|
|
141
173
|
clone --depth 1 "${branch_arg[@]+"${branch_arg[@]}"}" --quiet -- "$url" "$tmp/clone" 2>/dev/null; then
|
|
142
174
|
# A SHA is not clonable with --branch — fall back to a full clone + checkout.
|
|
143
175
|
rm -rf "$tmp/clone"
|
|
144
176
|
GIT_TERMINAL_PROMPT=0 git -c core.symlinks=false -c core.autocrlf=false -c core.eol=lf \
|
|
145
177
|
clone --quiet -- "$url" "$tmp/clone" 2>/dev/null \
|
|
146
178
|
|| { rm -rf "$tmp"; die "cannot clone $url"; }
|
|
147
|
-
[ -z "$ref" ] || git -
|
|
179
|
+
[ -z "$ref" ] || git -c core.symlinks=false -c core.autocrlf=false -c core.eol=lf \
|
|
180
|
+
-C "$tmp/clone" checkout --quiet "$ref" -- \
|
|
148
181
|
|| { rm -rf "$tmp"; die "ref '$ref' not found in $url"; }
|
|
149
182
|
fi
|
|
150
183
|
sha="$(git -C "$tmp/clone" rev-parse HEAD)"
|
|
184
|
+
if [ -n "$locked_sha" ] && [ "$sha" != "$locked_sha" ]; then
|
|
185
|
+
rm -rf "$tmp"
|
|
186
|
+
die "fetched $sha but the lock pins $locked_sha — refusing package replacement"
|
|
187
|
+
fi
|
|
151
188
|
src="$tmp/clone"
|
|
152
189
|
if [ -n "$subpath" ]; then
|
|
153
|
-
case "$subpath" in
|
|
154
|
-
*..*|/*|*[\"\']*) rm -rf "$tmp"; die "unsafe path '$subpath'" ;;
|
|
155
|
-
esac
|
|
156
190
|
src="$tmp/clone/$subpath"
|
|
157
191
|
[ -d "$src" ] || { rm -rf "$tmp"; die "path '$subpath' not found in $url${ref:+@$ref}" ; }
|
|
158
192
|
fi
|
package/engine/ENGINE_SHA
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
|
|
1
|
+
fe187a9bee5923603cec51798bd1ea5c2860593e
|
package/engine/VERSION
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
0.11.
|
|
1
|
+
0.11.9
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@ainova-systems/intelligence",
|
|
3
|
-
"version": "0.11.
|
|
3
|
+
"version": "0.11.9",
|
|
4
4
|
"description": "Build, version and distribute AI agent intelligence across your organization — one CLI, versioned Intelligence Packages, and a sync engine for Claude Code, Cursor, Copilot, Codex, Pi and OpenCode.",
|
|
5
5
|
"bin": {
|
|
6
6
|
"intelligence": "bin/intelligence.js"
|
|
@@ -11,12 +11,12 @@ context: fork
|
|
|
11
11
|
was requested). For Intelligence projects, this command first aligns project schema/content
|
|
12
12
|
with the installed CLI and restores a missing package store strictly from
|
|
13
13
|
`intelligence.lock`.
|
|
14
|
-
2. Require final `IS_STATUS=ok`; relay per-adapter counts and any
|
|
15
|
-
or unsynced-source warnings.
|
|
14
|
+
2. Require final `IS_STATUS=ok`; relay per-adapter counts and any integrity,
|
|
15
|
+
model-drift or unsynced-source warnings.
|
|
16
16
|
3. In CI, a required tracked project upgrade is intentionally refused. Report
|
|
17
17
|
the instruction to run `intelligence init --apply` locally, review and
|
|
18
18
|
commit its diff; never bypass the gate. A frozen restore refusal means the
|
|
19
|
-
manifest and lock disagree or
|
|
19
|
+
manifest and lock disagree or the locked commit cannot be verified/restored—report it without
|
|
20
20
|
hand-copying package content.
|
|
21
21
|
4. On a probable Intelligence defect, first ask whether to collect a sanitized
|
|
22
22
|
issue draft; collect nothing before approval and never use telemetry. Include
|