mango-lollipop 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/CLAUDE.md +69 -0
- package/LICENSE +21 -0
- package/README.md +264 -0
- package/bin/mango-lollipop.js +385 -0
- package/dist/excel.d.ts +4 -0
- package/dist/excel.js +342 -0
- package/dist/html.d.ts +4 -0
- package/dist/html.js +938 -0
- package/dist/schema.d.ts +120 -0
- package/dist/schema.js +211 -0
- package/lib/excel.ts +433 -0
- package/lib/html.ts +993 -0
- package/lib/schema.ts +394 -0
- package/package.json +44 -0
- package/skills/audit/SKILL.md +248 -0
- package/skills/dev-handoff/SKILL.md +295 -0
- package/skills/generate-dashboard/SKILL.md +195 -0
- package/skills/generate-matrix/SKILL.md +374 -0
- package/skills/generate-messages/SKILL.md +262 -0
- package/skills/iterate/SKILL.md +242 -0
- package/skills/start/SKILL.md +310 -0
- package/templates/copywriting-guide.md +155 -0
- package/templates/dashboard.html +522 -0
- package/templates/events/saas-collaboration.yaml +50 -0
- package/templates/events/saas-document.yaml +44 -0
- package/templates/events/saas-general.yaml +38 -0
- package/templates/events/saas-marketplace.yaml +48 -0
- package/templates/overview.html +598 -0
- package/templates/saas-matrix.json +172 -0
package/lib/excel.ts
ADDED
|
@@ -0,0 +1,433 @@
|
|
|
1
|
+
// =============================================================================
|
|
2
|
+
// Mango Lollipop — Excel Generation (xlsx-js-style)
|
|
3
|
+
// =============================================================================
|
|
4
|
+
|
|
5
|
+
import XLSX from "xlsx-js-style";
|
|
6
|
+
import { writeFileSync } from "fs";
|
|
7
|
+
import type {
|
|
8
|
+
Message,
|
|
9
|
+
EventTaxonomy,
|
|
10
|
+
Channel,
|
|
11
|
+
AARRRStage,
|
|
12
|
+
Analysis,
|
|
13
|
+
} from "./schema.js";
|
|
14
|
+
|
|
15
|
+
// -----------------------------------------------------------------------------
|
|
16
|
+
// Stage display names and colors
|
|
17
|
+
// -----------------------------------------------------------------------------
|
|
18
|
+
|
|
19
|
+
const STAGE_LABELS: Record<AARRRStage | "TX", string> = {
|
|
20
|
+
TX: "Transactional",
|
|
21
|
+
AQ: "Acquisition",
|
|
22
|
+
AC: "Activation",
|
|
23
|
+
RV: "Revenue",
|
|
24
|
+
RT: "Retention",
|
|
25
|
+
RF: "Referral",
|
|
26
|
+
};
|
|
27
|
+
|
|
28
|
+
const STAGE_COLORS: Record<string, { fill: string }> = {
|
|
29
|
+
TX: { fill: "F0F0F0" },
|
|
30
|
+
AQ: { fill: "D4EDDA" },
|
|
31
|
+
AC: { fill: "CCE5FF" },
|
|
32
|
+
RV: { fill: "FFF3CD" },
|
|
33
|
+
RT: { fill: "FFE5CC" },
|
|
34
|
+
RF: { fill: "E8D5F5" },
|
|
35
|
+
};
|
|
36
|
+
|
|
37
|
+
const HEADER_STYLE: XLSX.CellStyle = {
|
|
38
|
+
fill: { fgColor: { rgb: "374151" } },
|
|
39
|
+
font: { bold: true, color: { rgb: "FFFFFF" }, sz: 11 },
|
|
40
|
+
alignment: { vertical: "center" },
|
|
41
|
+
};
|
|
42
|
+
|
|
43
|
+
// -----------------------------------------------------------------------------
|
|
44
|
+
// Helpers
|
|
45
|
+
// -----------------------------------------------------------------------------
|
|
46
|
+
|
|
47
|
+
function guardsToString(msg: Message): string {
|
|
48
|
+
if (!msg.guards.length) return "\u2014";
|
|
49
|
+
return msg.guards.map((g) => g.condition).join("; ");
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
function suppressionsToString(msg: Message): string {
|
|
53
|
+
if (!msg.suppressions.length) return "\u2014";
|
|
54
|
+
return msg.suppressions.map((s) => s.condition).join("; ");
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
function channelToString(msg: Message): string {
|
|
58
|
+
if (msg.channel) return msg.channel;
|
|
59
|
+
if ((msg as any).channels) return (msg as any).channels.join(", ");
|
|
60
|
+
return "\u2014";
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
function tagsToString(tags: string[]): string {
|
|
64
|
+
return tags.join(", ");
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
// Apply header style to first row of a worksheet
|
|
68
|
+
function applyHeaderStyle(ws: XLSX.WorkSheet): void {
|
|
69
|
+
const range = XLSX.utils.decode_range(ws["!ref"] ?? "A1");
|
|
70
|
+
for (let c = range.s.c; c <= range.e.c; c++) {
|
|
71
|
+
const addr = XLSX.utils.encode_cell({ r: 0, c });
|
|
72
|
+
if (ws[addr]) {
|
|
73
|
+
ws[addr].s = HEADER_STYLE;
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
// Apply stage-based row fills to data rows
|
|
79
|
+
function applyStageColors(
|
|
80
|
+
ws: XLSX.WorkSheet,
|
|
81
|
+
stageColIndex: number
|
|
82
|
+
): void {
|
|
83
|
+
const range = XLSX.utils.decode_range(ws["!ref"] ?? "A1");
|
|
84
|
+
for (let r = 1; r <= range.e.r; r++) {
|
|
85
|
+
const stageCell =
|
|
86
|
+
ws[XLSX.utils.encode_cell({ r, c: stageColIndex })];
|
|
87
|
+
if (!stageCell) continue;
|
|
88
|
+
|
|
89
|
+
// Resolve stage code from label or raw value
|
|
90
|
+
const val = String(stageCell.v ?? "");
|
|
91
|
+
let stageCode = val;
|
|
92
|
+
for (const [code, label] of Object.entries(STAGE_LABELS)) {
|
|
93
|
+
if (label === val) {
|
|
94
|
+
stageCode = code;
|
|
95
|
+
break;
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
const color = STAGE_COLORS[stageCode];
|
|
100
|
+
if (!color) continue;
|
|
101
|
+
|
|
102
|
+
const fillStyle: XLSX.CellStyle = {
|
|
103
|
+
fill: { fgColor: { rgb: color.fill } },
|
|
104
|
+
};
|
|
105
|
+
|
|
106
|
+
for (let c = range.s.c; c <= range.e.c; c++) {
|
|
107
|
+
const addr = XLSX.utils.encode_cell({ r, c });
|
|
108
|
+
if (ws[addr]) {
|
|
109
|
+
ws[addr].s = fillStyle;
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
// -----------------------------------------------------------------------------
|
|
116
|
+
// Sheet builders
|
|
117
|
+
// -----------------------------------------------------------------------------
|
|
118
|
+
|
|
119
|
+
function buildWelcomeSheet(
|
|
120
|
+
analysis: Analysis,
|
|
121
|
+
messageCount: number
|
|
122
|
+
): XLSX.WorkSheet {
|
|
123
|
+
const pathLabel = analysis.path === "fresh" ? "Fresh" : "Improving Existing";
|
|
124
|
+
const channels = analysis.channels.join(", ");
|
|
125
|
+
const generated = new Date().toLocaleDateString("en-US", {
|
|
126
|
+
year: "numeric",
|
|
127
|
+
month: "long",
|
|
128
|
+
day: "numeric",
|
|
129
|
+
});
|
|
130
|
+
|
|
131
|
+
const data: (string | number)[][] = [
|
|
132
|
+
["Mango Lollipop \u2014 Lifecycle Messaging Matrix"],
|
|
133
|
+
[],
|
|
134
|
+
["Company", analysis.company.name],
|
|
135
|
+
["Product", analysis.company.product_type],
|
|
136
|
+
["Channels", channels],
|
|
137
|
+
["Generated", generated],
|
|
138
|
+
["Total Messages", messageCount],
|
|
139
|
+
["Path", pathLabel],
|
|
140
|
+
[],
|
|
141
|
+
["How to Use This Spreadsheet"],
|
|
142
|
+
[
|
|
143
|
+
"Transactional Messages",
|
|
144
|
+
"Mandatory system messages (email verification, password reset, receipts)",
|
|
145
|
+
],
|
|
146
|
+
[
|
|
147
|
+
"Lifecycle Matrix",
|
|
148
|
+
"Your AARRR messaging strategy with triggers, guards, and suppressions",
|
|
149
|
+
],
|
|
150
|
+
[
|
|
151
|
+
"Event Taxonomy",
|
|
152
|
+
"All product events and which messages they trigger",
|
|
153
|
+
],
|
|
154
|
+
["Tags", "Tag inventory with message counts"],
|
|
155
|
+
[
|
|
156
|
+
"Channel Strategy",
|
|
157
|
+
"Message distribution by channel and stage",
|
|
158
|
+
],
|
|
159
|
+
[],
|
|
160
|
+
[],
|
|
161
|
+
[
|
|
162
|
+
"Generated by Mango Lollipop \u2014 https://github.com/sr-kai/mango-lollipop",
|
|
163
|
+
],
|
|
164
|
+
["Made by Sasha Kai with probably too much coffee."],
|
|
165
|
+
];
|
|
166
|
+
|
|
167
|
+
const ws = XLSX.utils.aoa_to_sheet(data);
|
|
168
|
+
|
|
169
|
+
// Style the title row
|
|
170
|
+
if (ws["A1"]) {
|
|
171
|
+
ws["A1"].s = {
|
|
172
|
+
font: { bold: true, sz: 16, color: { rgb: "1F2937" } },
|
|
173
|
+
};
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
// Style "How to Use" header
|
|
177
|
+
if (ws["A10"]) {
|
|
178
|
+
ws["A10"].s = {
|
|
179
|
+
font: { bold: true, sz: 13, color: { rgb: "1F2937" } },
|
|
180
|
+
};
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
// Style info labels (rows 3-8, column A)
|
|
184
|
+
for (let r = 2; r <= 7; r++) {
|
|
185
|
+
const addr = XLSX.utils.encode_cell({ r, c: 0 });
|
|
186
|
+
if (ws[addr]) {
|
|
187
|
+
ws[addr].s = { font: { bold: true, color: { rgb: "6B7280" } } };
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
// Style sheet tab names (rows 11-15, column A)
|
|
192
|
+
for (let r = 10; r <= 14; r++) {
|
|
193
|
+
const addr = XLSX.utils.encode_cell({ r, c: 0 });
|
|
194
|
+
if (ws[addr]) {
|
|
195
|
+
ws[addr].s = { font: { bold: true, color: { rgb: "374151" } } };
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
// Style footer
|
|
200
|
+
if (ws["A18"]) {
|
|
201
|
+
ws["A18"].s = { font: { color: { rgb: "9CA3AF" }, sz: 10 } };
|
|
202
|
+
}
|
|
203
|
+
if (ws["A19"]) {
|
|
204
|
+
ws["A19"].s = { font: { color: { rgb: "9CA3AF" }, sz: 10 } };
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
// Column widths
|
|
208
|
+
ws["!cols"] = [{ wch: 30 }, { wch: 70 }];
|
|
209
|
+
|
|
210
|
+
return ws;
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
function buildTransactionalSheet(messages: Message[]): XLSX.WorkSheet {
|
|
214
|
+
const tx = messages.filter((m) => m.classification === "transactional");
|
|
215
|
+
const rows = tx.map((m) => ({
|
|
216
|
+
ID: m.id,
|
|
217
|
+
Name: m.name,
|
|
218
|
+
"Trigger Event": m.trigger.event,
|
|
219
|
+
"Trigger Type": m.trigger.type,
|
|
220
|
+
Wait: m.wait,
|
|
221
|
+
Channel: channelToString(m),
|
|
222
|
+
CTA: m.cta.text,
|
|
223
|
+
From: m.from,
|
|
224
|
+
Tags: tagsToString(m.tags),
|
|
225
|
+
}));
|
|
226
|
+
const ws = XLSX.utils.json_to_sheet(rows);
|
|
227
|
+
autoFitColumns(ws, rows);
|
|
228
|
+
applyHeaderStyle(ws);
|
|
229
|
+
// TX rows all get the TX color
|
|
230
|
+
applyStageColors(ws, -1); // special handling below
|
|
231
|
+
// For TX sheet, all rows are TX — apply fill to all data rows
|
|
232
|
+
const range = XLSX.utils.decode_range(ws["!ref"] ?? "A1");
|
|
233
|
+
const txFill: XLSX.CellStyle = {
|
|
234
|
+
fill: { fgColor: { rgb: STAGE_COLORS.TX.fill } },
|
|
235
|
+
};
|
|
236
|
+
for (let r = 1; r <= range.e.r; r++) {
|
|
237
|
+
for (let c = range.s.c; c <= range.e.c; c++) {
|
|
238
|
+
const addr = XLSX.utils.encode_cell({ r, c });
|
|
239
|
+
if (ws[addr]) ws[addr].s = txFill;
|
|
240
|
+
}
|
|
241
|
+
}
|
|
242
|
+
return ws;
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
function buildLifecycleSheet(messages: Message[]): XLSX.WorkSheet {
|
|
246
|
+
const lc = messages.filter((m) => m.classification === "lifecycle");
|
|
247
|
+
const rows = lc.map((m) => ({
|
|
248
|
+
ID: m.id,
|
|
249
|
+
Stage: STAGE_LABELS[m.stage] ?? m.stage,
|
|
250
|
+
Name: m.name,
|
|
251
|
+
"Trigger Event": m.trigger.event,
|
|
252
|
+
"Trigger Type": m.trigger.type,
|
|
253
|
+
Wait: m.wait,
|
|
254
|
+
Guards: guardsToString(m),
|
|
255
|
+
Suppressions: suppressionsToString(m),
|
|
256
|
+
Channel: channelToString(m),
|
|
257
|
+
CTA: m.cta.text,
|
|
258
|
+
Goal: m.goal,
|
|
259
|
+
Segment: m.segment,
|
|
260
|
+
Tags: tagsToString(m.tags),
|
|
261
|
+
Format: m.format,
|
|
262
|
+
From: m.from,
|
|
263
|
+
}));
|
|
264
|
+
const ws = XLSX.utils.json_to_sheet(rows);
|
|
265
|
+
autoFitColumns(ws, rows);
|
|
266
|
+
applyHeaderStyle(ws);
|
|
267
|
+
applyStageColors(ws, 1); // Stage is column B (index 1)
|
|
268
|
+
return ws;
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
function buildEventTaxonomySheet(
|
|
272
|
+
events: EventTaxonomy,
|
|
273
|
+
messages: Message[]
|
|
274
|
+
): XLSX.WorkSheet {
|
|
275
|
+
const rows: { Category: string; Event: string; "Used By": string }[] = [];
|
|
276
|
+
|
|
277
|
+
const categories: (keyof EventTaxonomy)[] = [
|
|
278
|
+
"identity",
|
|
279
|
+
"activation",
|
|
280
|
+
"engagement",
|
|
281
|
+
"conversion",
|
|
282
|
+
"retention",
|
|
283
|
+
];
|
|
284
|
+
|
|
285
|
+
for (const category of categories) {
|
|
286
|
+
const eventList = events[category] ?? [];
|
|
287
|
+
for (const event of eventList) {
|
|
288
|
+
const usedBy = messages
|
|
289
|
+
.filter((m) => m.trigger.event === event)
|
|
290
|
+
.map((m) => m.id)
|
|
291
|
+
.join(", ");
|
|
292
|
+
rows.push({
|
|
293
|
+
Category: category.charAt(0).toUpperCase() + category.slice(1),
|
|
294
|
+
Event: event,
|
|
295
|
+
"Used By": usedBy || "\u2014",
|
|
296
|
+
});
|
|
297
|
+
}
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
const ws = XLSX.utils.json_to_sheet(rows);
|
|
301
|
+
autoFitColumns(ws, rows);
|
|
302
|
+
applyHeaderStyle(ws);
|
|
303
|
+
return ws;
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
function buildTagsSheet(
|
|
307
|
+
messages: Message[],
|
|
308
|
+
tagDefinitions: string[]
|
|
309
|
+
): XLSX.WorkSheet {
|
|
310
|
+
const allTags = new Set<string>([
|
|
311
|
+
...tagDefinitions,
|
|
312
|
+
...messages.flatMap((m) => m.tags),
|
|
313
|
+
]);
|
|
314
|
+
|
|
315
|
+
const rows = Array.from(allTags)
|
|
316
|
+
.sort()
|
|
317
|
+
.map((tag) => {
|
|
318
|
+
const msgIds = messages
|
|
319
|
+
.filter((m) => m.tags.includes(tag))
|
|
320
|
+
.map((m) => m.id);
|
|
321
|
+
return {
|
|
322
|
+
Tag: tag,
|
|
323
|
+
"Message Count": msgIds.length,
|
|
324
|
+
"Used By": msgIds.join(", ") || "\u2014",
|
|
325
|
+
};
|
|
326
|
+
});
|
|
327
|
+
|
|
328
|
+
const ws = XLSX.utils.json_to_sheet(rows);
|
|
329
|
+
autoFitColumns(ws, rows);
|
|
330
|
+
applyHeaderStyle(ws);
|
|
331
|
+
return ws;
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
function buildChannelStrategySheet(messages: Message[]): XLSX.WorkSheet {
|
|
335
|
+
const channels: Channel[] = ["email", "sms", "in-app", "push"];
|
|
336
|
+
const stages: (AARRRStage | "TX")[] = ["TX", "AQ", "AC", "RV", "RT", "RF"];
|
|
337
|
+
|
|
338
|
+
const rows: Record<string, string | number>[] = [];
|
|
339
|
+
|
|
340
|
+
for (const ch of channels) {
|
|
341
|
+
const msgsWithChannel = messages.filter(
|
|
342
|
+
(m) =>
|
|
343
|
+
m.channel === ch ||
|
|
344
|
+
((m as any).channels && (m as any).channels.includes(ch))
|
|
345
|
+
);
|
|
346
|
+
if (msgsWithChannel.length === 0) continue;
|
|
347
|
+
|
|
348
|
+
const row: Record<string, string | number> = {
|
|
349
|
+
Channel: ch,
|
|
350
|
+
"Total Messages": msgsWithChannel.length,
|
|
351
|
+
};
|
|
352
|
+
|
|
353
|
+
for (const stage of stages) {
|
|
354
|
+
const count = msgsWithChannel.filter((m) => m.stage === stage).length;
|
|
355
|
+
row[STAGE_LABELS[stage]] = count;
|
|
356
|
+
}
|
|
357
|
+
|
|
358
|
+
rows.push(row);
|
|
359
|
+
}
|
|
360
|
+
|
|
361
|
+
const ws = XLSX.utils.json_to_sheet(rows);
|
|
362
|
+
autoFitColumns(ws, rows);
|
|
363
|
+
applyHeaderStyle(ws);
|
|
364
|
+
return ws;
|
|
365
|
+
}
|
|
366
|
+
|
|
367
|
+
// Auto-fit column widths based on content
|
|
368
|
+
function autoFitColumns(
|
|
369
|
+
ws: XLSX.WorkSheet,
|
|
370
|
+
rows: Record<string, unknown>[]
|
|
371
|
+
): void {
|
|
372
|
+
if (!rows.length) return;
|
|
373
|
+
const headers = Object.keys(rows[0]);
|
|
374
|
+
ws["!cols"] = headers.map((h) => {
|
|
375
|
+
let maxWidth = h.length;
|
|
376
|
+
for (const row of rows) {
|
|
377
|
+
const val = String(row[h] ?? "");
|
|
378
|
+
if (val.length > maxWidth) maxWidth = val.length;
|
|
379
|
+
}
|
|
380
|
+
return { wch: Math.min(maxWidth + 2, 60) };
|
|
381
|
+
});
|
|
382
|
+
}
|
|
383
|
+
|
|
384
|
+
// -----------------------------------------------------------------------------
|
|
385
|
+
// Public API
|
|
386
|
+
// -----------------------------------------------------------------------------
|
|
387
|
+
|
|
388
|
+
export function generateMatrixWorkbook(
|
|
389
|
+
messages: Message[],
|
|
390
|
+
events: EventTaxonomy,
|
|
391
|
+
tags: string[],
|
|
392
|
+
analysis?: Analysis
|
|
393
|
+
): XLSX.WorkBook {
|
|
394
|
+
const wb = XLSX.utils.book_new();
|
|
395
|
+
|
|
396
|
+
// Welcome sheet first (if analysis data available)
|
|
397
|
+
if (analysis) {
|
|
398
|
+
XLSX.utils.book_append_sheet(
|
|
399
|
+
wb,
|
|
400
|
+
buildWelcomeSheet(analysis, messages.length),
|
|
401
|
+
"Welcome"
|
|
402
|
+
);
|
|
403
|
+
}
|
|
404
|
+
|
|
405
|
+
XLSX.utils.book_append_sheet(
|
|
406
|
+
wb,
|
|
407
|
+
buildTransactionalSheet(messages),
|
|
408
|
+
"Transactional Messages"
|
|
409
|
+
);
|
|
410
|
+
XLSX.utils.book_append_sheet(
|
|
411
|
+
wb,
|
|
412
|
+
buildLifecycleSheet(messages),
|
|
413
|
+
"Lifecycle Matrix"
|
|
414
|
+
);
|
|
415
|
+
XLSX.utils.book_append_sheet(
|
|
416
|
+
wb,
|
|
417
|
+
buildEventTaxonomySheet(events, messages),
|
|
418
|
+
"Event Taxonomy"
|
|
419
|
+
);
|
|
420
|
+
XLSX.utils.book_append_sheet(wb, buildTagsSheet(messages, tags), "Tags");
|
|
421
|
+
XLSX.utils.book_append_sheet(
|
|
422
|
+
wb,
|
|
423
|
+
buildChannelStrategySheet(messages),
|
|
424
|
+
"Channel Strategy"
|
|
425
|
+
);
|
|
426
|
+
|
|
427
|
+
return wb;
|
|
428
|
+
}
|
|
429
|
+
|
|
430
|
+
export function writeWorkbook(workbook: XLSX.WorkBook, filePath: string): void {
|
|
431
|
+
const buf = XLSX.write(workbook, { type: "buffer", bookType: "xlsx" });
|
|
432
|
+
writeFileSync(filePath, buf);
|
|
433
|
+
}
|