@controllerai/cli 0.2.0
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/README.md +180 -0
- package/dist/api/client.js +330 -0
- package/dist/api/client.js.map +1 -0
- package/dist/appLinks.js +99 -0
- package/dist/appLinks.js.map +1 -0
- package/dist/argv.js +124 -0
- package/dist/argv.js.map +1 -0
- package/dist/commands/agent.js +813 -0
- package/dist/commands/agent.js.map +1 -0
- package/dist/commands/auth.js +327 -0
- package/dist/commands/auth.js.map +1 -0
- package/dist/commands/branch.js +441 -0
- package/dist/commands/branch.js.map +1 -0
- package/dist/commands/connect.js +148 -0
- package/dist/commands/connect.js.map +1 -0
- package/dist/commands/connection.js +148 -0
- package/dist/commands/connection.js.map +1 -0
- package/dist/commands/data.js +441 -0
- package/dist/commands/data.js.map +1 -0
- package/dist/commands/deps.js +63 -0
- package/dist/commands/deps.js.map +1 -0
- package/dist/commands/doctor.js +102 -0
- package/dist/commands/doctor.js.map +1 -0
- package/dist/commands/events.js +72 -0
- package/dist/commands/events.js.map +1 -0
- package/dist/commands/exec.js +306 -0
- package/dist/commands/exec.js.map +1 -0
- package/dist/commands/expr.js +346 -0
- package/dist/commands/expr.js.map +1 -0
- package/dist/commands/flow.js +447 -0
- package/dist/commands/flow.js.map +1 -0
- package/dist/commands/integration.js +102 -0
- package/dist/commands/integration.js.map +1 -0
- package/dist/commands/node.js +885 -0
- package/dist/commands/node.js.map +1 -0
- package/dist/commands/organize.js +234 -0
- package/dist/commands/organize.js.map +1 -0
- package/dist/commands/run.js +437 -0
- package/dist/commands/run.js.map +1 -0
- package/dist/commands/schema.js +269 -0
- package/dist/commands/schema.js.map +1 -0
- package/dist/commands/skills.js +1156 -0
- package/dist/commands/skills.js.map +1 -0
- package/dist/commands/trigger.js +992 -0
- package/dist/commands/trigger.js.map +1 -0
- package/dist/commands/workflow.js +503 -0
- package/dist/commands/workflow.js.map +1 -0
- package/dist/config.js +74 -0
- package/dist/config.js.map +1 -0
- package/dist/context.js +39 -0
- package/dist/context.js.map +1 -0
- package/dist/errors.js +59 -0
- package/dist/errors.js.map +1 -0
- package/dist/index.js +125 -0
- package/dist/index.js.map +1 -0
- package/dist/jsSource.js +32 -0
- package/dist/jsSource.js.map +1 -0
- package/dist/nodeProps.js +177 -0
- package/dist/nodeProps.js.map +1 -0
- package/dist/output.js +85 -0
- package/dist/output.js.map +1 -0
- package/dist/version.js +7 -0
- package/dist/version.js.map +1 -0
- package/package.json +41 -0
- package/skills/controllerai-agents/SKILL.md +169 -0
- package/skills/controllerai-agents/agents/openai.yaml +4 -0
- package/skills/controllerai-agents/references/instructions-and-versioning.md +76 -0
- package/skills/controllerai-ai-nodes/SKILL.md +83 -0
- package/skills/controllerai-ai-nodes/agents/openai.yaml +4 -0
- package/skills/controllerai-ai-nodes/references/selection-and-testing.md +46 -0
- package/skills/controllerai-data/SKILL.md +121 -0
- package/skills/controllerai-data/agents/openai.yaml +4 -0
- package/skills/controllerai-data/references/data-rails.md +46 -0
- package/skills/controllerai-expressions/SKILL.md +115 -0
- package/skills/controllerai-expressions/agents/openai.yaml +4 -0
- package/skills/controllerai-expressions/references/dsl-and-recipes.md +120 -0
- package/skills/controllerai-flow-control/SKILL.md +132 -0
- package/skills/controllerai-flow-control/agents/openai.yaml +4 -0
- package/skills/controllerai-flow-control/references/patterns.md +61 -0
- package/skills/controllerai-integration-actions/SKILL.md +95 -0
- package/skills/controllerai-integration-actions/agents/openai.yaml +4 -0
- package/skills/controllerai-node-config/SKILL.md +138 -0
- package/skills/controllerai-node-config/agents/openai.yaml +4 -0
- package/skills/controllerai-node-config/references/prop-and-readiness.md +72 -0
- package/skills/controllerai-runs/SKILL.md +109 -0
- package/skills/controllerai-runs/agents/openai.yaml +4 -0
- package/skills/controllerai-runs/references/debugging.md +53 -0
- package/skills/controllerai-start/SKILL.md +44 -0
- package/skills/controllerai-start/agents/openai.yaml +4 -0
- package/skills/controllerai-triggers/SKILL.md +174 -0
- package/skills/controllerai-triggers/agents/openai.yaml +4 -0
- package/skills/controllerai-triggers/references/lifecycle.md +54 -0
- package/skills/controllerai-workflows/SKILL.md +86 -0
- package/skills/controllerai-workflows/agents/openai.yaml +4 -0
- package/skills/controllerai-workflows/references/command-map.md +64 -0
- package/skills/manifest.json +248 -0
|
@@ -0,0 +1,992 @@
|
|
|
1
|
+
import { readFileSync } from 'node:fs';
|
|
2
|
+
import { requireContext } from '../context.js';
|
|
3
|
+
import { userError } from '../errors.js';
|
|
4
|
+
import { emit, printKeyValues, printTable, truncate, warn } from '../output.js';
|
|
5
|
+
/**
|
|
6
|
+
* Trigger canvases. A trigger targets exactly one flow. Real events execute
|
|
7
|
+
* LIVE after publish; explicit one-shot replay can verify either DEV or LIVE.
|
|
8
|
+
*/
|
|
9
|
+
/**
|
|
10
|
+
* `create_trigger.sourceBasis` is a must-ask gate enforced in the backend.
|
|
11
|
+
* Do not advertise its deliberately rejected "assumption" sentinel as a valid
|
|
12
|
+
* CLI choice: the user must answer before the command can proceed.
|
|
13
|
+
*/
|
|
14
|
+
const SOURCE_BASES = [
|
|
15
|
+
'explicit-in-request',
|
|
16
|
+
'user-answer',
|
|
17
|
+
'inferred-from-existing-workflow',
|
|
18
|
+
];
|
|
19
|
+
export const assertSourceBasis = (value) => {
|
|
20
|
+
if (!SOURCE_BASES.includes(value)) {
|
|
21
|
+
throw userError(`--source-basis must be one of ${SOURCE_BASES.join(', ')}. It declares what the trigger ` +
|
|
22
|
+
'source choice is based on; the backend rejects the call without it.', 'BAD_SOURCE_BASIS');
|
|
23
|
+
}
|
|
24
|
+
return value;
|
|
25
|
+
};
|
|
26
|
+
export const assertReplayTarget = (value, confirmLive) => {
|
|
27
|
+
if (value !== 'dev' && value !== 'live') {
|
|
28
|
+
throw userError('--target must be "dev" or "live".', 'BAD_REPLAY_TARGET');
|
|
29
|
+
}
|
|
30
|
+
if (value === 'live' && !confirmLive) {
|
|
31
|
+
throw userError('LIVE replay runs the published workflow and can perform real external actions. ' +
|
|
32
|
+
'Retry with `--target live --confirm-live` to acknowledge that explicitly.', 'LIVE_REPLAY_CONFIRMATION_REQUIRED');
|
|
33
|
+
}
|
|
34
|
+
if (value === 'dev' && confirmLive) {
|
|
35
|
+
throw userError('--confirm-live is only valid with `--target live`.', 'UNUSED_LIVE_CONFIRMATION');
|
|
36
|
+
}
|
|
37
|
+
return value;
|
|
38
|
+
};
|
|
39
|
+
export const assertTriggerTestEventsMode = (allow, disallow, acknowledgeDualRun) => {
|
|
40
|
+
if (allow === disallow) {
|
|
41
|
+
throw userError(allow
|
|
42
|
+
? '--allow and --disallow are mutually exclusive.'
|
|
43
|
+
: 'Choose exactly one of --allow or --disallow.', allow ? 'CONFLICTING_TEST_EVENTS_MODE' : 'MISSING_TEST_EVENTS_MODE');
|
|
44
|
+
}
|
|
45
|
+
if (!allow && acknowledgeDualRun) {
|
|
46
|
+
throw userError('--acknowledge-dual-run is only valid with --allow.', 'UNUSED_DUAL_RUN_ACKNOWLEDGEMENT');
|
|
47
|
+
}
|
|
48
|
+
return allow;
|
|
49
|
+
};
|
|
50
|
+
export const readJsonOption = (source, flag) => {
|
|
51
|
+
let raw;
|
|
52
|
+
try {
|
|
53
|
+
raw =
|
|
54
|
+
source === '-'
|
|
55
|
+
? readFileSync(0, 'utf8')
|
|
56
|
+
: /^[{[]/.test(source.trimStart())
|
|
57
|
+
? source
|
|
58
|
+
: readFileSync(source, 'utf8');
|
|
59
|
+
}
|
|
60
|
+
catch (error) {
|
|
61
|
+
throw userError(`${flag} "${source}" is not a readable file and does not look like inline JSON ` +
|
|
62
|
+
`(inline JSON must start with "{" or "["): ${error instanceof Error ? error.message : error}`, 'UNREADABLE_JSON_OPTION');
|
|
63
|
+
}
|
|
64
|
+
let parsed;
|
|
65
|
+
try {
|
|
66
|
+
parsed = JSON.parse(raw);
|
|
67
|
+
}
|
|
68
|
+
catch (error) {
|
|
69
|
+
throw userError(`${flag} is neither valid JSON nor a readable file: ${error instanceof Error ? error.message : error}`, 'BAD_JSON_OPTION');
|
|
70
|
+
}
|
|
71
|
+
if (!parsed || typeof parsed !== 'object' || Array.isArray(parsed)) {
|
|
72
|
+
throw userError(`${flag} must be a JSON object.`, 'BAD_JSON_SHAPE');
|
|
73
|
+
}
|
|
74
|
+
return parsed;
|
|
75
|
+
};
|
|
76
|
+
/**
|
|
77
|
+
* The scheduled trigger's `cron` prop is an OBJECT (`{cron}` / `{intervalSeconds}`),
|
|
78
|
+
* not a string. The backend coerces a bare string, but only for `cron` — say so
|
|
79
|
+
* rather than letting a plausible-looking string deploy a broken schedule.
|
|
80
|
+
*/
|
|
81
|
+
export const normalizeTriggerProps = (props) => {
|
|
82
|
+
if (typeof props.cron === 'string') {
|
|
83
|
+
return { ...props, cron: { cron: props.cron } };
|
|
84
|
+
}
|
|
85
|
+
return props;
|
|
86
|
+
};
|
|
87
|
+
/**
|
|
88
|
+
* `get_trigger_component_props` publishes an `inputType` and nothing about the
|
|
89
|
+
* JSON shape that inputType expects. For most types the mapping is obvious;
|
|
90
|
+
* for TIMER it is not — `cron` is a prop whose VALUE is an object, and the
|
|
91
|
+
* nesting is invisible in every field the backend returns except a defaultValue
|
|
92
|
+
* that is easy to read as decoration. Naming the shape per prop is the whole
|
|
93
|
+
* difference between `trigger props` being a listing and being a contract.
|
|
94
|
+
*/
|
|
95
|
+
export const triggerPropValueShape = (prop) => {
|
|
96
|
+
const options = (prop?.options ?? []);
|
|
97
|
+
switch (prop?.inputType) {
|
|
98
|
+
case 'TIMER':
|
|
99
|
+
return 'object {cron?, intervalSeconds?, timezone?}';
|
|
100
|
+
case 'CHECKBOX':
|
|
101
|
+
return 'boolean';
|
|
102
|
+
case 'NUMBER':
|
|
103
|
+
return 'number';
|
|
104
|
+
case 'NUMBER_ARRAY':
|
|
105
|
+
return 'number[]';
|
|
106
|
+
case 'SHORT_TEXT_ARRAY':
|
|
107
|
+
case 'MULTI_SELECT_DROPDOWN':
|
|
108
|
+
case 'STATIC_MULTI_SELECT_DROPDOWN':
|
|
109
|
+
return 'string[]';
|
|
110
|
+
case 'OBJECT':
|
|
111
|
+
case 'JSON':
|
|
112
|
+
case 'CUSTOM_DATA':
|
|
113
|
+
return 'object';
|
|
114
|
+
case 'STATIC_DROPDOWN':
|
|
115
|
+
return options.length
|
|
116
|
+
? `string, one of: ${options.map(o => String(o.value)).join(' | ')}`
|
|
117
|
+
: 'string';
|
|
118
|
+
case 'DROPDOWN':
|
|
119
|
+
return options.length
|
|
120
|
+
? `string, one of: ${options.map(o => String(o.value)).join(' | ')}`
|
|
121
|
+
: 'string (dynamic — re-run with --configured to load options)';
|
|
122
|
+
default:
|
|
123
|
+
return 'string';
|
|
124
|
+
}
|
|
125
|
+
};
|
|
126
|
+
/** A value of the right SHAPE for a prop, so the printed example parses and deploys. */
|
|
127
|
+
export const triggerPropExampleValue = (prop) => {
|
|
128
|
+
if (prop?.defaultValue !== undefined && prop?.defaultValue !== null)
|
|
129
|
+
return prop.defaultValue;
|
|
130
|
+
const firstOption = (prop?.options ?? [])[0];
|
|
131
|
+
switch (prop?.inputType) {
|
|
132
|
+
case 'TIMER':
|
|
133
|
+
return { cron: '0 9 * * *', timezone: 'America/Chicago' };
|
|
134
|
+
case 'CHECKBOX':
|
|
135
|
+
return false;
|
|
136
|
+
case 'NUMBER':
|
|
137
|
+
return 0;
|
|
138
|
+
case 'NUMBER_ARRAY':
|
|
139
|
+
case 'SHORT_TEXT_ARRAY':
|
|
140
|
+
return [];
|
|
141
|
+
case 'MULTI_SELECT_DROPDOWN':
|
|
142
|
+
case 'STATIC_MULTI_SELECT_DROPDOWN':
|
|
143
|
+
return firstOption ? [firstOption.value] : [];
|
|
144
|
+
case 'OBJECT':
|
|
145
|
+
case 'JSON':
|
|
146
|
+
case 'CUSTOM_DATA':
|
|
147
|
+
return {};
|
|
148
|
+
default:
|
|
149
|
+
return firstOption ? firstOption.value : `<${prop?.name ?? 'value'}>`;
|
|
150
|
+
}
|
|
151
|
+
};
|
|
152
|
+
const OPTION_INPUT_TYPES = new Set([
|
|
153
|
+
'DROPDOWN',
|
|
154
|
+
'STATIC_DROPDOWN',
|
|
155
|
+
'MULTI_SELECT',
|
|
156
|
+
'MULTI_SELECT_DROPDOWN',
|
|
157
|
+
'STATIC_MULTI_SELECT_DROPDOWN',
|
|
158
|
+
'REMOTE_OPTIONS',
|
|
159
|
+
]);
|
|
160
|
+
const hasConfiguredTriggerValue = (configuredProps, name) => typeof name === 'string' &&
|
|
161
|
+
Object.prototype.hasOwnProperty.call(configuredProps, name) &&
|
|
162
|
+
configuredProps[name] !== undefined &&
|
|
163
|
+
configuredProps[name] !== null &&
|
|
164
|
+
configuredProps[name] !== '';
|
|
165
|
+
export const unresolvedTriggerOptionProps = (props, configuredProps = {}) => (props ?? []).filter(prop => {
|
|
166
|
+
if (!prop?.required || prop?.hidden)
|
|
167
|
+
return false;
|
|
168
|
+
if (!prop.remoteOptions && !OPTION_INPUT_TYPES.has(String(prop.inputType ?? '')))
|
|
169
|
+
return false;
|
|
170
|
+
if (hasConfiguredTriggerValue(configuredProps, prop.name))
|
|
171
|
+
return false;
|
|
172
|
+
if (prop.defaultValue !== undefined && prop.defaultValue !== null)
|
|
173
|
+
return false;
|
|
174
|
+
return !Array.isArray(prop.options) || prop.options.length === 0;
|
|
175
|
+
});
|
|
176
|
+
/**
|
|
177
|
+
* The `--props` object a caller can paste: every required, non-hidden prop with
|
|
178
|
+
* a shape-correct value. Hidden props are Pipedream's own reload plumbing and
|
|
179
|
+
* are never set by hand.
|
|
180
|
+
*/
|
|
181
|
+
export const triggerPropsExample = (props, configuredProps = {}) => {
|
|
182
|
+
const example = {};
|
|
183
|
+
const unresolved = new Set(unresolvedTriggerOptionProps(props, configuredProps));
|
|
184
|
+
for (const prop of props ?? []) {
|
|
185
|
+
if (!prop?.required || prop?.hidden || unresolved.has(prop))
|
|
186
|
+
continue;
|
|
187
|
+
example[prop.name] = hasConfiguredTriggerValue(configuredProps, prop.name)
|
|
188
|
+
? configuredProps[prop.name]
|
|
189
|
+
: triggerPropExampleValue(prop);
|
|
190
|
+
}
|
|
191
|
+
return example;
|
|
192
|
+
};
|
|
193
|
+
/**
|
|
194
|
+
* The create-ready prop object for the newest dynamic schema. Values already
|
|
195
|
+
* selected by the caller win over generated defaults; values for props that
|
|
196
|
+
* disappeared during a reload are deliberately discarded.
|
|
197
|
+
*/
|
|
198
|
+
export const triggerPropsForCreate = (props, configuredProps = {}) => {
|
|
199
|
+
const result = triggerPropsExample(props, configuredProps);
|
|
200
|
+
for (const prop of props ?? []) {
|
|
201
|
+
if (prop?.hidden || !hasConfiguredTriggerValue(configuredProps, prop?.name))
|
|
202
|
+
continue;
|
|
203
|
+
result[prop.name] = configuredProps[prop.name];
|
|
204
|
+
}
|
|
205
|
+
return result;
|
|
206
|
+
};
|
|
207
|
+
export const shellQuote = (value) => `'${value.replace(/'/g, "'\\''")}'`;
|
|
208
|
+
/** A complete, copyable dynamic-prop reload command for the current event. */
|
|
209
|
+
export const triggerPropsReloadCommand = (opts, configuredProps, dynamicPropsId) => `cai trigger props --node ${opts.node} --event ${opts.event}` +
|
|
210
|
+
(opts.connection ? ` --connection ${opts.connection}` : '') +
|
|
211
|
+
` --configured ${shellQuote(JSON.stringify(configuredProps))}` +
|
|
212
|
+
(dynamicPropsId ? ` --dynamic-props-id ${dynamicPropsId}` : '') +
|
|
213
|
+
' --json';
|
|
214
|
+
/** A complete option lookup that preserves the current dynamic-prop context. */
|
|
215
|
+
export const triggerOptionLookupCommand = (opts, propName, configuredProps, dynamicPropsId) => `cai trigger options --node ${opts.node} --event ${opts.event} --prop ${propName}` +
|
|
216
|
+
(opts.connection ? ` --connection ${opts.connection}` : '') +
|
|
217
|
+
(Object.keys(configuredProps).length
|
|
218
|
+
? ` --configured ${shellQuote(JSON.stringify(configuredProps))}`
|
|
219
|
+
: '') +
|
|
220
|
+
(dynamicPropsId ? ` --dynamic-props-id ${dynamicPropsId}` : '') +
|
|
221
|
+
' --json';
|
|
222
|
+
const parseOptionPage = (raw) => {
|
|
223
|
+
if (raw === undefined)
|
|
224
|
+
return undefined;
|
|
225
|
+
const page = Number(raw);
|
|
226
|
+
if (!Number.isInteger(page) || page < 0) {
|
|
227
|
+
throw userError('--page must be a non-negative integer.', 'BAD_OPTION_PAGE');
|
|
228
|
+
}
|
|
229
|
+
return page;
|
|
230
|
+
};
|
|
231
|
+
export const buildTriggerOptionToolInput = (opts) => ({
|
|
232
|
+
nodeSlug: opts.node,
|
|
233
|
+
eventKey: opts.event,
|
|
234
|
+
propName: opts.prop,
|
|
235
|
+
...(opts.connection ? { connectionId: Number(opts.connection) } : {}),
|
|
236
|
+
...(opts.configuredProps ? { configuredProps: opts.configuredProps } : {}),
|
|
237
|
+
...(opts.dynamicPropsId ? { dynamicPropsId: opts.dynamicPropsId } : {}),
|
|
238
|
+
...(opts.query ? { query: opts.query } : {}),
|
|
239
|
+
...(opts.page !== undefined ? { page: opts.page } : {}),
|
|
240
|
+
...(opts.prevContext ? { prevContext: opts.prevContext } : {}),
|
|
241
|
+
});
|
|
242
|
+
const numericBackendTriggerId = (value) => {
|
|
243
|
+
if (typeof value === 'number' && Number.isSafeInteger(value) && value > 0)
|
|
244
|
+
return value;
|
|
245
|
+
if (typeof value === 'string' && /^\d+$/.test(value) && Number(value) > 0)
|
|
246
|
+
return Number(value);
|
|
247
|
+
return null;
|
|
248
|
+
};
|
|
249
|
+
const directBackendTriggerId = (trigger) => numericBackendTriggerId(trigger?.identity?.backendTriggerId ?? trigger?.backendTriggerId);
|
|
250
|
+
export const triggerReadinessStage = (trigger) => {
|
|
251
|
+
if (typeof trigger?.readiness === 'string')
|
|
252
|
+
return trigger.readiness;
|
|
253
|
+
return trigger?.readiness?.stage ?? '-';
|
|
254
|
+
};
|
|
255
|
+
export const triggerIssueSummary = (trigger) => {
|
|
256
|
+
const issues = Array.isArray(trigger?.issues) ? trigger.issues : [];
|
|
257
|
+
if (!issues.length)
|
|
258
|
+
return 'none';
|
|
259
|
+
return issues.map((issue) => issue?.code ?? String(issue)).join(', ');
|
|
260
|
+
};
|
|
261
|
+
/** Normalize current lifecycle reads and the legacy flat read shape. */
|
|
262
|
+
export const normalizeTriggerRow = (trigger, fallbackBackendTriggerId, requestedCanvasTriggerId) => {
|
|
263
|
+
const triggerId = trigger?.identity?.canvasTriggerId ??
|
|
264
|
+
trigger?.id ??
|
|
265
|
+
trigger?.triggerId ??
|
|
266
|
+
requestedCanvasTriggerId;
|
|
267
|
+
const enabled = typeof trigger?.devCanvasEnabled === 'boolean'
|
|
268
|
+
? trigger.devCanvasEnabled
|
|
269
|
+
: typeof trigger?.deployment?.devCanvasEnabled === 'boolean'
|
|
270
|
+
? trigger.deployment.devCanvasEnabled
|
|
271
|
+
: typeof trigger?.enabled === 'boolean'
|
|
272
|
+
? trigger.enabled
|
|
273
|
+
: null;
|
|
274
|
+
const readiness = trigger?.readiness ?? null;
|
|
275
|
+
const issues = Array.isArray(trigger?.issues) ? trigger.issues : [];
|
|
276
|
+
return {
|
|
277
|
+
...trigger,
|
|
278
|
+
id: triggerId,
|
|
279
|
+
triggerId,
|
|
280
|
+
backendTriggerId: directBackendTriggerId(trigger) ?? numericBackendTriggerId(fallbackBackendTriggerId),
|
|
281
|
+
enabled,
|
|
282
|
+
devCanvasEnabled: enabled,
|
|
283
|
+
flowId: trigger?.flowId ?? null,
|
|
284
|
+
nodeCount: trigger?.nodeCount ?? (Array.isArray(trigger?.nodes) ? trigger.nodes.length : null),
|
|
285
|
+
presence: trigger?.presence ?? null,
|
|
286
|
+
readiness,
|
|
287
|
+
issues,
|
|
288
|
+
};
|
|
289
|
+
};
|
|
290
|
+
/**
|
|
291
|
+
* Lifecycle reads normally expose `identity.backendTriggerId` directly. Older
|
|
292
|
+
* backends and malformed lifecycle rows may not, so recover only those missing
|
|
293
|
+
* ids from the development canvas node's `data.triggerId` as a compatibility
|
|
294
|
+
* fallback.
|
|
295
|
+
*/
|
|
296
|
+
export const backendTriggerIds = async (api, workflowId) => {
|
|
297
|
+
const ids = new Map();
|
|
298
|
+
const state = await api.rest(`/workflow-agent/development-version-state/${workflowId}`);
|
|
299
|
+
if (!state?.json)
|
|
300
|
+
return ids;
|
|
301
|
+
let document;
|
|
302
|
+
try {
|
|
303
|
+
document = JSON.parse(state.json);
|
|
304
|
+
}
|
|
305
|
+
catch {
|
|
306
|
+
return ids;
|
|
307
|
+
}
|
|
308
|
+
for (const trigger of document?.triggers ?? []) {
|
|
309
|
+
for (const node of trigger?.nodes ?? []) {
|
|
310
|
+
const backendId = node?.data?.triggerId;
|
|
311
|
+
if (typeof backendId === 'number')
|
|
312
|
+
ids.set(trigger.id, backendId);
|
|
313
|
+
}
|
|
314
|
+
}
|
|
315
|
+
return ids;
|
|
316
|
+
};
|
|
317
|
+
const normalizedTriggerRows = async (api, workflowId, rows) => {
|
|
318
|
+
const needsFallback = (rows ?? []).some(trigger => directBackendTriggerId(trigger) === null);
|
|
319
|
+
const fallbackIds = needsFallback
|
|
320
|
+
? await backendTriggerIds(api, workflowId)
|
|
321
|
+
: new Map();
|
|
322
|
+
return (rows ?? []).map(trigger => {
|
|
323
|
+
const triggerId = trigger?.identity?.canvasTriggerId ?? trigger?.id ?? trigger?.triggerId;
|
|
324
|
+
return normalizeTriggerRow(trigger, fallbackIds.get(triggerId));
|
|
325
|
+
});
|
|
326
|
+
};
|
|
327
|
+
const auditBoolean = (trigger, key) => {
|
|
328
|
+
const deploymentValue = trigger?.deployment?.[key];
|
|
329
|
+
if (typeof deploymentValue === 'boolean')
|
|
330
|
+
return deploymentValue;
|
|
331
|
+
const flatValue = trigger?.[key];
|
|
332
|
+
return typeof flatValue === 'boolean' ? flatValue : undefined;
|
|
333
|
+
};
|
|
334
|
+
const auditLiveCanvasEnabled = (trigger) => {
|
|
335
|
+
const deploymentValue = trigger?.deployment?.liveCanvasEnabled;
|
|
336
|
+
if (typeof deploymentValue === 'boolean' || deploymentValue === null)
|
|
337
|
+
return deploymentValue;
|
|
338
|
+
const flatValue = trigger?.liveCanvasEnabled;
|
|
339
|
+
return typeof flatValue === 'boolean' || flatValue === null ? flatValue : undefined;
|
|
340
|
+
};
|
|
341
|
+
const triggerAuditState = (trigger) => {
|
|
342
|
+
const devCanvasEnabled = auditBoolean(trigger, 'devCanvasEnabled');
|
|
343
|
+
const liveCanvasEnabled = auditLiveCanvasEnabled(trigger);
|
|
344
|
+
const inLiveVersion = auditBoolean(trigger, 'inLiveVersion');
|
|
345
|
+
const workflowActive = auditBoolean(trigger, 'workflowActive');
|
|
346
|
+
const allowTestEvents = auditBoolean(trigger, 'allowTestEvents');
|
|
347
|
+
const reportedDevDispatchArmed = auditBoolean(trigger, 'devDispatchArmed');
|
|
348
|
+
const devDispatchArmed = reportedDevDispatchArmed ??
|
|
349
|
+
(devCanvasEnabled !== undefined && allowTestEvents !== undefined
|
|
350
|
+
? devCanvasEnabled && allowTestEvents
|
|
351
|
+
: undefined);
|
|
352
|
+
const reportedDualDispatchLive = auditBoolean(trigger, 'dualDispatchLive');
|
|
353
|
+
const dualDispatchLive = reportedDualDispatchLive ??
|
|
354
|
+
(devDispatchArmed !== undefined &&
|
|
355
|
+
typeof liveCanvasEnabled === 'boolean' &&
|
|
356
|
+
workflowActive !== undefined
|
|
357
|
+
? devDispatchArmed && liveCanvasEnabled && workflowActive
|
|
358
|
+
: undefined);
|
|
359
|
+
const newlyLiveOnPublish = devCanvasEnabled === true && inLiveVersion === false;
|
|
360
|
+
return {
|
|
361
|
+
devCanvasEnabled,
|
|
362
|
+
liveCanvasEnabled,
|
|
363
|
+
inLiveVersion,
|
|
364
|
+
workflowActive,
|
|
365
|
+
devDispatchArmed,
|
|
366
|
+
dualDispatchLive,
|
|
367
|
+
dualDispatchReported: reportedDualDispatchLive !== undefined,
|
|
368
|
+
liveAfterPublish: liveCanvasEnabled === true || newlyLiveOnPublish,
|
|
369
|
+
newlyLiveOnPublish,
|
|
370
|
+
reducedFidelity: liveCanvasEnabled === undefined ||
|
|
371
|
+
inLiveVersion === undefined ||
|
|
372
|
+
workflowActive === undefined ||
|
|
373
|
+
devDispatchArmed === undefined ||
|
|
374
|
+
reportedDualDispatchLive === undefined,
|
|
375
|
+
};
|
|
376
|
+
};
|
|
377
|
+
export const summarizeTriggerAudit = (items) => ({
|
|
378
|
+
liveAfterPublish: items.filter(item => triggerAuditState(item).liveAfterPublish),
|
|
379
|
+
armed: items.filter(item => triggerAuditState(item).devDispatchArmed === true),
|
|
380
|
+
doubleRunRisk: items.filter(item => triggerAuditState(item).dualDispatchLive === true),
|
|
381
|
+
newlyLiveOnPublish: items.filter(item => triggerAuditState(item).newlyLiveOnPublish),
|
|
382
|
+
});
|
|
383
|
+
const auditBooleanCell = (value) => value === true ? 'yes' : value === false ? 'no' : '-';
|
|
384
|
+
const auditArmedCell = (value) => value === true ? 'armed' : value === false ? 'off' : '-';
|
|
385
|
+
const auditTriggerLabel = (trigger) => `${trigger?.name || 'Unnamed trigger'} (${trigger?.triggerId ?? '-'})`;
|
|
386
|
+
const auditDisarmCommand = (trigger) => `cai trigger test-events --id ${shellQuote(String(trigger?.triggerId ?? '-'))} --disallow`;
|
|
387
|
+
const liveAfterPublishReason = (trigger) => {
|
|
388
|
+
const state = triggerAuditState(trigger);
|
|
389
|
+
if (state.newlyLiveOnPublish)
|
|
390
|
+
return 'dev enabled; will enter the live version on publish';
|
|
391
|
+
if (state.workflowActive === true)
|
|
392
|
+
return 'already live-enabled and workflow active';
|
|
393
|
+
if (state.workflowActive === false)
|
|
394
|
+
return 'live-enabled; publishing reactivates the workflow';
|
|
395
|
+
return 'already live-enabled; workflow active state unavailable';
|
|
396
|
+
};
|
|
397
|
+
export const registerTrigger = (program) => {
|
|
398
|
+
const trigger = program
|
|
399
|
+
.command('trigger')
|
|
400
|
+
.description('Trigger canvases and captured events (triggers run the LIVE version only)');
|
|
401
|
+
trigger
|
|
402
|
+
.command('list')
|
|
403
|
+
.description('Trigger lifecycle rows: ids, development state, readiness and issues')
|
|
404
|
+
.action(async (_opts, command) => {
|
|
405
|
+
const { api, workflowId } = requireContext(command);
|
|
406
|
+
const rows = await api.tool(workflowId, 'get_triggers', {});
|
|
407
|
+
const triggers = await normalizedTriggerRows(api, workflowId, rows);
|
|
408
|
+
for (const t of triggers) {
|
|
409
|
+
if (t.backendTriggerId === null) {
|
|
410
|
+
warn(`Trigger ${t.triggerId} has no recoverable backendTriggerId after checking its ` +
|
|
411
|
+
'lifecycle identity and development canvas state — `cai trigger events` and ' +
|
|
412
|
+
'`cai trigger replay` need that numeric id.');
|
|
413
|
+
}
|
|
414
|
+
}
|
|
415
|
+
emit({ items: triggers, triggers }, () => {
|
|
416
|
+
const includeLegacyGraph = triggers.some(t => t.flowId !== null || t.nodeCount !== null);
|
|
417
|
+
printTable([
|
|
418
|
+
'triggerId',
|
|
419
|
+
'backendTriggerId',
|
|
420
|
+
'name',
|
|
421
|
+
'enabled',
|
|
422
|
+
'presence',
|
|
423
|
+
'readiness',
|
|
424
|
+
'issues',
|
|
425
|
+
'events',
|
|
426
|
+
'testEvents',
|
|
427
|
+
...(includeLegacyGraph ? ['flowId', 'nodes'] : []),
|
|
428
|
+
], triggers.map(t => [
|
|
429
|
+
t.triggerId,
|
|
430
|
+
t.backendTriggerId ?? '-',
|
|
431
|
+
t.name,
|
|
432
|
+
t.enabled === true ? 'yes' : t.enabled === false ? 'no' : '-',
|
|
433
|
+
t.presence ?? '-',
|
|
434
|
+
triggerReadinessStage(t),
|
|
435
|
+
truncate(triggerIssueSummary(t), 42),
|
|
436
|
+
t.recordedEventCount ?? '-',
|
|
437
|
+
t.allowTestEvents === true ? 'allowed' : t.allowTestEvents === false ? 'off' : '-',
|
|
438
|
+
...(includeLegacyGraph ? [t.flowId, t.nodeCount] : []),
|
|
439
|
+
]));
|
|
440
|
+
});
|
|
441
|
+
});
|
|
442
|
+
trigger
|
|
443
|
+
.command('audit')
|
|
444
|
+
.description('Pre-publish trigger coverage, development dispatch and double-run risk')
|
|
445
|
+
.action(async (_opts, command) => {
|
|
446
|
+
const { api, workflowId } = requireContext(command);
|
|
447
|
+
const rows = await api.tool(workflowId, 'get_triggers', {});
|
|
448
|
+
const items = await normalizedTriggerRows(api, workflowId, rows);
|
|
449
|
+
const summary = summarizeTriggerAudit(items);
|
|
450
|
+
const reducedFidelityCount = items.filter(item => triggerAuditState(item).reducedFidelity).length;
|
|
451
|
+
const reducedFidelityMessage = reducedFidelityCount
|
|
452
|
+
? `Reduced fidelity: ${reducedFidelityCount} trigger ${reducedFidelityCount === 1 ? 'row is' : 'rows are'} missing lifecycle deployment fields; unavailable values are shown as "-".`
|
|
453
|
+
: null;
|
|
454
|
+
if (reducedFidelityMessage)
|
|
455
|
+
warn(reducedFidelityMessage);
|
|
456
|
+
for (const item of summary.doubleRunRisk) {
|
|
457
|
+
warn(`DOUBLE-RUN RISK: ${auditTriggerLabel(item)} is armed for development dispatch ` +
|
|
458
|
+
'while live-active; one real event runs twice (both real, both billed). ' +
|
|
459
|
+
`Disarm with ${auditDisarmCommand(item)}.`);
|
|
460
|
+
}
|
|
461
|
+
emit({ items, summary }, () => {
|
|
462
|
+
printTable([
|
|
463
|
+
'name',
|
|
464
|
+
'flowId',
|
|
465
|
+
'triggerId',
|
|
466
|
+
'backendTriggerId',
|
|
467
|
+
'dev enabled',
|
|
468
|
+
'live enabled',
|
|
469
|
+
'in live version',
|
|
470
|
+
'test-events armed',
|
|
471
|
+
], items.map(item => {
|
|
472
|
+
const state = triggerAuditState(item);
|
|
473
|
+
return [
|
|
474
|
+
item.name,
|
|
475
|
+
item.flowId,
|
|
476
|
+
item.triggerId,
|
|
477
|
+
item.backendTriggerId,
|
|
478
|
+
auditBooleanCell(state.devCanvasEnabled),
|
|
479
|
+
auditBooleanCell(state.liveCanvasEnabled),
|
|
480
|
+
auditBooleanCell(state.inLiveVersion),
|
|
481
|
+
auditArmedCell(state.devDispatchArmed),
|
|
482
|
+
];
|
|
483
|
+
}));
|
|
484
|
+
process.stdout.write('\nWill start this workflow LIVE after publish:\n');
|
|
485
|
+
printTable(['trigger', 'state'], summary.liveAfterPublish.map(item => [
|
|
486
|
+
auditTriggerLabel(item),
|
|
487
|
+
liveAfterPublishReason(item),
|
|
488
|
+
]));
|
|
489
|
+
process.stdout.write('\nArmed for dev dispatch (finish setup):\n');
|
|
490
|
+
printTable(['trigger', 'disarm command'], summary.armed.map(item => [auditTriggerLabel(item), auditDisarmCommand(item)]));
|
|
491
|
+
process.stdout.write('\nDOUBLE-RUN RISK:\n');
|
|
492
|
+
printTable(['trigger', 'impact'], summary.doubleRunRisk.map(item => [
|
|
493
|
+
auditTriggerLabel(item),
|
|
494
|
+
'one real event runs twice (both real, both billed)',
|
|
495
|
+
]));
|
|
496
|
+
process.stdout.write('\nEnabled in dev but not in the live version yet (starts firing only after publish):\n');
|
|
497
|
+
printTable(['trigger', 'state'], summary.newlyLiveOnPublish.map(item => [
|
|
498
|
+
auditTriggerLabel(item),
|
|
499
|
+
'dev enabled; absent from live',
|
|
500
|
+
]));
|
|
501
|
+
if (reducedFidelityMessage)
|
|
502
|
+
process.stdout.write(`\nnote: ${reducedFidelityMessage}\n`);
|
|
503
|
+
});
|
|
504
|
+
});
|
|
505
|
+
trigger
|
|
506
|
+
.command('get')
|
|
507
|
+
.argument('<triggerId>', 'canvas trigger id from `cai trigger list`')
|
|
508
|
+
.description('Full lifecycle for one trigger: capture, deployment, verification and issues')
|
|
509
|
+
.action(async (triggerId, _opts, command) => {
|
|
510
|
+
const { api, workflowId } = requireContext(command);
|
|
511
|
+
const data = await api.tool(workflowId, 'get_trigger', { triggerId });
|
|
512
|
+
const fallbackIds = directBackendTriggerId(data) === null
|
|
513
|
+
? await backendTriggerIds(api, workflowId)
|
|
514
|
+
: new Map();
|
|
515
|
+
const normalized = normalizeTriggerRow(data, fallbackIds.get(triggerId), triggerId);
|
|
516
|
+
if (normalized.backendTriggerId === null) {
|
|
517
|
+
warn(`Trigger ${normalized.triggerId} has no recoverable backendTriggerId after checking its ` +
|
|
518
|
+
'lifecycle identity and development canvas state — `cai trigger events` and ' +
|
|
519
|
+
'`cai trigger replay` need that numeric id.');
|
|
520
|
+
}
|
|
521
|
+
emit(normalized, () => {
|
|
522
|
+
const lifecycle = Boolean(data?.identity && data?.deployment && data?.capture);
|
|
523
|
+
printKeyValues(lifecycle
|
|
524
|
+
? [
|
|
525
|
+
['triggerId', normalized.triggerId],
|
|
526
|
+
['backendTriggerId', normalized.backendTriggerId ?? '(not recoverable)'],
|
|
527
|
+
['development enabled', normalized.enabled === true ? 'yes' : 'no'],
|
|
528
|
+
['readiness', triggerReadinessStage(normalized)],
|
|
529
|
+
['next action', normalized.readiness?.nextAction ?? '-'],
|
|
530
|
+
['issues', triggerIssueSummary(normalized)],
|
|
531
|
+
['captured events', normalized.capture?.recordedEventCount ?? 0],
|
|
532
|
+
['latest event', normalized.capture?.latestEventAt ?? '-'],
|
|
533
|
+
['source enabled', normalized.capture?.sourceEnabled ? 'yes' : 'no'],
|
|
534
|
+
[
|
|
535
|
+
'live canvas enabled',
|
|
536
|
+
normalized.deployment?.liveCanvasEnabled === null
|
|
537
|
+
? '(not published)'
|
|
538
|
+
: normalized.deployment?.liveCanvasEnabled
|
|
539
|
+
? 'yes'
|
|
540
|
+
: 'no',
|
|
541
|
+
],
|
|
542
|
+
['workflow active', normalized.deployment?.workflowActive ? 'yes' : 'no'],
|
|
543
|
+
['live matches dev', normalized.deployment?.workflowLiveMatchesDev ? 'yes' : 'no'],
|
|
544
|
+
['test events allowed', normalized.deployment?.allowTestEvents ? 'yes' : 'no'],
|
|
545
|
+
['development dispatch', normalized.deployment?.devDispatchArmed ? 'armed' : 'off'],
|
|
546
|
+
['dual dispatch live', normalized.deployment?.dualDispatchLive ? 'possible' : 'no'],
|
|
547
|
+
['last dev run', normalized.verification?.lastDevRun?.workflowExecutionId ?? '-'],
|
|
548
|
+
['last live run', normalized.verification?.lastLiveRun?.workflowExecutionId ?? '-'],
|
|
549
|
+
]
|
|
550
|
+
: [
|
|
551
|
+
['triggerId', normalized.triggerId],
|
|
552
|
+
['backendTriggerId', normalized.backendTriggerId ?? '(not recoverable)'],
|
|
553
|
+
['name', normalized.name],
|
|
554
|
+
[
|
|
555
|
+
'enabled',
|
|
556
|
+
normalized.enabled === true ? 'yes' : normalized.enabled === false ? 'no' : '-',
|
|
557
|
+
],
|
|
558
|
+
['readiness', triggerReadinessStage(normalized)],
|
|
559
|
+
['issues', triggerIssueSummary(normalized)],
|
|
560
|
+
['flowId', normalized.flowId ?? '-'],
|
|
561
|
+
]);
|
|
562
|
+
if (normalized.issues.length) {
|
|
563
|
+
process.stdout.write('\nissues\n');
|
|
564
|
+
printTable(['code', 'severity', 'blocksNextAction', 'detail'], normalized.issues.map((issue) => [
|
|
565
|
+
issue.code,
|
|
566
|
+
issue.severity,
|
|
567
|
+
issue.blocksNextAction ? 'yes' : 'no',
|
|
568
|
+
truncate(issue.detail, 72),
|
|
569
|
+
]));
|
|
570
|
+
}
|
|
571
|
+
const nodes = normalized.nodes ?? [];
|
|
572
|
+
if (nodes.length) {
|
|
573
|
+
process.stdout.write('\nnodes\n');
|
|
574
|
+
printTable(['id', 'displayName', 'key'], nodes.map((n) => [
|
|
575
|
+
n.id,
|
|
576
|
+
n.displayName ?? n.data?.displayName,
|
|
577
|
+
n.key ?? n.data?.key,
|
|
578
|
+
]));
|
|
579
|
+
}
|
|
580
|
+
});
|
|
581
|
+
});
|
|
582
|
+
trigger
|
|
583
|
+
.command('test-events')
|
|
584
|
+
.argument('<triggerId>', 'canvas trigger id from `cai trigger list`')
|
|
585
|
+
.option('--allow', 'arm ambient real-event dispatch against the development canvas')
|
|
586
|
+
.option('--disallow', 'turn ambient development dispatch off')
|
|
587
|
+
.option('--acknowledge-dual-run', 'when arming, acknowledge that one real event may execute more than once')
|
|
588
|
+
.description('Set ambient development test-event dispatch (routine tests should use replay)')
|
|
589
|
+
.action(async (triggerId, opts, command) => {
|
|
590
|
+
const { api, workflowId } = requireContext(command);
|
|
591
|
+
const allow = assertTriggerTestEventsMode(Boolean(opts.allow), Boolean(opts.disallow), Boolean(opts.acknowledgeDualRun));
|
|
592
|
+
const result = await api.mutate(workflowId, () => api.tool(workflowId, 'set_trigger_test_events', {
|
|
593
|
+
triggerId,
|
|
594
|
+
allow,
|
|
595
|
+
...(opts.acknowledgeDualRun ? { acknowledgeDualRun: true } : {}),
|
|
596
|
+
}));
|
|
597
|
+
if (result?.warning) {
|
|
598
|
+
warn(String(result.warning).replace(/set_trigger_test_events(?:\s*\([^)]*\))?/gi, `cai trigger test-events --id ${shellQuote(triggerId)} --disallow`));
|
|
599
|
+
}
|
|
600
|
+
const allowTestEvents = result?.allowTestEvents ?? allow;
|
|
601
|
+
emit({ ...result, triggerId, allowTestEvents }, () => printKeyValues([
|
|
602
|
+
['triggerId', triggerId],
|
|
603
|
+
['allow test events', allowTestEvents ? 'yes (allowed)' : 'no (disallowed)'],
|
|
604
|
+
['development dispatch', result?.devDispatchArmed ? 'armed' : 'off'],
|
|
605
|
+
['dual dispatch live', result?.dualDispatchLive ? 'possible' : 'no'],
|
|
606
|
+
]));
|
|
607
|
+
});
|
|
608
|
+
trigger
|
|
609
|
+
.command('search')
|
|
610
|
+
.argument('[query]', 'search term, plain English')
|
|
611
|
+
.option('--node <slug>', 'also return this node’s full event list')
|
|
612
|
+
.option('--limit <n>', 'max nodes', '20')
|
|
613
|
+
.option('--events-per-node <n>', 'event rows listed per node (raise to see them all)', '6')
|
|
614
|
+
.description('Nodes that can trigger a flow, and the --event keys each one publishes')
|
|
615
|
+
.action(async (query, opts, command) => {
|
|
616
|
+
const { api, workflowId } = requireContext(command);
|
|
617
|
+
const result = await api.tool(workflowId, 'search_trigger_nodes', {
|
|
618
|
+
...(query ? { query } : {}),
|
|
619
|
+
...(opts.node ? { nodeSlug: opts.node } : {}),
|
|
620
|
+
limit: Number(opts.limit),
|
|
621
|
+
});
|
|
622
|
+
const nodes = result?.nodes?.data ?? result?.nodes ?? [];
|
|
623
|
+
const components = result?.components ?? [];
|
|
624
|
+
const perNode = Math.max(1, Number(opts.eventsPerNode) || 6);
|
|
625
|
+
// `eventKey` is required by both `trigger props` and `trigger create` and is
|
|
626
|
+
// published nowhere else. A one-line-per-node table truncated the very column
|
|
627
|
+
// that carries it — so the keys get their own rows, with the description that
|
|
628
|
+
// separates sibling events and the connection requirement that decides
|
|
629
|
+
// whether `--connection` is needed.
|
|
630
|
+
const eventRows = nodes.flatMap(n => (n.eventKeys ?? [])
|
|
631
|
+
.slice(0, perNode)
|
|
632
|
+
.map(e => [
|
|
633
|
+
n.slug,
|
|
634
|
+
e.key,
|
|
635
|
+
truncate(e.name, 34),
|
|
636
|
+
e.needsConnection ? 'yes' : 'no',
|
|
637
|
+
truncate(e.description ?? '-', 58),
|
|
638
|
+
]));
|
|
639
|
+
emit({ nodes, components }, () => {
|
|
640
|
+
printTable(['slug', 'name', 'source', 'connection', 'events'], nodes.map(n => [
|
|
641
|
+
n.slug,
|
|
642
|
+
n.name,
|
|
643
|
+
n.source,
|
|
644
|
+
n.authRequired ? 'required' : 'none',
|
|
645
|
+
n.eventsCount,
|
|
646
|
+
]));
|
|
647
|
+
process.stdout.write('\nevent keys (pass one as --event)\n');
|
|
648
|
+
printTable(['node', 'eventKey', 'name', 'conn', 'description'], eventRows);
|
|
649
|
+
for (const n of nodes) {
|
|
650
|
+
const total = (n.eventKeys ?? []).length;
|
|
651
|
+
if (total > perNode) {
|
|
652
|
+
process.stdout.write(`… ${n.slug} has ${total} events, ${perNode} shown — ` +
|
|
653
|
+
`cai trigger search --node ${n.slug} for the full list\n`);
|
|
654
|
+
}
|
|
655
|
+
}
|
|
656
|
+
if (components.length) {
|
|
657
|
+
process.stdout.write(`\nall events on ${opts.node}\n`);
|
|
658
|
+
printTable(['eventKey', 'name', 'conn', 'description'], components.map(c => [
|
|
659
|
+
c.key,
|
|
660
|
+
c.name,
|
|
661
|
+
c.needsConnection ? 'yes' : 'no',
|
|
662
|
+
truncate(c.description, 70),
|
|
663
|
+
]));
|
|
664
|
+
}
|
|
665
|
+
process.stdout.write('\nnext: cai trigger props --node <slug> --event <eventKey>' +
|
|
666
|
+
' (add --connection <id> when conn is yes; `cai connection list`)\n');
|
|
667
|
+
});
|
|
668
|
+
});
|
|
669
|
+
trigger
|
|
670
|
+
.command('props')
|
|
671
|
+
.requiredOption('--node <slug>', 'trigger node slug')
|
|
672
|
+
.requiredOption('--event <eventKey>', 'event key from `cai trigger search` — never guess it')
|
|
673
|
+
.option('--connection <id>', 'connection id when the trigger needs auth')
|
|
674
|
+
.option('--configured <json|file|->', 'already-set prop values, for dependent dynamic props')
|
|
675
|
+
.option('--dynamic-props-id <id>', 'dynamicPropsId from a previous call, to reload')
|
|
676
|
+
.description('Configurable props for a trigger event; returns the dynamicPropsId to reuse')
|
|
677
|
+
.action(async (opts, command) => {
|
|
678
|
+
const { api, workflowId } = requireContext(command);
|
|
679
|
+
const configuredProps = opts.configured
|
|
680
|
+
? readJsonOption(opts.configured, '--configured')
|
|
681
|
+
: undefined;
|
|
682
|
+
const data = await api.tool(workflowId, 'get_trigger_component_props', {
|
|
683
|
+
nodeSlug: opts.node,
|
|
684
|
+
eventKey: opts.event,
|
|
685
|
+
...(opts.connection ? { connectionId: Number(opts.connection) } : {}),
|
|
686
|
+
...(configuredProps ? { configuredProps } : {}),
|
|
687
|
+
...(opts.dynamicPropsId ? { dynamicPropsId: opts.dynamicPropsId } : {}),
|
|
688
|
+
});
|
|
689
|
+
const props = data?.props ?? [];
|
|
690
|
+
const propsForCreate = triggerPropsForCreate(props, configuredProps ?? {});
|
|
691
|
+
const unresolvedOptions = unresolvedTriggerOptionProps(props, configuredProps ?? {});
|
|
692
|
+
const unresolvedOptionProps = unresolvedOptions
|
|
693
|
+
.map(prop => prop.name)
|
|
694
|
+
.filter((name) => Boolean(name));
|
|
695
|
+
const readyForCreate = unresolvedOptionProps.length === 0;
|
|
696
|
+
const dynamicPropsId = data?.id || '';
|
|
697
|
+
emit({
|
|
698
|
+
...data,
|
|
699
|
+
propsExample: propsForCreate,
|
|
700
|
+
propsForCreate,
|
|
701
|
+
readyForCreate,
|
|
702
|
+
unresolvedOptionProps,
|
|
703
|
+
}, () => {
|
|
704
|
+
printKeyValues([
|
|
705
|
+
['node', opts.node],
|
|
706
|
+
['event', opts.event],
|
|
707
|
+
['dynamicPropsId', dynamicPropsId || '(none — registry nodes have no dynamic props)'],
|
|
708
|
+
['ready for create', readyForCreate ? 'yes' : 'no — resolve required options below'],
|
|
709
|
+
]);
|
|
710
|
+
if (!props.length) {
|
|
711
|
+
process.stdout.write('\nThis event has no configurable props — omit --props on `cai trigger create`.\n');
|
|
712
|
+
}
|
|
713
|
+
else {
|
|
714
|
+
process.stdout.write('\nprops\n');
|
|
715
|
+
printTable(['name', 'displayName', 'inputType', 'valueShape', 'required', 'default'], props.map(p => [
|
|
716
|
+
p.name,
|
|
717
|
+
truncate(p.displayName, 28),
|
|
718
|
+
p.inputType,
|
|
719
|
+
triggerPropValueShape(p),
|
|
720
|
+
p.required ? 'yes' : '',
|
|
721
|
+
p.hidden ? '(hidden — leave unset)' : truncate(p.defaultValue, 34),
|
|
722
|
+
]));
|
|
723
|
+
// inputType names the widget, not the JSON. Print the object that goes
|
|
724
|
+
// into --props so the shape never has to be inferred from the widget.
|
|
725
|
+
process.stdout.write(readyForCreate
|
|
726
|
+
? '\n--props value (configured values plus required defaults):\n'
|
|
727
|
+
: '\nresolved required props so far (not yet a create-ready object):\n');
|
|
728
|
+
process.stdout.write(` ${shellQuote(JSON.stringify(propsForCreate))}\n`);
|
|
729
|
+
if (props.some(p => p.inputType === 'TIMER')) {
|
|
730
|
+
process.stdout.write('\nTIMER props take an OBJECT, not a string: {"cron":"0 9 * * *",' +
|
|
731
|
+
'"timezone":"America/Chicago"} for a cron schedule, or {"intervalSeconds":900} ' +
|
|
732
|
+
'for a fixed interval. A bare string is coerced to {"cron": "…"} and silently ' +
|
|
733
|
+
'loses the timezone.\n');
|
|
734
|
+
}
|
|
735
|
+
if (props.some(p => p.reloadProps && !p.hidden)) {
|
|
736
|
+
const reloadCommand = triggerPropsReloadCommand(opts, propsForCreate, dynamicPropsId || undefined);
|
|
737
|
+
process.stdout.write('\nProps marked reloadProps change the prop set. Set their values, then re-run ' +
|
|
738
|
+
'this complete command to see the props they unlock:\n' +
|
|
739
|
+
` ${reloadCommand}\n`);
|
|
740
|
+
}
|
|
741
|
+
if (unresolvedOptions.length) {
|
|
742
|
+
process.stdout.write('\nRequired option props have no advertised values yet. Resolve each one; copy an ' +
|
|
743
|
+
'exact returned value (not its label), merge it into --configured, then rerun ' +
|
|
744
|
+
'`cai trigger props`:\n');
|
|
745
|
+
for (const prop of unresolvedOptions) {
|
|
746
|
+
process.stdout.write(` ${triggerOptionLookupCommand(opts, prop.name, propsForCreate, dynamicPropsId || undefined)}\n`);
|
|
747
|
+
}
|
|
748
|
+
}
|
|
749
|
+
}
|
|
750
|
+
if (readyForCreate) {
|
|
751
|
+
process.stdout.write(`\nnext: cai trigger create --flow <flowId> --node ${opts.node} --event ${opts.event} ` +
|
|
752
|
+
'--source-basis <explicit-in-request|user-answer|inferred-from-existing-workflow>' +
|
|
753
|
+
(opts.connection ? ` --connection ${opts.connection}` : '') +
|
|
754
|
+
(dynamicPropsId ? ` --dynamic-props-id ${dynamicPropsId}` : '') +
|
|
755
|
+
(props.length ? ` --props ${shellQuote(JSON.stringify(propsForCreate))}` : '') +
|
|
756
|
+
'\n');
|
|
757
|
+
}
|
|
758
|
+
});
|
|
759
|
+
});
|
|
760
|
+
trigger
|
|
761
|
+
.command('options')
|
|
762
|
+
.requiredOption('--node <slug>', 'trigger node slug')
|
|
763
|
+
.requiredOption('--event <eventKey>', 'event key from `cai trigger search`')
|
|
764
|
+
.requiredOption('--prop <name>', 'option-backed prop name from `cai trigger props`')
|
|
765
|
+
.option('--connection <id>', 'connection id when the trigger needs auth')
|
|
766
|
+
.option('--configured <json|file|->', 'already-set trigger prop values')
|
|
767
|
+
.option('--dynamic-props-id <id>', 'dynamicPropsId from `cai trigger props`')
|
|
768
|
+
.option('--query <text>', 'search by human option label')
|
|
769
|
+
.option('--page <n>', '0-based option page when not searching')
|
|
770
|
+
.option('--prev-context <json|file|->', 'context returned by a previous options page')
|
|
771
|
+
.description('Resolve exact values for one trigger dropdown prop before trigger creation')
|
|
772
|
+
.action(async (opts, command) => {
|
|
773
|
+
const { api, workflowId } = requireContext(command);
|
|
774
|
+
const configuredProps = opts.configured
|
|
775
|
+
? readJsonOption(opts.configured, '--configured')
|
|
776
|
+
: undefined;
|
|
777
|
+
const prevContext = opts.prevContext
|
|
778
|
+
? readJsonOption(opts.prevContext, '--prev-context')
|
|
779
|
+
: undefined;
|
|
780
|
+
const page = parseOptionPage(opts.page);
|
|
781
|
+
const data = await api.tool(workflowId, 'get_trigger_prop_options', buildTriggerOptionToolInput({
|
|
782
|
+
node: opts.node,
|
|
783
|
+
event: opts.event,
|
|
784
|
+
prop: opts.prop,
|
|
785
|
+
connection: opts.connection,
|
|
786
|
+
configuredProps,
|
|
787
|
+
dynamicPropsId: opts.dynamicPropsId,
|
|
788
|
+
query: opts.query,
|
|
789
|
+
page,
|
|
790
|
+
prevContext,
|
|
791
|
+
}));
|
|
792
|
+
emit(data, () => {
|
|
793
|
+
printKeyValues([
|
|
794
|
+
['prop', opts.prop],
|
|
795
|
+
['source', data?.source ?? '-'],
|
|
796
|
+
['context', data?.context ? JSON.stringify(data.context) : '(none)'],
|
|
797
|
+
]);
|
|
798
|
+
printTable(['label', 'value'], (data?.options ?? []).map((option) => [option.label, JSON.stringify(option.value)]));
|
|
799
|
+
process.stdout.write('\nCopy the exact returned value into --configured, then rerun `cai trigger props`; ' +
|
|
800
|
+
'do not substitute the display label.\n');
|
|
801
|
+
});
|
|
802
|
+
});
|
|
803
|
+
trigger
|
|
804
|
+
.command('create')
|
|
805
|
+
.requiredOption('--flow <flowId>', 'flow this trigger runs (a trigger targets exactly one)')
|
|
806
|
+
.requiredOption('--node <slug>', 'trigger node slug')
|
|
807
|
+
.requiredOption('--event <eventKey>', 'event key from `cai trigger search` — never guess it')
|
|
808
|
+
.requiredOption('--source-basis <basis>', `one of ${SOURCE_BASES.join(' | ')}`)
|
|
809
|
+
.option('--connection <id>', 'connection id when the trigger needs auth')
|
|
810
|
+
.option('--props <json|file|->', 'configured prop values')
|
|
811
|
+
.option('--dynamic-props-id <id>', 'dynamicPropsId from `cai trigger props`')
|
|
812
|
+
.description('Create a trigger source and its canvas')
|
|
813
|
+
.action(async (opts, command) => {
|
|
814
|
+
const { api, workflowId } = requireContext(command);
|
|
815
|
+
assertSourceBasis(opts.sourceBasis);
|
|
816
|
+
const props = opts.props
|
|
817
|
+
? normalizeTriggerProps(readJsonOption(opts.props, '--props'))
|
|
818
|
+
: undefined;
|
|
819
|
+
const result = await api.mutate(workflowId, () => api.tool(workflowId, 'create_trigger', {
|
|
820
|
+
flowId: opts.flow,
|
|
821
|
+
nodeSlug: opts.node,
|
|
822
|
+
eventKey: opts.event,
|
|
823
|
+
// The public backend handler consumes sourceBasis but does not persist
|
|
824
|
+
// sourceEvidence, so do not offer a flag whose value would be dropped.
|
|
825
|
+
sourceBasis: opts.sourceBasis,
|
|
826
|
+
...(opts.connection ? { connectionId: Number(opts.connection) } : {}),
|
|
827
|
+
...(props ? { props } : {}),
|
|
828
|
+
...(opts.dynamicPropsId ? { dynamicPropsId: opts.dynamicPropsId } : {}),
|
|
829
|
+
}));
|
|
830
|
+
warn('Trigger data does NOT flow into the target flow automatically: the run-subflow node ' +
|
|
831
|
+
'grows one mapping prop per flow input, and only after those inputs exist. Read ' +
|
|
832
|
+
'nextSteps and follow them in order. The trigger stays inert until `cai workflow publish`.');
|
|
833
|
+
emit(result, () => {
|
|
834
|
+
printKeyValues([
|
|
835
|
+
['triggerId (canvas)', result?.triggerId],
|
|
836
|
+
['backendTriggerId', result?.backendTriggerId],
|
|
837
|
+
['triggerNodeId', result?.triggerNodeId],
|
|
838
|
+
['runSubflowNodeId', result?.runSubflowNodeId],
|
|
839
|
+
['webhookUrl', result?.webhookUrl ?? '-'],
|
|
840
|
+
['emailAddress', result?.emailAddress ?? '-'],
|
|
841
|
+
['schedule', result?.schedule ?? '-'],
|
|
842
|
+
]);
|
|
843
|
+
if ((result?.nextSteps ?? []).length) {
|
|
844
|
+
process.stdout.write('\nnextSteps (in order)\n');
|
|
845
|
+
for (const [i, step] of result.nextSteps.entries()) {
|
|
846
|
+
process.stdout.write(`${i + 1}. ${step}\n`);
|
|
847
|
+
}
|
|
848
|
+
}
|
|
849
|
+
});
|
|
850
|
+
});
|
|
851
|
+
trigger
|
|
852
|
+
.command('events')
|
|
853
|
+
.requiredOption('--trigger <backendTriggerId>', 'the NUMERIC backendTriggerId, not the canvas id')
|
|
854
|
+
.option('--limit <n>', 'newest-first page size (default 5, max 50)')
|
|
855
|
+
.description('Captured events for a trigger source — compact payload previews')
|
|
856
|
+
.action(async (opts, command) => {
|
|
857
|
+
const { api, workflowId } = requireContext(command);
|
|
858
|
+
const triggerId = Number(opts.trigger);
|
|
859
|
+
if (!Number.isInteger(triggerId)) {
|
|
860
|
+
throw userError('--trigger takes the NUMERIC backendTriggerId (from `cai trigger create`), not the ' +
|
|
861
|
+
'canvas trigger id used by get/rename/delete. The two id spaces are different.', 'BAD_TRIGGER_ID');
|
|
862
|
+
}
|
|
863
|
+
const response = await api.tool(workflowId, 'get_trigger_events', {
|
|
864
|
+
triggerId,
|
|
865
|
+
...(opts.limit ? { limit: Number(opts.limit) } : {}),
|
|
866
|
+
});
|
|
867
|
+
// Envelope shape since the trigger-lifecycle BE branch: { events, capture }.
|
|
868
|
+
// The array fallback keeps this command working against older backends.
|
|
869
|
+
const rows = Array.isArray(response) ? response : (response?.events ?? []);
|
|
870
|
+
const capture = Array.isArray(response) ? undefined : response?.capture;
|
|
871
|
+
if (!rows.length) {
|
|
872
|
+
warn(capture?.emptyState ??
|
|
873
|
+
capture?.note ??
|
|
874
|
+
'No captured events. Sample-event selection and replay both need at least one — ' +
|
|
875
|
+
'send a real event to the trigger source first.');
|
|
876
|
+
}
|
|
877
|
+
emit({ triggerId, events: rows ?? [] }, () => printTable(['eventId', 'createdAt', 'dataTypeName', 'payloadPreview'], (rows ?? []).map(e => [
|
|
878
|
+
e.id,
|
|
879
|
+
e.createdAt,
|
|
880
|
+
e.dataTypeName ?? '-',
|
|
881
|
+
truncate(e.payloadPreview, 60),
|
|
882
|
+
])));
|
|
883
|
+
});
|
|
884
|
+
trigger
|
|
885
|
+
.command('event')
|
|
886
|
+
.argument('<eventId>', 'captured event id from `cai trigger events`')
|
|
887
|
+
.description('One captured event with its full payload')
|
|
888
|
+
.action(async (eventId, _opts, command) => {
|
|
889
|
+
const { api, workflowId } = requireContext(command);
|
|
890
|
+
const data = await api.tool(workflowId, 'get_trigger_event', { eventId: Number(eventId) });
|
|
891
|
+
if (data?.payload?._truncated || data?._truncated) {
|
|
892
|
+
warn('Payload over 8KB — the backend returned {_truncated, totalChars, preview} instead of ' +
|
|
893
|
+
'the whole thing. This is a preview, not the event.');
|
|
894
|
+
}
|
|
895
|
+
emit(data, () => process.stdout.write(`${JSON.stringify(data, null, 2)}\n`));
|
|
896
|
+
});
|
|
897
|
+
trigger
|
|
898
|
+
.command('select-event')
|
|
899
|
+
.requiredOption('--node <triggerNodeId>', 'trigger node id from `cai trigger create`')
|
|
900
|
+
.requiredOption('--event <eventId>', 'captured event id')
|
|
901
|
+
.description('Adopt a captured event as the trigger’s sample, defining its output schema')
|
|
902
|
+
.action(async (opts, command) => {
|
|
903
|
+
const { api, workflowId } = requireContext(command);
|
|
904
|
+
const result = await api.mutate(workflowId, () => api.tool(workflowId, 'select_trigger_event', {
|
|
905
|
+
triggerNodeId: opts.node,
|
|
906
|
+
eventId: Number(opts.event),
|
|
907
|
+
}));
|
|
908
|
+
if (!result?.note) {
|
|
909
|
+
warn('The selection defines the trigger OUTPUT SCHEMA only when the event carries a ' +
|
|
910
|
+
'dataTypeName. Without one the sample is stored but downstream bindings stay ' +
|
|
911
|
+
'untyped — check `cai expr context <downstream node>` before writing expressions.');
|
|
912
|
+
}
|
|
913
|
+
emit(result, () => printKeyValues([
|
|
914
|
+
['triggerNodeId', opts.node],
|
|
915
|
+
['eventId', opts.event],
|
|
916
|
+
['note', result?.note ?? '(none — see the warning above)'],
|
|
917
|
+
]));
|
|
918
|
+
});
|
|
919
|
+
trigger
|
|
920
|
+
.command('replay')
|
|
921
|
+
.requiredOption('--trigger <backendTriggerId>', 'the NUMERIC backendTriggerId')
|
|
922
|
+
.option('--event <eventId>', 'captured event to replay (exclusive with --payload)')
|
|
923
|
+
.option('--payload <json|file|->', 'custom raw payload (exclusive with --event)')
|
|
924
|
+
.option('--target <dev|live>', 'DEV pre-publish or LIVE post-publish verification', 'dev')
|
|
925
|
+
.option('--confirm-live', 'acknowledge LIVE replay side effects and usage', false)
|
|
926
|
+
.description('Replay through real ingestion; defaults to safe DEV-rail verification')
|
|
927
|
+
.action(async (opts, command) => {
|
|
928
|
+
const { api, workflowId } = requireContext(command);
|
|
929
|
+
if (opts.event && opts.payload) {
|
|
930
|
+
throw userError('--event and --payload are mutually exclusive.', 'CONFLICTING_REPLAY_INPUT');
|
|
931
|
+
}
|
|
932
|
+
const target = assertReplayTarget(opts.target, Boolean(opts.confirmLive));
|
|
933
|
+
const triggerId = Number(opts.trigger);
|
|
934
|
+
if (!Number.isInteger(triggerId) || triggerId <= 0) {
|
|
935
|
+
throw userError('--trigger must be a positive numeric backendTriggerId.', 'BAD_TRIGGER_ID');
|
|
936
|
+
}
|
|
937
|
+
warn(target === 'live'
|
|
938
|
+
? 'LIVE replay executes the published workflow: external actions fire and usage is spent.'
|
|
939
|
+
: 'DEV replay does not dispatch live, but it still executes real external actions and spends usage.');
|
|
940
|
+
const result = await api.tool(workflowId, 'replay_trigger_event', {
|
|
941
|
+
triggerId,
|
|
942
|
+
target,
|
|
943
|
+
...(opts.event ? { eventId: Number(opts.event) } : {}),
|
|
944
|
+
...(opts.payload ? { payload: readJsonOption(opts.payload, '--payload') } : {}),
|
|
945
|
+
});
|
|
946
|
+
for (const w of result?.warnings ?? [])
|
|
947
|
+
warn(String(w));
|
|
948
|
+
emit(result, () => printKeyValues([
|
|
949
|
+
['target', target],
|
|
950
|
+
['workflowExecutionId', result?.workflowExecutionId ?? '-'],
|
|
951
|
+
['eventId', result?.eventId ?? '-'],
|
|
952
|
+
['next', 'cai exec tree <workflowExecutionId> — and check resolvedConfigs'],
|
|
953
|
+
]));
|
|
954
|
+
});
|
|
955
|
+
trigger
|
|
956
|
+
.command('toggle')
|
|
957
|
+
.argument('<triggerId>', 'canvas trigger id')
|
|
958
|
+
.description('Flip a trigger between enabled and disabled (a toggle, not a setter)')
|
|
959
|
+
.action(async (triggerId, _opts, command) => {
|
|
960
|
+
const { api, workflowId } = requireContext(command);
|
|
961
|
+
const result = await api.mutate(workflowId, () => api.tool(workflowId, 'toggle_trigger_enabled', { triggerId }));
|
|
962
|
+
warn('toggle_trigger_enabled flips the current state — there is no set-to-enabled tool. ' +
|
|
963
|
+
'Read `cai trigger list` afterwards to see which way it went.');
|
|
964
|
+
emit({ triggerId, result }, () => printKeyValues([
|
|
965
|
+
['triggerId', triggerId],
|
|
966
|
+
['result', JSON.stringify(result)],
|
|
967
|
+
]));
|
|
968
|
+
});
|
|
969
|
+
trigger
|
|
970
|
+
.command('rename')
|
|
971
|
+
.argument('<triggerId>', 'canvas trigger id')
|
|
972
|
+
.requiredOption('--name <name>')
|
|
973
|
+
.description('Rename a trigger')
|
|
974
|
+
.action(async (triggerId, opts, command) => {
|
|
975
|
+
const { api, workflowId } = requireContext(command);
|
|
976
|
+
const result = await api.mutate(workflowId, () => api.tool(workflowId, 'rename_trigger', { triggerId, name: opts.name }));
|
|
977
|
+
emit({ triggerId, name: opts.name, result }, () => printKeyValues([
|
|
978
|
+
['triggerId', triggerId],
|
|
979
|
+
['name', opts.name],
|
|
980
|
+
]));
|
|
981
|
+
});
|
|
982
|
+
trigger
|
|
983
|
+
.command('delete')
|
|
984
|
+
.argument('<triggerId>', 'canvas trigger id')
|
|
985
|
+
.description('Delete a trigger and its canvas')
|
|
986
|
+
.action(async (triggerId, _opts, command) => {
|
|
987
|
+
const { api, workflowId } = requireContext(command);
|
|
988
|
+
const result = await api.mutate(workflowId, () => api.tool(workflowId, 'delete_trigger', { triggerId }));
|
|
989
|
+
emit({ triggerId, result }, () => printKeyValues([['deleted', triggerId]]));
|
|
990
|
+
});
|
|
991
|
+
};
|
|
992
|
+
//# sourceMappingURL=trigger.js.map
|