@ainova-systems/intelligence 0.16.0 → 0.16.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.
@@ -1,12 +1,56 @@
1
1
  #!/bin/bash
2
2
  # Generated-output policy derived from each adapter's ownership contract.
3
3
 
4
+ # The line that separates what this policy manages from what the project wrote
5
+ # before Intelligence touched the file: everything the CLI appends lands after
6
+ # it, so it is also the boundary of what the CLI may rewrite.
7
+ IS_GITIGNORE_HEADER='# Intelligence generated state and tool output'
8
+
9
+ # Line endings held in variables, never written as `$'\r'` at the point of use:
10
+ # inside a command substitution bash does not expand that form, so the very same
11
+ # expression stops stripping the CR depending on where it is called from.
12
+ IS_CR=$'\r'
13
+ IS_LF=$'\n'
14
+ IS_CRLF=$'\r\n'
15
+
16
+ # An ignore file checked out on Windows can hold CRLF, and a CR belongs to the
17
+ # pattern Git matches — so these two helpers decide presence ignoring it, and
18
+ # append in whatever ending the file already uses. Comparing with the CR
19
+ # attached made every existing line look absent on Linux and macOS (where it is
20
+ # part of the line, unlike under MSYS), so each alignment appended an LF copy of
21
+ # a line that was already there and the file grew forever.
22
+
23
+ # ignore_file_eol_var <file> — set IS_IGNORE_EOL from the file's first line.
24
+ ignore_file_eol_var() {
25
+ local file="$1" first=""
26
+ IS_IGNORE_EOL="$IS_LF"
27
+ [ -s "$file" ] || return 0
28
+ IFS= read -r first < "$file" || true
29
+ case "$first" in
30
+ *"$IS_CR") IS_IGNORE_EOL="$IS_CRLF" ;;
31
+ esac
32
+ }
33
+
34
+ # ignore_file_has_line <file> <line> — true when the file lists exactly this
35
+ # line, with or without a trailing CR.
36
+ ignore_file_has_line() {
37
+ local file="$1" line="$2"
38
+ [ -f "$file" ] || return 1
39
+ grep -Fqx -- "$line" "$file" 2>/dev/null && return 0
40
+ grep -Fqx -- "$line$IS_CR" "$file" 2>/dev/null
41
+ }
42
+
43
+ # ignore_file_append_line <file> <line> — append in the file's own ending.
44
+ ignore_file_append_line() {
45
+ local file="$1" line="$2"
46
+ ignore_file_eol_var "$file"
47
+ printf '%s%s' "$line" "$IS_IGNORE_EOL" >> "$file"
48
+ }
49
+
4
50
  gitignore_add_line() {
5
51
  local root="$1" line="$2" file="$1/.gitignore"
6
- if [ -f "$file" ] && grep -Fqx -- "$line" "$file"; then
7
- return 0
8
- fi
9
- printf '%s\n' "$line" >> "$file"
52
+ ignore_file_has_line "$file" "$line" && return 0
53
+ ignore_file_append_line "$file" "$line"
10
54
  }
11
55
 
12
56
  gitignore_path_is_ignored() {
@@ -21,10 +65,102 @@ gitignore_tail_is() {
21
65
  [ -f "$file" ] || return 1
22
66
  local want tail_lines
23
67
  want="$(printf '%s\n' "$@")"
24
- tail_lines="$(tail -n "$#" -- "$file")"
68
+ # CR stripped for the comparison only — the chain is the same chain whether
69
+ # the file is LF or CRLF.
70
+ tail_lines="$(tail -n "$#" -- "$file" | tr -d '\r')"
25
71
  [ "$tail_lines" = "$want" ]
26
72
  }
27
73
 
74
+ # gitignore_collapse_duplicates <root> <probe-path> <line…> — keep only the LAST
75
+ # occurrence of each given line inside the region this policy owns (its header
76
+ # to end of file).
77
+ #
78
+ # This removes residue, never meaning: Git applies the last matching rule, so
79
+ # identical earlier copies of a line decide nothing. The copies exist because
80
+ # the 0.12.0 repair appended the whole negation chain on every alignment — a
81
+ # project carries one chain per run it made, and 0.12.1 only stopped the growth.
82
+ #
83
+ # Lines above the header predate Intelligence and are left untouched, including
84
+ # a hand-written copy of a line this policy also writes. The probe path is
85
+ # re-checked afterwards: a collapse that somehow changed what Git ignores is
86
+ # rolled back rather than committed.
87
+ gitignore_collapse_duplicates() {
88
+ local root="$1" probe="$2"; shift 2
89
+ local file="$root/.gitignore" tmp backup before=0 after=0
90
+ local -a lines=() managed=("$@") last_idx=()
91
+ local line key i mi n hdr=-1 keep final_newline=1
92
+ [ -f "$file" ] || return 0
93
+
94
+ # Read and write the file's own bytes, without awk: on Windows it reads in
95
+ # text mode and would hand back every line stripped of its CR, rewriting a
96
+ # CRLF file as LF. A line's CR is significant to Git, and either way the
97
+ # lines this function does not remove must survive byte-for-byte.
98
+ line=""
99
+ while IFS= read -r line; do
100
+ lines[${#lines[@]}]="$line"
101
+ line=""
102
+ done < "$file"
103
+ # `read` returns 1 at EOF but still fills `line` when the file's last line
104
+ # carries no newline; preserve that ending instead of adding one.
105
+ if [ -n "$line" ]; then
106
+ lines[${#lines[@]}]="$line"
107
+ final_newline=0
108
+ fi
109
+ n=${#lines[@]}
110
+ for ((i = 0; i < n; i++)); do
111
+ if [ "${lines[i]%"$IS_CR"}" = "$IS_GITIGNORE_HEADER" ]; then hdr=$i; break; fi
112
+ done
113
+ [ "$hdr" -ge 0 ] || return 0
114
+
115
+ # Where each managed line last occurs below the header. Indexed arrays and
116
+ # arithmetic `for` only — Bash 3.2 has no associative arrays.
117
+ for ((mi = 0; mi < ${#managed[@]}; mi++)); do
118
+ last_idx[mi]=-1
119
+ for ((i = hdr + 1; i < n; i++)); do
120
+ [ "${lines[i]%"$IS_CR"}" = "${managed[mi]}" ] && last_idx[mi]=$i
121
+ done
122
+ done
123
+
124
+ tmp="$file.cli.tmp"
125
+ {
126
+ for ((i = 0; i < n; i++)); do
127
+ keep=1
128
+ if [ "$i" -gt "$hdr" ]; then
129
+ key="${lines[i]%"$IS_CR"}"
130
+ for ((mi = 0; mi < ${#managed[@]}; mi++)); do
131
+ if [ "$key" = "${managed[mi]}" ] && [ "$i" -ne "${last_idx[mi]}" ]; then
132
+ keep=0
133
+ break
134
+ fi
135
+ done
136
+ fi
137
+ [ "$keep" -eq 1 ] || continue
138
+ if [ "$i" -eq $((n - 1)) ] && [ "$final_newline" -eq 0 ]; then
139
+ printf '%s' "${lines[i]}"
140
+ else
141
+ printf '%s\n' "${lines[i]}"
142
+ fi
143
+ done
144
+ } > "$tmp"
145
+ if [ ! -s "$tmp" ] || cmp -s "$tmp" "$file"; then
146
+ rm -f "$tmp"
147
+ return 0
148
+ fi
149
+
150
+ before=0
151
+ gitignore_path_is_ignored "$root" "$probe" || before=$?
152
+ backup="$file.cli.bak"
153
+ cp -- "$file" "$backup"
154
+ mv -- "$tmp" "$file"
155
+ after=0
156
+ gitignore_path_is_ignored "$root" "$probe" || after=$?
157
+ if [ "$after" != "$before" ]; then
158
+ mv -- "$backup" "$file"
159
+ return 0
160
+ fi
161
+ rm -f "$backup"
162
+ }
163
+
28
164
  # Git cannot re-include a child of an excluded directory, so an include needs an
29
165
  # explicit negation for every parent. A negation only wins when it comes after
30
166
  # the rule excluding the parent, and the only repair an append can make is to
@@ -59,7 +195,7 @@ gitignore_add_effective_include() {
59
195
  gitignore_path_is_ignored "$root" "$value" || rc=$?
60
196
  case "$rc" in
61
197
  0) ;;
62
- 1|2) return 0 ;;
198
+ 1|2) gitignore_collapse_duplicates "$root" "$value" "${negations[@]}"; return 0 ;;
63
199
  *) return "$rc" ;;
64
200
  esac
65
201
 
@@ -67,17 +203,25 @@ gitignore_add_effective_include() {
67
203
  # chain last. Already last means appending cannot help — leave it, and let
68
204
  # `status --check` report the re-inclusion this policy could not make
69
205
  # effective rather than growing the file forever.
70
- gitignore_tail_is "$root/.gitignore" "${negations[@]}" && return 0
71
- printf '%s\n' "${negations[@]}" >> "$root/.gitignore"
206
+ if ! gitignore_tail_is "$root/.gitignore" "${negations[@]}"; then
207
+ for line in "${negations[@]}"; do
208
+ ignore_file_append_line "$root/.gitignore" "$line"
209
+ done
210
+ fi
211
+ # Either way the chain now stands last, so any earlier copy of it is the
212
+ # residue an older CLI appended; collapsing runs on both paths so a project
213
+ # converges on one copy whether or not this run had to move anything.
214
+ gitignore_collapse_duplicates "$root" "$value" "${negations[@]}"
72
215
  }
73
216
 
74
217
  ensure_gitignore_header() {
75
218
  local root="$1" file="$1/.gitignore"
76
- if [ ! -f "$file" ] || ! grep -Fqx '# Intelligence generated state and tool output' "$file"; then
219
+ if ! ignore_file_has_line "$file" "$IS_GITIGNORE_HEADER"; then
77
220
  if [ -f "$file" ] && [ -s "$file" ] && [ -n "$(tail -c 1 "$file" 2>/dev/null)" ]; then
78
- printf '\n' >> "$file"
221
+ ignore_file_eol_var "$file"
222
+ printf '%s' "$IS_IGNORE_EOL" >> "$file"
79
223
  fi
80
- printf '%s\n' '# Intelligence generated state and tool output' >> "$file"
224
+ ignore_file_append_line "$file" "$IS_GITIGNORE_HEADER"
81
225
  fi
82
226
  }
83
227
 
@@ -125,10 +269,8 @@ publisher_ignore_file_names() {
125
269
 
126
270
  publisher_ignore_add_line() {
127
271
  local file="$1" line="$2"
128
- if grep -Fqx -- "$line" "$file" 2>/dev/null; then
129
- return 0
130
- fi
131
- printf '%s\n' "$line" >> "$file"
272
+ ignore_file_has_line "$file" "$line" && return 0
273
+ ignore_file_append_line "$file" "$line"
132
274
  PUBLISH_IGNORE_FILE_CHANGED=1
133
275
  }
134
276
 
@@ -148,11 +290,12 @@ ensure_manifest_publisher_ignores() {
148
290
  file="$root/$rel"
149
291
  [ -f "$file" ] || continue
150
292
  PUBLISH_IGNORE_FILE_CHANGED=0
151
- if ! grep -Fqx '# Intelligence development context and generated output' "$file" 2>/dev/null; then
293
+ if ! ignore_file_has_line "$file" '# Intelligence development context and generated output'; then
152
294
  if [ -s "$file" ] && [ -n "$(tail -c 1 "$file" 2>/dev/null)" ]; then
153
- printf '\n' >> "$file"
295
+ ignore_file_eol_var "$file"
296
+ printf '%s' "$IS_IGNORE_EOL" >> "$file"
154
297
  fi
155
- printf '%s\n' '# Intelligence development context and generated output' >> "$file"
298
+ ignore_file_append_line "$file" '# Intelligence development context and generated output'
156
299
  PUBLISH_IGNORE_FILE_CHANGED=1
157
300
  fi
158
301
  publisher_ignore_add_path "$file" '.intelligence'
package/engine/ENGINE_SHA CHANGED
@@ -1 +1 @@
1
- 179b1e89ade2ac7d833750458de1babe0c7a6d90
1
+ 365ec21e2f2d4d9ef323ed9f1387b3c3c4bef845
package/engine/VERSION CHANGED
@@ -1 +1 @@
1
- 0.16.0
1
+ 0.16.1
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ainova-systems/intelligence",
3
- "version": "0.16.0",
3
+ "version": "0.16.1",
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"