loki-mode 7.28.1 → 7.29.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.
@@ -0,0 +1,249 @@
1
+ #!/usr/bin/env bash
2
+ # provider-offer.sh -- shared, self-contained provider install offer (v7.29.0).
3
+ #
4
+ # Single source of truth for "no AI provider CLI found" handling, used by BOTH
5
+ # the bash CLI (autonomy/loki, sourced) and the Bun-routed doctor
6
+ # (loki-ts/src/commands/doctor.ts, via child_process). Parity is by
7
+ # construction: there is exactly one prompt + npm install + login handoff
8
+ # implementation, and both routes call it.
9
+ #
10
+ # Self-containment contract (load-bearing for parity): this file depends ONLY
11
+ # on bash builtins + npm/claude on PATH. It defines its own colors and never
12
+ # reads $RED/$NC or any other variable owned by autonomy/loki, because when
13
+ # doctor.ts spawns it standalone those variables are unset. If this file ever
14
+ # starts depending on loki's environment, the bash-route and Bun-route bytes
15
+ # diverge and the bun-parity matrix breaks.
16
+ #
17
+ # Security posture (design 1.7): the ONLY command ever executed on the user's
18
+ # behalf is `npm install -g @anthropic-ai/claude-code`, only after explicit
19
+ # consent, with the exact command printed first. No sudo. No curl-pipe-bash.
20
+ # Non-interactive / CI contexts never run an install.
21
+
22
+ # Guard against double-source (loki may source this more than once via reloads).
23
+ if [ -n "${_LOKI_PROVIDER_OFFER_SOURCED:-}" ]; then
24
+ return 0 2>/dev/null || true
25
+ fi
26
+ _LOKI_PROVIDER_OFFER_SOURCED=1
27
+
28
+ # --- Self-contained colors (honor NO_COLOR; no dependency on loki) ----------
29
+ if [ -n "${NO_COLOR:-}" ] || [ ! -t 1 ]; then
30
+ _PO_RED=''; _PO_YELLOW=''; _PO_BOLD=''; _PO_DIM=''; _PO_NC=''
31
+ else
32
+ _PO_RED=$'\033[0;31m'
33
+ _PO_YELLOW=$'\033[1;33m'
34
+ _PO_BOLD=$'\033[1m'
35
+ _PO_NC=$'\033[0m'
36
+ fi
37
+
38
+ # The one canonical install command. Quoted everywhere; never re-derived.
39
+ _PO_INSTALL_CMD="npm install -g @anthropic-ai/claude-code"
40
+
41
+ # detect_any_provider: true (0) if any supported provider CLI is on PATH.
42
+ # Extracted verbatim from the loki doctor detection loop (design 1.2).
43
+ detect_any_provider() {
44
+ local _dp
45
+ for _dp in claude codex cline aider; do
46
+ command -v "$_dp" >/dev/null 2>&1 && return 0
47
+ done
48
+ return 1
49
+ }
50
+
51
+ # _po_assume_yes: true when the user has opted into unattended confirmation.
52
+ # Honors --yes (LOKI_AUTO_CONFIRM, set by loki:1013) and LOKI_ASSUME_YES.
53
+ _po_assume_yes() {
54
+ [ "${LOKI_ASSUME_YES:-}" = "1" ] && return 0
55
+ [ "${LOKI_AUTO_CONFIRM:-}" = "true" ] && return 0
56
+ return 1
57
+ }
58
+
59
+ # _po_non_interactive: true when we must NEVER prompt (non-TTY or CI).
60
+ # Mirrors cmd_welcome_maybe_firstrun (loki:4286) and maybe_show_auto_plan.
61
+ _po_non_interactive() {
62
+ [ ! -t 1 ] && return 0
63
+ [ ! -t 0 ] && return 0
64
+ [ -n "${CI:-}" ] && return 0
65
+ return 1
66
+ }
67
+
68
+ # _po_run_login: offer (or auto-accept) the claude auth login handoff after a
69
+ # successful install. Inherited stdio; Loki never handles credentials.
70
+ _po_run_login() {
71
+ # claude must actually be on PATH for login to make sense.
72
+ if ! command -v claude >/dev/null 2>&1; then
73
+ printf "%sInstalled, but 'claude' is not on your PATH yet. You may need to restart your shell or add npm's global bin to PATH (npm config get prefix). Run 'loki doctor' to recheck.%s\n" "$_PO_YELLOW" "$_PO_NC"
74
+ return 0
75
+ fi
76
+
77
+ local do_login=""
78
+ if _po_assume_yes; then
79
+ do_login="y"
80
+ else
81
+ printf 'Claude Code installed.\n'
82
+ printf '\n'
83
+ printf 'You still need to authenticate. Run the login flow now? [Y/n] '
84
+ read -r do_login || do_login="n"
85
+ fi
86
+ case "$do_login" in
87
+ ""|y|Y|yes|YES)
88
+ if claude auth login; then
89
+ # Do not trust the exit code alone: verify the session is
90
+ # actually authenticated before claiming readiness (council
91
+ # HIGH: the old path could falsely report success).
92
+ if claude auth status 2>/dev/null | grep -q '"loggedIn"[[:space:]]*:[[:space:]]*true'; then
93
+ printf "%sProvider ready. Run 'loki doctor' to confirm, or 'loki quickstart' to build.%s\n" "$_PO_BOLD" "$_PO_NC"
94
+ return 0
95
+ fi
96
+ printf "Login finished but authentication could not be confirmed. Run 'claude auth status' to check, then 'loki doctor'.\n"
97
+ return 0
98
+ fi
99
+ printf "Login not completed. Run 'claude auth login' when ready, then 'loki doctor'.\n"
100
+ return 0
101
+ ;;
102
+ *)
103
+ printf "Login not completed. Run 'claude auth login' when ready, then 'loki doctor'.\n"
104
+ return 0
105
+ ;;
106
+ esac
107
+ }
108
+
109
+ # _po_do_install: run the one consented command, print it first, handle result.
110
+ # Returns 0 on success, non-zero on failure (caller decides exit behavior).
111
+ _po_do_install() {
112
+ printf 'Installing Claude Code (%s) ...\n' "$_PO_INSTALL_CMD"
113
+ # The exact, fixed argv. No interpolation, no extra flags. (design 1.7)
114
+ # Capture npm's exit code directly (not via `if`, whose statement status is
115
+ # 0 when the condition is false with no else, masking the real npm code).
116
+ local code=0
117
+ npm install -g @anthropic-ai/claude-code || code=$?
118
+ if [ "$code" -eq 0 ]; then
119
+ printf '\n'
120
+ _po_run_login
121
+ return 0
122
+ fi
123
+ printf '%sInstall failed (npm exited %s). You can retry manually:%s\n' "$_PO_RED" "$code" "$_PO_NC"
124
+ printf ' %s\n' "$_PO_INSTALL_CMD"
125
+ printf 'If this is a permissions error, see https://docs.npmjs.com/resolving-eacces-permissions-errors\n'
126
+ return "$code"
127
+ }
128
+
129
+ # offer_provider_install <mode>
130
+ # mode = "report" -> doctor: append the offer on a TTY; on non-TTY/CI do
131
+ # NOTHING (doctor already printed the FAIL + install line,
132
+ # and we must keep non-TTY/json bytes identical for parity).
133
+ # Never exits the process.
134
+ # mode = "gate" -> start/demo/quick pre-flight: on non-TTY/CI print the
135
+ # honest one-liner to stderr and return 2. On a TTY, prompt;
136
+ # on decline return 2. On accept install + login.
137
+ #
138
+ # Honors:
139
+ # LOKI_NO_INSTALL_OFFER=1 -> never prompt; print manual command (1.4)
140
+ # --yes / LOKI_ASSUME_YES -> auto-accept install + login (1.4)
141
+ offer_provider_install() {
142
+ local mode="${1:-gate}"
143
+
144
+ # Opt-out: never offer, just surface the manual command.
145
+ if [ "${LOKI_NO_INSTALL_OFFER:-}" = "1" ]; then
146
+ if [ "$mode" = "gate" ]; then
147
+ printf 'No AI provider CLI found. Install one when ready:\n' >&2
148
+ printf ' %s (then: claude auth login)\n' "$_PO_INSTALL_CMD" >&2
149
+ return 2
150
+ fi
151
+ printf '\n'
152
+ printf 'Install a provider when ready:\n'
153
+ printf ' %s (then: claude auth login)\n' "$_PO_INSTALL_CMD"
154
+ printf ' Other supported providers: codex, cline, aider.\n'
155
+ return 0
156
+ fi
157
+
158
+ # Non-interactive / CI: NEVER prompt, NEVER install.
159
+ #
160
+ # gate (start/demo/quick): print the honest one-liner to stderr and return 2
161
+ # so the caller exits with an actionable message before any spend.
162
+ # report (doctor): stay SILENT. doctor has already printed the FAIL line and
163
+ # the install command on stdout, so no information is lost. Silence here is
164
+ # load-bearing for parity: doctor.ts gates its child_process bridge on
165
+ # process.stdout.isTTY, so on a non-TTY/CI run the Bun route emits nothing
166
+ # extra. If report-mode printed a stderr line, the bash route would diverge
167
+ # from Bun in exactly the no-provider/non-TTY case the bun-parity matrix
168
+ # captures (2>&1) on CI runners, which have no provider installed.
169
+ if _po_non_interactive; then
170
+ if [ "$mode" = "gate" ]; then
171
+ printf 'No AI provider CLI found; cannot prompt to install in a non-interactive shell. Run: %s\n' "$_PO_INSTALL_CMD" >&2
172
+ return 2
173
+ fi
174
+ return 0
175
+ fi
176
+
177
+ # npm missing: degraded path, never attempt a non-npm install.
178
+ if ! command -v npm >/dev/null 2>&1; then
179
+ printf '\n'
180
+ printf '%sNo AI provider CLI was found, and npm is not installed either, so Loki%s\n' "$_PO_BOLD" "$_PO_NC"
181
+ printf 'cannot install one for you.\n'
182
+ printf '\n'
183
+ printf 'Install Node.js + npm first (https://nodejs.org), then run:\n'
184
+ printf ' %s\n' "$_PO_INSTALL_CMD"
185
+ printf ' claude auth login\n'
186
+ printf '\n'
187
+ printf "Already have a provider via another method? Make sure 'claude' (or codex,\n"
188
+ printf "cline, aider) is on your PATH, then run 'loki doctor'.\n"
189
+ [ "$mode" = "gate" ] && return 2
190
+ return 0
191
+ fi
192
+
193
+ # TTY, npm present: the interactive offer.
194
+ printf '\n'
195
+ printf 'No AI provider CLI was found. Loki needs one agent CLI to run a build.\n'
196
+ printf '\n'
197
+ printf 'Claude Code is the recommended provider (full feature support).\n'
198
+ printf ' Install: %s\n' "$_PO_INSTALL_CMD"
199
+ printf ' Then: claude auth login\n'
200
+ printf '\n'
201
+
202
+ local answer=""
203
+ if _po_assume_yes; then
204
+ answer="y"
205
+ else
206
+ printf 'Install Claude Code now? [Y/n] '
207
+ read -r answer || answer="n"
208
+ fi
209
+
210
+ case "$answer" in
211
+ ""|y|Y|yes|YES)
212
+ if _po_do_install; then
213
+ return 0
214
+ fi
215
+ # Install failed: honest failure already printed by _po_do_install.
216
+ [ "$mode" = "gate" ] && return 2
217
+ return 1
218
+ ;;
219
+ *)
220
+ printf 'Skipped. Install a provider when ready:\n'
221
+ printf ' %s (then: claude auth login)\n' "$_PO_INSTALL_CMD"
222
+ printf 'Other supported providers: codex, cline, aider.\n'
223
+ [ "$mode" = "gate" ] && return 2
224
+ return 0
225
+ ;;
226
+ esac
227
+ }
228
+
229
+ # provider_offer_gate: convenience wrapper for the start/demo/quick pre-flight.
230
+ # Returns 0 if a provider is present (or one was just installed); returns 2 to
231
+ # signal the caller should `exit 2` (no provider, declined or non-interactive).
232
+ provider_offer_gate() {
233
+ detect_any_provider && return 0
234
+ offer_provider_install gate || return 2
235
+ # After an accepted install, re-detect; if still absent, fail the gate.
236
+ detect_any_provider && return 0
237
+ return 2
238
+ }
239
+
240
+ # Executed directly (doctor.ts child_process bridge, or manual): run the offer.
241
+ # When sourced by autonomy/loki, this block does not run.
242
+ if [ "${BASH_SOURCE[0]}" = "$0" ]; then
243
+ case "${1:-report}" in
244
+ offer|report) offer_provider_install report ;;
245
+ gate) offer_provider_install gate ;;
246
+ detect) detect_any_provider ;;
247
+ *) offer_provider_install report ;;
248
+ esac
249
+ fi