@better-zap/cli 0.0.1

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,15 @@
1
+ ISC License
2
+
3
+ Copyright (c) 2026 Better Zap
4
+
5
+ Permission to use, copy, modify, and/or distribute this software for any
6
+ purpose with or without fee is hereby granted, provided that the above
7
+ copyright notice and this permission notice appear in all copies.
8
+
9
+ THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH
10
+ REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY
11
+ AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT,
12
+ INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM
13
+ LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR
14
+ OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR
15
+ PERFORMANCE OF THIS SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,3 @@
1
+ # @better-zap/cli
2
+
3
+ CLI helpers for Better Zap template generation.
package/dist/cli.cjs ADDED
@@ -0,0 +1,55 @@
1
+ #!/usr/bin/env node
2
+ Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
3
+ const require_template_generator = require("./template-generator-CQM-kLEi.cjs");
4
+ let node_url = require("node:url");
5
+ let node_process = require("node:process");
6
+ node_process = require_template_generator.__toESM(node_process);
7
+ //#region src/cli.ts
8
+ function shouldPrintHelp(command, flags) {
9
+ return !command || command === "--help" || command === "-h" || Boolean(flags.help);
10
+ }
11
+ async function runCli(argv = node_process.default.argv.slice(2)) {
12
+ const { command, flags } = require_template_generator.parseCliArgs(argv);
13
+ const wantsHelp = shouldPrintHelp(command, flags);
14
+ if (!command || wantsHelp) {
15
+ printHelp();
16
+ return;
17
+ }
18
+ if (command !== "generate") throw new Error(`[better-zap] Unknown command "${command}". Supported commands: generate.`);
19
+ const options = await require_template_generator.resolveGenerateOptions({
20
+ cwd: node_process.default.cwd(),
21
+ env: node_process.default.env,
22
+ flags
23
+ });
24
+ const result = await require_template_generator.runGenerateCommand(options);
25
+ if (options.check) {
26
+ console.log(`[better-zap] ${result.output} is up to date (${result.templateCount} templates).`);
27
+ return;
28
+ }
29
+ console.log(`[better-zap] Generated ${result.templateCount} templates at ${result.output}.`);
30
+ }
31
+ function printHelp() {
32
+ console.log(`better-zap
33
+
34
+ Usage:
35
+ better-zap generate [--access-token <token>] [--waba-id <id>] [--output <path>] [--api-version <version>] [--check]
36
+
37
+ Options:
38
+ --access-token Meta access token. Falls back to WHATSAPP_TOKEN or META_ACCESS_TOKEN.
39
+ --waba-id WhatsApp Business Account ID. Falls back to WHATSAPP_BUSINESS_ACCOUNT_ID.
40
+ --output Output path for the generated registry file.
41
+ --api-version Graph API version. Defaults to v25.0.
42
+ --config Optional config file path. Also auto-discovers better-zap.config.(mjs|js|json).
43
+ --check Validate that the generated file is already up to date.
44
+ --help Show this help message.
45
+ `);
46
+ }
47
+ const entrypoint = node_process.default.argv[1];
48
+ if (entrypoint && require("url").pathToFileURL(__filename).href === (0, node_url.pathToFileURL)(entrypoint).href) runCli().catch((error) => {
49
+ const message = error instanceof Error ? error.message : String(error);
50
+ console.error(message);
51
+ node_process.default.exitCode = 1;
52
+ });
53
+ //#endregion
54
+ exports.runCli = runCli;
55
+ exports.shouldPrintHelp = shouldPrintHelp;
package/dist/cli.d.cts ADDED
@@ -0,0 +1,5 @@
1
+ //#region src/cli.d.ts
2
+ declare function shouldPrintHelp(command: string | undefined, flags: Record<string, boolean | string>): boolean;
3
+ declare function runCli(argv?: string[]): Promise<void>;
4
+ //#endregion
5
+ export { runCli, shouldPrintHelp };
package/dist/cli.d.mts ADDED
@@ -0,0 +1,5 @@
1
+ //#region src/cli.d.ts
2
+ declare function shouldPrintHelp(command: string | undefined, flags: Record<string, boolean | string>): boolean;
3
+ declare function runCli(argv?: string[]): Promise<void>;
4
+ //#endregion
5
+ export { runCli, shouldPrintHelp };
package/dist/cli.mjs ADDED
@@ -0,0 +1,52 @@
1
+ #!/usr/bin/env node
2
+ import { a as resolveGenerateOptions, i as parseCliArgs, o as runGenerateCommand } from "./template-generator-DQI3p1ce.mjs";
3
+ import { pathToFileURL } from "node:url";
4
+ import process from "node:process";
5
+ //#region src/cli.ts
6
+ function shouldPrintHelp(command, flags) {
7
+ return !command || command === "--help" || command === "-h" || Boolean(flags.help);
8
+ }
9
+ async function runCli(argv = process.argv.slice(2)) {
10
+ const { command, flags } = parseCliArgs(argv);
11
+ const wantsHelp = shouldPrintHelp(command, flags);
12
+ if (!command || wantsHelp) {
13
+ printHelp();
14
+ return;
15
+ }
16
+ if (command !== "generate") throw new Error(`[better-zap] Unknown command "${command}". Supported commands: generate.`);
17
+ const options = await resolveGenerateOptions({
18
+ cwd: process.cwd(),
19
+ env: process.env,
20
+ flags
21
+ });
22
+ const result = await runGenerateCommand(options);
23
+ if (options.check) {
24
+ console.log(`[better-zap] ${result.output} is up to date (${result.templateCount} templates).`);
25
+ return;
26
+ }
27
+ console.log(`[better-zap] Generated ${result.templateCount} templates at ${result.output}.`);
28
+ }
29
+ function printHelp() {
30
+ console.log(`better-zap
31
+
32
+ Usage:
33
+ better-zap generate [--access-token <token>] [--waba-id <id>] [--output <path>] [--api-version <version>] [--check]
34
+
35
+ Options:
36
+ --access-token Meta access token. Falls back to WHATSAPP_TOKEN or META_ACCESS_TOKEN.
37
+ --waba-id WhatsApp Business Account ID. Falls back to WHATSAPP_BUSINESS_ACCOUNT_ID.
38
+ --output Output path for the generated registry file.
39
+ --api-version Graph API version. Defaults to v25.0.
40
+ --config Optional config file path. Also auto-discovers better-zap.config.(mjs|js|json).
41
+ --check Validate that the generated file is already up to date.
42
+ --help Show this help message.
43
+ `);
44
+ }
45
+ const entrypoint = process.argv[1];
46
+ if (entrypoint && import.meta.url === pathToFileURL(entrypoint).href) runCli().catch((error) => {
47
+ const message = error instanceof Error ? error.message : String(error);
48
+ console.error(message);
49
+ process.exitCode = 1;
50
+ });
51
+ //#endregion
52
+ export { runCli, shouldPrintHelp };
package/dist/index.cjs ADDED
@@ -0,0 +1,9 @@
1
+ Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
2
+ const require_template_generator = require("./template-generator-CQM-kLEi.cjs");
3
+ exports.fetchAllMetaTemplates = require_template_generator.fetchAllMetaTemplates;
4
+ exports.generateTemplateRegistryFileContent = require_template_generator.generateTemplateRegistryFileContent;
5
+ exports.normalizeMetaTemplates = require_template_generator.normalizeMetaTemplates;
6
+ exports.parseCliArgs = require_template_generator.parseCliArgs;
7
+ exports.resolveGenerateOptions = require_template_generator.resolveGenerateOptions;
8
+ exports.runGenerateCommand = require_template_generator.runGenerateCommand;
9
+ exports.writeGeneratedTemplateFile = require_template_generator.writeGeneratedTemplateFile;
@@ -0,0 +1,75 @@
1
+ import { TemplateRegistry } from "better-zap";
2
+
3
+ //#region src/template-generator.d.ts
4
+ type CliFlagValue = boolean | string;
5
+ type CliFlags = Record<string, CliFlagValue>;
6
+ type GenerateOptions = {
7
+ accessToken?: string;
8
+ apiVersion?: string;
9
+ check?: boolean;
10
+ fetchImpl?: typeof fetch;
11
+ output: string;
12
+ wabaId?: string;
13
+ };
14
+ type ResolveGenerateOptionsInput = {
15
+ cwd: string;
16
+ env: NodeJS.ProcessEnv;
17
+ flags: CliFlags;
18
+ };
19
+ type MetaTemplateButton = {
20
+ type?: string;
21
+ phone_number?: string;
22
+ text?: string;
23
+ url?: string;
24
+ };
25
+ type MetaTemplateComponent = {
26
+ type?: string;
27
+ format?: string;
28
+ text?: string;
29
+ url?: string;
30
+ buttons?: MetaTemplateButton[];
31
+ };
32
+ type MetaTemplate = {
33
+ name?: string;
34
+ language?: string;
35
+ components?: MetaTemplateComponent[];
36
+ };
37
+ declare function parseCliArgs(argv: string[]): {
38
+ command: string | undefined;
39
+ flags: CliFlags;
40
+ };
41
+ declare function resolveGenerateOptions({
42
+ cwd,
43
+ env,
44
+ flags
45
+ }: ResolveGenerateOptionsInput): Promise<GenerateOptions>;
46
+ declare function fetchAllMetaTemplates({
47
+ accessToken,
48
+ apiVersion,
49
+ fetchImpl,
50
+ wabaId
51
+ }: Required<Pick<GenerateOptions, "accessToken" | "wabaId">> & Pick<GenerateOptions, "apiVersion" | "fetchImpl">): Promise<MetaTemplate[]>;
52
+ declare function normalizeMetaTemplates(metaTemplates: MetaTemplate[]): TemplateRegistry;
53
+ declare function generateTemplateRegistryFileContent(registry: TemplateRegistry): string;
54
+ declare function writeGeneratedTemplateFile({
55
+ check,
56
+ content,
57
+ output
58
+ }: {
59
+ check?: boolean;
60
+ content: string;
61
+ output: string;
62
+ }): Promise<void>;
63
+ declare function runGenerateCommand({
64
+ accessToken,
65
+ apiVersion,
66
+ check,
67
+ fetchImpl,
68
+ output,
69
+ wabaId
70
+ }: GenerateOptions): Promise<{
71
+ output: string;
72
+ templateCount: number;
73
+ }>;
74
+ //#endregion
75
+ export { fetchAllMetaTemplates, generateTemplateRegistryFileContent, normalizeMetaTemplates, parseCliArgs, resolveGenerateOptions, runGenerateCommand, writeGeneratedTemplateFile };
@@ -0,0 +1,75 @@
1
+ import { TemplateRegistry } from "better-zap";
2
+
3
+ //#region src/template-generator.d.ts
4
+ type CliFlagValue = boolean | string;
5
+ type CliFlags = Record<string, CliFlagValue>;
6
+ type GenerateOptions = {
7
+ accessToken?: string;
8
+ apiVersion?: string;
9
+ check?: boolean;
10
+ fetchImpl?: typeof fetch;
11
+ output: string;
12
+ wabaId?: string;
13
+ };
14
+ type ResolveGenerateOptionsInput = {
15
+ cwd: string;
16
+ env: NodeJS.ProcessEnv;
17
+ flags: CliFlags;
18
+ };
19
+ type MetaTemplateButton = {
20
+ type?: string;
21
+ phone_number?: string;
22
+ text?: string;
23
+ url?: string;
24
+ };
25
+ type MetaTemplateComponent = {
26
+ type?: string;
27
+ format?: string;
28
+ text?: string;
29
+ url?: string;
30
+ buttons?: MetaTemplateButton[];
31
+ };
32
+ type MetaTemplate = {
33
+ name?: string;
34
+ language?: string;
35
+ components?: MetaTemplateComponent[];
36
+ };
37
+ declare function parseCliArgs(argv: string[]): {
38
+ command: string | undefined;
39
+ flags: CliFlags;
40
+ };
41
+ declare function resolveGenerateOptions({
42
+ cwd,
43
+ env,
44
+ flags
45
+ }: ResolveGenerateOptionsInput): Promise<GenerateOptions>;
46
+ declare function fetchAllMetaTemplates({
47
+ accessToken,
48
+ apiVersion,
49
+ fetchImpl,
50
+ wabaId
51
+ }: Required<Pick<GenerateOptions, "accessToken" | "wabaId">> & Pick<GenerateOptions, "apiVersion" | "fetchImpl">): Promise<MetaTemplate[]>;
52
+ declare function normalizeMetaTemplates(metaTemplates: MetaTemplate[]): TemplateRegistry;
53
+ declare function generateTemplateRegistryFileContent(registry: TemplateRegistry): string;
54
+ declare function writeGeneratedTemplateFile({
55
+ check,
56
+ content,
57
+ output
58
+ }: {
59
+ check?: boolean;
60
+ content: string;
61
+ output: string;
62
+ }): Promise<void>;
63
+ declare function runGenerateCommand({
64
+ accessToken,
65
+ apiVersion,
66
+ check,
67
+ fetchImpl,
68
+ output,
69
+ wabaId
70
+ }: GenerateOptions): Promise<{
71
+ output: string;
72
+ templateCount: number;
73
+ }>;
74
+ //#endregion
75
+ export { fetchAllMetaTemplates, generateTemplateRegistryFileContent, normalizeMetaTemplates, parseCliArgs, resolveGenerateOptions, runGenerateCommand, writeGeneratedTemplateFile };
package/dist/index.mjs ADDED
@@ -0,0 +1,2 @@
1
+ import { a as resolveGenerateOptions, i as parseCliArgs, n as generateTemplateRegistryFileContent, o as runGenerateCommand, r as normalizeMetaTemplates, s as writeGeneratedTemplateFile, t as fetchAllMetaTemplates } from "./template-generator-DQI3p1ce.mjs";
2
+ export { fetchAllMetaTemplates, generateTemplateRegistryFileContent, normalizeMetaTemplates, parseCliArgs, resolveGenerateOptions, runGenerateCommand, writeGeneratedTemplateFile };
@@ -0,0 +1,307 @@
1
+ //#region \0rolldown/runtime.js
2
+ var __create = Object.create;
3
+ var __defProp = Object.defineProperty;
4
+ var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
5
+ var __getOwnPropNames = Object.getOwnPropertyNames;
6
+ var __getProtoOf = Object.getPrototypeOf;
7
+ var __hasOwnProp = Object.prototype.hasOwnProperty;
8
+ var __copyProps = (to, from, except, desc) => {
9
+ if (from && typeof from === "object" || typeof from === "function") for (var keys = __getOwnPropNames(from), i = 0, n = keys.length, key; i < n; i++) {
10
+ key = keys[i];
11
+ if (!__hasOwnProp.call(to, key) && key !== except) __defProp(to, key, {
12
+ get: ((k) => from[k]).bind(null, key),
13
+ enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable
14
+ });
15
+ }
16
+ return to;
17
+ };
18
+ var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", {
19
+ value: mod,
20
+ enumerable: true
21
+ }) : target, mod));
22
+ //#endregion
23
+ let node_fs_promises = require("node:fs/promises");
24
+ node_fs_promises = __toESM(node_fs_promises);
25
+ let node_path = require("node:path");
26
+ node_path = __toESM(node_path);
27
+ let node_url = require("node:url");
28
+ //#region src/template-generator.ts
29
+ const DEFAULT_API_VERSION = "v25.0";
30
+ const DEFAULT_CONFIG_FILES = [
31
+ "better-zap.config.mjs",
32
+ "better-zap.config.js",
33
+ "better-zap.config.json"
34
+ ];
35
+ function parseCliArgs(argv) {
36
+ const args = [...argv];
37
+ const command = args.shift();
38
+ const flags = {};
39
+ while (args.length > 0) {
40
+ const current = args.shift();
41
+ if (!current?.startsWith("--")) continue;
42
+ const flagName = current.slice(2);
43
+ const next = args[0];
44
+ if (!next || next.startsWith("--")) {
45
+ flags[flagName] = true;
46
+ continue;
47
+ }
48
+ const value = args.shift();
49
+ if (value) flags[flagName] = value;
50
+ }
51
+ return {
52
+ command,
53
+ flags
54
+ };
55
+ }
56
+ async function resolveGenerateOptions({ cwd, env, flags }) {
57
+ const configPath = typeof flags.config === "string" ? node_path.default.resolve(cwd, flags.config) : await findDefaultConfigPath(cwd);
58
+ const fileConfig = configPath ? await loadConfigFile(configPath) : {};
59
+ const templatesConfig = fileConfig.templates ?? fileConfig.whatsappTemplates ?? fileConfig;
60
+ const outputValue = firstDefined(asString(flags.output), templatesConfig.output, env.BETTER_ZAP_TEMPLATE_OUTPUT) ?? "";
61
+ const output = outputValue ? node_path.default.resolve(cwd, outputValue) : "";
62
+ return {
63
+ accessToken: firstDefined(asString(flags["access-token"]), templatesConfig.accessToken, env.WHATSAPP_TOKEN, env.META_ACCESS_TOKEN),
64
+ wabaId: firstDefined(asString(flags["waba-id"]), templatesConfig.wabaId, env.WHATSAPP_BUSINESS_ACCOUNT_ID),
65
+ apiVersion: firstDefined(asString(flags["api-version"]), templatesConfig.apiVersion, DEFAULT_API_VERSION),
66
+ output,
67
+ check: Boolean(flags.check)
68
+ };
69
+ }
70
+ async function fetchAllMetaTemplates({ accessToken, apiVersion, fetchImpl = fetch, wabaId }) {
71
+ let nextUrl = `https://graph.facebook.com/${apiVersion ?? DEFAULT_API_VERSION}/${wabaId}/message_templates?limit=100`;
72
+ const templates = [];
73
+ while (nextUrl) {
74
+ const response = await fetchImpl(nextUrl, { headers: { Authorization: `Bearer ${accessToken}` } });
75
+ if (!response.ok) {
76
+ const errorPayload = await response.json().catch(() => ({ error: { message: `HTTP ${response.status}` } }));
77
+ throw new Error(`[better-zap generate] Meta request failed: ${errorPayload.error?.message ?? `HTTP ${response.status}`}`);
78
+ }
79
+ const payload = await response.json();
80
+ templates.push(...payload.data ?? []);
81
+ nextUrl = payload.paging?.next ?? null;
82
+ }
83
+ return templates;
84
+ }
85
+ function normalizeMetaTemplates(metaTemplates) {
86
+ const normalizedEntries = metaTemplates.map((template) => [template.name, normalizeMetaTemplate(template)]).sort(([left], [right]) => (left ?? "").localeCompare(right ?? ""));
87
+ return Object.fromEntries(normalizedEntries);
88
+ }
89
+ function generateTemplateRegistryFileContent(registry) {
90
+ return `/* eslint-disable */
91
+ // Generated by better-zap generate. Edit the source templates in Meta and re-run the generator.
92
+
93
+ import { defineTemplates } from "better-zap";
94
+
95
+ export const whatsappTemplates = defineTemplates(${JSON.stringify(registry, null, 2)} as const);
96
+
97
+ export type AppWhatsAppTemplates = typeof whatsappTemplates;
98
+ `;
99
+ }
100
+ async function writeGeneratedTemplateFile({ check = false, content, output }) {
101
+ let currentContent = "";
102
+ try {
103
+ currentContent = await node_fs_promises.default.readFile(output, "utf8");
104
+ } catch (error) {
105
+ if (!(error instanceof Error) || !error.code || error.code !== "ENOENT") throw error;
106
+ }
107
+ if (check) {
108
+ if (currentContent !== content) throw new Error(`[better-zap generate] ${output} is out of date. Re-run the generator.`);
109
+ return;
110
+ }
111
+ await node_fs_promises.default.mkdir(node_path.default.dirname(output), { recursive: true });
112
+ await node_fs_promises.default.writeFile(output, content, "utf8");
113
+ }
114
+ async function runGenerateCommand({ accessToken, apiVersion = DEFAULT_API_VERSION, check = false, fetchImpl = fetch, output, wabaId }) {
115
+ if (!accessToken || !wabaId || !output) throw new Error("[better-zap generate] Missing required options. Expected access token, WABA ID, and output path.");
116
+ const registry = normalizeMetaTemplates(await fetchAllMetaTemplates({
117
+ accessToken,
118
+ apiVersion,
119
+ fetchImpl,
120
+ wabaId
121
+ }));
122
+ await writeGeneratedTemplateFile({
123
+ check,
124
+ content: generateTemplateRegistryFileContent(registry),
125
+ output
126
+ });
127
+ return {
128
+ output,
129
+ templateCount: Object.keys(registry).length
130
+ };
131
+ }
132
+ async function findDefaultConfigPath(cwd) {
133
+ for (const configFile of DEFAULT_CONFIG_FILES) {
134
+ const absolutePath = node_path.default.resolve(cwd, configFile);
135
+ try {
136
+ await node_fs_promises.default.access(absolutePath);
137
+ return absolutePath;
138
+ } catch {}
139
+ }
140
+ return null;
141
+ }
142
+ async function loadConfigFile(configPath) {
143
+ if (configPath.endsWith(".json")) {
144
+ const rawConfig = await node_fs_promises.default.readFile(configPath, "utf8");
145
+ return JSON.parse(rawConfig);
146
+ }
147
+ const module = await import((0, node_url.pathToFileURL)(configPath).href);
148
+ return module.default ?? module;
149
+ }
150
+ function normalizeMetaTemplate(template) {
151
+ if (!template.name || !template.language) throw new Error("[better-zap generate] Template entry is missing name or language.");
152
+ const normalizedComponents = [];
153
+ for (const component of template.components ?? []) {
154
+ const type = toUpper(component.type);
155
+ if (type === "BODY") {
156
+ normalizedComponents.push(normalizeBodyComponent(component));
157
+ continue;
158
+ }
159
+ if (type === "HEADER") {
160
+ normalizedComponents.push(normalizeHeaderComponent(component));
161
+ continue;
162
+ }
163
+ if (type === "BUTTONS") {
164
+ normalizedComponents.push(...normalizeButtonsComponent(component));
165
+ continue;
166
+ }
167
+ if (type === "FOOTER") continue;
168
+ throw new Error(`[better-zap generate] Unsupported template component type "${component.type}" in "${template.name}".`);
169
+ }
170
+ return {
171
+ language: template.language,
172
+ components: normalizedComponents
173
+ };
174
+ }
175
+ function normalizeBodyComponent(component) {
176
+ return {
177
+ type: "body",
178
+ parameters: extractPlaceholderTokens(component.text).map((token, index) => ({
179
+ name: createTextParameterName("body", token, index + 1),
180
+ type: "text"
181
+ }))
182
+ };
183
+ }
184
+ function normalizeHeaderComponent(component) {
185
+ const format = toUpper(component.format ?? "TEXT");
186
+ if (format === "LOCATION") return {
187
+ type: "header",
188
+ parameters: [{
189
+ name: "header_location",
190
+ type: "location"
191
+ }]
192
+ };
193
+ if (format === "TEXT") return {
194
+ type: "header",
195
+ parameters: extractPlaceholderTokens(component.text).map((token, index) => ({
196
+ name: createTextParameterName("header", token, index + 1),
197
+ type: "text"
198
+ }))
199
+ };
200
+ if (format === "IMAGE" || format === "VIDEO" || format === "DOCUMENT") return {
201
+ type: "header",
202
+ parameters: [{
203
+ name: `header_${format.toLowerCase()}`,
204
+ type: format.toLowerCase()
205
+ }]
206
+ };
207
+ throw new Error(`[better-zap generate] Unsupported header format "${component.format}".`);
208
+ }
209
+ function normalizeButtonsComponent(component) {
210
+ return (component.buttons ?? []).map((button, index) => normalizeButton(button, index));
211
+ }
212
+ function normalizeButton(button, index) {
213
+ const type = toUpper(button.type);
214
+ if (type === "QUICK_REPLY") return {
215
+ type: "button",
216
+ subType: "quick_reply",
217
+ index: String(index),
218
+ parameters: [{
219
+ name: `button_${index}_payload`,
220
+ type: "payload"
221
+ }]
222
+ };
223
+ if (type === "URL") {
224
+ const tokens = extractPlaceholderTokens(button.url);
225
+ return {
226
+ type: "button",
227
+ subType: "url",
228
+ index: String(index),
229
+ parameters: tokens.map((token, tokenIndex) => ({
230
+ name: createTextParameterName(`button_${index}_text`, token, tokenIndex + 1),
231
+ type: "text"
232
+ }))
233
+ };
234
+ }
235
+ throw new Error(`[better-zap generate] Unsupported button type "${button.type}" at index ${index}.`);
236
+ }
237
+ function extractPlaceholderTokens(value) {
238
+ if (typeof value !== "string" || value.length === 0) return [];
239
+ const tokens = [];
240
+ for (const match of value.matchAll(/\{\{\s*([A-Za-z0-9_]+)\s*\}\}/g)) tokens.push(match[1]);
241
+ return tokens;
242
+ }
243
+ function createTextParameterName(prefix, token, position) {
244
+ if (token && !/^\d+$/.test(token)) return `${prefix}_${sanitizeName(token)}`;
245
+ return `${prefix}_${position}`;
246
+ }
247
+ function sanitizeName(value) {
248
+ return value.toLowerCase().replace(/[^a-z0-9_]+/g, "_");
249
+ }
250
+ function toUpper(value) {
251
+ return typeof value === "string" ? value.toUpperCase() : "";
252
+ }
253
+ function firstDefined(...values) {
254
+ return values.find((value) => typeof value === "string" && value.length > 0);
255
+ }
256
+ function asString(value) {
257
+ return typeof value === "string" ? value : void 0;
258
+ }
259
+ //#endregion
260
+ Object.defineProperty(exports, "__toESM", {
261
+ enumerable: true,
262
+ get: function() {
263
+ return __toESM;
264
+ }
265
+ });
266
+ Object.defineProperty(exports, "fetchAllMetaTemplates", {
267
+ enumerable: true,
268
+ get: function() {
269
+ return fetchAllMetaTemplates;
270
+ }
271
+ });
272
+ Object.defineProperty(exports, "generateTemplateRegistryFileContent", {
273
+ enumerable: true,
274
+ get: function() {
275
+ return generateTemplateRegistryFileContent;
276
+ }
277
+ });
278
+ Object.defineProperty(exports, "normalizeMetaTemplates", {
279
+ enumerable: true,
280
+ get: function() {
281
+ return normalizeMetaTemplates;
282
+ }
283
+ });
284
+ Object.defineProperty(exports, "parseCliArgs", {
285
+ enumerable: true,
286
+ get: function() {
287
+ return parseCliArgs;
288
+ }
289
+ });
290
+ Object.defineProperty(exports, "resolveGenerateOptions", {
291
+ enumerable: true,
292
+ get: function() {
293
+ return resolveGenerateOptions;
294
+ }
295
+ });
296
+ Object.defineProperty(exports, "runGenerateCommand", {
297
+ enumerable: true,
298
+ get: function() {
299
+ return runGenerateCommand;
300
+ }
301
+ });
302
+ Object.defineProperty(exports, "writeGeneratedTemplateFile", {
303
+ enumerable: true,
304
+ get: function() {
305
+ return writeGeneratedTemplateFile;
306
+ }
307
+ });
@@ -0,0 +1,236 @@
1
+ import fs from "node:fs/promises";
2
+ import path from "node:path";
3
+ import { pathToFileURL } from "node:url";
4
+ //#region src/template-generator.ts
5
+ const DEFAULT_API_VERSION = "v25.0";
6
+ const DEFAULT_CONFIG_FILES = [
7
+ "better-zap.config.mjs",
8
+ "better-zap.config.js",
9
+ "better-zap.config.json"
10
+ ];
11
+ function parseCliArgs(argv) {
12
+ const args = [...argv];
13
+ const command = args.shift();
14
+ const flags = {};
15
+ while (args.length > 0) {
16
+ const current = args.shift();
17
+ if (!current?.startsWith("--")) continue;
18
+ const flagName = current.slice(2);
19
+ const next = args[0];
20
+ if (!next || next.startsWith("--")) {
21
+ flags[flagName] = true;
22
+ continue;
23
+ }
24
+ const value = args.shift();
25
+ if (value) flags[flagName] = value;
26
+ }
27
+ return {
28
+ command,
29
+ flags
30
+ };
31
+ }
32
+ async function resolveGenerateOptions({ cwd, env, flags }) {
33
+ const configPath = typeof flags.config === "string" ? path.resolve(cwd, flags.config) : await findDefaultConfigPath(cwd);
34
+ const fileConfig = configPath ? await loadConfigFile(configPath) : {};
35
+ const templatesConfig = fileConfig.templates ?? fileConfig.whatsappTemplates ?? fileConfig;
36
+ const outputValue = firstDefined(asString(flags.output), templatesConfig.output, env.BETTER_ZAP_TEMPLATE_OUTPUT) ?? "";
37
+ const output = outputValue ? path.resolve(cwd, outputValue) : "";
38
+ return {
39
+ accessToken: firstDefined(asString(flags["access-token"]), templatesConfig.accessToken, env.WHATSAPP_TOKEN, env.META_ACCESS_TOKEN),
40
+ wabaId: firstDefined(asString(flags["waba-id"]), templatesConfig.wabaId, env.WHATSAPP_BUSINESS_ACCOUNT_ID),
41
+ apiVersion: firstDefined(asString(flags["api-version"]), templatesConfig.apiVersion, DEFAULT_API_VERSION),
42
+ output,
43
+ check: Boolean(flags.check)
44
+ };
45
+ }
46
+ async function fetchAllMetaTemplates({ accessToken, apiVersion, fetchImpl = fetch, wabaId }) {
47
+ let nextUrl = `https://graph.facebook.com/${apiVersion ?? DEFAULT_API_VERSION}/${wabaId}/message_templates?limit=100`;
48
+ const templates = [];
49
+ while (nextUrl) {
50
+ const response = await fetchImpl(nextUrl, { headers: { Authorization: `Bearer ${accessToken}` } });
51
+ if (!response.ok) {
52
+ const errorPayload = await response.json().catch(() => ({ error: { message: `HTTP ${response.status}` } }));
53
+ throw new Error(`[better-zap generate] Meta request failed: ${errorPayload.error?.message ?? `HTTP ${response.status}`}`);
54
+ }
55
+ const payload = await response.json();
56
+ templates.push(...payload.data ?? []);
57
+ nextUrl = payload.paging?.next ?? null;
58
+ }
59
+ return templates;
60
+ }
61
+ function normalizeMetaTemplates(metaTemplates) {
62
+ const normalizedEntries = metaTemplates.map((template) => [template.name, normalizeMetaTemplate(template)]).sort(([left], [right]) => (left ?? "").localeCompare(right ?? ""));
63
+ return Object.fromEntries(normalizedEntries);
64
+ }
65
+ function generateTemplateRegistryFileContent(registry) {
66
+ return `/* eslint-disable */
67
+ // Generated by better-zap generate. Edit the source templates in Meta and re-run the generator.
68
+
69
+ import { defineTemplates } from "better-zap";
70
+
71
+ export const whatsappTemplates = defineTemplates(${JSON.stringify(registry, null, 2)} as const);
72
+
73
+ export type AppWhatsAppTemplates = typeof whatsappTemplates;
74
+ `;
75
+ }
76
+ async function writeGeneratedTemplateFile({ check = false, content, output }) {
77
+ let currentContent = "";
78
+ try {
79
+ currentContent = await fs.readFile(output, "utf8");
80
+ } catch (error) {
81
+ if (!(error instanceof Error) || !error.code || error.code !== "ENOENT") throw error;
82
+ }
83
+ if (check) {
84
+ if (currentContent !== content) throw new Error(`[better-zap generate] ${output} is out of date. Re-run the generator.`);
85
+ return;
86
+ }
87
+ await fs.mkdir(path.dirname(output), { recursive: true });
88
+ await fs.writeFile(output, content, "utf8");
89
+ }
90
+ async function runGenerateCommand({ accessToken, apiVersion = DEFAULT_API_VERSION, check = false, fetchImpl = fetch, output, wabaId }) {
91
+ if (!accessToken || !wabaId || !output) throw new Error("[better-zap generate] Missing required options. Expected access token, WABA ID, and output path.");
92
+ const registry = normalizeMetaTemplates(await fetchAllMetaTemplates({
93
+ accessToken,
94
+ apiVersion,
95
+ fetchImpl,
96
+ wabaId
97
+ }));
98
+ await writeGeneratedTemplateFile({
99
+ check,
100
+ content: generateTemplateRegistryFileContent(registry),
101
+ output
102
+ });
103
+ return {
104
+ output,
105
+ templateCount: Object.keys(registry).length
106
+ };
107
+ }
108
+ async function findDefaultConfigPath(cwd) {
109
+ for (const configFile of DEFAULT_CONFIG_FILES) {
110
+ const absolutePath = path.resolve(cwd, configFile);
111
+ try {
112
+ await fs.access(absolutePath);
113
+ return absolutePath;
114
+ } catch {}
115
+ }
116
+ return null;
117
+ }
118
+ async function loadConfigFile(configPath) {
119
+ if (configPath.endsWith(".json")) {
120
+ const rawConfig = await fs.readFile(configPath, "utf8");
121
+ return JSON.parse(rawConfig);
122
+ }
123
+ const module = await import(pathToFileURL(configPath).href);
124
+ return module.default ?? module;
125
+ }
126
+ function normalizeMetaTemplate(template) {
127
+ if (!template.name || !template.language) throw new Error("[better-zap generate] Template entry is missing name or language.");
128
+ const normalizedComponents = [];
129
+ for (const component of template.components ?? []) {
130
+ const type = toUpper(component.type);
131
+ if (type === "BODY") {
132
+ normalizedComponents.push(normalizeBodyComponent(component));
133
+ continue;
134
+ }
135
+ if (type === "HEADER") {
136
+ normalizedComponents.push(normalizeHeaderComponent(component));
137
+ continue;
138
+ }
139
+ if (type === "BUTTONS") {
140
+ normalizedComponents.push(...normalizeButtonsComponent(component));
141
+ continue;
142
+ }
143
+ if (type === "FOOTER") continue;
144
+ throw new Error(`[better-zap generate] Unsupported template component type "${component.type}" in "${template.name}".`);
145
+ }
146
+ return {
147
+ language: template.language,
148
+ components: normalizedComponents
149
+ };
150
+ }
151
+ function normalizeBodyComponent(component) {
152
+ return {
153
+ type: "body",
154
+ parameters: extractPlaceholderTokens(component.text).map((token, index) => ({
155
+ name: createTextParameterName("body", token, index + 1),
156
+ type: "text"
157
+ }))
158
+ };
159
+ }
160
+ function normalizeHeaderComponent(component) {
161
+ const format = toUpper(component.format ?? "TEXT");
162
+ if (format === "LOCATION") return {
163
+ type: "header",
164
+ parameters: [{
165
+ name: "header_location",
166
+ type: "location"
167
+ }]
168
+ };
169
+ if (format === "TEXT") return {
170
+ type: "header",
171
+ parameters: extractPlaceholderTokens(component.text).map((token, index) => ({
172
+ name: createTextParameterName("header", token, index + 1),
173
+ type: "text"
174
+ }))
175
+ };
176
+ if (format === "IMAGE" || format === "VIDEO" || format === "DOCUMENT") return {
177
+ type: "header",
178
+ parameters: [{
179
+ name: `header_${format.toLowerCase()}`,
180
+ type: format.toLowerCase()
181
+ }]
182
+ };
183
+ throw new Error(`[better-zap generate] Unsupported header format "${component.format}".`);
184
+ }
185
+ function normalizeButtonsComponent(component) {
186
+ return (component.buttons ?? []).map((button, index) => normalizeButton(button, index));
187
+ }
188
+ function normalizeButton(button, index) {
189
+ const type = toUpper(button.type);
190
+ if (type === "QUICK_REPLY") return {
191
+ type: "button",
192
+ subType: "quick_reply",
193
+ index: String(index),
194
+ parameters: [{
195
+ name: `button_${index}_payload`,
196
+ type: "payload"
197
+ }]
198
+ };
199
+ if (type === "URL") {
200
+ const tokens = extractPlaceholderTokens(button.url);
201
+ return {
202
+ type: "button",
203
+ subType: "url",
204
+ index: String(index),
205
+ parameters: tokens.map((token, tokenIndex) => ({
206
+ name: createTextParameterName(`button_${index}_text`, token, tokenIndex + 1),
207
+ type: "text"
208
+ }))
209
+ };
210
+ }
211
+ throw new Error(`[better-zap generate] Unsupported button type "${button.type}" at index ${index}.`);
212
+ }
213
+ function extractPlaceholderTokens(value) {
214
+ if (typeof value !== "string" || value.length === 0) return [];
215
+ const tokens = [];
216
+ for (const match of value.matchAll(/\{\{\s*([A-Za-z0-9_]+)\s*\}\}/g)) tokens.push(match[1]);
217
+ return tokens;
218
+ }
219
+ function createTextParameterName(prefix, token, position) {
220
+ if (token && !/^\d+$/.test(token)) return `${prefix}_${sanitizeName(token)}`;
221
+ return `${prefix}_${position}`;
222
+ }
223
+ function sanitizeName(value) {
224
+ return value.toLowerCase().replace(/[^a-z0-9_]+/g, "_");
225
+ }
226
+ function toUpper(value) {
227
+ return typeof value === "string" ? value.toUpperCase() : "";
228
+ }
229
+ function firstDefined(...values) {
230
+ return values.find((value) => typeof value === "string" && value.length > 0);
231
+ }
232
+ function asString(value) {
233
+ return typeof value === "string" ? value : void 0;
234
+ }
235
+ //#endregion
236
+ export { resolveGenerateOptions as a, parseCliArgs as i, generateTemplateRegistryFileContent as n, runGenerateCommand as o, normalizeMetaTemplates as r, writeGeneratedTemplateFile as s, fetchAllMetaTemplates as t };
package/package.json ADDED
@@ -0,0 +1,46 @@
1
+ {
2
+ "name": "@better-zap/cli",
3
+ "version": "0.0.1",
4
+ "description": "CLI tools for Better Zap.",
5
+ "license": "ISC",
6
+ "type": "module",
7
+ "main": "./dist/index.cjs",
8
+ "module": "./dist/index.mjs",
9
+ "types": "./dist/index.d.mts",
10
+ "bin": {
11
+ "better-zap": "./dist/cli.mjs"
12
+ },
13
+ "files": [
14
+ "dist",
15
+ "README.md",
16
+ "LICENSE"
17
+ ],
18
+ "exports": {
19
+ ".": {
20
+ "types": "./dist/index.d.mts",
21
+ "import": "./dist/index.mjs",
22
+ "require": "./dist/index.cjs"
23
+ },
24
+ "./package.json": "./package.json"
25
+ },
26
+ "engines": {
27
+ "node": ">=20"
28
+ },
29
+ "publishConfig": {
30
+ "access": "public"
31
+ },
32
+ "dependencies": {
33
+ "better-zap": "0.0.1"
34
+ },
35
+ "devDependencies": {
36
+ "@types/node": "^25.4.0",
37
+ "tsdown": "^0.21.2",
38
+ "typescript": "^5.9.3",
39
+ "vitest": "^4.1.0"
40
+ },
41
+ "scripts": {
42
+ "build": "tsdown",
43
+ "typecheck": "tsc --noEmit",
44
+ "test": "vitest run"
45
+ }
46
+ }