@wraps.dev/cli 2.14.3 → 2.14.4

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/dist/cli.js CHANGED
@@ -1756,18 +1756,6 @@ You may need to merge your existing rules into the wraps rule set.`,
1756
1756
  "TEMPLATE_PUSH_FAILED",
1757
1757
  "Check your API key and network connection.",
1758
1758
  "https://wraps.dev/docs/templates-as-code"
1759
- ),
1760
- workflowGenerationFailed: (message) => new WrapsError(
1761
- `Workflow generation failed${message ? `: ${message}` : ""}`,
1762
- "WORKFLOW_GENERATION_FAILED",
1763
- "Try rephrasing your description, or use a built-in template:\n wraps email workflows generate --template welcome\n wraps email workflows generate --template cart-recovery",
1764
- "https://wraps.dev/docs/guides/orchestration"
1765
- ),
1766
- aiUsageLimitReached: () => new WrapsError(
1767
- "AI generation usage limit reached",
1768
- "AI_USAGE_LIMIT_REACHED",
1769
- "Upgrade your plan for more AI generations, or use built-in templates:\n wraps email workflows generate --template welcome\n wraps email workflows generate --template cart-recovery",
1770
- "https://wraps.dev/docs/pricing"
1771
1759
  )
1772
1760
  };
1773
1761
  }
@@ -24735,852 +24723,171 @@ ${pc30.green("\u2713")} ${pc30.bold("Upgrade complete!")}
24735
24723
  // src/commands/email/workflows/generate.ts
24736
24724
  init_esm_shims();
24737
24725
  init_events();
24738
- import { existsSync as existsSync14, mkdirSync as mkdirSync2, writeFileSync } from "fs";
24739
- import { join as join15 } from "path";
24740
- import { cancel as cancel21, confirm as confirm14, intro as intro27, isCancel as isCancel20, log as log29 } from "@clack/prompts";
24726
+ init_json_output();
24727
+ import { existsSync as existsSync13, mkdirSync as mkdirSync2, writeFileSync } from "fs";
24728
+ import { join as join14 } from "path";
24729
+ import { intro as intro27, log as log29 } from "@clack/prompts";
24741
24730
  import pc31 from "picocolors";
24731
+ var TEMPLATES = {
24732
+ welcome: `import {
24733
+ defineWorkflow,
24734
+ sendEmail,
24735
+ delay,
24736
+ condition,
24737
+ exit,
24738
+ } from '@wraps.dev/client';
24742
24739
 
24743
- // src/utils/email/workflow-transform.ts
24744
- init_esm_shims();
24745
- function transformWorkflow(definition) {
24746
- const steps = [];
24747
- const transitions = [];
24748
- const triggerStep = createTriggerStep(definition.trigger);
24749
- steps.push(triggerStep);
24750
- if (definition.steps.length > 0) {
24751
- flattenSteps(definition.steps, steps, transitions, triggerStep.id, null);
24752
- }
24753
- assignPositions(steps, transitions);
24754
- return {
24755
- steps,
24756
- transitions,
24757
- triggerType: definition.trigger.type,
24758
- triggerConfig: extractTriggerConfig(definition.trigger),
24759
- settings: definition.settings,
24760
- defaults: definition.defaults
24761
- };
24762
- }
24763
- function createTriggerStep(trigger) {
24764
- const triggerConfig = {
24765
- type: "trigger",
24766
- triggerType: trigger.type,
24767
- ...extractTriggerConfig(trigger)
24768
- };
24769
- return {
24770
- id: "trigger",
24771
- type: "trigger",
24772
- name: getTriggerName(trigger.type),
24773
- position: { x: 0, y: 0 },
24774
- // Will be updated by assignPositions
24775
- config: triggerConfig
24776
- };
24777
- }
24778
- function extractTriggerConfig(trigger) {
24779
- const config2 = {};
24780
- if (trigger.eventName) {
24781
- config2.eventName = trigger.eventName;
24782
- }
24783
- if (trigger.segmentId) {
24784
- config2.segmentId = trigger.segmentId;
24785
- }
24786
- if (trigger.schedule) {
24787
- config2.schedule = trigger.schedule;
24788
- }
24789
- if (trigger.timezone) {
24790
- config2.timezone = trigger.timezone;
24791
- }
24792
- if (trigger.topicId) {
24793
- config2.topicId = trigger.topicId;
24794
- }
24795
- return config2;
24796
- }
24797
- function getTriggerName(type) {
24798
- const names = {
24799
- event: "When event occurs",
24800
- contact_created: "When contact is created",
24801
- contact_updated: "When contact is updated",
24802
- segment_entry: "When contact enters segment",
24803
- segment_exit: "When contact exits segment",
24804
- schedule: "On schedule",
24805
- api: "When triggered via API",
24806
- topic_subscribed: "When contact subscribes to topic",
24807
- topic_unsubscribed: "When contact unsubscribes from topic"
24808
- };
24809
- return names[type] || "Trigger";
24810
- }
24811
- function flattenSteps(stepDefs, steps, transitions, fromStepId, branch) {
24812
- let prevIds = [fromStepId];
24813
- let firstStepInBranch = true;
24814
- for (const def of stepDefs) {
24815
- const step = toWorkflowStep(def);
24816
- steps.push(step);
24817
- for (const prevId of prevIds) {
24818
- const transition = {
24819
- id: `t-${prevId}-${step.id}`,
24820
- fromStepId: prevId,
24821
- toStepId: step.id
24822
- };
24823
- if (firstStepInBranch && branch && prevId === fromStepId) {
24824
- transition.condition = { branch };
24825
- }
24826
- transitions.push(transition);
24827
- }
24828
- firstStepInBranch = false;
24829
- if (def.type === "condition" && def.branches) {
24830
- const leafIds = [];
24831
- if (def.branches.yes && def.branches.yes.length > 0) {
24832
- const yesLeaves = flattenSteps(
24833
- def.branches.yes,
24834
- steps,
24835
- transitions,
24836
- step.id,
24837
- "yes"
24838
- );
24839
- leafIds.push(...yesLeaves);
24840
- } else {
24841
- leafIds.push(step.id);
24842
- }
24843
- if (def.branches.no && def.branches.no.length > 0) {
24844
- const noLeaves = flattenSteps(
24845
- def.branches.no,
24846
- steps,
24847
- transitions,
24848
- step.id,
24849
- "no"
24850
- );
24851
- leafIds.push(...noLeaves);
24852
- } else if (!leafIds.includes(step.id)) {
24853
- leafIds.push(step.id);
24854
- }
24855
- prevIds = leafIds;
24856
- continue;
24857
- }
24858
- if (def.type === "exit") {
24859
- prevIds = [];
24860
- continue;
24861
- }
24862
- prevIds = [step.id];
24863
- }
24864
- return prevIds;
24865
- }
24866
- function toWorkflowStep(def) {
24867
- const step = {
24868
- id: def.id,
24869
- type: def.type,
24870
- name: def.name || getDefaultStepName(def.type),
24871
- position: { x: 0, y: 0 },
24872
- // Will be updated by assignPositions
24873
- config: def.config
24874
- };
24875
- if (def.cascadeGroupId) {
24876
- step.cascadeGroupId = def.cascadeGroupId;
24877
- }
24878
- return step;
24879
- }
24880
- function getDefaultStepName(type) {
24881
- const names = {
24882
- send_email: "Send Email",
24883
- send_sms: "Send SMS",
24884
- delay: "Wait",
24885
- exit: "Exit",
24886
- condition: "Condition",
24887
- webhook: "Webhook",
24888
- update_contact: "Update Contact",
24889
- wait_for_event: "Wait for Event",
24890
- wait_for_email_engagement: "Wait for Email Engagement",
24891
- subscribe_topic: "Subscribe to Topic",
24892
- unsubscribe_topic: "Unsubscribe from Topic"
24893
- };
24894
- return names[type] || type;
24895
- }
24896
- function assignPositions(steps, transitions) {
24897
- const LEVEL_HEIGHT = 200;
24898
- const BRANCH_OFFSET = 300;
24899
- const childrenMap = /* @__PURE__ */ new Map();
24900
- for (const t of transitions) {
24901
- if (!childrenMap.has(t.fromStepId)) {
24902
- childrenMap.set(t.fromStepId, []);
24903
- }
24904
- childrenMap.get(t.fromStepId)?.push({
24905
- id: t.toStepId,
24906
- branch: t.condition?.branch
24907
- });
24908
- }
24909
- const visited = /* @__PURE__ */ new Set();
24910
- const queue = [];
24911
- const triggerStep = steps.find((s) => s.type === "trigger");
24912
- if (triggerStep) {
24913
- queue.push({ stepId: triggerStep.id, level: 0, xOffset: 0 });
24914
- }
24915
- while (queue.length > 0) {
24916
- const { stepId, level, xOffset } = queue.shift();
24917
- if (visited.has(stepId)) {
24918
- continue;
24919
- }
24920
- visited.add(stepId);
24921
- const step = steps.find((s) => s.id === stepId);
24922
- if (step) {
24923
- step.position = {
24924
- x: xOffset,
24925
- y: level * LEVEL_HEIGHT
24926
- };
24927
- }
24928
- const children = childrenMap.get(stepId) || [];
24929
- for (const child of children) {
24930
- if (!visited.has(child.id)) {
24931
- let childXOffset = xOffset;
24932
- if (child.branch === "yes") {
24933
- childXOffset = xOffset - BRANCH_OFFSET;
24934
- } else if (child.branch === "no") {
24935
- childXOffset = xOffset + BRANCH_OFFSET;
24936
- }
24937
- queue.push({
24938
- stepId: child.id,
24939
- level: level + 1,
24940
- xOffset: childXOffset
24941
- });
24942
- }
24943
- }
24944
- }
24945
- for (const step of steps) {
24946
- if (!visited.has(step.id)) {
24947
- step.position = {
24948
- x: 600,
24949
- y: steps.indexOf(step) * LEVEL_HEIGHT
24950
- };
24951
- }
24952
- }
24953
- }
24954
-
24955
- // src/utils/email/workflow-ts.ts
24956
- init_esm_shims();
24957
- import { createHash as createHash2 } from "crypto";
24958
- import { existsSync as existsSync13 } from "fs";
24959
- import { mkdir as mkdir7, readdir as readdir3, readFile as readFile7, writeFile as writeFile9 } from "fs/promises";
24960
- import { basename, join as join14 } from "path";
24961
- async function discoverWorkflows(dir, filter) {
24962
- if (!existsSync13(dir)) {
24963
- return [];
24964
- }
24965
- const entries = await readdir3(dir);
24966
- const workflows = entries.filter(
24967
- (f) => (
24968
- // Include .ts files only (not .tsx for workflows)
24969
- f.endsWith(".ts") && // Exclude private/helper files starting with _
24970
- !f.startsWith("_") && // Exclude type definition files
24971
- !f.endsWith(".d.ts")
24972
- )
24973
- );
24974
- if (filter) {
24975
- const slug = filter.replace(/\.ts$/, "");
24976
- return workflows.filter((f) => f.replace(/\.ts$/, "") === slug);
24977
- }
24978
- return workflows;
24979
- }
24980
- async function parseWorkflowTs(filePath, wrapsDir) {
24981
- const { build: build2 } = await import("esbuild");
24982
- const source = await readFile7(filePath, "utf-8");
24983
- const sourceHash = createHash2("sha256").update(source).digest("hex");
24984
- const slug = basename(filePath, ".ts");
24985
- const shimDir = join14(wrapsDir, ".wraps", "_shims");
24986
- await mkdir7(shimDir, { recursive: true });
24987
- const clientShimContent = `
24988
- // Identity functions for workflow definitions
24989
- export const defineWorkflow = (def) => def;
24990
-
24991
- // Step helper functions - they just create step definition objects
24992
- export const sendEmail = (id, config) => ({
24993
- id,
24994
- type: 'send_email',
24995
- name: config.name ?? \`Send email: \${config.template || 'custom'}\`,
24996
- config: { type: 'send_email', ...config },
24997
- });
24740
+ /**
24741
+ * Welcome Sequence
24742
+ *
24743
+ * Send a welcome email when a contact is created,
24744
+ * wait 1 day, then check if they activated.
24745
+ * If not, send a follow-up with tips.
24746
+ */
24747
+ export default defineWorkflow({
24748
+ name: 'Welcome Sequence',
24749
+ trigger: {
24750
+ type: 'contact_created',
24751
+ },
24998
24752
 
24999
- export const sendSms = (id, config) => ({
25000
- id,
25001
- type: 'send_sms',
25002
- name: config.name ?? \`Send SMS: \${config.template || 'custom'}\`,
25003
- config: { type: 'send_sms', ...config },
24753
+ steps: [
24754
+ sendEmail('send-welcome', { template: 'welcome-email' }),
24755
+ delay('wait-1-day', { days: 1 }),
24756
+ condition('check-activated', {
24757
+ field: 'contact.hasActivated',
24758
+ operator: 'equals',
24759
+ value: true,
24760
+ branches: {
24761
+ yes: [exit('already-active')],
24762
+ no: [
24763
+ sendEmail('send-tips', { template: 'getting-started-tips' }),
24764
+ ],
24765
+ },
24766
+ }),
24767
+ ],
25004
24768
  });
24769
+ `,
24770
+ "cart-recovery": `import {
24771
+ defineWorkflow,
24772
+ sendEmail,
24773
+ sendSms,
24774
+ delay,
24775
+ cascade,
24776
+ exit,
24777
+ } from '@wraps.dev/client';
25005
24778
 
25006
- export const delay = (id, duration) => {
25007
- const { name, ...durationConfig } = duration;
25008
- const normalized = normalizeDuration(durationConfig);
25009
- return {
25010
- id,
25011
- type: 'delay',
25012
- name: name ?? \`Wait \${normalized.amount} \${normalized.unit}\`,
25013
- config: { type: 'delay', ...normalized },
25014
- };
25015
- };
25016
-
25017
- export const condition = (id, config) => {
25018
- const { branches, name, ...conditionConfig } = config;
25019
- return {
25020
- id,
25021
- type: 'condition',
25022
- name: name ?? \`Check: \${config.field} \${config.operator}\`,
25023
- config: { type: 'condition', ...conditionConfig },
25024
- branches,
25025
- };
25026
- };
25027
-
25028
- export const waitForEvent = (id, config) => {
25029
- const { name, timeout, ...eventConfig } = config;
25030
- return {
25031
- id,
25032
- type: 'wait_for_event',
25033
- name: name ?? \`Wait for: \${config.eventName}\`,
25034
- config: {
25035
- type: 'wait_for_event',
25036
- eventName: eventConfig.eventName,
25037
- timeoutSeconds: durationToSeconds(timeout),
25038
- },
25039
- };
25040
- };
25041
-
25042
- export const waitForEmailEngagement = (id, config) => {
25043
- const { name, timeout, emailStepId, engagementType } = config;
25044
- return {
25045
- id,
25046
- type: 'wait_for_email_engagement',
25047
- name: name ?? \`Wait for email \${engagementType}: \${emailStepId}\`,
25048
- config: {
25049
- type: 'wait_for_email_engagement',
25050
- timeoutSeconds: durationToSeconds(timeout),
25051
- },
25052
- };
25053
- };
25054
-
25055
- export const exit = (id, config) => {
25056
- const { name, ...exitConfig } = config ?? {};
25057
- return {
25058
- id,
25059
- type: 'exit',
25060
- name: name ?? 'Exit',
25061
- config: { type: 'exit', ...exitConfig },
25062
- };
25063
- };
25064
-
25065
- export const updateContact = (id, config) => {
25066
- const { name, ...updateConfig } = config;
25067
- return {
25068
- id,
25069
- type: 'update_contact',
25070
- name: name ?? 'Update contact',
25071
- config: { type: 'update_contact', ...updateConfig },
25072
- };
25073
- };
24779
+ /**
24780
+ * Cart Recovery Cascade
24781
+ *
24782
+ * When a cart is abandoned, wait 30 minutes, then try
24783
+ * email first. If not opened after 2 hours, fall back to SMS.
24784
+ */
24785
+ export default defineWorkflow({
24786
+ name: 'Cart Recovery Cascade',
24787
+ trigger: {
24788
+ type: 'event',
24789
+ eventName: 'cart.abandoned',
24790
+ },
25074
24791
 
25075
- export const subscribeTopic = (id, config) => {
25076
- const { name, ...topicConfig } = config;
25077
- return {
25078
- id,
25079
- type: 'subscribe_topic',
25080
- name: name ?? \`Subscribe to topic: \${config.topicId}\`,
25081
- config: { type: 'subscribe_topic', ...topicConfig },
25082
- };
25083
- };
24792
+ steps: [
24793
+ delay('initial-wait', { minutes: 30 }),
25084
24794
 
25085
- export const unsubscribeTopic = (id, config) => {
25086
- const { name, ...topicConfig } = config;
25087
- return {
25088
- id,
25089
- type: 'unsubscribe_topic',
25090
- name: name ?? \`Unsubscribe from topic: \${config.topicId}\`,
25091
- config: { type: 'unsubscribe_topic', ...topicConfig },
25092
- };
25093
- };
24795
+ ...cascade('recover-cart', {
24796
+ channels: [
24797
+ {
24798
+ type: 'email',
24799
+ template: 'cart-recovery',
24800
+ waitFor: { hours: 2 },
24801
+ engagement: 'opened',
24802
+ },
24803
+ {
24804
+ type: 'sms',
24805
+ template: 'cart-sms-reminder',
24806
+ },
24807
+ ],
24808
+ }),
25094
24809
 
25095
- export const webhook = (id, config) => {
25096
- const { name, ...webhookConfig } = config;
25097
- return {
25098
- id,
25099
- type: 'webhook',
25100
- name: name ?? \`Webhook: \${config.url}\`,
25101
- config: { type: 'webhook', method: 'POST', ...webhookConfig },
25102
- };
25103
- };
24810
+ exit('cascade-complete'),
24811
+ ],
24812
+ });
24813
+ `,
24814
+ "trial-conversion": `import {
24815
+ defineWorkflow,
24816
+ sendEmail,
24817
+ delay,
24818
+ condition,
24819
+ exit,
24820
+ } from '@wraps.dev/client';
25104
24821
 
25105
24822
  /**
25106
- * cascade(id, config) \u2014 expand a cross-channel cascade into primitive steps.
25107
- *
25108
- * For each email channel (except the last), we emit:
25109
- * send_email \u2192 wait_for_email_engagement \u2192 condition (engaged?)
25110
- * with the condition's "yes" branch containing an exit node and the "no"
25111
- * branch falling through to the next channel.
25112
- *
25113
- * Non-email channels (SMS) emit only a send step.
24823
+ * Trial Conversion
25114
24824
  *
25115
- * Every generated step carries cascadeGroupId = id so the execution
25116
- * engine can scope engagement queries to the correct group.
24825
+ * Remind users 3 days before their trial ends.
24826
+ * If they haven't upgraded after 1 day, send a final nudge.
25117
24827
  */
25118
- export const cascade = (id, config) => {
25119
- const channels = config.channels || [];
25120
- const steps = [];
25121
-
25122
- for (let i = 0; i < channels.length; i++) {
25123
- const channel = channels[i];
25124
- const isLast = i === channels.length - 1;
24828
+ export default defineWorkflow({
24829
+ name: 'Trial Conversion',
24830
+ trigger: {
24831
+ type: 'event',
24832
+ eventName: 'trial.ending',
24833
+ },
25125
24834
 
25126
- if (channel.type === 'email') {
25127
- // Send email step
25128
- steps.push({
25129
- id: id + '-send-' + i,
25130
- type: 'send_email',
25131
- name: 'Cascade: send ' + (channel.template || 'email'),
25132
- config: { type: 'send_email', templateId: channel.template },
25133
- cascadeGroupId: id,
25134
- });
24835
+ steps: [
24836
+ sendEmail('send-reminder', { template: 'trial-ending-reminder' }),
24837
+ delay('wait-1-day', { days: 1 }),
24838
+ condition('check-upgraded', {
24839
+ field: 'contact.plan',
24840
+ operator: 'not_equals',
24841
+ value: 'free',
24842
+ branches: {
24843
+ yes: [exit('already-upgraded')],
24844
+ no: [
24845
+ sendEmail('send-upgrade-nudge', { template: 'upgrade-offer' }),
24846
+ ],
24847
+ },
24848
+ }),
24849
+ ],
24850
+ });
24851
+ `,
24852
+ "re-engagement": `import {
24853
+ defineWorkflow,
24854
+ sendEmail,
24855
+ delay,
24856
+ condition,
24857
+ exit,
24858
+ } from '@wraps.dev/client';
25135
24859
 
25136
- // If not last channel, add wait + condition
25137
- if (!isLast && channel.waitFor) {
25138
- const waitSeconds = durationToSeconds(channel.waitFor) || 259200;
25139
- const waitId = id + '-wait-' + i;
25140
- const condId = id + '-cond-' + i;
25141
- const exitId = id + '-exit-' + i;
24860
+ /**
24861
+ * Re-engagement Campaign
24862
+ *
24863
+ * Win back inactive users with a personalized email.
24864
+ * Wait 3 days for engagement, then send a final offer.
24865
+ */
24866
+ export default defineWorkflow({
24867
+ name: 'Re-engagement Campaign',
24868
+ trigger: {
24869
+ type: 'event',
24870
+ eventName: 'contact.inactive',
24871
+ },
25142
24872
 
25143
- // Wait for engagement step
25144
- steps.push({
25145
- id: waitId,
25146
- type: 'wait_for_email_engagement',
25147
- name: 'Cascade: wait for ' + (channel.engagement || 'opened'),
25148
- config: { type: 'wait_for_email_engagement', timeoutSeconds: waitSeconds },
25149
- cascadeGroupId: id,
25150
- });
25151
-
25152
- // Condition step: check engagement.status
25153
- steps.push({
25154
- id: condId,
25155
- type: 'condition',
25156
- name: 'Cascade: email engaged?',
25157
- config: {
25158
- type: 'condition',
25159
- field: 'engagement.status',
25160
- operator: 'equals',
25161
- value: 'true',
25162
- },
25163
- cascadeGroupId: id,
25164
- branches: {
25165
- yes: [{
25166
- id: exitId,
25167
- type: 'exit',
25168
- name: 'Exit',
25169
- config: { type: 'exit', reason: 'Engaged via email' },
25170
- cascadeGroupId: id,
25171
- }],
25172
- },
25173
- });
25174
- }
25175
- } else if (channel.type === 'sms') {
25176
- // Send SMS step
25177
- steps.push({
25178
- id: id + '-send-' + i,
25179
- type: 'send_sms',
25180
- name: 'Cascade: send ' + (channel.template || 'sms'),
25181
- config: { type: 'send_sms', template: channel.template, body: channel.body },
25182
- cascadeGroupId: id,
25183
- });
25184
- }
25185
- }
25186
-
25187
- return steps;
25188
- };
25189
-
25190
- // Internal helpers
25191
- function normalizeDuration(duration) {
25192
- if (duration.days !== undefined) {
25193
- return { amount: duration.days, unit: 'days' };
25194
- }
25195
- if (duration.hours !== undefined) {
25196
- return { amount: duration.hours, unit: 'hours' };
25197
- }
25198
- if (duration.minutes !== undefined) {
25199
- return { amount: duration.minutes, unit: 'minutes' };
25200
- }
25201
- return { amount: 1, unit: 'hours' };
25202
- }
25203
-
25204
- function durationToSeconds(duration) {
25205
- if (!duration) return undefined;
25206
- let seconds = 0;
25207
- if (duration.days) seconds += duration.days * 24 * 60 * 60;
25208
- if (duration.hours) seconds += duration.hours * 60 * 60;
25209
- if (duration.minutes) seconds += duration.minutes * 60;
25210
- return seconds > 0 ? seconds : undefined;
25211
- }
25212
- `;
25213
- await writeFile9(
25214
- join14(shimDir, "wraps-client-shim.mjs"),
25215
- clientShimContent,
25216
- "utf-8"
25217
- );
25218
- const result = await build2({
25219
- entryPoints: [filePath],
25220
- bundle: true,
25221
- write: false,
25222
- format: "esm",
25223
- platform: "node",
25224
- target: "node20",
25225
- alias: {
25226
- "@wraps.dev/client": join14(shimDir, "wraps-client-shim.mjs")
25227
- }
25228
- });
25229
- const bundledCode = result.outputFiles[0].text;
25230
- const tmpDir = join14(wrapsDir, ".wraps", "_workflows");
25231
- await mkdir7(tmpDir, { recursive: true });
25232
- const tmpPath = join14(tmpDir, `${slug}.mjs`);
25233
- await writeFile9(tmpPath, bundledCode, "utf-8");
25234
- const mod = await import(`${tmpPath}?t=${Date.now()}`);
25235
- const definition = mod.default;
25236
- if (!definition || typeof definition !== "object") {
25237
- throw new Error(
25238
- "Workflow must have a default export (workflow definition from defineWorkflow())"
25239
- );
25240
- }
25241
- if (!definition.name) {
25242
- throw new Error("Workflow definition must have a 'name' property");
25243
- }
25244
- if (!definition.trigger) {
25245
- throw new Error("Workflow definition must have a 'trigger' property");
25246
- }
25247
- if (!Array.isArray(definition.steps)) {
25248
- throw new Error("Workflow definition must have a 'steps' array");
25249
- }
25250
- return {
25251
- slug,
25252
- filePath,
25253
- source,
25254
- sourceHash,
25255
- definition,
25256
- cliProjectPath: `workflows/${slug}.ts`
25257
- };
25258
- }
25259
-
25260
- // src/utils/email/workflow-validator.ts
25261
- init_esm_shims();
25262
- function validateTransformedWorkflow(transformed, localTemplateSlugs) {
25263
- const errors2 = [];
25264
- errors2.push(...validateStructure(transformed.steps, transformed.transitions));
25265
- for (const step of transformed.steps) {
25266
- errors2.push(...validateStep(step));
25267
- }
25268
- if (localTemplateSlugs) {
25269
- errors2.push(
25270
- ...validateTemplateReferences(transformed.steps, localTemplateSlugs)
25271
- );
25272
- }
25273
- const errorsByNodeId = /* @__PURE__ */ new Map();
25274
- for (const error of errors2) {
25275
- if (error.nodeId) {
25276
- const existing = errorsByNodeId.get(error.nodeId) || [];
25277
- existing.push(error);
25278
- errorsByNodeId.set(error.nodeId, existing);
25279
- }
25280
- }
25281
- return {
25282
- isValid: errors2.filter((e) => e.severity === "error").length === 0,
25283
- errors: errors2,
25284
- errorsByNodeId
25285
- };
25286
- }
25287
- function validateStructure(steps, transitions) {
25288
- const errors2 = [];
25289
- const triggerSteps = steps.filter((s) => s.type === "trigger");
25290
- if (triggerSteps.length === 0) {
25291
- errors2.push({
25292
- message: "Workflow must have a trigger node",
25293
- severity: "error"
25294
- });
25295
- } else if (triggerSteps.length > 1) {
25296
- errors2.push({
25297
- message: "Workflow can only have one trigger node",
25298
- severity: "error"
25299
- });
25300
- }
25301
- const actionSteps = steps.filter(
25302
- (s) => s.type !== "trigger" && s.type !== "exit"
25303
- );
25304
- if (actionSteps.length === 0 && steps.length > 1) {
25305
- errors2.push({
25306
- message: "Workflow must have at least one action step",
25307
- severity: "error"
25308
- });
25309
- }
25310
- if (triggerSteps.length === 1) {
25311
- const reachableIds = getReachableNodeIds(triggerSteps[0].id, transitions);
25312
- const orphanSteps = steps.filter(
25313
- (s) => s.type !== "trigger" && !reachableIds.has(s.id)
25314
- );
25315
- for (const orphan of orphanSteps) {
25316
- errors2.push({
25317
- nodeId: orphan.id,
25318
- message: `"${orphan.name}" is not connected to the workflow`,
25319
- severity: "warning"
25320
- });
25321
- }
25322
- }
25323
- const stepIds = new Set(steps.map((s) => s.id));
25324
- for (const transition of transitions) {
25325
- if (!stepIds.has(transition.fromStepId)) {
25326
- errors2.push({
25327
- message: `Transition references non-existent source step: ${transition.fromStepId}`,
25328
- severity: "error"
25329
- });
25330
- }
25331
- if (!stepIds.has(transition.toStepId)) {
25332
- errors2.push({
25333
- message: `Transition references non-existent target step: ${transition.toStepId}`,
25334
- severity: "error"
25335
- });
25336
- }
25337
- }
25338
- return errors2;
25339
- }
25340
- function getReachableNodeIds(startId, transitions) {
25341
- const reachable = /* @__PURE__ */ new Set();
25342
- const queue = [startId];
25343
- while (queue.length > 0) {
25344
- const currentId = queue.shift();
25345
- if (reachable.has(currentId)) {
25346
- continue;
25347
- }
25348
- reachable.add(currentId);
25349
- const outgoing = transitions.filter((t) => t.fromStepId === currentId);
25350
- for (const t of outgoing) {
25351
- if (!reachable.has(t.toStepId)) {
25352
- queue.push(t.toStepId);
25353
- }
25354
- }
25355
- }
25356
- return reachable;
25357
- }
25358
- function validateStep(step) {
25359
- const errors2 = [];
25360
- const config2 = step.config;
25361
- const configType = config2.type;
25362
- switch (configType) {
25363
- case "trigger":
25364
- errors2.push(...validateTrigger(step.id, config2));
25365
- break;
25366
- case "send_email":
25367
- errors2.push(...validateSendEmail(step.id, config2));
25368
- break;
25369
- case "condition":
25370
- errors2.push(...validateCondition(step.id, config2));
25371
- break;
25372
- case "webhook":
25373
- errors2.push(...validateWebhook(step.id, config2));
25374
- break;
25375
- case "subscribe_topic":
25376
- case "unsubscribe_topic":
25377
- errors2.push(...validateTopic(step.id, config2));
25378
- break;
25379
- case "wait_for_event":
25380
- errors2.push(...validateWaitForEvent(step.id, config2));
25381
- break;
25382
- case "delay":
25383
- errors2.push(...validateDelay(step.id, config2));
25384
- break;
25385
- }
25386
- return errors2;
25387
- }
25388
- function validateTrigger(nodeId, config2) {
25389
- const errors2 = [];
25390
- switch (config2.triggerType) {
25391
- case "event":
25392
- if (!config2.eventName) {
25393
- errors2.push({
25394
- nodeId,
25395
- field: "eventName",
25396
- message: "Event name is required",
25397
- severity: "error"
25398
- });
25399
- }
25400
- break;
25401
- case "segment_entry":
25402
- case "segment_exit":
25403
- if (!config2.segmentId) {
25404
- errors2.push({
25405
- nodeId,
25406
- field: "segmentId",
25407
- message: "Segment is required",
25408
- severity: "error"
25409
- });
25410
- }
25411
- break;
25412
- case "topic_subscribed":
25413
- case "topic_unsubscribed":
25414
- if (!config2.topicId) {
25415
- errors2.push({
25416
- nodeId,
25417
- field: "topicId",
25418
- message: "Topic is required",
25419
- severity: "error"
25420
- });
25421
- }
25422
- break;
25423
- case "schedule":
25424
- if (!config2.schedule) {
25425
- errors2.push({
25426
- nodeId,
25427
- field: "schedule",
25428
- message: "Schedule (cron expression) is required",
25429
- severity: "error"
25430
- });
25431
- }
25432
- if (!config2.timezone) {
25433
- errors2.push({
25434
- nodeId,
25435
- field: "timezone",
25436
- message: "Timezone is required",
25437
- severity: "error"
25438
- });
25439
- }
25440
- break;
25441
- }
25442
- return errors2;
25443
- }
25444
- function validateSendEmail(nodeId, config2) {
25445
- const errors2 = [];
25446
- const templateRef = config2.templateId || config2.template;
25447
- if (!templateRef) {
25448
- errors2.push({
25449
- nodeId,
25450
- field: "templateId",
25451
- message: "Email template is required",
25452
- severity: "error"
25453
- });
25454
- }
25455
- return errors2;
25456
- }
25457
- function validateCondition(nodeId, config2) {
25458
- const errors2 = [];
25459
- if (!config2.field) {
25460
- errors2.push({
25461
- nodeId,
25462
- field: "field",
25463
- message: "Condition field is required",
25464
- severity: "error"
25465
- });
25466
- }
25467
- if (!config2.operator) {
25468
- errors2.push({
25469
- nodeId,
25470
- field: "operator",
25471
- message: "Condition operator is required",
25472
- severity: "error"
25473
- });
25474
- }
25475
- if (config2.operator !== "is_set" && config2.operator !== "is_not_set" && config2.operator !== "is_true" && config2.operator !== "is_false" && (config2.value === void 0 || config2.value === "")) {
25476
- errors2.push({
25477
- nodeId,
25478
- field: "value",
25479
- message: "Condition value is required",
25480
- severity: "error"
25481
- });
25482
- }
25483
- return errors2;
25484
- }
25485
- function validateWebhook(nodeId, config2) {
25486
- const errors2 = [];
25487
- if (config2.url) {
25488
- try {
25489
- new URL(config2.url);
25490
- } catch {
25491
- errors2.push({
25492
- nodeId,
25493
- field: "url",
25494
- message: "Invalid URL format",
25495
- severity: "error"
25496
- });
25497
- }
25498
- } else {
25499
- errors2.push({
25500
- nodeId,
25501
- field: "url",
25502
- message: "Webhook URL is required",
25503
- severity: "error"
25504
- });
25505
- }
25506
- return errors2;
25507
- }
25508
- function validateTopic(nodeId, config2) {
25509
- const errors2 = [];
25510
- if (!config2.topicId) {
25511
- errors2.push({
25512
- nodeId,
25513
- field: "topicId",
25514
- message: "Topic is required",
25515
- severity: "error"
25516
- });
25517
- }
25518
- return errors2;
25519
- }
25520
- function validateWaitForEvent(nodeId, config2) {
25521
- const errors2 = [];
25522
- if (!config2.eventName) {
25523
- errors2.push({
25524
- nodeId,
25525
- field: "eventName",
25526
- message: "Event name is required",
25527
- severity: "error"
25528
- });
25529
- }
25530
- return errors2;
25531
- }
25532
- function validateDelay(nodeId, config2) {
25533
- const errors2 = [];
25534
- const amount = config2.amount;
25535
- if (!amount || amount < 1) {
25536
- errors2.push({
25537
- nodeId,
25538
- field: "amount",
25539
- message: "Delay duration must be at least 1",
25540
- severity: "error"
25541
- });
25542
- }
25543
- return errors2;
25544
- }
25545
- function validateTemplateReferences(steps, localTemplateSlugs) {
25546
- const errors2 = [];
25547
- for (const step of steps) {
25548
- if (step.config.type === "send_email") {
25549
- const config2 = step.config;
25550
- const templateRef = config2.templateId || config2.template;
25551
- if (templateRef && !localTemplateSlugs.has(templateRef)) {
25552
- errors2.push({
25553
- nodeId: step.id,
25554
- field: "templateId",
25555
- message: `Template "${templateRef}" not found in templates/ directory`,
25556
- severity: "error"
25557
- });
25558
- }
25559
- }
25560
- if (step.config.type === "send_sms") {
25561
- const config2 = step.config;
25562
- const templateRef = config2.templateId || config2.template;
25563
- if (templateRef && !localTemplateSlugs.has(templateRef)) {
25564
- errors2.push({
25565
- nodeId: step.id,
25566
- field: "templateId",
25567
- message: `SMS template "${templateRef}" not found in templates/ directory`,
25568
- severity: "warning"
25569
- // Warning for SMS since we might not have SMS templates yet
25570
- });
25571
- }
25572
- }
25573
- }
25574
- return errors2;
25575
- }
25576
-
25577
- // src/commands/email/workflows/generate.ts
25578
- init_config();
25579
- init_errors();
25580
- init_json_output();
25581
- init_output();
25582
- var TEMPLATES = {
25583
- welcome: `import {
24873
+ steps: [
24874
+ sendEmail('send-win-back', { template: 'we-miss-you' }),
24875
+ delay('wait-3-days', { days: 3 }),
24876
+ condition('check-engaged', {
24877
+ field: 'contact.lastActiveAt',
24878
+ operator: 'is_set',
24879
+ value: true,
24880
+ branches: {
24881
+ yes: [exit('re-engaged')],
24882
+ no: [
24883
+ sendEmail('send-final-offer', { template: 'final-offer' }),
24884
+ ],
24885
+ },
24886
+ }),
24887
+ ],
24888
+ });
24889
+ `,
24890
+ onboarding: `import {
25584
24891
  defineWorkflow,
25585
24892
  sendEmail,
25586
24893
  delay,
@@ -25589,196 +24896,38 @@ var TEMPLATES = {
25589
24896
  } from '@wraps.dev/client';
25590
24897
 
25591
24898
  /**
25592
- * Welcome Sequence
24899
+ * Multi-step Onboarding
25593
24900
  *
25594
- * Send a welcome email when a contact is created,
25595
- * wait 1 day, then check if they activated.
25596
- * If not, send a follow-up with tips.
24901
+ * Guide new users through setup with a series of emails.
24902
+ * Check progress at each step and skip ahead if they're done.
25597
24903
  */
25598
24904
  export default defineWorkflow({
25599
- name: 'Welcome Sequence',
24905
+ name: 'Onboarding Sequence',
25600
24906
  trigger: {
25601
24907
  type: 'contact_created',
25602
24908
  },
25603
24909
 
25604
24910
  steps: [
25605
- sendEmail('send-welcome', { template: 'welcome-email' }),
24911
+ sendEmail('send-welcome', { template: 'onboarding-welcome' }),
25606
24912
  delay('wait-1-day', { days: 1 }),
25607
- condition('check-activated', {
25608
- field: 'contact.hasActivated',
24913
+
24914
+ condition('check-profile-complete', {
24915
+ field: 'contact.profileComplete',
25609
24916
  operator: 'equals',
25610
24917
  value: true,
25611
24918
  branches: {
25612
- yes: [exit('already-active')],
24919
+ yes: [
24920
+ sendEmail('send-next-steps', { template: 'onboarding-next-steps' }),
24921
+ ],
25613
24922
  no: [
25614
- sendEmail('send-tips', { template: 'getting-started-tips' }),
24923
+ sendEmail('send-profile-reminder', { template: 'complete-your-profile' }),
24924
+ delay('wait-2-days', { days: 2 }),
24925
+ sendEmail('send-next-steps-delayed', { template: 'onboarding-next-steps' }),
25615
24926
  ],
25616
24927
  },
25617
24928
  }),
25618
- ],
25619
- });
25620
- `,
25621
- "cart-recovery": `import {
25622
- defineWorkflow,
25623
- sendEmail,
25624
- sendSms,
25625
- delay,
25626
- cascade,
25627
- exit,
25628
- } from '@wraps.dev/client';
25629
-
25630
- /**
25631
- * Cart Recovery Cascade
25632
- *
25633
- * When a cart is abandoned, wait 30 minutes, then try
25634
- * email first. If not opened after 2 hours, fall back to SMS.
25635
- */
25636
- export default defineWorkflow({
25637
- name: 'Cart Recovery Cascade',
25638
- trigger: {
25639
- type: 'event',
25640
- eventName: 'cart.abandoned',
25641
- },
25642
24929
 
25643
- steps: [
25644
- delay('initial-wait', { minutes: 30 }),
25645
-
25646
- ...cascade('recover-cart', {
25647
- channels: [
25648
- {
25649
- type: 'email',
25650
- template: 'cart-recovery',
25651
- waitFor: { hours: 2 },
25652
- engagement: 'opened',
25653
- },
25654
- {
25655
- type: 'sms',
25656
- template: 'cart-sms-reminder',
25657
- },
25658
- ],
25659
- }),
25660
-
25661
- exit('cascade-complete'),
25662
- ],
25663
- });
25664
- `,
25665
- "trial-conversion": `import {
25666
- defineWorkflow,
25667
- sendEmail,
25668
- delay,
25669
- condition,
25670
- exit,
25671
- } from '@wraps.dev/client';
25672
-
25673
- /**
25674
- * Trial Conversion
25675
- *
25676
- * Remind users 3 days before their trial ends.
25677
- * If they haven't upgraded after 1 day, send a final nudge.
25678
- */
25679
- export default defineWorkflow({
25680
- name: 'Trial Conversion',
25681
- trigger: {
25682
- type: 'event',
25683
- eventName: 'trial.ending',
25684
- },
25685
-
25686
- steps: [
25687
- sendEmail('send-reminder', { template: 'trial-ending-reminder' }),
25688
- delay('wait-1-day', { days: 1 }),
25689
- condition('check-upgraded', {
25690
- field: 'contact.plan',
25691
- operator: 'not_equals',
25692
- value: 'free',
25693
- branches: {
25694
- yes: [exit('already-upgraded')],
25695
- no: [
25696
- sendEmail('send-upgrade-nudge', { template: 'upgrade-offer' }),
25697
- ],
25698
- },
25699
- }),
25700
- ],
25701
- });
25702
- `,
25703
- "re-engagement": `import {
25704
- defineWorkflow,
25705
- sendEmail,
25706
- delay,
25707
- condition,
25708
- exit,
25709
- } from '@wraps.dev/client';
25710
-
25711
- /**
25712
- * Re-engagement Campaign
25713
- *
25714
- * Win back inactive users with a personalized email.
25715
- * Wait 3 days for engagement, then send a final offer.
25716
- */
25717
- export default defineWorkflow({
25718
- name: 'Re-engagement Campaign',
25719
- trigger: {
25720
- type: 'event',
25721
- eventName: 'contact.inactive',
25722
- },
25723
-
25724
- steps: [
25725
- sendEmail('send-win-back', { template: 'we-miss-you' }),
25726
- delay('wait-3-days', { days: 3 }),
25727
- condition('check-engaged', {
25728
- field: 'contact.lastActiveAt',
25729
- operator: 'is_set',
25730
- value: true,
25731
- branches: {
25732
- yes: [exit('re-engaged')],
25733
- no: [
25734
- sendEmail('send-final-offer', { template: 'final-offer' }),
25735
- ],
25736
- },
25737
- }),
25738
- ],
25739
- });
25740
- `,
25741
- onboarding: `import {
25742
- defineWorkflow,
25743
- sendEmail,
25744
- delay,
25745
- condition,
25746
- exit,
25747
- } from '@wraps.dev/client';
25748
-
25749
- /**
25750
- * Multi-step Onboarding
25751
- *
25752
- * Guide new users through setup with a series of emails.
25753
- * Check progress at each step and skip ahead if they're done.
25754
- */
25755
- export default defineWorkflow({
25756
- name: 'Onboarding Sequence',
25757
- trigger: {
25758
- type: 'contact_created',
25759
- },
25760
-
25761
- steps: [
25762
- sendEmail('send-welcome', { template: 'onboarding-welcome' }),
25763
- delay('wait-1-day', { days: 1 }),
25764
-
25765
- condition('check-profile-complete', {
25766
- field: 'contact.profileComplete',
25767
- operator: 'equals',
25768
- value: true,
25769
- branches: {
25770
- yes: [
25771
- sendEmail('send-next-steps', { template: 'onboarding-next-steps' }),
25772
- ],
25773
- no: [
25774
- sendEmail('send-profile-reminder', { template: 'complete-your-profile' }),
25775
- delay('wait-2-days', { days: 2 }),
25776
- sendEmail('send-next-steps-delayed', { template: 'onboarding-next-steps' }),
25777
- ],
25778
- },
25779
- }),
25780
-
25781
- delay('wait-3-days', { days: 3 }),
24930
+ delay('wait-3-days', { days: 3 }),
25782
24931
 
25783
24932
  condition('check-first-action', {
25784
24933
  field: 'contact.hasCompletedFirstAction',
@@ -25800,8 +24949,6 @@ async function workflowsGenerate(options) {
25800
24949
  const startTime = Date.now();
25801
24950
  if (options.template) {
25802
24951
  generateFromTemplate(options);
25803
- } else if (options.description) {
25804
- await generateFromDescription(options);
25805
24952
  } else {
25806
24953
  showUsage();
25807
24954
  return;
@@ -25809,31 +24956,34 @@ async function workflowsGenerate(options) {
25809
24956
  trackCommand("email:workflows:generate", {
25810
24957
  success: true,
25811
24958
  duration_ms: Date.now() - startTime,
25812
- mode: options.template ? "template" : "llm"
24959
+ mode: "template"
25813
24960
  });
25814
24961
  }
25815
24962
  function showUsage() {
25816
24963
  if (isJsonMode()) {
25817
24964
  jsonError("email.workflows.generate", {
25818
24965
  code: "MISSING_INPUT",
25819
- message: "Provide a --template name or a description as a positional argument"
24966
+ message: "Provide a --template name to generate a workflow"
25820
24967
  });
25821
24968
  return;
25822
24969
  }
25823
- log29.error("Provide a description or use --template to generate a workflow.");
24970
+ log29.error("Provide a --template to generate a workflow.");
25824
24971
  console.log();
25825
- console.log(` ${pc31.bold("Template mode:")}`);
24972
+ console.log(` ${pc31.bold("Usage:")}`);
25826
24973
  console.log(
25827
24974
  ` ${pc31.cyan("wraps email workflows generate --template welcome")}`
25828
24975
  );
25829
24976
  console.log();
25830
- console.log(` ${pc31.bold("AI mode:")}`);
25831
24977
  console.log(
25832
- ` ${pc31.cyan('wraps email workflows generate "Welcome series: send welcome on signup, wait 1 day, check activation"')}`
24978
+ ` ${pc31.bold("Available templates:")} ${TEMPLATE_NAMES.join(", ")}`
25833
24979
  );
25834
24980
  console.log();
24981
+ console.log(` ${pc31.bold("Want a custom workflow?")}`);
25835
24982
  console.log(
25836
- ` ${pc31.bold("Available templates:")} ${TEMPLATE_NAMES.join(", ")}`
24983
+ " Describe what you need to your AI coding assistant (Claude Code, Cursor, etc.)"
24984
+ );
24985
+ console.log(
24986
+ ` and it will generate a workflow file using the ${pc31.cyan("@wraps.dev/client")} DSL.`
25837
24987
  );
25838
24988
  console.log();
25839
24989
  }
@@ -25858,9 +25008,9 @@ function generateFromTemplate(options) {
25858
25008
  }
25859
25009
  const slug = options.name || templateName;
25860
25010
  const cwd = process.cwd();
25861
- const workflowsDir = join15(cwd, "wraps", "workflows");
25862
- const filePath = join15(workflowsDir, `${slug}.ts`);
25863
- if (existsSync14(filePath) && !options.force) {
25011
+ const workflowsDir = join14(cwd, "wraps", "workflows");
25012
+ const filePath = join14(workflowsDir, `${slug}.ts`);
25013
+ if (existsSync13(filePath) && !options.force) {
25864
25014
  if (isJsonMode()) {
25865
25015
  jsonError("email.workflows.generate", {
25866
25016
  code: "FILE_EXISTS",
@@ -25890,195 +25040,27 @@ function generateFromTemplate(options) {
25890
25040
  showNextSteps2(slug);
25891
25041
  }
25892
25042
  }
25893
- async function generateFromDescription(options) {
25894
- const description = options.description ?? "";
25895
- const slug = options.name || slugify(description);
25896
- if (!checkFileExists(slug, options.force)) {
25897
- return;
25898
- }
25899
- if (!isJsonMode()) {
25900
- intro27(pc31.bold("Generate Workflow"));
25901
- }
25902
- const progress = new DeploymentProgress();
25903
- const token = await resolveTokenAsync({ token: options.token });
25904
- if (!token) {
25905
- throw errors.notAuthenticated();
25906
- }
25907
- const code = await callGenerateApi(description, slug, token, progress);
25908
- if (!code) {
25909
- return;
25910
- }
25911
- if (options.dryRun) {
25912
- showDryRun(slug, code);
25913
- return;
25914
- }
25915
- if (!(options.yes || isJsonMode())) {
25916
- const shouldWrite = await showPreviewAndConfirm(slug, code);
25917
- if (!shouldWrite) {
25918
- return;
25919
- }
25920
- }
25921
- writeWorkflowFile(slug, code);
25922
- const workflowsDir = join15(process.cwd(), "wraps", "workflows");
25923
- const filePath = join15(workflowsDir, `${slug}.ts`);
25924
- await autoValidate(filePath, join15(process.cwd(), "wraps"), slug, progress);
25925
- if (isJsonMode()) {
25926
- jsonSuccess("email.workflows.generate", {
25927
- mode: "llm",
25928
- slug,
25929
- path: `wraps/workflows/${slug}.ts`
25930
- });
25931
- } else {
25932
- log29.success(`Created ${pc31.cyan(`wraps/workflows/${slug}.ts`)}`);
25933
- showNextSteps2(slug, true);
25934
- }
25935
- }
25936
- function slugify(text9) {
25937
- return text9.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "").slice(0, 50);
25938
- }
25939
- function checkFileExists(slug, force) {
25940
- const filePath = join15(process.cwd(), "wraps", "workflows", `${slug}.ts`);
25941
- if (existsSync14(filePath) && !force) {
25942
- if (isJsonMode()) {
25943
- jsonError("email.workflows.generate", {
25944
- code: "FILE_EXISTS",
25945
- message: `wraps/workflows/${slug}.ts already exists. Use --force to overwrite.`
25946
- });
25947
- } else {
25948
- log29.error(
25949
- `${pc31.cyan(`wraps/workflows/${slug}.ts`)} already exists. Use ${pc31.bold("--force")} to overwrite.`
25950
- );
25951
- }
25952
- return false;
25953
- }
25954
- return true;
25955
- }
25956
- async function callGenerateApi(description, slug, token, progress) {
25957
- progress.start("Generating workflow from description");
25958
- const apiBase = getApiBaseUrl();
25959
- const resp = await fetch(`${apiBase}/v1/workflows/generate`, {
25960
- method: "POST",
25961
- headers: {
25962
- "Content-Type": "application/json",
25963
- Authorization: `Bearer ${token}`
25964
- },
25965
- body: JSON.stringify({ description, slug })
25966
- });
25967
- if (!resp.ok) {
25968
- progress.fail("Generation failed");
25969
- if (resp.status === 429) {
25970
- throw errors.aiUsageLimitReached();
25971
- }
25972
- const body = await resp.text();
25973
- let message;
25974
- try {
25975
- const parsed = JSON.parse(body);
25976
- message = parsed.error || parsed.message || body;
25977
- } catch (e) {
25978
- if (e instanceof SyntaxError) {
25979
- message = body;
25980
- } else {
25981
- throw e;
25982
- }
25983
- }
25984
- throw errors.workflowGenerationFailed(message);
25985
- }
25986
- const data = await resp.json();
25987
- progress.succeed("Workflow generated");
25988
- return data.code;
25989
- }
25990
- function showDryRun(slug, code) {
25991
- if (isJsonMode()) {
25992
- jsonSuccess("email.workflows.generate", {
25993
- mode: "llm",
25994
- dryRun: true,
25995
- slug,
25996
- code
25997
- });
25998
- } else {
25999
- console.log();
26000
- log29.info(pc31.bold("Dry run \u2014 no file written"));
26001
- console.log();
26002
- console.log(pc31.dim("\u2500".repeat(60)));
26003
- console.log(code);
26004
- console.log(pc31.dim("\u2500".repeat(60)));
26005
- console.log();
26006
- }
26007
- }
26008
- async function showPreviewAndConfirm(slug, code) {
26009
- console.log();
26010
- console.log(pc31.dim("\u2500".repeat(60)));
26011
- console.log(code);
26012
- console.log(pc31.dim("\u2500".repeat(60)));
26013
- console.log();
26014
- const confirmed = await confirm14({
26015
- message: `Write to ${pc31.cyan(`wraps/workflows/${slug}.ts`)}?`,
26016
- initialValue: true
26017
- });
26018
- if (isCancel20(confirmed) || !confirmed) {
26019
- cancel21("Generation cancelled.");
26020
- return false;
26021
- }
26022
- return true;
26023
- }
26024
- function writeWorkflowFile(slug, code) {
26025
- const workflowsDir = join15(process.cwd(), "wraps", "workflows");
26026
- mkdirSync2(workflowsDir, { recursive: true });
26027
- writeFileSync(join15(workflowsDir, `${slug}.ts`), code, "utf-8");
26028
- }
26029
- function showNextSteps2(slug, isLlm = false) {
25043
+ function showNextSteps2(slug) {
26030
25044
  console.log();
26031
25045
  console.log(` ${pc31.bold("Next steps:")}`);
26032
- if (isLlm) {
26033
- console.log(
26034
- ` 1. Review the generated workflow in ${pc31.cyan(`wraps/workflows/${slug}.ts`)}`
26035
- );
26036
- } else {
26037
- console.log(
26038
- ` 1. Edit template references in ${pc31.cyan(`wraps/workflows/${slug}.ts`)}`
26039
- );
26040
- }
25046
+ console.log(
25047
+ ` 1. Edit template references in ${pc31.cyan(`wraps/workflows/${slug}.ts`)}`
25048
+ );
26041
25049
  console.log(
26042
25050
  ` 2. Validate: ${pc31.cyan(`wraps email workflows validate --workflow ${slug}`)}`
26043
25051
  );
26044
25052
  console.log(` 3. Push: ${pc31.cyan("wraps email workflows push")}`);
26045
25053
  console.log();
26046
25054
  }
26047
- async function autoValidate(filePath, wrapsDir, slug, progress) {
26048
- try {
26049
- progress.start(`Validating ${pc31.cyan(slug)}`);
26050
- const parsed = await parseWorkflowTs(filePath, wrapsDir);
26051
- const transformed = transformWorkflow(parsed.definition);
26052
- const result = validateTransformedWorkflow(transformed);
26053
- const errs = result.errors.filter((e) => e.severity === "error");
26054
- const warnings = result.errors.filter((e) => e.severity === "warning");
26055
- if (errs.length === 0 && warnings.length === 0) {
26056
- progress.succeed(`${pc31.cyan(slug)} is valid`);
26057
- } else if (errs.length === 0) {
26058
- progress.succeed(
26059
- `${pc31.cyan(slug)} is valid with ${warnings.length} warning(s)`
26060
- );
26061
- } else {
26062
- progress.fail(
26063
- `${pc31.cyan(slug)} has ${errs.length} validation error(s) \u2014 review and fix manually`
26064
- );
26065
- }
26066
- } catch (e) {
26067
- const msg = e instanceof Error ? e.message : String(e);
26068
- progress.info(
26069
- `Could not auto-validate ${pc31.cyan(slug)} \u2014 run ${pc31.cyan("wraps email workflows validate")} manually (${msg})`
26070
- );
26071
- }
26072
- }
26073
25055
 
26074
25056
  // src/commands/email/workflows/init.ts
26075
25057
  init_esm_shims();
26076
25058
  init_events();
26077
25059
  init_json_output();
26078
25060
  init_output();
26079
- import { existsSync as existsSync15 } from "fs";
26080
- import { mkdir as mkdir8, readFile as readFile8, writeFile as writeFile10 } from "fs/promises";
26081
- import { join as join16 } from "path";
25061
+ import { existsSync as existsSync14 } from "fs";
25062
+ import { mkdir as mkdir7, readFile as readFile7, writeFile as writeFile9 } from "fs/promises";
25063
+ import { join as join15 } from "path";
26082
25064
  import * as clack30 from "@clack/prompts";
26083
25065
  import pc32 from "picocolors";
26084
25066
 
@@ -26415,188 +25397,1024 @@ export default defineWorkflow({
26415
25397
  ],
26416
25398
  }),
26417
25399
 
26418
- exit('cascade-complete'),
26419
- ],
26420
- });
26421
- \`\`\`
25400
+ exit('cascade-complete'),
25401
+ ],
25402
+ });
25403
+ \`\`\`
25404
+
25405
+ ## Key Commands
25406
+
25407
+ \`\`\`bash
25408
+ wraps email workflows validate # Validate all workflow files
25409
+ wraps email workflows validate --workflow welcome # Validate a specific workflow
25410
+ wraps email workflows push # Push workflows to dashboard
25411
+ wraps email workflows generate --template welcome # Generate from built-in template
25412
+ wraps email workflows generate "description..." # Generate from AI description
25413
+ \`\`\`
25414
+ `;
25415
+
25416
+ // src/commands/email/workflows/init.ts
25417
+ var EXAMPLE_WORKFLOW = `import {
25418
+ defineWorkflow,
25419
+ sendEmail,
25420
+ delay,
25421
+ condition,
25422
+ exit,
25423
+ } from '@wraps.dev/client';
25424
+
25425
+ /**
25426
+ * Welcome Sequence
25427
+ *
25428
+ * Send a welcome email when a contact is created,
25429
+ * wait 1 day, then check if they activated.
25430
+ * If not, send a follow-up with tips.
25431
+ */
25432
+ export default defineWorkflow({
25433
+ name: 'Welcome Sequence',
25434
+ trigger: {
25435
+ type: 'contact_created',
25436
+ },
25437
+
25438
+ steps: [
25439
+ sendEmail('send-welcome', { template: 'welcome-email' }),
25440
+ delay('wait-1-day', { days: 1 }),
25441
+ condition('check-activated', {
25442
+ field: 'contact.hasActivated',
25443
+ operator: 'equals',
25444
+ value: true,
25445
+ branches: {
25446
+ yes: [exit('already-active')],
25447
+ no: [
25448
+ sendEmail('send-tips', { template: 'getting-started-tips' }),
25449
+ ],
25450
+ },
25451
+ }),
25452
+ ],
25453
+ });
25454
+ `;
25455
+ async function workflowsInit(options) {
25456
+ const startTime = Date.now();
25457
+ const cwd = process.cwd();
25458
+ const workflowsDir = join15(cwd, "wraps", "workflows");
25459
+ if (!isJsonMode()) {
25460
+ clack30.intro(pc32.bold("Workflows as Code"));
25461
+ }
25462
+ const progress = new DeploymentProgress();
25463
+ if (existsSync14(workflowsDir) && !options.force) {
25464
+ const { readdir: readdir4 } = await import("fs/promises");
25465
+ const files = await readdir4(workflowsDir);
25466
+ const tsFiles = files.filter(
25467
+ (f) => f.endsWith(".ts") && !f.startsWith("_")
25468
+ );
25469
+ if (tsFiles.length > 0 && !options.force && !isJsonMode()) {
25470
+ clack30.log.warn(
25471
+ `${pc32.cyan("wraps/workflows/")} already contains ${tsFiles.length} workflow file(s). Use ${pc32.bold("--force")} to overwrite.`
25472
+ );
25473
+ }
25474
+ }
25475
+ progress.start("Creating wraps/workflows/ directory");
25476
+ await mkdir7(workflowsDir, { recursive: true });
25477
+ const configPath = join15(cwd, "wraps", "wraps.config.ts");
25478
+ if (existsSync14(configPath)) {
25479
+ const configContent = await readFile7(configPath, "utf-8");
25480
+ if (!configContent.includes("workflowsDir")) {
25481
+ const updated = configContent.replace(
25482
+ /}\);(\s*)$/,
25483
+ ` workflowsDir: './workflows',
25484
+ });$1`
25485
+ );
25486
+ if (updated !== configContent) {
25487
+ await writeFile9(configPath, updated, "utf-8");
25488
+ }
25489
+ }
25490
+ } else {
25491
+ await writeFile9(configPath, generateMinimalConfig(), "utf-8");
25492
+ }
25493
+ const filesCreated = [];
25494
+ if (!options.noExample) {
25495
+ const examplePath = join15(workflowsDir, "welcome.ts");
25496
+ if (!existsSync14(examplePath) || options.force) {
25497
+ await writeFile9(examplePath, EXAMPLE_WORKFLOW, "utf-8");
25498
+ filesCreated.push("wraps/workflows/welcome.ts");
25499
+ }
25500
+ }
25501
+ progress.succeed("Workflows directory ready");
25502
+ if (!options.noClaude) {
25503
+ try {
25504
+ progress.start("Scaffolding Claude Code context");
25505
+ await scaffoldClaudeMdSection({
25506
+ projectDir: cwd,
25507
+ sectionId: "workflows",
25508
+ sectionContent: WORKFLOWS_CLAUDE_MD_SECTION
25509
+ });
25510
+ filesCreated.push(".claude/CLAUDE.md");
25511
+ await scaffoldClaudeSkill({
25512
+ projectDir: cwd,
25513
+ skillName: "wraps-workflows",
25514
+ skillContent: WORKFLOWS_SKILL_CONTENT
25515
+ });
25516
+ filesCreated.push(".claude/skills/wraps-workflows/SKILL.md");
25517
+ progress.succeed("Claude Code context scaffolded");
25518
+ } catch (e) {
25519
+ const msg = e instanceof Error ? e.message : String(e);
25520
+ progress.info(
25521
+ `Could not scaffold .claude/ context \u2014 workflow files are still ready (${msg})`
25522
+ );
25523
+ }
25524
+ }
25525
+ trackCommand("email:workflows:init", {
25526
+ success: true,
25527
+ duration_ms: Date.now() - startTime
25528
+ });
25529
+ if (isJsonMode()) {
25530
+ jsonSuccess("email.workflows.init", {
25531
+ dir: "wraps/workflows",
25532
+ files: filesCreated
25533
+ });
25534
+ return;
25535
+ }
25536
+ console.log();
25537
+ clack30.log.success(pc32.green("Workflows as Code initialized!"));
25538
+ console.log();
25539
+ console.log(` ${pc32.dim("Directory:")} ${pc32.cyan("wraps/workflows/")}`);
25540
+ if (!options.noExample) {
25541
+ console.log(
25542
+ ` ${pc32.dim("Example:")} ${pc32.cyan("wraps/workflows/welcome.ts")}`
25543
+ );
25544
+ }
25545
+ if (!options.noClaude) {
25546
+ console.log(
25547
+ ` ${pc32.dim("AI Context:")} ${pc32.cyan(".claude/skills/wraps-workflows/")}`
25548
+ );
25549
+ }
25550
+ console.log();
25551
+ console.log(`${pc32.bold("Next steps:")}`);
25552
+ console.log(
25553
+ ` 1. Edit or create workflows in ${pc32.cyan("wraps/workflows/")}`
25554
+ );
25555
+ console.log(` 2. Validate: ${pc32.cyan("wraps email workflows validate")}`);
25556
+ console.log(` 3. Push: ${pc32.cyan("wraps email workflows push")}`);
25557
+ if (!options.noClaude) {
25558
+ console.log(" 4. Use Claude Code to generate workflows from descriptions");
25559
+ }
25560
+ console.log();
25561
+ }
25562
+ function generateMinimalConfig() {
25563
+ return `import { defineConfig } from '@wraps.dev/client';
25564
+
25565
+ export default defineConfig({
25566
+ org: 'my-org',
25567
+ // from: { email: 'hello@yourapp.com', name: 'My App' },
25568
+ // region: 'us-east-1',
25569
+ templatesDir: './templates',
25570
+ workflowsDir: './workflows',
25571
+ });
25572
+ `;
25573
+ }
25574
+
25575
+ // src/commands/email/workflows/push.ts
25576
+ init_esm_shims();
25577
+ init_events();
25578
+ import { existsSync as existsSync16 } from "fs";
25579
+ import { join as join17 } from "path";
25580
+ import * as clack31 from "@clack/prompts";
25581
+ import pc33 from "picocolors";
25582
+
25583
+ // src/utils/email/workflow-transform.ts
25584
+ init_esm_shims();
25585
+ function transformWorkflow(definition) {
25586
+ const steps = [];
25587
+ const transitions = [];
25588
+ const triggerStep = createTriggerStep(definition.trigger);
25589
+ steps.push(triggerStep);
25590
+ if (definition.steps.length > 0) {
25591
+ flattenSteps(definition.steps, steps, transitions, triggerStep.id, null);
25592
+ }
25593
+ assignPositions(steps, transitions);
25594
+ return {
25595
+ steps,
25596
+ transitions,
25597
+ triggerType: definition.trigger.type,
25598
+ triggerConfig: extractTriggerConfig(definition.trigger),
25599
+ settings: definition.settings,
25600
+ defaults: definition.defaults
25601
+ };
25602
+ }
25603
+ function createTriggerStep(trigger) {
25604
+ const triggerConfig = {
25605
+ type: "trigger",
25606
+ triggerType: trigger.type,
25607
+ ...extractTriggerConfig(trigger)
25608
+ };
25609
+ return {
25610
+ id: "trigger",
25611
+ type: "trigger",
25612
+ name: getTriggerName(trigger.type),
25613
+ position: { x: 0, y: 0 },
25614
+ // Will be updated by assignPositions
25615
+ config: triggerConfig
25616
+ };
25617
+ }
25618
+ function extractTriggerConfig(trigger) {
25619
+ const config2 = {};
25620
+ if (trigger.eventName) {
25621
+ config2.eventName = trigger.eventName;
25622
+ }
25623
+ if (trigger.segmentId) {
25624
+ config2.segmentId = trigger.segmentId;
25625
+ }
25626
+ if (trigger.schedule) {
25627
+ config2.schedule = trigger.schedule;
25628
+ }
25629
+ if (trigger.timezone) {
25630
+ config2.timezone = trigger.timezone;
25631
+ }
25632
+ if (trigger.topicId) {
25633
+ config2.topicId = trigger.topicId;
25634
+ }
25635
+ return config2;
25636
+ }
25637
+ function getTriggerName(type) {
25638
+ const names = {
25639
+ event: "When event occurs",
25640
+ contact_created: "When contact is created",
25641
+ contact_updated: "When contact is updated",
25642
+ segment_entry: "When contact enters segment",
25643
+ segment_exit: "When contact exits segment",
25644
+ schedule: "On schedule",
25645
+ api: "When triggered via API",
25646
+ topic_subscribed: "When contact subscribes to topic",
25647
+ topic_unsubscribed: "When contact unsubscribes from topic"
25648
+ };
25649
+ return names[type] || "Trigger";
25650
+ }
25651
+ function flattenSteps(stepDefs, steps, transitions, fromStepId, branch) {
25652
+ let prevIds = [fromStepId];
25653
+ let firstStepInBranch = true;
25654
+ for (const def of stepDefs) {
25655
+ const step = toWorkflowStep(def);
25656
+ steps.push(step);
25657
+ for (const prevId of prevIds) {
25658
+ const transition = {
25659
+ id: `t-${prevId}-${step.id}`,
25660
+ fromStepId: prevId,
25661
+ toStepId: step.id
25662
+ };
25663
+ if (firstStepInBranch && branch && prevId === fromStepId) {
25664
+ transition.condition = { branch };
25665
+ }
25666
+ transitions.push(transition);
25667
+ }
25668
+ firstStepInBranch = false;
25669
+ if (def.type === "condition" && def.branches) {
25670
+ const leafIds = [];
25671
+ if (def.branches.yes && def.branches.yes.length > 0) {
25672
+ const yesLeaves = flattenSteps(
25673
+ def.branches.yes,
25674
+ steps,
25675
+ transitions,
25676
+ step.id,
25677
+ "yes"
25678
+ );
25679
+ leafIds.push(...yesLeaves);
25680
+ } else {
25681
+ leafIds.push(step.id);
25682
+ }
25683
+ if (def.branches.no && def.branches.no.length > 0) {
25684
+ const noLeaves = flattenSteps(
25685
+ def.branches.no,
25686
+ steps,
25687
+ transitions,
25688
+ step.id,
25689
+ "no"
25690
+ );
25691
+ leafIds.push(...noLeaves);
25692
+ } else if (!leafIds.includes(step.id)) {
25693
+ leafIds.push(step.id);
25694
+ }
25695
+ prevIds = leafIds;
25696
+ continue;
25697
+ }
25698
+ if (def.type === "exit") {
25699
+ prevIds = [];
25700
+ continue;
25701
+ }
25702
+ prevIds = [step.id];
25703
+ }
25704
+ return prevIds;
25705
+ }
25706
+ function toWorkflowStep(def) {
25707
+ const step = {
25708
+ id: def.id,
25709
+ type: def.type,
25710
+ name: def.name || getDefaultStepName(def.type),
25711
+ position: { x: 0, y: 0 },
25712
+ // Will be updated by assignPositions
25713
+ config: def.config
25714
+ };
25715
+ if (def.cascadeGroupId) {
25716
+ step.cascadeGroupId = def.cascadeGroupId;
25717
+ }
25718
+ return step;
25719
+ }
25720
+ function getDefaultStepName(type) {
25721
+ const names = {
25722
+ send_email: "Send Email",
25723
+ send_sms: "Send SMS",
25724
+ delay: "Wait",
25725
+ exit: "Exit",
25726
+ condition: "Condition",
25727
+ webhook: "Webhook",
25728
+ update_contact: "Update Contact",
25729
+ wait_for_event: "Wait for Event",
25730
+ wait_for_email_engagement: "Wait for Email Engagement",
25731
+ subscribe_topic: "Subscribe to Topic",
25732
+ unsubscribe_topic: "Unsubscribe from Topic"
25733
+ };
25734
+ return names[type] || type;
25735
+ }
25736
+ function assignPositions(steps, transitions) {
25737
+ const LEVEL_HEIGHT = 200;
25738
+ const BRANCH_OFFSET = 300;
25739
+ const childrenMap = /* @__PURE__ */ new Map();
25740
+ for (const t of transitions) {
25741
+ if (!childrenMap.has(t.fromStepId)) {
25742
+ childrenMap.set(t.fromStepId, []);
25743
+ }
25744
+ childrenMap.get(t.fromStepId)?.push({
25745
+ id: t.toStepId,
25746
+ branch: t.condition?.branch
25747
+ });
25748
+ }
25749
+ const visited = /* @__PURE__ */ new Set();
25750
+ const queue = [];
25751
+ const triggerStep = steps.find((s) => s.type === "trigger");
25752
+ if (triggerStep) {
25753
+ queue.push({ stepId: triggerStep.id, level: 0, xOffset: 0 });
25754
+ }
25755
+ while (queue.length > 0) {
25756
+ const { stepId, level, xOffset } = queue.shift();
25757
+ if (visited.has(stepId)) {
25758
+ continue;
25759
+ }
25760
+ visited.add(stepId);
25761
+ const step = steps.find((s) => s.id === stepId);
25762
+ if (step) {
25763
+ step.position = {
25764
+ x: xOffset,
25765
+ y: level * LEVEL_HEIGHT
25766
+ };
25767
+ }
25768
+ const children = childrenMap.get(stepId) || [];
25769
+ for (const child of children) {
25770
+ if (!visited.has(child.id)) {
25771
+ let childXOffset = xOffset;
25772
+ if (child.branch === "yes") {
25773
+ childXOffset = xOffset - BRANCH_OFFSET;
25774
+ } else if (child.branch === "no") {
25775
+ childXOffset = xOffset + BRANCH_OFFSET;
25776
+ }
25777
+ queue.push({
25778
+ stepId: child.id,
25779
+ level: level + 1,
25780
+ xOffset: childXOffset
25781
+ });
25782
+ }
25783
+ }
25784
+ }
25785
+ for (const step of steps) {
25786
+ if (!visited.has(step.id)) {
25787
+ step.position = {
25788
+ x: 600,
25789
+ y: steps.indexOf(step) * LEVEL_HEIGHT
25790
+ };
25791
+ }
25792
+ }
25793
+ }
25794
+
25795
+ // src/utils/email/workflow-ts.ts
25796
+ init_esm_shims();
25797
+ import { createHash as createHash2 } from "crypto";
25798
+ import { existsSync as existsSync15 } from "fs";
25799
+ import { mkdir as mkdir8, readdir as readdir3, readFile as readFile8, writeFile as writeFile10 } from "fs/promises";
25800
+ import { basename, join as join16 } from "path";
25801
+ async function discoverWorkflows(dir, filter) {
25802
+ if (!existsSync15(dir)) {
25803
+ return [];
25804
+ }
25805
+ const entries = await readdir3(dir);
25806
+ const workflows = entries.filter(
25807
+ (f) => (
25808
+ // Include .ts files only (not .tsx for workflows)
25809
+ f.endsWith(".ts") && // Exclude private/helper files starting with _
25810
+ !f.startsWith("_") && // Exclude type definition files
25811
+ !f.endsWith(".d.ts")
25812
+ )
25813
+ );
25814
+ if (filter) {
25815
+ const slug = filter.replace(/\.ts$/, "");
25816
+ return workflows.filter((f) => f.replace(/\.ts$/, "") === slug);
25817
+ }
25818
+ return workflows;
25819
+ }
25820
+ async function parseWorkflowTs(filePath, wrapsDir) {
25821
+ const { build: build2 } = await import("esbuild");
25822
+ const source = await readFile8(filePath, "utf-8");
25823
+ const sourceHash = createHash2("sha256").update(source).digest("hex");
25824
+ const slug = basename(filePath, ".ts");
25825
+ const shimDir = join16(wrapsDir, ".wraps", "_shims");
25826
+ await mkdir8(shimDir, { recursive: true });
25827
+ const clientShimContent = `
25828
+ // Identity functions for workflow definitions
25829
+ export const defineWorkflow = (def) => def;
25830
+
25831
+ // Step helper functions - they just create step definition objects
25832
+ export const sendEmail = (id, config) => ({
25833
+ id,
25834
+ type: 'send_email',
25835
+ name: config.name ?? \`Send email: \${config.template || 'custom'}\`,
25836
+ config: { type: 'send_email', ...config },
25837
+ });
25838
+
25839
+ export const sendSms = (id, config) => ({
25840
+ id,
25841
+ type: 'send_sms',
25842
+ name: config.name ?? \`Send SMS: \${config.template || 'custom'}\`,
25843
+ config: { type: 'send_sms', ...config },
25844
+ });
25845
+
25846
+ export const delay = (id, duration) => {
25847
+ const { name, ...durationConfig } = duration;
25848
+ const normalized = normalizeDuration(durationConfig);
25849
+ return {
25850
+ id,
25851
+ type: 'delay',
25852
+ name: name ?? \`Wait \${normalized.amount} \${normalized.unit}\`,
25853
+ config: { type: 'delay', ...normalized },
25854
+ };
25855
+ };
25856
+
25857
+ export const condition = (id, config) => {
25858
+ const { branches, name, ...conditionConfig } = config;
25859
+ return {
25860
+ id,
25861
+ type: 'condition',
25862
+ name: name ?? \`Check: \${config.field} \${config.operator}\`,
25863
+ config: { type: 'condition', ...conditionConfig },
25864
+ branches,
25865
+ };
25866
+ };
25867
+
25868
+ export const waitForEvent = (id, config) => {
25869
+ const { name, timeout, ...eventConfig } = config;
25870
+ return {
25871
+ id,
25872
+ type: 'wait_for_event',
25873
+ name: name ?? \`Wait for: \${config.eventName}\`,
25874
+ config: {
25875
+ type: 'wait_for_event',
25876
+ eventName: eventConfig.eventName,
25877
+ timeoutSeconds: durationToSeconds(timeout),
25878
+ },
25879
+ };
25880
+ };
25881
+
25882
+ export const waitForEmailEngagement = (id, config) => {
25883
+ const { name, timeout, emailStepId, engagementType } = config;
25884
+ return {
25885
+ id,
25886
+ type: 'wait_for_email_engagement',
25887
+ name: name ?? \`Wait for email \${engagementType}: \${emailStepId}\`,
25888
+ config: {
25889
+ type: 'wait_for_email_engagement',
25890
+ timeoutSeconds: durationToSeconds(timeout),
25891
+ },
25892
+ };
25893
+ };
25894
+
25895
+ export const exit = (id, config) => {
25896
+ const { name, ...exitConfig } = config ?? {};
25897
+ return {
25898
+ id,
25899
+ type: 'exit',
25900
+ name: name ?? 'Exit',
25901
+ config: { type: 'exit', ...exitConfig },
25902
+ };
25903
+ };
25904
+
25905
+ export const updateContact = (id, config) => {
25906
+ const { name, ...updateConfig } = config;
25907
+ return {
25908
+ id,
25909
+ type: 'update_contact',
25910
+ name: name ?? 'Update contact',
25911
+ config: { type: 'update_contact', ...updateConfig },
25912
+ };
25913
+ };
25914
+
25915
+ export const subscribeTopic = (id, config) => {
25916
+ const { name, ...topicConfig } = config;
25917
+ return {
25918
+ id,
25919
+ type: 'subscribe_topic',
25920
+ name: name ?? \`Subscribe to topic: \${config.topicId}\`,
25921
+ config: { type: 'subscribe_topic', ...topicConfig },
25922
+ };
25923
+ };
25924
+
25925
+ export const unsubscribeTopic = (id, config) => {
25926
+ const { name, ...topicConfig } = config;
25927
+ return {
25928
+ id,
25929
+ type: 'unsubscribe_topic',
25930
+ name: name ?? \`Unsubscribe from topic: \${config.topicId}\`,
25931
+ config: { type: 'unsubscribe_topic', ...topicConfig },
25932
+ };
25933
+ };
25934
+
25935
+ export const webhook = (id, config) => {
25936
+ const { name, ...webhookConfig } = config;
25937
+ return {
25938
+ id,
25939
+ type: 'webhook',
25940
+ name: name ?? \`Webhook: \${config.url}\`,
25941
+ config: { type: 'webhook', method: 'POST', ...webhookConfig },
25942
+ };
25943
+ };
25944
+
25945
+ /**
25946
+ * cascade(id, config) \u2014 expand a cross-channel cascade into primitive steps.
25947
+ *
25948
+ * For each email channel (except the last), we emit:
25949
+ * send_email \u2192 wait_for_email_engagement \u2192 condition (engaged?)
25950
+ * with the condition's "yes" branch containing an exit node and the "no"
25951
+ * branch falling through to the next channel.
25952
+ *
25953
+ * Non-email channels (SMS) emit only a send step.
25954
+ *
25955
+ * Every generated step carries cascadeGroupId = id so the execution
25956
+ * engine can scope engagement queries to the correct group.
25957
+ */
25958
+ export const cascade = (id, config) => {
25959
+ const channels = config.channels || [];
25960
+ const steps = [];
25961
+
25962
+ for (let i = 0; i < channels.length; i++) {
25963
+ const channel = channels[i];
25964
+ const isLast = i === channels.length - 1;
25965
+
25966
+ if (channel.type === 'email') {
25967
+ // Send email step
25968
+ steps.push({
25969
+ id: id + '-send-' + i,
25970
+ type: 'send_email',
25971
+ name: 'Cascade: send ' + (channel.template || 'email'),
25972
+ config: { type: 'send_email', templateId: channel.template },
25973
+ cascadeGroupId: id,
25974
+ });
25975
+
25976
+ // If not last channel, add wait + condition
25977
+ if (!isLast && channel.waitFor) {
25978
+ const waitSeconds = durationToSeconds(channel.waitFor) || 259200;
25979
+ const waitId = id + '-wait-' + i;
25980
+ const condId = id + '-cond-' + i;
25981
+ const exitId = id + '-exit-' + i;
25982
+
25983
+ // Wait for engagement step
25984
+ steps.push({
25985
+ id: waitId,
25986
+ type: 'wait_for_email_engagement',
25987
+ name: 'Cascade: wait for ' + (channel.engagement || 'opened'),
25988
+ config: { type: 'wait_for_email_engagement', timeoutSeconds: waitSeconds },
25989
+ cascadeGroupId: id,
25990
+ });
25991
+
25992
+ // Condition step: check engagement.status
25993
+ steps.push({
25994
+ id: condId,
25995
+ type: 'condition',
25996
+ name: 'Cascade: email engaged?',
25997
+ config: {
25998
+ type: 'condition',
25999
+ field: 'engagement.status',
26000
+ operator: 'equals',
26001
+ value: 'true',
26002
+ },
26003
+ cascadeGroupId: id,
26004
+ branches: {
26005
+ yes: [{
26006
+ id: exitId,
26007
+ type: 'exit',
26008
+ name: 'Exit',
26009
+ config: { type: 'exit', reason: 'Engaged via email' },
26010
+ cascadeGroupId: id,
26011
+ }],
26012
+ },
26013
+ });
26014
+ }
26015
+ } else if (channel.type === 'sms') {
26016
+ // Send SMS step
26017
+ steps.push({
26018
+ id: id + '-send-' + i,
26019
+ type: 'send_sms',
26020
+ name: 'Cascade: send ' + (channel.template || 'sms'),
26021
+ config: { type: 'send_sms', template: channel.template, body: channel.body },
26022
+ cascadeGroupId: id,
26023
+ });
26024
+ }
26025
+ }
26026
+
26027
+ return steps;
26028
+ };
26422
26029
 
26423
- ## Key Commands
26030
+ // Internal helpers
26031
+ function normalizeDuration(duration) {
26032
+ if (duration.days !== undefined) {
26033
+ return { amount: duration.days, unit: 'days' };
26034
+ }
26035
+ if (duration.hours !== undefined) {
26036
+ return { amount: duration.hours, unit: 'hours' };
26037
+ }
26038
+ if (duration.minutes !== undefined) {
26039
+ return { amount: duration.minutes, unit: 'minutes' };
26040
+ }
26041
+ return { amount: 1, unit: 'hours' };
26042
+ }
26424
26043
 
26425
- \`\`\`bash
26426
- wraps email workflows validate # Validate all workflow files
26427
- wraps email workflows validate --workflow welcome # Validate a specific workflow
26428
- wraps email workflows push # Push workflows to dashboard
26429
- wraps email workflows generate --template welcome # Generate from built-in template
26430
- wraps email workflows generate "description..." # Generate from AI description
26431
- \`\`\`
26044
+ function durationToSeconds(duration) {
26045
+ if (!duration) return undefined;
26046
+ let seconds = 0;
26047
+ if (duration.days) seconds += duration.days * 24 * 60 * 60;
26048
+ if (duration.hours) seconds += duration.hours * 60 * 60;
26049
+ if (duration.minutes) seconds += duration.minutes * 60;
26050
+ return seconds > 0 ? seconds : undefined;
26051
+ }
26432
26052
  `;
26053
+ await writeFile10(
26054
+ join16(shimDir, "wraps-client-shim.mjs"),
26055
+ clientShimContent,
26056
+ "utf-8"
26057
+ );
26058
+ const result = await build2({
26059
+ entryPoints: [filePath],
26060
+ bundle: true,
26061
+ write: false,
26062
+ format: "esm",
26063
+ platform: "node",
26064
+ target: "node20",
26065
+ alias: {
26066
+ "@wraps.dev/client": join16(shimDir, "wraps-client-shim.mjs")
26067
+ }
26068
+ });
26069
+ const bundledCode = result.outputFiles[0].text;
26070
+ const tmpDir = join16(wrapsDir, ".wraps", "_workflows");
26071
+ await mkdir8(tmpDir, { recursive: true });
26072
+ const tmpPath = join16(tmpDir, `${slug}.mjs`);
26073
+ await writeFile10(tmpPath, bundledCode, "utf-8");
26074
+ const mod = await import(`${tmpPath}?t=${Date.now()}`);
26075
+ const definition = mod.default;
26076
+ if (!definition || typeof definition !== "object") {
26077
+ throw new Error(
26078
+ "Workflow must have a default export (workflow definition from defineWorkflow())"
26079
+ );
26080
+ }
26081
+ if (!definition.name) {
26082
+ throw new Error("Workflow definition must have a 'name' property");
26083
+ }
26084
+ if (!definition.trigger) {
26085
+ throw new Error("Workflow definition must have a 'trigger' property");
26086
+ }
26087
+ if (!Array.isArray(definition.steps)) {
26088
+ throw new Error("Workflow definition must have a 'steps' array");
26089
+ }
26090
+ return {
26091
+ slug,
26092
+ filePath,
26093
+ source,
26094
+ sourceHash,
26095
+ definition,
26096
+ cliProjectPath: `workflows/${slug}.ts`
26097
+ };
26098
+ }
26433
26099
 
26434
- // src/commands/email/workflows/init.ts
26435
- var EXAMPLE_WORKFLOW = `import {
26436
- defineWorkflow,
26437
- sendEmail,
26438
- delay,
26439
- condition,
26440
- exit,
26441
- } from '@wraps.dev/client';
26442
-
26443
- /**
26444
- * Welcome Sequence
26445
- *
26446
- * Send a welcome email when a contact is created,
26447
- * wait 1 day, then check if they activated.
26448
- * If not, send a follow-up with tips.
26449
- */
26450
- export default defineWorkflow({
26451
- name: 'Welcome Sequence',
26452
- trigger: {
26453
- type: 'contact_created',
26454
- },
26455
-
26456
- steps: [
26457
- sendEmail('send-welcome', { template: 'welcome-email' }),
26458
- delay('wait-1-day', { days: 1 }),
26459
- condition('check-activated', {
26460
- field: 'contact.hasActivated',
26461
- operator: 'equals',
26462
- value: true,
26463
- branches: {
26464
- yes: [exit('already-active')],
26465
- no: [
26466
- sendEmail('send-tips', { template: 'getting-started-tips' }),
26467
- ],
26468
- },
26469
- }),
26470
- ],
26471
- });
26472
- `;
26473
- async function workflowsInit(options) {
26474
- const startTime = Date.now();
26475
- const cwd = process.cwd();
26476
- const workflowsDir = join16(cwd, "wraps", "workflows");
26477
- if (!isJsonMode()) {
26478
- clack30.intro(pc32.bold("Workflows as Code"));
26100
+ // src/utils/email/workflow-validator.ts
26101
+ init_esm_shims();
26102
+ function validateTransformedWorkflow(transformed, localTemplateSlugs) {
26103
+ const errors2 = [];
26104
+ errors2.push(...validateStructure(transformed.steps, transformed.transitions));
26105
+ for (const step of transformed.steps) {
26106
+ errors2.push(...validateStep(step));
26479
26107
  }
26480
- const progress = new DeploymentProgress();
26481
- if (existsSync15(workflowsDir) && !options.force) {
26482
- const { readdir: readdir4 } = await import("fs/promises");
26483
- const files = await readdir4(workflowsDir);
26484
- const tsFiles = files.filter(
26485
- (f) => f.endsWith(".ts") && !f.startsWith("_")
26108
+ if (localTemplateSlugs) {
26109
+ errors2.push(
26110
+ ...validateTemplateReferences(transformed.steps, localTemplateSlugs)
26486
26111
  );
26487
- if (tsFiles.length > 0 && !options.force && !isJsonMode()) {
26488
- clack30.log.warn(
26489
- `${pc32.cyan("wraps/workflows/")} already contains ${tsFiles.length} workflow file(s). Use ${pc32.bold("--force")} to overwrite.`
26490
- );
26491
- }
26492
26112
  }
26493
- progress.start("Creating wraps/workflows/ directory");
26494
- await mkdir8(workflowsDir, { recursive: true });
26495
- const configPath = join16(cwd, "wraps", "wraps.config.ts");
26496
- if (existsSync15(configPath)) {
26497
- const configContent = await readFile8(configPath, "utf-8");
26498
- if (!configContent.includes("workflowsDir")) {
26499
- const updated = configContent.replace(
26500
- /}\);(\s*)$/,
26501
- ` workflowsDir: './workflows',
26502
- });$1`
26503
- );
26504
- if (updated !== configContent) {
26505
- await writeFile10(configPath, updated, "utf-8");
26506
- }
26113
+ const errorsByNodeId = /* @__PURE__ */ new Map();
26114
+ for (const error of errors2) {
26115
+ if (error.nodeId) {
26116
+ const existing = errorsByNodeId.get(error.nodeId) || [];
26117
+ existing.push(error);
26118
+ errorsByNodeId.set(error.nodeId, existing);
26507
26119
  }
26508
- } else {
26509
- await writeFile10(configPath, generateMinimalConfig(), "utf-8");
26510
26120
  }
26511
- const filesCreated = [];
26512
- if (!options.noExample) {
26513
- const examplePath = join16(workflowsDir, "welcome.ts");
26514
- if (!existsSync15(examplePath) || options.force) {
26515
- await writeFile10(examplePath, EXAMPLE_WORKFLOW, "utf-8");
26516
- filesCreated.push("wraps/workflows/welcome.ts");
26121
+ return {
26122
+ isValid: errors2.filter((e) => e.severity === "error").length === 0,
26123
+ errors: errors2,
26124
+ errorsByNodeId
26125
+ };
26126
+ }
26127
+ function validateStructure(steps, transitions) {
26128
+ const errors2 = [];
26129
+ const triggerSteps = steps.filter((s) => s.type === "trigger");
26130
+ if (triggerSteps.length === 0) {
26131
+ errors2.push({
26132
+ message: "Workflow must have a trigger node",
26133
+ severity: "error"
26134
+ });
26135
+ } else if (triggerSteps.length > 1) {
26136
+ errors2.push({
26137
+ message: "Workflow can only have one trigger node",
26138
+ severity: "error"
26139
+ });
26140
+ }
26141
+ const actionSteps = steps.filter(
26142
+ (s) => s.type !== "trigger" && s.type !== "exit"
26143
+ );
26144
+ if (actionSteps.length === 0 && steps.length > 1) {
26145
+ errors2.push({
26146
+ message: "Workflow must have at least one action step",
26147
+ severity: "error"
26148
+ });
26149
+ }
26150
+ if (triggerSteps.length === 1) {
26151
+ const reachableIds = getReachableNodeIds(triggerSteps[0].id, transitions);
26152
+ const orphanSteps = steps.filter(
26153
+ (s) => s.type !== "trigger" && !reachableIds.has(s.id)
26154
+ );
26155
+ for (const orphan of orphanSteps) {
26156
+ errors2.push({
26157
+ nodeId: orphan.id,
26158
+ message: `"${orphan.name}" is not connected to the workflow`,
26159
+ severity: "warning"
26160
+ });
26517
26161
  }
26518
26162
  }
26519
- progress.succeed("Workflows directory ready");
26520
- if (!options.noClaude) {
26521
- try {
26522
- progress.start("Scaffolding Claude Code context");
26523
- await scaffoldClaudeMdSection({
26524
- projectDir: cwd,
26525
- sectionId: "workflows",
26526
- sectionContent: WORKFLOWS_CLAUDE_MD_SECTION
26163
+ const stepIds = new Set(steps.map((s) => s.id));
26164
+ for (const transition of transitions) {
26165
+ if (!stepIds.has(transition.fromStepId)) {
26166
+ errors2.push({
26167
+ message: `Transition references non-existent source step: ${transition.fromStepId}`,
26168
+ severity: "error"
26527
26169
  });
26528
- filesCreated.push(".claude/CLAUDE.md");
26529
- await scaffoldClaudeSkill({
26530
- projectDir: cwd,
26531
- skillName: "wraps-workflows",
26532
- skillContent: WORKFLOWS_SKILL_CONTENT
26170
+ }
26171
+ if (!stepIds.has(transition.toStepId)) {
26172
+ errors2.push({
26173
+ message: `Transition references non-existent target step: ${transition.toStepId}`,
26174
+ severity: "error"
26533
26175
  });
26534
- filesCreated.push(".claude/skills/wraps-workflows/SKILL.md");
26535
- progress.succeed("Claude Code context scaffolded");
26536
- } catch (e) {
26537
- const msg = e instanceof Error ? e.message : String(e);
26538
- progress.info(
26539
- `Could not scaffold .claude/ context \u2014 workflow files are still ready (${msg})`
26540
- );
26541
26176
  }
26542
26177
  }
26543
- trackCommand("email:workflows:init", {
26544
- success: true,
26545
- duration_ms: Date.now() - startTime
26546
- });
26547
- if (isJsonMode()) {
26548
- jsonSuccess("email.workflows.init", {
26549
- dir: "wraps/workflows",
26550
- files: filesCreated
26178
+ return errors2;
26179
+ }
26180
+ function getReachableNodeIds(startId, transitions) {
26181
+ const reachable = /* @__PURE__ */ new Set();
26182
+ const queue = [startId];
26183
+ while (queue.length > 0) {
26184
+ const currentId = queue.shift();
26185
+ if (reachable.has(currentId)) {
26186
+ continue;
26187
+ }
26188
+ reachable.add(currentId);
26189
+ const outgoing = transitions.filter((t) => t.fromStepId === currentId);
26190
+ for (const t of outgoing) {
26191
+ if (!reachable.has(t.toStepId)) {
26192
+ queue.push(t.toStepId);
26193
+ }
26194
+ }
26195
+ }
26196
+ return reachable;
26197
+ }
26198
+ function validateStep(step) {
26199
+ const errors2 = [];
26200
+ const config2 = step.config;
26201
+ const configType = config2.type;
26202
+ switch (configType) {
26203
+ case "trigger":
26204
+ errors2.push(...validateTrigger(step.id, config2));
26205
+ break;
26206
+ case "send_email":
26207
+ errors2.push(...validateSendEmail(step.id, config2));
26208
+ break;
26209
+ case "condition":
26210
+ errors2.push(...validateCondition(step.id, config2));
26211
+ break;
26212
+ case "webhook":
26213
+ errors2.push(...validateWebhook(step.id, config2));
26214
+ break;
26215
+ case "subscribe_topic":
26216
+ case "unsubscribe_topic":
26217
+ errors2.push(...validateTopic(step.id, config2));
26218
+ break;
26219
+ case "wait_for_event":
26220
+ errors2.push(...validateWaitForEvent(step.id, config2));
26221
+ break;
26222
+ case "delay":
26223
+ errors2.push(...validateDelay(step.id, config2));
26224
+ break;
26225
+ }
26226
+ return errors2;
26227
+ }
26228
+ function validateTrigger(nodeId, config2) {
26229
+ const errors2 = [];
26230
+ switch (config2.triggerType) {
26231
+ case "event":
26232
+ if (!config2.eventName) {
26233
+ errors2.push({
26234
+ nodeId,
26235
+ field: "eventName",
26236
+ message: "Event name is required",
26237
+ severity: "error"
26238
+ });
26239
+ }
26240
+ break;
26241
+ case "segment_entry":
26242
+ case "segment_exit":
26243
+ if (!config2.segmentId) {
26244
+ errors2.push({
26245
+ nodeId,
26246
+ field: "segmentId",
26247
+ message: "Segment is required",
26248
+ severity: "error"
26249
+ });
26250
+ }
26251
+ break;
26252
+ case "topic_subscribed":
26253
+ case "topic_unsubscribed":
26254
+ if (!config2.topicId) {
26255
+ errors2.push({
26256
+ nodeId,
26257
+ field: "topicId",
26258
+ message: "Topic is required",
26259
+ severity: "error"
26260
+ });
26261
+ }
26262
+ break;
26263
+ case "schedule":
26264
+ if (!config2.schedule) {
26265
+ errors2.push({
26266
+ nodeId,
26267
+ field: "schedule",
26268
+ message: "Schedule (cron expression) is required",
26269
+ severity: "error"
26270
+ });
26271
+ }
26272
+ if (!config2.timezone) {
26273
+ errors2.push({
26274
+ nodeId,
26275
+ field: "timezone",
26276
+ message: "Timezone is required",
26277
+ severity: "error"
26278
+ });
26279
+ }
26280
+ break;
26281
+ }
26282
+ return errors2;
26283
+ }
26284
+ function validateSendEmail(nodeId, config2) {
26285
+ const errors2 = [];
26286
+ const templateRef = config2.templateId || config2.template;
26287
+ if (!templateRef) {
26288
+ errors2.push({
26289
+ nodeId,
26290
+ field: "templateId",
26291
+ message: "Email template is required",
26292
+ severity: "error"
26551
26293
  });
26552
- return;
26553
26294
  }
26554
- console.log();
26555
- clack30.log.success(pc32.green("Workflows as Code initialized!"));
26556
- console.log();
26557
- console.log(` ${pc32.dim("Directory:")} ${pc32.cyan("wraps/workflows/")}`);
26558
- if (!options.noExample) {
26559
- console.log(
26560
- ` ${pc32.dim("Example:")} ${pc32.cyan("wraps/workflows/welcome.ts")}`
26561
- );
26295
+ return errors2;
26296
+ }
26297
+ function validateCondition(nodeId, config2) {
26298
+ const errors2 = [];
26299
+ if (!config2.field) {
26300
+ errors2.push({
26301
+ nodeId,
26302
+ field: "field",
26303
+ message: "Condition field is required",
26304
+ severity: "error"
26305
+ });
26562
26306
  }
26563
- if (!options.noClaude) {
26564
- console.log(
26565
- ` ${pc32.dim("AI Context:")} ${pc32.cyan(".claude/skills/wraps-workflows/")}`
26566
- );
26307
+ if (!config2.operator) {
26308
+ errors2.push({
26309
+ nodeId,
26310
+ field: "operator",
26311
+ message: "Condition operator is required",
26312
+ severity: "error"
26313
+ });
26567
26314
  }
26568
- console.log();
26569
- console.log(`${pc32.bold("Next steps:")}`);
26570
- console.log(
26571
- ` 1. Edit or create workflows in ${pc32.cyan("wraps/workflows/")}`
26572
- );
26573
- console.log(` 2. Validate: ${pc32.cyan("wraps email workflows validate")}`);
26574
- console.log(` 3. Push: ${pc32.cyan("wraps email workflows push")}`);
26575
- if (!options.noClaude) {
26576
- console.log(" 4. Use Claude Code to generate workflows from descriptions");
26315
+ if (config2.operator !== "is_set" && config2.operator !== "is_not_set" && config2.operator !== "is_true" && config2.operator !== "is_false" && (config2.value === void 0 || config2.value === "")) {
26316
+ errors2.push({
26317
+ nodeId,
26318
+ field: "value",
26319
+ message: "Condition value is required",
26320
+ severity: "error"
26321
+ });
26577
26322
  }
26578
- console.log();
26323
+ return errors2;
26579
26324
  }
26580
- function generateMinimalConfig() {
26581
- return `import { defineConfig } from '@wraps.dev/client';
26582
-
26583
- export default defineConfig({
26584
- org: 'my-org',
26585
- // from: { email: 'hello@yourapp.com', name: 'My App' },
26586
- // region: 'us-east-1',
26587
- templatesDir: './templates',
26588
- workflowsDir: './workflows',
26589
- });
26590
- `;
26325
+ function validateWebhook(nodeId, config2) {
26326
+ const errors2 = [];
26327
+ if (config2.url) {
26328
+ try {
26329
+ new URL(config2.url);
26330
+ } catch {
26331
+ errors2.push({
26332
+ nodeId,
26333
+ field: "url",
26334
+ message: "Invalid URL format",
26335
+ severity: "error"
26336
+ });
26337
+ }
26338
+ } else {
26339
+ errors2.push({
26340
+ nodeId,
26341
+ field: "url",
26342
+ message: "Webhook URL is required",
26343
+ severity: "error"
26344
+ });
26345
+ }
26346
+ return errors2;
26347
+ }
26348
+ function validateTopic(nodeId, config2) {
26349
+ const errors2 = [];
26350
+ if (!config2.topicId) {
26351
+ errors2.push({
26352
+ nodeId,
26353
+ field: "topicId",
26354
+ message: "Topic is required",
26355
+ severity: "error"
26356
+ });
26357
+ }
26358
+ return errors2;
26359
+ }
26360
+ function validateWaitForEvent(nodeId, config2) {
26361
+ const errors2 = [];
26362
+ if (!config2.eventName) {
26363
+ errors2.push({
26364
+ nodeId,
26365
+ field: "eventName",
26366
+ message: "Event name is required",
26367
+ severity: "error"
26368
+ });
26369
+ }
26370
+ return errors2;
26371
+ }
26372
+ function validateDelay(nodeId, config2) {
26373
+ const errors2 = [];
26374
+ const amount = config2.amount;
26375
+ if (!amount || amount < 1) {
26376
+ errors2.push({
26377
+ nodeId,
26378
+ field: "amount",
26379
+ message: "Delay duration must be at least 1",
26380
+ severity: "error"
26381
+ });
26382
+ }
26383
+ return errors2;
26384
+ }
26385
+ function validateTemplateReferences(steps, localTemplateSlugs) {
26386
+ const errors2 = [];
26387
+ for (const step of steps) {
26388
+ if (step.config.type === "send_email") {
26389
+ const config2 = step.config;
26390
+ const templateRef = config2.templateId || config2.template;
26391
+ if (templateRef && !localTemplateSlugs.has(templateRef)) {
26392
+ errors2.push({
26393
+ nodeId: step.id,
26394
+ field: "templateId",
26395
+ message: `Template "${templateRef}" not found in templates/ directory`,
26396
+ severity: "error"
26397
+ });
26398
+ }
26399
+ }
26400
+ if (step.config.type === "send_sms") {
26401
+ const config2 = step.config;
26402
+ const templateRef = config2.templateId || config2.template;
26403
+ if (templateRef && !localTemplateSlugs.has(templateRef)) {
26404
+ errors2.push({
26405
+ nodeId: step.id,
26406
+ field: "templateId",
26407
+ message: `SMS template "${templateRef}" not found in templates/ directory`,
26408
+ severity: "warning"
26409
+ // Warning for SMS since we might not have SMS templates yet
26410
+ });
26411
+ }
26412
+ }
26413
+ }
26414
+ return errors2;
26591
26415
  }
26592
26416
 
26593
26417
  // src/commands/email/workflows/push.ts
26594
- init_esm_shims();
26595
- init_events();
26596
- import { existsSync as existsSync16 } from "fs";
26597
- import { join as join17 } from "path";
26598
- import * as clack31 from "@clack/prompts";
26599
- import pc33 from "picocolors";
26600
26418
  init_config();
26601
26419
  init_errors();
26602
26420
  init_json_output();
@@ -27629,7 +27447,7 @@ import {
27629
27447
  IAMClient as IAMClient2,
27630
27448
  PutRolePolicyCommand
27631
27449
  } from "@aws-sdk/client-iam";
27632
- import { confirm as confirm15, intro as intro33, isCancel as isCancel21, log as log33, outro as outro19, select as select14 } from "@clack/prompts";
27450
+ import { confirm as confirm14, intro as intro33, isCancel as isCancel20, log as log33, outro as outro19, select as select14 } from "@clack/prompts";
27633
27451
  import * as pulumi21 from "@pulumi/pulumi";
27634
27452
  import pc37 from "picocolors";
27635
27453
  init_events();
@@ -27980,7 +27798,7 @@ async function resolveOrganization() {
27980
27798
  hint: org.slug
27981
27799
  }))
27982
27800
  });
27983
- if (isCancel21(selected)) {
27801
+ if (isCancel20(selected)) {
27984
27802
  outro19("Operation cancelled");
27985
27803
  process.exit(0);
27986
27804
  }
@@ -28042,11 +27860,11 @@ async function authenticatedConnect(token, options) {
28042
27860
  "Event tracking must be enabled to connect to the Wraps Platform."
28043
27861
  );
28044
27862
  }
28045
- const enableTracking = options.yes || await confirm15({
27863
+ const enableTracking = options.yes || await confirm14({
28046
27864
  message: "Enable event tracking now?",
28047
27865
  initialValue: true
28048
27866
  });
28049
- if (isCancel21(enableTracking) || !enableTracking) {
27867
+ if (isCancel20(enableTracking) || !enableTracking) {
28050
27868
  outro19("Platform connection cancelled.");
28051
27869
  process.exit(0);
28052
27870
  }
@@ -28236,11 +28054,11 @@ Run ${pc37.cyan("wraps email init")} or ${pc37.cyan("wraps sms init")} first.
28236
28054
  log33.info(
28237
28055
  "Enabling event tracking will allow SES events to be streamed to the dashboard."
28238
28056
  );
28239
- const enableEventTracking = await confirm15({
28057
+ const enableEventTracking = await confirm14({
28240
28058
  message: "Enable event tracking now?",
28241
28059
  initialValue: true
28242
28060
  });
28243
- if (isCancel21(enableEventTracking) || !enableEventTracking) {
28061
+ if (isCancel20(enableEventTracking) || !enableEventTracking) {
28244
28062
  outro19("Platform connection cancelled.");
28245
28063
  process.exit(0);
28246
28064
  }
@@ -28288,18 +28106,18 @@ Run ${pc37.cyan("wraps email init")} or ${pc37.cyan("wraps sms init")} first.
28288
28106
  }
28289
28107
  ]
28290
28108
  });
28291
- if (isCancel21(action)) {
28109
+ if (isCancel20(action)) {
28292
28110
  outro19("Operation cancelled");
28293
28111
  process.exit(0);
28294
28112
  }
28295
28113
  if (action === "keep") {
28296
28114
  webhookSecret = existingSecret;
28297
28115
  } else if (action === "disconnect") {
28298
- const confirmDisconnect = await confirm15({
28116
+ const confirmDisconnect = await confirm14({
28299
28117
  message: "Are you sure? Events will no longer be sent to the Wraps Platform.",
28300
28118
  initialValue: false
28301
28119
  });
28302
- if (isCancel21(confirmDisconnect) || !confirmDisconnect) {
28120
+ if (isCancel20(confirmDisconnect) || !confirmDisconnect) {
28303
28121
  outro19("Disconnect cancelled");
28304
28122
  process.exit(0);
28305
28123
  }
@@ -28510,7 +28328,7 @@ import {
28510
28328
  GetRoleCommand as GetRoleCommand2,
28511
28329
  IAMClient as IAMClient3
28512
28330
  } from "@aws-sdk/client-iam";
28513
- import { confirm as confirm16, intro as intro35, isCancel as isCancel22, log as log34, outro as outro20 } from "@clack/prompts";
28331
+ import { confirm as confirm15, intro as intro35, isCancel as isCancel21, log as log34, outro as outro20 } from "@clack/prompts";
28514
28332
  import pc39 from "picocolors";
28515
28333
  async function updateRole(options) {
28516
28334
  const startTime = Date.now();
@@ -28571,11 +28389,11 @@ Run ${pc39.cyan("wraps email init")} to deploy infrastructure first.
28571
28389
  if (!options.force) {
28572
28390
  progress.stop();
28573
28391
  const actionLabel = roleExists2 ? "Update" : "Create";
28574
- const shouldContinue = await confirm16({
28392
+ const shouldContinue = await confirm15({
28575
28393
  message: `${actionLabel} IAM role ${pc39.cyan(roleName)} with latest permissions?`,
28576
28394
  initialValue: true
28577
28395
  });
28578
- if (isCancel22(shouldContinue) || !shouldContinue) {
28396
+ if (isCancel21(shouldContinue) || !shouldContinue) {
28579
28397
  outro20(`${actionLabel} cancelled`);
28580
28398
  process.exit(0);
28581
28399
  }
@@ -36328,7 +36146,7 @@ function showHelp() {
36328
36146
  ` ${pc54.cyan("email workflows push")} Push workflows to dashboard`
36329
36147
  );
36330
36148
  console.log(
36331
- ` ${pc54.cyan("email workflows generate")} Generate workflow from template or AI
36149
+ ` ${pc54.cyan("email workflows generate")} Generate workflow from template
36332
36150
  `
36333
36151
  );
36334
36152
  console.log("SMS Commands:");
@@ -37106,14 +36924,10 @@ Available commands: ${pc54.cyan("init")}, ${pc54.cyan("push")}, ${pc54.cyan("pre
37106
36924
  break;
37107
36925
  case "generate":
37108
36926
  await workflowsGenerate({
37109
- description: args.sub[3],
37110
36927
  template: flags.template,
37111
36928
  name: flags.name,
37112
- dryRun: flags.dryRun,
37113
- yes: flags.yes,
37114
36929
  force: flags.force,
37115
- json: flags.json,
37116
- token: flags.token
36930
+ json: flags.json
37117
36931
  });
37118
36932
  break;
37119
36933
  default: