@customyai/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/CHANGELOG.md ADDED
@@ -0,0 +1,7 @@
1
+ # @customyai/cli
2
+
3
+ ## 0.1.0
4
+
5
+ ### Minor Changes
6
+
7
+ - Primera versión pública de `@customyai/cli` (antes `@customy/cli`, privada): `customy apps validate | codegen | sync` sobre el manifiesto `app/v1`, ejecutable como `customy` y usable como biblioteca (`validateManifest`, `generateAppTypes`, `syncApp`, `runCli`). Sin dependencias internas: el manifiesto viaja dentro del paquete y solo depende de `zod`.
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Customy
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,18 @@
1
+ # @customyai/cli
2
+
3
+ La CLI de Customy para apps: valida, tipa y sincroniza el `customy.app.json` de una app (manifiesto `app/v1`: productos y scopes, recursos, eventos con schema y propósitos de consentimiento, capabilities, planes y meters).
4
+
5
+ ```bash
6
+ npm install --save-dev @customyai/cli
7
+
8
+ npx customy apps validate # --file customy.app.json
9
+ npx customy apps codegen --out src/customy.generated.ts
10
+ CUSTOMY_ACCESS_URL=https://access-api.customy.ai CUSTOMY_ACCESS_TOKEN=… \
11
+ npx customy apps sync --workspace-env <envId> # --reason "…"
12
+ ```
13
+
14
+ - **validate**: sale 1 y enseña cada problema con su ruta (`plans.0.capabilities.missing: CAPABILITY_UNDECLARED`).
15
+ - **codegen**: escribe los tipos de la app — `CustomyEventName`, `CustomyEventProperties` (de los JSON Schema de sus eventos), `CustomyCapability`, `CustomyPlan`, `CustomyMeter` y los propósitos por evento — para `createCustomy<…>()` de `@customyai/sdk`, `createData<CustomyEventProperties>()` y `createBilling<CustomyMeter>()`.
16
+ - **sync**: idempotente. Instala la app en el Workspace si no existe y publica una versión nueva del manifiesto sobre la revisión actual; Access lo lleva a cada producto y la CLI enseña cómo quedó cada uno (sale 1 si alguno falló; repetir `sync` lo repara). Lee `CUSTOMY_ACCESS_URL` y `CUSTOMY_ACCESS_TOKEN` (el de un administrador del Workspace) del entorno, nunca de la línea de comandos, y nunca crea credenciales.
17
+
18
+ También se puede usar como biblioteca: `validateManifest`, `generateAppTypes`, `syncApp` y `runCli`.
package/dist/bin.cjs ADDED
@@ -0,0 +1,14 @@
1
+ #!/usr/bin/env node
2
+ 'use strict';
3
+
4
+ var chunkL2FDW7NF_cjs = require('./chunk-L2FDW7NF.cjs');
5
+
6
+ // src/bin.ts
7
+ chunkL2FDW7NF_cjs.runCli(process.argv.slice(2), {
8
+ cwd: process.cwd(),
9
+ env: process.env,
10
+ out: (line) => console.log(line),
11
+ err: (line) => console.error(line)
12
+ }).then((code) => {
13
+ process.exitCode = code;
14
+ });
package/dist/bin.d.cts ADDED
@@ -0,0 +1 @@
1
+ #!/usr/bin/env node
package/dist/bin.d.ts ADDED
@@ -0,0 +1 @@
1
+ #!/usr/bin/env node
package/dist/bin.mjs ADDED
@@ -0,0 +1,12 @@
1
+ #!/usr/bin/env node
2
+ import { runCli } from './chunk-XKJUU7AX.mjs';
3
+
4
+ // src/bin.ts
5
+ runCli(process.argv.slice(2), {
6
+ cwd: process.cwd(),
7
+ env: process.env,
8
+ out: (line) => console.log(line),
9
+ err: (line) => console.error(line)
10
+ }).then((code) => {
11
+ process.exitCode = code;
12
+ });
@@ -0,0 +1,389 @@
1
+ 'use strict';
2
+
3
+ var zod = require('zod');
4
+ var promises = require('fs/promises');
5
+ var path = require('path');
6
+
7
+ function _interopDefault (e) { return e && e.__esModule ? e : { default: e }; }
8
+
9
+ var path__default = /*#__PURE__*/_interopDefault(path);
10
+
11
+ // src/codegen.ts
12
+ var literal = (value) => JSON.stringify(value);
13
+ var union = (values) => values.length ? values.map(literal).join(" | ") : "never";
14
+ var key = (name) => /^[A-Za-z_$][A-Za-z0-9_$]*$/.test(name) ? name : literal(name);
15
+ function schemaToType(schema, indent = "") {
16
+ if (!schema || typeof schema !== "object") return "unknown";
17
+ if (Array.isArray(schema.enum)) return schema.enum.length ? schema.enum.map(literal).join(" | ") : "never";
18
+ const types = Array.isArray(schema.type) ? schema.type : schema.type ? [schema.type] : [];
19
+ if (types.length > 1) return types.map((type) => schemaToType({ ...schema, type }, indent)).join(" | ");
20
+ switch (types[0]) {
21
+ case "string":
22
+ return "string";
23
+ case "number":
24
+ case "integer":
25
+ return "number";
26
+ case "boolean":
27
+ return "boolean";
28
+ case "null":
29
+ return "null";
30
+ case "array":
31
+ return `Array<${schemaToType(schema.items, indent)}>`;
32
+ case "object": {
33
+ const required = new Set(schema.required ?? []);
34
+ const inner = `${indent} `;
35
+ const fields = Object.entries(schema.properties ?? {}).map(([name, value]) => `${inner}${key(name)}${required.has(name) ? "" : "?"}: ${schemaToType(value, inner)};`);
36
+ if (schema.additionalProperties !== false && !schema.properties) return "Record<string, unknown>";
37
+ return fields.length ? `{
38
+ ${fields.join("\n")}
39
+ ${indent}}` : "Record<string, never>";
40
+ }
41
+ default:
42
+ return "unknown";
43
+ }
44
+ }
45
+ function generateAppTypes(manifest) {
46
+ const events = manifest.events.map((event) => ` ${literal(event.name)}: ${schemaToType(event.properties, " ")};`);
47
+ return [
48
+ `// Generado por \`customy apps codegen\` desde customy.app.json (${manifest.key}). No editar a mano.`,
49
+ "",
50
+ `export type CustomyEventName = ${union(manifest.events.map((event) => event.name))};`,
51
+ "",
52
+ "export interface CustomyEventProperties {",
53
+ ...events,
54
+ "}",
55
+ "",
56
+ `export type CustomyCapability = ${union(manifest.capabilities.map((capability) => capability.lookupKey))};`,
57
+ `export type CustomyPlan = ${union(manifest.plans.map((plan) => plan.code))};`,
58
+ `export type CustomyMeter = ${union(manifest.meters.map((meter) => meter.code))};`,
59
+ "",
60
+ "/** Prop\xF3sitos de consentimiento que exige cada evento. */",
61
+ `export const CUSTOMY_EVENT_PURPOSES = {
62
+ ${manifest.events.map((event) => ` ${literal(event.name)}: ${literal(event.purposes)},`).join("\n")}
63
+ } as const;`,
64
+ ""
65
+ ].join("\n");
66
+ }
67
+ var EntitlementMeterAggregationSchema = zod.z.enum(["sum", "count", "last"]);
68
+ var identifier = zod.z.string().regex(/^[a-z][a-z0-9-]{1,62}$/);
69
+ var ConnectedOperationSchema = zod.z.enum([
70
+ "read",
71
+ "propose",
72
+ "submit",
73
+ "read_owned",
74
+ "write_owned",
75
+ "research",
76
+ "deliver_budget_alert",
77
+ "deliver_notification"
78
+ ]);
79
+ var ConnectedBindingKindSchema = zod.z.enum([
80
+ "product",
81
+ "internal_service",
82
+ "external_destination",
83
+ "public_form",
84
+ "private_collection",
85
+ "external_source",
86
+ "external_automation",
87
+ "external_communication"
88
+ ]);
89
+ var ProductBindingSchema = zod.z.object({
90
+ kind: ConnectedBindingKindSchema,
91
+ /** Claves de recurso admitidas; sin lista, cualquiera. Son nombres del producto, nunca de apps. */
92
+ keys: zod.z.array(identifier).min(1).optional(),
93
+ /** Patrón que debe cumplir el identificador del recurso en el producto. */
94
+ resourceIdPattern: zod.z.string().min(1).optional(),
95
+ operations: zod.z.object({
96
+ allowed: zod.z.array(ConnectedOperationSchema).max(4),
97
+ /** exact: exactamente `allowed`; subset: un subconjunto no vacío. */
98
+ mode: zod.z.enum(["exact", "subset"]),
99
+ /** false: las operaciones pueden omitirse (si se dan, cumplen el modo). */
100
+ required: zod.z.boolean().default(true)
101
+ }).strict().optional(),
102
+ maxPerConnection: zod.z.number().int().positive().optional()
103
+ }).strict();
104
+ var ProductManifestSchema = zod.z.object({
105
+ schema: zod.z.literal("product/v1"),
106
+ key: zod.z.string().regex(/^customy-[a-z][a-z0-9-]*$/),
107
+ /** Audiencia de sus tokens de máquina (discovery). */
108
+ audience: zod.z.string().regex(/^customy-[a-z][a-z0-9-]*$/),
109
+ /** Si true, todo enlace debe declarar `bindingKind` (no admite el enlace implícito). */
110
+ bindingRequired: zod.z.boolean().default(false),
111
+ bindings: zod.z.array(ProductBindingSchema).default([]),
112
+ /** Productos a los que llama en nombre de una app (token exchange). Solo esos lo aceptan como actor. */
113
+ requires: zod.z.array(zod.z.string().regex(/^customy-[a-z][a-z0-9-]*$/)).default([])
114
+ }).strict().superRefine((manifest, context) => {
115
+ const kinds = manifest.bindings.map((binding) => binding.kind);
116
+ if (new Set(kinds).size !== kinds.length) context.addIssue({ code: zod.z.ZodIssueCode.custom, message: "Binding kinds must be unique per product" });
117
+ if (manifest.requires.includes(manifest.key)) context.addIssue({ code: zod.z.ZodIssueCode.custom, message: "A product cannot require itself" });
118
+ });
119
+ var product = (key2, bindings = [], bindingRequired = false, requires = []) => ProductManifestSchema.parse({ schema: "product/v1", key: key2, audience: key2, bindingRequired, bindings, requires });
120
+ var CONNECTABLE_PRODUCT_MANIFESTS = [
121
+ product("customy-atlas", [{ kind: "internal_service", operations: { allowed: ["read", "propose"], mode: "subset" } }]),
122
+ product("customy-tables", [{ kind: "private_collection", keys: ["favorites", "comparisons", "preferences"], operations: { allowed: ["read_owned", "write_owned"], mode: "subset" } }]),
123
+ product("customy-agent", [{ kind: "external_automation", keys: ["catalog-updater"], operations: { allowed: ["research", "deliver_budget_alert"], mode: "subset" } }]),
124
+ product("customy-forms", [{ kind: "public_form", keys: ["subscription", "contact", "commercial"], resourceIdPattern: "^frm_[a-zA-Z0-9_-]+$", operations: { allowed: ["read", "submit"], mode: "exact" } }], false, ["customy-links"]),
125
+ product("customy-crm"),
126
+ product("customy-content", [{ kind: "external_destination", operations: { allowed: ["read"], mode: "exact" } }]),
127
+ product("customy-storage"),
128
+ product("customy-data", [{ kind: "external_source", keys: ["analytics-source"], operations: { allowed: ["submit"], mode: "exact" } }]),
129
+ product("customy-analytics"),
130
+ product("customy-campaigns", [{ kind: "external_communication", keys: ["weekly-brief"], operations: { allowed: ["read"], mode: "exact" } }]),
131
+ product("customy-engagement", [{ kind: "external_communication", keys: ["notification-delivery"], operations: { allowed: ["deliver_notification"], mode: "exact" } }]),
132
+ // Una referencia de solo lectura a un sitio de Pages del Workspace; no concede edición ni publicación.
133
+ product("customy-pages", [{ kind: "product", resourceIdPattern: "^[0-9]{6,20}$", operations: { allowed: ["read"], mode: "exact", required: false }, maxPerConnection: 1 }], true)
134
+ ];
135
+ function connectedResourceViolation(resource, manifests = CONNECTABLE_PRODUCT_MANIFESTS) {
136
+ const manifest = manifests.find((entry) => entry.key === resource.productKey);
137
+ if (!manifest) return "PRODUCT_NOT_CONNECTABLE";
138
+ if (manifest.bindingRequired && !resource.bindingKind) return "BINDING_KIND_REQUIRED";
139
+ const binding = resource.bindingKind ? manifest.bindings.find((entry) => entry.kind === resource.bindingKind) : void 0;
140
+ if (!binding) {
141
+ const plainReference = resource.bindingKind === void 0 || resource.bindingKind === "product";
142
+ if (plainReference && !resource.operations) return null;
143
+ return plainReference ? "OPERATIONS_REQUIRE_BINDING" : "BINDING_NOT_OFFERED";
144
+ }
145
+ if (binding.keys && !binding.keys.includes(resource.key)) return "BINDING_KEY_NOT_ALLOWED";
146
+ if (binding.resourceIdPattern && !new RegExp(binding.resourceIdPattern).test(resource.resourceId)) return "BINDING_RESOURCE_ID_INVALID";
147
+ const rule = binding.operations;
148
+ const operations = resource.operations;
149
+ if (!rule) return operations ? "OPERATIONS_REQUIRE_BINDING" : null;
150
+ if (!operations || operations.length === 0) return rule.required ? "BINDING_OPERATIONS_REQUIRED" : null;
151
+ if (operations.some((operation) => !rule.allowed.includes(operation))) return "BINDING_OPERATION_NOT_ALLOWED";
152
+ if (rule.mode === "exact" && (operations.length !== rule.allowed.length || rule.allowed.some((operation) => !operations.includes(operation)))) return "BINDING_OPERATIONS_MISMATCH";
153
+ return null;
154
+ }
155
+ function connectedResourceLimit(productKey2, manifests = CONNECTABLE_PRODUCT_MANIFESTS) {
156
+ const limits = manifests.find((entry) => entry.key === productKey2)?.bindings.map((binding) => binding.maxPerConnection).filter((value) => value !== void 0) ?? [];
157
+ return limits.length ? Math.min(...limits) : null;
158
+ }
159
+
160
+ // src/vendor/app-manifest.ts
161
+ var slug = zod.z.string().regex(/^[a-z][a-z0-9-]{1,38}[a-z0-9]$/);
162
+ var productKey = zod.z.string().regex(/^customy-[a-z][a-z0-9-]*$/);
163
+ var httpsOrigin = zod.z.string().url().max(512).refine((value) => {
164
+ const url = new URL(value);
165
+ return url.origin === value && (url.protocol === "https:" || ["localhost", "127.0.0.1"].includes(url.hostname));
166
+ }, "Origins must be bare https origins");
167
+ var eventName = zod.z.string().regex(/^[a-z][a-z0-9_.:-]{0,199}$/);
168
+ var lookupKey = zod.z.string().regex(/^[a-z][a-z0-9_.:-]{0,79}$/);
169
+ var AppProductUseSchema = zod.z.object({
170
+ product: productKey,
171
+ /** Scopes de producto que pedirán sus credenciales de máquina (`<producto>:<scope>`). */
172
+ scopes: zod.z.array(zod.z.string().regex(/^[a-z][a-z0-9_-]*:[a-z0-9_:.-]+$/)).max(64).default([])
173
+ }).strict();
174
+ var AppResourceSchema = zod.z.object({
175
+ key: zod.z.string().regex(/^[a-z][a-z0-9-]{1,62}$/),
176
+ product: productKey,
177
+ bindingKind: ConnectedBindingKindSchema.optional(),
178
+ operations: zod.z.array(ConnectedOperationSchema).min(1).max(2).optional()
179
+ }).strict();
180
+ var AppEventSchema = zod.z.object({
181
+ name: eventName,
182
+ type: zod.z.enum(["track", "identify", "group", "page", "screen", "alias"]),
183
+ schemaVersion: zod.z.number().int().positive(),
184
+ /** JSON Schema de `properties`; se publica como schema fijado del evento. */
185
+ properties: zod.z.record(zod.z.unknown()).default({ type: "object" }),
186
+ purposes: zod.z.array(zod.z.string().regex(/^[a-z][a-z0-9_.:-]{0,79}$/)).min(1).max(8)
187
+ }).strict();
188
+ var AppCapabilitySchema = zod.z.object({
189
+ lookupKey,
190
+ name: zod.z.string().min(1).max(120),
191
+ type: zod.z.enum(["boolean", "metered", "config"]),
192
+ meter: lookupKey.optional()
193
+ }).strict();
194
+ var AppPlanSchema = zod.z.object({
195
+ code: lookupKey,
196
+ name: zod.z.string().min(1).max(120),
197
+ rank: zod.z.number().int().nonnegative().default(100),
198
+ capabilities: zod.z.record(lookupKey, zod.z.unknown()).default({})
199
+ }).strict();
200
+ var AppMeterSchema = zod.z.object({
201
+ code: lookupKey,
202
+ aggregation: EntitlementMeterAggregationSchema,
203
+ unit: zod.z.string().min(1).max(40)
204
+ }).strict();
205
+ var AppManifestSchema = zod.z.object({
206
+ /** Referencia opcional al JSON Schema publicado, para autocompletado en editores. */
207
+ $schema: zod.z.string().url().max(200).optional(),
208
+ schema: zod.z.literal("app/v1"),
209
+ key: slug,
210
+ name: zod.z.string().trim().min(1).max(80),
211
+ origins: zod.z.object({ staging: httpsOrigin.optional(), production: httpsOrigin.optional() }).strict().refine((origins) => Boolean(origins.staging || origins.production), "Declare at least one origin"),
212
+ products: zod.z.array(AppProductUseSchema).max(32).default([]),
213
+ resources: zod.z.array(AppResourceSchema).max(64).default([]),
214
+ events: zod.z.array(AppEventSchema).max(256).default([]),
215
+ capabilities: zod.z.array(AppCapabilitySchema).max(256).default([]),
216
+ plans: zod.z.array(AppPlanSchema).max(32).default([]),
217
+ meters: zod.z.array(AppMeterSchema).max(64).default([])
218
+ }).strict();
219
+ var APP_ACCESS_SCOPES = ["capabilities:read", "users:contact:read"];
220
+ var APP_ACCESS_SCOPE_SET = new Set(APP_ACCESS_SCOPES);
221
+ function appScopeAllowed(product2, scope) {
222
+ if (product2 === "customy-access") return APP_ACCESS_SCOPE_SET.has(scope);
223
+ return scope.startsWith(`${product2.replace(/^customy-/, "")}:`);
224
+ }
225
+ function appManifestProblems(input, manifests = CONNECTABLE_PRODUCT_MANIFESTS) {
226
+ const parsed = AppManifestSchema.safeParse(input);
227
+ if (!parsed.success) return parsed.error.issues.map((issue) => ({ path: issue.path.join("."), code: "SCHEMA_INVALID" }));
228
+ const app = parsed.data;
229
+ const problems = [];
230
+ const unique = (items, key2, path2) => {
231
+ const seen = /* @__PURE__ */ new Set();
232
+ items.forEach((item, index) => {
233
+ const k = key2(item);
234
+ if (seen.has(k)) problems.push({ path: `${path2}.${index}`, code: "DUPLICATE" });
235
+ seen.add(k);
236
+ });
237
+ };
238
+ unique(app.products, (p) => p.product, "products");
239
+ unique(app.resources, (r) => r.key, "resources");
240
+ unique(app.events, (e) => e.name, "events");
241
+ unique(app.capabilities, (c) => c.lookupKey, "capabilities");
242
+ unique(app.plans, (p) => p.code, "plans");
243
+ unique(app.meters, (m) => m.code, "meters");
244
+ app.products.forEach((use, index) => {
245
+ use.scopes.forEach((scope, scopeIndex) => {
246
+ if (!appScopeAllowed(use.product, scope)) problems.push({ path: `products.${index}.scopes.${scopeIndex}`, code: "SCOPE_OUTSIDE_PRODUCT" });
247
+ });
248
+ });
249
+ const counts = /* @__PURE__ */ new Map();
250
+ app.resources.forEach((resource, index) => {
251
+ const violation = connectedResourceViolation({ ...resource, productKey: resource.product, resourceId: "manifest" }, manifests);
252
+ if (violation && violation !== "BINDING_RESOURCE_ID_INVALID") problems.push({ path: `resources.${index}`, code: violation });
253
+ counts.set(resource.product, (counts.get(resource.product) ?? 0) + 1);
254
+ });
255
+ for (const [product2, count] of counts) {
256
+ const limit = connectedResourceLimit(product2, manifests);
257
+ if (limit !== null && count > limit) problems.push({ path: "resources", code: "BINDING_LIMIT_EXCEEDED" });
258
+ }
259
+ const meters = new Set(app.meters.map((meter) => meter.code));
260
+ const capabilities = new Set(app.capabilities.map((capability) => capability.lookupKey));
261
+ app.capabilities.forEach((capability, index) => {
262
+ if (capability.type === "metered" && (!capability.meter || !meters.has(capability.meter))) problems.push({ path: `capabilities.${index}.meter`, code: "METER_UNDECLARED" });
263
+ if (capability.type !== "metered" && capability.meter) problems.push({ path: `capabilities.${index}.meter`, code: "METER_ON_UNMETERED" });
264
+ });
265
+ app.plans.forEach((plan, index) => {
266
+ for (const key2 of Object.keys(plan.capabilities)) if (!capabilities.has(key2)) problems.push({ path: `plans.${index}.capabilities.${key2}`, code: "CAPABILITY_UNDECLARED" });
267
+ });
268
+ return problems;
269
+ }
270
+
271
+ // src/apps.ts
272
+ function validateManifest(input) {
273
+ const problems = appManifestProblems(input);
274
+ return problems.length ? { ok: false, problems } : { ok: true, manifest: AppManifestSchema.parse(input) };
275
+ }
276
+ var SyncError = class extends Error {
277
+ constructor(code, status) {
278
+ super(`${code} (${status})`);
279
+ this.code = code;
280
+ this.status = status;
281
+ }
282
+ code;
283
+ status;
284
+ };
285
+ async function syncApp(options) {
286
+ const base = options.accessUrl.replace(/\/$/, "");
287
+ if (!/^https:\/\//.test(base) && !/^http:\/\/(localhost|127\.0\.0\.1)(:\d+)?$/.test(base)) throw new SyncError("ACCESS_URL_INSECURE", 0);
288
+ const call = async (method, path2, body) => {
289
+ const response = await (options.fetch ?? fetch)(`${base}${path2}`, {
290
+ method,
291
+ headers: { authorization: `Bearer ${options.token}`, "content-type": "application/json", "idempotency-key": crypto.randomUUID() },
292
+ body: JSON.stringify(body)
293
+ });
294
+ const json = await response.json().catch(() => ({}));
295
+ if (!response.ok) throw new SyncError(String(json.code ?? json.error ?? "ACCESS_REQUEST_FAILED"), response.status);
296
+ return json;
297
+ };
298
+ const env = encodeURIComponent(options.workspaceEnvironmentId);
299
+ const installed = await call("POST", `/api/admin/env/${env}/connected-applications/install`, { manifest: options.manifest, reason: options.reason });
300
+ const connection = installed.connection;
301
+ const reconciliation = (value) => value && typeof value === "object" ? value : {};
302
+ if (!installed.replayed) {
303
+ return { action: "installed", applicationId: connection.applicationId, environmentId: connection.environmentId, revision: connection.revision, reconciliation: reconciliation(installed.reconciliation) };
304
+ }
305
+ const updated = await call("PUT", `/api/admin/env/${encodeURIComponent(connection.environmentId)}/connected-application/manifest`, {
306
+ expectedRevision: connection.revision,
307
+ manifest: options.manifest,
308
+ reason: options.reason
309
+ });
310
+ const revision = Number(updated.revision ?? connection.revision);
311
+ return { action: updated.unchanged ? "unchanged" : "updated", applicationId: connection.applicationId, environmentId: connection.environmentId, revision, reconciliation: reconciliation(updated.reconciliation) };
312
+ }
313
+ function option(args, name) {
314
+ const index = args.indexOf(`--${name}`);
315
+ return index >= 0 ? args[index + 1] : void 0;
316
+ }
317
+ async function loadManifest(args, io) {
318
+ const file = path__default.default.resolve(io.cwd, option(args, "file") ?? "customy.app.json");
319
+ let raw;
320
+ try {
321
+ raw = JSON.parse(await promises.readFile(file, "utf8"));
322
+ } catch (error) {
323
+ io.err(`no se pudo leer ${path__default.default.relative(io.cwd, file) || file}: ${error instanceof Error ? error.message : error}`);
324
+ return null;
325
+ }
326
+ const result = validateManifest(raw);
327
+ if (!result.ok) {
328
+ for (const problem of result.problems) io.err(`\u2717 ${problem.path || "(ra\xEDz)"}: ${problem.code}`);
329
+ return null;
330
+ }
331
+ return result.manifest;
332
+ }
333
+ async function runCli(argv, io) {
334
+ const [group, command, ...args] = argv;
335
+ if (group !== "apps" || !["validate", "codegen", "sync"].includes(command ?? "")) {
336
+ io.err("uso: customy apps <validate|codegen|sync> [--file customy.app.json]");
337
+ return 2;
338
+ }
339
+ const manifest = await loadManifest(args, io);
340
+ if (!manifest) return 1;
341
+ if (command === "validate") {
342
+ io.out(`\u2713 ${manifest.key}: manifiesto app/v1 v\xE1lido (${manifest.events.length} eventos, ${manifest.capabilities.length} capabilities)`);
343
+ return 0;
344
+ }
345
+ if (command === "codegen") {
346
+ const out = path__default.default.resolve(io.cwd, option(args, "out") ?? "customy.generated.ts");
347
+ await promises.mkdir(path__default.default.dirname(out), { recursive: true });
348
+ await promises.writeFile(out, generateAppTypes(manifest));
349
+ io.out(`\u2713 tipos de ${manifest.key} en ${path__default.default.relative(io.cwd, out)}`);
350
+ return 0;
351
+ }
352
+ const workspaceEnvironmentId = option(args, "workspace-env");
353
+ const accessUrl = io.env.CUSTOMY_ACCESS_URL;
354
+ const token = io.env.CUSTOMY_ACCESS_TOKEN;
355
+ if (!workspaceEnvironmentId || !accessUrl || !token) {
356
+ io.err("sync necesita --workspace-env y las variables CUSTOMY_ACCESS_URL y CUSTOMY_ACCESS_TOKEN");
357
+ return 2;
358
+ }
359
+ try {
360
+ const result = await syncApp({
361
+ manifest,
362
+ accessUrl,
363
+ token,
364
+ workspaceEnvironmentId,
365
+ reason: option(args, "reason") ?? `customy apps sync ${manifest.key}`,
366
+ fetch: io.fetch
367
+ });
368
+ const verb = { installed: "instalada", updated: "actualizada", unchanged: "sin cambios" }[result.action];
369
+ io.out(`\u2713 ${manifest.key} ${verb}: aplicaci\xF3n ${result.applicationId}, entorno ${result.environmentId}, revisi\xF3n ${result.revision}`);
370
+ let failed = false;
371
+ for (const [product2, outcome] of Object.entries(result.reconciliation)) {
372
+ if (outcome.status === "failed") {
373
+ failed = true;
374
+ io.err(`\u2717 ${product2}: ${outcome.code} \u2014 vuelve a ejecutar sync para reintentar`);
375
+ } else if (outcome.status === "reconciled") io.out(`\u2713 ${product2}: ${outcome.changed ? "actualizado" : "al d\xEDa"}${outcome.resourceId ? ` (${outcome.resourceId})` : ""}`);
376
+ }
377
+ return failed ? 1 : 0;
378
+ } catch (error) {
379
+ io.err(`\u2717 sync: ${error instanceof Error ? error.message : error}`);
380
+ return 1;
381
+ }
382
+ }
383
+
384
+ exports.SyncError = SyncError;
385
+ exports.generateAppTypes = generateAppTypes;
386
+ exports.runCli = runCli;
387
+ exports.schemaToType = schemaToType;
388
+ exports.syncApp = syncApp;
389
+ exports.validateManifest = validateManifest;