@wraps.dev/cli 2.14.2 → 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
  }
@@ -8969,8 +8957,8 @@ async function authStatus(_options = {}) {
8969
8957
  init_esm_shims();
8970
8958
  init_events();
8971
8959
  init_aws();
8972
- init_json_output();
8973
8960
  init_aws_detection();
8961
+ init_json_output();
8974
8962
  import * as clack5 from "@clack/prompts";
8975
8963
  import pc6 from "picocolors";
8976
8964
  async function runDiagnostics(state) {
@@ -9902,8 +9890,8 @@ init_events();
9902
9890
  init_route53();
9903
9891
  init_aws();
9904
9892
  init_errors();
9905
- init_json_output();
9906
9893
  init_fs();
9894
+ init_json_output();
9907
9895
  init_metadata();
9908
9896
  init_output();
9909
9897
  import * as clack9 from "@clack/prompts";
@@ -11158,8 +11146,8 @@ function validateConfig2(config2) {
11158
11146
  // src/commands/cdn/init.ts
11159
11147
  init_aws();
11160
11148
  init_errors();
11161
- init_json_output();
11162
11149
  init_fs();
11150
+ init_json_output();
11163
11151
  init_metadata();
11164
11152
  init_output();
11165
11153
  init_prompts();
@@ -12083,10 +12071,10 @@ import * as pulumi7 from "@pulumi/pulumi";
12083
12071
  import pc13 from "picocolors";
12084
12072
  init_client();
12085
12073
  init_events();
12086
- init_errors();
12087
- init_json_output();
12088
12074
  init_aws();
12075
+ init_errors();
12089
12076
  init_fs();
12077
+ init_json_output();
12090
12078
  init_metadata();
12091
12079
  init_output();
12092
12080
  async function cdnSync(options) {
@@ -12262,10 +12250,10 @@ import * as pulumi8 from "@pulumi/pulumi";
12262
12250
  import pc14 from "picocolors";
12263
12251
  init_client();
12264
12252
  init_events();
12265
- init_errors();
12266
- init_json_output();
12267
12253
  init_aws();
12254
+ init_errors();
12268
12255
  init_fs();
12256
+ init_json_output();
12269
12257
  init_metadata();
12270
12258
  init_output();
12271
12259
  async function cdnUpgrade(options) {
@@ -12273,7 +12261,9 @@ async function cdnUpgrade(options) {
12273
12261
  const progress = new DeploymentProgress();
12274
12262
  if (!isJsonMode()) {
12275
12263
  clack13.intro(
12276
- pc14.bold(options.preview ? "Wraps CDN Upgrade Preview" : "Wraps CDN Upgrade")
12264
+ pc14.bold(
12265
+ options.preview ? "Wraps CDN Upgrade Preview" : "Wraps CDN Upgrade"
12266
+ )
12277
12267
  );
12278
12268
  }
12279
12269
  const identity = await progress.execute(
@@ -12562,10 +12552,10 @@ ${pc14.green("")} ${pc14.bold("Upgrade complete!")}
12562
12552
  // src/commands/cdn/verify.ts
12563
12553
  init_esm_shims();
12564
12554
  init_events();
12565
- init_errors();
12566
- init_json_output();
12567
12555
  init_aws();
12556
+ init_errors();
12568
12557
  init_fs();
12558
+ init_json_output();
12569
12559
  init_metadata();
12570
12560
  init_output();
12571
12561
  import * as clack14 from "@clack/prompts";
@@ -16941,8 +16931,8 @@ async function deployEmailStack(config2) {
16941
16931
  init_events();
16942
16932
  init_aws();
16943
16933
  init_errors();
16944
- init_json_output();
16945
16934
  init_fs();
16935
+ init_json_output();
16946
16936
  init_metadata();
16947
16937
  init_output();
16948
16938
  async function config(options) {
@@ -17230,8 +17220,8 @@ init_events();
17230
17220
  init_presets();
17231
17221
  init_aws();
17232
17222
  init_errors();
17233
- init_json_output();
17234
17223
  init_fs();
17224
+ init_json_output();
17235
17225
  init_metadata();
17236
17226
  init_output();
17237
17227
  init_prompts();
@@ -17825,11 +17815,10 @@ ${pc18.dim("Example:")}`);
17825
17815
  init_esm_shims();
17826
17816
  init_events();
17827
17817
  init_route53();
17828
- init_errors();
17829
- init_json_output();
17830
17818
  init_aws();
17831
17819
  init_errors();
17832
17820
  init_fs();
17821
+ init_json_output();
17833
17822
  init_metadata();
17834
17823
  init_output();
17835
17824
  import * as clack18 from "@clack/prompts";
@@ -19234,8 +19223,8 @@ async function deleteReceiptRuleSet(region) {
19234
19223
  // src/commands/email/inbound.ts
19235
19224
  init_aws();
19236
19225
  init_errors();
19237
- init_json_output();
19238
19226
  init_fs();
19227
+ init_json_output();
19239
19228
  init_metadata();
19240
19229
  init_output();
19241
19230
  init_prompts();
@@ -19636,9 +19625,7 @@ Enable it: ${pc21.cyan("wraps email inbound init")}
19636
19625
  console.log();
19637
19626
  console.log(pc21.bold(" Inbound Email Configuration"));
19638
19627
  console.log();
19639
- console.log(
19640
- ` ${pc21.dim("Receiving domain:")} ${pc21.cyan(receivingDomain)}`
19641
- );
19628
+ console.log(` ${pc21.dim("Receiving domain:")} ${pc21.cyan(receivingDomain)}`);
19642
19629
  console.log(
19643
19630
  ` ${pc21.dim("S3 bucket:")} ${pc21.cyan(inbound.bucketName || "")}`
19644
19631
  );
@@ -19913,7 +19900,6 @@ init_costs();
19913
19900
  init_presets();
19914
19901
  init_aws();
19915
19902
  init_errors();
19916
- init_json_output();
19917
19903
  init_fs();
19918
19904
 
19919
19905
  // src/utils/shared/iam-check.ts
@@ -20060,6 +20046,7 @@ function formatDeniedActions(actions) {
20060
20046
  }
20061
20047
 
20062
20048
  // src/commands/email/init.ts
20049
+ init_json_output();
20063
20050
  init_metadata();
20064
20051
  init_output();
20065
20052
  init_prompts();
@@ -20661,8 +20648,8 @@ init_esm_shims();
20661
20648
  init_events();
20662
20649
  init_aws();
20663
20650
  init_errors();
20664
- init_json_output();
20665
20651
  init_fs();
20652
+ init_json_output();
20666
20653
  init_metadata();
20667
20654
  init_output();
20668
20655
  import * as clack24 from "@clack/prompts";
@@ -20865,8 +20852,8 @@ init_events();
20865
20852
  init_aws();
20866
20853
  init_errors();
20867
20854
  init_fs();
20868
- init_metadata();
20869
20855
  init_json_output();
20856
+ init_metadata();
20870
20857
  init_output();
20871
20858
  import * as clack25 from "@clack/prompts";
20872
20859
  import * as pulumi19 from "@pulumi/pulumi";
@@ -21075,7 +21062,8 @@ async function scaffoldClaudeSkill({
21075
21062
  const skillDir = join8(projectDir, ".claude", "skills", skillName);
21076
21063
  const skillPath = join8(skillDir, "SKILL.md");
21077
21064
  await mkdir2(skillDir, { recursive: true });
21078
- await writeFile4(skillPath, skillContent.trim() + "\n", "utf-8");
21065
+ await writeFile4(skillPath, `${skillContent.trim()}
21066
+ `, "utf-8");
21079
21067
  }
21080
21068
  function escapeRegex(str) {
21081
21069
  return str.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
@@ -21587,9 +21575,10 @@ wraps/.wraps/
21587
21575
  });
21588
21576
  claudeFiles.push(".claude/skills/wraps-templates/SKILL.md");
21589
21577
  progress.succeed("Claude Code context scaffolded");
21590
- } catch {
21578
+ } catch (e) {
21579
+ const msg = e instanceof Error ? e.message : String(e);
21591
21580
  progress.info(
21592
- "Could not scaffold .claude/ context \u2014 template files are still ready"
21581
+ `Could not scaffold .claude/ context \u2014 template files are still ready (${msg})`
21593
21582
  );
21594
21583
  }
21595
21584
  }
@@ -22809,8 +22798,8 @@ init_costs();
22809
22798
  init_presets();
22810
22799
  init_aws();
22811
22800
  init_errors();
22812
- init_json_output();
22813
22801
  init_fs();
22802
+ init_json_output();
22814
22803
  init_metadata();
22815
22804
  init_output();
22816
22805
  init_prompts();
@@ -24734,852 +24723,133 @@ ${pc30.green("\u2713")} ${pc30.bold("Upgrade complete!")}
24734
24723
  // src/commands/email/workflows/generate.ts
24735
24724
  init_esm_shims();
24736
24725
  init_events();
24737
- import { existsSync as existsSync14, mkdirSync as mkdirSync2, writeFileSync } from "fs";
24738
- import { join as join15 } from "path";
24739
- 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";
24740
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';
24741
24739
 
24742
- // src/utils/email/workflow-transform.ts
24743
- init_esm_shims();
24744
- function transformWorkflow(definition) {
24745
- const steps = [];
24746
- const transitions = [];
24747
- const triggerStep = createTriggerStep(definition.trigger);
24748
- steps.push(triggerStep);
24749
- if (definition.steps.length > 0) {
24750
- flattenSteps(definition.steps, steps, transitions, triggerStep.id, null);
24751
- }
24752
- assignPositions(steps, transitions);
24753
- return {
24754
- steps,
24755
- transitions,
24756
- triggerType: definition.trigger.type,
24757
- triggerConfig: extractTriggerConfig(definition.trigger),
24758
- settings: definition.settings,
24759
- defaults: definition.defaults
24760
- };
24761
- }
24762
- function createTriggerStep(trigger) {
24763
- const triggerConfig = {
24764
- type: "trigger",
24765
- triggerType: trigger.type,
24766
- ...extractTriggerConfig(trigger)
24767
- };
24768
- return {
24769
- id: "trigger",
24770
- type: "trigger",
24771
- name: getTriggerName(trigger.type),
24772
- position: { x: 0, y: 0 },
24773
- // Will be updated by assignPositions
24774
- config: triggerConfig
24775
- };
24776
- }
24777
- function extractTriggerConfig(trigger) {
24778
- const config2 = {};
24779
- if (trigger.eventName) {
24780
- config2.eventName = trigger.eventName;
24781
- }
24782
- if (trigger.segmentId) {
24783
- config2.segmentId = trigger.segmentId;
24784
- }
24785
- if (trigger.schedule) {
24786
- config2.schedule = trigger.schedule;
24787
- }
24788
- if (trigger.timezone) {
24789
- config2.timezone = trigger.timezone;
24790
- }
24791
- if (trigger.topicId) {
24792
- config2.topicId = trigger.topicId;
24793
- }
24794
- return config2;
24795
- }
24796
- function getTriggerName(type) {
24797
- const names = {
24798
- event: "When event occurs",
24799
- contact_created: "When contact is created",
24800
- contact_updated: "When contact is updated",
24801
- segment_entry: "When contact enters segment",
24802
- segment_exit: "When contact exits segment",
24803
- schedule: "On schedule",
24804
- api: "When triggered via API",
24805
- topic_subscribed: "When contact subscribes to topic",
24806
- topic_unsubscribed: "When contact unsubscribes from topic"
24807
- };
24808
- return names[type] || "Trigger";
24809
- }
24810
- function flattenSteps(stepDefs, steps, transitions, fromStepId, branch) {
24811
- let prevIds = [fromStepId];
24812
- let firstStepInBranch = true;
24813
- for (const def of stepDefs) {
24814
- const step = toWorkflowStep(def);
24815
- steps.push(step);
24816
- for (const prevId of prevIds) {
24817
- const transition = {
24818
- id: `t-${prevId}-${step.id}`,
24819
- fromStepId: prevId,
24820
- toStepId: step.id
24821
- };
24822
- if (firstStepInBranch && branch && prevId === fromStepId) {
24823
- transition.condition = { branch };
24824
- }
24825
- transitions.push(transition);
24826
- }
24827
- firstStepInBranch = false;
24828
- if (def.type === "condition" && def.branches) {
24829
- const leafIds = [];
24830
- if (def.branches.yes && def.branches.yes.length > 0) {
24831
- const yesLeaves = flattenSteps(
24832
- def.branches.yes,
24833
- steps,
24834
- transitions,
24835
- step.id,
24836
- "yes"
24837
- );
24838
- leafIds.push(...yesLeaves);
24839
- } else {
24840
- leafIds.push(step.id);
24841
- }
24842
- if (def.branches.no && def.branches.no.length > 0) {
24843
- const noLeaves = flattenSteps(
24844
- def.branches.no,
24845
- steps,
24846
- transitions,
24847
- step.id,
24848
- "no"
24849
- );
24850
- leafIds.push(...noLeaves);
24851
- } else if (!leafIds.includes(step.id)) {
24852
- leafIds.push(step.id);
24853
- }
24854
- prevIds = leafIds;
24855
- continue;
24856
- }
24857
- if (def.type === "exit") {
24858
- prevIds = [];
24859
- continue;
24860
- }
24861
- prevIds = [step.id];
24862
- }
24863
- return prevIds;
24864
- }
24865
- function toWorkflowStep(def) {
24866
- const step = {
24867
- id: def.id,
24868
- type: def.type,
24869
- name: def.name || getDefaultStepName(def.type),
24870
- position: { x: 0, y: 0 },
24871
- // Will be updated by assignPositions
24872
- config: def.config
24873
- };
24874
- if (def.cascadeGroupId) {
24875
- step.cascadeGroupId = def.cascadeGroupId;
24876
- }
24877
- return step;
24878
- }
24879
- function getDefaultStepName(type) {
24880
- const names = {
24881
- send_email: "Send Email",
24882
- send_sms: "Send SMS",
24883
- delay: "Wait",
24884
- exit: "Exit",
24885
- condition: "Condition",
24886
- webhook: "Webhook",
24887
- update_contact: "Update Contact",
24888
- wait_for_event: "Wait for Event",
24889
- wait_for_email_engagement: "Wait for Email Engagement",
24890
- subscribe_topic: "Subscribe to Topic",
24891
- unsubscribe_topic: "Unsubscribe from Topic"
24892
- };
24893
- return names[type] || type;
24894
- }
24895
- function assignPositions(steps, transitions) {
24896
- const LEVEL_HEIGHT = 200;
24897
- const BRANCH_OFFSET = 300;
24898
- const childrenMap = /* @__PURE__ */ new Map();
24899
- for (const t of transitions) {
24900
- if (!childrenMap.has(t.fromStepId)) {
24901
- childrenMap.set(t.fromStepId, []);
24902
- }
24903
- childrenMap.get(t.fromStepId)?.push({
24904
- id: t.toStepId,
24905
- branch: t.condition?.branch
24906
- });
24907
- }
24908
- const visited = /* @__PURE__ */ new Set();
24909
- const queue = [];
24910
- const triggerStep = steps.find((s) => s.type === "trigger");
24911
- if (triggerStep) {
24912
- queue.push({ stepId: triggerStep.id, level: 0, xOffset: 0 });
24913
- }
24914
- while (queue.length > 0) {
24915
- const { stepId, level, xOffset } = queue.shift();
24916
- if (visited.has(stepId)) {
24917
- continue;
24918
- }
24919
- visited.add(stepId);
24920
- const step = steps.find((s) => s.id === stepId);
24921
- if (step) {
24922
- step.position = {
24923
- x: xOffset,
24924
- y: level * LEVEL_HEIGHT
24925
- };
24926
- }
24927
- const children = childrenMap.get(stepId) || [];
24928
- for (const child of children) {
24929
- if (!visited.has(child.id)) {
24930
- let childXOffset = xOffset;
24931
- if (child.branch === "yes") {
24932
- childXOffset = xOffset - BRANCH_OFFSET;
24933
- } else if (child.branch === "no") {
24934
- childXOffset = xOffset + BRANCH_OFFSET;
24935
- }
24936
- queue.push({
24937
- stepId: child.id,
24938
- level: level + 1,
24939
- xOffset: childXOffset
24940
- });
24941
- }
24942
- }
24943
- }
24944
- for (const step of steps) {
24945
- if (!visited.has(step.id)) {
24946
- step.position = {
24947
- x: 600,
24948
- y: steps.indexOf(step) * LEVEL_HEIGHT
24949
- };
24950
- }
24951
- }
24952
- }
24953
-
24954
- // src/utils/email/workflow-ts.ts
24955
- init_esm_shims();
24956
- import { createHash as createHash2 } from "crypto";
24957
- import { existsSync as existsSync13 } from "fs";
24958
- import { mkdir as mkdir7, readdir as readdir3, readFile as readFile7, writeFile as writeFile9 } from "fs/promises";
24959
- import { basename, join as join14 } from "path";
24960
- async function discoverWorkflows(dir, filter) {
24961
- if (!existsSync13(dir)) {
24962
- return [];
24963
- }
24964
- const entries = await readdir3(dir);
24965
- const workflows = entries.filter(
24966
- (f) => (
24967
- // Include .ts files only (not .tsx for workflows)
24968
- f.endsWith(".ts") && // Exclude private/helper files starting with _
24969
- !f.startsWith("_") && // Exclude type definition files
24970
- !f.endsWith(".d.ts")
24971
- )
24972
- );
24973
- if (filter) {
24974
- const slug = filter.replace(/\.ts$/, "");
24975
- return workflows.filter((f) => f.replace(/\.ts$/, "") === slug);
24976
- }
24977
- return workflows;
24978
- }
24979
- async function parseWorkflowTs(filePath, wrapsDir) {
24980
- const { build: build2 } = await import("esbuild");
24981
- const source = await readFile7(filePath, "utf-8");
24982
- const sourceHash = createHash2("sha256").update(source).digest("hex");
24983
- const slug = basename(filePath, ".ts");
24984
- const shimDir = join14(wrapsDir, ".wraps", "_shims");
24985
- await mkdir7(shimDir, { recursive: true });
24986
- const clientShimContent = `
24987
- // Identity functions for workflow definitions
24988
- export const defineWorkflow = (def) => def;
24989
-
24990
- // Step helper functions - they just create step definition objects
24991
- export const sendEmail = (id, config) => ({
24992
- id,
24993
- type: 'send_email',
24994
- name: config.name ?? \`Send email: \${config.template || 'custom'}\`,
24995
- config: { type: 'send_email', ...config },
24996
- });
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
+ },
24997
24752
 
24998
- export const sendSms = (id, config) => ({
24999
- id,
25000
- type: 'send_sms',
25001
- name: config.name ?? \`Send SMS: \${config.template || 'custom'}\`,
25002
- 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
+ ],
25003
24768
  });
24769
+ `,
24770
+ "cart-recovery": `import {
24771
+ defineWorkflow,
24772
+ sendEmail,
24773
+ sendSms,
24774
+ delay,
24775
+ cascade,
24776
+ exit,
24777
+ } from '@wraps.dev/client';
25004
24778
 
25005
- export const delay = (id, duration) => {
25006
- const { name, ...durationConfig } = duration;
25007
- const normalized = normalizeDuration(durationConfig);
25008
- return {
25009
- id,
25010
- type: 'delay',
25011
- name: name ?? \`Wait \${normalized.amount} \${normalized.unit}\`,
25012
- config: { type: 'delay', ...normalized },
25013
- };
25014
- };
25015
-
25016
- export const condition = (id, config) => {
25017
- const { branches, name, ...conditionConfig } = config;
25018
- return {
25019
- id,
25020
- type: 'condition',
25021
- name: name ?? \`Check: \${config.field} \${config.operator}\`,
25022
- config: { type: 'condition', ...conditionConfig },
25023
- branches,
25024
- };
25025
- };
25026
-
25027
- export const waitForEvent = (id, config) => {
25028
- const { name, timeout, ...eventConfig } = config;
25029
- return {
25030
- id,
25031
- type: 'wait_for_event',
25032
- name: name ?? \`Wait for: \${config.eventName}\`,
25033
- config: {
25034
- type: 'wait_for_event',
25035
- eventName: eventConfig.eventName,
25036
- timeoutSeconds: durationToSeconds(timeout),
25037
- },
25038
- };
25039
- };
25040
-
25041
- export const waitForEmailEngagement = (id, config) => {
25042
- const { name, timeout, emailStepId, engagementType } = config;
25043
- return {
25044
- id,
25045
- type: 'wait_for_email_engagement',
25046
- name: name ?? \`Wait for email \${engagementType}: \${emailStepId}\`,
25047
- config: {
25048
- type: 'wait_for_email_engagement',
25049
- timeoutSeconds: durationToSeconds(timeout),
25050
- },
25051
- };
25052
- };
25053
-
25054
- export const exit = (id, config) => {
25055
- const { name, ...exitConfig } = config ?? {};
25056
- return {
25057
- id,
25058
- type: 'exit',
25059
- name: name ?? 'Exit',
25060
- config: { type: 'exit', ...exitConfig },
25061
- };
25062
- };
25063
-
25064
- export const updateContact = (id, config) => {
25065
- const { name, ...updateConfig } = config;
25066
- return {
25067
- id,
25068
- type: 'update_contact',
25069
- name: name ?? 'Update contact',
25070
- config: { type: 'update_contact', ...updateConfig },
25071
- };
25072
- };
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
+ },
25073
24791
 
25074
- export const subscribeTopic = (id, config) => {
25075
- const { name, ...topicConfig } = config;
25076
- return {
25077
- id,
25078
- type: 'subscribe_topic',
25079
- name: name ?? \`Subscribe to topic: \${config.topicId}\`,
25080
- config: { type: 'subscribe_topic', ...topicConfig },
25081
- };
25082
- };
24792
+ steps: [
24793
+ delay('initial-wait', { minutes: 30 }),
25083
24794
 
25084
- export const unsubscribeTopic = (id, config) => {
25085
- const { name, ...topicConfig } = config;
25086
- return {
25087
- id,
25088
- type: 'unsubscribe_topic',
25089
- name: name ?? \`Unsubscribe from topic: \${config.topicId}\`,
25090
- config: { type: 'unsubscribe_topic', ...topicConfig },
25091
- };
25092
- };
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
+ }),
25093
24809
 
25094
- export const webhook = (id, config) => {
25095
- const { name, ...webhookConfig } = config;
25096
- return {
25097
- id,
25098
- type: 'webhook',
25099
- name: name ?? \`Webhook: \${config.url}\`,
25100
- config: { type: 'webhook', method: 'POST', ...webhookConfig },
25101
- };
25102
- };
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';
25103
24821
 
25104
24822
  /**
25105
- * cascade(id, config) \u2014 expand a cross-channel cascade into primitive steps.
25106
- *
25107
- * For each email channel (except the last), we emit:
25108
- * send_email \u2192 wait_for_email_engagement \u2192 condition (engaged?)
25109
- * with the condition's "yes" branch containing an exit node and the "no"
25110
- * branch falling through to the next channel.
25111
- *
25112
- * Non-email channels (SMS) emit only a send step.
24823
+ * Trial Conversion
25113
24824
  *
25114
- * Every generated step carries cascadeGroupId = id so the execution
25115
- * 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.
25116
24827
  */
25117
- export const cascade = (id, config) => {
25118
- const channels = config.channels || [];
25119
- const steps = [];
25120
-
25121
- for (let i = 0; i < channels.length; i++) {
25122
- const channel = channels[i];
25123
- const isLast = i === channels.length - 1;
25124
-
25125
- if (channel.type === 'email') {
25126
- // Send email step
25127
- steps.push({
25128
- id: id + '-send-' + i,
25129
- type: 'send_email',
25130
- name: 'Cascade: send ' + (channel.template || 'email'),
25131
- config: { type: 'send_email', templateId: channel.template },
25132
- cascadeGroupId: id,
25133
- });
24828
+ export default defineWorkflow({
24829
+ name: 'Trial Conversion',
24830
+ trigger: {
24831
+ type: 'event',
24832
+ eventName: 'trial.ending',
24833
+ },
25134
24834
 
25135
- // If not last channel, add wait + condition
25136
- if (!isLast && channel.waitFor) {
25137
- const waitSeconds = durationToSeconds(channel.waitFor) || 259200;
25138
- const waitId = id + '-wait-' + i;
25139
- const condId = id + '-cond-' + i;
25140
- const exitId = id + '-exit-' + i;
25141
-
25142
- // Wait for engagement step
25143
- steps.push({
25144
- id: waitId,
25145
- type: 'wait_for_email_engagement',
25146
- name: 'Cascade: wait for ' + (channel.engagement || 'opened'),
25147
- config: { type: 'wait_for_email_engagement', timeoutSeconds: waitSeconds },
25148
- cascadeGroupId: id,
25149
- });
25150
-
25151
- // Condition step: check engagement.status
25152
- steps.push({
25153
- id: condId,
25154
- type: 'condition',
25155
- name: 'Cascade: email engaged?',
25156
- config: {
25157
- type: 'condition',
25158
- field: 'engagement.status',
25159
- operator: 'equals',
25160
- value: 'true',
25161
- },
25162
- cascadeGroupId: id,
25163
- branches: {
25164
- yes: [{
25165
- id: exitId,
25166
- type: 'exit',
25167
- name: 'Exit',
25168
- config: { type: 'exit', reason: 'Engaged via email' },
25169
- cascadeGroupId: id,
25170
- }],
25171
- },
25172
- });
25173
- }
25174
- } else if (channel.type === 'sms') {
25175
- // Send SMS step
25176
- steps.push({
25177
- id: id + '-send-' + i,
25178
- type: 'send_sms',
25179
- name: 'Cascade: send ' + (channel.template || 'sms'),
25180
- config: { type: 'send_sms', template: channel.template, body: channel.body },
25181
- cascadeGroupId: id,
25182
- });
25183
- }
25184
- }
25185
-
25186
- return steps;
25187
- };
25188
-
25189
- // Internal helpers
25190
- function normalizeDuration(duration) {
25191
- if (duration.days !== undefined) {
25192
- return { amount: duration.days, unit: 'days' };
25193
- }
25194
- if (duration.hours !== undefined) {
25195
- return { amount: duration.hours, unit: 'hours' };
25196
- }
25197
- if (duration.minutes !== undefined) {
25198
- return { amount: duration.minutes, unit: 'minutes' };
25199
- }
25200
- return { amount: 1, unit: 'hours' };
25201
- }
25202
-
25203
- function durationToSeconds(duration) {
25204
- if (!duration) return undefined;
25205
- let seconds = 0;
25206
- if (duration.days) seconds += duration.days * 24 * 60 * 60;
25207
- if (duration.hours) seconds += duration.hours * 60 * 60;
25208
- if (duration.minutes) seconds += duration.minutes * 60;
25209
- return seconds > 0 ? seconds : undefined;
25210
- }
25211
- `;
25212
- await writeFile9(
25213
- join14(shimDir, "wraps-client-shim.mjs"),
25214
- clientShimContent,
25215
- "utf-8"
25216
- );
25217
- const result = await build2({
25218
- entryPoints: [filePath],
25219
- bundle: true,
25220
- write: false,
25221
- format: "esm",
25222
- platform: "node",
25223
- target: "node20",
25224
- alias: {
25225
- "@wraps.dev/client": join14(shimDir, "wraps-client-shim.mjs")
25226
- }
25227
- });
25228
- const bundledCode = result.outputFiles[0].text;
25229
- const tmpDir = join14(wrapsDir, ".wraps", "_workflows");
25230
- await mkdir7(tmpDir, { recursive: true });
25231
- const tmpPath = join14(tmpDir, `${slug}.mjs`);
25232
- await writeFile9(tmpPath, bundledCode, "utf-8");
25233
- const mod = await import(`${tmpPath}?t=${Date.now()}`);
25234
- const definition = mod.default;
25235
- if (!definition || typeof definition !== "object") {
25236
- throw new Error(
25237
- "Workflow must have a default export (workflow definition from defineWorkflow())"
25238
- );
25239
- }
25240
- if (!definition.name) {
25241
- throw new Error("Workflow definition must have a 'name' property");
25242
- }
25243
- if (!definition.trigger) {
25244
- throw new Error("Workflow definition must have a 'trigger' property");
25245
- }
25246
- if (!Array.isArray(definition.steps)) {
25247
- throw new Error("Workflow definition must have a 'steps' array");
25248
- }
25249
- return {
25250
- slug,
25251
- filePath,
25252
- source,
25253
- sourceHash,
25254
- definition,
25255
- cliProjectPath: `workflows/${slug}.ts`
25256
- };
25257
- }
25258
-
25259
- // src/utils/email/workflow-validator.ts
25260
- init_esm_shims();
25261
- function validateTransformedWorkflow(transformed, localTemplateSlugs) {
25262
- const errors2 = [];
25263
- errors2.push(...validateStructure(transformed.steps, transformed.transitions));
25264
- for (const step of transformed.steps) {
25265
- errors2.push(...validateStep(step));
25266
- }
25267
- if (localTemplateSlugs) {
25268
- errors2.push(
25269
- ...validateTemplateReferences(transformed.steps, localTemplateSlugs)
25270
- );
25271
- }
25272
- const errorsByNodeId = /* @__PURE__ */ new Map();
25273
- for (const error of errors2) {
25274
- if (error.nodeId) {
25275
- const existing = errorsByNodeId.get(error.nodeId) || [];
25276
- existing.push(error);
25277
- errorsByNodeId.set(error.nodeId, existing);
25278
- }
25279
- }
25280
- return {
25281
- isValid: errors2.filter((e) => e.severity === "error").length === 0,
25282
- errors: errors2,
25283
- errorsByNodeId
25284
- };
25285
- }
25286
- function validateStructure(steps, transitions) {
25287
- const errors2 = [];
25288
- const triggerSteps = steps.filter((s) => s.type === "trigger");
25289
- if (triggerSteps.length === 0) {
25290
- errors2.push({
25291
- message: "Workflow must have a trigger node",
25292
- severity: "error"
25293
- });
25294
- } else if (triggerSteps.length > 1) {
25295
- errors2.push({
25296
- message: "Workflow can only have one trigger node",
25297
- severity: "error"
25298
- });
25299
- }
25300
- const actionSteps = steps.filter(
25301
- (s) => s.type !== "trigger" && s.type !== "exit"
25302
- );
25303
- if (actionSteps.length === 0 && steps.length > 1) {
25304
- errors2.push({
25305
- message: "Workflow must have at least one action step",
25306
- severity: "error"
25307
- });
25308
- }
25309
- if (triggerSteps.length === 1) {
25310
- const reachableIds = getReachableNodeIds(triggerSteps[0].id, transitions);
25311
- const orphanSteps = steps.filter(
25312
- (s) => s.type !== "trigger" && !reachableIds.has(s.id)
25313
- );
25314
- for (const orphan of orphanSteps) {
25315
- errors2.push({
25316
- nodeId: orphan.id,
25317
- message: `"${orphan.name}" is not connected to the workflow`,
25318
- severity: "warning"
25319
- });
25320
- }
25321
- }
25322
- const stepIds = new Set(steps.map((s) => s.id));
25323
- for (const transition of transitions) {
25324
- if (!stepIds.has(transition.fromStepId)) {
25325
- errors2.push({
25326
- message: `Transition references non-existent source step: ${transition.fromStepId}`,
25327
- severity: "error"
25328
- });
25329
- }
25330
- if (!stepIds.has(transition.toStepId)) {
25331
- errors2.push({
25332
- message: `Transition references non-existent target step: ${transition.toStepId}`,
25333
- severity: "error"
25334
- });
25335
- }
25336
- }
25337
- return errors2;
25338
- }
25339
- function getReachableNodeIds(startId, transitions) {
25340
- const reachable = /* @__PURE__ */ new Set();
25341
- const queue = [startId];
25342
- while (queue.length > 0) {
25343
- const currentId = queue.shift();
25344
- if (reachable.has(currentId)) {
25345
- continue;
25346
- }
25347
- reachable.add(currentId);
25348
- const outgoing = transitions.filter((t) => t.fromStepId === currentId);
25349
- for (const t of outgoing) {
25350
- if (!reachable.has(t.toStepId)) {
25351
- queue.push(t.toStepId);
25352
- }
25353
- }
25354
- }
25355
- return reachable;
25356
- }
25357
- function validateStep(step) {
25358
- const errors2 = [];
25359
- const config2 = step.config;
25360
- const configType = config2.type;
25361
- switch (configType) {
25362
- case "trigger":
25363
- errors2.push(...validateTrigger(step.id, config2));
25364
- break;
25365
- case "send_email":
25366
- errors2.push(...validateSendEmail(step.id, config2));
25367
- break;
25368
- case "condition":
25369
- errors2.push(...validateCondition(step.id, config2));
25370
- break;
25371
- case "webhook":
25372
- errors2.push(...validateWebhook(step.id, config2));
25373
- break;
25374
- case "subscribe_topic":
25375
- case "unsubscribe_topic":
25376
- errors2.push(...validateTopic(step.id, config2));
25377
- break;
25378
- case "wait_for_event":
25379
- errors2.push(...validateWaitForEvent(step.id, config2));
25380
- break;
25381
- case "delay":
25382
- errors2.push(...validateDelay(step.id, config2));
25383
- break;
25384
- }
25385
- return errors2;
25386
- }
25387
- function validateTrigger(nodeId, config2) {
25388
- const errors2 = [];
25389
- switch (config2.triggerType) {
25390
- case "event":
25391
- if (!config2.eventName) {
25392
- errors2.push({
25393
- nodeId,
25394
- field: "eventName",
25395
- message: "Event name is required",
25396
- severity: "error"
25397
- });
25398
- }
25399
- break;
25400
- case "segment_entry":
25401
- case "segment_exit":
25402
- if (!config2.segmentId) {
25403
- errors2.push({
25404
- nodeId,
25405
- field: "segmentId",
25406
- message: "Segment is required",
25407
- severity: "error"
25408
- });
25409
- }
25410
- break;
25411
- case "topic_subscribed":
25412
- case "topic_unsubscribed":
25413
- if (!config2.topicId) {
25414
- errors2.push({
25415
- nodeId,
25416
- field: "topicId",
25417
- message: "Topic is required",
25418
- severity: "error"
25419
- });
25420
- }
25421
- break;
25422
- case "schedule":
25423
- if (!config2.schedule) {
25424
- errors2.push({
25425
- nodeId,
25426
- field: "schedule",
25427
- message: "Schedule (cron expression) is required",
25428
- severity: "error"
25429
- });
25430
- }
25431
- if (!config2.timezone) {
25432
- errors2.push({
25433
- nodeId,
25434
- field: "timezone",
25435
- message: "Timezone is required",
25436
- severity: "error"
25437
- });
25438
- }
25439
- break;
25440
- }
25441
- return errors2;
25442
- }
25443
- function validateSendEmail(nodeId, config2) {
25444
- const errors2 = [];
25445
- const templateRef = config2.templateId || config2.template;
25446
- if (!templateRef) {
25447
- errors2.push({
25448
- nodeId,
25449
- field: "templateId",
25450
- message: "Email template is required",
25451
- severity: "error"
25452
- });
25453
- }
25454
- return errors2;
25455
- }
25456
- function validateCondition(nodeId, config2) {
25457
- const errors2 = [];
25458
- if (!config2.field) {
25459
- errors2.push({
25460
- nodeId,
25461
- field: "field",
25462
- message: "Condition field is required",
25463
- severity: "error"
25464
- });
25465
- }
25466
- if (!config2.operator) {
25467
- errors2.push({
25468
- nodeId,
25469
- field: "operator",
25470
- message: "Condition operator is required",
25471
- severity: "error"
25472
- });
25473
- }
25474
- if (config2.operator !== "is_set" && config2.operator !== "is_not_set" && config2.operator !== "is_true" && config2.operator !== "is_false" && (config2.value === void 0 || config2.value === "")) {
25475
- errors2.push({
25476
- nodeId,
25477
- field: "value",
25478
- message: "Condition value is required",
25479
- severity: "error"
25480
- });
25481
- }
25482
- return errors2;
25483
- }
25484
- function validateWebhook(nodeId, config2) {
25485
- const errors2 = [];
25486
- if (config2.url) {
25487
- try {
25488
- new URL(config2.url);
25489
- } catch {
25490
- errors2.push({
25491
- nodeId,
25492
- field: "url",
25493
- message: "Invalid URL format",
25494
- severity: "error"
25495
- });
25496
- }
25497
- } else {
25498
- errors2.push({
25499
- nodeId,
25500
- field: "url",
25501
- message: "Webhook URL is required",
25502
- severity: "error"
25503
- });
25504
- }
25505
- return errors2;
25506
- }
25507
- function validateTopic(nodeId, config2) {
25508
- const errors2 = [];
25509
- if (!config2.topicId) {
25510
- errors2.push({
25511
- nodeId,
25512
- field: "topicId",
25513
- message: "Topic is required",
25514
- severity: "error"
25515
- });
25516
- }
25517
- return errors2;
25518
- }
25519
- function validateWaitForEvent(nodeId, config2) {
25520
- const errors2 = [];
25521
- if (!config2.eventName) {
25522
- errors2.push({
25523
- nodeId,
25524
- field: "eventName",
25525
- message: "Event name is required",
25526
- severity: "error"
25527
- });
25528
- }
25529
- return errors2;
25530
- }
25531
- function validateDelay(nodeId, config2) {
25532
- const errors2 = [];
25533
- const amount = config2.amount;
25534
- if (!amount || amount < 1) {
25535
- errors2.push({
25536
- nodeId,
25537
- field: "amount",
25538
- message: "Delay duration must be at least 1",
25539
- severity: "error"
25540
- });
25541
- }
25542
- return errors2;
25543
- }
25544
- function validateTemplateReferences(steps, localTemplateSlugs) {
25545
- const errors2 = [];
25546
- for (const step of steps) {
25547
- if (step.config.type === "send_email") {
25548
- const config2 = step.config;
25549
- const templateRef = config2.templateId || config2.template;
25550
- if (templateRef && !localTemplateSlugs.has(templateRef)) {
25551
- errors2.push({
25552
- nodeId: step.id,
25553
- field: "templateId",
25554
- message: `Template "${templateRef}" not found in templates/ directory`,
25555
- severity: "error"
25556
- });
25557
- }
25558
- }
25559
- if (step.config.type === "send_sms") {
25560
- const config2 = step.config;
25561
- const templateRef = config2.templateId || config2.template;
25562
- if (templateRef && !localTemplateSlugs.has(templateRef)) {
25563
- errors2.push({
25564
- nodeId: step.id,
25565
- field: "templateId",
25566
- message: `SMS template "${templateRef}" not found in templates/ directory`,
25567
- severity: "warning"
25568
- // Warning for SMS since we might not have SMS templates yet
25569
- });
25570
- }
25571
- }
25572
- }
25573
- return errors2;
25574
- }
25575
-
25576
- // src/commands/email/workflows/generate.ts
25577
- init_config();
25578
- init_errors();
25579
- init_json_output();
25580
- init_output();
25581
- var TEMPLATES = {
25582
- welcome: `import {
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 {
25583
24853
  defineWorkflow,
25584
24854
  sendEmail,
25585
24855
  delay,
@@ -25588,178 +24858,58 @@ var TEMPLATES = {
25588
24858
  } from '@wraps.dev/client';
25589
24859
 
25590
24860
  /**
25591
- * Welcome Sequence
24861
+ * Re-engagement Campaign
25592
24862
  *
25593
- * Send a welcome email when a contact is created,
25594
- * wait 1 day, then check if they activated.
25595
- * If not, send a follow-up with tips.
24863
+ * Win back inactive users with a personalized email.
24864
+ * Wait 3 days for engagement, then send a final offer.
25596
24865
  */
25597
24866
  export default defineWorkflow({
25598
- name: 'Welcome Sequence',
24867
+ name: 'Re-engagement Campaign',
25599
24868
  trigger: {
25600
- type: 'contact_created',
24869
+ type: 'event',
24870
+ eventName: 'contact.inactive',
25601
24871
  },
25602
24872
 
25603
24873
  steps: [
25604
- sendEmail('send-welcome', { template: 'welcome-email' }),
25605
- delay('wait-1-day', { days: 1 }),
25606
- condition('check-activated', {
25607
- field: 'contact.hasActivated',
25608
- operator: 'equals',
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',
25609
24879
  value: true,
25610
24880
  branches: {
25611
- yes: [exit('already-active')],
24881
+ yes: [exit('re-engaged')],
25612
24882
  no: [
25613
- sendEmail('send-tips', { template: 'getting-started-tips' }),
24883
+ sendEmail('send-final-offer', { template: 'final-offer' }),
25614
24884
  ],
25615
24885
  },
25616
24886
  }),
25617
24887
  ],
25618
24888
  });
25619
24889
  `,
25620
- "cart-recovery": `import {
24890
+ onboarding: `import {
25621
24891
  defineWorkflow,
25622
24892
  sendEmail,
25623
- sendSms,
25624
24893
  delay,
25625
- cascade,
24894
+ condition,
25626
24895
  exit,
25627
24896
  } from '@wraps.dev/client';
25628
24897
 
25629
24898
  /**
25630
- * Cart Recovery Cascade
24899
+ * Multi-step Onboarding
25631
24900
  *
25632
- * When a cart is abandoned, wait 30 minutes, then try
25633
- * email first. If not opened after 2 hours, fall back to SMS.
24901
+ * Guide new users through setup with a series of emails.
24902
+ * Check progress at each step and skip ahead if they're done.
25634
24903
  */
25635
24904
  export default defineWorkflow({
25636
- name: 'Cart Recovery Cascade',
24905
+ name: 'Onboarding Sequence',
25637
24906
  trigger: {
25638
- type: 'event',
25639
- eventName: 'cart.abandoned',
24907
+ type: 'contact_created',
25640
24908
  },
25641
24909
 
25642
24910
  steps: [
25643
- delay('initial-wait', { minutes: 30 }),
25644
-
25645
- ...cascade('recover-cart', {
25646
- channels: [
25647
- {
25648
- type: 'email',
25649
- template: 'cart-recovery',
25650
- waitFor: { hours: 2 },
25651
- engagement: 'opened',
25652
- },
25653
- {
25654
- type: 'sms',
25655
- template: 'cart-sms-reminder',
25656
- },
25657
- ],
25658
- }),
25659
-
25660
- exit('cascade-complete'),
25661
- ],
25662
- });
25663
- `,
25664
- "trial-conversion": `import {
25665
- defineWorkflow,
25666
- sendEmail,
25667
- delay,
25668
- condition,
25669
- exit,
25670
- } from '@wraps.dev/client';
25671
-
25672
- /**
25673
- * Trial Conversion
25674
- *
25675
- * Remind users 3 days before their trial ends.
25676
- * If they haven't upgraded after 1 day, send a final nudge.
25677
- */
25678
- export default defineWorkflow({
25679
- name: 'Trial Conversion',
25680
- trigger: {
25681
- type: 'event',
25682
- eventName: 'trial.ending',
25683
- },
25684
-
25685
- steps: [
25686
- sendEmail('send-reminder', { template: 'trial-ending-reminder' }),
25687
- delay('wait-1-day', { days: 1 }),
25688
- condition('check-upgraded', {
25689
- field: 'contact.plan',
25690
- operator: 'not_equals',
25691
- value: 'free',
25692
- branches: {
25693
- yes: [exit('already-upgraded')],
25694
- no: [
25695
- sendEmail('send-upgrade-nudge', { template: 'upgrade-offer' }),
25696
- ],
25697
- },
25698
- }),
25699
- ],
25700
- });
25701
- `,
25702
- "re-engagement": `import {
25703
- defineWorkflow,
25704
- sendEmail,
25705
- delay,
25706
- condition,
25707
- exit,
25708
- } from '@wraps.dev/client';
25709
-
25710
- /**
25711
- * Re-engagement Campaign
25712
- *
25713
- * Win back inactive users with a personalized email.
25714
- * Wait 3 days for engagement, then send a final offer.
25715
- */
25716
- export default defineWorkflow({
25717
- name: 'Re-engagement Campaign',
25718
- trigger: {
25719
- type: 'event',
25720
- eventName: 'contact.inactive',
25721
- },
25722
-
25723
- steps: [
25724
- sendEmail('send-win-back', { template: 'we-miss-you' }),
25725
- delay('wait-3-days', { days: 3 }),
25726
- condition('check-engaged', {
25727
- field: 'contact.lastActiveAt',
25728
- operator: 'is_set',
25729
- value: true,
25730
- branches: {
25731
- yes: [exit('re-engaged')],
25732
- no: [
25733
- sendEmail('send-final-offer', { template: 'final-offer' }),
25734
- ],
25735
- },
25736
- }),
25737
- ],
25738
- });
25739
- `,
25740
- onboarding: `import {
25741
- defineWorkflow,
25742
- sendEmail,
25743
- delay,
25744
- condition,
25745
- exit,
25746
- } from '@wraps.dev/client';
25747
-
25748
- /**
25749
- * Multi-step Onboarding
25750
- *
25751
- * Guide new users through setup with a series of emails.
25752
- * Check progress at each step and skip ahead if they're done.
25753
- */
25754
- export default defineWorkflow({
25755
- name: 'Onboarding Sequence',
25756
- trigger: {
25757
- type: 'contact_created',
25758
- },
25759
-
25760
- steps: [
25761
- sendEmail('send-welcome', { template: 'onboarding-welcome' }),
25762
- delay('wait-1-day', { days: 1 }),
24911
+ sendEmail('send-welcome', { template: 'onboarding-welcome' }),
24912
+ delay('wait-1-day', { days: 1 }),
25763
24913
 
25764
24914
  condition('check-profile-complete', {
25765
24915
  field: 'contact.profileComplete',
@@ -25799,8 +24949,6 @@ async function workflowsGenerate(options) {
25799
24949
  const startTime = Date.now();
25800
24950
  if (options.template) {
25801
24951
  generateFromTemplate(options);
25802
- } else if (options.description) {
25803
- await generateFromDescription(options);
25804
24952
  } else {
25805
24953
  showUsage();
25806
24954
  return;
@@ -25808,31 +24956,34 @@ async function workflowsGenerate(options) {
25808
24956
  trackCommand("email:workflows:generate", {
25809
24957
  success: true,
25810
24958
  duration_ms: Date.now() - startTime,
25811
- mode: options.template ? "template" : "llm"
24959
+ mode: "template"
25812
24960
  });
25813
24961
  }
25814
24962
  function showUsage() {
25815
24963
  if (isJsonMode()) {
25816
24964
  jsonError("email.workflows.generate", {
25817
24965
  code: "MISSING_INPUT",
25818
- message: "Provide a --template name or a description as a positional argument"
24966
+ message: "Provide a --template name to generate a workflow"
25819
24967
  });
25820
24968
  return;
25821
24969
  }
25822
- log29.error("Provide a description or use --template to generate a workflow.");
24970
+ log29.error("Provide a --template to generate a workflow.");
25823
24971
  console.log();
25824
- console.log(` ${pc31.bold("Template mode:")}`);
24972
+ console.log(` ${pc31.bold("Usage:")}`);
25825
24973
  console.log(
25826
24974
  ` ${pc31.cyan("wraps email workflows generate --template welcome")}`
25827
24975
  );
25828
24976
  console.log();
25829
- console.log(` ${pc31.bold("AI mode:")}`);
25830
24977
  console.log(
25831
- ` ${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(", ")}`
25832
24979
  );
25833
24980
  console.log();
24981
+ console.log(` ${pc31.bold("Want a custom workflow?")}`);
25834
24982
  console.log(
25835
- ` ${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.`
25836
24987
  );
25837
24988
  console.log();
25838
24989
  }
@@ -25857,9 +25008,9 @@ function generateFromTemplate(options) {
25857
25008
  }
25858
25009
  const slug = options.name || templateName;
25859
25010
  const cwd = process.cwd();
25860
- const workflowsDir = join15(cwd, "wraps", "workflows");
25861
- const filePath = join15(workflowsDir, `${slug}.ts`);
25862
- 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) {
25863
25014
  if (isJsonMode()) {
25864
25015
  jsonError("email.workflows.generate", {
25865
25016
  code: "FILE_EXISTS",
@@ -25889,190 +25040,27 @@ function generateFromTemplate(options) {
25889
25040
  showNextSteps2(slug);
25890
25041
  }
25891
25042
  }
25892
- async function generateFromDescription(options) {
25893
- const description = options.description ?? "";
25894
- const slug = options.name || slugify(description);
25895
- if (!checkFileExists(slug, options.force)) {
25896
- return;
25897
- }
25898
- if (!isJsonMode()) {
25899
- intro27(pc31.bold("Generate Workflow"));
25900
- }
25901
- const progress = new DeploymentProgress();
25902
- const token = await resolveTokenAsync({ token: options.token });
25903
- if (!token) {
25904
- throw errors.notAuthenticated();
25905
- }
25906
- const code = await callGenerateApi(description, slug, token, progress);
25907
- if (!code) {
25908
- return;
25909
- }
25910
- if (options.dryRun) {
25911
- showDryRun(slug, code);
25912
- return;
25913
- }
25914
- if (!(options.yes || isJsonMode())) {
25915
- const shouldWrite = await showPreviewAndConfirm(slug, code);
25916
- if (!shouldWrite) {
25917
- return;
25918
- }
25919
- }
25920
- writeWorkflowFile(slug, code);
25921
- const workflowsDir = join15(process.cwd(), "wraps", "workflows");
25922
- const filePath = join15(workflowsDir, `${slug}.ts`);
25923
- await autoValidate(filePath, join15(process.cwd(), "wraps"), slug, progress);
25924
- if (isJsonMode()) {
25925
- jsonSuccess("email.workflows.generate", {
25926
- mode: "llm",
25927
- slug,
25928
- path: `wraps/workflows/${slug}.ts`
25929
- });
25930
- } else {
25931
- log29.success(`Created ${pc31.cyan(`wraps/workflows/${slug}.ts`)}`);
25932
- showNextSteps2(slug, true);
25933
- }
25934
- }
25935
- function slugify(text9) {
25936
- return text9.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "").slice(0, 50);
25937
- }
25938
- function checkFileExists(slug, force) {
25939
- const filePath = join15(process.cwd(), "wraps", "workflows", `${slug}.ts`);
25940
- if (existsSync14(filePath) && !force) {
25941
- if (isJsonMode()) {
25942
- jsonError("email.workflows.generate", {
25943
- code: "FILE_EXISTS",
25944
- message: `wraps/workflows/${slug}.ts already exists. Use --force to overwrite.`
25945
- });
25946
- } else {
25947
- log29.error(
25948
- `${pc31.cyan(`wraps/workflows/${slug}.ts`)} already exists. Use ${pc31.bold("--force")} to overwrite.`
25949
- );
25950
- }
25951
- return false;
25952
- }
25953
- return true;
25954
- }
25955
- async function callGenerateApi(description, slug, token, progress) {
25956
- progress.start("Generating workflow from description");
25957
- const apiBase = getApiBaseUrl();
25958
- const resp = await fetch(`${apiBase}/v1/workflows/generate`, {
25959
- method: "POST",
25960
- headers: {
25961
- "Content-Type": "application/json",
25962
- Authorization: `Bearer ${token}`
25963
- },
25964
- body: JSON.stringify({ description, slug })
25965
- });
25966
- if (!resp.ok) {
25967
- progress.fail("Generation failed");
25968
- if (resp.status === 429) {
25969
- throw errors.aiUsageLimitReached();
25970
- }
25971
- const body = await resp.text();
25972
- let message;
25973
- try {
25974
- const parsed = JSON.parse(body);
25975
- message = parsed.error || parsed.message || body;
25976
- } catch {
25977
- message = body;
25978
- }
25979
- throw errors.workflowGenerationFailed(message);
25980
- }
25981
- const data = await resp.json();
25982
- progress.succeed("Workflow generated");
25983
- return data.code;
25984
- }
25985
- function showDryRun(slug, code) {
25986
- if (isJsonMode()) {
25987
- jsonSuccess("email.workflows.generate", {
25988
- mode: "llm",
25989
- dryRun: true,
25990
- slug,
25991
- code
25992
- });
25993
- } else {
25994
- console.log();
25995
- log29.info(pc31.bold("Dry run \u2014 no file written"));
25996
- console.log();
25997
- console.log(pc31.dim("\u2500".repeat(60)));
25998
- console.log(code);
25999
- console.log(pc31.dim("\u2500".repeat(60)));
26000
- console.log();
26001
- }
26002
- }
26003
- async function showPreviewAndConfirm(slug, code) {
26004
- console.log();
26005
- console.log(pc31.dim("\u2500".repeat(60)));
26006
- console.log(code);
26007
- console.log(pc31.dim("\u2500".repeat(60)));
26008
- console.log();
26009
- const confirmed = await confirm14({
26010
- message: `Write to ${pc31.cyan(`wraps/workflows/${slug}.ts`)}?`,
26011
- initialValue: true
26012
- });
26013
- if (isCancel20(confirmed) || !confirmed) {
26014
- cancel21("Generation cancelled.");
26015
- return false;
26016
- }
26017
- return true;
26018
- }
26019
- function writeWorkflowFile(slug, code) {
26020
- const workflowsDir = join15(process.cwd(), "wraps", "workflows");
26021
- mkdirSync2(workflowsDir, { recursive: true });
26022
- writeFileSync(join15(workflowsDir, `${slug}.ts`), code, "utf-8");
26023
- }
26024
- function showNextSteps2(slug, isLlm = false) {
25043
+ function showNextSteps2(slug) {
26025
25044
  console.log();
26026
25045
  console.log(` ${pc31.bold("Next steps:")}`);
26027
- if (isLlm) {
26028
- console.log(
26029
- ` 1. Review the generated workflow in ${pc31.cyan(`wraps/workflows/${slug}.ts`)}`
26030
- );
26031
- } else {
26032
- console.log(
26033
- ` 1. Edit template references in ${pc31.cyan(`wraps/workflows/${slug}.ts`)}`
26034
- );
26035
- }
25046
+ console.log(
25047
+ ` 1. Edit template references in ${pc31.cyan(`wraps/workflows/${slug}.ts`)}`
25048
+ );
26036
25049
  console.log(
26037
25050
  ` 2. Validate: ${pc31.cyan(`wraps email workflows validate --workflow ${slug}`)}`
26038
25051
  );
26039
25052
  console.log(` 3. Push: ${pc31.cyan("wraps email workflows push")}`);
26040
25053
  console.log();
26041
25054
  }
26042
- async function autoValidate(filePath, wrapsDir, slug, progress) {
26043
- try {
26044
- progress.start(`Validating ${pc31.cyan(slug)}`);
26045
- const parsed = await parseWorkflowTs(filePath, wrapsDir);
26046
- const transformed = transformWorkflow(parsed.definition);
26047
- const result = validateTransformedWorkflow(transformed);
26048
- const errs = result.errors.filter((e) => e.severity === "error");
26049
- const warnings = result.errors.filter((e) => e.severity === "warning");
26050
- if (errs.length === 0 && warnings.length === 0) {
26051
- progress.succeed(`${pc31.cyan(slug)} is valid`);
26052
- } else if (errs.length === 0) {
26053
- progress.succeed(
26054
- `${pc31.cyan(slug)} is valid with ${warnings.length} warning(s)`
26055
- );
26056
- } else {
26057
- progress.fail(
26058
- `${pc31.cyan(slug)} has ${errs.length} validation error(s) \u2014 review and fix manually`
26059
- );
26060
- }
26061
- } catch {
26062
- progress.info(
26063
- `Could not auto-validate ${pc31.cyan(slug)} \u2014 run ${pc31.cyan("wraps email workflows validate")} manually`
26064
- );
26065
- }
26066
- }
26067
25055
 
26068
25056
  // src/commands/email/workflows/init.ts
26069
25057
  init_esm_shims();
26070
25058
  init_events();
26071
25059
  init_json_output();
26072
25060
  init_output();
26073
- import { existsSync as existsSync15 } from "fs";
26074
- import { mkdir as mkdir8, readFile as readFile8, writeFile as writeFile10 } from "fs/promises";
26075
- 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";
26076
25064
  import * as clack30 from "@clack/prompts";
26077
25065
  import pc32 from "picocolors";
26078
25066
 
@@ -26425,177 +25413,1008 @@ wraps email workflows generate "description..." # Generate from AI descriptio
26425
25413
  \`\`\`
26426
25414
  `;
26427
25415
 
26428
- // src/commands/email/workflows/init.ts
26429
- var EXAMPLE_WORKFLOW = `import {
26430
- defineWorkflow,
26431
- sendEmail,
26432
- delay,
26433
- condition,
26434
- exit,
26435
- } from '@wraps.dev/client';
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
+ };
26436
26029
 
26437
- /**
26438
- * Welcome Sequence
26439
- *
26440
- * Send a welcome email when a contact is created,
26441
- * wait 1 day, then check if they activated.
26442
- * If not, send a follow-up with tips.
26443
- */
26444
- export default defineWorkflow({
26445
- name: 'Welcome Sequence',
26446
- trigger: {
26447
- type: 'contact_created',
26448
- },
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
+ }
26449
26043
 
26450
- steps: [
26451
- sendEmail('send-welcome', { template: 'welcome-email' }),
26452
- delay('wait-1-day', { days: 1 }),
26453
- condition('check-activated', {
26454
- field: 'contact.hasActivated',
26455
- operator: 'equals',
26456
- value: true,
26457
- branches: {
26458
- yes: [exit('already-active')],
26459
- no: [
26460
- sendEmail('send-tips', { template: 'getting-started-tips' }),
26461
- ],
26462
- },
26463
- }),
26464
- ],
26465
- });
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
+ }
26466
26052
  `;
26467
- async function workflowsInit(options) {
26468
- const startTime = Date.now();
26469
- const cwd = process.cwd();
26470
- const workflowsDir = join16(cwd, "wraps", "workflows");
26471
- if (!isJsonMode()) {
26472
- clack30.intro(pc32.bold("Workflows as Code"));
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
+ );
26473
26080
  }
26474
- const progress = new DeploymentProgress();
26475
- if (existsSync15(workflowsDir) && !options.force) {
26476
- const { readdir: readdir4 } = await import("fs/promises");
26477
- const files = await readdir4(workflowsDir);
26478
- const tsFiles = files.filter(
26479
- (f) => f.endsWith(".ts") && !f.startsWith("_")
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
+ }
26099
+
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));
26107
+ }
26108
+ if (localTemplateSlugs) {
26109
+ errors2.push(
26110
+ ...validateTemplateReferences(transformed.steps, localTemplateSlugs)
26480
26111
  );
26481
- if (tsFiles.length > 0 && !options.force) {
26482
- if (!isJsonMode()) {
26483
- clack30.log.warn(
26484
- `${pc32.cyan("wraps/workflows/")} already contains ${tsFiles.length} workflow file(s). Use ${pc32.bold("--force")} to overwrite.`
26485
- );
26486
- }
26487
- }
26488
26112
  }
26489
- progress.start("Creating wraps/workflows/ directory");
26490
- await mkdir8(workflowsDir, { recursive: true });
26491
- const configPath = join16(cwd, "wraps", "wraps.config.ts");
26492
- if (!existsSync15(configPath)) {
26493
- await writeFile10(configPath, generateMinimalConfig(), "utf-8");
26494
- } else {
26495
- const configContent = await readFile8(configPath, "utf-8");
26496
- if (!configContent.includes("workflowsDir")) {
26497
- const updated = configContent.replace(
26498
- /}\);(\s*)$/,
26499
- ` workflowsDir: './workflows',
26500
- });$1`
26501
- );
26502
- if (updated !== configContent) {
26503
- await writeFile10(configPath, updated, "utf-8");
26504
- }
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);
26505
26119
  }
26506
26120
  }
26507
- const filesCreated = [];
26508
- if (!options.noExample) {
26509
- const examplePath = join16(workflowsDir, "welcome.ts");
26510
- if (!existsSync15(examplePath) || options.force) {
26511
- await writeFile10(examplePath, EXAMPLE_WORKFLOW, "utf-8");
26512
- 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
+ });
26513
26161
  }
26514
26162
  }
26515
- progress.succeed("Workflows directory ready");
26516
- if (!options.noClaude) {
26517
- try {
26518
- progress.start("Scaffolding Claude Code context");
26519
- await scaffoldClaudeMdSection({
26520
- projectDir: cwd,
26521
- sectionId: "workflows",
26522
- 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"
26523
26169
  });
26524
- filesCreated.push(".claude/CLAUDE.md");
26525
- await scaffoldClaudeSkill({
26526
- projectDir: cwd,
26527
- skillName: "wraps-workflows",
26528
- 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"
26529
26175
  });
26530
- filesCreated.push(".claude/skills/wraps-workflows/SKILL.md");
26531
- progress.succeed("Claude Code context scaffolded");
26532
- } catch {
26533
- progress.info(
26534
- "Could not scaffold .claude/ context \u2014 workflow files are still ready"
26535
- );
26536
26176
  }
26537
26177
  }
26538
- trackCommand("email:workflows:init", {
26539
- success: true,
26540
- duration_ms: Date.now() - startTime
26541
- });
26542
- if (isJsonMode()) {
26543
- jsonSuccess("email.workflows.init", {
26544
- dir: "wraps/workflows",
26545
- 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"
26546
26293
  });
26547
- return;
26548
26294
  }
26549
- console.log();
26550
- clack30.log.success(pc32.green("Workflows as Code initialized!"));
26551
- console.log();
26552
- console.log(` ${pc32.dim("Directory:")} ${pc32.cyan("wraps/workflows/")}`);
26553
- if (!options.noExample) {
26554
- console.log(
26555
- ` ${pc32.dim("Example:")} ${pc32.cyan("wraps/workflows/welcome.ts")}`
26556
- );
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
+ });
26557
26306
  }
26558
- if (!options.noClaude) {
26559
- console.log(
26560
- ` ${pc32.dim("AI Context:")} ${pc32.cyan(".claude/skills/wraps-workflows/")}`
26561
- );
26307
+ if (!config2.operator) {
26308
+ errors2.push({
26309
+ nodeId,
26310
+ field: "operator",
26311
+ message: "Condition operator is required",
26312
+ severity: "error"
26313
+ });
26562
26314
  }
26563
- console.log();
26564
- console.log(`${pc32.bold("Next steps:")}`);
26565
- console.log(
26566
- ` 1. Edit or create workflows in ${pc32.cyan("wraps/workflows/")}`
26567
- );
26568
- console.log(
26569
- ` 2. Validate: ${pc32.cyan("wraps email workflows validate")}`
26570
- );
26571
- console.log(` 3. Push: ${pc32.cyan("wraps email workflows push")}`);
26572
- if (!options.noClaude) {
26573
- console.log(
26574
- ` 4. Use Claude Code to generate workflows from descriptions`
26575
- );
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
+ });
26576
26322
  }
26577
- console.log();
26323
+ return errors2;
26578
26324
  }
26579
- function generateMinimalConfig() {
26580
- return `import { defineConfig } from '@wraps.dev/client';
26581
-
26582
- export default defineConfig({
26583
- org: 'my-org',
26584
- // from: { email: 'hello@yourapp.com', name: 'My App' },
26585
- // region: 'us-east-1',
26586
- templatesDir: './templates',
26587
- workflowsDir: './workflows',
26588
- });
26589
- `;
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;
26590
26415
  }
26591
26416
 
26592
26417
  // src/commands/email/workflows/push.ts
26593
- init_esm_shims();
26594
- init_events();
26595
- import { existsSync as existsSync16 } from "fs";
26596
- import { join as join17 } from "path";
26597
- import * as clack31 from "@clack/prompts";
26598
- import pc33 from "picocolors";
26599
26418
  init_config();
26600
26419
  init_errors();
26601
26420
  init_json_output();
@@ -27628,15 +27447,15 @@ import {
27628
27447
  IAMClient as IAMClient2,
27629
27448
  PutRolePolicyCommand
27630
27449
  } from "@aws-sdk/client-iam";
27631
- 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";
27632
27451
  import * as pulumi21 from "@pulumi/pulumi";
27633
27452
  import pc37 from "picocolors";
27634
27453
  init_events();
27635
27454
  init_aws();
27636
27455
  init_config();
27637
27456
  init_fs();
27638
- init_metadata();
27639
27457
  init_json_output();
27458
+ init_metadata();
27640
27459
  init_output();
27641
27460
  init_prompts();
27642
27461
  function buildConsolePolicyDocument(emailConfig, smsConfig) {
@@ -27979,7 +27798,7 @@ async function resolveOrganization() {
27979
27798
  hint: org.slug
27980
27799
  }))
27981
27800
  });
27982
- if (isCancel21(selected)) {
27801
+ if (isCancel20(selected)) {
27983
27802
  outro19("Operation cancelled");
27984
27803
  process.exit(0);
27985
27804
  }
@@ -28041,11 +27860,11 @@ async function authenticatedConnect(token, options) {
28041
27860
  "Event tracking must be enabled to connect to the Wraps Platform."
28042
27861
  );
28043
27862
  }
28044
- const enableTracking = options.yes || await confirm15({
27863
+ const enableTracking = options.yes || await confirm14({
28045
27864
  message: "Enable event tracking now?",
28046
27865
  initialValue: true
28047
27866
  });
28048
- if (isCancel21(enableTracking) || !enableTracking) {
27867
+ if (isCancel20(enableTracking) || !enableTracking) {
28049
27868
  outro19("Platform connection cancelled.");
28050
27869
  process.exit(0);
28051
27870
  }
@@ -28235,11 +28054,11 @@ Run ${pc37.cyan("wraps email init")} or ${pc37.cyan("wraps sms init")} first.
28235
28054
  log33.info(
28236
28055
  "Enabling event tracking will allow SES events to be streamed to the dashboard."
28237
28056
  );
28238
- const enableEventTracking = await confirm15({
28057
+ const enableEventTracking = await confirm14({
28239
28058
  message: "Enable event tracking now?",
28240
28059
  initialValue: true
28241
28060
  });
28242
- if (isCancel21(enableEventTracking) || !enableEventTracking) {
28061
+ if (isCancel20(enableEventTracking) || !enableEventTracking) {
28243
28062
  outro19("Platform connection cancelled.");
28244
28063
  process.exit(0);
28245
28064
  }
@@ -28287,18 +28106,18 @@ Run ${pc37.cyan("wraps email init")} or ${pc37.cyan("wraps sms init")} first.
28287
28106
  }
28288
28107
  ]
28289
28108
  });
28290
- if (isCancel21(action)) {
28109
+ if (isCancel20(action)) {
28291
28110
  outro19("Operation cancelled");
28292
28111
  process.exit(0);
28293
28112
  }
28294
28113
  if (action === "keep") {
28295
28114
  webhookSecret = existingSecret;
28296
28115
  } else if (action === "disconnect") {
28297
- const confirmDisconnect = await confirm15({
28116
+ const confirmDisconnect = await confirm14({
28298
28117
  message: "Are you sure? Events will no longer be sent to the Wraps Platform.",
28299
28118
  initialValue: false
28300
28119
  });
28301
- if (isCancel21(confirmDisconnect) || !confirmDisconnect) {
28120
+ if (isCancel20(confirmDisconnect) || !confirmDisconnect) {
28302
28121
  outro19("Disconnect cancelled");
28303
28122
  process.exit(0);
28304
28123
  }
@@ -28509,7 +28328,7 @@ import {
28509
28328
  GetRoleCommand as GetRoleCommand2,
28510
28329
  IAMClient as IAMClient3
28511
28330
  } from "@aws-sdk/client-iam";
28512
- 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";
28513
28332
  import pc39 from "picocolors";
28514
28333
  async function updateRole(options) {
28515
28334
  const startTime = Date.now();
@@ -28570,11 +28389,11 @@ Run ${pc39.cyan("wraps email init")} to deploy infrastructure first.
28570
28389
  if (!options.force) {
28571
28390
  progress.stop();
28572
28391
  const actionLabel = roleExists2 ? "Update" : "Create";
28573
- const shouldContinue = await confirm16({
28392
+ const shouldContinue = await confirm15({
28574
28393
  message: `${actionLabel} IAM role ${pc39.cyan(roleName)} with latest permissions?`,
28575
28394
  initialValue: true
28576
28395
  });
28577
- if (isCancel22(shouldContinue) || !shouldContinue) {
28396
+ if (isCancel21(shouldContinue) || !shouldContinue) {
28578
28397
  outro20(`${actionLabel} cancelled`);
28579
28398
  process.exit(0);
28580
28399
  }
@@ -32699,8 +32518,8 @@ async function deleteSMSProtectConfigurationWithSDK(region) {
32699
32518
  init_events();
32700
32519
  init_aws();
32701
32520
  init_errors();
32702
- init_json_output();
32703
32521
  init_fs();
32522
+ init_json_output();
32704
32523
  init_metadata();
32705
32524
  init_output();
32706
32525
  async function smsDestroy(options) {
@@ -32931,8 +32750,8 @@ import pc44 from "picocolors";
32931
32750
  init_events();
32932
32751
  init_aws();
32933
32752
  init_errors();
32934
- init_json_output();
32935
32753
  init_fs();
32754
+ init_json_output();
32936
32755
  init_metadata();
32937
32756
  init_output();
32938
32757
  init_prompts();
@@ -34113,8 +33932,8 @@ import pc47 from "picocolors";
34113
33932
  init_events();
34114
33933
  init_aws();
34115
33934
  init_errors();
34116
- init_json_output();
34117
33935
  init_fs();
33936
+ init_json_output();
34118
33937
  init_metadata();
34119
33938
  init_output();
34120
33939
  async function smsSync(options) {
@@ -34548,8 +34367,8 @@ import pc49 from "picocolors";
34548
34367
  init_events();
34549
34368
  init_aws();
34550
34369
  init_errors();
34551
- init_json_output();
34552
34370
  init_fs();
34371
+ init_json_output();
34553
34372
  init_metadata();
34554
34373
  init_output();
34555
34374
  init_prompts();
@@ -36327,7 +36146,7 @@ function showHelp() {
36327
36146
  ` ${pc54.cyan("email workflows push")} Push workflows to dashboard`
36328
36147
  );
36329
36148
  console.log(
36330
- ` ${pc54.cyan("email workflows generate")} Generate workflow from template or AI
36149
+ ` ${pc54.cyan("email workflows generate")} Generate workflow from template
36331
36150
  `
36332
36151
  );
36333
36152
  console.log("SMS Commands:");
@@ -37105,14 +36924,10 @@ Available commands: ${pc54.cyan("init")}, ${pc54.cyan("push")}, ${pc54.cyan("pre
37105
36924
  break;
37106
36925
  case "generate":
37107
36926
  await workflowsGenerate({
37108
- description: args.sub[3],
37109
36927
  template: flags.template,
37110
36928
  name: flags.name,
37111
- dryRun: flags.dryRun,
37112
- yes: flags.yes,
37113
36929
  force: flags.force,
37114
- json: flags.json,
37115
- token: flags.token
36930
+ json: flags.json
37116
36931
  });
37117
36932
  break;
37118
36933
  default: