fidelizare-integrate 0.2.0 → 0.4.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 +1 -1
- package/src/cli.js +7 -14
- package/src/core/agent-loop.js +66 -28
- package/src/core/apply.js +6 -15
- package/src/core/detect.js +6 -0
- package/src/ui/anim.js +38 -18
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "fidelizare-integrate",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.4.0",
|
|
4
4
|
"description": "Asistent de integrare Fidelizare pentru softuri de gestiune si case de marcat. Scaneaza codul, gaseste locul potrivit si propune integrarea API-ului, in siguranta.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
package/src/cli.js
CHANGED
|
@@ -52,7 +52,7 @@ async function main() {
|
|
|
52
52
|
rawKey = 's2s_beta_demo_0000000000000000000000000000';
|
|
53
53
|
note('Cheie API demo incarcata in seif (nu apare niciodata in cod sau in agent).', C.ink);
|
|
54
54
|
} else {
|
|
55
|
-
rawKey = await ask('Lipeste cheia ta API Fidelizare:');
|
|
55
|
+
rawKey = val('--key') || process.env.FIDELIZARE_API_KEY || (await ask('Lipeste cheia ta API Fidelizare:'));
|
|
56
56
|
if (!rawKey) { note('Fara cheie API. Inchidem.', C.amber); rl.close(); return; }
|
|
57
57
|
}
|
|
58
58
|
const keyRef = vaultPut(rawKey, 'Fidelizare API key');
|
|
@@ -90,24 +90,17 @@ async function main() {
|
|
|
90
90
|
// 5a. Agentic tool-loop (real coding agent): the model uses tools and
|
|
91
91
|
// applies the integration itself, each write gated by confirmation.
|
|
92
92
|
if (has('--agent') && (process.env.OPENROUTER_API_KEY || process.env.FIDELIZARE_LLM_KEY)) {
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
else if (!git.clean) note('• Repo git cu modificari necomise (le poti anula cu git checkout).', C.amber);
|
|
96
|
-
else note('• Repo git curat. Orice modificare e reversibila.', C.green);
|
|
97
|
-
note('• Agent: ' + bold(paint(val('--model') || 'Opus 4.8', C.red)) +
|
|
98
|
-
paint(' tool-uri: list/read/grep/create/insert/check. Fiecare scriere cere confirmare.', C.dim));
|
|
99
|
-
out('\n');
|
|
93
|
+
out(' ' + paint('Agent ', C.ink) + bold(paint(val('--model') || 'Opus 4.8', C.red)) +
|
|
94
|
+
paint(' · scrierile cer confirmare (y / a / N)', C.dim) + '\n\n');
|
|
100
95
|
const r = await runAgentLoop({
|
|
101
96
|
stack: det.stack, rootDir: root, files,
|
|
102
|
-
askRaw: ask, auto: AUTO, model: val('--model'),
|
|
97
|
+
askRaw: ask, auto: AUTO, model: val('--model'),
|
|
98
|
+
guideUrl: val('--guide') || process.env.FIDELIZARE_GUIDE_URL,
|
|
103
99
|
});
|
|
104
100
|
if (r.ok) {
|
|
105
101
|
writeEnv(root, vaultResolve(keyRef));
|
|
106
|
-
|
|
107
|
-
note('
|
|
108
|
-
note('Fisiere atinse: ' + (r.changes.join(', ') || '—'), C.ink);
|
|
109
|
-
note('Configuratie: .env (cheia API, nu in cod). Anulare: fisiere .bak / git checkout.', C.dim);
|
|
110
|
-
note('Test: scaneaza un cod de fidelitate (ex 12333) si verifica punctele.', C.ink);
|
|
102
|
+
out('\n ' + paint('✓', C.green) + ' ' + paint(r.summary.split('\n')[0].slice(0, 88), C.white) + '\n');
|
|
103
|
+
note((r.changes.join(', ') || '—') + paint(' · cheia in .env, anulare: git checkout', C.dim), C.ink);
|
|
111
104
|
out('\n ' + paint('Integrare pregatita.', C.green) + ' ' + paint('Fidelizare', C.red) + '\n\n');
|
|
112
105
|
rl.close();
|
|
113
106
|
return;
|
package/src/core/agent-loop.js
CHANGED
|
@@ -8,28 +8,38 @@
|
|
|
8
8
|
// Runs on OpenRouter (function-calling), default Opus 4.8.
|
|
9
9
|
import { readFileSync, existsSync } from 'fs';
|
|
10
10
|
import { relative, join, resolve, extname } from 'path';
|
|
11
|
-
import { execFileSync } from 'child_process';
|
|
11
|
+
import { execFileSync, execSync } from 'child_process';
|
|
12
12
|
import { redact } from './vault.js';
|
|
13
13
|
import { safeCreateFile, safeInsert, insideRoot } from './apply.js';
|
|
14
14
|
import { C, paint, out } from '../ui/ansi.js';
|
|
15
|
+
import { thinking, toolLine } from '../ui/anim.js';
|
|
15
16
|
|
|
16
17
|
const URL = 'https://openrouter.ai/api/v1/chat/completions';
|
|
17
18
|
const DEFAULT_MODEL = 'anthropic/claude-opus-4.8';
|
|
18
|
-
const
|
|
19
|
+
const DEFAULT_GUIDE = 'https://mcp.fidelizare.ro/assets/api-guide.md';
|
|
20
|
+
const MAX_STEPS = 30;
|
|
21
|
+
// Only test/compile runners may be executed via run_test (no arbitrary shell).
|
|
22
|
+
const ALLOW_TEST = /^(node |npm (test|run |ci\b)|npx (jest|mocha|vitest)|python3? |pytest|php |phpunit|ruby |rspec|go (test|build|run)|dotnet (test|build)|javac |mvn )/i;
|
|
19
23
|
|
|
20
|
-
const SYSTEM = `Esti un agent de integrare care lucreaza DIRECT in codul unui soft de
|
|
21
|
-
|
|
22
|
-
Fidelizare astfel incat, cand scannerul citeste un cod de fidelitate (nu un
|
|
23
|
-
produs), softul sa apeleze POST https://fidelizare.ro/api/integration/s2s/submitReceipt.
|
|
24
|
+
const SYSTEM = `Esti un agent de integrare care lucreaza DIRECT in codul unui soft de gestiune
|
|
25
|
+
/ casa de marcat, prin apeluri de tool, ca un coding agent autonom.
|
|
24
26
|
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
apeleaza
|
|
27
|
+
Pasi:
|
|
28
|
+
1. Citeste documentatia API cu web_fetch pe URL-ul primit, ca sa stii endpoint,
|
|
29
|
+
antet, corp si reguli.
|
|
30
|
+
2. Orienteaza-te SINGUR in cod (list_files, read_file, grep) si gaseste fisierul si
|
|
31
|
+
linia unde codul detecteaza un cod de fidelitate (cod scanat care NU e produs).
|
|
32
|
+
3. Creeaza un helper (create_file) care apeleaza API-ul conform documentatiei, cu
|
|
33
|
+
cheia din variabila de mediu FIDELIZARE_API_KEY (NICIODATA in cod), in try/catch
|
|
34
|
+
ce nu blocheaza vanzarea.
|
|
35
|
+
4. Insereaza apelul (insert_code) exact pe acea ramura, plus import-ul necesar.
|
|
36
|
+
5. Scrie un test cu mock (create_file) care verifica: apelul se face cu fidelity_card
|
|
37
|
+
= codul scanat; o eroare de retea NU arunca si NU blocheaza fluxul. Ruleaza-l cu
|
|
38
|
+
run_test pana trece.
|
|
39
|
+
6. Verifica sintaxa (check_syntax) si apeleaza finish cu un rezumat scurt.
|
|
31
40
|
|
|
32
|
-
Reguli: modificari minime, fara refactor; pastreaza stilul
|
|
41
|
+
Reguli: modificari minime, fara refactor; pastreaza stilul si limbajul proiectului;
|
|
42
|
+
nu scrie chei in cod; nu inventa cai de fisier.`;
|
|
33
43
|
|
|
34
44
|
const TOOLS = [
|
|
35
45
|
{ type: 'function', function: { name: 'list_files', description: 'Listeaza fisierele sursa din proiect.', parameters: { type: 'object', properties: {} } } },
|
|
@@ -37,14 +47,17 @@ const TOOLS = [
|
|
|
37
47
|
{ type: 'function', function: { name: 'grep', description: 'Cauta un pattern (regex) in tot codul.', parameters: { type: 'object', properties: { pattern: { type: 'string' } }, required: ['pattern'] } } },
|
|
38
48
|
{ type: 'function', function: { name: 'create_file', description: 'Creeaza un fisier nou (gated: confirmare + backup).', parameters: { type: 'object', properties: { path: { type: 'string' }, content: { type: 'string' } }, required: ['path', 'content'] } } },
|
|
39
49
|
{ type: 'function', function: { name: 'insert_code', description: 'Insereaza linii dupa atLine (gated: diff + confirmare + backup).', parameters: { type: 'object', properties: { path: { type: 'string' }, atLine: { type: 'number' }, lines: { type: 'array', items: { type: 'string' } }, reason: { type: 'string' } }, required: ['path', 'atLine', 'lines'] } } },
|
|
50
|
+
{ type: 'function', function: { name: 'web_fetch', description: 'Descarca un document de pe web (https), ex. documentatia API.', parameters: { type: 'object', properties: { url: { type: 'string' } }, required: ['url'] } } },
|
|
51
|
+
{ type: 'function', function: { name: 'run_test', description: 'Ruleaza o comanda de test/compilare in proiect (gated, doar runnere cunoscute).', parameters: { type: 'object', properties: { command: { type: 'string' } }, required: ['command'] } } },
|
|
40
52
|
{ type: 'function', function: { name: 'check_syntax', description: 'Verifica sintaxa unui fisier (doar Node).', parameters: { type: 'object', properties: { path: { type: 'string' } }, required: ['path'] } } },
|
|
41
53
|
{ type: 'function', function: { name: 'finish', description: 'Termina integrarea.', parameters: { type: 'object', properties: { summary: { type: 'string' } }, required: ['summary'] } } },
|
|
42
54
|
];
|
|
43
55
|
|
|
44
|
-
export async function runAgentLoop({ stack, rootDir, files, askRaw, auto, model,
|
|
56
|
+
export async function runAgentLoop({ stack, rootDir, files, askRaw, auto, model, guideUrl }) {
|
|
45
57
|
const key = process.env.OPENROUTER_API_KEY || process.env.FIDELIZARE_LLM_KEY;
|
|
46
58
|
if (!key) return { ok: false, reason: 'no_key' };
|
|
47
59
|
const usedModel = model || DEFAULT_MODEL;
|
|
60
|
+
const guide = guideUrl || DEFAULT_GUIDE;
|
|
48
61
|
|
|
49
62
|
const rel = (p) => relative(rootDir, p);
|
|
50
63
|
const abs = (p) => resolve(join(rootDir, p));
|
|
@@ -53,7 +66,7 @@ export async function runAgentLoop({ stack, rootDir, files, askRaw, auto, model,
|
|
|
53
66
|
|
|
54
67
|
// Permission harness (opencode-style): read/grep/list/check are auto-allowed;
|
|
55
68
|
// writes ask, with an "always" reply that approves that action for the run.
|
|
56
|
-
const policy = { create_file: 'ask', insert_code: 'ask' };
|
|
69
|
+
const policy = { create_file: 'ask', insert_code: 'ask', run_test: 'ask' };
|
|
57
70
|
const permit = async (action, label) => {
|
|
58
71
|
if (auto || policy[action] === 'allow') return true;
|
|
59
72
|
const a = (await askRaw(label + ' [y=da / a=mereu / N=nu]')).toLowerCase();
|
|
@@ -82,23 +95,36 @@ export async function runAgentLoop({ stack, rootDir, files, askRaw, auto, model,
|
|
|
82
95
|
create_file: async ({ path, content }) => {
|
|
83
96
|
const a = abs(path);
|
|
84
97
|
if (!insideRoot(rootDir, a)) return 'REFUZAT: in afara proiectului';
|
|
85
|
-
|
|
86
|
-
content.split('\n').slice(0, 12).forEach((l) => log(' ' + paint('+ ', C.green) + paint(redact(l), C.ink) + '\n'));
|
|
87
|
-
if (!(await permit('create_file', 'Creez acest fisier?'))) return 'REFUZAT de utilizator';
|
|
98
|
+
if (!(await permit('create_file', ' ↳ scriu ' + rel(a) + '?'))) return 'REFUZAT de utilizator';
|
|
88
99
|
const r = safeCreateFile(rootDir, a, content);
|
|
89
100
|
changes.push(rel(a)); return 'OK creat (' + r.lines + ' linii)';
|
|
90
101
|
},
|
|
91
|
-
insert_code: async ({ path, atLine, lines
|
|
102
|
+
insert_code: async ({ path, atLine, lines }) => {
|
|
92
103
|
const a = abs(path);
|
|
93
104
|
if (!insideRoot(rootDir, a) || !existsSync(a)) return 'REFUZAT: fisier inexistent';
|
|
94
|
-
|
|
95
|
-
log('\n ' + paint('✎ ' + rel(a) + ' @ linia ' + atLine, C.amber) + (reason ? paint(' ' + reason, C.dim) : '') + '\n');
|
|
96
|
-
for (let i = Math.max(0, atLine - 2); i < atLine; i++) log(' ' + paint(' ' + (src[i] ?? ''), C.dim) + '\n');
|
|
97
|
-
lines.forEach((l) => log(' ' + paint('+ ' + l, C.green) + '\n'));
|
|
98
|
-
if (!(await permit('insert_code', 'Aplic aceasta modificare?'))) return 'REFUZAT de utilizator';
|
|
105
|
+
if (!(await permit('insert_code', ' ↳ modific ' + rel(a) + ' @' + atLine + '?'))) return 'REFUZAT de utilizator';
|
|
99
106
|
const r = safeInsert(rootDir, a, atLine, lines);
|
|
100
107
|
changes.push(rel(a)); return 'OK inserat ' + r.added + ' linii dupa ' + r.atLine;
|
|
101
108
|
},
|
|
109
|
+
web_fetch: async ({ url }) => {
|
|
110
|
+
if (!/^https:\/\//i.test(url || '')) return 'doar URL https';
|
|
111
|
+
try {
|
|
112
|
+
const r = await fetch(url, { signal: AbortSignal.timeout(15_000) });
|
|
113
|
+
if (!r.ok) return 'HTTP ' + r.status;
|
|
114
|
+
return redact(await r.text()).slice(0, 9000);
|
|
115
|
+
} catch (e) { return 'eroare fetch: ' + (e?.message || e); }
|
|
116
|
+
},
|
|
117
|
+
run_test: async ({ command }) => {
|
|
118
|
+
const cmd = String(command || '').trim();
|
|
119
|
+
if (!ALLOW_TEST.test(cmd)) return 'comanda nepermisa (doar runnere de test/compilare): ' + cmd;
|
|
120
|
+
if (!(await permit('run_test', ' ↳ rulez `' + cmd + '`?'))) return 'REFUZAT de utilizator';
|
|
121
|
+
try {
|
|
122
|
+
const o = execSync(cmd, { cwd: rootDir, timeout: 90_000, stdio: 'pipe' });
|
|
123
|
+
return 'OK\n' + o.toString().slice(0, 1500);
|
|
124
|
+
} catch (e) {
|
|
125
|
+
return 'ESUAT (cod ' + (e.status ?? '?') + ')\n' + (((e.stdout?.toString() || '') + (e.stderr?.toString() || '')).slice(0, 1500));
|
|
126
|
+
}
|
|
127
|
+
},
|
|
102
128
|
check_syntax: ({ path }) => {
|
|
103
129
|
const a = abs(path);
|
|
104
130
|
if (!existsSync(a)) return 'fisier inexistent';
|
|
@@ -111,10 +137,21 @@ export async function runAgentLoop({ stack, rootDir, files, askRaw, auto, model,
|
|
|
111
137
|
|
|
112
138
|
const messages = [
|
|
113
139
|
{ role: 'system', content: SYSTEM },
|
|
114
|
-
{ role: 'user', content: `Stack: ${stack}. Proiect cu ${files.length} fisiere sursa.
|
|
140
|
+
{ role: 'user', content: `Stack: ${stack}. Documentatie API: ${guide}. Proiect cu ${files.length} fisiere sursa. Incepe cu web_fetch pe documentatie, apoi orienteaza-te in cod, integreaza si scrie + ruleaza un test cu mock.` },
|
|
115
141
|
];
|
|
116
142
|
|
|
143
|
+
const NAMES = { list_files: 'List', read_file: 'Read', grep: 'Grep', create_file: 'Write', insert_code: 'Edit', web_fetch: 'Fetch', run_test: 'Test', check_syntax: 'Check', finish: 'Done' };
|
|
144
|
+
const brief = (name, a) => {
|
|
145
|
+
if (name === 'grep') return a.pattern ? '"' + a.pattern + '"' : '';
|
|
146
|
+
if (name === 'web_fetch') return a.url || '';
|
|
147
|
+
if (name === 'run_test') return a.command ? '`' + a.command + '`' : '';
|
|
148
|
+
if (name === 'insert_code') return (a.path ? rel(abs(a.path)) : '') + (a.atLine ? ' @' + a.atLine : '');
|
|
149
|
+
if (a.path) return rel(abs(a.path));
|
|
150
|
+
return '';
|
|
151
|
+
};
|
|
152
|
+
|
|
117
153
|
for (let step = 0; step < MAX_STEPS && !finished; step++) {
|
|
154
|
+
const stop = thinking('Gandeste');
|
|
118
155
|
let data;
|
|
119
156
|
try {
|
|
120
157
|
const res = await fetch(URL, {
|
|
@@ -123,16 +160,16 @@ export async function runAgentLoop({ stack, rootDir, files, askRaw, auto, model,
|
|
|
123
160
|
body: JSON.stringify({ model: usedModel, temperature: 0, tools: TOOLS, messages }),
|
|
124
161
|
signal: AbortSignal.timeout(90_000),
|
|
125
162
|
});
|
|
126
|
-
if (!res.ok) return { ok: false, reason: 'http_' + res.status };
|
|
163
|
+
if (!res.ok) { stop(); return { ok: false, reason: 'http_' + res.status }; }
|
|
127
164
|
data = await res.json();
|
|
128
|
-
} catch (e) { return { ok: false, reason: 'fetch:' + (e?.message || e) }; }
|
|
165
|
+
} catch (e) { stop(); return { ok: false, reason: 'fetch:' + (e?.message || e) }; }
|
|
166
|
+
stop();
|
|
129
167
|
|
|
130
168
|
const msg = data.choices?.[0]?.message;
|
|
131
169
|
if (!msg) return { ok: false, reason: 'no_message' };
|
|
132
170
|
messages.push(msg);
|
|
133
171
|
const calls = msg.tool_calls || [];
|
|
134
172
|
if (calls.length === 0) {
|
|
135
|
-
// model spoke without a tool call; nudge once, else stop
|
|
136
173
|
if (step > 0 && !finished) break;
|
|
137
174
|
messages.push({ role: 'user', content: 'Continua folosind tool-urile. Cand ai terminat, apeleaza finish.' });
|
|
138
175
|
continue;
|
|
@@ -140,6 +177,7 @@ export async function runAgentLoop({ stack, rootDir, files, askRaw, auto, model,
|
|
|
140
177
|
for (const call of calls) {
|
|
141
178
|
let arg = {};
|
|
142
179
|
try { arg = JSON.parse(call.function.arguments || '{}'); } catch { /* */ }
|
|
180
|
+
toolLine(NAMES[call.function.name] || call.function.name, brief(call.function.name, arg));
|
|
143
181
|
const fn = tools[call.function.name];
|
|
144
182
|
const result = fn ? await fn(arg) : 'tool necunoscut';
|
|
145
183
|
messages.push({ role: 'tool', tool_call_id: call.id, content: String(result) });
|
package/src/core/apply.js
CHANGED
|
@@ -28,30 +28,21 @@ function insideRoot(root, p) {
|
|
|
28
28
|
return rel && !rel.startsWith('..') && !resolve(p).includes('\0');
|
|
29
29
|
}
|
|
30
30
|
|
|
31
|
-
//
|
|
31
|
+
// Compact preview of the plan (one line per change), no writes.
|
|
32
32
|
export function renderPlan(plan, rootDir) {
|
|
33
33
|
for (const nf of plan.newFiles) {
|
|
34
|
-
out('
|
|
35
|
-
|
|
36
|
-
for (const l of lines) out(' ' + paint('+ ', C.green) + paint(l, C.ink) + '\n');
|
|
37
|
-
if (nf.content.split('\n').length > 14) out(' ' + paint(' …', C.dim) + '\n');
|
|
34
|
+
out(' ' + paint('●', C.red) + ' ' + paint('Write ', C.white) +
|
|
35
|
+
paint(relative(rootDir, nf.path) + ' (' + nf.content.split('\n').length + ' linii)', C.dim) + '\n');
|
|
38
36
|
}
|
|
39
|
-
// group edits by file
|
|
40
37
|
const byFile = new Map();
|
|
41
38
|
for (const e of plan.edits) {
|
|
42
39
|
if (!byFile.has(e.path)) byFile.set(e.path, []);
|
|
43
40
|
byFile.get(e.path).push(e);
|
|
44
41
|
}
|
|
45
42
|
for (const [file, edits] of byFile) {
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
const ctx = Math.max(0, e.atLine - 2);
|
|
50
|
-
out(' ' + paint(`@@ linia ${e.atLine} ${e.reason}`, C.dim) + '\n');
|
|
51
|
-
for (let i = ctx; i < e.atLine; i++) out(' ' + paint(' ' + (src[i] ?? ''), C.dim) + '\n');
|
|
52
|
-
for (const ins of e.insert) out(' ' + paint('+ ' + ins, C.green) + '\n');
|
|
53
|
-
if (src[e.atLine] !== undefined) out(' ' + paint(' ' + src[e.atLine], C.dim) + '\n');
|
|
54
|
-
}
|
|
43
|
+
const added = edits.reduce((s, e) => s + e.insert.length, 0);
|
|
44
|
+
out(' ' + paint('●', C.red) + ' ' + paint('Edit ', C.white) +
|
|
45
|
+
paint(relative(rootDir, file) + ' +' + added + ' linii', C.dim) + '\n');
|
|
55
46
|
}
|
|
56
47
|
}
|
|
57
48
|
|
package/src/core/detect.js
CHANGED
|
@@ -9,6 +9,9 @@ const MANIFESTS = [
|
|
|
9
9
|
{ file: 'pyproject.toml', stack: 'python', label: 'Python' },
|
|
10
10
|
{ file: 'pom.xml', stack: 'java', label: 'Java' },
|
|
11
11
|
{ file: 'build.gradle', stack: 'java', label: 'Java' },
|
|
12
|
+
{ file: 'Gemfile', stack: 'ruby', label: 'Ruby' },
|
|
13
|
+
{ file: 'go.mod', stack: 'go', label: 'Go' },
|
|
14
|
+
{ file: 'Cargo.toml', stack: 'rust', label: 'Rust' },
|
|
12
15
|
];
|
|
13
16
|
const EXT_STACK = {
|
|
14
17
|
'.cs': { stack: 'csharp', label: 'C# / .NET' },
|
|
@@ -21,6 +24,9 @@ const EXT_STACK = {
|
|
|
21
24
|
'.py': { stack: 'python', label: 'Python' },
|
|
22
25
|
'.ts': { stack: 'node', label: 'Node.js' },
|
|
23
26
|
'.js': { stack: 'node', label: 'Node.js' },
|
|
27
|
+
'.rb': { stack: 'ruby', label: 'Ruby' },
|
|
28
|
+
'.go': { stack: 'go', label: 'Go' },
|
|
29
|
+
'.rs': { stack: 'rust', label: 'Rust' },
|
|
24
30
|
};
|
|
25
31
|
const IGNORE = new Set(['node_modules', '.git', 'dist', 'build', 'bin', 'obj', 'vendor', '.next', '__pycache__']);
|
|
26
32
|
|
package/src/ui/anim.js
CHANGED
|
@@ -29,35 +29,55 @@ function topBar() {
|
|
|
29
29
|
out(C.onRed + ansi.bold + left + ' '.repeat(gap) + right + ansi.reset + '\n');
|
|
30
30
|
}
|
|
31
31
|
|
|
32
|
-
//
|
|
33
|
-
//
|
|
34
|
-
|
|
32
|
+
// Thinking indicator (Claude-style morphing star + elapsed time). Returns a
|
|
33
|
+
// stop() that clears the line so the next output starts clean.
|
|
34
|
+
const STARS = ['✶', '✸', '✹', '✺', '✦', '✧'];
|
|
35
|
+
export function thinking(label = 'Gandeste') {
|
|
36
|
+
if (!ANIM) { out(' ' + paint('✶', C.red) + ' ' + paint(label, C.ink) + '…\n'); return () => {}; }
|
|
37
|
+
out(ansi.hideCursor);
|
|
38
|
+
let i = 0;
|
|
39
|
+
const t0 = Date.now();
|
|
40
|
+
const id = setInterval(() => {
|
|
41
|
+
const s = Math.round((Date.now() - t0) / 1000);
|
|
42
|
+
out(ansi.toLineStart + ansi.clearLine + ' ' + paint(STARS[i++ % STARS.length], C.red) +
|
|
43
|
+
' ' + paint(label, C.ink) + paint('… ' + s + 's', C.dim));
|
|
44
|
+
}, 95);
|
|
45
|
+
return () => { clearInterval(id); out(ansi.toLineStart + ansi.clearLine + ansi.showCursor); };
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
// Compact tool-call line, Claude-style: ● Tool brief
|
|
49
|
+
export function toolLine(name, brief = '') {
|
|
50
|
+
out(' ' + paint('●', C.red) + ' ' + paint(name.padEnd(6), C.white) +
|
|
51
|
+
(brief ? paint(brief, C.dim) : '') + '\n');
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
async function swayLogo(logo, frames = [0, 1, 2, 1, 0, -1, -2, -1, 0]) {
|
|
55
|
+
if (!ANIM) return;
|
|
56
|
+
const base = Math.max(0, Math.floor((cols() - 26) / 2));
|
|
57
|
+
for (const off of frames) {
|
|
58
|
+
out(ansi.up(logo.length));
|
|
59
|
+
for (const line of logo) out(ansi.clearLine + ' '.repeat(Math.max(0, base + off)) + line + '\n');
|
|
60
|
+
await sleep(70);
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
// Startup screen: top bar, the real (image-rendered) logo that reveals then
|
|
65
|
+
// gently sways, a title, and one line. Terse.
|
|
35
66
|
export async function logoIntro() {
|
|
36
67
|
if (!ANIM) {
|
|
37
68
|
out(paint(' Fidelizare', C.red) + paint(' · asistent de integrare\n\n', C.ink));
|
|
38
69
|
return;
|
|
39
70
|
}
|
|
40
|
-
out('\x1b[2J\x1b[H' + ansi.hideCursor);
|
|
71
|
+
out('\x1b[2J\x1b[H' + ansi.hideCursor);
|
|
41
72
|
topBar();
|
|
42
73
|
out('\n');
|
|
43
|
-
|
|
44
74
|
const logo = loadLogo();
|
|
45
|
-
for (const line of logo) {
|
|
46
|
-
|
|
47
|
-
await sleep(26);
|
|
48
|
-
}
|
|
75
|
+
for (const line of logo) { out(center(line, 26) + '\n'); await sleep(24); }
|
|
76
|
+
await swayLogo(logo);
|
|
49
77
|
out('\n');
|
|
50
|
-
|
|
51
78
|
const title = ansi.bold + C.red + 'FIDELIZARE' + ansi.reset + paint(' · asistent de integrare', C.ink);
|
|
52
79
|
out(center(title, 'FIDELIZARE · asistent de integrare'.length) + '\n\n');
|
|
53
|
-
await sleep(
|
|
54
|
-
|
|
55
|
-
const d1 = 'Iti analizam codul si integram API-ul de fidelizare, in cativa pasi.';
|
|
56
|
-
const d2 = 'Cheile si .env raman pe calculatorul tau, nu pleaca nicaieri.';
|
|
57
|
-
out(center(paint(d1, C.white), d1.length) + '\n');
|
|
58
|
-
await sleep(80);
|
|
59
|
-
out(center(paint(d2, C.dim), d2.length) + '\n\n');
|
|
60
|
-
await sleep(120);
|
|
80
|
+
await sleep(140);
|
|
61
81
|
out(ansi.showCursor);
|
|
62
82
|
}
|
|
63
83
|
|