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