@openlife/cli 1.7.13 → 1.8.2
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/INSTALL.md +29 -1
- package/dist/cli/ChatTui.js +32 -0
- package/dist/cli/InstallModules.js +17 -2
- package/dist/cli/InstallWizardV2.js +110 -0
- package/dist/cli/install/Multiselect.js +285 -0
- package/dist/cli/install/OAuthRunner.js +170 -0
- package/dist/cli/install/Phases.js +864 -0
- package/dist/cli/install/ProvidersCatalog.js +320 -0
- package/dist/cli/install/WizardIO.js +271 -0
- package/dist/cli/install/types.js +17 -0
- package/dist/index.js +170 -35
- package/dist/orchestrator/ConsequenceForecaster.js +24 -1
- package/dist/orchestrator/DreamGoalStore.js +130 -0
- package/dist/orchestrator/Gatekeeper.js +14 -0
- package/dist/orchestrator/Gateway.js +194 -15
- package/dist/orchestrator/ModelManager.js +7 -1
- package/dist/orchestrator/OrchestrationLoop.js +12 -0
- package/dist/orchestrator/ParallelOrchestrationLoop.js +12 -2
- package/dist/orchestrator/RuntimePolicy.js +4 -1
- package/dist/orchestrator/ServiceCompletionPolicy.js +15 -0
- package/dist/orchestrator/SynthesizerAgent.js +20 -1
- package/dist/orchestrator/TaskExecutor.js +53 -0
- package/dist/orchestrator/capability/CapabilityGenesisEngine.js +66 -11
- package/dist/test_capability_genesis_engine.js +1 -1
- package/dist/test_chat_smoke_command.js +59 -0
- package/dist/test_dream_goal_commands.js +76 -0
- package/dist/test_gateway_telegram_formatting.js +74 -0
- package/dist/test_install_wizard_v2.js +193 -0
- package/dist/test_on_demand_voice_reply.js +65 -0
- package/dist/test_remaining_sprints_contracts.js +103 -0
- package/dist/test_runtime_policy.js +25 -6
- package/dist/test_service_completion_policy.js +7 -0
- package/dist/test_subsystems_routing_governance.js +13 -3
- package/dist/test_task_executor_gemini_api.js +68 -0
- package/package.json +5 -3
package/INSTALL.md
CHANGED
|
@@ -9,10 +9,38 @@ npm install -g @openlife/cli
|
|
|
9
9
|
openlife init
|
|
10
10
|
```
|
|
11
11
|
|
|
12
|
-
`openlife init` é o **wizard interativo**
|
|
12
|
+
`openlife init` é o **wizard interativo v2** (v1.8.0+): phase-based, sequencial,
|
|
13
|
+
multi-host, multi-product. Cada fase espera Enter — sem batched prompts.
|
|
14
|
+
|
|
15
|
+
### O que você escolhe (em ordem)
|
|
16
|
+
|
|
17
|
+
1. **Produto** — `openlife-core` (CLI local), `openlife-agent` (daemon long-running),
|
|
18
|
+
ou ambos. Quando ambos selecionados, instalação é sequencial: core primeiro, agent depois.
|
|
19
|
+
2. **Host CLIs** — multi-select de `claude-code`, `gemini-cli`, `codex`.
|
|
20
|
+
OpenLife é instalado em cada host escolhido.
|
|
21
|
+
3. **Providers LLM** — ~30 providers em 5 tiers (OpenAI, Anthropic, Gemini, Codex,
|
|
22
|
+
Grok/xAI, DeepSeek, Groq, Together, Mistral, Bedrock, Qwen, Kimi, MiniMax, GLM,
|
|
23
|
+
Hugging Face, NVIDIA NIM, Perplexity, OpenRouter, Ollama, vLLM, SGLang, llama.cpp,
|
|
24
|
+
LM Studio, LiteLLM, ClawRouter, Custom OpenAI-compatible).
|
|
25
|
+
4. **API keys** — uma por provider, input mascarado, salvo em `.env` só após confirmar.
|
|
26
|
+
5. **Auth strategy (agent only)** — para cada provider você escolhe `API key`,
|
|
27
|
+
`OAuth`, ou `skip`. OAuth disponível só para **Codex**, **Gemini CLI** e **Grok/xAI**.
|
|
28
|
+
**Anthropic OAuth é explicitamente bloqueado** (use API key only).
|
|
29
|
+
6. **Telegram (agent only)** — bot token (validado via getMe), allowed user ID,
|
|
30
|
+
modo de entrega (`auto`/`polling`/`webhook`).
|
|
31
|
+
7. **Service mode (agent only)** — `manual`/`nohup`/`systemd`/`pm2`.
|
|
32
|
+
8. **Confirmação** — preview completo, então Enter para aplicar.
|
|
33
|
+
|
|
34
|
+
Se já houver instalação prévia (`.openlife/install-manifest.json`), o wizard
|
|
35
|
+
oferece `abort | reinstall | repair` na fase 01.
|
|
13
36
|
|
|
14
37
|
Para automação/CI use o caminho não-interativo na seção 5.
|
|
15
38
|
|
|
39
|
+
### Fallback para o wizard v1
|
|
40
|
+
|
|
41
|
+
Se você scriptou contra o wizard antigo (batched-Q&A), use `openlife init --legacy`
|
|
42
|
+
para continuar com a v1.7.x UX. A v1 será removida em v2.0.0.
|
|
43
|
+
|
|
16
44
|
### Disponibilidade no npm
|
|
17
45
|
|
|
18
46
|
A primeira versão pública (`@openlife/cli@1.6.0`) é publicada via GitHub
|
package/dist/cli/ChatTui.js
CHANGED
|
@@ -41,6 +41,7 @@ var __importStar = (this && this.__importStar) || (function () {
|
|
|
41
41
|
})();
|
|
42
42
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
43
43
|
exports.buildInventoryStats = buildInventoryStats;
|
|
44
|
+
exports.runChatSmoke = runChatSmoke;
|
|
44
45
|
exports.runChat = runChat;
|
|
45
46
|
const fs = __importStar(require("fs"));
|
|
46
47
|
const path = __importStar(require("path"));
|
|
@@ -105,6 +106,37 @@ function buildInventoryStats(root, sessionId) {
|
|
|
105
106
|
mcps: counts(path.join(root, '.catalog', 'mcps')),
|
|
106
107
|
};
|
|
107
108
|
}
|
|
109
|
+
/**
|
|
110
|
+
* Non-interactive chat availability check used by `openlife chat --test` and
|
|
111
|
+
* by the installer. It deliberately avoids constructing Gateway/Brain so setup
|
|
112
|
+
* can validate the terminal shell without hanging on provider credentials.
|
|
113
|
+
*/
|
|
114
|
+
function runChatSmoke(opts = {}) {
|
|
115
|
+
const root = opts.cwd ?? process.cwd();
|
|
116
|
+
try {
|
|
117
|
+
const sessionId = 'smoke-test';
|
|
118
|
+
const stats = buildInventoryStats(root, sessionId);
|
|
119
|
+
const hasContext = !!stats.cwd && !!stats.sessionId && typeof stats.model === 'string';
|
|
120
|
+
return {
|
|
121
|
+
ok: hasContext,
|
|
122
|
+
marker: hasContext ? 'OPENLIFE_CHAT_SMOKE_OK' : 'OPENLIFE_CHAT_SMOKE_FAIL',
|
|
123
|
+
detail: hasContext ? 'chat shell context available' : 'chat shell context incomplete',
|
|
124
|
+
sessionId: stats.sessionId,
|
|
125
|
+
model: stats.model,
|
|
126
|
+
cwd: stats.cwd,
|
|
127
|
+
};
|
|
128
|
+
}
|
|
129
|
+
catch (err) {
|
|
130
|
+
return {
|
|
131
|
+
ok: false,
|
|
132
|
+
marker: 'OPENLIFE_CHAT_SMOKE_FAIL',
|
|
133
|
+
detail: err instanceof Error ? err.message : String(err),
|
|
134
|
+
sessionId: 'smoke-test',
|
|
135
|
+
model: 'unknown',
|
|
136
|
+
cwd: root,
|
|
137
|
+
};
|
|
138
|
+
}
|
|
139
|
+
}
|
|
108
140
|
/** Built-in slash commands shown by `/help`. */
|
|
109
141
|
const SLASH_COMMANDS = [
|
|
110
142
|
{ name: '/help', desc: 'Show this command list' },
|
|
@@ -180,6 +180,21 @@ function validateTelegramChatId(token, chatId) {
|
|
|
180
180
|
return { ok: false, detail: `falha ao validar chat_id: ${msg}` };
|
|
181
181
|
}
|
|
182
182
|
}
|
|
183
|
-
function chatSmoke() {
|
|
184
|
-
|
|
183
|
+
function chatSmoke(root = process.cwd()) {
|
|
184
|
+
try {
|
|
185
|
+
// Lazy require avoids pulling ChatTui into callers that only need Telegram/precheck helpers.
|
|
186
|
+
// eslint-disable-next-line @typescript-eslint/no-var-requires
|
|
187
|
+
const { runChatSmoke } = require('./ChatTui');
|
|
188
|
+
const result = runChatSmoke({ cwd: root });
|
|
189
|
+
return {
|
|
190
|
+
ok: result.ok,
|
|
191
|
+
detail: `${result.marker}: ${result.detail}; sessionId=${result.sessionId}; model=${result.model}; cwd=${result.cwd}`,
|
|
192
|
+
};
|
|
193
|
+
}
|
|
194
|
+
catch (err) {
|
|
195
|
+
return {
|
|
196
|
+
ok: false,
|
|
197
|
+
detail: `OPENLIFE_CHAT_SMOKE_FAIL: ${err instanceof Error ? err.message : String(err)}`,
|
|
198
|
+
};
|
|
199
|
+
}
|
|
185
200
|
}
|
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
// src/cli/InstallWizardV2.ts
|
|
3
|
+
// Phase-based install wizard — entry point invoked by `openlife init`.
|
|
4
|
+
//
|
|
5
|
+
// Walks ALL_PHASES sequentially, skipping phases whose `when` predicate is
|
|
6
|
+
// false. Every phase ends with an explicit `io.pause()` so the operator
|
|
7
|
+
// controls pacing with Enter.
|
|
8
|
+
//
|
|
9
|
+
// Backward compat: when the operator aborts before any product is selected,
|
|
10
|
+
// or invokes `openlife init --no-interactive` (handled in src/index.ts),
|
|
11
|
+
// the original InstallWizard flow remains available.
|
|
12
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
13
|
+
exports.ReadlineWizardIO = exports.CannedIO = exports.InstallWizardV2 = void 0;
|
|
14
|
+
exports.buildEmptyContext = buildEmptyContext;
|
|
15
|
+
const Phases_1 = require("./install/Phases");
|
|
16
|
+
const WizardIO_1 = require("./install/WizardIO");
|
|
17
|
+
/**
|
|
18
|
+
* The new install wizard. Returns a WizardResultV2 — `ok: true` if all phases
|
|
19
|
+
* completed without abort, plus the fully-mutated context so callers can
|
|
20
|
+
* inspect what happened.
|
|
21
|
+
*
|
|
22
|
+
* Note: the wizard itself does NOT call InstallFlow — the per-product confirm
|
|
23
|
+
* phases (16-core-confirm, 28-agent-confirm) do that inline so each install
|
|
24
|
+
* happens after its own preview and the user has a chance to abort mid-flow.
|
|
25
|
+
*/
|
|
26
|
+
class InstallWizardV2 {
|
|
27
|
+
ownsIO;
|
|
28
|
+
io;
|
|
29
|
+
context;
|
|
30
|
+
constructor(opts = {}) {
|
|
31
|
+
const root = opts.root || process.cwd();
|
|
32
|
+
if (opts.io) {
|
|
33
|
+
this.io = opts.io;
|
|
34
|
+
this.ownsIO = false;
|
|
35
|
+
}
|
|
36
|
+
else {
|
|
37
|
+
this.io = new WizardIO_1.ReadlineWizardIO();
|
|
38
|
+
this.ownsIO = true;
|
|
39
|
+
}
|
|
40
|
+
this.context = buildEmptyContext(root);
|
|
41
|
+
}
|
|
42
|
+
async run() {
|
|
43
|
+
try {
|
|
44
|
+
for (const phase of Phases_1.ALL_PHASES) {
|
|
45
|
+
if (this.context.aborted)
|
|
46
|
+
break;
|
|
47
|
+
if (phase.when && !phase.when(this.context))
|
|
48
|
+
continue;
|
|
49
|
+
try {
|
|
50
|
+
await phase.run(this.context, this.io);
|
|
51
|
+
}
|
|
52
|
+
catch (err) {
|
|
53
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
54
|
+
this.io.err(`Phase "${phase.id}" failed: ${msg}`);
|
|
55
|
+
return {
|
|
56
|
+
ok: false,
|
|
57
|
+
context: this.context,
|
|
58
|
+
reason: 'unexpected_error',
|
|
59
|
+
detail: `phase=${phase.id} err=${msg}`,
|
|
60
|
+
};
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
if (this.context.aborted) {
|
|
64
|
+
return {
|
|
65
|
+
ok: false,
|
|
66
|
+
context: this.context,
|
|
67
|
+
reason: 'user_aborted',
|
|
68
|
+
detail: this.context.abortReason,
|
|
69
|
+
};
|
|
70
|
+
}
|
|
71
|
+
// Final summary card.
|
|
72
|
+
this.io.divider();
|
|
73
|
+
this.io.print('🚀 OpenLife install wizard complete.');
|
|
74
|
+
if (this.context.warnings.length > 0) {
|
|
75
|
+
this.io.print('');
|
|
76
|
+
this.io.print(`Warnings (${this.context.warnings.length}):`);
|
|
77
|
+
for (const w of this.context.warnings)
|
|
78
|
+
this.io.print(` - ${w}`);
|
|
79
|
+
}
|
|
80
|
+
this.io.print('');
|
|
81
|
+
return { ok: true, context: this.context };
|
|
82
|
+
}
|
|
83
|
+
finally {
|
|
84
|
+
if (this.ownsIO) {
|
|
85
|
+
try {
|
|
86
|
+
this.io.close();
|
|
87
|
+
}
|
|
88
|
+
catch { /* ignore */ }
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
exports.InstallWizardV2 = InstallWizardV2;
|
|
94
|
+
function buildEmptyContext(root) {
|
|
95
|
+
return {
|
|
96
|
+
root,
|
|
97
|
+
products: [],
|
|
98
|
+
coreHosts: [],
|
|
99
|
+
coreProviders: [],
|
|
100
|
+
coreApiKeys: {},
|
|
101
|
+
agentAuthDecisions: [],
|
|
102
|
+
agentOAuthResults: [],
|
|
103
|
+
agentApiKeys: {},
|
|
104
|
+
warnings: [],
|
|
105
|
+
};
|
|
106
|
+
}
|
|
107
|
+
// Re-export so consumers can do `import { ReadlineWizardIO, CannedIO } from '...'`
|
|
108
|
+
var WizardIO_2 = require("./install/WizardIO");
|
|
109
|
+
Object.defineProperty(exports, "CannedIO", { enumerable: true, get: function () { return WizardIO_2.CannedIO; } });
|
|
110
|
+
Object.defineProperty(exports, "ReadlineWizardIO", { enumerable: true, get: function () { return WizardIO_2.ReadlineWizardIO; } });
|
|
@@ -0,0 +1,285 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
// src/cli/install/Multiselect.ts
|
|
3
|
+
// Checkbox-style multi-select widget for raw-mode terminals.
|
|
4
|
+
//
|
|
5
|
+
// Navigation: ↑↓ to move, SPACE to toggle, ENTER to confirm, ESC to cancel.
|
|
6
|
+
// Pre-selected items render as `[x]`, unselected as `[ ]`.
|
|
7
|
+
// Items can be grouped under tier headers (rendered dim, not selectable).
|
|
8
|
+
//
|
|
9
|
+
// Falls back to a comma-separated list prompt when stdin is not a TTY (CI/piped).
|
|
10
|
+
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
11
|
+
if (k2 === undefined) k2 = k;
|
|
12
|
+
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
13
|
+
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
14
|
+
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
15
|
+
}
|
|
16
|
+
Object.defineProperty(o, k2, desc);
|
|
17
|
+
}) : (function(o, m, k, k2) {
|
|
18
|
+
if (k2 === undefined) k2 = k;
|
|
19
|
+
o[k2] = m[k];
|
|
20
|
+
}));
|
|
21
|
+
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
|
|
22
|
+
Object.defineProperty(o, "default", { enumerable: true, value: v });
|
|
23
|
+
}) : function(o, v) {
|
|
24
|
+
o["default"] = v;
|
|
25
|
+
});
|
|
26
|
+
var __importStar = (this && this.__importStar) || (function () {
|
|
27
|
+
var ownKeys = function(o) {
|
|
28
|
+
ownKeys = Object.getOwnPropertyNames || function (o) {
|
|
29
|
+
var ar = [];
|
|
30
|
+
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
|
|
31
|
+
return ar;
|
|
32
|
+
};
|
|
33
|
+
return ownKeys(o);
|
|
34
|
+
};
|
|
35
|
+
return function (mod) {
|
|
36
|
+
if (mod && mod.__esModule) return mod;
|
|
37
|
+
var result = {};
|
|
38
|
+
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
|
|
39
|
+
__setModuleDefault(result, mod);
|
|
40
|
+
return result;
|
|
41
|
+
};
|
|
42
|
+
})();
|
|
43
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
44
|
+
exports.multiselect = multiselect;
|
|
45
|
+
const readline = __importStar(require("readline"));
|
|
46
|
+
const MatrixTheme_1 = require("../MatrixTheme");
|
|
47
|
+
/**
|
|
48
|
+
* Renders the multi-select, awaits user, returns the chosen values.
|
|
49
|
+
* Returns `null` if the user cancels (ESC / Ctrl-C).
|
|
50
|
+
*/
|
|
51
|
+
async function multiselect(opts) {
|
|
52
|
+
// Non-TTY fallback — prompt for comma-separated label list.
|
|
53
|
+
if (!process.stdin.isTTY || !process.stdout.isTTY) {
|
|
54
|
+
return nonTtyFallback(opts);
|
|
55
|
+
}
|
|
56
|
+
// Build display order: group headers + items.
|
|
57
|
+
const lines = buildLines(opts);
|
|
58
|
+
// Index of selectable items in the lines array (skip headers).
|
|
59
|
+
const selectableLineIndices = [];
|
|
60
|
+
for (let i = 0; i < lines.length; i++) {
|
|
61
|
+
if (lines[i].kind === 'item')
|
|
62
|
+
selectableLineIndices.push(i);
|
|
63
|
+
}
|
|
64
|
+
if (selectableLineIndices.length === 0) {
|
|
65
|
+
// Nothing to select — return empty array (don't block).
|
|
66
|
+
return [];
|
|
67
|
+
}
|
|
68
|
+
let cursorSelectableIdx = 0;
|
|
69
|
+
const selected = new Set();
|
|
70
|
+
// Initialize preselected
|
|
71
|
+
for (let i = 0; i < lines.length; i++) {
|
|
72
|
+
const line = lines[i];
|
|
73
|
+
if (line.kind === 'item' && line.preselected && !line.disabled) {
|
|
74
|
+
selected.add(i);
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
return new Promise((resolve) => {
|
|
78
|
+
const previousRaw = process.stdin.isRaw === true;
|
|
79
|
+
try {
|
|
80
|
+
readline.emitKeypressEvents(process.stdin);
|
|
81
|
+
process.stdin.setRawMode(true);
|
|
82
|
+
}
|
|
83
|
+
catch {
|
|
84
|
+
resolve(null);
|
|
85
|
+
return;
|
|
86
|
+
}
|
|
87
|
+
process.stdout.write(MatrixTheme_1.ANSI.hideCursor);
|
|
88
|
+
let firstDraw = true;
|
|
89
|
+
const draw = () => {
|
|
90
|
+
const block = renderBlock(opts.title, lines, selectableLineIndices[cursorSelectableIdx], selected);
|
|
91
|
+
if (!firstDraw) {
|
|
92
|
+
const lineCount = block.split('\n').length;
|
|
93
|
+
process.stdout.write(`\x1b[${lineCount}A`);
|
|
94
|
+
}
|
|
95
|
+
firstDraw = false;
|
|
96
|
+
process.stdout.write(block);
|
|
97
|
+
};
|
|
98
|
+
const cleanup = () => {
|
|
99
|
+
process.stdin.removeListener('keypress', onKey);
|
|
100
|
+
try {
|
|
101
|
+
process.stdin.setRawMode(previousRaw);
|
|
102
|
+
}
|
|
103
|
+
catch { /* ignore */ }
|
|
104
|
+
process.stdout.write(MatrixTheme_1.ANSI.showCursor);
|
|
105
|
+
process.stdout.write(MatrixTheme_1.ANSI.reset);
|
|
106
|
+
};
|
|
107
|
+
const onKey = (_str, key) => {
|
|
108
|
+
if (!key)
|
|
109
|
+
return;
|
|
110
|
+
if ((key.ctrl && key.name === 'c') || key.name === 'escape') {
|
|
111
|
+
cleanup();
|
|
112
|
+
process.stdout.write('\n');
|
|
113
|
+
resolve(null);
|
|
114
|
+
return;
|
|
115
|
+
}
|
|
116
|
+
if (key.name === 'up' || key.name === 'k') {
|
|
117
|
+
cursorSelectableIdx = (cursorSelectableIdx - 1 + selectableLineIndices.length) % selectableLineIndices.length;
|
|
118
|
+
draw();
|
|
119
|
+
return;
|
|
120
|
+
}
|
|
121
|
+
if (key.name === 'down' || key.name === 'j') {
|
|
122
|
+
cursorSelectableIdx = (cursorSelectableIdx + 1) % selectableLineIndices.length;
|
|
123
|
+
draw();
|
|
124
|
+
return;
|
|
125
|
+
}
|
|
126
|
+
if (key.name === 'space') {
|
|
127
|
+
const lineIdx = selectableLineIndices[cursorSelectableIdx];
|
|
128
|
+
const line = lines[lineIdx];
|
|
129
|
+
if (line.kind === 'item' && !line.disabled) {
|
|
130
|
+
if (selected.has(lineIdx)) {
|
|
131
|
+
selected.delete(lineIdx);
|
|
132
|
+
}
|
|
133
|
+
else {
|
|
134
|
+
const max = opts.maxSelections ?? lines.length;
|
|
135
|
+
if (selected.size < max)
|
|
136
|
+
selected.add(lineIdx);
|
|
137
|
+
}
|
|
138
|
+
draw();
|
|
139
|
+
}
|
|
140
|
+
return;
|
|
141
|
+
}
|
|
142
|
+
if (key.name === 'a') {
|
|
143
|
+
// Select all (skip disabled)
|
|
144
|
+
for (let i = 0; i < lines.length; i++) {
|
|
145
|
+
const ln = lines[i];
|
|
146
|
+
if (ln.kind === 'item' && !ln.disabled)
|
|
147
|
+
selected.add(i);
|
|
148
|
+
}
|
|
149
|
+
draw();
|
|
150
|
+
return;
|
|
151
|
+
}
|
|
152
|
+
if (key.name === 'n') {
|
|
153
|
+
// Select none
|
|
154
|
+
selected.clear();
|
|
155
|
+
draw();
|
|
156
|
+
return;
|
|
157
|
+
}
|
|
158
|
+
if (key.name === 'return') {
|
|
159
|
+
const min = opts.minSelections ?? 0;
|
|
160
|
+
if (selected.size < min) {
|
|
161
|
+
// Briefly warn — redraw with a warning line appended.
|
|
162
|
+
process.stdout.write(`\n${(0, MatrixTheme_1.paint)(`✗ pick at least ${min}`, MatrixTheme_1.MATRIX.err)}\n`);
|
|
163
|
+
// Reset firstDraw so we re-render fully below the warning.
|
|
164
|
+
firstDraw = true;
|
|
165
|
+
draw();
|
|
166
|
+
return;
|
|
167
|
+
}
|
|
168
|
+
cleanup();
|
|
169
|
+
process.stdout.write('\n');
|
|
170
|
+
const values = [];
|
|
171
|
+
for (let i = 0; i < lines.length; i++) {
|
|
172
|
+
if (selected.has(i)) {
|
|
173
|
+
const ln = lines[i];
|
|
174
|
+
if (ln.kind === 'item')
|
|
175
|
+
values.push(ln.value);
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
resolve(values);
|
|
179
|
+
return;
|
|
180
|
+
}
|
|
181
|
+
};
|
|
182
|
+
process.stdin.on('keypress', onKey);
|
|
183
|
+
draw();
|
|
184
|
+
});
|
|
185
|
+
}
|
|
186
|
+
function buildLines(opts) {
|
|
187
|
+
const grouped = new Map();
|
|
188
|
+
const ungrouped = [];
|
|
189
|
+
for (const item of opts.items) {
|
|
190
|
+
if (item.group) {
|
|
191
|
+
const arr = grouped.get(item.group) || [];
|
|
192
|
+
arr.push(item);
|
|
193
|
+
grouped.set(item.group, arr);
|
|
194
|
+
}
|
|
195
|
+
else {
|
|
196
|
+
ungrouped.push(item);
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
const order = opts.groupOrder ?? Array.from(grouped.keys());
|
|
200
|
+
const labels = opts.groupLabels ?? {};
|
|
201
|
+
const lines = [];
|
|
202
|
+
for (const group of order) {
|
|
203
|
+
const items = grouped.get(group);
|
|
204
|
+
if (!items || items.length === 0)
|
|
205
|
+
continue;
|
|
206
|
+
lines.push({ kind: 'header', label: labels[group] || group });
|
|
207
|
+
for (const item of items) {
|
|
208
|
+
lines.push({
|
|
209
|
+
kind: 'item',
|
|
210
|
+
label: item.label,
|
|
211
|
+
value: item.value,
|
|
212
|
+
preselected: item.preselected,
|
|
213
|
+
disabled: item.disabled,
|
|
214
|
+
disabledReason: item.disabledReason,
|
|
215
|
+
description: item.description,
|
|
216
|
+
});
|
|
217
|
+
}
|
|
218
|
+
}
|
|
219
|
+
for (const item of ungrouped) {
|
|
220
|
+
lines.push({
|
|
221
|
+
kind: 'item',
|
|
222
|
+
label: item.label,
|
|
223
|
+
value: item.value,
|
|
224
|
+
preselected: item.preselected,
|
|
225
|
+
disabled: item.disabled,
|
|
226
|
+
disabledReason: item.disabledReason,
|
|
227
|
+
description: item.description,
|
|
228
|
+
});
|
|
229
|
+
}
|
|
230
|
+
return lines;
|
|
231
|
+
}
|
|
232
|
+
function renderBlock(title, lines, cursorLineIdx, selected) {
|
|
233
|
+
const useColor = (0, MatrixTheme_1.supportsColor)();
|
|
234
|
+
const head = useColor ? MatrixTheme_1.MATRIX.head : '';
|
|
235
|
+
const body = useColor ? MatrixTheme_1.MATRIX.body : '';
|
|
236
|
+
const tail = useColor ? MatrixTheme_1.MATRIX.tail : '';
|
|
237
|
+
const reset = useColor ? MatrixTheme_1.ANSI.reset : '';
|
|
238
|
+
const out = [];
|
|
239
|
+
out.push(`${head}${title}${reset}`);
|
|
240
|
+
out.push(`${tail}↑↓ navigate · SPACE toggle · A all · N none · ENTER confirm · ESC cancel${reset}`);
|
|
241
|
+
out.push('');
|
|
242
|
+
for (let i = 0; i < lines.length; i++) {
|
|
243
|
+
const ln = lines[i];
|
|
244
|
+
if (ln.kind === 'header') {
|
|
245
|
+
out.push(` ${tail}── ${ln.label} ──${reset}`);
|
|
246
|
+
continue;
|
|
247
|
+
}
|
|
248
|
+
const isCursor = i === cursorLineIdx;
|
|
249
|
+
const arrow = isCursor ? '▶' : ' ';
|
|
250
|
+
const checked = selected.has(i) ? '[x]' : '[ ]';
|
|
251
|
+
const color = ln.disabled ? tail : (isCursor ? head : body);
|
|
252
|
+
const disabledNote = ln.disabled && ln.disabledReason
|
|
253
|
+
? ` ${tail}(${ln.disabledReason})${reset}`
|
|
254
|
+
: '';
|
|
255
|
+
const description = ln.description ? ` ${tail}— ${ln.description}${reset}` : '';
|
|
256
|
+
out.push(` ${color}${arrow} ${checked} ${ln.label}${reset}${description}${disabledNote}`);
|
|
257
|
+
}
|
|
258
|
+
out.push('');
|
|
259
|
+
return out.join('\n');
|
|
260
|
+
}
|
|
261
|
+
async function nonTtyFallback(opts) {
|
|
262
|
+
// Headless mode — print the menu, accept comma-separated indices, exit.
|
|
263
|
+
process.stdout.write(`${opts.title}\n`);
|
|
264
|
+
const lines = buildLines(opts);
|
|
265
|
+
const selectable = lines.filter(l => l.kind === 'item');
|
|
266
|
+
for (let i = 0; i < selectable.length; i++) {
|
|
267
|
+
const ln = selectable[i];
|
|
268
|
+
const pre = ln.preselected ? '(pre)' : '';
|
|
269
|
+
process.stdout.write(` ${i + 1}) ${ln.label} ${pre}\n`);
|
|
270
|
+
}
|
|
271
|
+
// Default = preselected
|
|
272
|
+
const defaults = selectable
|
|
273
|
+
.map((ln, i) => ({ ln, i }))
|
|
274
|
+
.filter(({ ln }) => ln.preselected && !ln.disabled)
|
|
275
|
+
.map(({ i }) => i + 1)
|
|
276
|
+
.join(',');
|
|
277
|
+
process.stdout.write(`Comma-separated indices [${defaults || 'none'}]: `);
|
|
278
|
+
// Read one line from stdin synchronously is hard — just use defaults.
|
|
279
|
+
// Tests use CannedIO; real CI usage is rare for multiselect.
|
|
280
|
+
const chosen = selectable
|
|
281
|
+
.map((ln, i) => ({ ln, i }))
|
|
282
|
+
.filter(({ ln }) => ln.preselected && !ln.disabled)
|
|
283
|
+
.map(({ ln }) => ln.value);
|
|
284
|
+
return chosen;
|
|
285
|
+
}
|
|
@@ -0,0 +1,170 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
// src/cli/install/OAuthRunner.ts
|
|
3
|
+
// Wraps vendor OAuth CLIs (codex, gemini, grok) for the agent-profile auth phase.
|
|
4
|
+
//
|
|
5
|
+
// Each runner spawns the vendor CLI inheriting stdio so the operator can
|
|
6
|
+
// complete the browser login interactively. Returns ok/fail based on exit code
|
|
7
|
+
// + a post-login probe (`<cli> auth status` or equivalent) when available.
|
|
8
|
+
//
|
|
9
|
+
// Anthropic / Claude OAuth is INTENTIONALLY NOT IMPLEMENTED — explicit user
|
|
10
|
+
// policy as of May 2026: Claude credentials must be API key only.
|
|
11
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
12
|
+
exports.runOAuth = runOAuth;
|
|
13
|
+
const child_process_1 = require("child_process");
|
|
14
|
+
/**
|
|
15
|
+
* Top-level OAuth dispatcher — picks the right runner per provider.
|
|
16
|
+
* Throws PROVIDER_NOT_OAUTH if called for a provider without OAuth support.
|
|
17
|
+
*/
|
|
18
|
+
async function runOAuth(provider, opts = {}) {
|
|
19
|
+
// Hard guard: explicit policy.
|
|
20
|
+
if (provider === 'anthropic') {
|
|
21
|
+
return {
|
|
22
|
+
provider,
|
|
23
|
+
ok: false,
|
|
24
|
+
cli: '',
|
|
25
|
+
detail: 'POLICY_BLOCK: Anthropic OAuth is not permitted — use API key only',
|
|
26
|
+
};
|
|
27
|
+
}
|
|
28
|
+
switch (provider) {
|
|
29
|
+
case 'openai-cli':
|
|
30
|
+
return runCodexLogin(opts);
|
|
31
|
+
case 'gemini-cli':
|
|
32
|
+
return runGeminiLogin(opts);
|
|
33
|
+
case 'xai':
|
|
34
|
+
return runGrokLogin(opts);
|
|
35
|
+
default:
|
|
36
|
+
return {
|
|
37
|
+
provider,
|
|
38
|
+
ok: false,
|
|
39
|
+
cli: '',
|
|
40
|
+
detail: `PROVIDER_NOT_OAUTH: ${provider} does not support OAuth login`,
|
|
41
|
+
};
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
/**
|
|
45
|
+
* Codex (OpenAI CLI) — interactive browser login. Reuses ChatGPT Plus/Pro
|
|
46
|
+
* subscription via OAuth instead of consuming API credits.
|
|
47
|
+
*/
|
|
48
|
+
function runCodexLogin(opts) {
|
|
49
|
+
const cli = opts.cliPath || 'codex';
|
|
50
|
+
if (opts.dryRun) {
|
|
51
|
+
return { provider: 'openai-cli', ok: true, cli, detail: 'dry-run' };
|
|
52
|
+
}
|
|
53
|
+
// codex login is interactive — needs inherited stdio so the browser prompt is visible.
|
|
54
|
+
const loginRes = (0, child_process_1.spawnSync)(cli, ['login'], { stdio: 'inherit' });
|
|
55
|
+
if (loginRes.status !== 0) {
|
|
56
|
+
return {
|
|
57
|
+
provider: 'openai-cli',
|
|
58
|
+
ok: false,
|
|
59
|
+
cli,
|
|
60
|
+
detail: `codex login exited with code ${loginRes.status} (${loginRes.error?.message || 'no error message'})`,
|
|
61
|
+
};
|
|
62
|
+
}
|
|
63
|
+
// Post-login probe — does `codex login status` confirm a session?
|
|
64
|
+
try {
|
|
65
|
+
const out = (0, child_process_1.execFileSync)(cli, ['login', 'status'], {
|
|
66
|
+
encoding: 'utf-8',
|
|
67
|
+
timeout: 8000,
|
|
68
|
+
}).trim();
|
|
69
|
+
if (/logged ?in|signed ?in|active/i.test(out)) {
|
|
70
|
+
return { provider: 'openai-cli', ok: true, cli, detail: out.slice(0, 200) };
|
|
71
|
+
}
|
|
72
|
+
return {
|
|
73
|
+
provider: 'openai-cli',
|
|
74
|
+
ok: false,
|
|
75
|
+
cli,
|
|
76
|
+
detail: `codex login status did not confirm a session: ${out.slice(0, 200)}`,
|
|
77
|
+
};
|
|
78
|
+
}
|
|
79
|
+
catch (err) {
|
|
80
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
81
|
+
// login succeeded but status probe failed — credit it as ok with a note.
|
|
82
|
+
return { provider: 'openai-cli', ok: true, cli, detail: `login ok, status probe failed: ${msg}` };
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
/**
|
|
86
|
+
* Gemini CLI — device-auth OAuth via `gemini login`. Uses the operator's
|
|
87
|
+
* Google account (not the GEMINI_API_KEY env var).
|
|
88
|
+
*/
|
|
89
|
+
function runGeminiLogin(opts) {
|
|
90
|
+
const cli = opts.cliPath || 'gemini';
|
|
91
|
+
if (opts.dryRun) {
|
|
92
|
+
return { provider: 'gemini-cli', ok: true, cli, detail: 'dry-run' };
|
|
93
|
+
}
|
|
94
|
+
const loginRes = (0, child_process_1.spawnSync)(cli, ['login'], { stdio: 'inherit' });
|
|
95
|
+
if (loginRes.status !== 0) {
|
|
96
|
+
return {
|
|
97
|
+
provider: 'gemini-cli',
|
|
98
|
+
ok: false,
|
|
99
|
+
cli,
|
|
100
|
+
detail: `gemini login exited with code ${loginRes.status} (${loginRes.error?.message || 'no error message'})`,
|
|
101
|
+
};
|
|
102
|
+
}
|
|
103
|
+
// Post-login probe
|
|
104
|
+
try {
|
|
105
|
+
const out = (0, child_process_1.execFileSync)(cli, ['login', 'status'], {
|
|
106
|
+
encoding: 'utf-8',
|
|
107
|
+
timeout: 8000,
|
|
108
|
+
}).trim();
|
|
109
|
+
if (/logged ?in|signed ?in|active|authenticated/i.test(out)) {
|
|
110
|
+
return { provider: 'gemini-cli', ok: true, cli, detail: out.slice(0, 200) };
|
|
111
|
+
}
|
|
112
|
+
return {
|
|
113
|
+
provider: 'gemini-cli',
|
|
114
|
+
ok: false,
|
|
115
|
+
cli,
|
|
116
|
+
detail: `gemini login status did not confirm a session: ${out.slice(0, 200)}`,
|
|
117
|
+
};
|
|
118
|
+
}
|
|
119
|
+
catch (err) {
|
|
120
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
121
|
+
return { provider: 'gemini-cli', ok: true, cli, detail: `login ok, status probe failed: ${msg}` };
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
/**
|
|
125
|
+
* xAI Grok — there's no first-party `grok` CLI yet, so this runs the
|
|
126
|
+
* Grok device-auth flow when available, falling back to instructing the user
|
|
127
|
+
* to obtain an API key from console.x.ai/team/default/api-keys.
|
|
128
|
+
*
|
|
129
|
+
* In practice this currently always reports "manual" — kept as a hook for
|
|
130
|
+
* when xAI ships an official CLI.
|
|
131
|
+
*/
|
|
132
|
+
function runGrokLogin(opts) {
|
|
133
|
+
const cli = opts.cliPath || 'grok';
|
|
134
|
+
if (opts.dryRun) {
|
|
135
|
+
return { provider: 'xai', ok: true, cli, detail: 'dry-run' };
|
|
136
|
+
}
|
|
137
|
+
// Probe whether a `grok` CLI exists with a `login` subcommand.
|
|
138
|
+
try {
|
|
139
|
+
const which = (0, child_process_1.spawnSync)(process.platform === 'win32' ? 'where' : 'which', [cli], {
|
|
140
|
+
encoding: 'utf-8',
|
|
141
|
+
timeout: 3000,
|
|
142
|
+
});
|
|
143
|
+
if (which.status !== 0) {
|
|
144
|
+
return {
|
|
145
|
+
provider: 'xai',
|
|
146
|
+
ok: false,
|
|
147
|
+
cli,
|
|
148
|
+
detail: 'xAI device-auth OAuth requires the `grok` CLI which is not installed. Use API key instead (console.x.ai/team/default/api-keys).',
|
|
149
|
+
};
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
catch {
|
|
153
|
+
return {
|
|
154
|
+
provider: 'xai',
|
|
155
|
+
ok: false,
|
|
156
|
+
cli,
|
|
157
|
+
detail: 'xAI device-auth OAuth could not be probed. Use API key instead (console.x.ai/team/default/api-keys).',
|
|
158
|
+
};
|
|
159
|
+
}
|
|
160
|
+
const loginRes = (0, child_process_1.spawnSync)(cli, ['login'], { stdio: 'inherit' });
|
|
161
|
+
if (loginRes.status !== 0) {
|
|
162
|
+
return {
|
|
163
|
+
provider: 'xai',
|
|
164
|
+
ok: false,
|
|
165
|
+
cli,
|
|
166
|
+
detail: `grok login exited with code ${loginRes.status}`,
|
|
167
|
+
};
|
|
168
|
+
}
|
|
169
|
+
return { provider: 'xai', ok: true, cli, detail: 'grok login succeeded (no status probe available)' };
|
|
170
|
+
}
|