@layerall/cli 0.1.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,479 @@
1
+ <!DOCTYPE html>
2
+ <html lang="pt-BR">
3
+ <head>
4
+ <meta charset="UTF-8" />
5
+ <meta name="viewport" content="width=device-width, initial-scale=1.0" />
6
+ <title>__LAYERALL_TITLE__</title>
7
+ <meta name="description" content="__LAYERALL_TAGLINE__" />
8
+
9
+ <link rel="preconnect" href="https://fonts.googleapis.com">
10
+ <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
11
+ <link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700;800&display=swap" rel="stylesheet">
12
+
13
+ <script src="https://cdn.tailwindcss.com"></script>
14
+ <script>
15
+ tailwind.config = {
16
+ theme: {
17
+ extend: {
18
+ fontFamily: { sans: ['Inter', 'system-ui', 'Segoe UI', 'Roboto', 'Helvetica Neue', 'Arial'] },
19
+ boxShadow: {
20
+ glow: '0 0 0 1px rgba(255,255,255,0.06), 0 18px 60px rgba(0,0,0,0.55)',
21
+ soft: '0 10px 30px rgba(0,0,0,0.35)'
22
+ }
23
+ }
24
+ }
25
+ }
26
+ </script>
27
+
28
+ <style>
29
+ :root{
30
+ --bg0:#070A12; --bg1:#0B1020;
31
+ --card: rgba(255,255,255,0.06); --stroke: rgba(255,255,255,0.10);
32
+ --txt: rgba(255,255,255,0.92); --muted: rgba(255,255,255,0.68); --muted2: rgba(255,255,255,0.55);
33
+ --accentA:#7C3AED; --accentB:#22D3EE; --accentC:#34D399;
34
+ --danger:#FB7185; --warn:#FBBF24;
35
+ }
36
+ html,body{height:100%;}
37
+ body{
38
+ color:var(--txt);
39
+ background:
40
+ radial-gradient(1200px 700px at 15% 15%, rgba(124,58,237,0.28), transparent 55%),
41
+ radial-gradient(1000px 600px at 80% 30%, rgba(34,211,238,0.20), transparent 55%),
42
+ radial-gradient(900px 600px at 55% 85%, rgba(52,211,153,0.16), transparent 55%),
43
+ linear-gradient(180deg, var(--bg0), var(--bg1));
44
+ overflow-x:hidden;
45
+ }
46
+ .glass{
47
+ background: linear-gradient(180deg, rgba(255,255,255,0.08), rgba(255,255,255,0.05));
48
+ border: 1px solid var(--stroke);
49
+ box-shadow: 0 16px 60px rgba(0,0,0,0.55);
50
+ backdrop-filter: blur(12px); -webkit-backdrop-filter: blur(12px);
51
+ }
52
+ .glass2{
53
+ background: linear-gradient(180deg, rgba(255,255,255,0.07), rgba(255,255,255,0.04));
54
+ border: 1px solid rgba(255,255,255,0.12);
55
+ backdrop-filter: blur(10px); -webkit-backdrop-filter: blur(10px);
56
+ }
57
+ .btn{
58
+ display:inline-flex; align-items:center; justify-content:center; gap:.55rem;
59
+ border-radius: 14px; padding:.85rem 1.05rem; font-weight:600;
60
+ border: 1px solid rgba(255,255,255,0.12); background: rgba(255,255,255,0.06);
61
+ transition: transform .15s ease, background .2s ease, border-color .2s ease, box-shadow .2s ease;
62
+ user-select:none;
63
+ }
64
+ .btn:hover{ transform: translateY(-1px); background: rgba(255,255,255,0.10); border-color: rgba(255,255,255,0.20); }
65
+ .btn-primary{
66
+ background: linear-gradient(135deg, var(--accentA), var(--accentB));
67
+ border: none; box-shadow: 0 12px 40px rgba(124,58,237,0.35);
68
+ }
69
+ .btn-primary:hover{ filter: brightness(1.1); }
70
+ .mono{ font-family: 'JetBrains Mono', 'SF Mono', ui-monospace, monospace; }
71
+ .chip{
72
+ display:inline-flex; align-items:center; gap:.4rem;
73
+ padding: .25rem .6rem; border-radius: 999px;
74
+ background: rgba(255,255,255,0.06); border: 1px solid rgba(255,255,255,0.12);
75
+ font-size: 12px; color: var(--muted);
76
+ }
77
+ .grad-text{
78
+ background: linear-gradient(135deg, #A78BFA, #22D3EE 60%, #34D399);
79
+ -webkit-background-clip: text; background-clip: text; color: transparent;
80
+ }
81
+ .fade-up{ opacity:0; transform: translateY(20px); transition: opacity .7s ease, transform .7s ease; }
82
+ .fade-up.show{ opacity:1; transform: translateY(0); }
83
+ pre{ font-size: 13px; line-height: 1.55; }
84
+ .tab-active{ background: rgba(255,255,255,0.12) !important; border-color: rgba(255,255,255,0.24) !important; color: #fff !important; }
85
+ </style>
86
+ </head>
87
+ <body class="font-sans antialiased">
88
+
89
+ <!-- NAV -->
90
+ <header class="sticky top-0 z-30 backdrop-blur-xl bg-black/30 border-b border-white/10">
91
+ <div class="max-w-6xl mx-auto px-4 sm:px-6 h-14 flex items-center justify-between">
92
+ <a href="#" class="flex items-center gap-2 font-bold">
93
+ <span class="w-8 h-8 rounded-xl bg-gradient-to-br from-violet-500 to-cyan-400 inline-flex items-center justify-center text-sm">◆</span>
94
+ <span id="navProduct">__LAYERALL_PRODUCT__</span>
95
+ </a>
96
+ <nav class="hidden md:flex items-center gap-6 text-sm text-white/70">
97
+ <a href="#providers" class="hover:text-white">Provedores</a>
98
+ <a href="#simulador" class="hover:text-white">Simulador</a>
99
+ <a href="#recursos" class="hover:text-white">Recursos</a>
100
+ <a href="#precios" class="hover:text-white">Preços</a>
101
+ <a href="#sdk" class="hover:text-white">SDK</a>
102
+ </nav>
103
+ <a id="ctaNav" href="#cta" class="btn btn-primary !py-2 !px-4 text-sm">Começar</a>
104
+ </div>
105
+ </header>
106
+
107
+ <!-- HERO -->
108
+ <section class="relative">
109
+ <div class="max-w-6xl mx-auto px-4 sm:px-6 pt-16 sm:pt-24 pb-16">
110
+ <div class="grid lg:grid-cols-2 gap-10 items-center">
111
+ <div class="fade-up">
112
+ <span class="chip mb-5">All-X · orquestração multi-provedor</span>
113
+ <h1 class="text-4xl sm:text-5xl font-extrabold leading-tight">
114
+ <span id="heroProduct">__LAYERALL_PRODUCT__</span> —<br>
115
+ <span class="grad-text" id="heroTagline">__LAYERALL_TAGLINE__</span>
116
+ </h1>
117
+ <p class="mt-5 text-lg text-white/70 max-w-xl">
118
+ Um SDK. <span id="heroProvidersCount">N</span> provedores de
119
+ <span id="heroDomain">__LAYERALL_DOMAIN__</span>. Estratégias plugáveis,
120
+ fallback automático, latência otimizada e observabilidade de verdade.
121
+ </p>
122
+ <div class="mt-7 flex flex-wrap gap-3">
123
+ <a id="heroCtaPrimary" href="#cta" class="btn btn-primary">Falar com vendas</a>
124
+ <a href="#sdk" class="btn">Ver exemplo de SDK</a>
125
+ </div>
126
+ <div id="heroStrategies" class="mt-6 flex flex-wrap gap-2 text-sm"></div>
127
+ </div>
128
+
129
+ <div class="fade-up glass rounded-3xl p-5 ring-1 ring-white/10">
130
+ <div class="flex items-center gap-2 mb-4">
131
+ <span class="w-3 h-3 rounded-full bg-rose-400"></span>
132
+ <span class="w-3 h-3 rounded-full bg-amber-400"></span>
133
+ <span class="w-3 h-3 rounded-full bg-emerald-400"></span>
134
+ <span class="ml-2 text-xs text-white/55 mono">example.ts</span>
135
+ </div>
136
+ <pre class="text-white/85 overflow-x-auto"><code id="heroCode">__LAYERALL_HERO_CODE__</code></pre>
137
+ </div>
138
+ </div>
139
+ </div>
140
+ </section>
141
+
142
+ <!-- PROVIDERS -->
143
+ <section id="providers" class="py-16">
144
+ <div class="max-w-6xl mx-auto px-4 sm:px-6">
145
+ <div class="fade-up mb-10">
146
+ <h2 class="text-3xl font-bold">Provedores integrados</h2>
147
+ <p class="text-white/65 mt-2">Adicione quantos precisar. O cliente nunca sabe qual foi usado.</p>
148
+ </div>
149
+ <div id="providersGrid" class="grid sm:grid-cols-2 lg:grid-cols-3 gap-4"></div>
150
+ </div>
151
+ </section>
152
+
153
+ <!-- SIMULADOR -->
154
+ <section id="simulador" class="py-16">
155
+ <div class="max-w-6xl mx-auto px-4 sm:px-6">
156
+ <div class="fade-up mb-8">
157
+ <h2 class="text-3xl font-bold">Simulador de orquestração</h2>
158
+ <p class="text-white/65 mt-2">Veja como o <span id="simProduct">__LAYERALL_PRODUCT__</span> escolhe provedores por estratégia.</p>
159
+ </div>
160
+
161
+ <div class="grid lg:grid-cols-3 gap-4">
162
+ <div class="glass rounded-2xl p-5">
163
+ <div class="text-sm text-white/55 mb-2">Operação</div>
164
+ <div id="opButtons" class="flex flex-wrap gap-2 mb-4"></div>
165
+ <div class="text-sm text-white/55 mb-2">Estratégia</div>
166
+ <div id="stratButtons" class="flex flex-col gap-2"></div>
167
+ <button id="runBtn" class="btn btn-primary w-full mt-5">Executar</button>
168
+ </div>
169
+
170
+ <div class="glass rounded-2xl p-5 lg:col-span-2">
171
+ <div class="text-sm text-white/55 mb-2">Resultado</div>
172
+ <div id="logBox" class="mono text-sm bg-black/40 rounded-xl p-3 h-64 overflow-y-auto"></div>
173
+ <div class="mt-4 grid grid-cols-3 gap-3 text-center">
174
+ <div class="glass2 rounded-xl p-3">
175
+ <div class="text-xs text-white/55">Tentativas</div>
176
+ <div id="statAttempts" class="text-xl font-bold">0</div>
177
+ </div>
178
+ <div class="glass2 rounded-xl p-3">
179
+ <div class="text-xs text-white/55">Latência</div>
180
+ <div id="statLatency" class="text-xl font-bold mono">0ms</div>
181
+ </div>
182
+ <div class="glass2 rounded-xl p-3">
183
+ <div class="text-xs text-white/55">Sorteio</div>
184
+ <div id="statChosen" class="text-xl font-bold">—</div>
185
+ </div>
186
+ </div>
187
+ </div>
188
+ </div>
189
+ </div>
190
+ </section>
191
+
192
+ <!-- RECURSOS -->
193
+ <section id="recursos" class="py-16">
194
+ <div class="max-w-6xl mx-auto px-4 sm:px-6">
195
+ <div class="fade-up mb-10">
196
+ <h2 class="text-3xl font-bold">Recursos</h2>
197
+ </div>
198
+ <div class="grid sm:grid-cols-2 lg:grid-cols-3 gap-4">
199
+ <div class="glass rounded-2xl p-5 fade-up">
200
+ <div class="text-2xl mb-2">🛰️</div>
201
+ <h3 class="font-semibold text-lg">Estratégias plugáveis</h3>
202
+ <p class="text-white/65 text-sm mt-1">round-robin, load-balance, most-fast e failover por operação. Override por request.</p>
203
+ </div>
204
+ <div class="glass rounded-2xl p-5 fade-up">
205
+ <div class="text-2xl mb-2">🔁</div>
206
+ <h3 class="font-semibold text-lg">Resiliência</h3>
207
+ <p class="text-white/65 text-sm mt-1">Retries com backoff, fallback automático e timeouts configuráveis.</p>
208
+ </div>
209
+ <div class="glass rounded-2xl p-5 fade-up">
210
+ <div class="text-2xl mb-2">📊</div>
211
+ <h3 class="font-semibold text-lg">Observabilidade</h3>
212
+ <p class="text-white/65 text-sm mt-1">Observer hooks prontos para Prometheus. Cada chamada gera um <span class="mono">providerReceipt</span> auditável.</p>
213
+ </div>
214
+ <div class="glass rounded-2xl p-5 fade-up">
215
+ <div class="text-2xl mb-2">🔌</div>
216
+ <h3 class="font-semibold text-lg">SDK unificado</h3>
217
+ <p class="text-white/65 text-sm mt-1">Um contrato estável (<span class="mono">create/send/status/cancel</span>) mapeado para cada provedor.</p>
218
+ </div>
219
+ <div class="glass rounded-2xl p-5 fade-up">
220
+ <div class="text-2xl mb-2">⚡</div>
221
+ <h3 class="font-semibold text-lg">Mais rápido por padrão</h3>
222
+ <p class="text-white/65 text-sm mt-1">A estratégia <span class="mono">most_fast</span> usa latência esperada e saúde para escolher o caminho mais curto.</p>
223
+ </div>
224
+ <div class="glass rounded-2xl p-5 fade-up">
225
+ <div class="text-2xl mb-2">🧩</div>
226
+ <h3 class="font-semibold text-lg">Multi-tenant</h3>
227
+ <p class="text-white/65 text-sm mt-1">Policy por tenant, com provedores e estratégias isolados.</p>
228
+ </div>
229
+ </div>
230
+ </div>
231
+ </section>
232
+
233
+ <!-- PRICING -->
234
+ <section id="precios" class="py-16">
235
+ <div class="max-w-6xl mx-auto px-4 sm:px-6">
236
+ <div class="fade-up mb-10">
237
+ <h2 class="text-3xl font-bold">Preços</h2>
238
+ <p class="text-white/65 mt-2">Pague conforme cresce. Sem custo por provedor.</p>
239
+ </div>
240
+ <div id="pricingGrid" class="grid sm:grid-cols-2 lg:grid-cols-3 gap-4"></div>
241
+ </div>
242
+ </section>
243
+
244
+ <!-- SDK -->
245
+ <section id="sdk" class="py-16">
246
+ <div class="max-w-6xl mx-auto px-4 sm:px-6">
247
+ <div class="fade-up mb-8">
248
+ <h2 class="text-3xl font-bold">Comece em minutos</h2>
249
+ <p class="text-white/65 mt-2">O mesmo contrato em Node, Python ou cURL.</p>
250
+ </div>
251
+
252
+ <div class="glass rounded-2xl overflow-hidden">
253
+ <div class="flex border-b border-white/10">
254
+ <button class="sdk-tab btn !rounded-none !border-0 !bg-transparent" data-tab="node">Node</button>
255
+ <button class="sdk-tab btn !rounded-none !border-0 !bg-transparent" data-tab="python">Python</button>
256
+ <button class="sdk-tab btn !rounded-none !border-0 !bg-transparent" data-tab="curl">cURL</button>
257
+ </div>
258
+ <pre class="p-5 overflow-x-auto"><code id="sdkCode"></code></pre>
259
+ </div>
260
+ </div>
261
+ </section>
262
+
263
+ <!-- CTA -->
264
+ <section id="cta" class="py-20">
265
+ <div class="max-w-4xl mx-auto px-4 sm:px-6">
266
+ <div class="glass rounded-3xl p-8 sm:p-12 text-center fade-up">
267
+ <h2 class="text-3xl sm:text-4xl font-extrabold">Pronto para orquestrar <span id="ctaProduct">__LAYERALL_PRODUCT__</span>?</h2>
268
+ <p class="text-white/70 mt-3 max-w-xl mx-auto">Fale com a gente e suba em horas, não em sprints.</p>
269
+ <a id="ctaEmail" href="#" class="btn btn-primary mt-6 inline-flex">Falar com vendas</a>
270
+ </div>
271
+ </div>
272
+ </section>
273
+
274
+ <!-- FOOTER -->
275
+ <footer class="border-t border-white/10 py-8">
276
+ <div class="max-w-6xl mx-auto px-4 sm:px-6 text-sm text-white/55 flex flex-wrap items-center justify-between gap-3">
277
+ <span>© <span id="footerYear"></span> <span id="footerProduct">__LAYERALL_PRODUCT__</span>. Orquestração por <a href="https://github.com/sidartaveloso/layerall" class="hover:text-white underline">LayerAll</a>.</span>
278
+ <span class="mono text-xs">made with @layerall/core</span>
279
+ </div>
280
+ </footer>
281
+
282
+ <!-- Toast -->
283
+ <div id="toast" class="fixed bottom-5 right-5 z-50 hidden"></div>
284
+
285
+ <script>window.__LAYERALL_CONFIG__ = __LAYERALL_CONFIG_JSON__;</script>
286
+ <script>
287
+ const CFG = window.__LAYERALL_CONFIG__ || {};
288
+ const $ = (s, r=document) => r.querySelector(s);
289
+ const $$ = (s, r=document) => Array.from(r.querySelectorAll(s));
290
+ const fmtPct = n => Math.round(n*100) + '%';
291
+ const escapeHtml = s => String(s ?? '').replace(/[&<>"']/g, c => ({'&':'&amp;','<':'&lt;','>':'&gt;','"':'&quot;',"'":'&#39;'}[c]));
292
+
293
+ // --- Set base text
294
+ const product = CFG.product || '__LAYERALL_PRODUCT__';
295
+ const tagline = CFG.tagline || '__LAYERALL_TAGLINE__';
296
+ const domain = CFG.domain || 'orquestração';
297
+ const email = CFG.cta?.email || 'contato@allx.com';
298
+ const year = new Date().getFullYear();
299
+ const providers = CFG.providers || [];
300
+ const operations = CFG.operations || ['create','send','status','cancel'];
301
+ const strategies = CFG.strategies || ['round_robin','load_balance','most_fast','failover'];
302
+
303
+ function setText(selector, text){
304
+ const el = $(selector); if (el) el.textContent = text;
305
+ }
306
+
307
+ setText('#navProduct', product);
308
+ setText('#heroProduct', product);
309
+ setText('#heroTagline', tagline);
310
+ setText('#heroDomain', domain);
311
+ setText('#simProduct', product);
312
+ setText('#ctaProduct', product);
313
+ setText('#footerProduct', product);
314
+ setText('#heroProvidersCount', String(providers.length));
315
+
316
+ $('#heroStrategies').innerHTML = strategies.map(s =>
317
+ `<span class="chip mono">${escapeHtml(s)}</span>`).join('');
318
+
319
+ const ctaLink = $('#ctaEmail'); if (ctaLink) ctaLink.href = `mailto:${email}`;
320
+ const ctaNav = $('#ctaNav'); if (ctaNav) ctaNav.href = `#cta`;
321
+ const heroPrimary = $('#heroCtaPrimary'); if (heroPrimary) heroPrimary.href = `mailto:${email}`;
322
+ setText('#footerYear', String(year));
323
+
324
+ // --- Hero code
325
+ const heroCodeEl = $('#heroCode');
326
+ if (heroCodeEl && CFG.sdkExamples?.node) {
327
+ heroCodeEl.textContent = CFG.sdkExamples.node;
328
+ }
329
+
330
+ // --- Providers grid
331
+ const grid = $('#providersGrid');
332
+ if (grid && providers.length) {
333
+ grid.innerHTML = providers.map(p => `
334
+ <div class="glass2 rounded-2xl p-5 fade-up">
335
+ <div class="flex items-center justify-between">
336
+ <div class="font-semibold">${escapeHtml(p.name)}</div>
337
+ <span class="chip mono">peso ${p.weight ?? 1}</span>
338
+ </div>
339
+ <div class="mt-3 grid grid-cols-3 gap-2 text-center text-xs">
340
+ <div class="glass rounded-lg p-2"><div class="text-white/55">latência</div><div class="mono font-semibold">${p.latency ?? '—'}ms</div></div>
341
+ <div class="glass rounded-lg p-2"><div class="text-white/55">peso</div><div class="mono font-semibold">${p.weight ?? '—'}</div></div>
342
+ <div class="glass rounded-lg p-2"><div class="text-white/55">saúde</div><div class="mono font-semibold">${p.health != null ? fmtPct(p.health) : '—'}</div></div>
343
+ </div>
344
+ </div>`).join('');
345
+ } else if (grid) {
346
+ grid.innerHTML = '<div class="text-white/55">Nenhum provedor declarado.</div>';
347
+ }
348
+
349
+ // --- Pricing
350
+ const pricing = CFG.pricing || {};
351
+ const pricingGrid = $('#pricingGrid');
352
+ if (pricingGrid) {
353
+ const tiers = Object.entries(pricing).map(([id, p]) => {
354
+ const price = p.price ?? '—';
355
+ const features = [];
356
+ if (p.requests) features.push(`${escapeHtml(p.requests)} requisições`);
357
+ if (p.providers != null) features.push(`${p.providers === 'ilimitado' ? 'provedores ilimitados' : `${p.providers} provedores`}`);
358
+ if (Array.isArray(p.features)) p.features.forEach(f => features.push(escapeHtml(f)));
359
+ const highlighted = id === 'pro';
360
+ return `
361
+ <div class="glass2 rounded-2xl p-6 fade-up ${highlighted ? 'ring-1 ring-violet-400/40' : ''}">
362
+ <div class="text-sm uppercase tracking-wide text-white/55">${escapeHtml(id)}</div>
363
+ <div class="text-3xl font-extrabold mt-1">${escapeHtml(price)}</div>
364
+ <ul class="mt-4 space-y-2 text-sm text-white/75">
365
+ ${features.map(f => `<li>• ${f}</li>`).join('')}
366
+ </ul>
367
+ </div>`;
368
+ }).join('');
369
+ pricingGrid.innerHTML = tiers || '<div class="text-white/55">Sem tabela de preços.</div>';
370
+ }
371
+
372
+ // --- SDK tabs
373
+ const sdkExamples = CFG.sdkExamples || {};
374
+ const sdkCodeEl = $('#sdkCode');
375
+ function setSdk(tab){
376
+ $$('.sdk-tab').forEach(b => b.classList.toggle('tab-active', b.dataset.tab === tab));
377
+ if (sdkCodeEl) sdkCodeEl.textContent = sdkExamples[tab] || `// ${tab} example not provided`;
378
+ }
379
+ $$('.sdk-tab').forEach(b => b.addEventListener('click', () => setSdk(b.dataset.tab)));
380
+ setSdk(sdkExamples.node ? 'node' : sdkExamples.python ? 'python' : 'curl');
381
+
382
+ // --- Simulator
383
+ const simProviders = providers.map(p => ({
384
+ id: String(p.name).toLowerCase().replace(/\s+/g, '-'),
385
+ name: p.name, weight: p.weight ?? 1, health: p.health ?? 0.95,
386
+ baseLatency: p.latency ?? 200, failRate: 0.05, enabled: true
387
+ }));
388
+ const simState = { op: operations[0] || 'create', strategy: strategies[0] || 'round_robin', rrIndex: 0, attempts: 0, latency: 0, chosen: null };
389
+
390
+ const opButtons = $('#opButtons');
391
+ if (opButtons) {
392
+ opButtons.innerHTML = operations.map(op =>
393
+ `<button class="btn !py-2 !px-3 text-xs op-btn ${op === simState.op ? 'tab-active' : ''}" data-op="${escapeHtml(op)}">${escapeHtml(op)}</button>`).join('');
394
+ $$('.op-btn', opButtons).forEach(b => b.addEventListener('click', () => { simState.op = b.dataset.op; $$('.op-btn', opButtons).forEach(x => x.classList.toggle('tab-active', x === b)); }));
395
+ }
396
+
397
+ const stratButtons = $('#stratButtons');
398
+ if (stratButtons) {
399
+ stratButtons.innerHTML = strategies.map(s =>
400
+ `<button class="btn !py-2 !px-3 text-xs strat-btn ${s === simState.strategy ? 'tab-active' : ''}" data-strategy="${escapeHtml(s)}">${escapeHtml(s)}</button>`).join('');
401
+ $$('.strat-btn', stratButtons).forEach(b => b.addEventListener('click', () => { simState.strategy = b.dataset.strategy; $$('.strat-btn', stratButtons).forEach(x => x.classList.toggle('tab-active', x === b)); }));
402
+ }
403
+
404
+ function pickProvider(){
405
+ const eligible = simProviders.filter(p => p.enabled);
406
+ if (eligible.length === 0) return null;
407
+ switch (simState.strategy){
408
+ case 'round_robin': {
409
+ const p = eligible[simState.rrIndex % eligible.length];
410
+ simState.rrIndex = (simState.rrIndex + 1) % eligible.length;
411
+ return p;
412
+ }
413
+ case 'load_balance': {
414
+ const total = eligible.reduce((s,p) => s + (p.weight || 1), 0);
415
+ let r = Math.random() * total;
416
+ for (const p of eligible){ r -= (p.weight || 1); if (r <= 0) return p; }
417
+ return eligible[eligible.length-1];
418
+ }
419
+ case 'most_fast': {
420
+ return eligible.slice().sort((a,b) => a.baseLatency - b.baseLatency)[0];
421
+ }
422
+ case 'failover': {
423
+ return eligible[0];
424
+ }
425
+ default:
426
+ return eligible[0];
427
+ }
428
+ }
429
+
430
+ function log(line){
431
+ const box = $('#logBox'); if (!box) return;
432
+ const ts = new Date().toLocaleTimeString();
433
+ const div = document.createElement('div');
434
+ div.className = 'text-white/85';
435
+ div.textContent = `[${ts}] ${line}`;
436
+ box.appendChild(div);
437
+ box.scrollTop = box.scrollHeight;
438
+ }
439
+
440
+ function showToast(msg){
441
+ const t = $('#toast'); if (!t) return;
442
+ t.innerHTML = `<div class="glass rounded-xl px-4 py-3 text-sm">${escapeHtml(msg)}</div>`;
443
+ t.classList.remove('hidden');
444
+ setTimeout(() => t.classList.add('hidden'), 1800);
445
+ }
446
+
447
+ const runBtn = $('#runBtn');
448
+ if (runBtn) runBtn.addEventListener('click', async () => {
449
+ const p = pickProvider();
450
+ if (!p){ log('✗ nenhum provedor elegível'); return; }
451
+ const latency = p.baseLatency + Math.round(Math.random() * 60) - 20;
452
+ const ok = Math.random() > (p.failRate || 0);
453
+ simState.attempts++; simState.latency = latency; simState.chosen = p.name;
454
+ $('#statAttempts').textContent = String(simState.attempts);
455
+ $('#statLatency').textContent = latency + 'ms';
456
+ $('#statChosen').textContent = p.name;
457
+ log(`${ok ? '✓' : '✗'} ${simState.op} via ${p.name} (${simState.strategy}) — ${latency}ms`);
458
+ if (!ok){
459
+ const fallback = pickProvider();
460
+ if (fallback && fallback !== p){
461
+ simState.attempts++;
462
+ $('#statAttempts').textContent = String(simState.attempts);
463
+ $('#statChosen').textContent = fallback.name;
464
+ log(`↳ retry via ${fallback.name} — ✓ recover`);
465
+ }
466
+ }
467
+ showToast(`Executado via ${p.name}`);
468
+ });
469
+
470
+ log(`Simulador pronto · ${simProviders.length} provedores · estratégia: ${simState.strategy}`);
471
+
472
+ // Fade-up observer
473
+ const io = new IntersectionObserver((entries) => {
474
+ entries.forEach(e => { if (e.isIntersecting) e.target.classList.add('show'); });
475
+ }, { threshold: 0.12 });
476
+ $$('.fade-up').forEach(el => io.observe(el));
477
+ </script>
478
+ </body>
479
+ </html>
@@ -0,0 +1,19 @@
1
+ # layerall policy schema reference
2
+
3
+ Tenants are keyed by id; `default` is always resolved.
4
+
5
+ ```jsonc
6
+ {
7
+ "tenants": {
8
+ "default": {
9
+ "providers": ["providerA", "providerB", "providerC"],
10
+ "operations": {
11
+ "create": { "strategy": "round_robin", "timeoutMs": 8000, "retries": { "max": 1, "backoffMs": 300 }, "failover": true },
12
+ "send": { "strategy": "load_balance", "weights": { "providerA": 50, "providerB": 30, "providerC": 20 }, "timeoutMs": 12000, "retries": { "max": 2, "backoffMs": 450 } },
13
+ "status": { "strategy": "most_fast", "timeoutMs": 6000, "cacheTtlMs": 5000 },
14
+ "cancel": { "strategy": "failover", "timeoutMs": 8000 }
15
+ }
16
+ }
17
+ }
18
+ }
19
+ ```
package/package.json ADDED
@@ -0,0 +1,67 @@
1
+ {
2
+ "name": "@layerall/cli",
3
+ "version": "0.1.0",
4
+ "description": "CLI tool for scaffolding LayerAll policies and provider configs",
5
+ "keywords": [
6
+ "cli",
7
+ "orchestration",
8
+ "scaffold",
9
+ "layerall",
10
+ "policy",
11
+ "typescript"
12
+ ],
13
+ "author": "Sidarta Veloso <sidartaveloso@gmail.com>",
14
+ "license": "MIT",
15
+ "repository": {
16
+ "type": "git",
17
+ "url": "https://github.com/sidartaveloso/layerall.git",
18
+ "directory": "packages/cli"
19
+ },
20
+ "bugs": {
21
+ "url": "https://github.com/sidartaveloso/layerall/issues"
22
+ },
23
+ "homepage": "https://github.com/sidartaveloso/layerall#readme",
24
+ "type": "module",
25
+ "publishConfig": {
26
+ "access": "public"
27
+ },
28
+ "bin": {
29
+ "layerall": "./dist/cli.js"
30
+ },
31
+ "main": "./dist/index.js",
32
+ "types": "./dist/index.d.ts",
33
+ "exports": {
34
+ ".": {
35
+ "types": "./dist/index.d.ts",
36
+ "import": "./dist/index.js"
37
+ },
38
+ "./cli": {
39
+ "import": "./dist/cli.js"
40
+ }
41
+ },
42
+ "files": [
43
+ "dist",
44
+ "README.md"
45
+ ],
46
+ "dependencies": {
47
+ "commander": "^12.1.0",
48
+ "@layerall/core": "0.1.0"
49
+ },
50
+ "devDependencies": {
51
+ "@types/node": "^20.11.30",
52
+ "@vitest/coverage-v8": "^2.1.0",
53
+ "tsup": "^8.3.0",
54
+ "typescript": "^5.6.0",
55
+ "vitest": "^2.1.0"
56
+ },
57
+ "scripts": {
58
+ "build": "tsup src/cli.ts src/index.ts --format esm --dts && cp -R src/templates dist/templates",
59
+ "dev": "tsup src/cli.ts src/index.ts --format esm --dts --watch",
60
+ "clean": "rm -rf dist",
61
+ "lint": "tsc --noEmit",
62
+ "typecheck": "tsc --noEmit",
63
+ "test": "vitest run --coverage",
64
+ "test:watch": "vitest",
65
+ "test:no-coverage": "vitest run"
66
+ }
67
+ }