@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,149 @@
|
|
|
1
|
+
#!/bin/bash
|
|
2
|
+
# Shared plumbing for every CLI command. Commands are separate processes (the
|
|
3
|
+
# dispatcher execs them), so each one sources this file first.
|
|
4
|
+
#
|
|
5
|
+
# All YAML reading goes through the engine's lib/common.sh — the CLI never
|
|
6
|
+
# grows a parallel parser for shapes the engine can already read. The only
|
|
7
|
+
# CLI-owned parsing lives in lib/manifest.sh (quoted-key `packages:` /
|
|
8
|
+
# `registries:` blocks, which the engine deliberately never reads).
|
|
9
|
+
|
|
10
|
+
# Engine libraries (readers, is_status, IS_RC_*, engine_version). Sourced in
|
|
11
|
+
# the same order sync.sh uses. CLI_DIR / IS_ENGINE_DIR come exported from the
|
|
12
|
+
# dispatcher.
|
|
13
|
+
source "$IS_ENGINE_DIR/scripts/lib/common.sh"
|
|
14
|
+
source "$IS_ENGINE_DIR/scripts/lib/layout.sh"
|
|
15
|
+
source "$IS_ENGINE_DIR/scripts/lib/migrations.sh"
|
|
16
|
+
|
|
17
|
+
die() {
|
|
18
|
+
echo "ERROR: $*" >&2
|
|
19
|
+
exit 1
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
source "$CLI_DIR/lib/manifest.sh"
|
|
23
|
+
source "$CLI_DIR/lib/semver.sh"
|
|
24
|
+
source "$CLI_DIR/lib/registry.sh"
|
|
25
|
+
source "$CLI_DIR/lib/lockfile.sh"
|
|
26
|
+
|
|
27
|
+
# Meta-skills that the CLI replaces with first-class commands — staging skips
|
|
28
|
+
# them so a v2 project never carries a skill telling the agent to run a flow
|
|
29
|
+
# the CLI already owns.
|
|
30
|
+
CLI_OBSOLETE_SKILLS="intelligence-update intelligence-sync"
|
|
31
|
+
|
|
32
|
+
# --- Project detection ---------------------------------------------------
|
|
33
|
+
# Sets: IP_MODE (v2|legacy|none), IP_ROOT, IP_UMBRELLA, IP_MODULE_DIR.
|
|
34
|
+
# These ARE this lib's public API — every command reads them after calling
|
|
35
|
+
# detect_project, which per-file shellcheck cannot see.
|
|
36
|
+
# shellcheck disable=SC2034
|
|
37
|
+
# v2 wins: the nearest ancestor holding intelligence.yaml. Legacy is a git
|
|
38
|
+
# repo holding an umbrella (a dir with config.yaml) whose module is identified
|
|
39
|
+
# by role — scripts/sync.sh + scripts/VERSION — never by name.
|
|
40
|
+
detect_project() {
|
|
41
|
+
IP_MODE="none"; IP_ROOT=""; IP_UMBRELLA=""; IP_MODULE_DIR=""
|
|
42
|
+
local dir="$PWD"
|
|
43
|
+
while :; do
|
|
44
|
+
if [ -f "$dir/intelligence.yaml" ]; then
|
|
45
|
+
IP_MODE="v2"
|
|
46
|
+
IP_ROOT="$(cd "$dir" && pwd)"
|
|
47
|
+
return 0
|
|
48
|
+
fi
|
|
49
|
+
[ "$dir" = "$(dirname "$dir")" ] && break
|
|
50
|
+
dir="$(dirname "$dir")"
|
|
51
|
+
done
|
|
52
|
+
local root
|
|
53
|
+
root="$(git rev-parse --show-toplevel 2>/dev/null || true)"
|
|
54
|
+
[ -n "$root" ] || return 0
|
|
55
|
+
root="$(cd "$root" && pwd)"
|
|
56
|
+
local cf umbrella mod
|
|
57
|
+
while IFS= read -r cf; do
|
|
58
|
+
[ -n "$cf" ] || continue
|
|
59
|
+
umbrella="$(cd "$(dirname "$cf")" && pwd)"
|
|
60
|
+
for mod in "$umbrella"/*/; do
|
|
61
|
+
[ -d "$mod" ] || continue
|
|
62
|
+
if [ -f "${mod}scripts/sync.sh" ] && [ -f "${mod}scripts/VERSION" ]; then
|
|
63
|
+
IP_MODE="legacy"; IP_ROOT="$root"; IP_UMBRELLA="$umbrella"
|
|
64
|
+
IP_MODULE_DIR="${mod%/}"
|
|
65
|
+
return 0
|
|
66
|
+
fi
|
|
67
|
+
done
|
|
68
|
+
done < <(find "$root" -maxdepth 2 -name 'config.yaml' -not -path '*/.*' 2>/dev/null)
|
|
69
|
+
return 0
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
require_v2() {
|
|
73
|
+
detect_project
|
|
74
|
+
case "$IP_MODE" in
|
|
75
|
+
v2) ;;
|
|
76
|
+
legacy) die "this is a vendored (v1) setup — run 'intelligence migrate' first, or keep using its own flow" ;;
|
|
77
|
+
*) die "no intelligence project found here — run 'intelligence init'" ;;
|
|
78
|
+
esac
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
# --- Manifest basics (engine-readable shapes) ----------------------------
|
|
82
|
+
|
|
83
|
+
manifest_intelligence_dir() {
|
|
84
|
+
local manifest="$1" v
|
|
85
|
+
v="$(get_yaml_field "$manifest" "project" "intelligence_dir")"
|
|
86
|
+
printf '%s' "${v:-intelligence}"
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
bundled_engine_version() {
|
|
90
|
+
tr -d ' \t\r\n' < "$IS_ENGINE_DIR/scripts/VERSION"
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
# --- Engine content staging ----------------------------------------------
|
|
94
|
+
# The v2 project vendors no engine code; the engine's own rules / agents /
|
|
95
|
+
# meta-skills / docs are staged into the package store so sources can reach
|
|
96
|
+
# them as ordinary repo-relative dirs. Re-staged whenever the bundled engine
|
|
97
|
+
# version differs from the `.version` stamp.
|
|
98
|
+
# stage_engine_content <dest-dir> — unconditional copy of the engine's own
|
|
99
|
+
# content into <dest-dir> plus the `.version` stamp.
|
|
100
|
+
stage_engine_content() {
|
|
101
|
+
local store="$1"
|
|
102
|
+
rm -rf "$store"
|
|
103
|
+
mkdir -p "$store"
|
|
104
|
+
local d s name skip
|
|
105
|
+
for d in rules agents docs; do
|
|
106
|
+
[ -d "$IS_ENGINE_DIR/$d" ] && cp -R "$IS_ENGINE_DIR/$d" "$store/$d"
|
|
107
|
+
done
|
|
108
|
+
if [ -d "$IS_ENGINE_DIR/skills" ]; then
|
|
109
|
+
mkdir -p "$store/skills"
|
|
110
|
+
for s in "$IS_ENGINE_DIR"/skills/*/; do
|
|
111
|
+
[ -d "$s" ] || continue
|
|
112
|
+
name="$(basename "$s")"
|
|
113
|
+
skip=0
|
|
114
|
+
for d in $CLI_OBSOLETE_SKILLS; do
|
|
115
|
+
[ "$name" = "$d" ] && skip=1
|
|
116
|
+
done
|
|
117
|
+
[ "$skip" = "1" ] && continue
|
|
118
|
+
cp -R "$s" "$store/skills/$name"
|
|
119
|
+
done
|
|
120
|
+
fi
|
|
121
|
+
bundled_engine_version > "$store/.version"
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
ensure_engine_staged() {
|
|
125
|
+
local root="$1"
|
|
126
|
+
local store="$root/.intelligence/engine"
|
|
127
|
+
local bundled_ver staged_ver
|
|
128
|
+
bundled_ver="$(bundled_engine_version)"
|
|
129
|
+
staged_ver=""
|
|
130
|
+
[ -f "$store/.version" ] && staged_ver="$(tr -d ' \t\r\n' < "$store/.version")"
|
|
131
|
+
[ "$staged_ver" = "$bundled_ver" ] && return 0
|
|
132
|
+
stage_engine_content "$store"
|
|
133
|
+
echo " engine content staged: .intelligence/engine ($bundled_ver)"
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
# --- The engine env contract ---------------------------------------------
|
|
137
|
+
# Everything the IS_CLI mode of sync.sh needs, derived from the manifest.
|
|
138
|
+
export_engine_env() {
|
|
139
|
+
local root="$1"
|
|
140
|
+
local umbrella
|
|
141
|
+
umbrella="$(manifest_intelligence_dir "$root/intelligence.yaml")"
|
|
142
|
+
export IS_CLI=1
|
|
143
|
+
export CONFIG_FILE="$root/intelligence.yaml"
|
|
144
|
+
export REPO_ROOT="$root"
|
|
145
|
+
export IS_UMBRELLA_REL="$umbrella"
|
|
146
|
+
export IS_MODULE_REL=".intelligence/engine"
|
|
147
|
+
export IS_SYNC_CMD="intelligence sync"
|
|
148
|
+
export IS_PROTECTED_DIRS="$umbrella:.intelligence"
|
|
149
|
+
}
|
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
#!/bin/bash
|
|
2
|
+
# intelligence.lock — the resolved state, committed with the project.
|
|
3
|
+
#
|
|
4
|
+
# The lock is fully generated (npm's model): readers use the qmap parser from
|
|
5
|
+
# manifest.sh, writers rebuild the whole file from a row stream of entries.
|
|
6
|
+
# Columns: name, requested, url, path, resolved, sha — separated by the ASCII
|
|
7
|
+
# unit separator, NOT a tab: tab is IFS whitespace, so `read` would collapse
|
|
8
|
+
# an empty field (a rootless `path`) and shift every column after it.
|
|
9
|
+
|
|
10
|
+
LOCKFILE_VERSION=1
|
|
11
|
+
LOCK_SEP=$'\x1f'
|
|
12
|
+
|
|
13
|
+
# top_scalar <file> <key> — one top-level scalar (`key: "value"`), quotes and
|
|
14
|
+
# trailing comment stripped. The declared exception to "no new parsing": the
|
|
15
|
+
# engine's read_engine_stamp is hardwired to its own key.
|
|
16
|
+
top_scalar() {
|
|
17
|
+
[ -f "$1" ] || return 0
|
|
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"
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
# lock_to_tsv <lock> — every locked package as one row.
|
|
32
|
+
lock_to_tsv() {
|
|
33
|
+
local lock="$1" name
|
|
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")
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
# lock_write_from_tsv <lock> <tsv-file> — rebuild the lock. Entries are
|
|
48
|
+
# written in the TSV's order; callers keep it stable (manifest order).
|
|
49
|
+
lock_write_from_tsv() {
|
|
50
|
+
local lock="$1" tsv="$2"
|
|
51
|
+
local tmp="$lock.cli.tmp"
|
|
52
|
+
{
|
|
53
|
+
echo "# Generated by the intelligence CLI. Do not edit."
|
|
54
|
+
echo "lockfile_version: $LOCKFILE_VERSION"
|
|
55
|
+
echo "engine_version: \"$(bundled_engine_version)\""
|
|
56
|
+
echo "packages:"
|
|
57
|
+
awk -F"$LOCK_SEP" '
|
|
58
|
+
NF >= 6 {
|
|
59
|
+
print " \"" $1 "\":"
|
|
60
|
+
if ($2 != "") print " requested: \"" $2 "\""
|
|
61
|
+
print " url: \"" $3 "\""
|
|
62
|
+
if ($4 != "") print " path: \"" $4 "\""
|
|
63
|
+
if ($5 != "") print " resolved: \"" $5 "\""
|
|
64
|
+
print " sha: \"" $6 "\""
|
|
65
|
+
}
|
|
66
|
+
' "$tsv"
|
|
67
|
+
} > "$tmp"
|
|
68
|
+
mv "$tmp" "$lock"
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
# lock_upsert <lock> <name> <requested> <url> <path> <resolved> <sha>
|
|
72
|
+
# Replace-or-append one entry, keeping the others.
|
|
73
|
+
lock_upsert() {
|
|
74
|
+
local lock="$1" name="$2" requested="$3" url="$4" path="$5" resolved="$6" sha="$7"
|
|
75
|
+
local tsv="${TMPDIR:-/tmp}/intelligence-lock-$$.tsv"
|
|
76
|
+
{
|
|
77
|
+
lock_to_tsv "$lock" | awk -F"$LOCK_SEP" -v n="$name" '$1 != n'
|
|
78
|
+
printf '%s\037%s\037%s\037%s\037%s\037%s\n' \
|
|
79
|
+
"$name" "$requested" "$url" "$path" "$resolved" "$sha"
|
|
80
|
+
} > "$tsv"
|
|
81
|
+
lock_write_from_tsv "$lock" "$tsv"
|
|
82
|
+
rm -f "$tsv"
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
# lock_remove <lock> <name>
|
|
86
|
+
lock_remove() {
|
|
87
|
+
local lock="$1" name="$2"
|
|
88
|
+
[ -f "$lock" ] || return 0
|
|
89
|
+
local tsv="${TMPDIR:-/tmp}/intelligence-lock-$$.tsv"
|
|
90
|
+
lock_to_tsv "$lock" | awk -F"$LOCK_SEP" -v n="$name" '$1 != n' > "$tsv"
|
|
91
|
+
if [ -s "$tsv" ]; then
|
|
92
|
+
lock_write_from_tsv "$lock" "$tsv"
|
|
93
|
+
else
|
|
94
|
+
rm -f "$lock"
|
|
95
|
+
fi
|
|
96
|
+
rm -f "$tsv"
|
|
97
|
+
}
|
|
@@ -0,0 +1,211 @@
|
|
|
1
|
+
#!/bin/bash
|
|
2
|
+
# The CLI-owned YAML shapes: quoted-key maps (`packages:`, `registries:` in
|
|
3
|
+
# the manifest, `packages:` in the lock and in registry indexes).
|
|
4
|
+
#
|
|
5
|
+
# The engine deliberately never reads these blocks, and its readers cannot
|
|
6
|
+
# hold `@scope/name` keys — so this is the CLI's single, deliberate parser,
|
|
7
|
+
# scoped to exactly one shape:
|
|
8
|
+
#
|
|
9
|
+
# block:
|
|
10
|
+
# "@scope/name": # 2-space indent, key always quoted
|
|
11
|
+
# field: "value" # 4-space indent
|
|
12
|
+
# "@scope/other": "value" # flat form (registries)
|
|
13
|
+
#
|
|
14
|
+
# Every editor stages to `<file>.cli.tmp` and `mv`s — the migrations
|
|
15
|
+
# discipline — and preserves unrelated lines and comments byte-for-byte.
|
|
16
|
+
|
|
17
|
+
# Package names are paths (store dirs) and YAML keys, so the charset is
|
|
18
|
+
# closed: scoped, one slash, no spaces / quotes / colons.
|
|
19
|
+
assert_valid_pkg_name() {
|
|
20
|
+
case "$1" in
|
|
21
|
+
@*/*) ;;
|
|
22
|
+
*) die "invalid package name '$1' — expected @scope/name" ;;
|
|
23
|
+
esac
|
|
24
|
+
case "$1" in
|
|
25
|
+
*[!@/A-Za-z0-9._-]*|*/*/*) die "invalid package name '$1' — allowed: letters, digits, . _ -, one '/'" ;;
|
|
26
|
+
esac
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
# qmap_keys <file> <block> — quoted keys, one per line.
|
|
30
|
+
qmap_keys() {
|
|
31
|
+
[ -f "$1" ] || return 0
|
|
32
|
+
awk -v block="$2" '
|
|
33
|
+
{ sub(/\r$/, "") }
|
|
34
|
+
$0 ~ "^" block ":[ \t]*$" { inb = 1; next }
|
|
35
|
+
inb && /^[^ #]/ { inb = 0 }
|
|
36
|
+
inb {
|
|
37
|
+
if ($0 ~ /^ "/) {
|
|
38
|
+
s = substr($0, 4)
|
|
39
|
+
q = index(s, "\"")
|
|
40
|
+
if (q > 0) print substr(s, 1, q - 1)
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
' "$1"
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
# qmap_field <file> <block> <key> <field> — 4-indent field value under a
|
|
47
|
+
# quoted key; strips surrounding quotes and a trailing unquoted comment.
|
|
48
|
+
qmap_field() {
|
|
49
|
+
[ -f "$1" ] || return 0
|
|
50
|
+
awk -v block="$2" -v key="$3" -v field="$4" '
|
|
51
|
+
{ sub(/\r$/, "") }
|
|
52
|
+
$0 ~ "^" block ":[ \t]*$" { inb = 1; next }
|
|
53
|
+
inb && /^[^ #]/ { inb = 0; ink = 0 }
|
|
54
|
+
inb && /^ "/ {
|
|
55
|
+
s = substr($0, 4); q = index(s, "\"")
|
|
56
|
+
ink = (q > 0 && substr(s, 1, q - 1) == key)
|
|
57
|
+
next
|
|
58
|
+
}
|
|
59
|
+
inb && ink && /^ [A-Za-z_]/ {
|
|
60
|
+
line = $0
|
|
61
|
+
sub(/^ /, "", line)
|
|
62
|
+
c = index(line, ":")
|
|
63
|
+
if (c == 0) next
|
|
64
|
+
if (substr(line, 1, c - 1) != field) next
|
|
65
|
+
v = substr(line, c + 1)
|
|
66
|
+
sub(/^[ \t]+/, "", v)
|
|
67
|
+
if (v ~ /^"/) { v = substr(v, 2); q2 = index(v, "\""); if (q2 > 0) v = substr(v, 1, q2 - 1) }
|
|
68
|
+
else { sub(/[ \t]+#.*$/, "", v); sub(/[ \t]+$/, "", v) }
|
|
69
|
+
print v
|
|
70
|
+
exit
|
|
71
|
+
}
|
|
72
|
+
' "$1"
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
# qmap_value <file> <block> <key> — flat form: ` "key": "value"`.
|
|
76
|
+
qmap_value() {
|
|
77
|
+
[ -f "$1" ] || return 0
|
|
78
|
+
awk -v block="$2" -v key="$3" '
|
|
79
|
+
{ sub(/\r$/, "") }
|
|
80
|
+
$0 ~ "^" block ":[ \t]*$" { inb = 1; next }
|
|
81
|
+
inb && /^[^ #]/ { inb = 0 }
|
|
82
|
+
inb && /^ "/ {
|
|
83
|
+
s = substr($0, 4); q = index(s, "\"")
|
|
84
|
+
if (q == 0 || substr(s, 1, q - 1) != key) next
|
|
85
|
+
v = substr(s, q + 1)
|
|
86
|
+
sub(/^:[ \t]*/, "", v)
|
|
87
|
+
if (v ~ /^"/) { v = substr(v, 2); q2 = index(v, "\""); if (q2 > 0) v = substr(v, 1, q2 - 1) }
|
|
88
|
+
else { sub(/[ \t]+#.*$/, "", v); sub(/[ \t]+$/, "", v) }
|
|
89
|
+
if (v != "") print v
|
|
90
|
+
exit
|
|
91
|
+
}
|
|
92
|
+
' "$1"
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
# _qmap_stage <file> <awk-program> [awk args…] — run an editing pass, verify
|
|
96
|
+
# it produced output, commit.
|
|
97
|
+
_qmap_stage() {
|
|
98
|
+
local file="$1"; shift
|
|
99
|
+
local tmp="$file.cli.tmp"
|
|
100
|
+
awk "$@" "$file" > "$tmp"
|
|
101
|
+
[ -s "$tmp" ] || { rm -f "$tmp"; die "internal: manifest edit produced an empty file for $file"; }
|
|
102
|
+
mv "$tmp" "$file"
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
# qmap_set <file> <block> <key> <field> <value> — upsert one field, creating
|
|
106
|
+
# the block and the key as needed.
|
|
107
|
+
qmap_set() {
|
|
108
|
+
local file="$1" block="$2" key="$3" field="$4" value="$5"
|
|
109
|
+
[ -f "$file" ] || die "no such file: $file"
|
|
110
|
+
_qmap_stage "$file" -v block="$block" -v key="$key" -v field="$field" -v value="$value" '
|
|
111
|
+
function keyline() { return " \"" key "\":" }
|
|
112
|
+
function fieldline() { return " " field ": \"" value "\"" }
|
|
113
|
+
function flush_key() {
|
|
114
|
+
# leaving the key without having written the field -> append it
|
|
115
|
+
if (ink && !done) { print fieldline(); done = 1 }
|
|
116
|
+
ink = 0
|
|
117
|
+
}
|
|
118
|
+
function flush_block() {
|
|
119
|
+
if (inb && !keyseen && !done) { print keyline(); print fieldline(); keyseen = 1; done = 1 }
|
|
120
|
+
inb = 0
|
|
121
|
+
}
|
|
122
|
+
{ sub(/\r$/, "") }
|
|
123
|
+
$0 ~ "^" block ":[ \t]*$" { blockseen = 1; inb = 1; print; next }
|
|
124
|
+
inb && /^[^ #]/ { flush_key(); flush_block() }
|
|
125
|
+
inb && /^ "/ {
|
|
126
|
+
flush_key()
|
|
127
|
+
s = substr($0, 4); q = index(s, "\"")
|
|
128
|
+
if (q > 0 && substr(s, 1, q - 1) == key) { keyseen = 1; ink = 1 }
|
|
129
|
+
print; next
|
|
130
|
+
}
|
|
131
|
+
inb && ink && /^ [A-Za-z_]/ {
|
|
132
|
+
line = $0; sub(/^ /, "", line)
|
|
133
|
+
c = index(line, ":")
|
|
134
|
+
if (c > 0 && substr(line, 1, c - 1) == field) { print fieldline(); done = 1; next }
|
|
135
|
+
print; next
|
|
136
|
+
}
|
|
137
|
+
{ print }
|
|
138
|
+
END {
|
|
139
|
+
flush_key(); flush_block()
|
|
140
|
+
if (!blockseen) {
|
|
141
|
+
print block ":"
|
|
142
|
+
print keyline()
|
|
143
|
+
print fieldline()
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
'
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
# qmap_set_value <file> <block> <key> <value> — flat-form upsert.
|
|
150
|
+
qmap_set_value() {
|
|
151
|
+
local file="$1" block="$2" key="$3" value="$4"
|
|
152
|
+
[ -f "$file" ] || die "no such file: $file"
|
|
153
|
+
_qmap_stage "$file" -v block="$block" -v key="$key" -v value="$value" '
|
|
154
|
+
function entry() { return " \"" key "\": \"" value "\"" }
|
|
155
|
+
{ sub(/\r$/, "") }
|
|
156
|
+
$0 ~ "^" block ":[ \t]*$" { blockseen = 1; inb = 1; print; next }
|
|
157
|
+
inb && /^[^ #]/ { if (!done) { print entry(); done = 1 }; inb = 0 }
|
|
158
|
+
inb && /^ "/ {
|
|
159
|
+
s = substr($0, 4); q = index(s, "\"")
|
|
160
|
+
if (q > 0 && substr(s, 1, q - 1) == key) { print entry(); done = 1; next }
|
|
161
|
+
}
|
|
162
|
+
{ print }
|
|
163
|
+
END {
|
|
164
|
+
if (inb && !done) { print entry(); done = 1 }
|
|
165
|
+
if (!blockseen) { print block ":"; print entry() }
|
|
166
|
+
}
|
|
167
|
+
'
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
# qmap_delete_key <file> <block> <key> — drop the key and everything indented
|
|
171
|
+
# under it. Flat-form entries are one line, block-form keys take their fields
|
|
172
|
+
# with them. The block header stays even when it empties.
|
|
173
|
+
qmap_delete_key() {
|
|
174
|
+
local file="$1" block="$2" key="$3"
|
|
175
|
+
[ -f "$file" ] || return 0
|
|
176
|
+
_qmap_stage "$file" -v block="$block" -v key="$key" '
|
|
177
|
+
{ sub(/\r$/, "") }
|
|
178
|
+
$0 ~ "^" block ":[ \t]*$" { inb = 1; print; next }
|
|
179
|
+
inb && /^[^ #]/ { inb = 0; drop = 0 }
|
|
180
|
+
inb && /^ "/ {
|
|
181
|
+
s = substr($0, 4); q = index(s, "\"")
|
|
182
|
+
drop = (q > 0 && substr(s, 1, q - 1) == key)
|
|
183
|
+
if (drop) next
|
|
184
|
+
}
|
|
185
|
+
inb && drop && /^ / { next }
|
|
186
|
+
{ print }
|
|
187
|
+
'
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
# sources_remove_entry <file> <section> <entry> — remove `- "entry"` from
|
|
191
|
+
# sources.<section>. Inverse of the engine-side _mig_add_source.
|
|
192
|
+
sources_remove_entry() {
|
|
193
|
+
local file="$1" section="$2" entry="$3"
|
|
194
|
+
[ -f "$file" ] || return 0
|
|
195
|
+
_qmap_stage "$file" -v section="$section" -v entry="$entry" '
|
|
196
|
+
{ sub(/\r$/, "") }
|
|
197
|
+
/^sources:[ \t]*$/ { ins = 1; print; next }
|
|
198
|
+
ins && /^[^ #]/ { ins = 0; insec = 0 }
|
|
199
|
+
ins && $0 ~ "^ " section ":[ \t]*$" { insec = 1; print; next }
|
|
200
|
+
ins && /^ [A-Za-z_]/ { insec = 0 }
|
|
201
|
+
insec {
|
|
202
|
+
line = $0
|
|
203
|
+
sub(/^[ \t]*-[ \t]*/, "", line)
|
|
204
|
+
gsub(/["\x27]/, "", line)
|
|
205
|
+
sub(/[ \t]+#.*$/, "", line)
|
|
206
|
+
sub(/[ \t]+$/, "", line)
|
|
207
|
+
if (line == entry) next
|
|
208
|
+
}
|
|
209
|
+
{ print }
|
|
210
|
+
'
|
|
211
|
+
}
|
|
@@ -0,0 +1,141 @@
|
|
|
1
|
+
#!/bin/bash
|
|
2
|
+
# Name -> source resolution and package fetching.
|
|
3
|
+
#
|
|
4
|
+
# Resolution order (first hit wins):
|
|
5
|
+
# 1. project `registries:` — a scope bound to a registry repo (a git repo
|
|
6
|
+
# holding index.yaml); private orgs override anything shipped
|
|
7
|
+
# 2. the bundled default index (registry/index.yaml next to the CLI)
|
|
8
|
+
# 3. convention: @org/name -> https://github.com/org/name.git, content at
|
|
9
|
+
# the repo root — the zero-infrastructure default
|
|
10
|
+
#
|
|
11
|
+
# An index is itself fetched with git (never curl): auth, proxies and private
|
|
12
|
+
# hosting all come for free, and the CLI keeps the engine's bash+awk+git-only
|
|
13
|
+
# dependency footprint.
|
|
14
|
+
|
|
15
|
+
default_index_file() {
|
|
16
|
+
local f="$CLI_DIR/../registry/index.yaml"
|
|
17
|
+
[ -f "$f" ] && printf '%s' "$f"
|
|
18
|
+
return 0
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
# _fetch_index <registry-repo-url> — clones the registry repo (shallow) and
|
|
22
|
+
# prints the path of its index.yaml; empty on failure. Cached per run+url.
|
|
23
|
+
_fetch_index() {
|
|
24
|
+
local url="$1" cache key tmp
|
|
25
|
+
cache="${TMPDIR:-/tmp}/intelligence-cli-index-$$"
|
|
26
|
+
key="$(printf '%s' "$url" | cksum | awk '{print $1}')"
|
|
27
|
+
if [ -f "$cache/$key/index.yaml" ]; then
|
|
28
|
+
printf '%s' "$cache/$key/index.yaml"
|
|
29
|
+
return 0
|
|
30
|
+
fi
|
|
31
|
+
mkdir -p "$cache"
|
|
32
|
+
tmp="$cache/$key"
|
|
33
|
+
if GIT_TERMINAL_PROMPT=0 git -c core.autocrlf=false clone --depth 1 --quiet "$url" "$tmp" 2>/dev/null \
|
|
34
|
+
&& [ -f "$tmp/index.yaml" ]; then
|
|
35
|
+
printf '%s' "$tmp/index.yaml"
|
|
36
|
+
fi
|
|
37
|
+
return 0
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
# resolve_package_source <manifest-or-empty> <@scope/name>
|
|
41
|
+
# Sets RES_URL, RES_PATH (may be empty = repo root), RES_VIA — the caller's
|
|
42
|
+
# result contract, invisible to per-file shellcheck.
|
|
43
|
+
# shellcheck disable=SC2034
|
|
44
|
+
resolve_package_source() {
|
|
45
|
+
local manifest="$1" name="$2"
|
|
46
|
+
local scope="${name%%/*}" short="${name#*/}"
|
|
47
|
+
RES_URL=""; RES_PATH=""; RES_VIA=""
|
|
48
|
+
|
|
49
|
+
if [ -n "$manifest" ] && [ -f "$manifest" ]; then
|
|
50
|
+
local reg_url index
|
|
51
|
+
reg_url="$(qmap_value "$manifest" "registries" "$scope")"
|
|
52
|
+
if [ -n "$reg_url" ]; then
|
|
53
|
+
index="$(_fetch_index "${reg_url#git+}")"
|
|
54
|
+
[ -n "$index" ] || die "registry for scope '$scope' is unreachable or has no index.yaml: $reg_url"
|
|
55
|
+
RES_URL="$(qmap_field "$index" "packages" "$name" "url")"
|
|
56
|
+
RES_PATH="$(qmap_field "$index" "packages" "$name" "path")"
|
|
57
|
+
[ -n "$RES_URL" ] || die "package '$name' not found in the '$scope' registry ($reg_url)"
|
|
58
|
+
RES_VIA="registry:$reg_url"
|
|
59
|
+
return 0
|
|
60
|
+
fi
|
|
61
|
+
fi
|
|
62
|
+
|
|
63
|
+
local bundled
|
|
64
|
+
bundled="$(default_index_file)"
|
|
65
|
+
if [ -n "$bundled" ]; then
|
|
66
|
+
RES_URL="$(qmap_field "$bundled" "packages" "$name" "url")"
|
|
67
|
+
if [ -n "$RES_URL" ]; then
|
|
68
|
+
RES_PATH="$(qmap_field "$bundled" "packages" "$name" "path")"
|
|
69
|
+
RES_VIA="index"
|
|
70
|
+
return 0
|
|
71
|
+
fi
|
|
72
|
+
fi
|
|
73
|
+
|
|
74
|
+
RES_URL="https://github.com/${scope#@}/$short.git"
|
|
75
|
+
RES_PATH=""
|
|
76
|
+
RES_VIA="convention"
|
|
77
|
+
return 0
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
# fetch_package <url> <ref> <subpath> <dest-dir>
|
|
81
|
+
# Shallow-clones url@ref (tag, branch, or SHA fallback), copies <subpath>
|
|
82
|
+
# (or the repo root) into <dest-dir>, prints the resolved commit sha.
|
|
83
|
+
# The clone pins autocrlf/eol/symlinks exactly like the engine does, so the
|
|
84
|
+
# store holds the bytes the package published.
|
|
85
|
+
fetch_package() {
|
|
86
|
+
local url="$1" ref="$2" subpath="$3" dest="$4"
|
|
87
|
+
local tmp sha src
|
|
88
|
+
local -a branch_arg=()
|
|
89
|
+
[ -n "$ref" ] && branch_arg=(--branch "$ref")
|
|
90
|
+
tmp="$(mktemp -d -t intelligence-pkg-XXXXXX 2>/dev/null || mktemp -d)"
|
|
91
|
+
if ! GIT_TERMINAL_PROMPT=0 git -c core.symlinks=false -c core.autocrlf=false -c core.eol=lf \
|
|
92
|
+
clone --depth 1 "${branch_arg[@]+"${branch_arg[@]}"}" --quiet "$url" "$tmp/clone" 2>/dev/null; then
|
|
93
|
+
# A SHA is not clonable with --branch — fall back to a full clone + checkout.
|
|
94
|
+
rm -rf "$tmp/clone"
|
|
95
|
+
GIT_TERMINAL_PROMPT=0 git -c core.symlinks=false -c core.autocrlf=false -c core.eol=lf \
|
|
96
|
+
clone --quiet "$url" "$tmp/clone" 2>/dev/null \
|
|
97
|
+
|| { rm -rf "$tmp"; die "cannot clone $url"; }
|
|
98
|
+
[ -z "$ref" ] || git -C "$tmp/clone" checkout --quiet "$ref" \
|
|
99
|
+
|| { rm -rf "$tmp"; die "ref '$ref' not found in $url"; }
|
|
100
|
+
fi
|
|
101
|
+
sha="$(git -C "$tmp/clone" rev-parse HEAD)"
|
|
102
|
+
src="$tmp/clone"
|
|
103
|
+
if [ -n "$subpath" ]; then
|
|
104
|
+
case "$subpath" in
|
|
105
|
+
*..*) rm -rf "$tmp"; die "unsafe path '$subpath'" ;;
|
|
106
|
+
esac
|
|
107
|
+
src="$tmp/clone/$subpath"
|
|
108
|
+
[ -d "$src" ] || { rm -rf "$tmp"; die "path '$subpath' not found in $url${ref:+@$ref}" ; }
|
|
109
|
+
fi
|
|
110
|
+
rm -rf "$dest"
|
|
111
|
+
mkdir -p "$dest"
|
|
112
|
+
cp -R "$src/." "$dest/"
|
|
113
|
+
# A nested repository would land as an untracked .git — the store must
|
|
114
|
+
# hold plain files only.
|
|
115
|
+
rm -rf "$dest/.git"
|
|
116
|
+
rm -rf "$tmp"
|
|
117
|
+
printf '%s' "$sha"
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
# wire_package_sources <manifest> <@scope/name> <store-rel-dir>
|
|
121
|
+
# The provides convention: whichever of rules/agents/skills the installed
|
|
122
|
+
# package has becomes a sources entry. Reuses the engine's idempotent,
|
|
123
|
+
# comment-preserving _mig_add_source.
|
|
124
|
+
wire_package_sources() {
|
|
125
|
+
local manifest="$1" name="$2" rel="$3" root="$4"
|
|
126
|
+
local section
|
|
127
|
+
for section in rules agents skills; do
|
|
128
|
+
if [ -d "$root/$rel/$section" ]; then
|
|
129
|
+
_mig_add_source "$manifest" "$section" "$rel/$section"
|
|
130
|
+
fi
|
|
131
|
+
done
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
# unwire_package_sources <manifest> <store-rel-dir>
|
|
135
|
+
unwire_package_sources() {
|
|
136
|
+
local manifest="$1" rel="$2"
|
|
137
|
+
local section
|
|
138
|
+
for section in rules agents skills; do
|
|
139
|
+
sources_remove_entry "$manifest" "$section" "$rel/$section"
|
|
140
|
+
done
|
|
141
|
+
}
|