@axplusb/kepler 1.0.9 → 2.0.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/package.json +5 -2
- package/src/context/retriever.mjs +42 -4
- package/src/context/symbol-indexer.mjs +375 -0
- package/src/core/approval.mjs +154 -95
- package/src/core/backend-url.mjs +2 -2
- package/src/core/headless.mjs +5 -0
- package/src/core/pricing.mjs +23 -1
- package/src/core/risk-tier.mjs +239 -0
- package/src/core/tool-executor.mjs +78 -5
- package/src/onboarding/preflight.mjs +274 -0
- package/src/state/orbit.mjs +263 -0
- package/src/state/verbosity.mjs +99 -0
- package/src/terminal/ansi.mjs +47 -27
- package/src/terminal/repl.mjs +407 -121
- package/src/ui/approval.mjs +167 -0
- package/src/ui/banner.mjs +133 -122
- package/src/ui/dock.mjs +88 -0
- package/src/ui/icons.mjs +164 -0
- package/src/ui/mission-report.mjs +264 -0
- package/src/ui/palette.mjs +189 -0
- package/src/ui/spinner.mjs +116 -0
- package/src/ui/status-bar.mjs +275 -0
- package/src/ui/sub-agent.mjs +152 -0
- package/src/ui/term.mjs +159 -0
- package/src/ui/tool-card.mjs +314 -0
- package/src/ui/tool-details.mjs +277 -0
|
@@ -28,6 +28,7 @@ import { execSync } from 'node:child_process';
|
|
|
28
28
|
export function createToolExecutor({
|
|
29
29
|
projectRegistry = new ProjectRegistry(),
|
|
30
30
|
skillsLoader = new SkillsLoader().load(process.cwd()),
|
|
31
|
+
checkpoints = null,
|
|
31
32
|
} = {}) {
|
|
32
33
|
const occRegistry = createToolRegistry();
|
|
33
34
|
const skillTool = occRegistry.get('Skill');
|
|
@@ -113,6 +114,49 @@ export function createToolExecutor({
|
|
|
113
114
|
}
|
|
114
115
|
}
|
|
115
116
|
|
|
117
|
+
// ── Post-edit verification hint ──────────────────────────────
|
|
118
|
+
// Appended to edit_file/write_file results so the model knows
|
|
119
|
+
// exactly how to verify. Uses detected project commands.
|
|
120
|
+
|
|
121
|
+
function verificationHint(filePath) {
|
|
122
|
+
const project = projectRegistry.projectForPath(filePath);
|
|
123
|
+
const commands = project?.resource?.commands || {};
|
|
124
|
+
const parts = [];
|
|
125
|
+
if (commands.test) {
|
|
126
|
+
parts.push(`Run tests: ${commands.test}`);
|
|
127
|
+
}
|
|
128
|
+
if (parts.length === 0) {
|
|
129
|
+
const ext = path.extname(filePath);
|
|
130
|
+
if (ext === '.py') parts.push('Run tests: python -m pytest');
|
|
131
|
+
else if (['.js', '.ts', '.tsx', '.mjs'].includes(ext)) parts.push('Run tests: npm test');
|
|
132
|
+
}
|
|
133
|
+
return parts.length ? `\n--- Verify ---\n${parts.join('\n')}` : '';
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
// ── Solution nudge after exploration ───────────────────────
|
|
137
|
+
// After the agent has read enough code, nudge it to formulate
|
|
138
|
+
// a solution based on the goal — not to blindly edit, but to
|
|
139
|
+
// synthesize what it learned into a fix approach.
|
|
140
|
+
let _codeReadsCount = 0;
|
|
141
|
+
let _hasEdited = false;
|
|
142
|
+
|
|
143
|
+
function solutionNudge(filePath) {
|
|
144
|
+
const ext = path.extname(filePath).toLowerCase();
|
|
145
|
+
const isCode = ['.py', '.js', '.ts', '.tsx', '.mjs', '.go', '.rs', '.java', '.rb'].includes(ext);
|
|
146
|
+
if (!isCode || _hasEdited) return '';
|
|
147
|
+
|
|
148
|
+
_codeReadsCount++;
|
|
149
|
+
if (_codeReadsCount < 4) return '';
|
|
150
|
+
|
|
151
|
+
// Only nudge once at threshold, not every read after
|
|
152
|
+
if (_codeReadsCount === 4) {
|
|
153
|
+
return '\n\n--- You have explored enough code to formulate a solution. ' +
|
|
154
|
+
'Based on what you have read, determine the fix and apply it. ' +
|
|
155
|
+
'If the approach is unclear, call plan() with your findings. ---';
|
|
156
|
+
}
|
|
157
|
+
return '';
|
|
158
|
+
}
|
|
159
|
+
|
|
116
160
|
// ── Tool mapping table ──────────────────────────────────────
|
|
117
161
|
|
|
118
162
|
const toolMap = {
|
|
@@ -239,10 +283,11 @@ export function createToolExecutor({
|
|
|
239
283
|
});
|
|
240
284
|
const output = typeof result === 'string' ? result : String(result);
|
|
241
285
|
const content = output.replace(/^\s*\d+[→\t]/gm, '');
|
|
286
|
+
const actNudge = solutionNudge(filePath);
|
|
242
287
|
return {
|
|
243
288
|
success: !isError(output),
|
|
244
289
|
content,
|
|
245
|
-
output: output + nudge,
|
|
290
|
+
output: output + nudge + actNudge,
|
|
246
291
|
_tool: 'read_file',
|
|
247
292
|
_output_type: 'file_content',
|
|
248
293
|
};
|
|
@@ -265,6 +310,10 @@ export function createToolExecutor({
|
|
|
265
310
|
await occRegistry.call('Read', { file_path: filePath, limit: 1 });
|
|
266
311
|
}
|
|
267
312
|
} catch { /* file may not exist yet */ }
|
|
313
|
+
// Checkpoint before overwrite so /undo can restore the previous content.
|
|
314
|
+
if (checkpoints && fs.existsSync(filePath)) {
|
|
315
|
+
try { checkpoints.save(filePath); } catch { /* best effort */ }
|
|
316
|
+
}
|
|
268
317
|
const result = await occRegistry.call('Write', {
|
|
269
318
|
file_path: filePath,
|
|
270
319
|
content: args.content,
|
|
@@ -275,10 +324,14 @@ export function createToolExecutor({
|
|
|
275
324
|
// Auto-lint the written file
|
|
276
325
|
const lintOutput = autoLint(filePath);
|
|
277
326
|
if (lintOutput) {
|
|
278
|
-
wrapped.output += `\n\n--- Lint
|
|
327
|
+
wrapped.output += `\n\n--- Lint ---\n${lintOutput}`;
|
|
279
328
|
wrapped.lint = lintOutput;
|
|
280
329
|
}
|
|
281
330
|
|
|
331
|
+
// Nudge: tell the model how to verify
|
|
332
|
+
const hint = verificationHint(filePath);
|
|
333
|
+
if (hint) wrapped.output += hint;
|
|
334
|
+
|
|
282
335
|
return wrapped;
|
|
283
336
|
},
|
|
284
337
|
|
|
@@ -357,6 +410,11 @@ export function createToolExecutor({
|
|
|
357
410
|
await occRegistry.call('Read', { file_path: filePath, limit: 1 });
|
|
358
411
|
} catch { /* best effort */ }
|
|
359
412
|
|
|
413
|
+
// Checkpoint before edit so /undo can restore the previous content.
|
|
414
|
+
if (checkpoints) {
|
|
415
|
+
try { checkpoints.save(filePath); } catch { /* best effort */ }
|
|
416
|
+
}
|
|
417
|
+
|
|
360
418
|
let result;
|
|
361
419
|
try {
|
|
362
420
|
result = await occRegistry.call('Edit', {
|
|
@@ -395,14 +453,19 @@ print('OK: replaced')
|
|
|
395
453
|
|
|
396
454
|
const wrapped = wrapResult(result, 'edit_file');
|
|
397
455
|
updateProjectIndex(filePath);
|
|
456
|
+
_hasEdited = true;
|
|
398
457
|
|
|
399
458
|
// Auto-lint the edited file
|
|
400
459
|
const lintOutput = autoLint(filePath);
|
|
401
460
|
if (lintOutput) {
|
|
402
|
-
wrapped.output += `\n\n--- Lint
|
|
461
|
+
wrapped.output += `\n\n--- Lint ---\n${lintOutput}`;
|
|
403
462
|
wrapped.lint = lintOutput;
|
|
404
463
|
}
|
|
405
464
|
|
|
465
|
+
// Nudge: tell the model how to verify
|
|
466
|
+
const hint = verificationHint(filePath);
|
|
467
|
+
if (hint) wrapped.output += hint;
|
|
468
|
+
|
|
406
469
|
return wrapped;
|
|
407
470
|
},
|
|
408
471
|
|
|
@@ -457,9 +520,16 @@ print('OK: replaced')
|
|
|
457
520
|
}
|
|
458
521
|
} catch { /* rg not found or no results */ }
|
|
459
522
|
|
|
460
|
-
// Layer 2:
|
|
523
|
+
// Layer 2: Symbol search — AST-extracted functions/classes with signatures
|
|
461
524
|
if (project?.retriever) {
|
|
462
525
|
if (!project.retriever.index) project.retriever.loadIndex();
|
|
526
|
+
const symbols = project.retriever.searchSymbols(query, 5);
|
|
527
|
+
if (symbols.length > 0) {
|
|
528
|
+
const symOutput = project.retriever.formatSymbolResults(symbols);
|
|
529
|
+
parts.push(`## Symbols (functions/classes)\n${symOutput}`);
|
|
530
|
+
}
|
|
531
|
+
|
|
532
|
+
// Layer 3: BM25 chunks — broader context when symbols aren't enough
|
|
463
533
|
const chunks = project.retriever.retrieve(query, 5);
|
|
464
534
|
if (chunks.length > 0) {
|
|
465
535
|
const bm25Output = chunks.map(c => {
|
|
@@ -582,7 +652,7 @@ print('OK: replaced')
|
|
|
582
652
|
return { success: true, files: results, _tool: 'read_files' };
|
|
583
653
|
},
|
|
584
654
|
|
|
585
|
-
// 9. delete_file + safety check
|
|
655
|
+
// 9. delete_file + safety check + checkpoint for undo
|
|
586
656
|
delete_file: async (args) => {
|
|
587
657
|
try {
|
|
588
658
|
const filePath = resolvePath(args.file_path || args.path, args);
|
|
@@ -590,6 +660,9 @@ print('OK: replaced')
|
|
|
590
660
|
if (!delCheck.safe) {
|
|
591
661
|
return { success: false, output: `🛡️ BLOCKED: ${delCheck.reason}`, _tool: 'delete_file', _blocked: true };
|
|
592
662
|
}
|
|
663
|
+
if (checkpoints) {
|
|
664
|
+
try { checkpoints.save(filePath); } catch { /* best effort */ }
|
|
665
|
+
}
|
|
593
666
|
fs.unlinkSync(filePath);
|
|
594
667
|
updateProjectIndex(filePath);
|
|
595
668
|
return { success: true, message: `Deleted ${args.path}`, _tool: 'delete_file' };
|
|
@@ -0,0 +1,274 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Preflight diagnostic — Mission Control (PRD-055 §9).
|
|
3
|
+
*
|
|
4
|
+
* Prints a non-blocking summary of the runtime environment before the REPL
|
|
5
|
+
* starts so the user can see what is and is not aligned:
|
|
6
|
+
*
|
|
7
|
+
* 🔭 Kepler v1.0.4 · initializing orbit
|
|
8
|
+
*
|
|
9
|
+
* [✓] Auth token
|
|
10
|
+
* [✓] OpenRouter key
|
|
11
|
+
* [✓] Backend http://127.0.0.1:8000
|
|
12
|
+
* [✓] Git repository main · clean
|
|
13
|
+
* [⚠] Linter (ruff) not found → /install ruff to enable lint_check
|
|
14
|
+
* [✓] Project map 142 files, Python + TypeScript
|
|
15
|
+
*
|
|
16
|
+
* All systems aligned. What are we building today?
|
|
17
|
+
*
|
|
18
|
+
* Checks are non-blocking. A failure shows a one-line next-step hint.
|
|
19
|
+
*
|
|
20
|
+
* Exposed via `runPreflight()` (called from REPL startup) and `/preflight`
|
|
21
|
+
* (registered as a slash command).
|
|
22
|
+
*/
|
|
23
|
+
|
|
24
|
+
import fs from 'node:fs';
|
|
25
|
+
import path from 'node:path';
|
|
26
|
+
import { execSync } from 'node:child_process';
|
|
27
|
+
import http from 'node:http';
|
|
28
|
+
import https from 'node:https';
|
|
29
|
+
import { URL } from 'node:url';
|
|
30
|
+
import { paint } from '../ui/palette.mjs';
|
|
31
|
+
import { icons } from '../ui/icons.mjs';
|
|
32
|
+
import { term } from '../ui/term.mjs';
|
|
33
|
+
|
|
34
|
+
const OK = (s) => `${paint.state.success('[✓]')} ${s}`;
|
|
35
|
+
const WARN = (s) => `${paint.state.warn('[⚠]')} ${s}`;
|
|
36
|
+
const FAIL = (s) => `${paint.state.danger('[✗]')} ${s}`;
|
|
37
|
+
|
|
38
|
+
// ── Individual checks (each returns { status, label, hint? }) ──────────
|
|
39
|
+
|
|
40
|
+
function checkAuthToken(auth) {
|
|
41
|
+
const creds = auth.loadCredentials();
|
|
42
|
+
if (creds.token) return { status: 'ok', label: `Auth token` };
|
|
43
|
+
return { status: 'warn', label: 'Auth token missing', hint: '/login to sign in' };
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
function checkProviderKey(auth) {
|
|
47
|
+
const creds = auth.loadCredentials();
|
|
48
|
+
if (creds.openRouterKey) return { status: 'ok', label: 'OpenRouter key' };
|
|
49
|
+
if (creds.anthropicKey) return { status: 'ok', label: 'Anthropic key' };
|
|
50
|
+
if (creds.openaiKey) return { status: 'ok', label: 'OpenAI key' };
|
|
51
|
+
if (creds.googleKey) return { status: 'ok', label: 'Google key' };
|
|
52
|
+
return { status: 'warn', label: 'No model provider key configured', hint: 'set OPENROUTER_API_KEY or run /config' };
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
async function checkBackend(auth, { timeoutMs = 1500 } = {}) {
|
|
56
|
+
const creds = auth.loadCredentials();
|
|
57
|
+
const url = creds.backendUrl;
|
|
58
|
+
if (!url) return { status: 'warn', label: 'Backend not configured' };
|
|
59
|
+
try {
|
|
60
|
+
const reachable = await ping(url, timeoutMs);
|
|
61
|
+
if (reachable) return { status: 'ok', label: `Backend ${shorten(url, 48)}` };
|
|
62
|
+
return { status: 'warn', label: `Backend ${shorten(url, 48)}`, hint: 'unreachable — check network or start backend' };
|
|
63
|
+
} catch {
|
|
64
|
+
return { status: 'warn', label: `Backend ${shorten(url, 48)}`, hint: 'unreachable' };
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
function checkGit(cwd) {
|
|
69
|
+
if (!hasGitDir(cwd)) return { status: 'warn', label: 'Not a git repository', hint: '`git init` to enable diff / checkpoints' };
|
|
70
|
+
try {
|
|
71
|
+
const branch = execSync('git rev-parse --abbrev-ref HEAD 2>/dev/null', { cwd, encoding: 'utf-8' }).trim();
|
|
72
|
+
const status = execSync('git status --porcelain 2>/dev/null', { cwd, encoding: 'utf-8' });
|
|
73
|
+
const dirty = status.split('\n').filter(Boolean).length;
|
|
74
|
+
const summary = dirty > 0
|
|
75
|
+
? `${branch} · ${paint.state.warn(`${dirty} dirty`)}`
|
|
76
|
+
: `${branch} · clean`;
|
|
77
|
+
return { status: 'ok', label: `Git repository ${summary}` };
|
|
78
|
+
} catch {
|
|
79
|
+
return { status: 'warn', label: 'Git repository present but unreadable' };
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
function checkLinters(cwd) {
|
|
84
|
+
const present = [];
|
|
85
|
+
const missing = [];
|
|
86
|
+
for (const [name, kind] of LINTERS) {
|
|
87
|
+
if (which(name)) present.push({ name, kind });
|
|
88
|
+
else if (projectUses(cwd, kind)) missing.push({ name, kind });
|
|
89
|
+
}
|
|
90
|
+
if (present.length === 0 && missing.length === 0) {
|
|
91
|
+
return { status: 'ok', label: 'Linters none required' };
|
|
92
|
+
}
|
|
93
|
+
if (missing.length === 0) {
|
|
94
|
+
return { status: 'ok', label: `Linters ${present.map(p => p.name).join(', ')}` };
|
|
95
|
+
}
|
|
96
|
+
const hint = missing.map(m => `/install ${m.name} to enable lint_check for ${m.kind}`).join(' · ');
|
|
97
|
+
return { status: 'warn', label: `Linter (${missing.map(m => m.name).join(', ')}) not found`, hint };
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
const LINTERS = [
|
|
101
|
+
['ruff', 'python'],
|
|
102
|
+
['eslint', 'javascript'],
|
|
103
|
+
['tsc', 'typescript'],
|
|
104
|
+
['cargo', 'rust'],
|
|
105
|
+
];
|
|
106
|
+
|
|
107
|
+
function projectUses(cwd, kind) {
|
|
108
|
+
try {
|
|
109
|
+
const files = fs.readdirSync(cwd);
|
|
110
|
+
switch (kind) {
|
|
111
|
+
case 'python': return files.some(f => /\.py$/.test(f)) || files.includes('pyproject.toml') || files.includes('requirements.txt');
|
|
112
|
+
case 'javascript': return files.includes('package.json');
|
|
113
|
+
case 'typescript': return files.includes('tsconfig.json');
|
|
114
|
+
case 'rust': return files.includes('Cargo.toml');
|
|
115
|
+
default: return false;
|
|
116
|
+
}
|
|
117
|
+
} catch { return false; }
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
function checkProjectMap(cwd) {
|
|
121
|
+
try {
|
|
122
|
+
const counts = quickFileCount(cwd, { max: 5000 });
|
|
123
|
+
if (!counts.total) return { status: 'warn', label: 'Project map no files indexed yet' };
|
|
124
|
+
const langs = topLanguages(counts.byExt, 2);
|
|
125
|
+
const langStr = langs.length ? langs.join(' + ') : 'mixed';
|
|
126
|
+
return { status: 'ok', label: `Project map ${counts.total} files, ${langStr}` };
|
|
127
|
+
} catch {
|
|
128
|
+
return { status: 'warn', label: 'Project map unreadable' };
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
// ── Helpers ─────────────────────────────────────────────────────────────
|
|
133
|
+
|
|
134
|
+
function shorten(s, n) {
|
|
135
|
+
const str = String(s || '');
|
|
136
|
+
return str.length <= n ? str : str.slice(0, n - 1) + '…';
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
function hasGitDir(cwd) {
|
|
140
|
+
let dir = cwd;
|
|
141
|
+
for (let i = 0; i < 6; i++) {
|
|
142
|
+
if (fs.existsSync(path.join(dir, '.git'))) return true;
|
|
143
|
+
const parent = path.dirname(dir);
|
|
144
|
+
if (parent === dir) break;
|
|
145
|
+
dir = parent;
|
|
146
|
+
}
|
|
147
|
+
return false;
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
function which(name) {
|
|
151
|
+
try {
|
|
152
|
+
execSync(`command -v ${name}`, { encoding: 'utf-8', stdio: ['ignore', 'pipe', 'ignore'] });
|
|
153
|
+
return true;
|
|
154
|
+
} catch { return false; }
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
function ping(url, timeoutMs) {
|
|
158
|
+
return new Promise((resolve) => {
|
|
159
|
+
let u;
|
|
160
|
+
try { u = new URL(url); } catch { resolve(false); return; }
|
|
161
|
+
const lib = u.protocol === 'https:' ? https : http;
|
|
162
|
+
const req = lib.request({
|
|
163
|
+
hostname: u.hostname,
|
|
164
|
+
port: u.port || (u.protocol === 'https:' ? 443 : 80),
|
|
165
|
+
path: u.pathname || '/',
|
|
166
|
+
method: 'GET',
|
|
167
|
+
timeout: timeoutMs,
|
|
168
|
+
}, (res) => {
|
|
169
|
+
// Any response means the host is reachable, even 404.
|
|
170
|
+
res.resume();
|
|
171
|
+
resolve(true);
|
|
172
|
+
});
|
|
173
|
+
req.on('error', () => resolve(false));
|
|
174
|
+
req.on('timeout', () => { try { req.destroy(); } catch {} resolve(false); });
|
|
175
|
+
req.end();
|
|
176
|
+
});
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
const EXT_TO_LANG = {
|
|
180
|
+
'.py': 'Python', '.ts': 'TypeScript', '.tsx': 'TypeScript', '.js': 'JavaScript',
|
|
181
|
+
'.jsx': 'JavaScript', '.mjs': 'JavaScript', '.go': 'Go', '.rs': 'Rust',
|
|
182
|
+
'.java': 'Java', '.rb': 'Ruby', '.php': 'PHP', '.swift': 'Swift', '.kt': 'Kotlin',
|
|
183
|
+
'.c': 'C', '.cc': 'C++', '.cpp': 'C++', '.h': 'C/C++', '.hpp': 'C++',
|
|
184
|
+
};
|
|
185
|
+
|
|
186
|
+
function topLanguages(byExt, n) {
|
|
187
|
+
const ranked = Object.entries(byExt)
|
|
188
|
+
.map(([ext, count]) => [EXT_TO_LANG[ext], count])
|
|
189
|
+
.filter(([lang]) => lang)
|
|
190
|
+
.reduce((acc, [lang, count]) => { acc.set(lang, (acc.get(lang) || 0) + count); return acc; }, new Map());
|
|
191
|
+
return [...ranked.entries()]
|
|
192
|
+
.sort((a, b) => b[1] - a[1])
|
|
193
|
+
.slice(0, n)
|
|
194
|
+
.map(([lang]) => lang);
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
function quickFileCount(cwd, { max = 5000 } = {}) {
|
|
198
|
+
// Shallow walk: skip node_modules, .git, dist, build, .venv, __pycache__.
|
|
199
|
+
const SKIP = new Set(['node_modules', '.git', 'dist', 'build', '.next', '.venv', 'venv', '__pycache__', '.kepler', '.terraform']);
|
|
200
|
+
const byExt = {};
|
|
201
|
+
let total = 0;
|
|
202
|
+
const stack = [cwd];
|
|
203
|
+
while (stack.length && total < max) {
|
|
204
|
+
const dir = stack.pop();
|
|
205
|
+
let entries;
|
|
206
|
+
try { entries = fs.readdirSync(dir, { withFileTypes: true }); }
|
|
207
|
+
catch { continue; }
|
|
208
|
+
for (const e of entries) {
|
|
209
|
+
if (e.name.startsWith('.') && e.name !== '.kepler') continue;
|
|
210
|
+
if (SKIP.has(e.name)) continue;
|
|
211
|
+
const full = path.join(dir, e.name);
|
|
212
|
+
if (e.isDirectory()) stack.push(full);
|
|
213
|
+
else if (e.isFile()) {
|
|
214
|
+
total++;
|
|
215
|
+
const ext = path.extname(e.name).toLowerCase();
|
|
216
|
+
if (ext) byExt[ext] = (byExt[ext] || 0) + 1;
|
|
217
|
+
if (total >= max) break;
|
|
218
|
+
}
|
|
219
|
+
}
|
|
220
|
+
}
|
|
221
|
+
return { total, byExt };
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
// ── Renderer ────────────────────────────────────────────────────────────
|
|
225
|
+
|
|
226
|
+
function formatRow(check) {
|
|
227
|
+
switch (check.status) {
|
|
228
|
+
case 'ok': return ` ${OK(paint.text.primary(check.label))}`;
|
|
229
|
+
case 'warn': return ` ${WARN(paint.text.primary(check.label))}` +
|
|
230
|
+
(check.hint ? ` ${paint.text.dim('→ ' + check.hint)}` : '');
|
|
231
|
+
case 'fail': return ` ${FAIL(paint.text.primary(check.label))}` +
|
|
232
|
+
(check.hint ? ` ${paint.text.dim('→ ' + check.hint)}` : '');
|
|
233
|
+
default: return ` ${paint.text.dim(check.label)}`;
|
|
234
|
+
}
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
/**
|
|
238
|
+
* Run the preflight diagnostic. Writes to stderr and resolves with the
|
|
239
|
+
* collected check results.
|
|
240
|
+
*
|
|
241
|
+
* @param {object} opts
|
|
242
|
+
* @param {object} opts.auth — TarangAuth instance
|
|
243
|
+
* @param {string} opts.cwd — working directory
|
|
244
|
+
* @param {string} opts.version — package version string
|
|
245
|
+
* @param {boolean} [opts.silent] — if true, do not write (useful for tests)
|
|
246
|
+
*/
|
|
247
|
+
export async function runPreflight({ auth, cwd, version, silent = false } = {}) {
|
|
248
|
+
const t = term();
|
|
249
|
+
const write = (s) => { if (!silent) process.stderr.write(s); };
|
|
250
|
+
|
|
251
|
+
const header = `${icons.search} ${paint.bold(paint.brand.primary('Kepler v' + (version || '?')))} ${paint.text.dim('· initializing orbit')}`;
|
|
252
|
+
write('\n' + header + '\n\n');
|
|
253
|
+
|
|
254
|
+
const checks = [];
|
|
255
|
+
checks.push(checkAuthToken(auth));
|
|
256
|
+
checks.push(checkProviderKey(auth));
|
|
257
|
+
checks.push(await checkBackend(auth));
|
|
258
|
+
checks.push(checkGit(cwd));
|
|
259
|
+
checks.push(checkLinters(cwd));
|
|
260
|
+
checks.push(checkProjectMap(cwd));
|
|
261
|
+
|
|
262
|
+
for (const c of checks) write(formatRow(c) + '\n');
|
|
263
|
+
|
|
264
|
+
const fails = checks.filter(c => c.status === 'fail').length;
|
|
265
|
+
const warns = checks.filter(c => c.status === 'warn').length;
|
|
266
|
+
const tail = fails === 0 && warns === 0
|
|
267
|
+
? paint.state.success('All systems aligned.')
|
|
268
|
+
: fails > 0
|
|
269
|
+
? paint.state.danger(`${fails} blocker${fails === 1 ? '' : 's'}, ${warns} warning${warns === 1 ? '' : 's'} — see hints above.`)
|
|
270
|
+
: paint.state.warn(`${warns} warning${warns === 1 ? '' : 's'} — non-blocking.`);
|
|
271
|
+
|
|
272
|
+
write('\n ' + tail + '\n\n');
|
|
273
|
+
return checks;
|
|
274
|
+
}
|