@wraps.dev/cli 2.14.3 → 2.14.5
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 +1216 -1368
- package/dist/cli.js.map +1 -1
- package/dist/lambda/event-processor/.bundled +1 -1
- package/dist/lambda/event-processor/index.js +1 -1
- package/dist/lambda/event-processor/index.ts +1 -1
- package/dist/lambda/inbound-processor/.bundled +1 -1
- package/dist/lambda/inbound-processor/index.js +1 -1
- package/dist/lambda/inbound-processor/index.ts +1 -1
- package/dist/lambda/sms-event-processor/.bundled +1 -1
- package/dist/lambda/sms-event-processor/index.js +1 -1
- package/dist/lambda/sms-event-processor/index.ts +13 -14
- package/package.json +1 -1
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
|
}
|
|
@@ -22226,7 +22214,7 @@ function renderViewerPage(compiled, allSlugs) {
|
|
|
22226
22214
|
try {
|
|
22227
22215
|
const doc = iframe.contentDocument || iframe.contentWindow.document;
|
|
22228
22216
|
iframe.style.height = doc.documentElement.scrollHeight + 'px';
|
|
22229
|
-
} catch {} //
|
|
22217
|
+
} catch {} // baseline:allow-no-swallowed-errors \u2014 cross-origin iframe resize
|
|
22230
22218
|
}
|
|
22231
22219
|
iframe.addEventListener('load', resizeIframe);
|
|
22232
22220
|
|
|
@@ -22237,7 +22225,7 @@ function renderViewerPage(compiled, allSlugs) {
|
|
|
22237
22225
|
const data = JSON.parse(e.data);
|
|
22238
22226
|
// Reload iframe (and toolbar metadata by reloading the page)
|
|
22239
22227
|
iframe.src = '/${compiled.slug}/render?t=' + Date.now();
|
|
22240
|
-
} catch { //
|
|
22228
|
+
} catch { // baseline:allow-no-swallowed-errors \u2014 SSE parse error is expected
|
|
22241
22229
|
// "connected" message or parse error \u2014 ignore
|
|
22242
22230
|
}
|
|
22243
22231
|
};
|
|
@@ -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
|
|
|
@@ -26440,163 +25422,999 @@ var EXAMPLE_WORKFLOW = `import {
|
|
|
26440
25422
|
exit,
|
|
26441
25423
|
} from '@wraps.dev/client';
|
|
26442
25424
|
|
|
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
|
-
},
|
|
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
|
+
};
|
|
26455
26029
|
|
|
26456
|
-
|
|
26457
|
-
|
|
26458
|
-
|
|
26459
|
-
|
|
26460
|
-
|
|
26461
|
-
|
|
26462
|
-
|
|
26463
|
-
|
|
26464
|
-
|
|
26465
|
-
|
|
26466
|
-
|
|
26467
|
-
|
|
26468
|
-
|
|
26469
|
-
|
|
26470
|
-
|
|
26471
|
-
|
|
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
|
+
}
|
|
26043
|
+
|
|
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
|
+
}
|
|
26472
26052
|
`;
|
|
26473
|
-
|
|
26474
|
-
|
|
26475
|
-
|
|
26476
|
-
|
|
26477
|
-
|
|
26478
|
-
|
|
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
|
+
);
|
|
26479
26080
|
}
|
|
26480
|
-
|
|
26481
|
-
|
|
26482
|
-
|
|
26483
|
-
|
|
26484
|
-
|
|
26485
|
-
|
|
26081
|
+
if (!definition.name) {
|
|
26082
|
+
throw new Error("Workflow definition must have a 'name' property");
|
|
26083
|
+
}
|
|
26084
|
+
if (!definition.trigger) {
|
|
26085
|
+
throw new Error("Workflow definition must have a 'trigger' property");
|
|
26086
|
+
}
|
|
26087
|
+
if (!Array.isArray(definition.steps)) {
|
|
26088
|
+
throw new Error("Workflow definition must have a 'steps' array");
|
|
26089
|
+
}
|
|
26090
|
+
return {
|
|
26091
|
+
slug,
|
|
26092
|
+
filePath,
|
|
26093
|
+
source,
|
|
26094
|
+
sourceHash,
|
|
26095
|
+
definition,
|
|
26096
|
+
cliProjectPath: `workflows/${slug}.ts`
|
|
26097
|
+
};
|
|
26098
|
+
}
|
|
26099
|
+
|
|
26100
|
+
// src/utils/email/workflow-validator.ts
|
|
26101
|
+
init_esm_shims();
|
|
26102
|
+
function validateTransformedWorkflow(transformed, localTemplateSlugs) {
|
|
26103
|
+
const errors2 = [];
|
|
26104
|
+
errors2.push(...validateStructure(transformed.steps, transformed.transitions));
|
|
26105
|
+
for (const step of transformed.steps) {
|
|
26106
|
+
errors2.push(...validateStep(step));
|
|
26107
|
+
}
|
|
26108
|
+
if (localTemplateSlugs) {
|
|
26109
|
+
errors2.push(
|
|
26110
|
+
...validateTemplateReferences(transformed.steps, localTemplateSlugs)
|
|
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();
|
|
@@ -26758,7 +26576,10 @@ async function workflowsPush(options) {
|
|
|
26758
26576
|
return;
|
|
26759
26577
|
}
|
|
26760
26578
|
const token = await resolveTokenAsync({ token: options.token });
|
|
26761
|
-
const apiResults = await pushToAPI2(toProcess, token, progress,
|
|
26579
|
+
const apiResults = await pushToAPI2(toProcess, token, progress, {
|
|
26580
|
+
force: options.force,
|
|
26581
|
+
draft: options.draft
|
|
26582
|
+
});
|
|
26762
26583
|
for (const w of toProcess) {
|
|
26763
26584
|
const apiResult = apiResults.find((r) => r.slug === w.slug);
|
|
26764
26585
|
if (apiResult?.success) {
|
|
@@ -26775,12 +26596,15 @@ async function workflowsPush(options) {
|
|
|
26775
26596
|
await saveLockfile(wrapsDir, lockfile);
|
|
26776
26597
|
const pushed = apiResults.filter((r) => r.success);
|
|
26777
26598
|
const conflicts = apiResults.filter((r) => r.conflict);
|
|
26599
|
+
const drafts = pushed.filter((r) => r.status === "draft");
|
|
26600
|
+
const enabled = pushed.filter((r) => r.status !== "draft");
|
|
26778
26601
|
if (isJsonMode()) {
|
|
26779
26602
|
if (conflicts.length === 0) {
|
|
26780
26603
|
jsonSuccess("email.workflows.push", {
|
|
26781
26604
|
pushed: pushed.map((r) => ({
|
|
26782
26605
|
slug: r.slug,
|
|
26783
|
-
id: r.id
|
|
26606
|
+
id: r.id,
|
|
26607
|
+
status: r.status
|
|
26784
26608
|
})),
|
|
26785
26609
|
unchanged,
|
|
26786
26610
|
conflicts: []
|
|
@@ -26793,9 +26617,14 @@ async function workflowsPush(options) {
|
|
|
26793
26617
|
}
|
|
26794
26618
|
} else {
|
|
26795
26619
|
console.log();
|
|
26796
|
-
if (
|
|
26620
|
+
if (enabled.length > 0) {
|
|
26621
|
+
clack31.log.success(
|
|
26622
|
+
pc33.green(`${enabled.length} workflow(s) pushed and enabled`)
|
|
26623
|
+
);
|
|
26624
|
+
}
|
|
26625
|
+
if (drafts.length > 0) {
|
|
26797
26626
|
clack31.log.success(
|
|
26798
|
-
pc33.green(`${
|
|
26627
|
+
pc33.green(`${drafts.length} workflow(s) pushed as draft`)
|
|
26799
26628
|
);
|
|
26800
26629
|
}
|
|
26801
26630
|
if (unchanged.length > 0) {
|
|
@@ -26819,7 +26648,7 @@ async function workflowsPush(options) {
|
|
|
26819
26648
|
conflict_count: conflicts.length
|
|
26820
26649
|
});
|
|
26821
26650
|
}
|
|
26822
|
-
async function pushToAPI2(workflows, token, progress,
|
|
26651
|
+
async function pushToAPI2(workflows, token, progress, options) {
|
|
26823
26652
|
if (!token) {
|
|
26824
26653
|
progress.info(
|
|
26825
26654
|
"No API token \u2014 skipping dashboard sync. Run: wraps auth login"
|
|
@@ -26851,7 +26680,8 @@ async function pushToAPI2(workflows, token, progress, force) {
|
|
|
26851
26680
|
settings: w.transformed.settings,
|
|
26852
26681
|
defaults: w.transformed.defaults,
|
|
26853
26682
|
cliProjectPath: w.parsed.cliProjectPath,
|
|
26854
|
-
force: force ?? false
|
|
26683
|
+
force: options.force ?? false,
|
|
26684
|
+
draft: options.draft ?? false
|
|
26855
26685
|
}))
|
|
26856
26686
|
})
|
|
26857
26687
|
});
|
|
@@ -26861,7 +26691,12 @@ async function pushToAPI2(workflows, token, progress, force) {
|
|
|
26861
26691
|
results.push({ slug: c.slug, success: false, conflict: true });
|
|
26862
26692
|
}
|
|
26863
26693
|
for (const r of data.results ?? []) {
|
|
26864
|
-
results.push({
|
|
26694
|
+
results.push({
|
|
26695
|
+
slug: r.slug,
|
|
26696
|
+
id: r.id,
|
|
26697
|
+
status: r.status,
|
|
26698
|
+
success: true
|
|
26699
|
+
});
|
|
26865
26700
|
}
|
|
26866
26701
|
const successCount = data.results?.length ?? 0;
|
|
26867
26702
|
const conflictCount = data.conflicts?.length ?? 0;
|
|
@@ -26882,7 +26717,12 @@ async function pushToAPI2(workflows, token, progress, force) {
|
|
|
26882
26717
|
} else if (resp.ok) {
|
|
26883
26718
|
const data = await resp.json();
|
|
26884
26719
|
for (const r of data.results) {
|
|
26885
|
-
results.push({
|
|
26720
|
+
results.push({
|
|
26721
|
+
slug: r.slug,
|
|
26722
|
+
id: r.id,
|
|
26723
|
+
status: r.status,
|
|
26724
|
+
success: true
|
|
26725
|
+
});
|
|
26886
26726
|
}
|
|
26887
26727
|
progress.succeed(`Synced ${workflows.length} workflows to dashboard`);
|
|
26888
26728
|
} else {
|
|
@@ -26919,7 +26759,8 @@ async function pushToAPI2(workflows, token, progress, force) {
|
|
|
26919
26759
|
settings: w.transformed.settings,
|
|
26920
26760
|
defaults: w.transformed.defaults,
|
|
26921
26761
|
cliProjectPath: w.parsed.cliProjectPath,
|
|
26922
|
-
force: force ?? false
|
|
26762
|
+
force: options.force ?? false,
|
|
26763
|
+
draft: options.draft ?? false
|
|
26923
26764
|
})
|
|
26924
26765
|
});
|
|
26925
26766
|
if (Number(resp.status) === 409) {
|
|
@@ -26929,7 +26770,12 @@ async function pushToAPI2(workflows, token, progress, force) {
|
|
|
26929
26770
|
);
|
|
26930
26771
|
} else if (resp.ok) {
|
|
26931
26772
|
const data = await resp.json();
|
|
26932
|
-
results.push({
|
|
26773
|
+
results.push({
|
|
26774
|
+
slug: data.slug,
|
|
26775
|
+
id: data.id,
|
|
26776
|
+
status: data.status,
|
|
26777
|
+
success: true
|
|
26778
|
+
});
|
|
26933
26779
|
progress.succeed(`Synced ${pc33.cyan(w.slug)} to dashboard`);
|
|
26934
26780
|
} else {
|
|
26935
26781
|
const body = await resp.text();
|
|
@@ -27629,7 +27475,7 @@ import {
|
|
|
27629
27475
|
IAMClient as IAMClient2,
|
|
27630
27476
|
PutRolePolicyCommand
|
|
27631
27477
|
} from "@aws-sdk/client-iam";
|
|
27632
|
-
import { confirm as
|
|
27478
|
+
import { confirm as confirm14, intro as intro33, isCancel as isCancel20, log as log33, outro as outro19, select as select14 } from "@clack/prompts";
|
|
27633
27479
|
import * as pulumi21 from "@pulumi/pulumi";
|
|
27634
27480
|
import pc37 from "picocolors";
|
|
27635
27481
|
init_events();
|
|
@@ -27980,7 +27826,7 @@ async function resolveOrganization() {
|
|
|
27980
27826
|
hint: org.slug
|
|
27981
27827
|
}))
|
|
27982
27828
|
});
|
|
27983
|
-
if (
|
|
27829
|
+
if (isCancel20(selected)) {
|
|
27984
27830
|
outro19("Operation cancelled");
|
|
27985
27831
|
process.exit(0);
|
|
27986
27832
|
}
|
|
@@ -28042,11 +27888,11 @@ async function authenticatedConnect(token, options) {
|
|
|
28042
27888
|
"Event tracking must be enabled to connect to the Wraps Platform."
|
|
28043
27889
|
);
|
|
28044
27890
|
}
|
|
28045
|
-
const enableTracking = options.yes || await
|
|
27891
|
+
const enableTracking = options.yes || await confirm14({
|
|
28046
27892
|
message: "Enable event tracking now?",
|
|
28047
27893
|
initialValue: true
|
|
28048
27894
|
});
|
|
28049
|
-
if (
|
|
27895
|
+
if (isCancel20(enableTracking) || !enableTracking) {
|
|
28050
27896
|
outro19("Platform connection cancelled.");
|
|
28051
27897
|
process.exit(0);
|
|
28052
27898
|
}
|
|
@@ -28236,11 +28082,11 @@ Run ${pc37.cyan("wraps email init")} or ${pc37.cyan("wraps sms init")} first.
|
|
|
28236
28082
|
log33.info(
|
|
28237
28083
|
"Enabling event tracking will allow SES events to be streamed to the dashboard."
|
|
28238
28084
|
);
|
|
28239
|
-
const enableEventTracking = await
|
|
28085
|
+
const enableEventTracking = await confirm14({
|
|
28240
28086
|
message: "Enable event tracking now?",
|
|
28241
28087
|
initialValue: true
|
|
28242
28088
|
});
|
|
28243
|
-
if (
|
|
28089
|
+
if (isCancel20(enableEventTracking) || !enableEventTracking) {
|
|
28244
28090
|
outro19("Platform connection cancelled.");
|
|
28245
28091
|
process.exit(0);
|
|
28246
28092
|
}
|
|
@@ -28288,18 +28134,18 @@ Run ${pc37.cyan("wraps email init")} or ${pc37.cyan("wraps sms init")} first.
|
|
|
28288
28134
|
}
|
|
28289
28135
|
]
|
|
28290
28136
|
});
|
|
28291
|
-
if (
|
|
28137
|
+
if (isCancel20(action)) {
|
|
28292
28138
|
outro19("Operation cancelled");
|
|
28293
28139
|
process.exit(0);
|
|
28294
28140
|
}
|
|
28295
28141
|
if (action === "keep") {
|
|
28296
28142
|
webhookSecret = existingSecret;
|
|
28297
28143
|
} else if (action === "disconnect") {
|
|
28298
|
-
const confirmDisconnect = await
|
|
28144
|
+
const confirmDisconnect = await confirm14({
|
|
28299
28145
|
message: "Are you sure? Events will no longer be sent to the Wraps Platform.",
|
|
28300
28146
|
initialValue: false
|
|
28301
28147
|
});
|
|
28302
|
-
if (
|
|
28148
|
+
if (isCancel20(confirmDisconnect) || !confirmDisconnect) {
|
|
28303
28149
|
outro19("Disconnect cancelled");
|
|
28304
28150
|
process.exit(0);
|
|
28305
28151
|
}
|
|
@@ -28510,7 +28356,7 @@ import {
|
|
|
28510
28356
|
GetRoleCommand as GetRoleCommand2,
|
|
28511
28357
|
IAMClient as IAMClient3
|
|
28512
28358
|
} from "@aws-sdk/client-iam";
|
|
28513
|
-
import { confirm as
|
|
28359
|
+
import { confirm as confirm15, intro as intro35, isCancel as isCancel21, log as log34, outro as outro20 } from "@clack/prompts";
|
|
28514
28360
|
import pc39 from "picocolors";
|
|
28515
28361
|
async function updateRole(options) {
|
|
28516
28362
|
const startTime = Date.now();
|
|
@@ -28571,11 +28417,11 @@ Run ${pc39.cyan("wraps email init")} to deploy infrastructure first.
|
|
|
28571
28417
|
if (!options.force) {
|
|
28572
28418
|
progress.stop();
|
|
28573
28419
|
const actionLabel = roleExists2 ? "Update" : "Create";
|
|
28574
|
-
const shouldContinue = await
|
|
28420
|
+
const shouldContinue = await confirm15({
|
|
28575
28421
|
message: `${actionLabel} IAM role ${pc39.cyan(roleName)} with latest permissions?`,
|
|
28576
28422
|
initialValue: true
|
|
28577
28423
|
});
|
|
28578
|
-
if (
|
|
28424
|
+
if (isCancel21(shouldContinue) || !shouldContinue) {
|
|
28579
28425
|
outro20(`${actionLabel} cancelled`);
|
|
28580
28426
|
process.exit(0);
|
|
28581
28427
|
}
|
|
@@ -36328,7 +36174,7 @@ function showHelp() {
|
|
|
36328
36174
|
` ${pc54.cyan("email workflows push")} Push workflows to dashboard`
|
|
36329
36175
|
);
|
|
36330
36176
|
console.log(
|
|
36331
|
-
` ${pc54.cyan("email workflows generate")} Generate workflow from template
|
|
36177
|
+
` ${pc54.cyan("email workflows generate")} Generate workflow from template
|
|
36332
36178
|
`
|
|
36333
36179
|
);
|
|
36334
36180
|
console.log("SMS Commands:");
|
|
@@ -36612,6 +36458,11 @@ args.options([
|
|
|
36612
36458
|
description: "Preview changes without pushing",
|
|
36613
36459
|
defaultValue: false
|
|
36614
36460
|
},
|
|
36461
|
+
{
|
|
36462
|
+
name: "draft",
|
|
36463
|
+
description: "Push workflow as draft without enabling it",
|
|
36464
|
+
defaultValue: false
|
|
36465
|
+
},
|
|
36615
36466
|
{
|
|
36616
36467
|
name: "name",
|
|
36617
36468
|
description: "Output file slug for generated workflow",
|
|
@@ -37098,6 +36949,7 @@ Available commands: ${pc54.cyan("init")}, ${pc54.cyan("push")}, ${pc54.cyan("pre
|
|
|
37098
36949
|
await workflowsPush({
|
|
37099
36950
|
workflow: flags.workflow,
|
|
37100
36951
|
dryRun: flags.dryRun,
|
|
36952
|
+
draft: flags.draft,
|
|
37101
36953
|
force: flags.force,
|
|
37102
36954
|
yes: flags.yes,
|
|
37103
36955
|
json: flags.json,
|
|
@@ -37106,14 +36958,10 @@ Available commands: ${pc54.cyan("init")}, ${pc54.cyan("push")}, ${pc54.cyan("pre
|
|
|
37106
36958
|
break;
|
|
37107
36959
|
case "generate":
|
|
37108
36960
|
await workflowsGenerate({
|
|
37109
|
-
description: args.sub[3],
|
|
37110
36961
|
template: flags.template,
|
|
37111
36962
|
name: flags.name,
|
|
37112
|
-
dryRun: flags.dryRun,
|
|
37113
|
-
yes: flags.yes,
|
|
37114
36963
|
force: flags.force,
|
|
37115
|
-
json: flags.json
|
|
37116
|
-
token: flags.token
|
|
36964
|
+
json: flags.json
|
|
37117
36965
|
});
|
|
37118
36966
|
break;
|
|
37119
36967
|
default:
|