primitive-admin 1.0.21 → 1.0.23

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.
@@ -0,0 +1,366 @@
1
+ import { ApiClient } from "../lib/api-client.js";
2
+ import { getCurrentAppId } from "../lib/config.js";
3
+ import { success, error, info, keyValue, formatTable, formatId, formatDate, json, } from "../lib/output.js";
4
+ function resolveAppId(appId, options) {
5
+ const resolved = appId || options.app || getCurrentAppId();
6
+ if (!resolved) {
7
+ error("No app specified. Use <app-id>, --app, or 'primitive use <app-id>' to set context.");
8
+ process.exit(1);
9
+ }
10
+ return resolved;
11
+ }
12
+ export function registerRuleSetsCommands(program) {
13
+ const ruleSets = program
14
+ .command("rule-sets")
15
+ .description("Create and manage access rule sets for databases")
16
+ .addHelpText("after", `
17
+ Examples:
18
+ $ primitive rule-sets list
19
+ $ primitive rule-sets create "My Rules" --resource-type database --rules '{"Task":{"read":"true","write":"record.createdBy == user.userId"}}'
20
+ $ primitive rule-sets get <rule-set-id>
21
+ $ primitive rule-sets update <rule-set-id> --name "Updated Rules"
22
+ $ primitive rule-sets delete <rule-set-id>
23
+ `);
24
+ // List rule sets
25
+ ruleSets
26
+ .command("list")
27
+ .description("List access rule sets in an app")
28
+ .argument("[app-id]", "App ID (uses current app if not specified)")
29
+ .option("--app <app-id>", "App ID")
30
+ .option("--resource-type <type>", "Filter by resource type (database, document, workflow, api)")
31
+ .option("--json", "Output as JSON")
32
+ .action(async (appId, options) => {
33
+ const resolvedAppId = resolveAppId(appId, options);
34
+ const client = new ApiClient();
35
+ try {
36
+ const params = {};
37
+ if (options.resourceType)
38
+ params.resourceType = options.resourceType;
39
+ const result = await client.listRuleSets(resolvedAppId, params);
40
+ const list = Array.isArray(result) ? result : result?.ruleSets ?? [];
41
+ if (options.json) {
42
+ json(list);
43
+ return;
44
+ }
45
+ if (list.length === 0) {
46
+ info("No rule sets found.");
47
+ return;
48
+ }
49
+ console.log(formatTable(list, [
50
+ { header: "ID", key: "ruleSetId", format: formatId },
51
+ { header: "NAME", key: "name" },
52
+ { header: "TYPE", key: "resourceType" },
53
+ { header: "VERSION", key: "version" },
54
+ { header: "MODIFIED", key: "modifiedAt", format: formatDate },
55
+ ]));
56
+ }
57
+ catch (err) {
58
+ error(err.message);
59
+ process.exit(1);
60
+ }
61
+ });
62
+ // Create rule set
63
+ ruleSets
64
+ .command("create")
65
+ .description("Create a new access rule set")
66
+ .argument("<name>", "Rule set name")
67
+ .requiredOption("--resource-type <type>", "Resource type: database, document, workflow, or api")
68
+ .requiredOption("--rules <json>", "Rules as JSON string")
69
+ .option("--description <text>", "Optional description")
70
+ .option("--app <app-id>", "App ID")
71
+ .option("--json", "Output as JSON")
72
+ .action(async (name, options) => {
73
+ const resolvedAppId = resolveAppId(undefined, options);
74
+ const client = new ApiClient();
75
+ let rules;
76
+ try {
77
+ rules = JSON.parse(options.rules);
78
+ }
79
+ catch {
80
+ error("Invalid JSON in --rules. Provide a valid JSON object.");
81
+ process.exit(1);
82
+ }
83
+ try {
84
+ const data = {
85
+ name,
86
+ resourceType: options.resourceType,
87
+ rules,
88
+ };
89
+ if (options.description)
90
+ data.description = options.description;
91
+ const result = await client.createRuleSet(resolvedAppId, data);
92
+ if (options.json) {
93
+ json(result);
94
+ return;
95
+ }
96
+ success("Rule set created.");
97
+ keyValue("Rule Set ID", result.ruleSetId);
98
+ keyValue("Name", result.name);
99
+ keyValue("Resource Type", result.resourceType);
100
+ keyValue("Version", result.version);
101
+ }
102
+ catch (err) {
103
+ error(err.message);
104
+ process.exit(1);
105
+ }
106
+ });
107
+ // Get rule set
108
+ ruleSets
109
+ .command("get")
110
+ .description("Get rule set details")
111
+ .argument("<rule-set-id>", "Rule set ID")
112
+ .option("--app <app-id>", "App ID")
113
+ .option("--json", "Output as JSON")
114
+ .action(async (ruleSetId, options) => {
115
+ const resolvedAppId = resolveAppId(undefined, options);
116
+ const client = new ApiClient();
117
+ try {
118
+ const result = await client.getRuleSet(resolvedAppId, ruleSetId);
119
+ if (options.json) {
120
+ json(result);
121
+ return;
122
+ }
123
+ keyValue("Rule Set ID", result.ruleSetId);
124
+ keyValue("Name", result.name);
125
+ if (result.description)
126
+ keyValue("Description", result.description);
127
+ keyValue("Resource Type", result.resourceType);
128
+ keyValue("Version", String(result.version));
129
+ keyValue("Created", formatDate(result.createdAt));
130
+ keyValue("Modified", formatDate(result.modifiedAt));
131
+ console.log("\nRules:");
132
+ console.log(JSON.stringify(result.rules, null, 2));
133
+ }
134
+ catch (err) {
135
+ error(err.message);
136
+ process.exit(1);
137
+ }
138
+ });
139
+ // Update rule set
140
+ ruleSets
141
+ .command("update")
142
+ .description("Update a rule set")
143
+ .argument("<rule-set-id>", "Rule set ID")
144
+ .option("--name <name>", "New name")
145
+ .option("--description <text>", "New description")
146
+ .option("--rules <json>", "New rules as JSON string")
147
+ .option("--app <app-id>", "App ID")
148
+ .option("--json", "Output as JSON")
149
+ .action(async (ruleSetId, options) => {
150
+ const resolvedAppId = resolveAppId(undefined, options);
151
+ const client = new ApiClient();
152
+ const data = {};
153
+ if (options.name)
154
+ data.name = options.name;
155
+ if (options.description !== undefined)
156
+ data.description = options.description;
157
+ if (options.rules) {
158
+ try {
159
+ data.rules = JSON.parse(options.rules);
160
+ }
161
+ catch {
162
+ error("Invalid JSON in --rules. Provide a valid JSON object.");
163
+ process.exit(1);
164
+ }
165
+ }
166
+ if (Object.keys(data).length === 0) {
167
+ error("Nothing to update. Provide --name, --description, or --rules.");
168
+ process.exit(1);
169
+ }
170
+ try {
171
+ const result = await client.updateRuleSet(resolvedAppId, ruleSetId, data);
172
+ if (options.json) {
173
+ json(result);
174
+ return;
175
+ }
176
+ success("Rule set updated.");
177
+ keyValue("Rule Set ID", result.ruleSetId);
178
+ keyValue("Name", result.name);
179
+ keyValue("Version", String(result.version));
180
+ }
181
+ catch (err) {
182
+ error(err.message);
183
+ process.exit(1);
184
+ }
185
+ });
186
+ // Delete rule set
187
+ ruleSets
188
+ .command("delete")
189
+ .description("Delete a rule set")
190
+ .argument("<rule-set-id>", "Rule set ID")
191
+ .option("--app <app-id>", "App ID")
192
+ .option("-y, --yes", "Skip confirmation prompt")
193
+ .action(async (ruleSetId, options) => {
194
+ const resolvedAppId = resolveAppId(undefined, options);
195
+ if (!options.yes) {
196
+ const inquirer = await import("inquirer");
197
+ const { confirm } = await inquirer.default.prompt([
198
+ {
199
+ type: "confirm",
200
+ name: "confirm",
201
+ message: `Delete rule set ${ruleSetId}? This cannot be undone.`,
202
+ default: false,
203
+ },
204
+ ]);
205
+ if (!confirm) {
206
+ info("Cancelled.");
207
+ return;
208
+ }
209
+ }
210
+ const client = new ApiClient();
211
+ try {
212
+ await client.deleteRuleSet(resolvedAppId, ruleSetId);
213
+ success(`Rule set ${ruleSetId} deleted.`);
214
+ }
215
+ catch (err) {
216
+ error(err.message);
217
+ process.exit(1);
218
+ }
219
+ });
220
+ // Schema
221
+ ruleSets
222
+ .command("schema")
223
+ .description("Display the rule set schema (available context variables and functions)")
224
+ .option("--app <app-id>", "App ID")
225
+ .option("--json", "Output as JSON")
226
+ .action(async (options) => {
227
+ const resolvedAppId = resolveAppId(undefined, options);
228
+ const client = new ApiClient();
229
+ try {
230
+ const result = await client.getRuleSetSchema(resolvedAppId);
231
+ if (options.json) {
232
+ json(result);
233
+ return;
234
+ }
235
+ console.log(JSON.stringify(result, null, 2));
236
+ }
237
+ catch (err) {
238
+ error(err.message);
239
+ process.exit(1);
240
+ }
241
+ });
242
+ // Test / dry-run
243
+ ruleSets
244
+ .command("test")
245
+ .description("Test a rule set against a simulated scenario (dry-run)")
246
+ .argument("<rule-set-id>", "Rule set ID")
247
+ .requiredOption("--scenario <json>", "Scenario as JSON string")
248
+ .option("--app <app-id>", "App ID")
249
+ .option("--json", "Output as JSON")
250
+ .action(async (ruleSetId, options) => {
251
+ const resolvedAppId = resolveAppId(undefined, options);
252
+ const client = new ApiClient();
253
+ let scenario;
254
+ try {
255
+ scenario = JSON.parse(options.scenario);
256
+ }
257
+ catch {
258
+ error("Invalid JSON in --scenario. Provide a valid JSON object.");
259
+ process.exit(1);
260
+ }
261
+ try {
262
+ const result = await client.testRuleSet(resolvedAppId, ruleSetId, scenario);
263
+ if (options.json) {
264
+ json(result);
265
+ return;
266
+ }
267
+ if (result.error) {
268
+ info(`Result: DENIED`);
269
+ keyValue("Error", result.error);
270
+ }
271
+ else {
272
+ info(`Result: ${result.allowed ? "ALLOWED" : "DENIED"}`);
273
+ if (result.expression)
274
+ keyValue("Expression", result.expression);
275
+ }
276
+ if (result.context) {
277
+ console.log("\nContext:");
278
+ console.log(JSON.stringify(result.context, null, 2));
279
+ }
280
+ if (result.trace && result.trace.length > 0) {
281
+ console.log("\nTrace:");
282
+ console.log(formatTable(result.trace, [
283
+ { header: "FUNCTION", key: "function" },
284
+ { header: "ARGS", key: "args", format: (v) => JSON.stringify(v) },
285
+ { header: "RESULT", key: "result", format: (v) => JSON.stringify(v) },
286
+ ]));
287
+ }
288
+ }
289
+ catch (err) {
290
+ error(err.message);
291
+ process.exit(1);
292
+ }
293
+ });
294
+ // Debug (live evaluation with real data)
295
+ ruleSets
296
+ .command("debug")
297
+ .description("Debug a rule evaluation using real user and group data (console-admin only)")
298
+ .requiredOption("--user <userId>", "Target user ID")
299
+ .requiredOption("--group-type <type>", "Group type")
300
+ .requiredOption("--category <category>", "Rule category (group or member)")
301
+ .requiredOption("--operation <operation>", "Rule operation (create, edit, delete, list)")
302
+ .option("--group-id <groupId>", "Group ID (for operations on existing groups)")
303
+ .option("--target-user <userId>", "Target user ID (for member operations)")
304
+ .option("--app <app-id>", "App ID")
305
+ .option("--json", "Output as JSON")
306
+ .action(async (options) => {
307
+ const resolvedAppId = resolveAppId(undefined, options);
308
+ const client = new ApiClient();
309
+ const data = {
310
+ userId: options.user,
311
+ groupType: options.groupType,
312
+ category: options.category,
313
+ operation: options.operation,
314
+ };
315
+ if (options.groupId)
316
+ data.groupId = options.groupId;
317
+ if (options.targetUser)
318
+ data.targetUserId = options.targetUser;
319
+ try {
320
+ const result = await client.debugRuleSet(resolvedAppId, data);
321
+ if (options.json) {
322
+ json(result);
323
+ return;
324
+ }
325
+ info(`Result: ${result.allowed ? "ALLOWED" : "DENIED"}`);
326
+ if (result.reason)
327
+ keyValue("Reason", result.reason);
328
+ if (result.expression)
329
+ keyValue("Expression", result.expression);
330
+ if (result.ruleSetId)
331
+ keyValue("Rule Set", `${result.ruleSetName} (${result.ruleSetId})`);
332
+ if (result.user) {
333
+ console.log("\nUser:");
334
+ keyValue(" User ID", result.user.userId);
335
+ keyValue(" App Role", result.user.appRole);
336
+ }
337
+ if (result.memberships && result.memberships.length > 0) {
338
+ console.log("\nMemberships:");
339
+ console.log(formatTable(result.memberships, [
340
+ { header: "GROUP TYPE", key: "groupType" },
341
+ { header: "GROUP ID", key: "groupId", format: formatId },
342
+ ]));
343
+ }
344
+ else if (result.memberships) {
345
+ console.log("\nMemberships: (none)");
346
+ }
347
+ if (result.context) {
348
+ console.log("\nContext:");
349
+ console.log(JSON.stringify(result.context, null, 2));
350
+ }
351
+ if (result.trace && result.trace.length > 0) {
352
+ console.log("\nTrace:");
353
+ console.log(formatTable(result.trace, [
354
+ { header: "FUNCTION", key: "function" },
355
+ { header: "ARGS", key: "args", format: (v) => JSON.stringify(v) },
356
+ { header: "RESULT", key: "result", format: (v) => JSON.stringify(v) },
357
+ ]));
358
+ }
359
+ }
360
+ catch (err) {
361
+ error(err.message);
362
+ process.exit(1);
363
+ }
364
+ });
365
+ }
366
+ //# sourceMappingURL=rule-sets.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"rule-sets.js","sourceRoot":"","sources":["../../../src/commands/rule-sets.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,SAAS,EAAE,MAAM,sBAAsB,CAAC;AACjD,OAAO,EAAE,eAAe,EAAE,MAAM,kBAAkB,CAAC;AACnD,OAAO,EACL,OAAO,EACP,KAAK,EACL,IAAI,EACJ,QAAQ,EACR,WAAW,EACX,QAAQ,EACR,UAAU,EACV,IAAI,GACL,MAAM,kBAAkB,CAAC;AAE1B,SAAS,YAAY,CAAC,KAAyB,EAAE,OAAY;IAC3D,MAAM,QAAQ,GAAG,KAAK,IAAI,OAAO,CAAC,GAAG,IAAI,eAAe,EAAE,CAAC;IAC3D,IAAI,CAAC,QAAQ,EAAE,CAAC;QACd,KAAK,CAAC,oFAAoF,CAAC,CAAC;QAC5F,OAAO,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC;IAClB,CAAC;IACD,OAAO,QAAQ,CAAC;AAClB,CAAC;AAED,MAAM,UAAU,wBAAwB,CAAC,OAAgB;IACvD,MAAM,QAAQ,GAAG,OAAO;SACrB,OAAO,CAAC,WAAW,CAAC;SACpB,WAAW,CAAC,kDAAkD,CAAC;SAC/D,WAAW,CAAC,OAAO,EAAE;;;;;;;CAOzB,CAAC,CAAC;IAED,iBAAiB;IACjB,QAAQ;SACL,OAAO,CAAC,MAAM,CAAC;SACf,WAAW,CAAC,iCAAiC,CAAC;SAC9C,QAAQ,CAAC,UAAU,EAAE,4CAA4C,CAAC;SAClE,MAAM,CAAC,gBAAgB,EAAE,QAAQ,CAAC;SAClC,MAAM,CAAC,wBAAwB,EAAE,6DAA6D,CAAC;SAC/F,MAAM,CAAC,QAAQ,EAAE,gBAAgB,CAAC;SAClC,MAAM,CAAC,KAAK,EAAE,KAAK,EAAE,OAAO,EAAE,EAAE;QAC/B,MAAM,aAAa,GAAG,YAAY,CAAC,KAAK,EAAE,OAAO,CAAC,CAAC;QACnD,MAAM,MAAM,GAAG,IAAI,SAAS,EAAE,CAAC;QAE/B,IAAI,CAAC;YACH,MAAM,MAAM,GAAQ,EAAE,CAAC;YACvB,IAAI,OAAO,CAAC,YAAY;gBAAE,MAAM,CAAC,YAAY,GAAG,OAAO,CAAC,YAAY,CAAC;YAErE,MAAM,MAAM,GAAG,MAAM,MAAM,CAAC,YAAY,CAAC,aAAa,EAAE,MAAM,CAAC,CAAC;YAChE,MAAM,IAAI,GAAG,KAAK,CAAC,OAAO,CAAC,MAAM,CAAC,CAAC,CAAC,CAAC,MAAM,CAAC,CAAC,CAAE,MAAc,EAAE,QAAQ,IAAI,EAAE,CAAC;YAE9E,IAAI,OAAO,CAAC,IAAI,EAAE,CAAC;gBACjB,IAAI,CAAC,IAAI,CAAC,CAAC;gBACX,OAAO;YACT,CAAC;YAED,IAAI,IAAI,CAAC,MAAM,KAAK,CAAC,EAAE,CAAC;gBACtB,IAAI,CAAC,qBAAqB,CAAC,CAAC;gBAC5B,OAAO;YACT,CAAC;YAED,OAAO,CAAC,GAAG,CACT,WAAW,CAAC,IAAI,EAAE;gBAChB,EAAE,MAAM,EAAE,IAAI,EAAE,GAAG,EAAE,WAAW,EAAE,MAAM,EAAE,QAAQ,EAAE;gBACpD,EAAE,MAAM,EAAE,MAAM,EAAE,GAAG,EAAE,MAAM,EAAE;gBAC/B,EAAE,MAAM,EAAE,MAAM,EAAE,GAAG,EAAE,cAAc,EAAE;gBACvC,EAAE,MAAM,EAAE,SAAS,EAAE,GAAG,EAAE,SAAS,EAAE;gBACrC,EAAE,MAAM,EAAE,UAAU,EAAE,GAAG,EAAE,YAAY,EAAE,MAAM,EAAE,UAAU,EAAE;aAC9D,CAAC,CACH,CAAC;QACJ,CAAC;QAAC,OAAO,GAAQ,EAAE,CAAC;YAClB,KAAK,CAAC,GAAG,CAAC,OAAO,CAAC,CAAC;YACnB,OAAO,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC;QAClB,CAAC;IACH,CAAC,CAAC,CAAC;IAEL,kBAAkB;IAClB,QAAQ;SACL,OAAO,CAAC,QAAQ,CAAC;SACjB,WAAW,CAAC,8BAA8B,CAAC;SAC3C,QAAQ,CAAC,QAAQ,EAAE,eAAe,CAAC;SACnC,cAAc,CAAC,wBAAwB,EAAE,qDAAqD,CAAC;SAC/F,cAAc,CAAC,gBAAgB,EAAE,sBAAsB,CAAC;SACxD,MAAM,CAAC,sBAAsB,EAAE,sBAAsB,CAAC;SACtD,MAAM,CAAC,gBAAgB,EAAE,QAAQ,CAAC;SAClC,MAAM,CAAC,QAAQ,EAAE,gBAAgB,CAAC;SAClC,MAAM,CAAC,KAAK,EAAE,IAAI,EAAE,OAAO,EAAE,EAAE;QAC9B,MAAM,aAAa,GAAG,YAAY,CAAC,SAAS,EAAE,OAAO,CAAC,CAAC;QACvD,MAAM,MAAM,GAAG,IAAI,SAAS,EAAE,CAAC;QAE/B,IAAI,KAA0B,CAAC;QAC/B,IAAI,CAAC;YACH,KAAK,GAAG,IAAI,CAAC,KAAK,CAAC,OAAO,CAAC,KAAK,CAAC,CAAC;QACpC,CAAC;QAAC,MAAM,CAAC;YACP,KAAK,CAAC,uDAAuD,CAAC,CAAC;YAC/D,OAAO,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC;QAClB,CAAC;QAED,IAAI,CAAC;YACH,MAAM,IAAI,GAAQ;gBAChB,IAAI;gBACJ,YAAY,EAAE,OAAO,CAAC,YAAY;gBAClC,KAAK;aACN,CAAC;YACF,IAAI,OAAO,CAAC,WAAW;gBAAE,IAAI,CAAC,WAAW,GAAG,OAAO,CAAC,WAAW,CAAC;YAEhE,MAAM,MAAM,GAAG,MAAM,MAAM,CAAC,aAAa,CAAC,aAAa,EAAE,IAAI,CAAC,CAAC;YAE/D,IAAI,OAAO,CAAC,IAAI,EAAE,CAAC;gBACjB,IAAI,CAAC,MAAM,CAAC,CAAC;gBACb,OAAO;YACT,CAAC;YAED,OAAO,CAAC,mBAAmB,CAAC,CAAC;YAC7B,QAAQ,CAAC,aAAa,EAAE,MAAM,CAAC,SAAS,CAAC,CAAC;YAC1C,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAC,IAAI,CAAC,CAAC;YAC9B,QAAQ,CAAC,eAAe,EAAE,MAAM,CAAC,YAAY,CAAC,CAAC;YAC/C,QAAQ,CAAC,SAAS,EAAE,MAAM,CAAC,OAAO,CAAC,CAAC;QACtC,CAAC;QAAC,OAAO,GAAQ,EAAE,CAAC;YAClB,KAAK,CAAC,GAAG,CAAC,OAAO,CAAC,CAAC;YACnB,OAAO,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC;QAClB,CAAC;IACH,CAAC,CAAC,CAAC;IAEL,eAAe;IACf,QAAQ;SACL,OAAO,CAAC,KAAK,CAAC;SACd,WAAW,CAAC,sBAAsB,CAAC;SACnC,QAAQ,CAAC,eAAe,EAAE,aAAa,CAAC;SACxC,MAAM,CAAC,gBAAgB,EAAE,QAAQ,CAAC;SAClC,MAAM,CAAC,QAAQ,EAAE,gBAAgB,CAAC;SAClC,MAAM,CAAC,KAAK,EAAE,SAAS,EAAE,OAAO,EAAE,EAAE;QACnC,MAAM,aAAa,GAAG,YAAY,CAAC,SAAS,EAAE,OAAO,CAAC,CAAC;QACvD,MAAM,MAAM,GAAG,IAAI,SAAS,EAAE,CAAC;QAE/B,IAAI,CAAC;YACH,MAAM,MAAM,GAAG,MAAM,MAAM,CAAC,UAAU,CAAC,aAAa,EAAE,SAAS,CAAC,CAAC;YAEjE,IAAI,OAAO,CAAC,IAAI,EAAE,CAAC;gBACjB,IAAI,CAAC,MAAM,CAAC,CAAC;gBACb,OAAO;YACT,CAAC;YAED,QAAQ,CAAC,aAAa,EAAE,MAAM,CAAC,SAAS,CAAC,CAAC;YAC1C,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAC,IAAI,CAAC,CAAC;YAC9B,IAAI,MAAM,CAAC,WAAW;gBAAE,QAAQ,CAAC,aAAa,EAAE,MAAM,CAAC,WAAW,CAAC,CAAC;YACpE,QAAQ,CAAC,eAAe,EAAE,MAAM,CAAC,YAAY,CAAC,CAAC;YAC/C,QAAQ,CAAC,SAAS,EAAE,MAAM,CAAC,MAAM,CAAC,OAAO,CAAC,CAAC,CAAC;YAC5C,QAAQ,CAAC,SAAS,EAAE,UAAU,CAAC,MAAM,CAAC,SAAS,CAAC,CAAC,CAAC;YAClD,QAAQ,CAAC,UAAU,EAAE,UAAU,CAAC,MAAM,CAAC,UAAU,CAAC,CAAC,CAAC;YACpD,OAAO,CAAC,GAAG,CAAC,UAAU,CAAC,CAAC;YACxB,OAAO,CAAC,GAAG,CAAC,IAAI,CAAC,SAAS,CAAC,MAAM,CAAC,KAAK,EAAE,IAAI,EAAE,CAAC,CAAC,CAAC,CAAC;QACrD,CAAC;QAAC,OAAO,GAAQ,EAAE,CAAC;YAClB,KAAK,CAAC,GAAG,CAAC,OAAO,CAAC,CAAC;YACnB,OAAO,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC;QAClB,CAAC;IACH,CAAC,CAAC,CAAC;IAEL,kBAAkB;IAClB,QAAQ;SACL,OAAO,CAAC,QAAQ,CAAC;SACjB,WAAW,CAAC,mBAAmB,CAAC;SAChC,QAAQ,CAAC,eAAe,EAAE,aAAa,CAAC;SACxC,MAAM,CAAC,eAAe,EAAE,UAAU,CAAC;SACnC,MAAM,CAAC,sBAAsB,EAAE,iBAAiB,CAAC;SACjD,MAAM,CAAC,gBAAgB,EAAE,0BAA0B,CAAC;SACpD,MAAM,CAAC,gBAAgB,EAAE,QAAQ,CAAC;SAClC,MAAM,CAAC,QAAQ,EAAE,gBAAgB,CAAC;SAClC,MAAM,CAAC,KAAK,EAAE,SAAS,EAAE,OAAO,EAAE,EAAE;QACnC,MAAM,aAAa,GAAG,YAAY,CAAC,SAAS,EAAE,OAAO,CAAC,CAAC;QACvD,MAAM,MAAM,GAAG,IAAI,SAAS,EAAE,CAAC;QAE/B,MAAM,IAAI,GAAQ,EAAE,CAAC;QACrB,IAAI,OAAO,CAAC,IAAI;YAAE,IAAI,CAAC,IAAI,GAAG,OAAO,CAAC,IAAI,CAAC;QAC3C,IAAI,OAAO,CAAC,WAAW,KAAK,SAAS;YAAE,IAAI,CAAC,WAAW,GAAG,OAAO,CAAC,WAAW,CAAC;QAC9E,IAAI,OAAO,CAAC,KAAK,EAAE,CAAC;YAClB,IAAI,CAAC;gBACH,IAAI,CAAC,KAAK,GAAG,IAAI,CAAC,KAAK,CAAC,OAAO,CAAC,KAAK,CAAC,CAAC;YACzC,CAAC;YAAC,MAAM,CAAC;gBACP,KAAK,CAAC,uDAAuD,CAAC,CAAC;gBAC/D,OAAO,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC;YAClB,CAAC;QACH,CAAC;QAED,IAAI,MAAM,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC,MAAM,KAAK,CAAC,EAAE,CAAC;YACnC,KAAK,CAAC,+DAA+D,CAAC,CAAC;YACvE,OAAO,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC;QAClB,CAAC;QAED,IAAI,CAAC;YACH,MAAM,MAAM,GAAG,MAAM,MAAM,CAAC,aAAa,CAAC,aAAa,EAAE,SAAS,EAAE,IAAI,CAAC,CAAC;YAE1E,IAAI,OAAO,CAAC,IAAI,EAAE,CAAC;gBACjB,IAAI,CAAC,MAAM,CAAC,CAAC;gBACb,OAAO;YACT,CAAC;YAED,OAAO,CAAC,mBAAmB,CAAC,CAAC;YAC7B,QAAQ,CAAC,aAAa,EAAE,MAAM,CAAC,SAAS,CAAC,CAAC;YAC1C,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAC,IAAI,CAAC,CAAC;YAC9B,QAAQ,CAAC,SAAS,EAAE,MAAM,CAAC,MAAM,CAAC,OAAO,CAAC,CAAC,CAAC;QAC9C,CAAC;QAAC,OAAO,GAAQ,EAAE,CAAC;YAClB,KAAK,CAAC,GAAG,CAAC,OAAO,CAAC,CAAC;YACnB,OAAO,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC;QAClB,CAAC;IACH,CAAC,CAAC,CAAC;IAEL,kBAAkB;IAClB,QAAQ;SACL,OAAO,CAAC,QAAQ,CAAC;SACjB,WAAW,CAAC,mBAAmB,CAAC;SAChC,QAAQ,CAAC,eAAe,EAAE,aAAa,CAAC;SACxC,MAAM,CAAC,gBAAgB,EAAE,QAAQ,CAAC;SAClC,MAAM,CAAC,WAAW,EAAE,0BAA0B,CAAC;SAC/C,MAAM,CAAC,KAAK,EAAE,SAAS,EAAE,OAAO,EAAE,EAAE;QACnC,MAAM,aAAa,GAAG,YAAY,CAAC,SAAS,EAAE,OAAO,CAAC,CAAC;QAEvD,IAAI,CAAC,OAAO,CAAC,GAAG,EAAE,CAAC;YACjB,MAAM,QAAQ,GAAG,MAAM,MAAM,CAAC,UAAU,CAAC,CAAC;YAC1C,MAAM,EAAE,OAAO,EAAE,GAAG,MAAM,QAAQ,CAAC,OAAO,CAAC,MAAM,CAAC;gBAChD;oBACE,IAAI,EAAE,SAAS;oBACf,IAAI,EAAE,SAAS;oBACf,OAAO,EAAE,mBAAmB,SAAS,0BAA0B;oBAC/D,OAAO,EAAE,KAAK;iBACf;aACF,CAAC,CAAC;YACH,IAAI,CAAC,OAAO,EAAE,CAAC;gBACb,IAAI,CAAC,YAAY,CAAC,CAAC;gBACnB,OAAO;YACT,CAAC;QACH,CAAC;QAED,MAAM,MAAM,GAAG,IAAI,SAAS,EAAE,CAAC;QAE/B,IAAI,CAAC;YACH,MAAM,MAAM,CAAC,aAAa,CAAC,aAAa,EAAE,SAAS,CAAC,CAAC;YACrD,OAAO,CAAC,YAAY,SAAS,WAAW,CAAC,CAAC;QAC5C,CAAC;QAAC,OAAO,GAAQ,EAAE,CAAC;YAClB,KAAK,CAAC,GAAG,CAAC,OAAO,CAAC,CAAC;YACnB,OAAO,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC;QAClB,CAAC;IACH,CAAC,CAAC,CAAC;IAEL,SAAS;IACT,QAAQ;SACL,OAAO,CAAC,QAAQ,CAAC;SACjB,WAAW,CAAC,yEAAyE,CAAC;SACtF,MAAM,CAAC,gBAAgB,EAAE,QAAQ,CAAC;SAClC,MAAM,CAAC,QAAQ,EAAE,gBAAgB,CAAC;SAClC,MAAM,CAAC,KAAK,EAAE,OAAO,EAAE,EAAE;QACxB,MAAM,aAAa,GAAG,YAAY,CAAC,SAAS,EAAE,OAAO,CAAC,CAAC;QACvD,MAAM,MAAM,GAAG,IAAI,SAAS,EAAE,CAAC;QAE/B,IAAI,CAAC;YACH,MAAM,MAAM,GAAG,MAAM,MAAM,CAAC,gBAAgB,CAAC,aAAa,CAAC,CAAC;YAE5D,IAAI,OAAO,CAAC,IAAI,EAAE,CAAC;gBACjB,IAAI,CAAC,MAAM,CAAC,CAAC;gBACb,OAAO;YACT,CAAC;YAED,OAAO,CAAC,GAAG,CAAC,IAAI,CAAC,SAAS,CAAC,MAAM,EAAE,IAAI,EAAE,CAAC,CAAC,CAAC,CAAC;QAC/C,CAAC;QAAC,OAAO,GAAQ,EAAE,CAAC;YAClB,KAAK,CAAC,GAAG,CAAC,OAAO,CAAC,CAAC;YACnB,OAAO,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC;QAClB,CAAC;IACH,CAAC,CAAC,CAAC;IAEL,iBAAiB;IACjB,QAAQ;SACL,OAAO,CAAC,MAAM,CAAC;SACf,WAAW,CAAC,wDAAwD,CAAC;SACrE,QAAQ,CAAC,eAAe,EAAE,aAAa,CAAC;SACxC,cAAc,CAAC,mBAAmB,EAAE,yBAAyB,CAAC;SAC9D,MAAM,CAAC,gBAAgB,EAAE,QAAQ,CAAC;SAClC,MAAM,CAAC,QAAQ,EAAE,gBAAgB,CAAC;SAClC,MAAM,CAAC,KAAK,EAAE,SAAS,EAAE,OAAO,EAAE,EAAE;QACnC,MAAM,aAAa,GAAG,YAAY,CAAC,SAAS,EAAE,OAAO,CAAC,CAAC;QACvD,MAAM,MAAM,GAAG,IAAI,SAAS,EAAE,CAAC;QAE/B,IAAI,QAAa,CAAC;QAClB,IAAI,CAAC;YACH,QAAQ,GAAG,IAAI,CAAC,KAAK,CAAC,OAAO,CAAC,QAAQ,CAAC,CAAC;QAC1C,CAAC;QAAC,MAAM,CAAC;YACP,KAAK,CAAC,0DAA0D,CAAC,CAAC;YAClE,OAAO,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC;QAClB,CAAC;QAED,IAAI,CAAC;YACH,MAAM,MAAM,GAAG,MAAM,MAAM,CAAC,WAAW,CAAC,aAAa,EAAE,SAAS,EAAE,QAAQ,CAAC,CAAC;YAE5E,IAAI,OAAO,CAAC,IAAI,EAAE,CAAC;gBACjB,IAAI,CAAC,MAAM,CAAC,CAAC;gBACb,OAAO;YACT,CAAC;YAED,IAAI,MAAM,CAAC,KAAK,EAAE,CAAC;gBACjB,IAAI,CAAC,gBAAgB,CAAC,CAAC;gBACvB,QAAQ,CAAC,OAAO,EAAE,MAAM,CAAC,KAAK,CAAC,CAAC;YAClC,CAAC;iBAAM,CAAC;gBACN,IAAI,CAAC,WAAW,MAAM,CAAC,OAAO,CAAC,CAAC,CAAC,SAAS,CAAC,CAAC,CAAC,QAAQ,EAAE,CAAC,CAAC;gBACzD,IAAI,MAAM,CAAC,UAAU;oBAAE,QAAQ,CAAC,YAAY,EAAE,MAAM,CAAC,UAAU,CAAC,CAAC;YACnE,CAAC;YAED,IAAI,MAAM,CAAC,OAAO,EAAE,CAAC;gBACnB,OAAO,CAAC,GAAG,CAAC,YAAY,CAAC,CAAC;gBAC1B,OAAO,CAAC,GAAG,CAAC,IAAI,CAAC,SAAS,CAAC,MAAM,CAAC,OAAO,EAAE,IAAI,EAAE,CAAC,CAAC,CAAC,CAAC;YACvD,CAAC;YACD,IAAI,MAAM,CAAC,KAAK,IAAI,MAAM,CAAC,KAAK,CAAC,MAAM,GAAG,CAAC,EAAE,CAAC;gBAC5C,OAAO,CAAC,GAAG,CAAC,UAAU,CAAC,CAAC;gBACxB,OAAO,CAAC,GAAG,CACT,WAAW,CAAC,MAAM,CAAC,KAAK,EAAE;oBACxB,EAAE,MAAM,EAAE,UAAU,EAAE,GAAG,EAAE,UAAU,EAAE;oBACvC,EAAE,MAAM,EAAE,MAAM,EAAE,GAAG,EAAE,MAAM,EAAE,MAAM,EAAE,CAAC,CAAM,EAAE,EAAE,CAAC,IAAI,CAAC,SAAS,CAAC,CAAC,CAAC,EAAE;oBACtE,EAAE,MAAM,EAAE,QAAQ,EAAE,GAAG,EAAE,QAAQ,EAAE,MAAM,EAAE,CAAC,CAAM,EAAE,EAAE,CAAC,IAAI,CAAC,SAAS,CAAC,CAAC,CAAC,EAAE;iBAC3E,CAAC,CACH,CAAC;YACJ,CAAC;QACH,CAAC;QAAC,OAAO,GAAQ,EAAE,CAAC;YAClB,KAAK,CAAC,GAAG,CAAC,OAAO,CAAC,CAAC;YACnB,OAAO,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC;QAClB,CAAC;IACH,CAAC,CAAC,CAAC;IAEL,yCAAyC;IACzC,QAAQ;SACL,OAAO,CAAC,OAAO,CAAC;SAChB,WAAW,CAAC,6EAA6E,CAAC;SAC1F,cAAc,CAAC,iBAAiB,EAAE,gBAAgB,CAAC;SACnD,cAAc,CAAC,qBAAqB,EAAE,YAAY,CAAC;SACnD,cAAc,CAAC,uBAAuB,EAAE,iCAAiC,CAAC;SAC1E,cAAc,CAAC,yBAAyB,EAAE,6CAA6C,CAAC;SACxF,MAAM,CAAC,sBAAsB,EAAE,8CAA8C,CAAC;SAC9E,MAAM,CAAC,wBAAwB,EAAE,wCAAwC,CAAC;SAC1E,MAAM,CAAC,gBAAgB,EAAE,QAAQ,CAAC;SAClC,MAAM,CAAC,QAAQ,EAAE,gBAAgB,CAAC;SAClC,MAAM,CAAC,KAAK,EAAE,OAAO,EAAE,EAAE;QACxB,MAAM,aAAa,GAAG,YAAY,CAAC,SAAS,EAAE,OAAO,CAAC,CAAC;QACvD,MAAM,MAAM,GAAG,IAAI,SAAS,EAAE,CAAC;QAE/B,MAAM,IAAI,GAAQ;YAChB,MAAM,EAAE,OAAO,CAAC,IAAI;YACpB,SAAS,EAAE,OAAO,CAAC,SAAS;YAC5B,QAAQ,EAAE,OAAO,CAAC,QAAQ;YAC1B,SAAS,EAAE,OAAO,CAAC,SAAS;SAC7B,CAAC;QACF,IAAI,OAAO,CAAC,OAAO;YAAE,IAAI,CAAC,OAAO,GAAG,OAAO,CAAC,OAAO,CAAC;QACpD,IAAI,OAAO,CAAC,UAAU;YAAE,IAAI,CAAC,YAAY,GAAG,OAAO,CAAC,UAAU,CAAC;QAE/D,IAAI,CAAC;YACH,MAAM,MAAM,GAAG,MAAM,MAAM,CAAC,YAAY,CAAC,aAAa,EAAE,IAAI,CAAC,CAAC;YAE9D,IAAI,OAAO,CAAC,IAAI,EAAE,CAAC;gBACjB,IAAI,CAAC,MAAM,CAAC,CAAC;gBACb,OAAO;YACT,CAAC;YAED,IAAI,CAAC,WAAW,MAAM,CAAC,OAAO,CAAC,CAAC,CAAC,SAAS,CAAC,CAAC,CAAC,QAAQ,EAAE,CAAC,CAAC;YACzD,IAAI,MAAM,CAAC,MAAM;gBAAE,QAAQ,CAAC,QAAQ,EAAE,MAAM,CAAC,MAAM,CAAC,CAAC;YACrD,IAAI,MAAM,CAAC,UAAU;gBAAE,QAAQ,CAAC,YAAY,EAAE,MAAM,CAAC,UAAU,CAAC,CAAC;YACjE,IAAI,MAAM,CAAC,SAAS;gBAAE,QAAQ,CAAC,UAAU,EAAE,GAAG,MAAM,CAAC,WAAW,KAAK,MAAM,CAAC,SAAS,GAAG,CAAC,CAAC;YAE1F,IAAI,MAAM,CAAC,IAAI,EAAE,CAAC;gBAChB,OAAO,CAAC,GAAG,CAAC,SAAS,CAAC,CAAC;gBACvB,QAAQ,CAAC,WAAW,EAAE,MAAM,CAAC,IAAI,CAAC,MAAM,CAAC,CAAC;gBAC1C,QAAQ,CAAC,YAAY,EAAE,MAAM,CAAC,IAAI,CAAC,OAAO,CAAC,CAAC;YAC9C,CAAC;YAED,IAAI,MAAM,CAAC,WAAW,IAAI,MAAM,CAAC,WAAW,CAAC,MAAM,GAAG,CAAC,EAAE,CAAC;gBACxD,OAAO,CAAC,GAAG,CAAC,gBAAgB,CAAC,CAAC;gBAC9B,OAAO,CAAC,GAAG,CACT,WAAW,CAAC,MAAM,CAAC,WAAW,EAAE;oBAC9B,EAAE,MAAM,EAAE,YAAY,EAAE,GAAG,EAAE,WAAW,EAAE;oBAC1C,EAAE,MAAM,EAAE,UAAU,EAAE,GAAG,EAAE,SAAS,EAAE,MAAM,EAAE,QAAQ,EAAE;iBACzD,CAAC,CACH,CAAC;YACJ,CAAC;iBAAM,IAAI,MAAM,CAAC,WAAW,EAAE,CAAC;gBAC9B,OAAO,CAAC,GAAG,CAAC,uBAAuB,CAAC,CAAC;YACvC,CAAC;YAED,IAAI,MAAM,CAAC,OAAO,EAAE,CAAC;gBACnB,OAAO,CAAC,GAAG,CAAC,YAAY,CAAC,CAAC;gBAC1B,OAAO,CAAC,GAAG,CAAC,IAAI,CAAC,SAAS,CAAC,MAAM,CAAC,OAAO,EAAE,IAAI,EAAE,CAAC,CAAC,CAAC,CAAC;YACvD,CAAC;YAED,IAAI,MAAM,CAAC,KAAK,IAAI,MAAM,CAAC,KAAK,CAAC,MAAM,GAAG,CAAC,EAAE,CAAC;gBAC5C,OAAO,CAAC,GAAG,CAAC,UAAU,CAAC,CAAC;gBACxB,OAAO,CAAC,GAAG,CACT,WAAW,CAAC,MAAM,CAAC,KAAK,EAAE;oBACxB,EAAE,MAAM,EAAE,UAAU,EAAE,GAAG,EAAE,UAAU,EAAE;oBACvC,EAAE,MAAM,EAAE,MAAM,EAAE,GAAG,EAAE,MAAM,EAAE,MAAM,EAAE,CAAC,CAAM,EAAE,EAAE,CAAC,IAAI,CAAC,SAAS,CAAC,CAAC,CAAC,EAAE;oBACtE,EAAE,MAAM,EAAE,QAAQ,EAAE,GAAG,EAAE,QAAQ,EAAE,MAAM,EAAE,CAAC,CAAM,EAAE,EAAE,CAAC,IAAI,CAAC,SAAS,CAAC,CAAC,CAAC,EAAE;iBAC3E,CAAC,CACH,CAAC;YACJ,CAAC;QACH,CAAC;QAAC,OAAO,GAAQ,EAAE,CAAC;YAClB,KAAK,CAAC,GAAG,CAAC,OAAO,CAAC,CAAC;YACnB,OAAO,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC;QAClB,CAAC;IACH,CAAC,CAAC,CAAC;AAEP,CAAC"}
@@ -127,6 +127,24 @@ function serializeWorkflow(workflow, draft, configs) {
127
127
  };
128
128
  return TOML.stringify(data);
129
129
  }
130
+ function serializeEmailTemplate(template) {
131
+ // The API returns { emailType, hasOverride, override: { subject, htmlBody, textBody }, default: { ... } }
132
+ // We only serialize overrides (custom templates), using the override fields.
133
+ const override = template.override || {};
134
+ const data = {
135
+ template: {
136
+ emailType: template.emailType,
137
+ subject: override.subject || "",
138
+ },
139
+ };
140
+ if (override.htmlBody) {
141
+ data.template.htmlBody = override.htmlBody;
142
+ }
143
+ if (override.textBody) {
144
+ data.template.textBody = override.textBody;
145
+ }
146
+ return TOML.stringify(data);
147
+ }
130
148
  // Parsing helpers
131
149
  function parseTomlFile(filePath) {
132
150
  const content = readFileSync(filePath, "utf-8");
@@ -466,6 +484,7 @@ Directory Structure:
466
484
  prompts/{key}.tests/*.toml # Prompt test cases
467
485
  workflows/*.toml # Workflow definitions
468
486
  workflows/{key}.tests/*.toml # Workflow test cases
487
+ email-templates/*.toml # Email template overrides
469
488
  `);
470
489
  // Init
471
490
  sync
@@ -481,6 +500,7 @@ Directory Structure:
481
500
  ensureDir(join(configDir, "integrations"));
482
501
  ensureDir(join(configDir, "prompts"));
483
502
  ensureDir(join(configDir, "workflows"));
503
+ ensureDir(join(configDir, "email-templates"));
484
504
  const state = {
485
505
  appId: resolvedAppId,
486
506
  serverUrl: getServerUrl(),
@@ -506,11 +526,12 @@ Directory Structure:
506
526
  info(`Pulling configuration for app ${resolvedAppId}...`);
507
527
  try {
508
528
  // Fetch all data
509
- const [settings, integrationsResult, promptsResult, workflowsResult] = await Promise.all([
529
+ const [settings, integrationsResult, promptsResult, workflowsResult, emailTemplatesResult] = await Promise.all([
510
530
  client.getAppSettings(resolvedAppId).catch(() => null),
511
531
  client.listIntegrations(resolvedAppId, { limit: 100 }),
512
532
  client.listPrompts(resolvedAppId, { limit: 100 }),
513
533
  client.listWorkflows(resolvedAppId, { limit: 100 }),
534
+ client.listEmailTemplates(resolvedAppId).catch(() => ({ templates: [] })),
514
535
  ]);
515
536
  // Fetch details for each entity
516
537
  const integrations = await Promise.all(integrationsResult.items.map((i) => client.getIntegration(resolvedAppId, i.integrationId)));
@@ -536,8 +557,11 @@ Directory Structure:
536
557
  }
537
558
  return workflowData;
538
559
  }));
560
+ // Fetch full details for email template overrides
561
+ const emailTemplateOverrides = emailTemplatesResult.templates.filter((t) => t.hasOverride);
562
+ const emailTemplates = await Promise.all(emailTemplateOverrides.map((t) => client.getEmailTemplate(resolvedAppId, t.emailType).catch(() => null))).then((results) => results.filter(Boolean));
539
563
  if (options.json) {
540
- json({ settings, integrations, prompts, workflows });
564
+ json({ settings, integrations, prompts, workflows, emailTemplates });
541
565
  return;
542
566
  }
543
567
  // Ensure directories exist
@@ -545,6 +569,7 @@ Directory Structure:
545
569
  ensureDir(join(configDir, "integrations"));
546
570
  ensureDir(join(configDir, "prompts"));
547
571
  ensureDir(join(configDir, "workflows"));
572
+ ensureDir(join(configDir, "email-templates"));
548
573
  // Write app settings
549
574
  if (settings) {
550
575
  writeFileSync(join(configDir, "app.toml"), serializeAppSettings(settings));
@@ -584,6 +609,30 @@ Directory Structure:
584
609
  };
585
610
  info(` Wrote workflows/${filename}`);
586
611
  }
612
+ // Write email templates
613
+ const emailTemplateEntities = {};
614
+ for (const template of emailTemplates) {
615
+ const filename = `${template.emailType}.toml`;
616
+ writeFileSync(join(configDir, "email-templates", filename), serializeEmailTemplate(template));
617
+ emailTemplateEntities[template.emailType] = {
618
+ id: template.emailType,
619
+ modifiedAt: template.override?.modifiedAt || new Date().toISOString(),
620
+ };
621
+ info(` Wrote email-templates/${filename}`);
622
+ }
623
+ // Clean up local email template files for remotely-deleted overrides
624
+ const emailTemplatesDir = join(configDir, "email-templates");
625
+ if (existsSync(emailTemplatesDir)) {
626
+ const pulledTypes = new Set(emailTemplates.map((t) => t.emailType));
627
+ const localFiles = readdirSync(emailTemplatesDir).filter((f) => f.endsWith(".toml"));
628
+ for (const file of localFiles) {
629
+ const localType = basename(file, ".toml");
630
+ if (!pulledTypes.has(localType)) {
631
+ unlinkSync(join(emailTemplatesDir, file));
632
+ info(` Removed email-templates/${file} (override deleted on server)`);
633
+ }
634
+ }
635
+ }
587
636
  // Pull test cases for prompts and workflows
588
637
  const testCaseEntities = {};
589
638
  let totalTestCases = 0;
@@ -611,6 +660,7 @@ Directory Structure:
611
660
  integrations: integrationEntities,
612
661
  prompts: promptEntities,
613
662
  workflows: workflowEntities,
663
+ emailTemplates: Object.keys(emailTemplateEntities).length > 0 ? emailTemplateEntities : undefined,
614
664
  testCases: Object.keys(testCaseEntities).length > 0 ? testCaseEntities : undefined,
615
665
  },
616
666
  };
@@ -620,6 +670,7 @@ Directory Structure:
620
670
  keyValue("Integrations", integrations.length);
621
671
  keyValue("Prompts", prompts.length);
622
672
  keyValue("Workflows", workflows.length);
673
+ keyValue("Email Templates", emailTemplates.length);
623
674
  keyValue("Test Cases", totalTestCases);
624
675
  }
625
676
  catch (err) {
@@ -919,6 +970,42 @@ Directory Structure:
919
970
  }
920
971
  }
921
972
  }
973
+ // Process email templates
974
+ const emailTemplatesDir = join(configDir, "email-templates");
975
+ if (existsSync(emailTemplatesDir)) {
976
+ const files = readdirSync(emailTemplatesDir).filter((f) => f.endsWith(".toml"));
977
+ for (const file of files) {
978
+ const filePath = join(emailTemplatesDir, file);
979
+ const tomlData = parseTomlFile(filePath);
980
+ const template = tomlData.template || {};
981
+ const emailType = template.emailType || basename(file, ".toml");
982
+ const payload = {
983
+ subject: template.subject || "",
984
+ htmlBody: template.htmlBody || "",
985
+ textBody: template.textBody || "",
986
+ };
987
+ changes.push({ type: "email-template", action: "update", key: emailType });
988
+ if (!options.dryRun) {
989
+ try {
990
+ const result = await client.upsertEmailTemplate(resolvedAppId, emailType, payload);
991
+ info(` Updated email template: ${emailType}`);
992
+ // Update sync state
993
+ if (syncState) {
994
+ if (!syncState.entities.emailTemplates) {
995
+ syncState.entities.emailTemplates = {};
996
+ }
997
+ syncState.entities.emailTemplates[emailType] = {
998
+ id: emailType,
999
+ modifiedAt: result?.modifiedAt || new Date().toISOString(),
1000
+ };
1001
+ }
1002
+ }
1003
+ catch (err) {
1004
+ warn(` Failed to update email template ${emailType}: ${err.message}`);
1005
+ }
1006
+ }
1007
+ }
1008
+ }
922
1009
  // Push test cases for prompts and workflows
923
1010
  const promptsDir2 = join(configDir, "prompts");
924
1011
  if (existsSync(promptsDir2)) {
@@ -1016,18 +1103,23 @@ Directory Structure:
1016
1103
  info(`Comparing local configuration with app ${resolvedAppId}...`);
1017
1104
  try {
1018
1105
  // Fetch remote state
1019
- const [integrationsResult, promptsResult, workflowsResult] = await Promise.all([
1106
+ const [integrationsResult, promptsResult, workflowsResult, emailTemplatesResult] = await Promise.all([
1020
1107
  client.listIntegrations(resolvedAppId, { limit: 100 }),
1021
1108
  client.listPrompts(resolvedAppId, { limit: 100 }),
1022
1109
  client.listWorkflows(resolvedAppId, { limit: 100 }),
1110
+ client.listEmailTemplates(resolvedAppId).catch(() => ({ templates: [] })),
1023
1111
  ]);
1024
1112
  const remoteIntegrations = new Set(integrationsResult.items.map((i) => i.integrationKey));
1025
1113
  const remotePrompts = new Set(promptsResult.items.map((p) => p.promptKey));
1026
1114
  const remoteWorkflows = new Set(workflowsResult.items.map((w) => w.workflowKey));
1115
+ const remoteEmailTemplates = new Set((emailTemplatesResult.templates || [])
1116
+ .filter((t) => t.hasOverride)
1117
+ .map((t) => t.emailType));
1027
1118
  // Get local files
1028
1119
  const localIntegrations = new Set();
1029
1120
  const localPrompts = new Set();
1030
1121
  const localWorkflows = new Set();
1122
+ const localEmailTemplates = new Set();
1031
1123
  const integrationsDir = join(configDir, "integrations");
1032
1124
  if (existsSync(integrationsDir)) {
1033
1125
  for (const file of readdirSync(integrationsDir).filter((f) => f.endsWith(".toml"))) {
@@ -1052,6 +1144,14 @@ Directory Structure:
1052
1144
  localWorkflows.add(key);
1053
1145
  }
1054
1146
  }
1147
+ const emailTemplatesDirPath = join(configDir, "email-templates");
1148
+ if (existsSync(emailTemplatesDirPath)) {
1149
+ for (const file of readdirSync(emailTemplatesDirPath).filter((f) => f.endsWith(".toml"))) {
1150
+ const tomlData = parseTomlFile(join(emailTemplatesDirPath, file));
1151
+ const emailType = tomlData.template?.emailType || basename(file, ".toml");
1152
+ localEmailTemplates.add(emailType);
1153
+ }
1154
+ }
1055
1155
  // Compare
1056
1156
  const differences = [];
1057
1157
  // Integrations
@@ -1096,6 +1196,20 @@ Directory Structure:
1096
1196
  differences.push({ type: "workflow", key, status: "remote only" });
1097
1197
  }
1098
1198
  }
1199
+ // Email Templates
1200
+ for (const key of localEmailTemplates) {
1201
+ if (!remoteEmailTemplates.has(key)) {
1202
+ differences.push({ type: "email-template", key, status: "local only" });
1203
+ }
1204
+ else {
1205
+ differences.push({ type: "email-template", key, status: "exists" });
1206
+ }
1207
+ }
1208
+ for (const key of remoteEmailTemplates) {
1209
+ if (!localEmailTemplates.has(key)) {
1210
+ differences.push({ type: "email-template", key, status: "remote only" });
1211
+ }
1212
+ }
1099
1213
  // Compare test cases for synced prompts and workflows
1100
1214
  const testCaseDiffs = [];
1101
1215
  // Helper to compare test cases for a block