@bacano/sdk 1.2.7 → 1.2.8
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/CATALOG_MANAGEMENT.md +261 -0
- package/README.md +14 -5
- package/STOREFRONT_FORMS.md +38 -0
- package/STOREFRONT_LEADS.md +5 -1
- package/STOREFRONT_PAYMENTS.md +49 -2
- package/catalog-draft-types.d.ts +145 -0
- package/catalog-management-types.d.ts +143 -0
- package/catalog-management.cjs +125 -0
- package/catalog-management.d.ts +37 -0
- package/catalog-management.js +104 -0
- package/index.cjs +153 -63
- package/index.js +153 -63
- package/modules/checkout-contact.d.ts +4 -0
- package/package.json +6 -1
- package/react.cjs +153 -63
- package/react.js +153 -63
- package/skills/bacano-web/SKILL.md +10 -3
- package/skills/bacano-web/references/common.md +5 -0
- package/skills/bacano-web/references/forms.md +2 -0
- package/skills/bacano-web/references/storefront.md +15 -0
- package/skills/bacano-web/scripts/cli.mjs +28 -5
package/react.js
CHANGED
|
@@ -5877,6 +5877,78 @@ function hasRecoverableCartSessionError(errors) {
|
|
|
5877
5877
|
);
|
|
5878
5878
|
}
|
|
5879
5879
|
|
|
5880
|
+
// libs/sdk/src/modules/checkout-contact.ts
|
|
5881
|
+
var EMAIL_PATTERN = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
|
|
5882
|
+
var PHONE_PATTERN = /^\+?\d{7,15}$/;
|
|
5883
|
+
function validEmail(value) {
|
|
5884
|
+
if (value.length > 320 || !EMAIL_PATTERN.test(value)) return false;
|
|
5885
|
+
const [local, domain] = value.split("@");
|
|
5886
|
+
if (!local || !domain || local.length > 64 || domain.length > 255 || local.startsWith(".") || local.endsWith(".") || local.includes("..")) {
|
|
5887
|
+
return false;
|
|
5888
|
+
}
|
|
5889
|
+
return domain.split(".").every((label) => /^[a-z\d](?:[a-z\d-]*[a-z\d])?$/i.test(label));
|
|
5890
|
+
}
|
|
5891
|
+
function normalizePhone(value) {
|
|
5892
|
+
return value.trim().replace(/[\s().-]/g, "");
|
|
5893
|
+
}
|
|
5894
|
+
function invalidContact(field) {
|
|
5895
|
+
const messages = {
|
|
5896
|
+
email: "Revisa el correo electr\xF3nico: debe estar completo y sin espacios.",
|
|
5897
|
+
phone: "Revisa el n\xFAmero de tel\xE9fono: usa entre 7 y 15 d\xEDgitos.",
|
|
5898
|
+
contact: "Ingresa un correo electr\xF3nico o un n\xFAmero de tel\xE9fono v\xE1lido."
|
|
5899
|
+
};
|
|
5900
|
+
return new WebsiteError(
|
|
5901
|
+
messages[field],
|
|
5902
|
+
`INVALID_CHECKOUT_${field.toUpperCase()}`
|
|
5903
|
+
);
|
|
5904
|
+
}
|
|
5905
|
+
function normalizeCheckoutContact(contact, mode) {
|
|
5906
|
+
const email = contact.email?.trim() || void 0;
|
|
5907
|
+
const phone = contact.phone ? normalizePhone(contact.phone) : void 0;
|
|
5908
|
+
const cellphone = contact.cellphone ? normalizePhone(contact.cellphone) : void 0;
|
|
5909
|
+
const emailValid = !email || validEmail(email);
|
|
5910
|
+
const phoneValid = !phone || PHONE_PATTERN.test(phone);
|
|
5911
|
+
const cellphoneValid = !cellphone || PHONE_PATTERN.test(cellphone);
|
|
5912
|
+
if (mode === "submit") {
|
|
5913
|
+
if (!emailValid) throw invalidContact("email");
|
|
5914
|
+
if (!phoneValid || !cellphoneValid) throw invalidContact("phone");
|
|
5915
|
+
}
|
|
5916
|
+
const normalized = {
|
|
5917
|
+
...contact,
|
|
5918
|
+
name: contact.name?.trim() || void 0,
|
|
5919
|
+
firstName: contact.firstName?.trim() || void 0,
|
|
5920
|
+
lastName: contact.lastName?.trim() || void 0,
|
|
5921
|
+
email: emailValid ? email : void 0,
|
|
5922
|
+
phone: phoneValid ? phone : void 0,
|
|
5923
|
+
cellphone: cellphoneValid ? cellphone : void 0
|
|
5924
|
+
};
|
|
5925
|
+
if ((normalized.name?.length ?? 0) > 160 || (normalized.firstName?.length ?? 0) > 80 || (normalized.lastName?.length ?? 0) > 80) {
|
|
5926
|
+
throw new WebsiteError(
|
|
5927
|
+
"Revisa los datos de contacto: el nombre es demasiado largo.",
|
|
5928
|
+
"INVALID_CHECKOUT_CONTACT"
|
|
5929
|
+
);
|
|
5930
|
+
}
|
|
5931
|
+
if (!normalized.email && !normalized.phone && !normalized.cellphone) {
|
|
5932
|
+
throw invalidContact(
|
|
5933
|
+
!emailValid ? "email" : !phoneValid || !cellphoneValid ? "phone" : "contact"
|
|
5934
|
+
);
|
|
5935
|
+
}
|
|
5936
|
+
return normalized;
|
|
5937
|
+
}
|
|
5938
|
+
function explainCheckoutRequestError(error) {
|
|
5939
|
+
if (error instanceof GraphQLRequestError && error.graphqlErrors.some(
|
|
5940
|
+
(item) => /invalid request payload|hay un error en los datos del pedido/i.test(
|
|
5941
|
+
item.message
|
|
5942
|
+
)
|
|
5943
|
+
)) {
|
|
5944
|
+
throw new WebsiteError(
|
|
5945
|
+
"Hay un error en los datos del pedido. Rev\xEDsalos e intenta de nuevo.",
|
|
5946
|
+
"INVALID_CHECKOUT_DATA"
|
|
5947
|
+
);
|
|
5948
|
+
}
|
|
5949
|
+
throw error;
|
|
5950
|
+
}
|
|
5951
|
+
|
|
5880
5952
|
// libs/sdk/src/modules/cart.ts
|
|
5881
5953
|
function mapCartItem(raw) {
|
|
5882
5954
|
return {
|
|
@@ -5952,6 +6024,15 @@ function requireCartSession(storage) {
|
|
|
5952
6024
|
return session;
|
|
5953
6025
|
}
|
|
5954
6026
|
function createCartModule(graphqlUrl, getHeaders, getContext, cartSessionStorage) {
|
|
6027
|
+
let pendingCartSessionOperation = Promise.resolve();
|
|
6028
|
+
function enqueueCartSessionOperation(run) {
|
|
6029
|
+
const operation = pendingCartSessionOperation.then(run, run);
|
|
6030
|
+
pendingCartSessionOperation = operation.then(
|
|
6031
|
+
() => void 0,
|
|
6032
|
+
() => void 0
|
|
6033
|
+
);
|
|
6034
|
+
return operation;
|
|
6035
|
+
}
|
|
5955
6036
|
async function fetchCart(orderId, cartToken) {
|
|
5956
6037
|
const data = await executeGraphQL(graphqlUrl, GET_CART, { orderId, cartToken }, getHeaders);
|
|
5957
6038
|
const result = data.websiteGetCart;
|
|
@@ -5974,72 +6055,80 @@ function createCartModule(graphqlUrl, getHeaders, getContext, cartSessionStorage
|
|
|
5974
6055
|
}
|
|
5975
6056
|
return {
|
|
5976
6057
|
async getOrCreate(contact, fulfillmentFamily) {
|
|
5977
|
-
|
|
5978
|
-
|
|
5979
|
-
|
|
5980
|
-
|
|
5981
|
-
|
|
5982
|
-
|
|
5983
|
-
|
|
5984
|
-
|
|
5985
|
-
|
|
5986
|
-
|
|
5987
|
-
CREATE_CART,
|
|
5988
|
-
{
|
|
5989
|
-
websiteId: ctx.websiteId,
|
|
5990
|
-
cartToken: existing?.cartToken,
|
|
5991
|
-
contact,
|
|
5992
|
-
fulfillmentFamily
|
|
5993
|
-
},
|
|
5994
|
-
getHeaders
|
|
5995
|
-
);
|
|
5996
|
-
const result = data.websiteCreateCart;
|
|
5997
|
-
assertActionSuccess(result, "CART_CREATE_ERROR");
|
|
5998
|
-
cartSessionStorage.set({
|
|
5999
|
-
orderId: result.order_id,
|
|
6000
|
-
cartToken: result.cart_token
|
|
6001
|
-
});
|
|
6002
|
-
return mapCart(result.cart, result.cart_token);
|
|
6003
|
-
},
|
|
6004
|
-
async startCheckout(input) {
|
|
6005
|
-
const ctx = getContext();
|
|
6006
|
-
if (!ctx.chainId) {
|
|
6007
|
-
throw new WebsiteError(
|
|
6008
|
-
"This website has no chain configured. Checkout requires a chain_id.",
|
|
6009
|
-
"NO_CHAIN"
|
|
6010
|
-
);
|
|
6011
|
-
}
|
|
6012
|
-
const requestStartCheckout = async (cartToken) => {
|
|
6058
|
+
return enqueueCartSessionOperation(async () => {
|
|
6059
|
+
const normalizedContact = contact ? normalizeCheckoutContact(contact, "draft") : void 0;
|
|
6060
|
+
const ctx = getContext();
|
|
6061
|
+
if (!ctx.chainId) {
|
|
6062
|
+
throw new WebsiteError(
|
|
6063
|
+
"This website has no chain configured. Cart requires a chain_id.",
|
|
6064
|
+
"NO_CHAIN"
|
|
6065
|
+
);
|
|
6066
|
+
}
|
|
6067
|
+
const existing = cartSessionStorage.get();
|
|
6013
6068
|
const data = await executeGraphQL(
|
|
6014
6069
|
graphqlUrl,
|
|
6015
|
-
|
|
6070
|
+
CREATE_CART,
|
|
6016
6071
|
{
|
|
6017
6072
|
websiteId: ctx.websiteId,
|
|
6018
|
-
cartToken,
|
|
6019
|
-
contact:
|
|
6020
|
-
fulfillmentFamily
|
|
6021
|
-
items: input.items.map((item) => ({
|
|
6022
|
-
product_variant_id: item.productVariantId,
|
|
6023
|
-
quantity: item.quantity
|
|
6024
|
-
})),
|
|
6025
|
-
marketingAttribution: input.marketingAttribution ? serializeWebsiteMarketingAttribution(input.marketingAttribution) : void 0
|
|
6073
|
+
cartToken: existing?.cartToken,
|
|
6074
|
+
contact: normalizedContact,
|
|
6075
|
+
fulfillmentFamily
|
|
6026
6076
|
},
|
|
6027
6077
|
getHeaders
|
|
6028
|
-
);
|
|
6029
|
-
|
|
6030
|
-
|
|
6031
|
-
|
|
6032
|
-
|
|
6033
|
-
|
|
6034
|
-
|
|
6035
|
-
result
|
|
6036
|
-
}
|
|
6037
|
-
|
|
6038
|
-
|
|
6039
|
-
|
|
6040
|
-
|
|
6078
|
+
).catch(explainCheckoutRequestError);
|
|
6079
|
+
const result = data.websiteCreateCart;
|
|
6080
|
+
assertActionSuccess(result, "CART_CREATE_ERROR");
|
|
6081
|
+
cartSessionStorage.set({
|
|
6082
|
+
orderId: result.order_id,
|
|
6083
|
+
cartToken: result.cart_token
|
|
6084
|
+
});
|
|
6085
|
+
return mapCart(result.cart, result.cart_token);
|
|
6086
|
+
});
|
|
6087
|
+
},
|
|
6088
|
+
async startCheckout(input) {
|
|
6089
|
+
return enqueueCartSessionOperation(async () => {
|
|
6090
|
+
const contact = normalizeCheckoutContact(input.contact, "draft");
|
|
6091
|
+
const ctx = getContext();
|
|
6092
|
+
if (!ctx.chainId) {
|
|
6093
|
+
throw new WebsiteError(
|
|
6094
|
+
"This website has no chain configured. Checkout requires a chain_id.",
|
|
6095
|
+
"NO_CHAIN"
|
|
6096
|
+
);
|
|
6097
|
+
}
|
|
6098
|
+
const requestStartCheckout = async (cartToken) => {
|
|
6099
|
+
const data = await executeGraphQL(
|
|
6100
|
+
graphqlUrl,
|
|
6101
|
+
START_GUEST_CHECKOUT,
|
|
6102
|
+
{
|
|
6103
|
+
websiteId: ctx.websiteId,
|
|
6104
|
+
cartToken,
|
|
6105
|
+
contact,
|
|
6106
|
+
fulfillmentFamily: input.fulfillmentFamily,
|
|
6107
|
+
items: input.items.map((item) => ({
|
|
6108
|
+
product_variant_id: item.productVariantId,
|
|
6109
|
+
quantity: item.quantity
|
|
6110
|
+
})),
|
|
6111
|
+
marketingAttribution: input.marketingAttribution ? serializeWebsiteMarketingAttribution(
|
|
6112
|
+
input.marketingAttribution
|
|
6113
|
+
) : void 0
|
|
6114
|
+
},
|
|
6115
|
+
getHeaders
|
|
6116
|
+
).catch(explainCheckoutRequestError);
|
|
6117
|
+
return data.websiteStartGuestCheckout;
|
|
6118
|
+
};
|
|
6119
|
+
const existing = cartSessionStorage.get();
|
|
6120
|
+
let result = await requestStartCheckout(existing?.cartToken);
|
|
6121
|
+
if (existing && !result.success && hasRecoverableCartSessionError(result.errors)) {
|
|
6122
|
+
cartSessionStorage.clear();
|
|
6123
|
+
result = await requestStartCheckout();
|
|
6124
|
+
}
|
|
6125
|
+
assertActionSuccess(result, "CHECKOUT_START_ERROR");
|
|
6126
|
+
cartSessionStorage.set({
|
|
6127
|
+
orderId: result.order_id,
|
|
6128
|
+
cartToken: result.cart_token
|
|
6129
|
+
});
|
|
6130
|
+
return mapCart(result.cart, result.cart_token);
|
|
6041
6131
|
});
|
|
6042
|
-
return mapCart(result.cart, result.cart_token);
|
|
6043
6132
|
},
|
|
6044
6133
|
async get() {
|
|
6045
6134
|
const session = cartSessionStorage.get();
|
|
@@ -6578,6 +6667,7 @@ function createCheckoutModule(graphqlUrl, getHeaders, getContext, cartSessionSto
|
|
|
6578
6667
|
return mapCouponResult(data.websiteRemoveCoupon);
|
|
6579
6668
|
},
|
|
6580
6669
|
async submit(input) {
|
|
6670
|
+
const contact = normalizeCheckoutContact(input.contact, "submit");
|
|
6581
6671
|
const session = requireCartSession2(cartSessionStorage);
|
|
6582
6672
|
const data = await executeGraphQL(
|
|
6583
6673
|
graphqlUrl,
|
|
@@ -6585,7 +6675,7 @@ function createCheckoutModule(graphqlUrl, getHeaders, getContext, cartSessionSto
|
|
|
6585
6675
|
{
|
|
6586
6676
|
orderId: session.orderId,
|
|
6587
6677
|
cartToken: session.cartToken,
|
|
6588
|
-
contact
|
|
6678
|
+
contact,
|
|
6589
6679
|
fulfillmentFamily: input.fulfillmentFamily,
|
|
6590
6680
|
address: input.address,
|
|
6591
6681
|
shippingQuoteId: input.shippingQuoteId,
|
|
@@ -6605,7 +6695,7 @@ function createCheckoutModule(graphqlUrl, getHeaders, getContext, cartSessionSto
|
|
|
6605
6695
|
} : void 0
|
|
6606
6696
|
},
|
|
6607
6697
|
getHeaders
|
|
6608
|
-
);
|
|
6698
|
+
).catch(explainCheckoutRequestError);
|
|
6609
6699
|
const result = data.websiteCheckout;
|
|
6610
6700
|
if (!result.success && result.errors && result.errors.length > 0) {
|
|
6611
6701
|
throw new CheckoutError(result.errors);
|
|
@@ -6646,7 +6736,7 @@ function createCheckoutModule(graphqlUrl, getHeaders, getContext, cartSessionSto
|
|
|
6646
6736
|
returnUrl: input.returnUrl
|
|
6647
6737
|
},
|
|
6648
6738
|
getHeaders
|
|
6649
|
-
);
|
|
6739
|
+
).catch(explainCheckoutRequestError);
|
|
6650
6740
|
const result = data.websiteCreatePaymentSession;
|
|
6651
6741
|
if (result.success && result.payment_session_id && (result.integration_mode !== "redirect" || !result.redirect_url)) {
|
|
6652
6742
|
throw new WebsiteError(
|
|
@@ -6681,7 +6771,7 @@ function createCheckoutModule(graphqlUrl, getHeaders, getContext, cartSessionSto
|
|
|
6681
6771
|
gatewayTransactionId: input.gatewayTransactionId
|
|
6682
6772
|
},
|
|
6683
6773
|
getHeaders
|
|
6684
|
-
);
|
|
6774
|
+
).catch(explainCheckoutRequestError);
|
|
6685
6775
|
const result = data.websiteReconcilePaymentSession;
|
|
6686
6776
|
return {
|
|
6687
6777
|
success: result.success,
|
|
@@ -54,9 +54,16 @@ otros contratos únicamente cuando esas capacidades existan. Leer
|
|
|
54
54
|
`STOREFRONT_CONSTRUCTION.md` cuando el website permita ocultarse temporalmente.
|
|
55
55
|
Si la tienda acepta pagos, leer obligatoriamente `STOREFRONT_PAYMENTS.md` antes
|
|
56
56
|
de implementar, depurar o auditar checkout, retorno, reintentos o webhooks.
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
57
|
+
En un checkout con autoguardado, comprobar además que no cree pedidos con
|
|
58
|
+
contactos parciales ni solicitudes simultáneas; tras un rechazo, el reintento
|
|
59
|
+
debe conservar el mismo pedido incluso si la sesión de pasarela sigue
|
|
60
|
+
`pending`. Aplicar los casos de [references/storefront.md](references/storefront.md).
|
|
61
|
+
Si existe cualquier formulario visible, leer obligatoriamente
|
|
62
|
+
`STOREFRONT_FORMS.md`. Para formularios propios del storefront, comprobar que
|
|
63
|
+
Zod valida el flujo real, con pruebas de errores por campo y bloqueo del envío
|
|
64
|
+
inválido; su mera presencia en `package.json` no basta. Si el formulario es
|
|
65
|
+
administrado desde Bacano, leer además [references/forms.md](references/forms.md)
|
|
66
|
+
y no duplicar su definición publicada en Zod.
|
|
60
67
|
Cuando marketing esté activo y existan CTA o formularios, leer
|
|
61
68
|
[references/interactions.md](references/interactions.md): inventariar los CTA,
|
|
62
69
|
asignar un propietario por evento y usar los helpers oficiales del SDK. Esta
|
|
@@ -76,6 +76,11 @@ función apagada en un bloqueo artificial.
|
|
|
76
76
|
- Navegación por teclado, foco visible, landmarks, labels, contraste y zoom.
|
|
77
77
|
- Formularios con errores específicos, estados de envío y prevención de doble
|
|
78
78
|
envío. No usar solo color para comunicar estado.
|
|
79
|
+
- En formularios propios del storefront, verificar Zod en dependencias **y** en
|
|
80
|
+
el código que valida antes del envío; comprobar pruebas para valores válidos,
|
|
81
|
+
inválidos y corrección del error. Si falta, registrar `P1 · Calidad`; si un
|
|
82
|
+
dato inválido llega a checkout o impide comprar, evaluar además el flujo como
|
|
83
|
+
`P0 · Gate`. No exigir un esquema Zod paralelo a `BacanoForm`.
|
|
79
84
|
- Probar anchos móviles, tablet y escritorio sin scroll interno accidental.
|
|
80
85
|
|
|
81
86
|
## Legal y privacidad
|
|
@@ -25,6 +25,8 @@ ya resuelve la identidad; no ejecutar una segunda captura por campaña.
|
|
|
25
25
|
- Reutilizar el único `BacanoProvider` y cliente global del sitio. No crear otro
|
|
26
26
|
cliente, consultar GraphQL ni llamar endpoints internos.
|
|
27
27
|
- No duplicar keys, validaciones, opciones o estructura en un esquema local.
|
|
28
|
+
En particular, no imponer un esquema Zod paralelo a la definición publicada;
|
|
29
|
+
Zod corresponde a formularios propios del storefront (ver `STOREFRONT_FORMS.md`).
|
|
28
30
|
- En Static First, obtener la definición durante build/servidor y entregarla
|
|
29
31
|
como `initialForm`. La revalidación runtime no debe borrar el HTML inicial.
|
|
30
32
|
- Renderizar campos directos, secciones visuales y pasos según su semántica. Una
|
|
@@ -56,8 +56,18 @@ Aplicar además de los controles comunes.
|
|
|
56
56
|
## Carrito y checkout
|
|
57
57
|
|
|
58
58
|
- Carrito invitado persiste y sincroniza sin duplicar líneas.
|
|
59
|
+
- Los campos propios de checkout se validan con Zod antes de llamar al SDK.
|
|
60
|
+
Inspeccionar el esquema, el punto de envío y pruebas de errores por campo:
|
|
61
|
+
tener Zod instalado o usar solo `type="email"`/`required` no basta. El SDK y
|
|
62
|
+
Bacano mantienen la validación autoritativa; la web no inventa reglas de pago.
|
|
59
63
|
- Si una operación bloquea temporalmente otras mutaciones del carrito, mostrar
|
|
60
64
|
el indicador de carga en el producto accionado, no en todas las tarjetas.
|
|
65
|
+
- Si hay autoguardado, no crear un pedido con datos de contacto parciales.
|
|
66
|
+
Validar los campos requeridos por ese checkout y probar escritura rápida del
|
|
67
|
+
teléfono/correo: una intención de compra mantiene un `orderId` estable.
|
|
68
|
+
- Serializar los inicios de checkout que pueden solaparse (autoguardado, cupón,
|
|
69
|
+
envío y botón final). Confirmar en un trace que el segundo usa el token
|
|
70
|
+
entregado por el primero; no asumir que un debounce evita todas las carreras.
|
|
61
71
|
- Probar alta, baja, cambios de cantidad, límites de stock, cupón, envío gratis,
|
|
62
72
|
impuestos, moneda y redondeo.
|
|
63
73
|
- Probar solo los modos activos entre COUNTER, PICKUP y SHIPPING; propia y
|
|
@@ -78,6 +88,11 @@ Aplicar además de los controles comunes.
|
|
|
78
88
|
de riesgo.
|
|
79
89
|
- La página de respuesta no emite `purchase`; Bacano lo hace desde hechos
|
|
80
90
|
autoritativos. Repetir webhook/conciliación no duplica conversiones.
|
|
91
|
+
- Después de abrir la pasarela, comprobar que el storefront conserva el enlace
|
|
92
|
+
al pedido pero no un carrito local que cree otro pedido al volver. Ante un
|
|
93
|
+
rechazo con sesión todavía `pending`, la respuesta debe ofrecer
|
|
94
|
+
`RETRY_PAYMENT` en el mismo pedido; nunca inferir aprobación o rechazo solo
|
|
95
|
+
del retorno del navegador.
|
|
81
96
|
- Pedido, pago, entrega y desembolso permanecen conceptos separados.
|
|
82
97
|
- Probar consulta pública segura y cuenta autenticada sin filtrar pedidos de
|
|
83
98
|
otra persona, website o canal.
|
|
@@ -6,6 +6,7 @@ import {
|
|
|
6
6
|
existsSync,
|
|
7
7
|
lstatSync,
|
|
8
8
|
mkdirSync,
|
|
9
|
+
readdirSync,
|
|
9
10
|
readFileSync,
|
|
10
11
|
readlinkSync,
|
|
11
12
|
rmSync,
|
|
@@ -17,7 +18,6 @@ import { fileURLToPath } from "node:url";
|
|
|
17
18
|
|
|
18
19
|
const scriptDirectory = dirname(fileURLToPath(import.meta.url));
|
|
19
20
|
const sourceSkill = resolve(scriptDirectory, "..");
|
|
20
|
-
const sourceSkillFile = resolve(sourceSkill, "SKILL.md");
|
|
21
21
|
const projectRoot = process.cwd();
|
|
22
22
|
const canonicalSkill = resolve(projectRoot, ".agents", "skills", "bacano-web");
|
|
23
23
|
|
|
@@ -43,8 +43,27 @@ function option(name) {
|
|
|
43
43
|
return index >= 0 ? process.argv[index + 1] : null;
|
|
44
44
|
}
|
|
45
45
|
|
|
46
|
-
function
|
|
47
|
-
|
|
46
|
+
function skillFingerprint(directory) {
|
|
47
|
+
const digest = createHash("sha256");
|
|
48
|
+
|
|
49
|
+
function visit(current, prefix = "") {
|
|
50
|
+
for (const entry of readdirSync(current, { withFileTypes: true }).sort(
|
|
51
|
+
(first, second) => first.name.localeCompare(second.name),
|
|
52
|
+
)) {
|
|
53
|
+
const name = prefix ? `${prefix}/${entry.name}` : entry.name;
|
|
54
|
+
const path = resolve(current, entry.name);
|
|
55
|
+
if (entry.isDirectory()) {
|
|
56
|
+
visit(path, name);
|
|
57
|
+
} else if (entry.isFile()) {
|
|
58
|
+
digest.update(name).update("\0").update(readFileSync(path));
|
|
59
|
+
} else if (entry.isSymbolicLink()) {
|
|
60
|
+
digest.update(name).update("\0").update(readlinkSync(path));
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
visit(directory);
|
|
66
|
+
return digest.digest("hex");
|
|
48
67
|
}
|
|
49
68
|
|
|
50
69
|
function skillFile(directory) {
|
|
@@ -70,7 +89,8 @@ function installCanonical(force) {
|
|
|
70
89
|
if (resolve(sourceSkill) === canonicalSkill) return;
|
|
71
90
|
|
|
72
91
|
if (existsSync(skillFile(canonicalSkill))) {
|
|
73
|
-
if (
|
|
92
|
+
if (skillFingerprint(canonicalSkill) === skillFingerprint(sourceSkill))
|
|
93
|
+
return;
|
|
74
94
|
removeExisting(canonicalSkill, force);
|
|
75
95
|
} else if (existsSync(canonicalSkill)) {
|
|
76
96
|
removeExisting(canonicalSkill, force);
|
|
@@ -170,7 +190,10 @@ function checkPath(name, path) {
|
|
|
170
190
|
return {
|
|
171
191
|
name,
|
|
172
192
|
path: relativeDisplay(path),
|
|
173
|
-
status:
|
|
193
|
+
status:
|
|
194
|
+
skillFingerprint(path) === skillFingerprint(sourceSkill)
|
|
195
|
+
? "current"
|
|
196
|
+
: "outdated",
|
|
174
197
|
link: lstatSync(path).isSymbolicLink(),
|
|
175
198
|
};
|
|
176
199
|
}
|