@ainova-systems/intelligence 0.11.0-rc.1
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/LICENSE +21 -0
- package/README.md +46 -0
- package/bin/intelligence.js +59 -0
- package/cli/commands/add.sh +133 -0
- package/cli/commands/doctor.sh +100 -0
- package/cli/commands/init.sh +96 -0
- package/cli/commands/install.sh +84 -0
- package/cli/commands/list.sh +28 -0
- package/cli/commands/migrate.sh +251 -0
- package/cli/commands/registry.sh +51 -0
- package/cli/commands/remove.sh +39 -0
- package/cli/commands/status.sh +34 -0
- package/cli/commands/sync.sh +22 -0
- package/cli/commands/update.sh +59 -0
- package/cli/commands/upgrade.sh +27 -0
- package/cli/intelligence +71 -0
- package/cli/lib/cli-common.sh +149 -0
- package/cli/lib/lockfile.sh +97 -0
- package/cli/lib/manifest.sh +211 -0
- package/cli/lib/registry.sh +141 -0
- package/cli/lib/semver.sh +127 -0
- package/engine/INIT.md +498 -0
- package/engine/agents/intelligence-architect.md +53 -0
- package/engine/agents/intelligence-operator.md +49 -0
- package/engine/docs/ADAPTERS.md +212 -0
- package/engine/docs/CLI.md +91 -0
- package/engine/docs/CONVENTIONS.md +440 -0
- package/engine/rules/intelligence-authoring.md +114 -0
- package/engine/scripts/VERSION +1 -0
- package/engine/scripts/adapters/_template.sh +86 -0
- package/engine/scripts/adapters/agents.sh +299 -0
- package/engine/scripts/adapters/claude.sh +136 -0
- package/engine/scripts/adapters/codex.sh +118 -0
- package/engine/scripts/adapters/copilot.sh +193 -0
- package/engine/scripts/adapters/cursor.sh +146 -0
- package/engine/scripts/adapters/opencode.sh +200 -0
- package/engine/scripts/adapters/pi.sh +256 -0
- package/engine/scripts/lib/common.sh +1602 -0
- package/engine/scripts/lib/layout.sh +51 -0
- package/engine/scripts/lib/migrations.sh +708 -0
- package/engine/scripts/sync.sh +311 -0
- package/engine/scripts/update.sh +237 -0
- package/engine/skills/intelligence-add-agent/SKILL.md +62 -0
- package/engine/skills/intelligence-add-rule/SKILL.md +54 -0
- package/engine/skills/intelligence-add-skill/SKILL.md +53 -0
- package/engine/skills/intelligence-extract-skill/SKILL.md +47 -0
- package/engine/skills/intelligence-install-adapter/SKILL.md +31 -0
- package/engine/skills/intelligence-learn-from-context/SKILL.md +69 -0
- package/engine/skills/intelligence-review-skills/SKILL.md +86 -0
- package/engine/skills/intelligence-sync/SKILL.md +18 -0
- package/engine/skills/intelligence-uninstall-adapter/SKILL.md +42 -0
- package/engine/skills/intelligence-update/SKILL.md +159 -0
- package/package.json +39 -0
- package/registry/index.yaml +15 -0
|
@@ -0,0 +1,708 @@
|
|
|
1
|
+
#!/bin/bash
|
|
2
|
+
# shellcheck disable=SC2034 # IS_RC_*/IS_MIGRATED/IS_VERSION_KEY are the public bash↔skill contract, consumed by the scripts that source this lib
|
|
3
|
+
# intelligence-sync: versioned migrations — breaking-change update architecture
|
|
4
|
+
# Source this file — never execute directly. Requires layout.sh already
|
|
5
|
+
# sourced (uses no globals from it directly; callers pass paths explicitly).
|
|
6
|
+
#
|
|
7
|
+
# THE MODEL (how breaking changes are shipped & absorbed):
|
|
8
|
+
# * The project carries a version stamp (.intelligence-sync-version); the
|
|
9
|
+
# engine carries scripts/VERSION. The gap stamped → engine IS the set of
|
|
10
|
+
# breaking changes to apply.
|
|
11
|
+
# * Each breaking structural change ships as ONE registered migrate_to_<ver>
|
|
12
|
+
# in MIGRATIONS (ordered, ascending). The dispatcher applies only the
|
|
13
|
+
# pending ones (target version > stamped), in order, so a project several
|
|
14
|
+
# versions behind is walked forward step by step, stamping after each.
|
|
15
|
+
# * Every migrate_to_<ver> obeys the contract: precondition → stage → verify
|
|
16
|
+
# postcondition (sentinel) → commit → cleanup; idempotent; fail-closed
|
|
17
|
+
# (never destroy prior state before the replacement is verified).
|
|
18
|
+
# * A stale engine refuses a project stamped newer than it knows
|
|
19
|
+
# (ahead-of-engine). sync.sh refuses to sync across an un-applied gap
|
|
20
|
+
# (needs-update). The intelligence-update SKILL is the brain: it reads the
|
|
21
|
+
# CHANGELOG across the gap, surfaces breaking items, runs this chain, and
|
|
22
|
+
# verifies after.
|
|
23
|
+
#
|
|
24
|
+
# Naming carries the target version (bash forbids dots → underscores):
|
|
25
|
+
# migrate_to_0_3_1 flat <umbrella>/scripts → modular <umbrella>/sync/
|
|
26
|
+
# Future breaking changes: append a new suffix to MIGRATIONS and add the
|
|
27
|
+
# matching migrate_to_<ver> — nothing here is rewritten or reordered.
|
|
28
|
+
|
|
29
|
+
# Ordered (ascending) list of migration target versions. Append only.
|
|
30
|
+
MIGRATIONS=( "0_3_1" "0_7_0" "0_10_0" )
|
|
31
|
+
|
|
32
|
+
# The applied-schema version is a managed key in config.yaml — NOT a dotfile,
|
|
33
|
+
# NOT scripts/VERSION. config.yaml is what most future breaking changes will
|
|
34
|
+
# reshape, so the schema version lives with what it versions.
|
|
35
|
+
#
|
|
36
|
+
# INVARIANT: this key is a PERMANENT, format-stable, top-level scalar contract.
|
|
37
|
+
# Migrations may restructure anything else in config.yaml, but never the name,
|
|
38
|
+
# location, or shape of this key — so any engine (however old/new) can always
|
|
39
|
+
# read "what schema is this?" before parsing the rest. The bootstrap/INIT flow
|
|
40
|
+
# must emit it for fresh projects and preserve it on re-bootstrap.
|
|
41
|
+
IS_VERSION_KEY="sync_version"
|
|
42
|
+
|
|
43
|
+
# "0_3_1" → "0.3.1"
|
|
44
|
+
_mig_ver_dotted() { printf '%s' "$1" | tr '_' '.'; }
|
|
45
|
+
|
|
46
|
+
# read_engine_stamp <config_file> → applied version, or "" if absent.
|
|
47
|
+
read_engine_stamp() {
|
|
48
|
+
local cf="$1"
|
|
49
|
+
[ -f "$cf" ] || return 0
|
|
50
|
+
awk -v k="$IS_VERSION_KEY" '
|
|
51
|
+
{ sub(/\r$/, "") }
|
|
52
|
+
$0 ~ "^" k ":" {
|
|
53
|
+
v = $0; sub(/^[^:]*:[[:space:]]*/, "", v)
|
|
54
|
+
gsub(/^["\047]|["\047][[:space:]]*$/, "", v)
|
|
55
|
+
sub(/[[:space:]]+$/, "", v)
|
|
56
|
+
print v; exit
|
|
57
|
+
}
|
|
58
|
+
' "$cf"
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
# stamp_version <config_file> <version> — idempotent, transactional upsert of
|
|
62
|
+
# the contract key (replace in place if present, else append at top level).
|
|
63
|
+
# No-op if config.yaml does not exist yet (pre-bootstrap).
|
|
64
|
+
stamp_version() {
|
|
65
|
+
local cf="$1" ver="$2"
|
|
66
|
+
[ -f "$cf" ] || return 0
|
|
67
|
+
local tmp="$cf.ver.tmp"
|
|
68
|
+
awk -v k="$IS_VERSION_KEY" -v val="$ver" '
|
|
69
|
+
{ sub(/\r$/, "") }
|
|
70
|
+
$0 ~ "^" k ":" { print k ": \"" val "\""; found=1; next }
|
|
71
|
+
{ print }
|
|
72
|
+
END { if (!found) print k ": \"" val "\"" }
|
|
73
|
+
' "$cf" > "$tmp" && mv "$tmp" "$cf"
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
# --- bash ↔ skill status contract -------------------------------------------
|
|
77
|
+
# Bash is the deterministic, fail-closed core: it never guesses. Any state it
|
|
78
|
+
# cannot resolve safely is reported as a machine-readable status line on
|
|
79
|
+
# stdout plus a stable exit code, and the intelligence-update SKILL (the
|
|
80
|
+
# intelligent layer) decides what to do. Codes are part of the public
|
|
81
|
+
# contract — do not renumber.
|
|
82
|
+
IS_RC_OK=0 # success (synced / migrated / nothing to do)
|
|
83
|
+
IS_RC_ERROR=1 # generic error
|
|
84
|
+
IS_RC_CONFIG_MISSING=2 # no config.yaml found
|
|
85
|
+
IS_RC_AMBIGUOUS=3 # conflicting state; skill/human-only — bash never emits this itself, it is reserved for the intelligence-update skill to report
|
|
86
|
+
IS_RC_AHEAD=4 # project stamped newer than this engine understands
|
|
87
|
+
IS_RC_ABORTED_INCOMPLETE=5 # staged module incomplete; legacy left intact
|
|
88
|
+
IS_RC_NEEDS_UPDATE=6 # pending breaking changes (stamp < engine) — run the update flow first
|
|
89
|
+
|
|
90
|
+
# is_status <code-name> [detail] — emit one parseable line for the skill.
|
|
91
|
+
is_status() {
|
|
92
|
+
local code="$1" detail="${2:-}"
|
|
93
|
+
if [ -n "$detail" ]; then
|
|
94
|
+
echo "IS_STATUS=$code IS_DETAIL=$detail"
|
|
95
|
+
else
|
|
96
|
+
echo "IS_STATUS=$code"
|
|
97
|
+
fi
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
# Engine version = scripts/VERSION next to this lib (BASH_SOURCE works when
|
|
101
|
+
# sourced). Empty if unreadable — callers treat empty as "no guard".
|
|
102
|
+
engine_version() {
|
|
103
|
+
local vf
|
|
104
|
+
vf="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." 2>/dev/null && pwd)/VERSION"
|
|
105
|
+
[ -f "$vf" ] && tr -d ' \t\r\n' < "$vf"
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
# _ver_gt A B → true if semver A is strictly greater than B (numeric x.y.z;
|
|
109
|
+
# any non-numeric suffix on a field is ignored). Missing fields = 0.
|
|
110
|
+
# Pre-release/build metadata ordering is intentionally NOT handled — the
|
|
111
|
+
# stamp only ever stores plain x.y.z, so this is sufficient.
|
|
112
|
+
_ver_gt() {
|
|
113
|
+
local a="$1" b="$2" i ai bi
|
|
114
|
+
local -a A B
|
|
115
|
+
IFS=. read -r -a A <<< "$a"
|
|
116
|
+
IFS=. read -r -a B <<< "$b"
|
|
117
|
+
for i in 0 1 2; do
|
|
118
|
+
ai=$(printf '%s' "${A[$i]:-0}" | tr -cd '0-9'); ai=${ai:-0}
|
|
119
|
+
bi=$(printf '%s' "${B[$i]:-0}" | tr -cd '0-9'); bi=${bi:-0}
|
|
120
|
+
if [ "$((10#$ai))" -gt "$((10#$bi))" ]; then return 0; fi
|
|
121
|
+
if [ "$((10#$ai))" -lt "$((10#$bi))" ]; then return 1; fi
|
|
122
|
+
done
|
|
123
|
+
return 1
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
# check_version_compat <config_file> — refuse to operate on a project whose
|
|
127
|
+
# config schema is stamped newer than this engine knows (a stale engine must
|
|
128
|
+
# never rewrite/sync a newer schema). Emits status + returns IS_RC_AHEAD on
|
|
129
|
+
# conflict, else 0.
|
|
130
|
+
check_version_compat() {
|
|
131
|
+
local cf="$1" stamp eng
|
|
132
|
+
stamp="$(read_engine_stamp "$cf")"
|
|
133
|
+
[ -n "$stamp" ] || return 0
|
|
134
|
+
eng="$(engine_version)"
|
|
135
|
+
[ -n "$eng" ] || return 0
|
|
136
|
+
if _ver_gt "$stamp" "$eng"; then
|
|
137
|
+
is_status ahead-of-engine "stamp=$stamp engine=$eng"
|
|
138
|
+
echo " ERROR: project stamped $stamp but this engine is $eng — refusing." >&2
|
|
139
|
+
echo " Update the engine first (the intelligence-update skill handles this)." >&2
|
|
140
|
+
return "$IS_RC_AHEAD"
|
|
141
|
+
fi
|
|
142
|
+
return 0
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
# Set by migrate_to_* when it actually performed work this run, so the caller
|
|
146
|
+
# can report `migrated` vs `ok`.
|
|
147
|
+
IS_MIGRATED=0
|
|
148
|
+
|
|
149
|
+
# Idempotent directory replace (rsync if present, else rm+cp). Returns non-zero
|
|
150
|
+
# if any step fails: a migration runs in an `||` context, so `set -e` is off and
|
|
151
|
+
# an unchecked copy failure (permissions, full disk) would otherwise sail on to
|
|
152
|
+
# the cleanup and delete the legacy files it never actually copied.
|
|
153
|
+
_mig_copy_dir() {
|
|
154
|
+
local src="$1" dst="$2"
|
|
155
|
+
[ -d "$src" ] || return 0
|
|
156
|
+
if command -v rsync >/dev/null 2>&1; then
|
|
157
|
+
mkdir -p "$dst" || return 1
|
|
158
|
+
rsync -a --delete "$src/" "$dst/" || return 1
|
|
159
|
+
else
|
|
160
|
+
rm -rf "$dst" || return 1
|
|
161
|
+
mkdir -p "$(dirname "$dst")" || return 1
|
|
162
|
+
cp -r "$src" "$dst" || return 1
|
|
163
|
+
fi
|
|
164
|
+
return 0
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
_mig_copy_file() {
|
|
168
|
+
local src="$1" dst="$2"
|
|
169
|
+
[ -f "$src" ] || return 0
|
|
170
|
+
mkdir -p "$(dirname "$dst")" || return 1
|
|
171
|
+
cp "$src" "$dst" || return 1
|
|
172
|
+
return 0
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
# Repo-root-relative path of the umbrella — the prefix every `sources:` entry is
|
|
176
|
+
# resolved against (resolve_source_dir does "$repo_root/$entry"). `basename` is
|
|
177
|
+
# only correct when the umbrella sits directly at the repo root; a nested one
|
|
178
|
+
# (`platform/intelligence/`) would yield `intelligence/...` and register a source
|
|
179
|
+
# that resolves nowhere. Ask git for the real prefix, and fall back to basename
|
|
180
|
+
# when there is no git (a tarball checkout), which is the flat case anyway.
|
|
181
|
+
_mig_umbrella_rel() {
|
|
182
|
+
local umbrella="$1" prefix=""
|
|
183
|
+
if command -v git >/dev/null 2>&1; then
|
|
184
|
+
prefix="$(git -C "$umbrella" rev-parse --show-prefix 2>/dev/null || true)"
|
|
185
|
+
fi
|
|
186
|
+
prefix="${prefix%/}"
|
|
187
|
+
if [ -n "$prefix" ]; then
|
|
188
|
+
printf '%s' "$prefix"
|
|
189
|
+
else
|
|
190
|
+
printf '%s' "$(basename "$umbrella")"
|
|
191
|
+
fi
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
# True (0) if <entry> is already listed anywhere in config.yaml (quoted or bare).
|
|
195
|
+
_mig_has_source() {
|
|
196
|
+
local config="$1" entry="$2"
|
|
197
|
+
[ -f "$config" ] || return 1
|
|
198
|
+
grep -Fq -- "\"$entry\"" "$config" || grep -Fq -- "- $entry" "$config"
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
# Idempotently add one entry to `sources.<section>` in config.yaml. No backup —
|
|
202
|
+
# the edit is a single additive list item. Name-agnostic: the caller passes the
|
|
203
|
+
# already-resolved "<base>/<module>/<dir>". If the section does not exist under
|
|
204
|
+
# `sources:` yet, it is created with the entry as its only item.
|
|
205
|
+
# Usage: _mig_add_source <config.yaml> <rules|agents|skills> <entry>
|
|
206
|
+
_mig_add_source() {
|
|
207
|
+
local config="$1" section="$2" entry="$3"
|
|
208
|
+
if [ ! -f "$config" ]; then
|
|
209
|
+
echo " [migrate] no config.yaml at $config — add this under sources.$section manually:" >&2
|
|
210
|
+
echo " - \"$entry\"" >&2
|
|
211
|
+
return 0
|
|
212
|
+
fi
|
|
213
|
+
_mig_has_source "$config" "$entry" && return 0
|
|
214
|
+
|
|
215
|
+
local tmp="$config.mig.tmp"
|
|
216
|
+
awk -v section="$section" -v entry="$entry" '
|
|
217
|
+
function emit() { print " - \"" entry "\""; inserted = 1 }
|
|
218
|
+
function close_here() {
|
|
219
|
+
if (in_sec && !inserted) { emit(); in_sec = 0 }
|
|
220
|
+
}
|
|
221
|
+
{ sub(/\r$/, "") }
|
|
222
|
+
# Top-level key: ends the sources block (and any open section in it).
|
|
223
|
+
/^[A-Za-z]/ {
|
|
224
|
+
close_here()
|
|
225
|
+
# `sources:` existed but never declared this section — declare it.
|
|
226
|
+
if (in_src && !inserted) { print " " section ":"; emit() }
|
|
227
|
+
in_sec = 0
|
|
228
|
+
in_src = ($0 ~ /^sources:[[:space:]]*$/) ? 1 : 0
|
|
229
|
+
print; next
|
|
230
|
+
}
|
|
231
|
+
in_src && $0 ~ "^ " section ":[[:space:]]*$" { print; in_sec = 1; next }
|
|
232
|
+
# Another 2-space sub-key ends this section.
|
|
233
|
+
in_src && /^ [A-Za-z]/ { close_here(); print; next }
|
|
234
|
+
in_sec && /^ -[[:space:]]/ { print; next } # existing item
|
|
235
|
+
in_sec && /^[[:space:]]*$/ { close_here(); print; next }
|
|
236
|
+
{ print }
|
|
237
|
+
END {
|
|
238
|
+
close_here()
|
|
239
|
+
if (in_src && !inserted) { print " " section ":"; emit() }
|
|
240
|
+
}
|
|
241
|
+
' "$config" > "$tmp" && mv "$tmp" "$config"
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
# Back-compat shim: 0.3.1 shipped with this name, and a shipped migration is
|
|
245
|
+
# never rewritten.
|
|
246
|
+
_mig_add_skill_source() {
|
|
247
|
+
_mig_add_source "$1" "skills" "$2"
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
# --- migrate_to_0_3_1 -------------------------------------------------------
|
|
251
|
+
# Pre-0.3.1: engine + meta-skills + INIT.md + docs lived flat under the
|
|
252
|
+
# umbrella, mixed with project content. 0.3.1: they move into the self-
|
|
253
|
+
# contained module subfolder <umbrella>/<module>/. Project content
|
|
254
|
+
# (rules/, agents/, non-meta skills/, config.yaml) is never moved or deleted.
|
|
255
|
+
#
|
|
256
|
+
# Ownership of meta-skills is by the reserved `intelligence-` prefix. A
|
|
257
|
+
# project skill must not use that prefix (documented in CONVENTIONS.md).
|
|
258
|
+
#
|
|
259
|
+
# migrate_to_0_3_1 <umbrella_dir> <module_name> [<upstream_module_dir>]
|
|
260
|
+
# upstream_module_dir set → authoritative content source (update.sh)
|
|
261
|
+
# upstream_module_dir empty → relocate local legacy files (sync.sh, offline)
|
|
262
|
+
migrate_to_0_3_1() {
|
|
263
|
+
local umbrella="$1" module_name="$2" upstream="${3:-}"
|
|
264
|
+
local module_dir="$umbrella/$module_name"
|
|
265
|
+
local s
|
|
266
|
+
|
|
267
|
+
# Precondition: any legacy upstream-owned artifact directly under the
|
|
268
|
+
# umbrella. None ⇒ already modular or fresh ⇒ idempotent no-op.
|
|
269
|
+
#
|
|
270
|
+
# The flat engine is identified by its entry script `scripts/sync.sh`, never
|
|
271
|
+
# by the bare `scripts/` directory: a project may legitimately own
|
|
272
|
+
# <umbrella>/scripts/ for its own tooling, and mistaking it for the engine
|
|
273
|
+
# would relocate then delete the project's code (the postcondition below
|
|
274
|
+
# already treats scripts/sync.sh as the engine sentinel).
|
|
275
|
+
local has_legacy=0
|
|
276
|
+
local legacy_engine=0
|
|
277
|
+
[ -f "$umbrella/scripts/sync.sh" ] && legacy_engine=1
|
|
278
|
+
[ "$legacy_engine" -eq 1 ] && has_legacy=1
|
|
279
|
+
[ -e "$umbrella/INIT.md" ] && has_legacy=1
|
|
280
|
+
[ -d "$umbrella/docs" ] && has_legacy=1
|
|
281
|
+
for s in "$umbrella"/skills/intelligence-*; do
|
|
282
|
+
[ -e "$s" ] && { has_legacy=1; break; }
|
|
283
|
+
done
|
|
284
|
+
# Already modular / fresh ⇒ nothing to relocate. Stamping is owned by the
|
|
285
|
+
# dispatcher (run_migrations), not here.
|
|
286
|
+
if [ "$has_legacy" -eq 0 ]; then
|
|
287
|
+
return 0
|
|
288
|
+
fi
|
|
289
|
+
|
|
290
|
+
echo " [migrate 0.3.1] legacy flat layout detected — relocating engine into '$module_name/'"
|
|
291
|
+
IS_MIGRATED=1
|
|
292
|
+
mkdir -p "$module_dir"
|
|
293
|
+
|
|
294
|
+
# Authoritative content source: the fresh upstream clone when update.sh
|
|
295
|
+
# supplies one, else the project's own legacy files (sync.sh, offline).
|
|
296
|
+
local src_root="$umbrella"
|
|
297
|
+
if [ -n "$upstream" ] && [ -d "$upstream" ]; then
|
|
298
|
+
src_root="$upstream"
|
|
299
|
+
fi
|
|
300
|
+
|
|
301
|
+
# Stage. Every copy's exit status is captured — see _mig_copy_dir.
|
|
302
|
+
local copy_rc=0
|
|
303
|
+
_mig_copy_dir "$src_root/scripts" "$module_dir/scripts" || copy_rc=1
|
|
304
|
+
_mig_copy_file "$src_root/INIT.md" "$module_dir/INIT.md" || copy_rc=1
|
|
305
|
+
_mig_copy_dir "$src_root/docs" "$module_dir/docs" || copy_rc=1
|
|
306
|
+
mkdir -p "$module_dir/skills" || copy_rc=1
|
|
307
|
+
for s in "$src_root"/skills/intelligence-*; do
|
|
308
|
+
[ -d "$s" ] || continue
|
|
309
|
+
_mig_copy_dir "$s" "$module_dir/skills/$(basename "$s")" || copy_rc=1
|
|
310
|
+
done
|
|
311
|
+
|
|
312
|
+
# Verify the FULL postcondition BEFORE any destructive cleanup: every
|
|
313
|
+
# artifact present at the source must now exist in the module, non-empty.
|
|
314
|
+
# This is the crash-safety gate — a half-populated module must never
|
|
315
|
+
# trigger legacy deletion, so two script sentinels are not enough.
|
|
316
|
+
local missing=""
|
|
317
|
+
[ "$copy_rc" -eq 0 ] || missing="$missing copy-failed"
|
|
318
|
+
[ -s "$module_dir/scripts/sync.sh" ] || missing="$missing scripts/sync.sh"
|
|
319
|
+
[ -s "$module_dir/scripts/lib/common.sh" ] || missing="$missing scripts/lib/common.sh"
|
|
320
|
+
if [ -f "$src_root/INIT.md" ] && [ ! -s "$module_dir/INIT.md" ]; then
|
|
321
|
+
missing="$missing INIT.md"
|
|
322
|
+
fi
|
|
323
|
+
if [ -d "$src_root/docs" ] && [ -z "$(find "$module_dir/docs" -type f 2>/dev/null | head -1)" ]; then
|
|
324
|
+
missing="$missing docs/"
|
|
325
|
+
fi
|
|
326
|
+
for s in "$src_root"/skills/intelligence-*; do
|
|
327
|
+
[ -d "$s" ] || continue
|
|
328
|
+
if [ ! -s "$module_dir/skills/$(basename "$s")/SKILL.md" ]; then
|
|
329
|
+
missing="$missing skills/$(basename "$s")"
|
|
330
|
+
fi
|
|
331
|
+
done
|
|
332
|
+
|
|
333
|
+
if [ -n "$missing" ]; then
|
|
334
|
+
is_status aborted-incomplete "module=$module_name missing=${missing# }"
|
|
335
|
+
echo " ERROR: migration aborted — '$module_name/' incomplete (${missing# }); legacy left intact." >&2
|
|
336
|
+
return "$IS_RC_ABORTED_INCOMPLETE"
|
|
337
|
+
fi
|
|
338
|
+
|
|
339
|
+
# Remove ONLY the legacy upstream-owned locations. Meta-skills / INIT /
|
|
340
|
+
# docs are never the running process, so these always succeed → no
|
|
341
|
+
# duplicate intelligence-* ever survives under <umbrella>/skills/.
|
|
342
|
+
rm -f "$umbrella/INIT.md"
|
|
343
|
+
rm -rf "$umbrella/docs"
|
|
344
|
+
for s in "$umbrella"/skills/intelligence-*; do
|
|
345
|
+
[ -e "$s" ] && rm -rf "$s"
|
|
346
|
+
done
|
|
347
|
+
# The legacy scripts/ dir may host the *currently running* update.sh.
|
|
348
|
+
# On Linux deleting an open script is fine; some Windows shells refuse
|
|
349
|
+
# it. Try, and if it lingers print a one-line manual cleanup instead of
|
|
350
|
+
# failing — the new location, config, and stamp are already correct, and
|
|
351
|
+
# the next sync/update run removes the dead dir. Guarded by the engine
|
|
352
|
+
# sentinel so a project-owned <umbrella>/scripts/ (no sync.sh) is never
|
|
353
|
+
# removed, even if another legacy signal set has_legacy.
|
|
354
|
+
if [ "$legacy_engine" -eq 1 ]; then
|
|
355
|
+
rm -rf "$umbrella/scripts" 2>/dev/null || true
|
|
356
|
+
if [ -d "$umbrella/scripts" ]; then
|
|
357
|
+
echo " NOTE: legacy '$umbrella/scripts' still present (likely in use)." >&2
|
|
358
|
+
echo " Remove it manually once this process exits:" >&2
|
|
359
|
+
echo " rm -rf \"$umbrella/scripts\"" >&2
|
|
360
|
+
fi
|
|
361
|
+
fi
|
|
362
|
+
|
|
363
|
+
# config.yaml: name-agnostic relative path under the actual umbrella base.
|
|
364
|
+
_mig_add_skill_source "$umbrella/config.yaml" "$(_mig_umbrella_rel "$umbrella")/$module_name/skills"
|
|
365
|
+
|
|
366
|
+
echo " [migrate 0.3.1] done — engine at '$module_name/', legacy removed, no duplicates"
|
|
367
|
+
}
|
|
368
|
+
|
|
369
|
+
# --- migrate_to_0_7_0 -------------------------------------------------------
|
|
370
|
+
# 0.7.0 is the first release where the engine ships a RULE and an AGENT of its
|
|
371
|
+
# own (`intelligence-authoring`, `intelligence-architect`), inside the module
|
|
372
|
+
# beside the meta-skills. Those reach the IDEs only if `config.yaml` lists the
|
|
373
|
+
# module's `rules/` and `agents/` directories as sources — so this migration
|
|
374
|
+
# registers them, exactly as 0.3.1 registered the module's `skills/`.
|
|
375
|
+
#
|
|
376
|
+
# Precondition is structural: both entries already present ⇒ applied ⇒ silent
|
|
377
|
+
# no-op. Nothing else in config.yaml is read or rewritten, and no file is
|
|
378
|
+
# deleted, so there is nothing to stage — the postcondition is simply that both
|
|
379
|
+
# entries are readable afterwards.
|
|
380
|
+
#
|
|
381
|
+
# migrate_to_0_7_0 <umbrella_dir> <module_name> [<upstream_module_dir> — unused]
|
|
382
|
+
migrate_to_0_7_0() {
|
|
383
|
+
local umbrella="$1" module_name="$2"
|
|
384
|
+
local config="$umbrella/config.yaml"
|
|
385
|
+
[ -f "$config" ] || return 0
|
|
386
|
+
|
|
387
|
+
local base rules_entry agents_entry
|
|
388
|
+
base="$(_mig_umbrella_rel "$umbrella")"
|
|
389
|
+
rules_entry="$base/$module_name/rules"
|
|
390
|
+
agents_entry="$base/$module_name/agents"
|
|
391
|
+
|
|
392
|
+
if _mig_has_source "$config" "$rules_entry" && _mig_has_source "$config" "$agents_entry"; then
|
|
393
|
+
return 0
|
|
394
|
+
fi
|
|
395
|
+
|
|
396
|
+
echo " [migrate 0.7.0] registering the module's rules/ and agents/ as sources"
|
|
397
|
+
IS_MIGRATED=1
|
|
398
|
+
_mig_add_source "$config" "rules" "$rules_entry"
|
|
399
|
+
_mig_add_source "$config" "agents" "$agents_entry"
|
|
400
|
+
|
|
401
|
+
if ! _mig_has_source "$config" "$rules_entry" || ! _mig_has_source "$config" "$agents_entry"; then
|
|
402
|
+
is_status error "migrate_to_0_7_0 could not register module sources"
|
|
403
|
+
echo " ERROR: failed to add the module's rules/agents to sources in $config." >&2
|
|
404
|
+
echo " Add them by hand under 'sources:':" >&2
|
|
405
|
+
echo " rules: - \"$rules_entry\"" >&2
|
|
406
|
+
echo " agents: - \"$agents_entry\"" >&2
|
|
407
|
+
return "$IS_RC_ERROR"
|
|
408
|
+
fi
|
|
409
|
+
|
|
410
|
+
echo " [migrate 0.7.0] done — sources.rules += $rules_entry, sources.agents += $agents_entry"
|
|
411
|
+
}
|
|
412
|
+
|
|
413
|
+
# --- migrate_to_0_10_0 ------------------------------------------------------
|
|
414
|
+
|
|
415
|
+
# Split `git+<url>[@<ref>][#<subpath>]` into _MIG_URL / _MIG_REF / _MIG_SUBPATH.
|
|
416
|
+
# Same grammar resolve_source_dir parses; kept here so a migration never has to
|
|
417
|
+
# source the engine library it is migrating towards.
|
|
418
|
+
#
|
|
419
|
+
# Globals, never a delimited string on stdout: two of the three fields are
|
|
420
|
+
# optional, and `read` with a whitespace IFS collapses a run of delimiters into
|
|
421
|
+
# one — so an unpinned `git+<url>#rules` would come back as ref=rules with no
|
|
422
|
+
# subpath, silently rewriting a whole source into a branch that does not exist.
|
|
423
|
+
_mig_split_git_token() {
|
|
424
|
+
local rest="${1#git+}" urlref after cand
|
|
425
|
+
_MIG_SUBPATH=""; _MIG_REF=""
|
|
426
|
+
case "$rest" in
|
|
427
|
+
*\#*) _MIG_SUBPATH="${rest#*#}"; urlref="${rest%%#*}" ;;
|
|
428
|
+
*) urlref="$rest" ;;
|
|
429
|
+
esac
|
|
430
|
+
_MIG_URL="$urlref"
|
|
431
|
+
after="${urlref#*://}"
|
|
432
|
+
case "$after" in
|
|
433
|
+
*@*)
|
|
434
|
+
cand="${after##*@}"
|
|
435
|
+
case "$cand" in
|
|
436
|
+
*/*|"") ;;
|
|
437
|
+
*) _MIG_REF="$cand"; _MIG_URL="${urlref%@$cand}" ;;
|
|
438
|
+
esac
|
|
439
|
+
;;
|
|
440
|
+
esac
|
|
441
|
+
}
|
|
442
|
+
|
|
443
|
+
# Pack name from a repo URL: basename minus `.git`, restricted to a safe
|
|
444
|
+
# filename charset. This is the ONLY place a name is still derived, it runs
|
|
445
|
+
# once, and the result is written into config.yaml where a human can rename it.
|
|
446
|
+
_mig_pack_name_from_url() {
|
|
447
|
+
local name="${1%/}"
|
|
448
|
+
name="${name##*/}"
|
|
449
|
+
name="${name%.git}"
|
|
450
|
+
name="$(printf '%s' "$name" | tr -c 'A-Za-z0-9._-' '-')"
|
|
451
|
+
case "$name" in ""|.*|-*) name="pack" ;; esac
|
|
452
|
+
printf '%s' "$name"
|
|
453
|
+
}
|
|
454
|
+
|
|
455
|
+
# 0.10.0 replaces the single `external: { dir: … }` block with a `packs:` block:
|
|
456
|
+
# a remote source is DECLARED once (url + ref + optional mirror) and referenced
|
|
457
|
+
# from `sources.*` by name (`@<pack>/<subpath>`). That removes the duplicated
|
|
458
|
+
# `url@ref` an inline spec forced into every section, and makes the mirror
|
|
459
|
+
# directory declared rather than derived from the URL.
|
|
460
|
+
#
|
|
461
|
+
# This migration rewrites the config for the project: every inline `git+` spec
|
|
462
|
+
# becomes a declared pack plus an `@name` reference, and `external.dir` becomes
|
|
463
|
+
# each pack's `mirror:` so vendored content keeps landing where it already is.
|
|
464
|
+
# Inline `git+` specs remain legal afterwards — they are simply no longer the
|
|
465
|
+
# only way to reach a remote source, and they are always transient.
|
|
466
|
+
#
|
|
467
|
+
# Precondition is structural, and each key is a one-way marker: `external:` only
|
|
468
|
+
# ever existed BEFORE 0.10.0, `packs:` only ever exists after it. So an
|
|
469
|
+
# `external:` key ⇒ convert; otherwise a `packs:` key ⇒ applied ⇒ silent no-op;
|
|
470
|
+
# otherwise convert only if there is an inline spec to convert. That last clause
|
|
471
|
+
# is what keeps the migration idempotent once inline specs are legal again: the
|
|
472
|
+
# run that converts them writes `packs:`, and every later run stops at the marker
|
|
473
|
+
# instead of appending a SECOND top-level `packs:` key for each spec added since.
|
|
474
|
+
# Fail-closed: the rewrite is staged in a temp file and verified before it
|
|
475
|
+
# replaces config.yaml.
|
|
476
|
+
#
|
|
477
|
+
# migrate_to_0_10_0 <umbrella_dir> <module_name> [<upstream_module_dir> — unused]
|
|
478
|
+
migrate_to_0_10_0() {
|
|
479
|
+
local umbrella="$1"
|
|
480
|
+
local config="$umbrella/config.yaml"
|
|
481
|
+
[ -f "$config" ] || return 0
|
|
482
|
+
|
|
483
|
+
local has_external=0
|
|
484
|
+
grep -q '^external:[[:space:]]*$' "$config" && has_external=1
|
|
485
|
+
local has_packs=0
|
|
486
|
+
grep -q '^packs:[[:space:]]*$' "$config" && has_packs=1
|
|
487
|
+
local has_inline=0
|
|
488
|
+
# ERE, and anchored to a list entry: `\|` is a GNU BRE extension that BSD
|
|
489
|
+
# grep (the macOS default) reads as a literal, and matching `git+` anywhere
|
|
490
|
+
# would fire on the comment that documents the old spec format.
|
|
491
|
+
grep -qE '^[[:space:]]*-[[:space:]]*["'\'']?git\+' "$config" && has_inline=1
|
|
492
|
+
if [ "$has_external" -eq 0 ] && { [ "$has_packs" -eq 1 ] || [ "$has_inline" -eq 0 ]; }; then
|
|
493
|
+
return 0
|
|
494
|
+
fi
|
|
495
|
+
|
|
496
|
+
echo " [migrate 0.10.0] converting remote sources to declared packs"
|
|
497
|
+
IS_MIGRATED=1
|
|
498
|
+
|
|
499
|
+
# The old external dir, if any — it becomes each pack's mirror parent.
|
|
500
|
+
local ext_dir=""
|
|
501
|
+
if [ "$has_external" -eq 1 ]; then
|
|
502
|
+
ext_dir="$(awk '
|
|
503
|
+
{ sub(/\r$/, "") }
|
|
504
|
+
/^external:[[:space:]]*$/ { in_ext = 1; next }
|
|
505
|
+
/^[A-Za-z]/ { in_ext = 0 }
|
|
506
|
+
in_ext && /^ dir:/ {
|
|
507
|
+
v = $0
|
|
508
|
+
sub(/^[[:space:]]*[^:]*:[[:space:]]*/, "", v)
|
|
509
|
+
# Two subs, never one alternation: POSIX awk takes the LONGEST
|
|
510
|
+
# match at the leftmost position, so `^["]|["].*$` would match
|
|
511
|
+
# the whole quoted value at position 1 and erase it.
|
|
512
|
+
sub(/^["\047]/, "", v)
|
|
513
|
+
sub(/["\047][[:space:]]*$/, "", v)
|
|
514
|
+
sub(/[[:space:]]+$/, "", v)
|
|
515
|
+
print v; exit
|
|
516
|
+
}
|
|
517
|
+
' "$config")"
|
|
518
|
+
ext_dir="${ext_dir%/}"
|
|
519
|
+
fi
|
|
520
|
+
|
|
521
|
+
# Collect the distinct url@ref pairs across every section, in first-seen
|
|
522
|
+
# order, assigning each a unique name.
|
|
523
|
+
local names=() urls=() refs=() seen=()
|
|
524
|
+
local section token url ref subpath sig i found name base n
|
|
525
|
+
for section in rules agents skills; do
|
|
526
|
+
while IFS= read -r token; do
|
|
527
|
+
case "$token" in git+*) ;; *) continue ;; esac
|
|
528
|
+
_mig_split_git_token "$token"
|
|
529
|
+
url="$_MIG_URL"; ref="$_MIG_REF"
|
|
530
|
+
sig="$url@$ref"
|
|
531
|
+
# `${#arr[@]}` guards, never `"${!arr[@]}"` on a possibly-empty
|
|
532
|
+
# array: bash 3.2 (macOS default) treats that as unbound under
|
|
533
|
+
# `set -u`, which every script here runs with.
|
|
534
|
+
found=0
|
|
535
|
+
i=0
|
|
536
|
+
while [ "$i" -lt "${#seen[@]}" ]; do
|
|
537
|
+
[ "${seen[$i]}" = "$sig" ] && { found=1; break; }
|
|
538
|
+
i=$((i + 1))
|
|
539
|
+
done
|
|
540
|
+
[ "$found" -eq 1 ] && continue
|
|
541
|
+
base="$(_mig_pack_name_from_url "$url")"
|
|
542
|
+
name="$base"; n=2
|
|
543
|
+
while :; do
|
|
544
|
+
found=0
|
|
545
|
+
i=0
|
|
546
|
+
while [ "$i" -lt "${#names[@]}" ]; do
|
|
547
|
+
[ "${names[$i]}" = "$name" ] && { found=1; break; }
|
|
548
|
+
i=$((i + 1))
|
|
549
|
+
done
|
|
550
|
+
[ "$found" -eq 0 ] && break
|
|
551
|
+
name="$base-$n"; n=$((n + 1))
|
|
552
|
+
done
|
|
553
|
+
seen+=("$sig"); names+=("$name"); urls+=("$url"); refs+=("$ref")
|
|
554
|
+
done < <(_mig_read_sources "$config" "$section")
|
|
555
|
+
done
|
|
556
|
+
|
|
557
|
+
# Build the packs: block.
|
|
558
|
+
local packs_block="" mirror
|
|
559
|
+
i=0
|
|
560
|
+
while [ "$i" -lt "${#names[@]}" ]; do
|
|
561
|
+
packs_block="$packs_block ${names[$i]}:"$'\n'
|
|
562
|
+
packs_block="$packs_block url: ${urls[$i]}"$'\n'
|
|
563
|
+
[ -n "${refs[$i]}" ] && packs_block="$packs_block ref: ${refs[$i]}"$'\n'
|
|
564
|
+
if [ -n "$ext_dir" ]; then
|
|
565
|
+
mirror="$ext_dir/${names[$i]}"
|
|
566
|
+
packs_block="$packs_block mirror: \"$mirror\""$'\n'
|
|
567
|
+
fi
|
|
568
|
+
i=$((i + 1))
|
|
569
|
+
done
|
|
570
|
+
|
|
571
|
+
# Map every inline spec to its `@name[/subpath]` replacement.
|
|
572
|
+
local map=""
|
|
573
|
+
for section in rules agents skills; do
|
|
574
|
+
while IFS= read -r token; do
|
|
575
|
+
case "$token" in git+*) ;; *) continue ;; esac
|
|
576
|
+
_mig_split_git_token "$token"
|
|
577
|
+
url="$_MIG_URL"; ref="$_MIG_REF"; subpath="$_MIG_SUBPATH"
|
|
578
|
+
sig="$url@$ref"
|
|
579
|
+
i=0
|
|
580
|
+
while [ "$i" -lt "${#seen[@]}" ]; do
|
|
581
|
+
if [ "${seen[$i]}" = "$sig" ]; then
|
|
582
|
+
map="$map$token"$'\t'"@${names[$i]}${subpath:+/$subpath}"$'\n'
|
|
583
|
+
break
|
|
584
|
+
fi
|
|
585
|
+
i=$((i + 1))
|
|
586
|
+
done
|
|
587
|
+
done < <(_mig_read_sources "$config" "$section")
|
|
588
|
+
done
|
|
589
|
+
|
|
590
|
+
# An `external:` block with no remote source at all declares nothing — emit
|
|
591
|
+
# no `packs:` key rather than a dangling empty one, and drop `external:`.
|
|
592
|
+
local emit_packs=1
|
|
593
|
+
[ "${#names[@]}" -eq 0 ] && emit_packs=0
|
|
594
|
+
|
|
595
|
+
local tmp="$config.mig.tmp" mapfile_="$config.mig.map"
|
|
596
|
+
printf '%s' "$map" > "$mapfile_"
|
|
597
|
+
awk -v packs="$packs_block" -v mapfile="$mapfile_" -v emit="$emit_packs" '
|
|
598
|
+
BEGIN {
|
|
599
|
+
while ((getline line < mapfile) > 0) {
|
|
600
|
+
sub(/\r$/, "", line)
|
|
601
|
+
t = index(line, "\t")
|
|
602
|
+
if (t > 0) repl[substr(line, 1, t - 1)] = substr(line, t + 1)
|
|
603
|
+
}
|
|
604
|
+
close(mapfile)
|
|
605
|
+
}
|
|
606
|
+
{ sub(/\r$/, "") }
|
|
607
|
+
# Drop the whole external: block.
|
|
608
|
+
/^external:[[:space:]]*$/ { in_ext = 1; next }
|
|
609
|
+
in_ext && /^[[:space:]]/ { next }
|
|
610
|
+
in_ext { in_ext = 0 }
|
|
611
|
+
# Emit packs: immediately before sources:.
|
|
612
|
+
/^sources:[[:space:]]*$/ && emit == 1 && !done_packs {
|
|
613
|
+
printf "packs:\n%s\n", packs
|
|
614
|
+
done_packs = 1
|
|
615
|
+
}
|
|
616
|
+
# Rewrite an inline spec in place, preserving indentation and quoting.
|
|
617
|
+
/^[[:space:]]*-[[:space:]]*["\047]?git\+/ {
|
|
618
|
+
val = $0
|
|
619
|
+
sub(/^[[:space:]]*-[[:space:]]*/, "", val)
|
|
620
|
+
gsub(/^["\047]|["\047][[:space:]]*$/, "", val)
|
|
621
|
+
if (val in repl) {
|
|
622
|
+
indent = $0
|
|
623
|
+
sub(/-.*$/, "", indent)
|
|
624
|
+
print indent "- \"" repl[val] "\""
|
|
625
|
+
next
|
|
626
|
+
}
|
|
627
|
+
}
|
|
628
|
+
{ print }
|
|
629
|
+
END { if (emit == 1 && !done_packs) printf "packs:\n%s\n", packs }
|
|
630
|
+
' "$config" > "$tmp"
|
|
631
|
+
|
|
632
|
+
# Verify the staged file before it replaces anything: `external:` gone, and
|
|
633
|
+
# EXACTLY the expected number of top-level `packs:` keys — a second one
|
|
634
|
+
# would be duplicate-key YAML that strict parsers reject.
|
|
635
|
+
local packs_keys
|
|
636
|
+
packs_keys="$(grep -c '^packs:[[:space:]]*$' "$tmp" || true)"
|
|
637
|
+
if [ "$packs_keys" -ne "$emit_packs" ] || grep -q '^external:[[:space:]]*$' "$tmp"; then
|
|
638
|
+
rm -f "$tmp" "$mapfile_"
|
|
639
|
+
is_status error "migrate_to_0_10_0 could not rewrite config.yaml"
|
|
640
|
+
echo " ERROR: failed to convert remote sources to packs in $config." >&2
|
|
641
|
+
echo " Declare each remote under 'packs:' and reference it as '@<name>/<subpath>'." >&2
|
|
642
|
+
return "$IS_RC_ERROR"
|
|
643
|
+
fi
|
|
644
|
+
|
|
645
|
+
mv "$tmp" "$config"
|
|
646
|
+
rm -f "$mapfile_"
|
|
647
|
+
if [ "$emit_packs" -eq 0 ]; then
|
|
648
|
+
echo " [migrate 0.10.0] done — no remote source to declare, dropped the empty 'external:' block"
|
|
649
|
+
else
|
|
650
|
+
echo " [migrate 0.10.0] done — ${#names[@]} pack(s) declared${ext_dir:+, mirrored under $ext_dir}"
|
|
651
|
+
fi
|
|
652
|
+
}
|
|
653
|
+
|
|
654
|
+
# Read one `sources.<section>` list, one raw value per line (quotes stripped).
|
|
655
|
+
# Local to migrations so the chain never depends on the engine library.
|
|
656
|
+
_mig_read_sources() {
|
|
657
|
+
local config="$1" section="$2"
|
|
658
|
+
awk -v section="$section" '
|
|
659
|
+
{ sub(/\r$/, "") }
|
|
660
|
+
/^sources:[[:space:]]*$/ { in_src = 1; next }
|
|
661
|
+
/^[A-Za-z]/ { in_src = 0; in_sec = 0 }
|
|
662
|
+
in_src && $0 ~ "^ " section ":[[:space:]]*$" { in_sec = 1; next }
|
|
663
|
+
in_src && /^ [A-Za-z]/ { in_sec = 0 }
|
|
664
|
+
in_sec && /^[[:space:]]*-[[:space:]]*/ {
|
|
665
|
+
v = $0
|
|
666
|
+
sub(/^[[:space:]]*-[[:space:]]*/, "", v)
|
|
667
|
+
gsub(/^["\047]|["\047][[:space:]]*$/, "", v)
|
|
668
|
+
sub(/[[:space:]]+$/, "", v)
|
|
669
|
+
if (v != "") print v
|
|
670
|
+
}
|
|
671
|
+
' "$config"
|
|
672
|
+
}
|
|
673
|
+
|
|
674
|
+
# run_migrations <umbrella_dir> <module_name> [<upstream_module_dir>]
|
|
675
|
+
# The dispatcher of the breaking-change chain. Correctness rests on idempotent
|
|
676
|
+
# structural preconditions, NOT on the stamp: every migrate_to_* self-detects
|
|
677
|
+
# whether its change is already applied and is a silent no-op if so, so the
|
|
678
|
+
# whole chain is simply run in order — a wrong/missing stamp can never cause a
|
|
679
|
+
# needed migration to be skipped. The stamp is only a safety guard
|
|
680
|
+
# (ahead-of-engine / needs-update) + reporting. Returns 0 on success
|
|
681
|
+
# (IS_MIGRATED tells whether work happened) or the failing migration's IS_RC_*
|
|
682
|
+
# code; never partially destroys (each migrate_to_* is transactional and
|
|
683
|
+
# fail-closed). Caller maps the code to an exit status + IS_STATUS line.
|
|
684
|
+
run_migrations() {
|
|
685
|
+
local umbrella="$1" module_name="$2" upstream="${3:-}"
|
|
686
|
+
local cf="$umbrella/config.yaml"
|
|
687
|
+
local v ver rc
|
|
688
|
+
# A stale engine must never touch a project schema stamped newer than it
|
|
689
|
+
# knows. (Reads the frozen contract key from config.yaml.)
|
|
690
|
+
check_version_compat "$cf" || return $?
|
|
691
|
+
for v in "${MIGRATIONS[@]}"; do
|
|
692
|
+
ver="$(_mig_ver_dotted "$v")"
|
|
693
|
+
"migrate_to_$v" "$umbrella" "$module_name" "$upstream"
|
|
694
|
+
rc=$?
|
|
695
|
+
if [ "$rc" -ne 0 ]; then return "$rc"; fi
|
|
696
|
+
# Commit progress: stamp this version so an interrupted chain resumes
|
|
697
|
+
# from the last good point. Idempotent migrations that no-op'd just
|
|
698
|
+
# re-assert the same value.
|
|
699
|
+
stamp_version "$cf" "$ver"
|
|
700
|
+
done
|
|
701
|
+
# Fresh project with no stamp at all (e.g. just-bootstrapped, nothing in
|
|
702
|
+
# the chain applied) → stamp to the engine version so future runs have a
|
|
703
|
+
# baseline. Safe: migrations already self-skipped.
|
|
704
|
+
if [ -z "$(read_engine_stamp "$cf")" ]; then
|
|
705
|
+
stamp_version "$cf" "$(engine_version)"
|
|
706
|
+
fi
|
|
707
|
+
return 0
|
|
708
|
+
}
|