@ainova-systems/intelligence 0.12.0 → 0.13.0

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/README.md CHANGED
@@ -35,6 +35,7 @@ intelligence sync
35
35
  | `intelligence init [--preview\|--apply]` | Create, convert, restore or align a project |
36
36
  | `intelligence sync [adapter]` | Restore locked content if needed, then render all or one enabled adapter |
37
37
  | `intelligence update [@scope/name] [--preview\|--apply]` | Plan or apply project and ranged-package updates |
38
+ | `intelligence upgrade [--next] [--preview\|--apply]` | Replace the installed CLI with the newest version on its npm channel |
38
39
  | `intelligence package add\|remove\|list\|search` | Manage versioned Intelligence Packages |
39
40
  | `intelligence adapter list\|create\|enable\|disable\|remove` | Manage render adapters |
40
41
  | `intelligence status [--check]` | Inspect project state and consistency |
@@ -47,10 +47,19 @@ function findBash() {
47
47
 
48
48
  const cli = path.join(__dirname, '..', 'cli', 'intelligence').replace(/\\/g, '/');
49
49
  const pkg = require(path.join(__dirname, '..', 'package.json'));
50
+ const binName = Object.keys(pkg.bin)[0];
50
51
 
51
52
  const result = spawnSync(findBash(), [cli].concat(process.argv.slice(2)), {
52
53
  stdio: 'inherit',
53
- env: Object.assign({}, process.env, { INTELLIGENCE_NPM_VERSION: pkg.version }),
54
+ // What npm installed, for `update` (channel check) and `upgrade` (replace
55
+ // exactly this package): its version and name, the command npm linked as
56
+ // its shim, and this launcher's path inside the package.
57
+ env: Object.assign({}, process.env, {
58
+ INTELLIGENCE_NPM_VERSION: pkg.version,
59
+ INTELLIGENCE_NPM_PACKAGE: pkg.name,
60
+ INTELLIGENCE_NPM_BIN: binName,
61
+ INTELLIGENCE_NPM_LAUNCHER: pkg.bin[binName],
62
+ }),
54
63
  });
55
64
  if (result.error) {
56
65
  console.error('intelligence: failed to launch bash: ' + result.error.message);
@@ -2,8 +2,9 @@
2
2
  # intelligence update [@scope/name] [--preview|--apply]
3
3
  #
4
4
  # One update surface: show the installed-CLI/project/package plan, then either
5
- # stop, ask interactively, or apply without a prompt. Updating the global npm
6
- # installation remains an explicit package-manager operation.
5
+ # stop, ask interactively, or apply without a prompt. Replacing the global npm
6
+ # installation is `intelligence upgrade`, which the plan names; it never
7
+ # happens inside a project command.
7
8
  set -euo pipefail
8
9
  source "$CLI_DIR/lib/cli-common.sh"
9
10
 
@@ -30,18 +31,27 @@ project_change=0
30
31
  echo "Update plan"
31
32
  echo ""
32
33
  echo "CLI:"
33
- if [ -n "${INTELLIGENCE_NPM_VERSION:-}" ] && [ "${IS_SKIP_NPM_CHECK:-0}" != "1" ] && command -v npm >/dev/null 2>&1; then
34
- channel="latest"
35
- case "$INTELLIGENCE_NPM_VERSION" in *-*) channel="next" ;; esac
36
- available="$(npm view "@ainova-systems/intelligence" "dist-tags.$channel" 2>/dev/null || true)"
37
- if [ -z "$available" ]; then
38
- echo " installed: $INTELLIGENCE_NPM_VERSION; registry check unavailable"
39
- elif [ "$available" = "$INTELLIGENCE_NPM_VERSION" ]; then
40
- echo " $INTELLIGENCE_NPM_VERSION (up to date on npm $channel)"
34
+ if [ -n "${INTELLIGENCE_NPM_VERSION:-}" ] && [ -n "${INTELLIGENCE_NPM_PACKAGE:-}" ] \
35
+ && [ "${IS_SKIP_NPM_CHECK:-0}" != "1" ] && command -v npm >/dev/null 2>&1; then
36
+ channel="$(npm_channel_for "$INTELLIGENCE_NPM_VERSION")"
37
+ # Ask the way `upgrade` will install: in global mode at this tree's own
38
+ # prefix when npm made it, so the plan and the install read one config.
39
+ cli_install_classify "$(cd "$CLI_DIR/.." && pwd -P)" "$INTELLIGENCE_NPM_PACKAGE" "${INTELLIGENCE_NPM_BIN:-}"
40
+ npm_prefix=""
41
+ [ "$CLI_INSTALL_KIND" != "npm" ] || npm_prefix="$(native_path "$CLI_INSTALL_PREFIX")"
42
+ # An empty or non-version answer is "not checked", never a comparison.
43
+ if available="$(cli_registry_version "$INTELLIGENCE_NPM_PACKAGE" "$channel" "$npm_prefix")"; then
44
+ case "$(semver_cmp_full "$available" "$INTELLIGENCE_NPM_VERSION")" in
45
+ 1)
46
+ echo " $INTELLIGENCE_NPM_VERSION -> $available (npm $channel)"
47
+ echo " run: intelligence upgrade"
48
+ echo " then rerun: intelligence update --apply"
49
+ ;;
50
+ 0) echo " $INTELLIGENCE_NPM_VERSION (up to date on npm $channel)" ;;
51
+ *) echo " $INTELLIGENCE_NPM_VERSION (ahead of npm $channel $available)" ;;
52
+ esac
41
53
  else
42
- echo " $INTELLIGENCE_NPM_VERSION -> $available (npm $channel)"
43
- echo " run: npm install -g @ainova-systems/intelligence@$channel"
44
- echo " then rerun: intelligence update --apply"
54
+ echo " installed: $INTELLIGENCE_NPM_VERSION; registry check unavailable"
45
55
  fi
46
56
  else
47
57
  echo " source checkout/registry check skipped; engine $eng"
@@ -50,7 +60,7 @@ fi
50
60
  echo ""
51
61
  echo "Project:"
52
62
  if project_stamped_ahead "$IP_ROOT"; then
53
- echo " schema $stamp is ahead of engine $eng — left as is; update the CLI to align it"
63
+ echo " schema $stamp is ahead of engine $eng — left as is; update the CLI to align it: intelligence upgrade (--next when the stamp came from a prerelease line)"
54
64
  elif project_needs_upgrade "$IP_ROOT"; then
55
65
  echo " lifecycle alignment required (stamp ${stamp:-unstamped}, engine $eng)"
56
66
  project_change=1
@@ -0,0 +1,100 @@
1
+ #!/bin/bash
2
+ # intelligence upgrade [--next] [--preview|--apply]
3
+ #
4
+ # Replace the installed CLI with the newest version on its npm channel:
5
+ # `next` when the running version is a prerelease or --next is given,
6
+ # `latest` otherwise. `update` plans this step and points here; this is the
7
+ # one command that writes to an npm prefix, and it never reads or writes a
8
+ # project.
9
+ set -euo pipefail
10
+ source "$CLI_DIR/lib/cli-common.sh"
11
+
12
+ mode="ask" preview_seen=0 apply_seen=0 channel=""
13
+ while [ $# -gt 0 ]; do
14
+ case "$1" in
15
+ --preview) preview_seen=1; mode="preview" ;;
16
+ --apply) apply_seen=1; mode="apply" ;;
17
+ --next) channel="next" ;;
18
+ *) die "usage: intelligence upgrade [--next] [--preview|--apply]" ;;
19
+ esac
20
+ shift
21
+ done
22
+ [ "$preview_seen" -eq 0 ] || [ "$apply_seen" -eq 0 ] || die "choose either --preview or --apply"
23
+
24
+ pkg_dir="$(cd "$CLI_DIR/.." && pwd -P)"
25
+ installed="${INTELLIGENCE_NPM_VERSION:-}"
26
+ pkg="${INTELLIGENCE_NPM_PACKAGE:-}"
27
+ bin="${INTELLIGENCE_NPM_BIN:-}"
28
+ # All of it comes from the npm launcher; `bash cli/intelligence` in a checkout has none.
29
+ [ -n "$installed" ] && [ -n "$pkg" ] && [ -n "$bin" ] && [ -n "${INTELLIGENCE_NPM_LAUNCHER:-}" ] \
30
+ || die "this CLI runs from a source checkout ($pkg_dir), not an npm installation — pull the repository instead"
31
+ command -v npm >/dev/null 2>&1 || die "npm is not on PATH — it installed this CLI and is needed to replace it"
32
+ [ -n "$channel" ] || channel="$(npm_channel_for "$installed")"
33
+
34
+ # Where this tree lives is decided locally, before any network: a tree npm
35
+ # did not make is refused whatever the registry would say. The hint names
36
+ # the exact version when the registry answers and the channel otherwise.
37
+ cli_install_classify "$pkg_dir" "$pkg" "$bin"
38
+ if [ "$CLI_INSTALL_KIND" != "npm" ]; then
39
+ available="$(cli_registry_version "$pkg" "$channel" || true)"
40
+ die "this CLI ($pkg_dir) is not an npm global installation — $(cli_install_hint "$CLI_INSTALL_KIND" "$pkg" "${available:-$channel}")"
41
+ fi
42
+ prefix="$(native_path "$CLI_INSTALL_PREFIX")"
43
+ launcher="$(native_path "$pkg_dir/$INTELLIGENCE_NPM_LAUNCHER")"
44
+
45
+ available="$(cli_registry_version "$pkg" "$channel" "$prefix")" \
46
+ || die "npm could not report a version for $pkg dist-tag $channel — check the network or the registry and retry"
47
+
48
+ case "$(semver_cmp_full "$available" "$installed")" in
49
+ 0)
50
+ echo "CLI $installed is up to date (npm $channel)"
51
+ exit 0
52
+ ;;
53
+ -1)
54
+ # Never a downgrade: a channel moved back on purpose is a decision
55
+ # the user takes by hand, with the command that does it.
56
+ echo "CLI $installed is newer than npm $channel ($available); nothing to do"
57
+ echo " to move back deliberately: npm install -g --prefix $(quote_for_shell "$prefix") $pkg@$available"
58
+ exit 0
59
+ ;;
60
+ esac
61
+
62
+ echo "CLI upgrade: $installed -> $available (npm $channel)"
63
+ echo " npm install -g --prefix $(quote_for_shell "$prefix") $pkg@$available"
64
+ [ "$mode" != "preview" ] || exit 0
65
+ if [ "$mode" = "ask" ]; then
66
+ [ -t 0 ] || die "upgrade requires confirmation — rerun with --preview or --apply"
67
+ printf '\nInstall %s? [Y/n] ' "$available"
68
+ read -r answer
69
+ case "$answer" in
70
+ ""|y|Y|yes|YES) ;;
71
+ *) echo "upgrade cancelled"; exit 0 ;;
72
+ esac
73
+ fi
74
+ echo ""
75
+
76
+ # npm rewrites the directory this script lives in, and Windows refuses to
77
+ # delete a file another process still holds open. `bash -c` parses its whole
78
+ # program before running it, so the fresh shell reads nothing from the
79
+ # package while npm replaces it, and exec leaves no old process behind.
80
+ # The version installed is the one the plan showed, never a moving tag, and
81
+ # success is what the new launcher reports, not what npm returned.
82
+ exec "$BASH" -c '
83
+ set -euo pipefail
84
+ pkg="$1" from="$2" to="$3" launcher="$4" prefix="$5" bin="$6"
85
+ npm install -g --prefix "$prefix" "$pkg@$to"
86
+ echo ""
87
+ if [ -f "$launcher" ] && command -v node >/dev/null 2>&1; then
88
+ got="$(node "$launcher" version 2>/dev/null || true)"
89
+ case "$got" in
90
+ "$to"|"$to "*) echo "upgraded: $from -> $to"; echo "$got" ;;
91
+ *)
92
+ echo "ERROR: npm reported success but the installed launcher answers: ${got:-<nothing>}" >&2
93
+ echo " expected $to — inspect the installation at $prefix" >&2
94
+ exit 1
95
+ ;;
96
+ esac
97
+ else
98
+ echo "upgraded: $from -> $to (not verified — run: $bin version)"
99
+ fi
100
+ ' intelligence-upgrade "$pkg" "$installed" "$available" "$launcher" "$prefix" "$bin"
package/cli/intelligence CHANGED
@@ -19,7 +19,7 @@ if [ -f "$CLI_DIR/../engine/sync.sh" ] && [ -f "$CLI_DIR/../engine/VERSION" ]; t
19
19
  fi
20
20
  if [ -z "$IS_ENGINE_DIR" ]; then
21
21
  echo "ERROR: bundled sync engine not found next to the CLI ($CLI_DIR)." >&2
22
- echo " Reinstall: npm i -g @ainova-systems/intelligence" >&2
22
+ echo " Reinstall: npm i -g ${INTELLIGENCE_NPM_PACKAGE:-<the CLI package>}" >&2
23
23
  exit 1
24
24
  fi
25
25
  export IS_ENGINE_DIR
@@ -51,6 +51,8 @@ Usage: intelligence <command> [args]
51
51
  update [@scope/name] Show an update plan; ask before applying it
52
52
  --preview Show the plan without asking or writing
53
53
  --apply Apply the plan without asking
54
+ upgrade [--next] [--preview|--apply] Replace the installed CLI with the newest npm version
55
+ --next Follow the prerelease line instead of the stable one
54
56
  package <command> add | remove | list | search
55
57
  adapter <command> list | create | enable | disable | remove
56
58
  status [--check] Project state; --check runs deep consistency checks
@@ -44,9 +44,9 @@ if [ -z "$stamp" ]; then
44
44
  warn "manifest has no schema_version — run 'intelligence init'"
45
45
  elif _ver_gt "$stamp" "$eng"; then
46
46
  if [ "$(_ver_major "$stamp")" -gt "$(_ver_major "$eng")" ]; then
47
- warn "manifest schema $stamp is a newer major than this CLI's engine $eng — update the CLI: npm i -g @ainova-systems/intelligence@latest"
47
+ warn "manifest schema $stamp is a newer major than this CLI's engine $eng — update the CLI: intelligence upgrade (--next for a prerelease line)"
48
48
  else
49
- note "manifest schema $stamp is newer than this CLI's engine $eng — the project uses a newer CLI; update it: npm i -g @ainova-systems/intelligence@latest"
49
+ note "manifest schema $stamp is newer than this CLI's engine $eng — the project uses a newer CLI; update it: intelligence upgrade (--next for a prerelease line)"
50
50
  fi
51
51
  elif _ver_gt "$eng" "$stamp"; then
52
52
  warn "manifest schema $stamp behind engine $eng — run 'intelligence init'"
@@ -80,6 +80,7 @@ pin_label() {
80
80
 
81
81
  source "$CLI_DIR/lib/manifest.sh"
82
82
  source "$CLI_DIR/lib/semver.sh"
83
+ source "$CLI_DIR/lib/cli-install.sh"
83
84
  source "$CLI_DIR/lib/registry.sh"
84
85
  source "$CLI_DIR/lib/lockfile.sh"
85
86
  source "$CLI_DIR/lib/adapter-lifecycle.sh"
@@ -0,0 +1,131 @@
1
+ #!/bin/bash
2
+ # The installed CLI's own npm installation — shared by `upgrade`, which
3
+ # replaces the tree it runs from, and `update`, which plans that step.
4
+ #
5
+ # npm is never asked where its global root is: `npm root -g` answers for the
6
+ # current configuration rather than for the running tree (nvm, a custom
7
+ # prefix), and npm redacts UUID-like path segments from everything it prints.
8
+ # The tree's own location and the shim npm linked for it say where it lives.
9
+
10
+ cli_on_windows() {
11
+ case "${OSTYPE:-}" in msys*|cygwin*) return 0 ;; esac
12
+ return 1
13
+ }
14
+
15
+ # native_path <posix-path> — the spelling a native program (npm, node) needs:
16
+ # Windows form under Git Bash, unchanged elsewhere.
17
+ native_path() {
18
+ if cli_on_windows; then cygpath -w "$1"; else printf '%s' "$1"; fi
19
+ }
20
+
21
+ # quote_for_shell <string> — one shell word for a command printed to be
22
+ # copied. Windows paths carry no `$` or backtick and cmd.exe reads single
23
+ # quotes literally, so they get double quotes; elsewhere single quotes with
24
+ # an embedded quote as '\''. sed rather than ${var//…}: Bash 3.2 (macOS)
25
+ # expands a backslash-quote replacement differently.
26
+ quote_for_shell() {
27
+ if cli_on_windows; then
28
+ printf '"%s"' "$1"
29
+ else
30
+ printf "'%s'" "$(printf '%s' "$1" | sed "s/'/'\\\\''/g")"
31
+ fi
32
+ }
33
+
34
+ # cli_install_classify <pkg_dir> <pkg> <bin> — where the running tree lives.
35
+ # Sets CLI_INSTALL_KIND to npm | npx | volta | pnpm | bun | yarn | nix |
36
+ # dependency | unknown, and CLI_INSTALL_PREFIX (POSIX spelling) for npm only.
37
+ #
38
+ # npm's global layout is `<prefix>/lib/node_modules/<pkg>` on POSIX and
39
+ # `<prefix>/node_modules/<pkg>` on Windows, and a project's own node_modules
40
+ # looks exactly like the latter. The shim npm links at the prefix root is
41
+ # what proves a prefix: `<prefix>/bin/<bin>` pointing into this tree, or
42
+ # `<prefix>/<bin>.cmd` naming it. Known foreign stores are named first
43
+ # because Volta's package images are laid out like an npm prefix without
44
+ # being one.
45
+ cli_install_classify() {
46
+ local pkg_dir="$1" pkg="$2" bin="$3" prefix="" kind="unknown" shim target pkg_bs
47
+ CLI_INSTALL_KIND=""; CLI_INSTALL_PREFIX=""
48
+ case "$pkg_dir" in
49
+ */_npx/*) kind="npx" ;;
50
+ */.volta/*|*/Volta/*) kind="volta" ;;
51
+ */.pnpm/*) kind="pnpm" ;;
52
+ */.bun/install/global/*) kind="bun" ;;
53
+ */yarn/global/*|*/Yarn/Data/global/*) kind="yarn" ;;
54
+ /nix/store/*|/gnu/store/*) kind="nix" ;;
55
+ *)
56
+ if cli_on_windows; then
57
+ case "$pkg_dir" in
58
+ */node_modules/"$pkg")
59
+ prefix="${pkg_dir%/node_modules/"$pkg"}"
60
+ prefix="${prefix:-/}"
61
+ shim="$prefix/$bin.cmd"
62
+ # The cmd shim spells its target with backslashes.
63
+ pkg_bs="$(printf '%s' "$pkg" | tr '/' '\\')"
64
+ if [ ! -f "$shim" ] || ! grep -Fq "node_modules\\$pkg_bs\\bin\\" "$shim"; then
65
+ prefix=""
66
+ fi
67
+ ;;
68
+ esac
69
+ else
70
+ case "$pkg_dir" in
71
+ */lib/node_modules/"$pkg")
72
+ prefix="${pkg_dir%/lib/node_modules/"$pkg"}"
73
+ prefix="${prefix:-/}"
74
+ shim="$prefix/bin/$bin"
75
+ target=""
76
+ if [ -L "$shim" ]; then
77
+ target="$(readlink "$shim")"
78
+ case "$target" in /*) ;; *) target="$prefix/bin/$target" ;; esac
79
+ target="$(cd "$(dirname "$target")" 2>/dev/null && pwd -P || true)"
80
+ fi
81
+ [ "$target" = "$pkg_dir/bin" ] || prefix=""
82
+ ;;
83
+ esac
84
+ fi
85
+ if [ -n "$prefix" ]; then
86
+ kind="npm"
87
+ else
88
+ case "$pkg_dir" in */node_modules/"$pkg") kind="dependency" ;; esac
89
+ fi
90
+ ;;
91
+ esac
92
+ # Read by upgrade and update after the call — per-file shellcheck cannot see that.
93
+ # shellcheck disable=SC2034
94
+ CLI_INSTALL_KIND="$kind"
95
+ # shellcheck disable=SC2034
96
+ CLI_INSTALL_PREFIX="$prefix"
97
+ }
98
+
99
+ # cli_install_hint <kind> <pkg> <version-or-channel> — the command that
100
+ # upgrades a tree npm did not make, for the refusal message.
101
+ cli_install_hint() {
102
+ local kind="$1" spec="$2@$3"
103
+ case "$kind" in
104
+ npx) printf 'nothing is installed globally (this run came through npx) — install it: npm install -g %s' "$spec" ;;
105
+ volta) printf 'volta install %s' "$spec" ;;
106
+ pnpm) printf 'pnpm add -g %s' "$spec" ;;
107
+ bun) printf 'bun add -g %s' "$spec" ;;
108
+ yarn) printf 'yarn global add %s' "$spec" ;;
109
+ nix) printf 'this tree is a Nix/Guix store path — upgrade it with that package manager (%s)' "$spec" ;;
110
+ dependency) printf 'this is a project dependency — move it in package.json: npm install -D %s' "$spec" ;;
111
+ *) printf 'upgrade it with the tool that installed it (ask for %s), or pull it if it is a checkout' "$spec" ;;
112
+ esac
113
+ }
114
+
115
+ # cli_registry_version <pkg> <channel> [native-prefix] — the version the
116
+ # registry serves on that dist-tag, validated; prints nothing and fails when
117
+ # npm does not answer with one. With a prefix the lookup runs in global mode
118
+ # at that prefix — the configuration `npm install -g --prefix` will use, so
119
+ # the plan and the install cannot read different registries. `--no-json`
120
+ # overrides a `json=true` in any npmrc, which would quote the answer.
121
+ cli_registry_version() {
122
+ local pkg="$1" channel="$2" prefix="${3:-}" out
123
+ if [ -n "$prefix" ]; then
124
+ out="$(npm view --global --prefix "$prefix" --no-json "$pkg" "dist-tags.$channel" 2>/dev/null || true)"
125
+ else
126
+ out="$(npm view --no-json "$pkg" "dist-tags.$channel" 2>/dev/null || true)"
127
+ fi
128
+ out="${out//[$' \t\r\n']/}"
129
+ is_npm_version "$out" || return 1
130
+ printf '%s' "$out"
131
+ }
@@ -15,34 +15,60 @@ gitignore_path_is_ignored() {
15
15
  git -C "$root" check-ignore -q --no-index -- "$path"
16
16
  }
17
17
 
18
- # Git cannot re-include a child of an excluded directory. Add explicit parent
19
- # reinclusions, then make sure both parent and child negations occur after any
20
- # pre-existing rule that still wins. The check-ignore probe also catches broad
21
- # patterns such as an old `.claude/` or `.cursor/**`, not just exact matches.
18
+ # True when the file's last lines are exactly these, in order.
19
+ gitignore_tail_is() {
20
+ local file="$1"; shift
21
+ [ -f "$file" ] || return 1
22
+ local want tail_lines
23
+ want="$(printf '%s\n' "$@")"
24
+ tail_lines="$(tail -n "$#" -- "$file")"
25
+ [ "$tail_lines" = "$want" ]
26
+ }
27
+
28
+ # Git cannot re-include a child of an excluded directory, so an include needs an
29
+ # explicit negation for every parent. A negation only wins when it comes after
30
+ # the rule excluding the parent, and the only repair an append can make is to
31
+ # move the whole chain last — so the repair runs when, and only when, the chain
32
+ # is not already there.
33
+ #
34
+ # Probe the include TARGET, never a parent with a trailing slash: `dir/*` — the
35
+ # ignore this very policy writes — matches `dir/` because `*` matches the empty
36
+ # string, so a parent probe answers "still ignored" in the healthy end state.
37
+ # Keyed on that, the repair re-appended a line already last, which cannot change
38
+ # the answer, and the file grew by one line on every run.
22
39
  gitignore_add_effective_include() {
23
- local root="$1" value="${2#./}" dir parent="" part rc
40
+ local root="$1" value="${2#./}" dir parent="" part line rc
41
+ local -a negations=()
24
42
  dir="${value%/*}"
25
43
  if [ "$dir" != "$value" ]; then
26
44
  while IFS= read -r part; do
27
45
  [ -n "$part" ] || continue
28
46
  if [ -n "$parent" ]; then parent="$parent/$part"; else parent="$part"; fi
29
- gitignore_add_line "$root" "!$parent/"
30
- if gitignore_path_is_ignored "$root" "$parent/"; then
31
- printf '!%s/\n' "$parent" >> "$root/.gitignore"
32
- else
33
- rc=$?
34
- [ "$rc" -eq 1 ] || [ "$rc" -eq 2 ] || return "$rc"
35
- fi
47
+ negations+=("!$parent/")
36
48
  done < <(printf '%s\n' "$dir" | tr '/' '\n')
37
49
  fi
50
+ negations+=("!$value")
38
51
 
39
- gitignore_add_line "$root" "!$value"
40
- if gitignore_path_is_ignored "$root" "$value"; then
41
- printf '!%s\n' "$value" >> "$root/.gitignore"
42
- else
43
- rc=$?
44
- [ "$rc" -eq 1 ] || [ "$rc" -eq 2 ] || return "$rc"
45
- fi
52
+ for line in "${negations[@]}"; do
53
+ gitignore_add_line "$root" "$line"
54
+ done
55
+
56
+ # `|| rc=$?` keeps the probe out of `set -e`'s reach: its honest "not
57
+ # ignored" is exit 1, and a bare call would abort the whole command there.
58
+ rc=0
59
+ gitignore_path_is_ignored "$root" "$value" || rc=$?
60
+ case "$rc" in
61
+ 0) ;;
62
+ 1|2) return 0 ;;
63
+ *) return "$rc" ;;
64
+ esac
65
+
66
+ # Still ignored: an earlier negation is shadowed by a later rule. Move the
67
+ # chain last. Already last means appending cannot help — leave it, and let
68
+ # `status --check` report the re-inclusion this policy could not make
69
+ # effective rather than growing the file forever.
70
+ gitignore_tail_is "$root/.gitignore" "${negations[@]}" && return 0
71
+ printf '%s\n' "${negations[@]}" >> "$root/.gitignore"
46
72
  }
47
73
 
48
74
  ensure_gitignore_header() {
@@ -136,7 +136,7 @@ fetch_package() {
136
136
  if is_bundle_source "$url" "$ref" "$subpath" \
137
137
  && { [ -z "$locked_sha" ] || [ -z "$bundle_sha" ] || [ "$locked_sha" = "$bundle_sha" ]; }; then
138
138
  [ -n "${IS_BUNDLED_PKG_DIR:-}" ] && [ -d "$IS_BUNDLED_PKG_DIR" ] \
139
- || die "bundled engine content not found next to the CLI — reinstall @ainova-systems/intelligence"
139
+ || die "bundled engine content not found next to the CLI — reinstall ${INTELLIGENCE_NPM_PACKAGE:-the CLI package}"
140
140
  rm -rf "$dest"
141
141
  mkdir -p "$dest"
142
142
  cp -R "$IS_BUNDLED_PKG_DIR/." "$dest/"
package/cli/lib/semver.sh CHANGED
@@ -21,6 +21,72 @@ semver_cmp() {
21
21
  }'
22
22
  }
23
23
 
24
+ # semver_cmp_full <a> <b> — prints -1 / 0 / 1 with SemVer prerelease
25
+ # precedence: 1.0.0-rc.1 < 1.0.0-rc.2 < 1.0.0; build metadata is ignored.
26
+ # Package ranges never see prereleases, so semver_cmp stays digits-only; this
27
+ # one is for the CLI's own npm versions, whose `next` channel is a prerelease
28
+ # line.
29
+ semver_cmp_full() {
30
+ awk -v a="${1#v}" -v b="${2#v}" '
31
+ # Numeric identifiers compare as numbers (no leading zeros, so length
32
+ # then digits — exact beyond what a double holds) and sort before
33
+ # alphanumeric ones; those compare as ASCII strings, never as numbers
34
+ # awk might read into them (1e5).
35
+ function idcmp(x, y, xn, yn) {
36
+ xn = (x ~ /^[0-9]+$/); yn = (y ~ /^[0-9]+$/)
37
+ if (xn && yn) {
38
+ if (length(x) != length(y)) return (length(x) < length(y)) ? -1 : 1
39
+ return (x "" < y "") ? -1 : (x "" > y "") ? 1 : 0
40
+ }
41
+ if (xn) return -1
42
+ if (yn) return 1
43
+ return (x "" < y "") ? -1 : (x "" > y "") ? 1 : 0
44
+ }
45
+ BEGIN {
46
+ sub(/\+.*$/, "", a); sub(/\+.*$/, "", b)
47
+ pa = ""; pb = ""
48
+ if (i = index(a, "-")) { pa = substr(a, i + 1); a = substr(a, 1, i - 1) }
49
+ if (i = index(b, "-")) { pb = substr(b, i + 1); b = substr(b, 1, i - 1) }
50
+ na = split(a, A, "."); nb = split(b, B, ".")
51
+ for (i = 1; i <= 3; i++) {
52
+ x = (i <= na) ? A[i] + 0 : 0
53
+ y = (i <= nb) ? B[i] + 0 : 0
54
+ if (x < y) { print -1; exit }
55
+ if (x > y) { print 1; exit }
56
+ }
57
+ # A release outranks every prerelease of the same core.
58
+ if (pa == "" && pb == "") { print 0; exit }
59
+ if (pa == "") { print 1; exit }
60
+ if (pb == "") { print -1; exit }
61
+ na = split(pa, PA, "."); nb = split(pb, PB, ".")
62
+ n = (na < nb) ? na : nb
63
+ for (i = 1; i <= n; i++) {
64
+ c = idcmp(PA[i], PB[i])
65
+ if (c != 0) { print c; exit }
66
+ }
67
+ r = (na < nb) ? -1 : (na > nb) ? 1 : 0
68
+ print r
69
+ }'
70
+ }
71
+
72
+ # npm_channel_for <version> — the npm dist-tag a CLI version came from:
73
+ # `next` for a prerelease, `latest` otherwise. `update` and `upgrade` both
74
+ # derive the channel from the running version; one rule here keeps them equal.
75
+ npm_channel_for() {
76
+ case "$1" in
77
+ *-*) printf 'next' ;;
78
+ *) printf 'latest' ;;
79
+ esac
80
+ }
81
+
82
+ # is_npm_version <string> — 0 iff the string is a version as npm publishes
83
+ # it: `x.y.z`, an optional `-prerelease`, an optional `+build`, nothing else.
84
+ # A registry answer becomes an npm argument, so anything else is refused.
85
+ is_npm_version() {
86
+ local re='^[0-9]+\.[0-9]+\.[0-9]+(-[0-9A-Za-z.-]+)?(\+[0-9A-Za-z.-]+)?$'
87
+ [[ "$1" =~ $re ]]
88
+ }
89
+
24
90
  # semver_is_stable <version> — 0 iff `[v]x.y.z` with nothing else.
25
91
  semver_is_stable() {
26
92
  case "${1#v}" in
package/engine/ENGINE_SHA CHANGED
@@ -1 +1 @@
1
- b1242a74527d0295e9017fcd6b13ebfe00892710
1
+ 012f75bfb3c3036df3df9e914597ba937ab75400
package/engine/VERSION CHANGED
@@ -1 +1 @@
1
- 0.12.0
1
+ 0.13.0
@@ -126,14 +126,14 @@ check_version_compat() {
126
126
  if [ "$(_ver_major "$stamp")" -gt "$(_ver_major "$eng")" ]; then
127
127
  is_status ahead-of-engine "stamp=$stamp engine=$eng"
128
128
  echo " ERROR: project stamped $stamp but this engine is $eng — a newer major schema; refusing." >&2
129
- echo " Update the CLI first: npm i -g @ainova-systems/intelligence@latest" >&2
129
+ echo " Update the CLI first: intelligence upgrade (--next for a prerelease line)" >&2
130
130
  return "$IS_RC_AHEAD"
131
131
  fi
132
132
  # One warning per command, however many processes re-check on the way to
133
133
  # the engine (CLI preflight first, then the engine itself). The line has
134
134
  # no indent on purpose: `sync --compact` keeps `WARNING:` lines.
135
135
  if [ "${IS_SCHEMA_AHEAD_WARNED:-}" != "$stamp" ]; then
136
- echo "WARNING: project schema $stamp is newer than this CLI's engine $eng — the project uses a newer CLI; update it: npm i -g @ainova-systems/intelligence@latest" >&2
136
+ echo "WARNING: project schema $stamp is newer than this CLI's engine $eng — the project uses a newer CLI; update it: intelligence upgrade (--next for a prerelease line)" >&2
137
137
  export IS_SCHEMA_AHEAD_WARNED="$stamp"
138
138
  fi
139
139
  return 0
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ainova-systems/intelligence",
3
- "version": "0.12.0",
3
+ "version": "0.13.0",
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"
@@ -392,6 +392,7 @@ The public lifecycle is deliberately compact:
392
392
  - `intelligence init [--preview|--apply]` is universal: it creates a new setup, aligns an existing Intelligence project, or plans/applies conversion of an eligible legacy Intelligence Sync project.
393
393
  - `intelligence sync [adapter] [--compact]` first aligns an existing Intelligence project with the installed CLI, restores a missing store strictly from `intelligence.lock`, then renders. Compact mode shows context sizes, actionable warnings and final status on success, and all diagnostics on failure. In CI it refuses an alignment that would change tracked files and points to a local `intelligence init --apply` plus review/commit.
394
394
  - `intelligence update [@scope/name] [--preview|--apply]` is the only update surface. It prints the CLI/project/package plan; default mode prompts, `--preview` never writes, and `--apply` does not prompt. It never moves `ref:` pins.
395
+ - `intelligence upgrade [--next] [--preview|--apply]` replaces the installed CLI with the newest version on its npm channel (`next` for a prerelease or with `--next`, otherwise `latest`) with the same modes. It touches no project, never downgrades, and refuses an installation that npm did not make.
395
396
  - `intelligence package add|remove|list|search` owns package inventory.
396
397
  - `intelligence adapter list|create|enable|disable|remove` owns adapter inventory and target state.
397
398
  - `intelligence status [--check]` reports state; `--check` runs deep consistency checks.
@@ -28,10 +28,15 @@ result.
28
28
  mode also shows the plan and prompts; after approval, use `--apply` for an
29
29
  unambiguous non-interactive execution.
30
30
 
31
- 4. If the plan reports a newer global CLI, run exactly the npm command it
32
- prints after approval, then rerun `intelligence update --preview` with the
33
- new executable. Apply the resulting plan with `intelligence update --apply`,
34
- or `intelligence update $ARGUMENTS --apply` when one package was requested.
31
+ 4. If the plan reports a newer global CLI, run `intelligence upgrade --apply`
32
+ after approval — it installs exactly the version the plan showed — then
33
+ rerun `intelligence update --preview` with the new executable. When the
34
+ project section reports a schema stamped ahead of the stable line, the CLI
35
+ that stamped it was a prerelease: use `intelligence upgrade --next --apply`.
36
+ If `upgrade` refuses because npm did not make this installation, run the
37
+ command its message names instead. Apply the resulting plan with
38
+ `intelligence update --apply`, or `intelligence update $ARGUMENTS --apply`
39
+ when one package was requested.
35
40
 
36
41
  5. An applied update that renders must finish with `IS_STATUS=ok`; preserve and
37
42
  stop on any other status. Then run `intelligence status --check`. Do not run