@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.
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Sidarta Veloso
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,63 @@
1
+ # @layerall/cli
2
+
3
+ CLI tool for scaffolding LayerAll policies and validating them.
4
+
5
+ ## Install
6
+
7
+ ```bash
8
+ npm install -g @layerall/cli
9
+ # or
10
+ npx @layerall/cli <command>
11
+ ```
12
+
13
+ ## Commands
14
+
15
+ ### `layerall init`
16
+
17
+ Scaffolds a `layerall.policy.json` with sensible defaults covering all four operations.
18
+
19
+ ```bash
20
+ layerall init -p providerA,providerB,providerC -w providerA=50,providerB=30,providerC=20 -o layerall.policy.json
21
+ ```
22
+
23
+ ### `layerall validate <file>`
24
+
25
+ Validates the shape of a policy document and reports issues.
26
+
27
+ ```bash
28
+ layerall validate layerall.policy.json
29
+ ```
30
+
31
+ ### `layerall init-landing`
32
+
33
+ Generates a static marketing landing page (`index.html`) from a JSON config describing
34
+ the product, providers, strategies, pricing and SDK snippets. With no `--config`,
35
+ emits a brandable agnostic "AllX" template.
36
+
37
+ ```bash
38
+ layerall init-landing --config allgeo.json --out landing-geo
39
+ layerall init-landing --out landing-default
40
+ ```
41
+
42
+ Schema:
43
+
44
+ ```jsonc
45
+ {
46
+ "product": "AllGeo", // required
47
+ "tagline": "Geocode reverso unificado", // required
48
+ "domain": "geocode reverso", // required
49
+ "operations": ["reverse", "batch"], // required
50
+ "strategies": ["round_robin", "most_fast", "failover"], // required
51
+ "providers": [ // required (≥1)
52
+ { "name": "Google Maps", "weight": 50, "latency": 180, "health": 0.98 }
53
+ ],
54
+ "sdkExamples": { /* "node" | "python" | "curl": "..." */ }, // optional
55
+ "pricing": { /* "<tier>": { "price": "...", "requests": "...", "providers": number | "ilimitado", "features": ["..."] } */ }, // optional
56
+ "cta": { "email": "geo@allx.com" } // optional
57
+ }
58
+ ```
59
+
60
+ The generated HTML is fully static (Tailwind CDN + Google Fonts, no build step) and
61
+ can be hosted on any CDN.
62
+
63
+ See the root [README](../../README.md).
@@ -0,0 +1,204 @@
1
+ // src/policy.ts
2
+ var ALL_OPS = ["create", "send", "status", "cancel"];
3
+ var STRATEGIES_PER_OP = {
4
+ create: "round_robin",
5
+ send: "load_balance",
6
+ status: "most_fast",
7
+ cancel: "failover"
8
+ };
9
+ function buildPolicy(opts) {
10
+ const providers = opts.providers.slice();
11
+ const failures = { max: opts.retries?.max ?? 1, backoffMs: opts.retries?.backoffMs ?? 300 };
12
+ const operations = {};
13
+ for (const op of ALL_OPS) {
14
+ const strategy = STRATEGIES_PER_OP[op];
15
+ operations[op] = {
16
+ strategy,
17
+ timeoutMs: opts.timeoutMs ?? 8e3,
18
+ retries: failures,
19
+ failover: strategy === "failover",
20
+ ...opts.weights ? { weights: opts.weights } : {}
21
+ };
22
+ }
23
+ return { tenants: { default: { providers, operations } } };
24
+ }
25
+ function validatePolicy(policy) {
26
+ const issues = [];
27
+ if (!policy || typeof policy !== "object") return ["policy must be an object"];
28
+ const root = policy;
29
+ if (!root.tenants || typeof root.tenants !== "object") issues.push("missing tenants map");
30
+ for (const [tenant, value] of Object.entries(root.tenants ?? {})) {
31
+ const t = value;
32
+ if (!Array.isArray(t.providers)) issues.push(`tenant "${tenant}" providers must be an array`);
33
+ if (!t.operations || typeof t.operations !== "object")
34
+ issues.push(`tenant "${tenant}" operations must be an object`);
35
+ }
36
+ return issues;
37
+ }
38
+
39
+ // src/landing.ts
40
+ import { readFileSync } from "fs";
41
+ import { fileURLToPath } from "url";
42
+ import { dirname, join } from "path";
43
+ var __dirname = dirname(fileURLToPath(import.meta.url));
44
+ var TEMPLATE_PATH = join(__dirname, "templates", "landing.html");
45
+ var STRATEGY_NAMES = ["round_robin", "load_balance", "most_fast", "failover"];
46
+ var VALID_STRATEGIES = new Set(STRATEGY_NAMES);
47
+ function validateLandingConfig(input) {
48
+ const issues = [];
49
+ if (!input || typeof input !== "object" || Array.isArray(input)) {
50
+ return { ok: false, issues: [{ path: "", message: "config deve ser um objeto" }] };
51
+ }
52
+ const c = input;
53
+ if (typeof c.product !== "string" || !c.product.trim()) {
54
+ issues.push({ path: "product", message: "product deve ser uma string n\xE3o vazia" });
55
+ }
56
+ if (typeof c.tagline !== "string" || !c.tagline.trim()) {
57
+ issues.push({ path: "tagline", message: "tagline deve ser uma string n\xE3o vazia" });
58
+ }
59
+ if (typeof c.domain !== "string" || !c.domain.trim()) {
60
+ issues.push({ path: "domain", message: "domain deve ser uma string n\xE3o vazia" });
61
+ }
62
+ if (!Array.isArray(c.operations) || !c.operations.every((o) => typeof o === "string" && o.trim())) {
63
+ issues.push({ path: "operations", message: "operations deve ser um array de strings" });
64
+ } else if (c.operations.length === 0) {
65
+ issues.push({ path: "operations", message: "operations n\xE3o pode ser vazio" });
66
+ }
67
+ if (!Array.isArray(c.strategies) || !c.strategies.every((s) => typeof s === "string")) {
68
+ issues.push({ path: "strategies", message: "strategies deve ser um array de strings" });
69
+ } else {
70
+ for (const s of c.strategies) {
71
+ if (!VALID_STRATEGIES.has(s)) {
72
+ issues.push({ path: "strategies", message: `estrat\xE9gia desconhecida: "${s}". V\xE1lidas: ${STRATEGY_NAMES.join(", ")}` });
73
+ }
74
+ }
75
+ if (c.strategies.length === 0) {
76
+ issues.push({ path: "strategies", message: "strategies n\xE3o pode ser vazio" });
77
+ }
78
+ }
79
+ if (!Array.isArray(c.providers) || !c.providers.every((p) => p && typeof p === "object" && typeof p.name === "string")) {
80
+ issues.push({ path: "providers", message: "providers deve ser um array de objetos { name: string }" });
81
+ } else {
82
+ c.providers.forEach((p, i) => {
83
+ const prov = p;
84
+ if (prov.weight != null && typeof prov.weight !== "number") {
85
+ issues.push({ path: `providers[${i}].weight`, message: "weight deve ser number" });
86
+ }
87
+ if (prov.latency != null && typeof prov.latency !== "number") {
88
+ issues.push({ path: `providers[${i}].latency`, message: "latency deve ser number" });
89
+ }
90
+ if (prov.health != null && (typeof prov.health !== "number" || prov.health < 0 || prov.health > 1)) {
91
+ issues.push({ path: `providers[${i}].health`, message: "health deve ser number em [0,1]" });
92
+ }
93
+ });
94
+ if (c.providers.length === 0) {
95
+ issues.push({ path: "providers", message: "providers n\xE3o pode ser vazio" });
96
+ }
97
+ }
98
+ if (c.sdkExamples != null) {
99
+ if (typeof c.sdkExamples !== "object" || Array.isArray(c.sdkExamples)) {
100
+ issues.push({ path: "sdkExamples", message: "sdkExamples deve ser um objeto { node?, python?, curl? }" });
101
+ } else {
102
+ const allowed = /* @__PURE__ */ new Set(["node", "python", "curl"]);
103
+ for (const k of Object.keys(c.sdkExamples)) {
104
+ if (!allowed.has(k)) issues.push({ path: `sdkExamples.${k}`, message: "chave de sdkExamples inv\xE1lida (use node, python ou curl)" });
105
+ else if (typeof c.sdkExamples[k] !== "string") {
106
+ issues.push({ path: `sdkExamples.${k}`, message: "sdkExamples value deve ser string" });
107
+ }
108
+ }
109
+ }
110
+ }
111
+ if (c.pricing != null) {
112
+ if (typeof c.pricing !== "object" || Array.isArray(c.pricing)) {
113
+ issues.push({ path: "pricing", message: "pricing deve ser um objeto de tiers por id" });
114
+ } else {
115
+ for (const [tier, v] of Object.entries(c.pricing)) {
116
+ if (!v || typeof v !== "object") {
117
+ issues.push({ path: `pricing.${tier}`, message: "tier deve ser um objeto" });
118
+ continue;
119
+ }
120
+ const t = v;
121
+ if (t.price != null && typeof t.price !== "string") {
122
+ issues.push({ path: `pricing.${tier}.price`, message: "price deve ser string" });
123
+ }
124
+ if (t.requests != null && typeof t.requests !== "string") {
125
+ issues.push({ path: `pricing.${tier}.requests`, message: "requests deve ser string" });
126
+ }
127
+ if (t.providers != null && typeof t.providers !== "number" && t.providers !== "ilimitado") {
128
+ issues.push({ path: `pricing.${tier}.providers`, message: 'providers deve ser number ou "ilimitado"' });
129
+ }
130
+ if (t.features != null && !(Array.isArray(t.features) && t.features.every((f) => typeof f === "string"))) {
131
+ issues.push({ path: `pricing.${tier}.features`, message: "features deve ser array de strings" });
132
+ }
133
+ }
134
+ }
135
+ }
136
+ if (c.cta != null) {
137
+ if (typeof c.cta !== "object" || Array.isArray(c.cta)) {
138
+ issues.push({ path: "cta", message: "cta deve ser um objeto { email? }" });
139
+ } else if (c.cta.email != null && typeof c.cta.email !== "string") {
140
+ issues.push({ path: "cta.email", message: "cta.email deve ser string" });
141
+ }
142
+ }
143
+ if (issues.length > 0) return { ok: false, issues };
144
+ return { ok: true, issues: [], config: c };
145
+ }
146
+ var DEFAULT_HERO_CODE = `import { Orchestrator } from '@layerall/sdk';
147
+
148
+ const client = new Orchestrator({
149
+ apiKey: process.env.LAYERALL_API_KEY!,
150
+ baseUrl: 'https://api.seu-allx.com',
151
+ });
152
+
153
+ const result = await client.operation('create', {
154
+ payload: { data: {} },
155
+ strategy: 'most_fast',
156
+ });
157
+ console.log(result.provider, result.status);`;
158
+ function defaultLandingConfig() {
159
+ return {
160
+ product: "AllX",
161
+ tagline: "Um SDK. N provedores. Orquestra\xE7\xE3o plug\xE1vel.",
162
+ domain: "orquestra\xE7\xE3o",
163
+ operations: ["create", "send", "status", "cancel"],
164
+ strategies: ["round_robin", "load_balance", "most_fast", "failover"],
165
+ providers: [
166
+ { name: "Provider A", weight: 50, latency: 180, health: 0.96 },
167
+ { name: "Provider B", weight: 30, latency: 120, health: 0.92 },
168
+ { name: "Provider C", weight: 20, latency: 90, health: 0.88 }
169
+ ],
170
+ sdkExamples: {
171
+ node: DEFAULT_HERO_CODE,
172
+ python: `from layerall_sdk import Orchestrator
173
+ client = Orchestrator(api_key=...)
174
+ result = client.operation('create', payload={'data': {}}, strategy='most_fast')`,
175
+ curl: `curl -X POST 'https://api.seu-allx.com/v1/operation/create' \\
176
+ -H 'Authorization: Bearer $KEY' \\
177
+ -d '{"payload":{"data":{}},"strategy":"most_fast"}'`
178
+ },
179
+ pricing: {
180
+ starter: { price: "R$ 0", requests: "10k/m\xEAs", providers: 2 },
181
+ pro: { price: "R$ 499", requests: "100k/m\xEAs", providers: "ilimitado" },
182
+ enterprise: { price: "Sob consulta", features: ["SSO", "SLA", "Suporte dedicado"] }
183
+ },
184
+ cta: { email: "contato@allx.com" }
185
+ };
186
+ }
187
+ function escapeHtml(s) {
188
+ return s.replace(/[&<>]/g, (c) => ({ "&": "&amp;", "<": "&lt;", ">": "&gt;" })[c]);
189
+ }
190
+ function renderLanding(config) {
191
+ const template = readFileSync(TEMPLATE_PATH, "utf8");
192
+ const heroCode = config.sdkExamples?.node ?? DEFAULT_HERO_CODE;
193
+ const title = `${config.product} \u2014 ${config.tagline}`;
194
+ const json = JSON.stringify(config).replace(/</g, "\\u003c").replace(/>/g, "\\u003e").replace(/&/g, "\\u0026");
195
+ return template.replace(/__LAYERALL_TITLE__/g, escapeHtml(title)).replace(/__LAYERALL_TAGLINE__/g, escapeHtml(config.tagline)).replace(/__LAYERALL_PRODUCT__/g, escapeHtml(config.product)).replace(/__LAYERALL_DOMAIN__/g, escapeHtml(config.domain)).replace(/__LAYERALL_HERO_CODE__/g, escapeHtml(heroCode)).replace(/__LAYERALL_CONFIG_JSON__/g, json);
196
+ }
197
+
198
+ export {
199
+ buildPolicy,
200
+ validatePolicy,
201
+ validateLandingConfig,
202
+ defaultLandingConfig,
203
+ renderLanding
204
+ };
package/dist/cli.d.ts ADDED
@@ -0,0 +1 @@
1
+ #!/usr/bin/env node
package/dist/cli.js ADDED
@@ -0,0 +1,106 @@
1
+ #!/usr/bin/env node
2
+ import {
3
+ buildPolicy,
4
+ defaultLandingConfig,
5
+ renderLanding,
6
+ validateLandingConfig,
7
+ validatePolicy
8
+ } from "./chunk-WDABYCHT.js";
9
+
10
+ // src/cli.ts
11
+ import { Command } from "commander";
12
+ import { readFileSync, writeFileSync, existsSync, mkdirSync } from "fs";
13
+ import { resolve } from "path";
14
+ var program = new Command();
15
+ program.name("layerall").description("CLI for scaffolding LayerAll policies and provider configs").version(getVersion());
16
+ program.command("init").description("Scaffold a default layerall.policy.json").option("-o, --out <path>", "output file", "layerall.policy.json").option("-p, --providers <ids>", "comma-separated provider ids", "providerA,providerB,providerC").option("-w, --weights <pairs>", "comma-separated id=weight pairs").action((opts) => {
17
+ const providers = String(opts.providers).split(",").map((s) => s.trim()).filter(Boolean);
18
+ const weights = parseWeights(opts.weights);
19
+ const policy = buildPolicy({
20
+ providers,
21
+ weights,
22
+ timeoutMs: 8e3,
23
+ retries: { max: 1, backoffMs: 300 }
24
+ });
25
+ const outPath = resolve(process.cwd(), opts.out);
26
+ writeFileSync(outPath, JSON.stringify(policy, null, 2) + "\n", "utf8");
27
+ console.log(`\u2714 Policy written to ${outPath}`);
28
+ });
29
+ program.command("validate <file>").description("Validate a layerall.policy.json file").action((file) => {
30
+ const path = resolve(process.cwd(), file);
31
+ if (!existsSync(path)) {
32
+ console.error(`\u2716 File not found: ${path}`);
33
+ process.exitCode = 1;
34
+ return;
35
+ }
36
+ const raw = readFileSync(path, "utf8");
37
+ let json;
38
+ try {
39
+ json = JSON.parse(raw);
40
+ } catch (err) {
41
+ console.error(`\u2716 Invalid JSON: ${err.message}`);
42
+ process.exitCode = 1;
43
+ return;
44
+ }
45
+ const issues = validatePolicy(json);
46
+ if (issues.length === 0) {
47
+ console.log("\u2714 Policy is valid.");
48
+ } else {
49
+ console.error("\u2716 Policy issues:");
50
+ for (const i of issues) console.error(" -", i);
51
+ process.exitCode = 1;
52
+ }
53
+ });
54
+ program.command("init-landing").description("Generate a static marketing landing page from a JSON config").option("-c, --config <path>", "path to landing-config JSON file").option("-o, --out <path>", "output directory", "landing").action((opts) => {
55
+ let configInput;
56
+ if (opts.config) {
57
+ const cfgPath = resolve(process.cwd(), opts.config);
58
+ if (!existsSync(cfgPath)) {
59
+ console.error(`\u2716 Config file not found: ${cfgPath}`);
60
+ process.exitCode = 1;
61
+ return;
62
+ }
63
+ try {
64
+ configInput = JSON.parse(readFileSync(cfgPath, "utf8"));
65
+ } catch (err) {
66
+ console.error(`\u2716 Invalid JSON config: ${err.message}`);
67
+ process.exitCode = 1;
68
+ return;
69
+ }
70
+ } else {
71
+ configInput = defaultLandingConfig();
72
+ }
73
+ const result = validateLandingConfig(configInput);
74
+ if (!result.ok || !result.config) {
75
+ console.error("\u2716 Landing config issues:");
76
+ for (const i of result.issues) console.error(` - ${i.path || "(root)"}: ${i.message}`);
77
+ process.exitCode = 1;
78
+ return;
79
+ }
80
+ const html = renderLanding(result.config);
81
+ const outDir = resolve(process.cwd(), opts.out);
82
+ mkdirSync(outDir, { recursive: true });
83
+ const outPath = resolve(outDir, "index.html");
84
+ writeFileSync(outPath, html, "utf8");
85
+ console.log(`\u2714 Landing written to ${outPath}`);
86
+ });
87
+ program.parse(process.argv);
88
+ function parseWeights(input) {
89
+ if (!input) return void 0;
90
+ const out = {};
91
+ for (const pair of String(input).split(",")) {
92
+ const [id, w] = pair.split("=");
93
+ if (id) out[id.trim()] = Number(w ?? 1);
94
+ }
95
+ return out;
96
+ }
97
+ function getVersion() {
98
+ try {
99
+ const pkg = JSON.parse(
100
+ readFileSync(new URL("../package.json", import.meta.url), "utf8")
101
+ );
102
+ return pkg.version ?? "0.0.0";
103
+ } catch {
104
+ return "0.0.0";
105
+ }
106
+ }
@@ -0,0 +1,77 @@
1
+ import { PolicyDocument } from '@layerall/core';
2
+
3
+ /** Builds a sensible default policy document for a set of provider ids. */
4
+ declare function buildPolicy(opts: {
5
+ providers: string[];
6
+ weights?: Record<string, number>;
7
+ timeoutMs?: number;
8
+ retries?: {
9
+ max: number;
10
+ backoffMs: number;
11
+ };
12
+ }): PolicyDocument;
13
+ /** Validates a policy document shape; returns a list of human-readable issues. */
14
+ declare function validatePolicy(policy: unknown): string[];
15
+
16
+ declare const STRATEGY_NAMES: readonly ["round_robin", "load_balance", "most_fast", "failover"];
17
+ /** Public landing page config — drives every section of the template. */
18
+ interface LandingConfig {
19
+ /** Product / brand name (e.g. "AllGeo"). */
20
+ product: string;
21
+ /** One-line tagline used in `<title>`, meta description and the hero gradient line. */
22
+ tagline: string;
23
+ /** Word describing the orchestration domain (e.g. "geocode reverso"). */
24
+ domain: string;
25
+ /** Operation names the orchestrator exposes (e.g. ["reverse","batch"]). */
26
+ operations: string[];
27
+ /** Strategy names enabled on this product (subset of LayerAll strategies). */
28
+ strategies: (typeof STRATEGY_NAMES)[number][];
29
+ /** One entry per integrated provider. */
30
+ providers: LandingProvider[];
31
+ /** Source snippets shown in the hero and SDK tabs, keyed by language id. */
32
+ sdkExamples?: Partial<Record<'node' | 'python' | 'curl', string>>;
33
+ /** Pricing tiers keyed by tier id (e.g. "starter" / "pro" / "enterprise"). */
34
+ pricing?: Record<string, LandingPricingTier>;
35
+ /** Contact call-to-action. */
36
+ cta?: {
37
+ /** Contact email used by the primary CTA buttons. */
38
+ email?: string;
39
+ };
40
+ }
41
+ interface LandingProvider {
42
+ /** Display name (e.g. "Google Maps"). */
43
+ name: string;
44
+ /** Traffic weight used by load_balance (defaults to 1). */
45
+ weight?: number;
46
+ /** Base latency in ms shown in the provider card. */
47
+ latency?: number;
48
+ /** Health score in [0,1] shown in the provider card. */
49
+ health?: number;
50
+ }
51
+ interface LandingPricingTier {
52
+ /** Display price (e.g. "R$ 0", "Sob consulta"). */
53
+ price?: string;
54
+ /** Volume legend (e.g. "10k/mês"). */
55
+ requests?: string;
56
+ /** Count of providers allowed, or "ilimitado". */
57
+ providers?: number | 'ilimitado';
58
+ /** Extra bullet features. */
59
+ features?: string[];
60
+ }
61
+ interface LandingValidationIssue {
62
+ path: string;
63
+ message: string;
64
+ }
65
+ interface LandingValidationResult {
66
+ ok: boolean;
67
+ issues: LandingValidationIssue[];
68
+ config?: LandingConfig;
69
+ }
70
+ /** Validates a raw config object against the LandingConfig shape. */
71
+ declare function validateLandingConfig(input: unknown): LandingValidationResult;
72
+ /** Default config used when --config is not provided: a brandable agnostic template. */
73
+ declare function defaultLandingConfig(): LandingConfig;
74
+ /** Renders the full single-file landing HTML for the supplied config. */
75
+ declare function renderLanding(config: LandingConfig): string;
76
+
77
+ export { type LandingConfig, type LandingPricingTier, type LandingProvider, type LandingValidationIssue, type LandingValidationResult, buildPolicy, defaultLandingConfig, renderLanding, validateLandingConfig, validatePolicy };
package/dist/index.js ADDED
@@ -0,0 +1,14 @@
1
+ import {
2
+ buildPolicy,
3
+ defaultLandingConfig,
4
+ renderLanding,
5
+ validateLandingConfig,
6
+ validatePolicy
7
+ } from "./chunk-WDABYCHT.js";
8
+ export {
9
+ buildPolicy,
10
+ defaultLandingConfig,
11
+ renderLanding,
12
+ validateLandingConfig,
13
+ validatePolicy
14
+ };